Post-Quantum Signatures, Part 3: Inside FIPS 205 Through Quranium’s SLH-DSA Adoption

기술적 분석 교육적인
Post-Quantum Signatures, Part 3: Inside FIPS 205 Through Quranium’s SLH-DSA Adoption

Introduction

FIPS 205 is no longer just a standardization milestone. It provides a concrete framework for deploying stateless hash-based signatures in production-oriented systems. One concrete example is Quranium, a post-quantum Layer 1 blockchain project that positions SLH-DSA as a core signing primitive for quantum-secure Web3 and financial infrastructure.

CertiK’s security work with Quranium included a review of the project’s core SLH-DSA implementation. That audit context gives this post a practical anchor: FIPS 205 is not only an algorithm specification to read in isolation, but a standard that implementers translate into key formats, signing flows, parameter choices, domain separation rules, and operational assumptions.

FIPS 205 standardizes SLH-DSA, a stateless hash-based digital signature algorithm based on SPHINCS+. The construction still uses familiar ingredients from the previous post: WOTS+, authentication paths, Merkle trees, FORS, and hypertrees. But it removes the XMSS-style persistent signing index. Instead, the signing position is derived from the message, public key material, and per-signature randomization.

In this post, we use the structure of FIPS 205 to explain how SLH-DSA works and why it matters for real protocol implementations. We start with Forest of Random Subsets (FORS), the few-time signature component that signs part of the randomized message digest, then build up the hypertree that authenticates the reconstructed FORS public key to the public root. Finally, we examine how FIPS 205 defines the concrete SLH-DSA algorithm used by interoperable implementations.

FORS: Turning Message into Forest Choices

SLH-DSA removes the need for an XMSS-style persistent signing counter. Instead, it computes a randomized hash of the message and splits the resulting digest into three parts: mdmd, idxtreeidx_{tree}, and idxleafidx_{leaf}.

The values idxtreeidx_{tree} and idxleafidx_{leaf} select a layer-0 XMSS tree and the WOTS+/FORS key pair used for this signature. The mdmd portion is passed to FORS, where it is interpreted as several indices selecting one secret value from each of several small trees.

This is where FORS enters. FORS stands for Forest of Random Subsets. It is the few-time signature component used inside SPHINCS+ and standardized as part of SLH-DSA in FIPS 205.

At a high level, FORS works like this. First, the message is hashed together with signing randomness and public context to produce a digest. We can write this digest as mdmd. Then mdmd is interpreted as a sequence of indices:

md(i0,i1,,ik1)md \rightarrow (i_0, i_1, \dots, i_{k-1})

Each index iji_j selects one leaf in the jj-th tree of a forest. If each tree has height aa, then each tree has 2a2^a leaves, and iji_j is an integer in the range:

0ij<2a0 \leq i_j < 2^a

The signer then reveals the secret value corresponding to each selected leaf, together with the authentication path from that leaf to the root of its small tree. Conceptually, a FORS signature looks like:

σFORS=((sk0,i0,Auth0,i0),,(skk1,ik1,Authk1,ik1))\sigma_{FORS} = ((sk_{0,i_0}, Auth_{0,i_0}), \dots, (sk_{k-1,i_{k-1}}, Auth_{k-1,i_{k-1}}))

This should look familiar. It is still the same Merkle authentication idea we used in XMSS: reveal one leaf and prove that it belongs to a committed tree. The difference is that FORS does this several times in parallel, across a forest of small trees.

Chart 1

From the verifier’s point of view, the process is also straightforward. Given the message, the signature randomizer RR, and the public key, the verifier recomputes the digest, extracts mdmd, and derives the same FORS indices.:

(i0,i1,,ik1)(i_0, i_1, \dots, i_{k-1})

For each selected leaf, the verifier hashes the revealed secret value to reconstruct a leaf candidate, then uses the authentication path to recompute the corresponding tree root. After doing this for all kk trees, the verifier obtains a list of roots:

