1use sha2::Digest;
15
16pub struct TrustAnchor {
18 cert: CertInfo,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum ChainStatus {
24 Trusted(Vec<String>),
27 Untrusted(String),
30 Unsupported(String),
33}
34
35impl ChainStatus {
36 pub fn as_str(&self) -> &'static str {
37 match self {
38 ChainStatus::Trusted(_) => "trusted",
39 ChainStatus::Untrusted(_) => "untrusted",
40 ChainStatus::Unsupported(_) => "unsupported",
41 }
42 }
43}
44
45pub fn parse_trust_anchors(data: &[u8]) -> Vec<TrustAnchor> {
48 let mut out = Vec::new();
49 let text = String::from_utf8_lossy(data);
50 let mut found_pem = false;
51 let mut collecting = false;
52 let mut b64 = String::new();
53 for line in text.lines() {
54 let line = line.trim();
55 if line.starts_with("-----BEGIN CERTIFICATE") {
56 collecting = true;
57 found_pem = true;
58 b64.clear();
59 } else if line.starts_with("-----END CERTIFICATE") {
60 collecting = false;
61 if let Some(der) = base64_decode(&b64) {
62 if let Some(cert) = CertInfo::parse(&der) {
63 out.push(TrustAnchor { cert });
64 }
65 }
66 } else if collecting {
67 b64.push_str(line);
68 }
69 }
70 if !found_pem {
71 if let Some(cert) = CertInfo::parse(data) {
72 out.push(TrustAnchor { cert });
73 }
74 }
75 out
76}
77
78pub fn verify_certificate_chain(
84 cms_blob: &[u8],
85 anchors: &[TrustAnchor],
86 at_seconds_since_epoch: Option<u64>,
87) -> ChainStatus {
88 if anchors.is_empty() {
89 return ChainStatus::Unsupported("no trust anchors provided".into());
90 }
91 let certs = match extract_cms_certificates(cms_blob) {
92 Some(c) if !c.is_empty() => c,
93 _ => return ChainStatus::Unsupported("no certificates in signature".into()),
94 };
95 let parsed: Vec<CertInfo> = certs.iter().filter_map(|d| CertInfo::parse(d)).collect();
96 if parsed.is_empty() {
97 return ChainStatus::Unsupported("certificates could not be parsed".into());
98 }
99
100 let mut chain: Vec<&CertInfo> = vec![&parsed[0]];
103 let mut names = vec![parsed[0].subject_cn.clone().unwrap_or_default()];
104
105 const MAX_CHAIN: usize = 16;
106 loop {
107 if chain.len() > MAX_CHAIN {
108 return ChainStatus::Untrusted("chain too long".into());
109 }
110 let current = *chain.last().expect("nonempty");
111
112 if let Some(now) = at_seconds_since_epoch {
114 if let Some((nb, na)) = current.validity {
115 if now < nb {
116 return ChainStatus::Untrusted(format!(
117 "certificate '{}' not yet valid",
118 current.subject_cn.as_deref().unwrap_or("?")
119 ));
120 }
121 if now > na {
122 return ChainStatus::Untrusted(format!(
123 "certificate '{}' expired",
124 current.subject_cn.as_deref().unwrap_or("?")
125 ));
126 }
127 }
128 }
129
130 for anchor in anchors {
133 if anchor.cert.subject_raw == current.subject_raw
134 && anchor.cert.spki_raw == current.spki_raw
135 {
136 return ChainStatus::Trusted(names);
137 }
138 if anchor.cert.subject_raw == current.issuer_raw {
139 match verify_cert_signature(current, &anchor.cert) {
140 Some(true) => {
141 names.push(anchor.cert.subject_cn.clone().unwrap_or_default());
142 return ChainStatus::Trusted(names);
143 }
144 Some(false) => {
145 return ChainStatus::Untrusted(format!(
146 "signature of '{}' does not verify against anchor",
147 current.subject_cn.as_deref().unwrap_or("?")
148 ));
149 }
150 None => {} }
152 }
153 }
154
155 let next = parsed.iter().find(|c| {
157 c.subject_raw == current.issuer_raw
158 && !std::ptr::eq(*c, current)
159 && !chain.iter().any(|seen| std::ptr::eq(*seen, *c))
160 });
161 match next {
162 Some(issuer) => match verify_cert_signature(current, issuer) {
163 Some(true) => {
164 names.push(issuer.subject_cn.clone().unwrap_or_default());
165 chain.push(issuer);
166 }
167 Some(false) => {
168 return ChainStatus::Untrusted(format!(
169 "signature of '{}' does not verify against its issuer",
170 current.subject_cn.as_deref().unwrap_or("?")
171 ));
172 }
173 None => {
174 return ChainStatus::Unsupported(format!(
175 "unsupported signature algorithm in chain at '{}'",
176 current.subject_cn.as_deref().unwrap_or("?")
177 ));
178 }
179 },
180 None => {
181 return ChainStatus::Untrusted(format!(
182 "no path to a trust anchor from '{}'",
183 names.last().map(String::as_str).unwrap_or("?")
184 ));
185 }
186 }
187 }
188}
189
190const SEQUENCE: u8 = 0x30;
195const SET: u8 = 0x31;
196const OID: u8 = 0x06;
197const BIT_STRING: u8 = 0x03;
198const UTC_TIME: u8 = 0x17;
199const GENERALIZED_TIME: u8 = 0x18;
200const CONTEXT_0: u8 = 0xA0;
201
202const OID_CN: &[u8] = &[0x55, 0x04, 0x03];
203const OID_SIGNED_DATA: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02];
204const OID_RSA_PREFIX: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01];
205const OID_ECDSA_PREFIX: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04];
206const OID_EC_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01];
207const OID_RSA_PUBLIC_KEY: &[u8] = &[0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01];
208const OID_CURVE_P256: &[u8] = &[0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07];
209const OID_CURVE_P384: &[u8] = &[0x2b, 0x81, 0x04, 0x00, 0x22];
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212enum HashAlg {
213 Sha1,
214 Sha256,
215 Sha384,
216 Sha512,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220enum ChainKeyAlg {
221 Rsa,
222 EcP256,
223 EcP384,
224}
225
226struct CertInfo {
228 tbs_raw: Vec<u8>,
231 subject_raw: Vec<u8>,
233 issuer_raw: Vec<u8>,
234 subject_cn: Option<String>,
235 validity: Option<(u64, u64)>,
237 spki_raw: Vec<u8>,
239 key_alg: Option<ChainKeyAlg>,
240 key_bytes: Vec<u8>,
242 sig_hash: Option<HashAlg>,
244 sig_is_ecdsa: bool,
245 signature: Vec<u8>,
246}
247
248impl CertInfo {
249 fn parse(der: &[u8]) -> Option<CertInfo> {
250 let (tag, cert, _) = tlv(der)?;
252 if tag != SEQUENCE {
253 return None;
254 }
255 let parts = children_raw(cert, 4);
256 if parts.len() < 3 {
257 return None;
258 }
259 let (tbs_tag, tbs, tbs_raw) = parts[0];
260 if tbs_tag != SEQUENCE {
261 return None;
262 }
263 let (alg_tag, alg, _) = parts[1];
264 if alg_tag != SEQUENCE {
265 return None;
266 }
267 let (sig_tag, sig_bits, _) = parts[2];
268 if sig_tag != BIT_STRING {
269 return None;
270 }
271 let signature = sig_bits
272 .split_first()
273 .and_then(|(unused, rest)| (*unused == 0).then(|| rest.to_vec()))?;
274
275 let alg_oid = children(alg, 2)
277 .into_iter()
278 .find(|(t, _)| *t == OID)?
279 .1
280 .to_vec();
281 let (sig_hash, sig_is_ecdsa) = classify_sig_alg(&alg_oid);
282
283 let tbs_children = children_raw(tbs, 16);
292 let seqs: Vec<(u8, &[u8], &[u8])> = tbs_children
293 .iter()
294 .filter(|(t, _, _)| *t == SEQUENCE)
295 .copied()
296 .collect();
297 if seqs.len() < 5 {
298 return None;
299 }
300 let issuer_raw = seqs[1].1.to_vec();
301 let validity_body = seqs[2].1;
302 let subject_raw = seqs[3].1.to_vec();
303 let subject_cn = name_cn(seqs[3].1);
304 let spki = seqs[4];
305
306 let times = children(validity_body, 2);
308 let validity = match (times.first(), times.get(1)) {
309 (Some(&(t1, v1)), Some(&(t2, v2))) => match (parse_time(t1, v1), parse_time(t2, v2)) {
310 (Some(nb), Some(na)) => Some((nb, na)),
311 _ => None,
312 },
313 _ => None,
314 };
315
316 let spki_parts = children(spki.1, 2);
318 let key_alg_body = spki_parts.iter().find(|(t, _)| *t == SEQUENCE)?.1;
319 let key_bits = spki_parts.iter().find(|(t, _)| *t == BIT_STRING)?.1;
320 let key_bytes = key_bits
321 .split_first()
322 .and_then(|(unused, rest)| (*unused == 0).then(|| rest.to_vec()))?;
323 let alg_children = children(key_alg_body, 2);
324 let key_oid = alg_children.iter().find(|(t, _)| *t == OID)?.1;
325 let key_alg = if key_oid == OID_RSA_PUBLIC_KEY {
326 Some(ChainKeyAlg::Rsa)
327 } else if key_oid == OID_EC_PUBLIC_KEY {
328 match alg_children.iter().filter(|(t, _)| *t == OID).nth(1) {
330 Some((_, curve)) if *curve == OID_CURVE_P256 => Some(ChainKeyAlg::EcP256),
331 Some((_, curve)) if *curve == OID_CURVE_P384 => Some(ChainKeyAlg::EcP384),
332 _ => None,
333 }
334 } else {
335 None
336 };
337
338 Some(CertInfo {
339 tbs_raw: tbs_raw.to_vec(),
340 subject_raw,
341 issuer_raw,
342 subject_cn,
343 validity,
344 spki_raw: spki.2.to_vec(),
345 key_alg,
346 key_bytes,
347 sig_hash,
348 sig_is_ecdsa,
349 signature,
350 })
351 }
352}
353
354fn classify_sig_alg(oid: &[u8]) -> (Option<HashAlg>, bool) {
356 if oid.starts_with(OID_RSA_PREFIX) && oid.len() == 9 {
357 let hash = match oid[8] {
358 0x05 => Some(HashAlg::Sha1), 0x0b => Some(HashAlg::Sha256), 0x0c => Some(HashAlg::Sha384),
361 0x0d => Some(HashAlg::Sha512),
362 _ => None,
363 };
364 (hash, false)
365 } else if oid.starts_with(OID_ECDSA_PREFIX) {
366 let hash = match (oid.get(6), oid.get(7)) {
368 (Some(0x01), None) => Some(HashAlg::Sha1),
369 (Some(0x03), Some(0x02)) => Some(HashAlg::Sha256),
370 (Some(0x03), Some(0x03)) => Some(HashAlg::Sha384),
371 (Some(0x03), Some(0x04)) => Some(HashAlg::Sha512),
372 _ => None,
373 };
374 (hash, true)
375 } else {
376 (None, false)
377 }
378}
379
380fn verify_cert_signature(child: &CertInfo, issuer: &CertInfo) -> Option<bool> {
382 let hash = child.sig_hash?;
383 let digest: Vec<u8> = match hash {
384 HashAlg::Sha1 => sha1_digest(&child.tbs_raw),
385 HashAlg::Sha256 => sha2::Sha256::digest(&child.tbs_raw).to_vec(),
386 HashAlg::Sha384 => sha2::Sha384::digest(&child.tbs_raw).to_vec(),
387 HashAlg::Sha512 => sha2::Sha512::digest(&child.tbs_raw).to_vec(),
388 };
389 match (issuer.key_alg?, child.sig_is_ecdsa) {
390 (ChainKeyAlg::Rsa, false) => rsa_verify(hash, &issuer.key_bytes, &digest, &child.signature),
391 (ChainKeyAlg::EcP256, true) => {
392 use p256::ecdsa::signature::hazmat::PrehashVerifier;
393 use p256::ecdsa::{Signature, VerifyingKey};
394 let key = VerifyingKey::from_sec1_bytes(&issuer.key_bytes).ok()?;
395 let sig = Signature::from_der(&child.signature).ok()?;
396 Some(key.verify_prehash(&digest, &sig).is_ok())
397 }
398 (ChainKeyAlg::EcP384, true) => {
399 use p384::ecdsa::signature::hazmat::PrehashVerifier;
400 use p384::ecdsa::{Signature, VerifyingKey};
401 let key = VerifyingKey::from_sec1_bytes(&issuer.key_bytes).ok()?;
402 let sig = Signature::from_der(&child.signature).ok()?;
403 Some(key.verify_prehash(&digest, &sig).is_ok())
404 }
405 _ => None, }
407}
408
409fn rsa_verify(alg: HashAlg, key_der: &[u8], hashed: &[u8], sig: &[u8]) -> Option<bool> {
410 use rsa::pkcs1::DecodeRsaPublicKey;
411 use rsa::{Pkcs1v15Sign, RsaPublicKey};
412 let key = RsaPublicKey::from_pkcs1_der(key_der).ok()?;
413 let scheme = match alg {
414 HashAlg::Sha1 => Pkcs1v15Sign::new::<sha1::Sha1>(),
415 HashAlg::Sha256 => Pkcs1v15Sign::new::<sha2::Sha256>(),
416 HashAlg::Sha384 => Pkcs1v15Sign::new::<sha2::Sha384>(),
417 HashAlg::Sha512 => Pkcs1v15Sign::new::<sha2::Sha512>(),
418 };
419 Some(key.verify(scheme, hashed, sig).is_ok())
420}
421
422fn sha1_digest(data: &[u8]) -> Vec<u8> {
423 use sha1::{Digest as _, Sha1};
424 Sha1::digest(data).to_vec()
425}
426
427fn extract_cms_certificates(blob: &[u8]) -> Option<Vec<Vec<u8>>> {
429 let (tag, ci, _) = tlv(blob)?;
430 if tag != SEQUENCE {
431 return None;
432 }
433 let ci = children(ci, 4);
434 let ctype = ci.iter().find(|(t, _)| *t == OID)?;
435 if ctype.1 != OID_SIGNED_DATA {
436 return None;
437 }
438 let content = ci.iter().find(|(t, _)| *t == CONTEXT_0)?;
439 let (tag, signed_data, _) = tlv(content.1)?;
440 if tag != SEQUENCE {
441 return None;
442 }
443 let sd = children(signed_data, 16);
444 let certs_body = sd.iter().find(|(t, _)| *t == CONTEXT_0)?.1;
445
446 let mut out = Vec::new();
447 let mut rest = certs_body;
448 while !rest.is_empty() && out.len() < 32 {
449 let before = rest;
450 let Some((tag, _, next)) = tlv(rest) else {
451 break;
452 };
453 let consumed = before.len() - next.len();
454 if tag == SEQUENCE {
455 out.push(before[..consumed].to_vec());
456 }
457 rest = next;
458 }
459 Some(out)
460}
461
462fn name_cn(name_body: &[u8]) -> Option<String> {
464 for (tag, rdn) in children(name_body, 32) {
465 if tag != SET {
466 continue;
467 }
468 for (tag, atv) in children(rdn, 8) {
469 if tag != SEQUENCE {
470 continue;
471 }
472 let parts = children(atv, 2);
473 let is_cn = parts
474 .iter()
475 .find(|(t, _)| *t == OID)
476 .is_some_and(|(_, o)| *o == OID_CN);
477 if is_cn {
478 if let Some((_, value)) = parts.iter().rev().find(|(t, _)| *t != OID) {
479 return Some(String::from_utf8_lossy(value).into_owned());
480 }
481 }
482 }
483 }
484 None
485}
486
487fn parse_time(tag: u8, body: &[u8]) -> Option<u64> {
490 let s = std::str::from_utf8(body).ok()?;
491 let s = s.strip_suffix('Z').unwrap_or(s);
492 let (year, rest): (i64, &str) = match tag {
493 UTC_TIME => {
494 let yy: i64 = s.get(0..2)?.parse().ok()?;
495 (if yy < 50 { 2000 + yy } else { 1900 + yy }, s.get(2..)?)
497 }
498 GENERALIZED_TIME => (s.get(0..4)?.parse().ok()?, s.get(4..)?),
499 _ => return None,
500 };
501 let month: u32 = rest.get(0..2)?.parse().ok()?;
502 let day: u64 = rest.get(2..4)?.parse().ok()?;
503 let hour: u64 = rest.get(4..6)?.parse().ok()?;
504 let minute: u64 = rest.get(6..8)?.parse().ok()?;
505 let second: u64 = rest.get(8..10).and_then(|t| t.parse().ok()).unwrap_or(0);
506 if !(1..=12).contains(&month) || day == 0 || day > 31 {
507 return None;
508 }
509
510 let y = year - i64::from(month <= 2);
512 let era = if y >= 0 { y } else { y - 399 } / 400;
513 let yoe = (y - era * 400) as u64;
514 let mp = ((month + 9) % 12) as u64;
515 let doy = (153 * mp + 2) / 5 + day - 1;
516 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
517 let days = era * 146_097 + doe as i64 - 719_468;
518 if days < 0 {
519 return None;
520 }
521 Some(days as u64 * 86_400 + hour * 3_600 + minute * 60 + second)
522}
523
524fn tlv(buf: &[u8]) -> Option<(u8, &[u8], &[u8])> {
527 if buf.len() < 2 {
528 return None;
529 }
530 let tag = buf[0];
531 let first = buf[1];
532 let (len, header) = if first < 0x80 {
533 (first as usize, 2)
534 } else {
535 let n = (first & 0x7f) as usize;
536 if n == 0 || n > 4 || buf.len() < 2 + n {
537 return None;
538 }
539 let mut len = 0usize;
540 for &b in &buf[2..2 + n] {
541 len = (len << 8) | b as usize;
542 }
543 (len, 2 + n)
544 };
545 let end = header.checked_add(len)?;
546 if end > buf.len() {
547 return None;
548 }
549 Some((tag, &buf[header..end], &buf[end..]))
550}
551
552fn children(content: &[u8], max: usize) -> Vec<(u8, &[u8])> {
553 let mut out = Vec::new();
554 let mut rest = content;
555 while !rest.is_empty() && out.len() < max {
556 let Some((tag, body, next)) = tlv(rest) else {
557 break;
558 };
559 out.push((tag, body));
560 rest = next;
561 }
562 out
563}
564
565#[allow(clippy::type_complexity)]
566fn children_raw(content: &[u8], max: usize) -> Vec<(u8, &[u8], &[u8])> {
567 let mut out = Vec::new();
568 let mut rest = content;
569 while !rest.is_empty() && out.len() < max {
570 let before = rest;
571 let Some((tag, body, next)) = tlv(rest) else {
572 break;
573 };
574 let consumed = before.len() - next.len();
575 out.push((tag, body, &before[..consumed]));
576 rest = next;
577 }
578 out
579}
580
581fn base64_decode(s: &str) -> Option<Vec<u8>> {
583 fn val(c: u8) -> Option<u8> {
584 match c {
585 b'A'..=b'Z' => Some(c - b'A'),
586 b'a'..=b'z' => Some(c - b'a' + 26),
587 b'0'..=b'9' => Some(c - b'0' + 52),
588 b'+' => Some(62),
589 b'/' => Some(63),
590 _ => None,
591 }
592 }
593 let mut out = Vec::with_capacity(s.len() * 3 / 4);
594 let mut acc = 0u32;
595 let mut bits = 0u32;
596 for &c in s.as_bytes() {
597 if c.is_ascii_whitespace() || c == b'=' {
598 continue;
599 }
600 acc = (acc << 6) | u32::from(val(c)?);
601 bits += 6;
602 if bits >= 8 {
603 bits -= 8;
604 out.push((acc >> bits) as u8);
605 }
606 }
607 Some(out)
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613
614 #[test]
615 fn base64_roundtrip() {
616 assert_eq!(base64_decode("aGVsbG8=").unwrap(), b"hello");
617 assert_eq!(base64_decode("aGVs\nbG8=").unwrap(), b"hello");
618 assert!(base64_decode("!!!").is_none());
619 }
620
621 #[test]
622 fn utc_time_parses() {
623 let t = parse_time(UTC_TIME, b"260102030405Z").unwrap();
625 assert_eq!(t, 1_767_323_045);
626 let g = parse_time(GENERALIZED_TIME, b"20260102030405Z").unwrap();
628 assert_eq!(g, t);
629 }
630
631 #[test]
632 fn empty_anchor_set_is_unsupported() {
633 let status = verify_certificate_chain(b"junk", &[], None);
634 assert_eq!(status.as_str(), "unsupported");
635 }
636
637 #[test]
638 fn garbage_cms_is_unsupported() {
639 let anchors = parse_trust_anchors(b"not a cert");
640 let status = verify_certificate_chain(b"junk", &anchors, None);
642 assert_eq!(status.as_str(), "unsupported");
643 }
644}