Skip to content

feat(s143): the ML-DSA-87 boot check by role - #139

Merged
HackTuah merged 1 commit into
mainfrom
slice/143-boot-check-by-role
Oct 21, 2026
Merged

HackTuah merged 1 commit into
mainfrom
slice/143-boot-check-by-role

Conversation

@HackTuah

Copy link
Copy Markdown
Member

Slice 143. Trinity's ML-DSA-87 check at boot is now chosen by role.

  • Every node runs a verifier check against nine published cases.
  • A node that signs receipts with ML-DSA-87 also runs a signer check. It uses a published test key and generates no key at boot.
  • On the nss profile a node refuses to start unless FIPS mode is on and its FIPS provider reports the build string of the validated module.

What it does

  • The verifier check (Trinity.Crypto.Preflight.verifier/1) runs at boot in every release and profile, right after the toolchain floor. Each case must give exactly its expected answer:

    • must verify: NIST ACVP's ML-DSA-87 sigGen case (external interface, pure, empty context) and Wycheproof's valid signature;
    • must not verify: ACVP's modified signature; Wycheproof's modified signature, hint encoding, infinity norm and wrong length; and the valid signature with one bit of its message, then of its signature, flipped.

    A verifier that says true to everything fails at the first negative. The cases and their measured answers are in priv/kat/mldsa87_boot_check.json; the vectors are trimmed from the same pinned commits as slice 137's and carry their source and SHA-256.

  • What refuses a boot (Trinity.Crypto.BootCheck):

    • a case answered wrongly, in every profile, naming the case;
    • on nss: FIPS mode not enabled, no FIPS provider, a provider whose build string is not "3.6.0-r4", or a runtime that cannot run the check. Each is named.

    Elsewhere, a runtime without ML-DSA-87 boots and the boot receipt says unavailable.

  • The build string is the one the validated module's security policy names. This is the node checking itself, not the operator check the policy describes, and it is never cited as one. The policy also names the provider's name, which cannot be checked: OTP's crypto:info/0 does not expose it.

  • The signer check (Trinity.Crypto.Preflight.signer/1) replaces slice 137's six steps where receipts sign with ML-DSA-87:

    • the verifier check;
    • ACVP's keyGen case;
    • a signature made with ACVP's published test key, which must verify, and must not on a flipped message.

    No key is generated: the provider's own self-tests cover generation, and a key made at boot cannot be wiped from the BEAM's memory.

  • The boot receipt records mldsa87_verifier and, where it ran, mldsa87_signer. The check runs once per boot, where slice 137 ran its steps, a fresh key included, each time the boot receipt was written.

  • Neither check is FIPS evidence. Self-tests are the module's duty. Outside the approved mode the provider owes none, so on commercial and on development runtimes this is the only ML-DSA-87 test that runs.

Evidence

  • Red first. On main before the change, a node whose verifier answers a published case wrongly booted. On this branch it refuses, naming the case.
  • The real :crypto. On OTP 28.5.0.8 with OpenSSL 3.5.9 every case gives its expected answer and the signer check passes, run locally. On the gate's OpenSSL 3.0.13 the check is unavailable and a commercial node boots.
  • The fault seam. A planted wrong answer at each case and each step is refused by name.
  • Real boots on nss refuse by mode on runtimes outside FIPS mode, and by build string in FIPS mode (the fips job). The pass on nss needs the validated module's provider, which no CI image carries. It is shown by the decision's own tests, and docs/fips-leg.md records the gap.

CI

The fips job's mode-off ML-DSA-87 step also runs test/trinity/crypto/boot_check_test.exs and test/trinity/crypto/boot_check_node_test.exs.

Not in this PR

  • Loading or claiming the validated module inside the fielded image.
  • The provider-name check, which OTP cannot do.

… verifier check on every node, the signer check without a fresh key, the FIPS provider's identity on nss)

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit 2ad4ed8 into main Oct 21, 2026
12 checks passed
@HackTuah
HackTuah deleted the slice/143-boot-check-by-role branch October 21, 2026 00:05
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