Passkeys on iOS and Safari
Apple platforms were the first to ship synced passkeys at scale, and most of what surprises relying parties about them follows from that one decision. This page covers the four behaviours that differ from a naive expectation, what they mean for policy, and how native surfaces share credentials with the web. It sits under Cross-Platform Browser Behaviour.
Behaviour Reference
| Behaviour | What a relying party sees | Consequence |
|---|---|---|
| credentials sync to the platform account | backup bits both set | one credential is usually resilient |
| attestation is anonymous | a verifiable statement with no model identity | model allow-lists are not possible |
| no signature counter | zero on every assertion | counter-based clone detection unavailable |
| associated domains | native app uses the web RP ID | one credential works in app and browser |
| autofill via the platform surface | passkeys appear in the ordinary suggestion list | conditional mediation is well supported |
None of these are limitations to work around; they are the design, and building against them is the productive posture.
Root Cause Analysis
1. Expecting a device-bound credential. A passkey created on an Apple device is, by default, available on that user’s other devices. Policies written on the assumption that a platform credential stays put will not hold, and the backup bits will say so on every assertion.
2. Expecting attestation to identify a model. The Apple format is verifiable — it chains to an Apple root and binds to the ceremony through a nonce — and deliberately anonymous. Code that reads an AAGUID from it and looks the model up will find nothing useful.
3. Building on the signature counter. It is zero, always, which is permitted and expected for a synced credential. A policy treating a zero counter as suspicious will flag every Apple credential.
4. Treating the app and the site as separate relying parties. With an associated-domains file, a native application uses the same RP ID as the website, so credentials are shared. Configuring the app with its own identifier silos them and forces users to enrol twice.
5. Assuming a signature field in the attestation. The Apple format has none; the binding is a nonce in the certificate. A verifier written against packed attestation rejects every Apple registration with a confusing message about a missing field.
Step-by-Step Resolution
Step 1 — read the backup bits and act on them
They will report eligible and backed up. That is the signal that one credential is sufficient, and the reason not to prompt these users for a second one.
Step 2 — implement the Apple attestation path properly, or request none
If you have no consumer for a model claim, none is simpler and loses nothing. If you do, the verification is nonce-based rather than signature-based and needs its own branch.
Step 3 — skip the counter check on zero
On either side. It is not an anomaly and treating it as one produces a permanent false-positive stream.
Step 4 — configure associated domains for native surfaces
Host the association file at the well-known path on the domain that is your RP ID, and use the platform credential APIs in the app rather than a web view.
Step 5 — rely on autofill, with the button as backup
Conditional mediation is well supported here, and the passkey appears in the ordinary suggestion list. The explicit button still covers users whose credential is elsewhere.
Verification and Testing
The behaviours that matter here cannot be simulated, so this is checklist territory. Enrol on one device, then confirm the credential is offered on a second signed into the same platform account — that single check confirms sync, the backup bits and your handling of both.
Confirm your verification accepts an Apple attestation, using a captured fixture. A verifier that only handles packed will fail, and the failure message will not say why.
Confirm the counter path: complete two assertions and assert your code did not record an anomaly on either.
For a native surface, confirm the association file is reachable at the well-known path and that a credential created in the browser is offered in the app.
Pitfalls
1. Flagging a zero counter. Every synced credential reports one.
2. Requiring a device-bound credential. No request option achieves it here.
3. Giving the app its own RP ID. Silos credentials and doubles the enrolment burden.
Frequently Asked Questions
Can I prevent a passkey from syncing?
Not through the WebAuthn request. Syncing is governed by the user’s platform account settings, and the protocol reports the outcome through the backup bits rather than accepting a preference about it. Deployments that genuinely require non-syncable credentials use dedicated hardware authenticators.
Does the anonymous attestation prove anything useful?
It proves the credential was created by genuine Apple hardware, which is a real statement — just not one that identifies a model or a device. If your policy needs only “this is a legitimate platform authenticator”, it is sufficient; if it needs a specific model, no configuration will supply one.
Why does my attestation verifier reject Apple registrations?
Almost always because it expects a signature field the format does not have. The binding is a nonce embedded in the certificate, compared against a hash you compute from the ceremony, and the verification path is structurally different from packed.
How does a native app share credentials with the site?
Through an associated-domains configuration and a file hosted at a well-known path on your domain. The RP ID remains the web domain, so a credential enrolled in the browser is offered in the app and the other way round.
Should I still offer the cross-device flow to these users?
Yes. A user with an Apple passkey signing in on a machine that is not theirs — a borrowed laptop, a work computer with a different platform account — needs the phone-scanning flow exactly as much as anybody else.
Does autofill need anything special here?
Only the standard annotation on the username field and an armed conditional ceremony. The suggestion appears in the platform’s ordinary autofill list, which is why the experience feels native when it is wired up correctly and shows nothing at all when the annotation is missing.
What changes when a third-party password manager is installed?
The credential provider changes, and with it the chooser the user sees and the set of passkeys offered. From your implementation nothing differs: the ceremony runs through the same API, the assertion verifies identically, and the backup bits still describe whether the credential is synced — though now by the manager’s own mechanism rather than the platform’s. What changes is the support conversation, because a user describing an unfamiliar dialog is describing software you have never seen and cannot reproduce. The productive response is to confirm the credential works rather than to attempt to match the screenshot, and to keep your own settings page readable so the user can identify what they have from your side.
Do web views inside apps behave the same way?
No, and this is the most common source of unexplained failures on these platforms. A web view loading your site with a proper origin will run ceremonies normally, but a shell loading local content has no registrable domain and WebAuthn refuses outright. The fix is architectural rather than a workaround: native surfaces should use the platform credential APIs with an associated-domains configuration, which gives them the same credentials without attempting a browser ceremony at all.
Related
- Cross-Platform Browser Behaviour — the parent topic area and the four layers
- Passkeys on Android and Windows Hello — the other major platforms
- Apple and Android Attestation Formats — verifying the anonymous format
- Interpreting signCount Anomalies — why a zero counter is normal