passkey-authenticator 0.5.0

A webauthn authenticator supporting passkeys.
Documentation
use p256::ecdsa::{SigningKey, signature::SignerMut};
use passkey_types::{
    Bytes,
    ctap2::{
        AuthenticatorData, Ctap2Error, Flags, StatusCode,
        get_assertion::{Request, Response},
    },
    webauthn::PublicKeyCredentialUserEntity,
};

use crate::{
    Authenticator, CredentialStore, UserValidationMethod,
    passkey::{AsCredentialDescriptor, PasskeyAccessor},
    private_key_from_cose_key,
    user_validation::UiHint,
};

impl<S, U> Authenticator<S, U>
where
    S: CredentialStore + Sync,
    U: UserValidationMethod<PasskeyItem = <S as CredentialStore>::PasskeyItem> + Sync,
{
    /// This method is used by a host to request cryptographic proof of user authentication as well
    /// as user consent to a given transaction, using a previously generated credential that is
    /// bound to the authenticator and relying party identifier.
    pub async fn get_assertion(&mut self, input: Request) -> Result<Response, StatusCode> {
        // 1. Locate all credentials that are eligible for retrieval under the specified criteria:
        //     1. If an allowList is present and is non-empty, locate all denoted credentials
        //        present on this authenticator and bound to the specified rpId.
        //     2. If an allowList is not present, locate all credentials that are present on this
        //        authenticator and bound to the specified rpId.
        //     3. Let numberOfCredentials be the number of credentials found.
        //        --> Seeing as we handle 1 credential per account for an RP, returning the number
        //            of credentials leaks the number of accounts that is stored. This is not ideal,
        //            therefore we will never populate this field.
        let maybe_credential = self
            .store()
            .find_credentials(
                input
                    .allow_list
                    .as_deref()
                    .filter(|inner| !inner.is_empty()),
                &input.rp_id,
                // User handle is not available in assertion calls
                None,
            )
            .await
            .and_then(|c| c.into_iter().next().ok_or(Ctap2Error::NoCredentials.into()));

        // 2. 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.
        // 3. If pinAuth parameter is present and the pinProtocol is not supported,
        //    return CTAP2_ERR_PIN_AUTH_INVALID.
        // 4. If pinAuth parameter is not present and clientPin has been set on the authenticator,
        //    set the "uv" bit to 0 in the response.
        if input.pin_auth.is_some() {
            return Err(Ctap2Error::PinAuthInvalid.into());
        }

        // 5. If the options parameter is present, process all the options.
        //     1. If the option is known but not supported, terminate this procedure and
        //        return CTAP2_ERR_UNSUPPORTED_OPTION.
        //     2. If the option is known but not valid for this command, terminate this procedure
        //        and return CTAP2_ERR_INVALID_OPTION.
        //     3. 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.

        // 6. 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.

        // 7. Collect user consent if required. This step MUST happen before the following steps due
        //    to privacy reasons (i.e., authenticator cannot disclose existence of a credential
        //    until the user interacted with the device):
        let hint = match &maybe_credential {
            Ok(credential) => UiHint::RequestExistingCredential(credential),
            Err(_) => UiHint::InformNoCredentialsFound,
        };
        let flags = self.check_user(hint, &input.options).await?;

        // 8. If no credentials were located in step 1, return CTAP2_ERR_NO_CREDENTIALS.
        let mut credential = maybe_credential?;

        // 9. If more than one credential was located in step 1 and allowList is present and not
        //    empty, select any applicable credential and proceed to step 12. Otherwise, order the
        //    credentials by the time when they were created in reverse order. The first credential
        //    is the most recent credential that was created.
        // NB: This should be done within the `CredentialStore::find_any` implementation. Essentially
        // if multiple credentials are found, use the most recently created one.

        // 10. If authenticator does not have a display:
        //     1. Remember the authenticatorGetAssertion parameters.
        //     2. Create a credential counter(credentialCounter) and set it 1. This counter
        //        signifies how many credentials are sent to the platform by the authenticator.
        //     3. Start a timer. This is used during authenticatorGetNextAssertion command.
        //        This step is optional if transport is done over NFC.
        //     4. Update the response to include the first credential’s publicKeyCredentialUserEntity
        //        information and numberOfCredentials. User identifiable information (name,
        //        DisplayName, icon) inside publicKeyCredentialUserEntity MUST not be returned if
        //        user verification is not done by the authenticator.

        // 11. If authenticator has a display:
        //     1. Display all these credentials to the user, using their friendly name along with
        //        other stored account information.
        //     2. Also, display the rpId of the requester (specified in the request) and ask the
        //        user to select a credential.
        //     3. If the user declines to select a credential or takes too long (as determined by
        //        the authenticator), terminate this procedure and return the
        //        CTAP2_ERR_OPERATION_DENIED error.

        // [WebAuthn-9]. Increment the credential associated signature counter or the global signature
        //               counter value, depending on which approach is implemented by the authenticator,
        //               by some positive value. If the authenticator does not implement a signature
        //               counter, let the signature counter value remain constant at zero.
        if let Some(counter) = credential.counter() {
            credential.set_counter(counter + 1);
            self.store_mut().update_credential(&credential).await?;
        }

        let extensions =
            self.get_extensions(&credential, input.extensions, flags.contains(Flags::UV))?;
        // 12. Sign the clientDataHash along with authData with the selected credential.
        //     Let signature be the assertion signature of the concatenation `authenticatorData` ||
        //     `client_data_hash` using the privateKey of selectedCredential. A simple, undelimited
        //      concatenation is safe to use here because the authenticator data describes its own
        //      length. The hash of the serialized client data (which potentially has a variable
        //      length) is always the last element.
        let auth_data = AuthenticatorData::new(&input.rp_id, credential.counter())
            .set_flags(flags)
            .set_assertion_extensions(extensions.signed)?;

        let mut signature_target = auth_data.to_vec();
        signature_target.extend(input.client_data_hash);

        let secret_key = private_key_from_cose_key(&credential.key())?;

        let mut private_key = SigningKey::from(secret_key);

        let signature: p256::ecdsa::Signature = private_key.sign(&signature_target);
        let signature_bytes = signature.to_der().to_bytes().to_vec().into();

        let user_handle = credential.user_handle().map(Bytes::from);

        Ok(Response {
            credential: Some(credential.as_credential_descriptor(None)),
            auth_data,
            signature: signature_bytes,
            user: user_handle.map(|id| PublicKeyCredentialUserEntity {
                id,
                // TODO: make a Authenticator version of this struct similar to make_credential::PublicKeyCredentialRpEntity
                // since these fields are optional at the authenticator boundary, but required at the client boundary.
                display_name: "".into(),
                name: "".into(),
            }),
            number_of_credentials: None,
            user_selected: None,
            large_blob_key: None,
            unsigned_extension_outputs: extensions.unsigned,
        })
    }
}

#[cfg(test)]
mod tests;