FIDO: ScreenLockTransportHandler rejects registration when excludeCredentials matches USB credential
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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