(Root0,Root1,,Rootk1)(Root_0, Root_1, \dots, Root_{k-1})

These roots are then compressed into a single FORS public value, which we can denote informally as:

pkFORS=H(Root0Root1Rootk1)pk_{FORS} = H(Root_0 \parallel Root_1 \parallel \dots \parallel Root_{k-1})

This is a simplified notation. In FIPS 205, this compression step is instantiated through the standardized tweakable hash functions with address-based domain separation, rather than a plain hash invocation. But for understanding the construction, the important point is simple: FORS lets the verifier reconstruct a compact value from the message-dependent forest openings.

Why is this useful?

Because SPHINCS+ needs a way to bind the actual message to the rest of the hypertree. FORS provides exactly that bottom layer. It signs the message digest by opening selected leaves in multiple small trees, and the resulting pkFORSpk_{FORS} becomes the value that the upper structure will authenticate.

Compared with XMSS, the important change is not that we stopped using Merkle authentication paths. We still use them. The change is how the leaves are selected. XMSS consumes one WOTS+ leaf according to the private signing state idxidx. FORS has no such state. It interprets the randomized message digest mdmd as kk indices, and these indices select one leaf in each of the kk small trees.

In this sense, the opened leaves are tied to the message being signed and the per-signature randomness, rather than to a persistent counter maintained by the signer.

This does not give us a complete signature scheme yet. The verifier can reconstruct pkFORSpk_{FORS}, but still needs to know that this value is tied to the global public key. That is the job of the SPHINCS+ hypertree, which we will build next.

SPHINCS+: From FORS Public Key to Public Root

After the FORS step, the verifier reconstructs a candidate pkFORSpk_{FORS} from the message digest and the FORS signature. The message digest determines which FORS leaves and authentication paths are revealed, while the reconstructed public key is then authenticated by the hypertree.

But pkFORSpk_{FORS} is not yet the public key.

This is the gap SPHINCS+ has to close. FORS binds the message to a compact value, and the rest of the construction authenticates that value back to the global public root. The mechanism is the hypertree: a stack of XMSS-like trees connected by WOTS+ signatures.

What FORS Still Cannot Prove

FORS processing reconstructs a candidate public key from mdmd and the FORS signature. That candidate is not trusted until the hypertree connects it to PK.rootPK.root. It shows that, for the message digest mdmd, the signer opened the right leaves in the FORS forest and provided valid paths to the corresponding FORS roots. From those roots, the verifier obtains pkFORSpk_{FORS}.

However, a standalone pkFORSpk_{FORS} does not tell the verifier which long-term public key it belongs to. The FORS signature depends on mdmd and lets the verifier reconstruct a candidate FORS public key. The hypertree then authenticates that public key to PK.rootPK.root.

This is why SPHINCS+ places FORS at the bottom of a larger structure. The FORS output becomes the first value that needs to be authenticated upward.

In other words:

  • FORS handles the message.
  • The hypertree connects the FORS output to the public key.

Deriving Tree Position

SPHINCS+ does not maintain a signing counter like XMSS. Instead, each signature derives the relevant tree position from the message and per-signature randomness.

A simplified view is:

Hmsg(R,PK.seed,PK.root,M)(md,idxtree,idxleaf)H_{msg}(R, PK.seed, PK.root, M) \rightarrow (md, idx_{tree}, idx_{leaf})

Here, RR is the per-signature randomizer, MM is the message, and PK.seedPK.seed and PK.rootPK.root are public-key values. The output is split into pieces with different jobs.

The digest mdmd is passed to FORS. As we saw in the previous section, FORS interprets mdmd as several indices and opens one selected leaf in each small tree.

The other values, idxtreeidx_{tree} and idxleafidx_{leaf}, tell the hypertree where this FORS output should be attached. Roughly speaking, they identify which bottom-layer tree is used and which WOTS+ leaf inside that tree will sign the FORS public value.

The important point is that this position is not read from persistent private-key state. It is derived for this signature. The signer is not saying, “I am now at leaf number ss.” Instead, the signing context determines which leaf is relevant for this message.

