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

  1. Treat as a security event: reject the login, audit-log it, and offer the user re-registration of the credential
  2. Make the finish endpoint idempotent per ceremony (one challenge = one successful finish) so replays cannot reach counter validation
  3. In tests, increment the authenticator model's sign counter between assertions
  4. 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

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

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/e707d41feeb47be7. Report an issue: GitHub.