377 lines
14 KiB
Rust
377 lines
14 KiB
Rust
pub use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc, BigUnsigned};
|
|
use crate::data_model::oid::{
|
|
OID_AUTHORITY_KEY_IDENTIFIER, OID_CRL_NUMBER, OID_SHA256_WITH_RSA_ENCRYPTION,
|
|
OID_SUBJECT_KEY_IDENTIFIER,
|
|
};
|
|
use x509_parser::extensions::{AuthorityKeyIdentifier, ParsedExtension, X509Extension};
|
|
use x509_parser::prelude::FromDer;
|
|
use x509_parser::prelude::X509Version;
|
|
use x509_parser::revocation_list::CertificateRevocationList;
|
|
use x509_parser::certificate::X509Certificate;
|
|
use x509_parser::x509::SubjectPublicKeyInfo;
|
|
use x509_parser::x509::AlgorithmIdentifier;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RevokedCert {
|
|
pub serial_number: BigUnsigned,
|
|
pub revocation_date: Asn1TimeUtc,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct CrlExtensions {
|
|
pub authority_key_identifier: Vec<u8>,
|
|
pub crl_number: BigUnsigned,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct RpkixCrl {
|
|
pub raw_der: Vec<u8>,
|
|
pub version: u32,
|
|
pub issuer_dn: String,
|
|
pub signature_algorithm_oid: String,
|
|
pub this_update: Asn1TimeUtc,
|
|
pub next_update: Asn1TimeUtc,
|
|
pub revoked_certs: Vec<RevokedCert>,
|
|
pub extensions: CrlExtensions,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum CrlDecodeError {
|
|
#[error("X.509 CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
|
Parse(String),
|
|
|
|
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
|
TrailingBytes(usize),
|
|
|
|
#[error("CRL version must be v2, got {0:?} (RFC 5280 §5.1; RFC 6487 §5)")]
|
|
InvalidVersion(Option<u32>),
|
|
|
|
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0} (RFC 6487 §5; RFC 7935 §2)")]
|
|
InvalidSignatureAlgorithm(String),
|
|
|
|
#[error("CRL signature algorithm parameters must be absent or NULL (RFC 5280 §4.1.1.2; RFC 7935 §2)")]
|
|
InvalidSignatureAlgorithmParameters,
|
|
|
|
#[error("CRL signatureAlgorithm must match TBSCertList.signature (RFC 5280 §5.1)")]
|
|
SignatureAlgorithmMismatch,
|
|
|
|
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0} (RFC 9829 §3.1)")]
|
|
InvalidExtensionsCount(usize),
|
|
|
|
#[error("unsupported CRL extension OID {0} (RFC 9829 §3.1)")]
|
|
UnsupportedExtension(String),
|
|
|
|
#[error("duplicate CRL extension OID {0} (RFC 5280 §4.2; RFC 9829 §3.1)")]
|
|
DuplicateExtension(String),
|
|
|
|
#[error("AuthorityKeyIdentifier must contain keyIdentifier (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
|
AkiMissingKeyIdentifier,
|
|
|
|
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber (RFC 5280 §5.2.1; RFC 9829 §3.1)")]
|
|
AkiHasOtherFields,
|
|
|
|
#[error("CRLNumber must be non-critical (RFC 9829 §3.1; RFC 5280 §5.2.3)")]
|
|
CrlNumberCritical,
|
|
|
|
#[error("CRLNumber out of range (must fit in 0..2^159-1) (RFC 9829 §3.1)")]
|
|
CrlNumberOutOfRange,
|
|
|
|
#[error("CRL entry extensions must not be present (RFC 6487 §5; RFC 5280 §5.1)")]
|
|
EntryExtensionsNotAllowed,
|
|
|
|
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5; RFC 6487 §5)")]
|
|
NextUpdateMissing,
|
|
|
|
#[error("{field} time encoding invalid for year {year}: got {encoding:?} (RFC 5280 §5.1.2.4-§5.1.2.6)")]
|
|
InvalidTimeEncoding {
|
|
field: &'static str,
|
|
year: i32,
|
|
encoding: Asn1TimeEncoding,
|
|
},
|
|
}
|
|
|
|
impl RpkixCrl {
|
|
/// Decode a DER-encoded X.509 v2 CRL and enforce the RPKI profile constraints from
|
|
/// `specs/prepare/data_models/04_crl.md` (RFC 6487 §5; RFC 9829 §3.1; RFC 5280 §5.1).
|
|
pub fn decode_der(der: &[u8]) -> Result<Self, CrlDecodeError> {
|
|
let (rem, crl) = CertificateRevocationList::from_der(der)
|
|
.map_err(|e| CrlDecodeError::Parse(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(CrlDecodeError::TrailingBytes(rem.len()));
|
|
}
|
|
|
|
let version = match crl.version() {
|
|
Some(X509Version::V2) => 2,
|
|
Some(v) => return Err(CrlDecodeError::InvalidVersion(Some(v.0))),
|
|
None => return Err(CrlDecodeError::InvalidVersion(None)),
|
|
};
|
|
|
|
let sig_oid = crl.signature_algorithm.algorithm.to_id_string();
|
|
let tbs_sig_oid = crl.tbs_cert_list.signature.algorithm.to_id_string();
|
|
if sig_oid != tbs_sig_oid {
|
|
return Err(CrlDecodeError::SignatureAlgorithmMismatch);
|
|
}
|
|
if sig_oid != OID_SHA256_WITH_RSA_ENCRYPTION {
|
|
return Err(CrlDecodeError::InvalidSignatureAlgorithm(sig_oid));
|
|
}
|
|
validate_sig_params(&crl.signature_algorithm)?;
|
|
validate_sig_params(&crl.tbs_cert_list.signature)?;
|
|
|
|
let extensions = parse_and_validate_extensions(crl.extensions())?;
|
|
|
|
let revoked_certs = crl
|
|
.iter_revoked_certificates()
|
|
.map(|rc| {
|
|
if !rc.extensions().is_empty() {
|
|
return Err(CrlDecodeError::EntryExtensionsNotAllowed);
|
|
}
|
|
let revocation_date = crate::data_model::common::asn1_time_to_model(rc.revocation_date);
|
|
validate_time_encoding_rfc5280("revocationDate", &revocation_date)?;
|
|
Ok(RevokedCert {
|
|
serial_number: BigUnsigned::from_biguint(rc.serial()),
|
|
revocation_date,
|
|
})
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
|
|
let this_update = crate::data_model::common::asn1_time_to_model(crl.last_update());
|
|
validate_time_encoding_rfc5280("thisUpdate", &this_update)?;
|
|
|
|
let next_update = crl
|
|
.next_update()
|
|
.map(crate::data_model::common::asn1_time_to_model)
|
|
.ok_or(CrlDecodeError::NextUpdateMissing)?;
|
|
validate_time_encoding_rfc5280("nextUpdate", &next_update)?;
|
|
|
|
Ok(RpkixCrl {
|
|
raw_der: der.to_vec(),
|
|
version,
|
|
issuer_dn: crl.issuer().to_string(),
|
|
signature_algorithm_oid: OID_SHA256_WITH_RSA_ENCRYPTION.to_string(),
|
|
this_update,
|
|
next_update,
|
|
revoked_certs,
|
|
extensions,
|
|
})
|
|
}
|
|
|
|
/// Verify the cryptographic signature on this CRL using the issuer certificate.
|
|
///
|
|
/// Signature verification needs the issuer public key (RFC 5280 §6.3.3 (f)-(g)).
|
|
/// In RPKI practice, this public key is obtained from the CRL issuer CA certificate
|
|
/// (and that certificate must already be validated up to the same trust anchor).
|
|
///
|
|
/// This helper also performs common binding checks:
|
|
/// - CRL `issuer_dn` must equal issuer certificate `subject`
|
|
/// - if issuer KeyUsage is present, require `cRLSign`
|
|
/// - if issuer SKI is present, require it matches CRL AKI.keyIdentifier
|
|
pub fn verify_signature_with_issuer_certificate_der(
|
|
&self,
|
|
issuer_cert_der: &[u8],
|
|
) -> Result<(), CrlVerifyError> {
|
|
let (rem, issuer_cert) = X509Certificate::from_der(issuer_cert_der)
|
|
.map_err(|e| CrlVerifyError::IssuerCertificateParse(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(CrlVerifyError::IssuerCertificateTrailingBytes(rem.len()));
|
|
}
|
|
|
|
let subject_dn = issuer_cert.subject().to_string();
|
|
if subject_dn != self.issuer_dn {
|
|
return Err(CrlVerifyError::IssuerSubjectMismatch {
|
|
crl_issuer_dn: self.issuer_dn.clone(),
|
|
issuer_subject_dn: subject_dn,
|
|
});
|
|
}
|
|
|
|
if let Some(ku) = issuer_cert
|
|
.key_usage()
|
|
.map_err(|e| CrlVerifyError::IssuerCertificateParse(e.to_string()))?
|
|
{
|
|
if !ku.value.crl_sign() {
|
|
return Err(CrlVerifyError::IssuerKeyUsageMissingCrlSign);
|
|
}
|
|
}
|
|
|
|
if let Some(issuer_ski) = get_subject_key_identifier(&issuer_cert) {
|
|
if issuer_ski != self.extensions.authority_key_identifier {
|
|
return Err(CrlVerifyError::AkiSkiMismatch);
|
|
}
|
|
}
|
|
|
|
self.verify_signature_with_issuer_spki(issuer_cert.public_key())
|
|
}
|
|
|
|
/// Verify the cryptographic signature on this CRL using the issuer SubjectPublicKeyInfo.
|
|
pub fn verify_signature_with_issuer_spki(
|
|
&self,
|
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
|
) -> Result<(), CrlVerifyError> {
|
|
let (rem, crl) = CertificateRevocationList::from_der(&self.raw_der)
|
|
.map_err(|e| CrlVerifyError::CrlParse(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(CrlVerifyError::CrlTrailingBytes(rem.len()));
|
|
}
|
|
crl.verify_signature(issuer_spki)
|
|
.map_err(|e| CrlVerifyError::InvalidSignature(e.to_string()))
|
|
}
|
|
|
|
/// Verify the cryptographic signature on this CRL using a DER-encoded SubjectPublicKeyInfo.
|
|
pub fn verify_signature_with_issuer_spki_der(
|
|
&self,
|
|
issuer_spki_der: &[u8],
|
|
) -> Result<(), CrlVerifyError> {
|
|
let (rem, spki) = SubjectPublicKeyInfo::from_der(issuer_spki_der)
|
|
.map_err(|e| CrlVerifyError::IssuerSpkiParse(e.to_string()))?;
|
|
if !rem.is_empty() {
|
|
return Err(CrlVerifyError::IssuerSpkiTrailingBytes(rem.len()));
|
|
}
|
|
self.verify_signature_with_issuer_spki(&spki)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum CrlVerifyError {
|
|
#[error("issuer certificate parse error: {0} (RFC 5280 §4.1; RFC 6487 §4)")]
|
|
IssuerCertificateParse(String),
|
|
|
|
#[error("trailing bytes after issuer certificate DER: {0} bytes (DER; RFC 5280 §4.1)")]
|
|
IssuerCertificateTrailingBytes(usize),
|
|
|
|
#[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("CRL parse error: {0} (RFC 5280 §5.1; RFC 6487 §5)")]
|
|
CrlParse(String),
|
|
|
|
#[error("trailing bytes after CRL DER: {0} bytes (DER; RFC 5280 §5.1)")]
|
|
CrlTrailingBytes(usize),
|
|
|
|
#[error("CRL issuer DN does not match issuer certificate subject (RFC 5280 §5.1; RFC 5280 §6.3.3(b))")]
|
|
IssuerSubjectMismatch {
|
|
crl_issuer_dn: String,
|
|
issuer_subject_dn: String,
|
|
},
|
|
|
|
#[error("issuer certificate keyUsage present but missing cRLSign (RFC 5280 §4.2.1.3; RFC 5280 §6.3.3(f))")]
|
|
IssuerKeyUsageMissingCrlSign,
|
|
|
|
#[error("CRL AKI.keyIdentifier does not match issuer certificate SKI (RFC 5280 §4.2.1.1; RFC 5280 §4.2.1.2; RFC 5280 §6.3.3(c)/(f))")]
|
|
AkiSkiMismatch,
|
|
|
|
#[error("CRL signature verification failed: {0} (RFC 5280 §6.3.3(g); RFC 7935 §2)")]
|
|
InvalidSignature(String),
|
|
}
|
|
|
|
fn validate_time_encoding_rfc5280(
|
|
field: &'static str,
|
|
t: &Asn1TimeUtc,
|
|
) -> Result<(), CrlDecodeError> {
|
|
let year = t.utc.year();
|
|
let expected = if year <= 2049 {
|
|
Asn1TimeEncoding::UtcTime
|
|
} else {
|
|
Asn1TimeEncoding::GeneralizedTime
|
|
};
|
|
if t.encoding != expected {
|
|
return Err(CrlDecodeError::InvalidTimeEncoding {
|
|
field,
|
|
year,
|
|
encoding: t.encoding,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_sig_params(sig: &AlgorithmIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
|
if crate::data_model::common::algorithm_params_absent_or_null(sig) {
|
|
Ok(())
|
|
} else {
|
|
Err(CrlDecodeError::InvalidSignatureAlgorithmParameters)
|
|
}
|
|
}
|
|
|
|
fn parse_and_validate_extensions(exts: &[X509Extension<'_>]) -> Result<CrlExtensions, CrlDecodeError> {
|
|
if exts.len() != 2 {
|
|
return Err(CrlDecodeError::InvalidExtensionsCount(exts.len()));
|
|
}
|
|
|
|
let mut authority_key_identifier: Option<Vec<u8>> = None;
|
|
let mut crl_number: Option<BigUnsigned> = None;
|
|
|
|
for ext in exts {
|
|
let oid = ext.oid.to_id_string();
|
|
match oid.as_str() {
|
|
OID_AUTHORITY_KEY_IDENTIFIER => {
|
|
if authority_key_identifier.is_some() {
|
|
return Err(CrlDecodeError::DuplicateExtension(oid));
|
|
}
|
|
let aki = parse_aki(ext)?;
|
|
authority_key_identifier = Some(aki);
|
|
}
|
|
OID_CRL_NUMBER => {
|
|
if crl_number.is_some() {
|
|
return Err(CrlDecodeError::DuplicateExtension(oid));
|
|
}
|
|
if ext.critical {
|
|
return Err(CrlDecodeError::CrlNumberCritical);
|
|
}
|
|
let n = parse_crl_number(ext)?;
|
|
if n.bits() > 159 {
|
|
return Err(CrlDecodeError::CrlNumberOutOfRange);
|
|
}
|
|
crl_number = Some(BigUnsigned::from_biguint(&n));
|
|
}
|
|
_ => return Err(CrlDecodeError::UnsupportedExtension(oid)),
|
|
}
|
|
}
|
|
|
|
Ok(CrlExtensions {
|
|
authority_key_identifier: authority_key_identifier.unwrap(),
|
|
crl_number: crl_number.unwrap(),
|
|
})
|
|
}
|
|
|
|
fn parse_aki(ext: &X509Extension<'_>) -> Result<Vec<u8>, CrlDecodeError> {
|
|
let ParsedExtension::AuthorityKeyIdentifier(aki) = ext.parsed_extension() else {
|
|
return Err(CrlDecodeError::Parse("AKI extension parse failed".into()));
|
|
};
|
|
validate_aki_profile(aki)?;
|
|
Ok(aki
|
|
.key_identifier
|
|
.as_ref()
|
|
.ok_or(CrlDecodeError::AkiMissingKeyIdentifier)?
|
|
.0
|
|
.to_vec())
|
|
}
|
|
|
|
fn validate_aki_profile(aki: &AuthorityKeyIdentifier<'_>) -> Result<(), CrlDecodeError> {
|
|
if aki.key_identifier.is_none() {
|
|
return Err(CrlDecodeError::AkiMissingKeyIdentifier);
|
|
}
|
|
if aki.authority_cert_issuer.is_some() || aki.authority_cert_serial.is_some() {
|
|
return Err(CrlDecodeError::AkiHasOtherFields);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_crl_number(ext: &X509Extension<'_>) -> Result<der_parser::num_bigint::BigUint, CrlDecodeError> {
|
|
match ext.parsed_extension() {
|
|
ParsedExtension::CRLNumber(n) => Ok(n.clone()),
|
|
ParsedExtension::ParseError { error } => Err(CrlDecodeError::Parse(error.to_string())),
|
|
_ => Err(CrlDecodeError::Parse("CRLNumber extension parse failed".into())),
|
|
}
|
|
}
|
|
|
|
fn get_subject_key_identifier(cert: &X509Certificate<'_>) -> Option<Vec<u8>> {
|
|
cert.extensions()
|
|
.iter()
|
|
.find(|ext| ext.oid.to_id_string() == OID_SUBJECT_KEY_IDENTIFIER)
|
|
.and_then(|ext| match ext.parsed_extension() {
|
|
ParsedExtension::SubjectKeyIdentifier(ki) => Some(ki.0.to_vec()),
|
|
_ => None,
|
|
})
|
|
}
|