Skip to content

feat(s137): the toolchain floor refused at boot, and ML-DSA-87 known-answer tests through :crypto - #132

Merged
HackTuah merged 3 commits into
mainfrom
slice/137-toolchain-floor
Oct 20, 2026
Merged

HackTuah merged 3 commits into
mainfrom
slice/137-toolchain-floor

Conversation

@HackTuah

Copy link
Copy Markdown
Member

Slice 137. A node refuses to boot on an Erlang runtime below the release that fixed CVE-2026-55737 for its line, or on Elixir outside 1.20.x. Whether ML-DSA-87 is usable is answered by known-answer tests through :crypto, not by a listing that is always empty in FIPS mode.

What it does

  • The floor, in every profile, before any child starts. Trinity.Toolchain reads the three rows of erlang/otp's otp_versions.table for the fixing releases (OTP-27.3.4.15, OTP-28.5.0.4, OTP-29.0.4), checked in verbatim at priv/otp_floor.table with their source commit and SHA-256. It compares erts and every loaded application the row names, because the same releases fix CVEs in ssl, public_key, crypto, ssh and stdlib. Majors above 29 pass, below 27 refuse. Elixir must match ~> 1.20.0. The comparison uses erts and the loaded applications, not OTP_VERSION, because an assembled release carries no OTP_VERSION file.
  • ML-DSA-87 by known answers. Trinity.Crypto.Preflight.mldsa87/1 runs six named steps: NIST ACVP's keyGen case (the published secret key regenerates the published public key), Wycheproof's valid, bit-flipped and malformed-hints signatures, then a fresh key's generate, sign, verify and one-bit refusal. Vectors are at priv/kat/, each with its source commit and file SHA-256. OTP's build_pkey_type_list/3 (lib/crypto/c_src/pkey.c) lists no ML-DSA or SLH-DSA type whenever FIPS mode is on, at OTP-28.5.0.7, OTP-29.0.6 and master, whatever the provider can do.
  • Two findings in Trinity's own signer, fixed.
    • Signer.MLDSA87.available?/0 was that listing, so on a FIPS node whose provider signs ML-DSA-87 a regulated boot would have refused a node that can sign. It now answers from the preflight, and KeyCustody.select/0 returns the preflight's reason, so a refused regulated boot names the step. :default stays fail-open over the signer, held by a node test.
    • OTP 28.5.0.8 returns the ML-DSA-87 private key bare (4,896 bytes) where earlier runtimes returned it tagged, and encode_private/1 matched only the tagged form, so the receipts supervisor crashed at boot. A bare key is now stored tagged by its size; ML-DSA-87 has exactly two private-key sizes, so this is unambiguous.
  • The boot receipt carries erts, elixir and mldsa87_preflight in the signed subject, and the receipts page shows them.
  • Control mapping: SI-2 and SC-13 rows. The census now enumerates Trinity.Toolchain's checks and the preflight, so a check no row cites fails.

Evidence

  • Red first, on the unchanged lib/: a boot below the floor printed BOOT_OK, a regulated ML-DSA-87 refusal named no_approved_signer instead of the step, and a bare private key raised FunctionClauseError. All green with the change.
  • A real boot on OTP 28.5.0.3 (erts 16.4.0.3, prebuilt by Hex, sha256-pinned), run locally, refuses with otp_below_floor naming 16.4.0.3 and 16.4.0.4. CI repeats it in a new gate step.
  • On OTP 28.5.0.8 with OpenSSL 3.5.9, a regulated node with ML-DSA-87 receipts boots, and its boot receipt is signed receipt_v3_mldsa87 and verifies offline. A planted garbage signature turns that case red with {:signature_invalid, 1}.
  • The census fails with the SI-2 row removed, naming the uncited check.
  • ML-DSA-87 signs only where libcrypto is 3.5 or later; the gate runners link OpenSSL 3.0. The fips job runs the three ML-DSA-87 test files whole with the mode off, and TRINITY_REQUIRE_MLDSA87_RUNTIME=1 fails that step if the runtime cannot sign, rather than excluding the tests.

Declared gap

In FIPS mode, on the image's Red Hat provider, ML-DSA-87 does not sign, so a regulated FIPS node cannot use ML-DSA-87 receipts yet. A provider validated with ML-DSA-87 (CMVP #5523) would change that. Getting one is a procurement item, outside this change.

…ated ML-DSA-87 refusal does not name its step, a bare private key crashes the signer

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…and ML-DSA-87 known-answer tests through :crypto)

Signed-off-by: Ayla Croft <aylacroft@proton.me>
…state, so the gate in FIPS mode inherits no keys or rows

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit 6b05aa9 into main Oct 20, 2026
12 checks passed
@HackTuah
HackTuah deleted the slice/137-toolchain-floor branch October 20, 2026 00:35
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.

1 participant