{"record":{"id":"3594a1e1a440ecc9","repo":"zeroclaw-labs/zeroclaw","slug":"authenticator-data-does-not-assert-user-presence","errorCode":null,"errorMessage":"Authenticator data does not assert user presence","messagePattern":"Authenticator data does not assert user presence","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/security/webauthn.rs","lineNumber":599,"sourceCode":"            .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\nfn extract_public_key_from_attestation(attestation_bytes: &[u8]) -> Result<(Vec<u8>, u32)> {\n    // Try JSON format first (from our enrollment UI)\n    if let Ok(att) = serde_json::from_slice::<AttestationObject>(attestation_bytes) {\n        let pk = URL_SAFE_NO_PAD","sourceCodeStart":581,"sourceCodeEnd":617,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/webauthn.rs#L581-L617","documentation":"The flags byte at auth_data[32] must have the User Presence (UP, 0x01) bit set. UP proves the authenticator physically confirmed the user (touch, biometric, PIN entry) during this assertion. The library requires it unconditionally, so an assertion without UP is rejected as unauthenticated.","triggerScenarios":"finish_authentication receives authenticatorData whose flags byte has bit 0 clear: synthetic test fixtures that zero the flags, authenticators or platform flows that skip the user gesture, assertions replayed from a ceremony where the token was not tapped, or hand-built assertions from a test harness.","commonSituations":"Unit tests constructing authenticator data by hand (they forget to OR in 0x01), NFC security keys that time out before the tap, browsers or WebAuthn polyfills that misreport flags, conditional-mediation/silent flows on non-compliant authenticators.","solutions":["In tests, set the UP bit when building authenticator data: flags |= 0x01 before signing.","On real hardware, make sure the user actually performs the gesture (tap/biometric) and that the client requests userVerification appropriately.","If a specific authenticator never sets UP, treat it as non-compliant for this flow; UP is mandatory and cannot be relaxed via configuration.","Verify the signed bytes match the authenticatorData sent (a mismatch often indicates the client signed a different blob than it returned)."],"exampleFix":"// before: synthetic fixture with zeroed flags\nlet mut auth_data = [0u8; 37];\nauth_data[..32].copy_from_slice(&rp_id_hash);\n// flags byte left as 0 -> assertion rejected\n\n// after: assert user presence in the fixture\nlet mut auth_data = [0u8; 37];\nauth_data[..32].copy_from_slice(&rp_id_hash);\nauth_data[32] = 0x01; // AUTHENTICATOR_FLAG_UP\nauth_data[33..37].copy_from_slice(&sign_count.to_be_bytes());","handlingStrategy":"validation","validationCode":"const AUTHENTICATOR_FLAG_UP: u8 = 0x01;\n\nfn user_presence_asserted(encoded: &str) -> Option<bool> {\n    let bytes = base64url::decode(encoded).ok()?;\n    (bytes.len() >= 33).then(|| bytes[32] & AUTHENTICATOR_FLAG_UP != 0)\n}","typeGuard":null,"tryCatchPattern":"match webauthn::finish_authentication(&cred, &assertion, rp_id).await {\n    Err(e) if e.to_string().contains(\"does not assert user presence\") => {\n        deny_login(\"user presence not confirmed\"); // policy: UP is mandatory, no bypass\n        Err(e)\n    }\n    other => other,\n}","preventionTips":["When building test authenticators, always set the UP bit (flags |= 0x01) before signing.","Smoke-test each supported authenticator model once; drop ones that never assert UP from your compatibility list.","Do not add a config to relax UP; treat the check as a fixed security property."],"tags":["webauthn","authentication","user-presence","flags","security"],"backgroundTag":"webauthn-user-presence-not-asserted","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}