One Hypertree Layer

Now suppose the verifier already has some value that came from the layer below. At the very bottom, that value is pkFORSpk_{FORS}.

A single hypertree layer uses one WOTS+ key and one authentication path to move this value upward.

At the bottom layer, WOTS+ signs pkFORSpk_{FORS}:

σWOTS(0)=WOTS.Sign(skWOTS(0),pkFORS)\sigma_{WOTS}^{(0)} = WOTS.Sign(sk_{WOTS}^{(0)}, pk_{FORS})

The verifier does not directly trust this WOTS+ key. Instead, it uses the WOTS+ signature and the signed value to reconstruct the corresponding WOTS+ public key. Then that WOTS+ public key is compressed into a leaf of the current Merkle tree.

From there, the authentication path does the same job it did in XMSS: it lets the verifier recompute the root of the current tree.

So one layer performs a simple move:

  • WOTS+ signs the value from below.
  • The verifier reconstructs the WOTS+ public key.
  • That public key becomes a Merkle-tree leaf.
  • The authentication path lifts the verifier from that leaf to the tree root.

This is the core step. SPHINCS+ repeats it several times.

Repeating Climb to Root

Once the verifier reaches the root of the bottom tree, that root becomes the value signed in the next layer. The next WOTS+ key signs this lower-layer root, and the next authentication path lifts the verifier to the root of the next tree.

The process repeats until the verifier reaches the top of the hypertree.

If the hypertree has dd layers, the signature contains one WOTS+ signature and one authentication path per layer. A high-level signature structure is:

σ=(R,σFORS,σHT)\sigma = (R, \sigma_{FORS}, \sigma_{HT})

where:

σHT=((σWOTS(0),Auth(0)),,(σWOTS(d1),Auth(d1)))\sigma_{HT} = ((\sigma_{WOTS}^{(0)}, Auth^{(0)}), \dots, (\sigma_{WOTS}^{(d-1)}, Auth^{(d-1)}))

This notation hides the exact address handling and domain-separated hash calls, but it shows why the signature has several parts. The FORS part proves the message-dependent bottom value. The hypertree part repeatedly proves that each value is connected to a tree root one layer above.

Chart 2

From the verifier’s point of view, verification is a climb:

  1. Recompute mdmd, idxtreeidx_{tree}, and idxleafidx_{leaf} from RR, MM, and the public key.
  2. Use σFORS\sigma_{FORS} to reconstruct pkFORSpk_{FORS}.
  3. Verify the bottom WOTS+ signature on pkFORSpk_{FORS}.
  4. Use the authentication path to compute the bottom tree root.
  5. Treat that root as the value authenticated in the next layer.
  6. Repeat until the top root is reached.

The signature is valid only if the final computed root equals PK.rootPK.root.

This gives us the conceptual SPHINCS+ construction. There is no new magic above FORS: the system repeatedly uses WOTS+ to sign a short value, and Merkle authentication paths to climb one tree higher. What changes from XMSS is how the path is chosen. XMSS consumes leaves through a persistent signing state. SPHINCS+ derives the relevant position from the signing context and carries the path inside the signature.

At this point, we have the structure. FIPS 205 turns this structure into SLH-DSA by fixing the concrete parameter sets, hash-function variants, address formats, and domain separation rules.

FIPS 205: From SPHINCS+ to SLH-DSA

So far, we have described a construction. FORS binds the message digest, WOTS+ signs the FORS output and intermediate roots, and the hypertree carries the verifier back to a single public root.

FIPS 205 is based on SPHINCS+ v3.1 but includes several standard-specific changes, so earlier SPHINCS+ implementations should not automatically be assumed to be compatible with SLH-DSA.

The high-level idea is still SPHINCS+. FIPS 205 does not replace FORS, WOTS+, or the hypertree with a different mechanism. Instead, it fixes the details that our conceptual explanation left abstract: what the keys look like, how signatures are encoded, which hash functions instantiate the internal calls, how addresses separate different hash contexts, and which parameter sets are approved.

