手动执行全量同步
This commit is contained in:
@@ -8,6 +8,8 @@ use crate::data_model::rc::{
|
||||
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 ValidatedSubordinateCa {
|
||||
@@ -18,6 +20,13 @@ pub struct ValidatedSubordinateCa {
|
||||
pub effective_as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ValidatedSubordinateCaLite {
|
||||
pub child_ca: ResourceCertificate,
|
||||
pub effective_ip_resources: Option<IpResourceSet>,
|
||||
pub effective_as_resources: Option<AsResourceSet>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CaPathError {
|
||||
#[error("child CA certificate decode failed: {0} (RFC 6487 §4; RFC 5280 §4.1)")]
|
||||
@@ -50,6 +59,14 @@ pub enum CaPathError {
|
||||
#[error("child CA certificate signature verification failed: {0} (RFC 5280 §6.1)")]
|
||||
ChildSignatureInvalid(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("certificate not valid at validation_time (RFC 5280 §4.1.2.5; RFC 5280 §6.1)")]
|
||||
CertificateNotValidAtTime,
|
||||
|
||||
@@ -136,6 +153,7 @@ pub fn validate_subordinate_ca_cert(
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
let issuer_spki = parse_subject_pki_from_der(&issuer_ca.tbs.subject_public_key_info)?;
|
||||
|
||||
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CaPathError::IssuerSubjectMismatch {
|
||||
@@ -162,8 +180,9 @@ pub fn validate_subordinate_ca_cert(
|
||||
return Err(CaPathError::CertificateNotValidAtTime);
|
||||
}
|
||||
|
||||
verify_cert_signature_with_issuer(child_ca_der, issuer_ca_der)?;
|
||||
validate_child_ca_key_usage(child_ca_der)?;
|
||||
let child_x509 = parse_x509_cert(child_ca_der)?;
|
||||
verify_child_signature(&child_x509, &issuer_spki)?;
|
||||
validate_child_ca_key_usage(&child_x509)?;
|
||||
|
||||
let issuer_crl = RpkixCrl::decode_der(issuer_crl_der)?;
|
||||
issuer_crl.verify_signature_with_issuer_certificate_der(issuer_ca_der)?;
|
||||
@@ -196,6 +215,116 @@ pub fn validate_subordinate_ca_cert(
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate a subordinate child CA using *pre-decoded issuer CA* and *pre-decoded+verified issuer CRL*.
|
||||
///
|
||||
/// This avoids repeating issuer CA decode and issuer CRL decode+signature verification for every
|
||||
/// child CA certificate discovered in a publication point.
|
||||
pub fn validate_subordinate_ca_cert_with_prevalidated_issuer(
|
||||
child_ca_der: &[u8],
|
||||
child_ca: ResourceCertificate,
|
||||
issuer_ca: &ResourceCertificate,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
issuer_crl: &RpkixCrl,
|
||||
issuer_crl_revoked_serials: &HashSet<Vec<u8>>,
|
||||
issuer_ca_rsync_uri: Option<&str>,
|
||||
issuer_crl_rsync_uri: &str,
|
||||
issuer_effective_ip: Option<&IpResourceSet>,
|
||||
issuer_effective_as: Option<&AsResourceSet>,
|
||||
validation_time: time::OffsetDateTime,
|
||||
) -> Result<ValidatedSubordinateCaLite, CaPathError> {
|
||||
if child_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::ChildNotCa);
|
||||
}
|
||||
if issuer_ca.kind != ResourceCertKind::Ca {
|
||||
return Err(CaPathError::IssuerNotCa);
|
||||
}
|
||||
|
||||
if !x509_names_equivalent(&child_ca.tbs.issuer_name, &issuer_ca.tbs.subject_name) {
|
||||
return Err(CaPathError::IssuerSubjectMismatch {
|
||||
child_issuer_dn: child_ca.tbs.issuer_name.to_string(),
|
||||
issuer_subject_dn: issuer_ca.tbs.subject_name.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
validate_child_aki_matches_issuer_ski(&child_ca, issuer_ca)?;
|
||||
if let Some(expected_issuer_uri) = issuer_ca_rsync_uri {
|
||||
validate_child_aia_points_to_issuer_uri(&child_ca, expected_issuer_uri)?;
|
||||
}
|
||||
validate_child_crldp_contains_issuer_crl_uri(&child_ca, issuer_crl_rsync_uri)?;
|
||||
|
||||
if !time_within_validity(
|
||||
validation_time,
|
||||
child_ca.tbs.validity_not_before,
|
||||
child_ca.tbs.validity_not_after,
|
||||
) || !time_within_validity(
|
||||
validation_time,
|
||||
issuer_ca.tbs.validity_not_before,
|
||||
issuer_ca.tbs.validity_not_after,
|
||||
) {
|
||||
return Err(CaPathError::CertificateNotValidAtTime);
|
||||
}
|
||||
|
||||
let child_x509 = parse_x509_cert(child_ca_der)?;
|
||||
verify_child_signature(&child_x509, issuer_spki)?;
|
||||
validate_child_ca_key_usage(&child_x509)?;
|
||||
|
||||
if !crl_valid_at_time(issuer_crl, validation_time) {
|
||||
return Err(CaPathError::CrlNotValidAtTime);
|
||||
}
|
||||
|
||||
let serial = BigUnsigned::from_biguint(&child_ca.tbs.serial_number);
|
||||
if issuer_crl_revoked_serials.contains(&serial.bytes_be) {
|
||||
return Err(CaPathError::ChildRevoked);
|
||||
}
|
||||
|
||||
let effective_ip_resources = resolve_child_ip_resources(
|
||||
child_ca.tbs.extensions.ip_resources.as_ref(),
|
||||
issuer_effective_ip,
|
||||
)?;
|
||||
let effective_as_resources = resolve_child_as_resources(
|
||||
child_ca.tbs.extensions.as_resources.as_ref(),
|
||||
issuer_effective_as,
|
||||
)?;
|
||||
if effective_ip_resources.is_none() && effective_as_resources.is_none() {
|
||||
return Err(CaPathError::ResourcesMissing);
|
||||
}
|
||||
|
||||
Ok(ValidatedSubordinateCaLite {
|
||||
child_ca,
|
||||
effective_ip_resources,
|
||||
effective_as_resources,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_subject_pki_from_der(der: &[u8]) -> Result<SubjectPublicKeyInfo<'_>, CaPathError> {
|
||||
let (rem, spki) = SubjectPublicKeyInfo::from_der(der)
|
||||
.map_err(|e| CaPathError::IssuerSpkiParse(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CaPathError::IssuerSpkiTrailingBytes(rem.len()));
|
||||
}
|
||||
Ok(spki)
|
||||
}
|
||||
|
||||
fn parse_x509_cert(der: &[u8]) -> Result<X509Certificate<'_>, CaPathError> {
|
||||
let (rem, cert) = X509Certificate::from_der(der)
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CaPathError::ChildSignatureInvalid(
|
||||
"trailing bytes after child certificate".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(cert)
|
||||
}
|
||||
|
||||
fn verify_child_signature(
|
||||
child: &X509Certificate<'_>,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CaPathError> {
|
||||
child
|
||||
.verify_signature(Some(issuer_spki))
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
||||
}
|
||||
|
||||
fn validate_child_aki_matches_issuer_ski(
|
||||
child: &ResourceCertificate,
|
||||
issuer: &ResourceCertificate,
|
||||
@@ -238,15 +367,7 @@ fn validate_child_crldp_contains_issuer_crl_uri(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_child_ca_key_usage(child_ca_der: &[u8]) -> Result<(), CaPathError> {
|
||||
let (rem, cert) = X509Certificate::from_der(child_ca_der)
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CaPathError::ChildSignatureInvalid(
|
||||
"trailing bytes after child CA certificate".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
fn validate_child_ca_key_usage(cert: &X509Certificate<'_>) -> Result<(), CaPathError> {
|
||||
let mut ku_critical: Option<bool> = None;
|
||||
for ext in cert.extensions() {
|
||||
if ext.oid.as_bytes() == OID_KEY_USAGE_RAW {
|
||||
@@ -286,30 +407,6 @@ fn validate_child_ca_key_usage(child_ca_der: &[u8]) -> Result<(), CaPathError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_cert_signature_with_issuer(
|
||||
child_der: &[u8],
|
||||
issuer_ca_der: &[u8],
|
||||
) -> Result<(), CaPathError> {
|
||||
let (rem, child) = X509Certificate::from_der(child_der)
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CaPathError::ChildSignatureInvalid(
|
||||
"trailing bytes after child certificate".to_string(),
|
||||
));
|
||||
}
|
||||
let (rem, issuer) = X509Certificate::from_der(issuer_ca_der)
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))?;
|
||||
if !rem.is_empty() {
|
||||
return Err(CaPathError::ChildSignatureInvalid(
|
||||
"trailing bytes after issuer certificate".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
child
|
||||
.verify_signature(Some(&issuer.tbs_certificate.subject_pki))
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
||||
}
|
||||
|
||||
fn time_within_validity(
|
||||
t: time::OffsetDateTime,
|
||||
not_before: time::OffsetDateTime,
|
||||
@@ -691,13 +788,13 @@ fn bytes_leq(a: &[u8], b: &[u8]) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::data_model::common::X509NameDer;
|
||||
use crate::data_model::rc::{
|
||||
Afi, AsIdentifierChoice, AsResourceSet, IpAddressChoice, IpAddressFamily, IpResourceSet,
|
||||
};
|
||||
use crate::data_model::rc::{
|
||||
RcExtensions, ResourceCertKind, ResourceCertificate, RpkixTbsCertificate,
|
||||
};
|
||||
use crate::data_model::common::X509NameDer;
|
||||
use der_parser::num_bigint::BigUint;
|
||||
use std::process::Command;
|
||||
fn dummy_cert(
|
||||
@@ -823,7 +920,10 @@ mod tests {
|
||||
};
|
||||
|
||||
// Issuer self-signed.
|
||||
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&issuer_key).arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["genrsa", "-out"])
|
||||
.arg(&issuer_key)
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&issuer_key)
|
||||
@@ -843,7 +943,10 @@ mod tests {
|
||||
.arg(&issuer_der));
|
||||
|
||||
// Child signed by issuer.
|
||||
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&child_key).arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["genrsa", "-out"])
|
||||
.arg(&child_key)
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&child_key)
|
||||
@@ -865,7 +968,10 @@ mod tests {
|
||||
.arg(&child_der));
|
||||
|
||||
// Other self-signed issuer.
|
||||
run(Command::new("openssl").args(["genrsa", "-out"]).arg(&other_key).arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["genrsa", "-out"])
|
||||
.arg(&other_key)
|
||||
.arg("2048"));
|
||||
run(Command::new("openssl")
|
||||
.args(["req", "-new", "-key"])
|
||||
.arg(&other_key)
|
||||
@@ -1116,20 +1222,23 @@ mod tests {
|
||||
td.path(),
|
||||
Some("keyUsage = critical, keyCertSign, cRLSign"),
|
||||
);
|
||||
validate_child_ca_key_usage(&der).expect("key usage ok");
|
||||
let cert = parse_x509_cert(&der).expect("x509 parse ok");
|
||||
validate_child_ca_key_usage(&cert).expect("key usage ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_child_ca_key_usage_rejects_missing_noncritical_and_invalid_bits() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let missing = write_cert_der_with_addext(td.path(), None);
|
||||
let err = validate_child_ca_key_usage(&missing).unwrap_err();
|
||||
let cert = parse_x509_cert(&missing).expect("x509 parse ok");
|
||||
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageMissing), "{err}");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let noncritical =
|
||||
write_cert_der_with_addext(td.path(), Some("keyUsage = keyCertSign, cRLSign"));
|
||||
let err = validate_child_ca_key_usage(&noncritical).unwrap_err();
|
||||
let cert = parse_x509_cert(&noncritical).expect("x509 parse ok");
|
||||
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageNotCritical), "{err}");
|
||||
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
@@ -1137,7 +1246,8 @@ mod tests {
|
||||
td.path(),
|
||||
Some("keyUsage = critical, keyCertSign, cRLSign, digitalSignature"),
|
||||
);
|
||||
let err = validate_child_ca_key_usage(&invalid).unwrap_err();
|
||||
let cert = parse_x509_cert(&invalid).expect("x509 parse ok");
|
||||
let err = validate_child_ca_key_usage(&cert).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::KeyUsageInvalidBits), "{err}");
|
||||
}
|
||||
|
||||
@@ -1145,9 +1255,17 @@ mod tests {
|
||||
fn verify_cert_signature_with_issuer_accepts_valid_chain_and_rejects_wrong_issuer() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let (issuer, child, other) = gen_issuer_and_child_der(td.path());
|
||||
verify_cert_signature_with_issuer(&child, &issuer).expect("signature ok");
|
||||
let err = verify_cert_signature_with_issuer(&child, &other).unwrap_err();
|
||||
assert!(matches!(err, CaPathError::ChildSignatureInvalid(_)), "{err}");
|
||||
let issuer_cert = parse_x509_cert(&issuer).expect("x509 parse issuer");
|
||||
let child_cert = parse_x509_cert(&child).expect("x509 parse child");
|
||||
verify_child_signature(&child_cert, &issuer_cert.tbs_certificate.subject_pki)
|
||||
.expect("signature ok");
|
||||
let other_cert = parse_x509_cert(&other).expect("x509 parse other");
|
||||
let err = verify_child_signature(&child_cert, &other_cert.tbs_certificate.subject_pki)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CaPathError::ChildSignatureInvalid(_)),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1214,10 +1332,9 @@ mod tests {
|
||||
assert!(matches!(err, CaPathError::ResourcesNotSubset), "{err}");
|
||||
|
||||
let parent_as = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Range {
|
||||
min: 1,
|
||||
max: 100,
|
||||
}])),
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![
|
||||
AsIdOrRange::Range { min: 1, max: 100 },
|
||||
])),
|
||||
rdi: None,
|
||||
};
|
||||
let child_as_inherit = AsResourceSet {
|
||||
@@ -1238,7 +1355,9 @@ mod tests {
|
||||
.expect("some");
|
||||
|
||||
let child_as_bad = AsResourceSet {
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(200)])),
|
||||
asnum: Some(AsIdentifierChoice::AsIdsOrRanges(vec![AsIdOrRange::Id(
|
||||
200,
|
||||
)])),
|
||||
rdi: None,
|
||||
};
|
||||
let err = resolve_child_as_resources(Some(&child_as_bad), Some(&parent_as)).unwrap_err();
|
||||
|
||||
Reference in New Issue
Block a user