1use std::{
34 fmt::Display,
35 time::{Duration, SystemTime, UNIX_EPOCH},
36};
37
38#[cfg(doc)]
39use crate::Error;
40use crate::{Extensions, Result};
41
42pub const YUBICO_OPENPGP_ATTESTATION_CA_PEM: &[u8] = include_bytes!("yubico-opgp-ca-1-pem");
47
48#[repr(u8)]
50#[derive(Debug, PartialEq, Eq, Clone, Copy)]
51#[non_exhaustive]
52pub enum UserInteractionFlag {
53 TouchDisabled = 0x00,
55
56 TouchEnabled = 0x01,
58
59 TouchPermanent = 0x02,
61
62 TouchCached = 0x03,
64
65 TouchPermanentCached = 0x04,
67
68 Unknown(u8),
70}
71
72impl Display for UserInteractionFlag {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.write_str(match self {
75 UserInteractionFlag::TouchDisabled => "Touch disabled",
76 UserInteractionFlag::TouchEnabled => "Touch enabled",
77 UserInteractionFlag::TouchPermanent => "Touch permanent",
78 UserInteractionFlag::TouchCached => "Touch cached",
79 UserInteractionFlag::TouchPermanentCached => "Touch permanent, cached",
80 UserInteractionFlag::Unknown(value) => return write!(f, "Unknown UIF ({value:X})"),
81 })
82 }
83}
84
85impl From<u8> for UserInteractionFlag {
86 fn from(value: u8) -> Self {
87 match value {
88 0x00 => Self::TouchDisabled,
89 0x01 => Self::TouchEnabled,
90 0x02 => Self::TouchPermanent,
91 0x03 => Self::TouchCached,
92 0x04 => Self::TouchPermanentCached,
93 _ => Self::Unknown(value),
94 }
95 }
96}
97
98impl From<UserInteractionFlag> for u8 {
99 fn from(value: UserInteractionFlag) -> Self {
100 match value {
101 UserInteractionFlag::TouchDisabled => 0x00,
102 UserInteractionFlag::TouchEnabled => 0x01,
103 UserInteractionFlag::TouchPermanent => 0x02,
104 UserInteractionFlag::TouchCached => 0x03,
105 UserInteractionFlag::TouchPermanentCached => 0x04,
106 UserInteractionFlag::Unknown(value) => value,
107 }
108 }
109}
110
111#[repr(u8)]
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114#[non_exhaustive]
115pub enum KeySource {
116 Imported = 0x00,
118
119 GeneratedOnDevice = 0x01,
121
122 Unknown(u8),
124}
125
126impl Display for KeySource {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.write_str(match self {
129 KeySource::Imported => "Imported",
130 KeySource::GeneratedOnDevice => "Generated on device",
131 KeySource::Unknown(value) => return write!(f, "Unknown key source ({value:X})"),
132 })
133 }
134}
135
136impl From<u8> for KeySource {
137 fn from(value: u8) -> Self {
138 match value {
139 0x00 => Self::Imported,
140 0x01 => Self::GeneratedOnDevice,
141 _ => Self::Unknown(value),
142 }
143 }
144}
145
146impl From<KeySource> for u8 {
147 fn from(value: KeySource) -> Self {
148 match value {
149 KeySource::Imported => 0x00,
150 KeySource::GeneratedOnDevice => 0x01,
151 KeySource::Unknown(value) => value,
152 }
153 }
154}
155
156#[repr(u8)]
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159#[non_exhaustive]
160pub enum FormFactor {
161 Unspecified = 0x00,
163
164 UsbAKeychain = 0x01,
166
167 UsbANano = 0x02,
169
170 UsbCKeychain = 0x03,
172
173 UsbCNano = 0x04,
175
176 UsbCLightningKeychain = 0x05,
178
179 Unknown(u8),
181}
182
183impl Display for FormFactor {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 f.write_str(match self {
186 FormFactor::Unspecified => "Unspecified",
187 FormFactor::UsbAKeychain => "USB-A Keychain",
188 FormFactor::UsbANano => "USB-A Nano",
189 FormFactor::UsbCKeychain => "USB-C Keychain",
190 FormFactor::UsbCNano => "USB-C Nano",
191 FormFactor::UsbCLightningKeychain => "USB-C/Lightning Keychain",
192 FormFactor::Unknown(value) => return write!(f, "Unknown form factor ({value:X})"),
193 })
194 }
195}
196
197impl From<u8> for FormFactor {
198 fn from(value: u8) -> Self {
199 match value {
200 0x00 => Self::Unspecified,
201 0x01 => Self::UsbAKeychain,
202 0x02 => Self::UsbANano,
203 0x03 => Self::UsbCKeychain,
204 0x04 => Self::UsbCNano,
205 0x05 => Self::UsbCLightningKeychain,
206 _ => Self::Unknown(value),
207 }
208 }
209}
210
211impl From<FormFactor> for u8 {
212 fn from(value: FormFactor) -> Self {
213 match value {
214 FormFactor::Unspecified => 0x00,
215 FormFactor::UsbAKeychain => 0x01,
216 FormFactor::UsbANano => 0x02,
217 FormFactor::UsbCKeychain => 0x03,
218 FormFactor::UsbCNano => 0x04,
219 FormFactor::UsbCLightningKeychain => 0x05,
220 FormFactor::Unknown(value) => value,
221 }
222 }
223}
224
225#[non_exhaustive]
229#[derive(Debug)]
230pub struct YubikeyOpenPgpAttestation {
231 pub firmware_version: (u8, u8, u8),
233
234 pub serial_number: u32,
236
237 pub cardholder_name: String,
239
240 pub key_source: KeySource,
242
243 pub key_fingerprint: [u8; 20],
245
246 pub generation_date: SystemTime,
248
249 pub signature_counter: u32,
251
252 pub user_interaction_flag: UserInteractionFlag,
254
255 pub form_factor: FormFactor,
257}
258
259impl Display for YubikeyOpenPgpAttestation {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 writeln!(
262 f,
263 "Yubikey OpenPGP Attestation for device with serial number {}",
264 self.serial_number
265 )?;
266 writeln!(
267 f,
268 "Firmware version: {}.{}.{}",
269 self.firmware_version.0, self.firmware_version.1, self.firmware_version.2
270 )?;
271 writeln!(f, "Cardholder's name: {}", self.cardholder_name)?;
272 writeln!(f, "Key source: {}", self.key_source)?;
273 write!(f, "Key fingerprint: ")?;
274 for byte in self.key_fingerprint {
275 write!(f, "{byte:02x}")?;
276 }
277 writeln!(f)?;
278
279 if let Ok(diff) = self.generation_date.duration_since(UNIX_EPOCH) {
280 writeln!(
281 f,
282 "Key generated {} seconds from the Unix Epoch",
283 diff.as_secs()
284 )?;
285 }
286
287 writeln!(f, "Number of signatures made: {}", self.signature_counter)?;
288 writeln!(f, "User Interaction Flag: {}", self.user_interaction_flag)?;
289 writeln!(f, "Device form factor: {}", self.form_factor)?;
290
291 Ok(())
292 }
293}
294
295impl YubikeyOpenPgpAttestation {
296 pub fn new(extensions: &Extensions) -> Result<Self> {
305 let cardholder_name = extensions.get("1.3.6.1.4.1.41482.5.1")?;
306 cardholder_name.assert_value_byte(0, 12, "utf-8 string")?;
307 let cardholder_name = String::from_utf8_lossy(&cardholder_name[2..]).into_owned();
308
309 let key_source = extensions.get("1.3.6.1.4.1.41482.5.2")?;
310 key_source.assert_value_byte(0, 2, "integer")?;
311 key_source.assert_value_byte(1, 1, "length")?;
312 let key_source = key_source[2].into();
313
314 let firmware_version = extensions.get("1.3.6.1.4.1.41482.5.3")?;
315 firmware_version.assert_value_byte(0, 4, "octet string")?;
316 firmware_version.assert_value_byte(1, 3, "length")?;
317 let firmware_version = (
318 firmware_version[2],
319 firmware_version[3],
320 firmware_version[4],
321 );
322
323 let key_fingerprint = extensions.get("1.3.6.1.4.1.41482.5.4")?;
324 key_fingerprint.assert_value_byte(0, 4, "octet string")?;
325 key_fingerprint.assert_value_byte(1, 20, "length")?;
326 let key_fingerprint = {
327 let mut tmp = [0; 20];
328 tmp.copy_from_slice(&key_fingerprint[2..22]);
329 tmp
330 };
331
332 let generation_date = extensions.get("1.3.6.1.4.1.41482.5.5")?;
333 generation_date.assert_value_byte(0, 4, "octet string")?;
334 generation_date.assert_value_byte(1, 4, "length")?;
335 let generation_date = {
336 let mut tmp = [0; 4];
337 tmp.copy_from_slice(&generation_date[2..6]);
338 u32::from_be_bytes(tmp)
339 };
340 let generation_date = UNIX_EPOCH + Duration::from_secs(generation_date.into());
341
342 let signature_counter = extensions.get("1.3.6.1.4.1.41482.5.6")?;
343 signature_counter.assert_value_byte(0, 2, "integer")?;
344 let signature_counter = {
345 let mut tmp = [0; 4];
346 let start = tmp.len() - (signature_counter[1] as usize);
347 tmp[start..].copy_from_slice(&signature_counter[2..]);
348 u32::from_be_bytes(tmp)
349 };
350
351 let serial_number = extensions.get("1.3.6.1.4.1.41482.5.7")?;
352 serial_number.assert_value_byte(0, 2, "integer")?;
353 serial_number.assert_value_byte(1, 4, "length")?;
354 let serial_number = {
355 let mut sn: [u8; 4] = [0; 4];
356 sn.copy_from_slice(&serial_number[2..6]);
357 u32::from_be_bytes(sn)
358 };
359
360 let user_interaction_flag = extensions.get("1.3.6.1.4.1.41482.5.8")?;
361 user_interaction_flag.assert_value_byte(0, 4, "octet string")?;
362 user_interaction_flag.assert_value_byte(1, 1, "length")?;
363 let user_interaction_flag = user_interaction_flag[2].into();
364
365 let form_factor = extensions.get("1.3.6.1.4.1.41482.5.9")?;
366 form_factor.assert_value_byte(0, 4, "octet string")?;
367 form_factor.assert_value_byte(1, 1, "length")?;
368 let form_factor = form_factor[2].into();
369
370 Ok(Self {
371 firmware_version,
372 serial_number,
373 cardholder_name,
374 key_source,
375 key_fingerprint,
376 generation_date,
377 signature_counter,
378 user_interaction_flag,
379 form_factor,
380 })
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use std::{collections::HashMap, fs::File, io::Cursor};
387
388 use testresult::TestResult;
389
390 use super::*;
391 use crate::{Error, Validator};
392
393 #[test]
394 fn test_sample_chain() -> TestResult {
395 let mut validator = Validator::default();
396 validator.add_from_pem(Cursor::new(YUBICO_OPENPGP_ATTESTATION_CA_PEM))?;
397 validator.add_from_pem(File::open("openpgp-card-pem")?)?;
398 validator.add_from_pem(File::open("key-statement-pem")?)?;
399
400 Ok(())
401 }
402
403 #[test]
404 fn test_broken_chain() -> TestResult {
405 let mut validator = Validator::default();
406 validator.add_from_pem(Cursor::new(YUBICO_OPENPGP_ATTESTATION_CA_PEM))?;
407
408 let result = validator.add_from_pem(File::open("key-statement-pem")?);
411 assert!(result.is_err());
412
413 Ok(())
414 }
415
416 #[test]
417 fn test_leaf_extensions() -> TestResult {
418 let mut validator = Validator::default();
419
420 validator.add_from_pem(File::open("key-statement-pem")?)?;
421
422 let extensions = validator.leaf_extensions()?.into_inner();
423 assert_eq!(
424 extensions["1.3.6.1.4.1.41482.5.1"],
425 vec![
426 12, 20, 75, 119, 97, 112, 105, 115, 105, 101, 119, 105, 99, 122, 60, 60, 87, 105,
427 107, 116, 111, 114,
428 ]
429 );
430 assert_eq!(extensions["1.3.6.1.4.1.41482.5.2"], vec![2, 1, 1]);
431 assert_eq!(extensions["1.3.6.1.4.1.41482.5.3"], vec![4, 3, 5, 2, 7,]);
432 assert_eq!(
433 extensions["1.3.6.1.4.1.41482.5.4"],
434 vec![
435 4, 20, 12, 124, 84, 145, 47, 217, 50, 188, 223, 19, 114, 106, 118, 124, 226, 36,
436 219, 49, 27, 60,
437 ]
438 );
439 assert_eq!(
440 extensions["1.3.6.1.4.1.41482.5.5"],
441 vec![4, 4, 100, 236, 133, 99]
442 );
443 assert_eq!(extensions["1.3.6.1.4.1.41482.5.6"], vec![2, 1, 1]);
444 assert_eq!(
445 extensions["1.3.6.1.4.1.41482.5.7"],
446 vec![2, 4, 0, 235, 84, 3]
447 );
448 assert_eq!(extensions["1.3.6.1.4.1.41482.5.8"], vec![4, 1, 2]);
449 assert_eq!(extensions["1.3.6.1.4.1.41482.5.9"], vec![4, 1, 3]);
450
451 Ok(())
452 }
453
454 #[test]
455 fn test_yubikey_openpgp_attestation_extensions() -> TestResult {
456 let mut validator = Validator::default();
457
458 validator.add_from_pem(File::open("key-statement-pem")?)?;
459
460 let attestation = YubikeyOpenPgpAttestation::new(&validator.leaf_extensions()?)?;
461 assert_eq!(attestation.firmware_version, (5, 2, 7));
462 assert_eq!(attestation.cardholder_name, "Kwapisiewicz<<Wiktor");
463 assert_eq!(attestation.serial_number, 15_422_467);
464 assert_eq!(attestation.key_source, KeySource::GeneratedOnDevice);
465 assert_eq!(
466 attestation.key_fingerprint,
467 [
468 12, 124, 84, 145, 47, 217, 50, 188, 223, 19, 114, 106, 118, 124, 226, 36, 219, 49,
469 27, 60,
470 ]
471 );
472 assert_eq!(
473 attestation
474 .generation_date
475 .duration_since(UNIX_EPOCH)?
476 .as_secs(),
477 1_693_222_243
478 );
479 assert_eq!(attestation.signature_counter, 1);
480 assert_eq!(
481 attestation.user_interaction_flag,
482 UserInteractionFlag::TouchPermanent
483 );
484 assert_eq!(attestation.form_factor, FormFactor::UsbCKeychain);
485
486 Ok(())
487 }
488
489 #[test]
490 fn test_yubikey_openpgp_attestation_extensions_fail() -> TestResult {
491 let mut validator = Validator::default();
492
493 validator.add_from_pem(File::open("openpgp-card-pem")?)?;
494
495 let attestation = YubikeyOpenPgpAttestation::new(&validator.leaf_extensions()?);
496
497 let Err(e) = attestation else {
498 panic!("Parsing succeeded but should fail");
499 };
500
501 let Error::OidMissing { oid } = e else {
502 panic!("The error should indicate OidMissing but was: {e:?}");
503 };
504
505 assert_eq!(oid, "1.3.6.1.4.1.41482.5.1");
506
507 Ok(())
508 }
509
510 #[test]
511 fn test_yubikey_openpgp_attestation_extensions_value_fail() {
512 let mut map = HashMap::new();
513 map.insert("1.3.6.1.4.1.41482.5.1".into(), vec![1, 2, 3]);
514 let ext = Extensions(map);
515 let att = YubikeyOpenPgpAttestation::new(&ext);
516 let Err(e) = att else {
517 panic!("Parsing succeeded but should fail");
518 };
519 let Error::UnexpectedFieldValue {
520 oid,
521 field,
522 expected,
523 actual,
524 } = e
525 else {
526 panic!("The error should indicate UnexpectedFieldValue but was: {e:?}");
527 };
528
529 assert_eq!(oid, "1.3.6.1.4.1.41482.5.1");
530 assert_eq!(field, "utf-8 string");
531 assert_eq!(expected, "12");
532 assert_eq!(actual, "1");
533 }
534}