In the standard, this algorithm is called SLH-DSA, short for Stateless Hash-Based Digital Signature Algorithm.

From Construction to Algorithm

When we explain SPHINCS+ informally, it is useful to write simple expressions such as H()H(\cdot), pkFORSpk_{FORS}, and σHT\sigma_{HT}. These symbols help us understand the flow of the construction, but they are not enough for implementation.

An implementation needs exact byte strings. It needs to know how a public key is serialized, how a signature is parsed, which hash call is being made, and which address fields are included. If two implementations make different choices for any of these details, they may both follow the same idea but fail to verify each other’s signatures.

This is the role of FIPS 205. It turns the SPHINCS+ design into an interoperable algorithm. The standard is not only saying “use a stateless hash-based signature.” It specifies the exact form of SLH-DSA key generation, signature generation, and signature verification.

This distinction is also important for the internal components. FORS, WOTS+, XMSS-like trees, and hypertree signatures are useful for understanding SLH-DSA, but FIPS 205 does not approve them as stand-alone signature schemes. They are components inside the standardized SLH-DSA construction.

Keys and Signatures as Concrete Objects

The public key in SLH-DSA has a compact form:

PK=(PK.seed,PK.root)PK = (PK.seed, PK.root)

The value PK.rootPK.root is the root of the top layer of the hypertree. It is the final value that verification tries to reproduce. The value PK.seedPK.seed is public and is used throughout the scheme to parameterize hash calls and separate computations belonging to one key pair from computations belonging to another.

The private key contains two secret values and a copy of the public key:

SK=(SK.seed,SK.prf,PK.seed,PK.root)SK = (SK.seed, SK.prf, PK.seed, PK.root)

Here, SK.seedSK.seed derives the secret material used by FORS and WOTS+. This is why the signer does not need to store every FORS or WOTS+ secret value explicitly. The value SK.prfSK.prf is used in deriving the per-signature randomization string.

The signature has the same shape as the construction we built in the previous two sections:

σ=(R,σFORS,σHT)\sigma = (R, \sigma_{FORS}, \sigma_{HT})

The first part, RR, is the randomization string used when deriving the message digest. The second part, σFORS\sigma_{FORS}, lets the verifier reconstruct pkFORSpk_{FORS}. The third part, σHT\sigma_{HT}, contains the WOTS+ signatures and authentication paths that connect pkFORSpk_{FORS} to PK.rootPK.root.

This layout makes the stateless nature of SLH-DSA visible. The private key does not contain a mutable leaf counter, and signing does not return an updated private key state. The data needed for verification is carried by the signature itself.

Why Domain Separation Matters More Than It Looks

In the previous sections, we often wrote a generic hash function HH. That is fine for building intuition, but it hides an important problem: SLH-DSA uses hash functions in many different roles.

A hash call may derive a FORS secret value. Another may hash a FORS leaf. Another may compress two Merkle nodes. Another may advance a WOTS+ chain. Another may compress a WOTS+ public key. Another may derive the message digest and the tree indices.

These calls should not be treated as interchangeable uses of one undifferentiated hash function.

FIPS 205 handles this by defining named internal functions such as HmsgH_{msg}, PRFmsgPRF_{msg}, PRFPRF, FF, HH, and TlT_l. Depending on the parameter set, these functions are instantiated using either SHAKE or SHA-2. The standard also uses an address object, usually written as ADRSADRS, to encode where a hash call occurs in the construction.

The address carries context: which layer of the hypertree we are in, which tree is being used, what type of object is being hashed, which key pair address is relevant, and which node position is involved. In other words, ADRSADRS prevents two hash calls that look similar at the notation level from being treated as the same operation.

This is the standardized version of a theme we already saw in XMSS. Hash-based schemes reuse simple primitives many times, so the security story depends heavily on separating those uses. FIPS 205 makes that separation explicit across FORS, WOTS+, tree hashing, and message hashing.

