Skip to main content

webauthn/
authentication.rs

1//! Authentication ceremony — W3C WebAuthn §7.2.
2//!
3//! The authentication ceremony is how a user proves possession of a previously
4//! registered credential. The relying party's job:
5//!
6//! 1. Verify the response was produced for *this* challenge and *this* origin.
7//! 2. Verify the authenticator data is bound to *this* RP ID.
8//! 3. Verify the ECDSA signature over `authData || SHA-256(clientDataJSON)`.
9//! 4. Check the sign count to detect cloned authenticators.
10//!
11//! Spec: <https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion>
12
13use crate::authenticator_data;
14use crate::challenge::CHALLENGE_MAX_AGE_SECS;
15use crate::client_data;
16use crate::credential::{AuthenticationResult, Challenge, Credential, PublicKey};
17use crate::crypto::{
18    rsa_components_to_der, sha256, verify_eddsa, verify_es256, verify_es384, verify_rs256,
19};
20use crate::error::{Result, WebAuthnError};
21use crate::registration::RelyingParty;
22
23/// The browser's response after a `navigator.credentials.get()` call.
24///
25/// All fields carry **raw decoded bytes** — base64url decoding must happen
26/// outside the library before constructing this struct.
27#[derive(Debug, Clone)]
28pub struct AuthenticatorAssertionResponse {
29    /// Raw UTF-8 bytes of the `clientDataJSON` object.
30    pub client_data_json: Vec<u8>,
31
32    /// Raw bytes of the `authenticatorData` structure.
33    pub authenticator_data: Vec<u8>,
34
35    /// DER-encoded ECDSA signature bytes.
36    pub signature: Vec<u8>,
37
38    /// Optional raw user handle bytes (some authenticators omit it).
39    pub user_handle: Option<Vec<u8>>,
40}
41
42impl RelyingParty {
43    /// Verify an authentication ceremony response (W3C WebAuthn §7.2).
44    ///
45    /// Call this after the client returns an `AuthenticatorAssertionResponse`.
46    /// On `Ok`, update the stored credential's `sign_count` to
47    /// `result.new_sign_count` before responding to the client.
48    ///
49    /// # Arguments
50    /// * `stored_credential` — Retrieved from your database by credential ID.
51    /// * `challenge`         — The challenge you issued for this ceremony.
52    /// * `response`          — The assertion response from the authenticator.
53    ///
54    /// # Errors
55    /// Returns a [`crate::error::WebAuthnError`] variant indicating exactly which
56    /// verification step failed, including `SignCountInvalid` for suspected
57    /// authenticator clones.
58    pub fn verify_authentication(
59        &self,
60        stored_credential: &Credential,
61        challenge: &Challenge,
62        response: &AuthenticatorAssertionResponse,
63    ) -> Result<AuthenticationResult> {
64        verify_authentication_inner(self, stored_credential, challenge, response)
65    }
66}
67
68// ─── Ceremony implementation ──────────────────────────────────────────────────
69
70fn verify_authentication_inner(
71    rp: &RelyingParty,
72    stored_credential: &Credential,
73    challenge: &Challenge,
74    response: &AuthenticatorAssertionResponse,
75) -> Result<AuthenticationResult> {
76    // ── Pre-check: challenge expiry ───────────────────────────────────────────
77    if challenge.is_expired(CHALLENGE_MAX_AGE_SECS) {
78        return Err(WebAuthnError::ChallengeExpired);
79    }
80
81    // ── §7.2 step 11 ─────────────────────────────────────────────────────────
82    // Parse clientDataJSON bytes (already raw UTF-8).
83    let parsed_cd = client_data::parse_client_data(&response.client_data_json)?;
84
85    // ── §7.2 step 13 ─────────────────────────────────────────────────────────
86    // Verify type == "webauthn.get".
87    // ── §7.2 step 14 ─────────────────────────────────────────────────────────
88    // Verify the challenge matches.
89    // ── §7.2 step 15 ─────────────────────────────────────────────────────────
90    // Verify the origin matches rp.origin.
91    // ── §7.2 step 12 ─────────────────────────────────────────────────────────
92    // If reject_cross_origin is set, reject crossOrigin: true.
93    client_data::validate_client_data(
94        &parsed_cd,
95        "webauthn.get",
96        &challenge.bytes,
97        &rp.allowed_origins,
98        rp.reject_cross_origin,
99    )?;
100
101    // ── §7.2 step 17 ─────────────────────────────────────────────────────────
102    // Let hash be SHA-256(clientDataJSON bytes).
103    let client_data_hash = sha256(&parsed_cd.raw_json);
104
105    // ── §7.2 step 18 ─────────────────────────────────────────────────────────
106    // Parse the authenticator data binary structure.
107    let auth_data = authenticator_data::parse_authenticator_data(&response.authenticator_data)?;
108
109    // ── §7.2 step 19 ─────────────────────────────────────────────────────────
110    // Verify rpIdHash = SHA-256(stored credential's rp_id).
111    let expected_rp_id_hash = sha256(stored_credential.rp_id.as_bytes());
112    if auth_data.rp_id_hash != expected_rp_id_hash {
113        return Err(WebAuthnError::RpIdHashMismatch);
114    }
115
116    // ── §7.2 step 20 ─────────────────────────────────────────────────────────
117    // Verify the User Present (UP) flag.
118    if !auth_data.flags.user_present {
119        return Err(WebAuthnError::UserNotPresent);
120    }
121
122    // ── §7.2 step 21 ─────────────────────────────────────────────────────────
123    // If the RP requires user verification, the UV flag must be set.
124    if rp.require_user_verification && !auth_data.flags.user_verified {
125        return Err(WebAuthnError::UserNotVerified);
126    }
127
128    // ── §7.2 step 21 — Backup Eligibility policy ──────────────────────────────
129    if rp.reject_backup_eligible && auth_data.flags.backup_eligible {
130        return Err(WebAuthnError::BackupEligibleNotAllowed);
131    }
132    if rp.require_backup_eligible && !auth_data.flags.backup_eligible {
133        return Err(WebAuthnError::BackupEligibilityRequired);
134    }
135
136    // ── §7.2 step 21 — Backup Eligibility consistency ─────────────────────────
137    // BE is immutable per spec — a mismatch between the stored registration value
138    // and the current assertion signals a credential substitution attempt.
139    if auth_data.flags.backup_eligible != stored_credential.backup_eligible {
140        return Err(WebAuthnError::BackupEligibilityChanged);
141    }
142
143    // ── §7.2 step 24 ─────────────────────────────────────────────────────────
144    // Verify the signature over: authData || SHA-256(clientDataJSON).
145    //
146    // ES256 is ECDSA-P256-SHA256: ring hashes the *message* internally.
147    // The message is `authData_bytes || clientDataHash` (not pre-hashed again).
148    let mut signed_data = auth_data.raw.clone();
149    signed_data.extend_from_slice(&client_data_hash);
150
151    // §7.2 step 24
152    match &stored_credential.public_key {
153        PublicKey::ES256 { x, y } => {
154            // Reconstruct the 65-byte uncompressed point ring expects.
155            let mut pk = Vec::with_capacity(65);
156            pk.push(0x04);
157            pk.extend_from_slice(x);
158            pk.extend_from_slice(y);
159            verify_es256(&pk, &signed_data, &response.signature)?;
160        }
161        PublicKey::ES384 { x, y } => {
162            // Reconstruct the 97-byte uncompressed P-384 point ring expects.
163            let mut pk = Vec::with_capacity(97);
164            pk.push(0x04);
165            pk.extend_from_slice(x);
166            pk.extend_from_slice(y);
167            verify_es384(&pk, &signed_data, &response.signature)?;
168        }
169        PublicKey::EdDSA(pk) => {
170            // Ed25519 signature is raw 64 bytes; ring processes the message directly.
171            verify_eddsa(pk, &signed_data, &response.signature)?;
172        }
173        PublicKey::RS256 { n, e } => {
174            let der = rsa_components_to_der(n, e)?;
175            verify_rs256(&der, &signed_data, &response.signature)?;
176        }
177    }
178
179    // ── §7.2 step 25 ─────────────────────────────────────────────────────────
180    // Verify the sign count is strictly greater than the stored value.
181    //
182    // Per spec: if either counter is non-zero, the received counter must exceed
183    // the stored one. Both being 0 indicates a counter-less authenticator, which
184    // is accepted. A stored non-zero counter with received=0 is rejected: it
185    // could indicate a wrap-around (overflow) or a counter-less authenticator
186    // being substituted for a counter-bearing one — both are suspicious.
187    let received = auth_data.sign_count;
188    let stored = stored_credential.sign_count;
189
190    if (stored > 0 || received > 0) && received <= stored {
191        return Err(WebAuthnError::SignCountInvalid { stored, received });
192    }
193
194    Ok(AuthenticationResult {
195        credential_id: stored_credential.id.clone(),
196        new_sign_count: received,
197        user_present: auth_data.flags.user_present,
198        user_verified: auth_data.flags.user_verified,
199        backup_eligible: auth_data.flags.backup_eligible,
200        backup_state: auth_data.flags.backup_state,
201        extensions: auth_data.extensions,
202    })
203}