Skip to content

feat(sign): optional detached minisign signature over the evidence bundle (J1) - #8

Merged
henleda merged 2 commits into
mainfrom
feat-j1-sign-bundle
Jul 27, 2026
Merged

feat(sign): optional detached minisign signature over the evidence bundle (J1)#8
henleda merged 2 commits into
mainfrom
feat-j1-sign-bundle

Conversation

@henleda

@henleda henleda commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Roadmap 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 / generated_on are unauthenticated strings anyone can edit.

sign.py signs the manifest and drops manifest.json.minisig beside it. Because the manifest
digests every member, coverage is transitive.

Three design calls

Shell out to minisign, add no dependency. export.py stays stdlib-only — which J2's
acceptance 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 is
a 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.json in the bundle.

The bundle carries its own limits

A caveat — true whether or not a signature is present — travels inside the manifest:

If manifest.json.minisig is present it signs THIS manifest, which SHA-256s every member — so it
attests who exported the bundle, not that the audit log inside is truthful. Verify it with the
signer's public key obtained out of band; a key shipped inside a bundle proves nothing about
that bundle.

docs/USAGE.md spells out generation and verification, and states plainly what the signature does
and does not attest.

Verification status — one gap, deliberately left for you

10 new tests, all offline against a stub minisign on PATH, covering every failure path and the
signed-bytes-match-shipped-bytes guarantee. 278 pass · coverage 73% · ruff clean.

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. minisign is not
installed here and I did not want to change your machine unasked. brew install minisign and I'll
do a real keygen → sign → verify round-trip and push the result to this branch before you merge.

🤖 Generated with Claude Code

henleda and others added 2 commits July 27, 2026 12:37
…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>
@henleda
henleda merged commit e0464e4 into main Jul 27, 2026
4 checks passed
@henleda
henleda deleted the feat-j1-sign-bundle branch July 27, 2026 17:47
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