Signing Interfaces: Pure, Pre-hash, and Hedged

FIPS 205 also specifies how the applications call SLH-DSA. This is more subtle than it first appears, because applications do not always present messages to a signature scheme in the same form.

The standard defines a pure version and a pre-hash version. In the pure version, the content itself is passed into the SLH-DSA signing process, together with domain-separation information and an optional context string. In the pre-hash version, the content is first processed by an approved hash function or XOF, and the resulting digest is signed.

This distinction avoids leaving message handling conventions to applications. If a protocol signs raw messages while another signs pre-hashed messages, the difference should be explicit and domain-separated. Otherwise, the boundary between “what the application hashed” and “what the signature scheme signed” becomes a source of ambiguity.

The standard also distinguishes hedged and deterministic signing. Both variants produce a signature containing the randomization string RR. In hedged signing, additional randomness is used in deriving this value. In deterministic signing, no fresh external randomness is provided, so signing the same message with the same key produces the same signature.

For our purposes, the important point is that neither mode reintroduces XMSS-style signing state. Whether the randomizer is derived deterministically or with additional randomness, the signer does not update a persistent leaf index after signing.

Reading SLH-DSA Parameter Sets

FIPS 205 approves 12 SLH-DSA parameter sets. The names look long, but they are systematic.

Take this example:

SLH-DSA-SHAKE-128sSLH\text{-}DSA\text{-}SHAKE\text{-}128s

The name tells us three things. SHAKESHAKE is the hash-function family. The number indicates the bit length of the security parameter: 128, 192, or 256 bits; FIPS formulas express nn in bytes as 16, 24, or 32. These parameter choices correspond to different NIST security categories. The suffix ss means the parameter set is optimized for smaller signatures.

The alternative suffix is ff, which means faster signing at the cost of larger signatures. Each security category therefore has four variants: SHA2-small, SHA2-fast, SHAKE-small, and SHAKE-fast.

A compact summary is:

Security category Hash families Variants Public key size Signature size
1 SHA2 / SHAKE 128s / 128f 32 bytes 7,856 / 17,088 bytes
3 SHA2 / SHAKE 192s / 192f 48 bytes 16,224 / 35,664 bytes
5 SHA2 / SHAKE 256s / 256f 64 bytes 29,792 / 49,856 bytes

The table shows the main engineering trade-off. SLH-DSA public keys are very small, but signatures are large. The ss variants reduce signature size, while the ff variants spend more bytes to improve signing performance.

It is also worth noting what “stateless” does not mean. It does not mean that a key pair can be used without any lifetime planning. The FIPS 205 parameter sets were designed to meet their listed security categories when no more than 2642^64 messages are signed with one key pair. Statelessness removes the need to track a consumed leaf index; it does not remove the need to choose parameters and manage key lifetimes responsibly.

What Standardization Adds

At this point, the relationship between SPHINCS+ and SLH-DSA is fairly simple. SPHINCS+ provides the underlying construction: FORS signs the message digest and enables the verifier to reconstruct a candidate FORS public key, WOTS+ signs that public key and the roots above it, and authentication paths carry the verifier up to the public root. FIPS 205 takes this construction and fixes the boundaries that an implementation must agree on.

The first boundary is the object layout. In the conceptual construction, the root is the main verification anchor. In SLH-DSA, the public key is the concrete pair PK=(PK.seed,PK.root)PK = (PK.seed, PK.root). Similarly, the signature is not just an abstract proof path. It has a specified structure containing the message randomizer RR, the FORS signature, and the hypertree signature data.

The second boundary is the hash context. In an explanatory diagram, it is fine to write many steps as H()H(\cdot). In the standard, these calls have to be separated. A hash call used inside FORS should not be confused with one used inside WOTS+, and a node hash in one layer should not be confused with a node hash in another. This is why FIPS 205 specifies named functions and address-based domain separation.

