Skip to content

android-key attestation fallback rejects RKP devices — assumes x5c includes s elf-signed root #783

Description

@arthurkaizer

Describe the issue

verifyAttestationAndroidKey's fallback path (used when no FIDO MDS metadata statement is found for the authenticator's aaguid) rejects valid Android Key Attestation chains from devices using Remote Key Provisioning (RKP).

In February 2026 Google published a new self-signed root for Android Key Attestation, "Key Attestation CA1" (ECDSA P-384), which RKP-enabled devices have used exclusively since 2026-04-10 (see https://android.googleapis.com/attestation/root, which now returns both the legacy root and this new one).

With this change, the x5c array sent by RKP devices no longer includes the self-signed root certificate as its last element. Instead, x5c terminates in an intermediate certificate (e.g. CN=Droid CA2, O=Google LLC) signed by "Key Attestation CA1" — the root itself is never included in x5c (same practice TLS servers already follow by omitting the root).

The fallback code assumes the last certificate in x5c is the root, comparing it by exact string equality:

const x5cNoRootPEM = x5c.slice(0, -1).map(convertCertBufferToPEM);
const x5cRootPEM = x5c.slice(-1).map(convertCertBufferToPEM);
await validateCertificatePath(x5cNoRootPEM, x5cRootPEM);
if (rootCertificates.length > 0 && rootCertificates.indexOf(x5cRootPEM[0]) < 0) {
    throw new Error('x5c root certificate was not a known root certificate (Android Key)');
}

Since Droid CA2 is never byte-identical to Key Attestation CA1, this always fails for RKP devices, even with the new root correctly configured via SettingsService.

Reproduction Steps

  1. Register a passkey via Android Credential Manager on an RKP-enabled device (most current Android hardware).
  2. attestationObject has fmt = "android-key", x5c of 4 certs, last one CN=Droid CA2 issued by CN=Key Attestation CA1, OU=Android, O=Google LLC, C=US.
  3. Call verifyRegistrationResponse with rootCertificates containing the new root.
  4. No FIDO MDS statement exists for this aaguid, so the fallback path runs.

Expected behavior

Should verify successfully — the chain genuinely reaches the configured root, it's just not included in x5c. Instead throws: x5c root certificate was not a known root certificate (Android Key).

Code Samples + WebAuthn Options and Responses

Real (redacted) failure log:

fmt: android-key
aaguid: b93fd961-f2e6-462f-b122-82002247de78
x5cLength: 4
rootSubject: O=Google LLC, CN=Droid CA2
rootIssuer: CN=Key Attestation CA1, OU=Android, O=Google LLC, C=US
rootValidFrom: Feb  9 19:59:18 2026 GMT
rootValidTo: Feb  8 19:59:18 2029 GMT

Suggested fix (validated locally via patch-package on 13.3.2):

await validateCertificatePath(x5c.map(convertCertBufferToPEM), rootCertificates);

Dependencies

  • OS: Android (RKP-enabled devices, current hardware/OS as of 2026)
  • Browser: N/A (native Credential Manager)
  • Authenticator: Android platform authenticator, aaguid b93fd961-f2e6-462f-b122-82002247de78

SimpleWebAuthn Libraries

$ npm list --depth=0 | grep @simplewebauthn
├── @simplewebauthn/server@13.2.2   # originally reported on; also reproduced on 13.3.2

Additional context

Validated the fix with a positive test (leaf → intermediate → "Droid CA2", root omitted, against the real self-signed root) and a negative test (leaf → forged root, correctly rejected). Important: this fix is only safe on top of @simplewebauthn/server >= 13.3.2 — see the next message, there's a separate security issue in 13.2.2's validateCertificatePath that makes this same patch dangerous on that version.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions