Skip to main content

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;