Private
Public Access
1
0

Compare commits

..

1 Commits

Author SHA1 Message Date
17629dc814 chore: bump version to 1.3.0
Some checks failed
CI/CD Pipeline / Code Format (push) Successful in 2s
CI/CD Pipeline / Clippy Lints (push) Successful in 1m45s
CI/CD Pipeline / All Unit Tests (push) Successful in 11m0s
CI/CD Pipeline / Security Audit (push) Successful in 6s
CI/CD Pipeline / Enrollment Tests (push) Successful in 1m17s
CI/CD Pipeline / Build Debian Package (Ubuntu 22.04) (push) Failing after 4s
CI/CD Pipeline / Verify Enrollment CLI Flag (push) Successful in 1m2s
CI/CD Pipeline / Build Arch Package (push) Successful in 2m31s
CI/CD Pipeline / Build RPM Package (push) Successful in 2m23s
CI/CD Pipeline / Build Debian Package (push) Failing after 6s
CI/CD Pipeline / Build Alpine Package (push) Failing after 3m32s
2026-06-05 17:35:51 -05:00
7 changed files with 4 additions and 118 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.1" version = "1.2.0"
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.2" version = "1.3.0"
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,16 +165,7 @@ pub fn load_crl(crl_path: &Path, ca_cert_der: &[u8]) -> CrlState {
}; };
// Verify CRL signature against CA // Verify CRL signature against CA
// Extract DER from PEM if the CA cert is PEM-encoded let (_, ca_cert) = match x509_parser::parse_x509_certificate(ca_cert_der) {
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");
@ -229,29 +220,6 @@ 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>> {
@ -720,47 +688,4 @@ 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"
);
}
} }

View File

