webauthn_authenticator_rs/ctap2/commands/
credential_management.rs

1//! `authenticatorCredentialManagement` commands.
2#[cfg(doc)]
3use crate::stubs::*;
4
5use serde::{Deserialize, Serialize};
6use serde_cbor_2::{
7    ser::to_vec_packed,
8    value::{from_value, to_value},
9    Value,
10};
11use std::fmt::Debug;
12use webauthn_rs_core::proto::COSEKey;
13use webauthn_rs_proto::CredentialProtectionPolicy;
14
15use crate::crypto::{compute_sha256, SHA256Hash};
16
17use super::*;
18
19/// Macro to generate for both CTAP 2.1 and 2.1-PRE.
20macro_rules! cred_struct {
21    (
22        $(#[$outer:meta])*
23        $vis:vis struct $name:ident = $cmd:tt
24    ) => {
25        $(#[$outer])*
26        ///
27        /// Related:
28        ///
29        /// * [CredSubCommand] for dynamically-constructed commands
30        /// * [ENUMERATE_RPS_GET_NEXT][Self::ENUMERATE_RPS_GET_NEXT]
31        /// * [ENUMERATE_CREDENTIALS_GET_NEXT][Self::ENUMERATE_CREDENTIALS_GET_NEXT]
32        ///
33        /// Reference: [CTAP protocol reference][ref]
34        #[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)]
35        #[serde(into = "BTreeMap<u32, Value>")]
36        pub struct $name {
37            /// Action being requested
38            sub_command: u8,
39            /// Parameters for the [`sub_command`][Self::sub_command].
40            ///
41            /// **See also:** [CredSubCommand]
42            sub_command_params: Option<BTreeMap<Value, Value>>,
43            /// PIN / UV protocol version chosen by the platform
44            pin_uv_protocol: Option<u32>,
45            /// Output of calling "Authenticate" on some context specific to
46            /// [`sub_command`][Self::sub_command].
47            pin_uv_auth_param: Option<Vec<u8>>,
48        }
49
50        impl CBORCommand for $name {
51            const CMD: u8 = $cmd;
52            type Response = CredentialManagementResponse;
53        }
54
55        impl CredentialManagementRequestTrait for $name {
56            const ENUMERATE_RPS_GET_NEXT: Self = Self {
57                sub_command: 0x03,
58                sub_command_params: None,
59                pin_uv_protocol: None,
60                pin_uv_auth_param: None,
61            };
62
63            const ENUMERATE_CREDENTIALS_GET_NEXT: Self = Self {
64                sub_command: 0x05,
65                sub_command_params: None,
66                pin_uv_protocol: None,
67                pin_uv_auth_param: None,
68            };
69
70            fn new(
71                s: CredSubCommand,
72                pin_uv_protocol: Option<u32>,
73                pin_uv_auth_param: Option<Vec<u8>>,
74            ) -> Self {
75                let sub_command = (&s).into();
76                let sub_command_params = s.into();
77
78                Self {
79                    sub_command,
80                    sub_command_params,
81                    pin_uv_protocol,
82                    pin_uv_auth_param,
83                }
84            }
85        }
86
87        impl From<$name> for BTreeMap<u32, Value> {
88            fn from(value: $name) -> Self {
89                let $name {
90                    sub_command,
91                    sub_command_params,
92                    pin_uv_protocol,
93                    pin_uv_auth_param,
94                } = value;
95
96                let mut keys = BTreeMap::new();
97                keys.insert(0x01, Value::Integer(sub_command.into()));
98
99                if let Some(v) = sub_command_params {
100                    keys.insert(0x02, Value::Map(v));
101                }
102
103                if let Some(v) = pin_uv_protocol {
104                    keys.insert(0x03, Value::Integer(v.into()));
105                }
106
107                if let Some(v) = pin_uv_auth_param {
108                    keys.insert(0x04, Value::Bytes(v));
109                }
110
111                keys
112            }
113        }
114    };
115}
116
117/// Common functionality for CTAP 2.1 and 2.1-PRE `CredentialManegement` request types.
118pub trait CredentialManagementRequestTrait:
119    CBORCommand<Response = CredentialManagementResponse>
120{
121    /// Creates a new [CredentialManagementRequest] from the given [CredSubCommand].
122    fn new(
123        s: CredSubCommand,
124        pin_uv_protocol: Option<u32>,
125        pin_uv_auth_param: Option<Vec<u8>>,
126    ) -> Self;
127
128    /// Command to get the next RP while enumerating RPs with discoverable
129    /// credentials on the authenticator.
130    ///
131    /// **See also:** [`CredSubCommand::EnumerateRPsBegin`]
132    const ENUMERATE_RPS_GET_NEXT: Self;
133
134    /// Command to get the next credential while enumerating discoverable
135    /// credentials on the authenticator for an RP.
136    ///
137    /// **See also:** [`CredSubCommand::EnumerateCredentialsBegin`]
138    const ENUMERATE_CREDENTIALS_GET_NEXT: Self;
139}
140
141/// Wrapper for credential management command types, which can be passed to
142/// [CredentialManagementRequestTrait::new].
143///
144/// Static commands (not requiring authentication) are declared as constants of
145/// [CredentialManagementRequestTrait], see:
146///
147/// * [ENUMERATE_RPS_GET_NEXT][CredentialManagementRequestTrait::ENUMERATE_RPS_GET_NEXT]
148/// * [ENUMERATE_CREDENTIALS_GET_NEXT][CredentialManagementRequestTrait::ENUMERATE_CREDENTIALS_GET_NEXT]
149#[derive(Debug, Clone, PartialEq, Eq, Default)]
150pub enum CredSubCommand {
151    #[default]
152    Unknown,
153
154    /// Gets metadata about the authenticator's discoverable credential storage.
155    ///
156    /// See [CredentialStorageMetadata] for more details.
157    GetCredsMetadata,
158
159    /// Starts enumerating all relying parties with discoverable credentials
160    /// stored on this authenticator.
161    ///
162    /// To get the next relying party, use
163    /// [ENUMERATE_RPS_GET_NEXT][CredentialManagementRequestTrait::ENUMERATE_RPS_GET_NEXT].
164    EnumerateRPsBegin,
165
166    /// Starts enumerating all credentials for a relying party, by the SHA-256
167    /// hash of the relying party ID.
168    ///
169    /// To enumerate credentials by relying party ID (rather than its hash), use
170    /// [`enumerate_credentials_by_rpid()`][0].
171    ///
172    /// To get the next credential, use [ENUMERATE_CREDENTIALS_GET_NEXT][1].
173    ///
174    /// [0]: CredSubCommand::enumerate_credentials_by_rpid
175    /// [1]: CredentialManagementRequestTrait::ENUMERATE_CREDENTIALS_GET_NEXT
176    EnumerateCredentialsBegin(/* rpIdHash */ SHA256Hash),
177
178    /// Deletes a discoverable credential from the authenticator.
179    DeleteCredential(PublicKeyCredentialDescriptorCM),
180
181    /// Updates user information for a discoverable credential.
182    ///
183    /// This is only available on authenticators supporting CTAP 2.1 or later.
184    UpdateUserInformation(PublicKeyCredentialDescriptorCM, UserCM),
185}
186
187impl From<&CredSubCommand> for u8 {
188    fn from(c: &CredSubCommand) -> Self {
189        use CredSubCommand::*;
190        match c {
191            Unknown => 0x00,
192            GetCredsMetadata => 0x01,
193            EnumerateRPsBegin => 0x02,
194            // EnumerateRPsGetNextRP => 0x03,
195            EnumerateCredentialsBegin(_) => 0x04,
196            // EnumerateCredentialsGetNextCredential => 0x05,
197            DeleteCredential(_) => 0x06,
198            UpdateUserInformation(_, _) => 0x07,
199        }
200    }
201}
202
203impl From<CredSubCommand> for Option<BTreeMap<Value, Value>> {
204    fn from(c: CredSubCommand) -> Self {
205        use CredSubCommand::*;
206        match c {
207            Unknown | GetCredsMetadata | EnumerateRPsBegin => None,
208            EnumerateCredentialsBegin(rp_id_hash) => Some(BTreeMap::from([(
209                Value::Integer(0x01),
210                Value::Bytes(rp_id_hash.to_vec()),
211            )])),
212            DeleteCredential(credential_id) => Some(BTreeMap::from([(
213                Value::Integer(0x02),
214                to_value(credential_id).ok()?,
215            )])),
216            UpdateUserInformation(credential_id, user) => Some(BTreeMap::from([
217                (Value::Integer(0x02), to_value(credential_id).ok()?),
218                (Value::Integer(0x03), to_value(user).ok()?),
219            ])),
220        }
221    }
222}
223
224impl CredSubCommand {
225    /// The [PRF (pseudo-random function)][prf] for [CredSubCommand], used to
226    /// sign requests for PIN/UV authentication.
227    ///
228    /// [prf]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#prfValues
229    pub fn prf(&self) -> Vec<u8> {
230        let subcommand = self.into();
231        let sub_command_params: Option<BTreeMap<Value, Value>> = self.to_owned().into();
232
233        let mut o = Vec::new();
234        o.push(subcommand);
235        if let Some(p) = sub_command_params
236            .as_ref()
237            .and_then(|p| to_vec_packed(p).ok())
238        {
239            o.extend_from_slice(p.as_slice())
240        }
241
242        o
243    }
244
245    /// Creates an [EnumerateCredentialsBegin][0] for enumerating credentials by
246    /// relying party ID.
247    ///
248    /// See [EnumerateCredentialsBegin][0] for enumerating credentials by the
249    /// SHA-256 hash of the relying party ID.
250    ///
251    /// [0]: CredSubCommand::EnumerateCredentialsBegin
252    #[inline]
253    pub fn enumerate_credentials_by_rpid(rp_id: &str) -> Self {
254        Self::EnumerateCredentialsBegin(compute_sha256(rp_id.as_bytes()))
255    }
256}
257
258/// Potentially-abridged form of [RelyingParty][] for credential management.
259///
260/// Per [CTAP 2.1 specification §6.8.3: Enumerating RPs][2]:
261///
262/// > `PublicKeyCredentialRpEntity`, where the `id` field *should* be included,
263/// > and other fields *may* be included. See
264/// > [§6.8.7 Truncation of relying party identifiers][0] about possible
265/// > truncation of the `id` field…
266///
267/// Most authenticators we've tested only store the `id` field. However, we've
268/// observed authenticators from the same vendor, supporting the same CTAP
269/// version sometimes *also* including the `name` and `icon` fields.
270///
271/// **Note:** [the `icon` field is deprecated][1], so this field is not
272/// supported by this library.
273///
274/// [RelyingParty]: webauthn_rs_proto::RelyingParty
275/// [0]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#rpid-truncation
276/// [1]: https://github.com/w3c/webauthn/pull/1337
277/// [2]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#enumeratingRPs
278#[derive(Debug, Default, Serialize, Clone, Deserialize, PartialEq, Eq)]
279#[serde(rename_all = "camelCase")]
280pub struct RelyingPartyCM {
281    /// The name of the relying party.
282    ///
283    /// This might be omitted by the authenticator to save storage space.
284    pub name: Option<String>,
285
286    /// The relying party ID, typically a domain name.
287    ///
288    /// This *should* be included by all authenticators, but the value
289    /// [may be truncated][0] (so [`hash`][Self::hash] might not be
290    /// `sha256(id)`).
291    ///
292    /// [0]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#rpid-truncation
293    pub id: Option<String>,
294
295    /// The SHA-256 hash of the [*untruncated*][0] [relying party ID][Self::id].
296    ///
297    /// [0]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#rpid-truncation
298    #[serde(skip)]
299    pub hash: Option<SHA256Hash>,
300}
301
302/// User entity
303///
304/// **Note:** [the `icon` field is deprecated][1], so this field is not
305/// supported by this library.
306///
307/// [1]: https://github.com/w3c/webauthn/pull/1337
308#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)]
309#[serde(rename_all = "camelCase")]
310pub struct UserCM {
311    /// The user ID.
312    #[serde(with = "serde_bytes")]
313    pub id: Vec<u8>,
314
315    /// A human-palatable identifier for the account, such as a username, email
316    /// address or phone number.
317    ///
318    /// This value **can** change, so **must not** be used as a primary key.
319    pub name: Option<String>,
320
321    /// Human-palatable display name for the user account.
322    ///
323    /// This value **can** change, so **must not** be used as a primary key.
324    pub display_name: Option<String>,
325}
326
327#[derive(Debug, Serialize, Clone, Deserialize, PartialEq, Eq)]
328#[serde(rename_all = "camelCase")]
329pub struct PublicKeyCredentialDescriptorCM {
330    /// The type of credential
331    #[serde(rename = "type")]
332    pub type_: String,
333    /// The credential id.
334    #[serde(with = "serde_bytes")]
335    pub id: Vec<u8>,
336}
337
338impl From<Vec<u8>> for PublicKeyCredentialDescriptorCM {
339    fn from(id: Vec<u8>) -> Self {
340        Self {
341            type_: "public-key".to_string(),
342            id,
343        }
344    }
345}
346
347/// `authenticatorCredentialManagement` response type.
348///
349/// References:
350/// * <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorCredentialManagement>
351/// * <https://fidoalliance.org/specs/fido2/vendor/CredentialManagementPrototype.pdf>
352#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
353#[serde(try_from = "BTreeMap<u32, Value>")]
354pub struct CredentialManagementResponse {
355    pub storage_metadata: Option<CredentialStorageMetadata>,
356    pub rp: Option<RelyingPartyCM>,
357    pub total_rps: Option<u32>,
358    pub discoverable_credential: DiscoverableCredential,
359    pub total_credentials: Option<u32>,
360}
361
362impl TryFrom<BTreeMap<u32, Value>> for CredentialManagementResponse {
363    type Error = &'static str;
364    fn try_from(mut raw: BTreeMap<u32, Value>) -> Result<Self, Self::Error> {
365        trace!(?raw);
366
367        // Parse the relying party field if present.
368        let mut rp: Option<RelyingPartyCM> = if let Some(v) = raw.remove(&0x03) {
369            Some(from_value(v).map_err(|e| {
370                error!("parsing rp: {e:?}");
371                "parsing rp"
372            })?)
373        } else {
374            None
375        };
376
377        // Parse the relying party ID hash field if present.
378        if let Some(rp_id_hash) = raw
379            .remove(&0x04)
380            .and_then(|v| value_to_vec_u8(v, "0x04"))
381            .and_then(|v| v.try_into().ok())
382        {
383            if let Some(rp) = &mut rp {
384                // Add the hash to the existing RelyingPartyCM
385                rp.hash = Some(rp_id_hash);
386            } else {
387                rp = Some(RelyingPartyCM {
388                    hash: Some(rp_id_hash),
389                    ..Default::default()
390                });
391            }
392        }
393
394        Ok(Self {
395            storage_metadata: CredentialStorageMetadata::try_from(&mut raw).ok(),
396            rp,
397            total_rps: raw.remove(&0x05).and_then(|v| value_to_u32(&v, "0x05")),
398            total_credentials: raw.remove(&0x09).and_then(|v| value_to_u32(&v, "0x09")),
399            discoverable_credential: DiscoverableCredential::try_from(&mut raw)?,
400        })
401    }
402}
403
404crate::deserialize_cbor!(CredentialManagementResponse);
405
406/// Discoverable credential storage metadata, returned by
407/// [`CredentialManagementAuthenticator::get_credentials_metadata()`][1].
408///
409/// [1]: crate::ctap2::ctap21_cred::CredentialManagementAuthenticator::get_credentials_metadata
410#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
411pub struct CredentialStorageMetadata {
412    /// Number of discoverable credentials present on the authenticator.
413    pub existing_resident_credentials_count: u32,
414
415    /// Estimated number of additional discoverable credentials which could be
416    /// created on this authenticator, assuming *minimally-sized* fields for all
417    /// requests (ie: errs high).
418    ///
419    /// This value may vary over time, depending on the size of individual
420    /// discoverable credentials, and the token's storage allocation strategy.
421    ///
422    /// ## CTAP compatibility
423    ///
424    /// On authenticators supporting CTAP 2.1 or later, a pessimistic estimate
425    /// (ie: presuming *maximally-sized* credentials) *might* be available
426    /// *without* authentication in
427    /// [`GetInfoResponse::remaining_discoverable_credentials`][0].
428    ///
429    /// [0]: super::GetInfoResponse::remaining_discoverable_credentials
430    pub max_possible_remaining_resident_credentials_count: u32,
431}
432
433impl TryFrom<&mut BTreeMap<u32, Value>> for CredentialStorageMetadata {
434    type Error = WebauthnCError;
435    fn try_from(raw: &mut BTreeMap<u32, Value>) -> Result<Self, Self::Error> {
436        Ok(Self {
437            existing_resident_credentials_count: raw
438                .remove(&0x01)
439                .and_then(|v| value_to_u32(&v, "0x01"))
440                .ok_or(WebauthnCError::MissingRequiredField)?,
441            max_possible_remaining_resident_credentials_count: raw
442                .remove(&0x02)
443                .and_then(|v| value_to_u32(&v, "0x02"))
444                .ok_or(WebauthnCError::MissingRequiredField)?,
445        })
446    }
447}
448
449#[derive(Deserialize, Debug, Default, PartialEq, Eq)]
450pub struct DiscoverableCredential {
451    pub user: Option<UserCM>,
452    pub credential_id: Option<PublicKeyCredentialDescriptorCM>,
453    pub public_key: Option<COSEKey>,
454    pub cred_protect: Option<CredentialProtectionPolicy>,
455    pub large_blob_key: Option<Vec<u8>>,
456}
457
458impl TryFrom<&mut BTreeMap<u32, Value>> for DiscoverableCredential {
459    type Error = &'static str;
460    fn try_from(raw: &mut BTreeMap<u32, Value>) -> Result<Self, Self::Error> {
461        Ok(Self {
462            user: if let Some(v) = raw.remove(&0x06) {
463                Some(from_value(v).map_err(|e| {
464                    error!("parsing user: {e:?}");
465                    "parsing user"
466                })?)
467            } else {
468                None
469            },
470            credential_id: if let Some(v) = raw.remove(&0x07) {
471                Some(from_value(v).map_err(|e| {
472                    error!("parsing credentialID: {e:?}");
473                    "parsing credentialID"
474                })?)
475            } else {
476                None
477            },
478            public_key: if let Some(v) = raw.remove(&0x08) {
479                // publicKey is a Map<u32, Value>, need to use
480                // `impl TryFrom<&Value> for COSEKey`
481                Some(COSEKey::try_from(&v).map_err(|e| {
482                    error!("parsing publicKey: {e:?}");
483                    "parsing publicKey"
484                })?)
485            } else {
486                None
487            },
488            cred_protect: raw
489                .remove(&0x0A)
490                .and_then(|v| value_to_u8(&v, "0x0A"))
491                .and_then(|v| CredentialProtectionPolicy::try_from(v).ok()),
492            large_blob_key: raw.remove(&0x0B).and_then(|v| value_to_vec_u8(v, "0x0B")),
493        })
494    }
495}
496
497cred_struct! {
498    /// CTAP 2.1 `authenticatorCredentialManagement` command (`0x0a`).
499    ///
500    /// [ref]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#authenticatorCredentialManagement
501    pub struct CredentialManagementRequest = 0x0a
502}
503
504cred_struct! {
505    /// CTAP 2.1-PRE prototype `authenticatorCredentialManagement` command (`0x41`).
506    ///
507    /// [ref]: https://fidoalliance.org/specs/fido2/vendor/CredentialManagementPrototype.pdf
508    pub struct PrototypeCredentialManagementRequest = 0x41
509}
510
511#[cfg(test)]
512mod test {
513    use webauthn_rs_core::proto::{COSEEC2Key, COSEKeyType, ECDSACurve};
514    use webauthn_rs_proto::COSEAlgorithm;
515
516    use super::*;
517    const PIN_UV_AUTH_PARAM: [u8; 32] = [
518        0x13, 0xd7, 0xf2, 0xe7, 0xa0, 0xb9, 0x61, 0x78, 0x39, 0xb3, 0xfb, 0xbf, 0xc2, 0xf9, 0x0c,
519        0xec, 0x5e, 0xc0, 0xc6, 0xc0, 0x41, 0x54, 0x72, 0x74, 0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e,
520        0xfa, 0x5d,
521    ];
522
523    #[test]
524    fn get_cred_metadata() {
525        let _ = tracing_subscriber::fmt::try_init();
526
527        const SUBCOMMAND: CredSubCommand = CredSubCommand::GetCredsMetadata;
528        assert_eq!(vec![0x01], SUBCOMMAND.prf());
529
530        let c =
531            CredentialManagementRequest::new(SUBCOMMAND, Some(2), Some(PIN_UV_AUTH_PARAM.to_vec()));
532
533        assert_eq!(
534            vec![
535                0x0a, 0xa3, 0x01, 0x01, 0x03, 0x02, 0x04, 0x58, 0x20, 0x13, 0xd7, 0xf2, 0xe7, 0xa0,
536                0xb9, 0x61, 0x78, 0x39, 0xb3, 0xfb, 0xbf, 0xc2, 0xf9, 0x0c, 0xec, 0x5e, 0xc0, 0xc6,
537                0xc0, 0x41, 0x54, 0x72, 0x74, 0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e, 0xfa, 0x5d
538            ],
539            c.cbor().expect("encode error")
540        );
541
542        let c = PrototypeCredentialManagementRequest::new(
543            SUBCOMMAND,
544            Some(1),
545            Some(PIN_UV_AUTH_PARAM.to_vec()),
546        );
547
548        assert_eq!(
549            vec![
550                0x41, 0xa3, 0x01, 0x01, 0x03, 0x01, 0x04, 0x58, 0x20, 0x13, 0xd7, 0xf2, 0xe7, 0xa0,
551                0xb9, 0x61, 0x78, 0x39, 0xb3, 0xfb, 0xbf, 0xc2, 0xf9, 0x0c, 0xec, 0x5e, 0xc0, 0xc6,
552                0xc0, 0x41, 0x54, 0x72, 0x74, 0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e, 0xfa, 0x5d
553            ],
554            c.cbor().expect("encode error")
555        );
556
557        let r = [0xa2, 0x01, 0x03, 0x02, 0x16];
558        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
559            .expect("Failed to decode message");
560
561        assert_eq!(
562            CredentialManagementResponse {
563                storage_metadata: Some(CredentialStorageMetadata {
564                    existing_resident_credentials_count: 3,
565                    max_possible_remaining_resident_credentials_count: 22,
566                }),
567                ..Default::default()
568            },
569            a
570        )
571    }
572
573    #[test]
574    fn enumerate_rps_begin() {
575        let _ = tracing_subscriber::fmt::try_init();
576
577        const SUBCOMMAND: CredSubCommand = CredSubCommand::EnumerateRPsBegin;
578        assert_eq!(vec![0x02], SUBCOMMAND.prf());
579
580        let c =
581            CredentialManagementRequest::new(SUBCOMMAND, Some(2), Some(PIN_UV_AUTH_PARAM.to_vec()));
582
583        assert_eq!(
584            vec![
585                0x0a, 0xa3, 0x01, 0x02, 0x03, 0x02, 0x04, 0x58, 0x20, 0x13, 0xd7, 0xf2, 0xe7, 0xa0,
586                0xb9, 0x61, 0x78, 0x39, 0xb3, 0xfb, 0xbf, 0xc2, 0xf9, 0x0c, 0xec, 0x5e, 0xc0, 0xc6,
587                0xc0, 0x41, 0x54, 0x72, 0x74, 0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e, 0xfa, 0x5d
588            ],
589            c.cbor().expect("encode error")
590        );
591
592        let c = PrototypeCredentialManagementRequest::new(
593            SUBCOMMAND,
594            Some(1),
595            Some(PIN_UV_AUTH_PARAM.to_vec()),
596        );
597
598        assert_eq!(
599            vec![
600                0x41, 0xa3, 0x01, 0x02, 0x03, 0x01, 0x04, 0x58, 0x20, 0x13, 0xd7, 0xf2, 0xe7, 0xa0,
601                0xb9, 0x61, 0x78, 0x39, 0xb3, 0xfb, 0xbf, 0xc2, 0xf9, 0x0c, 0xec, 0x5e, 0xc0, 0xc6,
602                0xc0, 0x41, 0x54, 0x72, 0x74, 0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e, 0xfa, 0x5d
603            ],
604            c.cbor().expect("encode error")
605        );
606
607        // Response with RP ID and hash only
608        let r = [
609            0xa3, 0x03, 0xa1, 0x62, 0x69, 0x64, 0x78, 0x18, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74,
610            0x68, 0x6e, 0x2e, 0x66, 0x69, 0x72, 0x73, 0x74, 0x79, 0x65, 0x61, 0x72, 0x2e, 0x69,
611            0x64, 0x2e, 0x61, 0x75, 0x04, 0x58, 0x20, 0x6a, 0xb9, 0xbb, 0xf0, 0xdf, 0x9a, 0x16,
612            0xf9, 0x1d, 0xbb, 0x33, 0xbb, 0xb1, 0x32, 0xfa, 0xf9, 0xd1, 0x7c, 0x78, 0x2c, 0x48,
613            0x26, 0xc6, 0xec, 0x70, 0xec, 0xee, 0x58, 0xd9, 0x7e, 0xf5, 0x2a, 0x05, 0x02,
614        ];
615        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
616            .expect("Failed to decode message");
617
618        assert_eq!(
619            CredentialManagementResponse {
620                rp: Some(RelyingPartyCM {
621                    name: None,
622                    id: Some("webauthn.firstyear.id.au".to_string()),
623                    hash: Some([
624                        0x6a, 0xb9, 0xbb, 0xf0, 0xdf, 0x9a, 0x16, 0xf9, 0x1d, 0xbb, 0x33, 0xbb,
625                        0xb1, 0x32, 0xfa, 0xf9, 0xd1, 0x7c, 0x78, 0x2c, 0x48, 0x26, 0xc6, 0xec,
626                        0x70, 0xec, 0xee, 0x58, 0xd9, 0x7e, 0xf5, 0x2a
627                    ]),
628                }),
629                total_rps: Some(2),
630                ..Default::default()
631            },
632            a
633        );
634
635        // Response with RP ID, name, icon (ignored) and hash
636        let r = [
637            0xa3, 0x03, 0xa3, 0x62, 0x69, 0x64, 0x78, 0x1e, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74,
638            0x68, 0x6e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x61, 0x7a, 0x75, 0x72, 0x65, 0x77, 0x65,
639            0x62, 0x73, 0x69, 0x74, 0x65, 0x73, 0x2e, 0x6e, 0x65, 0x74, 0x64, 0x69, 0x63, 0x6f,
640            0x6e, 0x78, 0x1e, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x65, 0x78, 0x61,
641            0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x72, 0x70, 0x49, 0x63, 0x6f,
642            0x6e, 0x2e, 0x70, 0x6e, 0x67, 0x64, 0x6e, 0x61, 0x6d, 0x65, 0x74, 0x57, 0x65, 0x62,
643            0x41, 0x75, 0x74, 0x68, 0x6e, 0x20, 0x54, 0x65, 0x73, 0x74, 0x20, 0x53, 0x65, 0x72,
644            0x76, 0x65, 0x72, 0x04, 0x58, 0x20, 0xe4, 0x53, 0x29, 0xd0, 0x3a, 0x20, 0x68, 0xd1,
645            0xca, 0xf7, 0xf7, 0xbb, 0x0a, 0xe9, 0x54, 0xe6, 0xb0, 0xe6, 0x25, 0x97, 0x45, 0xf3,
646            0x2f, 0x48, 0x29, 0xf7, 0x50, 0xf0, 0x50, 0x11, 0xf9, 0xc2, 0x05, 0x03,
647        ];
648        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
649            .expect("Failed to decode message");
650
651        assert_eq!(
652            CredentialManagementResponse {
653                rp: Some(RelyingPartyCM {
654                    name: Some("WebAuthn Test Server".to_string()),
655                    id: Some("webauthntest.azurewebsites.net".to_string()),
656                    hash: Some([
657                        0xe4, 0x53, 0x29, 0xd0, 0x3a, 0x20, 0x68, 0xd1, 0xca, 0xf7, 0xf7, 0xbb,
658                        0x0a, 0xe9, 0x54, 0xe6, 0xb0, 0xe6, 0x25, 0x97, 0x45, 0xf3, 0x2f, 0x48,
659                        0x29, 0xf7, 0x50, 0xf0, 0x50, 0x11, 0xf9, 0xc2
660                    ]),
661                }),
662                total_rps: Some(3),
663                ..Default::default()
664            },
665            a
666        );
667
668        // Feitian keys with zero credentials give an empty CredentialManagementResponse
669        let r = [];
670        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
671            .expect("Failed to decode message");
672        assert_eq!(
673            CredentialManagementResponse {
674                total_rps: None,
675                ..Default::default()
676            },
677            a
678        );
679
680        // Yubikey 5 with zero credentials gives an explicit zero
681        let r = [0xa1, 0x05, 0x00];
682        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
683            .expect("Failed to decode message");
684
685        assert_eq!(
686            CredentialManagementResponse {
687                total_rps: Some(0),
688                ..Default::default()
689            },
690            a
691        );
692    }
693
694    #[test]
695    fn enumerate_rps_next() {
696        let _ = tracing_subscriber::fmt::try_init();
697
698        assert_eq!(
699            vec![0x0a, 0xa1, 0x01, 0x03],
700            CredentialManagementRequest::ENUMERATE_RPS_GET_NEXT
701                .cbor()
702                .expect("encode error")
703        );
704
705        assert_eq!(
706            vec![0x41, 0xa1, 0x01, 0x03],
707            PrototypeCredentialManagementRequest::ENUMERATE_RPS_GET_NEXT
708                .cbor()
709                .expect("encode error")
710        );
711
712        let r = [
713            0xa2, 0x03, 0xa1, 0x62, 0x69, 0x64, 0x78, 0x21, 0x77, 0x65, 0x62, 0x61, 0x75, 0x74,
714            0x68, 0x6e, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74,
715            0x79, 0x73, 0x74, 0x61, 0x6e, 0x64, 0x61, 0x72, 0x64, 0x73, 0x2e, 0x69, 0x6f, 0x04,
716            0x58, 0x20, 0x0b, 0x99, 0x7c, 0xcc, 0xeb, 0x3a, 0xeb, 0x29, 0xc5, 0x5c, 0x94, 0xa8,
717            0x94, 0xb1, 0x1c, 0xf0, 0x1a, 0x24, 0xb4, 0xc8, 0xae, 0x70, 0x6f, 0x32, 0x8c, 0xc2,
718            0xea, 0x8c, 0xeb, 0xc4, 0xad, 0x5c,
719        ];
720        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
721            .expect("Failed to decode message");
722
723        assert_eq!(
724            CredentialManagementResponse {
725                rp: Some(RelyingPartyCM {
726                    name: None,
727                    id: Some("webauthntest.identitystandards.io".to_string()),
728                    hash: Some([
729                        0x0b, 0x99, 0x7c, 0xcc, 0xeb, 0x3a, 0xeb, 0x29, 0xc5, 0x5c, 0x94, 0xa8,
730                        0x94, 0xb1, 0x1c, 0xf0, 0x1a, 0x24, 0xb4, 0xc8, 0xae, 0x70, 0x6f, 0x32,
731                        0x8c, 0xc2, 0xea, 0x8c, 0xeb, 0xc4, 0xad, 0x5c
732                    ]),
733                }),
734                ..Default::default()
735            },
736            a
737        );
738    }
739
740    #[test]
741    fn enumerate_credentials_begin() {
742        let _ = tracing_subscriber::fmt::try_init();
743        const SUBCOMMAND: CredSubCommand = CredSubCommand::EnumerateCredentialsBegin([
744            0x0b, 0x99, 0x7c, 0xcc, 0xeb, 0x3a, 0xeb, 0x29, 0xc5, 0x5c, 0x94, 0xa8, 0x94, 0xb1,
745            0x1c, 0xf0, 0x1a, 0x24, 0xb4, 0xc8, 0xae, 0x70, 0x6f, 0x32, 0x8c, 0xc2, 0xea, 0x8c,
746            0xeb, 0xc4, 0xad, 0x5c,
747        ]);
748
749        assert_eq!(
750            vec![
751                0x04, 0xa1, 0x01, 0x58, 0x20, 0x0b, 0x99, 0x7c, 0xcc, 0xeb, 0x3a, 0xeb, 0x29, 0xc5,
752                0x5c, 0x94, 0xa8, 0x94, 0xb1, 0x1c, 0xf0, 0x1a, 0x24, 0xb4, 0xc8, 0xae, 0x70, 0x6f,
753                0x32, 0x8c, 0xc2, 0xea, 0x8c, 0xeb, 0xc4, 0xad, 0x5c
754            ],
755            SUBCOMMAND.prf()
756        );
757
758        assert_eq!(
759            CredSubCommand::enumerate_credentials_by_rpid("webauthntest.identitystandards.io"),
760            SUBCOMMAND
761        );
762
763        let c =
764            CredentialManagementRequest::new(SUBCOMMAND, Some(2), Some(PIN_UV_AUTH_PARAM.to_vec()));
765
766        assert_eq!(
767            vec![
768                0x0a, 0xa4, 0x01, 0x04, 0x02, 0xa1, 0x01, 0x58, 0x20, 0x0b, 0x99, 0x7c, 0xcc, 0xeb,
769                0x3a, 0xeb, 0x29, 0xc5, 0x5c, 0x94, 0xa8, 0x94, 0xb1, 0x1c, 0xf0, 0x1a, 0x24, 0xb4,
770                0xc8, 0xae, 0x70, 0x6f, 0x32, 0x8c, 0xc2, 0xea, 0x8c, 0xeb, 0xc4, 0xad, 0x5c, 0x03,
771                0x02, 0x04, 0x58, 0x20, 0x13, 0xd7, 0xf2, 0xe7, 0xa0, 0xb9, 0x61, 0x78, 0x39, 0xb3,
772                0xfb, 0xbf, 0xc2, 0xf9, 0x0c, 0xec, 0x5e, 0xc0, 0xc6, 0xc0, 0x41, 0x54, 0x72, 0x74,
773                0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e, 0xfa, 0x5d
774            ],
775            c.cbor().expect("encode error")
776        );
777
778        let c = PrototypeCredentialManagementRequest::new(
779            SUBCOMMAND,
780            Some(1),
781            Some(PIN_UV_AUTH_PARAM.to_vec()),
782        );
783
784        assert_eq!(
785            vec![
786                0x41, 0xa4, 0x01, 0x04, 0x02, 0xa1, 0x01, 0x58, 0x20, 0x0b, 0x99, 0x7c, 0xcc, 0xeb,
787                0x3a, 0xeb, 0x29, 0xc5, 0x5c, 0x94, 0xa8, 0x94, 0xb1, 0x1c, 0xf0, 0x1a, 0x24, 0xb4,
788                0xc8, 0xae, 0x70, 0x6f, 0x32, 0x8c, 0xc2, 0xea, 0x8c, 0xeb, 0xc4, 0xad, 0x5c, 0x03,
789                0x01, 0x04, 0x58, 0x20, 0x13, 0xd7, 0xf2, 0xe7, 0xa0, 0xb9, 0x61, 0x78, 0x39, 0xb3,
790                0xfb, 0xbf, 0xc2, 0xf9, 0x0c, 0xec, 0x5e, 0xc0, 0xc6, 0xc0, 0x41, 0x54, 0x72, 0x74,
791                0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e, 0xfa, 0x5d
792            ],
793            c.cbor().expect("encode error")
794        );
795
796        let r = [
797            0xa6, 0x06, 0xa3, 0x62, 0x69, 0x64, 0x51, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x40, 0x65,
798            0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x64, 0x6e, 0x61, 0x6d,
799            0x65, 0x73, 0x61, 0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e, 0x40, 0x65, 0x78, 0x61, 0x6d,
800            0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61,
801            0x79, 0x4e, 0x61, 0x6d, 0x65, 0x6b, 0x41, 0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e, 0x20,
802            0x44, 0x6f, 0x65, 0x07, 0xa2, 0x62, 0x69, 0x64, 0x50, 0x39, 0x24, 0xdb, 0xf7, 0xba,
803            0x5d, 0xe8, 0x82, 0x9c, 0x69, 0xee, 0x10, 0x15, 0x89, 0x76, 0xf1, 0x64, 0x74, 0x79,
804            0x70, 0x65, 0x6a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x6b, 0x65, 0x79, 0x08,
805            0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0x6f, 0xd0, 0x2c, 0xd1,
806            0x31, 0x25, 0x1d, 0x12, 0xda, 0x03, 0x8a, 0x1b, 0xd2, 0xdb, 0xb6, 0x47, 0xe2, 0x45,
807            0x4d, 0x71, 0x47, 0x7a, 0xd3, 0x1d, 0xbd, 0x7c, 0xdb, 0x15, 0xd2, 0x8d, 0xf8, 0xbf,
808            0x22, 0x58, 0x20, 0x04, 0x42, 0x2c, 0xca, 0xa2, 0x61, 0xc1, 0x97, 0x92, 0x1a, 0xab,
809            0xad, 0xa5, 0x57, 0x8e, 0x91, 0x55, 0xde, 0x56, 0xc4, 0xca, 0xd9, 0x1d, 0x8d, 0xd0,
810            0x7e, 0xe3, 0x78, 0x71, 0xf9, 0xf1, 0xf5, 0x09, 0x02, 0x0a, 0x01, 0x0b, 0x58, 0x20,
811            0xa3, 0x81, 0x0f, 0xaf, 0xc1, 0x2a, 0xe8, 0xa3, 0xf5, 0xdd, 0x47, 0x21, 0x2f, 0xf8,
812            0x2a, 0x30, 0xd8, 0x2a, 0xd9, 0xf8, 0x6e, 0xdf, 0x06, 0x34, 0x71, 0x76, 0x5c, 0x85,
813            0x3a, 0xa8, 0x0a, 0xe6,
814        ];
815        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
816            .expect("Failed to decode message");
817
818        assert_eq!(
819            CredentialManagementResponse {
820                discoverable_credential: DiscoverableCredential {
821                    user: Some(UserCM {
822                        id: b"alice@example.com".to_vec(),
823                        name: Some("allison@example.com".to_string()),
824                        display_name: Some("Allison Doe".to_string()),
825                    }),
826                    credential_id: Some(
827                        vec![
828                            0x39, 0x24, 0xdb, 0xf7, 0xba, 0x5d, 0xe8, 0x82, 0x9c, 0x69, 0xee, 0x10,
829                            0x15, 0x89, 0x76, 0xf1
830                        ]
831                        .into()
832                    ),
833                    public_key: Some(COSEKey {
834                        type_: COSEAlgorithm::ES256,
835                        key: COSEKeyType::EC_EC2(COSEEC2Key {
836                            curve: ECDSACurve::SECP256R1,
837                            x: vec![
838                                0x6f, 0xd0, 0x2c, 0xd1, 0x31, 0x25, 0x1d, 0x12, 0xda, 0x03, 0x8a,
839                                0x1b, 0xd2, 0xdb, 0xb6, 0x47, 0xe2, 0x45, 0x4d, 0x71, 0x47, 0x7a,
840                                0xd3, 0x1d, 0xbd, 0x7c, 0xdb, 0x15, 0xd2, 0x8d, 0xf8, 0xbf
841                            ]
842                            .into(),
843                            y: vec![
844                                0x04, 0x42, 0x2c, 0xca, 0xa2, 0x61, 0xc1, 0x97, 0x92, 0x1a, 0xab,
845                                0xad, 0xa5, 0x57, 0x8e, 0x91, 0x55, 0xde, 0x56, 0xc4, 0xca, 0xd9,
846                                0x1d, 0x8d, 0xd0, 0x7e, 0xe3, 0x78, 0x71, 0xf9, 0xf1, 0xf5
847                            ]
848                            .into()
849                        })
850                    }),
851                    cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
852                    large_blob_key: Some(vec![
853                        0xa3, 0x81, 0x0f, 0xaf, 0xc1, 0x2a, 0xe8, 0xa3, 0xf5, 0xdd, 0x47, 0x21,
854                        0x2f, 0xf8, 0x2a, 0x30, 0xd8, 0x2a, 0xd9, 0xf8, 0x6e, 0xdf, 0x06, 0x34,
855                        0x71, 0x76, 0x5c, 0x85, 0x3a, 0xa8, 0x0a, 0xe6
856                    ]),
857                },
858                total_credentials: Some(2),
859                ..Default::default()
860            },
861            a
862        );
863    }
864
865    #[test]
866    fn enumerate_credentials_next() {
867        let _ = tracing_subscriber::fmt::try_init();
868
869        assert_eq!(
870            vec![0x0a, 0xa1, 0x01, 0x05],
871            CredentialManagementRequest::ENUMERATE_CREDENTIALS_GET_NEXT
872                .cbor()
873                .expect("encode error")
874        );
875
876        assert_eq!(
877            vec![0x41, 0xa1, 0x01, 0x05],
878            PrototypeCredentialManagementRequest::ENUMERATE_CREDENTIALS_GET_NEXT
879                .cbor()
880                .expect("encode error")
881        );
882
883        let r = [
884            0xa5, 0x06, 0xa3, 0x62, 0x69, 0x64, 0x51, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x40, 0x65,
885            0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x64, 0x6e, 0x61, 0x6d,
886            0x65, 0x73, 0x61, 0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e, 0x40, 0x65, 0x78, 0x61, 0x6d,
887            0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61,
888            0x79, 0x4e, 0x61, 0x6d, 0x65, 0x6b, 0x41, 0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e, 0x20,
889            0x44, 0x6f, 0x65, 0x07, 0xa2, 0x62, 0x69, 0x64, 0x50, 0x39, 0x24, 0xdb, 0xf7, 0xba,
890            0x5d, 0xe8, 0x82, 0x9c, 0x69, 0xee, 0x10, 0x15, 0x89, 0x76, 0xf1, 0x64, 0x74, 0x79,
891            0x70, 0x65, 0x6a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x6b, 0x65, 0x79, 0x08,
892            0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20, 0x6f, 0xd0, 0x2c, 0xd1,
893            0x31, 0x25, 0x1d, 0x12, 0xda, 0x03, 0x8a, 0x1b, 0xd2, 0xdb, 0xb6, 0x47, 0xe2, 0x45,
894            0x4d, 0x71, 0x47, 0x7a, 0xd3, 0x1d, 0xbd, 0x7c, 0xdb, 0x15, 0xd2, 0x8d, 0xf8, 0xbf,
895            0x22, 0x58, 0x20, 0x04, 0x42, 0x2c, 0xca, 0xa2, 0x61, 0xc1, 0x97, 0x92, 0x1a, 0xab,
896            0xad, 0xa5, 0x57, 0x8e, 0x91, 0x55, 0xde, 0x56, 0xc4, 0xca, 0xd9, 0x1d, 0x8d, 0xd0,
897            0x7e, 0xe3, 0x78, 0x71, 0xf9, 0xf1, 0xf5, 0x0a, 0x01, 0x0b, 0x58, 0x20, 0xa3, 0x81,
898            0x0f, 0xaf, 0xc1, 0x2a, 0xe8, 0xa3, 0xf5, 0xdd, 0x47, 0x21, 0x2f, 0xf8, 0x2a, 0x30,
899            0xd8, 0x2a, 0xd9, 0xf8, 0x6e, 0xdf, 0x06, 0x34, 0x71, 0x76, 0x5c, 0x85, 0x3a, 0xa8,
900            0x0a, 0xe6,
901        ];
902        let a = <CredentialManagementResponse as CBORResponse>::try_from(r.as_slice())
903            .expect("Failed to decode message");
904
905        assert_eq!(
906            CredentialManagementResponse {
907                discoverable_credential: DiscoverableCredential {
908                    user: Some(UserCM {
909                        id: b"alice@example.com".to_vec(),
910                        name: Some("allison@example.com".to_string()),
911                        display_name: Some("Allison Doe".to_string()),
912                    }),
913                    credential_id: Some(
914                        vec![
915                            0x39, 0x24, 0xdb, 0xf7, 0xba, 0x5d, 0xe8, 0x82, 0x9c, 0x69, 0xee, 0x10,
916                            0x15, 0x89, 0x76, 0xf1
917                        ]
918                        .into()
919                    ),
920                    public_key: Some(COSEKey {
921                        type_: COSEAlgorithm::ES256,
922                        key: COSEKeyType::EC_EC2(COSEEC2Key {
923                            curve: ECDSACurve::SECP256R1,
924                            x: vec![
925                                0x6f, 0xd0, 0x2c, 0xd1, 0x31, 0x25, 0x1d, 0x12, 0xda, 0x03, 0x8a,
926                                0x1b, 0xd2, 0xdb, 0xb6, 0x47, 0xe2, 0x45, 0x4d, 0x71, 0x47, 0x7a,
927                                0xd3, 0x1d, 0xbd, 0x7c, 0xdb, 0x15, 0xd2, 0x8d, 0xf8, 0xbf
928                            ]
929                            .into(),
930                            y: vec![
931                                0x04, 0x42, 0x2c, 0xca, 0xa2, 0x61, 0xc1, 0x97, 0x92, 0x1a, 0xab,
932                                0xad, 0xa5, 0x57, 0x8e, 0x91, 0x55, 0xde, 0x56, 0xc4, 0xca, 0xd9,
933                                0x1d, 0x8d, 0xd0, 0x7e, 0xe3, 0x78, 0x71, 0xf9, 0xf1, 0xf5
934                            ]
935                            .into()
936                        })
937                    }),
938                    cred_protect: Some(CredentialProtectionPolicy::UserVerificationOptional),
939                    large_blob_key: Some(vec![
940                        0xa3, 0x81, 0x0f, 0xaf, 0xc1, 0x2a, 0xe8, 0xa3, 0xf5, 0xdd, 0x47, 0x21,
941                        0x2f, 0xf8, 0x2a, 0x30, 0xd8, 0x2a, 0xd9, 0xf8, 0x6e, 0xdf, 0x06, 0x34,
942                        0x71, 0x76, 0x5c, 0x85, 0x3a, 0xa8, 0x0a, 0xe6
943                    ]),
944                },
945                ..Default::default()
946            },
947            a
948        );
949    }
950
951    #[test]
952    fn update_user_information() {
953        let _ = tracing_subscriber::fmt::try_init();
954
955        let s = CredSubCommand::UpdateUserInformation(
956            vec![
957                0x39, 0x24, 0xdb, 0xf7, 0xba, 0x5d, 0xe8, 0x82, 0x9c, 0x69, 0xee, 0x10, 0x15, 0x89,
958                0x76, 0xf1,
959            ]
960            .into(),
961            UserCM {
962                id: b"alice@example.com".to_vec(),
963                name: Some("allison@example.com".to_string()),
964                display_name: Some("Allison Doe".to_string()),
965            },
966        );
967        assert_eq!(
968            vec![
969                0x07, 0xa2, 0x02, 0xa2, 0x62, 0x69, 0x64, 0x50, 0x39, 0x24, 0xdb, 0xf7, 0xba, 0x5d,
970                0xe8, 0x82, 0x9c, 0x69, 0xee, 0x10, 0x15, 0x89, 0x76, 0xf1, 0x64, 0x74, 0x79, 0x70,
971                0x65, 0x6a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x6b, 0x65, 0x79, 0x03, 0xa3,
972                0x62, 0x69, 0x64, 0x51, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x40, 0x65, 0x78, 0x61, 0x6d,
973                0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x64, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x61,
974                0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e, 0x40, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
975                0x2e, 0x63, 0x6f, 0x6d, 0x6b, 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x4e, 0x61,
976                0x6d, 0x65, 0x6b, 0x41, 0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e, 0x20, 0x44, 0x6f, 0x65
977            ],
978            s.prf()
979        );
980
981        // UpdateUserInformation only supported in CTAP 2.1 (CredentialManagementRequest)
982        let c = CredentialManagementRequest::new(s, Some(2), Some(PIN_UV_AUTH_PARAM.into()));
983
984        assert_eq!(
985            vec![
986                0x0a, 0xa4, 0x01, 0x07, 0x02, 0xa2, 0x02, 0xa2, 0x62, 0x69, 0x64, 0x50, 0x39, 0x24,
987                0xdb, 0xf7, 0xba, 0x5d, 0xe8, 0x82, 0x9c, 0x69, 0xee, 0x10, 0x15, 0x89, 0x76, 0xf1,
988                0x64, 0x74, 0x79, 0x70, 0x65, 0x6a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x2d, 0x6b,
989                0x65, 0x79, 0x03, 0xa3, 0x62, 0x69, 0x64, 0x51, 0x61, 0x6c, 0x69, 0x63, 0x65, 0x40,
990                0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x64, 0x6e, 0x61,
991                0x6d, 0x65, 0x73, 0x61, 0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e, 0x40, 0x65, 0x78, 0x61,
992                0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6b, 0x64, 0x69, 0x73, 0x70, 0x6c,
993                0x61, 0x79, 0x4e, 0x61, 0x6d, 0x65, 0x6b, 0x41, 0x6c, 0x6c, 0x69, 0x73, 0x6f, 0x6e,
994                0x20, 0x44, 0x6f, 0x65, 0x03, 0x02, 0x04, 0x58, 0x20, 0x13, 0xd7, 0xf2, 0xe7, 0xa0,
995                0xb9, 0x61, 0x78, 0x39, 0xb3, 0xfb, 0xbf, 0xc2, 0xf9, 0x0c, 0xec, 0x5e, 0xc0, 0xc6,
996                0xc0, 0x41, 0x54, 0x72, 0x74, 0x22, 0x56, 0xac, 0x6e, 0xd7, 0x9e, 0xfa, 0x5d,
997            ],
998            c.cbor().expect("encode error")
999        );
1000    }
1001}