Signature scheme

SLH-DSA explained mathematically

How SLH-DSA-SHA2-256f (FIPS 205) signs without any hard problem other than hash functions — from hash chains via Merkle trees to a forest of FORS trees.

Last updated: 2026-11-09

1. What this is about

At McGesund, customers on Reviews Premium and Signals Premium can choose the signature tier PQ-Dual-Diversified. Every QR code then carries two quantum-resistant signatures in addition to the conventional Ed25519 signature: ML-DSA-87 and SLH-DSA-SHA2-256f.

This article explains the second one. It is built differently from everything else we use.

SLH-DSA is not encryption. The content stays publicly readable. SLH-DSA proves origin and integrity — like any digital signature.

SLH-DSA stands for Stateless Hash-Based Digital Signature Algorithm. It was developed from SPHINCS+ and standardised in 2024 as FIPS 205. The name already gives away what is special about it: the scheme uses nothing but hash functions. No lattices, no elliptic curves, no large primes.


2. What exactly is signed?

At McGesund, SLH-DSA signs the same data that the Ed25519 signature in the QR code covers: the QR code’s payload. Among other things, it states which company the code belongs to (fid), what kind of code it is (typ), when it was issued (iat) and until when it is valid (exp) — as a compactly encoded byte string (CBOR).

This byte string is our message MM. If anyone later changes a single bit, none of the signatures match any more.

The SLH-DSA signature is not stored in the QR code itself. At around 49 KB it is far too large for that. It is kept as a separate “stamp” on the QR record in the McGesund backend and loaded into the browser when you click “Verify authenticity”.


3. Why a scheme built only from hash functions?

Every signature scheme rests on a task that is practically impossible without the secret key:

SchemeSecurity rests on
Ed25519discrete logarithm on an elliptic curve
FALCONshort vectors in NTRU lattices
ML-DSAModule-LWE and Module-SIS (lattices)
SLH-DSAproperties of the hash function only

A large quantum computer would break Ed25519. FALCON and ML-DSA are considered quantum-resistant — but both rely on lattice problems. Should a fundamental attack on lattices ever be found, both would be affected at once.

SLH-DSA, by contrast, only needs the hash function to do what it was built for:

From H(x) you cannot compute x, and nobody finds a second x′ with H(x′)=H(x).\text{From } H(x) \text{ you cannot compute } x, \text{ and nobody finds a second } x' \text{ with } H(x') = H(x).

This is the longest and most thoroughly studied assumption in all of cryptography. Even a quantum computer helps only to a limited extent: Grover’s algorithm halves the security bits. That is why the 256 variant uses hash values of n=32n = 32 bytes =256= 256 bits — after Grover, 128 bits still remain.

This is exactly why the tier is called Diversified: ML-DSA (lattices) and SLH-DSA (hashes) come from different families of mathematics. If one falls, the other still holds.


4. Building block 1: signing with hash chains (WOTS+)

The basic idea is surprisingly simple. We start with a single digit.

Take a secret random value ss and hash it again and again:

s  →  H(s)  →  H2(s)  →  …  →  H15(s)=ps \;\to\; H(s) \;\to\; H^2(s) \;\to\; \dots \;\to\; H^{15}(s) = p

The end of the chain, pp, is published. It is the public key for this one digit.

To sign a digit dd between 0 and 15, you reveal the chain value at position dd:

σ=Hd(s)\sigma = H^{d}(s)

Anyone who wants to verify hashes σ\sigma another 15−d15 - d times. If exactly pp comes out, the signature is genuine:

H15−d(σ)=?pH^{15-d}(\sigma) \stackrel{?}{=} p

A tiny example

We sign the digit d=6d = 6:

  • The signer publishes σ=H6(s)\sigma = H^6(s).
  • The verifier computes H9(σ)=H15(s)H^{9}(\sigma) = H^{15}(s) and compares it with pp. It matches.

A forger who has seen σ\sigma can easily compute H7(s)H^7(s), H8(s)H^8(s) and so on from it — and thus sign larger digits. They cannot sign smaller ones, because that would require hashing backwards.

The checksum against “counting up”

To stop anyone from simply increasing digits, a checksum is signed as well:

C=∑i(15−di)C = \sum_{i} (15 - d_i)

If a forger increases a digit of the message, CC becomes smaller — and for that they would have to compute backwards in the checksum chain. Both directions are blocked.

Scaled up to a hash value

A 32-byte hash value has 64 digits in base 16. Add 3 checksum digits:

len=64+3=67 chains\text{len} = 64 + 3 = 67 \text{ chains}