@ -38,12 +38,8 @@ pub enum EnrollmentStatusResponse {
Pending, Pending,
Approved { Approved {
ca_crt: String, ca_crt: String,
#[serde(default)]
ca_chain: String,
server_crt: String, server_crt: String,
server_key: String, server_key: String,
#[serde(default)]
crl_pem: String,
}, },
Denied, Denied,
NotFound, NotFound,
@ -53,10 +49,8 @@ pub enum EnrollmentStatusResponse {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PkiBundle { pub struct PkiBundle {
pub ca_crt: String, pub ca_crt: String,
pub ca_chain: String,
pub server_crt: String, pub server_crt: String,
pub server_key: String, pub server_key: String,
pub crl_pem: String,
} }
impl From<EnrollmentStatusResponse> for Option<PkiBundle> { impl From<EnrollmentStatusResponse> for Option<PkiBundle> {
@ -64,16 +58,12 @@ impl From<EnrollmentStatusResponse> for Option<PkiBundle> {
match response { match response {
EnrollmentStatusResponse::Approved { EnrollmentStatusResponse::Approved {
ca_crt, ca_crt,
ca_chain,
server_crt, server_crt,
server_key, server_key,
crl_pem,
} => Some(PkiBundle { } => Some(PkiBundle {
ca_crt, ca_crt,
ca_chain,
server_crt, server_crt,
server_key, server_key,
crl_pem,
}), }),
_ => None, _ => None,
} }
@ -461,10 +451,8 @@ impl EnrollmentClient {
} }
EnrollmentStatusResponse::Approved { EnrollmentStatusResponse::Approved {
ca_crt, ca_crt,
ca_chain,
server_crt, server_crt,
server_key, server_key,
crl_pem,
} => { } => {
tracing::info!( tracing::info!(
elapsed_seconds = start.elapsed().as_secs(), elapsed_seconds = start.elapsed().as_secs(),
@ -473,10 +461,8 @@ impl EnrollmentClient {
); );
return Ok(PkiBundle { return Ok(PkiBundle {
ca_crt, ca_crt,
ca_chain,
server_crt, server_crt,
server_key, server_key,
crl_pem,
}); });
} }
EnrollmentStatusResponse::Denied => { EnrollmentStatusResponse::Denied => {
@ -580,10 +566,8 @@ mod tests {
fn approved_to_pki_bundle() { fn approved_to_pki_bundle() {
let status = EnrollmentStatusResponse::Approved { let status = EnrollmentStatusResponse::Approved {
ca_crt: "ca".into(), ca_crt: "ca".into(),
ca_chain: String::new(),
server_crt: "crt".into(), server_crt: "crt".into(),
server_key: "key".into(), server_key: "key".into(),
crl_pem: String::new(),
}; };
let bundle: Option<PkiBundle> = status.into(); let bundle: Option<PkiBundle> = status.into();
assert!(bundle.is_some()); assert!(bundle.is_some());

View File

@ -160,10 +160,8 @@ pub async fn run_enrollment(
// Write certificates to configured paths (or defaults) // Write certificates to configured paths (or defaults)
provision::provision_pki_bundle( provision::provision_pki_bundle(
&pki_bundle.ca_crt, &pki_bundle.ca_crt,
&pki_bundle.ca_chain,
&pki_bundle.server_crt, &pki_bundle.server_crt,
&pki_bundle.server_key, &pki_bundle.server_key,
&pki_bundle.crl_pem,
config.tls_config(), config.tls_config(),
) )
.await?; .await?;

View File

@ -16,8 +16,6 @@ const DEFAULT_CA_CERT: &str = "/etc/linux_patch_api/certs/ca.pem";
const DEFAULT_SERVER_CERT: &str = "/etc/linux_patch_api/certs/server.pem"; const DEFAULT_SERVER_CERT: &str = "/etc/linux_patch_api/certs/server.pem";
/// Default server key path. /// Default server key path.
const DEFAULT_SERVER_KEY: &str = "/etc/linux_patch_api/certs/server.key.pem"; const DEFAULT_SERVER_KEY: &str = "/etc/linux_patch_api/certs/server.key.pem";
/// Default CRL path.
const DEFAULT_CRL_PATH: &str = "/etc/linux_patch_api/certs/crl.pem";
/// Validate that a PEM string has proper format (BEGIN/END markers present). /// Validate that a PEM string has proper format (BEGIN/END markers present).
/// ///
@ -130,14 +128,12 @@ pub fn write_pem_file(path: &str, pem_data: &str, is_key: bool) -> Result<()> {
/// Provision the full PKI bundle from an approved enrollment response. /// Provision the full PKI bundle from an approved enrollment response.
/// ///
/// Writes CA cert, CA chain, server cert, server key, and CRL to configured paths. /// Writes CA cert, server cert, and server key to configured paths.
/// Paths are read from TLS config if available, otherwise defaults are used. /// Paths are read from TLS config if available, otherwise defaults are used.
pub async fn provision_pki_bundle( pub async fn provision_pki_bundle(
ca_crt: &str, ca_crt: &str,
_ca_chain: &str,
server_crt: &str, server_crt: &str,
server_key: &str, server_key: &str,
crl_pem: &str,
tls_config: Option<&super::super::config::loader::TlsConfig>, tls_config: Option<&super::super::config::loader::TlsConfig>,
) -> Result<()> { ) -> Result<()> {
// Determine target paths from config or defaults // Determine target paths from config or defaults
@ -177,19 +173,6 @@ pub async fn provision_pki_bundle(
write_pem_file(&key_path, server_key, true).context("Failed to write server key")?; write_pem_file(&key_path, server_key, true).context("Failed to write server key")?;
// Write CRL if provided (non-empty)
let crl_path = if let Some(tls) = tls_config {
tls.crl_path.clone()
} else {
DEFAULT_CRL_PATH.to_string()
};
if !crl_pem.trim().is_empty() {
write_pem_file(&crl_path, crl_pem, false).context("Failed to write CRL")?;
tracing::info!(path = %crl_path, "CRL written from enrollment bundle");
} else {
tracing::info!("No CRL in enrollment bundle — agent will fetch on refresh cycle");
}
// 3. Log successful provisioning with structured fields // 3. Log successful provisioning with structured fields
tracing::info!( tracing::info!(
ca_cert = %ca_path, ca_cert = %ca_path,

View File

@ -178,10 +178,8 @@ async fn test_full_enrollment_flow_happy_path() {
let tls_config = build_tls_config(cert_dir.path()); let tls_config = build_tls_config(cert_dir.path());
provision::provision_pki_bundle( provision::provision_pki_bundle(
&bundle.ca_crt, &bundle.ca_crt,
&bundle.ca_chain,
&bundle.server_crt, &bundle.server_crt,
&bundle.server_key, &bundle.server_key,
&bundle.crl_pem,
Some(&tls_config), Some(&tls_config),
) )
.await .await
@ -447,10 +445,8 @@ async fn test_certificate_permission_verification() {
let tls_config = build_tls_config(cert_dir.path()); let tls_config = build_tls_config(cert_dir.path());
provision::provision_pki_bundle( provision::provision_pki_bundle(
&bundle.ca_crt, &bundle.ca_crt,
&bundle.ca_chain,
&bundle.server_crt, &bundle.server_crt,
&bundle.server_key, &bundle.server_key,
&bundle.crl_pem,
Some(&tls_config), Some(&tls_config),
) )
.await .await