Skip to content

Commit 7cfb890

Browse files
tcoratgerclaude
andauthored
docs(xmss): document verify() length-validation guards (#1121)
The verify() docstring enumerated the slot bound-check, codeword recomputation, chain completion, and Merkle rebuild, but omitted the two length-validation guards the body performs first: an exact-length check on the released hashes and on the path siblings. These guards defend against the SSZ offset-spoofing attack, where an equal-length byte string decodes into a list of the wrong arity. A malformed length now returns False up front, before the per-chain loop can index out of range. Add an explicit phase to the docstring for these guards and renumber the later phases so the docstring and the body phase labels stay one-to-one. Documentation only; no behavior change. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0c4b0ae commit 7cfb890

1 file changed

Lines changed: 8 additions & 4 deletions

File tree

src/lean_spec/spec/crypto/xmss/interface.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,11 @@ def verify(
287287
Phase 1: bound-check the slot.
288288
Phase 1 rejects without raising on bad input.
289289
Phase 2: recompute the codeword using the randomness carried by the signature.
290-
Phase 3: complete each Winternitz chain from the released hash to its endpoint.
291-
Phase 4: rebuild the Merkle root from the chain endpoints and the opening.
290+
Phase 3: reject a wrong number of released hashes or path siblings.
291+
These list lengths arrive from the wire with only an upper bound.
292+
An exact-length check rejects the malformed signature before the per-chain loop.
293+
Phase 4: complete each Winternitz chain from the released hash to its endpoint.
294+
Phase 5: rebuild the Merkle root from the chain endpoints and the opening.
292295
293296
Args:
294297
public_key: Public key.
@@ -315,6 +318,7 @@ def verify(
315318
if codeword is None:
316319
return False
317320

321+
# Phase 3: reject malformed list lengths before iterating over them.
318322
# The released chain count is recovered from the wire with only an upper bound.
319323
# An attacker can send fewer than one hash per chain.
320324
# Reject the malformed length here so the per-chain loop never indexes out of range.
@@ -327,7 +331,7 @@ def verify(
327331
if len(signature.path.siblings) != config.LOG_LIFETIME:
328332
return False
329333

330-
# Phase 3: finish each chain from the released hash to its endpoint.
334+
# Phase 4: finish each chain from the released hash to its endpoint.
331335
chain_ends: list[HashDigestVector] = []
332336
for chain_index, digit in enumerate(codeword):
333337
# The signature provides the digest after digit steps along the chain.
@@ -344,7 +348,7 @@ def verify(
344348
)
345349
chain_ends.append(end_digest)
346350

347-
# Phase 4: rebuild and compare against the trusted root.
351+
# Phase 5: rebuild and compare against the trusted root.
348352
return verify_path(
349353
poseidon=self.poseidon,
350354
config=config,

0 commit comments

Comments
 (0)