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