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, key management endpoints. No @simplewebauthn or webauthn-rs to bolt on.

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, and pylon focuses on the assertion path that matters for sign-in.

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 only accepts fmt = "none" attestation (the passkey default — Touch ID, Face ID, Windows Hello, 1Password all use it); packed / tpm / android-key are rejected because verifying them needs a FIDO MDS trust store that’s not shipped.

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) would replay an old counter value — Pylon’s verify path rejects any assertion whose sign_count is less than or equal to the stored value (with one exception: authenticators that legitimately don’t 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 will reject every login with PASSKEY_VERIFY_FAILED. For subdomain apps, RP_ID should be the parent — set to 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 — drop it into 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