ctap_types/ctap2/
get_info.rs

1use crate::webauthn::FilteredPublicKeyCredentialParameters;
2use crate::{Bytes, TryFromStrError, Vec};
3use serde::{Deserialize, Serialize};
4use serde_indexed::{DeserializeIndexed, SerializeIndexed};
5
6pub type AuthenticatorInfo = Response;
7
8#[derive(Clone, Debug, Eq, PartialEq, SerializeIndexed, DeserializeIndexed)]
9#[non_exhaustive]
10#[serde_indexed(offset = 1)]
11pub struct Response {
12    // 0x01
13    pub versions: Vec<Version, 4>,
14
15    // 0x02
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub extensions: Option<Vec<Extension, 4>>,
18
19    // 0x03
20    pub aaguid: Bytes<16>,
21
22    // 0x04
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub options: Option<CtapOptions>,
25
26    // 0x05
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub max_msg_size: Option<usize>,
29
30    // 0x06
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub pin_protocols: Option<Vec<u8, 2>>,
33
34    // 0x07
35    // FIDO_2_1
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub max_creds_in_list: Option<usize>,
38
39    // 0x08
40    // FIDO_2_1
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub max_cred_id_length: Option<usize>,
43
44    // 0x09
45    // FIDO_2_1
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub transports: Option<Vec<Transport, 4>>,
48
49    // 0x0A
50    // FIDO_2_1
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub algorithms: Option<FilteredPublicKeyCredentialParameters>,
53
54    // 0x0B
55    // FIDO_2_1
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub max_serialized_large_blob_array: Option<usize>,
58
59    // 0x0C
60    // FIDO_2_1
61    #[cfg(feature = "get-info-full")]
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub force_pin_change: Option<bool>,
64
65    // 0x0D
66    // FIDO_2_1
67    #[cfg(feature = "get-info-full")]
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub min_pin_length: Option<usize>,
70
71    // 0x0E
72    // FIDO_2_1
73    #[cfg(feature = "get-info-full")]
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub firmware_version: Option<usize>,
76
77    // 0x0F
78    // FIDO_2_1
79    #[cfg(feature = "get-info-full")]
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub max_cred_blob_length: Option<usize>,
82
83    // 0x10
84    // FIDO_2_1
85    #[cfg(feature = "get-info-full")]
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub max_rpids_for_set_min_pin_length: Option<usize>,
88
89    // 0x11
90    // FIDO_2_1
91    #[cfg(feature = "get-info-full")]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub preferred_platform_uv_attempts: Option<usize>,
94
95    // 0x12
96    // FIDO_2_1
97    #[cfg(feature = "get-info-full")]
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub uv_modality: Option<usize>,
100
101    // 0x13
102    // FIDO_2_1
103    #[cfg(feature = "get-info-full")]
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub certifications: Option<Certifications>,
106
107    // 0x14
108    // FIDO_2_1
109    #[cfg(feature = "get-info-full")]
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub remaining_discoverable_credentials: Option<usize>,
112
113    // 0x15
114    // FIDO_2_1
115    #[cfg(feature = "get-info-full")]
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub vendor_prototype_config_commands: Option<usize>,
118
119    // 0x16
120    // FIDO_2_2
121    #[cfg(feature = "get-info-full")]
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub attestation_formats: Option<Vec<super::AttestationStatementFormat, 2>>,
124
125    // 0x17
126    // FIDO_2_2
127    #[cfg(feature = "get-info-full")]
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub uv_count_since_last_pin_entry: Option<usize>,
130
131    // 0x18
132    // FIDO_2_2
133    #[cfg(feature = "get-info-full")]
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub long_touch_for_reset: Option<bool>,
136}
137
138impl Default for Response {
139    fn default() -> Self {
140        let mut zero_aaguid = Vec::<u8, 16>::new();
141        zero_aaguid.resize_default(16).unwrap();
142        let aaguid = Bytes::<16>::from(zero_aaguid);
143
144        let mut response = ResponseBuilder {
145            aaguid,
146            versions: Vec::new(),
147        }
148        .build();
149        response.options = Some(CtapOptions::default());
150        response
151    }
152}
153
154#[derive(Debug)]
155pub struct ResponseBuilder {
156    pub versions: Vec<Version, 4>,
157    pub aaguid: Bytes<16>,
158}
159
160impl ResponseBuilder {
161    #[inline(always)]
162    pub fn build(self) -> Response {
163        Response {
164            versions: self.versions,
165            aaguid: self.aaguid,
166            extensions: None,
167            options: None,
168            max_msg_size: None,
169            pin_protocols: None,
170            max_creds_in_list: None,
171            max_cred_id_length: None,
172            transports: None,
173            algorithms: None,
174            max_serialized_large_blob_array: None,
175            #[cfg(feature = "get-info-full")]
176            force_pin_change: None,
177            #[cfg(feature = "get-info-full")]
178            min_pin_length: None,
179            #[cfg(feature = "get-info-full")]
180            firmware_version: None,
181            #[cfg(feature = "get-info-full")]
182            max_cred_blob_length: None,
183            #[cfg(feature = "get-info-full")]
184            max_rpids_for_set_min_pin_length: None,
185            #[cfg(feature = "get-info-full")]
186            preferred_platform_uv_attempts: None,
187            #[cfg(feature = "get-info-full")]
188            uv_modality: None,
189            #[cfg(feature = "get-info-full")]
190            certifications: None,
191            #[cfg(feature = "get-info-full")]
192            remaining_discoverable_credentials: None,
193            #[cfg(feature = "get-info-full")]
194            vendor_prototype_config_commands: None,
195            #[cfg(feature = "get-info-full")]
196            attestation_formats: None,
197            #[cfg(feature = "get-info-full")]
198            uv_count_since_last_pin_entry: None,
199            #[cfg(feature = "get-info-full")]
200            long_touch_for_reset: None,
201        }
202    }
203}
204
205#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
206#[non_exhaustive]
207#[serde(into = "&str", try_from = "&str")]
208pub enum Version {
209    Fido2_0,
210    Fido2_1,
211    Fido2_1Pre,
212    U2fV2,
213}
214
215impl Version {
216    const FIDO_2_0: &'static str = "FIDO_2_0";
217    const FIDO_2_1: &'static str = "FIDO_2_1";
218    const FIDO_2_1_PRE: &'static str = "FIDO_2_1_PRE";
219    const U2F_V2: &'static str = "U2F_V2";
220}
221
222impl From<Version> for &str {
223    fn from(version: Version) -> Self {
224        match version {
225            Version::Fido2_0 => Version::FIDO_2_0,
226            Version::Fido2_1 => Version::FIDO_2_1,
227            Version::Fido2_1Pre => Version::FIDO_2_1_PRE,
228            Version::U2fV2 => Version::U2F_V2,
229        }
230    }
231}
232
233impl TryFrom<&str> for Version {
234    type Error = TryFromStrError;
235
236    fn try_from(s: &str) -> Result<Self, Self::Error> {
237        match s {
238            Self::FIDO_2_0 => Ok(Self::Fido2_0),
239            Self::FIDO_2_1 => Ok(Self::Fido2_1),
240            Self::FIDO_2_1_PRE => Ok(Self::Fido2_1Pre),
241            Self::U2F_V2 => Ok(Self::U2fV2),
242            _ => Err(TryFromStrError),
243        }
244    }
245}
246
247#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
248#[non_exhaustive]
249#[serde(into = "&str", try_from = "&str")]
250pub enum Extension {
251    CredProtect,
252    HmacSecret,
253    LargeBlobKey,
254    ThirdPartyPayment,
255}
256
257impl Extension {
258    const CRED_PROTECT: &'static str = "credProtect";
259    const HMAC_SECRET: &'static str = "hmac-secret";
260    const LARGE_BLOB_KEY: &'static str = "largeBlobKey";
261    const THIRD_PARTY_PAYMENT: &'static str = "thirdPartyPayment";
262}
263
264impl From<Extension> for &str {
265    fn from(extension: Extension) -> Self {
266        match extension {
267            Extension::CredProtect => Extension::CRED_PROTECT,
268            Extension::HmacSecret => Extension::HMAC_SECRET,
269            Extension::LargeBlobKey => Extension::LARGE_BLOB_KEY,
270            Extension::ThirdPartyPayment => Extension::THIRD_PARTY_PAYMENT,
271        }
272    }
273}
274
275impl TryFrom<&str> for Extension {
276    type Error = TryFromStrError;
277
278    fn try_from(s: &str) -> Result<Self, Self::Error> {
279        match s {
280            Self::CRED_PROTECT => Ok(Self::CredProtect),
281            Self::HMAC_SECRET => Ok(Self::HmacSecret),
282            Self::LARGE_BLOB_KEY => Ok(Self::LargeBlobKey),
283            Self::THIRD_PARTY_PAYMENT => Ok(Self::ThirdPartyPayment),
284            _ => Err(TryFromStrError),
285        }
286    }
287}
288
289#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
290#[non_exhaustive]
291#[serde(into = "&str", try_from = "&str")]
292pub enum Transport {
293    Nfc,
294    Usb,
295}
296
297impl Transport {
298    const NFC: &'static str = "nfc";
299    const USB: &'static str = "usb";
300}
301
302impl From<Transport> for &str {
303    fn from(transport: Transport) -> Self {
304        match transport {
305            Transport::Nfc => Transport::NFC,
306            Transport::Usb => Transport::USB,
307        }
308    }
309}
310
311impl TryFrom<&str> for Transport {
312    type Error = TryFromStrError;
313
314    fn try_from(s: &str) -> Result<Self, Self::Error> {
315        match s {
316            Self::NFC => Ok(Self::Nfc),
317            Self::USB => Ok(Self::Usb),
318            _ => Err(TryFromStrError),
319        }
320    }
321}
322
323#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
324#[non_exhaustive]
325#[serde(rename_all = "camelCase")]
326pub struct CtapOptions {
327    #[cfg(feature = "get-info-full")]
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub ep: Option<bool>, // default false
330    pub rk: bool,
331    pub up: bool,
332    #[serde(skip_serializing_if = "Option::is_none")]
333    /// Note: This capability means capability to perform UV
334    /// *within the authenticator*, for instance with biometrics
335    /// or on-device PIN entry.
336    pub uv: Option<bool>, // default not capable
337    #[serde(skip_serializing_if = "Option::is_none")]
338    pub plat: Option<bool>, // default false
339    #[cfg(feature = "get-info-full")]
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub uv_acfg: Option<bool>, // default false
342    #[cfg(feature = "get-info-full")]
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub always_uv: Option<bool>,
345    #[serde(skip_serializing_if = "Option::is_none")]
346    pub cred_mgmt: Option<bool>,
347    #[cfg(feature = "get-info-full")]
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub authnr_cfg: Option<bool>,
350    #[cfg(feature = "get-info-full")]
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub bio_enroll: Option<bool>, // default false
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub client_pin: Option<bool>,
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub large_blobs: Option<bool>,
357    #[cfg(feature = "get-info-full")]
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub uv_bio_enroll: Option<bool>,
360    #[cfg(feature = "get-info-full")]
361    #[serde(rename = "setMinPINLength", skip_serializing_if = "Option::is_none")]
362    pub set_min_pin_length: Option<bool>, // default false
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub pin_uv_auth_token: Option<bool>,
365    #[cfg(feature = "get-info-full")]
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub make_cred_uv_not_rqd: Option<bool>,
368    #[cfg(feature = "get-info-full")]
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub credential_mgmt_preview: Option<bool>,
371    #[cfg(feature = "get-info-full")]
372    #[serde(skip_serializing_if = "Option::is_none")]
373    pub user_verification_mgmt_preview: Option<bool>,
374    #[cfg(feature = "get-info-full")]
375    #[serde(skip_serializing_if = "Option::is_none")]
376    pub no_mc_ga_permissions_with_client_pin: Option<bool>,
377}
378
379impl Default for CtapOptions {
380    fn default() -> Self {
381        Self {
382            #[cfg(feature = "get-info-full")]
383            ep: None,
384            rk: false,
385            up: true,
386            uv: None,
387            plat: None,
388            #[cfg(feature = "get-info-full")]
389            uv_acfg: None,
390            #[cfg(feature = "get-info-full")]
391            always_uv: None,
392            cred_mgmt: None,
393            #[cfg(feature = "get-info-full")]
394            authnr_cfg: None,
395            #[cfg(feature = "get-info-full")]
396            bio_enroll: None,
397            client_pin: None,
398            large_blobs: None,
399            #[cfg(feature = "get-info-full")]
400            uv_bio_enroll: None,
401            pin_uv_auth_token: None,
402            #[cfg(feature = "get-info-full")]
403            set_min_pin_length: None,
404            #[cfg(feature = "get-info-full")]
405            make_cred_uv_not_rqd: None,
406            #[cfg(feature = "get-info-full")]
407            credential_mgmt_preview: None,
408            #[cfg(feature = "get-info-full")]
409            user_verification_mgmt_preview: None,
410            #[cfg(feature = "get-info-full")]
411            no_mc_ga_permissions_with_client_pin: None,
412        }
413    }
414}
415
416#[cfg(feature = "get-info-full")]
417#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
418#[non_exhaustive]
419pub struct Certifications {
420    #[serde(rename = "FIPS-CMVP-2")]
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub fips_cmpv2: Option<u8>,
423
424    #[serde(rename = "FIPS-CMVP-3")]
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub fips_cmpv3: Option<u8>,
427
428    #[serde(rename = "FIPS-CMVP-2-PHY")]
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub fips_cmpv2_phy: Option<u8>,
431
432    #[serde(rename = "FIPS-CMVP-3-PHY")]
433    #[serde(skip_serializing_if = "Option::is_none")]
434    pub fips_cmpv3_phy: Option<u8>,
435
436    #[serde(rename = "CC-EAL")]
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub cc_eal: Option<u8>,
439
440    #[serde(rename = "FIDO")]
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub fido: Option<u8>,
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448    use serde_test::{assert_ser_tokens, assert_tokens, Token};
449
450    #[test]
451    fn test_serde_version() {
452        let versions = [
453            (Version::Fido2_0, "FIDO_2_0"),
454            (Version::Fido2_1, "FIDO_2_1"),
455            (Version::Fido2_1Pre, "FIDO_2_1_PRE"),
456            (Version::U2fV2, "U2F_V2"),
457        ];
458        for (version, s) in versions {
459            assert_tokens(&version, &[Token::BorrowedStr(s)]);
460        }
461    }
462
463    #[test]
464    fn test_serde_extension() {
465        let extensions = [
466            (Extension::CredProtect, "credProtect"),
467            (Extension::HmacSecret, "hmac-secret"),
468            (Extension::LargeBlobKey, "largeBlobKey"),
469        ];
470        for (extension, s) in extensions {
471            assert_tokens(&extension, &[Token::BorrowedStr(s)]);
472        }
473    }
474
475    #[test]
476    fn test_serde_transport() {
477        let transports = [(Transport::Nfc, "nfc"), (Transport::Usb, "usb")];
478        for (transport, s) in transports {
479            assert_tokens(&transport, &[Token::BorrowedStr(s)]);
480        }
481    }
482
483    #[test]
484    fn test_serde_get_info_minimal() {
485        let versions = Vec::from_slice(&[Version::Fido2_0, Version::Fido2_1]).unwrap();
486        let aaguid = Bytes::from_slice(&[0xff; 16]).unwrap();
487        let response = ResponseBuilder { versions, aaguid }.build();
488        assert_tokens(
489            &response,
490            &[
491                Token::Map { len: Some(2) },
492                Token::U64(1),
493                Token::Seq { len: Some(2) },
494                Token::BorrowedStr("FIDO_2_0"),
495                Token::BorrowedStr("FIDO_2_1"),
496                Token::SeqEnd,
497                Token::U64(3),
498                Token::BorrowedBytes(&[0xff; 16]),
499                Token::MapEnd,
500            ],
501        );
502    }
503
504    #[test]
505    fn test_serde_get_info_default() {
506        // This corresponds to the response sent by the Nitrokey 3, see for example:
507        // https://github.com/Nitrokey/nitrokey-3-firmware/blob/0d7209f1f75354878c0cf3454055defe8372ed14/utils/fido2-mds/metadata/v4/metadata-nk3xn-v4.json
508        const AAGUID: &[u8] = &[
509            236, 153, 219, 25, 205, 31, 76, 6, 162, 169, 148, 15, 23, 166, 163, 11,
510        ];
511        let versions =
512            Vec::from_slice(&[Version::U2fV2, Version::Fido2_0, Version::Fido2_1]).unwrap();
513        let aaguid = Bytes::from_slice(AAGUID).unwrap();
514        let mut options = CtapOptions::default();
515        options.rk = true;
516        options.plat = Some(false);
517        options.client_pin = Some(false);
518        options.cred_mgmt = Some(true);
519        options.large_blobs = Some(false);
520        options.pin_uv_auth_token = Some(true);
521        let mut response = ResponseBuilder { versions, aaguid }.build();
522        response.extensions =
523            Some(Vec::from_slice(&[Extension::CredProtect, Extension::HmacSecret]).unwrap());
524        response.options = Some(options);
525        response.max_msg_size = Some(3072);
526        response.pin_protocols = Some(Vec::from_slice(&[1, 0]).unwrap());
527        response.max_creds_in_list = Some(10);
528        response.max_cred_id_length = Some(255);
529        response.transports = Some(Vec::from_slice(&[Transport::Nfc, Transport::Usb]).unwrap());
530        assert_ser_tokens(
531            &response,
532            &[
533                Token::Map { len: Some(9) },
534                // 0x01: versions
535                Token::U64(0x01),
536                Token::Seq { len: Some(3) },
537                Token::BorrowedStr("U2F_V2"),
538                Token::BorrowedStr("FIDO_2_0"),
539                Token::BorrowedStr("FIDO_2_1"),
540                Token::SeqEnd,
541                // 0x02: extensions
542                Token::U64(0x02),
543                Token::Some,
544                Token::Seq { len: Some(2) },
545                Token::BorrowedStr("credProtect"),
546                Token::BorrowedStr("hmac-secret"),
547                Token::SeqEnd,
548                // 0x03: aaguid
549                Token::U64(0x03),
550                Token::BorrowedBytes(AAGUID),
551                // 0x04: options
552                Token::U64(0x04),
553                Token::Some,
554                Token::Struct {
555                    name: "CtapOptions",
556                    len: 7,
557                },
558                Token::BorrowedStr("rk"),
559                Token::Bool(true),
560                Token::BorrowedStr("up"),
561                Token::Bool(true),
562                Token::BorrowedStr("plat"),
563                Token::Some,
564                Token::Bool(false),
565                Token::BorrowedStr("credMgmt"),
566                Token::Some,
567                Token::Bool(true),
568                Token::BorrowedStr("clientPin"),
569                Token::Some,
570                Token::Bool(false),
571                Token::BorrowedStr("largeBlobs"),
572                Token::Some,
573                Token::Bool(false),
574                Token::BorrowedStr("pinUvAuthToken"),
575                Token::Some,
576                Token::Bool(true),
577                Token::StructEnd,
578                // 0x05: maxMsgSize
579                Token::U64(0x05),
580                Token::Some,
581                Token::U64(3072),
582                // 0x06: pinUvAuthProtocols
583                Token::U64(0x06),
584                Token::Some,
585                Token::Seq { len: Some(2) },
586                Token::U8(1),
587                Token::U8(0),
588                Token::SeqEnd,
589                // 0x07: maxCredentialCountInList
590                Token::U64(0x07),
591                Token::Some,
592                Token::U64(10),
593                // 0x08: maxCredentialIdLength
594                Token::U64(0x08),
595                Token::Some,
596                Token::U64(255),
597                // 0x09: transports
598                Token::U64(0x09),
599                Token::Some,
600                Token::Seq { len: Some(2) },
601                Token::BorrowedStr("nfc"),
602                Token::BorrowedStr("usb"),
603                Token::SeqEnd,
604                Token::MapEnd,
605            ],
606        );
607    }
608}