QuipNetwork / QuipNetwork/hashsigs-solidity
F3 (low): verifyWithRandomizationElements accepts publicSeed then ignores it
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
verifyWithRandomizationElementsreturns 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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