Such a signature is called WOTS+ (Winternitz One-Time Signature). Its big catch is in the name: one-time. Anyone who signs two different messages with the same key reveals so many chain values that forgeries become possible.


5. Building block 2: many one-time keys under one roof (Merkle tree)

If every key may be used only once, you need a great many of them — but you want to publish only one public key.

The solution is a Merkle tree: the public keys of 2h′2^{h'} WOTS+ key pairs form the leaves. Each pair of neighbours is hashed together, the results again in pairs, until a single root sits at the top.

node=H(left ∥ right)\text{node} = H(\text{left} \,\|\, \text{right})

In SLH-DSA-SHA2-256f, each tree has height h′=4h' = 4, i.e. 24=162^4 = 16 leaves.

To show that a particular leaf belongs to the tree, you provide its authentication path: the 4 sibling nodes on the way up. With them, the verifier hashes their way from the leaf to the root. If the known root comes out, the leaf belongs to the tree.


6. Building block 3: trees on top of trees (hypertree)

Sixteen one-time keys are of course not enough. So SLH-DSA stacks trees on top of each other: the root of a tree is signed with a WOTS+ key of the tree above it. This tower is called the hypertree.

SLH-DSA-SHA2-256f has d=17d = 17 layers, each of height 4:

h=d⋅h′=17⋅4=68h = d \cdot h' = 17 \cdot 4 = 68

At the very bottom, that gives

268≈2.95⋅10202^{68} \approx 2.95 \cdot 10^{20}

leaves. The root of the topmost tree is called PK.root\text{PK.root} — it is the actual public key.

Importantly, nobody computes these trees in advance. All secret values are derived on demand from a secret seed SK.seed\text{SK.seed}. Only four values of 32 bytes each are stored:

PartContentSize
SK.seedseed for all secret chain values32 B
SK.prfseed for the randomiser RR32 B
PK.seedpublic seed, separates key pairs from one another32 B
PK.rootroot of the topmost tree32 B

The private key consists of all four (128 bytes), the public key only of PK.seed and PK.root (64 bytes).


7. Building block 4: a forest against chance hits (FORS)

Now the real problem: how does the signer know which of the 2682^{68} leaves is still unused?

Older hash-based schemes (XMSS, LMS) keep a counter for this. It must never be reset — not by a backup, not by a second server. If that happens anyway, a one-time key is used twice and the scheme is broken. Such schemes are called stateful.

SLH-DSA is stateless: the leaf is chosen pseudo-randomly from the message itself. Then two messages may hit the same leaf. That is why the bottom layer is not a one-time signature but a few-time signature: FORS (Forest of Random Subsets).

FORS consists of k=35k = 35 small Merkle trees, each with 2a=29=5122^a = 2^9 = 512 leaves. The leaves are secret random values.

To sign, the message hash is split into 35 numbers of 9 bits each. Each number selects exactly one leaf in “its” tree. The signature contains, for every tree:

  • the secret value of that leaf
  • its authentication path (9 nodes)

For a different message, a forger would have to hit leaves in all 35 trees whose secrets have already been revealed. Even if the same FORS instance has been used a few times, that is hopeless.


8. The whole signing process

  1. Randomiser. From SK.prf\text{SK.prf}, a fresh random number and the message, a 32-byte value RR is derived.
  2. Message hash.
    digest=Hmsg(R, PK.seed, PK.root, M)\text{digest} = H_{\text{msg}}(R,\ \text{PK.seed},\ \text{PK.root},\ M)
    The 49 bytes of the digest are split up: 40 bytes select the 35 FORS leaves, the rest determines which tree (idx_tree) and which leaf (idx_leaf) at the bottom of the hypertree is used.
  3. FORS signature over the message. It yields a FORS public key.
  4. Hypertree signature over this FORS key: 17 WOTS+ signatures, each with an authentication path, layer by layer up to PK.root\text{PK.root}.

The finished signature is simply the concatenation:

σ=R ∥ σFORS ∥ σHT\sigma = R \,\|\, \sigma_{\text{FORS}} \,\|\, \sigma_{\text{HT}}

9. Why the signature is 49,856 bytes

With the parameters of SLH-DSA-SHA2-256f, the size can be computed exactly (n=32n = 32 bytes per hash value):

PartCalculationBytes
RRnn32
FORSk⋅(1+a)⋅n=35⋅10⋅32k \cdot (1 + a) \cdot n = 35 \cdot 10 \cdot 3211,200
Hypertree(h+d⋅len)⋅n=(68+17⋅67)⋅32(h + d \cdot \text{len}) \cdot n = (68 + 17 \cdot 67) \cdot 3238,624
Total49,856

For comparison: an Ed25519 signature is 64 bytes, ML-DSA-87 is 4,627 bytes. The size is the price for SLH-DSA getting by with nothing but hash functions.


10. How the browser verifies

With SLH-DSA, verifying means: hashing again.

  1. Recompute the digest from RR, the public key and the message.
  2. From the 35 revealed FORS leaves and their paths, compute the 35 tree roots and from them the FORS public key.
  3. With each of the 17 WOTS+ signatures, hash the chains to their ends, form the WOTS+ public key and hash up the path to the tree root.
  4. At the end, exactly PK.root\text{PK.root} must come out.

That is a few thousand hash calls — a trifle for a computer. In the browser, verification takes us about 6 to 12 milliseconds.


11. What happens if someone changes the data?

If even one bit of the message MM changes, the digest changes completely. The 35 numbers then point to different FORS leaves, and the scheme to a different place in the hypertree.

The revealed secrets no longer fit: the recomputed roots differ, and PK.root\text{PK.root} does not come out at the end. Verification fails.

To build a valid signature anyway, an attacker would need secrets that were never published — that is, compute a hash function backwards.


12. Why 256f in particular?

FIPS 205 defines twelve parameter sets: two hash families (SHA-2 and SHAKE), three security levels (128, 192, 256), and for each a fast (“f”) and a small (“s”) variant.

VariantSignatureSigning (our measurement)Verifying
SHA2-256s29,792 Bapprox. 2.6 sapprox. 3 ms
SHA2-256f49,856 Bapprox. 0.26 sapprox. 6 ms

256 means NIST security category 5 — the same level as FALCON-1024 and ML-DSA-87.

We chose “f” because companies can issue up to 500 QR codes in one go. With “s”, signing would take more than 20 minutes; with “f”, less than a minute. The signature is larger in return — since it does not live in the QR code, that is acceptable.

SHA-2 rather than SHAKE, because SHA-256 and SHA-512 are hardware-accelerated on practically every device.


13. Randomness when signing

FIPS 205 allows two ways of generating RR:

  • deterministic: same message, same signature
  • randomised (“hedged”): a fresh random number is mixed in

We use the randomised variant. Two signatures over the same message therefore look different — both are valid. This makes attacks harder that combine measurements on the device (power consumption, timing) with repeated signatures.


14. What McGesund does with it

The tier. PQ-Dual-Diversified is available on Reviews Premium and Signals Premium. A QR code then carries three signatures:

SignatureFamilyLocation
Ed25519elliptic curvein the QR code
ML-DSA-87latticesstamp in the backend
SLH-DSA-SHA2-256fhash functionsstamp in the backend

Signing. An SLH-DSA signature costs about a quarter of a second of computing time. So that the server stays available to everyone else in the meantime, dedicated background threads do the work.

Verification. The reader’s browser loads both stamps and checks them with the same WASM module as all other PQ signatures. We supply the data; the computation happens on the visitor’s device.

The NIST check. Our module passes the official NIST test vectors for SLH-DSA-SHA2-256f: NIST’s seeds yield byte-for-byte the same keys, and all deliberately tampered signatures are rejected. You can check this yourself in the browser on the NIST test page.

The time anchor. As with all keys, the fingerprint of the SLH-DSA key is anchored in a Bitcoin block via OpenTimestamps.


15. SLH-DSA, ML-DSA and FALCON side by side

PropertyFALCON (FN-DSA)ML-DSA-87SLH-DSA-SHA2-256f
FoundationNTRU latticesmodule latticeshash functions only
StandardFIPS 206 (in progress)FIPS 204, finalFIPS 205, final
Public key1,793 B2,592 B64 B
Signature1,280 B4,627 B49,856 B
Signingmillisecondsmillisecondsapprox. 0.26 s
Verifyingmillisecondsmillisecondsmilliseconds
Assumptionlattice problemlattice problemhash function

In short: SLH-DSA is the slowest and largest of the three schemes — and the most conservative. It relies on nothing that every other signature does not already assume anyway: a secure hash function.


16. One sentence to take away

SLH-DSA signs with hash chains, bundles them in Merkle treesand picks the leaf from the message itself — secure as long asthe hash function is secure.\boxed{ \begin{array}{c} \text{SLH-DSA signs with hash chains, bundles them in Merkle trees}\\ \text{and picks the leaf from the message itself — secure as long as}\\ \text{the hash function is secure.} \end{array}}

Whoever holds the secret seed signs in a quarter of a second. Whoever does not would have to compute SHA-256 or SHA-512 backwards — even with a quantum computer.

For the reader of a review, this means: even if all lattice schemes should fall one day, this signature remains verifiable. They do not have to believe us. They can do the maths.