Skip to content

D1: owner-self-signed offline receipts — seal with the install key, one verifier, three evidence levels - #681

Merged
DealAppSeo merged 7 commits into
mainfrom
claude/run-signed-receipt
Nov 8, 2026
Merged

DealAppSeo merged 7 commits into
mainfrom
claude/run-signed-receipt

Conversation

@DealAppSeo

@DealAppSeo DealAppSeo commented Nov 7, 2026 •

Copy link
Copy Markdown
Owner

D1 — owner-self-signed offline receipts

A skeptic should be able to run the whole loop with nothing but this repo and a shell: emit a receipt, then verify it — no network, no engine, no account. This PR adds the owner's half of that loop and wires it to the verifier that already ships.

echo '<receipt-body.json>' | trustshell sign-receipt -   # seal with THIS install's key
trustshell verify-receipt sealed.json --root 0x<pin>     # re-derive its integrity, offline

One wire format, one verifier

signOwnerReceipt emits the engine's exact kernel-receipt/2 bytes — same canonical JSON, same Ed25519 signature domain, same keccak256 receipt hash — so the existing verifyReceiptOffline reproduces its integrity unchanged. No second verifier, no second format. The seam converts this install's own Ed25519 key (the one init already creates) into the engine's 0x-hex signer shape.

Three evidence levels that never collapse

evidenceLevel keeps an owner's signature honest about what it proves:

level means earns production RepID
OWNER_SELF_SIGNED authorship + wholeness only never (repid_eligible: 'no')
INDEPENDENTLY_VERIFIED engine key + authenticated envelope + ≥1 VERIFIED claim engine's call
WHOLE_BUT_UNSEALED / NOT_ESTABLISHED hash re-derived but no trusted root / nothing no
  • A missing trust root does not silently become VERIFIED — it stays whole-but-unsealed (NOT_CHECKED, never exit 0).
  • A foreign root → NOT_AUTHENTIC, integrity FAILED.
  • Provenance is checked before any engine-claim promotion, so a smuggled authenticated envelope can't launder a self-signature into RepID.

Adversarial coverage

tests/owner-receipt.test.ts (12) proves the guards bite: canonical serialization, domain separation, tampering, wrong key, absent root, replay, key rotation, mismatched execution evidence, expired authorization, and the RepID-laundering attempt above. tests/cli-sign-receipt.test.ts (4) proves the CLI round trip at the real process boundary (ephemeral TRUSTSHELL_HOME, tamper → VETO). The stranger walk now runs sign-receipt → verify-receipt end to end.

Surface

  • src/lib/owner-receipt.ts (new) — the seam.
  • src/verify/index.ts — re-exports on the /verify subpath; the import-boundary guard still holds (node:crypto only, no next/react/supabase, no network).
  • src/cli/index.ts — trustshell sign-receipt <file|-> (exit 0 sealed / 2 bad body / 3 could not seal).
  • docs/api-reference.md, tests/e2e-stranger.test.ts — the two Command-union gates.

CI note

The SDK/verify/CLI suites and both Command-union gates are green (owner-receipt 12, cli-sign-receipt 4, e2e-stranger + docs pass). Six suites fail on this branch and equally on main — all assert on files this PR does not touch: kernel-client (schema-file sha256 pin), the two .github workflow tests (receipts-workflow, publish-trusted-publishing), measure-home-samples (npm-script/workflow), and two Windows-only path bugs in the tests themselves (stranger-rail joins two absolute paths; scrub-battery splits C:\Program Files\… on whitespace). None reference this PR's command, files, or the Command union.

Draft: owner review before merge (this is the engine-signed path's offline sibling, not a replacement).

🤖 Generated with Claude Code


Also in this branch — the rest of the offline loop

npm run skeptic (D5) — a runnable skeptic's challenge: seal a receipt with a throwaway install key, then attack it with the real sign-receipt/verify-receipt (forge, tamper, transplant a signature, wrong key, expired authz, self-promotion). Each PASSES only when the verifier refuses. Writes a report (docs/proof-pack/skeptic-receipt-<date>.{json,md}) naming the commit, version, inputs, outputs and the honest limitation (offline verify is stateless — replay is the engine's receipt_id/nullifier). Ran PASS at 253ed5a6 (all 9).

