Passki
A simple and secure WebAuthn/Passkey implementation for Rust.
Features
- โจ Simple API - Easy-to-use interface for passkey registration and authentication
- ๐ Multiple Algorithms - Support for EdDSA (Ed25519), ES256/ES384 (P-256/P-384), and RS256/RS384 (RSA)
- ๐ก๏ธ Security First - Built-in replay attack protection via signature counters
- ๐ฆ Framework Agnostic - No web framework lock-in, works with any HTTP server
- ๐ Extensions - Support for
credProps(discoverable credential reporting), PRF (key derivation / E2E encryption) andlargeBlob(blob storage on the authenticator) - ๐ Attestation - Statement verification for
packed,tpm,android-keyandfido-u2f, with opt-in trust path validation against your own roots - ๐ฆ Pure Rust - Memory-safe implementation with no unsafe code
Installation
Add this to your Cargo.toml:
[]
= "0.3"
Quick Start
use ;
let passki = new;
// Registration step 1: issue a challenge
let user_id = b"unique_user_identifier_12345"; // at least 16 bytes
let = passki.start_passkey_registration.expect;
// Send registration_challenge to the client as JSON, keep registration_state.
// Registration step 2: verify the credential the client created
let mut stored_passkey = passki.finish_passkey_registration?;
// Save stored_passkey in your database, associated with the user.
// Authentication step 1: issue a challenge
let = passki.start_passkey_authentication;
// Authentication step 2: verify the signature
let result = passki.finish_passkey_authentication?;
// Persist the new counter, or replay detection has nothing to compare against.
stored_passkey.counter = result.counter;
Supported Algorithms
Passki supports the following COSE algorithms:
- EdDSA (Ed25519) - Algorithm ID: -8
- ES256 (ECDSA with P-256 and SHA-256) - Algorithm ID: -7
- ES384 (ECDSA with P-384 and SHA-384) - Algorithm ID: -35
- RS256 (RSASSA-PKCS1-v1_5 with SHA-256) - Algorithm ID: -257
- RS384 (RSASSA-PKCS1-v1_5 with SHA-384) - Algorithm ID: -258
AAGUID
StoredPasskey::aaguid is the 16-byte identifier of the authenticator model - which YubiKey, which password manager. Look it up in the FIDO Metadata Service or a community AAGUID list.
All zero is the common case, and means there is no model to look up: under the default AttestationConveyancePreference::None the browser zeroes the AAGUID before passing the credential on. A non-zero value is worth acting on only once it has been validated, which is what Attestation sets up.
The same StoredPasskey also carries be (backup eligible - the credential is synced rather than bound to one device) and bs (currently backed up), both straight from the authenticator data flags.
Extensions
credProps
The credProps extension reports whether the authenticator created a discoverable (resident) credential - one stored on the device and usable in passwordless flows. Request it during registration; the result is stored in StoredPasskey::rk.
use ;
// Request credProps during registration
let mut extensions = default;
extensions.cred_props = Some;
let mut options = default;
options.extensions = Some;
let = passki.start_passkey_registration?;
let passkey = passki.finish_passkey_registration?;
// passkey.rk == Some(true) โ discoverable credential created
// passkey.rk == Some(false) โ non-discoverable credential created
// passkey.rk == None โ authenticator did not report
PRF
The WebAuthn PRF extension lets a passkey derive deterministic secret bytes from the authenticator's internal HMAC-secret. This is useful for end-to-end encryption, per-user key derivation, and other scenarios where you need a stable secret tied to a specific passkey. Passki passes the outputs through without processing them.
use ;
// During registration, probe for PRF support
let mut extensions = default;
extensions.prf = Some;
let mut options = default;
options.extensions = Some;
let = passki.start_passkey_registration?;
// Check client_extension_results.prf.enabled in the credential before calling finish
// to know whether the authenticator supports PRF
// During authentication, request a PRF derivation for a given context
let mut extensions = default;
extensions.prf = Some;
let mut options = default;
options.extensions = Some;
let = passki.start_passkey_authentication;
// result.prf_first contains the derived key bytes (32 bytes)
// The same passkey + same context always yields the same bytes
largeBlob
The largeBlob extension stores a small opaque blob on the authenticator itself, such as an SSH key or a certificate. Registration only probes whether the credential can hold one; reads and writes happen in later authentication ceremonies, one per ceremony, and a write replaces whatever the credential held.
The blob is base64url in both directions, like the PRF inputs and outputs: encode what you write, and AuthenticationResult::large_blob hands back the decoded bytes.
use ;
// During registration, ask for a credential that can store a blob
let mut extensions = default;
extensions.large_blob = Some;
let mut options = default;
options.extensions = Some;
let = passki.start_passkey_registration?;
let passkey = passki.finish_passkey_registration?;
// passkey.large_blob_supported == Some(true) โ the credential can hold a blob
// Store it: the authenticator only reports this at registration
// During authentication, write a blob
let mut extensions = default;
extensions.large_blob = Some;
let mut options = default;
options.extensions = Some;
let = passki.start_passkey_authentication;
// result.large_blob_written == Some(true) โ the blob was stored
// A later ceremony reads it back
let mut extensions = default;
extensions.large_blob = Some;
// result.large_blob contains the decoded bytes
LargeBlobSupport::Required fails the registration when the authenticator cannot store a blob; Preferred creates the credential either way and reports what it got.
Attestation
Attestation is the authenticator proving its make and model - "genuine YubiKey 5 NFC" rather than "some passkey". Skip this section unless your policy depends on the hardware; most applications accept any passkey, and the defaults are already right for that.
Why the AAGUID needs it
By default the AAGUID is self-asserted. It comes out of authData, which the client controls. A malicious client can claim any AAGUID and mint an attestation certificate to match, and every check Passki performs by default will pass - those checks verify the statement against the certificate the statement itself supplied, which settles internal consistency and nothing else.
Trust path validation closes the gap: the x5c chain is validated against root certificates you supply out of band. Same shape as TLS - the peer sends leaf and intermediates, you hold the roots.
Two things are needed, and either one alone is useless:
AttestationConveyancePreference::Direct, so the browser sends a statement at allPasski::with_attestation_trust, so there is something to validate it against
Setup
use ;
const YUBICO_ROOT: & = include_bytes!;
let passki = new
.with_attestation_trust?;
let mut options = default;
options.attestation = Direct;
let = passki.start_passkey_registration?;
Requesting attestation makes some browsers show the user an extra consent prompt.
Result
finish_passkey_registration either fails, or returns a StoredPasskey whose attestation_type records what the statement was worth:
let passkey = passki.finish_passkey_registration?;
match passkey.attestation_type
attestation_type |
Meaning |
|---|---|
None |
fmt was none. No statement to assess. |
SelfAttested |
The credential key signed its own statement. Proves possession, not model. |
Unverified |
An x5c chain arrived but the policy is Ignore, so it was never validated. |
Basic or AttCa |
The chain validated up to one of your roots. aaguid is attested. |
Basic and AttCa differ in whether the vendor issues one certificate per production batch or one per device. Both mean the chain validated, so treat them alike.
Policies
| Policy | Statement with x5c (security key) |
Statement without x5c (synced passkey) |
|---|---|---|
Ignore (default) |
Accepted as Unverified |
Accepted |
VerifyWhenPresent |
Must chain to a root, else UntrustedAttestation |
Accepted |
Required |
Must chain to a root, else UntrustedAttestation |
MissingAttestationChain |
Ignore preserves pre-0.3 behaviour exactly. Required is effectively "security keys only": iCloud Keychain, Google Password Manager and 1Password return none attestation regardless of what is requested, so it excludes every phone and laptop passkey.
Anchors act as a vendor whitelist. Install only the Yubico root and a genuine Feitian key is rejected, because its chain ends at a root you do not have.
Trust anchors
Anchors are DER root certificates you supply. Passki bundles none and performs no network I/O.
For a fixed set of approved models, embed the vendor roots with include_bytes!. For broad coverage there is the FIDO Metadata Service, which publishes roots for every certified authenticator - but consuming it means fetching and verifying a signed JWT on a refresh schedule, which belongs in your application rather than in this crate.
What validation covers
Name chaining, the signature of every link, notBefore/notAfter, basicConstraints (CA flag and pathLenConstraint), and keyUsage/keyCertSign when present. An anchor is trusted by configuration, so its own validity period is not re-checked.
Revocation is not checked: attestation chains have no CRL or OCSP to consult. Certificate signatures are supported for ECDSA P-256/P-384 with SHA-256/384, RSA PKCS#1 v1.5 with SHA-256/384, and Ed25519; RSASSA-PSS is not.
Security Considerations
- ๐ Always use HTTPS in production - browsers refuse WebAuthn on insecure origins
- ๐ Store the counter returned by each authentication, or cloned authenticators go undetected
- ๐ Require user verification for sensitive operations
- โฑ๏ธ Keep ceremony timeouts short; the state stored between the two steps expires with them
Requirements
- Rust 1.85 or later (Edition 2024)
- HTTPS in production (required by WebAuthn specification)
Examples
The examples/ directory has complete registration and authentication flows for several web frameworks:
Actix-web | Axum | Poem | Rocket | Warp
All examples request both credProps and PRF during registration. Registration reports whether a resident key was created; authentication accepts an optional key context string (prf_salt) to derive a 32-byte key.
Then visit http://localhost:3000 in your browser.
WebAuthn Specification Levels
WebAuthn has three specification levels published by the W3C, plus extensions that other specifications define. Checkboxes mark features currently implemented in passki.
Level 1 (2019)
The initial recommendation. Defined the core protocol:
- Registration ceremony (
create) and authentication ceremony (get) - Challenge generation and binding
- Client data JSON origin verification
- Authenticator data parsing
- COSE public key extraction
- Signature verification (EdDSA/Ed25519, ES256/P-256, ES384/P-384, RS256, RS384)
- Signature counter tracking and replay detection
- Credential exclusion (
excludeCredentials) -
AttestationConveyancePreference(none/indirect/direct) - Attestation object CBOR parsing
- Attestation statement verification (
packed,tpm,android-key,fido-u2f) - rpId hash verification in authenticator data
- UP (user present) flag enforcement
- UV (user verified) flag enforcement
- AAGUID exposure
-
authenticatorAttachment(platform/cross-platform) - Attestation trust path validation
Level 2 (2021)
A substantial expansion, still the most widely implemented level today:
- Discoverable credentials / usernameless flows (empty
allowCredentials) -
ResidentKeyRequirement(discouraged/preferred/required) -
enterpriseattestation conveyance preference - Zero-counter authenticator support
-
credPropsextension -
largeBlobextension -
userHandlein authentication response -
transportson credential descriptors
Level 3 (Candidate Recommendation, not yet a W3C Recommendation)
Still under active development:
- PRF extension (
prf) - BE/BS flags (backup eligibility/state)
- Related origin requests
-
RegistrationResponseJSONandAuthenticationResponseJSONrequest shapes - Signal API
-
hints(security-key/client-device/hybrid) -
attestationFormats -
evalByCredentialin theprfextension -
authenticatorDisplayNamein thecredPropsextension -
remoteClientDataJSONextension -
compoundattestation statement format
Defined outside WebAuthn
These extensions are registered in the IANA WebAuthn extension identifiers registry but specified elsewhere, so they are not tied to a WebAuthn level:
-
credProtectextension (CTAP 2.1 ยง12.1) -
minPinLengthextension (CTAP 2.1 ยง12.4) -
paymentextension (Secure Payment Confirmation ยง5)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the Apache License, Version 2.0 (LICENSE).
Acknowledgments
Passki is built on top of aws-lc-rs for cryptographic operations.