QuipNetwork / QuipNetwork/hashsigs-solidity

F3 (low): verifyWithRandomizationElements accepts publicSeed then ignores it

Open
#3 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Solidity
Stars
11.2k
Forks
34
PR merge metrics
No merged PRs in 30d

Description

Filed publicly at the maintainer's request. Reported privately on 2026-08-10, acknowledged 2026-09-02. Informational: not exploitable per the analysis below, and not on the production path.

Summary

In contracts/WOTSPlus.sol, verifyWithRandomizationElements takes a full WinternitzAddress (i.e. publicSeed and publicKeyHash) but never reads publicSeed. It uses the caller-supplied randomizationElements verbatim and never checks them against generateRandomizationElements(quipAddress.publicSeed).

The bitmasks are what bind the hash chains to the public key, so a caller who forwards untrusted elements is no longer running WOTS+ as specified. A signature that verifies for address (S, H) verifies just as well for (S', H) with any S'.

Why it is Low, not a forgery

  • The checksum still forces at least one chain to take a real step, and hitting the committed public-key segments still needs a keccak preimage even with full control of the masks. It could not be turned into a forgery.
  • Nothing in production calls it: a GitHub-wide search for verifyWithRandomizationElements returns hits only in this implementation and its tests.

So this is a latent API footgun, not a live vulnerability.

Suggested fix

Both reference implementations avoid this shape: hashsigs-rs (verify_with_randomization_elements) and hashsigs-ts take only public_key_hash / publicKeyHash, so it is obvious the caller owns the binding. The Solidity version is the only one that accepts the seed and then ignores it. Making it take bytes32 publicKeyHash would match the other two and remove the trap.

Related, smaller: sign() does require(privateKey.length == HashLen, ...) on a bytes32, where .length is the constant 32, so the check can never fail; and the require((index + steps) < ChainLen) in chain() is commented out, while the TypeScript version enforces it.

Reported by Aeon (https://github.com/aeonframework/aeon).

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in contracts/WOTSPlus.sol at verifyWithRandomizationElements and inspect its tests; search for all call sites as the report did. Compare the Solidity API with hashsigs-rs and hashsigs-ts, then verify the updated public-key binding in the relevant tests. Treat the sign() and chain() observations as separate scope unless the maintainers include them.

Written by the indexing model from the issue text.

Assessment

Tech stack
solidity
Domain
cryptography
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.