4. XMSS¶
Where we are
Left over from chapter 3: a Merkle tree over WOTS+ keys works, but the hashing must be position-aware, and we need a concrete specification rather than a sketch. Goal: name and pin down the combination.
The combination¶
XMSS — the eXtended Merkle Signature Scheme — is exactly chapter 2 plus chapter 3, made rigorous:
a Merkle tree of height
h'whose leaves are WOTS+ public keys, with every hash tweaked byPK.seedand anADRSgiving its position.
It exists as a standard in its own right (RFC 8391) and is deployed where a stateful signer is acceptable. In SLH-DSA it is a component: the hypertree of chapter 7 is a stack of XMSS trees.
Three operations¶
FIPS 205 §6 gives XMSS three algorithms — implemented in
src/xmss.zig:
| Algorithm | Section | Purpose |
|---|---|---|
xmss_node |
§6.1, Alg 9 | Compute the node at height z, index i — recursively |
xmss_sign |
§6.2, Alg 10 | WOTS+ signature + authentication path |
xmss_pkFromSig |
§6.3, Alg 11 | Reconstruct the root from a signature |
xmss_node — the recursive heart¶
Everything else is built on this. It says: to get a node, get its two children and hash them.
xmss_node(i, z):
if z == 0: # leaf layer
return T_len( wots_pkGen(leaf i) )
else:
left = xmss_node(2i, z-1)
right = xmss_node(2i + 1, z-1)
return H(left || right)
The base case compresses a whole WOTS+ public key (len blocks of n bytes)
into a single n-byte leaf using the multi-block tweakable hash T_len. The
recursive case is ordinary Merkle hashing.
This recursion is the cost centre
xmss_node(0, h') — computing a root — expands into 2^h' leaf
computations, each a full wots_pkGen running len chains of up to w-1
steps. For h' = 9, len = 35, w = 16 that is on the order of 512 × 35 ×
15 ≈ 270,000 hash invocations for one root, and a signature computes
several roots. When you see SLH-DSA benchmarks in the hundreds of
milliseconds, this recursion is where the time goes.
This library implements it as written — genuine recursion, depth bounded by
h' ≤ 9 across all parameter sets, so a handful of n-byte locals per frame.
The SPHINCS+ reference implementation instead uses an iterative treehash
that computes leaves left to right and merges completed subtrees with an
explicit stack. That is a control-flow difference, not a work saving: one
root costs 2^h' leaf computations either way. The cost centre is real — it
is simply not redundant work, here or in xmss_sign — see
the component page.
xmss_pkFromSig — verification by reconstruction¶
Note the name, matching wots_pkFromSig. XMSS verification does not compare
against a stored root; it recomputes one:
xmss_pkFromSig(idx, sig, M):
node = T_len( wots_pkFromSig(sig.wots, M) ) # leaf from the WOTS+ sig
for z in 0 .. h'-1:
if bit z of idx == 0:
node = H(node || sig.auth[z]) # we are the left child
else:
node = H(sig.auth[z] || node) # we are the right child
return node
The caller compares the returned root to what it expected. A wrong signature, a wrong message or a wrong index all produce a wrong root and fail the comparison.
This "return a value, let the caller compare" shape is what makes XMSS stackable. In the hypertree, the returned root is not compared against the public key at all — it becomes the message for the layer above. Chapter 7 uses this directly.
Why the address matters so much here¶
The same H compresses every internal node of every tree. Without a tweak,
identical child pairs anywhere in the structure would hash identically, and an
attacker could relocate a subtree from one position to another — or amortise
precomputation across the entire structure, the multi-target attack from
chapter 2 applied at tree scale.
So each hash is tweaked with an ADRS recording:
- the layer in the hypertree,
- the tree address within that layer,
- the type of hash (WOTS+ chain, leaf compression, internal node, …),
- the height and index of this node.
Every node in the whole structure is therefore computed by a function unique to its position. See ADRS for the encoding — including a genuine trap: the SHA-2 and SHAKE families use different address sizes (22 bytes compressed versus the full 32), and conflating them yields a library that passes its own round-trip tests while failing every NIST vector.
What XMSS does not fix¶
XMSS inherits both problems unchanged.
Key generation is still exponential in h'. xmss_node(0, h') visits every
leaf. Height 63 remains impossible.
The signer must still track the index. Each leaf is one WOTS+ key, usable exactly once. XMSS-the-standard's answer is to require the signer to keep state — a counter, persisted, monotonic, never rolled back.
That requirement is the subject of the next chapter, and it is the reason SLH-DSA exists as a separate scheme rather than a parameter choice for XMSS.
Chapter 4 outcome
XMSS = WOTS+ leaves + Merkle tree + position-tweaked hashing. Three
algorithms: xmss_node, xmss_sign, xmss_pkFromSig. Verification
reconstructs a root and hands it back, which makes trees stackable.
Unfixed: exponential key generation, and a stateful signer.