This project implements a blind signature over XMSS (a stateful Merkle signature scheme) with target-sum WOTS+ one-time signatures in the leaves, and a ZK proof in the ZKBoo / MPC-in-the-head style to prove knowledge of a valid signature without revealing the secret material (the commitment opening, the leaf index, or the signature).
It is the ZKBoo-based instantiation of the generic hash-based blind signature construction: a commitment scheme, a hash-based signature scheme, and a NIZK. Here the commitment is Halevi–Micali over GF(2¹²⁸), the signature is target-sum WOTS+/XMSS, and the NIZK is ZKBoo.
⚠️ This code is for research/education. Do not use in production.
This work was carried out during my final internship for the Master's degree in Cryptology and Computer Security at the University of Bordeaux. The internship took place in the first half of 2025 at UPC (Barcelona), supervised by Javier Herranz Sotoca.
Requirements:
- A C compiler (GCC/Clang) with OpenMP support
- OpenSSL libcrypto
make
make # build everything
make clean # remove binaries and intermediatesBinaries produced: CLIENT_blinding_message, SIGNER_XMSS_keygen, SIGNER_XMSS_sign, CLIENT_blind_sign, VERIFIER_verify.
Signature scheme (xmss.h), matching the Longfellow/Binius64 instantiations:
XMSS_H = 10— Merkle tree height (2^10 = 1024signatures per key pair)XMSS_WOTS_W = 4— Winternitz parameter (2-bit coordinates)XMSS_WOTS_LEN = 72— number of WOTS+ chains (target-sum encoding, no checksum)XMSS_TARGET_SUM = 132— required sum of the 72 coordinatesXMSS_NODE_BYTES = 16— every internal node is a SHA-256 output truncated to 128 bitsXMSS_PK_SEED_BYTES = 16,XMSS_NONCE_LEN = 6- All hashing is SHA-256, SPHINCS+-style keyed/tweaked (tweaks
0x00chain,0x01tree/pk,0x02message).
Halevi–Micali commitment (commitment.h):
HM_NONCES = 6,HM_LINES = 2, fieldGF(2¹²⁸)— same layout as the Longfellow-based instantiation.- Opening
(r, a)is96 + 192 = 288bytes; the commitmentcom = a‖b‖yis256bytes.
ZKBoo (shared.c):
NUM_ROUNDS = 137parallel executions (soundness error(2/3)^137); raise to219for2^-128.INPUT_LEN = 1610,ySize = 191448(nonlinear-gate transcript words per view).
All hex is UPPERCASE without spaces.
-
XMSS_secret_key.txt(SIGNER_XMSS_keygen)- Line 1:
sk_seed— 32 bytes (64 hex) - Line 2:
pk_seed— 16 bytes (32 hex) - Line 3:
leaf_index— decimal (initially0)
- Line 1:
-
XMSS_public_key.txt(SIGNER_XMSS_keygen)- Line 1:
pk_seed— 16 bytes (32 hex) - Line 2: XMSS root — 16 bytes (32 hex)
- Line 1:
-
blinding_key.txt(CLIENT_blinding_message) — the secret opening(r, a)- Line 1: nonces
r₁‖…‖r₆— 96 bytes (192 hex) - Line 2: line matrix
a(row-majora_{0,0..5} ‖ a_{1,0..5}) — 192 bytes (384 hex)
- Line 1: nonces
-
blinded_message.txt(CLIENT_blinding_message)- Commitment
com = a ‖ b ‖ y— 256 bytes (512 hex). The signer derivesd = SHA256(com).
- Commitment
-
XMSS_signature.txt(SIGNER_XMSS_sign)- Line 1:
leaf_index— decimal - Line 2:
nonce— 6 bytes (12 hex) - Next
72lines: WOTS+ chain values — 16 bytes (32 hex) each - Next
10lines: XMSS authentication path — 16 bytes (32 hex) each
- Line 1:
-
signature_proof.bin(CLIENT_blind_sign)- Binary ZKBoo proof that a valid XMSS signature exists on the committed message.
- Signer generates keys:
./SIGNER_XMSS_keygen→XMSS_secret_key.txt,XMSS_public_key.txt. - Client blinds a message:
./CLIENT_blinding_message(prompts form) →blinding_key.txt(the secret opening(r, a)),blinded_message.txt(the commitmentcom = a‖b‖y). The client keeps(r, a)secret and sendscomto the signer. - Signer signs the commitment:
./SIGNER_XMSS_sign(readsXMSS_secret_key.txt,blinded_message.txt; derivesd = SHA256(com), signsd, self-checks against the public key) →XMSS_signature.txt, and advances the leaf index. - Client proves:
./CLIENT_blind_sign(prompts form; readsblinding_key.txt,XMSS_signature.txt,XMSS_public_key.txt). It first re-checks that the XMSS signature is valid ford = SHA256(a‖b‖y), then writes the ZK proof tosignature_proof.bin. - Verifier checks:
./VERIFIER_verify(prompts form; readsXMSS_public_key.txt,signature_proof.bin).
Message length affects only the native
m̂ = SHA256(m), so prove/verify time is independent of |m|.
| Artefact | Size |
|---|---|
Public key (pk_seed ‖ root) |
|
Secret key (sk_seed ‖ pk_seed ‖ leaf_index) |
|
Commitment com = a ‖ b ‖ y
|
|
Raw XMSS signature (leaf ‖ nonce ‖ 72 chains ‖ 10 path) |
|
| Blind signature (ZKBoo proof) |
|
| Phase | Mean time |
|---|---|
| Commitment computation |
|
| Key generation |
|
| Signing |
|
| Proof generation |
|
| Proof verification |
|
The proof is huge because it is inherent to ZKBoo: each of the NUM_ROUNDS rounds
serialises one full per-view nonlinear-gate transcript, so
proof ≈ NUM_ROUNDS · (ySize·4 + 2·INPUT_LEN + sizeof(a) + 128)
= 219 · (765792 + 3220 + 192 + 128) ≈ 168.5 MB