Skip to content

feat(export): --verify re-reads a bundle and checks it against its own manifest (J2) - #9

Merged
henleda merged 1 commit into
mainfrom
feat-j2-verify-bundle
Jul 27, 2026
Merged

feat(export): --verify re-reads a bundle and checks it against its own manifest (J2)#9
henleda merged 1 commit into
mainfrom
feat-j2-verify-bundle

Conversation

@henleda

@henleda henleda commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Roadmap J2. J1 produces a signature; this checks one — and checks the member digests whether or
not a signature exists. Exits non-zero on any problem, so it drops into CI.

vpcopilot export --verify audit-bundle.zip                         # digests only
vpcopilot export --verify audit-bundle.zip --pubkey vpcopilot.pub  # digests + signature

Verified end to end against minisign 0.12

case result exit
clean + pubkey signature verified · OK, 4 digests match 0
clean, no pubkey present-unverified · OK, with a note to pass --pubkey 0
tampered member signature still verified, MISMATCH audit.log 1
smuggled extra file UNLISTED extra-note.txt 1
missing / corrupt zip reported as an error, never a traceback 1

The tampered-member row is worth reading twice. The signature stays valid because the manifest
was not touched — the digest is what fails. It is the chain that catches it, not either half
alone. That is also what makes the signature worth having: it is the reason to trust the digest.

Two design calls

Four member verdicts, not twook / mismatch / missing (listed but absent) / unlisted
(present but in no manifest). A file added to a bundle is exactly as suspicious as one altered,
and a check over only the listed members would wave it straight through.

present-unverified is a third signature state and not a failure. Without a public key the
signature genuinely cannot be checked, and one shipped inside the bundle proves nothing about that
bundle. A reviewer without the key must still be able to check digests — reporting "I cannot check
this" the same way as "this is forged" would destroy the distinction that matters most.

Console surface, deliberately narrow

GET /api/audit-verify checks <out>/audit-bundle.zip only — a path derived from server state,
never from the request. An endpoint taking a caller-supplied path would be an arbitrary-file reader,
and localhost is not a reason to ship one. Verifying a bundle that has already left the machine is
the CLI's job, because that is where the reviewer is. Pinned by a test asserting the endpoint takes
no parameters.

Flagging it against the two-surfaces invariant: this is a narrowed twin, not a full one, and the
roadmap item now says why.

20 new tests · 299 pass · coverage 73% · ruff clean.

🤖 Generated with Claude Code

…own manifest (J2)

The other half of the evidence story: J1 produces a signature, this checks one — and checks the
member digests whether or not a signature exists. Exits non-zero on any problem, so it drops into
CI. The digest half is pure stdlib, the same check docs/AUDIT.md documents by hand; only the
optional signature check shells out to minisign, adding no dependency.

Four member verdicts, not two: ok / mismatch / missing (listed but absent) / unlisted (present but
in no manifest). A file ADDED to a bundle is exactly as suspicious as one altered, and a check over
only the listed members would wave it through.

`present-unverified` is a third signature state and deliberately NOT a failure. Without a public key
the signature genuinely cannot be checked, and one shipped inside the bundle proves nothing about
that bundle. A reviewer without the key must still be able to check digests; reporting "I cannot
check this" the same way as "this is forged" would destroy the distinction that matters most.

Verified end to end against a real signed bundle and minisign 0.12:

  clean + pubkey        signature verified · OK, 4 member digests match      exit 0
  clean, no pubkey      present-unverified · OK, with a note to pass --pubkey exit 0
  tampered member       signature STILL verified, MISMATCH audit.log         exit 1
  smuggled extra file   UNLISTED extra-note.txt                              exit 1
  missing / corrupt zip reported as an error, never a traceback              exit 1

The tampered-member case is the one worth reading twice: the signature stays valid because the
manifest was not touched, and the digest is what fails. It is the chain, not either half alone.

The console surface is deliberately narrow — GET /api/audit-verify checks <out>/audit-bundle.zip
only, a path derived from server state and never from the request. An endpoint taking a
caller-supplied path would be an arbitrary-file reader, and localhost is not a reason to ship one;
verifying a bundle that has left the machine is the CLI's job, because that is where the reviewer
is. Pinned by a test asserting the endpoint takes no parameters.

20 new tests. 299 pass, coverage 73%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@henleda
henleda merged commit 8e921be into main Jul 27, 2026
4 checks passed
@henleda
henleda deleted the feat-j2-verify-bundle branch July 27, 2026 18:03
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