fix(release): publish cosign sigs to Sigstore Rekor (#81) #82

Merged
james merged 1 commit from 81-cosign-no-rekor into main 2026-06-17 19:46:17 +00:00
Owner

Closes #81.

(Branch is misnamed 81-cosign-no-rekor — that was the original direction before the trade-off was reconsidered. The fix is the opposite: publish to Rekor explicitly.)

What happened

First successful release build (after #79, #76, #16 all landed) produces a signed image, but the verification command from the release notes fails:

Error: no matching signatures: no known key found for this signature in database

That message is Rekor returning "no entry matches the signature's public key + image digest". cosign 2.5.x changed the default for --tlog-upload on keyed signing, so our sign step stopped publishing to Sigstore's public-good Rekor — but the verify step (which still consults Rekor by default) can't find the entry it expects.

Fix

Explicitly --tlog-upload=true on both cosign sign and cosign attest. This:

  • Keeps the verify commands at the canonical default shape (no --insecure-ignore-tlog, no --rekor-url override). A self-hoster's verify command is plain cosign verify --key ... <image>@<digest> — what they'd type by reflex.
  • Restores the public-timestamping property: every release now has a Rekor entry saying "this key signed this digest at time T", which is useful forensically if the key ever leaks.
  • Takes an explicit runtime dependency on rekor.sigstore.dev. If Rekor is unreachable during a tag push, the sign step fails and the release doesn't publish; that's the intentional fail-closed behaviour, documented.

Files

File Change
.forgejo/workflows/release.yml cosign sign --tlog-upload=true, cosign attest --tlog-upload=true, with a comment explaining the default-flip and why we're being explicit
docs/ci.md New paragraph under "Verifying a published image" explaining the Rekor dependency, how to inspect a release's entry with cosign tree, and the failure mode if Rekor is down
docs/adr/0014-release-pipeline.md Positive consequence: Rekor entries + timestamping. Negative consequence: runtime dep on Sigstore public-good infra, with the self-hosting escape hatch noted

Acceptance criteria

  • --tlog-upload=true on sign and attest, with intent-explaining comments.
  • Release-notes verify commands stay at the canonical default shape.
  • docs/ci.md and ADR-0014 document the explicit Rekor dependency.
  • Next tag push (e.g. v0.0.1-rc.2) produces an image whose signature is in Rekor — cosign tree forge.wynning.tech/james/carol@<digest> reports the entry; verify command from release notes succeeds with Verified OK. (CI to confirm.)

Out of scope

  • Self-hosting Rekor (Trillian + rekor-server + persistent storage — multi-week infra; revisit only if Sigstore public-good becomes unacceptable).
  • Re-signing the existing broken release. Forward-only: the next tag push will work; the old release's signature is permanently un-verifiable because no Rekor entry was ever created for it.
Closes #81. (Branch is misnamed `81-cosign-no-rekor` — that was the original direction before the trade-off was reconsidered. The fix is the opposite: publish to Rekor explicitly.) ## What happened First successful release build (after #79, #76, #16 all landed) produces a signed image, but the verification command from the release notes fails: ``` Error: no matching signatures: no known key found for this signature in database ``` That message is Rekor returning "no entry matches the signature's public key + image digest". cosign 2.5.x changed the default for `--tlog-upload` on keyed signing, so our sign step stopped publishing to Sigstore's public-good Rekor — but the verify step (which still consults Rekor by default) can't find the entry it expects. ## Fix Explicitly `--tlog-upload=true` on both `cosign sign` and `cosign attest`. This: - Keeps the verify commands at the canonical default shape (no `--insecure-ignore-tlog`, no `--rekor-url` override). A self-hoster's verify command is plain `cosign verify --key ... <image>@<digest>` — what they'd type by reflex. - Restores the public-timestamping property: every release now has a Rekor entry saying "this key signed this digest at time T", which is useful forensically if the key ever leaks. - Takes an explicit runtime dependency on `rekor.sigstore.dev`. If Rekor is unreachable during a tag push, the sign step fails and the release doesn't publish; that's the intentional fail-closed behaviour, documented. ## Files | File | Change | |---|---| | `.forgejo/workflows/release.yml` | `cosign sign --tlog-upload=true`, `cosign attest --tlog-upload=true`, with a comment explaining the default-flip and why we're being explicit | | `docs/ci.md` | New paragraph under "Verifying a published image" explaining the Rekor dependency, how to inspect a release's entry with `cosign tree`, and the failure mode if Rekor is down | | `docs/adr/0014-release-pipeline.md` | Positive consequence: Rekor entries + timestamping. Negative consequence: runtime dep on Sigstore public-good infra, with the self-hosting escape hatch noted | ## Acceptance criteria - [x] `--tlog-upload=true` on sign and attest, with intent-explaining comments. - [x] Release-notes verify commands stay at the canonical default shape. - [x] `docs/ci.md` and `ADR-0014` document the explicit Rekor dependency. - [ ] Next tag push (e.g. `v0.0.1-rc.2`) produces an image whose signature is in Rekor — `cosign tree forge.wynning.tech/james/carol@<digest>` reports the entry; verify command from release notes succeeds with `Verified OK`. *(CI to confirm.)* ## Out of scope - Self-hosting Rekor (Trillian + rekor-server + persistent storage — multi-week infra; revisit only if Sigstore public-good becomes unacceptable). - Re-signing the existing broken release. Forward-only: the next tag push will work; the old release's signature is permanently un-verifiable because no Rekor entry was ever created for it.
fix(release): explicitly publish cosign sigs to Rekor (#81)
All checks were successful
Secrets / gitleaks (pull_request) Successful in 26s
PR / Trivy (image) (pull_request) Successful in 38s
PR / OSV-Scanner (pull_request) Successful in 40s
PR / npm audit (pull_request) Successful in 45s
PR / Lint (pull_request) Successful in 49s
PR / Static analysis (Semgrep) (pull_request) Successful in 53s
PR / Typecheck (pull_request) Successful in 1m0s
PR / Test (sqlite) (pull_request) Successful in 1m3s
PR / Test (postgres) (pull_request) Successful in 1m7s
PR / Build (pull_request) Successful in 1m13s
Release / Build, sign, and publish (push) Successful in 21s
da6eece9b0
cosign 2.5.x changed the default for --tlog-upload on keyed signing,
so the workflow's sign step stopped uploading to Sigstore's
public-good Rekor transparency log. The verify command (which still
queries Rekor by default) then fails with "no known key found for
this signature in database" because the entry isn't there.

Be explicit on both sign and attest: --tlog-upload=true. This:

  - Keeps verify commands at the canonical default shape (no
    --insecure-ignore-tlog), so what self-hosters paste from the
    release notes is plain `cosign verify --key ...`.
  - Gives every release a public, append-only Rekor entry — useful
    forensically if the key is ever compromised.
  - Takes a runtime dep on rekor.sigstore.dev; sign fails if Rekor
    is unreachable. Documented in ADR-0014 + docs/ci.md.

Old releases signed before this fix can't be re-verified without
re-signing (the legitimate Rekor entry never existed). Forward-only.

Closes #81.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
james merged commit c1097ae89b into main 2026-06-17 19:46:17 +00:00
Author
Owner

Post-merge note: this PR did upload sigs to Rekor as designed (logIndex: 1854384861 on the rc.2 sign step), but the user-visible "no known key found" verify error it was meant to fix turned out to have a different root cause — the repo was private at debug time, and cosign's anonymous GET on the cosign.pub URL was getting a Forgejo login page instead of the PEM. Once the repo was made public, verify succeeded with Existence of the claims in the transparency log was verified offline, which is the intended fast path against the embedded SignedEntryTimestamp bundle.

The --tlog-upload=true change is still a real improvement (we are genuinely uploading to Rekor now, and a future verifier without the embedded-bundle fast path can still find the entry). The missing prerequisite + troubleshooting are documented in #83 / PR #84.

Post-merge note: this PR did upload sigs to Rekor as designed (`logIndex: 1854384861` on the rc.2 sign step), but the user-visible "no known key found" verify error it was meant to fix turned out to have a different root cause — the repo was private at debug time, and cosign's anonymous GET on the `cosign.pub` URL was getting a Forgejo login page instead of the PEM. Once the repo was made public, verify succeeded with `Existence of the claims in the transparency log was verified offline`, which is the intended fast path against the embedded `SignedEntryTimestamp` bundle. The `--tlog-upload=true` change is still a real improvement (we are genuinely uploading to Rekor now, and a future verifier without the embedded-bundle fast path can still find the entry). The missing prerequisite + troubleshooting are documented in #83 / PR #84.
Sign in to join this conversation.
No description provided.