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;