feat(sign): optional detached minisign signature over the evidence bundle (J1) - #8
Merged
Conversation
…ndle (J1) manifest.json already SHA-256s every member, so tampering with a member is detectable from inside the bundle. What it could not do is bind the bundle to whoever produced it — `generated_by` and `generated_on` are unauthenticated strings anyone can edit. `sign.py` signs the manifest, which covers every member transitively through their digests. Shells out to the `minisign` binary rather than adding a crypto dependency: export.py stays stdlib-only (which J2's acceptance also asks for), and a reviewer verifies with the same one-line command whether or not they have this toolchain. Optional at every level, because an evidence bundle that refuses to be produced for want of a signing key is worse than an unsigned one. No key configured, no binary on PATH, an unreadable key, a signer that exits non-zero, a signer that hangs — each logs one line and exports unsigned. No passphrase handling, by design: the key must be unencrypted (`minisign -G -W`). A signing passphrase is a credential this tool has no business holding, and the constraint is documented rather than discovered at a prompt. It signs the EXACT manifest bytes that ship — signing a different serialization would make every verification fail, or pass against something the reviewer never received. Pinned by a test that captures what the signer was handed and compares it to the member in the zip. The bundle carries its own limits: a caveat, true whether or not a signature is present, says the signature attests who exported the bundle rather than that the log inside is truthful, and that the public key must come from out of band — a key shipped inside a bundle proves nothing about it. 10 new tests, all offline against a stub signer on PATH. 278 pass, coverage 73%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… path
Round-trip against the real minisign 0.12, which the stub-based tests could not do:
keygen -G -W -> export -> minisign -V "Signature and comment signature verified"
tampered manifest "Signature verification failed"
tampered member (audit.log) caught by the manifest's own SHA-256, signature intact
— the chain is what makes that digest trustworthy
wrong public key fails on key-id mismatch
The CLI flag contract I had assumed (-S -s <key> -m <in> -x <out> -t <comment>) matches the real
binary exactly, so the shell-out needed no change.
What the real run did surface: the trusted comment — which is SIGNED and travels with the bundle —
embedded the exporter's absolute filesystem path. Unnecessary disclosure for an artifact whose whole
purpose is to leave the machine. It now carries the tool version and run_id, the join key that
already identifies the run, with a test pinning that no local path appears in it.
279 pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap J1.
manifest.jsonalready SHA-256s every member, so tampering with a member isdetectable from inside the bundle. What it could not do is bind the bundle to whoever produced it —
generated_by/generated_onare unauthenticated strings anyone can edit.sign.pysigns the manifest and dropsmanifest.json.minisigbeside it. Because the manifestdigests every member, coverage is transitive.
Three design calls
Shell out to
minisign, add no dependency.export.pystays stdlib-only — which J2'sacceptance also requires — and a reviewer verifies with a one-line command whether or not they have
this toolchain installed.
Optional at every level. No key configured, no binary on PATH, an unreadable key, a signer that
exits non-zero, a signer that hangs — each logs one line and exports unsigned. An evidence bundle
that refuses to be produced for want of a signing key is worse than an unsigned one.
No passphrase handling. The key must be unencrypted (
minisign -G -W). A signing passphrase isa credential this tool has no business holding; requiring a key managed by whatever already manages
your secrets is the honest trade, documented rather than discovered at a prompt.
It signs the bytes that actually ship
Signing a different serialization than the member in the zip would make every verification fail —
or, worse, pass against something the reviewer never received. Pinned by a test that captures what
the signer was handed and compares it byte-for-byte to
manifest.jsonin the bundle.The bundle carries its own limits
A caveat — true whether or not a signature is present — travels inside the manifest:
docs/USAGE.mdspells out generation and verification, and states plainly what the signature doesand does not attest.
Verification status — one gap, deliberately left for you
10 new tests, all offline against a stub
minisignon PATH, covering every failure path and thesigned-bytes-match-shipped-bytes guarantee. 278 pass · coverage 73% ·
ruffclean.What the stub cannot check is my CLI flag contract (
-S -s <key> -m <in> -x <out> -t <comment>)against the real binary — the stub validates it against my own assumption.
minisignis notinstalled here and I did not want to change your machine unasked.
brew install minisignand I'lldo a real keygen → sign → verify round-trip and push the result to this branch before you merge.
🤖 Generated with Claude Code