Repository navigation
BeamMCP.Signer.MLDSA87: ML-DSA-87 through OTP's :crypto, checked before it is returned - #19
Merged
Merged
Conversation
…re it is returned
A second module behind core's unchanged BeamMCP.Signer behaviour:
ML-DSA-87 (FIPS 204), pure, external interface, empty context, the form
RFC 9964 requires for COSE. Ed25519 is untouched.
- The host hands in the 32-byte seed under :private_key and its
2,592-byte public key under :public_key, each as bytes or a zero-arity
function. The public key is required: :crypto signs from a seed but
cannot derive the public key from one.
- Every signature is verified under that public key, and must not
verify over the bytes with one bit flipped, before it is returned.
Otherwise the answer is {:error, {:public_key, :does_not_verify}} or
{:error, :self_check_failed}, and no signature leaves the module.
- The :crypto calls are sign/4 and verify/5 with :mldsa87 and :none as
literals and no options argument, so no prehash and no context; the
key census now covers both modules and pins that.
- Refusals are answers that carry no key byte, as for Ed25519. On a
runtime without ML-DSA-87 (OTP 27, or OpenSSL before 3.5) every call
is {:error, :crypto_refused}; the package's OTP floor stays 27.
Tests:
- test_helper.exs runs the ML-DSA tests where :crypto offers mldsa87
and the :crypto_refused test where it does not; with MLDSA_REQUIRED=1
(CI's box leg) a runtime without it is refused.
- The runtime is held to NIST ACVP and Wycheproof ML-DSA-87 vectors:
every case :crypto can express (ACVP 1 of 60, Wycheproof 234 of 241),
the rest counted by reason (a context, HashML-DSA, the internal
interface), so a case that comes into reach fails the count.
tools/vectors_mldsa87.sh writes the fixtures at pinned commits and
sha256s.
- CI: the box leg writes a signature, which a new job verifies with
Go's crypto/mldsa (which also derives the public key from the seed)
and with aws-lc-rs; each must refuse it over the message with one bit
flipped. The box leg also records ML-DSA-87 latency, no threshold.
- Ed25519's "nothing kept" test compared table names, and ExUnit's own
per-module tables matched once more test files compiled; it now
refuses any table the call leaves owned by the calling process.
Docs: README, SECURITY.md, the assurance case, the architecture page,
the security review checklist, the roadmap (the second algorithm it
said would be a separate module), GOVERNANCE (seven checks),
CONTRIBUTING and the CHANGELOG.
Measured locally:
OTP 28.5.0.8 over OpenSSL 3.5.9, Elixir 1.20.4: every CI step
exit 0; Result: 37 passed, 1 excluded
OTP 28.5.0.8 over OpenSSL 3.0.13, Elixir 1.18.4: every CI step
exit 0; 38 tests, 0 failures, 11 excluded
the same, MLDSA_REQUIRED=1: refused (exit 1)
Go 1.27.3 crypto/mldsa and aws-lc-rs 1.18.2: the signature
verifies, the flipped message does not, a tampered signature
fails (exit 1)
Eleven mutants of the module, each compiling clean: ten killed. The
one that survives removes the second check; on a correct runtime a
signature never verifies over different bytes, so no test can see it
go. That check guards against a faulty runtime.
Signed-off-by: Ayla Croft <aylacroft@proton.me>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
BeamMCP.Signer.MLDSA87, an ML-DSA-87 (FIPS 204) signer behind core's unchangedBeamMCP.Signerbehaviour. It signs pure ML-DSA-87 through the external interface with an empty context, the form RFC 9964 requires for COSE (algorithm -50).BeamMCP.Signer.Ed25519is unchanged.The module
:private_key) and its 2,592-byte public key (:public_key), each as bytes or a zero-arity function. The public key is required because:cryptosigns from a seed but cannot derive the public key from one.{:error, {:public_key, :does_not_verify}}or{:error, :self_check_failed}, and no signature leaves the module. A seed and public key that are not a pair cannot produce a signature.:crypto.sign/4and:crypto.verify/5with:mldsa87and:noneas literals and no options argument. The census now covers both modules and pins this.{:error, :crypto_refused}. The package floor stays OTP 27.Tests and CI
test_helper.exsruns the ML-DSA tests where:cryptooffersmldsa87, and the:crypto_refusedtest where it does not. WithMLDSA_REQUIRED=1(the box leg) a runtime without ML-DSA-87 is refused, so that leg cannot pass by skipping.:cryptocan express (ACVP 1 of 60, Wycheproof 234 of 241). The rest are counted by reason (a non-empty context, HashML-DSA, the internal interface), so a case that comes into reach fails the count until it runs.tools/vectors_mldsa87.shwrites the fixtures from pinned commits, checked by sha256. The fixtures are test-only and not in the Hex package.crypto/mldsaand with aws-lc-rs 1.18.2 (Cargo.lock, Rust 1.92.0). Each must refuse it over the message with one bit flipped. Go also confirms the public key is the one the seed derives.tools/bench_mldsa.exs), with no threshold. A shared runner's figure is the runner's.Evidence (local)
Result: 37 passed, 1 excluded.38 tests, 0 failures, 11 excluded. WithMLDSA_REQUIRED=1it is refused (exit 1).reuse lint: compliant. The ACVP fixtures are underNIST-Software, Wycheproof under Apache-2.0, each with the change noted.Not yet measured
Before merging