zeroclaw-labs/zeroclaw · critical
Sign counter did not increase ({new_count} <= {}). Possible
Error message
Sign counter did not increase ({new_count} <= {}). Possible cloned authenticator. What it means
finish_authentication enforces WebAuthn clone detection: when counters are in use (the new count is > 0 or the stored credential sign_count is > 0), each authentication must strictly increase the signature counter. A counter that fails to increase indicates a replayed assertion or a cloned authenticator, and the error flags possible credential cloning.
Source
Thrown at crates/zeroclaw-runtime/src/security/webauthn.rs:467
// The signed message is: authenticatorData || SHA-256(clientDataJSON)
let client_data_hash = ring::digest::digest(&ring::digest::SHA256, &client_data_bytes);
let mut signed_data = auth_data_bytes.clone();
signed_data.extend_from_slice(client_data_hash.as_ref());
let public_key_bytes = URL_SAFE_NO_PAD
.decode(&credential.public_key)
.context("Invalid base64url in stored public key")?;
let sig_bytes = URL_SAFE_NO_PAD
.decode(&response.signature)
.context("Invalid base64url in signature")?;
verify_es256_signature(&public_key_bytes, &signed_data, &sig_bytes)?;
// 5. Verify and update sign counter (clone detection)
if new_count > 0 || credential.sign_count > 0 {
anyhow::ensure!(
new_count > credential.sign_count,
"Sign counter did not increase ({new_count} <= {}). Possible cloned authenticator.",
credential.sign_count
);
}
// Update the sign counter
if let Some(user_creds) = all_credentials.get_mut(&credential.user_id)
&& let Some(cred) = user_creds
.iter_mut()
.find(|c| c.credential_id == response.id)
{
cred.sign_count = new_count;
}
self.save_all_credentials(&all_credentials)?;
Ok(())
}View on GitHub (pinned to 88bb9c8533)
Solutions
- Treat as a security event: reject the login, audit-log it, and offer the user re-registration of the credential
- Make the finish endpoint idempotent per ceremony (one challenge = one successful finish) so replays cannot reach counter validation
- In tests, increment the authenticator model's sign counter between assertions
- If a specific authenticator model is known to never increment counters, prefer counter=0 consistently rather than mixed values
Defensive patterns
Strategy: try-catch
Try / catch
catch this error separately from other auth failures: deny the login (401), invalidate the session, write an audit event (user, credential id, counters), and notify/offer re-registration — never retry and never bypass the counter check
Prevention
- Make the finish endpoint idempotent per ceremony so double-submits are dropped before signature verification
- Disable automatic retries of auth-finish requests in frontend HTTP clients
- In test harnesses, always increment the sign counter between assertions
- Monitor for repeat counter failures per credential — a pattern indicates cloning or a firmware bug
When it happens
Trigger: The same assertion being processed twice (double-submit, replayed request, missing idempotency); a genuinely cloned credential used on two devices; some authenticators with buggy counters that plateau; restoring a passkey from a backup onto a second device; test harnesses always signing with the same counter value.
Common situations: Retry logic in the frontend resubmitting a finish request; proxy retries duplicating a POST; users restoring synced passkeys across devices; QA fixtures reusing canned assertions.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- security.otp.cache_valid_secs must be greater than or equal
- security.otp.gated_actions[{i}] contains invalid characters:
- AgentScopedMemory refuses purge_namespace: cross-agent bulk
- memory write blocked by content scan: {kinds}
- xAI OAuth discovery returned non-HTTPS {label}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/e707d41feeb47be7.
Report an issue: GitHub.