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::{Ctap2Error, get_assertion, make_credential};
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(self.attested_credential_data.clone().into_iter().flatten())
212            .chain(
213                self.extensions
214                    .as_ref()
215                    .map(|val| {
216                        let mut bytes = Vec::new();
217                        ciborium::ser::into_writer(val, &mut bytes).unwrap();
218                        bytes
219                    })
220                    .into_iter()
221                    .flatten(),
222            )
223            .collect()
224    }
225}
226
227/// Attested credential data is a variable-length byte array added to the authenticator data when
228/// generating an attestation object for a credential
229///
230/// <https://w3c.github.io/webauthn/#attested-credential-data>
231#[derive(Debug, Clone, PartialEq)]
232pub struct AttestedCredentialData {
233    /// The AAGUID of the authenticator.
234    pub aaguid: Aaguid,
235
236    /// The credential ID whose length is prepended to the byte array. This is not public as it
237    /// should not be modifiable to be longer than a u16.
238    credential_id: Vec<u8>,
239
240    /// The credential public key encoded in COSE_Key format, as defined in Section 7 of [RFC9052],
241    /// using the CTAP2 canonical CBOR encoding form. The COSE_Key-encoded credential public key
242    /// MUST contain the "alg" parameter and MUST NOT contain any other OPTIONAL parameters.
243    /// The "alg" parameter MUST contain a [coset::iana::Algorithm] value. The encoded credential
244    /// public key MUST also contain any additional REQUIRED parameters stipulated by the relevant
245    /// key type specification, i.e. REQUIRED for the key type "kty" and algorithm "alg"
246    /// (see Section 2 of [RFC9053]).
247    ///
248    /// [RFC9052]: https://www.rfc-editor.org/rfc/rfc9052
249    /// [RFC9053]: https://www.rfc-editor.org/rfc/rfc9053
250    pub key: CoseKey,
251}
252
253impl AttestedCredentialData {
254    /// Create a new [AttestedCredentialData]
255    ///
256    /// # Error
257    /// Returns an error if the length of `credential_id` cannot be represented by a u16.
258    pub fn new(
259        aaguid: Aaguid,
260        credential_id: Vec<u8>,
261        key: CoseKey,
262    ) -> Result<Self, TryFromIntError> {
263        // assert that the credential id's length can be represented by a u16
264        u16::try_from(credential_id.len())?;
265
266        Ok(Self {
267            aaguid,
268            credential_id,
269            key,
270        })
271    }
272
273    /// Get read access to the credential ID,
274    pub fn credential_id(&self) -> &[u8] {
275        &self.credential_id
276    }
277}
278
279impl AttestedCredentialData {
280    fn from_reader<R: Read>(reader: &mut R) -> coset::Result<Self> {
281        let mut aaguid = [0; 16];
282        reader.read_exact(&mut aaguid).map_err(io_error)?;
283        let aaguid = Aaguid(aaguid);
284
285        let mut cred_len = [0; 2];
286        reader.read_exact(&mut cred_len).map_err(io_error)?;
287        let cred_len: usize = u16::from_be_bytes(cred_len).into();
288
289        let mut credential_id = vec![0; cred_len];
290        reader.read_exact(&mut credential_id).map_err(io_error)?;
291
292        let cose_val = ciborium::de::from_reader(reader).map_err(io_error)?;
293        let key = CoseKey::from_cbor_value(cose_val)?;
294
295        Ok(Self {
296            aaguid,
297            credential_id,
298            key,
299        })
300    }
301}
302
303/// Iterator for attested credential data.
304pub struct AttestedCredentialDataIterator {
305    // data: &'a AttestedCredentialData,
306    aaguid: [u8; 16],
307    credential_id_len: [u8; 2],
308    credential_id: Vec<u8>,
309    // unfortunately any serialization in Coset does not use serde::Serialize
310    // and takes by value, so this must be owned.
311    cose_key: Vec<u8>,
312    state: AttestedCredentialDataIteratorState,
313}
314
315enum AttestedCredentialDataIteratorState {
316    Aaguid(u8),
317    CredIdLen(u8),
318    CredId(u16),
319    CoseKey(usize),
320    Done,
321}
322
323impl AttestedCredentialDataIterator {
324    fn new(data: AttestedCredentialData) -> Self {
325        // SAFETY: AAGUID length is guaranteed to be 16 bytes by AttestedCredentialData constructors
326        let aaguid = data.aaguid.0;
327        let cred_id_len: [u8; 2] = u16::try_from(data.credential_id.len())
328            .expect("Credential ID length is guaranteed to fit within 16 bytes by AttestedCredentialData constructors")
329            .to_be_bytes();
330        // SAFETY: if this unwrap fails, it is programmer error
331        let cose_key = data
332            .key
333            .clone()
334            .to_vec()
335            .expect("Properly formatted COSE key");
336        AttestedCredentialDataIterator {
337            aaguid,
338            credential_id_len: cred_id_len,
339            credential_id: data.credential_id,
340            cose_key,
341            state: AttestedCredentialDataIteratorState::Aaguid(0),
342        }
343    }
344}
345
346impl Iterator for AttestedCredentialDataIterator {
347    type Item = u8;
348
349    fn next(&mut self) -> Option<Self::Item> {
350        match self.state {
351            AttestedCredentialDataIteratorState::Aaguid(x) => {
352                debug_assert!(x < 16);
353                if x == 15 {
354                    self.state = AttestedCredentialDataIteratorState::CredIdLen(0);
355                } else {
356                    self.state = AttestedCredentialDataIteratorState::Aaguid(x + 1)
357                }
358                Some(self.aaguid[usize::from(x)])
359            }
360            AttestedCredentialDataIteratorState::CredIdLen(x) => {
361                debug_assert!(x < 2);
362                if x == 1 {
363                    self.state = AttestedCredentialDataIteratorState::CredId(0);
364                } else {
365                    self.state = AttestedCredentialDataIteratorState::CredIdLen(x + 1);
366                }
367                Some(self.credential_id_len[usize::from(x)])
368            }
369            AttestedCredentialDataIteratorState::CredId(x) => {
370                // SAFETY: credential ID is constrained to 2^16 -1 bytes by constructors.
371                let cred_id_len: u16 = u16::try_from(self.credential_id.len())
372                    .expect("credential ID length to be less than 2^16");
373                debug_assert!(x < cred_id_len);
374                if x == cred_id_len - 1 {
375                    self.state = AttestedCredentialDataIteratorState::CoseKey(0);
376                } else {
377                    self.state = AttestedCredentialDataIteratorState::CredId(x + 1);
378                }
379                Some(self.credential_id[usize::from(x)])
380            }
381            AttestedCredentialDataIteratorState::CoseKey(x) => {
382                if x == self.cose_key.len() - 1 {
383                    self.state = AttestedCredentialDataIteratorState::Done;
384                } else {
385                    self.state = AttestedCredentialDataIteratorState::CoseKey(x + 1);
386                }
387                Some(self.cose_key[x])
388            }
389            AttestedCredentialDataIteratorState::Done => None,
390        }
391    }
392}
393
394impl IntoIterator for AttestedCredentialData {
395    type Item = u8;
396    type IntoIter = AttestedCredentialDataIterator;
397
398    fn into_iter(self) -> Self::IntoIter {
399        AttestedCredentialDataIterator::new(self)
400    }
401}
402
403#[cfg(test)]
404mod test;