diff --git a/Cargo.lock b/Cargo.lock index 50cca2ed41..4c57ac79ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3910,6 +3910,7 @@ dependencies = [ "tempfile", "tokio", "tracing", + "x509-parser", ] [[package]] diff --git a/crates/openshell-bootstrap/Cargo.toml b/crates/openshell-bootstrap/Cargo.toml index 435024e302..3705bdced2 100644 --- a/crates/openshell-bootstrap/Cargo.toml +++ b/crates/openshell-bootstrap/Cargo.toml @@ -22,6 +22,7 @@ tokio = { workspace = true } tracing = { workspace = true } [dev-dependencies] +x509-parser = "0.16" [lints] workspace = true diff --git a/crates/openshell-bootstrap/src/pki.rs b/crates/openshell-bootstrap/src/pki.rs index ed6e839bf6..5e5839a11a 100644 --- a/crates/openshell-bootstrap/src/pki.rs +++ b/crates/openshell-bootstrap/src/pki.rs @@ -3,7 +3,9 @@ use crate::jwt::{JwtKeyMaterial, generate_jwt_key}; use miette::{IntoDiagnostic, Result, WrapErr}; -use rcgen::{BasicConstraints, CertificateParams, DnType, Ia5String, IsCa, KeyPair, SanType}; +use rcgen::{ + BasicConstraints, CertificateParams, DnType, Ia5String, IsCa, KeyPair, KeyUsagePurpose, SanType, +}; use std::net::IpAddr; /// All PEM-encoded materials produced by [`generate_pki`]. @@ -59,6 +61,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { .into_diagnostic() .wrap_err("failed to create CA params")?; ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + ca_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign]; ca_params .distinguished_name .push(DnType::OrganizationName, "openshell"); @@ -80,6 +83,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { .into_diagnostic() .wrap_err("failed to create server cert params")?; server_params.subject_alt_names = server_sans; + server_params.use_authority_key_identifier_extension = true; server_params .distinguished_name .push(DnType::CommonName, "openshell-server"); @@ -96,6 +100,7 @@ pub fn generate_pki(extra_sans: &[String]) -> Result { let mut client_params = CertificateParams::new(Vec::::new()) .into_diagnostic() .wrap_err("failed to create client cert params")?; + client_params.use_authority_key_identifier_extension = true; client_params .distinguished_name .push(DnType::CommonName, "openshell-client"); @@ -178,6 +183,58 @@ mod tests { assert!(bundle.server_cert_pem.contains("BEGIN CERTIFICATE")); } + #[test] + fn generate_pki_emits_strict_verifier_extensions() { + use x509_parser::pem::parse_x509_pem; + use x509_parser::prelude::{FromDer, ParsedExtension, X509Certificate}; + + let bundle = generate_pki(&[]).expect("generate_pki failed"); + let parse = |pem: &str| -> Vec { + parse_x509_pem(pem.as_bytes()) + .expect("valid PEM") + .1 + .contents + }; + let ca_der = parse(&bundle.ca_cert_pem); + let ca = X509Certificate::from_der(&ca_der).expect("valid CA cert").1; + let ca_key_usage = ca + .key_usage() + .expect("readable key usage") + .expect("CA has a key usage extension"); + assert!(ca_key_usage.value.key_cert_sign()); + assert!(ca_key_usage.value.crl_sign()); + let ca_ski = ca + .get_extension_unique(&x509_parser::oid_registry::OID_X509_EXT_SUBJECT_KEY_IDENTIFIER) + .expect("readable SKI") + .expect("CA has a Subject Key Identifier"); + + for (name, pem) in [ + ("server", &bundle.server_cert_pem), + ("client", &bundle.client_cert_pem), + ] { + let der = parse(pem); + let cert = X509Certificate::from_der(&der).expect("valid leaf cert").1; + let aki = cert + .get_extension_unique( + &x509_parser::oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER, + ) + .expect("readable AKI") + .unwrap_or_else(|| panic!("{name} cert has no Authority Key Identifier")); + let ( + ParsedExtension::AuthorityKeyIdentifier(aki), + ParsedExtension::SubjectKeyIdentifier(ski), + ) = (aki.parsed_extension(), ca_ski.parsed_extension()) + else { + panic!("{name}: unexpected extension shapes"); + }; + let key_id = aki + .key_identifier + .as_ref() + .unwrap_or_else(|| panic!("{name} AKI has no key identifier")); + assert_eq!(key_id.0, ski.0, "{name} AKI must match the CA SKI"); + } + } + #[test] fn build_server_sans_includes_defaults_and_extras() { let extras = vec!["192.168.1.100".to_string(), "remote.host".to_string()];