passkey_types/ctap2/
make_credential.rs

1//! <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential>
2
3use ciborium::cbor;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    Bytes,
8    ctap2::AuthenticatorData,
9    utils::serde::{ignore_unknown_opt_vec, ignore_unknown_vec},
10    webauthn,
11};
12
13#[cfg(doc)]
14use {
15    crate::webauthn::{
16        CollectedClientData, PublicKeyCredentialCreationOptions, PublicKeyCredentialDescriptor,
17    },
18    ciborium::value::Value,
19};
20
21use super::extensions::{AuthenticatorPrfInputs, AuthenticatorPrfMakeOutputs, HmacGetSecretInput};
22
23serde_workaround! {
24    /// While similar in structure to [`PublicKeyCredentialCreationOptions`],
25    /// it is not completely identical, namely the presence of the `options` key.
26    #[derive(Debug, PartialEq)]
27    pub struct Request {
28        /// Hash of the ClientData contextual binding specified by host.
29        #[serde(rename = 0x01)]
30        pub client_data_hash: Bytes,
31
32        /// This [`PublicKeyCredentialRpEntity`] data structure describes a Relying Party with which the
33        /// new public key credential will be associated. It contains the Relying party identifier
34        /// of type text string, (optionally) a human-friendly RP name of type text string,
35        /// and (optionally) a URL of type text string, referencing a RP icon image. The RP name is
36        /// to be used by the authenticator when displaying the credential to the user for selection
37        /// and usage authorization. The RP name and URL are optional so that the RP can be more
38        /// privacy friendly if it chooses to. For example, for authenticators with a display, RP
39        /// may not want to display name/icon for single-factor scenarios.
40        #[serde(rename = 0x02)]
41        pub rp: PublicKeyCredentialRpEntity,
42
43        /// This [`PublicKeyCredentialUserEntity`] data structure describes the user account to
44        /// which the new public key credential will be associated at the RP. It contains an
45        /// RP-specific user account identifier of type byte array, (optionally) a user name of type
46        /// text string, (optionally) a user display name of type text string, and (optionally) a
47        /// URL of type text string, referencing a user icon image (of a user avatar, for example).
48        /// The authenticator associates the created public key credential with the account
49        /// identifier, and MAY also associate any or all of the user name, user display name, and
50        /// image data (pointed to by the URL, if any). The user name, display name, and URL are
51        /// optional for privacy reasons for single-factor scenarios where only user presence is
52        /// required. For example, in certain closed physical environments like factory floors, user
53        /// presence only authenticators can satisfy RP’s productivity and security needs. In these
54        /// environments, omitting user name, display name and URL makes the credential more privacy
55        /// friendly. Although this information is not available without user verification, devices
56        /// which support user verification but do not have it configured, can be tricked into
57        /// releasing this information by configuring the user verification.
58        #[serde(rename = 0x03)]
59        pub user: webauthn::PublicKeyCredentialUserEntity,
60
61        /// A sequence of CBOR maps consisting of pairs of PublicKeyCredentialType (a string) and
62        /// cryptographic algorithm (a positive or negative integer), where algorithm identifiers
63        /// are values that SHOULD be registered in the IANA COSE Algorithms registry
64        /// [`coset::iana::Algorithm`]. This sequence is ordered from most preferred (by the RP) to least
65        /// preferred.
66        #[serde(rename = 0x04, deserialize_with = ignore_unknown_vec)]
67        pub pub_key_cred_params: Vec<webauthn::PublicKeyCredentialParameters>,
68
69        /// A sequence of [`PublicKeyCredentialDescriptor`] structures, as specified in [`webauthn`].
70        /// The authenticator returns an error if the authenticator already contains one of
71        /// the credentials enumerated in this sequence. This allows RPs to limit the creation of
72        /// multiple credentials for the same account on a single authenticator.
73        #[serde(
74            rename = 0x05;
75            default,
76            skip_serializing_if = Option::is_none,
77            deserialize_with = ignore_unknown_opt_vec
78        )]
79        pub exclude_list: Option<Vec<webauthn::PublicKeyCredentialDescriptor>>,
80
81        /// Parameters to influence authenticator operation, as specified in [`webauthn`].
82        /// These parameters might be authenticator specific.
83        #[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)]
84        pub extensions: Option<ExtensionInputs>,
85
86        /// Parameters to influence authenticator operation, see [`Options`] for more details.
87        #[serde(rename = 0x07; default)]
88        pub options: Options,
89
90        /// First 16 bytes of HMAC-SHA-256 of clientDataHash using pinToken which platform got from
91        /// the authenticator: HMAC-SHA-256(pinToken, clientDataHash). (NOT YET SUPPORTED)
92        #[serde(rename = 0x08; default, skip_serializing_if = Option::is_none)]
93        pub pin_auth: Option<Bytes>,
94
95        /// PIN protocol version chosen by the client
96        ///
97        /// if ever we hit more than 256 protocol versions, an enhacement request should be filed.
98        #[serde(rename = 0x09; default, skip_serializing_if = Option::is_none)]
99        pub pin_protocol: Option<u8>,
100    }
101}
102
103/// This is a copy of [`webauthn::PublicKeyCredentialRpEntity`] but where the `id` is required
104/// and the `name` is optional which is the inverse of what is defined in the [WebAuthn]. These are
105/// the requirements of the [CTAP2] version of this struct.
106///
107/// [WebAuthn]: https://w3c.github.io/webauthn/#dictdef-publickeycredentialrpentity
108/// [CTAP2]: https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorMakeCredential
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
110pub struct PublicKeyCredentialRpEntity {
111    /// The domain of the relying party
112    pub id: String,
113    /// A human friendly name for the Relying Party
114    #[serde(default, skip_serializing_if = "Option::is_none")]
115    pub name: Option<String>,
116}
117
118/// This is a copy of [`webauthn::PublicKeyCredentialUserEntity`] with differing optional fields.
119#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120pub struct PublicKeyCredentialUserEntity {
121    /// The ID of the user
122    pub id: Bytes,
123    /// Optional user name
124    #[serde(default, skip_serializing_if = "Option::is_none")]
125    pub name: Option<String>,
126    /// Optional display name
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub display_name: Option<String>,
129    /// Optional URL pointing to a user icon
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub icon_url: Option<String>,
132}
133
134impl From<webauthn::PublicKeyCredentialUserEntity> for PublicKeyCredentialUserEntity {
135    fn from(value: webauthn::PublicKeyCredentialUserEntity) -> Self {
136        Self {
137            id: value.id,
138            name: Some(value.name),
139            display_name: Some(value.display_name),
140            icon_url: None,
141        }
142    }
143}
144
145impl TryFrom<PublicKeyCredentialUserEntity> for webauthn::PublicKeyCredentialUserEntity {
146    type Error = &'static str;
147    fn try_from(value: PublicKeyCredentialUserEntity) -> Result<Self, Self::Error> {
148        match (value.name, value.display_name) {
149            (Some(name), Some(display_name)) => Ok(Self {
150                id: value.id,
151                name,
152                display_name,
153            }),
154            _ => Err(
155                "PublicKeyCredentialUserEntity is missing one or more required fields: name, display_name",
156            ),
157        }
158    }
159}
160
161/// In the case of a missing `rp_id` on [`webauthn::PublicKeyCredentialRpEntity`] use this to
162/// construct a [`PublicKeyCredentialRpEntity`] using a effective domain.
163#[non_exhaustive]
164#[derive(Debug)]
165pub struct MissingRpId {
166    /// Human friendly name for the Relying Party, extracted from [`webauthn::PublicKeyCredentialRpEntity::name`].
167    pub rp_name: String,
168}
169
170impl TryFrom<webauthn::PublicKeyCredentialRpEntity> for PublicKeyCredentialRpEntity {
171    type Error = MissingRpId;
172    /// Convert the webauthn version of the struct to the CTAP2 version with the effective domain if
173    /// the id was not provided.
174    fn try_from(value: webauthn::PublicKeyCredentialRpEntity) -> Result<Self, Self::Error> {
175        if let Some(id) = value.id {
176            Ok(Self {
177                id,
178                name: Some(value.name),
179            })
180        } else {
181            Err(MissingRpId {
182                rp_name: value.name,
183            })
184        }
185    }
186}
187
188/// The options that control how an authenticator will behave.
189#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
190pub struct Options {
191    /// Specifies whether this credential is to be discoverable or not.
192    #[serde(default)]
193    pub rk: bool,
194    /// Instructs the authenticator to require a gesture that verifies the user to complete the request. Examples of such gestures are fingerprint scan or a PIN.
195    #[serde(default = "default_true")]
196    pub up: bool,
197    /// User Verification:
198    ///
199    /// If the "uv" option is absent, let the "uv" option be treated as being present with the value false.
200    #[serde(default)]
201    pub uv: bool,
202}
203
204impl Default for Options {
205    fn default() -> Self {
206        Self {
207            rk: false,
208            up: true,
209            uv: false,
210        }
211    }
212}
213
214const fn default_true() -> bool {
215    true
216}
217
218/// All supported Authenticator extensions inputs during credential creation
219#[derive(Debug, Serialize, Deserialize, Default, PartialEq)]
220pub struct ExtensionInputs {
221    /// A boolean value to indicate that this extension is requested by the Relying Party
222    ///
223    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-hmac-secret-extension>
224    #[serde(
225        rename = "hmac-secret",
226        default,
227        skip_serializing_if = "Option::is_none"
228    )]
229    pub hmac_secret: Option<bool>,
230
231    /// The input salts for fetching and deriving a symmetric secret during registration.
232    ///
233    /// TODO: link to the hmac-secret-mc extension in the spec once it's published.
234    #[serde(
235        rename = "hmac-secret-mc",
236        default,
237        skip_serializing_if = "Option::is_none"
238    )]
239    pub hmac_secret_mc: Option<HmacGetSecretInput>,
240
241    /// The direct input from a on-system client for the prf extension.
242    ///
243    /// The output from a request using the `prf` extension will not be signed
244    /// and will be un-encrypted.
245    /// This input should already be hashed by the client.
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub prf: Option<AuthenticatorPrfInputs>,
248}
249
250impl ExtensionInputs {
251    /// Validates that there is at least one extension field that is `Some`.
252    /// If all fields are `None` then this returns `None` as well.
253    pub fn zip_contents(self) -> Option<Self> {
254        let Self {
255            hmac_secret,
256            hmac_secret_mc,
257            prf,
258        } = &self;
259
260        let has_hmac_secret = hmac_secret.is_some();
261        let has_hmac_secret_mc = hmac_secret_mc.is_some();
262        let has_prf = prf.is_some();
263
264        (has_hmac_secret || has_hmac_secret_mc || has_prf).then_some(self)
265    }
266}
267
268serde_workaround! {
269    /// Upon successful creation of a credential, the authenticator returns an attestation object.
270    #[derive(Debug)]
271    pub struct Response {
272        /// The attestation statement format identifier
273        #[serde(rename = 0x01)]
274        pub fmt: String,
275
276        /// The authenticator data object
277        #[serde(rename = 0x02)]
278        pub auth_data: AuthenticatorData,
279
280        /// The attestation statement, whose format is identified by the "fmt" object member.
281        /// The client treats it as an opaque object.
282        //
283        // TODO: Change to a flattened enum when `content, type` serde enums can use numbers as
284        // the keys
285        #[serde(rename = 0x03)]
286        pub att_stmt: ciborium::value::Value,
287
288        /// Indicates whether an enterprise attestation was returned for this credential.
289        /// If `ep_att` is absent or present and set to false, then an enterprise attestation was not returned.
290        /// If `ep_att` is present and set to true, then an enterprise attestation was returned.
291        ///
292        /// Enterprise attestation is currently unsupported by this library.
293        #[serde(rename = 0x04; default, skip_serializing_if = Option::is_none)]
294        pub ep_att: Option<bool>,
295
296        /// Contains the `largeBlobKey` for the credential, if requested with the `largeBlobKey` extension.
297        ///
298        /// The `largeBlobKey` extension is currently unsupported by this library.
299        #[serde(rename = 0x05; default, skip_serializing_if = Option::is_none)]
300        pub large_blob_key: Option<Bytes>,
301
302        /// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any.
303        /// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs.
304        /// Clients MUST treat an empty map the same as an omitted field.
305        #[serde(rename = 0x06; default, skip_serializing_if = Option::is_none)]
306        pub unsigned_extension_outputs: Option<UnsignedExtensionOutputs>,
307    }
308}
309
310impl Response {
311    // Note: Technically the authenticator response should be a CBOR object, but we add this method instead
312    // for backwards compatibility with previous versions of this library.
313    // [WebAuthn]: Return the attestation object as a CBOR map with the following syntax, filled in with variables initialized by this algorithm
314
315    /// Convert response into a CBOR-encoded WebAuthn attestation object.
316    ///
317    /// Note: This uses the WebAuthn string labels for fields rather than the CTAP2 integer labels.
318    pub fn as_webauthn_bytes(&self) -> Bytes {
319        let mut attestation_object = Vec::with_capacity(128);
320        // SAFETY: The Results here are from serializing all the internals of `cbor!` into `ciborium::Value`
321        // then serializing said value to bytes. The unwraps here are safe because it would otherwise be
322        // programmer error.
323        // TODO: Create strong attestation type definitions, part of CTAP2
324        let attestation_object_value = cbor!({
325               // TODO: Follow preference and/or implement AnonCA https://w3c.github.io/webauthn/#anonymization-ca
326               "fmt" => "none",
327                "attStmt" => {},
328                // Explicitly define these fields as bytes since specialization is still fairly far
329               "authData" => ciborium::value::Value::Bytes(self.auth_data.to_vec()),
330        })
331        .unwrap();
332        ciborium::ser::into_writer(&attestation_object_value, &mut attestation_object).unwrap();
333        attestation_object.into()
334    }
335}
336
337/// All supported Authenticator extensions outputs during credential creation
338///
339/// This is to be serialized to [`Value`] in [`AuthenticatorData::extensions`]
340#[derive(Debug, Serialize, Deserialize)]
341pub struct SignedExtensionOutputs {
342    /// A boolean value to indicate that this extension was successfully processed by the extension
343    ///
344    /// <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-hmac-secret-extension>
345    #[serde(
346        rename = "hmac-secret",
347        default,
348        skip_serializing_if = "Option::is_none"
349    )]
350    pub hmac_secret: Option<bool>,
351
352    /// Outputs the symmetric secrets after successfull processing. The output MUST be encrypted.
353    ///
354    /// TODO: link to the hmac-secret-mc extension in the spec once it's published.
355    #[serde(
356        rename = "hmac-secret-mc",
357        default,
358        skip_serializing_if = "Option::is_none"
359    )]
360    pub hmac_secret_mc: Option<Bytes>,
361}
362
363impl SignedExtensionOutputs {
364    /// Validates that there is at least one extension field that is `Some`.
365    /// If all fields are `None` then this returns `None` as well.
366    pub fn zip_contents(self) -> Option<Self> {
367        let Self {
368            hmac_secret,
369            hmac_secret_mc,
370        } = &self;
371        let has_hmac_secret = hmac_secret.is_some();
372        let has_hmac_secret_mc = hmac_secret_mc.is_some();
373
374        (has_hmac_secret || has_hmac_secret_mc).then_some(self)
375    }
376}
377
378/// A map, keyed by extension identifiers, to unsigned outputs of extensions, if any.
379/// Authenticators SHOULD omit this field if no processed extensions define unsigned outputs.
380/// Clients MUST treat an empty map the same as an omitted field.
381#[derive(Debug, Serialize, Deserialize, Default)]
382#[serde(rename_all = "camelCase")]
383pub struct UnsignedExtensionOutputs {
384    /// This output is supported in the Webauthn specification and will be used when the authenticator
385    /// and the client are in memory or communicating through an internal channel.
386    ///
387    /// If you are using transports where this needs to pass through a wire, use hmac-secret instead.
388    pub prf: Option<AuthenticatorPrfMakeOutputs>,
389}
390
391impl UnsignedExtensionOutputs {
392    /// Validates that there is at least one extension field that is `Some`.
393    /// If all fields are `None` then this returns `None` as well.
394    pub fn zip_contents(self) -> Option<Self> {
395        let Self { prf } = &self;
396
397        prf.is_some().then_some(self)
398    }
399}
400
401#[cfg(test)]
402mod tests;