passkey-authenticator 0.5.0

A webauthn authenticator supporting passkeys.
Documentation
use p256::SecretKey;
use passkey_types::{
    Passkey,
    ctap2::{
        AttestedCredentialData, AuthenticatorData, Ctap2Error, Flags, StatusCode,
        make_credential::{Request, Response},
    },
};

use crate::{Authenticator, CoseKeyPair, CredentialStore, UiHint, UserValidationMethod};

impl<S, U> Authenticator<S, U>
where
    S: CredentialStore + Sync,
    U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
{
    /// This method is invoked by the host to request generation of a new credential in the authenticator.
    pub async fn make_credential(&mut self, input: Request) -> Result<Response, StatusCode> {
        if !input.options.up {
            return Err(Ctap2Error::InvalidOption.into());
        };

        // 1. If the excludeList parameter is present and contains a credential ID that is present
        //    on this authenticator and bound to the specified rpId, wait for user presence, then
        //    terminate this procedure and return error code CTAP2_ERR_CREDENTIAL_EXCLUDED. User
        //    presence check is required for CTAP2 authenticators before the RP gets told that the
        //    token is already registered to behave similarly to CTAP1/U2F authenticators.

        if input
            .exclude_list
            .as_ref()
            .filter(|list| !list.is_empty())
            .is_some()
        {
            // Handle the case where find_credentials returns NoCredentials error.
            // An empty credential store should not prevent credential creation when checking
            // the exclude list. NoCredentials simply means there are no credentials to exclude.
            let excluded_credentials = match self
                .store()
                .find_credentials(
                    input.exclude_list.as_deref(),
                    &input.rp.id,
                    Some(&input.user.id),
                )
                .await
            {
                Ok(creds) => creds,
                Err(status) if status == StatusCode::from(Ctap2Error::NoCredentials) => vec![],
                Err(e) => return Err(e),
            };

            if let Some(excluded_credential) = excluded_credentials.first() {
                self.check_user(
                    UiHint::InformExcludedCredentialFound(excluded_credential),
                    &input.options,
                )
                .await?;
            }
        }

        // 2. If the pubKeyCredParams parameter does not contain a valid COSEAlgorithmIdentifier
        //    value that is supported by the authenticator, terminate this procedure and return
        //    error code CTAP2_ERR_UNSUPPORTED_ALGORITHM.
        let algorithm = self.choose_algorithm(&input.pub_key_cred_params)?;

        // 3. If the options parameter is present, process all the options. If the option is known
        //    but not supported, terminate this procedure and return CTAP2_ERR_UNSUPPORTED_OPTION.
        //    If the option is known but not valid for this command, terminate this procedure and
        //    return CTAP2_ERR_INVALID_OPTION. Ignore any options that are not understood.
        //    Note that because this specification defines normative behaviors for them, all
        //    authenticators MUST understand the "rk", "up", and "uv" options.
        // NOTE: Some of this step is handled at the very begining of the method

        //    4. If the "rk" option is present then:
        //       1. If the rk option ID is not present in authenticatorGetInfo response, end the operation by returning CTAP2_ERR_UNSUPPORTED_OPTION.
        if input.options.rk && !self.get_info().await.options.unwrap_or_default().rk {
            return Err(Ctap2Error::UnsupportedOption.into());
        }

        // 4. TODO, if the extensions parameter is present, process any extensions that this
        //    authenticator supports. Authenticator extension outputs generated by the authenticator
        //    extension processing are returned in the authenticator data.

        // NB: We do not currently support any Pin Protocols (1 or 2) as this does not make sense
        // in the context of 1Password. This is to be revisited to see if we can hook this into
        // using some key that we already have, such as the Biometry unlock key for example.
        // 5. If pinAuth parameter is present and pinProtocol is 1, verify it by matching it against
        //    first 16 bytes of HMAC-SHA-256 of clientDataHash parameter using
        //    pinToken: HMAC- SHA-256(pinToken, clientDataHash).
        //     1. If the verification succeeds, set the "uv" bit to 1 in the response.
        //     2. If the verification fails, return CTAP2_ERR_PIN_AUTH_INVALID error.
        // 6. If pinAuth parameter is not present and clientPin been set on the authenticator,
        //    return CTAP2_ERR_PIN_REQUIRED error.
        // 7. If pinAuth parameter is present and the pinProtocol is not supported,
        //    return CTAP2_ERR_PIN_AUTH_INVALID.
        if input.pin_auth.is_some() {
            // we currently don't support pin authentication
            return Err(Ctap2Error::UnsupportedOption.into());
        }

        // 8. If the authenticator has a display, show the items contained within the user and rp
        //    parameter structures to the user. Alternatively, request user interaction in an
        //    authenticator-specific way (e.g., flash the LED light). Request permission to create
        //    a credential. If the user declines permission, return the CTAP2_ERR_OPERATION_DENIED
        //    error.
        let flags = self
            .check_user(
                UiHint::RequestNewCredential(&input.user.clone().into(), &input.rp),
                &input.options,
            )
            .await?;

        // 9. Generate a new credential key pair for the algorithm specified.
        let credential_id = passkey_types::rand::random_vec(self.credential_id_length.into());

        let private_key = {
            let mut rng = rand::thread_rng();
            SecretKey::random(&mut rng)
        };

        let extensions = self.make_extensions(input.extensions, flags.contains(Flags::UV))?;

        // Encoding of the key pair into their CoseKey representation before moving the private CoseKey
        // into the passkey. Keeping the public key ready for step 11 below and returning the attested
        // credential.
        let CoseKeyPair { public, private } = CoseKeyPair::from_secret_key(&private_key, algorithm);

        let store_info = self.store.get_info().await;

        let is_passkey_rk = store_info
            .discoverability
            .is_passkey_discoverable(input.options.rk);

        let passkey = Passkey {
            key: private,
            rp_id: input.rp.id.clone(),
            credential_id: credential_id.into(),
            user_handle: is_passkey_rk.then(|| input.user.id.clone()),
            username: is_passkey_rk.then(|| input.user.name.clone()),
            user_display_name: is_passkey_rk.then(|| input.user.display_name.clone()),
            counter: self.make_credentials_with_signature_counter.then_some(0),
            extensions: extensions.credential,
        };

        // 10. If "rk" in options parameter is set to true:
        //     1. If a credential for the same RP ID and account ID already exists on the
        //        authenticator, overwrite that credential.
        //     2. Store the user parameter along the newly-created key pair.
        //     3. If authenticator does not have enough internal storage to persist the new
        //        credential, return CTAP2_ERR_KEY_STORE_FULL.
        // --> This seems like in the wrong place since we still need the passkey, see after step 11.

        // 11. Generate an attestation statement for the newly-created key using clientDataHash.

        // SAFETY: the only case where this fails is if credential_id's length cannot be represented
        // as a u16. This is checked at step 9, therefore this will never return an error
        let acd = AttestedCredentialData::new(
            *self.aaguid(),
            passkey.credential_id.clone().into(),
            public,
        )
        .unwrap();

        let auth_data = AuthenticatorData::new(&input.rp.id, passkey.counter)
            .set_flags(flags)
            .set_attested_credential_data(acd)
            .set_make_credential_extensions(extensions.signed)?;

        let response = Response {
            fmt: "none".into(),
            auth_data,
            att_stmt: coset::cbor::value::Value::Map(vec![]),
            ep_att: None,
            large_blob_key: None,
            unsigned_extension_outputs: extensions.unsigned,
        };

        // 10
        self.store_mut()
            .save_credential(passkey, input.user.into(), input.rp, input.options)
            .await?;

        Ok(response)
    }
}

#[cfg(test)]
mod tests;