Skip to content

feat(s140): receipts v4, N6 statements in COSE_Sign1 - #138

Merged
HackTuah merged 1 commit into
mainfrom
slice/140-receipts-v4
Oct 20, 2026
Merged

HackTuah merged 1 commit into
mainfrom
slice/140-receipts-v4

Conversation

@HackTuah

Copy link
Copy Markdown
Member

Slice 140. New receipts and checkpoints are N6 statements: deterministic CBOR inside COSE_Sign1, in the single-signer carrier, signed with ML-DSA-87 (-50), Ed25519 (-19) or, in FIPS mode, ECDSA P-384 as ESP384 (-51). Receipts written before keep their DSSE form and still verify, and a chain that began in v3 continues in v4.

What it does

  • Receipts v4 (Trinity.Receipts.V4).
    • Six kinds (decision, effect, query, boot, cap, checkpoint), each with one typ and one schema number, provisional, in Trinity's block 0x83.
    • The body signs what a verifier judges: issuer, predecessor, seq, scope, time and clock.
    • subject and decision are referenced by SHA-384 over their RFC 8785 bytes, which are stored beside the row and hashed as stored. No receipt carries a tool call's arguments or its result; those enter as the approval fingerprint and a digest.
  • Linking by statement_id, the same whether or not the row is signed. The first v4 row after a v3 row links to SHA-384 over that row's PAE bytes.
  • ESP384 (Trinity.N6.EC2).
    • The signature is the 96-byte r || s and nothing else. s and n - s both verify.
    • The key must be an uncompressed point on P-384.
    • Its kid is the RFC 9679 thumbprint over 107 bytes, each coordinate padded to 48.
    • It is refused at the nss entry point.
  • The profile layer follows the publisher's latest answers.
    • A typ no longer names an algorithm, as slice 139 had it: the key's registration does, and a header naming another algorithm is alg_not_permitted.
    • A body under another kind's typ is typ_schema_mismatch; a signed body without an issuer is issuer_missing.
    • Schema refusals come in the order schema_reserved, test_schema_outside_test_mode, unknown_schema.
    • The single-signer carrier's schema number is now assigned rather than a placeholder.
  • A receipts profile setting, config :trinity, :receipts, profile: :commercial | :nss. Under :nss, any signer but ML-DSA-87 is refused at selection: a regulated boot then refuses to start, and a default one denies every effect.
  • Every verifier reads mixed chains: the in-app verifier, the verify task, the receipts page, the merge and the writer's rehydrate check. A missing or altered attachment is reported as a finding and never changes the verdict.
  • bin/verify_receipt.exs loads the N6 sources and Trinity.Receipts.V4 from Trinity's lib/ (or --n6) when it meets a v4 row, and runs the same rules. A v2 or v3 chain still needs nothing but OTP. The script loads the crypto application first, because OTP 28.5.0.8 otherwise prints a warning into its output.
  • Storage: a receipts migration adds nullable binary columns (body, carrier, subject_bytes, decision_bytes; body and carrier on checkpoints). No v2 or v3 row is touched.
  • Vectors: the candidate set gains the ESP384 samples, the P-384 thumbprint (including a key whose x begins with a zero byte) and the new refusal codes. Slice 139's two alg_not_bound_to_typ header vectors are withdrawn, and its Ed25519 samples now carry an issuer.

Evidence

  • Red first. On main before the change, a new row is receipt_v3_ed25519; on this branch it is v4, with its body and carrier stored.
  • Each algorithm, on the runtime that has it. The gate's runtime writes and verifies -19 and verifies -51. The fips job checks -51 rows in the mode. The ML-DSA-87 step writes -50 rows with the nss profile and verifies them at nss, run locally on OTP 28.5.0.8 with OpenSSL 3.5.9.
  • The thumbprint is checked against sha384sum run outside Elixir.
  • n - s verified 200 of 200 times on OpenSSL 3.0.13 and on 3.5.9.
  • A chain of three v3 rows and three v4 rows, with a v4 checkpoint over a v3 query row, verifies, merges as identical and rehydrates twice. A gap, a reorder, a wrong link and a checkpoint naming another tail are each refused at their position.
  • The standalone script and the in-app verifier give the same exit on every variant.
  • PostgreSQL: the receipts, N6 and effects suites pass locally (197 tests).

CI

  • The fips job's mode-off ML-DSA-87 step now also runs test/trinity/receipts/v4_test.exs and test/trinity/n6/esp384_test.exs.
  • Locally, the gate passed every check but one: the known Lua sandbox timeout under full-suite load. That test file passes alone.

Not in this PR

  • Submitting receipts to a per-node log, whose append interface does not exist yet.
  • The twin envelope, the note form, the issuer key descriptor chain, and test-key mode.
  • Rewriting any v2 or v3 row.
  • MCP envelopes, which keep DSSE until their own slice.

…gle-signer carrier, ESP384 in FIPS mode, statement_id links, subject and decision by digest; v2 and v3 still verify)

Signed-off-by: Ayla Croft <aylacroft@proton.me>
@HackTuah
HackTuah merged commit b9f2290 into main Oct 20, 2026
12 checks passed
@HackTuah
HackTuah deleted the slice/140-receipts-v4 branch October 20, 2026 19:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant