Skip to content

Add receipt signing (schema 0.0.14) - #22

Merged
joshft merged 1 commit into
masterfrom
receipt-signing
Aug 18, 2026
Merged

Add receipt signing (schema 0.0.14)#22
joshft merged 1 commit into
masterfrom
receipt-signing

Conversation

@joshft

@joshft joshft commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Makes the word "authenticated" in the receipt definition true: an Ed25519 signature over the domain-separated canonical payload, with the trust model reviewed adversarially before a line was written.

The review's CRITICAL finding shaped the design. The draft put -sign on the main command. That command builds and executes the change's own test code — any key it can read, reviewed code can read. The shipped design has no signing flag there at all: correctful sign runs no probes and reads no tree, so CI can mount the key for that step alone.

What ships:

  • keygen / sign / verify / render subcommands. verify demands the expected head SHA (a valid signature over SOME receipt is worthless to a gate), takes trust only from the caller's pinned key, and binds an audience inside the preimage so a shared CI key cannot confuse repositories.
  • Exactly one byte-form of a signed receipt verifies: the canonical encoding, frozen by golden-vector test. Strict parsing (unknown fields, duplicate keys, trailing bytes, invalid UTF-8) shared with intake via the new internal/strictjson — which also fixes a live bug the review found in merged code: the old Decoder.More EOF check let a stray closing delimiter pass as end-of-input.
  • Consistency validation on both sides of the trust boundary: Sign refuses an internally inconsistent receipt, and Verify re-derives every computable field, so the tampered-summary attack (one refuted result, Summary.Refuted zeroed, gate reads the summary) fails even when a bypassing signer minted the signature.
  • The input digest now pins each file's kind and never follows symlinks — an exec-bit flip or a file-to-symlink swap used to change probe behavior under an unchanged digest.
  • Test evidence includes an every-byte mutation sweep of a signed artifact, an RFC 8032 vector, and keygen atomicity (no partial pair, planted-symlink refusal).

Deferred with stated rationale in DESIGN.md: freshness/replay policy (the verifier's CI owns freshness) and receipt chaining (its own backlog item — until then these are authenticated individual records, and the docs avoid the word "chain").

🤖 Generated with Claude Code

https://claude.ai/code/session_015GeUG1gboWiZSnFyzQghyp

The adopted definition calls a receipt "an authenticated record"; until
now nothing made it authentic. The design went through an adversarial
external review BEFORE implementation, and the review's CRITICAL finding
shaped the shipped shape: the probe-running command has NO signing flag,
because it executes the change's own test code and any key it can read,
reviewed code can read. Signing is a separate `sign` subcommand that runs
no probes and reads no tree; CI mounts the key only for that step.

- schema 0.0.14: SignatureBlock (alg/public_key/audience/sig)
- internal/signing: sign, verify (canonical byte-identity, mandatory
  subject match, pinned-key-only trust, audience bound in the preimage),
  PEM keygen/load with atomic exclusive creation
- internal/strictjson: the strict decode contract shared with intake —
  also fixes the Decoder.More EOF gap the review found in merged code
  (a stray closing delimiter passed as end-of-input)
- receipt.Canonical + ValidateConsistency: one byte-form per receipt
  (golden-vector frozen) and re-derivation of every computable field on
  both sides, so a tampered-summary receipt cannot be signed or verified
- gitdiff.InputDigest now pins file kind (regular/exec/symlink) and never
  follows symlinks: an exec-bit flip or file-to-symlink swap changed
  probe behavior under an unchanged digest
- render subcommand: PR markdown from the signed JSON without a second
  probe run; renderings state their signature is UNVERIFIED HERE
- deferred with stated rationale: freshness/replay policy (verifier's CI
  owns it), receipt chaining (own backlog item)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GeUG1gboWiZSnFyzQghyp
@github-actions

Copy link
Copy Markdown

correctful receipt

30 claims — ✅ 30 verified · ❌ 0 refuted · ⚠️ 0 unverified

Change: origin/master...HEAD (3b8756f..3f1c4de · input:29cd9ae0c3ea) — 19 files

⚠️ Unverified remainder (0) — what nothing checked

Empty — every harvested claim reached a probe.

✅ Verified (30)
Tier Claim Statement
T1-assertion TestDetectBaseFallsBackToLocalDefaultBranch Detect Base Falls Back To Local Default Branch
T1-assertion TestDetectBaseHonorsCIBaseRef Detect Base Honors CIBase Ref
T1-assertion TestDetectBaseFailsLoudWithNoCandidate Detect Base Fails Loud With No Candidate
T1-assertion TestTrackedByPattern Tracked By Pattern
T1-assertion TestPatchCarriesCommittedAndUncommittedHunks Patch Carries Committed And Uncommitted Hunks
T1-assertion TestResolveIncludesUntrackedButNotHiddenState Resolve Includes Untracked But Not Hidden State
T1-assertion TestInputDigestPinsWorkingTreeContent Input Digest Pins Working Tree Content
T1-assertion TestInputDigestPinsKindNotJustContent Input Digest Pins Kind Not Just Content
T1-assertion TestValidateConsistencyAcceptsAssembledReceipt Validate Consistency Accepts Assembled Receipt
T1-assertion TestValidateConsistencyRejectsTampering Validate Consistency Rejects Tampering
T1-assertion TestCanonicalGoldenVector Canonical Golden Vector
T1-assertion TestKeygenAndLoadRoundTrip Keygen And Load Round Trip
T1-assertion TestKeygenRefusesExistingAndLeavesNoPartialPair Keygen Refuses Existing And Leaves No Partial Pair
T1-assertion TestKeygenRefusesPlantedSymlink Keygen Refuses Planted Symlink
T1-assertion TestLoadKeyRejections Load Key Rejections
T1-assertion TestSignVerifyRoundTrip Sign Verify Round Trip
T1-assertion TestVerifyWrongKeyFails Verify Wrong Key Fails
T1-assertion TestVerifyAudienceBinding Verify Audience Binding
T1-assertion TestVerifySubjectMatching Verify Subject Matching
T1-assertion TestVerifyRejectsNonCanonicalArtifact Verify Rejects Non Canonical Artifact
T1-assertion TestVerifyRejectsEveryByteMutation Verify Rejects Every Byte Mutation
T1-assertion TestSignRefusesInconsistentReceipt Sign Refuses Inconsistent Receipt
T1-assertion TestVerifyRejectsSignedInconsistency Verify Rejects Signed Inconsistency
T1-assertion TestSignRefusesAlreadySigned Sign Refuses Already Signed
T1-assertion TestSignRejectsControlAudience Sign Rejects Control Audience
T1-assertion TestVerifyRejectsMalleableBase64 Verify Rejects Malleable Base64
T1-assertion TestGoldenSignature Golden Signature
T1-assertion TestRFC8032Vector RFC8032 Vector
T1-assertion TestCleanDocumentDecodes Clean Document Decodes
T1-assertion TestStrictRejections Strict Rejections

Harvest coverage: 19 files — 6 claimed · 13 scanned · 0 unread
3 spec-id mention(s) not minted as claims — the repo defines no spec-id corpus, so a reference has no possible referent

schema 0.0.14 · correctful v0.0.0-20260818031256-3f1c4dee112d 3f1c4de · exit gate: refuted claims block; the remainder informs, never fails

@joshft
joshft merged commit f48cbc3 into master Aug 18, 2026
1 check passed
@joshft
joshft deleted the receipt-signing branch August 18, 2026 03:14
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