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
67 lines
3.4 KiB
Markdown
67 lines
3.4 KiB
Markdown
# 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).
|