microg / microg/GmsCore

FIDO: ScreenLockTransportHandler rejects registration when excludeCredentials matches USB credential

Open
#3,751 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
14.6k
Forks
3.2k
Avg merge
12d 11h
Merged PRs (30d)
4

Description

# FIDO: ScreenLockTransportHandler rejects registration when excludeCredentials matches other credentials

## Description

When creating a new screen-lock/platform passkey, `ScreenLockTransportHandler`
appears to reject the registration if an `excludeCredentials` entry matches a
credential known through another authenticator/transport, such as USB.

This prevents creating a screen-lock credential when the relying party already
has a security-key credential registered.

The issue was encountered using hw-fido2-provider / Passchain, which uses microG
fido-core.

## Reproduction

Environment:

- GrapheneOS
- Pixel 10 XL
- Passchain / hw-fido2-provider 1.1.0
- Package: `s1m.hwfido2provider`
- Browser: Brave, also reproducible with Vanadium
- RP: `openai.com`

Account state:

- The account already has a FIDO credential registered using a YubiKey.

Steps:

1. Start registration of another passkey with the same RP.
2. RP supplies the existing YubiKey credential in `excludeCredentials`.
3. Select Passchain / hw-fido2-provider.
4. Select the screen-lock/on-device authenticator.
5. Complete biometric verification.
6. Registration fails with:

```text
An excluded credential has already been registered with the device
```

## Log evidence

The WebAuthn registration request contains an exclusion entry whose credential
ID begins:

```text
5eUGCSsy...
```

Immediately before registration fails, fido-core logs the same credential as:

```yaml
getKnownRegistrationInfo:
credential: 5eUGCSsy...
transport: USB
```

It then fails in the screen-lock transport:

```text
org.microg.gms.fido.core.RequestHandlingException:
An excluded credential has already been registered with the device

at org.microg.gms.fido.core.transport.screenlock.
ScreenLockTransportHandler.register(...)

Finish with error:
An excluded credential has already been registered with the device
(NOT_ALLOWED_ERR)
```

The complete credential IDs and WebAuthn challenge have been redacted here.

## Suspected cause

Current `ScreenLockTransportHandler.register()` appears to do approximately:

```java
val knownRegistrationInfo =
database.getKnownRegistrationInfo(options.rpId)

for (descriptor in options.registerOptions.excludeList.orEmpty()) {
val credentialBase64 = descriptor.id.toBase64(...)
val excluded =
knownRegistrationInfo.any {
it.credential == credentialBase64
}

if (store.containsKey(options.rpId, descriptor.id) || excluded) {
throw RequestHandlingException(...)
}
}
```

`Database.getKnownRegistrationInfo(rpId)` returns registrations across
transports and includes a `transport` field.

Therefore, a credential stored as `Transport.USB` can make `excluded == true`
inside `ScreenLockTransportHandler`.

This appears to treat a credential belonging to a different authenticator as
though it belonged to the screen-lock authenticator.

`excludeCredentials` should prevent registration only if the authenticator
currently handling the request already contains one of the excluded credentials.
A USB credential known to fido-core should not cause the screen-lock/platform
transport to reject registration unless that same credential is actually present
in the screen-lock credential store.

Interestingly, the signing path already filters known registrations using:

```java
.filter { it.transport == Transport.SCREEN_LOCK }
```

but the registration/exclusion path does not.

## Expected behaviour

An existing credential on a USB/NFC security key should not prevent creation of
a separate credential on the screen-lock/platform authenticator.

The CTAP specification describes `excludeList` in terms of whether the
authenticator being asked to create the credential already contains one of the
excluded credentials.

## Possible fix

The database check may need to be limited to registrations belonging to the
screen-lock transport, for example:

```java
val excluded = knownRegistrationInfo.any {
it.transport == Transport.SCREEN_LOCK &&
it.credential == credentialBase64
}
```

Alternatively, if `ScreenLockCredentialStore` is authoritative for the local
platform authenticator, the existing `store.containsKey(...)` check may already
be sufficient.

## Regression candidate

Git blame indicates the `knownRegistrationInfo` / `excluded` logic in
`ScreenLockTransportHandler.register()` was introduced by:

```text
microg/GmsCore#3031
"Fido: Add credential selection for discoverable keys"
```

merged as `f0e2434` on 2025-10-14.

## Additional observations

- Creating the same RP passkey using another password/passkey provider works.
- A YubiKey works with the RP.
- Passchain screen-lock passkey creation works on WebAuthn.io.
- The problem reproduces in multiple browsers.
- Removing old on-device credentials did not resolve the problem.

This narrows the failure to fido-core's screen-lock handling of
`excludeCredentials` when another transport has a matching known registration.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at fido-core's ScreenLockTransportHandler.register() and trace Database.getKnownRegistrationInfo(options.rpId), including the transport field and the existing screen-lock credential store check. Compare this registration path with the signing path's SCREEN_LOCK filtering. Done means a matching USB or NFC credential no longer blocks screen-lock registration, while a matching screen-lock credential still does, with regression coverage added if the existing FIDO tests support it.

Written by the indexing model from the issue text.

Assessment

Tech stack
android, java, kotlin
Domain
authentication, mobile, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.