WO-02 — SHAKE256 / KMAC256 wrapper

7 October 2026 · Built by Claude on branch wo-02-shake (from wo-01-librl). Local checks passed. Not committed or merged without the owner’s approval; awaiting the owner’s review.

Delivered

File

What

e46/include/e46/shake.hpp, e46/src/shake.cpp

shake256, shake256_256, incremental Shake256, E46 domain tagging shake256_tagged (SHAKE256(ASCII(tag) ‖ 0x00 ‖ data)), kmac256 (SP 800-185, output length bound into the MAC)

CMakeLists.txt

static target E46::shake linked to OpenSSL 3 (find_package(OpenSSL 3.0), Homebrew openssl@3 path hint); three new tests

e46/tools/shake_cli.cpp

command-line driver for differential tests and surprise exams

e46/tests/shake_tests.cpp

unit checks: known answers, XOF prefix property, KMAC length binding, tag rule, incremental splits, every error path

e46/golden/generate_wo02.py, e46/golden/wo02-golden.json

independent pure-Python reference (Keccak-f[1600] from FIPS 202; cSHAKE256/KMAC256 from SP 800-185; no OpenSSL) and its frozen vectors

e46/tests/verify_wo02.py

C++ vs golden, every vector, every mode

No SHA-2, no floating point, no cryptographic code of our own beyond the independent test reference. Nothing was downloaded or installed: OpenSSL 3.6.5 was already present via Homebrew.

How the reference was trusted before freezing

Check

Result

Pure-Python SHAKE256 vs Python hashlib.shake_256

identical on every vector (asserted during generation)

Pure-Python KMAC256 vs the OpenSSL command-line tool (openssl mac … KMAC256)

40/40 random cases identical

NIST SP 800-185 KMAC256 sample #4 (key 0x40…0x5F, data 00010203, “My Tagged Application”, 512 bits)

20c570c3…77a8dd reproduced by pure Python, OpenSSL CLI and the C++ library

Golden regenerates byte for byte (--check, also a CTest)

yes

Results

Gate (WORK-ORDERS.md)

Result

NIST known answers

SHAKE256(“”) and SHAKE256(“abc”) (FIPS 202), KMAC256 sample #4 (SP 800-185): pass

Matches Python hashlib.shake_256

yes: golden + surprise exam

Golden differential (442 SHAKE256 × 4 modes, 60 tagged, 98 KMAC256)

1,926 comparisons, 0 mismatches

Surprise exam (fresh inputs up to 20,000 bytes, seed never recorded): SHAKE vs hashlib, tagged vs hashlib, KMAC vs pure Python

17,800 comparisons, 0 mismatches

Release CTest (WO-01 + WO-02)

5/5 passed

ASan + UBSan CTest

5/5 passed

-Wall -Wextra -Werror

no warnings

clang-format 22.1.8 (repo UNC style) --dry-run --Werror

clean

Source fingerprints (SHAKE256, first 16 bytes): shake.hpp f4060cdc…, shake.cpp d6ef4269…, shake_cli.cpp dc888abd…, shake_tests.cpp d1ca55f9…, verify_wo02.py 8f20ab5e…, generate_wo02.py 12657d9d…, wo02-golden.json 777daba1…, CMakeLists.txt 515ab685….

Decisions to review

  1. Tag rule: tags must be non-empty printable ASCII with no spaces or NUL; the separator is one 0x00 byte, exactly as the spec writes "E46-txid\0" ‖ data.

  2. Limits: SHAKE output ≤ 1 MiB per call (wrapper policy; E46 uses 16/32/64 bytes). KMAC256 key 4–512 bytes, customization ≤ 512 bytes, output 1–2,097,151 bytes, checked before calling OpenSSL (these mirror OpenSSL’s own bounds), so errors are deterministic.

  3. KMAC mode: non-XOF (output length bound into the MAC), as SP 800-185 KMAC256 defines; a test proves the 32-byte and 64-byte results differ.

  4. Not done: no fuzz harness (the wrapper has no parser of its own; input parsing lives in the CLI’s shared hex helper from WO-01, which WO-01 already fuzzed); no benchmark (speed is OpenSSL’s, and no WO-02 gate requires it).

  5. Dependency: E46 now requires OpenSSL ≥ 3.0 at build time. On Linux the system OpenSSL 3 works; the macOS path hint is only used if OPENSSL_ROOT_DIR isn’t set.

Reproduce

cmake --preset release && cmake --build --preset release && ctest --preset release
cmake --preset sanitize && cmake --build --preset sanitize && ctest --preset sanitize
python3 -B e46/golden/generate_wo02.py --check