1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4use std::{
5 collections::HashMap,
6 io::{BufReader, Read, Seek},
7};
8
9use x509_parser::{
10 error::X509Error,
11 pem::Pem,
12 prelude::{FromDer, X509Certificate},
13};
14
15#[non_exhaustive]
17#[derive(Debug, thiserror::Error)]
18pub enum Error {
19 #[error("X509 error")]
21 X509(#[from] x509_parser::error::X509Error),
22
23 #[error("PEM error")]
25 Pem(#[from] x509_parser::error::PEMError),
26
27 #[error("Expected to find OID {oid} but it is missing")]
29 OidMissing {
30 oid: String,
32 },
33
34 #[error("Field has unexpected value")]
36 UnexpectedFieldValue {
37 field: String,
39
40 expected: String,
42
43 actual: String,
45 },
46
47 #[error("Certificate is missing")]
49 CertificateMissing,
50}
51
52impl From<x509_parser::asn1_rs::Err<x509_parser::error::X509Error>> for Error {
53 fn from(value: x509_parser::asn1_rs::Err<x509_parser::error::X509Error>) -> Self {
54 use x509_parser::nom::Err;
55 let inner = match value {
56 Err::Incomplete(_) => X509Error::Generic,
57 Err::Error(e) | Err::Failure(e) => e,
58 };
59 Self::X509(inner)
60 }
61}
62
63pub type Result<T> = std::result::Result<T, Error>;
65
66#[derive(Default, Debug)]
68pub struct Validator {
69 cert_chain: Vec<OwnedCert>,
70}
71
72#[derive(Debug)]
73struct OwnedCert {
74 contents: Vec<u8>,
75}
76
77impl OwnedCert {
78 fn new(contents: Vec<u8>) -> Self {
79 Self { contents }
80 }
81}
82
83impl Validator {
84 fn add_and_validate(&mut self, der: Vec<u8>) -> Result<()> {
85 if let Some(parent) = self.cert_chain.last() {
86 let parent = X509Certificate::from_der(&parent.contents)?.1;
87 let current = X509Certificate::from_der(&der)?.1;
88 current.verify_signature(Some(parent.public_key()))?;
89 }
90 self.cert_chain.push(OwnedCert::new(der));
91 Ok(())
92 }
93
94 pub fn add_from_pem(&mut self, reader: impl Read + Seek) -> Result<()> {
102 for pem in Pem::iter_from_reader(BufReader::new(reader)) {
103 let pem = pem?;
104 self.add_and_validate(pem.contents)?;
105 }
106 Ok(())
107 }
108
109 pub fn add_from_der(&mut self, der: Vec<u8>) -> Result<()> {
117 self.add_and_validate(der)?;
118 Ok(())
119 }
120
121 pub fn leaf_extensions(&self) -> Result<Extensions> {
127 let leaf = self.cert_chain.last().ok_or(Error::CertificateMissing)?;
128 let leaf = X509Certificate::from_der(&leaf.contents)?.1;
129 let ext = leaf.extensions_map()?;
130 Ok(Extensions(
131 ext.into_iter()
132 .map(|(k, v)| (k.to_string(), v.value.to_vec()))
133 .collect(),
134 ))
135 }
136}
137
138macro_rules! check_eq {
139 ($actual:expr, $expected:expr, $descr:tt) => {
140 match (&$actual, &$expected, &$descr) {
141 (actual, expected, descr) => {
142 if actual != expected {
143 return Err(Error::UnexpectedFieldValue {
144 field: (*descr).into(),
145 expected: (*expected).to_string(),
146 actual: (*actual).to_string(),
147 });
148 }
149 }
150 }
151 };
152}
153
154#[derive(Debug)]
156pub struct Extensions(HashMap<String, Vec<u8>>);
157
158impl Extensions {
159 #[must_use]
161 pub fn into_inner(self) -> HashMap<String, Vec<u8>> {
162 self.0
163 }
164
165 pub fn get(&self, oid: &str) -> Result<&[u8]> {
171 self.0
172 .get(oid)
173 .map(|v| &v[..])
174 .ok_or_else(|| Error::OidMissing { oid: oid.into() })
175 }
176
177 pub fn to_yubihsm_attestation(&self) -> Result<YubiHsmAttestation> {
184 let firmware_version = self.get("1.3.6.1.4.1.41482.4.1")?;
185 check_eq!(firmware_version[0], 4, "octet string");
186 check_eq!(firmware_version[1], 3, "length");
187 let firmware_version = (
188 firmware_version[2],
189 firmware_version[3],
190 firmware_version[4],
191 );
192
193 let serial_number = self.get("1.3.6.1.4.1.41482.4.2")?;
194 check_eq!(serial_number[0], 2, "integer");
195 check_eq!(serial_number[1], 4, "length");
196 let mut sn: [u8; 4] = [0; 4];
197 sn.copy_from_slice(&serial_number[2..6]);
198 let serial_number = u32::from_be_bytes(sn);
199
200 let origin = self.get("1.3.6.1.4.1.41482.4.3")?;
201 check_eq!(origin[0], 3, "bit string");
202 check_eq!(origin[1], 2, "length");
203 check_eq!(origin[2], 0, "padding");
204 let origin = origin[3];
205
206 let domains = self.get("1.3.6.1.4.1.41482.4.4")?;
207 check_eq!(domains[0], 3, "bit string");
208 check_eq!(domains[1], 3, "length");
209 check_eq!(domains[2], 0, "padding");
210 let domains = u16::from_be_bytes([domains[3], domains[4]]);
211
212 let capabilities = self.get("1.3.6.1.4.1.41482.4.5")?;
213 check_eq!(capabilities[0], 3, "bit string");
214 check_eq!(capabilities[1], 9, "length");
215 check_eq!(capabilities[2], 0, "padding");
216 let mut caps = [0; 8];
217 caps.copy_from_slice(&capabilities[3..]);
218
219 let object_id = self.get("1.3.6.1.4.1.41482.4.6")?;
220 check_eq!(object_id[0], 2, "integer");
221 check_eq!(object_id[1], 1, "length");
222 let object_id = u16::from_be_bytes([0, object_id[2]]);
223
224 let label = self.get("1.3.6.1.4.1.41482.4.9")?;
225 check_eq!(label[0], 12, "utf-8 string");
226 let label = String::from_utf8_lossy(&label[2..]).into_owned();
227
228 Ok(YubiHsmAttestation {
229 firmware_version,
230 serial_number,
231 origin,
232 domains,
233 capabilities: caps,
234 object_id,
235 label,
236 })
237 }
238}
239
240#[non_exhaustive]
244#[derive(Debug)]
245pub struct YubiHsmAttestation {
246 pub firmware_version: (u8, u8, u8),
248
249 pub serial_number: u32,
251
252 pub origin: u8,
256
257 pub domains: u16,
261
262 pub capabilities: [u8; 8],
266
267 pub object_id: u16,
271
272 pub label: String,
276}
277
278#[cfg(test)]
279mod tests {
280 use std::fs::File;
281
282 use testresult::TestResult;
283
284 use super::*;
285
286 #[test]
287 fn test_sample_chain() -> TestResult {
288 let mut validator = Validator::default();
289 validator.add_from_pem(File::open("yubihsm2-attest-ca-crt-pem")?)?;
290 validator.add_from_pem(File::open("intermediate-pem")?)?;
291
292 let binding = std::fs::read("attestation-cert.cer")?;
293 validator.add_from_der(binding)?;
294
295 validator.add_from_pem(File::open("attestation-pem")?)?;
296
297 Ok(())
298 }
299
300 #[test]
301 fn test_broken_chain() -> TestResult {
302 let mut validator = Validator::default();
303 validator.add_from_pem(File::open("yubihsm2-attest-ca-crt-pem")?)?;
304 validator.add_from_pem(File::open("intermediate-pem")?)?;
305
306 let result = validator.add_from_pem(File::open("attestation-pem")?);
309 assert!(result.is_err());
310
311 Ok(())
312 }
313
314 #[test]
315 fn test_leaf_extensions() -> TestResult {
316 let mut validator = Validator::default();
317
318 validator.add_from_pem(File::open("attestation-pem")?)?;
319 let extensions = validator.leaf_extensions()?.into_inner();
321 assert_eq!(extensions["1.3.6.1.4.1.41482.4.1"], vec![4, 3, 2, 4, 1]);
322 assert_eq!(
323 extensions["1.3.6.1.4.1.41482.4.2"],
324 vec![2, 4, 2, 11, 217, 253]
325 );
326 assert_eq!(extensions["1.3.6.1.4.1.41482.4.3"], vec![3, 2, 0, 1]);
327 assert_eq!(extensions["1.3.6.1.4.1.41482.4.4"], vec![3, 3, 0, 0, 1]);
328 assert_eq!(
329 extensions["1.3.6.1.4.1.41482.4.5"],
330 vec![3, 9, 0, 0, 0, 0, 0, 0, 0, 1, 0]
331 );
332 assert_eq!(extensions["1.3.6.1.4.1.41482.4.6"], vec![2, 1, 3]);
333 assert_eq!(extensions["1.3.6.1.4.1.41482.4.9"], vec![12, 0]);
334
335 Ok(())
336 }
337
338 #[test]
339 fn test_yubihsm_attestation_extensions() -> TestResult {
340 let mut validator = Validator::default();
341
342 validator.add_from_pem(File::open("attestation-pem")?)?;
343
344 let attestation = validator.leaf_extensions()?.to_yubihsm_attestation()?;
345 assert_eq!(attestation.firmware_version, (2, 4, 1));
346 assert_eq!(attestation.serial_number, 34_331_133);
347 assert_eq!(attestation.origin, 0x01);
348 assert_eq!(attestation.domains, 0x00_01);
349 assert_eq!(attestation.capabilities, [0, 0, 0, 0, 0, 0, 1, 0]);
350 assert_eq!(attestation.object_id, 0x03);
351 assert_eq!(attestation.label, "");
352
353 Ok(())
354 }
355
356 #[test]
357 fn test_yubihsm_attestation_extensions_fail() -> TestResult {
358 let mut validator = Validator::default();
359
360 validator.add_from_pem(File::open("intermediate-pem")?)?;
361
362 let attestation = validator.leaf_extensions()?.to_yubihsm_attestation();
363
364 let Err(e) = attestation else {
365 panic!("Parsing succeeded but should fail");
366 };
367
368 let Error::OidMissing { oid } = e else {
369 panic!("The error should indicate OidMissing but was: {e:?}");
370 };
371
372 assert_eq!(oid, "1.3.6.1.4.1.41482.4.1");
373
374 Ok(())
375 }
376}