juice-safe-v6 WebAuthn review: one assertion authorizes three operations

A caller-controlled clientDataJSON split checks challenge inclusion rather than equality, allowing one genuine passkey assertion to validate three distinct operation hashes.

juice-safe-v6 WebAuthn review

Executive summary

I found a novel WebAuthn challenge-validation flaw in the deployed juice-safe-v6 authorization path. The wallet does not verify that clientDataJSON.challenge equals the one expected 32-byte operation hash. Instead, the helper verifies a caller-selected prefix || base64url(expectedHash) || suffix reconstruction.

A single genuine browser assertion can therefore contain three different operation hashes inside one longer, valid WebAuthn challenge. By changing only the caller-supplied prefix/suffix split, the same authenticator data and P-256 signature verify for all three hashes. The replay map does not stop this because it is keyed separately by each operation hash.

No private key compromise or signature forgery is required. A malicious or compromised relying-party frontend can place multiple precomputed wallet operations behind one user-verification gesture.

H-01 — One WebAuthn assertion can authorize three distinct operations

Severity: High, with a conditional downgrade to Medium if every frontend and every origin permitted to exercise the parent-domain credential is assumed fully trusted.

Affected code

  • juice-safe-v6.clar:1175–1204: verify-signature passes the expected operation message-hash and caller-supplied client-data-prefix / client-data-suffix to clarity-5-webauthn-v3.
  • juice-safe-v6.clar:1208–1223: consume-signature records replay use by message-hash, not by the signed assertion or client-data hash.
  • Deployed clarity-5-webauthn-v3.clar:45–63: base64url-32 encodes a 32-byte hash as 43 unpadded base64url bytes.
  • Deployed clarity-5-webauthn-v3.clar:87–119: the helper hashes prefix || base64url32(challenge) || suffix and checks the P-256 signature, without proving that this occurrence is the complete challenge member.

Contract sources:

Deterministic proof

Let m1, m2, and m3 be three distinct, valid operation message hashes built by the wallet’s existing SIP-018 helpers, and let:

h1 = base64url32(m1)
h2 = base64url32(m2)
h3 = base64url32(m3)

Each h is 43 bytes. Ask the browser for a normal WebAuthn assertion using challenge bytes whose canonical unpadded base64url representation is:

h1 || "A" || h2 || "A" || h3

This is a valid 131-character base64url string. Its final character is the canonical final character of a 32-byte base64url encoding, so its unused low bits are zero and the string decodes and round-trips canonically. WebAuthn challenges are arbitrary byte strings, so the browser signs the resulting ordinary clientDataJSON.

For the canonical serialization:

{"type":"webauthn.get","challenge":"<131 chars>","origin":"https://juiceofbtc.com","crossOrigin":false}

the fixed JSON prefix before the challenge is 36 bytes and the suffix after it is 56 bytes. The same signed clientDataJSON can be reconstructed three ways:

Wallet expects Prefix length Inserted by helper Suffix length Reconstructed bytes
m1 36 h1 144 full signed clientDataJSON
m2 80 h2 100 full signed clientDataJSON
m3 124 h3 56 full signed clientDataJSON

All three prefixes fit (buff 128) and all three suffixes fit (buff 512). For every row, compute-client-data-hash produces the same hash of the same browser-signed JSON, so the same authenticator-data, signature, and registered passkey verify.

The three calls then pass the replay guard independently:

(map-get? used-pubkey-authorizations m1) ;; none
(map-get? used-pubkey-authorizations m2) ;; none
(map-get? used-pubkey-authorizations m3) ;; none

Afterward, each distinct hash is marked once. The map prevents a fourth use of the same operation hash, but it does not recognize that all three authorizations came from one WebAuthn assertion.

Impact

A frontend can precompute three different wallet operations, place all three hashes in one browser challenge, obtain one user-verification gesture, then submit the same assertion against three passkey-authorized entrypoints with different prefix/suffix splits. The user has authenticated once, while the wallet treats it as three independent authorizations.

This breaks the intended one-challenge/one-operation binding and weakens the passkey as a second factor across transfer, configuration, ownership, recovery, veto, immediate-execution, and stacking paths that route through consume-signature. Existing threshold/timelock rules may limit particular combinations, but they do not restore per-operation user intent.

The W3C verification algorithm requires exact challenge equality, and its security guidance warns that tolerating a mismatch compromises the protocol:

Recommended fix

Do not accept a caller-selected substring boundary around the expected hash.

  1. Implement the WebAuthn Level 3 limited-verification algorithm or an equivalent full parser. Require the serialized challenge member to equal base64url32(message-hash) in its entirety.
  2. If fixed-template verification is preferred on-chain, require the exact canonical prefix through the opening challenge quote and validate the suffix beginning immediately with the closing challenge quote. Do not allow arbitrary bytes before or after the inserted hash inside the challenge string.
  3. Key replay protection on a digest of the accepted signed assertion (for example, the client-data hash plus authenticator data/signature) in addition to the operation hash, so one assertion cannot be consumed for multiple messages.
  4. Add regressions for: a valid single 32-byte challenge; a longer challenge containing multiple valid 43-byte hash strings; shifted prefix/suffix splits; and reuse of one assertion against two distinct operation hashes.

M-02 — Signed WebAuthn context is not validated

verify-signature checks the RP-ID hash and UV flag, but neither it nor clarity-5-webauthn-v3 validates the signed client-data fields type, origin, crossOrigin, or topOrigin. They are opaque caller-supplied bytes.

This matters independently and also broadens H-01. A credential scoped to RP ID juiceofbtc.com can be exercised from a qualifying subdomain, while the relying party is still required to reject an unexpected signed origin. An unintended or compromised sibling subdomain can therefore obtain an RP-ID-correct, UV-valid assertion that the contract accepts.

The WebAuthn assertion-verification algorithm requires:

  • type == "webauthn.get";
  • exact challenge equality;
  • an expected origin;
  • expected cross-origin and topOrigin context; and
  • the expected RP-ID hash.

The contract currently performs only the last item plus UV. See WebAuthn Level 3 origin validation.

Recommended fix: validate an explicit allowlist of exact origins and enforce the expected type and cross-origin policy as part of the same exact client-data verification. Treat the parent RP ID and the allowed web origins as separate checks.

Scope and novelty note

I reviewed the four public submissions visible on the bounty before filing this report. They cover ambient tx-sender authority, token-lock asymmetry, threshold omissions, execute-now hash contents, ownership/passkey handoff, and gas accounting. None reports challenge multiplexing, non-exact client-data reconstruction, or missing origin/type/cross-origin validation.

This report deliberately does not restate those prior findings.


Write a comment