Skip to content

Conversation

@cquintana92
Copy link

@cquintana92 cquintana92 commented Nov 11, 2025

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: #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 cquintana92 force-pushed the fix/handle-exclude-credentials-empty-list branch from a8aef90 to 7fd9d92 Compare November 11, 2025 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant