add crl data model

This commit is contained in:
2026-01-27 10:33:31 +08:00
parent 177883c50c
commit 5e474fffd2
76 changed files with 882 additions and 0 deletions

429
src/data_model/crl.rs Normal file
View File

@@ -0,0 +1,429 @@
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::asn1_rs::Tag;
use x509_parser::certificate::X509Certificate;
use x509_parser::x509::SubjectPublicKeyInfo;
use x509_parser::x509::AlgorithmIdentifier;
const OID_SHA256_WITH_RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.11";
const OID_AUTHORITY_KEY_IDENTIFIER: &str = "2.5.29.35";
const OID_CRL_NUMBER: &str = "2.5.29.20";
const OID_SUBJECT_KEY_IDENTIFIER: &str = "2.5.29.14";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Asn1TimeEncoding {
UtcTime,
GeneralizedTime,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Asn1TimeUtc {
pub utc: time::OffsetDateTime,
pub encoding: Asn1TimeEncoding,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BigUnsigned {
/// Minimal big-endian bytes. For zero, this is `[0]`.
pub bytes_be: Vec<u8>,
}
impl BigUnsigned {
pub fn to_hex_upper(&self) -> String {
hex::encode_upper(&self.bytes_be)
}
pub fn to_u64(&self) -> Option<u64> {
if self.bytes_be.len() > 8 {
return None;
}
let mut value: u64 = 0;
for &b in &self.bytes_be {
value = (value << 8) | (b as u64);
}
Some(value)
}
}
#[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}")]
Parse(String),
#[error("trailing bytes after CRL DER: {0} bytes")]
TrailingBytes(usize),
#[error("CRL version must be v2, got {0:?}")]
InvalidVersion(Option<u32>),
#[error("CRL signatureAlgorithm must be sha256WithRSAEncryption ({OID_SHA256_WITH_RSA_ENCRYPTION}), got {0}")]
InvalidSignatureAlgorithm(String),
#[error("CRL signature algorithm parameters must be absent or NULL")]
InvalidSignatureAlgorithmParameters,
#[error("CRL signatureAlgorithm must match TBSCertList.signature")]
SignatureAlgorithmMismatch,
#[error("CRL extensions must be exactly two (AKI + CRLNumber), got {0}")]
InvalidExtensionsCount(usize),
#[error("unsupported CRL extension OID {0}")]
UnsupportedExtension(String),
#[error("duplicate CRL extension OID {0}")]
DuplicateExtension(String),
#[error("AuthorityKeyIdentifier must contain keyIdentifier")]
AkiMissingKeyIdentifier,
#[error("AuthorityKeyIdentifier must not contain authorityCertIssuer or authorityCertSerialNumber")]
AkiHasOtherFields,
#[error("CRLNumber must be non-critical")]
CrlNumberCritical,
#[error("CRLNumber out of range (must fit in 0..2^159-1)")]
CrlNumberOutOfRange,
#[error("CRL entry extensions must not be present")]
EntryExtensionsNotAllowed,
#[error("CRL nextUpdate must be present (RFC 5280 §5.1.2.5)")]
NextUpdateMissing,
#[error("{field} time encoding invalid for year {year}: got {encoding:?}")]
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 = asn1_time_to_model(rc.revocation_date);
validate_time_encoding("revocationDate", &revocation_date)?;
Ok(RevokedCert {
serial_number: biguint_to_big_unsigned(rc.serial()),
revocation_date,
})
})
.collect::<Result<Vec<_>, _>>()?;
let this_update = asn1_time_to_model(crl.last_update());
validate_time_encoding("thisUpdate", &this_update)?;
let next_update = crl
.next_update()
.map(asn1_time_to_model)
.ok_or(CrlDecodeError::NextUpdateMissing)?;
validate_time_encoding("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}")]
IssuerCertificateParse(String),
#[error("trailing bytes after issuer certificate DER: {0} bytes")]
IssuerCertificateTrailingBytes(usize),
#[error("issuer SubjectPublicKeyInfo parse error: {0}")]
IssuerSpkiParse(String),
#[error("trailing bytes after issuer SubjectPublicKeyInfo DER: {0} bytes")]
IssuerSpkiTrailingBytes(usize),
#[error("CRL parse error: {0}")]
CrlParse(String),
#[error("trailing bytes after CRL DER: {0} bytes")]
CrlTrailingBytes(usize),
#[error("CRL issuer DN does not match issuer certificate subject")]
IssuerSubjectMismatch {
crl_issuer_dn: String,
issuer_subject_dn: String,
},
#[error("issuer certificate keyUsage present but missing cRLSign")]
IssuerKeyUsageMissingCrlSign,
#[error("CRL AKI.keyIdentifier does not match issuer certificate SKI")]
AkiSkiMismatch,
#[error("CRL signature verification failed: {0}")]
InvalidSignature(String),
}
fn asn1_time_to_model(t: x509_parser::time::ASN1Time) -> Asn1TimeUtc {
let encoding = if t.is_utctime() {
Asn1TimeEncoding::UtcTime
} else {
Asn1TimeEncoding::GeneralizedTime
};
Asn1TimeUtc {
utc: t.to_datetime(),
encoding,
}
}
fn validate_time_encoding(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> {
match sig.parameters.as_ref() {
None => Ok(()),
Some(p) if p.tag() == Tag::Null => Ok(()),
Some(_p) => 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(biguint_to_big_unsigned(&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 biguint_to_big_unsigned(n: &der_parser::num_bigint::BigUint) -> BigUnsigned {
let mut bytes = n.to_bytes_be();
if bytes.is_empty() {
bytes.push(0);
}
BigUnsigned { bytes_be: bytes }
}
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,
})
}

2
src/data_model/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod crl;

1
src/lib.rs Normal file
View File

@@ -0,0 +1 @@
pub mod data_model;