Skip to main content

hoike_sign/
crl.rs

1use der::{Decode, Encode};
2use std::collections::BTreeMap;
3use x509_cert::crl::CertificateList;
4use x509_cert::ext::pkix::CrlReason;
5
6use crate::error::{Result, SignError};
7use crate::source::{
8    CaIdentity, CertificateStatus, Epoch, RevocationSource, StatusChange, StatusSnapshot,
9};
10
11pub struct CrlSource {
12    crl: CertificateList,
13    issuer: Option<x509_cert::Certificate>,
14}
15
16impl CrlSource {
17    pub fn from_der(data: Vec<u8>) -> Result<Self> {
18        let crl = CertificateList::from_der(&data)
19            .map_err(|e| SignError::CrlParse(format!("failed to parse CRL DER: {e}")))?;
20        Ok(CrlSource { crl, issuer: None })
21    }
22
23    /// Bind this source to an independently provisioned, authorized issuer certificate.
24    pub fn with_issuer_certificate(mut self, der: &[u8]) -> Result<Self> {
25        self.issuer = Some(
26            crate::rotation::parse_certificate(der)
27                .map_err(|e| SignError::Verify(format!("CRL issuer certificate: {e}")))?,
28        );
29        Ok(self)
30    }
31
32    fn authenticate(&self, ca: &CaIdentity) -> Result<()> {
33        use x509_cert::ext::pkix::KeyUsage;
34        let cert = self.issuer.as_ref().ok_or_else(|| {
35            SignError::Verify("CRL requires an authorized issuer certificate".into())
36        })?;
37        let tbs = &cert.tbs_certificate;
38        let crl = &self.crl;
39        let now = crate::source::unix_now()?;
40        if tbs.subject.to_der()? != ca.issuer_name_der
41            || tbs.subject_public_key_info.subject_public_key.raw_bytes() != ca.issuer_key_bytes
42            || crl.tbs_cert_list.issuer != tbs.subject
43            || tbs.subject.0.is_empty()
44        {
45            return Err(SignError::Verify(
46                "CRL issuer does not match authorized CA".into(),
47            ));
48        }
49        if now < time_to_epoch(tbs.validity.not_before)
50            || now >= time_to_epoch(tbs.validity.not_after)
51        {
52            return Err(SignError::Verify(
53                "CRL issuer certificate is outside validity".into(),
54            ));
55        }
56        if let Some(exts) = &tbs.extensions {
57            for ext in exts {
58                if ext.extn_id == KeyUsage::OID
59                    && !KeyUsage::from_der(ext.extn_value.as_bytes())?.crl_sign()
60                {
61                    return Err(SignError::Verify(
62                        "issuer key usage forbids CRL signing".into(),
63                    ));
64                }
65            }
66        }
67        if crl.signature_algorithm != crl.tbs_cert_list.signature {
68            return Err(SignError::Verify(
69                "CRL signature algorithms disagree".into(),
70            ));
71        }
72        if let Some(exts) = &crl.tbs_cert_list.crl_extensions {
73            let mut seen = std::collections::BTreeSet::new();
74            for ext in exts {
75                let oid = ext.extn_id.to_string();
76                if oid == "2.5.29.20" {
77                    let _ = x509_cert::ext::pkix::CrlNumber::from_der(ext.extn_value.as_bytes())?;
78                }
79                if oid == "2.5.29.35" {
80                    use x509_cert::ext::pkix::{AuthorityKeyIdentifier, SubjectKeyIdentifier};
81                    let aki = AuthorityKeyIdentifier::from_der(ext.extn_value.as_bytes())?;
82                    if let Some(serial) = aki.authority_cert_serial_number {
83                        if serial != tbs.serial_number {
84                            return Err(SignError::Verify("CRL authority serial mismatch".into()));
85                        }
86                    }
87                    if let Some(names) = aki.authority_cert_issuer {
88                        if !names.iter().any(|n| matches!(n, x509_cert::ext::pkix::name::GeneralName::DirectoryName(name) if name == &tbs.issuer)) {
89                            return Err(SignError::Verify("CRL authority certificate issuer mismatch".into()));
90                        }
91                    }
92                    if let Some(id) = aki.key_identifier {
93                        if let Some(ski) = tbs.extensions.as_ref().and_then(|es| {
94                            es.iter().find(|e| e.extn_id == SubjectKeyIdentifier::OID)
95                        }) {
96                            if SubjectKeyIdentifier::from_der(ski.extn_value.as_bytes())?.0 != id {
97                                return Err(SignError::Verify(
98                                    "CRL authority key identifier mismatch".into(),
99                                ));
100                            }
101                        }
102                    }
103                }
104                if !seen.insert(oid.clone())
105                    || matches!(oid.as_str(), "2.5.29.27" | "2.5.29.28")
106                    || (ext.critical && !matches!(oid.as_str(), "2.5.29.20" | "2.5.29.35"))
107                {
108                    return Err(SignError::Verify(format!(
109                        "unsupported or duplicate CRL extension {oid}"
110                    )));
111                }
112            }
113        }
114        let bytes = crl.tbs_cert_list.to_der()?;
115        let signature = crl
116            .signature
117            .as_bytes()
118            .ok_or_else(|| SignError::Verify("invalid CRL signature bit string".into()))?;
119        let spki = &tbs.subject_public_key_info;
120        match crl.signature_algorithm.oid.to_string().as_str() {
121            "1.2.840.10045.4.3.2" => {
122                if crl.signature_algorithm.parameters.is_some() {
123                    return Err(SignError::Verify(
124                        "ECDSA signature parameters must be absent".into(),
125                    ));
126                }
127                use p256::pkcs8::DecodePublicKey;
128                use signature::Verifier;
129                let key = p256::ecdsa::VerifyingKey::from_public_key_der(&spki.to_der()?)
130                    .map_err(|e| SignError::Verify(format!("CRL P-256 key: {e}")))?;
131                let sig = p256::ecdsa::Signature::from_der(signature)
132                    .map_err(|e| SignError::Verify(format!("CRL signature: {e}")))?;
133                key.verify(&bytes, &sig)
134                    .map_err(|_| SignError::Verify("CRL signature verification failed".into()))
135            }
136            oid @ ("1.2.840.113549.1.1.11" | "1.2.840.113549.1.1.12" | "1.2.840.113549.1.1.13") => {
137                use aws_lc_rs::signature as aws;
138                if spki.algorithm.oid.to_string() != "1.2.840.113549.1.1.1" {
139                    return Err(SignError::Verify(
140                        "CRL signature requires RSA public key".into(),
141                    ));
142                }
143                for parameters in [
144                    &spki.algorithm.parameters,
145                    &crl.signature_algorithm.parameters,
146                ]
147                .into_iter()
148                .flatten()
149                {
150                    if parameters.to_der()? != [5, 0] {
151                        return Err(SignError::Verify("invalid RSA algorithm parameters".into()));
152                    }
153                }
154                let algorithm = match oid {
155                    "1.2.840.113549.1.1.11" => &aws::RSA_PKCS1_2048_8192_SHA256,
156                    "1.2.840.113549.1.1.12" => &aws::RSA_PKCS1_2048_8192_SHA384,
157                    _ => &aws::RSA_PKCS1_2048_8192_SHA512,
158                };
159                let key = spki
160                    .subject_public_key
161                    .as_bytes()
162                    .ok_or_else(|| SignError::Verify("invalid RSA key bit string".into()))?;
163                aws::UnparsedPublicKey::new(algorithm, key).verify(&bytes, signature)
164                    .map_err(|_| SignError::Verify("CRL RSA signature verification failed (supported keys: 2048..8192 bits)".into()))
165            }
166
167            oid @ ("2.16.840.1.101.3.4.3.17"
168            | "2.16.840.1.101.3.4.3.18"
169            | "2.16.840.1.101.3.4.3.19") => {
170                if spki.algorithm.oid.to_string() != oid
171                    || spki.algorithm.parameters.is_some()
172                    || crl.signature_algorithm.parameters.is_some()
173                {
174                    return Err(SignError::Verify(
175                        "ML-DSA CRL algorithm/key mismatch".into(),
176                    ));
177                }
178                let key = spki
179                    .subject_public_key
180                    .as_bytes()
181                    .ok_or_else(|| SignError::Verify("invalid public key bits".into()))?;
182                match oid {
183                    "2.16.840.1.101.3.4.3.17" => {
184                        verify_ml_crl::<ml_dsa::MlDsa44>(&bytes, signature, key)
185                    }
186                    "2.16.840.1.101.3.4.3.18" => {
187                        verify_ml_crl::<ml_dsa::MlDsa65>(&bytes, signature, key)
188                    }
189                    _ => verify_ml_crl::<ml_dsa::MlDsa87>(&bytes, signature, key),
190                }
191            }
192            other => Err(SignError::Verify(format!(
193                "unsupported CRL signature algorithm: {other}; supported: ECDSA P-256/SHA-256, RSA PKCS1v1.5/SHA-256/384/512, ML-DSA"
194            ))),
195        }
196    }
197
198    pub fn from_pem(pem_text: &str) -> Result<Self> {
199        let der_data = pem_to_der(pem_text)?;
200        Self::from_der(der_data)
201    }
202}
203
204impl RevocationSource for CrlSource {
205    fn snapshot(&self, ca: &CaIdentity) -> Result<StatusSnapshot> {
206        self.authenticate(ca)?;
207        let crl = &self.crl;
208        let mut entries = BTreeMap::new();
209
210        if let Some(revoked_certs) = &crl.tbs_cert_list.revoked_certificates {
211            for rc in revoked_certs {
212                if let Some(exts) = &rc.crl_entry_extensions {
213                    for ext in exts {
214                        if ext.extn_id.to_string() == "2.5.29.29"
215                            || (ext.critical && ext.extn_id != CrlReason::OID)
216                        {
217                            return Err(SignError::Verify(
218                                "unsupported CRL entry extension".into(),
219                            ));
220                        }
221                        if ext.extn_id == CrlReason::OID {
222                            let reason = CrlReason::from_der(ext.extn_value.as_bytes())?;
223                            if reason == CrlReason::RemoveFromCRL {
224                                return Err(SignError::Verify(
225                                    "removeFromCRL requires unsupported delta semantics".into(),
226                                ));
227                            }
228                        }
229                    }
230                }
231                let serial = rc.serial_number.as_bytes().to_vec();
232
233                let reason = rc.crl_entry_extensions.as_ref().and_then(|exts| {
234                    exts.iter()
235                        .find(|ext| ext.extn_id == CrlReason::OID)
236                        .and_then(|ext| CrlReason::from_der(ext.extn_value.as_bytes()).ok())
237                });
238
239                let revocation_time = time_to_epoch(rc.revocation_date);
240
241                entries.insert(
242                    serial,
243                    CertificateStatus::Revoked {
244                        revocation_time,
245                        reason,
246                    },
247                );
248            }
249        }
250
251        let this_update = time_to_epoch(crl.tbs_cert_list.this_update);
252        let next_update = crl.tbs_cert_list.next_update.map(time_to_epoch);
253
254        let snapshot = StatusSnapshot {
255            entries,
256            this_update,
257            next_update,
258            // CRLs carry no per-certificate notAfter, so archive_cutoff pruning
259            // is a no-op for this source.
260            not_after: std::collections::BTreeMap::new(),
261        };
262        snapshot.validate_at(crate::source::unix_now()?)?;
263        Ok(snapshot)
264    }
265
266    fn changes_since(&self, ca: &CaIdentity, _since: Epoch) -> Result<Vec<StatusChange>> {
267        let snapshot = self.snapshot(ca)?;
268        Ok(snapshot
269            .entries
270            .into_iter()
271            .map(|(serial, status)| StatusChange {
272                serial,
273                status,
274                timestamp: snapshot.this_update,
275            })
276            .collect())
277    }
278
279    fn supports_streaming(&self) -> bool {
280        false
281    }
282}
283
284fn verify_ml_crl<P: ml_dsa::MlDsaParams>(data: &[u8], sig: &[u8], key: &[u8]) -> Result<()> {
285    use ml_dsa::Verifier;
286    let encoded = ml_dsa::EncodedVerifyingKey::<P>::try_from(key)
287        .map_err(|_| SignError::Verify("invalid ML-DSA key".into()))?;
288    let key = ml_dsa::VerifyingKey::<P>::decode(&encoded);
289    let sig = ml_dsa::Signature::<P>::try_from(sig)
290        .map_err(|_| SignError::Verify("invalid ML-DSA signature".into()))?;
291    key.verify(data, &sig)
292        .map_err(|_| SignError::Verify("CRL signature verification failed".into()))
293}
294
295fn time_to_epoch(time: x509_cert::time::Time) -> u64 {
296    let dt = match time {
297        x509_cert::time::Time::UtcTime(t) => t.to_date_time(),
298        x509_cert::time::Time::GeneralTime(t) => t.to_date_time(),
299    };
300    datetime_to_epoch(dt)
301}
302
303fn datetime_to_epoch(dt: der::DateTime) -> u64 {
304    crate::generate::datetime_to_epoch(dt)
305}
306
307fn pem_to_der(pem: &str) -> Result<Vec<u8>> {
308    use base64::Engine;
309    let mut collecting = false;
310    let mut found_end = false;
311    let mut b64 = String::new();
312    for line in pem.lines() {
313        if line.starts_with("-----BEGIN") {
314            collecting = true;
315            continue;
316        }
317        if line.starts_with("-----END") {
318            found_end = true;
319            break;
320        }
321        if collecting {
322            b64.push_str(line.trim());
323        }
324    }
325    if !collecting {
326        return Err(SignError::CrlParse("PEM has no BEGIN marker".into()));
327    }
328    if !found_end {
329        return Err(SignError::CrlParse(
330            "PEM is truncated: found BEGIN but no END marker".into(),
331        ));
332    }
333    base64::engine::general_purpose::STANDARD
334        .decode(&b64)
335        .map_err(|e| SignError::CrlParse(format!("PEM base64 decode: {e}")))
336}
337
338use const_oid::AssociatedOid;
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    fn build_test_crl_der() -> Vec<u8> {
345        use der::Encode;
346        use der::asn1::BitString;
347        use der::asn1::UtcTime;
348        use spki::AlgorithmIdentifierOwned;
349        use x509_cert::crl::{CertificateList, RevokedCert, TbsCertList};
350        use x509_cert::name::RdnSequence;
351        use x509_cert::time::Time;
352
353        let now_dt = der::DateTime::new(2025, 1, 15, 12, 0, 0).unwrap();
354        let next_dt = der::DateTime::new(2025, 1, 16, 12, 0, 0).unwrap();
355        let revoke_dt = der::DateTime::new(2025, 1, 10, 8, 0, 0).unwrap();
356
357        let this_update = Time::UtcTime(UtcTime::from_date_time(now_dt).unwrap());
358        let next_update = Time::UtcTime(UtcTime::from_date_time(next_dt).unwrap());
359        let revoke_time = Time::UtcTime(UtcTime::from_date_time(revoke_dt).unwrap());
360
361        let serial_42 = x509_cert::serial_number::SerialNumber::new(&[42u8]).unwrap();
362        let serial_100 = x509_cert::serial_number::SerialNumber::new(&[100u8]).unwrap();
363
364        let revoked_certs = vec![
365            RevokedCert {
366                serial_number: serial_42,
367                revocation_date: revoke_time,
368                crl_entry_extensions: None,
369            },
370            RevokedCert {
371                serial_number: serial_100,
372                revocation_date: revoke_time,
373                crl_entry_extensions: None,
374            },
375        ];
376
377        let sha256_with_ecdsa = const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
378
379        let tbs = TbsCertList {
380            version: x509_cert::Version::V2,
381            signature: AlgorithmIdentifierOwned {
382                oid: sha256_with_ecdsa,
383                parameters: None,
384            },
385            issuer: RdnSequence::default(),
386            this_update,
387            next_update: Some(next_update),
388            revoked_certificates: Some(revoked_certs),
389            crl_extensions: None,
390        };
391
392        let crl = CertificateList {
393            tbs_cert_list: tbs,
394            signature_algorithm: AlgorithmIdentifierOwned {
395                oid: sha256_with_ecdsa,
396                parameters: None,
397            },
398            signature: BitString::from_bytes(&[0u8; 64]).unwrap(),
399        };
400
401        crl.to_der().expect("CRL encoding failed")
402    }
403
404    #[test]
405    fn parse_crl_extracts_revoked_serials() {
406        let crl_der = build_test_crl_der();
407        let source = CrlSource::from_der(crl_der).unwrap();
408
409        let ca = CaIdentity {
410            label: "test".into(),
411            issuer_name_der: vec![],
412            issuer_key_bytes: vec![],
413        };
414
415        assert!(source.snapshot(&ca).is_err());
416    }
417    fn authenticated_fixture() -> (CrlSource, CaIdentity) {
418        use signature::Signer;
419        let key = crate::demo_ecdsa_p256_key();
420        let cert_der = crate::generate_seal_cert(&key).unwrap();
421        let cert = x509_cert::Certificate::from_der(&cert_der).unwrap();
422        let ca = CaIdentity {
423            label: "fixture".into(),
424            issuer_name_der: cert.tbs_certificate.subject.to_der().unwrap(),
425            issuer_key_bytes: cert
426                .tbs_certificate
427                .subject_public_key_info
428                .subject_public_key
429                .raw_bytes()
430                .to_vec(),
431        };
432        let mut crl = CertificateList::from_der(&build_test_crl_der()).unwrap();
433        crl.tbs_cert_list.issuer = cert.tbs_certificate.subject;
434        let now = crate::source::unix_now().unwrap();
435        let time = |n| {
436            x509_cert::time::Time::GeneralTime(
437                der::asn1::GeneralizedTime::from_unix_duration(std::time::Duration::from_secs(n))
438                    .unwrap(),
439            )
440        };
441        crl.tbs_cert_list.this_update = time(now - 1);
442        crl.tbs_cert_list.next_update = Some(time(now + 3600));
443        let sig: p256::ecdsa::DerSignature = key.sign(&crl.tbs_cert_list.to_der().unwrap());
444        crl.signature = der::asn1::BitString::from_bytes(sig.as_bytes()).unwrap();
445        (
446            CrlSource::from_der(crl.to_der().unwrap())
447                .unwrap()
448                .with_issuer_certificate(&cert_der)
449                .unwrap(),
450            ca,
451        )
452    }
453
454    #[test]
455    fn authenticated_crl_accepts_only_matching_issuer_and_signature() {
456        let (mut source, ca) = authenticated_fixture();
457        assert_eq!(source.snapshot(&ca).unwrap().entries.len(), 2);
458        let mut wrong = ca.clone();
459        wrong.issuer_key_bytes[0] ^= 1;
460        assert!(source.snapshot(&wrong).is_err());
461        source
462            .crl
463            .tbs_cert_list
464            .revoked_certificates
465            .as_mut()
466            .unwrap()
467            .clear();
468        assert!(source.snapshot(&ca).is_err());
469    }
470
471    #[test]
472    fn authenticated_crl_rejects_delta_semantics() {
473        let (mut source, ca) = authenticated_fixture();
474        source.crl.tbs_cert_list.crl_extensions = Some(vec![x509_cert::ext::Extension {
475            extn_id: const_oid::ObjectIdentifier::new_unwrap("2.5.29.27"),
476            critical: true,
477            extn_value: der::asn1::OctetString::new(vec![2, 1, 1]).unwrap(),
478        }]);
479        assert!(
480            source
481                .snapshot(&ca)
482                .unwrap_err()
483                .to_string()
484                .contains("unsupported")
485        );
486    }
487    #[test]
488    fn rsa_crls_verify_sha256_sha384_sha512_and_reject_tampering() {
489        use aws_lc_rs::signature::{self as aws, KeyPair};
490        let key = aws::RsaKeyPair::generate(aws_lc_rs::rsa::KeySize::Rsa2048).unwrap();
491        let (template, _) = authenticated_fixture();
492        for oid in [
493            "1.2.840.113549.1.1.11",
494            "1.2.840.113549.1.1.12",
495            "1.2.840.113549.1.1.13",
496        ] {
497            let mut cert = template.issuer.clone().unwrap();
498            cert.tbs_certificate.subject_public_key_info = spki::SubjectPublicKeyInfoOwned {
499                algorithm: spki::AlgorithmIdentifierOwned {
500                    oid: "1.2.840.113549.1.1.1".parse().unwrap(),
501                    parameters: None,
502                },
503                subject_public_key: der::asn1::BitString::from_bytes(key.public_key().as_ref())
504                    .unwrap(),
505            };
506            let ca = CaIdentity {
507                label: "rsa".into(),
508                issuer_name_der: cert.tbs_certificate.subject.to_der().unwrap(),
509                issuer_key_bytes: cert
510                    .tbs_certificate
511                    .subject_public_key_info
512                    .subject_public_key
513                    .raw_bytes()
514                    .to_vec(),
515            };
516            let mut crl = template.crl.clone();
517            crl.signature_algorithm.oid = oid.parse().unwrap();
518            crl.tbs_cert_list.signature = crl.signature_algorithm.clone();
519            let tbs = crl.tbs_cert_list.to_der().unwrap();
520            let algorithm = match oid {
521                "1.2.840.113549.1.1.11" => &aws::RSA_PKCS1_SHA256,
522                "1.2.840.113549.1.1.12" => &aws::RSA_PKCS1_SHA384,
523                _ => &aws::RSA_PKCS1_SHA512,
524            };
525            let mut signature = vec![0; key.public_modulus_len()];
526            key.sign(
527                algorithm,
528                &aws_lc_rs::rand::SystemRandom::new(),
529                &tbs,
530                &mut signature,
531            )
532            .unwrap();
533            crl.signature = der::asn1::BitString::from_bytes(&signature).unwrap();
534            let mut source = CrlSource {
535                crl,
536                issuer: Some(cert),
537            };
538            assert!(source.snapshot(&ca).is_ok(), "{oid}");
539            source.crl.tbs_cert_list.revoked_certificates = None;
540            assert!(source.snapshot(&ca).is_err());
541        }
542    }
543    #[test]
544    fn issuer_certificate_accepts_pem_and_der_identically() {
545        use der::EncodePem;
546        let (source, ca) = authenticated_fixture();
547        let pem = source
548            .issuer
549            .as_ref()
550            .unwrap()
551            .to_pem(der::pem::LineEnding::LF)
552            .unwrap();
553        let from_pem = CrlSource::from_der(source.crl.to_der().unwrap())
554            .unwrap()
555            .with_issuer_certificate(pem.as_bytes())
556            .unwrap();
557        assert_eq!(
558            source.snapshot(&ca).unwrap().entries.len(),
559            from_pem.snapshot(&ca).unwrap().entries.len()
560        );
561    }
562}