defmodule VavPipeline.LedgerTest do @moduledoc "The provenance ledger: canonical form, signature, key digest (2026-10-21)." use ExUnit.Case, async: true alias VavPipeline.Ledger @seed :crypto.strong_rand_bytes(32) defp record do %VavPipeline.L3.BedikahRecord{ id: 7, bedikah_ref: "bedikah-0123456789abcdef", provenance: "claude", query: "q", output_sha256: String.duplicate("ab", 32), output_length: 12, vital_content: "", propositions: [], references: ["Devarim 6:4"], authority_claims: [], actions: [], sources: [%{"id" => "system", "sha256" => "00", "length" => 2}], claims: [%{"index" => 1, "sentence" => "x", "anchors" => []}], structure: %{"self_validation" => []}, frames: [], rebukes: nil, mesorah: [], coupling_trace: [], violations: [], verdict: "tov", examined_at: ~U[2026-10-21 12:00:00Z], key_id: "deadbeef", inserted_at: ~U[2026-10-21 12:00:01Z] } end test "key_id is a digest of the key, never the key, and nil for none" do assert Ledger.key_id(nil) == nil id = Ledger.key_id("courtyard-secret-key") assert String.length(id) == 32 refute id =~ "courtyard" assert id == Ledger.key_id("courtyard-secret-key") refute id == Ledger.key_id("another") end test "the entry carries the examination's fields and neither the database's nor the owner's" do e = Ledger.entry(record()) assert Map.keys(e) |> Enum.sort() == Enum.sort(Ledger.ledger_fields()) refute Map.has_key?(e, :id) refute Map.has_key?(e, :key_id) refute Map.has_key?(e, :inserted_at) assert e.examined_at == "2026-10-21T12:00:00Z" end test "canonical form sorts keys at every level and carries no whitespace" do a = Ledger.canonical(%{"b" => 1, "a" => %{"z" => [%{"y" => 2, "x" => 1}], "m" => "s"}}) assert a == ~s({"a":{"m":"s","z":[{"x":1,"y":2}]},"b":1}) # the same map with keys given in another order canonicalizes the same assert a == Ledger.canonical(%{"a" => %{"m" => "s", "z" => [%{"x" => 1, "y" => 2}]}, "b" => 1}) # atom and string keys meet in the same place assert Ledger.canonical(%{b: 1, a: 2}) == ~s({"a":2,"b":1}) end test "a signature verifies with the public key and fails on one changed byte" do bytes = Ledger.canonical(Ledger.entry(record())) sig = Ledger.sign(bytes, @seed) pub = Ledger.public_key(@seed) assert Ledger.verify(bytes, sig, pub) refute Ledger.verify(bytes <> " ", sig, pub) refute Ledger.verify(bytes, sig, Ledger.public_key(:crypto.strong_rand_bytes(32))) refute Ledger.verify(bytes, "not base64!", pub) end test "without a seed nothing is signed, and the rendering says so" do assert Ledger.sign("x", nil) == nil assert Ledger.public_key(nil) == nil r = Ledger.render(record()) assert r.entry.bedikah_ref == "bedikah-0123456789abcdef" assert String.length(r.canonical_sha256) == 64 # the test env has no signing key configured assert r.signed == false and r.signature == nil end test "a rendering with a seed configured is signed and self-consistent" do Application.put_env(:vav_pipeline, :ledger_signing_key, Base.encode64(@seed)) try do r = Ledger.render(record()) assert r.signed bytes = Ledger.canonical(r.entry) assert :crypto.hash(:sha256, bytes) |> Base.encode16(case: :lower) == r.canonical_sha256 assert Ledger.verify(bytes, r.signature, Ledger.public_key()) after Application.delete_env(:vav_pipeline, :ledger_signing_key) end end end