Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17629dc814 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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?;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user