webauthn-kit
Standalone WebAuthn / FIDO2 passkey verification kit for Rust servers: a real,
custom CTAP2/COSE implementation on ring —
not a wrapper. Zero framework coupling: no storage, no HTTP types, no config
files.
- ES256 (ECDSA P-256 + SHA-256) and RS256 (RSA PKCS#1 v1.5 + SHA-256) signature verification
- CTAP2 authenticator data parsing (rpIdHash, flags, signCount, attested credential data)
- Full
verify_registration/verify_authenticationceremonies - Challenge generation from the OS CSPRNG, single-use consumption (replay protection), freshness/expiry enforcement, sign-count clone detection
#![forbid(unsafe_code)],#![deny(missing_docs)]
Quick start
[]
= "0.1"
use ;
let config = WebauthnConfig ;
// 1. Issue + store a single-use challenge, send options to the browser.
let mut store = new;
let =
store.generate_registration_challenge;
store.store_registration_challenge;
// 2. On the callback: consume the challenge (single use), then verify.
let = store.consume_registration_challenge?;
// let registration = webauthn_kit::verify_registration(
// &challenge_bytes, &resp.client_data_json, &resp.attestation_object,
// existing_credential_id, &config.rp_id, &config.rp_origins)?;
// 3. Store a credential record; for logins use verify_authentication and
// persist AuthenticationResult::new_sign_count (clone detection).
A complete synthetic registration→authentication roundtrip lives in
tests/vectors.rs; hostile-input property tests in
tests/fuzz.rs.
Storage contract (what this crate does NOT do)
This crate is deliberately storage-free. You own:
- Credential persistence. After
verify_registrationsucceeds, store theWebauthnCredential(ID, COSE public key, sign count, timestamps). After each successfulverify_authentication, atomically persistAuthenticationResult::new_sign_count— ideally compare-and-swap so concurrent logins cannot both bump the counter. - Challenge durability.
ChallengeStorekeeps pending challenges in memory; they are lost on restart (users simply retry). Always consume-then-verify so restarts can never enable replays. - User mapping, sessions, cookies/JWTs, UI, HTTP DTOs.
- Attestation verification — see below.
Security notes
- All cryptography is
ring(constant-time ECDSA/RSA verification). The only hand-written encoding is DER wrapping of already-validated RSA integers. - Every parser accepts hostile input and returns
Errinstead of panicking; this is enforced with property-based tests (proptest) over arbitrary bytes, truncations, and malformed CBOR. - Challenges are 32 bytes from the OS CSPRNG, consumed exactly once, with a configurable freshness window.
- Supported algorithms: ES256 (COSE −7), RS256 (COSE −257). Other
algorithms (OKP/Ed25519, ES384/512, P-384, RS1) are rejected with
UnsupportedAlgorithm. Key-type/algorithm mismatches (e.g. RSA key claiming ES256) are rejected. - Sign-count policy: a stored counter of
0disables the check (counterless authenticators); a strictly decreasing counter is rejected as evidence of a cloned authenticator. Equal counters are allowed by design. - The UV (user verification) flag is reported, never required — enforce
your own policy per ceremony from
user_verifiedin the results. - Error strings may echo attacker-controlled input fragments; log them server-side, don't forward them to clients.
Attestation
The attestation object is decoded to extract authenticator data, and the
fmt string is recorded informationally. Attestation statement signatures
are not verified — any fmt ("none", "packed", "fido-u2f",
"android-key", ...) is tolerated. Registration in this kit proves key
possession, not device provenance. If you need verified device provenance,
add an attestation verifier on top.
Features
| Feature | Default | Description |
|---|---|---|
std |
yes | Timed challenge store (std::time clock). |
serde |
no | Serde (de)serialization for the wire DTOs. |
License
Dual-licensed under MIT or Apache-2.0, at your option.
Security
Threat model: THREAT-MODEL.md.