Private
Public Access
1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
913d7286e1 chore: bump version to 1.3.2
Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 3s
CI/CD Pipeline / Clippy Lints (push) Successful in 43s
CI/CD Pipeline / All Unit Tests (push) Successful in 2m48s
CI/CD Pipeline / Security Audit (push) Successful in 7s
CI/CD Pipeline / Enrollment Tests (push) Successful in 1m23s
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Failing after 5s
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Successful in 59s
CI/CD Pipeline / Build Debian Package (push) Failing after 4s
CI/CD Pipeline / Build RPM Package (push) Successful in 2m13s
CI/CD Pipeline / Build Arch Package (push) Successful in 2m18s
CI/CD Pipeline / Build Alpine Package (push) Failing after 2m58s
* fix: extract DER from PEM-encoded CA cert before CRL signature verification

* chore: bump version to 1.3.2

---------

Co-authored-by: git-echo <git-echo@moon-dragon.us>
Co-authored-by: Draco Lunaris <331325+Draco-Lunaris@users.noreply.github.com>
2026-06-06 08:44:34 -05:00
3c70b15831 fix: extract DER from PEM-encoded CA cert before CRL signature verification
Co-authored-by: git-echo <git-echo@moon-dragon.us>
2026-06-06 08:31:20 -05:00
3 changed files with 78 additions and 3 deletions

2
Cargo.lock generated
View File

@ -1931,7 +1931,7 @@ dependencies = [
[[package]] [[package]]
name = "linux-patch-api" name = "linux-patch-api"
version = "1.3.0" version = "1.3.1"
dependencies = [ dependencies = [
"actix", "actix",
"actix-rt", "actix-rt",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "linux-patch-api" name = "linux-patch-api"
version = "1.3.1" version = "1.3.2"
edition = "2021" edition = "2021"
authors = ["Echo <echo@moon-dragon.us>"] authors = ["Echo <echo@moon-dragon.us>"]
description = "Secure remote package management API for Linux systems" description = "Secure remote package management API for Linux systems"

View File

@ -165,7 +165,16 @@ pub fn load_crl(crl_path: &Path, ca_cert_der: &[u8]) -> CrlState {
}; };
// Verify CRL signature against CA // Verify CRL signature against CA
let (_, ca_cert) = match x509_parser::parse_x509_certificate(ca_cert_der) { // Extract DER from PEM if the CA cert is PEM-encoded
let ca_der = match extract_pem_cert_der(ca_cert_der) {
Some(der) => der,
None => {
// Not PEM — assume it's already DER
ca_cert_der.to_vec()
}
};
let (_, ca_cert) = match x509_parser::parse_x509_certificate(&ca_der) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
error!(error = %e, "Failed to parse CA cert for CRL signature verification"); error!(error = %e, "Failed to parse CA cert for CRL signature verification");
@ -220,6 +229,29 @@ pub fn load_crl(crl_path: &Path, ca_cert_der: &[u8]) -> CrlState {
} }
} }
/// Extract DER bytes from a PEM-encoded certificate.
/// Looks for `-----BEGIN CERTIFICATE-----` / `-----END CERTIFICATE-----` markers
/// and base64-decodes the content between them.
pub fn extract_pem_cert_der(pem_bytes: &[u8]) -> Option<Vec<u8>> {
let pem_str = String::from_utf8_lossy(pem_bytes);
let begin_marker = "-----BEGIN CERTIFICATE-----";
let end_marker = "-----END CERTIFICATE-----";
let begin_idx = pem_str.find(begin_marker)?;
let after_begin = begin_idx + begin_marker.len();
let end_idx = pem_str[after_begin..].find(end_marker)?;
// Strip all whitespace (including newlines) from the base64 block
// before decoding, since PEM format wraps lines at 64 characters.
let b64_block: String = pem_str[after_begin..after_begin + end_idx]
.split_whitespace()
.collect();
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(&b64_block)
.ok()
}
/// Extract DER bytes from a PEM-encoded CRL. /// Extract DER bytes from a PEM-encoded CRL.
/// Looks for `-----BEGIN X509 CRL-----` / `-----END X509 CRL-----` blocks. /// Looks for `-----BEGIN X509 CRL-----` / `-----END X509 CRL-----` blocks.
fn extract_pem_crl_der(pem_bytes: &[u8]) -> Option<Vec<u8>> { fn extract_pem_crl_der(pem_bytes: &[u8]) -> Option<Vec<u8>> {
@ -688,4 +720,47 @@ mod tests {
"Invalid CRL should not match any serial" "Invalid CRL should not match any serial"
); );
} }
#[test]
fn test_extract_pem_cert_der_invalid() {
// Not PEM
assert!(extract_pem_cert_der(b"not pem").is_none());
// PEM but wrong type (CRL instead of CERTIFICATE)
assert!(
extract_pem_cert_der(b"-----BEGIN X509 CRL-----\nAA==\n-----END X509 CRL-----")
.is_none()
);
}
#[test]
fn test_extract_pem_cert_der_valid() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let (_, ca_cert) = generate_test_ca();
let cert_pem = ca_cert.pem();
// Verify PEM extraction succeeds
let der = extract_pem_cert_der(cert_pem.as_bytes());
assert!(
der.is_some(),
"PEM extraction should succeed for valid certificate PEM"
);
// Verify the DER can be parsed as an X.509 certificate
let der_bytes = der.unwrap();
let parsed = x509_parser::parse_x509_certificate(&der_bytes);
assert!(
parsed.is_ok(),
"DER should parse as a valid X.509 certificate"
);
}
#[test]
fn test_extract_pem_cert_der_rejects_crl_pem() {
// CERTIFICATE extraction should reject CRL PEM
let crl_pem = "-----BEGIN X509 CRL-----\nAA==\n-----END X509 CRL-----";
assert!(
extract_pem_cert_der(crl_pem.as_bytes()).is_none(),
"CRL PEM should not extract as CERTIFICATE"
);
}
} }