Skip to content

SLH-DSA: build with NO_ASN and leave out unused hash families - #11894

Merged
philljj merged 4 commits into
wolfSSL:masterfrom
LinuxJedi:slhdsa-noasn-hash-gating
Oct 26, 2026
Merged

philljj merged 4 commits into
wolfSSL:masterfrom
LinuxJedi:slhdsa-noasn-hash-gating

Conversation

@LinuxJedi

@LinuxJedi LinuxJedi commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

Description

Two footprint problems in wc_slhdsa.c, found while measuring post-quantum verify-only builds on Cortex-M33:

  1. SLH-DSA did not build with NO_ASN. asn.h was included unconditionally and the DER encode/decode functions had no NO_ASN guard. asn.h needs an mp_int type, so a minimal verify-only build (no RSA/ECC/DH, no big-int math) had to compile asn.c, sp_int.c and wolfmath.c just to link. The include, the DER functions and their prototypes are now guarded on NO_ASN. settings.h also keeps misc.c's c32toa/ato32 for SLH-DSA when NO_ASN + WOLFCRYPT_ONLY would drop them, as it already does for XMSS and LMS.
  2. A SHA2-128s-only build linked SHA-512 and SHA-3 code it never runs. The SHA2/SHAKE hash dispatch was chosen at run time from the parameter set. So even with only SHA2_128S enabled and WOLFSSL_SLHDSA_NO_SHAKE defined, the SHA-512 paths (used only by the category 3 and 5 SHA2 sets) and the SHAKE paths stayed referenced. Now:
    • SHAKE code is compiled only when a SHAKE parameter set is enabled.
    • SHA-512 branches are compiled only when a SHA2-192/256 set is (new WC_SLHDSA_NEED_SHA512).
    • With a single family, the HASH_* macros call it directly and SLHDSA_IS_SHA2() is a constant, so no runtime family check is left.
    • SlhDsaKey only holds the hash state that is used.
    • The pre-hash buffer static assert hard-coded 64 bytes, which fails when only SHA-256 is built. It is replaced by a check in slhdsakey_validate_prehash() that the expected digest fits in WC_MAX_DIGEST_SIZE.

The MC/DC white-box tests in tests/unit-mcdc/ that call these static functions directly are guarded the same way.

SLH-DSA-SHA2-128s verify-only (WOLFSSL_WC_SLHDSA_SMALL + WOLFSSL_WC_SLHDSA_SMALL_MEM), arm-none-eabi-gcc 15.3 -mcpu=cortex-m33 -Os, Thumb-2 asm, --gc-sections:

master this PR
wolfCrypt sources needed to link wc_slhdsa, sha256, sha512, hmac, sha3, asn, sp_int, wolfmath + 3 Thumb-2 asm wc_slhdsa, sha256, thumb2-sha256-asm
wc_slhdsa.o text 5,645 3,143
wc_slhdsa + hash code after gc-sections 28,197 7,591
Whole test image ROM (incl. harness and newlib) 36,040 15,432
sizeof(SlhDsaKey) 1,160 480
Verify stack 892 852

The "this PR" column is the NO_ASN build with only SHA-256. Built with this PR, master's config, which still compiles the extra sources, comes to 15,440 bytes, because nothing references them any more. SLH-DSA-SHAKE-128s is unchanged.

Testing

  • make check with ./configure --enable-slhdsa, --enable-slhdsa=yes,sha2 and --enable-slhdsa=verify-only,small,128s,sha2 (macOS arm64, from a git checkout so -Werror is on).
  • wolfcrypt/test/testwolfcrypt with:
    • --enable-slhdsa=sha2-128s, =verify-only,small,sha2-128s, =sha2-192s and =128s,sha2-128s,small.
    • --enable-cryptonly --disable-asn --disable-rsa --disable-ecc --disable-dh, with =verify-only,sha2-128s, the default, and =yes,sha2.
  • x86_64 --enable-intelasm builds (CC="clang -arch x86_64", run under Rosetta) with =sha2-128s, =yes,sha2 and =verify-only,sha2-192s: build, then testwolfcrypt.
  • test_slhdsa_whitebox and test_slhdsa_hash_fault_whitebox, built and run the way run-whitebox-smoke.sh does, against static =sha2-128s, =verify-only,sha2-128s and =yes,sha2 libraries.
  • QEMU mps2-an505 (Cortex-M33): user_settings.h builds of SHA2-128s and SHAKE-128s verify-only, including NO_ASN builds that compile only wc_slhdsa.c and the one hash they use. Each verifies a host-generated signature and rejects a tampered one.
  • STM32U585 board: the NO_ASN SHA2-128s verifier verifies a signature and rejects a tampered one, using 852 bytes of stack and 12.1 M cycles at the 4 MHz reset clock.

Not covered by this PR:

  • A TLS build with NO_ASN and SLH-DSA still fails in src/internal.c, because wc_SlhDsaOidToParam is only declared when ASN is enabled. That build also failed before this change, earlier, in wc_slhdsa.c.
  • configure still requires SHA-512 and HMAC for any SHA2 parameter set.

Checklist

  • added tests
  • updated/added doxygen
  • updated appropriate READMEs
  • Updated manual and documentation

wc_slhdsa.c included asn.h unconditionally and compiled its DER key
encode/decode functions without a NO_ASN guard. asn.h needs an mp_int
type, so a minimal verify-only build with no big-int math had to compile
asn.c, sp_int.c and wolfmath.c just to link. Guard the include, the DER
functions and their prototypes on NO_ASN. A NO_ASN + WOLFCRYPT_ONLY
build also drops misc.c's c32toa/ato32, which SLH-DSA uses, so keep them
for SLH-DSA as is already done for XMSS and LMS.

