the ledger: a caller's own bedikot, read back and signed
The examination is the product; the record of it is what a caller can carry away. Every bedikah was already engraved in L3 — output hash and length, source digests, coupling trace, claims, violations, verdict, never the text — and no caller could get theirs back. - bedikot.key_id (migration): a SHA-256 digest of the courtyard key that brought the offering, never the key; threaded as :key_id through Bedikah.examine from every site that goes through the gate (courtyard bedikah, pair, the gate's pass and stream). Unique index on bedikah_ref. - VavPipeline.Ledger: entry (the examination's fields, none of the database's, not the owner's), canonical JSON (keys sorted at every level, no whitespace, UTF-8), Ed25519 sign/verify, public key, render. Seed from BIKORET_LEDGER_SIGNING_KEY (base64 of 32 bytes) in shorashim-secrets; absent → unsigned and says so. - GET /courtyard/ledger?limit&before — this key's bedikot, newest first, each rendered and signed; GET /courtyard/bedikah/<ref> — one, 404 for another key's or none alike; GET /peah/ledger_key — the public key. - Tamid §1.1 "The ledger" (Doorpost pin moved → 02ddf313…), KOHEN "Issuing a key" (the seed, once), secrets.yaml template. Tests: ledger_test (digest, entry, canonical, sign/verify, unsigned rendering, signed rendering); ledger_l3_test (own records listed newest first and only own; 404 alike for another's and none; an examination engraved under the key reads back signed and verifies against /peah/ledger_key). Elixir 642/0; L3 13/0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
e6c3702a22
commit
d6cf9355fd
13 files changed
+542
-13
No files matched your search
@@ -0,0 +1,97 @@
|
||||
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
|
||||
Reference in new issue
Block a user