{"record":{"id":"91dfdc34b045b8da","repo":"zeroclaw-labs/zeroclaw","slug":"challenge-mismatch-in-registration-response","errorCode":null,"errorMessage":"Challenge mismatch in registration response","messagePattern":"Challenge mismatch in registration response","errorType":"exception","errorClass":null,"httpStatus":400,"severity":"error","filePath":"crates/zeroclaw-runtime/src/security/webauthn.rs","lineNumber":283,"sourceCode":"        response: &RegisterCredentialResponse,\n    ) -> Result<WebAuthnCredential> {\n        // 1. Validate client data JSON\n        let client_data_bytes = URL_SAFE_NO_PAD\n            .decode(&response.client_data_json)\n            .context(\"Invalid base64url in client_data_json\")?;\n        let client_data: serde_json::Value =\n            serde_json::from_slice(&client_data_bytes).context(\"Invalid client data JSON\")?;\n\n        // Verify type\n        let cd_type = client_data[\"type\"].as_str().unwrap_or_default();\n        anyhow::ensure!(\n            cd_type == \"webauthn.create\",\n            \"Expected type 'webauthn.create', got '{cd_type}'\"\n        );\n\n        // Verify challenge matches\n        let cd_challenge = client_data[\"challenge\"].as_str().unwrap_or_default();\n        anyhow::ensure!(\n            cd_challenge == reg_state.challenge,\n            \"Challenge mismatch in registration response\"\n        );\n\n        // Verify origin\n        let cd_origin = client_data[\"origin\"].as_str().unwrap_or_default();\n        anyhow::ensure!(\n            cd_origin == self.config.rp_origin,\n            \"Origin mismatch: expected '{}', got '{cd_origin}'\",\n            self.config.rp_origin\n        );\n\n        // 2. Parse attestation object to extract public key and auth data\n        let attestation_bytes = URL_SAFE_NO_PAD\n            .decode(&response.attestation_object)\n            .context(\"Invalid base64url in attestation_object\")?;\n\n        // For \"none\" attestation, we extract the authData which contains the","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/webauthn.rs#L265-L301","documentation":"finish_registration compares the challenge inside client_data_json against the challenge stored in the pending AuthenticationState/registration state created by register start. A mismatch means the response was not produced for this ceremony instance — expired state, a second start overwriting the first, or encoding differences.","triggerScenarios":"Calling register start twice and finishing with options from the first call; the browser session restarting so server-side state was regenerated; encoding mismatches (raw vs base64url, padded vs unpadded challenge strings); replaying a captured finish payload.","commonSituations":"SPA flows that refetch registration options on re-render; multiple tabs each calling start; server storing one pending challenge per user that gets clobbered; clients re-encoding the challenge before hashing.","solutions":["Restart the ceremony: call register start, then immediately finish with the fresh challenge","Ensure exactly one start per registration attempt and that finish uses the options from that same start","Compare challenges using the identical base64url (no padding) encoding on both client and server"],"exampleFix":"// before: stale options reused after a re-render\nconst regOptions = cachedFromLastPageLoad;\n// after: fetch fresh options, then create the credential\nconst regOptions = await fetch('/register/start').then(r => r.json());","handlingStrategy":"validation","validationCode":"// client: create the credential with the exact challenge bytes from the latest start call\nconst opts = await fetch('/register/start', { cache: 'no-store' }).then(r => r.json());\nconst cred = await navigator.credentials.create({ publicKey: opts });\n// server: confirm state freshness\nif reg_state.created_at + TTL < now { /* re-run start instead of finish */ }","typeGuard":null,"tryCatchPattern":"catch the mismatch; respond 400 with a 'restart-registration' code so the client calls start again, and invalidate the stored reg_state","preventionTips":["Fetch registration options with cache: 'no-store' right before creating the credential","Store one pending ceremony per user/session and expire it (TTL) so stale challenges are rejected early","Use a single base64url encoding (unpadded) for challenges across client and server"],"tags":["webauthn","challenge","registration","session-state","rust"],"backgroundTag":"webauthn-challenge-mismatch","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}