The SHA2/SHAKE hash dispatch was decided at run time from the parameter
set, so a SHA2-128s-only build with WOLFSSL_SLHDSA_NO_SHAKE still
referenced SHA-512, which only the category 3 and 5 SHA2 sets use, and
SHAKE-256. Compile the SHAKE code only when a SHAKE parameter set is
built, and the SHA-512 branches only when a SHA2-192/256 set is (new
WC_SLHDSA_NEED_SHA512). With a single family the hash macros call it
directly, and SlhDsaKey holds only the hash state that is used. The
pre-hash buffer bound now follows the hashes that are compiled in, so a
SHA-256-only build no longer fails its static assert.

The MC/DC white-box tests that call these static functions directly are
guarded the same way.

SLH-DSA-SHA2-128s verify-only on Cortex-M33 (-Os, Thumb-2 asm,
gc-sections): wc_slhdsa plus linked hash code goes from 28,197 to 7,629
bytes, and SlhDsaKey from 1,160 to 480 bytes. Verified on QEMU
mps2-an505 and on an STM32U585.
@LinuxJedi

Copy link
Copy Markdown
Member Author

@claude review

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #11894

Scan targets checked: wolfcrypt-src, wolfcrypt-bugs, wolfssl-src, wolfssl-bugs
Coverage: 3 of 5 in-scope changed file(s) opened by the reviewer; not opened: tests/unit-mcdc/test_slhdsa_hash_fault_whitebox.c, wolfssl/wolfcrypt/settings.h

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

Review tier: Lite

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. A human should still look at it, because it changes the SLH-DSA crypto code's hash dispatch and the public SlhDsaKey layout across several new build-macro combinations.

What was reviewed:

  • The #ifdef'd if/else chains in the SHA2 helpers and in Init/Free/sign/verify. With SHAKE or SHA-512 compiled out, no else is left dangling, and n is either guarded or (void)'d.
  • The single-family HASH_* and HASH_T_* macros, and the SHA-256-sized block, digest and HMAC buffers in builds without SHA-512.
  • The SLHDSA_LARGEST_APPROVED_PHM_LEN static assert, and the NO_ASN guards on the DER functions and their prototypes. I also looked at --disable-asn unit tests that still call the DER APIs and ruled that out.
Extended reasoning...

The change affects wc_slhdsa.c, wc_slhdsa.h and settings.h in wolfCrypt, plus the MC/DC whitebox tests. It adds compile-time guards that remove the SHAKE code, the SHA-512 code or the DER/ASN code from SLH-DSA builds. This is crypto signing and verification code, and the size of the public SlhDsaKey struct now depends on WOLFSSL_SLHDSA_NO_SHAKE and WC_SLHDSA_NEED_SHA512. If the library and an application are built with different macros, the struct layouts will not match. The edits are mechanical and I found no bugs, but there are many macro combinations and the surface is security-sensitive, so I deferred instead of approving.

@Frauschi Frauschi left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice footprint win. Built and ran testwolfcrypt on a cryptonly --disable-asn config, a SHAKE + SHA2-128 build and a gcc-16 SHA2-128s verify-only build; all pass. A few small things inline, nothing blocking.

Comment thread wolfssl/wolfcrypt/wc_slhdsa.h Resolved
Comment thread wolfcrypt/src/wc_slhdsa.c Resolved
Comment thread tests/unit-mcdc/test_slhdsa_whitebox.c Resolved
Comment thread wolfcrypt/src/wc_slhdsa.c Resolved
Review follow-ups for the NO_ASN and hash family gating:

- SLHDSA_IS_SHA2() is now a constant when only SHA2 parameter sets are
  compiled in. The runtime family checks then fold away like the HASH_*
  macros, and no SHA2-only path can fall through to a SHAKE branch that
  is compiled out.
- Add SLHDSA_SHA2_USE_SHA256(n) so the SHA2 helpers keep a plain if/else
  with only the SHA-512 arm under WC_SLHDSA_NEED_SHA512, instead of an
  #ifdef'd if and a bare block.
- Replace the pre-hash size static assert, which now mirrored
  WC_MAX_DIGEST_SIZE, with a check in slhdsakey_validate_prehash() that
  the expected PHM fits in WC_MAX_DIGEST_SIZE.
- Update the white-box test rationale: in 128-bit-only builds the SHA-512
  decisions are compiled out rather than unreachable.
@LinuxJedi LinuxJedi removed their assignment Oct 26, 2026
With only SHA2 parameter sets compiled in, SLHDSA_IS_SHA2() is constant,
so the hash family re-init in wc_SlhDsaKey_PrivateKeyDecode() and
wc_SlhDsaKey_PublicKeyDecode() and the SHAKE pre-hash choice in
slhdsa_test() became dead code that clang -Wunreachable-code reports.
The family can only change when both are compiled in, so compile them
only then.
@philljj philljj self-assigned this Oct 26, 2026
@philljj

philljj commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

tests good with multi-test with gcc-c89, c89-clang-tidy, quantum-safe, linuxkm, etc.

Comment thread wolfcrypt/src/wc_slhdsa.c Resolved
Without SHA-512 the SHA-256 branch is the only one left, so the function
read as if control could reach its end without returning a value. Take
the SHA-512 case as an early return instead and finish with the SHA-256
update, and say in the SLHDSA_SHA2_USE_SHA256() comment that it is
constant true when no category 3 or 5 SHA2 set is compiled in.
@philljj
philljj merged commit 32564c5 into wolfSSL:master Oct 26, 2026
447 of 459 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants