Passkeys on Android and Windows Hello

Android and Windows Hello are the two platforms most likely to break an implementation that was developed and tested against a single one, for entirely different reasons: Windows authenticators frequently produce RSA keys, and Android’s attestation carries optional fields that vary by vendor. This page covers what differs, what breaks, and what to build instead. It sits under Cross-Platform Browser Behaviour.


Behaviour Reference

Behaviour Android Windows Hello
credential sync through the platform account increasingly available; read the bits
typical algorithm ES256 ES256 or RS256, depending on configuration
attestation chains to a Google root; keystore properties optional commonly available
signature counter usually zero for synced credentials may be present on device-bound ones
cross-device role frequently the scanning device frequently the requesting device

The algorithm row is the one that produces outright failures rather than degraded behaviour.

Two platforms, two different shapes Android credentials sync through a platform account; Windows Hello credentials have historically been device-bound. Read the bits rather than assuming from the platform Android synced through the platform account backup bits typically both set attestation chains to a Google root can assert keystore properties Windows Hello historically device-bound, now increasingly synced backup bits worth reading rather than assuming attestation available on many configurations RSA keys are common The Windows story has been changing, which is exactly why the flags matter more than a static assumption.

Root Cause Analysis

1. Omitting RS256 from the options. The single most common Windows-specific defect. A registration request offering only ES256 cannot be satisfied by an authenticator that produces RSA keys, and the ceremony fails with an error the user cannot act on. Nothing in the failure names the cause.

2. A verifier hard-coded to elliptic curve. The complement of the same problem: the credential enrols, and then every assertion fails because the verification path assumed a curve. The symptom is a user who can register and never sign in.

3. Assuming Windows credentials are device-bound. Historically true and increasingly not. A policy built on the assumption will be wrong for a growing share of users, and the backup bits report the truth on every ceremony.

4. Requiring Android keystore properties. The attestation can carry rich claims about how the key is protected, and the fields are optional and vendor-dependent. Requiring one produces failures confined to particular manufacturers and Android versions.

5. Assuming the phone is always the scanning device. On Android the same device is frequently both the authenticator and the browser, which changes which flows are relevant and which prompts a user sees.

What actually catches people out Four behaviours produce most of the platform-specific defects on these two. Four behaviours, one of which breaks verification outright RSA keys from Windows a verifier hard-coded to ES256 fails on enrolment device-bound Windows credentials the account needs a second credential; the bits say so Android keystore attestation richer claims, but every field is optional credential provider variation a third-party manager may own the chooser

Step-by-Step Resolution

Step 1 — offer ES256 and RS256, in that order

ES256 first because it is universal and compact; RS256 included so that RSA-producing authenticators can enrol at all.

Step 2 — store the negotiated algorithm and dispatch on it

Never infer it from key length or from what you offered. The response reports the choice, and that value is the input to verification.

Step 3 — read the backup bits rather than assuming

They tell you whether a given Windows credential is device-bound, which is the input to the second-credential prompt. Platform assumptions age; the bits do not.

Step 4 — treat every Android keystore field as optional

Verify the chain, read what is present, and let absent fields fall through to your documented no-claim path.

Step 5 — test on both

A virtual authenticator will not surface either of these differences. One real Windows machine and one Android device, once per release, covers them.

Handling the RSA case correctly Offer the algorithm, store what was negotiated, and dispatch verification on the stored value. Four steps, and the first is the one usually missing options include RS256 authenticator chooses it store key plus algorithm verify dispatch on the stored value Omitting RS256 from the options is what makes these users unable to enrol at all, with no message explaining why.

Verification and Testing

The RSA path needs a captured fixture, because a virtual authenticator will not produce one. Register once on a Windows machine configured to produce an RSA key, store the payload, and assert that it verifies end to end. That single fixture protects the branch that otherwise only runs in production.

Assert the algorithm dispatch with a negative test: run the RSA fixture through the elliptic-curve path and confirm it fails loudly at the dispatch rather than quietly at the signature check.

For Android, assert that a fixture missing an optional keystore field still verifies and reaches your policy branch rather than an exception.

Manually, complete one enrolment and one sign-in on each platform per release. Ten minutes, and it covers the behaviours no fixture anticipates.


Pitfalls

1. An algorithm list with one entry. Excludes a real population with no diagnosable error.

2. Requiring an optional attestation field. Produces vendor-specific failures that look random.

3. Hard-coding platform assumptions about sync. They have already changed once.


Frequently Asked Questions

Why do some Windows authenticators produce RSA keys?

Because the underlying key store supports it and, in some configurations, prefers it. It is not a sign of an old or weak device — RSA with SHA-256 is a perfectly standard choice — but it does mean your verification path needs a second branch and your stored keys will be substantially larger.

Are Windows Hello passkeys device-bound?

Historically yes, and increasingly not: sync has been arriving, and the honest answer for any individual credential is whatever the backup bits report on the ceremony. Reading them rather than assuming is what keeps a second-credential prompt firing for the accounts that need it and staying quiet for the ones that do not.

Should I consume Android keystore attestation?

Only if a policy reads it. It can assert useful properties — that the key is hardware-backed, that user authentication is enforced by the keystore — but the fields are optional and vary by vendor and version, so a policy that requires them will exclude legitimate devices.

Does Android behave differently as the scanning device?

The cross-device flow works in both directions, but the common shape on Android is that the same device is browser and authenticator, so no scanning is involved at all. Designing the interface so the cross-device option is offered rather than assumed keeps both cases working.

What is the fastest way to confirm my implementation handles both?

One captured RSA registration in your fixture set, and one manual sign-in per platform per release. Those two together cover the failures that actually occur; broader automation adds little because a virtual authenticator does not reproduce the differences.

Do third-party credential managers change anything on these platforms?

They change which chooser the user sees and which credentials are offered, in the same way as on any other platform. Your implementation is unaffected, but user reports will describe an interface you cannot reproduce, which is worth knowing before spending an afternoon trying to.



Building an Algorithm Policy That Covers Both

Because the algorithm question is the one that produces outright enrolment failures rather than degraded behaviour, it deserves a deliberate policy rather than a default.

Offer three identifiers, ordered: ES256 first, then RS256, then EdDSA if your verification stack handles it cleanly. That list satisfies essentially every authenticator these two platforms produce, and the ordering means the common path stays common — the rare device that would have chosen something else is a small population using a branch you have nonetheless tested.

Store the negotiated identifier in its own column, indexed nowhere and read on every verification. It is a small integer, it never changes for a credential, and it is the single input that makes verification deterministic. A deployment that stores keys without it ends up guessing, and guessing wrong is indistinguishable from a bad signature.

Test each branch with a captured fixture rather than a generated one. Generated keys agree with the code that generated them; a key from a real Windows machine does not, which is exactly why it catches the padding and length assumptions that a synthetic case sails through.

Finally, treat a verification failure concentrated on one platform as an algorithm problem until proven otherwise. It is the most common cause by a wide margin, and checking the stored identifier against the branch that ran takes seconds — far less than the encoding investigation most teams start with.

Related