use base64::prelude::*;
use x509_parser::nom;
use x509_parser::pem::Pem;
pub const ALLOWED_REVOCATION_REASONS: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10];
#[must_use]
pub fn is_valid_revocation_reason(reason: u32) -> bool {
ALLOWED_REVOCATION_REASONS.contains(&reason)
}
#[must_use]
pub fn issuer_id(spki_der: &[u8]) -> String {
hex::encode(ring::digest::digest(&ring::digest::SHA256, spki_der).as_ref())
}
pub fn subject_key_identifier(der: &[u8]) -> Option<Vec<u8>> {
let (_, cert) = x509_parser::parse_x509_certificate(der).ok()?;
cert.iter_extensions()
.find_map(|extension| match extension.parsed_extension() {
x509_parser::extensions::ParsedExtension::SubjectKeyIdentifier(id) => {
Some(id.0.to_vec())
}
_ => None,
})
}
pub fn cert_serial_and_spki(
der: &[u8],
) -> Result<(String, Vec<u8>), nom::Err<x509_parser::error::X509Error>> {
let (_, cert) = x509_parser::parse_x509_certificate(der)?;
Ok((
hex::encode(cert.tbs_certificate.raw_serial()),
cert.tbs_certificate.subject_pki.raw.to_vec(),
))
}
pub fn leaf_der_from_chain(chain_pem: &str) -> anyhow::Result<Vec<u8>> {
match Pem::iter_from_buffer(chain_pem.as_bytes()).next() {
Some(result) => Ok(result?.contents),
None => anyhow::bail!("no CERTIFICATE block found in chain"),
}
}
pub fn ari_cert_id(der: &[u8]) -> anyhow::Result<String> {
let (aki, serial) = ari_cert_id_parts(der)?;
Ok(format!(
"{}.{}",
BASE64_URL_SAFE_NO_PAD.encode(aki),
BASE64_URL_SAFE_NO_PAD.encode(serial),
))
}
pub fn ari_cert_id_parts(der: &[u8]) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
use x509_parser::extensions::ParsedExtension;
use x509_parser::oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER;
let (_, cert) = x509_parser::parse_x509_certificate(der)?;
let extension = cert
.get_extension_unique(&OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER)?
.ok_or_else(|| anyhow::anyhow!("certificate has no Authority Key Identifier extension"))?;
let ParsedExtension::AuthorityKeyIdentifier(aki) = extension.parsed_extension() else {
anyhow::bail!("Authority Key Identifier extension could not be parsed");
};
let key_identifier = aki
.key_identifier
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Authority Key Identifier carries no keyIdentifier"))?;
Ok((
key_identifier.0.to_vec(),
cert.tbs_certificate.raw_serial().to_vec(),
))
}
#[derive(Debug, PartialEq, Eq)]
pub struct AriCertId {
pub aki: Vec<u8>,
pub serial: Vec<u8>,
}
impl AriCertId {
#[must_use]
pub fn serial_hex(&self) -> String {
hex::encode(&self.serial)
}
}
#[must_use]
pub fn normalize_serial(value: &str) -> String {
value
.chars()
.filter(|character| !matches!(character, ':' | '-') && !character.is_whitespace())
.flat_map(char::to_lowercase)
.collect()
}
pub fn parse_ari_cert_id(cert_id: &str) -> anyhow::Result<AriCertId> {
let (aki_b64, serial_b64) = cert_id
.split_once('.')
.ok_or_else(|| anyhow::anyhow!("certID must be two base64url parts separated by '.'"))?;
if serial_b64.contains('.') {
anyhow::bail!("certID must contain exactly one '.'");
}
if aki_b64.is_empty() || serial_b64.is_empty() {
anyhow::bail!("both halves of a certID must be non-empty");
}
Ok(AriCertId {
aki: BASE64_URL_SAFE_NO_PAD
.decode(aki_b64)
.map_err(|_| anyhow::anyhow!("invalid key identifier encoding in certID"))?,
serial: BASE64_URL_SAFE_NO_PAD
.decode(serial_b64)
.map_err(|_| anyhow::anyhow!("invalid serial number encoding in certID"))?,
})
}
pub fn cert_validity(der: &[u8]) -> Result<(i64, i64), nom::Err<x509_parser::error::X509Error>> {
let (_, cert) = x509_parser::parse_x509_certificate(der)?;
Ok((
cert.tbs_certificate.validity.not_before.timestamp(),
cert.tbs_certificate.validity.not_after.timestamp(),
))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_cert(name: &str) -> rcgen::Certificate {
let key_pair = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec![name.to_string()]).unwrap();
params.self_signed(&key_pair).unwrap()
}
fn make_cert_der(name: &str) -> Vec<u8> {
make_cert(name).der().to_vec()
}
#[test]
fn is_valid_revocation_reason_accepts_every_defined_code_but_the_reserved_one() {
for code in 0..=10u32 {
assert_eq!(is_valid_revocation_reason(code), code != 7, "code {code}");
}
assert!(!is_valid_revocation_reason(999));
}
#[test]
fn cert_serial_and_spki_round_trips_a_real_certificate() {
let der = make_cert_der("example.com");
let (serial_hex, spki) = cert_serial_and_spki(&der).unwrap();
assert!(!serial_hex.is_empty());
assert!(hex::decode(&serial_hex).is_ok());
assert!(!spki.is_empty());
let (_, parsed) = x509_parser::parse_x509_certificate(&der).unwrap();
assert_eq!(spki, parsed.tbs_certificate.subject_pki.raw);
}
#[test]
fn cert_serial_and_spki_rejects_garbage() {
assert!(cert_serial_and_spki(&[0xde, 0xad, 0xbe, 0xef]).is_err());
}
#[test]
fn leaf_der_from_chain_takes_the_first_block() {
let leaf_cert = make_cert("leaf.example.com");
let ca_cert = make_cert("ca.example.com");
let chain = format!("{}{}", leaf_cert.pem(), ca_cert.pem());
let leaf = leaf_der_from_chain(&chain).unwrap();
assert_eq!(leaf, leaf_cert.der().to_vec());
}
#[test]
fn leaf_der_from_chain_rejects_a_chain_with_no_certificate_block() {
assert!(leaf_der_from_chain("not a pem file at all").is_err());
}
fn ca_signed_leaf_with_aki() -> Vec<u8> {
let ca_key = rcgen::KeyPair::generate().unwrap();
let mut ca_params = rcgen::CertificateParams::new(vec!["ca.example".to_string()]).unwrap();
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
let ca_pem = ca_params.self_signed(&ca_key).unwrap().pem();
let issuer = rcgen::Issuer::from_ca_cert_pem(&ca_pem, ca_key).unwrap();
let leaf_key = rcgen::KeyPair::generate().unwrap();
let mut leaf_params =
rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
leaf_params.use_authority_key_identifier_extension = true;
leaf_params
.signed_by(&leaf_key, &issuer)
.unwrap()
.der()
.to_vec()
}
#[test]
fn ari_cert_id_joins_the_aki_and_the_serial() {
let leaf = ca_signed_leaf_with_aki();
let cert_id = ari_cert_id(&leaf).unwrap();
let (aki_b64, serial_b64) = cert_id.split_once('.').expect("certID is two parts");
let (serial_hex, _) = cert_serial_and_spki(&leaf).unwrap();
assert_eq!(
hex::encode(BASE64_URL_SAFE_NO_PAD.decode(serial_b64).unwrap()),
serial_hex
);
let aki = BASE64_URL_SAFE_NO_PAD.decode(aki_b64).unwrap();
assert!(!aki.is_empty());
assert!(!cert_id.contains('=') && !cert_id.contains('+') && !cert_id.contains('/'));
}
#[test]
fn ari_cert_id_refuses_a_certificate_without_an_aki() {
let der = make_cert_der("example.com");
let error = ari_cert_id(&der).unwrap_err().to_string();
assert!(error.contains("Authority Key Identifier"), "{error}");
}
#[test]
fn ari_cert_id_rejects_garbage() {
assert!(ari_cert_id(&[0xde, 0xad, 0xbe, 0xef]).is_err());
}
#[test]
fn cert_validity_extracts_correct_timestamps() {
let cert = make_cert("example.com");
let der = cert.der().to_vec();
let (not_before, not_after) = cert_validity(&der).unwrap();
assert!(not_before > 0);
assert!(not_after > not_before);
}
}
#[cfg(test)]
mod normalize_serial_tests {
use super::normalize_serial;
#[test]
fn the_shapes_an_operator_pastes_all_fold_to_the_stored_form() {
for input in [
"0a1b2c3d",
"0A1B2C3D",
"0a:1b:2c:3d",
"0A:1B:2C:3D",
"0a 1b 2c 3d",
"0a-1b-2c-3d",
" 0A1B2C3D ",
] {
assert_eq!(normalize_serial(input), "0a1b2c3d", "{input}");
}
}
#[test]
fn nothing_else_is_touched() {
assert_eq!(normalize_serial(""), "");
assert_eq!(normalize_serial("0x0a1b"), "0x0a1b");
assert_eq!(normalize_serial("not/a/serial"), "not/a/serial");
}
}