{"record":{"id":"0c2ab728327a9800","repo":"zeroclaw-labs/zeroclaw","slug":"authenticator-data-relying-party-id-hash-mismatch","errorCode":null,"errorMessage":"Authenticator data relying party ID hash mismatch","messagePattern":"Authenticator data relying party ID hash mismatch","errorType":"exception","errorClass":null,"httpStatus":401,"severity":"error","filePath":"crates/zeroclaw-runtime/src/security/webauthn.rs","lineNumber":595,"sourceCode":"            std::fs::set_permissions(\n                &self.credentials_path,\n                std::fs::Permissions::from_mode(0o600),\n            )\n            .context(\"Failed to set credentials file permissions\")?;\n        }\n\n        Ok(())\n    }\n}\n\nfn validate_assertion_authenticator_data(auth_data: &[u8], rp_id: &str) -> Result<u32> {\n    anyhow::ensure!(\n        auth_data.len() >= AUTHENTICATOR_DATA_FIXED_LEN,\n        \"Authenticator data is shorter than the required fixed fields\"\n    );\n\n    let expected_rp_id_hash = ring::digest::digest(&ring::digest::SHA256, rp_id.as_bytes());\n    anyhow::ensure!(\n        &auth_data[..32] == expected_rp_id_hash.as_ref(),\n        \"Authenticator data relying party ID hash mismatch\"\n    );\n    anyhow::ensure!(\n        auth_data[32] & AUTHENTICATOR_FLAG_UP != 0,\n        \"Authenticator data does not assert user presence\"\n    );\n\n    Ok(u32::from_be_bytes([\n        auth_data[33],\n        auth_data[34],\n        auth_data[35],\n        auth_data[36],\n    ]))\n}\n\n// ── Attestation parsing ─────────────────────────────────────────\n","sourceCodeStart":577,"sourceCodeEnd":613,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/webauthn.rs#L577-L613","documentation":"The first 32 bytes of authenticator data must equal SHA-256(rp_id). validate_assertion_authenticator_data recomputes the hash of the configured RP ID and compares; a mismatch means the credential was produced for a different relying party (or the blob is corrupt), so the assertion is rejected to prevent cross-site credential use.","triggerScenarios":"finish_authentication invoked with an rp_id different from the one used at registration (e.g. registered on \"example.com\", asserting against \"api.example.com\" or \"localhost\"), a frontend served from a different origin than the configured RP ID, or a credential key copied from another deployment.","commonSituations":"Domain changes during deployment (apex vs www), running the web UI on localhost while the server expects the production host, staging and production sharing a credential database, RP ID typos in config, proxy frontends that change the effective origin.","solutions":["Compare the rp_id passed to finish_authentication with the rp_id stored on the credential record from registration; they must be byte-identical.","Use the eTLD+1 of the origin the browser actually runs on (\"example.com\" covers app.example.com; \"localhost\" only works on localhost).","If the RP ID changed intentionally, old credentials cannot be salvaged: delete them and re-run the registration ceremony.","Check for payload corruption if the RP ID is definitely correct (hash mismatch can also indicate mangled authenticatorData)."],"exampleFix":"// before: asserting with a hardcoded host that differs from registration\nwebauthn::finish_authentication(&cred, &assertion, \"api.example.com\").await?;\n\n// after: always derive rp_id from the stored credential / single config source\nlet rp_id = cred.rp_id.clone(); // recorded at begin_registration time\nwebauthn::finish_authentication(&cred, &assertion, &rp_id).await?;","handlingStrategy":"validation","validationCode":"fn rp_id_matches_registration(cred: &Credential, rp_id: &str) -> bool {\n    cred.rp_id == rp_id\n}\n\n// derive the assertion rp_id from the stored credential, never from the request:\nlet rp_id = cred.rp_id.as_str();\nassert!(rp_id_matches_registration(&cred, rp_id));","typeGuard":null,"tryCatchPattern":"match webauthn::finish_authentication(&cred, &assertion, &cred.rp_id).await {\n    Err(e) if e.to_string().contains(\"relying party ID hash mismatch\") => {\n        // credential belongs to another RP: prompt re-registration, never auto-retry\n        prompt_re_registration(&user).await;\n        Err(e)\n    }\n    other => other,\n}","preventionTips":["Store the rp_id on the credential at registration and reuse that exact value for assertions.","Keep one config constant for the public origin/RP ID shared by frontend and backend.","When the domain changes, plan a re-registration campaign; WebAuthn credentials are not portable across RP IDs."],"tags":["webauthn","authentication","rp-id","origin-mismatch","security"],"backgroundTag":"webauthn-rp-id-mismatch","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}