Skip to content

feat(selfupdate): keyless cosign signing in release + verify-on-update - #94

Merged
gustavobertoi merged 2 commits into
mainfrom
feat/selfupdate-signing
Jul 1, 2026
Merged

feat(selfupdate): keyless cosign signing in release + verify-on-update#94
gustavobertoi merged 2 commits into
mainfrom
feat/selfupdate-signing

Conversation

@gustavobertoi

Copy link
Copy Markdown
Contributor

What

Wires keyless cosign (sigstore) signing into the release pipeline and adds cosign signature verification to the self update flow — closing the "signature verification is mandatory" gap in spec 14, now that release signing exists (spec 25).

Release pipeline (signing)

  • .github/workflows/release.yml: adds id-token: write permission and a sigstore/cosign-installer@v3 step. Keyless signing uses the job's GitHub OIDC token — no long-lived private key.
  • .goreleaser.yaml: new signs: block runs cosign sign-blob --yes over checksums.txt (the root of the integrity chain), publishing checksums.txt.sig (signature) + checksums.txt.pem (Fulcio certificate) as release assets.

Verify-on-update (internal/selfupdate)

  • New Verifier interface (verify.go) shelling cosign verify-blob, pinning --certificate-identity-regexp to the release.yml workflow identity and --certificate-oidc-issuer to https://token.actions.githubusercontent.com.
  • downloadBinary now verifies the checksums.txt signature before trusting any per-archive SHA-256 line; SHA-256 remains enforced in all cases.
  • --insecure-skip-verify escape hatch, default verify-on. Keeps the existing IsDevBuild + package-manager CanSelfReplace refusals.

Why

Auto-updating a binary that drives Docker, edits /etc/hosts, and installs a root CA is a supply-chain target — an altered/unsigned release must be refused, not warned-and-installed (spec 14). The signing side (spec 25) had been deferred; this connects both ends.

Default behavior (honest note)

Signature verification is ON by default and requires the cosign binary. When cosign is absent, the update aborts with a remediation (install cosign, or re-run with --insecure-skip-verify) rather than silently degrading. --insecure-skip-verify bypasses only the cosign check — SHA-256 is still enforced, so it never drops to "no integrity check".

How tested

internal/selfupdate table-driven tests (fake verifier, no network):

  • signature-verify happy path (verifier handed the exact checksums.txt bytes + published .sig/.pem)
  • tamper detection (bad signature aborts)
  • verify-runs-before-checksum ordering
  • cosign-unavailable abort (names cosign + the escape hatch)
  • missing-signature-asset abort
  • --insecure-skip-verify bypass (SHA-256 still fails a tampered archive)
  • release.yml YAML parses and declares id-token: write + installs cosign

Gates green: CGO_ENABLED=0 go build ./..., CGO_ENABLED=1 go test ./internal/..., gofmt -l clean, go vet ./.... No internal/generate/templates/ changes, so determinism is unaffected.

🤖 Generated with Claude Code

gustavobertoi and others added 2 commits July 1, 2026 00:18
Wire keyless cosign (sigstore) signing into the release pipeline and add
signature verification to the self-update flow (spec 14 + spec 25).

Release pipeline:
- release.yml gains `id-token: write` and a sigstore/cosign-installer step.
- .goreleaser.yaml `signs:` block runs `cosign sign-blob --yes` (keyless,
  GitHub OIDC — no private key) over checksums.txt, publishing
  checksums.txt.sig + checksums.txt.pem as release assets.

Verify-on-update (internal/selfupdate):
- New Verifier interface (verify.go) shelling `cosign verify-blob`, pinning
  --certificate-identity-regexp to the release.yml workflow identity and
  --certificate-oidc-issuer to token.actions.githubusercontent.com.
- downloadBinary verifies the checksums.txt signature (the trust root) BEFORE
  trusting any per-archive SHA-256 line; SHA-256 is always enforced.
- --insecure-skip-verify escape hatch (default verify-on). cosign absent
  aborts with a clear remediation unless bypassed.
- Keeps the existing IsDevBuild + package-manager CanSelfReplace refusals.

Tests: happy-path verify, tamper-detection, verify-before-checksum ordering,
cosign-unavailable abort, missing-signature abort, skip-verify bypass (SHA
still enforced), and a release.yml YAML-parse assert that id-token: write is
declared. No network required.

Default: signature verification is ON; it requires the cosign binary. When
cosign is absent the update refuses with a remediation rather than silently
degrading — pass --insecure-skip-verify to proceed on checksum-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The dry-run runs 'goreleaser release --snapshot', which now hits the new
cosign signs: block and fails with 'cosign: executable file not found' —
keyless OIDC signing can't run in a PR dry-run. Skip the sign pipe there;
the real tagged release (release.yml) installs cosign and signs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gustavobertoi
gustavobertoi merged commit 3f76b36 into main Jul 1, 2026
4 checks passed
@gustavobertoi
gustavobertoi deleted the feat/selfupdate-signing branch July 1, 2026 12:40
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