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;