Skip to main content
Passkeys (FIDO2 / WebAuthn) are phishing-resistant credentials backed by the user’s device’s secure enclave or hardware key. Pylon ships a complete WebAuthn server implementation in the binary: challenge mint, signature verification (ES256 + Ed25519), counter-regression detection, and key management endpoints. You do not add @simplewebauthn or webauthn-rs.

Supported algorithms

The verify path supports the two algorithms every shipping authenticator implements:
  • ES256 (alg=-7) — ECDSA P-256, the default for Apple / iCloud Keychain, 1Password, most YubiKeys.
  • Ed25519 (alg=-8) — Edwards-curve, used by newer Linux + Android authenticators.
RS256, EdDSA curve negotiation beyond Ed25519, and the attestation chain are deliberately not implemented. Every authenticator in practical use signs with one of the two supported algorithms. Pylon verifies the assertion path that sign-in needs.

Endpoints

All under /api/auth/:

Registering a passkey

The user must already be signed in (this is a credential add for an existing account).
The challenge is single-use. verify_registration consumes the matching record from the PasskeyStore on every call (success or failure), so a replay attempt gets 401 PASSKEY_REGISTER_FAILED. Pylon accepts only fmt = "none" attestation (the passkey default that Touch ID, Face ID, Windows Hello, and 1Password use). It rejects packed, tpm, and android-key because verifying them needs a FIDO MDS trust store that Pylon does not ship.

Signing in with a passkey

Pylon verifies the assertion server-side:
  1. Decode clientDataJSON and confirm origin == PYLON_WEBAUTHN_ORIGIN and type == "webauthn.get".
  2. Confirm the rpIdHash in authenticatorData matches SHA-256(PYLON_WEBAUTHN_RP_ID).
  3. Recompute the signed payload (authenticatorData || SHA-256(clientDataJSON)).
  4. ECDSA-P256 or Ed25519 verify against the stored public key.
  5. Counter-regression check: the new sign count must be strictly greater than the stored value (with 0 → 0 as the only allowed equality, for authenticators that don’t increment).
  6. Update sign_count + last_used_at; mint a session.
Failure modes:

Counter regression

WebAuthn authenticators increment a 32-bit counter on every signature. A clone of an authenticator (or a leaked private key) replays an old counter value. Pylon’s verify path rejects any assertion whose sign_count is less than or equal to the stored value. One exception: authenticators that do not implement counters keep emitting 0, which is permitted as long as the stored value is also 0. When a regression fires, the assertion is rejected with PASSKEY_VERIFY_FAILED. The right operational response is to revoke that credential and have the user re-register.

Listing + revoking keys

Response:
Revoke:
Only the credential’s owner can revoke it (caller’s user_id must match the stored passkey’s user_id).

Configuration

Defaults are localhost / https://localhost for dev. Production must set both. A mismatch between rpIdHash in the assertion and the configured RP_ID rejects every login with PASSKEY_VERIFY_FAILED. For subdomain apps, set RP_ID to the parent. Set example.com so credentials work across app.example.com and admin.example.com. Browsers enforce the same-registrable-domain rule client-side.

Composing the verify path

If you need a custom passkey flow (e.g., step-up auth for a specific high-value action), import the building block directly:
verify_assertion does the full ES256 / Ed25519 verify, counter check, and metadata update. The pylon-auth crate is Send + Sync, so you can call it from any handler.

Where to go next

  • TOTP / 2FA — software-token 2FA for accounts without a passkey
  • Trusted devices — remember-this-browser cookie that skips the second factor on re-login
  • Sessions — what /login/finish mints