From aec08619784942ec40e125047eb33056e30f5410 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Mon, 19 Oct 2026 16:58:32 -0400 Subject: [PATCH 1/3] test(s137): red first; a boot below the floor prints BOOT_OK, a regulated ML-DSA-87 refusal does not name its step, a bare private key crashes the signer Signed-off-by: Ayla Croft --- test/support/mldsa87_oracle.ex | 22 ++ test/test_helper.exs | 25 ++- test/trinity/receipts/signer_test.exs | 36 +++- test/trinity/toolchain_node_test.exs | 295 ++++++++++++++++++++++++++ 4 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 test/support/mldsa87_oracle.ex create mode 100644 test/trinity/toolchain_node_test.exs diff --git a/test/support/mldsa87_oracle.ex b/test/support/mldsa87_oracle.ex new file mode 100644 index 0000000..bdcc85c --- /dev/null +++ b/test/support/mldsa87_oracle.ex @@ -0,0 +1,22 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.TestSupport.MLDSA87Oracle do + @moduledoc """ + Whether this runtime can generate, sign and verify ML-DSA-87, asked of `:crypto` directly. + + An oracle for slice 137's tests, deliberately independent of `Trinity.Crypto.Preflight`, the code + those tests are about. It answers false on a runtime linked to OpenSSL older than 3.5 and, in + FIPS mode, wherever the provider refuses ML-DSA. + """ + + @spec signs?() :: boolean() + def signs? do + {pub, priv} = :crypto.generate_key(:mldsa87, []) + sig = :crypto.sign(:mldsa87, :none, "slice 137 oracle", priv) + :crypto.verify(:mldsa87, :none, "slice 137 oracle", sig, pub) == true + rescue + _ -> false + catch + _, _ -> false + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index be84b45..eec45ad 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -10,13 +10,36 @@ # Slice 032: :local_model (the real embedder through the serving) runs where # TRINITY_LOCAL_MODEL_CACHE names a cache that already holds all-MiniLM-L6-v2 (no download, # no network: a missing model is a failure there, not a skip); excluded by tag elsewhere. +# Slice 137: :below_floor_otp (a real boot on an Erlang runtime below the CVE-2026-55737 floor) +# runs where TRINITY_BELOW_FLOOR_OTP names that runtime's bin directory; excluded by tag elsewhere. +# :mldsa87_runtime (planted faults in the ML-DSA-87 known-answer check, which can only be seen +# where the runtime signs) runs where the oracle says it does. That is derived, not declared, so +# the decision is printed: a leg that loses ML-DSA-87 says so in its own log. fips_leg? = System.get_env("TRINITY_FIPS_LEG") == "1" local_model? = System.get_env("TRINITY_LOCAL_MODEL_CACHE") not in [nil, ""] +below_floor_otp? = System.get_env("TRINITY_BELOW_FLOOR_OTP") not in [nil, ""] +mldsa87_runtime? = Trinity.TestSupport.MLDSA87Oracle.signs?() + +# A step that exists to run the :mldsa87_runtime tests sets this, so a runtime that lost ML-DSA-87 +# fails the step rather than excluding the tests it came to run. +if System.get_env("TRINITY_REQUIRE_MLDSA87_RUNTIME") == "1" and not mldsa87_runtime? do + raise "TRINITY_REQUIRE_MLDSA87_RUNTIME=1, but ML-DSA-87 does not sign on this runtime " <> + "(fips #{:crypto.info_fips()})" +end + +IO.puts( + "test_helper: ML-DSA-87 #{if mldsa87_runtime?, do: "signs", else: "does not sign"} on this " <> + "runtime (fips #{:crypto.info_fips()}); :mldsa87_runtime " <> + if(mldsa87_runtime?, do: "included", else: "excluded") +) ExUnit.start( exclude: [:live, :desktop, :eval] ++ - if(fips_leg?, do: [], else: [:fips]) ++ if(local_model?, do: [], else: [:local_model]) + if(fips_leg?, do: [], else: [:fips]) ++ + if(local_model?, do: [], else: [:local_model]) ++ + if(below_floor_otp?, do: [], else: [:below_floor_otp]) ++ + if(mldsa87_runtime?, do: [], else: [:mldsa87_runtime]) ) # Slice 024's boot receipt is written by a transient Task after the tree starts; once the diff --git a/test/trinity/receipts/signer_test.exs b/test/trinity/receipts/signer_test.exs index 4425797..e09244e 100644 --- a/test/trinity/receipts/signer_test.exs +++ b/test/trinity/receipts/signer_test.exs @@ -44,8 +44,29 @@ defmodule Trinity.Receipts.SignerTest do assert :error = Signer.impl_for_scheme("receipt_v1_ed25519") end + # Slice 137, F-137-1: this compared with `:mldsa87 in :crypto.supports(:public_keys)`, the + # listing that never holds in FIPS mode. The answer is now asked of the operation itself, and + # compared here with an oracle that does the operation independently of the signer. test "ML-DSA-87 reports itself unavailable or available from the runtime, never from the build" do - assert Signer.MLDSA87.available?() == :mldsa87 in :crypto.supports(:public_keys) + assert Signer.MLDSA87.available?() == Trinity.TestSupport.MLDSA87Oracle.signs?() + end + + # Slice 137, F-137-2: OTP 28.5.0.8 on OpenSSL 3.5.9 returns the private key from + # `generate_key(:mldsa87, [])` as a bare 4,896-byte expanded key, where the runtime slice 024 + # measured returned `{:seed | :expandedkey, binary}`; `encode_private/1` matched only the + # tagged form, so the receipts supervisor crashed at boot on the newer runtime. Both shapes + # are taken; the key file keeps its tag either way. + test "a bare private key, as OTP 28.5.0.8 returns it, is stored tagged by its size" do + expanded = :binary.copy(<<7>>, 4896) + seed = :binary.copy(<<9>>, 32) + + assert Signer.MLDSA87.encode_private(expanded) == "expandedkey:" <> expanded + assert Signer.MLDSA87.encode_private(seed) == "seed:" <> seed + + assert Signer.MLDSA87.decode_private(Signer.MLDSA87.encode_private(expanded)) == + {:expandedkey, expanded} + + assert Signer.MLDSA87.encode_private({:seed, seed}) == "seed:" <> seed end test "the RFC 7638 thumbprint: lexicographic members, no whitespace, SHA-256, base64url" do @@ -63,13 +84,20 @@ defmodule Trinity.Receipts.SignerTest do assert {:ok, ^expected} = KeyCustody.select() end - test "a configured ML-DSA-87 is refused where the runtime lacks it, naming the algorithm" do + # Slice 137: the refusal names the known-answer step that failed. This asserted + # `{:no_approved_signer, :mldsa87, _}` until then, a reason that said the algorithm was + # missing without saying which operation the runtime could not do. + test "a configured ML-DSA-87 is refused where the runtime lacks it, naming the step" do Application.put_env(:trinity, :receipts, algorithm: :mldsa87, keys_dir: nil) on_exit(fn -> Application.put_env(:trinity, :receipts, keys_dir: test_keys_dir()) end) case Signer.MLDSA87.available?() do - false -> assert {:error, {:no_approved_signer, :mldsa87, _}} = KeyCustody.select() - true -> assert {:ok, :mldsa87} = KeyCustody.select() + false -> + assert {:error, {:mldsa87_preflight_failed, step, _}} = KeyCustody.select() + assert step in Trinity.Crypto.Preflight.steps() + + true -> + assert {:ok, :mldsa87} = KeyCustody.select() end end end diff --git a/test/trinity/toolchain_node_test.exs b/test/trinity/toolchain_node_test.exs new file mode 100644 index 0000000..6b28ce6 --- /dev/null +++ b/test/trinity/toolchain_node_test.exs @@ -0,0 +1,295 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.ToolchainNodeTest do + @moduledoc """ + Slice 137 at the only place its refusals are real: a boot, in a separate OS process. + + The recipe is `Trinity.RegulatedBootNodeTest`'s: `XDG_DATA_HOME` and `Trinity.BootIsolation` per + case, `MIX_ENV=test` so the child reuses the built test tree, `mix run --no-start`, and every + assertion on the one line the child prints, never on the whole captured output. + + ## The below-floor runtime + + AC2 needs an Erlang runtime that carries CVE-2026-55737. None ships with this tree, so the case + is tagged `:below_floor_otp` and runs where `TRINITY_BELOW_FLOOR_OTP` names the `bin` directory + of one: OTP 28.1.1 on a developer machine, OTP 28.5.0.3 in the CI job built for it. Where the + variable is set, a directory with no `erl` in it is a failure, not a skip. + + The child is the same compiled tree run on the older runtime, which is what a release copied + onto an older ERTS would be. Measured before this file was written: code compiled on OTP + 28.5.0.6 loads and runs on erts 16.1.1 (OTP 28.1.1), Elixir 1.20.4 and `crypto` included. + """ + use ExUnit.Case, async: false + + @moduletag timeout: 180_000 + + @isolate """ + Trinity.BootIsolation.isolate!(System.fetch_env!("TRINITY_BOOT_TAG")) + """ + + @report """ + case Application.ensure_all_started(:trinity) do + {:ok, _} -> + IO.puts("BOOT_OK") + System.halt(0) + + {:error, reason} -> + IO.puts("BOOT_REFUSED " <> inspect(reason, limit: :infinity, printable_limit: :infinity)) + System.halt(3) + end + """ + + # The tail for a node expected to boot with ML-DSA-87 receipts. Booting is not enough: the boot + # receipt is signed afterwards by a transient task (`Trinity.Effects.Boot`), so a key that + # cannot sign would still print BOOT_OK. This waits for the receipt and prints its scheme and the + # offline verifier's answer over the exported chain. + @report_signed """ + case Application.ensure_all_started(:trinity) do + {:ok, _} -> + IO.puts("BOOT_OK") + + receipt = + Enum.find_value(1..50, fn _ -> + Trinity.Receipts.boot_receipt() || (Process.sleep(100) && nil) + end) + + IO.puts("SIGNED " <> ((receipt && receipt.scheme) || "none")) + {:ok, export} = Trinity.Receipts.export("boot") + IO.puts("VERIFY " <> inspect(Trinity.Receipts.Verifier.verify(export |> JSON.encode!() |> JSON.decode!()))) + System.halt(0) + + {:error, reason} -> + IO.puts("BOOT_REFUSED " <> inspect(reason, limit: :infinity, printable_limit: :infinity)) + System.halt(3) + end + """ + + @fixture """ + defmodule ToolchainBootFixture do + @behaviour Trinity.Authority + def stage(staged, _ctx), do: {:ok, staged} + def decide(_staged, decision, _ctx), do: {:ok, decision, "fixture"} + def execute(_staged, _decision, _ctx), do: {:error, :not_used} + def receipt(_kind, _attrs), do: {:error, :not_used} + end + """ + + @regulated_config """ + Application.put_env(:trinity, :mcp_auth, profile: :production) + llm = Application.get_env(:trinity, :llm, []) + Application.put_env(:trinity, :llm, Keyword.put(llm, :models, [ + %{id: "allowed", provider: :req_llm, model: "openai:x", + base_url: "https://models.internal/v1", api_key_env: "NONE", + caps: [:stream], price: %{input: 0.0, output: 0.0}} + ])) + """ + + @mldsa87_receipts """ + r = Application.get_env(:trinity, :receipts, []) + Application.put_env(:trinity, :receipts, Keyword.put(r, :algorithm, :mldsa87)) + """ + + defp case_setup(name) do + tag = Trinity.BootIsolation.tag(name) + on_exit(fn -> Trinity.BootIsolation.drop!(tag) end) + dir = Path.join(System.tmp_dir!(), "toolchain-#{name}-#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + on_exit(fn -> File.rm_rf(dir) end) + {dir, tag} + end + + defp boot(env, script) do + base = [ + {"MIX_ENV", "test"}, + {"TRINITY_PROFILE", nil}, + {"TRINITY_AUTHORITY", nil}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", nil}, + {"TRINITY_REGULATED_EGRESS", "docs.internal"}, + {"TRINITY_MCP_AUTH_PROFILE", nil} + ] + + System.cmd("mix", ["run", "--no-start", "-e", @isolate <> script], + env: base ++ env, + stderr_to_stdout: true + ) + end + + defp refusal_line(out) do + out + |> String.split("\n") + |> Enum.find("", &String.starts_with?(&1, "BOOT_REFUSED")) + end + + # The erts version and OTP major of the runtime in `bin`, asked of that runtime itself. + defp runtime_of(bin) do + {out, 0} = + System.cmd( + Path.join(bin, "erl"), + [ + "-noshell", + "-eval", + ~s|io:format("~s ~s", [erlang:system_info(otp_release), erlang:system_info(version)]), halt().| + ] + ) + + [major, erts] = String.split(String.trim(out), " ") + {major, erts} + end + + describe "AC2: a runtime below the floor refuses before any child starts" do + @describetag :below_floor_otp + + test "the line the child prints names the refusal, the runtime's erts and the floor" do + bin = System.fetch_env!("TRINITY_BELOW_FLOOR_OTP") + + assert File.exists?(Path.join(bin, "erl")), + "TRINITY_BELOW_FLOOR_OTP=#{bin} has no erl; a missing runtime is a failure here" + + {major, erts} = runtime_of(bin) + floor = %{"27" => "15.2.7.11", "28" => "16.4.0.4", "29" => "17.0.4"} + + assert Map.has_key?(floor, major), + "the below-floor runtime is OTP #{major}, which has no row to compare against" + + {root, tag} = case_setup("below-floor") + + {out, status} = + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_BOOT_TAG", tag}, + {"PATH", bin <> ":" <> System.get_env("PATH")} + ], + @report + ) + + line = refusal_line(out) + + assert status == 3, + """ + a node on OTP #{major} (erts #{erts}) did not refuse to boot; that runtime carries \ + CVE-2026-55737, fixed in erts #{floor[major]}. + + #{String.slice(out, -3000, 3000)} + """ + + assert line =~ "otp_below_floor", line + assert line =~ erts, line + assert line =~ floor[major], line + end + end + + describe "AC3: the pinned runtime boots, so the refusal can be met" do + test "a default node boots" do + {root, tag} = case_setup("pinned-default") + {out, status} = boot([{"XDG_DATA_HOME", root}, {"TRINITY_BOOT_TAG", tag}], @report) + assert status == 0, String.slice(out, -3000, 3000) + assert out =~ "BOOT_OK" + end + + test "a regulated node with every condition met boots" do + {root, tag} = case_setup("pinned-regulated") + + {out, status} = + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_BOOT_TAG", tag}, + {"TRINITY_PROFILE", "regulated"}, + {"TRINITY_AUTHORITY", "ToolchainBootFixture"}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"} + ], + @fixture <> @regulated_config <> @report + ) + + assert status == 0, String.slice(out, -4000, 4000) + assert out =~ "BOOT_OK" + end + end + + describe "AC7: a regulated node configured to sign with ML-DSA-87 refuses when it cannot" do + # Through the path that already exists: `KeyCustody.select/0` answers an error, and + # `Profile.check_receipts/2` refuses a regulated boot on it. What this slice adds is that the + # answer comes from the known-answer preflight and names the step that failed. + defp regulated_mldsa87(name, planted, tail \\ @report) do + {root, tag} = case_setup(name) + + fault = + if planted, + do: "Application.put_env(:trinity, :mldsa87_preflight_fault, #{inspect(planted)})\n", + else: "" + + boot( + [ + {"XDG_DATA_HOME", root}, + {"TRINITY_BOOT_TAG", tag}, + {"TRINITY_PROFILE", "regulated"}, + {"TRINITY_AUTHORITY", "ToolchainBootFixture"}, + {"TRINITY_REGULATED_LLM_ENDPOINTS", "https://models.internal"} + ], + @fixture <> @regulated_config <> @mldsa87_receipts <> fault <> tail + ) + end + + # Only where the runtime signs ML-DSA-87: elsewhere the check fails at :keygen before a fault + # planted later can be observed. `test/test_helper.exs` derives the tag from the oracle. + @tag :mldsa87_runtime + test "a failing preflight refuses the boot, naming the step" do + # The seam plants a failure at one step. A tree without the preflight ignores the key and + # boots, which is this test's red. + {out, status} = regulated_mldsa87("mldsa87-preflight-fails", :verify) + line = refusal_line(out) + + assert status == 3, + """ + a regulated node configured to sign receipts with ML-DSA-87 booted although the \ + known-answer check failed at :verify. + + #{String.slice(out, -3000, 3000)} + """ + + assert line =~ "mldsa87_preflight_failed", line + assert line =~ ":verify", line + end + + # With no fault planted the answer is the runtime's own, and the expectation comes from an + # oracle here, not from the code under test: ML-DSA-87 tried directly through `:crypto`. It + # signs only where the linked OpenSSL is 3.5 or later with the mode off (the FIPS image with + # the mode off). It does not on a host linked to OpenSSL 3.0, measured on hacktuah 2026-10-19 + # (`{:no_approved_signer, :mldsa87, :not_supported}`), nor in the mode on Red Hat's provider + # (slice 003). The child inherits this VM's runtime and mode, so the oracle answers for it. + test "with no fault planted, the runtime's own answer decides" do + {out, status} = regulated_mldsa87("mldsa87-preflight-unplanted", nil, @report_signed) + + if Trinity.TestSupport.MLDSA87Oracle.signs?() do + assert status == 0, String.slice(out, -3000, 3000) + assert out =~ "BOOT_OK" + + # The boot receipt was signed with the ML-DSA-87 key this boot made, and the chain verifies + # offline: the path F-137-2 broke, end to end. + assert [_, scheme] = Regex.run(~r/^SIGNED (\S+)$/m, out), out + assert scheme =~ "mldsa87", out + assert out =~ ~r/^VERIFY \{:ok, /m, out + else + assert status == 3, String.slice(out, -3000, 3000) + assert refusal_line(out) =~ "mldsa87_preflight_failed", refusal_line(out) + end + end + end + + describe "the default profile stays fail-open over the signer" do + # Rule 1 of the regulated-boot slice: under :default a signer that cannot sign sets the alarm + # and the node starts with every effect denied. This slice must not change that. + test "a failing ML-DSA-87 preflight does not stop a default node" do + {root, tag} = case_setup("mldsa87-default-fail-open") + + script = + @mldsa87_receipts <> + "Application.put_env(:trinity, :mldsa87_preflight_fault, :sign)\n" <> @report + + {out, status} = boot([{"XDG_DATA_HOME", root}, {"TRINITY_BOOT_TAG", tag}], script) + assert status == 0, String.slice(out, -3000, 3000) + assert out =~ "BOOT_OK" + end + end +end From 4eca675905d866d46c7e2fa227fa977b2c11d304 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Mon, 19 Oct 2026 16:58:41 -0400 Subject: [PATCH 2/3] feat(s137): complete slice 137 (the toolchain floor refused at boot, and ML-DSA-87 known-answer tests through :crypto) Signed-off-by: Ayla Croft --- .github/workflows/gate.yml | 27 +++ CHANGELOG.md | 25 +++ REUSE.toml | 17 ++ docs/07-security-model.md | 24 +++ docs/regulated/control-mapping.md | 2 + lib/trinity.ex | 5 + lib/trinity/application.ex | 14 ++ lib/trinity/crypto/preflight.ex | 171 +++++++++++++++++++ lib/trinity/effects/boot.ex | 12 ++ lib/trinity/receipts/key_custody.ex | 21 ++- lib/trinity/receipts/signer/mldsa87.ex | 24 ++- lib/trinity/toolchain.ex | 151 ++++++++++++++++ lib/trinity_web/live/receipts_live.ex | 6 + priv/kat/acvp/mldsa87_keygen.json | 11 ++ priv/kat/wycheproof/mldsa87_verify.json | 36 ++++ priv/otp_floor.table | 9 + test/control_mapping_test.exs | 15 ++ test/trinity/crypto/preflight_test.exs | 162 ++++++++++++++++++ test/trinity/effects/boot_receipt_test.exs | 24 +++ test/trinity/toolchain_test.exs | 167 ++++++++++++++++++ test/trinity_web/live/receipts_live_test.exs | 3 + 21 files changed, 919 insertions(+), 7 deletions(-) create mode 100644 lib/trinity/crypto/preflight.ex create mode 100644 lib/trinity/toolchain.ex create mode 100644 priv/kat/acvp/mldsa87_keygen.json create mode 100644 priv/kat/wycheproof/mldsa87_verify.json create mode 100644 priv/otp_floor.table create mode 100644 test/trinity/crypto/preflight_test.exs create mode 100644 test/trinity/toolchain_test.exs diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 01bcca7..ece572c 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -101,6 +101,21 @@ jobs: - run: mix gate + # Slice 137, AC2: a real boot on an Erlang runtime below the CVE-2026-55737 floor refuses. + # OTP 28.5.0.3 (erts 16.4.0.3) is the release just before the fix, prebuilt by Hex for this + # runner's OS and pinned by the sha256 builds.hex.pm publishes in builds.txt. The tree is the + # one `mix gate` just compiled on the pinned OTP; only the runtime is older. `--only` with a + # tag that selects nothing makes `mix test` exit non-zero, so this cannot pass by running + # no test. + - name: A boot on OTP 28.5.0.3, below the floor, refuses + run: | + curl -fsSL -o /tmp/otp-28.5.0.3.tar.gz https://builds.hex.pm/builds/otp/ubuntu-24.04/OTP-28.5.0.3.tar.gz + echo "d4116a141da5081b849bd057a30679b48f229fb41dbe88da171739883a4797fb /tmp/otp-28.5.0.3.tar.gz" | sha256sum -c - + mkdir -p /tmp/otp-28.5.0.3 + tar -xzf /tmp/otp-28.5.0.3.tar.gz -C /tmp/otp-28.5.0.3 --strip-components=1 + (cd /tmp/otp-28.5.0.3 && ./Install -minimal "$PWD" > /dev/null) + TRINITY_BELOW_FLOOR_OTP=/tmp/otp-28.5.0.3/bin mix test --only below_floor_otp test/trinity/toolchain_node_test.exs + # Slice 120 AC1. `reuse lint` is a Python tool and this is the only leg that installs one, # deliberately: `mix gate` has to stay runnable inside the FIPS container and on a developer's # machine without a Python environment, so the REUSE check is a step here rather than a row in @@ -351,6 +366,18 @@ jobs: exit 1 fi + # Slice 137, AC5 and AC7: ML-DSA-87 with the mode off. This image links OpenSSL 3.5, the only + # runtime in CI that signs ML-DSA-87 (the gate runners link 3.0; in the mode, Red Hat's + # provider refuses it), so the ML-DSA-87 path runs here end to end: the planted-step cases, + # a regulated node that boots with ML-DSA-87 receipts and whose boot receipt verifies, and the + # signer's key shapes. TRINITY_REQUIRE_MLDSA87_RUNTIME makes test_helper raise if the runtime + # cannot sign, rather than excluding the cases it came to run. + - name: ML-DSA-87 end to end (FIPS mode off) + env: + ERL_AFLAGS: "-eval application:load(crypto)" + TRINITY_REQUIRE_MLDSA87_RUNTIME: "1" + run: mix test test/trinity/crypto/preflight_test.exs test/trinity/toolchain_node_test.exs test/trinity/receipts/signer_test.exs + - run: mix gate # AC2 and AC3 by test name with this run's id in the log, and AC5's coverage line for diff --git a/CHANGELOG.md b/CHANGELOG.md index 9260622..0f931f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,31 @@ evidence for each increment is retained by the maintainers and summarised here. tree, engineering rules consolidated into `docs/03-conventions.md`, and this changelog added as the public record of delivered work. +### `slice/137`: Trinity refuses a runtime it should not trust + +A node now refuses to start, before anything else runs, on an Erlang runtime that still carries +CVE-2026-55737 (heap corruption when a malformed external term is decoded) or on Elixir older than +1.20.x. The floor is the release that fixed it on each Erlang/OTP line (27.3.4.15, 28.5.0.4, +29.0.4), whose rows of Erlang/OTP's own version table are checked in verbatim: the runtime and every +loaded Erlang/OTP application, ssl and crypto among them, are compared with that release's +versions. Every pinned build is above it; a release copied onto an older runtime is what it stops. + +Whether ML-DSA-87 can be used is now answered by doing it at boot, against published answers (a +NIST key-generation case and three Wycheproof verification cases) and then with a fresh key, +rather than by asking whether the runtime lists the algorithm. +Erlang/OTP lists no ML-DSA type in FIPS mode whatever the cryptographic provider can do, so the +listing would have told a FIPS node with a capable provider that it could not sign. A regulated +node configured to sign receipts with ML-DSA-87 that fails the check refuses to boot and says +which step failed; a default node keeps running with effects denied, as it does for any signer +fault. The boot receipt records the runtime's erts and Elixir versions and the check's answer. +ML-DSA-87 receipt keys made by Erlang/OTP 28.5.0.8, which returns the private key in a different +shape from earlier releases, no longer stop the node from starting. + +Verified by the test suite: a real boot on Erlang/OTP 28.5.0.3, the release before the fix, +refusing in its own operating system process; the floor at every boundary and a property over +generated versions; every step of the ML-DSA-87 check failing by name; and, on the FIPS image with +the mode off, the planted failures against the real cryptographic library. + ## 2026-10-08 ### `slice/136`: the web pages ask who is there diff --git a/REUSE.toml b/REUSE.toml index 968b18e..3815662 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -67,3 +67,20 @@ path = ["test/support/fixtures/nist/**"] precedence = "override" SPDX-FileCopyrightText = "National Institute of Standards and Technology" SPDX-License-Identifier = "CC0-1.0" + +# Slice 137. **Not ours.** The ML-DSA-87 known-answer vectors the boot preflight checks against. +# `priv/kat/acvp/` is one case from NIST's ACVP-Server (`usnistgov/ACVP-Server`), a work of the US +# Government, annotated CC0-1.0 as the NIST catalogues above are. `priv/kat/wycheproof/` is three +# cases from C2SP's Wycheproof, Apache-2.0. Each file names its source commit and the SHA-256 of the +# file it was cut from. The `**/*.json` line above would otherwise assign them to this project. +[[annotations]] +path = ["priv/kat/acvp/**"] +precedence = "override" +SPDX-FileCopyrightText = "National Institute of Standards and Technology" +SPDX-License-Identifier = "CC0-1.0" + +[[annotations]] +path = ["priv/kat/wycheproof/**"] +precedence = "override" +SPDX-FileCopyrightText = "Google LLC and the C2SP Wycheproof contributors" +SPDX-License-Identifier = "Apache-2.0" diff --git a/docs/07-security-model.md b/docs/07-security-model.md index ed82ea8..ed6df74 100644 --- a/docs/07-security-model.md +++ b/docs/07-security-model.md @@ -522,6 +522,30 @@ where the runtime carries it; the boot receipt names the choice and the authorit hash in unsigned metadata (the R21 default). `bin/verify_receipt.exs` verifies an export with `elixir` alone, from an empty directory, with the exit vocabulary 0, 1, 2, 5, 6. +## The runtime floor, and ML-DSA-87 by doing it (Slice 137, as built) + +Before anything else starts, and in every profile, `Trinity.Application` refuses a runtime below +the fix for CVE-2026-55737 ("heap corruption when an invalidly encoded tuple with an arity of 2^31 +or larger is decoded from Erlang's External Term Format"). The defect is in erts, so erts is what +is compared: `Trinity.Toolchain.floor/0` holds the fixed erts per OTP line, read from +`otp_versions.table`. An assembled release has no `OTP_VERSION` file, and `:erlang.system_info(:version)` +answers in every runtime. The same releases fix CVEs in ssl, public_key, crypto, ssh and stdlib, +so erts is not enough: `priv/otp_floor.table` holds the three fixing releases' rows of +`otp_versions.table` verbatim, with their source commit and SHA-256, and every loaded application +the row names must be at or above its version there. Elixir outside 1.20.x is refused the same way. + +`crypto:supports/0` cannot say whether ML-DSA-87 is usable in FIPS mode: OTP's +`build_pkey_type_list/3` omits every ML-DSA and SLH-DSA type whenever the mode is on, whatever the +loaded provider can do. `Trinity.Crypto.Preflight.mldsa87/1` asks the provider by doing the +operation: NIST's ACVP keyGen case (the published secret key must yield the published public key), +Wycheproof's valid, bit-flipped and malformed-hints verify cases, then a fresh key. The ML-DSA-87 +receipts signer reports itself available exactly when that passes. +Under `:regulated`, a node configured for ML-DSA-87 receipts that fails it refuses to boot with +`{:mldsa87_preflight_failed, step, reason}`; under `:default` the receipts supervisor stays +fail-open. Where it signs today: a runtime linked to OpenSSL 3.5 or later with the mode off. In the +mode, on the image's current FIPS provider, it does not; a provider validated with ML-DSA-87 +(CMVP #5523) is what would change that. + ## The effect population, published (Slice 027, as built) The set of things that can cause an effect is small, derived from the tree, and published as diff --git a/docs/regulated/control-mapping.md b/docs/regulated/control-mapping.md index 2ce316c..288b836 100644 --- a/docs/regulated/control-mapping.md +++ b/docs/regulated/control-mapping.md @@ -117,11 +117,13 @@ register does not claim is never Trinity's. | `SC-12` | Keys pass through one behaviour, so a KMS or HSM adapter is a module and a configuration line; none ships in this tree | `lib/trinity/keys.ex` | `test/trinity/keys/signer_seam_test.exs` "a sealed key still signs, which is the point of sealing it" | shared | A KMS or HSM adapter, where one is required | CRM "A KMS or HSM adapter" | | `SC-12` | Custody of the signing key is not addressed: a key in a file shows the records were not altered by anything without read access to it, and nothing about who held it | `docs/10-assurance-case.md` | none | the deployment's | Custody of the signing key | CRM "Custody of the signing key" | | `SC-13` | Signing picks its algorithm by the runtime's mode: Ed25519 by default, ECDSA P-384 with SHA-384 in FIPS mode, where Ed25519 reports itself unavailable. The gate and the suite run in FIPS mode on every push | `lib/trinity/receipts/signer.ex`, `docs/fips-leg.md`, `docs/regulated/crypto-inventory.md` | `test/trinity/receipts/signer_test.exs` "outside FIPS mode the default is Ed25519; in FIPS mode (the fips leg) it is P-384"; `test/fips/receipts_test.exs` "the mode is on, Ed25519 reports unavailable, P-384 is selected and the boot receipt names it" | shared | A validated cryptographic module on the host, established from its certificate and the running binary | Register "FIPS 140-3 validated cryptography in FIPS mode": :unknown; Register "FIPS 140-2 certificates on the Historical list from 2026-09-22": real-world dependency | +| `SC-13` | ML-DSA-87 is offered for receipts only where a known-answer check passes through `:crypto` in the mode the node is in: NIST's ACVP keyGen case (the published secret key yields the published public key), Wycheproof's verify cases (a valid signature, a bit-flipped one and one with malformed hints), then a fresh key. Not where the algorithm is merely listed: OTP lists no ML-DSA type in FIPS mode whatever the provider can do. A regulated node configured for ML-DSA-87 that fails the check refuses to boot naming the step; the boot receipt records the check's answer | `lib/trinity/crypto/preflight.ex`, `priv/kat/`, `lib/trinity/receipts/signer/mldsa87.ex`, `lib/trinity/receipts/key_custody.ex` | `Trinity.Crypto.Preflight.mldsa87/1`; `test/trinity/crypto/preflight_test.exs` "a provider that derives the wrong public key fails at :keygen_kat"; `test/trinity/crypto/preflight_test.exs` "a verifier that accepts Wycheproof's bit-flipped signature fails at :sigver_kat, naming the case"; `test/trinity/crypto/preflight_test.exs` "the ML-DSA-87 signer is available exactly when the preflight passes (F-137-1)"; `test/trinity/toolchain_node_test.exs` "a failing preflight refuses the boot, naming the step" | Trinity's | none | none | | `SC-23` | A request whose `Host` is not the loopback or the configured host is refused before routing, and the websocket origin check is an explicit list in every environment; under `TRINITY_PROFILE=regulated` an origin check of `false` or `:conn` refuses to boot, because a DNS rebinding page controls both | `lib/trinity_web/plugs/host_allow_list.ex`, `lib/trinity/profile.ex` | `Trinity.Profile.check_origin/2`; `test/trinity_web/auth/host_and_origin_test.exs` "a foreign Host is 403 before routing, wherever it points"; `test/trinity_web/auth/host_and_origin_test.exs` "check_origin is an explicit list in dev, test and prod" | shared | Setting `PHX_HOST` to the host users reach | CRM "Refusing a request whose Host is not this deployment's" | | `SC-23` | The state an MCP client carries between rounds is sealed with AES-256-GCM: a replay, one tampered byte, an expired envelope and a state bound to other arguments are refused | `lib/trinity/mcp/server/envelope.ex` | `test/trinity/mcp/server_mrtr_test.exs` "a replay inside the partition, one tampered byte, an expired envelope and a state bound to other arguments are refused; a missing state is a first call" | Trinity's | none | Register "Envelope MAC in FIPS mode is an approved algorithm": :unknown | | `SC-24` | Failure denies: an unavailable signer denies every effect, a tool absent from the catalogue or a decision other than allow is denied before anything runs, and an authority adapter that cannot be loaded refuses the boot | `lib/trinity/effects.ex`, `lib/trinity/authority/selection.ex` | `test/trinity/effects/membrane_test.exs` "a :catalog tool absent from the catalog, or a decision other than allow, is denied before anything runs"; `test/trinity/authority/selection_test.exs` "an absent module is refused as not loaded, by name" | Trinity's | none | Register "Nothing fails open": :unknown | | `SC-28` | Anything SQLite indexes is left unencrypted by design, for the volume beneath it | `docs/encryption-at-rest.md` | none | the deployment's | Volume or page-level encryption below SQLite | CRM "Encryption of everything SQLite indexes" | | `SC-28(1)` | Blobs nothing indexes (skill files, staged changes, archives) are sealed with AES-256-GCM under a fresh data key each | `lib/trinity/vault.ex` | `test/trinity/vault_test.exs` "a sealed blob is ciphertext: the plaintext does not appear in it"; `test/trinity/vault_test.exs` "every blob gets its own data key, so one compromise does not open the next" | Trinity's | none | CRM "Envelope encryption of blobs"; Register "Encryption of data at rest with deployment-controlled keys": :unknown | +| `SI-2` | Every profile refuses to boot, before any child starts, on a runtime below the release that fixed CVE-2026-55737 for its line (OTP 27.3.4.15, 28.5.0.4 or 29.0.4): erts and every loaded application are compared with that release's row of `otp_versions.table`, checked in verbatim with its source commit and SHA-256. Elixir outside 1.20.x is refused the same way, and the boot receipt records the erts and Elixir it ran on | `lib/trinity/toolchain.ex`, `priv/otp_floor.table`, `lib/trinity/application.ex`, `lib/trinity/effects/boot.ex` | `Trinity.Toolchain.check_otp/3`; `Trinity.Toolchain.check_otp/2`; `Trinity.Toolchain.check_elixir/1`; `test/trinity/toolchain_node_test.exs` "the line the child prints names the refusal, the runtime's erts and the floor"; `test/trinity/toolchain_test.exs` "erts at the floor with an application below it refuses, naming the application"; `test/trinity/toolchain_node_test.exs` "a default node boots" | Trinity's | none | none | | `SI-4` | Documented telemetry events an operator can subscribe to; Trinity emits them and alerts no one | `lib/trinity/telemetry.ex`, `docs/telemetry.md` | `test/trinity/telemetry/telemetry_test.exs` "every event the catalogue lists appears in docs/telemetry.md" | shared | Collecting the events, and alerting on them | CRM "Telemetry events an operator can subscribe to" | | `SI-7` | A changed MCP tool definition is held, not called, until the owner accepts it; the first sighting is the baseline | `lib/trinity/tools/surface.ex`, `lib/trinity/tools/definition_digest.ex` | `test/trinity/mcp/surface_drift_test.exs` "a description change alone holds the tool, with the schema untouched" | Trinity's | none | none | | `SI-7` | Every input to the image build that is not a UBI package is declared with its digest and copied only if declared, and the published bases are pinned by digest | `ci/ironbank/hardening_manifest.yaml`, `lib/mix/tasks/trinity.ironbank.lint.ex` | `mix trinity.ironbank.lint`; `test/ironbank_submission_test.exs` "copying a file no resource declares is refused"; `test/ironbank_submission_test.exs` "a published base pinned by tag and not by digest is refused (planted red)" | Trinity's | none | none | diff --git a/lib/trinity.ex b/lib/trinity.ex index b59c45c..58a42f4 100644 --- a/lib/trinity.ex +++ b/lib/trinity.ex @@ -42,6 +42,11 @@ defmodule Trinity do # starts anything. Pure, so the refusals are unit testable and so this boundary does not # grow a dependency on Authority or Receipts to ask a question about a value. Profile, + # Slice 137: the runtime floor the application refuses to boot below, and the ML-DSA-87 + # known-answer check the receipts signer and the boot receipt ask. Pure questions about + # the runtime, asked before anything starts. + Toolchain, + Crypto.Preflight, # Slice 136: the web authentication mode, its rules and the mode in force. WebAuth, UUID, diff --git a/lib/trinity/application.ex b/lib/trinity/application.ex index 64aa461..a5f1dfe 100644 --- a/lib/trinity/application.ex +++ b/lib/trinity/application.ex @@ -31,6 +31,11 @@ defmodule Trinity.Application do # project's rules about key material, and the filter is the last line for exactly that case. install_log_redaction() + # Slice 137, in every profile, before every other refusal and every child: a runtime below + # the CVE-2026-55737 floor, or an Elixir below 1.20, is not one this node trusts to decode + # anything. The floor is erts per OTP line (`Trinity.Toolchain.floor/0`). + verify_toolchain!() + # Slice 135, in every profile: a filesystem root that is, contains or sits inside the data # directory or the secrets directory refuses the boot, before any child (and before the # regulated refusals, so the reason printed is this one when both apply). @@ -184,6 +189,15 @@ defmodule Trinity.Application do :ok end + # Slice 137. The decision is `Trinity.Toolchain.check/0`'s, pure; the reason is raised in the + # shape `verify_web_auth!/0` uses, so a refused child prints it whole. + defp verify_toolchain! do + case Trinity.Toolchain.check() do + :ok -> :ok + {:error, reason} -> raise "Trinity refuses to boot: #{inspect(reason)}" + end + end + # Slice 136 AC4, AC6 and AC13. The decisions are `Trinity.WebAuth`'s and `Trinity.Profile`'s, # both pure; this reads the endpoint's configuration and hands them the values. defp verify_web_auth! do diff --git a/lib/trinity/crypto/preflight.ex b/lib/trinity/crypto/preflight.ex new file mode 100644 index 0000000..9b315b5 --- /dev/null +++ b/lib/trinity/crypto/preflight.ex @@ -0,0 +1,171 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.Crypto.Preflight do + @moduledoc """ + Whether this runtime can do ML-DSA-87, answered by doing it against published answers (slice 137). + + Six steps through `:crypto`, in order, the answer being `:ok` or + `{:error, {:mldsa87_preflight_failed, step, reason}}` for the first that failed. Nothing is + raised out of it. + + 1. `:keygen_kat`: the NIST ACVP ML-DSA-87 keyGen case (`priv/kat/acvp/`): the published + secret key regenerates exactly the published public key, and signing with the published seed + verifies under it. + 2. `:sigver_kat`: Wycheproof's ML-DSA-87 verify cases (`priv/kat/wycheproof/`): a valid + signature verifies, a signature with one bit flipped does not, and one whose hints are out of + order does not (the malleability class of CVE-2026-24850). + 3. to 6. `:keygen`, `:sign`, `:verify`, `:negative`: a fresh key signs a fixed message, the + signature verifies, and the message with one bit changed does not. + + The published cases catch a provider that is consistently wrong, which a fresh key alone would + not. Signing is hedged, so no signature-bytes answer is compared. + + ## Why not `crypto:supports/0` + + OTP's `build_pkey_type_list/3` (`lib/crypto/c_src/pkey.c`) lists the pkey types, every ML-DSA + and SLH-DSA set among them, only when FIPS mode is off. It is the same at `OTP-28.5.0.7`, + `OTP-29.0.6` and `master`. In the mode the listing says no whatever the provider can do, and a + provider that signs ML-DSA-87 inside its validated boundary (CMVP #5523) would be reported as + unable. Doing the operation asks the provider itself. + + ## Where it signs + + Where the linked OpenSSL is 3.5 or later: on the FIPS image with the mode off (slice 003), and on + an OTP built against OpenSSL 3.5.9. Not on a runtime linked to OpenSSL 3.0 (hacktuah's asdf + OTPs, the `gate` runners), and not in the mode on Red Hat's provider (slice 003). + + ## The fault seam + + `config :trinity, :mldsa87_preflight_fault, step` makes that step fail as if the runtime had + refused it, so a test can plant a failure at each step on a runtime that signs. It can only turn + an answer into a refusal, never the other way. + """ + + @message "Trinity ML-DSA-87 known-answer preflight (slice 137)" + @steps [:keygen_kat, :sigver_kat, :keygen, :sign, :verify, :negative] + + @priv Path.join([__DIR__, "..", "..", "..", "priv", "kat"]) |> Path.expand() + @acvp_path Path.join(@priv, "acvp/mldsa87_keygen.json") + @wycheproof_path Path.join(@priv, "wycheproof/mldsa87_verify.json") + @external_resource @acvp_path + @external_resource @wycheproof_path + + hex = &Base.decode16!(&1, case: :mixed) + acvp = @acvp_path |> File.read!() |> JSON.decode!() + wy = @wycheproof_path |> File.read!() |> JSON.decode!() + + @keygen %{seed: hex.(acvp["seed"]), pk: hex.(acvp["pk"]), sk: hex.(acvp["sk"])} + @sigver for name <- ["valid", "invalid", "invalid_hints"], + into: %{}, + do: + {name, + %{ + tc: wy[name]["tcId"], + pk: hex.(wy[name]["pk"]), + msg: hex.(wy[name]["msg"]), + sig: hex.(wy[name]["sig"]) + }} + + @type step :: :keygen_kat | :sigver_kat | :keygen | :sign | :verify | :negative + @type result :: :ok | {:error, {:mldsa87_preflight_failed, step(), term()}} + + @doc "The steps, in the order they run." + @spec steps() :: [step()] + def steps, do: @steps + + @doc false + # The published cases, decoded, for the tests' stand-in. + def vectors, do: %{keygen: @keygen, sigver: @sigver} + + @doc """ + Runs the six steps against `crypto` (`:crypto` unless a test passes a stand-in exposing + `generate_key/2`, `generate_key/3`, `sign/4` and `verify/5`). + """ + @spec mldsa87(module()) :: result() + def mldsa87(crypto \\ :crypto) do + fault = Application.get_env(:trinity, :mldsa87_preflight_fault) + + with :ok <- step(:keygen_kat, fault, fn -> keygen_kat(crypto) end), + :ok <- step(:sigver_kat, fault, fn -> sigver_kat(crypto) end), + {:ok, {pub, priv}} <- run(:keygen, fault, fn -> crypto.generate_key(:mldsa87, []) end), + {:ok, sig} <- run(:sign, fault, fn -> crypto.sign(:mldsa87, :none, @message, priv) end), + :ok <- step(:verify, fault, fn -> expect(verify(crypto, @message, sig, pub), true) end) do + step(:negative, fault, fn -> expect(verify(crypto, flip(@message), sig, pub), false) end) + end + end + + @doc "True when `mldsa87/1` answers `:ok`." + @spec ok?(module()) :: boolean() + def ok?(crypto \\ :crypto), do: mldsa87(crypto) == :ok + + defp keygen_kat(crypto) do + %{seed: seed, pk: pk, sk: sk} = @keygen + + with {:pk, {^pk, _}} <- {:pk, crypto.generate_key(:mldsa87, [], sk)}, + sig = crypto.sign(:mldsa87, :none, @message, {:seed, seed}), + {:seed_signs, true} <- {:seed_signs, verify(crypto, @message, sig, pk)} do + :ok + else + {:pk, other} -> {:wrong, {:public_key_from_secret_key, summarize(other)}} + {:seed_signs, other} -> {:wrong, {:seed_signature_under_public_key, other}} + end + end + + defp sigver_kat(crypto) do + Enum.reduce_while([{"valid", true}, {"invalid", false}, {"invalid_hints", false}], :ok, fn + {name, want}, :ok -> + %{tc: tc, pk: pk, msg: msg, sig: sig} = @sigver[name] + + case verify(crypto, msg, sig, pk) do + ^want -> {:cont, :ok} + got -> {:halt, {:wrong, {:wycheproof, tc, want, got}}} + end + end) + end + + defp expect(want, want), do: :ok + defp expect(got, _want), do: {:wrong, {:answered, got}} + + defp verify(crypto, message, sig, pub), do: crypto.verify(:mldsa87, :none, message, sig, pub) + + # A step whose function answers :ok or {:wrong, why}. + defp step(name, name, _fun), do: failed(name, :planted) + + defp step(name, _fault, fun) do + case attempt(fun) do + {:ok, :ok} -> :ok + {:ok, {:wrong, why}} -> failed(name, why) + {:ok, other} -> failed(name, {:unexpected, other}) + {:raised, reason} -> failed(name, reason) + end + end + + # A step that produces a value the next step needs. + defp run(name, name, _fun), do: failed(name, :planted) + + defp run(name, _fault, fun) do + case attempt(fun) do + {:ok, {pub, priv}} when name == :keygen -> {:ok, {pub, priv}} + {:ok, sig} when name == :sign and is_binary(sig) -> {:ok, sig} + {:ok, other} -> failed(name, {:unexpected, summarize(other)}) + {:raised, reason} -> failed(name, reason) + end + end + + defp attempt(fun) do + {:ok, fun.()} + rescue + e -> {:raised, Exception.message(e)} + catch + kind, reason -> {:raised, {kind, reason}} + end + + defp failed(step, reason), do: {:error, {:mldsa87_preflight_failed, step, reason}} + + # Keys are kilobytes; a reason names their shape, not their bytes. + defp summarize({a, b}) when is_binary(a), do: {:binary, byte_size(a), summarize(b)} + defp summarize(b) when is_binary(b), do: {:binary, byte_size(b)} + defp summarize(other), do: other + + defp flip(<>), do: <> +end diff --git a/lib/trinity/effects/boot.ex b/lib/trinity/effects/boot.ex index 52ecc08..32aa14f 100644 --- a/lib/trinity/effects/boot.ex +++ b/lib/trinity/effects/boot.ex @@ -15,6 +15,13 @@ defmodule Trinity.Effects.Boot do def start_link(_), do: Task.start_link(__MODULE__, :run, []) + defp preflight_subject do + case Trinity.Crypto.Preflight.mldsa87() do + :ok -> "ok" + {:error, {:mldsa87_preflight_failed, step, _reason}} -> "failed:#{step}" + end + end + @doc "Writes the boot receipt and records its hash; returns it." @spec run() :: {:ok, Trinity.Receipts.Receipt.t()} | {:error, term()} def run do @@ -23,6 +30,11 @@ defmodule Trinity.Effects.Boot do "signer" => signer_subject(), "key_custody" => key_custody_subject(), "otp_release" => List.to_string(:erlang.system_info(:otp_release)), + # Slice 137: the runtime the floor was checked against, and whether ML-DSA-87 can be done + # here, as the known-answer check found it at this boot. + "erts" => Trinity.Toolchain.current().erts, + "elixir" => Trinity.Toolchain.current().elixir, + "mldsa87_preflight" => preflight_subject(), "fips" => Atom.to_string(:crypto.info_fips()), "node" => Atom.to_string(node()), # Slice 136: who the web pages answer on this run, and on what address, so a reader of the diff --git a/lib/trinity/receipts/key_custody.ex b/lib/trinity/receipts/key_custody.ex index ea097af..5ff8428 100644 --- a/lib/trinity/receipts/key_custody.ex +++ b/lib/trinity/receipts/key_custody.ex @@ -138,7 +138,8 @@ defmodule Trinity.Receipts.KeyCustody do @doc """ The algorithm the rules select on this runtime: the configured one when it is available, else P-384 in FIPS mode, else Ed25519; `{:error, :no_approved_signer}` when the selected - implementation reports itself unavailable. + implementation reports itself unavailable, and for ML-DSA-87 the known-answer check's own + `{:mldsa87_preflight_failed, step, reason}` (slice 137). """ @spec select() :: {:ok, Signer.algorithm()} | {:error, term()} def select do @@ -151,9 +152,21 @@ defmodule Trinity.Receipts.KeyCustody do true -> :ed25519 end - if Signer.impl(algorithm).available?(), - do: {:ok, algorithm}, - else: {:error, {:no_approved_signer, algorithm, :crypto.info_fips()}} + cond do + # Slice 137: for ML-DSA-87 the reason names the known-answer step that failed, so a refused + # regulated boot says which operation the runtime could not do. + algorithm == :mldsa87 -> + case Trinity.Crypto.Preflight.mldsa87() do + :ok -> {:ok, algorithm} + {:error, reason} -> {:error, reason} + end + + Signer.impl(algorithm).available?() -> + {:ok, algorithm} + + true -> + {:error, {:no_approved_signer, algorithm, :crypto.info_fips()}} + end end @doc "Signs the PAE bytes with the selected key, read from its file at this call." diff --git a/lib/trinity/receipts/signer/mldsa87.ex b/lib/trinity/receipts/signer/mldsa87.ex index 2403a49..48a9212 100644 --- a/lib/trinity/receipts/signer/mldsa87.ex +++ b/lib/trinity/receipts/signer/mldsa87.ex @@ -4,10 +4,12 @@ defmodule Trinity.Receipts.Signer.MLDSA87 do @moduledoc """ ML-DSA-87 (FIPS 204) through `:crypto`, opt-in and never the default (slice 024, amendment 6). Available only where the linked OpenSSL is 3.5 or later; `available?/0` asks the runtime - rather than the build, so the module compiles everywhere and refuses where it cannot run. + rather than the build, by generating, signing and verifying (`Trinity.Crypto.Preflight`, slice + 137), so the module compiles everywhere and refuses where it cannot run. Measured on the slice 003 image (OpenSSL 3.5.8, mode off): a 4,627-byte signature, 905 µs to sign, 172 µs to verify; refused in FIPS mode there. The private key OTP returns is a - tagged tuple (`{:seed | :expandedkey, binary}`); the key file keeps the tag. + tagged tuple (`{:seed | :expandedkey, binary}`), and OTP 28.5.0.8 returns it bare (slice 137, + F-137-2); the key file keeps the tag either way. No JWK form is registered for ML-DSA at this date; `jwk/1` is `nil` and the key id is the SHA-256 of the raw public key (`kid_scheme` `sha256-raw` in the registry row). @@ -20,8 +22,11 @@ defmodule Trinity.Receipts.Signer.MLDSA87 do @impl true def scheme, do: "receipt_v3_mldsa87" + # Slice 137, F-137-1. This was `:mldsa87 in :crypto.supports(:public_keys)`, which never holds in + # FIPS mode: OTP lists no ML-DSA type there whatever the provider can do (`pkey.c`, + # `build_pkey_type_list/3`). The known-answer check asks the provider by doing the operation. @impl true - def available?, do: :mldsa87 in :crypto.supports(:public_keys) + def available?, do: Trinity.Crypto.Preflight.ok?() @impl true def generate_key, do: :crypto.generate_key(:mldsa87, []) @@ -36,10 +41,23 @@ defmodule Trinity.Receipts.Signer.MLDSA87 do @impl true def jwk(_pub), do: nil + # Slice 137, F-137-2: OTP 28.5.0.8 on OpenSSL 3.5.9 returns the private key bare, as the + # 4,896-byte expanded key, where the runtime measured at slice 024 returned it tagged. A bare key + # is stored tagged by its size, so the key file's format is the same whichever runtime made it, + # and `{:expandedkey, key}` signs on both (measured on 28.5.0.8). + @expanded_key_bytes 4896 + @seed_bytes 32 + @impl true def encode_private({tag, bin}) when tag in [:seed, :expandedkey] and is_binary(bin), do: Atom.to_string(tag) <> ":" <> bin + def encode_private(bin) when is_binary(bin) and byte_size(bin) == @expanded_key_bytes, + do: "expandedkey:" <> bin + + def encode_private(bin) when is_binary(bin) and byte_size(bin) == @seed_bytes, + do: "seed:" <> bin + @impl true def decode_private("seed:" <> bin), do: {:seed, bin} def decode_private("expandedkey:" <> bin), do: {:expandedkey, bin} diff --git a/lib/trinity/toolchain.ex b/lib/trinity/toolchain.ex new file mode 100644 index 0000000..fbc6161 --- /dev/null +++ b/lib/trinity/toolchain.ex @@ -0,0 +1,151 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.Toolchain do + @moduledoc """ + The runtime floor Trinity refuses to boot below (slice 137). + + The floor is "OTP 28.5.0.4 or later, or the 27.3.4.15 or 29.0.4 lines": the releases that fix + CVE-2026-55737, "heap corruption when an invalidly encoded tuple with an arity of 2^31 or larger + is decoded from Erlang's External Term Format" (OTP-20214, erts). The same releases fix CVEs in + ssl, public_key, crypto, ssh and stdlib, so erts alone does not say a runtime is at the floor. + + ## What is compared + + `priv/otp_floor.table` holds the three rows of erlang/otp's `otp_versions.table`, verbatim, with + the commit and SHA-256 they came from. Each row lists every application at its version in that + release. A runtime is at the floor for its line when erts and every loaded application the row + names are at or above the row's version. erts comes from `:erlang.system_info(:version)` and the + applications from `:application.loaded_applications/0`, both of which answer in an assembled + release, which carries no `OTP_VERSION` file (slice 130, D-130-11). By the time + `Trinity.Application.start/2` asks, every application Trinity depends on is loaded. + + A major above the last row passes: fixes land forward into later majors. A major below the first + row refuses: no fixed release exists for it. + + Elixir must be 1.20.x (`~> 1.20.0`), as the shared contract states it for every box release. + """ + + @table Path.join([__DIR__, "..", "..", "priv", "otp_floor.table"]) |> Path.expand() + @external_resource @table + + @rows (for line <- File.read!(@table) |> String.split("\n"), + String.starts_with?(line, "OTP-"), + into: %{} do + [release, apps] = String.split(line, " : ", parts: 2) + "OTP-" <> version = release + major = version |> String.split(".") |> hd() |> String.to_integer() + + versions = + for entry <- String.split(apps, [" ", "#", ":"], trim: true), + [app, vsn] <- [String.split(entry, "-", parts: 2)], + into: %{}, + do: {String.to_atom(app), vsn} + + {major, %{release: version, apps: versions}} + end) + + @elixir_requirement "~> 1.20.0" + + @type finding :: {atom(), String.t(), String.t()} + @type reason :: + {:otp_below_floor, non_neg_integer(), String.t(), [finding()]} + | {:otp_major_unsupported, non_neg_integer(), String.t()} + | {:elixir_outside_floor, String.t(), String.t()} + + @doc "The floor rows, by OTP major: the fixing release and every application's version in it." + @spec floor() :: %{non_neg_integer() => %{release: String.t(), apps: %{atom() => String.t()}}} + def floor, do: @rows + + @doc "The Elixir requirement every node must meet." + @spec elixir_requirement() :: String.t() + def elixir_requirement, do: @elixir_requirement + + @doc "The runtime this node is on: OTP major, erts, Elixir, and every loaded application's version." + @spec current() :: %{ + otp_release: String.t(), + erts: String.t(), + elixir: String.t(), + apps: %{atom() => String.t()} + } + def current do + %{ + otp_release: List.to_string(:erlang.system_info(:otp_release)), + erts: List.to_string(:erlang.system_info(:version)), + elixir: System.version(), + apps: + Map.new(:application.loaded_applications(), fn {app, _, vsn} -> {app, to_string(vsn)} end) + } + end + + @doc """ + Whether a runtime on OTP major `major`, with erts `erts` and the loaded applications `apps`, is at + the floor. A refusal lists every component below its row's version, erts first. + """ + @spec check_otp(non_neg_integer(), String.t(), %{atom() => String.t()}) :: + :ok | {:error, reason()} + def check_otp(major, erts, apps \\ %{}) when is_integer(major) and is_binary(erts) do + {first, last} = Enum.min_max(Map.keys(@rows)) + + cond do + major > last -> + :ok + + major < first -> + {:error, {:otp_major_unsupported, major, erts}} + + true -> + compare_row(major, Map.fetch!(@rows, major), [ + {:erts, erts} | apps |> Map.delete(:erts) |> Enum.sort() + ]) + end + end + + # Every component the row names that is below the row's version, erts first. + defp compare_row(major, %{release: release, apps: row}, components) do + below = + for {app, got} <- components, + required = Map.get(row, app), + required != nil, + not at_least?(got, required), + do: {app, got, required} + + if below == [], do: :ok, else: {:error, {:otp_below_floor, major, release, below}} + end + + @doc "Whether Elixir `version` meets `elixir_requirement/0`." + @spec check_elixir(String.t()) :: :ok | {:error, reason()} + def check_elixir(version) when is_binary(version) do + if Version.match?(version, @elixir_requirement), + do: :ok, + else: {:error, {:elixir_outside_floor, version, @elixir_requirement}} + end + + @doc "Both checks against the running runtime, the OTP floor first." + @spec check() :: :ok | {:error, reason()} + def check do + %{otp_release: major, erts: erts, elixir: elixir, apps: apps} = current() + + with :ok <- check_otp(String.to_integer(major), erts, apps) do + check_elixir(elixir) + end + end + + @doc """ + True when dotted version `a` is at or above `b`, compared as integer lists, so `16.4.0.10` sorts + above `16.4.0.4` and `16.4` below `16.4.0.1`. A component that is not an integer compares as 0, + so an unusual version string cannot raise during boot. + """ + @spec at_least?(String.t(), String.t()) :: boolean() + def at_least?(a, b), do: parts(a) >= parts(b) + + defp parts(version) do + version + |> String.split(".") + |> Enum.map(fn part -> + case Integer.parse(part) do + {n, _} -> n + :error -> 0 + end + end) + end +end diff --git a/lib/trinity_web/live/receipts_live.ex b/lib/trinity_web/live/receipts_live.ex index cc56ffb..4cefaca 100644 --- a/lib/trinity_web/live/receipts_live.ex +++ b/lib/trinity_web/live/receipts_live.ex @@ -101,6 +101,12 @@ defmodule TrinityWeb.ReceiptsLive do
{@boot.subject["fips"]}
{gettext("otp")}
{@boot.subject["otp_release"]}
+
{gettext("erts")}
+
{@boot.subject["erts"]}
+
{gettext("elixir")}
+
{@boot.subject["elixir"]}
+
{gettext("ML-DSA-87 check")}
+
{@boot.subject["mldsa87_preflight"]}
{gettext("core policy hash")}
{@boot.meta["core_policy_hash"]}
{gettext("receipt hash")}
diff --git a/priv/kat/acvp/mldsa87_keygen.json b/priv/kat/acvp/mldsa87_keygen.json new file mode 100644 index 0000000..d663891 --- /dev/null +++ b/priv/kat/acvp/mldsa87_keygen.json @@ -0,0 +1,11 @@ +{ + "source": "https://raw.githubusercontent.com/usnistgov/ACVP-Server/975de31eb83d87039ec88934fdc47d8c312b892d/gen-val/json-files/ML-DSA-keyGen-FIPS204/internalProjection.json", + "source_sha256": "e67ee6540d40e11506c3c4e3b1f79fc1cefcd49820db99fc61f87cc8ba463baf", + "licence": "NIST work, not subject to copyright in the United States; annotated CC0-1.0 in REUSE.toml", + "parameterSet": "ML-DSA-87", + "tgId": 3, + "tcId": 51, + "seed": "A16F5B0796703E2D1A0140A35CBF36EFABE70E752BA59B6A9A0E9C4B05302F73", + "pk": "A5787E8044248F3F85AAC54E9469FC98F1B1138CC127B120F9946C80B96E3D89CCFE38C995645D4B6A559EACB2AFB81621D765C6E42E73031D44CBE74D322C7B16249576EB4C500253538D1A2C6B408E681B93B9014E3147DFBECF9D9858F7E8635F8598BA6847127D216A888FFB1636CC761616A0389C39A4245695DDE0C86CCF8A3BC5A50EA6FDBCC0A34457D4DEFE35F775C5993685AEF2237C31912A619FF804AFE8AC3418C13502820AEE5249D6E577EE0B2A5E3E8DA2DD30F514A076B50556D7581BA1F9B2E4671756A63065C20EBDA6EE2C33C9D97AB14C5F2204FC5359ADB2BDAA3AAB7AE1DACFF18A67D801BCB8F054BBC444F0FD0001A7908EBDCDF2B84F3C026EEC1282498D31AC33AE6A309ACAB17B70DC9F0EFBE52648D1AD2A4CD5964DC619B66CDC9D35EDA7A3DC21C729B9929024DB8B852DFDF102A086845702FD249EE43B54D2D033ABA95110C4F0A66EEAEE78F3C41D5D792A37D1D2299252A5498A44CA354F6F37FDC2F3B72B1A8378A5BA8B4997556E5A6F4125AD946BD4FC402C4320B27111BC204B8B5448F43F7E77A8166A48137D85584BEFE2D9C85CCCD9BBF3F8A4E05930180AFDD697DA82ED9F1069150FFC76578C941CDBCC5BD2ECB7D6ABF9E68327DF51C26C8B42CB1DD8BDA98A82C4C6BA6A991E651BDFF68F0A62D5DBFEA020D4303E0C53D474CB11D553C5BEE1156917D72AC2A6CCC4EC1C775D41EE660E2485A45AF5A7CA083DDDCC3EE4FFFC5E77BA97CC4473303C77D8B6FDB35E2A20627BEBBE327DD2D1AD1F880CA8EAE1E0067A9093929E5AB18D406A518EDD8C1E0F0AB07736F55FCECF4A3B2ADCE1CE3E080B58DDF85A2262D0803A7E5B4E485E642BA533E2EC7A43F9E8DB20F75292ADAC704395469408C15641A9C28B83E8C1C799FAE0652F51369978F4C089FE15DD78C5F560CD28F5F75FE0A39A60A61AEF6C7D802141E9809E7ACA68A38BE9BDF5312258704F8B11AF4262220CB55641FE95DF83EC9F786D6E69202C91EC4CAA4E38A21C3CC609C28B8F65552FE8334850858BBB30B837D874867EAD330A1F5B4D7F6BEC4748BE54A782D5B21A192BC00A7F2240FBD900460785D1971A94C587493D21CDA249676EDAC6C865147E269488B9CA78FAEFDC778FF60E79735DD5539879182424B054FAE8A9E153BFFD6957C533AEDC105E43AD7626312C8D229327D3E72AA9644BF3E2C9A08ABF807CF3A472C7DAF0AC4290A9E8F88D07AE8FEDB8A4B218C3EBBDBE53882781F2DE034B17FEFE69302B4975CF43E03645DC53BD355AF988B3A3D2431BF9D9A865750051EED7BAEFD1BAD4939C7EC293509A5851A145F79DCBEEFD195571AC2172AC6036812B4DC8040D186B8984CF9DC24F8F765166C6B2E8389DD24ED63CEE951442861669BDA0622BD90B041DC477C01A0D95D547E07A892CE1F26275ABC6F97702E03B776E2CA71E3D0EBB88D1ADF591122E6F0EC95A6CFBB976AE64BD0C7F074CB6E78E644DEE8200E2D626435907B9134000DBC3B50271F5A8F254D2CF02DA039D458E80FA13A33567AA9B374B47B799D0FF1A14CA92A50EFA192EA1641FE29A380E11386528B7248D6DE6AA90E1C9744713B768264B72A13CCAA94F3E19ADA5129E0D8155EC1B267E2CDC7F0E7A08F203D9A18F8C8D18529709EF746A21D5FEC36EB547A2FD4490092AD0F06C55EE0A1CB104E2C3C3CF2BCBCBFFEAEC744A13E37310962CE40E072E7ECAE223943A03002077D3D96C7B4C3FB1E2C9EE9C5222A63252F26372BC2BD94507CE5728CEB7A0AA33527E0662B4561D1BD255806450772EABB7ED40F787A2E3C664FA6BC3BE9BCD84FC7B42F16F94CC56CFB67297E475177E19E51010CFB74BA996BA1C3D793EA010C99901E2223B375E364BA193772B329D5D05AED959EBB924B698F0657AF43EEE8E0D54D15C93511209AAA9E10251BF81AD8B467D8FAEE2A440782C372F55A62CFEF408803E4B3BD8EFB94061EAB525BD31957779B89EB1C75AB97F278B3EAF99A05686E04873D7A685868D1C0F4510ADAB0B267FE4D3CB70C35B295AFC671B60C6575B60EEB99756E7B204A24D7095DF277BE18668E0F1AA5621D16F204575F76C3B13385BAF61AFF7F36D56111FD88FF093BEC4FFC26BA63720660B5BD209A9C14C0AC6B4E08B38FF580C18E39A22A9AC36912892537B0FD42800445DBEF1A03D2D8624C1B125519927C282EEFAC4AFC16128B07456FBC99D34C783AE08EBB0C46915971429FE64E448F1678BF76C7C20F91AD80E213E39AD89962AEA44E06EAB351C9B3A5859D5A884DE0CD767260CB60A96F508C62B731047CB1F3CA6AE28742771F4EC3BE45DF500F0132B6E947D2468AAB9410BC1F581328471B3E53E8E29A0794F4EA8BDF9FDB3922CECBD2AAAF75A2AF4C7CEBA03382332D39DFB770559789708930E4C77966C7364E2A4D762C122BE3FCBD37276EA6194071BB7C18E2627524F3AB2BE7C0C6E59F62D1F075E1951DD3352F6CA9762F243F6691FE6A9DCE4278B178F688E433B990272000F23C92F74A1CFF1429BDF1C3FE1B9AA1C0E7A58ABFFBB54D3F38ED93B11FB12233CFF48C812A227747849012F7BB85529D87459E11DCA9F7A9C3242054DB6BB93B48A47D69BA791B2A5D694A8776B22B8881BB2D192AB0A45AEC71A7171F844E3B1C4149D118EFA899E88D96A296D5EB811AF923AFD1A92E0C71464B90C8E5EC24954FE8C9BFB518A308B3D301D8D6C62E5AD40F63BCE24699AF0BBD2E48FD9AA7C3A1C597731700E3D86E16AD36A67BC031BF381B82CF40B1B235F51727913BBC311A186FDC23EE267739740CC57C36BE05F9331EA40279C34C44FB8FDFCC7E9CDA54C394CEF9C17F8529A56E3BCCBA327CABD07F5F9C567B70DFBD9D7139E39B7E1A493482321B11C881BA9D44CBABEA64F1AF18C18B3B5F98FF4A95FD907E112AED857B033BB7E0FF9466CF5FC691CCC1142BFB8CE81BAD7DB554409B79E23D0A41063857E4AA05583525E69EDAA017F6A7FA96C25A38E7A7D01E22F96ADBB683B4EB8A487202426D68D297B30BE0A803D4FCA1E037DB3BD63A2F06EFD68BD4D7BEAD8338D36AC1425163B3C739B86AC9D967ED54DE24CA58BC129CFF73585B667DC33F32BC4A4D9E106076849BC2574F0E5AB2C0B5CBDBCA59BB644BC86F3339B23B3B12959FC7887F7291136343E54F4A56C344DC5BE641DBCE62AED4D4BB958C4D54D7196DB4ACB8A1F43F88CDBB758546F44C59293501E0569F00B97FC244507C248031D07EE4DBB6A97270B772791E758E582989D93124AB82F15A9DCA469C1A0E98CC75FF683430036F16B4E94E94DECA06EBE7FE2B8E9C0A690AAD7A91877799FCF24CE275844A63E68F1A5C799BC83C7F5384CD9322E20B619D39D0031482FFED8224F6C522C8533CB29C04AF154D920D747D81616EA219E358385AA9FDB8E94A7EE5B53F2CC31B3A7BAC787E54AB9536FC42A3E369043C6F5C11D0F7D452852C3FB3F1845942186044385FB9E482962B1DAEBD2B3DF125D1A61843F71A272E3A1D97AAB97DE5831C56D16A6A6620F8C6CD0F4F1E41AFE6895BA3664D23A03EAB3531126E87335F4BCBBF2E39C47B5A58BA15066F79717D8296667553ECD0991F14D42F8934D753929F146E4B58D00A3E0139D66", + "sk": "A5787E8044248F3F85AAC54E9469FC98F1B1138CC127B120F9946C80B96E3D898CFB9219F5616156C30E9ED4C2C060256A1248DC7B0BA16B8A37C9412493D4DD2674A8FF7151B493FB452DAA920A5FEB5CE5E8D70677735C0543AEC2BB161C07E2C1065FC5CCA8EA34EA53C7D879A57F60BEF6D318280489CD568229DEA833EA03326C82482218104DCA1070C3280111834900B2691B3544E408061935600398441BB509C19285C3200C8A488AD4842D4AC08881B24811C851589845A1C80521424C58248E0C298899A46584A220114606D4C04018B704E2C085C91668A330621AA090D9183154A40421086E189408D34604DC24264204125B0221541401D4A240242206822892889890643884C11671C2340423978D4408449008450184510B31690B30900217641388001885711C89650AC770D3C870D0A021E124015C202A94864090425063142890C211044528002552642046982841E3346D220202D4006A2319500A40814884204C02529A468C1AA028C32002DA14041B208C01286CD4106493108CD3966CD32652E148224842005042889B4470DAB468E2805101262052404108328254A0601247255A928C000632819849D9386AC846848AB28121A16D88A085092530C2908D9C4040D14222D9A440E1045043165219016524354C1108608B4689100551E4008D03B161D306060A2049491850E1C888580691480621E486704042918B46108A128123189214474562281193C64904B66DCC2008A002208C284E90A84103B9640A058980881014957160A621591864C2029019154A13278D21904804800902440901A29001C750D3262913C9448C88082433629994518400118AB0700882255924404288099A407210B26CA12011912485A000520840600946899388911B46111B906C84002D1B1642122012CC3042CB806988126C8010884C9064C9125262886889B48190922123B711C1228C12B88908069210044513094502068C01B6508B0800148869004028620245D42204A0C68088B0004B128000388ED040844CB65103169221A249D4824101408D02923058B4241104650B0044134586C8A40C238610C3208EC9404A2393050CC08553164C11170ADBB64C593804D13040C8442601326019268A434465E03460E4308D22416403484D09384890900954800818104D82C27021C55123377043340408818C88C661D0404A4224880C26490481314A94211C1025040391D93089E0063181348D24388E84A82584888C4A40669A220A48120D1B012699266814C74CD24291244332228920980668A006260849890BB340A4086AD4040D58268DC1C07182484ECCA84CE0380D01166208C8409C046DC2A2898B0629C106312386300A9365E3347280A4616486711AA50121A581CC006ACCC02CC108644B304D931231104090914246024048DB182E611691D34800CC34890C888898182114018611A3601906211A053208044D4A40105B444914429111384413110442462223366D44B22559B0650B161163C62DD22621188408D2A66C0B120E92904510955053186919B16920A06524436ED9C6480126600CA02CA3062110C62C94C68118128141262408439009310D0A276A09040909482883B480609800208824199730A1B461D4382AD08680182026248984A4B661C0462D4AC60D09C97019C50408438224888091C8851AB50D44B08DD416289A8224C0864C03246C200546182912A046654224281110501A1961C9B48082A2810B0708CAA06011A920E216655A103094266A0BC52522046DC08091A232724C4621DBC068181701633085D44089890086CB2204224650D4168000432688844C2280911845722400054BB42C81B02908256AD8402D00A43088341010153009323100404E13C31123A420114921432860C1206D51C62DD1108A19339108312D5B346984022644A064C11681944465189500DBC04C9A2850C2183218B869D2A28100B131648061D0126D14A3242283711C0880C80050A1402E0A838C9B9028D296510C144DA1940880420810320A09198891B609D9B67123B44C903468D286891C906C00248A52362418418EE0C64580426C1CC77118124882184D502272D1384C0C14881B187100299121934544842D9B8210DC28285B2052C1346CE4802500448164044CC9222E14435010474293206519B83080122D1C006163108924C34CF692DDF651A0F08DDE43395656847184F33E137C04F3426909179E9DEF4A914BCBEB9A4530ADAED1ADBEC4D3AFFBB57A70BCC7FD6BE902F7B19A13B6255BA27AF9CA8028D67403BE3E5DC168244A989165899D0E759104B072114F7B6CC107D7514235468DD88420616EEDEA618B8B6CF692981087ECD26EB7C049BA29E1918456154B4B62D7F2054B2DFA8D56BDA46605726C47EDC9A3F82EC1AA41235888846C3C428815D4607EADEBD81AB7B72E6A5D92ABE542E2C9F1C64E3B54FDBECAC29DF0C45E0BAE8AEB2427073355A2C4DFDB8B633C77E9D438DC8C4786AD5FDABD817AA011C18ED90F4B6A0542AD5EE5B42224283392AF775B1122B8CF27B126AD2D806CBA48DD6D6D0FC97A3F123E4A3964F579A6345C2468D9F8C0A5388BEEEB15657A150D7C73D4EC6395CD3F24DDCCCE2FDE77F6335A22A79463E071050FCA1DA34E04E224B4C6759C54E08359A7B228F7F7936D45452B9A8E793BBD87EDA4F6D81189AA5501B8957CD54B9ED4690814811AAD2AFB3DD8E9464A829FD82CF4D941BC86A749C72F82AF36AFE766812F0367DCFA395B9A5D2A38EEC569EDAA793164080188540B070E1F9E3C2921C2115E456ED7B3C7EB8E4063517133CEA544E4B81A3AB24CBC4B58D2BEBC6B7094ACF1A8E3DC22198D1E85B42E80A09A138023D1FB2C9D0C4650CF2D3BE3E5D43F57DA4CA25F23D0E58373161810BFEDAB0DDECF0F1BAFD5229E07C5BF15E52AF391876BD2751C57F8D8BEA0AC50E110178DB04456CFB1FB7B174F5BD3471C5CB572B5B052F0525225E694DE9588A8372831139A5EB41B6A58AB0E590186472C151209F2D2BF99D6D9E583BBFEE0B2E2A5E26265884CF6D2B0F498442E6CFF79A7333FB0835391145F4075758B04DFF4B3F86B9AD8BE10FF01E4DDF067B6E66EA96CC9D335B36A4C9ECDCB89A591CA4834B5ABEF15560CCD78396568851B4BA0ECDB7831C5B27AC459ACA7D307D97868A8613636A3D7845BA2110781D200C2AF170983C5D773379D350F70F48DDD3B37D02E750FD0BEAC3951EE52C831BBED1B26EC3B086CC5E901EFB1B7392B2643FF7328210CAD3CC0529D9909569B7A77E7B459425E1E6E717155B3A7CBF2A62F8C5436F4668F7EDC652189FB54FE49885CDE2B35F7B7BAB41E460F99B3621D73E1C07DEE392661A0066BC40002422F2E3C0CCE1ABA02B1ACC0B4792F3BB9950DB6638E89CF9467CC716FD31BF08C70EF7B0695197656F1F07EC01797D84D332DBC440E8C8B48F3B4399EA8DE593FCD36A8C4AE69C275E69CB7C2F80F284B0A05B25F8BA4597CB56B18C1150F4127BA1054CAD65CEEBEAC54ED8210A61CAF049AD212995FF924157D8B6827F73AA46FF5534C7187BA24705B68F7C6C7998E60F54DD77038D4902CD923D137ED447C157E647AE91C85096A8A3A13B4C7829CC4BB5B05C1BDC2673ADA69ED599DD9F379182C7B7DE21DA504C7C2283B1E10AC2AE9EC092B95027F27F1364401B3CF9E6E2270DF1B607CBD7DA8F9AB36D340C9057D7A38DE7F7197DD75A4E24BA5525ACCFF7B6E0BE21136E08D6AED502864B8B6DEFB690AEA7ADBF36D75D37437693F279E57AB7E8048E031AB029EA799A45C46FF8491087372C787E36C9093A0926B15D383B4F8CA26201272F97F91AEA95B05BFF56D1D35E047E4D19B234082FB13B754BF87F9A998D94D81AB5EC9B7DB5F182DF41F9D64264BFCACC3810919F9DF780FE00EAFDE884A70F17E8913DDD14BDE92D915544DFC830690A980BECA539DCE75CBFAADC80291D6DAAD7C871FEDEE7DBE20DC7ABCC56E0D4F317EA57791EF8DA9569C9C4DB52DC5135E7806A2821788D7E3BB43C44C827AFFF07302D21D14AB0F47B0E74A0973E05471259BFD650787B7CAADE4CD7AE155F01867C624DF65A0F47F65F675B48AF6671C39AEAA0F1660D5EFCADE3212BD70FAC0AC342EA12E686E79CFDBC4C0068FAAF6B801C5E9B6C687C0BA9E883CEFC3D59263458736C6FD358ECE9EADBEB5DA31D03A87F3A5EF581947E80116696097F916808CE634FD5455296BD8D9DD899CE19C34ADE6CFCEE97F3D8930AB5D24F33706AF6A315C6B4021756F8D1CA3C5ADBDBED71D0CCF3AC15B75AE7BEDE62F08760D4EF03D584674B7E4B98CAAF9B05904CE4A30896D42C78D31C5D3E548D1AAC12BA1B4AF6D5DEEE2BEC4B9242E9CE340D2B5C94197C736BA15FA2D40FB9ADCB840CE8B905E05EA928FC1CEF280C68A2BD3FE1FBD4ED47443DA3F42F482DB6BD467724EBA128FAE13E28F325FFAC53061B9BD009CB00100B2CF0998F8CFC498AB723FB093FA5F68B573595E50518EF184FB04DB336FB9335781BF7A9CCB5BEF71AC131F61A87FF2A07A2131BC40003FA7170F7F43D10BB53368EF7CBAED8208F5ED1DB135E5232E06DB1B394E88D99D8E9E4997326DB9F5367F0DA6F26B24F8E7F772E4D23E0F4BE11C7F91E7F643DD932772887F50A82DC5C2D3B916E612B53AA402731E99A401BBFBCF5D3182B3D85126288C160EC5A442A8F95D4929756CFA5DE4B98CCF93BB9EDF0C5E6717D52651EAA58633ABABC4B10C0A0EA14EF796C3724312E97E4D2A7265E006A325D27517F11547B91BAF56AD11268BFFD0E864962830200F1D85F9571AFFADBBF24FBF69F1E0A658E4B3F97726ADA13D7DEF371855A535826BE77AAB5F03D520E3C3F3AAA83DF603019B635B047D58210BADCF9252857A49AF7909942D793069F441B1133C855D71F9C5702E0DEB6EC2E406A617219F928F9ADD14848B73AA14AF4C22F9607814A53BD832147060C797345D9017653F166723F484A4CB271DAA3CBF64D70FDF7110C1C4864BA12414F23C06C79AE70C0F3973716C114C7C64C8537D76188DDC0B5E5F99CDAC1B93A6D67444C3238F586322083EAACA34B5A791E74957FD3EED8F1A0A297B4F72880F9F08618F0BB92A599797C206190973E9DAF2FDF0B9C06F6BF70603B9D2F9233A14B14F263F0F9E0B200342D4099EFF7C405D18DE9FF10D3A7CED4867BB12EE110984CB51F46ACC1A63FD4A8577EB6B6E0A35805390882538F203F8C96E6E9E44F9FC0CF891BCA0D3D49ABDAE3E0CF6EBD1C5CB231EDEC40BAF1D8C63828B41033D5DE389ABAD6FF95E497AF0753E798172FB638B93D49D96766A1EC78EDFB13A8C6862DAA245A59D3C36C408349C62AAB197D9308420CDE0A26F77D9DB8BC64CED2349FA0199A977520442CC787F9432CD9F07279D19AC77D0D3B35B6E04508108EDE9842EF22AE5DD3D8B96D8DFBD775B2EB150FD70ED80F3F3259B289203265F7A4E26BC7855562481A7705CEB34F1B3F917A28CAC6EFE47ED3ECC170C7FF29961EEF0299ACCCB6696606AAC9699D7992099C6BB2B288EB17B5907D78319A15D0CAFB8CE96B654BE2F1200B722164A4A5A3289D314B46BAD462EC95E9D069F82652C3C4095314B3504DF63463D0CF2F2D746CE2F883B8C6A7805DBCC94004CDE0B5A3A79C3549B3C312A426CBF1FE3E8D46CCF15375959D8DE784C725653152DB3B97917A1951F6FDF313FC593CD8425D6A4EE55F321D27CCE899C4AF8C83EC25A95669267080F6CDC4B16DCB0E2D9BAD8FFD1F31590BFC5C82C80BEE12143C210B6C104F3E173438D6CBE0F42E6F2EB6A2750AF3A1380D2125D8EDE383E6162065745400B90AF9682B2BCE9FEB2CA3D187B148A3A592772D4BF135105C938FDC7904A799A5DB2D7503515A54EF84F5DA5FAD4D83369652A32EADDD555ADEB6B3218BF2932C54AC95B552D12E27BA99BBC9119645594EAFB5BA859AA77683E294D3CC12E8A8A88BC8892675F3AA1E166C027A0686B33323EA45A9274D51DB772B15C5D06A66E179FF5AB8952B2D3CF78627B47F9A8911847F59F99387970FFC27DDA6A24DAEF8AD873665C9B7D5C85FA2DEC1FCCE52E680D3622B930B42597F09B72DCAA7C9661306243531AC1A20A3963DADA81974DD2C22AB535FF2D87870C9B73CD95D64C41F4F40C0884D980FDDE9445F3ACCCF66D9D5AA44DDF823448F1162A99CA017FF44B309550D1793CF3BE8DF7E6FCEF5F440127799D6E6411C4C8FDD31E4D8CA550F0226817309E233B7EF9AFF3D1E65132740C79263B62BCDFA974682D3C4314D9033E295D0D712C2881F6A0E1EDEBEAF922B6E410CC9A1D2DD9A0CAE142914C4845B3AC60CB9D71852EC35AD7FE74CDFB1D1F4E5560A77FAE212BCDE1D6AE651AFF3C6B18185F8F6D32933DC524A1C5BE33146C8C6FA37B01F722354B9DFAFC8A4F769CD7B304A3C088366C4C7BC6D8ED6283E1555B4A2F7F6CF9C462AB5B4D52D8CFE937FA855F008ECE0C2C02EE3787CFDF8C16200619FA7F956F41696CA78CC0D691C55B018870D26F236FD30785D7AEA9F90F16491488E3F615ED6AC390DC14E5FC8032D23579C24AFA29241830C4D2628D790179B792CAACB5EF865997DA83A60AC8305B09C133003121F6B3F0CBABCB74A62A7DD2AFDD1501E2AB594C6E3FBA51D83D68A68A835C726CE42D966215C0BA4BC7774A439A1D6DD12B4578F5E713CF500EC7D456095EAF9BBFF8A6779DE5136BD8980D90301A1584F221989794CD651057B021A9D6E15C030FDF04EBE6BCC85353EA23A4C1EF70BAD07846D81B948761DD83D18B09CFCB09E487A518BE97733E799E8A55083BA3409B9139AB44144F6E283D7532284FFA52A1CE1EAD335F5AFE051FC50A" +} \ No newline at end of file diff --git a/priv/kat/wycheproof/mldsa87_verify.json b/priv/kat/wycheproof/mldsa87_verify.json new file mode 100644 index 0000000..6db74ad --- /dev/null +++ b/priv/kat/wycheproof/mldsa87_verify.json @@ -0,0 +1,36 @@ +{ + "source": "https://raw.githubusercontent.com/C2SP/wycheproof/7a325932231bd7a724c791c8b0d65ea4d6d2f435/testvectors_v1/mldsa_87_verify_test.json", + "source_sha256": "54adffe10f6dad7941ffd97f77ad4e5c85cb3a54d44cf5ce1c95cb1757b9ba14", + "licence": "Apache-2.0 (C2SP/wycheproof)", + "algorithm": "ML-DSA-87", + "valid": { + "tcId": 1, + "comment": "baseline", + "flags": [ + "ValidSignature" + ], + "pk": "17a508179b35057099111733da28fd1a2265de7d8ab22d5279f13bca84cc42a5b8c9644c121e7e1b81723c5295be288fb6c36bfa188b6e08d913a152350947fa2c8ccc3fd01b319f65a2058a1dff54133946cfeb408d0b6dfde6bbebd7e0591cfe83b8b5452ceef6c855f7d33e06a0d269345089ed0d3ad67d84d8a4a34d16836004cff125469e8c3387abd788b620e30c1fc23909117a0e34c42a6631d9791347b1b2a3c9ab3082416211afb7bc3f6ce630a7019af19f736cdfacb1e7db66b65ef56844d2a2b0753d09283a7a0b66f77596384e95f7ceddd1c4ba20edc11f1eaab695bb963f6eda1c383754aa372a0d7729bfa6e0f142131c2367ba3f89ce3de6c357f9a7225b7cb85f6b3e8a3a122e8501fd1446b8152a415c19dda1d2e4590cd994f6664b4d1abd7381468c3a085abe2741a0cfbb81880664b271677245c4a471bf8bb8e0192eb32e4fb5e8560f3c50d6b19a353e486d0fcc2a35ac046286e707e095f61786d92212686a65d39b6863e0f8cec1e1997f2f845e4878ca9df650c746765296790863e51d012d32dffcbd746aa2276d04c0a57cd1b3d6ed06c0d66a0897aae5c49c97b6f19ae829baaafbfed28a52c05963c6eea9eff69528294207f8cda75280f7c486e6848791c8e37015479f2e13c28a9fe654dbde11689875203aaec51be3da7cab1cf31e4ec476c0c830cbdd04ac02167c0a6fbfdd6548b1fa525d235c7e3fca8d63e6427503b0a45c0bfddb428b837c32e8755441077bfe1c0142bac357b012a46545bf4148d465472dcf89c9d73b62357087e229f53a450d3cce41c8ee21a9d54b61e34a794f5b1406a70724ab0c3712c49df231ef30a956075e907c51b63dd1f9453dbe60e25b0f3cc0354dfd7c9119313919e77cb2c92f544d3e5302b8827603e936b567e99bfe9904932585a9f01a5a1b5bce07565f1d84c6b1c5c86259e1fefcff18cd06861122be6836be21e40be4eaf6bcabee8f634f95520aa914bb51c54dbd67d1b9dc5e38831e786c283979a963a3206b98e339edec4128b0502d4d47813869713e431a529a03c7f54b50123680f2b7f256f5d2b40642203259b9e85c62253d5670ce372193f28b5aa48ddd643c54756a2cff808c109f74772961d8db6bb8a17547c8f29c7f5ff3ea06740b867d84917e07f3978ad0281a20689eef58467e768b6178a9b36a567289fd39762bb3e4254031b2798a4550857f6af369d484392cddd7b48eaa2942e2cbfe754d5ee2da2b7fa71222e4a525ff5224d551a778ebd828e4e0499adc74ff0d59a5abc78ad6a8abafeedb3c99045a14423507f85597b1a7f540982f7d72ea13449110b442d54b78029b4c7fe3b49396dc6c3b7d58792538fa907963de10a4b724548142541cdf1512e0f7ff1b10a93de63541b8cc3268b4de20ed26739ee8973b6507ebe48965602c35fa3f7d4278146b598d7d7044e16e97e9351f7c51ac25573b7232ae2432638e9166190e7f7a7dcb5096ecb5d10017cdea2a82b4f56c7385041c6919a7e36e11beac77ec3f25df44e7b596c1542c1e376de3667c0e903fe25b57c338e9d93c5570c484f0ddab4f57d38f292b23599d9efc7a9fd9e078aaddca0acb1a196d6c45d3c8be6f39e8cdbe3299e370b262e0bf6fb5f005cae2b12879289d00bd8039de6a571c310d87557f5c9a4f64a0bde7177a8464722a04bf87fa2cb0e312d4fa6e536c61d65dc2c1baf144b0d1d1d75f4c860626ff773933efa9941d105c53a1d92c4f7c7bba4aa969590acef1e50901870f59715ac14d9846d83871a77367be57c63f88bc2c02eabafe678f44925a3e605979282fcd3f284736a1d346c033cb782dd615e886683fc37cd87a91422857774c63c6659096eba393c56225ed8c3485b4f89ecb07d53526281a6426ae7d67cda52fec5ac32320caae9b96000bcbe9e8782be88cb1ca6dcaffb74ef04c77e03a994bea2c89e4fcfa44cd0c9f4e30705a8b7b20df8c76b05a4479400e07db03d243e9fe4c90d34e9245f1e574be9a388f5355482077e4e98b919de024e666fdd7d51ed2a0d58a823e7497eb07303cf1d6d5f10a536be980220de5856727e5c13981839cfa19740988e7771a2b984f53ae3a5916ed881a4a90fe524f0bb3778355882864f8961fade32e656fcf9f524e748c8196a1f1bbc57bf8da7b36de9b0080f0c7bb8487a2b7bb7a81a8ff43a2539b367c9a48c70041520f05ca3dae316dbbe3118218216f52b7bcdba7557c4c9d861803a5e2ee01d3682e1261d7cae0a99fb8de909eb2bc1e112aa43cc2fa9c76a222bd85faaaba5d9ec2198ac45a295181a324a0592632b89e2752582cd5e01e1a610e7563faee10b76d853109e257e7c0c248a9fb7933f514b07b4f4e3a4a3d2cd22e8cc45ebda3bef5948aa050f01eff85ae98d19f69c51e67ff89f2df0c5268acfdd325e84591317e05cab4f9e6358f249c4ddf4019fbc8f511549a733898a50efa9e0793083de0b15b5bf78d9f63d8df830d42df2fefa27b89e0ede2a702eb9467118fc0ed44edc63ad1b1935877c34843fea06fdf388bbf83e501723a13cc6cc2efbb9691fe28fc1d45270591e5bdf7aa1c82673544ee29d9e6c9da3328f21e9729bffd7f4e56de585909679a74037105fdac3f51ae35f69d9763d2e4cfeb1d4a8fdce99bf1aa21f866a9f523b2a9549e12258a4d19900cf5db37b67da19b23563bd1d701c6106fccb28e4689c62e1a6cf1abd763d7239c2258b765610d4478be9f1650cb8d18923592ad0024076e52f9bd0a3894fe97bc0a1646b4c37f62c27f32d0df270260f47c49a5caf110e4cf80168a7d54b1c70bed9bd5d9a143ce869a05cd44ee266aecd6bfedb39be79e7c7d5c11a99575ebc0f389cc55a4fe1469a2d61b70bfe4b74e3e27521a037d2b9f4fdb377231e2ceb214ba90f6953865c683215203ce963875c6524c01b789e0389a9f0c386eb236f0dfba6c95df4f28ccc7ae7cd473f9dcd20817cccdd211bcbc78b064e936e4ba2813df531128428ddf410e6ca07044aeb4cfcc0a16c995ec51c8af16a541ce18dbeb69a26635632dcc24ee52a5eedce38c502cd0e356ec31341c893f92e6063c3a160a53d34b85e92357a8ebaaad8f206771be43ee48cc409825a7094bda529ee18776d9e67f1fa1c1419514309d70ba2443be2f63b6943478d6c0f56dd058731e53de4c30bfc7d915e9284a56248e81944392881666680d4991f04269ec9a83b24b458ed59a6c274de452ab3013c103a4920543e6a7d22dadfd764f6ea39d49b910ee0dc216e547aa5fb4382a72a568ebe83ec00416fb5830dc21c24ae72416602870cb52c3a8a1c4c12a4b287b9b800d31c287ca161f404a9e598a5358d28b3aae43e534846bcd0d7a9c7652ae01e6698c79e315aca8198f36de45af7084b1cb21ca2ba0ee3a547a7343a10ef9e3fd17b0a4060badd1409a0562cba25b84fd578268fac53cfbca08e6cf6e5419f57262eb5813c1d1324e0df1d483ade08d8f6c62498e262485ac7c2872b11b42e5c1b797fc12e838b38a711d364d45cd1ed35f7faffdf4b0fb0eaa312fc3d5af77909b0649cbbacea10c9831273922b5b05172face9ce6cf324edf6e2f5f5fa0a9f0463eee938b30adf3e55664f94d274cd87dea901a7e08e805", + "msg": "48656c6c6f20776f726c64", + "sig": "ba4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a111518202731" + }, + "invalid": { + "tcId": 8, + "comment": "signature with a bit flip in c_tilde", + "flags": [ + "ModifiedSignature" + ], + "pk": "17a508179b35057099111733da28fd1a2265de7d8ab22d5279f13bca84cc42a5b8c9644c121e7e1b81723c5295be288fb6c36bfa188b6e08d913a152350947fa2c8ccc3fd01b319f65a2058a1dff54133946cfeb408d0b6dfde6bbebd7e0591cfe83b8b5452ceef6c855f7d33e06a0d269345089ed0d3ad67d84d8a4a34d16836004cff125469e8c3387abd788b620e30c1fc23909117a0e34c42a6631d9791347b1b2a3c9ab3082416211afb7bc3f6ce630a7019af19f736cdfacb1e7db66b65ef56844d2a2b0753d09283a7a0b66f77596384e95f7ceddd1c4ba20edc11f1eaab695bb963f6eda1c383754aa372a0d7729bfa6e0f142131c2367ba3f89ce3de6c357f9a7225b7cb85f6b3e8a3a122e8501fd1446b8152a415c19dda1d2e4590cd994f6664b4d1abd7381468c3a085abe2741a0cfbb81880664b271677245c4a471bf8bb8e0192eb32e4fb5e8560f3c50d6b19a353e486d0fcc2a35ac046286e707e095f61786d92212686a65d39b6863e0f8cec1e1997f2f845e4878ca9df650c746765296790863e51d012d32dffcbd746aa2276d04c0a57cd1b3d6ed06c0d66a0897aae5c49c97b6f19ae829baaafbfed28a52c05963c6eea9eff69528294207f8cda75280f7c486e6848791c8e37015479f2e13c28a9fe654dbde11689875203aaec51be3da7cab1cf31e4ec476c0c830cbdd04ac02167c0a6fbfdd6548b1fa525d235c7e3fca8d63e6427503b0a45c0bfddb428b837c32e8755441077bfe1c0142bac357b012a46545bf4148d465472dcf89c9d73b62357087e229f53a450d3cce41c8ee21a9d54b61e34a794f5b1406a70724ab0c3712c49df231ef30a956075e907c51b63dd1f9453dbe60e25b0f3cc0354dfd7c9119313919e77cb2c92f544d3e5302b8827603e936b567e99bfe9904932585a9f01a5a1b5bce07565f1d84c6b1c5c86259e1fefcff18cd06861122be6836be21e40be4eaf6bcabee8f634f95520aa914bb51c54dbd67d1b9dc5e38831e786c283979a963a3206b98e339edec4128b0502d4d47813869713e431a529a03c7f54b50123680f2b7f256f5d2b40642203259b9e85c62253d5670ce372193f28b5aa48ddd643c54756a2cff808c109f74772961d8db6bb8a17547c8f29c7f5ff3ea06740b867d84917e07f3978ad0281a20689eef58467e768b6178a9b36a567289fd39762bb3e4254031b2798a4550857f6af369d484392cddd7b48eaa2942e2cbfe754d5ee2da2b7fa71222e4a525ff5224d551a778ebd828e4e0499adc74ff0d59a5abc78ad6a8abafeedb3c99045a14423507f85597b1a7f540982f7d72ea13449110b442d54b78029b4c7fe3b49396dc6c3b7d58792538fa907963de10a4b724548142541cdf1512e0f7ff1b10a93de63541b8cc3268b4de20ed26739ee8973b6507ebe48965602c35fa3f7d4278146b598d7d7044e16e97e9351f7c51ac25573b7232ae2432638e9166190e7f7a7dcb5096ecb5d10017cdea2a82b4f56c7385041c6919a7e36e11beac77ec3f25df44e7b596c1542c1e376de3667c0e903fe25b57c338e9d93c5570c484f0ddab4f57d38f292b23599d9efc7a9fd9e078aaddca0acb1a196d6c45d3c8be6f39e8cdbe3299e370b262e0bf6fb5f005cae2b12879289d00bd8039de6a571c310d87557f5c9a4f64a0bde7177a8464722a04bf87fa2cb0e312d4fa6e536c61d65dc2c1baf144b0d1d1d75f4c860626ff773933efa9941d105c53a1d92c4f7c7bba4aa969590acef1e50901870f59715ac14d9846d83871a77367be57c63f88bc2c02eabafe678f44925a3e605979282fcd3f284736a1d346c033cb782dd615e886683fc37cd87a91422857774c63c6659096eba393c56225ed8c3485b4f89ecb07d53526281a6426ae7d67cda52fec5ac32320caae9b96000bcbe9e8782be88cb1ca6dcaffb74ef04c77e03a994bea2c89e4fcfa44cd0c9f4e30705a8b7b20df8c76b05a4479400e07db03d243e9fe4c90d34e9245f1e574be9a388f5355482077e4e98b919de024e666fdd7d51ed2a0d58a823e7497eb07303cf1d6d5f10a536be980220de5856727e5c13981839cfa19740988e7771a2b984f53ae3a5916ed881a4a90fe524f0bb3778355882864f8961fade32e656fcf9f524e748c8196a1f1bbc57bf8da7b36de9b0080f0c7bb8487a2b7bb7a81a8ff43a2539b367c9a48c70041520f05ca3dae316dbbe3118218216f52b7bcdba7557c4c9d861803a5e2ee01d3682e1261d7cae0a99fb8de909eb2bc1e112aa43cc2fa9c76a222bd85faaaba5d9ec2198ac45a295181a324a0592632b89e2752582cd5e01e1a610e7563faee10b76d853109e257e7c0c248a9fb7933f514b07b4f4e3a4a3d2cd22e8cc45ebda3bef5948aa050f01eff85ae98d19f69c51e67ff89f2df0c5268acfdd325e84591317e05cab4f9e6358f249c4ddf4019fbc8f511549a733898a50efa9e0793083de0b15b5bf78d9f63d8df830d42df2fefa27b89e0ede2a702eb9467118fc0ed44edc63ad1b1935877c34843fea06fdf388bbf83e501723a13cc6cc2efbb9691fe28fc1d45270591e5bdf7aa1c82673544ee29d9e6c9da3328f21e9729bffd7f4e56de585909679a74037105fdac3f51ae35f69d9763d2e4cfeb1d4a8fdce99bf1aa21f866a9f523b2a9549e12258a4d19900cf5db37b67da19b23563bd1d701c6106fccb28e4689c62e1a6cf1abd763d7239c2258b765610d4478be9f1650cb8d18923592ad0024076e52f9bd0a3894fe97bc0a1646b4c37f62c27f32d0df270260f47c49a5caf110e4cf80168a7d54b1c70bed9bd5d9a143ce869a05cd44ee266aecd6bfedb39be79e7c7d5c11a99575ebc0f389cc55a4fe1469a2d61b70bfe4b74e3e27521a037d2b9f4fdb377231e2ceb214ba90f6953865c683215203ce963875c6524c01b789e0389a9f0c386eb236f0dfba6c95df4f28ccc7ae7cd473f9dcd20817cccdd211bcbc78b064e936e4ba2813df531128428ddf410e6ca07044aeb4cfcc0a16c995ec51c8af16a541ce18dbeb69a26635632dcc24ee52a5eedce38c502cd0e356ec31341c893f92e6063c3a160a53d34b85e92357a8ebaaad8f206771be43ee48cc409825a7094bda529ee18776d9e67f1fa1c1419514309d70ba2443be2f63b6943478d6c0f56dd058731e53de4c30bfc7d915e9284a56248e81944392881666680d4991f04269ec9a83b24b458ed59a6c274de452ab3013c103a4920543e6a7d22dadfd764f6ea39d49b910ee0dc216e547aa5fb4382a72a568ebe83ec00416fb5830dc21c24ae72416602870cb52c3a8a1c4c12a4b287b9b800d31c287ca161f404a9e598a5358d28b3aae43e534846bcd0d7a9c7652ae01e6698c79e315aca8198f36de45af7084b1cb21ca2ba0ee3a547a7343a10ef9e3fd17b0a4060badd1409a0562cba25b84fd578268fac53cfbca08e6cf6e5419f57262eb5813c1d1324e0df1d483ade08d8f6c62498e262485ac7c2872b11b42e5c1b797fc12e838b38a711d364d45cd1ed35f7faffdf4b0fb0eaa312fc3d5af77909b0649cbbacea10c9831273922b5b05172face9ce6cf324edf6e2f5f5fa0a9f0463eee938b30adf3e55664f94d274cd87dea901a7e08e805", + "msg": "48656c6c6f20776f726c64", + "sig": "bb4275ff54c22d2d09ea1937a0667362acd44925c6d6965fad350b111d1cbcce68ddbd0e576d1a8810eb4e71623781f32f747d44c8e693749df191682f588906949d97617a4b0ec54ad966818dee88b95f0f28ca24bfc5bfe0c316140b0662c43093ae48b899cc71e5739e9d67095ed987a79b6a0e7aac960c3c4125f0e92bc9435d10bfffae34bb3af05e977ebe0bafcbeb2381c5afe3379667b4c201aebf162dbd0a4bd1baa88fb2f88fa970499a848737d3cf94cc8ce278880a169cad91f304e4e8f1091d4cf39d9a3ab9f88dcc6f3bc4df311a5be0cba290365b3e879527e2a77f0cb6eccc9d85a5e592fb00f3a2e925a26d295a6b82746d7f534c83c35bc4826ee4910216b9a2867032698996fb0e1669b539ccd2ec74d181f4844e8f4d28f9c174316c12dadb54cb1dde7338238a20731c2565bb959f8e3086273ed03abc7ac515728750633083c0b397b29d385d13f5afd2529b32f00dce66c9dc8ea93d99c8b61c5e0ab2fc70de2a8dabdcaf290d53e8fca7561bda8c516ad475e4ec6c7cd2603aa3c8a71d9fa5dc7efc33cf318bebc1f1594e6ea25c69b8f9ce34a65b8ab0ae8dd3538bd267d86c584b8f354d7e4776ed4dd59a73f9e70a1df572f033b69b3eafa5a901e02515472e37258608875ca469de07db71cd6b8dc7edeb3d866ed2d219e44fcb133a066e89d8e3013569ca6f1fee7bf4ae56a6d32a5f3e5a530819c31aadabc8a88503edbea9cdfa3171762e1e8bcebaf9bb6af7e540102d5fc810bbcf1e02ae564e04d9dc55dab0a9392d6c95a317730d9793954da2cb16544d15403d0db01e85881e2d4f1b9b98458e1af0985f98b014f08f200558f2fed7a70c352a27423bedeebb3775c0a1ede9d461d2aea303c09c8b1f73fc37a5b3a01fb24a131574f7eac90c78baa38cebe81e2335dafda20299f76d6a0ba77d2954d11381674f4069f45e133886d64222d92583d5908e3ab6eb6a72cfb41f7dc3e71c1383888f61624bdcaf12fb716de98232ee329af1dd045f30d377234db7bfd11ddab0b3108329e16ce568c8bada39a98df5a5f72a72f063fa4253313a806013f61ce5adbe54cf42180ebf6e496bd4b42aeacf069aaa8e1c231fd037b394d78a69d1742b45bc5784ab8593a198077423c4c357d734f9899cfe9b3b62b6b9c3f4d781b8484a3fd0eea7e8d6945f4ba2b046fee079b7f032bebbc402918daa2aa1c9433dc3bcfbd7f49a5d7a293833c21c1bade3e8a7ec3c83d485529de51b5993cdffe23b770e25acab0ab9fb3059f14952d9464ebfa36a6274d8da317a7b07c2afe38ba28ca942cf7bfaee4020e59911c047b2aa24d1787baae1bc3546364b782358676d75c7698769f1d4a0d5dccade7fcf80e308437f7fc24fdfbf72625abf1b0f534da62cf860da1efd986950e3e19a085999f4d008fce38459c673befcdf2287c1766e106beb4f3175e97812da141330dcb2beb3265e38c8423c19dd50a655c9e8dc969f6367f3383b644d53a26875d53cea26de429266b506e70e7e6832886ed06d81738b0b482bd23bda396eb674ddfdb64803d6c4fae2f040170b5a28923279838b7b876220d02dc7478666f7c3287b1ba4a2f8228e8c491a55ba459805c601b986caea27ee9436f63383351c74d673643b15f6007fc6dc49e337a65a8a96ecd7eef4ad730bf3dc1972fc396703ee26af1156dc4beb46c47d99bb69ebdaf81c2d738d0f70e57a2c7162f5a55242255675f22082aa43f5b3dd862b535b2a15bb815b4e9faf16a302552cd6098d40930dfad7a7c609d6aeade814242a8bf0721c1c26d0d3daa7a638880a6411c6538d80c259d31b639aadaea49563a8d7f5ddb64291d6b086c80d72bfe7d26802cbd20fd6ba5495011e42da1c82483cbe8e37838e73c48f79f65b205476bee600399feda6aaaa939eee12cb34be4e6bdbd18032c85b54a2713551de677a16ca0142ff97b77bc963e8f3c57d91df9b49474b01ca514641842893abc3181caee3f49b635d17daf41bcc4aeb1116ec4b3e78ff1480cf3a5d9c6a154384b88516834d19196976e2ba97cee91f7a0d73f7f8146e58dc0fc8f510511d39d82a7ba531a4abcce6b624035d753a37c5980343cfc7724cf83efb0c33fc4abb5b002241bf57a46b67cb5a4cbd637b2f19bc93b368d97e13c6a62c8443c8222e0a90c3ed1972cc739b824fdf729ed8eca02ad96bd78bf6d2b3d2853e24fa93199ff41635176b31aab2207013d0a9317fa49668dfeb672b8129e6175a2998642ab8e74f0823e3b5480ae80180ca395f5348744a3c7b344891f008aab65914760b5fe852615ad6425216b1c5e777db1a46517cd01a77b277cfa2c6f250c2d68c495fef28feee6e0716817d6b30716f5ca48001805042133eca17a41c2219784ce0f12858ccfd371c77b90966ea04c3996851edf31aef962946628007de5531b06fddf3449f6c552eaf6e16b3e9160e265900b8c8e414732505e02660e123d45a3d6f1b15e56fd759d38e821e27e84967e95c4b0d2a48e008897e655d1b65b76299f1e3074209abe44b37f8786b02df23aa4dcebe512e6312f1a3d6d781d749635247aca897626f5ee688f517df6cfe947ddead820f07fed4bcbe7f1bee3f19117b5666dc123d528f2bf03db346d6afb53804b0681aea98a9479fc6de5ae974d2c0d2055f07ba8a1d2d8b6a7e08a6805bd8634cc190c4988b502f475c36d78c7b3dda0057c818835bbf21c5a7c13bb6fd91cd3cfd76abe4c2ff908a08a3000b9021500ff94b297cec0fb3e51b1cae7026ab4347bfd5b6a641a3d347f45a2a2aacd22e521e00da0c3986c67672c5d7d7e61e7edafc15d42aa42cb37fa8621f79e9096b092efe853ab318b3e4bbd90f20165a1be1d3aa5b3de43751b65abbd952599869778393c4351ab8f534fd49e16547df01c40bfaf56de60b4fb0019bc34177cc8e2236d219fb3c0b84dedae6b88e134280eabd1823420cd1afe6d929774967fd7d885fbad33d89ed9dc5e0eb978eab5d96c50bed5887aa8277880c7b06bf2780cee82ce639a1e3344c88a25102edcc17a4cb48989c4ad6a998726ede31deb0b98107f40858bad7864983f6ecdf0c9761a42751b19d5360daa7fddbbd2e2292638b95c763ca1e747eedef0dd387e9d9ec9e5afee207d8c45703c5befc2de3878d313655ce85ad984250cdf054360f33d41dc193060d42cf9528b1fb91d6ca3395199e25a1a7739eba9a6a4ac2c417ae615940b3eb1b746dd0ecc7b2f7acdff887110115629f70877dafcd7a6625fa1b9e256bc8fe1d66005dbcf12fde0a5fcda5d4f23d58ece91d60eb91274df8d9d17d4a39e63533acf1b317db979b04ca3ab9a0bcba652d0010ca3fcd33ed8f8a62faf42f78b37912d3adb410f20bf16b31359cacc25fb783083a3f065f0a2dd6fe58b8f594e11a87bf0f4c5f5493f334c18b03ebdefebce50228937ec13a8c221b617450486291071f3c14f64f66c927dd4bd623c214ac35433b8a6875cdf00916476eac0f196858aa1484bc1cd45b726d33a965619829b8deaa9d9fa0c3c210f23967ce26a4bdd939cff8aa662f70fb0af97ee44bcb9e2755000a195741d8919e4dcb1a5caae21009f686fa1489c72f16f9fee76b5410ece7f406947f4a19f394a5121da79f3216777b0fee5423328156ecf0b4548dbd5b3f7b526d6b9cfd57576f67dd521c314c2d37474ed0cf732c3b073a101c735a4c4e6b33c9aaa12c91d147ad1075a20287d36c388657614a9c648d8e49cce8cfa282b2a2e8e9da6e4444e4b6aed1bd0ada5009a335cd0500bdae9a01b97f7e8cfe8372398e750de92ee4a524393a19826d19de5e762fb53a88b9b4657e9c6d7d01a4124e3e39532f614aec5cb88d982b78b2ff568017c92f6a1ce5298b5f323b5ee61695038ef0c3a7a339cfac31cf7875a4563046a40e7b35cef1d37d811b342fcbb5373122415befc23cb656a619f7c262c443403b23ba20e341a079918dec6f4f801b92781179ad7ac1951f39ddb0b1f1fb95b28c0f4593a04486f0e0e86bb3b014674879aad10f41e0bad34d40bc817b6fd43c1dde8547882d82e5111e208107e9c9736d16ca77ac7453d7b6c7976a7dd6a4c6c0252185bc9948660a07b66151b09b980d8572ae829ab2f6d900cc63066c4ecb3a0317e8a9acaa8e22a216721f3d69c67843df2fbedd88bc424f761cdf420de4ff2c55b6925b1826c1c4134090d2aae82a7f64a8d428efe1b21097a6fdbc9b8a31c4d46c7b32d478bf5b9181bef8af4d3486958d7c198c46a5ad771090bb64e7fc2bd7677ae7618b861c83c13177075063517eff40cfd52ae56650e9e7035f783bfe8920c754e98470b327909ac2a407e07864710544b8adbc075502c75b5f0fdbca0cde5d94b013141c96b55ae1b60ab63f9e792641690af41b05a78233935fd7f82852ebb2602a10459a709256c41d108c2aeb71925089cc79b121eabd5edc54f3f7c8929685b88ab29700c7fb2f247cd7962c8ce6dc9f79f9358d6a7989d24ccc17d0dbaa0b7fbe73479164181ae7d6b6a02678cce46f74ea2bd387ecb73041817b429a0220e1c3635fe492f5f3a8e2b65c086fe24c375563d7220856dd8f970716d548492964a156554dc88810c3c4f81dcc3ae80243e19679a3be9c9b1b8c707b416e9166c54a568bb9d84728c1a283d9231a12b13688ce2362901342652d66bf44cec223f561d2735d977c6adcf57e9066220d0770fc77fdf6ff367a2f36ae20062887beb88d1b453f267aef9bc763ab716bacb9214c38b95f4f2f3f6c236aa71aef83c1ae4b26133678884476c1d7c6d3e7b99f13e028ac6cebddfae4793f9ae975f3d66b725b500e7c7d2f664eaa0c358deb91cb2d6173394b306749d2bfb2684b985769bdb682e922b75555d38dad1899057a64ef6ea361e4712244d02d0dfec8c3d40770122770c2538d6a14a8462ef18eb705c16e5ba30aa5366447c94869060e4df155f7d01ffda04c1ced0ad5fbe5fdd85856e1e49320319687d31a6e4c7145479f45f43a9b8e9ffe4cbacfad4a21e5445e119df2996cce8b11d0f224efb4f18b544d456e2fb1d96fcd99fc319dbd86720621ac25b490f3611f7e5655bb3940a503c07dbf41f4b87593595a66008808744668371a54ce1b9dfdaa16f90415e57470bc23898d13d8351ba34369e96347da13d012b4eab32ada90668654c5ed2b433716b1b4170f640cf4b40659efcf4150237bbc25f72b248be85cd482b55ce2f5f73e8f02efd2465805b37d12487465ec1085bc6602b71862771af13e13baece4916b1ddef9ce016c92db9fb9e82aee5e1c4e22c45090ad1c19801ce1c541ff3902baea7a12dbcac6ec2d128ac7acb203463921ca6ce1d182d60d553ddecc4a3175eec2e924e9191e0d69aa49ca0b653495b8c62b802e443e669220f2a4047a56ed5cb7431a3387a435070795e6e63d242a74555e97371989c6d0040748e89ac316618d5d6eb7bff8d91e953afbe00464df4e4f6380c273b7ac5934cacb6c3be4da6649c8a5ea12bdf9afa1ec1e5053db7668c10ae2df75c4b3b14525369bf33741525a7630158aca3d3c7da5c1d71d3f63c0c2948a43236968d623c6c163eb757f0c78d6ae682ff4e4b673be07193e8d6c106c92851b393f0523491d5152e06de675fa22ae7bded329836a8ca0b955a59cb575395952c6ea0cb1644f2cada196b96b44ee12115ff9668e32886103d8f8109fac4a2287738ee1d2d4c1c19dab94eaa2757ea32016df60d7286099eb010ba570b5791ce4c54d860b15d56c53a5ddb543b0b602b3a87ca5213aa9647e51b1cb1736697506081240f4f7163a646f2be30e62c617d7572d820a113f96320834a2f43842160511923bd1ef4f723a2ab9b242fffe97cf0199c9f2ebca266a63beea0af279f2d18651b2ea9789d03025857c8aefc5f8bc6ee92ab7c6ffed2606f9c7ef25cb6c96140256a08cc58218869c52f3e5074fe5ace87a86057cc29fc845ff275f1b2dbc5991cbebe4c556b29ce3da3415958f0c9c68a5c664a1da60365ce56b9a4df24ec5d69c6f5a2e685885f6973c0ccd4caf7e000346bdea502b1418c8819e05bb7bee31bbd235819b49c892f7dadade576cb8ca68bda916a598c32ec03cceba2d01a7960f38680facccdaaf05e254dd8c3631a7222d1561c998c54b16f17a425f3247232dca035d545202e1b180340219ce31c66987bc86f983240e7fd2c926e8af77e6ea9f4139ff1edc0c323b67ceadead8f64a8004164d942f6aa65a28544d7205d41aafe62c5bfc6fd9fc322e2a8b620532d00d1add7a0d25a9dbaff90e19de14ac2ca97e83a2d77e63888ba0f7eaf9b18a7f2e5f148d16887580e55e894ae79024385f439acd071494e6469b77d07e763553652a470f3b4bd8bb7968052f3a969dacced51572bd125870849ce3e55359b2e17eabc3153b5f62868ea0ca1f40738488dbe0020c103f53678b9eb1c10000000000000000000000000000000000000000000000000000070a111518202731" + }, + "invalid_hints": { + "tcId": 18, + "comment": "signature with hints in reverse order", + "flags": [ + "InvalidHintsEncoding" + ], + "pk": "17a508179b35057099111733da28fd1a2265de7d8ab22d5279f13bca84cc42a5b8c9644c121e7e1b81723c5295be288fb6c36bfa188b6e08d913a152350947fa2c8ccc3fd01b319f65a2058a1dff54133946cfeb408d0b6dfde6bbebd7e0591cfe83b8b5452ceef6c855f7d33e06a0d269345089ed0d3ad67d84d8a4a34d16836004cff125469e8c3387abd788b620e30c1fc23909117a0e34c42a6631d9791347b1b2a3c9ab3082416211afb7bc3f6ce630a7019af19f736cdfacb1e7db66b65ef56844d2a2b0753d09283a7a0b66f77596384e95f7ceddd1c4ba20edc11f1eaab695bb963f6eda1c383754aa372a0d7729bfa6e0f142131c2367ba3f89ce3de6c357f9a7225b7cb85f6b3e8a3a122e8501fd1446b8152a415c19dda1d2e4590cd994f6664b4d1abd7381468c3a085abe2741a0cfbb81880664b271677245c4a471bf8bb8e0192eb32e4fb5e8560f3c50d6b19a353e486d0fcc2a35ac046286e707e095f61786d92212686a65d39b6863e0f8cec1e1997f2f845e4878ca9df650c746765296790863e51d012d32dffcbd746aa2276d04c0a57cd1b3d6ed06c0d66a0897aae5c49c97b6f19ae829baaafbfed28a52c05963c6eea9eff69528294207f8cda75280f7c486e6848791c8e37015479f2e13c28a9fe654dbde11689875203aaec51be3da7cab1cf31e4ec476c0c830cbdd04ac02167c0a6fbfdd6548b1fa525d235c7e3fca8d63e6427503b0a45c0bfddb428b837c32e8755441077bfe1c0142bac357b012a46545bf4148d465472dcf89c9d73b62357087e229f53a450d3cce41c8ee21a9d54b61e34a794f5b1406a70724ab0c3712c49df231ef30a956075e907c51b63dd1f9453dbe60e25b0f3cc0354dfd7c9119313919e77cb2c92f544d3e5302b8827603e936b567e99bfe9904932585a9f01a5a1b5bce07565f1d84c6b1c5c86259e1fefcff18cd06861122be6836be21e40be4eaf6bcabee8f634f95520aa914bb51c54dbd67d1b9dc5e38831e786c283979a963a3206b98e339edec4128b0502d4d47813869713e431a529a03c7f54b50123680f2b7f256f5d2b40642203259b9e85c62253d5670ce372193f28b5aa48ddd643c54756a2cff808c109f74772961d8db6bb8a17547c8f29c7f5ff3ea06740b867d84917e07f3978ad0281a20689eef58467e768b6178a9b36a567289fd39762bb3e4254031b2798a4550857f6af369d484392cddd7b48eaa2942e2cbfe754d5ee2da2b7fa71222e4a525ff5224d551a778ebd828e4e0499adc74ff0d59a5abc78ad6a8abafeedb3c99045a14423507f85597b1a7f540982f7d72ea13449110b442d54b78029b4c7fe3b49396dc6c3b7d58792538fa907963de10a4b724548142541cdf1512e0f7ff1b10a93de63541b8cc3268b4de20ed26739ee8973b6507ebe48965602c35fa3f7d4278146b598d7d7044e16e97e9351f7c51ac25573b7232ae2432638e9166190e7f7a7dcb5096ecb5d10017cdea2a82b4f56c7385041c6919a7e36e11beac77ec3f25df44e7b596c1542c1e376de3667c0e903fe25b57c338e9d93c5570c484f0ddab4f57d38f292b23599d9efc7a9fd9e078aaddca0acb1a196d6c45d3c8be6f39e8cdbe3299e370b262e0bf6fb5f005cae2b12879289d00bd8039de6a571c310d87557f5c9a4f64a0bde7177a8464722a04bf87fa2cb0e312d4fa6e536c61d65dc2c1baf144b0d1d1d75f4c860626ff773933efa9941d105c53a1d92c4f7c7bba4aa969590acef1e50901870f59715ac14d9846d83871a77367be57c63f88bc2c02eabafe678f44925a3e605979282fcd3f284736a1d346c033cb782dd615e886683fc37cd87a91422857774c63c6659096eba393c56225ed8c3485b4f89ecb07d53526281a6426ae7d67cda52fec5ac32320caae9b96000bcbe9e8782be88cb1ca6dcaffb74ef04c77e03a994bea2c89e4fcfa44cd0c9f4e30705a8b7b20df8c76b05a4479400e07db03d243e9fe4c90d34e9245f1e574be9a388f5355482077e4e98b919de024e666fdd7d51ed2a0d58a823e7497eb07303cf1d6d5f10a536be980220de5856727e5c13981839cfa19740988e7771a2b984f53ae3a5916ed881a4a90fe524f0bb3778355882864f8961fade32e656fcf9f524e748c8196a1f1bbc57bf8da7b36de9b0080f0c7bb8487a2b7bb7a81a8ff43a2539b367c9a48c70041520f05ca3dae316dbbe3118218216f52b7bcdba7557c4c9d861803a5e2ee01d3682e1261d7cae0a99fb8de909eb2bc1e112aa43cc2fa9c76a222bd85faaaba5d9ec2198ac45a295181a324a0592632b89e2752582cd5e01e1a610e7563faee10b76d853109e257e7c0c248a9fb7933f514b07b4f4e3a4a3d2cd22e8cc45ebda3bef5948aa050f01eff85ae98d19f69c51e67ff89f2df0c5268acfdd325e84591317e05cab4f9e6358f249c4ddf4019fbc8f511549a733898a50efa9e0793083de0b15b5bf78d9f63d8df830d42df2fefa27b89e0ede2a702eb9467118fc0ed44edc63ad1b1935877c34843fea06fdf388bbf83e501723a13cc6cc2efbb9691fe28fc1d45270591e5bdf7aa1c82673544ee29d9e6c9da3328f21e9729bffd7f4e56de585909679a74037105fdac3f51ae35f69d9763d2e4cfeb1d4a8fdce99bf1aa21f866a9f523b2a9549e12258a4d19900cf5db37b67da19b23563bd1d701c6106fccb28e4689c62e1a6cf1abd763d7239c2258b765610d4478be9f1650cb8d18923592ad0024076e52f9bd0a3894fe97bc0a1646b4c37f62c27f32d0df270260f47c49a5caf110e4cf80168a7d54b1c70bed9bd5d9a143ce869a05cd44ee266aecd6bfedb39be79e7c7d5c11a99575ebc0f389cc55a4fe1469a2d61b70bfe4b74e3e27521a037d2b9f4fdb377231e2ceb214ba90f6953865c683215203ce963875c6524c01b789e0389a9f0c386eb236f0dfba6c95df4f28ccc7ae7cd473f9dcd20817cccdd211bcbc78b064e936e4ba2813df531128428ddf410e6ca07044aeb4cfcc0a16c995ec51c8af16a541ce18dbeb69a26635632dcc24ee52a5eedce38c502cd0e356ec31341c893f92e6063c3a160a53d34b85e92357a8ebaaad8f206771be43ee48cc409825a7094bda529ee18776d9e67f1fa1c1419514309d70ba2443be2f63b6943478d6c0f56dd058731e53de4c30bfc7d915e9284a56248e81944392881666680d4991f04269ec9a83b24b458ed59a6c274de452ab3013c103a4920543e6a7d22dadfd764f6ea39d49b910ee0dc216e547aa5fb4382a72a568ebe83ec00416fb5830dc21c24ae72416602870cb52c3a8a1c4c12a4b287b9b800d31c287ca161f404a9e598a5358d28b3aae43e534846bcd0d7a9c7652ae01e6698c79e315aca8198f36de45af7084b1cb21ca2ba0ee3a547a7343a10ef9e3fd17b0a4060badd1409a0562cba25b84fd578268fac53cfbca08e6cf6e5419f57262eb5813c1d1324e0df1d483ade08d8f6c62498e262485ac7c2872b11b42e5c1b797fc12e838b38a711d364d45cd1ed35f7faffdf4b0fb0eaa312fc3d5af77909b0649cbbacea10c9831273922b5b05172face9ce6cf324edf6e2f5f5fa0a9f0463eee938b30adf3e55664f94d274cd87dea901a7e08e805", + "msg": "0100000000000000000000000000000000000000000000000000000000000000", + "sig": "8243e3f28052056de7cc4b042fe1d959767644a1b000c5fc3cab98cd8d86670c2025a62784c13c5eb0f71b9546dae058ec5027d4e6ef29dd36e8ac6ba1dbea78cd5126919dec30e8bf50e9034aa5582d522bb97d30a7dbfb1af52d9cb655b7561a17a9df3d09b277849b0f4106cec822d715bc4dacea258f60f4b9adea3b394627b318d2f305dd612fa0826791b03c0a55f456ac9b8ddcbc67c88b6e07375e3aa5114a95afdc98e1167e7cb1c74bce4c835d1b47cd1be95356cf6abc3c9b36dcd2ce4b0267a1e0afc0cc9b7e3d0df7111efe0f936c79bb2ec2b3e65b0641373ba1096bd1006f19e0fda9c7d8082f5e09ae1673cbae86caf98cfe853a2edd27c48fffba468dd6808b0996cdc5eb50f7847f80e51f353ae9afc4a0008248901c25b5328dcae28617d3dcc9556f3e1bd86000e1630210bc8a6ca56d4f0bd61ec09b0ed6d4961cb56ff253039bc8f3b4f9e406ce7e408e8861e599ad05e238d214fface8605e3a3e2173d4b5e1456edced533cacf7135e0d212c4934982072a924074dd6ff9eb3985eaa4e108799c5609b936e2a0dcb32369ccf34766462dd2e310e561fbb09800719bdea3eff789b64b3602c2303286499cbf45cc769d8e955c2a0bc87e6dd60a94284fea121540966d1a8cb893695279771a12eb8b05e33cfe1ce5566337bf90dd241d9c31fb07c6205940b77f158a7250c5698d6c5c251e787bc81338f2171bbdeb3dfb29b2e9202c29365f33e18fe927ef0919482260c01b2fc94c90222ba4dcb14723dfbb3d6fce66da6eb6013bc455848e5c2e3f52f1bddc5596d4a5f18c8f9085b3f25b4d95f8497b818808e5628f94f03b4d838c013a3c2bae7498ed2f23449d77ce94069d2d1c50d12df6d8fb8c8f72b03bf04a3df67ce2990b56595fec781ecbedced215fc0d0b5e34c10819c242a122a4deeb607fb9d3dc68ead4ee07726082d132beb524ce9937b66ef218c221d77cee77ce255b5f18e7cd7c3e2ef9afe80c7bb6675128c90cf6b483d96bc6156f2b7a4d4c98097d2b3086519cacdc3bf35f8127cdd071136b889f1111c0db2953d4d47f8a9de23df6372d65e25b4f8887b4553ad11de8ffe01264ee810a5caac6cb214baf3ec4e5062bdeb0f892014e3551e1e8f5fa50defd5bbc7ec14839b1660d2b5afa5d67a945989f934205902b3c8d62a2190621c6fd3f47be0a69f68b61a5ed1b2c4ff03c7c22fb67a621a64889b1bdf1c27ca1c7322e6233d433cf9bf0ef2c3877bc9780fea6b1a7cc15af6d370e57ef36ddd0a612f1f7e25efdc7762c70c9cd925e545672ec8acdc77872745c7fee82c572d34c9341ba176a2f1e4e629bd1342d00d8bdbc12eaf17e3c2399057ff2a6ebc7834cc80f3ead92b86e0c9cd0a909eb074d97e7783074374454eeeee970e8aa7fe4c27eccc340adb62fc838fd23554554e62c0e4ee0a50a1f2cba806b6172dfca4e3943d4af28fc4e6beb054487d47f26d0d56b8c0f70393eba137c5f3c278438d264d35ff2225f4b8400ee97e550d7770637745cb7ca1cca4825cdcb9c9eedea9bccb9e7c9c2f43e5b95892422514890c465a4784ac10dfd3c3c5ecaeb4f74d7c7c1f41b2ef8a6e27879ce049054a3947e2f71a36c11f439af849bedddd2d4e23fa71857aaf34d8a082d034b3e6a438bd195e07c9d9a0d66a10a7c6a93bd9346314d8ba8ad5b2df800db47d38266b5052d82f7585ff57c9a4ab2f5c09f95b2e050b57e3135fc1b3832d86bde221baf6996c9a95e50984c7a2776a948f4ec98a21ee77507932e8da0fa1baaeeee4ccf34ee03f62bf3c267b3e02c327ec57c17e4cb2ef1be78e22c4090ec356c0c9d2b729b44e5d74d1583aa73de3d2187c910c5f30cb41586c014e1e44fa91c6155c646ead2f60e42017156326d8a4258addc65dc2fcf63d80c9d504d77222ce363b313bf396cffd8cfb802bf6c2f99e5e440fdbb374900b569a31f04e2ccbcaa29985e60c980b75dc8167e6526825005b07ee9dfe7df419ca5660315a5ddd47d70aa0a47e9e914f0160fccf4980a77a36fb78f02530dc01a88fc2e2be50d20f6066410671ad48bb65f4eee60b296abe47aa86967e454a4892c46d1b125fbf121c2972575de626b5b3320b419a58c844f6d0e4b4aa7308cffd4240aa4180f97b1241ff641d09c90bb40d4d4071f85b913a1a35421aac0be77db6c53e964537ca6ffd8795f21345b62d37a50a6544420f7142d54e6440864a0726172937974a12d775f69037266f501a9120fa31f71dbb64d562e11749260d4d3b3ddec8049199e0c34593dc34b7f5f51cdb963205ff57250d7bed8856deebceb18e8bec50dc74e9b3bfa359f43fdca881c9412fe5c791a7b0e0313300d6bd0b9b7177efffebb7aab23af990ca0fdea4957c2412ea51a7b72efc6be1aba08ed7bba5097be312b4cd3e684e47ef0aa96a5bc18da275bb9ef77f9ba05f4eb5fe1d8b88bfb7a8edfee8705dbe213005052d6c137ac399fb29c5c146bfb371a1f994f0b0563e5aa443167321318bb002434a79b2705aa24cfc36f7f748128bf7e38041a64cdc5459f2446ba34ce73959fe504587b5f5c2f91d2b5b9ded36724cf7f375cebc67dc5f3c5eb37a9d6095250c82d414544726e22392e9a7859898aacfdf18d2361f55139d32dd4786634ea2f59b0ea8db2f03ec335a3d8add4491c3377e3d5113b59e79360d816ec53834c8994ef04f634368d3d135aecaa1d54da5b63b606dac31c5969007e501360e60559e6f01c25a608724b13b978835b4cfcb69ca2deeb2d8b6235ebce9b0123ffad14c4cba462e47c54f7a5a93c5c8e1abe257e21ead4a6216a06aa454a15f8780c1632895dc497062048f04085e360918eaf7d800d4dc7b9907adb5a1efd1a982375792ea5c27d73bfb9f837dec8077cdbdcbadf95b02aacb8b88a725df65743db5a59348bbce2bd46a80bc9485aba23500f03890e8d114153bfd037a87d1ae27a7576d86bf435373d021c85a2a31cb1d978127f89fdcb7ecad2fef36de77ce19307e4ef1ad6fbf2c23224613868c184b318b77430c5e783ed0745c9b76e635fc808cbb62652baef12c848730893b7c6c3c713711f6d7819c8bbd126be7c743e66dfeef86b13ee6d74735edd1884badfa8dcad7107d86f841f69aeea05b28d75670696ee81fc34fa3a822b31a945141c56ae5154a56a04353e6a3c120d47ca11144d460005c75789e5251a225155d3655f6566dbbe26e248532ee4b5345c97b9eadb6f4bf629eca64d269eec8487efb7f7557052490d8fa144f5c0deadef6549cf5c167a02f208f2889d8cf9f380cea5960394ab163cad4b7a56dfdbc67be3f0e915ab030bc5018ac5f5a636b219aa00c077a22437d975689cee84b2b2555a3d5ff145ddf2a0e27b724265cd60c4db153e9975cd8ac5bb5658e5f61cddd821c31c349f770536be773ddf11b42d44fc22d5f954617b49a74e998459f230498e428927e875afe309779aca4bdab38eeb03fb8345b58ffac10a7b1e862e1fc9eb44def447a1e89ad707d36cd9d74f5f38a36251bdef3adc807ac384976b7c82133ff8d0ad5041116c428a45c9447934945def2f008aeac3477cafe3508392bcb5fd3db35a589f38c1b944e079aefbfadccc22061227936566c1b662c62d3816324582058dab11512eb1d6bbafac7cf7bbc4ff88801431ba5b14146fe27456e67fa285b81ebd34cf82fea2977dc70979b82f03d7e17815cf783b54e6e5096609653181c94dc8183c8b5561a8f8a3790f22c261ef3dcb7eb8c7a3eb0f432f016c92e0ee5084d121cfdd34b214bf7715de4dd6bba38d05a90806787cda65aa739d41d9511d7e0bdd9b6395f8b205be597590980a17f631d877a801c469f2e81fe6a44b915d89b04a3b35f087517be09cc2460af73d34b9c898ebe8dc48176316b3b747e1a8d6e0fd7d19753ca03b5c3ef1d0d9122e7c5fd373bbabec86da39fc3a223d3de048b2519fbd892634821dc195c011c078eddacde76fcc1a728906214e12be5901da2739751c2a5aed665b10a55b265e80c75e6d11e1d8ba9ae2e27fced1234b11ec8296eaafc0cbce4d65f1750a6fd82cd3fce774d66249d0d4e21272330cfbe89ab722ee78ea6ccc3113f6829e7d437b307b6eb0cec0082511ef8c69492ed46752523d5e4bbef2799eb76940f4035c101833f7059fa4c9c7765aa0d378c96982e47285aad8a2c5dc33303e2063166c10a8158f5e9e0b86fe152035d767be0dec7b4fc97313bd50c27bbd2323aee3d8b508a2fe395024cd8331996fed16f97ebf70ecbe997de5b8bb888ae8e522360097eed1c96fb2ad50af4a2a8961be88b46aecbcf31c573d5f172074f20e98d68be6129a1360a7c8dbc0dd74a21d936deb873d33e66f8d8f6ba73d8a05a473272b249025a8ad5740f7f37aa5661efe6558138d8487644ffc05f952777083ee67d180b80b1128a3a1b93dd3aee189f9eea635a49da7adfa127b7e2e5567e3e0d58efb6ae588d336bf38121e6f5494e66bc1a9e1f975d596468563f2af990bf08ec933016e9c50f086904f30ec130bf4a735d7b414d1726aa03222ccddef29b4ee69062e217928a00eb0dab678ecacd601ef33d6482e39bfcb04325289f7316d38938460e023ed12f38c38e08fdf6c75341909902408a5fe8786c25716683661f0060222db5d9a1188e9624d6eff0bbd3164adc28d6e09b339c0d825a6653503bf7251972f208cfdc0bdd1557f2f9892f3db0a9efc8409bf6c83b2e31bad172cb2dd95ae311d873bb80e642342814bb71890b5ac9e116f0f367bc85a790839e8dc1dd52b167f6abb3809ef2e8a34f6dde611aa4f75409f6c9e0bef4353d1c2c50df590130365e20204ffe392ee3438d644b8eea3f4a92c3184e232745957ffb88326b7846d4a64eda2cdd5b95412235e6420de10158930f675900f9b558867afbe6c182e37cbeb3625d88adf0dec9c8a3a096dca43b3c7e9eaa54384f4f987354407f2855f4ee5746ffcac55293d9187a668ec72c12468f588b7088b069b20aa03cd1ff3bd9af26e922922e327ba6a424eb0aa9180cb60da4c45a6b76aea03258818d9010ffb6ed4fd0e4453188513a15382e4e057708f4ef24d57d00968009c1022f3a40acca9dfa5523d7fd3e63603f198a5c72855e1b6169a333925fa5c4325efab41ca4800149dfdc8ce7635836978f797bdebf875294128f3ad72ed91ca8b52476f4c7e0c67082113d2dc327a4f6deee33bda8a7db96672b990614edcc7138cea1a006867ff22528f8eb7602720c58564cc8a9cff02b404355a03d60f03fa07cc74a6ba70dc4aa8f2122e5b7863af835ce5fa0e80ce1e62534615a240aadaf1e09f8d201559e19879f6f7715cc459229784bd67a3db8e6d65cf4b149b678a89661b3d0a2f22709347e28ba2bc4d4c408c7df1861266ae1fe47a32c22eea95164a42fdd9070e4403f5a8cf39bcff30f97b7dbbe54cfe09f1dde66f84be59754555e64af1d52508e59b97110440ae59da8cb70f2e7c551d3e41b281b80bead47668dcf8e7f1b2abc09af9af89f51c8690ac965aa2ef6ef565a532c7a0aba5520c41c1d365326a5d5e6f1b6796ccc828290e18a502c2fd403d915f96e01cf1936703ef01a781d7f48efde73d99e80f551768f1cab025f176cda4c9222a074426af4f94547b5108b89534344424f1985558111b570ca48955ed1035af01a507e93abb177447f778461a5908e5e1040b9ebae6d1d2f303cfb463d9784cffc3a95c6631b318ccd952ae9915c6de13374446cad492f84ecc36a4a95d520a76ada6a5437ee4719e73cf997b5a3a590a68356419888ddee7ba6db7288c36d5aef6030123bbf029f8ecb8302155d4160ff4c4c82cbc3f97b463791bbdd561431ca1fec380b31b5fcbdc920e634bd2b42eb8826d96b605a69c60c92a51359ab687b90b78ae6d94e51b64d721844b8b73a71fd1f201c41f2871e3a6e5d0b0e0050437938dc298f241974ae1c00e90e72e04c6ca294416c2ea243174076463b027addf5c423a93bbffa08893f9d401dd970ab0a907ecadde16a41bc95d492dfb8ad2d7ba7f96a98c30afe47011501608df033bf1d49b1b5135542ae8b0f6e556e0e49a9883e4632c6da8f6f4734e2754736280e0979faa883c5815033896644361363045923b8774b8aae0241f2ece112daf259ce072a1fb2c236913a52a9c2a9ad0b2e3a74a3c612a654f313b230673161bd54f9c0c907652a4a1e13677661fead894b893f8245525e6e39d43e5cef2f5686b07c94dfddf62d35f9c256df6c71ed2e4aed383cc6841b3ed6de8019ba6464c6db9c7c7156fe67b899745a65f0fe624b25a2763b7453233d522fd3a2073309ad4228452b84367d229831c86156df0e04aac208b9433c46d8fd03f252453505295ea12278724d473e261c1b05edcdbcad15e3ce847c6f1f04dd9241dbd7cf816f52332e250efff6beb7b3976452282708d4c6917f59422af0dcb9a89a81775b410900000000000000000000000000090e1518222d343e" + } +} \ No newline at end of file diff --git a/priv/otp_floor.table b/priv/otp_floor.table new file mode 100644 index 0000000..6611d22 --- /dev/null +++ b/priv/otp_floor.table @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: Ericsson AB +# SPDX-License-Identifier: Apache-2.0 +# +# Slice 137. The three rows of erlang/otp's otp_versions.table that fix CVE-2026-55737, verbatim, +# read by Trinity.Toolchain at compile time. Source: https://raw.githubusercontent.com/erlang/otp/499d6a4b8bf8e145f33b7b7fc8a6b11de9e7bda2/otp_versions.table +# (commit 499d6a4b8bf8e145f33b7b7fc8a6b11de9e7bda2, file SHA-256 8990c38937ba7bb91ac206e659ba5f87289a41f3d69f58ad48f1e6dff5fee197). +OTP-29.0.4 : compiler-10.0.3 crypto-5.9.2 diameter-2.7.2 erts-17.0.4 megaco-4.9.1 public_key-1.21.4 ssh-6.0.3 ssl-11.7.4 stdlib-8.0.3 # asn1-5.5 common_test-1.31.1 debugger-7.0 dialyzer-6.0.2 edoc-1.5 eldap-1.3 erl_interface-5.8.1 et-1.8 eunit-2.11 ftp-1.2.6 inets-9.7.1 jinterface-1.16 kernel-11.0.3 mnesia-4.26.1 observer-2.19 odbc-2.17 os_mon-2.12 parsetools-2.8 reltool-1.1 runtime_tools-2.4 sasl-4.4 snmp-5.20.4 syntax_tools-4.1 tftp-1.3 tools-4.2.1 wx-2.6 xmerl-2.2 : +OTP-28.5.0.4 : crypto-5.8.3.2 diameter-2.6.1.2 erts-16.4.0.4 megaco-4.8.3.1 public_key-1.20.3.4 ssh-5.5.2.3 ssl-11.6.0.4 stdlib-7.3.0.1 # asn1-5.4.3 common_test-1.30.0.1 compiler-9.0.6.1 debugger-6.0.3 dialyzer-5.4.0.1 edoc-1.4.1 eldap-1.2.16 erl_interface-5.7.0.1 et-1.7.3 eunit-2.10.3 ftp-1.2.4.1 inets-9.6.2.2 jinterface-1.15 kernel-10.6.3.3 mnesia-4.25.3.1 observer-2.18.2 odbc-2.16.1 os_mon-2.11.2 parsetools-2.7.1 reltool-1.0.3 runtime_tools-2.3.1 sasl-4.3.2 snmp-5.20.2.1 syntax_tools-4.0.3 tftp-1.2.4 tools-4.1.4 wx-2.5.4.1 xmerl-2.1.9 : +OTP-27.3.4.15 : crypto-5.5.3.4 diameter-2.4.1.3 erts-15.2.7.11 megaco-4.7.2.2 public_key-1.17.1.5 ssh-5.2.11.10 ssl-11.2.12.11 stdlib-6.2.2.4 # asn1-5.3.4.2 common_test-1.27.7.1 compiler-8.6.1.5 debugger-5.5.0.1 dialyzer-5.3.1.1 edoc-1.3.2 eldap-1.2.14.1 erl_interface-5.5.2.1 et-1.7.1 eunit-2.9.1 ftp-1.2.3.1 inets-9.3.2.6 jinterface-1.14.1 kernel-10.2.7.4 mnesia-4.23.5.3 observer-2.17 odbc-2.15 os_mon-2.10.1 parsetools-2.6 reltool-1.0.1 runtime_tools-2.1.1 sasl-4.2.2.1 snmp-5.18.2 syntax_tools-3.2.2.2 tftp-1.2.2.1 tools-4.1.1 wx-2.4.3.1 xmerl-2.1.3.3 : diff --git a/test/control_mapping_test.exs b/test/control_mapping_test.exs index c00bc75..2f0387a 100644 --- a/test/control_mapping_test.exs +++ b/test/control_mapping_test.exs @@ -312,6 +312,21 @@ defmodule Trinity.ControlMappingTest do assert CM.uncited(refusals, CM.cited_checks(rows()).funs, "regulated refusal") == [] end + # Slice 137: the boot refusals every profile makes on the runtime, and the ML-DSA-87 + # known-answer check a regulated receipts signer refuses on, are enumerated from the modules + # rather than listed, so a new check that no row cites fails here. + test "every runtime refusal and the ML-DSA-87 known-answer check" do + checks = + for {fun, arity} <- Trinity.Toolchain.__info__(:functions), + String.starts_with?(Atom.to_string(fun), "check_"), + do: "Trinity.Toolchain.#{fun}/#{arity}" + + checks = checks ++ ["Trinity.Crypto.Preflight.mldsa87/1"] + + assert length(checks) >= 3 + assert CM.uncited(checks, CM.cited_checks(rows()).funs, "runtime refusal") == [] + end + test "every census in the tree" do {out, 0} = System.cmd("git", ["ls-files", "test/*census*_test.exs"]) censuses = String.split(out, "\n", trim: true) diff --git a/test/trinity/crypto/preflight_test.exs b/test/trinity/crypto/preflight_test.exs new file mode 100644 index 0000000..ac1a898 --- /dev/null +++ b/test/trinity/crypto/preflight_test.exs @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.Crypto.PreflightTest do + @moduledoc """ + Slice 137, AC5: the ML-DSA-87 known-answer check. + + The step logic is proven against a stand-in for `:crypto`, so it holds on every leg, including + the ones where the real runtime has no ML-DSA. The real `:crypto` is exercised where the + oracle says the runtime signs (tag `:mldsa87_runtime`), and against the runtime's own answer + everywhere. + """ + # Not async: the fault seam is application environment. + use ExUnit.Case, async: false + + alias Trinity.Crypto.Preflight + alias Trinity.TestSupport.MLDSA87Oracle + + defmodule GoodCrypto do + @moduledoc false + # Answers the published cases as a correct ML-DSA-87 would, from the vectors themselves, and + # signs fresh keys with a keyed hash so that a changed message fails. + @v Trinity.Crypto.Preflight.vectors() + + def generate_key(:mldsa87, []), do: {"pub", {:seed, "priv"}} + + def generate_key(:mldsa87, [], sk) do + if sk == @v.keygen.sk, do: {@v.keygen.pk, sk}, else: {"other", sk} + end + + def sign(:mldsa87, :none, msg, {:seed, seed}), do: :crypto.hash(:sha512, [seed, msg]) + + def verify(:mldsa87, :none, msg, sig, pk) do + cond do + pk == @v.keygen.pk -> + sig == :crypto.hash(:sha512, [@v.keygen.seed, msg]) + + known = Enum.find(Map.values(@v.sigver), &(&1.sig == sig and &1.msg == msg)) -> + known == @v.sigver["valid"] + + pk == "pub" -> + sig == :crypto.hash(:sha512, ["priv", msg]) + + true -> + false + end + end + end + + defmodule RaisingKeygen do + @moduledoc false + def generate_key(:mldsa87, [], _sk), do: raise(ErlangError, original: :notsup) + end + + defmodule WrongPublicKey do + @moduledoc false + defdelegate generate_key(a, b), to: GoodCrypto + def generate_key(:mldsa87, [], sk), do: {"not the published key", sk} + defdelegate sign(a, b, c, d), to: GoodCrypto + defdelegate verify(a, b, c, d, e), to: GoodCrypto + end + + defmodule AcceptsAnything do + @moduledoc false + defdelegate generate_key(a, b), to: GoodCrypto + defdelegate generate_key(a, b, c), to: GoodCrypto + defdelegate sign(a, b, c, d), to: GoodCrypto + def verify(:mldsa87, :none, _msg, _sig, _pub), do: true + end + + defmodule RejectsEverything do + @moduledoc false + defdelegate generate_key(a, b), to: GoodCrypto + defdelegate generate_key(a, b, c), to: GoodCrypto + defdelegate sign(a, b, c, d), to: GoodCrypto + def verify(:mldsa87, :none, _msg, _sig, _pub), do: false + end + + setup do + on_exit(fn -> Application.delete_env(:trinity, :mldsa87_preflight_fault) end) + end + + describe "AC5: the step logic, against a stand-in" do + test "a provider that does all four answers :ok" do + assert Preflight.mldsa87(GoodCrypto) == :ok + assert Preflight.ok?(GoodCrypto) + end + + test "the steps run in this order, the published answers first" do + assert Preflight.steps() == [:keygen_kat, :sigver_kat, :keygen, :sign, :verify, :negative] + end + + for step <- [:keygen_kat, :sigver_kat, :keygen, :sign, :verify, :negative] do + test "a planted failure at #{step} is named, and no later step is reached" do + Application.put_env(:trinity, :mldsa87_preflight_fault, unquote(step)) + + assert Preflight.mldsa87(GoodCrypto) == + {:error, {:mldsa87_preflight_failed, unquote(step), :planted}} + end + end + + test "a runtime with no ML-DSA raises in the first step, which is a named failure, not a crash" do + assert {:error, {:mldsa87_preflight_failed, :keygen_kat, reason}} = + Preflight.mldsa87(RaisingKeygen) + + assert is_binary(reason) + end + + test "a provider that derives the wrong public key fails at :keygen_kat" do + assert {:error, {:mldsa87_preflight_failed, :keygen_kat, {:public_key_from_secret_key, _}}} = + Preflight.mldsa87(WrongPublicKey) + end + + test "a verifier that accepts Wycheproof's bit-flipped signature fails at :sigver_kat, naming the case" do + %{"invalid" => %{tc: tc}} = Preflight.vectors().sigver + + assert Preflight.mldsa87(AcceptsAnything) == + {:error, {:mldsa87_preflight_failed, :sigver_kat, {:wycheproof, tc, false, true}}} + end + + test "a verifier that refuses everything fails at :keygen_kat, before any fresh key" do + assert Preflight.mldsa87(RejectsEverything) == + {:error, + {:mldsa87_preflight_failed, :keygen_kat, + {:seed_signature_under_public_key, false}}} + end + + test "the published cases are the ones the fixtures name, with their provenance" do + acvp = File.read!("priv/kat/acvp/mldsa87_keygen.json") |> JSON.decode!() + wy = File.read!("priv/kat/wycheproof/mldsa87_verify.json") |> JSON.decode!() + assert {acvp["tgId"], acvp["tcId"], acvp["parameterSet"]} == {3, 51, "ML-DSA-87"} + assert acvp["source"] =~ "975de31eb83d87039ec88934fdc47d8c312b892d" + + assert {wy["valid"]["tcId"], wy["invalid"]["tcId"], wy["invalid_hints"]["tcId"]} == + {1, 8, 18} + + assert wy["source"] =~ "7a325932231bd7a724c791c8b0d65ea4d6d2f435" + assert byte_size(Preflight.vectors().keygen.pk) == 2592 + assert byte_size(Preflight.vectors().sigver["valid"].sig) == 4627 + end + end + + describe "the real :crypto" do + test "the preflight agrees with the independent oracle on this runtime" do + assert Preflight.ok?() == MLDSA87Oracle.signs?(), + "preflight #{inspect(Preflight.mldsa87())}, fips #{:crypto.info_fips()}" + end + + @tag :mldsa87_runtime + test "where the runtime signs, a planted failure at each step is named" do + assert Preflight.mldsa87() == :ok + + for step <- Preflight.steps() do + Application.put_env(:trinity, :mldsa87_preflight_fault, step) + assert {:error, {:mldsa87_preflight_failed, ^step, :planted}} = Preflight.mldsa87() + end + end + + test "the ML-DSA-87 signer is available exactly when the preflight passes (F-137-1)" do + assert Trinity.Receipts.Signer.MLDSA87.available?() == Preflight.ok?() + end + end +end diff --git a/test/trinity/effects/boot_receipt_test.exs b/test/trinity/effects/boot_receipt_test.exs index 9b1ec7d..753a18d 100644 --- a/test/trinity/effects/boot_receipt_test.exs +++ b/test/trinity/effects/boot_receipt_test.exs @@ -35,6 +35,30 @@ defmodule Trinity.Effects.BootReceiptTest do assert n >= 1 end + # Slice 137 AC8: the runtime the floor was checked against, and the ML-DSA-87 known-answer + # check's answer at this boot, are in the signed subject, and the chain still verifies. + test "the boot receipt records erts, Elixir and the ML-DSA-87 check, signed" do + r = Receipts.boot_receipt() + %{erts: erts, elixir: elixir} = Trinity.Toolchain.current() + + want = + case Trinity.Crypto.Preflight.mldsa87() do + :ok -> "ok" + {:error, {:mldsa87_preflight_failed, step, _}} -> "failed:#{step}" + end + + body = JSON.decode!(r.signed_payload) + + for {key, value} <- [{"erts", erts}, {"elixir", elixir}, {"mldsa87_preflight", want}] do + assert r.subject[key] == value, key + assert body["subject"][key] == value, "#{key} is not in the signed body" + end + + {:ok, export} = Receipts.export("boot") + assert {:ok, %{receipts: n}} = Receipts.Verifier.verify(export) + assert n >= 1 + end + test "the list covers the modules that decide" do for m <- [ Trinity.Permissions.Policy.Layered, diff --git a/test/trinity/toolchain_test.exs b/test/trinity/toolchain_test.exs new file mode 100644 index 0000000..53af670 --- /dev/null +++ b/test/trinity/toolchain_test.exs @@ -0,0 +1,167 @@ +# SPDX-FileCopyrightText: Sudo Apt Holdings LLC +# SPDX-License-Identifier: Apache-2.0 +defmodule Trinity.ToolchainTest do + @moduledoc """ + Slice 137, AC1 and AC4: the floor decision, at every boundary, against the rows checked in at + `priv/otp_floor.table`. The boot that applies it is `Trinity.ToolchainNodeTest`. + """ + use ExUnit.Case, async: true + use ExUnitProperties + + alias Trinity.Toolchain + + # erts at each fixing release, and the patch either side. + @erts [ + {27, "15.2.7.10", "15.2.7.11", "15.2.7.12"}, + {28, "16.4.0.3", "16.4.0.4", "16.4.0.5"}, + {29, "17.0.3", "17.0.4", "17.0.5"} + ] + + describe "the rows" do + test "are the three fixing releases, read from the checked-in table" do + assert %{ + 27 => %{release: "27.3.4.15"}, + 28 => %{release: "28.5.0.4"}, + 29 => %{release: "29.0.4"} + } = + Toolchain.floor() + end + + test "each names erts and the applications whose CVEs the release fixed" do + for {major, erts} <- [{27, "15.2.7.11"}, {28, "16.4.0.4"}, {29, "17.0.4"}] do + apps = Toolchain.floor()[major].apps + assert apps.erts == erts + + for app <- [:crypto, :public_key, :ssl, :ssh, :stdlib, :kernel], + do: assert(is_binary(apps[app]), "OTP #{major} row has no #{app}") + end + + assert Toolchain.floor()[28].apps.ssl == "11.6.0.4" + assert Toolchain.floor()[28].apps.public_key == "1.20.3.4" + end + + test "carry their source: the commit and the SHA-256 of the table they were copied from" do + text = File.read!("priv/otp_floor.table") + assert text =~ "499d6a4b8bf8e145f33b7b7fc8a6b11de9e7bda2" + assert text =~ "8990c38937ba7bb91ac206e659ba5f87289a41f3d69f58ad48f1e6dff5fee197" + end + end + + describe "AC1: check_otp/3 at each line's boundary" do + for {major, below, at, above} <- @erts do + test "OTP #{major}: erts #{below} refuses, #{at} and #{above} pass" do + major = unquote(major) + release = Toolchain.floor()[major].release + + assert Toolchain.check_otp(major, unquote(below)) == + {:error, + {:otp_below_floor, major, release, [{:erts, unquote(below), unquote(at)}]}} + + assert Toolchain.check_otp(major, unquote(at)) == :ok + assert Toolchain.check_otp(major, unquote(above)) == :ok + end + end + + test "erts at the floor with an application below it refuses, naming the application" do + apps = %{ssl: "11.6.0.3", crypto: "5.8.3.2", kernel: "10.6.3.3"} + + assert Toolchain.check_otp(28, "16.4.0.4", apps) == + {:error, {:otp_below_floor, 28, "28.5.0.4", [{:ssl, "11.6.0.3", "11.6.0.4"}]}} + end + + test "every component below the floor is listed, erts first" do + apps = %{stdlib: "7.3", ssl: "11.6.0.3", crypto: "5.8.3.1"} + + assert {:error, {:otp_below_floor, 28, _, [{:erts, "16.4.0.3", _} | rest]}} = + Toolchain.check_otp(28, "16.4.0.3", apps) + + assert Enum.map(rest, &elem(&1, 0)) == [:crypto, :ssl, :stdlib] + end + + test "applications the row does not name are not judged" do + assert Toolchain.check_otp(28, "16.4.0.6", %{ + trinity: "0.1.0", + elixir: "1.20.4", + logger: "1.20.4" + }) == + :ok + end + + test "the pinned runtimes' erts passes: 16.4.0.6 is both 28.5.0.6 and 28.5.0.7" do + assert Toolchain.check_otp(28, "16.4.0.6") == :ok + end + + test "a runtime from before the patch line refuses: OTP 28.1.1 is erts 16.1.1" do + assert {:error, {:otp_below_floor, 28, "28.5.0.4", [{:erts, "16.1.1", "16.4.0.4"}]}} = + Toolchain.check_otp(28, "16.1.1") + end + + test "a two-digit patch sorts as a number, not as text; OTP 29.1's erts 17.1 passes" do + assert Toolchain.check_otp(28, "16.4.0.10") == :ok + assert Toolchain.check_otp(29, "17.1") == :ok + end + + test "a shorter version is below a longer one with the same prefix" do + assert {:error, _} = Toolchain.check_otp(28, "16.4") + assert {:error, _} = Toolchain.check_otp(28, "16.4.0") + end + + test "an unusual component does not raise" do + assert Toolchain.at_least?("16.4.0.4-rc1", "16.4.0.4") == true + assert Toolchain.at_least?("x", "1") == false + end + + test "majors below the first row refuse; majors above the last pass" do + assert Toolchain.check_otp(26, "14.2.5.12") == + {:error, {:otp_major_unsupported, 26, "14.2.5.12"}} + + assert Toolchain.check_otp(30, "18.0") == :ok + end + + property "agrees with a reference comparison over generated erts versions" do + check all( + major <- member_of([27, 28, 29]), + erts <- list_of(integer(0..40), min_length: 1, max_length: 5) + ) do + version = Enum.join(erts, ".") + + floor = + Toolchain.floor()[major].apps.erts + |> String.split(".") + |> Enum.map(&String.to_integer/1) + + want = if erts >= floor, do: :ok, else: :error + + got = + case Toolchain.check_otp(major, version) do + :ok -> :ok + {:error, {:otp_below_floor, ^major, _, [{:erts, ^version, _}]}} -> :error + end + + assert got == want, "#{major} #{version}" + end + end + end + + describe "AC4: check_elixir/1" do + test "1.20.x passes; 1.19.5, a 1.20 release candidate and 1.21.0 refuse" do + assert Toolchain.check_elixir("1.20.0") == :ok + assert Toolchain.check_elixir("1.20.4") == :ok + + for v <- ["1.19.5", "1.20.0-rc.1", "1.21.0"], + do: + assert(Toolchain.check_elixir(v) == {:error, {:elixir_outside_floor, v, "~> 1.20.0"}}) + end + end + + describe "the runtime this suite is on" do + test "current/0 reads the running VM, and check/0 passes on the pin" do + %{otp_release: release, erts: erts, elixir: elixir, apps: apps} = Toolchain.current() + assert release == List.to_string(:erlang.system_info(:otp_release)) + assert erts == List.to_string(:erlang.system_info(:version)) + assert elixir == System.version() + assert apps[:ssl] == to_string(Application.spec(:ssl, :vsn)) + assert Toolchain.check() == :ok + end + end +end diff --git a/test/trinity_web/live/receipts_live_test.exs b/test/trinity_web/live/receipts_live_test.exs index 0bc3344..652e243 100644 --- a/test/trinity_web/live/receipts_live_test.exs +++ b/test/trinity_web/live/receipts_live_test.exs @@ -71,6 +71,9 @@ defmodule TrinityWeb.ReceiptsLiveTest do assert html =~ boot.subject["signer"]["key_id"] assert html =~ boot.meta["core_policy_hash"] assert html =~ boot.receipt_hash + # Slice 137 AC8: the runtime and the ML-DSA-87 check are on the page. + assert html =~ boot.subject["erts"] + assert html =~ boot.subject["mldsa87_preflight"] html = view |> element("#verify") |> render_click() assert html =~ "verified: " assert html =~ "exit 0" From e7865a412ad82f04823ec991781bb0fd930f5bb8 Mon Sep 17 00:00:00 2001 From: Ayla Croft Date: Mon, 19 Oct 2026 19:04:56 -0400 Subject: [PATCH 3/3] fix(s137): the mode-off ML-DSA-87 step runs last, from a fresh suite state, so the gate in FIPS mode inherits no keys or rows Signed-off-by: Ayla Croft --- .github/workflows/gate.yml | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index ece572c..f19b18e 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -366,24 +366,32 @@ jobs: exit 1 fi + - run: mix gate + + # AC2 and AC3 by test name with this run's id in the log, and AC5's coverage line for + # this leg. The gate above already ran the suite; this repeats the four FIPS tests with + # their names printed, then the whole suite once more under the coverage tool. + - name: The FIPS tests by name, and this leg's coverage + run: | + mix test --trace test/fips + mix test --cover | grep -E 'Result:|\| *Total' + # Slice 137, AC5 and AC7: ML-DSA-87 with the mode off. This image links OpenSSL 3.5, the only # runtime in CI that signs ML-DSA-87 (the gate runners link 3.0; in the mode, Red Hat's # provider refuses it), so the ML-DSA-87 path runs here end to end: the planted-step cases, # a regulated node that boots with ML-DSA-87 receipts and whose boot receipt verifies, and the # signer's key shapes. TRINITY_REQUIRE_MLDSA87_RUNTIME makes test_helper raise if the runtime # cannot sign, rather than excluding the cases it came to run. + # + # It runs last, from a fresh suite state. The suite keeps its receipt keys in tmp/test_keys + # and its receipts in trinity_test_receipts.db across runs, so a run with the mode off leaves + # Ed25519 and ML-DSA-87 keys and rows that the mode refuses to verify. Run before `mix gate`, + # it turned five of the gate's tests red on PR #132 (`:notsup`, "Unsupported algorithm in + # FIPS mode"; three receipt keys in one export). Nothing in the mode runs after this step. - name: ML-DSA-87 end to end (FIPS mode off) env: ERL_AFLAGS: "-eval application:load(crypto)" TRINITY_REQUIRE_MLDSA87_RUNTIME: "1" - run: mix test test/trinity/crypto/preflight_test.exs test/trinity/toolchain_node_test.exs test/trinity/receipts/signer_test.exs - - - run: mix gate - - # AC2 and AC3 by test name with this run's id in the log, and AC5's coverage line for - # this leg. The gate above already ran the suite; this repeats the four FIPS tests with - # their names printed, then the whole suite once more under the coverage tool. - - name: The FIPS tests by name, and this leg's coverage run: | - mix test --trace test/fips - mix test --cover | grep -E 'Result:|\| *Total' + rm -rf tmp trinity_test.db* trinity_test_receipts.db* + mix test test/trinity/crypto/preflight_test.exs test/trinity/toolchain_node_test.exs test/trinity/receipts/signer_test.exs