trustshell demo (D4) — the whole loop in one command: seal a sample receipt with this install's key and verify it inline under that key, offline, printing the six-claim verdict and the reproduce line. --json returns { receipt, verify, root }. No disk write, no network.

docs/QUICKSTART.md (D4) — 10 steps from npx … demo to "what a pass does NOT prove", strictly operational (commands + exit codes + honest limits), no new claims.

CI (GitHub, Linux) green. The six locally-failing suites are Windows path bugs + .github/schema drift that this PR does not touch and main shares.

DealAppSeo and others added 3 commits November 7, 2026 00:54
…all key, one verifier, three evidence levels (#679 D1)

The engine signs kernel-receipt/2 with its production key; this SDK only ever VERIFIED those. This adds
the owner's side: an agent's accountable owner seals a receipt with their OWN install key
(bundle-signing-key.pem) so a stranger reproduces its integrity OFFLINE with the SAME verifier
(verifyReceiptOffline) — identical wire format (Ed25519 over DOMAIN + canonical bytes, keccak256
receipt_hash, 0x-hex signer/signature). One format, one verifier; the engine-signed path is untouched.

THREE EVIDENCE LEVELS, never collapsed to one:
  - every owner receipt carries header.provenance='owner-self-signed', covered by BOTH the signature
    and the hash, so it can't be mistaken for an engine receipt and can't be flipped without breaking both;
  - evidenceLevel() reports OWNER_SELF_SIGNED (authorship + wholeness ONLY) and never promotes a
    self-signature to INDEPENDENTLY_VERIFIED, and never reports any receipt RepID-eligible — whether a
    receipt earns production RepID is the ENGINE's call against its own pinned key, never an offline verdict;
  - a missing/foreign trust root never silently passes: no pin => WHOLE_BUT_UNSEALED (sealer.trusted=false),
    a foreign pin => integrity FAILED. "I can't check who sealed it" is never "the owner sealed it".

src/lib/owner-receipt.ts: signOwnerReceipt, installKeySigner/installKeyRootHex (the install key in the
engine's 0x-hex encoding), ownerReceiptProvenance, evidenceLevel. Re-exported from the
@hyperdag/trustshell/verify subpath (node:crypto only; the import-boundary guard still refuses
next/react/supabase and any network call).

tests/owner-receipt.test.ts (12, all pass) proves the guards bite, not just the happy path: canonical
serialization (key order is immaterial), domain separation (the tag is load-bearing), tampering, a
foreign key, an absent root, a transplanted signature (replay), key rotation (old/new/both pinned),
a smuggled six-claim envelope (can't make a self-signature independently-verified), and an expired policy.

Offline on a BARE install (no ethers) additionally needs the @noble keccak path from the demo/#679 arc;
with ethers present (as in CI) this is fully offline (no network) today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ipt from the shell, so emit→verify is one pipe (#679 D1)

Completes the owner-receipt seam at the command line: reads a kernel-receipt body (a file, or - for
stdin), seals it with THIS install's own key (TRUSTSHELL_HOME/bundle-signing-key.pem) via
signOwnerReceipt, prints the sealed kernel-receipt/2 to stdout and, on stderr, the 0x key a verifier
pins plus the plain caveat that a self-signature proves authorship + wholeness only and earns no
production RepID. The whole skeptic loop is now two shell commands:

  trustshell sign-receipt body.json > r.json
  trustshell verify-receipt r.json --root 0x<thatkey>     # integrity VERIFIED, sealer trusted, exit 0
  trustshell verify-receipt r.json                        # no pin => NOT_CHECKED, never exit 0

Reuses the existing verify-receipt command unchanged; the engine-signed path is untouched. EXIT 0
sealed, 2 bad body, 3 could not seal. tests/cli-sign-receipt.test.ts (4) drives parse+run in-process
with an ephemeral TRUSTSHELL_HOME and asserts the round trip, the no-pin non-pass, and tamper→VETO.
Verified too at the real built-dist CLI boundary (sign→verify→no-pin).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CLI command `trustshell sign-receipt` landed with its own suite, but two
existing full-repo gates key off the Command union:

- docs.test.ts requires every shipped command to have a section in
  docs/api-reference.md — add the `### trustshell sign-receipt <file>` section.
- e2e-stranger.test.ts asserts every Command-union member is exercised by the
  stranger walk (ran == commandsOnMain). Add a sign-receipt -> verify-receipt
  round trip under an ephemeral TRUSTSHELL_HOME: seal a minimal body with this
  install's own key, then re-derive its integrity under the printed root. This
  is the offline emit->verify loop, no network.

Both suites green (177/177). No src/dist change; docs + test only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
trustshell-landing Ready Ready Preview Nov 7, 2026 8:14pm UTC

Request Review

`npm run skeptic` seals an owner-self-signed receipt with a throwaway install
key, then attacks it with the shipped `trustshell sign-receipt` /
`verify-receipt` — offline, no account. Each attack PASSES only when the
verifier REFUSES it:

  positive-control   a real seal verifies under its own pinned key (or the rest is vacuous)
  missing-root       no --root -> NOT_CHECKED, never exit 0
  wrong-key          a foreign pin -> integrity FAILED
  tampered-evidence  flip decision.verdict -> integrity FAILED
  forged-signature   swap the signature -> FAILED
  transplanted-sig   lift A's signature onto B's bytes (same key) -> FAILED
  expired-authz      a policy expired before --as-of -> authorization claim FAILED
  no-self-promotion  self-sig + authenticated envelope is STILL owner-self-signed, repid_eligible 'no'

The attack constructions are copied from tests/owner-receipt.test.ts (no
invented fields); this harness proves them at the real process boundary a
skeptic uses, and writes a report naming the commit, version, inputs, outputs
and the honest limitation (offline verify is stateless — replay is the
engine's receipt_id/nullifier, not the verifier).

Ran PASS at commit 253ed5a (all 9), report committed under docs/proof-pack/.
Manual artifact like `npm run proof-pack`; the same guards are jest-gated in
owner-receipt.test.ts. No src/dist change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e command

A developer-facing front door for "receipts, not promises": `trustshell demo`
seals a sample kernel-receipt/2 with this install's own key and VERIFIES it
right there under that key, offline, printing the six-claim verdict and the
exact sign-receipt -> verify-receipt lines to reproduce it. `--json` emits
{ receipt, verify, root } so a skeptic re-derives the verify step. Nothing is
written to disk; nothing leaves the machine. Exit 0 verified / 2 not / 3 could
not seal. It only orchestrates signOwnerReceipt + verifyReceiptOffline, which
the CLI already ships.

- src/cli: `demo` command (union, parse, help, execute) + dist.
- tests/cli-demo.test.ts (3): parse, the human loop (Integrity VERIFIED, five
  engine claims NOT_CHECKED, the pin line, the RepID caveat on stderr), and the
  --json contract.
- docs/api-reference.md: the `demo` section (the two Command-union gates:
  docs coverage + the stranger walk, which now runs `demo` under a throwaway home).
- docs/QUICKSTART.md: 10 steps from `npx … demo` to "what a pass does NOT prove"
  — strictly operational (commands + expected exit codes + honest limits), no
  new claims; it documents only what the shipped commands already do.

Affected suites green: cli-demo, e2e-stranger, docs, cli-sign-receipt (184).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…non-owner key is not independence (#681)

evidenceLevel() labelled any whole receipt sealed by a pinned non-owner key
with >=1 engine claim VERIFIED as INDEPENDENTLY_VERIFIED. But "a pinned key
that is not the owner's self-sign stamp" does NOT establish the issuer is
independent of the owner's beneficial control (two keys, one controller).
Offline the SDK has no controller graph, so it cannot assert independence —
the label overclaimed exactly the word a skeptic attacks first (Grok + ChatGPT
review, 2026-11-07).

Rename the rung to ISSUER_ATTESTED — attested UNDER A NAMED ISSUER, not a
finding of independence — and state the caveat in `why`. repid_eligible stays
`engine-decides` (unchanged): independence and RepID remain the engine's call
against its own pinned key. No behaviour change beyond the honest label.

Test-first: tests/owner-receipt.test.ts gains the first case that actually
exercises this branch (a pinned non-owner issuer + an authenticated six-claim
envelope) and pins the honest semantics (level, repid_eligible, why caveat).
Docs (api-reference), the /verify re-export note and the `demo` CLI message
aligned. sdk:build clean; owner-receipt/verify-receipt/cli-sign/cli-verify/
e2e-stranger suites green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 7, 2026

Copy link
Copy Markdown

🧾 Receipt

1 claim(s) checked against GitHub's own check runs on 6e5de78: 1 verified · 0 not checked · 0 failed

Claim Said in Evidence Result
build: sdk:build clean; owner-receipt/verify-receipt/cli-sign/cli-verify/ commit 6e5de78 step `Compile the mission CLI` in passports, step `Compile the mission CLI and the C15 containment probes` in mission (runsc), step `Build the trustshell CLI` in mission (runsc), step `Run npm run sdk:build` in verify-paris, step `Compile the mission CLI and the C15 containment probes` in mission (runc), step `Build the trustshell CLI` in mission (runc) passed and the job's own workflow file at this commit runs a matching command, and every other check on this commit finished green, skipped or neutral. ✅ VERIFIED
What this receipt does not prove
  • A green check proves that check passed on this commit. It does not prove the check tested this change.
  • Evidence is read at the strongest level each check offers: the run: command of the job, taken from the workflow file at this commit; else the step name and that step own result; else the check name alone. The Result column says which. A check name is a label, not evidence.
  • At command level the receipt confirms the job runs a command that normally runs this kind of check. It cannot confirm the command asserted anything: a test script that exits 0 without running a test still reads as tests.
  • A job whose steps cannot be read (a third-party check, or no actions: read permission) is matched by its name alone, and a job whose work happens inside a composite action shows no command.
  • Claims are read from the PR description and the latest commit message by plain patterns. A claim worded unusually is not seen. Text in double quotes, code blocks and block quotes is ignored on purpose; text in single quotes is read, because a single quote is also an apostrophe.
  • A check created after this receipt was written is not counted until the receipt runs again.

Receipts by TrustShell. Read-only: it reads this PR and its check runs, and writes only this comment. No code leaves the repo.

…ant distinction (#681)

The one receipt test named "replay / signature transplant" only exercised signature
transplantation. Separate the two attacks honestly and add the cross-verifier coverage the
owner asked for — no source change; the verifier already behaves correctly, these prove it.

- rename to "signature transplant …"; a transplanted signature still fails offline integrity.
- add a verbatim-REPLAY test stating plainly that offline verification does NOT reject a
  re-submitted identical receipt — it is stateless (whole + sealed, never fresh/once-used).
  Rejecting replay is one-time settlement in the engine (receipt_id/nullifier); a stranger's
  offline check cannot see it and must not claim "replay-proof".
- add cross-verifier key isolation (both directions): an owner-signed receipt under an
  engine-only verifier, and an engine-style receipt under an owner-only verifier, each fail
  CLOSED (foreign key → integrity FAILED, untrusted, NOT_ESTABLISHED) and never promote to
  ISSUER_ATTESTED just because some other pinned key or an authenticated envelope is handed in.
- add: an engine-style receipt whose sealing key is NOT pinned stays WHOLE_BUT_UNSEALED even
  with an authenticated envelope (the 5 engine claims are gated on a trusted sealer).
- keeps integrity / issuer-authentication / independent-observation / RepID-eligibility distinct.

signEngineReceipt helper factors the non-owner receipt shape. owner-receipt + e2e-stranger
suites green (26). Draft #681; not merged — independent review + fresh CI first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DealAppSeo added a commit that referenced this pull request Nov 7, 2026
…; fix fabricated commands

- Restructure the explainer to the owner's five-step IA (step rail + one step per section);
  the six-claim verdict panel lives in Verify, the evidence ladder in Earn.
- Remove three fabricated CLI commands (trustshell demo, npx … demo, sign-receipt) — none
  exist; every command shown now runs offline (guard/run/inspect/verify-receipt/bundle),
  verified against src/cli/index.ts and L-E2E.
- Rung 2 relabeled Issuer-attested (not independently-verified), reconciling with #681's
  INDEPENDENTLY_VERIFIED→ISSUER_ATTESTED: the engine's key is not independent of the issuer.
- Add Enforce (guard pin), Observe (inspect + 'observed nothing'), Reuse (bundle; authority
  never travels). Verified rendered desktop + mobile; no console errors.

DRAFT only — no deploy, no new public claim ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DealAppSeo
DealAppSeo marked this pull request as ready for review November 8, 2026 04:00
@strix-security

strix-security Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Strix Security Review

To fix before merging (2)

PR summary: Adds owner-self-signed offline receipts: a new `src/lib/owner-receipt.ts` seam that seals a kernel-receipt body with the install's own Ed25519 key in the engine's exact wire format, an `evidenceLevel` helper that classifies the result as NOT_ESTABLISHED / WHOLE_BUT_UNSEALED / OWNER_SELF_SIGNED / ISSUER_ATTESTED, and two new CLI commands (`sign-receipt`, `demo`) plus docs, a skeptic challenge script and tests.

Review summary

The PR adds the owner's half of the offline receipt loop: signOwnerReceipt seals a kernel-receipt/2 body with the install's own Ed25519 key so the existing verifyReceiptOffline checks it unchanged, plus evidenceLevel to keep owner self-signatures from reading as engine attestation, and the sign-receipt / demo CLI commands. The wire format, signature domain, hash construction, key handling (0600 key, 0700 dir, O_EXCL), prototype-pollution resistance and the committed dist/ (byte-identical to a fresh tsc of src/) all hold up. Two issues were confirmed: the new evidenceLevel guard takes its owner-self-signed discriminator from a caller-supplied receipt argument that is never bound to the verdict it was computed over, so an owner self-signature launders to ISSUER_ATTESTED whenever the caller passes the wrapper document shape the CLI itself unwraps; and sign-receipt prints the sealed receipt with a plain JSON.stringify, letting control and bidi sequences from the receipt body reach the terminal raw while its sibling outputs sanitize.

Ruled out (4)
  • Does `evidenceLevel()` return a fail-open `repid_eligible: 'engine-decides'` for tampered or unsealed receipts while a valid owner receipt returns 'no'?
    The inversion is real but matches the field's documented contract ('the SDK never says yes'), the authoritative `level` field is correct and correctly ordered, no shipped consumer gates on `repid_eligible`, and the engine re-verifies and is the actual RepID authority, so no impact materialises.
  • Can a repo-committed or TRUSTSHELL_HOME-pointed `bundle-signing-key.pem` be used by `sign-receipt` to mint a receipt that `verify-receipt --root` accepts at ex…
    The key is created 0600 with O_EXCL in a 0700 directory, an existing file must parse as Ed25519 or the command fails, TRUSTSHELL_HOME is controlled by the same invoking user so no boundary is crossed, and only public key material is ever printed.
  • Do the committed `dist/` artifacts diverge from the TypeScript sources, or does the new `/verify` export drag in network or framework code?
    A full `tsc` rebuild of `src/` is byte-identical to the committed `dist/` (73/73 files, both directions), and the `/verify` module graph contains only `node:crypto` plus the already-present `kernel-client`, whose ethers dependency is a lazy `await import()`.
  • Can a `__proto__` or `constructor` key in the receipt body pollute the prototype or override the forced schema/version/provenance/signer fields?
    `JSON.parse` makes `__proto__` an own data property, object spread uses CreateDataProperty so it never reaches `Object.prototype`, and the forced fields are applied after `...rest` with `receipt_hash`/`signature` destructured out first.

Fixed the findings? re-run the review, or tag @strix-security in a PR comment to run a fresh review.

Review 1 on this PR · 73b80a6 · requested Nov 8 04:00 UTC when the PR was opened


Reviewed by Strix
Re-run review · Configure security review settings

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strix flagged 2 new security findings below. See the pinned summary comment for the full PR status.

Comment thread src/cli/index.ts
const receipt = await signOwnerReceipt(body as SealableReceipt, key);
// The receipt to stdout stays machine-clean (pipe it, or `> r.json`); the human-facing pin
// and the warning that this is a self-attestation go to stderr so they never corrupt the JSON.
io.out(JSON.stringify(receipt, null, 2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Terminal control-sequence passthrough in trustshell sign-receipt stdout (CWE-150)

Severity: LOW · CWE-150

trustshell sign-receipt prints the sealed receipt with a plain JSON.stringify, so every string in the receipt body reaches the terminal verbatim. JSON.stringify escapes only U+0000–U+001F, so C1 controls (U+009B CSI, U+009D OSC, U+009C ST), U+2028/2029 and bidi overrides survive raw: a body containing "note":"\u009b31mPWNED\u009b0m \u202eREVERSED" emits the raw CSI and RTL-override bytes to stdout. Receipt bodies are commonly produced by an agent or received from a counterparty and piped in unread (echo '<body>' | trustshell sign-receipt -), so whoever controls any string in the body can drive the operator's terminal — spoofed output, cursor/scrollback manipulation, OSC 52 clipboard writes, OSC 8 hyperlinks. The same PR routes its sibling outputs (demo --json, verify-receipt --json, formatOfflineVerify) through terminalSafeJson/cleanCodePoints for exactly this reason; sign-receipt is the one new path left raw.

Suggested change
io.out(JSON.stringify(receipt, null, 2));
io.out(terminalSafeJson(receipt));
Prompt to fix with AI
This is a security vulnerability found during a code review.

Vulnerability: Terminal control-sequence passthrough in `trustshell sign-receipt` stdout (CWE-150)
Severity: LOW
CWE: CWE-150

`trustshell sign-receipt` prints the sealed receipt with a plain `JSON.stringify`, so every string in the receipt body reaches the terminal verbatim. `JSON.stringify` escapes only U+0000–U+001F, so C1 controls (U+009B CSI, U+009D OSC, U+009C ST), U+2028/2029 and bidi overrides survive raw: a body containing `"note":"\u009b31mPWNED\u009b0m \u202eREVERSED"` emits the raw CSI and RTL-override bytes to stdout. Receipt bodies are commonly produced by an agent or received from a counterparty and piped in unread (`echo '<body>' | trustshell sign-receipt -`), so whoever controls any string in the body can drive the operator's terminal — spoofed output, cursor/scrollback manipulation, OSC 52 clipboard writes, OSC 8 hyperlinks. The same PR routes its sibling outputs (`demo --json`, `verify-receipt --json`, `formatOfflineVerify`) through `terminalSafeJson`/`cleanCodePoints` for exactly this reason; `sign-receipt` is the one new path left raw.

Location: src/cli/index.ts:1531-1531
Context: sign-receipt writes the receipt body to stdout without terminal sanitisation
```
// Before:
        io.out(JSON.stringify(receipt, null, 2));
// After:
        io.out(terminalSafeJson(receipt));
```

How to fix:
Emit the sealed receipt through the existing `terminalSafeJson` helper instead of `JSON.stringify`. It rewrites C1 controls, U+2028/2029 and invisible/bidi characters as `\uXXXX` escapes, so the parsed value — and therefore the receipt's canonical bytes and digest — is unchanged while the terminal never receives a raw escape sequence. Apply the same helper to any other command that prints a receipt body.

Please fix this vulnerability. If you propose a fix, make it concise and minimal.

React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding.

Comment thread src/lib/owner-receipt.ts
Comment on lines +141 to +143
if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) {
return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 evidenceLevel() reads the owner-self-signed provenance stamp from an unbound receipt argument, letting an owner self-signature launder to ISSUER_ATTESTED

Severity: MEDIUM · CWE-807

evidenceLevel(verdict, receipt) decides whether a receipt is an owner self-signature solely by reading receipt.header.provenance, and never checks that receipt is the artifact the verdict was computed over — OfflineVerifyResult carries no provenance field, so the second argument is the only source and it is typed unknown. Any caller that passes a wrapper document instead of the bare receipt (the {receipt, six_claims} and {receipt_response: {...}} shapes the CLI itself unwraps and the API reference documents), or omits the argument, gets null from ownerReceiptProvenance and falls through to the ISSUER_ATTESTED branch. A receipt sealed with the owner's own install key then reports ISSUER_ATTESTED / repid_eligible: 'engine-decides' instead of OWNER_SELF_SIGNED / 'no', defeating the PR's stated guarantee that a smuggled authenticated envelope cannot launder a self-signature into RepID.

Suggested change
if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) {
return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` };
}
// BIND the provenance read to the artifact this verdict was computed over. `receipt` is caller-supplied
// and untyped, so a wrapper document (`{receipt, six_claims}`, an engine `receipt_response`) or a missing
// argument would read as "no provenance stamp" and fall through to ISSUER_ATTESTED — laundering an owner
// self-signature. Fail closed unless the receipt's own hash is the one the verifier recomputed.
const suppliedHash = (receipt as { header?: { receipt_hash?: unknown } } | null | undefined)?.header?.receipt_hash;
if (typeof suppliedHash !== 'string' || !verdict.receipt_hash || suppliedHash.toLowerCase() !== verdict.receipt_hash.toLowerCase()) {
return { level: 'NOT_ESTABLISHED', repid_eligible: 'engine-decides', why: 'the supplied receipt is not the one this verdict was computed over (receipt_hash mismatch or absent), so its provenance cannot be read' };
}
if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) {
return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` };
}
Prompt to fix with AI
This is a security vulnerability found during a code review.

Vulnerability: `evidenceLevel()` reads the owner-self-signed provenance stamp from an unbound `receipt` argument, letting an owner self-signature launder to `ISSUER_ATTESTED`
Severity: MEDIUM
CWE: CWE-807

`evidenceLevel(verdict, receipt)` decides whether a receipt is an owner self-signature solely by reading `receipt.header.provenance`, and never checks that `receipt` is the artifact the verdict was computed over — `OfflineVerifyResult` carries no provenance field, so the second argument is the only source and it is typed `unknown`. Any caller that passes a wrapper document instead of the bare receipt (the `{receipt, six_claims}` and `{receipt_response: {...}}` shapes the CLI itself unwraps and the API reference documents), or omits the argument, gets `null` from `ownerReceiptProvenance` and falls through to the `ISSUER_ATTESTED` branch. A receipt sealed with the owner's own install key then reports `ISSUER_ATTESTED` / `repid_eligible: 'engine-decides'` instead of `OWNER_SELF_SIGNED` / `'no'`, defeating the PR's stated guarantee that a smuggled authenticated envelope cannot launder a self-signature into RepID.

Location: src/lib/owner-receipt.ts:141-143
Context: Provenance check reads an unbound caller-supplied object; bind it to the verified verdict
```
// Before:
  if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) {
    return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` };
  }
// After:
  // BIND the provenance read to the artifact this verdict was computed over. `receipt` is caller-supplied
  // and untyped, so a wrapper document (`{receipt, six_claims}`, an engine `receipt_response`) or a missing
  // argument would read as "no provenance stamp" and fall through to ISSUER_ATTESTED — laundering an owner
  // self-signature. Fail closed unless the receipt's own hash is the one the verifier recomputed.
  const suppliedHash = (receipt as { header?: { receipt_hash?: unknown } } | null | undefined)?.header?.receipt_hash;
  if (typeof suppliedHash !== 'string' || !verdict.receipt_hash || suppliedHash.toLowerCase() !== verdict.receipt_hash.toLowerCase()) {
    return { level: 'NOT_ESTABLISHED', repid_eligible: 'engine-decides', why: 'the supplied receipt is not the one this verdict was computed over (receipt_hash mismatch or absent), so its provenance cannot be read' };
  }
  if (ownerReceiptProvenance(receipt) === OWNER_SELF_SIGNED) {
    return { level: 'OWNER_SELF_SIGNED', repid_eligible: 'no', why: `sealed by the owner's own install key (${verdict.sealer.fingerprint ?? 'pinned key'}); self-attestation earns authorship, never production RepID` };
  }
```

How to fix:
Bind the provenance read to the verified artifact: before consulting `ownerReceiptProvenance`, require that the supplied receipt's `header.receipt_hash` equals `verdict.receipt_hash` (the hash the verifier recomputed locally) and fail closed to `NOT_ESTABLISHED` when it does not match or is absent. Alternatively, have `verifyReceiptOffline` record the provenance stamp it observed on the verified receipt in `OfflineVerifyResult` and read the level from that field only, so the decision never depends on a caller-supplied object. Add a regression test that passes a wrapper document and `null` and asserts the level is not `ISSUER_ATTESTED`.

Please fix this vulnerability. If you propose a fix, make it concise and minimal.

React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding.

@DealAppSeo
DealAppSeo merged commit dacb4c2 into main Nov 8, 2026
14 checks passed
DealAppSeo added a commit that referenced this pull request Nov 8, 2026
…AFT — no deploy) (#682)

* D7: trustshell.dev "Receipts, not promises" draft section (drafts-only, no deploy)

A self-contained HTML draft of one explainer section for trustshell.dev, built
to be shown and reviewed, then ported into the live site by the owner if
approved. Nothing here deploys; no new public claim goes live from this folder.

- Inherits trustshell.dev's own world verbatim (ground #0a0a0a, accent #f6851b,
  IBM Plex Serif / Inter / JetBrains Mono, state palette teal/slate/rose) so it
  drops in without a visual seam. This is an extension, not a redesign.
- Renders the offline receipt loop (D1/D4/D5) as a page a skeptic reads first:
  the six-claim non-collapsing verdict, the three-rung evidence ladder
  (owner-self-signed -> independently-verified -> RepID-eligible), and a
  "What a receipt does not prove" block.
- Honesty rules honored: no first/only/unique/unhackable; NOT_CHECKED shown and
  never rounded up; three evidence levels kept distinct; owner self-signature
  earns no production RepID; developer-preview framing; no accuracy number.
- impeccable detect: 1 accepted finding (Inter = the site's own body face);
  all others fixed. Verified rendered at desktop and 375px mobile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(site): align Receipts draft to Enforce→Observe→Verify→Earn→Reuse; fix fabricated commands

- Restructure the explainer to the owner's five-step IA (step rail + one step per section);
  the six-claim verdict panel lives in Verify, the evidence ladder in Earn.
- Remove three fabricated CLI commands (trustshell demo, npx … demo, sign-receipt) — none
  exist; every command shown now runs offline (guard/run/inspect/verify-receipt/bundle),
  verified against src/cli/index.ts and L-E2E.
- Rung 2 relabeled Issuer-attested (not independently-verified), reconciling with #681's
  INDEPENDENTLY_VERIFIED→ISSUER_ATTESTED: the engine's key is not independent of the issuer.
- Add Enforce (guard pin), Observe (inspect + 'observed nothing'), Reuse (bundle; authority
  never travels). Verified rendered desktop + mobile; no console errors.

DRAFT only — no deploy, no new public claim ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 73b80a64 Deployed Nov 7, 2026 by vercel[bot]
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