zeroclaw-labs/zeroclaw · error
Authenticator data does not assert user presence
Error message
Authenticator data does not assert user presence
What it means
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.
Source
Thrown at crates/zeroclaw-runtime/src/security/webauthn.rs:599
.context("Failed to set credentials file permissions")?;
}
Ok(())
}
}
fn validate_assertion_authenticator_data(auth_data: &[u8], rp_id: &str) -> Result<u32> {
anyhow::ensure!(
auth_data.len() >= AUTHENTICATOR_DATA_FIXED_LEN,
"Authenticator data is shorter than the required fixed fields"
);
let expected_rp_id_hash = ring::digest::digest(&ring::digest::SHA256, rp_id.as_bytes());
anyhow::ensure!(
&auth_data[..32] == expected_rp_id_hash.as_ref(),
"Authenticator data relying party ID hash mismatch"
);
anyhow::ensure!(
auth_data[32] & AUTHENTICATOR_FLAG_UP != 0,
"Authenticator data does not assert user presence"
);
Ok(u32::from_be_bytes([
auth_data[33],
auth_data[34],
auth_data[35],
auth_data[36],
]))
}
// ── Attestation parsing ─────────────────────────────────────────
fn extract_public_key_from_attestation(attestation_bytes: &[u8]) -> Result<(Vec<u8>, u32)> {
// Try JSON format first (from our enrollment UI)
if let Ok(att) = serde_json::from_slice::<AttestationObject>(attestation_bytes) {
let pk = URL_SAFE_NO_PADView on GitHub (pinned to 88bb9c8533)
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).
Example fix
// before: synthetic fixture with zeroed flags let mut auth_data = [0u8; 37]; auth_data[..32].copy_from_slice(&rp_id_hash); // flags byte left as 0 -> assertion rejected // after: assert user presence in the fixture let mut auth_data = [0u8; 37]; auth_data[..32].copy_from_slice(&rp_id_hash); auth_data[32] = 0x01; // AUTHENTICATOR_FLAG_UP auth_data[33..37].copy_from_slice(&sign_count.to_be_bytes());
Defensive patterns
Strategy: validation
Validate before calling
const AUTHENTICATOR_FLAG_UP: u8 = 0x01;
fn user_presence_asserted(encoded: &str) -> Option<bool> {
let bytes = base64url::decode(encoded).ok()?;
(bytes.len() >= 33).then(|| bytes[32] & AUTHENTICATOR_FLAG_UP != 0)
} Try / catch
match webauthn::finish_authentication(&cred, &assertion, rp_id).await {
Err(e) if e.to_string().contains("does not assert user presence") => {
deny_login("user presence not confirmed"); // policy: UP is mandatory, no bypass
Err(e)
}
other => other,
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Authenticator data is shorter than the required fixed fields
- Authenticator data relying party ID hash mismatch
- Invalid OTP code; estop resume denied
- Expected type 'webauthn.create', got '{cd_type}'
- No registered credentials for user '{user_id}'
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3594a1e1a440ecc9.
Report an issue: GitHub.