use alloc::string::String;
use alloc::vec::Vec;
use super::{AnyPublicKey, CertSigner, DistinguishedName, Error, Time, oid};
use crate::der::{
Reader, encode_bit_string, encode_integer, encode_octet_string, encode_sequence, encode_tlv,
oid_tlv, parse_oid, pem_decode, pem_encode, tag,
};
const PEM_LABEL: &str = "X509 CRL";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum CrlReason {
Unspecified = 0,
KeyCompromise = 1,
CACompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
RemoveFromCRL = 8,
PrivilegeWithdrawn = 9,
AaCompromise = 10,
}
impl CrlReason {
pub fn from_u8(v: u8) -> Result<Self, Error> {
match v {
0 => Ok(CrlReason::Unspecified),
1 => Ok(CrlReason::KeyCompromise),
2 => Ok(CrlReason::CACompromise),
3 => Ok(CrlReason::AffiliationChanged),
4 => Ok(CrlReason::Superseded),
5 => Ok(CrlReason::CessationOfOperation),
6 => Ok(CrlReason::CertificateHold),
8 => Ok(CrlReason::RemoveFromCRL),
9 => Ok(CrlReason::PrivilegeWithdrawn),
10 => Ok(CrlReason::AaCompromise),
_ => Err(Error::Malformed),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RevokedCertificate {
pub serial: Vec<u8>,
pub revocation_date: Time,
pub reason: Option<CrlReason>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CertificateRevocationList {
der: Vec<u8>,
}
pub struct CrlBuilder {
issuer_der: Vec<u8>,
this_update: Time,
next_update: Option<Time>,
entries: Vec<RevokedCertificate>,
}
impl CrlBuilder {
pub fn new(issuer: &DistinguishedName, this_update: Time, next_update: Option<Time>) -> Self {
CrlBuilder {
issuer_der: issuer.to_der(),
this_update,
next_update,
entries: Vec::new(),
}
}
pub fn revoke(
&mut self,
serial_be: &[u8],
revocation_date: Time,
reason: Option<CrlReason>,
) -> &mut Self {
self.entries.push(RevokedCertificate {
serial: serial_be.to_vec(),
revocation_date,
reason,
});
self
}
pub fn sign(self, signer: &CertSigner<'_>) -> Result<CertificateRevocationList, Error> {
let algid = signer.algorithm_identifier();
let tbs = encode_tbs_cert_list(
&self.issuer_der,
&self.this_update,
self.next_update.as_ref(),
&self.entries,
&algid,
);
let sig = signer.sign(&tbs)?;
let der = encode_sequence(&[tbs, algid, encode_bit_string(&sig)].concat());
Ok(CertificateRevocationList { der })
}
}
fn crl_reason_extension(reason: CrlReason) -> Vec<u8> {
let enumerated = encode_tlv(0x0a, &[reason as u8]);
let mut ext = oid_tlv(oid::CRL_REASON_CODE);
ext.extend_from_slice(&encode_octet_string(&enumerated));
encode_sequence(&ext)
}
fn encode_revoked(entry: &RevokedCertificate) -> Vec<u8> {
let serial = encode_integer(&entry.serial);
let rdate = entry.revocation_date.to_der_choice();
let mut body = Vec::new();
body.extend_from_slice(&serial);
body.extend_from_slice(&rdate);
if let Some(reason) = entry.reason
&& reason != CrlReason::Unspecified
{
let ext = crl_reason_extension(reason);
body.extend_from_slice(&encode_sequence(&ext));
}
encode_sequence(&body)
}
fn encode_tbs_cert_list(
issuer_der: &[u8],
this_update: &Time,
next_update: Option<&Time>,
entries: &[RevokedCertificate],
algid: &[u8],
) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&encode_integer(&[1]));
body.extend_from_slice(algid);
body.extend_from_slice(issuer_der);
body.extend_from_slice(&this_update.to_der_choice());
if let Some(n) = next_update {
body.extend_from_slice(&n.to_der_choice());
}
if !entries.is_empty() {
let mut list = Vec::new();
for e in entries {
list.extend_from_slice(&encode_revoked(e));
}
body.extend_from_slice(&encode_sequence(&list));
}
encode_sequence(&body)
}
struct CrlParts<'a> {
tbs: &'a [u8],
sig_alg: Vec<u64>,
signature: &'a [u8],
}
impl CertificateRevocationList {
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
let mut r = Reader::new(&der);
r.read_sequence()?;
r.finish()?;
Ok(CertificateRevocationList { der })
}
pub fn from_pem(pem: &str) -> Result<Self, Error> {
Ok(CertificateRevocationList {
der: pem_decode(pem, PEM_LABEL)?,
})
}
pub fn to_der(&self) -> &[u8] {
&self.der
}
pub fn to_pem(&self) -> String {
pem_encode(PEM_LABEL, &self.der)
}
fn parts(&self) -> Result<CrlParts<'_>, Error> {
let mut outer = Reader::new(&self.der);
let mut crl = outer.read_sequence()?;
let tbs = crl.read_element()?;
let mut alg = crl.read_sequence()?;
let sig_alg = parse_oid(alg.read_oid()?)?;
let signature = crl.read_bit_string()?;
crl.finish()?;
Ok(CrlParts {
tbs,
sig_alg,
signature,
})
}
fn tbs_after_algid(&self) -> Result<Reader<'_>, Error> {
let tbs = self.parts()?.tbs;
let mut outer = Reader::new(tbs);
let mut seq = outer.read_sequence()?;
if seq.peek_tag() == Some(tag::INTEGER) {
let v = seq.read_integer_bytes()?;
if v.len() != 1 || (v[0] != 0 && v[0] != 1) {
return Err(Error::Malformed);
}
}
seq.read_sequence()?; Ok(seq)
}
pub(crate) fn inner_signature_algid_der(&self) -> Result<&[u8], Error> {
let tbs = self.parts()?.tbs;
let mut outer = Reader::new(tbs);
let mut seq = outer.read_sequence()?;
if seq.peek_tag() == Some(tag::INTEGER) {
let v = seq.read_integer_bytes()?;
if v.len() != 1 || (v[0] != 0 && v[0] != 1) {
return Err(Error::Malformed);
}
}
Ok(seq.read_element()?)
}
pub(crate) fn outer_signature_algid_der(&self) -> Result<&[u8], Error> {
let mut outer = Reader::new(&self.der);
let mut crl = outer.read_sequence()?;
crl.read_element()?; Ok(crl.read_element()?)
}
pub fn check_signature_algid_consistent(&self) -> Result<(), Error> {
let inner = self.inner_signature_algid_der()?;
let outer = self.outer_signature_algid_der()?;
if inner == outer {
Ok(())
} else {
Err(Error::Malformed)
}
}
pub fn issuer(&self) -> Result<DistinguishedName, Error> {
let mut seq = self.tbs_after_algid()?;
DistinguishedName::decode(&mut seq)
}
pub fn issuer_der(&self) -> Result<&[u8], Error> {
let mut seq = self.tbs_after_algid()?;
Ok(seq.read_element()?)
}
pub fn this_update(&self) -> Result<Time, Error> {
let mut seq = self.tbs_after_algid()?;
seq.read_element()?; read_time(&mut seq)
}
pub fn next_update(&self) -> Result<Option<Time>, Error> {
let mut seq = self.tbs_after_algid()?;
seq.read_element()?; read_time(&mut seq)?; if seq.is_empty() {
return Ok(None);
}
match seq.peek_tag() {
Some(t) if t == tag::UTC_TIME || t == tag::GENERALIZED_TIME => {
read_time(&mut seq).map(Some)
}
_ => Ok(None),
}
}
pub fn signature_algorithm_oid(&self) -> Result<Vec<u64>, Error> {
Ok(self.parts()?.sig_alg)
}
pub fn verify_signature_with(&self, issuer_key: &AnyPublicKey) -> Result<(), Error> {
let parts = self.parts()?;
issuer_key.verify(&parts.sig_alg, parts.tbs, parts.signature)
}
pub fn entries(&self) -> Result<Vec<RevokedCertificate>, Error> {
let mut out = Vec::new();
let mut seq = self.tbs_after_algid()?;
seq.read_element()?; read_time(&mut seq)?;
if let Some(t) = seq.peek_tag()
&& (t == tag::UTC_TIME || t == tag::GENERALIZED_TIME)
{
read_time(&mut seq)?;
}
if seq.peek_tag() == Some(tag::SEQUENCE) {
let inner = seq.read_element()?;
let mut r = Reader::new(inner);
let mut list = r.read_sequence()?;
while !list.is_empty() {
let mut entry = list.read_sequence()?;
let serial = entry.read_unsigned_integer_bytes()?.to_vec();
let revocation_date = read_time(&mut entry)?;
let mut reason = None;
if !entry.is_empty() {
let mut exts = entry.read_sequence()?;
while !exts.is_empty() {
let mut ext = exts.read_sequence()?;
let id = parse_oid(ext.read_oid()?)?;
let _critical = if ext.peek_tag() == Some(tag::BOOLEAN) {
ext.read_boolean()?
} else {
false
};
let value = ext.read_octet_string()?;
if id.as_slice() == oid::CRL_REASON_CODE {
let mut vr = Reader::new(value);
let enum_body = vr.read_tlv(0x0a)?;
if enum_body.len() != 1 {
return Err(Error::Malformed);
}
reason = Some(CrlReason::from_u8(enum_body[0])?);
}
}
}
out.push(RevokedCertificate {
serial,
revocation_date,
reason,
});
}
}
Ok(out)
}
pub fn is_revoked(&self, serial_be: &[u8]) -> Result<bool, Error> {
let needle = strip_leading_sign_zero(serial_be);
for e in self.entries()? {
if strip_leading_sign_zero(&e.serial) == needle {
return Ok(true);
}
}
Ok(false)
}
}
fn strip_leading_sign_zero(bytes: &[u8]) -> &[u8] {
if bytes.len() > 1 && bytes[0] == 0x00 {
&bytes[1..]
} else {
bytes
}
}
fn read_time(reader: &mut Reader) -> Result<Time, Error> {
let (t, value) = reader.read_any()?;
if t != tag::UTC_TIME && t != tag::GENERALIZED_TIME {
return Err(Error::Malformed);
}
let s = core::str::from_utf8(value).map_err(|_| Error::Malformed)?;
Ok(Time::from_repr(s))
}
#[cfg(test)]
mod tests {
use super::super::algorithm_identifier;
use super::*;
use crate::rsa::BoxedRsaPrivateKey;
use crate::x509::{CertSigner, Validity};
fn rsa_a() -> BoxedRsaPrivateKey {
BoxedRsaPrivateKey::from_pkcs1_pem(include_str!("../../testdata/rsa2048_test_a.pem"))
.expect("rsa key A")
}
fn rsa_b() -> BoxedRsaPrivateKey {
BoxedRsaPrivateKey::from_pkcs1_pem(include_str!("../../testdata/rsa2048_test_b.pem"))
.expect("rsa key B")
}
fn validity() -> Validity {
Validity::new(
Time::utc(2024, 1, 1, 0, 0, 0),
Time::utc(2034, 1, 1, 0, 0, 0),
)
}
fn issuer_dn() -> DistinguishedName {
DistinguishedName::common_name("purecrypto CRL test")
}
#[test]
fn roundtrip_two_entries_and_verify() {
let key = rsa_a();
let signer = CertSigner::Rsa(&key);
let dn = issuer_dn();
let mut b = CrlBuilder::new(
&dn,
Time::utc(2026, 1, 1, 0, 0, 0),
Some(Time::utc(2026, 12, 31, 0, 0, 0)),
);
b.revoke(
&[0x01],
Time::utc(2026, 2, 1, 0, 0, 0),
Some(CrlReason::KeyCompromise),
);
b.revoke(
&[0x02, 0x03],
Time::utc(2026, 2, 2, 0, 0, 0),
Some(CrlReason::Superseded),
);
let crl = b.sign(&signer).unwrap();
let from_der = CertificateRevocationList::from_der(crl.to_der().to_vec()).unwrap();
assert_eq!(from_der, crl);
let pem = crl.to_pem();
assert!(pem.contains("BEGIN X509 CRL"));
let from_pem = CertificateRevocationList::from_pem(&pem).unwrap();
assert_eq!(from_pem, crl);
assert_eq!(crl.issuer().unwrap(), dn);
assert_eq!(crl.this_update().unwrap(), Time::utc(2026, 1, 1, 0, 0, 0));
assert_eq!(
crl.next_update().unwrap().unwrap(),
Time::utc(2026, 12, 31, 0, 0, 0)
);
crl.check_signature_algid_consistent().unwrap();
let entries = crl.entries().unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].serial, alloc::vec![0x01]);
assert_eq!(entries[0].reason, Some(CrlReason::KeyCompromise));
assert_eq!(entries[1].serial, alloc::vec![0x02, 0x03]);
assert_eq!(entries[1].reason, Some(CrlReason::Superseded));
let issuer_pub = signer.public_key();
crl.verify_signature_with(&issuer_pub).unwrap();
assert!(crl.is_revoked(&[0x01]).unwrap());
assert!(crl.is_revoked(&[0x02, 0x03]).unwrap());
assert!(crl.is_revoked(&[0x00, 0x01]).unwrap());
assert!(!crl.is_revoked(&[0x07]).unwrap());
}
#[test]
fn verify_signature_rejects_wrong_key() {
let key_a = rsa_a();
let key_b = rsa_b();
let signer = CertSigner::Rsa(&key_a);
let dn = issuer_dn();
let b = CrlBuilder::new(&dn, Time::utc(2026, 1, 1, 0, 0, 0), None);
let crl = b.sign(&signer).unwrap();
crl.verify_signature_with(&CertSigner::Rsa(&key_a).public_key())
.unwrap();
assert!(
crl.verify_signature_with(&CertSigner::Rsa(&key_b).public_key())
.is_err()
);
}
#[test]
fn verify_signature_rejects_tampered_byte() {
let key = rsa_a();
let signer = CertSigner::Rsa(&key);
let dn = issuer_dn();
let mut b = CrlBuilder::new(&dn, Time::utc(2026, 1, 1, 0, 0, 0), None);
b.revoke(&[0x05], Time::utc(2026, 1, 2, 0, 0, 0), None);
let crl = b.sign(&signer).unwrap();
let mut der = crl.to_der().to_vec();
let idx = der.len() / 3;
der[idx] ^= 1;
let bad = CertificateRevocationList::from_der(der).unwrap();
assert!(bad.verify_signature_with(&signer.public_key()).is_err());
crl.verify_signature_with(&signer.public_key()).unwrap();
let _ = validity(); }
#[test]
fn rejects_inner_outer_algid_mismatch() {
let key = rsa_a();
let signer = CertSigner::Rsa(&key);
let dn = issuer_dn();
let inner_algid = algorithm_identifier(oid::SHA256_WITH_RSA, true);
let tbs = encode_tbs_cert_list(
&dn.to_der(),
&Time::utc(2026, 1, 1, 0, 0, 0),
None,
&[],
&inner_algid,
);
let sig = signer.sign(&tbs).unwrap();
let outer_algid = algorithm_identifier(oid::SHA1_WITH_RSA, true);
let der = encode_sequence(&[tbs, outer_algid, encode_bit_string(&sig)].concat());
let mismatched = CertificateRevocationList::from_der(der).unwrap();
assert!(matches!(
mismatched.check_signature_algid_consistent(),
Err(Error::Malformed)
));
}
#[test]
fn rejects_unknown_version() {
let key = rsa_a();
let signer = CertSigner::Rsa(&key);
let dn = issuer_dn();
let algid = algorithm_identifier(oid::SHA256_WITH_RSA, true);
let mut body = Vec::new();
body.extend_from_slice(&encode_integer(&[99])); body.extend_from_slice(&algid);
body.extend_from_slice(&dn.to_der());
body.extend_from_slice(&Time::utc(2026, 1, 1, 0, 0, 0).to_der_choice());
let tbs = encode_sequence(&body);
let sig = signer.sign(&tbs).unwrap();
let der = encode_sequence(&[tbs, algid, encode_bit_string(&sig)].concat());
let crl = CertificateRevocationList::from_der(der).unwrap();
assert!(matches!(crl.issuer(), Err(Error::Malformed)));
assert!(matches!(crl.entries(), Err(Error::Malformed)));
}
#[test]
fn rejects_unknown_revocation_reason() {
let key = rsa_a();
let signer = CertSigner::Rsa(&key);
let dn = issuer_dn();
let algid = algorithm_identifier(oid::SHA256_WITH_RSA, true);
let serial = encode_integer(&[0x07]);
let rdate = Time::utc(2026, 1, 2, 0, 0, 0).to_der_choice();
let enumerated = encode_tlv(0x0a, &[0xff]);
let mut ext_body = oid_tlv(oid::CRL_REASON_CODE);
ext_body.extend_from_slice(&encode_octet_string(&enumerated));
let ext = encode_sequence(&ext_body);
let exts = encode_sequence(&ext);
let mut entry = Vec::new();
entry.extend_from_slice(&serial);
entry.extend_from_slice(&rdate);
entry.extend_from_slice(&exts);
let revoked = encode_sequence(&entry);
let revoked_seq = encode_sequence(&revoked);
let mut tbs_body = Vec::new();
tbs_body.extend_from_slice(&encode_integer(&[1]));
tbs_body.extend_from_slice(&algid);
tbs_body.extend_from_slice(&dn.to_der());
tbs_body.extend_from_slice(&Time::utc(2026, 1, 1, 0, 0, 0).to_der_choice());
tbs_body.extend_from_slice(&revoked_seq);
let tbs = encode_sequence(&tbs_body);
let sig = signer.sign(&tbs).unwrap();
let der = encode_sequence(&[tbs, algid, encode_bit_string(&sig)].concat());
let crl = CertificateRevocationList::from_der(der).unwrap();
assert!(matches!(crl.entries(), Err(Error::Malformed)));
}
#[test]
fn empty_crl_has_no_entries() {
let key = rsa_a();
let signer = CertSigner::Rsa(&key);
let dn = issuer_dn();
let b = CrlBuilder::new(&dn, Time::utc(2026, 1, 1, 0, 0, 0), None);
let crl = b.sign(&signer).unwrap();
assert!(crl.entries().unwrap().is_empty());
assert!(!crl.is_revoked(&[0x42]).unwrap());
assert!(crl.next_update().unwrap().is_none());
}
}