Skip to content

Close nine adversarial-verification holes in signing and intake - #23

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

Close nine adversarial-verification holes in signing and intake#23
joshft merged 1 commit into
masterfrom
signing-hardening

Conversation

@joshft

@joshft joshft commented Aug 18, 2026

Copy link
Copy Markdown
Owner

After #22 merged, the reviewer that shaped the signing design attacked the implementation with live fixtures and confirmed nine holes — each reproduced, not argued. All nine are closed here with regression tests.

The highest-value finding was not on the signing path at all: a case-variant intake key ({"outcome":"counterexample","Outcome":"verified"}) slipped the exact-match duplicate detector, and Go's case-insensitive struct-field matching then read the second key — turning a counterexample into a pass on every intake run, signed or not. The strict decoder now rejects any two keys in one object that are equal under case folding but not byte-equal.

# Severity Hole Fix
1 CRITICAL case-variant intake key launders a counterexample reject case-fold key collisions in strictjson
2 HIGH head-only -gate accepts the wrong diff -gate requires -base
3 HIGH evidence can verify a renamed claim claim_id must match its claim; coverage file set must equal change file set
4 HIGH negative intake count slips the required gate reject negative counts; gate blocks on Accepted < 1
5 MEDIUM renderers inject terminal/Markdown control scrubForDisplay strips control runes + Markdown structure in both renderers
6 MEDIUM integer above 2⁵³ reads differently cross-parser counts bounded to the exact-integer range
7 MEDIUM invalid tier 99 is signable tiers validated in T0..T4 before weighing
8 LOW audience admits C1 controls unicode.IsControl
9 LOW keygen follows a symlinked -out dir open dir O_NOFOLLOW, create via openat

Findings 1, 3, 4, 6, 7 share one lesson: a signature authenticates bytes, and byte-authenticity is worthless if the bytes are internally incoherent. The consistency validator is the load-bearing companion to the signature and now re-derives and range-checks every field a reader or a gate trusts, not just the summary arithmetic.

Every attack was reproduced against the fixed binary (all now fail loudly) and the honest sign→verify→gate happy path still passes end to end.

🤖 Generated with Claude Code

https://claude.ai/code/session_015GeUG1gboWiZSnFyzQghyp

The same reviewer that shaped the signing design attacked the merged
implementation with live fixtures and confirmed nine holes. All nine are
closed with regression tests. The highest-value one was not on the
signing path: a case-variant intake key (outcome + Outcome) slipped the
exact-match duplicate check, and Go's case-insensitive field matching
read the second — turning a counterexample into a pass on every intake
run. The strict decoder now rejects case-fold key collisions.

- strictjson: reject case-fold key collisions (CRITICAL, live intake path)
- verify -gate now requires -base: one head commit has many diffs
- ValidateConsistency completed: evidence claim_id must match its claim,
  coverage file set must equal the change file set, tiers bounded to
  T0..T4, all counts non-negative and within the exact-integer range,
  negative intake counts rejected; GateBlocked blocks on Accepted < 1
- both renderers scrub every string field of control runes and strip
  Markdown-structural characters (scrubForDisplay) — a rendered receipt
  can no longer inject a terminal escape or a forged heading
- checkAudience uses unicode.IsControl (C1 range, not ASCII only)
- keygen opens the -out dir with O_NOFOLLOW and creates files via openat,
  so a symlinked output directory cannot redirect the write

DESIGN.md records all nine and the lesson: a signature authenticates
bytes, and byte-authenticity is worthless if the bytes are internally
incoherent — the consistency validator is the load-bearing companion to
the signature.

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

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

Change: origin/master...HEAD (f48cbc3..e37ce06 · input:5415bde72b6b) — 17 files

⚠️ Unverified remainder (0) — what nothing checked

Empty — every harvested claim reached a probe.

✅ Verified (33)
Tier Claim Statement
T1-assertion TestContradictoryVerdictsFailLoudly Contradictory Verdicts Fail Loudly
T1-assertion TestCrossSupplierCounterexampleSurvives Cross Supplier Counterexample Survives
T1-assertion TestDuplicateJSONKeysRejected Duplicate JSONKeys Rejected
T1-assertion TestParentSymlinkCannotSmuggleInTreeFiles Parent Symlink Cannot Smuggle In Tree Files
T1-assertion TestRequiredNeedsUsableEvidence Required Needs Usable Evidence
T1-assertion TestRejectedRowsAreScrubbed Rejected Rows Are Scrubbed
T1-assertion TestCaseVariantOutcomeKeyRejected Case Variant Outcome Key Rejected
T1-assertion TestRenderersStripControlRunes Renderers Strip Control Runes
T1-assertion TestMarkdownRejectsInjectedHeading Markdown Rejects Injected Heading
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 TestKeygenRefusesSymlinkedParentDir Keygen Refuses Symlinked Parent Dir
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 TestSignRejectsC1Audience Sign Rejects C1 Audience
T1-assertion TestCleanDocumentDecodes Clean Document Decodes
T1-assertion TestStrictRejections Strict Rejections

Harvest coverage: 17 files — 7 claimed · 10 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-20260818034916-e37ce06b7125 e37ce06 · exit gate: refuted claims block; the remainder informs, never fails

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