1use base64::prelude::*;
9use x509_parser::nom;
10use x509_parser::pem::Pem;
11
12pub const ALLOWED_REVOCATION_REASONS: [u32; 10] = [0, 1, 2, 3, 4, 5, 6, 8, 9, 10];
17
18#[must_use]
20pub fn is_valid_revocation_reason(reason: u32) -> bool {
21 ALLOWED_REVOCATION_REASONS.contains(&reason)
22}
23
24pub fn cert_serial_and_spki(
32 der: &[u8],
33) -> Result<(String, Vec<u8>), nom::Err<x509_parser::error::X509Error>> {
34 let (_, cert) = x509_parser::parse_x509_certificate(der)?;
35 Ok((
36 hex::encode(cert.tbs_certificate.raw_serial()),
37 cert.tbs_certificate.subject_pki.raw.to_vec(),
38 ))
39}
40
41pub fn leaf_der_from_chain(chain_pem: &str) -> anyhow::Result<Vec<u8>> {
46 match Pem::iter_from_buffer(chain_pem.as_bytes()).next() {
47 Some(result) => Ok(result?.contents),
48 None => anyhow::bail!("no CERTIFICATE block found in chain"),
49 }
50}
51
52pub fn ari_cert_id(der: &[u8]) -> anyhow::Result<String> {
66 let (aki, serial) = ari_cert_id_parts(der)?;
67 Ok(format!(
68 "{}.{}",
69 BASE64_URL_SAFE_NO_PAD.encode(aki),
70 BASE64_URL_SAFE_NO_PAD.encode(serial),
71 ))
72}
73
74pub fn ari_cert_id_parts(der: &[u8]) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
77 use x509_parser::extensions::ParsedExtension;
78 use x509_parser::oid_registry::OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER;
79
80 let (_, cert) = x509_parser::parse_x509_certificate(der)?;
81 let extension = cert
82 .get_extension_unique(&OID_X509_EXT_AUTHORITY_KEY_IDENTIFIER)?
83 .ok_or_else(|| anyhow::anyhow!("certificate has no Authority Key Identifier extension"))?;
84
85 let ParsedExtension::AuthorityKeyIdentifier(aki) = extension.parsed_extension() else {
86 anyhow::bail!("Authority Key Identifier extension could not be parsed");
87 };
88 let key_identifier = aki
89 .key_identifier
90 .as_ref()
91 .ok_or_else(|| anyhow::anyhow!("Authority Key Identifier carries no keyIdentifier"))?;
92
93 Ok((
94 key_identifier.0.to_vec(),
95 cert.tbs_certificate.raw_serial().to_vec(),
96 ))
97}
98
99#[derive(Debug, PartialEq, Eq)]
107pub struct AriCertId {
108 pub aki: Vec<u8>,
110 pub serial: Vec<u8>,
112}
113
114impl AriCertId {
115 #[must_use]
118 pub fn serial_hex(&self) -> String {
119 hex::encode(&self.serial)
120 }
121}
122
123pub fn parse_ari_cert_id(cert_id: &str) -> anyhow::Result<AriCertId> {
132 let (aki_b64, serial_b64) = cert_id
133 .split_once('.')
134 .ok_or_else(|| anyhow::anyhow!("certID must be two base64url parts separated by '.'"))?;
135
136 if serial_b64.contains('.') {
137 anyhow::bail!("certID must contain exactly one '.'");
138 }
139 if aki_b64.is_empty() || serial_b64.is_empty() {
140 anyhow::bail!("both halves of a certID must be non-empty");
141 }
142
143 Ok(AriCertId {
144 aki: BASE64_URL_SAFE_NO_PAD
145 .decode(aki_b64)
146 .map_err(|_| anyhow::anyhow!("invalid key identifier encoding in certID"))?,
147 serial: BASE64_URL_SAFE_NO_PAD
148 .decode(serial_b64)
149 .map_err(|_| anyhow::anyhow!("invalid serial number encoding in certID"))?,
150 })
151}
152
153pub fn cert_validity(der: &[u8]) -> Result<(i64, i64), nom::Err<x509_parser::error::X509Error>> {
156 let (_, cert) = x509_parser::parse_x509_certificate(der)?;
157 Ok((
158 cert.tbs_certificate.validity.not_before.timestamp(),
159 cert.tbs_certificate.validity.not_after.timestamp(),
160 ))
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 fn make_cert(name: &str) -> rcgen::Certificate {
168 let key_pair = rcgen::KeyPair::generate().unwrap();
169 let params = rcgen::CertificateParams::new(vec![name.to_string()]).unwrap();
170 params.self_signed(&key_pair).unwrap()
171 }
172
173 fn make_cert_der(name: &str) -> Vec<u8> {
174 make_cert(name).der().to_vec()
175 }
176
177 #[test]
178 fn is_valid_revocation_reason_accepts_every_defined_code_but_the_reserved_one() {
179 for code in 0..=10u32 {
180 assert_eq!(is_valid_revocation_reason(code), code != 7, "code {code}");
181 }
182 assert!(!is_valid_revocation_reason(999));
183 }
184
185 #[test]
186 fn cert_serial_and_spki_round_trips_a_real_certificate() {
187 let der = make_cert_der("example.com");
188 let (serial_hex, spki) = cert_serial_and_spki(&der).unwrap();
189 assert!(!serial_hex.is_empty());
190 assert!(hex::decode(&serial_hex).is_ok());
191 assert!(!spki.is_empty());
192
193 let (_, parsed) = x509_parser::parse_x509_certificate(&der).unwrap();
196 assert_eq!(spki, parsed.tbs_certificate.subject_pki.raw);
197 }
198
199 #[test]
200 fn cert_serial_and_spki_rejects_garbage() {
201 assert!(cert_serial_and_spki(&[0xde, 0xad, 0xbe, 0xef]).is_err());
202 }
203
204 #[test]
205 fn leaf_der_from_chain_takes_the_first_block() {
206 let leaf_cert = make_cert("leaf.example.com");
207 let ca_cert = make_cert("ca.example.com");
208 let chain = format!("{}{}", leaf_cert.pem(), ca_cert.pem());
209
210 let leaf = leaf_der_from_chain(&chain).unwrap();
211 assert_eq!(leaf, leaf_cert.der().to_vec());
212 }
213
214 #[test]
215 fn leaf_der_from_chain_rejects_a_chain_with_no_certificate_block() {
216 assert!(leaf_der_from_chain("not a pem file at all").is_err());
217 }
218
219 fn ca_signed_leaf_with_aki() -> Vec<u8> {
228 let ca_key = rcgen::KeyPair::generate().unwrap();
229 let mut ca_params = rcgen::CertificateParams::new(vec!["ca.example".to_string()]).unwrap();
230 ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Constrained(0));
231 let ca_pem = ca_params.self_signed(&ca_key).unwrap().pem();
232 let issuer = rcgen::Issuer::from_ca_cert_pem(&ca_pem, ca_key).unwrap();
233
234 let leaf_key = rcgen::KeyPair::generate().unwrap();
235 let mut leaf_params =
236 rcgen::CertificateParams::new(vec!["example.com".to_string()]).unwrap();
237 leaf_params.use_authority_key_identifier_extension = true;
238 leaf_params
239 .signed_by(&leaf_key, &issuer)
240 .unwrap()
241 .der()
242 .to_vec()
243 }
244
245 #[test]
246 fn ari_cert_id_joins_the_aki_and_the_serial() {
247 let leaf = ca_signed_leaf_with_aki();
248
249 let cert_id = ari_cert_id(&leaf).unwrap();
250 let (aki_b64, serial_b64) = cert_id.split_once('.').expect("certID is two parts");
251
252 let (serial_hex, _) = cert_serial_and_spki(&leaf).unwrap();
255 assert_eq!(
256 hex::encode(BASE64_URL_SAFE_NO_PAD.decode(serial_b64).unwrap()),
257 serial_hex
258 );
259
260 let aki = BASE64_URL_SAFE_NO_PAD.decode(aki_b64).unwrap();
262 assert!(!aki.is_empty());
263 assert!(!cert_id.contains('=') && !cert_id.contains('+') && !cert_id.contains('/'));
265 }
266
267 #[test]
272 fn ari_cert_id_refuses_a_certificate_without_an_aki() {
273 let der = make_cert_der("example.com");
274 let error = ari_cert_id(&der).unwrap_err().to_string();
275 assert!(error.contains("Authority Key Identifier"), "{error}");
276 }
277
278 #[test]
279 fn ari_cert_id_rejects_garbage() {
280 assert!(ari_cert_id(&[0xde, 0xad, 0xbe, 0xef]).is_err());
281 }
282
283 #[test]
284 fn cert_validity_extracts_correct_timestamps() {
285 let cert = make_cert("example.com");
286 let der = cert.der().to_vec();
287
288 let (not_before, not_after) = cert_validity(&der).unwrap();
289
290 assert!(not_before > 0);
294 assert!(not_after > not_before);
295 }
296}