passkey-rs icon indicating copy to clipboard operation
passkey-rs copied to clipboard

Fix NoCredentials error when checking excludeCredentials in make_credential

Open cquintana92 opened this issue 5 months ago • 0 comments

Problem

When creating a credential (make_credential) with an excludeCredentials list containing entries and using an empty credential store, the operation fails with CTAP2 error 46 (CTAP2_ERR_NO_CREDENTIALS).

Common scenarios where this bug could happen:

  • First-time passkey registration on a new authenticator
  • User account creation with passkey on sites that check for existing credentials

Root cause

The make_credential implementation calls find_credentials() to check if any credentials in the exclude list exist in the store. When the store is empty, find_credentials() returns Err(Ctap2Error::NoCredentials). The current implementation uses .await? which propagates this error, causing the credential creation to fail.

However, an empty store simply means there are no credentials to exclude, which is a valid state. The credential creation should proceed normally.

The return of Err(Ctap2Error::NoCredentials) is based on the impementation of the CredentialStore trait, but returning Err(Ctap2Error::NoCredentials) in case there are none is a correct implementation. This could also be mitigated by handling the "no credentials" case returning an empty list, but having it return this error variant should also be compliant.

Example scenario

// Assume a empty store, and a request with excludeCredentials containing one credential ID
let request = Request {
    exclude_list: Some(vec![PublicKeyCredentialDescriptor {
        ty: PublicKeyCredentialType::PublicKey,
        id: some_credential_id,
        transports: Some(vec![AuthenticatorTransport::Usb]),
    }]),
    // ... other fields
};

// Previously: This would fail with NoCredentials error
// Expected: Should succeed since there's nothing to exclude
authenticator.make_credential(request).await?;

Solution

Handle the NoCredentials error specifically by treating it as an empty list (no credentials to exclude), while still propagating other errors.

Code changes

I think the issue comes from the changes performed in this PR: https://github.com/1Password/passkey-rs/pull/59

The change I propose is subtle, handling the NoCredentials error specifically, as it's a valid state

if let Some(excluded_credential) = self
    .store()
    .find_credentials(
        input.exclude_list.as_deref(),
        &input.rp.id,
        Some(&input.user.id),
    )
    .await?  // This propagated the NoCredentials error
    .first()

To:

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()

Tests

Added three new tests:

  1. empty_store_with_exclude_credentials_succeeds: Verifies that make_credential succeeds when an empty store is used with a non-empty excludeCredentials list. This is the primary fix test.
  2. empty_exclude_credentials_with_empty_store_succeeds: Edge case test ensuring an empty exclude list with an empty store also works correctly.
  3. store_with_credentials_not_in_exclude_list_succeeds: Verifies that when the store contains credentials but none of them are in the excludeCredentials list, credential creation should complete successfully. Added for completeness.

cquintana92 avatar Nov 11 '25 12:11 cquintana92