Skip to content

feat: minisign release signing for herdr manifests (#102) - #103

Merged
jerryfane merged 1 commit into
mainfrom
feat/herdr-release-sign
Oct 28, 2026
Merged

jerryfane merged 1 commit into
mainfrom
feat/herdr-release-sign

Conversation

@jerryfane

Copy link
Copy Markdown
Contributor

Closes #102. Owner request via herdr-app (2026-10-28): keep the herdr release signing key in the keyring, not on GitHub or an agent box.

What it does

A new sign-only auth, minisign-sign. The caller sends the BLAKE2b-512 digest of the release file (b2sum) and a trusted comment; the keyring returns a standard prehashed minisign .minisig. The file never leaves the caller and the key never leaves the keyring.

"herdr-release": {"key": "HERDR_RELEASE_KEY", "auth": "minisign-sign",
                  "minisign": {"trusted_comment_prefix": "herdr-preview "}}
  • POST /_keyring/minisign/<service> {"blake2b512_hex", "trusted_comment"} → {"minisig", "key_id", "public_key"}.
  • GET on the same route → {"key_id", "public_key", "public_key_file"} for embedding in the herdr binary.
  • keyring minisign-keygen --store FILE NAME generates the key inside the store (never shown) and prints the public key. It refuses an existing name.
  • The owner approves a role per service in the dashboard, as for any service. The dashboard labels it "minisign (sign only)".
  • Every call is audited: role, time, status, and for signatures the digest and trusted comment. No key material.

Answers to the issue's open questions

  • Payload limits: only a 64-byte BLAKE2b-512 digest, and a signed trusted comment that must start with the service's prefix (e.g. herdr-preview ), be longer than it, and be ≤256 bytes of printable ASCII on one line. Unknown body fields are refused. Only prehashed (ED) signatures are made.
  • One key per product: one service per product, and the existing rule that a signing key cannot be shared with another service also stops the same key from signing raw bytes through /_keyring/sign/.
  • Rotation: generate a new key name, ship both public keys in herdr, then point the service at the new key. Key IDs are derived from the public key, so they change with the key.

Verification

  • gofmt -l . empty; go vet ./...; go test -race ./... all pass (Go 1.24.13, offline).
  • New tests: signature verifies exactly as minisign does (algorithm, key ID, signature over digest, global signature over signature‖comment), public-key export format, audit fields, every refusal (other product's prefix, prefix alone, newline injection, 257-byte comment, short/non-hex digest, unknown field, trailing object, raw ed25519 route, proxy path, query, other method), bad stored key → 503, policy validation, keygen never replaces a key.
  • Real-path smoke (keyring serve + keyring relay on loopback, throwaway key): the official minisign 0.12 binary (release tarball verified against the author's published key) verifies the relay-issued signature, also with -H (prehash required); a tampered manifest and a forged trusted comment are both refused; relay refusals 400/400/403; audit log has role/time/digest/comment and no key.
  • test-check prove: inconclusive for all new tests (old_code_build_failed): this is a new feature, so the old code lacks the API and the tests cannot compile against it. Not a regression fix; no red-on-old claim.

Not done here

Deploying to the Mac, generating the production key and granting herdr-app's role are separate, owner-approved steps after merge.

A minisign-sign service signs the BLAKE2b-512 digest of a release file as a
prehashed minisign signature. Its trusted comment must start with the
service's prefix, so one key signs only one product's releases. GET on the
same route exports the minisign public key. keyring minisign-keygen makes the
key inside the store without showing it. Every call is audited with role,
time, digest and trusted comment.
@jerryfane

Copy link
Copy Markdown
Contributor Author

Level 3 exact-head review: approved, 0 findings, at 84e7171124ebca72a64678fe2a20430c3649f02f. Gitmoot job local-review-review-router-18e2b5bc818da3ba-1 (reviewer adspower-fleet-review, devin/swe-2; implementer keyring seat, Anthropic, so different model family). Reviewer ran build, vet, full go test ./..., overlay probes of the real handler and policy, and minisign-keygen. Hosted CI green at this head.

@jerryfane
jerryfane merged commit 57d611d into main Oct 28, 2026
3 checks passed
@jerryfane

Copy link
Copy Markdown
Contributor Author

Agent: adspower-fleet-review
Runtime: omp
Job: local-review-review-router-18e2b5bc818da3ba-1

Decision: approved
Summary: PR #103 @ 84e7171 (minisign release signing) reviewed against base 31ac165 with executed checks. Build, vet, and the full suite pass. Independent overlay probes exercised the real handler: POST-only role blocks GET export (403), GET export consumes daily quota (429 after), trailing slash/HEAD/OPTIONS refused, prefix matching is case-sensitive and strictly longer-than-prefix, malformed digests/fields 400. Byte-level probe verified the .minisig layout (ED‖keynum‖sig, global sig over sig‖comment, Ed‖keynum‖pub pubkey) so minisign -Vm will verify; the keynum in sig and pubkey is consistent. Policy probes confirmed prefix bounds (1-64 printable, no leading space), auth-minisign coupling, and that the signing key cannot be shared or reached via /_keyring/sign/. keyring minisign-keygen run end-to-end: generates the key into a 0600 store, prints only the public key, refuses existing names. The minisign binary is not installed, so verification vs the real tool was structural (decoding the emitted file per the minisign spec). Prior verdicts file contains none for PR #102/#103 (latest recorded is #101) — no prior findings to answer.

Tests Run

  • CGO_ENABLED=0 go build ./... — pass
  • CGO_ENABLED=0 go vet ./... — clean
  • CGO_ENABLED=0 go test ./... — all packages pass (internal/server 74.08s, cmd/keyring, policy, dashboard, consent, instance, requests, store, googlelogin, admin, relay ok)
  • CGO_ENABLED=0 go test -overlay ./internal/server/ -run TestProbe -v — PASS: method-restricted role GET=403, GET quota consumption→429, path/body edge cases, .minisig and pubkey byte layout + Ed25519 verification
  • CGO_ENABLED=0 go test -overlay ./internal/policy/ -run TestProbeMinisignValidation -v — PASS: all 14 accept/reject validation cases incl. key-sharing
  • go run ./cmd/keyring minisign-keygen --store HERDR_RELEASE_KEY — exit 0, store 0600, public key only printed; second run refused exit 1

Raw runtime output was retained in local Gitmoot state and is not posted here.

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.

Sign-only service for herdr release manifests (ed25519/minisign)

1 participant