passkey_types/ctap2/
attestation_fmt.rs

1use std::{
2    io::{Cursor, Read},
3    num::TryFromIntError,
4};
5
6use ciborium::value::Value;
7use coset::{AsCborValue, CborSerializable, CoseKey};
8use serde::{Deserialize, Serialize};
9
10use crate::{
11    crypto::sha256,
12    ctap2::{Aaguid, Flags},
13};
14
15use super::{get_assertion, make_credential, Ctap2Error};
16
17/// The authenticator data structure encodes contextual bindings made by the authenticator. These
18/// bindings are controlled by the authenticator itself, and derive their trust from the WebAuthn
19/// Relying Party's assessment of the security properties of the authenticator. In one extreme case,
20/// the authenticator may be embedded in the client, and its bindings may be no more trustworthy
21/// than the client data. At the other extreme, the authenticator may be a discrete entity with
22/// high-security hardware and software, connected to the client over a secure channel. In both
23/// cases, the Relying Party receives the authenticator data in the same format, and uses its
24/// knowledge of the authenticator to make trust decisions.
25///
26/// <https://w3c.github.io/webauthn/#sctn-authenticator-data>
27#[derive(Debug, PartialEq)]
28pub struct AuthenticatorData {
29    /// SHA-256 hash of the RP ID the credential is scoped to.
30    rp_id_hash: [u8; 32],
31
32    /// The flags representing the information of this credential. See [Flags] for more information.
33    pub flags: Flags,
34
35    /// Signature counter, 32-bit unsigned big-endian integer.
36    pub counter: Option<u32>,
37
38    /// An optional [AttestedCredentialData], if present, the [Flags::AT] needs to be set to true.
39    /// See [AttestedCredentialData] for more information. Its length depends on the length of the
40    /// credential ID and credential public key being attested.
41    pub attested_credential_data: Option<AttestedCredentialData>,
42
43    /// Extension-defined authenticator data. This is a CBOR [RFC8949] map with extension identifiers
44    /// as keys, and authenticator extension outputs as values. See [WebAuthn Extensions] for details.
45    ///
46    /// This field uses the generic `Value` rather than a HashMap or the internal map representation for the
47    /// following reasons:
48    /// 1. `Value` does not implement `Hash` so it can't be used as a key in a `HashMap`
49    /// 2. Even if `Vec<(Value, Value)>` is the internal representation of a map in `Value`, it
50    ///    serializes to an array rather than a map, so in order to serialize it needs to be cloned
51    ///    into a `Value::Map`.
52    ///
53    /// Instead we just assert that it is a map during deserialization.
54    ///
55    /// [RFC8949]: https://www.rfc-editor.org/rfc/rfc8949.html
56    /// [WebAuthn Extensions]: https://w3c.github.io/webauthn/#sctn-extensions
57    pub extensions: Option<Value>,
58}
59
60impl AuthenticatorData {
61    /// Create a new AuthenticatorData object for an RP ID and an optional counter.
62    ///
63    /// The flags will be set to their default values.
64    pub fn new(rp_id: &str, counter: Option<u32>) -> Self {
65        Self {
66            rp_id_hash: sha256(rp_id.as_bytes()),
67            flags: Flags::default(),
68            counter,
69            attested_credential_data: None,
70            extensions: None,
71        }
72    }
73
74    /// Add an [`AttestedCredentialData`] to the authenticator data.
75    ///
76    /// This sets the [`Flags::AT`] value as well.
77    pub fn set_attested_credential_data(mut self, acd: AttestedCredentialData) -> Self {
78        self.attested_credential_data = Some(acd);
79        self.set_flags(Flags::AT)
80    }
81
82    /// Set additional [`Flags`] to the authenticator data.
83    pub fn set_flags(mut self, flags: Flags) -> Self {
84        self.flags |= flags;
85        self
86    }
87
88    /// Get read access to the RP ID hash
89    pub fn rp_id_hash(&self) -> &[u8] {
90        &self.rp_id_hash
91    }
92
93    /// Set make credential authenticator extensions
94    pub fn set_make_credential_extensions(
95        mut self,
96        extensions: Option<make_credential::SignedExtensionOutputs>,
97    ) -> Result<Self, Ctap2Error> {
98        let Some(ext) = extensions.and_then(|e| e.zip_contents()) else {
99            return Ok(self);
100        };
101
102        self.extensions =
103            Some(Value::serialized(&ext).map_err(|_| Ctap2Error::CborUnexpectedType)?);
104
105        Ok(self.set_flags(Flags::ED))
106    }
107
108    /// Set assertion authenticator extensions
109    pub fn set_assertion_extensions(
110        mut self,
111        extensions: Option<get_assertion::SignedExtensionOutputs>,
112    ) -> Result<Self, Ctap2Error> {
113        let Some(ext) = extensions.and_then(|e| e.zip_contents()) else {
114            return Ok(self);
115        };
116
117        self.extensions =
118            Some(Value::serialized(&ext).map_err(|_| Ctap2Error::CborUnexpectedType)?);
119
120        Ok(self.set_flags(Flags::ED))
121    }
122}
123
124impl Serialize for AuthenticatorData {
125    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
126    where
127        S: serde::Serializer,
128    {
129        let bytes = self.to_vec();
130        serializer.serialize_bytes(&bytes)
131    }
132}
133
134impl<'de> Deserialize<'de> for AuthenticatorData {
135    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136    where
137        D: serde::Deserializer<'de>,
138    {
139        struct Visitor;
140        impl serde::de::Visitor<'_> for Visitor {
141            type Value = AuthenticatorData;
142
143            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
144                formatter.write_str("Authenticator Data")
145            }
146            fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
147            where
148                E: serde::de::Error,
149            {
150                AuthenticatorData::from_slice(v).map_err(|e| E::custom(e.to_string()))
151            }
152        }
153        deserializer.deserialize_bytes(Visitor)
154    }
155}
156
157/// Because CoseError does not implement `From` for either `ciborium::de::Error<E>` or `std::io::Error`...
158fn io_error<E>(_: E) -> coset::CoseError {
159    coset::CoseError::DecodeFailed(ciborium::de::Error::Io(coset::EndOfFile))
160}
161
162impl AuthenticatorData {
163    /// Decode an Authenticator data from a byte slice
164    pub fn from_slice(v: &[u8]) -> coset::Result<Self> {
165        // hash len (32 bytes) + flags (1 byte) + counter (4 bytes)
166        if v.len() < 37 {
167            return Err(io_error(()));
168        }
169
170        // SAFETY: split at panics if the param is creater than the length. These are safe due to
171        // guard above.
172        let (rp_id_hash, v) = v.split_at(32);
173        let (flag_byte, v) = v.split_at(1);
174        let (counter, v) = v.split_at(4);
175
176        let flags =
177            Flags::from_bits(flag_byte[0]).ok_or(coset::CoseError::OutOfRangeIntegerValue)?;
178        let mut managed_reader = Cursor::new(v);
179        let attested_credential_data = flags
180            .contains(Flags::AT)
181            .then(|| AttestedCredentialData::from_reader(&mut managed_reader))
182            .transpose()?;
183        let extensions = flags
184            .contains(Flags::ED)
185            .then(|| ciborium::de::from_reader(&mut managed_reader).map_err(io_error))
186            .transpose()?;
187
188        // SAFETY: These unwraps are safe since these variables are created using `split_at` which
189        // creates slices of specific size.
190        Ok(AuthenticatorData {
191            rp_id_hash: rp_id_hash.try_into().unwrap(),
192            flags,
193            counter: Some(u32::from_be_bytes(counter.try_into().unwrap())),
194            attested_credential_data,
195            extensions,
196        })
197    }
198
199    /// Encode an authenticator data to its byte representation.
200    pub fn to_vec(&self) -> Vec<u8> {
201        let flags = if self.attested_credential_data.is_some() {
202            self.flags | Flags::AT
203        } else {
204            self.flags
205        };
206
207        self.rp_id_hash
208            .into_iter()
209            .chain(std::iter::once(flags.into()))
210            .chain(self.counter.unwrap_or_default().to_be_bytes())
211            .chain(
212                self.attested_credential_data
213                    .clone()
214                    .map(AttestedCredentialData::into_iter)
215                    .into_iter()
216                    .flatten(),
217            )
218            .chain(
219                self.extensions
220                    .as_ref()
221                    .map(|val| {
222                        let mut bytes = Vec::new();
223                        ciborium::ser::into_writer(val, &mut bytes).unwrap();
224                        bytes
225                    })
226                    .into_iter()
227                    .flatten(),
228            )
229            .collect()
230    }
231}
232
233/// Attested credential data is a variable-length byte array added to the authenticator data when
234/// generating an attestation object for a credential
235///
236/// <https://w3c.github.io/webauthn/#attested-credential-data>
237#[derive(Debug, Clone, PartialEq)]
238pub struct AttestedCredentialData {
239    /// The AAGUID of the authenticator.
240    pub aaguid: Aaguid,
241
242    /// The credential ID whose length is prepended to the byte array. This is not public as it
243    /// should not be modifiable to be longer than a u16.
244    credential_id: Vec<u8>,
245
246    /// The credential public key encoded in COSE_Key format, as defined in Section 7 of [RFC9052],
247    /// using the CTAP2 canonical CBOR encoding form. The COSE_Key-encoded credential public key
248    /// MUST contain the "alg" parameter and MUST NOT contain any other OPTIONAL parameters.
249    /// The "alg" parameter MUST contain a [coset::iana::Algorithm] value. The encoded credential
250    /// public key MUST also contain any additional REQUIRED parameters stipulated by the relevant
251    /// key type specification, i.e. REQUIRED for the key type "kty" and algorithm "alg"
252    /// (see Section 2 of [RFC9053]).
253    ///
254    /// [RFC9052]: https://www.rfc-editor.org/rfc/rfc9052
255    /// [RFC9053]: https://www.rfc-editor.org/rfc/rfc9053
256    pub key: CoseKey,
257}
258
259impl AttestedCredentialData {
260    /// Create a new [AttestedCredentialData]
261    ///
262    /// # Error
263    /// Returns an error if the length of `credential_id` cannot be represented by a u16.
264    pub fn new(
265        aaguid: Aaguid,
266        credential_id: Vec<u8>,
267        key: CoseKey,
268    ) -> Result<Self, TryFromIntError> {
269        // assert that the credential id's length can be represented by a u16
270        u16::try_from(credential_id.len())?;
271
272        Ok(Self {
273            aaguid,
274            credential_id,
275            key,
276        })
277    }
278
279    /// Get read access to the credential ID,
280    pub fn credential_id(&self) -> &[u8] {
281        &self.credential_id
282    }
283}
284
285impl AttestedCredentialData {
286    /// Custom implementation rather than IntoIterator because the iterator type is complicated.
287    fn into_iter(self) -> impl Iterator<Item = u8> {
288        // SAFETY: if this unwrap fails, it is programmer error
289        // unfortunately any serialization in Coset does not use serde::Serialize and takes by value ...
290        let cose_key = self.key.to_vec().unwrap();
291        self.aaguid
292            .0
293            .into_iter()
294            // SAFETY: the length has been asserted to be less than u16::MAX in the constructor.
295            .chain(
296                u16::try_from(self.credential_id.len())
297                    .unwrap()
298                    .to_be_bytes(),
299            )
300            .chain(self.credential_id)
301            .chain(cose_key)
302    }
303
304    fn from_reader<R: Read>(reader: &mut R) -> coset::Result<Self> {
305        let mut aaguid = [0; 16];
306        reader.read_exact(&mut aaguid).map_err(io_error)?;
307        let aaguid = Aaguid(aaguid);
308
309        let mut cred_len = [0; 2];
310        reader.read_exact(&mut cred_len).map_err(io_error)?;
311        let cred_len: usize = u16::from_be_bytes(cred_len).into();
312
313        let mut credential_id = vec![0; cred_len];
314        reader.read_exact(&mut credential_id).map_err(io_error)?;
315
316        let cose_val = ciborium::de::from_reader(reader).map_err(io_error)?;
317        let key = CoseKey::from_cbor_value(cose_val)?;
318
319        Ok(Self {
320            aaguid,
321            credential_id,
322            key,
323        })
324    }
325}
326
327#[cfg(test)]
328mod test {
329    use ciborium::cbor;
330    use coset::CoseKeyBuilder;
331
332    use super::*;
333    use crate::utils::rand::random_vec;
334
335    #[test]
336    fn deserialize_authenticator_data_with_at_and_ed() {
337        // This is authenticator data extracted from a yubikey version 5
338        let data = [
339            0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20,
340            0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0x0b,
341            0x60, 0x84, 0x1e, 0xf0, 0xc5, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
342            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x0c,
343            0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c, 0xbf, 0x5e, 0x4c,
344            0x14, 0x04, 0x4f, 0xf8, 0x87, 0x04, 0x11, 0x5e, 0x6c, 0x58, 0x94, 0xb8, 0x69, 0xbb,
345            0x45, 0x3c, 0x3f, 0xe2, 0x1e, 0xb1, 0x22, 0x44, 0xc6, 0xe7, 0xe9, 0x6a, 0xbe, 0xd3,
346            0x0f, 0x18, 0x1b, 0x9f, 0x86, 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58,
347            0x20, 0x0c, 0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c, 0xbf,
348            0xad, 0xd9, 0xa6, 0x97, 0xbb, 0x48, 0xd9, 0xd7, 0xff, 0x91, 0x0f, 0x0a, 0x6a, 0xc1,
349            0x0b, 0x91, 0x2b, 0xe9, 0x58, 0x22, 0x58, 0x20, 0x46, 0x78, 0x6f, 0x2a, 0x95, 0x76,
350            0x69, 0x8c, 0x9f, 0x3a, 0xe2, 0x52, 0x3b, 0x4e, 0xb9, 0x4b, 0x8e, 0x07, 0x4c, 0x35,
351            0xab, 0xc4, 0xdf, 0x68, 0x8f, 0xcd, 0x85, 0xd2, 0x9a, 0x01, 0xab, 0xba, 0xa1, 0x6b,
352            0x63, 0x72, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x02,
353        ];
354        let auth_data =
355            AuthenticatorData::from_slice(&data).expect("could not parse the authenticator data");
356
357        let expected = AuthenticatorData {
358            rp_id_hash: [
359                0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20,
360                0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0x0b,
361                0x60, 0x84, 0x1e, 0xf0,
362            ],
363            flags: Flags::UP | Flags::UV | Flags::AT | Flags::ED,
364            counter: Some(1),
365            attested_credential_data: Some(AttestedCredentialData {
366                // interestingly a yubikey returns an empty AAGUID
367                aaguid: Aaguid([0; 16]),
368                credential_id: vec![
369                    0x0c, 0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c, 0xbf,
370                    0x5e, 0x4c, 0x14, 0x04, 0x4f, 0xf8, 0x87, 0x04, 0x11, 0x5e, 0x6c, 0x58, 0x94,
371                    0xb8, 0x69, 0xbb, 0x45, 0x3c, 0x3f, 0xe2, 0x1e, 0xb1, 0x22, 0x44, 0xc6, 0xe7,
372                    0xe9, 0x6a, 0xbe, 0xd3, 0x0f, 0x18, 0x1b, 0x9f, 0x86,
373                ],
374                key: CoseKeyBuilder::new_ec2_pub_key(
375                    coset::iana::EllipticCurve::P_256,
376                    vec![
377                        0x0c, 0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c,
378                        0xbf, 0xad, 0xd9, 0xa6, 0x97, 0xbb, 0x48, 0xd9, 0xd7, 0xff, 0x91, 0x0f,
379                        0x0a, 0x6a, 0xc1, 0x0b, 0x91, 0x2b, 0xe9, 0x58,
380                    ],
381                    vec![
382                        0x46, 0x78, 0x6f, 0x2a, 0x95, 0x76, 0x69, 0x8c, 0x9f, 0x3a, 0xe2, 0x52,
383                        0x3b, 0x4e, 0xb9, 0x4b, 0x8e, 0x07, 0x4c, 0x35, 0xab, 0xc4, 0xdf, 0x68,
384                        0x8f, 0xcd, 0x85, 0xd2, 0x9a, 0x01, 0xab, 0xba,
385                    ],
386                )
387                .algorithm(coset::iana::Algorithm::ES256)
388                .build(),
389            }),
390            extensions: Some(
391                cbor!({
392                    "credProtect" => 2
393                })
394                .unwrap(),
395            ),
396        };
397        assert_eq!(expected, auth_data);
398    }
399
400    #[test]
401    fn deserialize_authenticator_data_with_only_at() {
402        // This is authenticator data extracted from a yubikey version 5 with the extensions
403        // parameter removed
404        let data = [
405            0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20,
406            0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0x0b,
407            0x60, 0x84, 0x1e, 0xf0, 0x45, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
408            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x0c,
409            0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c, 0xbf, 0x5e, 0x4c,
410            0x14, 0x04, 0x4f, 0xf8, 0x87, 0x04, 0x11, 0x5e, 0x6c, 0x58, 0x94, 0xb8, 0x69, 0xbb,
411            0x45, 0x3c, 0x3f, 0xe2, 0x1e, 0xb1, 0x22, 0x44, 0xc6, 0xe7, 0xe9, 0x6a, 0xbe, 0xd3,
412            0x0f, 0x18, 0x1b, 0x9f, 0x86, 0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58,
413            0x20, 0x0c, 0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c, 0xbf,
414            0xad, 0xd9, 0xa6, 0x97, 0xbb, 0x48, 0xd9, 0xd7, 0xff, 0x91, 0x0f, 0x0a, 0x6a, 0xc1,
415            0x0b, 0x91, 0x2b, 0xe9, 0x58, 0x22, 0x58, 0x20, 0x46, 0x78, 0x6f, 0x2a, 0x95, 0x76,
416            0x69, 0x8c, 0x9f, 0x3a, 0xe2, 0x52, 0x3b, 0x4e, 0xb9, 0x4b, 0x8e, 0x07, 0x4c, 0x35,
417            0xab, 0xc4, 0xdf, 0x68, 0x8f, 0xcd, 0x85, 0xd2, 0x9a, 0x01, 0xab, 0xba,
418        ];
419        let auth_data =
420            AuthenticatorData::from_slice(&data).expect("could not parse the authenticator data");
421
422        let expected = AuthenticatorData {
423            rp_id_hash: [
424                0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20,
425                0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0x0b,
426                0x60, 0x84, 0x1e, 0xf0,
427            ],
428            flags: Flags::UP | Flags::UV | Flags::AT,
429            counter: Some(1),
430            attested_credential_data: Some(AttestedCredentialData {
431                // interestingly a yubikey returns an empty AAGUID
432                aaguid: Aaguid([0; 16]),
433                credential_id: vec![
434                    0x0c, 0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c, 0xbf,
435                    0x5e, 0x4c, 0x14, 0x04, 0x4f, 0xf8, 0x87, 0x04, 0x11, 0x5e, 0x6c, 0x58, 0x94,
436                    0xb8, 0x69, 0xbb, 0x45, 0x3c, 0x3f, 0xe2, 0x1e, 0xb1, 0x22, 0x44, 0xc6, 0xe7,
437                    0xe9, 0x6a, 0xbe, 0xd3, 0x0f, 0x18, 0x1b, 0x9f, 0x86,
438                ],
439                key: CoseKeyBuilder::new_ec2_pub_key(
440                    coset::iana::EllipticCurve::P_256,
441                    vec![
442                        0x0c, 0x98, 0x51, 0xdc, 0x8b, 0xd1, 0xef, 0x2d, 0x08, 0x4b, 0x20, 0x1c,
443                        0xbf, 0xad, 0xd9, 0xa6, 0x97, 0xbb, 0x48, 0xd9, 0xd7, 0xff, 0x91, 0x0f,
444                        0x0a, 0x6a, 0xc1, 0x0b, 0x91, 0x2b, 0xe9, 0x58,
445                    ],
446                    vec![
447                        0x46, 0x78, 0x6f, 0x2a, 0x95, 0x76, 0x69, 0x8c, 0x9f, 0x3a, 0xe2, 0x52,
448                        0x3b, 0x4e, 0xb9, 0x4b, 0x8e, 0x07, 0x4c, 0x35, 0xab, 0xc4, 0xdf, 0x68,
449                        0x8f, 0xcd, 0x85, 0xd2, 0x9a, 0x01, 0xab, 0xba,
450                    ],
451                )
452                .algorithm(coset::iana::Algorithm::ES256)
453                .build(),
454            }),
455            extensions: None,
456        };
457        assert_eq!(expected, auth_data);
458    }
459
460    #[test]
461    fn deserialize_authenticator_data_with_only_ed() {
462        // This is authenticator data extracted from a yubikey version 5 with the Attested credential
463        // data removed.
464        let data = [
465            0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20,
466            0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0x0b,
467            0x60, 0x84, 0x1e, 0xf0, 0x85, 0x00, 0x00, 0x00, 0x01, 0xa1, 0x6b, 0x63, 0x72, 0x65,
468            0x64, 0x50, 0x72, 0x6f, 0x74, 0x65, 0x63, 0x74, 0x02,
469        ];
470        let auth_data =
471            AuthenticatorData::from_slice(&data).expect("could not parse the authenticator data");
472
473        let expected = AuthenticatorData {
474            rp_id_hash: [
475                0x74, 0xa6, 0xea, 0x92, 0x13, 0xc9, 0x9c, 0x2f, 0x74, 0xb2, 0x24, 0x92, 0xb3, 0x20,
476                0xcf, 0x40, 0x26, 0x2a, 0x94, 0xc1, 0xa9, 0x50, 0xa0, 0x39, 0x7f, 0x29, 0x25, 0x0b,
477                0x60, 0x84, 0x1e, 0xf0,
478            ],
479            flags: Flags::UP | Flags::UV | Flags::ED,
480            counter: Some(1),
481            attested_credential_data: None,
482            extensions: Some(
483                cbor!({
484                    "credProtect" => 2
485                })
486                .unwrap(),
487            ),
488        };
489        assert_eq!(expected, auth_data);
490    }
491
492    #[test]
493    fn round_trip_deserialization() {
494        let expected = AuthenticatorData::new("future.1password.com", Some(0))
495            .set_attested_credential_data(AttestedCredentialData {
496                aaguid: Aaguid::new_empty(),
497                credential_id: random_vec(16),
498                key: CoseKeyBuilder::new_ec2_pub_key(
499                    coset::iana::EllipticCurve::P_256,
500                    // seeing as these are random, it is not a valid key, so don't use this.
501                    random_vec(32),
502                    random_vec(32),
503                )
504                .algorithm(coset::iana::Algorithm::ES256)
505                .build(),
506            });
507
508        let auth_data_bytes = expected.to_vec();
509
510        let auth_data =
511            AuthenticatorData::from_slice(&auth_data_bytes).expect("could not deserialize");
512
513        assert_eq!(expected, auth_data);
514    }
515
516    #[test]
517    fn add_empty_extensions_does_not_add_flag() {
518        // Make credential with None
519        let make_auth_data = AuthenticatorData::new("1password.com", None)
520            .set_make_credential_extensions(None)
521            .expect("falsely tried to serialize");
522        assert!(!make_auth_data.flags.contains(Flags::ED));
523
524        // Make credential with empty extension
525        let make_auth_data = AuthenticatorData::new("1password.com", None)
526            .set_make_credential_extensions(Some(make_credential::SignedExtensionOutputs {
527                hmac_secret: None,
528                hmac_secret_mc: None,
529            }))
530            .expect("falsely tried to serialize");
531        assert!(!make_auth_data.flags.contains(Flags::ED));
532
533        // Get assertion with None
534        let make_auth_data = AuthenticatorData::new("1password.com", None)
535            .set_assertion_extensions(None)
536            .expect("falsely tried to serialize");
537        assert!(!make_auth_data.flags.contains(Flags::ED));
538
539        // Get assertion with empty extension
540        let make_auth_data = AuthenticatorData::new("1password.com", None)
541            .set_assertion_extensions(Some(get_assertion::SignedExtensionOutputs {
542                hmac_secret: None,
543            }))
544            .expect("falsely tried to serialize");
545        assert!(!make_auth_data.flags.contains(Flags::ED));
546    }
547}