串行验证通过

This commit is contained in:
2026-02-09 19:35:54 +08:00
parent 7be865d7f1
commit afc31c02ab
66 changed files with 12049 additions and 4 deletions

497
src/validation/cert_path.rs Normal file
View File

@@ -0,0 +1,497 @@
use crate::data_model::common::BigUnsigned;
use crate::data_model::crl::{CrlDecodeError, CrlVerifyError, RpkixCrl};
use crate::data_model::rc::{
ResourceCertKind, ResourceCertificate, ResourceCertificateDecodeError,
};
use x509_parser::prelude::{FromDer, X509Certificate};
#[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(
"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("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<ValidatedEeCertPath, CertPathError> {
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);
}
if ee.tbs.issuer_dn != issuer_ca.tbs.subject_dn {
return Err(CertPathError::IssuerSubjectMismatch {
ee_issuer_dn: ee.tbs.issuer_dn.clone(),
issuer_subject_dn: issuer_ca.tbs.subject_dn.clone(),
});
}
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);
}
verify_cert_signature_with_issuer(ee_cert_der, issuer_ca_der)?;
validate_ee_key_usage(ee_cert_der)?;
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,
})
}
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 Err(CertPathError::EeAiaIssuerUriMismatch);
}
Ok(())
}
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(ee_cert_der: &[u8]) -> Result<(), CertPathError> {
let (rem, cert) = X509Certificate::from_der(ee_cert_der)
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::EeSignatureInvalid(
"trailing bytes after EE certificate".to_string(),
));
}
let mut ku_critical: Option<bool> = None;
for ext in cert.extensions() {
if ext.oid.to_id_string() == crate::data_model::oid::OID_KEY_USAGE {
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 verify_cert_signature_with_issuer(
ee_cert_der: &[u8],
issuer_ca_der: &[u8],
) -> Result<(), CertPathError> {
let (rem, ee) = X509Certificate::from_der(ee_cert_der)
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::EeSignatureInvalid(
"trailing bytes after EE certificate".to_string(),
));
}
let (rem, issuer) = X509Certificate::from_der(issuer_ca_der)
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))?;
if !rem.is_empty() {
return Err(CertPathError::EeSignatureInvalid(
"trailing bytes after issuer certificate".to_string(),
));
}
ee.verify_signature(Some(&issuer.tbs_certificate.subject_pki))
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
}
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::rc::{RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate};
use der_parser::num_bigint::BigUint;
use url::Url;
fn dummy_cert(
kind: ResourceCertKind,
subject_dn: &str,
issuer_dn: &str,
ski: Option<Vec<u8>>,
aki: Option<Vec<u8>>,
aia: Option<Vec<&str>>,
crldp: Option<Vec<&str>>,
) -> ResourceCertificate {
let aia = aia.map(|v| {
v.into_iter()
.map(|s| Url::parse(s).expect("url"))
.collect::<Vec<_>>()
});
let crldp = crldp.map(|v| {
v.into_iter()
.map(|s| Url::parse(s).expect("url"))
.collect::<Vec<_>>()
});
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_dn: issuer_dn.to_string(),
subject_dn: subject_dn.to_string(),
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"]),
);
let err =
validate_ee_aia_points_to_issuer_uri(&ee_wrong_aia, "rsync://example.test/issuer.cer")
.unwrap_err();
assert!(
matches!(err, CertPathError::EeAiaIssuerUriMismatch),
"{err}"
);
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");
}
}