stackchain-lab-loop/docs/path-proof-threat-boundary.md
vincent f61ef22f48 [Vincent] #28: executable replay/forgery gate for path-proof receipts
Dependency-free path_proof package under a dedicated module. Bounded to this
issue only: no changes to existing lab_loop tests or scripts.

Deliverables:
1. path_proof/ package (receipt.py, keys.py, __init__.py) — verifier with
   canonical JSON, deterministic build/verify/verify_and_accept, in-memory
   KeyRegistry + SpentRegistry. No I/O, no wall clock, no randomness.
2. Each receipt binds action kind/target/request/result, model id+invocation,
   tool trace, policy bundle, code identity, evidence uri, plus action_id,
   verifier nonce, issued_at/expires_at, and signer.
3. Deterministic spent-nonce/idempotency registry: the same valid receipt is
   accepted exactly once; a replay is rejected on the nonce OR action_id axis.
4. Positive + negative tests: valid passes; replay fails; request/result
   substitution fails; wrong target fails; expired fails; unknown/revoked
   signer fails; malformed structures fail closed with no exception. Every
   negative case asserts rejection AND a specific reason.
5. docs/path-proof-threat-boundary.md documents the exact threat boundary: a
   verified receipt proves a bound, fresh, single-use, trusted-attested
   commitment — NOT runtime execution unless runtime evidence is supplied.

Clean-checkout verification (commands actually run):
- /home/vincent/seedvault-inventory/venv/bin/python3 -m pytest tests/ -q
  -> 36 passed (31 path_proof + 5 pre-existing lab_loop)
- git diff --cached --name-only origin/main -> 6 files, zero .pyc
- __pycache__ gitignored; clean checkout stays clean after tests

Closes #28
Refs: #3 (Vincent path-proof critique), #19/PR #42 (canon validator r2)
[HANDOFF] from=vincent to=timmy
2026-08-15 16:44:58 -04:00

3.4 KiB

Path Proof Receipt — Threat Boundary

This document states, precisely, what the path_proof verification gate proves and does not prove. It is the contract the verifier enforces and the limit a consumer must respect. The field rationale lives in docs/path-proof-receipt.md (branch timmy/3-path-proof-receipt); this file is the executable-side boundary statement required by issue #28.

What a verified, freshly-accepted receipt proves

A receipt that passes verify_receipt and is accepted by verify_and_accept proves exactly the following, and nothing more:

  1. Binding. The receipt commits, as a single signed unit, to:
    • the consequential action (action.kind, action.target),
    • the exact request and observed result it claims (action.request_sha256, action.result_sha256),
    • the model invocation identity (path.model.id, path.model.invocation),
    • the ordered tool path and effective policy (path.tools.trace_sha256, path.policy.bundle_sha256),
    • the immutable code identity (path.code.repo, path.code.commit, path.code.entrypoint), and
    • the retrievable evidence bundle (evidence.uri).
  2. Trust. The attestation resolves to a signer that is present in the caller's trusted key registry, is not revoked, and is within its validity window at the time of verification.
  3. Freshness. The receipt is inside its [issued_at, expires_at] window.
  4. Single-use. The receipt's nonce and action_id have not been previously consumed, so an identical receipt cannot be accepted a second time (idempotency / replay protection).

What a verified receipt does NOT prove

  • It does not prove runtime execution of the consequential effect. result_sha256 is the hash of an observed result the recorder claims it saw. Proving the side effect actually took place requires the verifier to independently re-observe it (for example, read the Gitea label state back and compare it to result_sha256). Until that happens the effect is attested, not confirmed.
  • It does not prove the provider ran the advertised weights. path.model.id is an identity string, not a cryptographic attestation of model provenance. Only a separately trusted provider model attestation could close that gap.
  • It does not survive compromise of both the recorder and its signing key. The trust anchor is the key registry. If both are compromised, a forged receipt with a valid signature is indistinguishable from a genuine one.
  • It does not prove the evidence bundle was not selectively redacted ambiguously. The redaction manifest is part of the bundle; verifying the bundle digest proves integrity of what is present, not that nothing material was omitted.

Failure semantics

  • Any single failed check yields status unverified with specific reasons — never a partial pass.
  • A structurally unreadable receipt yields status malformed.
  • A rejected receipt has no side effects: it does not consume a nonce or action_id. Only a verified acceptance marks identities as spent.

Determinism guarantees

  • Verification takes now (epoch seconds) as an explicit argument and never reads the wall clock.
  • The key registry and spent-identity registry are in-memory objects the caller controls; the module performs no I/O, no randomness, and no network.
  • Therefore the gate is fully reproducible from a clean checkout and leaves no generated artifacts (test caches are gitignored).