Releases: nandanito/slh-dsa-zig
Release list
v0.1.1 — supersedes v0.1.0 (EXPERIMENTAL)
⚠️ Supersedes v0.1.0
v0.1.0was tagged hours earlier and shipped two documentation defects. Use this release. The library itself is byte-for-byte behaviour-identical —git diff v0.1.0..v0.1.1 -- src/is comment-only. See What changed since v0.1.0 at the end.
🚧 EXPERIMENTAL — DO NOT USE IN PRODUCTION
Not audited by a third party. Passing KATs proves conformance to the spec, not resistance to an attacker. A tag is not an audit. Do not use this to protect anything you care about.
A pure-Zig implementation of SLH-DSA (FIPS 205) — the stateless hash-based post-quantum signature scheme also known as SPHINCS+. All 12 parameter sets, comptime-parameterised, no dependencies beyond the Zig standard library.
What's in it
The complete scheme, validated against external ground truth:
- Key generation, signing, verification — FIPS 205 §9 (internal) and §10 (external, with context strings)
- WOTS+ (§5), XMSS (§6), hypertree (§7), FORS (§8)
- Both hash families — SHAKE (§11.1) and SHA-2 (§11.2), including the SHA-512 widening at security category 3 and above
NIST ACVP: keyGen 120/120 · sigGen 336/336 byte-identical · sigVer 336/336 accept/reject — across all 12 parameter sets, internal and external interfaces.
Phase gates
| Gate | Status |
|---|---|
| 1. Functional | ✅ ACVP passes on x86_64 and ARM64 |
| 2. Constant-time | ✅ keygen + signing verified end-to-end in SK.seed/SK.prf under Valgrind, plus WOTS+/FORS primitives in isolation, with a negative control proving the taint is live. x86-64-v3 only (#6) |
| 3. Fuzz | 🚧 harnesses + nightly cumulative workflow merged; 24h total still accruing |
| 4. Benchmark | ✅ with one named exception — see below |
| 5. Documentation | ✅ nandan.me/slh-dsa-zig |
| 6. Banner | 🚧 EXPERIMENTAL stays until a third-party audit |
On the benchmark gate
Measured portable-against-portable on x86-64 (-Dcpu=x86_64_v3 — AVX2, no SHA-NI), against PQClean's portable clean C reference: 35 of 36 measurements within 2×.
SLH-DSA-SHA2-256s keygen sits at 2.06×. That is reported rather than papered over, and it is attributed rather than excused: the SHAKE sets sit at 0.96×, and both families run the same structural code — wots.zig, xmss.zig, hypertree.zig and fors.zig are all generic over hash.zig's Hash(p), and nothing above the adapter branches on hash family. That control isolates the excess to the hash-adapter layer — std.crypto's SHA-2 implementations — rather than to this library's SLH-DSA machinery.
The two adapters differ by more than the primitive, which is worth stating precisely: §11.1 hashes the expanded 32-byte address while §11.2 hashes the 22-byte compressed ADRSc; §11.2's H_msg adds an MGF1 layer §11.1 has no equivalent of; and within §11.2 the primitive changes with security level — H, T_l and H_msg are SHA-256 at n = 16 but SHA-512 at n = 24, 32. Since SLH-DSA-SHA2-256s is n = 32, its keygen drives F (SHA-256) and H (SHA-512), so the exceedance is not attributable to SHA-256 specifically. The conclusion holds regardless — §11.2 feeds less data per hash call and is still slower, and MGF1 runs once per operation against millions of F/H calls.
An accelerated build (+sha) passes 36/36 at 0.33×–1.00×, and is published alongside but never gated — a gate you can pass with a CPU feature is a gate that cannot fail for the right reason. Full numbers and reasoning in bench/README.md.
Known limitations
- HashSLH-DSA pre-hash variants are not implemented (#45) — those ACVP groups are skipped, never silently dropped
- Verification is not constant-time audited, by construction: it handles only public data
- The constant-time audit is x86-64 only, with AVX-512 paths excluded (#6)
xmss_signrecomputes shared subtrees — iterative treehash is open (#38)
Install
zig fetch --save git+https://github.com/nandanito/slh-dsa-zig.git#v0.1.1Requires Zig 0.16.0.
Documentation
nandan.me/slh-dsa-zig — a learning-oriented guide: why hash-based signatures exist, a chapter per component in dependency order, per-module walkthroughs mapped to FIPS 205, a glossary, and a build-it-yourself study path for deriving SLH-DSA from the standard yourself.
Licence
Code 0BSD. Documentation CC BY 4.0 (LICENSE-DOCS) — reuse it, credit it.
What changed since v0.1.0
Documentation and comments only.
1. A stale example header. examples/basic_sign.zig still carried "This example will @panic until the scheme bodies land" from the stub era. examples is in build.zig.zon's paths, so that shipped inside the package — the most obvious first file told a new reader the library does not work.
2. The benchmark attribution named the wrong primitive. Four documents said the SHAKE and SHA-2 sets "differ only in the hash primitive" and pinned the one over-gate measurement on std.crypto's SHA-256. The two hash adapters differ on three axes — ADRS encoding, MGF1, and the SHA-256/SHA-512 split by security level — and SLH-DSA-SHA2-256s is n = 32, where H and T_l are SHA-512. Nine instances across four files, six of them inside the published package.
The structural half of that argument is unchanged and still holds: every layer above hash.zig is generic over Hash(p), so the SHAKE control does establish that this library's SLH-DSA machinery is at parity with PQClean's. What moved is that the claim is now made at hash-adapter granularity, where it is true.
Everything else — all 12 parameter sets, ACVP 120/120 and 336/336, the constant-time audit, and the benchmark numbers themselves — is unchanged from v0.1.0.
v0.1.0 stays published and marked superseded. It is the honest record of what was tagged.
v0.1.0 — superseded by v0.1.1 (EXPERIMENTAL)
⛔ Superseded by v0.1.1
Use
v0.1.1instead. This tag shipped two documentation defects, both fixed hours later: a stale header inexamples/basic_sign.zigclaiming the example panics, and a benchmark attribution namingstd.crypto's SHA-256 where the failing measurement isn = 32and therefore SHA-512 (nine instances across four files, six of them inside the package).The library is behaviour-identical —
git diff v0.1.0..v0.1.1 -- src/is comment-only — so nothing here is unsafe, it is inaccurate. This release stays published as the honest record of what was tagged.
🚧 EXPERIMENTAL — DO NOT USE IN PRODUCTION
Not audited by a third party. Passing KATs proves conformance to the spec, not resistance to an attacker. A tag is not an audit. Do not use this to protect anything you care about.
A pure-Zig implementation of SLH-DSA (FIPS 205) — the stateless hash-based post-quantum signature scheme also known as SPHINCS+. All 12 parameter sets, comptime-parameterised, no dependencies beyond the Zig standard library.
What's in it
The complete scheme, validated against external ground truth:
- Key generation, signing, verification — FIPS 205 §9 (internal) and §10 (external, with context strings)
- WOTS+ (§5), XMSS (§6), hypertree (§7), FORS (§8)
- Both hash families — SHAKE (§11.1) and SHA-2 (§11.2), including the SHA-512 widening at security category 3 and above
NIST ACVP: keyGen 120/120 · sigGen 336/336 byte-identical · sigVer 336/336 accept/reject — across all 12 parameter sets, internal and external interfaces.
Phase gates
| Gate | Status |
|---|---|
| 1. Functional | ✅ ACVP passes on x86_64 and ARM64 |
| 2. Constant-time | ✅ keygen + signing verified end-to-end in SK.seed/SK.prf under Valgrind, plus WOTS+/FORS primitives in isolation, with a negative control proving the taint is live. x86-64-v3 only (#6) |
| 3. Fuzz | 🚧 harnesses + nightly cumulative workflow merged; 24h total still accruing |
| 4. Benchmark | ✅ with one named exception — see below |
| 5. Documentation | ✅ nandan.me/slh-dsa-zig |
| 6. Banner | 🚧 EXPERIMENTAL stays until a third-party audit |
On the benchmark gate
Measured portable-against-portable on x86-64 (-Dcpu=x86_64_v3 — AVX2, no SHA-NI), against PQClean's portable clean C reference: 35 of 36 measurements within 2×.
SLH-DSA-SHA2-256s keygen sits at 2.06×. That is reported rather than papered over, and it is attributed rather than excused: the SHAKE sets sit at 0.96×, and both families run the same structural code — wots.zig, xmss.zig, hypertree.zig and fors.zig are all generic over hash.zig's Hash(p), and nothing above the adapter branches on hash family. That control isolates the excess to the hash-adapter layer — std.crypto's SHA-2 implementations — rather than to this library's SLH-DSA machinery.
The two adapters differ by more than the primitive, which is worth stating precisely: §11.1 hashes the expanded 32-byte address while §11.2 hashes the 22-byte compressed ADRSc; §11.2's H_msg adds an MGF1 layer §11.1 has no equivalent of; and within §11.2 the primitive changes with security level — H, T_l and H_msg are SHA-256 at n = 16 but SHA-512 at n = 24, 32. Since SLH-DSA-SHA2-256s is n = 32, its keygen drives F (SHA-256) and H (SHA-512), so the exceedance is not attributable to SHA-256 specifically. The conclusion holds regardless — §11.2 feeds less data per hash call and is still slower, and MGF1 runs once per operation against millions of F/H calls.
An accelerated build (+sha) passes 36/36 at 0.33×–1.00×, and is published alongside but never gated — a gate you can pass with a CPU feature is a gate that cannot fail for the right reason. Full numbers and reasoning in bench/README.md.
Known limitations
- HashSLH-DSA pre-hash variants are not implemented (#45) — those ACVP groups are skipped, never silently dropped
- Verification is not constant-time audited, by construction: it handles only public data
- The constant-time audit is x86-64 only, with AVX-512 paths excluded (#6)
xmss_signrecomputes shared subtrees — iterative treehash is open (#38)
Install
zig fetch --save git+https://github.com/nandanito/slh-dsa-zig.git#v0.1.0Requires Zig 0.16.0.
Documentation
nandan.me/slh-dsa-zig — a learning-oriented guide: why hash-based signatures exist, a chapter per component in dependency order, per-module walkthroughs mapped to FIPS 205, a glossary, and a build-it-yourself study path for deriving SLH-DSA from the standard yourself.
Licence
Code 0BSD. Documentation CC BY 4.0 (LICENSE-DOCS) — reuse it, credit it.