The third boundary is parameter selection. SLH-DSA does not leave implementers to invent their own tree heights, FORS parameters, or hash-function variants. FIPS 205 defines named parameter sets, such as SLH-DSA-SHAKE-128sSLH\text{-}DSA\text{-}SHAKE\text{-}128s, with fixed security categories and size/speed trade-offs.

This is the main value of the standard. It turns the SPHINCS+ design from a construction we can explain into an algorithm that independent implementations can generate, exchange, and verify consistently.

With SLH-DSA now defined as a standard algorithm, we can finally discuss the trade-off directly: removing XMSS-style signing state makes the scheme easier to operate safely, but the price is larger signatures and a more layered verification path.

Conclusion

In this post, we completed the journey from XMSS to FIPS 205 and explored how stateless hash-based signatures overcome the operational limitations of stateful schemes. Starting from FORS, we saw how SPHINCS+ binds a message to a compact value, and how WOTS+, authentication paths, and hypertrees extend this value back to a single public root.

FIPS 205 turns this construction into the standardized SLH-DSA algorithm. By fixing the key formats, hash-function instantiations, domain separation rules, and parameter sets, the standard makes SPHINCS+ practical for interoperable implementations. At the same time, SLH-DSA preserves the conservative security foundation of hash-based signatures while removing the need for a persistent signing index.

The trade-off is clear. Statelessness eliminates one of the most fragile operational requirements in XMSS, but it comes with larger signatures and a more complex verification structure. As post-quantum cryptography moves from research into real systems, these engineering trade-offs will become just as important as the underlying security assumptions.

References

  1. National Institute of Standards and Technology (NIST), 2024: FIPS 205: Stateless Hash-Based Digital Signature Standard.
  2. Daniel J. Bernstein, Daira Hopwood, Andreas Hülsing, Tanja Lange, Ruben Niederhagen, Louiza Papachristodoulou, Michael Schneider, Peter Schwabe, and Zooko Wilcox-O’Hearn, 2015: SPHINCS: Practical Stateless Hash-Based Signatures.
  3. Andreas Hülsing, Denis Butin, Stefan-Lukas Gazdag, Joost Rijneveld, Aziz Mohaisen, 2018: RFC 8391: XMSS: eXtended Merkle Signature Scheme.
  4. Andreas Hülsing, 2013: W-OTS+ — Shorter Signatures for Hash-Based Signature Schemes.
  5. Daniel J. Bernstein, Andreas Hülsing, Stefan Kölbl, Ruben Niederhagen, Joost Rijneveld, and Peter Schwabe, 2022: SPHINCS+: Submission to the NIST Post-Quantum Cryptography Standardization Process.
  6. Quranium: https://www.quranium.org/
  7. CertiK Skynet — Quranium Security Audit: https://skynet.certik.com/projects/quranium

관련 블로그

GnosisPay Incident Analysis

GnosisPay Incident Analysis

On 01 June 2026 an attacker drained dozens of GnosisPay Safes on Gnosis Chain. The attack vector was a signature-verification flaw in the GnosisPay Delay module.

Post-Quantum Signatures, Part 2: From Trees to Forests

Post-Quantum Signatures, Part 2: From Trees to Forests

XMSS builds on one-time signatures by organizing OTS keys into Merkle trees and hyper-trees, delivering a practical post-quantum signature scheme with compact proofs, fast verification, and a critical trade-off: strict state management.

CertiK, IDAI Summit 2026에서 AI 도입과 디지털 자산 사이버보안 인사이트 공유

CertiK, IDAI Summit 2026에서 AI 도입과 디지털 자산 사이버보안 인사이트 공유

CertiK은 IDAI Summit 2026에서 Web3 생태계의 AI 도입이 가속화됨에 따라 새롭게 부상하는 보안 위협을 분석했습니다. 간접 참조 취약점, 메모리 오염 등 AI 에이전트의 구조적 보안 리스크를 살펴보고, 디지털 자산 보호를 위한 지능형 실시간 감사의 중요성을 강조했습니다.