A WebAuthn correctness checklist for passkey providers, and where HostSpica Passkey stands
SHORT ANSWER
What does a passkey provider have to get right to be WebAuthn-correct?
It must honour the request's algorithm list, excluded and allowed credentials, user-verification and attestation preferences, build correctly laid-out authenticator data with the right flags, and sign data that binds the real origin. HostSpica Passkey gets the data layout, flags and signing right, and does not yet honour the algorithm list, credential lists or attestation preference.
Key takeaways
- Most failures in the field are not cryptography bugs. They are requests the provider quietly ignored: a duplicate credential created, a wrong algorithm chosen, a credential offered that the site did not allow.
- The binary formats (authenticator data, COSE key, attestation object) are strict. A single wrong byte makes sites reject the registration.
- HostSpica Passkey is correct on formats, flags and signing. It is not yet correct on five request options, listed below with our planned order of fixing them.
- The same checklist works as a test plan for any provider, including your own.
Who this is for
If you build a passkey provider, or you want to judge one, this is the list of things the WebAuthn specification expects from the authenticator side. We wrote it while auditing HostSpica Passkey against it, and we show where we fall short. For the story of how our provider works, read how an Android passkey provider works. For the basics, start with passkeys from first principles.
Status key
Done: implemented and exercised on at least one real site. Partly: implemented for the common case, with a known gap. Not yet: not implemented. Not reviewed: we have not examined it closely enough to say. Statuses describe HostSpica Passkey in HostSpica Identity 1.0.0.
Registration: honouring the request
| Item | What WebAuthn expects | HostSpica Passkey |
|---|---|---|
| Site ID and challenge | The authenticator data contains the SHA-256 of the site's relying party ID, and the client data carries the site's challenge unchanged. | Done |
Algorithm list (pubKeyCredParams) | Pick the first algorithm in the site's list that the authenticator supports. If none is supported, fail with NotSupportedError. | Not yet: we always make an ES256 (P-256) key and do not read the list. Sites that list only another algorithm would fail. |
Excluded credentials (excludeCredentials) | If the authenticator already holds one of the listed credentials for this site, it must not create another (InvalidStateError). | Not yet: duplicates can be created. |
Discoverable credentials (residentKey) | Honour required; preferred and discouraged are preferences. | Done: we always create a discoverable credential, which satisfies all three. |
| User verification | When required, verify the user and set the UV flag only if that really happened. | Done: a BIOMETRIC_STRONG prompt bound to the signing operation, every time. |
| Attestation preference | If the site asks for none, return no identifying attestation and a zeroed authenticator ID. | Partly: we send android-key attestation when available and ignore the preference. In our fallback path to none, the authenticator ID is not zeroed. |
| Extensions | Process requested extensions the authenticator supports, such as credProps; ignore the rest. | Not yet: the response carries an empty clientExtensionResults object. |
Registration: what you return
| Item | What WebAuthn expects | HostSpica Passkey |
|---|---|---|
| Authenticator data layout | 32-byte site ID hash, 1 flags byte, 4-byte big-endian signature counter, then attested credential data: 16-byte authenticator ID, 2-byte credential ID length, credential ID, public key. | Done |
| Flags | UP (user present) and UV (user verified) set honestly; AT set when attested data is included; BE and BS (backup eligible and backed up) set to match the credential. If BE is 0, BS must be 0. | Done: UP, UV and AT set; BE and BS are 0 because our passkeys are device-bound. |
| Credential ID | At least 16 random bytes, at most 1,023. | Done: 32 random bytes. |
| Public key encoding | COSE_Key in CTAP2 canonical CBOR. For ES256: key type 2, algorithm -7, curve 1, then x and y, in canonical order. | Done: map keys written in the order 1, 3, -1, -2, -3. |
| Attestation object | A CBOR map with fmt, attStmt and authData in canonical order. For android-key, attStmt holds alg, sig and the certificate chain x5c. | Done |
| Authenticator ID (AAGUID) | A 16-byte model identifier. Authenticators registered in the FIDO Metadata Service use their registered value. | Partly: self-assigned and not registered with the FIDO Alliance. |
| User handle | Store the site's user handle (up to 64 bytes) and return it at sign-in for discoverable credentials. | Done |
Sign-in
| Item | What WebAuthn expects | HostSpica Passkey |
|---|---|---|
Allowed credentials (allowCredentials) | Only offer or use credentials the site listed. If the list is empty, any discoverable credential for the site may be used. | Not yet: we list every credential we hold for the site. |
| Signature | Sign the authenticator data followed by the client-data hash with the credential's private key. | Done |
| Signature counter | If the authenticator keeps a counter, increase it for every assertion so a site can notice cloned authenticators. | Done: increased and saved before the signature is returned. |
| User handle in the response | Included for discoverable credentials. | Done |
| User verification | As at registration. | Done |
The caller and the origin
| Item | What WebAuthn expects | HostSpica Passkey |
|---|---|---|
| Browsers | The browser knows the real origin. A provider should accept an origin claim only from callers it trusts. | Done: we check browsers against Google's published allowlist of privileged apps. |
| Client data | type, challenge and origin in the client data JSON, whose hash is what gets signed. | Done |
| Native-app callers | An app asking for a passkey must be tied to the site that owns it. | Not reviewed |
| Timeouts and cancellation | Honour the site's timeout and clean up if the user cancels. | Partly: we delete the new key if you cancel, but we do not implement the site's timeout. |
How to test any provider
Run these on webauthn.io or any site that supports passkeys, in at least two browsers. Each one targets a row above.
- Register, then sign in with the credential you just made.
- Register the same account twice. A correct provider refuses or warns instead of creating a second credential (excluded credentials).
- Sign in with an account that has two passkeys on two providers. Only the one the site allows should be usable (allowed credentials).
- Cancel the fingerprint prompt halfway through registration and check no half-made credential remains.
- Leave a prompt open until the site's timeout expires.
- Register on a site that offers only an algorithm other than ES256 and see whether the provider reports that it cannot (algorithm list).
- Use Chrome's DevTools WebAuthn panel with its virtual authenticator to see the exact data a site sends and expects, then compare it with what your provider returns.
What we plan to fix, in order
- Excluded credentials, so users do not end up with duplicate passkeys.
- Allowed credentials, so only the permitted credentials are offered.
- The algorithm list, returning a clear error when we cannot satisfy it.
- The attestation preference, including a zeroed authenticator ID in the fallback.
- The
credPropsextension.
We have set no dates. When an item ships, this page changes its status and says so.
Frequently asked questions
Why do some sites work even though items are missing?
Large sites send simple requests: they list ES256 first, usually send no excluded credentials for new accounts and do not insist on attestation. The gaps show up on sites with stricter or unusual requests.
Is a provider that skips these items insecure?
Not by itself. The signing, origin binding and key protection are what keep sign-in safe. The skipped items affect correctness and interoperability: duplicates, wrong credential choice, or a failed registration on an unusual site.
Does passing this checklist make a provider FIDO certified?
No. FIDO certification is a separate process run by the FIDO Alliance. HostSpica Passkey is not FIDO certified.
References
- W3C WebAuthn Level 2: authenticatorMakeCredential
- W3C WebAuthn Level 2: authenticatorGetAssertion
- W3C WebAuthn Level 2: Authenticator data
- W3C WebAuthn Level 2: Client data
- RFC 8949: CBOR, deterministic encoding
- IANA COSE algorithms registry
- FIDO CTAP 2.3 review draft (canonical CBOR, authenticator behaviour)
- Android Developers: Credential Manager provider documentation
Review status
Last technical self-review by the author on 3 October 2026. No independent reviewer yet. If you spot an error, write to [email protected] and we will correct it and note the change.
Rohan builds HostSpica's Android apps — Authenticator, Passkey and Identity — and writes up how they work, including the mistakes along the way.
ABOUT THE PRODUCTS
RELATED