Fix NoCredentials error when checking excludeCredentials in make_credential
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:
-
empty_store_with_exclude_credentials_succeeds: Verifies thatmake_credentialsucceeds when an empty store is used with a non-emptyexcludeCredentialslist. This is the primary fix test. -
empty_exclude_credentials_with_empty_store_succeeds: Edge case test ensuring an empty exclude list with an empty store also works correctly. -
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.