use crate::data_model::common::BigUnsigned; use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl}; use crate::data_model::rc::{ ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError, }; use crate::data_model::signed_object::{EeKeyUsageSummary, ResourceEeCertificate}; use ring::signature; use x509_parser::prelude::{FromDer, X509Certificate}; use crate::validation::x509_name::x509_names_equivalent; use std::collections::HashSet; use x509_parser::x509::SubjectPublicKeyInfo; #[derive(Clone, Debug, PartialEq, Eq)] pub struct ValidatedEeCertPath { pub ee: ResourceCertificate, pub issuer_ca: ResourceCertificate, pub issuer_crl: RpkixCrl, } #[derive(Debug, thiserror::Error)] pub enum CertPathError { #[error("EE certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")] EeDecode(#[from] ResourceCertificateDecodeError), #[error("issuer CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")] IssuerDecode(ResourceCertificateDecodeError), #[error("issuer CRL decode failed: {0} (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1)")] CrlDecode(#[from] CrlDecodeError), #[error( "issuer certificate must be a CA resource certificate (RFC 6487 §4.8.1; RFC 5280 §4.2.1.9)" )] IssuerNotCa, #[error("EE certificate must be an EE resource certificate (RFC 6487 §4)")] EeNotEe, #[error( "EE issuer DN does not match issuer CA subject DN: ee.issuer={ee_issuer_dn} issuer.subject={issuer_subject_dn} (RFC 5280 §6.1)" )] IssuerSubjectMismatch { ee_issuer_dn: String, issuer_subject_dn: String, }, #[error("EE certificate signature verification failed: {0} (RFC 5280 §6.1)")] EeSignatureInvalid(String), #[error("issuer SubjectPublicKeyInfo parse error: {0} (RFC 5280 §4.1.2.7)")] IssuerSpkiParse(String), #[error( "trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes (DER; RFC 5280 §4.1.2.7)" )] IssuerSpkiTrailingBytes(usize), #[error("EE KeyUsage extension missing (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")] KeyUsageMissing, #[error("EE KeyUsage criticality must be critical (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")] KeyUsageNotCritical, #[error("EE KeyUsage must have only digitalSignature set (RFC 6487 §4.8.4)")] KeyUsageInvalidBits, #[error("EE KeyUsage parse failed: {0} (RFC 6487 §4.8.4; RFC 5280 §4.2.1.3)")] KeyUsageParse(String), #[error("issuer CA subjectKeyIdentifier missing (RFC 6487 §4.8.2)")] IssuerSkiMissing, #[error("EE authorityKeyIdentifier missing (RFC 6487 §4.8.3; RFC 5280 §4.2.1.1)")] EeAkiMissing, #[error( "EE authorityKeyIdentifier does not match issuer subjectKeyIdentifier (RFC 6487 §4.8.3)" )] EeAkiMismatch, #[error("EE authorityInfoAccess missing (RFC 6487 §4.8.7; RFC 5280 §4.2.2.1)")] EeAiaMissing, #[error( "EE authorityInfoAccess does not reference issuer certificate rsync URI (RFC 6487 §4.8.7)" )] EeAiaIssuerUriMismatch, #[error("EE CRLDistributionPoints missing (RFC 6487 §4.8.6; RFC 5280 §4.2.1.13)")] EeCrlDpMissing, #[error("EE CRLDistributionPoints does not reference issuer CRL rsync URI (RFC 6487 §4.8.6)")] EeCrlDpUriMismatch, #[error("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")] CertificateNotValidAtTime, #[error( "CRL signature/binding verification failed: {0} (RFC 5280 §6.3.3; RFC 6487 §5; RFC 9829 §3.1)" )] CrlVerify(#[from] CrlVerifyError), #[error( "CRL not valid at validation_time (RFC 5280 §6.3.3(g); RFC 5280 §5.1.2.4-§5.1.2.5; RFC 6487 §5)" )] CrlNotValidAtTime, #[error("EE certificate is revoked by issuer CRL (RFC 5280 §6.3.3; RFC 6487 §5)")] EeRevoked, } pub fn validate_ee_cert_path( ee_cert_der: &[u8], issuer_ca_der: &[u8], issuer_crl_der: &[u8], issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result { let ee = ResourceCertificate::decode_der(ee_cert_der)?; if ee.kind != ResourceCertKind::Ee { return Err(CertPathError::EeNotEe); } let issuer_ca = ResourceCertificate::decode_der(issuer_ca_der).map_err(CertPathError::IssuerDecode)?; if issuer_ca.kind != ResourceCertKind::Ca { return Err(CertPathError::IssuerNotCa); } let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?; if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) { return Err(CertPathError::IssuerSubjectMismatch { ee_issuer_dn: ee.tbs.issuer_name.to_string(), issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(), }); } validate_ee_aki_matches_issuer_ski(&ee, &issuer_ca)?; if let Some(expected_issuer_uri) = issuer_ca_rsync_uri { validate_ee_aia_points_to_issuer_uri(&ee, expected_issuer_uri)?; } if let Some(expected_crl_uri) = issuer_crl_rsync_uri { validate_ee_crldp_contains_issuer_crl_uri(&ee, expected_crl_uri)?; } if !time_within_validity( validation_time, ee.tbs.validity_not_before, ee.tbs.validity_not_after, ) || !time_within_validity( validation_time, issuer_ca.tbs.validity_not_before, issuer_ca.tbs.validity_not_after, ) { return Err(CertPathError::CertificateNotValidAtTime); } let ee_x509 = parse_x509_cert(ee_cert_der)?; verify_ee_cert_signature(&ee_x509, &issuer_spki)?; validate_ee_key_usage(&ee_x509)?; let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?; issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?; if !crl_valid_at_time(&issuer_crl, validation_time) { return Err(CertPathError::CrlNotValidAtTime); } if is_serial_revoked_by_crl(&ee, &issuer_crl) { return Err(CertPathError::EeRevoked); } Ok(ValidatedEeCertPath { ee, issuer_ca, issuer_crl, }) } /// Validate the EE certificate path using a *pre-decoded issuer CA* and a *pre-decoded and /// pre-verified issuer CRL*. /// /// This is a performance-oriented helper for stage2 serial runs: it avoids repeating issuer CA /// decode and issuer CRL decode+signature verification for every signed object in a publication point. /// /// The caller must ensure: /// - `issuer_ca` corresponds to `issuer_ca_der` /// - `issuer_crl` cryptographic signature/binding has already been verified against `issuer_ca_der` pub fn validate_ee_cert_path_with_prevalidated_issuer( ee_cert_der: &[u8], issuer_ca: &ResourceCertificate, issuer_spki: &SubjectPublicKeyInfo<'_>, issuer_crl: &RpkixCrl, issuer_crl_revoked_serials: &HashSet>, issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result { let ee = ResourceCertificate::decode_der(ee_cert_der)?; validate_ee_cert_path_components( &ee, ee_cert_der, issuer_ca, issuer_spki, issuer_crl, issuer_crl_revoked_serials, issuer_ca_rsync_uri, issuer_crl_rsync_uri, validation_time, )?; Ok(ee) } /// Validate the EE certificate path using a pre-decoded EE certificate and a pre-validated issuer. /// /// This avoids re-decoding the embedded EE certificate when the caller already parsed it while /// decoding a signed object (e.g. ROA / ASPA). pub fn validate_ee_cert_path_with_predecoded_ee( ee: &ResourceCertificate, ee_cert_der: &[u8], issuer_ca: &ResourceCertificate, issuer_spki: &SubjectPublicKeyInfo<'_>, issuer_crl: &RpkixCrl, issuer_crl_revoked_serials: &HashSet>, issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result<(), CertPathError> { validate_ee_cert_path_components( ee, ee_cert_der, issuer_ca, issuer_spki, issuer_crl, issuer_crl_revoked_serials, issuer_ca_rsync_uri, issuer_crl_rsync_uri, validation_time, ) } /// Validate a signed-object embedded EE certificate path using signed-object cached fields. /// /// This avoids reparsing the EE certificate as `X509Certificate` and bypasses the /// x509-parser signature wrapper by verifying the cached TBS/signature bytes directly. pub fn validate_signed_object_ee_cert_path_fast( ee: &ResourceEeCertificate, issuer_ca: &ResourceCertificate, issuer_spki: &SubjectPublicKeyInfo<'_>, issuer_crl: &RpkixCrl, issuer_crl_revoked_serials: &HashSet>, issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result<(), CertPathError> { validate_ee_cert_path_bindings_and_status( &ee.resource_cert, issuer_ca, issuer_crl, issuer_crl_revoked_serials, issuer_ca_rsync_uri, issuer_crl_rsync_uri, validation_time, )?; validate_ee_key_usage_summary(&ee.key_usage_summary)?; verify_ee_cert_signature_fast(&ee.tbs_certificate_der, &ee.signature_bytes, issuer_spki)?; Ok(()) } fn validate_ee_cert_path_components( ee: &ResourceCertificate, ee_cert_der: &[u8], issuer_ca: &ResourceCertificate, issuer_spki: &SubjectPublicKeyInfo<'_>, issuer_crl: &RpkixCrl, issuer_crl_revoked_serials: &HashSet>, issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result<(), CertPathError> { validate_ee_cert_path_bindings_and_status( ee, issuer_ca, issuer_crl, issuer_crl_revoked_serials, issuer_ca_rsync_uri, issuer_crl_rsync_uri, validation_time, )?; let ee_x509 = parse_x509_cert(ee_cert_der)?; verify_ee_cert_signature(&ee_x509, issuer_spki)?; validate_ee_key_usage(&ee_x509)?; Ok(()) } fn validate_ee_cert_path_bindings_and_status( ee: &ResourceCertificate, issuer_ca: &ResourceCertificate, issuer_crl: &RpkixCrl, issuer_crl_revoked_serials: &HashSet>, issuer_ca_rsync_uri: Option<&str>, issuer_crl_rsync_uri: Option<&str>, validation_time: time::OffsetDateTime, ) -> Result<(), CertPathError> { if ee.kind != ResourceCertKind::Ee { return Err(CertPathError::EeNotEe); } if issuer_ca.kind != ResourceCertKind::Ca { return Err(CertPathError::IssuerNotCa); } if !x509_names_equivalent(&ee.tbs.issuer_name, &issuer_ca.tbs.subject_name) { return Err(CertPathError::IssuerSubjectMismatch { ee_issuer_dn: ee.tbs.issuer_name.to_string(), issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(), }); } validate_ee_aki_matches_issuer_ski(ee, issuer_ca)?; if let Some(expected_issuer_uri) = issuer_ca_rsync_uri { validate_ee_aia_points_to_issuer_uri(ee, expected_issuer_uri)?; } if let Some(expected_crl_uri) = issuer_crl_rsync_uri { validate_ee_crldp_contains_issuer_crl_uri(ee, expected_crl_uri)?; } if !time_within_validity( validation_time, ee.tbs.validity_not_before, ee.tbs.validity_not_after, ) || !time_within_validity( validation_time, issuer_ca.tbs.validity_not_before, issuer_ca.tbs.validity_not_after, ) { return Err(CertPathError::CertificateNotValidAtTime); } if !crl_valid_at_time(issuer_crl, validation_time) { return Err(CertPathError::CrlNotValidAtTime); } let serial = BigUnsigned::from_biguint(&ee.tbs.serial_number); if issuer_crl_revoked_serials.contains(&serial.bytes_be) { return Err(CertPathError::EeRevoked); } Ok(()) } fn parse_subject_pki_from_der(der: &[u8]) -> Result, CertPathError> { let (rem, spki) = SubjectPublicKeyInfo::from_der(der) .map_err(|e| CertPathError::IssuerSpkiParse(e.to_string()))?; if !rem.is_empty() { return Err(CertPathError::IssuerSpkiTrailingBytes(rem.len())); } Ok(spki) } fn parse_x509_cert(der: &[u8]) -> Result, CertPathError> { let (rem, cert) = X509Certificate::from_der(der) .map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?; if !rem.is_empty() { return Err(CertPathError::EeSignatureInvalid( "trailing bytes after EE certificate".to_string(), )); } Ok(cert) } fn verify_ee_cert_signature( ee: &X509Certificate<'_>, issuer_spki: &SubjectPublicKeyInfo<'_>, ) -> Result<(), CertPathError> { ee.verify_signature(Some(issuer_spki)) .map_err(|e| CertPathError::EeSignatureInvalid(e.to_string())) } fn verify_ee_cert_signature_fast( ee_tbs_der: &[u8], ee_signature_bytes: &[u8], issuer_spki: &SubjectPublicKeyInfo<'_>, ) -> Result<(), CertPathError> { let key = signature::UnparsedPublicKey::new( &signature::RSA_PKCS1_2048_8192_SHA256, &issuer_spki.subject_public_key.data, ); key.verify(ee_tbs_der, ee_signature_bytes) .map_err(|e| CertPathError::EeSignatureInvalid(e.to_string())) } fn validate_ee_aki_matches_issuer_ski( ee: &ResourceCertificate, issuer_ca: &ResourceCertificate, ) -> Result<(), CertPathError> { let Some(issuer_ski) = issuer_ca.tbs.extensions.subject_key_identifier.as_deref() else { return Err(CertPathError::IssuerSkiMissing); }; let Some(ee_aki) = ee.tbs.extensions.authority_key_identifier.as_deref() else { return Err(CertPathError::EeAkiMissing); }; if ee_aki != issuer_ski { return Err(CertPathError::EeAkiMismatch); } Ok(()) } fn validate_ee_aia_points_to_issuer_uri( ee: &ResourceCertificate, issuer_ca_rsync_uri: &str, ) -> Result<(), CertPathError> { let Some(uris) = ee.tbs.extensions.ca_issuers_uris.as_ref() else { return Err(CertPathError::EeAiaMissing); }; if uris.iter().any(|u| u.as_str() == issuer_ca_rsync_uri) { return Ok(()); } if uris.iter().any(|u| u.starts_with("rsync://")) { return Ok(()); } Err(CertPathError::EeAiaIssuerUriMismatch) } fn validate_ee_crldp_contains_issuer_crl_uri( ee: &ResourceCertificate, issuer_crl_rsync_uri: &str, ) -> Result<(), CertPathError> { let Some(uris) = ee.tbs.extensions.crl_distribution_points_uris.as_ref() else { return Err(CertPathError::EeCrlDpMissing); }; if !uris.iter().any(|u| u.as_str() == issuer_crl_rsync_uri) { return Err(CertPathError::EeCrlDpUriMismatch); } Ok(()) } fn validate_ee_key_usage(cert: &X509Certificate<'_>) -> Result<(), CertPathError> { let mut ku_critical: Option = None; for ext in cert.extensions() { if ext.oid.as_bytes() == crate::data_model::oid::OID_KEY_USAGE_RAW { ku_critical = Some(ext.critical); break; } } let Some(critical) = ku_critical else { return Err(CertPathError::KeyUsageMissing); }; if !critical { return Err(CertPathError::KeyUsageNotCritical); } let Some(ku) = cert .key_usage() .map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))? else { return Err(CertPathError::KeyUsageMissing); }; let v = &ku.value; let ok = v.digital_signature() && !v.key_cert_sign() && !v.crl_sign() && !v.non_repudiation() && !v.key_encipherment() && !v.data_encipherment() && !v.key_agreement() && !v.encipher_only() && !v.decipher_only(); if !ok { return Err(CertPathError::KeyUsageInvalidBits); } Ok(()) } fn validate_ee_key_usage_summary(summary: &EeKeyUsageSummary) -> Result<(), CertPathError> { match summary { EeKeyUsageSummary::DigitalSignatureOnly => Ok(()), EeKeyUsageSummary::Missing => Err(CertPathError::KeyUsageMissing), EeKeyUsageSummary::NotCritical => Err(CertPathError::KeyUsageNotCritical), EeKeyUsageSummary::InvalidBits => Err(CertPathError::KeyUsageInvalidBits), EeKeyUsageSummary::ParseError(err) => Err(CertPathError::KeyUsageParse(err.clone())), } } fn time_within_validity( t: time::OffsetDateTime, not_before: time::OffsetDateTime, not_after: time::OffsetDateTime, ) -> bool { let t = t.to_offset(time::UtcOffset::UTC); let not_before = not_before.to_offset(time::UtcOffset::UTC); let not_after = not_after.to_offset(time::UtcOffset::UTC); t >= not_before && t <= not_after } fn crl_valid_at_time(crl: &RpkixCrl, t: time::OffsetDateTime) -> bool { let t = t.to_offset(time::UtcOffset::UTC); let this_update = crl.this_update.utc.to_offset(time::UtcOffset::UTC); let next_update = crl.next_update.utc.to_offset(time::UtcOffset::UTC); t >= this_update && t < next_update } fn is_serial_revoked_by_crl(ee: &ResourceCertificate, crl: &RpkixCrl) -> bool { let serial = BigUnsigned::from_biguint(&ee.tbs.serial_number); crl.revoked_certs .iter() .any(|rc| rc.serial_number == serial) } #[cfg(test)] mod tests { use super::*; use crate::data_model::common::X509NameDer; use crate::data_model::rc::{ RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate, }; use der_parser::num_bigint::BigUint; fn dummy_cert( kind: ResourceCertKind, subject_dn: &str, issuer_dn: &str, ski: Option>, aki: Option>, aia: Option>, crldp: Option>, ) -> ResourceCertificate { let aia = aia.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); let crldp = crldp.map(|v| v.into_iter().map(|s| s.to_string()).collect::>()); ResourceCertificate { raw_der: Vec::new(), kind, tbs: RpkixTbsCertificate { version: 2, serial_number: BigUint::from(1u8), signature_algorithm: "1.2.840.113549.1.1.11".to_string(), issuer_name: X509NameDer(issuer_dn.as_bytes().to_vec()), subject_name: X509NameDer(subject_dn.as_bytes().to_vec()), validity_not_before: time::OffsetDateTime::UNIX_EPOCH, validity_not_after: time::OffsetDateTime::UNIX_EPOCH, subject_public_key_info: Vec::new(), extensions: RcExtensions { basic_constraints_ca: kind == ResourceCertKind::Ca, subject_key_identifier: ski, authority_key_identifier: aki, crl_distribution_points_uris: crldp, ca_issuers_uris: aia, subject_info_access: None, certificate_policies_oid: None, ip_resources: None, as_resources: None, }, }, } } #[test] fn ee_aki_and_aia_and_crldp_binding_checks_report_errors() { let issuer = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", Some(vec![1]), None, None, None, ); let ee = dummy_cert( ResourceCertKind::Ee, "CN=ee", "CN=issuer", Some(vec![2]), Some(vec![9]), Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); let err = validate_ee_aki_matches_issuer_ski(&ee, &issuer).unwrap_err(); assert!(matches!(err, CertPathError::EeAkiMismatch), "{err}"); let issuer_missing_ski = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", None, None, None, None, ); let err = validate_ee_aki_matches_issuer_ski(&ee, &issuer_missing_ski).unwrap_err(); assert!(matches!(err, CertPathError::IssuerSkiMissing), "{err}"); let ee_missing_aki = dummy_cert( ResourceCertKind::Ee, "CN=ee", "CN=issuer", Some(vec![2]), None, Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); let err = validate_ee_aki_matches_issuer_ski(&ee_missing_aki, &issuer).unwrap_err(); assert!(matches!(err, CertPathError::EeAkiMissing), "{err}"); let ee_missing_aia = dummy_cert( ResourceCertKind::Ee, "CN=ee", "CN=issuer", Some(vec![2]), Some(vec![1]), None, Some(vec!["rsync://example.test/issuer.crl"]), ); let err = validate_ee_aia_points_to_issuer_uri( &ee_missing_aia, "rsync://example.test/issuer.cer", ) .unwrap_err(); assert!(matches!(err, CertPathError::EeAiaMissing), "{err}"); let ee_wrong_aia = dummy_cert( ResourceCertKind::Ee, "CN=ee", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/other.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); validate_ee_aia_points_to_issuer_uri(&ee_wrong_aia, "rsync://example.test/issuer.cer") .expect("non-matching rsync AIA is currently accepted"); let ee_missing_crldp = dummy_cert( ResourceCertKind::Ee, "CN=ee", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/issuer.cer"]), None, ); let err = validate_ee_crldp_contains_issuer_crl_uri( &ee_missing_crldp, "rsync://example.test/issuer.crl", ) .unwrap_err(); assert!(matches!(err, CertPathError::EeCrlDpMissing), "{err}"); let ee_wrong_crldp = dummy_cert( ResourceCertKind::Ee, "CN=ee", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/other.crl"]), ); let err = validate_ee_crldp_contains_issuer_crl_uri( &ee_wrong_crldp, "rsync://example.test/issuer.crl", ) .unwrap_err(); assert!(matches!(err, CertPathError::EeCrlDpUriMismatch), "{err}"); } #[test] fn ee_binding_checks_accept_when_matching() { let issuer = dummy_cert( ResourceCertKind::Ca, "CN=issuer", "CN=issuer", Some(vec![1]), None, None, None, ); let ee = dummy_cert( ResourceCertKind::Ee, "CN=ee", "CN=issuer", Some(vec![2]), Some(vec![1]), Some(vec!["rsync://example.test/issuer.cer"]), Some(vec!["rsync://example.test/issuer.crl"]), ); validate_ee_aki_matches_issuer_ski(&ee, &issuer).expect("aki ok"); validate_ee_aia_points_to_issuer_uri(&ee, "rsync://example.test/issuer.cer") .expect("aia ok"); validate_ee_crldp_contains_issuer_crl_uri(&ee, "rsync://example.test/issuer.crl") .expect("crldp ok"); } }