passkey_authenticator/authenticator/make_credential.rs
1use p256::SecretKey;
2use passkey_types::{
3 Passkey,
4 ctap2::{
5 AttestedCredentialData, AuthenticatorData, Ctap2Error, Flags, StatusCode,
6 make_credential::{Request, Response},
7 },
8};
9
10use crate::{Authenticator, CoseKeyPair, CredentialStore, UiHint, UserValidationMethod};
11
12impl<S, U> Authenticator<S, U>
13where
14 S: CredentialStore + Sync,
15 U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
16{
17 /// This method is invoked by the host to request generation of a new credential in the authenticator.
18 pub async fn make_credential(&mut self, input: Request) -> Result<Response, StatusCode> {
19 if !input.options.up {
20 return Err(Ctap2Error::InvalidOption.into());
21 };
22
23 // 1. If the excludeList parameter is present and contains a credential ID that is present
24 // on this authenticator and bound to the specified rpId, wait for user presence, then
25 // terminate this procedure and return error code CTAP2_ERR_CREDENTIAL_EXCLUDED. User
26 // presence check is required for CTAP2 authenticators before the RP gets told that the
27 // token is already registered to behave similarly to CTAP1/U2F authenticators.
28
29 if input
30 .exclude_list
31 .as_ref()
32 .filter(|list| !list.is_empty())
33 .is_some()
34 {
35 // Handle the case where find_credentials returns NoCredentials error.
36 // An empty credential store should not prevent credential creation when checking
37 // the exclude list. NoCredentials simply means there are no credentials to exclude.
38 let excluded_credentials = match self
39 .store()
40 .find_credentials(
41 input.exclude_list.as_deref(),
42 &input.rp.id,
43 Some(&input.user.id),
44 )
45 .await
46 {
47 Ok(creds) => creds,
48 Err(status) if status == StatusCode::from(Ctap2Error::NoCredentials) => vec![],
49 Err(e) => return Err(e),
50 };
51
52 if let Some(excluded_credential) = excluded_credentials.first() {
53 self.check_user(
54 UiHint::InformExcludedCredentialFound(excluded_credential),
55 &input.options,
56 )
57 .await?;
58 }
59 }
60
61 // 2. If the pubKeyCredParams parameter does not contain a valid COSEAlgorithmIdentifier
62 // value that is supported by the authenticator, terminate this procedure and return
63 // error code CTAP2_ERR_UNSUPPORTED_ALGORITHM.
64 let algorithm = self.choose_algorithm(&input.pub_key_cred_params)?;
65
66 // 3. If the options parameter is present, process all the options. If the option is known
67 // but not supported, terminate this procedure and return CTAP2_ERR_UNSUPPORTED_OPTION.
68 // If the option is known but not valid for this command, terminate this procedure and
69 // return CTAP2_ERR_INVALID_OPTION. Ignore any options that are not understood.
70 // Note that because this specification defines normative behaviors for them, all
71 // authenticators MUST understand the "rk", "up", and "uv" options.
72 // NOTE: Some of this step is handled at the very begining of the method
73
74 // 4. If the "rk" option is present then:
75 // 1. If the rk option ID is not present in authenticatorGetInfo response, end the operation by returning CTAP2_ERR_UNSUPPORTED_OPTION.
76 if input.options.rk && !self.get_info().await.options.unwrap_or_default().rk {
77 return Err(Ctap2Error::UnsupportedOption.into());
78 }
79
80 // 4. TODO, if the extensions parameter is present, process any extensions that this
81 // authenticator supports. Authenticator extension outputs generated by the authenticator
82 // extension processing are returned in the authenticator data.
83
84 // NB: We do not currently support any Pin Protocols (1 or 2) as this does not make sense
85 // in the context of 1Password. This is to be revisited to see if we can hook this into
86 // using some key that we already have, such as the Biometry unlock key for example.
87 // 5. If pinAuth parameter is present and pinProtocol is 1, verify it by matching it against
88 // first 16 bytes of HMAC-SHA-256 of clientDataHash parameter using
89 // pinToken: HMAC- SHA-256(pinToken, clientDataHash).
90 // 1. If the verification succeeds, set the "uv" bit to 1 in the response.
91 // 2. If the verification fails, return CTAP2_ERR_PIN_AUTH_INVALID error.
92 // 6. If pinAuth parameter is not present and clientPin been set on the authenticator,
93 // return CTAP2_ERR_PIN_REQUIRED error.
94 // 7. If pinAuth parameter is present and the pinProtocol is not supported,
95 // return CTAP2_ERR_PIN_AUTH_INVALID.
96 if input.pin_auth.is_some() {
97 // we currently don't support pin authentication
98 return Err(Ctap2Error::UnsupportedOption.into());
99 }
100
101 // 8. If the authenticator has a display, show the items contained within the user and rp
102 // parameter structures to the user. Alternatively, request user interaction in an
103 // authenticator-specific way (e.g., flash the LED light). Request permission to create
104 // a credential. If the user declines permission, return the CTAP2_ERR_OPERATION_DENIED
105 // error.
106 let flags = self
107 .check_user(
108 UiHint::RequestNewCredential(&input.user.clone().into(), &input.rp),
109 &input.options,
110 )
111 .await?;
112
113 // 9. Generate a new credential key pair for the algorithm specified.
114 let credential_id = passkey_types::rand::random_vec(self.credential_id_length.into());
115
116 let private_key = {
117 let mut rng = rand::thread_rng();
118 SecretKey::random(&mut rng)
119 };
120
121 let extensions = self.make_extensions(input.extensions, flags.contains(Flags::UV))?;
122
123 // Encoding of the key pair into their CoseKey representation before moving the private CoseKey
124 // into the passkey. Keeping the public key ready for step 11 below and returning the attested
125 // credential.
126 let CoseKeyPair { public, private } = CoseKeyPair::from_secret_key(&private_key, algorithm);
127
128 let store_info = self.store.get_info().await;
129
130 let is_passkey_rk = store_info
131 .discoverability
132 .is_passkey_discoverable(input.options.rk);
133
134 let passkey = Passkey {
135 key: private,
136 rp_id: input.rp.id.clone(),
137 credential_id: credential_id.into(),
138 user_handle: is_passkey_rk.then(|| input.user.id.clone()),
139 username: is_passkey_rk.then(|| input.user.name.clone()),
140 user_display_name: is_passkey_rk.then(|| input.user.display_name.clone()),
141 counter: self.make_credentials_with_signature_counter.then_some(0),
142 extensions: extensions.credential,
143 };
144
145 // 10. If "rk" in options parameter is set to true:
146 // 1. If a credential for the same RP ID and account ID already exists on the
147 // authenticator, overwrite that credential.
148 // 2. Store the user parameter along the newly-created key pair.
149 // 3. If authenticator does not have enough internal storage to persist the new
150 // credential, return CTAP2_ERR_KEY_STORE_FULL.
151 // --> This seems like in the wrong place since we still need the passkey, see after step 11.
152
153 // 11. Generate an attestation statement for the newly-created key using clientDataHash.
154
155 // SAFETY: the only case where this fails is if credential_id's length cannot be represented
156 // as a u16. This is checked at step 9, therefore this will never return an error
157 let acd = AttestedCredentialData::new(
158 *self.aaguid(),
159 passkey.credential_id.clone().into(),
160 public,
161 )
162 .unwrap();
163
164 let auth_data = AuthenticatorData::new(&input.rp.id, passkey.counter)
165 .set_flags(flags)
166 .set_attested_credential_data(acd)
167 .set_make_credential_extensions(extensions.signed)?;
168
169 let response = Response {
170 fmt: "none".into(),
171 auth_data,
172 att_stmt: coset::cbor::value::Value::Map(vec![]),
173 ep_att: None,
174 large_blob_key: None,
175 unsigned_extension_outputs: extensions.unsigned,
176 };
177
178 // 10
179 self.store_mut()
180 .save_credential(passkey, input.user.into(), input.rp, input.options)
181 .await?;
182
183 Ok(response)
184 }
185}
186
187#[cfg(test)]
188mod tests;