add crl data model
This commit is contained in:
429
src/data_model/crl.rs
Normal file
429
src/data_model/crl.rs
Normal 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
2
src/data_model/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod crl;
|
||||
|
||||
1
src/lib.rs
Normal file
1
src/lib.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod data_model;
|
||||
Reference in New Issue
Block a user