From 61326c4d61ca1e357bbb60bf6ede16211ba34cf9 Mon Sep 17 00:00:00 2001 From: Emanuele Cesena Date: Sat, 23 May 2026 14:04:39 -0400 Subject: [PATCH] =?UTF-8?q?ctap2.3:=20Reset=20clears=20all=20=C2=A76.7=20f?= =?UTF-8?q?eature=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/state.rs | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/state.rs b/src/state.rs index 3223000..7ce6493 100644 --- a/src/state.rs +++ b/src/state.rs @@ -360,11 +360,20 @@ impl PersistentState { self.consecutive_pin_mismatches = 0; self.pin_hash = None; self.timestamp = 0; - // CTAP 2.1 §6.7 authenticatorReset: "Always Require User Verification" - // is explicitly listed as a feature that MUST be reset. Other §6.7 - // feature flags (min_pin_length / min_pin_length_rp_ids / - // force_pin_change) are left for a follow-up — see AUDIT.md. + // CTAP 2.1 §6.7 authenticatorReset MUST reset the following features: + // - "Always Require User Verification" (alwaysUv) + // - "Set Minimum PIN Length" — its three pieces of state: + // * `minPINLength` → back to default (the `Default` impl + // leaves the raw field at 0, which our `min_pin_length()` + // getter reads as DEFAULT_MIN_PIN_LENGTH) + // * `minPinLengthRPIDs` → empty + // * `forcePINChange` → false + // - Enterprise attestation (we don't support it, so no state to + // clear). self.always_uv = false; + self.min_pin_length = 0; + self.min_pin_length_rp_ids = heapless::Vec::new(); + self.force_pin_change = false; self.save(trussed) } @@ -638,6 +647,21 @@ impl RuntimeState { self.clear_credential_cache(); self.active_get_assertion = None; + // Clear any in-flight credMgmt enumeration cursors. Otherwise a + // `next_relying_party`/`next_credential` call straight after + // `authenticatorReset` succeeds with stale data instead of + // returning `NotAllowed` (caught by fido2-tests + // test_rpnext_without_rpbegin). + self.cached_rp = None; + self.cached_rk = None; + + // The per-power-cycle pinAuthFailedAttempts counter is runtime + // state and lives here. `authenticatorReset` removes the PIN + // entirely (caller resets the persistent retries counter via + // `PersistentState::reset`), so the per-power-cycle counter + // should drop with it. + self.consecutive_pin_mismatches = 0; + if let Some(pin_protocol) = self.pin_protocol.take() { pin_protocol.reset(trussed); }