Tightens the passkey-management endpoints against concurrent-mutation
races, stale challenges, and token-replay; routes the unauthenticated-
login path through atomic challenge consumption; and pins the behaviour
with new test coverage.
Atomic challenge consumption:
- `WebAuthnLoginChallenge::take` performs a transactional
SELECT+DELETE on the challenge row, replacing the previous "find
then delete" pattern that could leave a stale challenge consumable
across tabs.
- `TwoFactor::take_by_user_and_type` does the same for the
passkey-management registration / assertion challenges, replacing
the prior `find_by_user_and_type` + `tf.delete` pattern at every
passkey-management call site.
Challenge state binding:
- `passkey_management_challenge_is_fresh` enforces a TTL + clock-skew
window on persisted challenge rows.
- `passkey_registration_challenge_state` /
`passkey_assertion_challenge_state` enforce per-request token
equality, freshness, and user `security_stamp` equality before
unwrapping the saved webauthn-rs state. A password change
mid-ceremony invalidates the in-flight challenge; a stale tab
cannot consume a fresh one.
Identity.rs (`webauthn_login`):
- Consume the challenge via `WebAuthnLoginChallenge::take` before any
cryptographic verification, so a malformed `device_response` cannot
poison subsequent grant attempts that share the same token.
- AUTH_FAILED-uniformity: every pre-bind error path returns the same
generic "Passkey authentication failed" rather than leaking the
underlying cause (DB transient vs missing user vs malformed
assertion vs verification failure).
- Drop the local `AssertionResponseCopy` in favour of the shared
`PublicKeyCredentialCopy` from `core::two_factor::webauthn`, so the
passkey-login and 2FA-webauthn paths agree on the serde wire shape
for the assertion response.
Other:
- `ciphers.rs` / `accounts.rs`: `/sync` gates `webAuthnPrfOptions` on
`account_passkeys_allowed`; PRF rotation path tolerates a credential
that was removed mid-rotation.
- `vaultwarden.scss.hbs`: hide the lock-screen "Unlock with passkey"
button until PRF enrolment completes so the affordance only appears
when the server will actually accept it.
- `playwright/tests/passkey.spec.ts`: additional coverage for the
hardened flows.