attestation_validator/yubico/
openpgp.rs

1//! Yubikey OpenPGP Attestation.
2//!
3//! See [OpenPGP Attestation](https://developers.yubico.com/PGP/Attestation.html) for more details.
4//!
5//! # Examples
6//!
7//! The following example validates attestation certificate and displays attestation info:
8//!
9//! ```
10//! # fn main() -> testresult::TestResult {
11//! use std::fs::File;
12//! use std::io::Cursor;
13//!
14//! use attestation_validator::Validator;
15//! use attestation_validator::yubico::openpgp::{
16//!     YUBICO_OPENPGP_ATTESTATION_CA_PEM, YubikeyOpenPgpAttestation,
17//! };
18//!
19//! let mut validator = Validator::default();
20//! validator.add_from_pem(Cursor::new(YUBICO_OPENPGP_ATTESTATION_CA_PEM))?;
21//! validator.add_from_pem(File::open("openpgp-card-pem")?)?;
22//! validator.add_from_pem(File::open("key-statement-pem")?)?;
23//!
24//! eprintln!(
25//!     "Extensions: {:#?}",
26//!     YubikeyOpenPgpAttestation::new(&validator.leaf_extensions()?)?
27//! );
28//!
29//! eprintln!("Raw public key (DER): {:?}", validator.leaf_public_key()?);
30//! # Ok(()) }
31//! ```
32
33use std::{
34    fmt::Display,
35    time::{Duration, SystemTime, UNIX_EPOCH},
36};
37
38#[cfg(doc)]
39use crate::Error;
40use crate::{Extensions, Result};
41
42/// Root Certification Authority for Yubikey's OpenPGP certificate chains for firmware prior to 5.7.4.
43///
44/// The contents of this constant are fetched from <https://developers.yubico.com/PKI/yubico-opgp-ca-1.pem>
45/// as referenced in [OpenPGP Attestation](https://developers.yubico.com/PGP/Attestation.html).
46pub const YUBICO_OPENPGP_ATTESTATION_CA_PEM: &[u8] = include_bytes!("yubico-opgp-ca-1-pem");
47
48/// User Interaction Flag for given key.
49#[repr(u8)]
50#[derive(Debug, PartialEq, Eq, Clone, Copy)]
51#[non_exhaustive]
52pub enum UserInteractionFlag {
53    /// Touch has been disabled.
54    TouchDisabled = 0x00,
55
56    /// Touch is enabled, but can be changed.
57    TouchEnabled = 0x01,
58
59    /// Touch is permanently enabled.
60    TouchPermanent = 0x02,
61
62    /// Touch is cached for a given number of seconds.
63    TouchCached = 0x03,
64
65    /// Touch is permanently enabled and is cached for a given number of seconds.
66    TouchPermanentCached = 0x04,
67
68    /// Unknown value of the UIF flag.
69    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/// Key source.
112#[repr(u8)]
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114#[non_exhaustive]
115pub enum KeySource {
116    /// Key has been imported (this value is not permitted).
117    Imported = 0x00,
118
119    /// Key has been generated on device.
120    GeneratedOnDevice = 0x01,
121
122    /// Other, unknown value.
123    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/// Token form factor.
157#[repr(u8)]
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159#[non_exhaustive]
160pub enum FormFactor {
161    /// Unspecified form factor.
162    Unspecified = 0x00,
163
164    /// USB-A Keychain.
165    UsbAKeychain = 0x01,
166
167    /// USB-A Nano.
168    UsbANano = 0x02,
169
170    /// USB-C Keychain.
171    UsbCKeychain = 0x03,
172
173    /// USB-C Nano.
174    UsbCNano = 0x04,
175
176    /// USB-C/Lightning Keychain.
177    UsbCLightningKeychain = 0x05,
178
179    /// Other, unknown variant.
180    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/// Yubikey's OpenPGP specific attestation values.
226///
227/// See [OpenPGP Attestation](https://developers.yubico.com/PGP/Attestation.html) for more details.
228#[non_exhaustive]
229#[derive(Debug)]
230pub struct YubikeyOpenPgpAttestation {
231    /// Firmware version.
232    pub firmware_version: (u8, u8, u8),
233
234    /// Device serial number.
235    pub serial_number: u32,
236
237    /// Cardholder name.
238    pub cardholder_name: String,
239
240    /// Attested key’s source.
241    pub key_source: KeySource,
242
243    /// Attested key’s fingerprint.
244    pub key_fingerprint: [u8; 20],
245
246    /// Attested key’s generation date in seconds since the Unix epoch.
247    pub generation_date: SystemTime,
248
249    /// Attested key’s signature counter (if applicable).
250    pub signature_counter: u32,
251
252    /// User Interaction Flag (UIF).
253    pub user_interaction_flag: UserInteractionFlag,
254
255    /// Form factor.
256    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    /// Creates an attestation object specific to Yubico's OpenPGP applet.
297    ///
298    /// See [OpenPGP Attestation](https://developers.yubico.com/PGP/Attestation.html) for a detailed list of inspected certificate extensions.
299    ///
300    /// # Errors
301    ///
302    /// If the value for given `oid` is missing this function returns [`Error::OidMissing`].
303    /// If field values have unexpected values [`Error::UnexpectedFieldValue`] is returned.
304    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        // device attestation cert is missing so the next line should fail
409
410        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}