Asset-signature verification for the composite action (authenticity, not just integrity) — R6 follow-up to #502 #513

Closed
opened 2026-07-24 23:47:31 +02:00 by shipwright · 3 comments
Owner

Follow-up filed from Surveyor's review 4531 on #511 (S1, pass-with-disclosure — non-blocking, landed with #502).

The gap

The composite action.yml (#502, ADR-0008 §4a lean-D) fetches rt-<os>-<arch> and verifies it against the release's checksums.txt via sha256. That gives integrity — the asset matches the checksum manifest — but not authenticity: the asset and checksums.txt are same-origin (both fetched from the same Forgejo release, no signature). A compromise that can rewrite the release asset can rewrite checksums.txt in the same motion, and the checksum check passes.

This is honestly scoped in #502 as a minimal prototype per ADR-0009 §9 ("prototyped in Phase 0a") and is fine for the current private, single-audience distribution model. But ADR-0008 §6 R6 (distribution integrity, couples to §4a) has an authenticity half with no tracker — only #367 exists, which is unrelated docs. Filing so the deferral is tracked, not just commented (Surveyor's framing).

Scope when picked up

  • Sign the release artifacts at publish time in goreleaser.yml (goreleaser has native signs: support — cosign / gpg / minisign).
  • Verify the signature in the composite action.yml before the sha256 check (signature over the checksums.txt, then checksums.txt over the asset — the standard goreleaser chain).
  • Reference this tracker from action.yml inline (the inline pointer was deferred out of the #502 PR to avoid staling the head-pinned approval Bosun was landing — it belongs in this follow-up's own PR, which touches action.yml regardless).
  • Decide the trust root (which key, where the consumer gets it) — the real design question; it's a distribution-authenticity/values call, adjacent to ADR-0009 §9 sub-fork #3 (public-substrate strategy).

Formal acceptance criteria and disposition

The following seven ACs were recorded before implementation in comment 104351. They are checked here against PR #1058; the selected method is minisign and the gate remains fail-closed.

  • AC1 — Trusted verification root and rotation. The release channel uses the operator-managed RELEASE_TOOLKIT_MINISIGN_PUBLIC_KEY Actions variable, delivered explicitly to the composite FETCH path; it is not learned from the release being verified. The variable accepts concatenated complete public-key blocks for overlap rotation. Documentation names operator control, key-id recording, append-and-verify rotation, old-key support window, and retirement; missing or unusable root fails.
    Evidence: PR #1058 changes composite/bootstrap-rt/action.yml, all six reusable callers, scripts/fetch-rt.sh, and docs/integration.md.

  • AC2 — Publish-side signature. GoReleaser signs the exact generated checksums.txt with the operator-held RELEASE_TOOLKIT_MINISIGN_SECRET_KEY and publishes checksums.txt.minisig; signing capability and secret presence are checked before publish, and publish writes the secret only to a mode-0600 runner-temporary file.
    Evidence: .goreleaser.yaml signs pipe and .forgejo/workflows/goreleaser.yml; pinned GoReleaser v2.17.0 check reports 1 configuration file(s) validated.

  • AC3 — Verification ordering. The composite's shared FETCH implementation downloads the detached sidecar and verifies it with minisign before parsing or using checksums.txt, selecting an asset, accepting a cache hit, or computing SHA-256. The existing checksum and install path continue only after signature success. The root Docker action.yml does not fetch release assets and carries the tracker linkage separately.
    Evidence: scripts/fetch-rt.sh, composite/bootstrap-rt/action.yml, and the structural tests/workflows.bats #513 arm.

  • AC4 — Fail-closed error contract. Missing sidecar/unsigned manifest, missing or invalid public key, unverifiable or wrong-key signature, and a changed manifest each produce an attributable nonzero failure; no path warns, skips, or continues to asset use.
    Evidence: tests/fetch-rt.bats arms for tampered checksums, missing signature, missing trusted root, and wrong/unverifiable key all pass with the required early-failure assertions.

  • AC5 — Artifact-checkable controls. The implementation includes a valid signed-manifest positive control, tampered-manifest/signature, missing-signature, missing-key, wrong-key, and overlapping-key-rotation controls. The pre-existing valid checksum/asset mismatch control remains covered, so authenticity does not weaken integrity verification.
    Evidence: bats tests/fetch-rt.bats reports 1..17 with all tests passing; the full suite reports 1..153 with all tests passing.

  • AC6 — Key-material hygiene. The private signing key enters only through the operator secret, is written to a 0600 temporary file, is not committed, embedded in a release asset, passed as a command argument, or printed; the workflow unsets the secret before invoking GoReleaser. The repository tests use non-secret fake key fixtures only.
    Evidence: workflow and GoReleaser diff plus the staged-source scan report none for private-key material. Public-key delivery is external operator configuration, not repository content.

  • AC7 — Documentation and tracker linkage. The threat model now states the precise same-origin authenticity protection for checksums.txt without claiming general supply-chain attestation. action.yml references release-toolkit#513; integration/ADR/Arc42 docs name the signature asset, trust-root delivery, rotation, fail-closed behavior, and the alcatraz-infra#528 image dependency.
    Evidence: PR #1058 documentation changes and action.yml tracker comment.

Verification evidence

  • bats --print-output-on-failure tests/1..153, all tests passed.
  • go run ./cmd/rt fragment-check changelog.d → supported kinds/header/reference checks pass; density PASS.
  • shellcheck scripts/fetch-rt.sh scripts/bootstrap-rt.sh scripts/release-assets.sh, bash -n ..., and git diff --check → pass.
  • PyYAML parse of all workflow/config files → YAML parsed: 23 files.
  • go run github.com/goreleaser/goreleaser/v2@v2.17.0 check --config .goreleaser.yaml1 configuration file(s) validated.
  • A real temporary minisign key pair signed checksums.txt; scripts/fetch-rt.sh verified the detached signature and checksum before installing the test asset → exit 0.

Status

Implemented in PR #1058 and merged at exact merge commit cfffa822b188e361aafeff57ef163d176d81c670; its reviewed PR head was 9e443cebe073927c8b09da0048c01e3af705f1eb. Official Sentry review 6171 was exact-bound and current, and Forgejo CI was 25/25 successful. Current main matches the merge commit.

The merged tree enforces minisign authenticity before checksum and asset use, with hard failures for missing, invalid, wrong-key, or unverifiable signatures. The operator-managed trust root and private signing secret remain deployment configuration; no key material is placed in the repository. All seven acceptance criteria are discharged.

Not urgent

Gated on the distribution model widening beyond private single-audience. The integrity check is the correct floor for now; this is the authenticity ceiling for when adoption justifies it.

Refs: #502 · #511 (review 4531, S1) · ADR-0008 §4a lean-D + §6 R6 · ADR-0009 §9

Follow-up filed from Surveyor's review 4531 on #511 (S1, pass-with-disclosure — non-blocking, landed with #502). ## The gap The composite `action.yml` (#502, ADR-0008 §4a lean-D) fetches `rt-<os>-<arch>` and verifies it against the release's `checksums.txt` via sha256. That gives **integrity** — the asset matches the checksum manifest — but **not authenticity**: the asset and `checksums.txt` are **same-origin** (both fetched from the same Forgejo release, no signature). A compromise that can rewrite the release asset can rewrite `checksums.txt` in the same motion, and the checksum check passes. This is honestly scoped in #502 as a **minimal prototype** per ADR-0009 §9 ("prototyped in Phase 0a") and is fine for the current **private, single-audience** distribution model. But ADR-0008 §6 **R6 (distribution integrity, couples to §4a)** has an authenticity half with **no tracker** — only #367 exists, which is unrelated docs. Filing so the deferral is **tracked, not just commented** (Surveyor's framing). ## Scope when picked up - Sign the release artifacts at publish time in `goreleaser.yml` (goreleaser has native `signs:` support — cosign / gpg / minisign). - Verify the signature in the composite `action.yml` **before** the sha256 check (signature over the checksums.txt, then checksums.txt over the asset — the standard goreleaser chain). - **Reference this tracker from `action.yml`** inline (the inline pointer was deferred out of the #502 PR to avoid staling the head-pinned approval Bosun was landing — it belongs in this follow-up's own PR, which touches `action.yml` regardless). - Decide the trust root (which key, where the consumer gets it) — the real design question; it's a distribution-authenticity/values call, adjacent to ADR-0009 §9 sub-fork #3 (public-substrate strategy). ## Formal acceptance criteria and disposition The following seven ACs were recorded before implementation in comment 104351. They are checked here against PR #1058; the selected method is minisign and the gate remains fail-closed. - [x] **AC1 — Trusted verification root and rotation.** The release channel uses the operator-managed `RELEASE_TOOLKIT_MINISIGN_PUBLIC_KEY` Actions variable, delivered explicitly to the composite FETCH path; it is not learned from the release being verified. The variable accepts concatenated complete public-key blocks for overlap rotation. Documentation names operator control, key-id recording, append-and-verify rotation, old-key support window, and retirement; missing or unusable root fails. Evidence: PR #1058 changes `composite/bootstrap-rt/action.yml`, all six reusable callers, `scripts/fetch-rt.sh`, and `docs/integration.md`. - [x] **AC2 — Publish-side signature.** GoReleaser signs the exact generated `checksums.txt` with the operator-held `RELEASE_TOOLKIT_MINISIGN_SECRET_KEY` and publishes `checksums.txt.minisig`; signing capability and secret presence are checked before publish, and publish writes the secret only to a mode-0600 runner-temporary file. Evidence: `.goreleaser.yaml` `signs` pipe and `.forgejo/workflows/goreleaser.yml`; pinned GoReleaser v2.17.0 `check` reports `1 configuration file(s) validated`. - [x] **AC3 — Verification ordering.** The composite's shared FETCH implementation downloads the detached sidecar and verifies it with minisign before parsing or using `checksums.txt`, selecting an asset, accepting a cache hit, or computing SHA-256. The existing checksum and install path continue only after signature success. The root Docker `action.yml` does not fetch release assets and carries the tracker linkage separately. Evidence: `scripts/fetch-rt.sh`, `composite/bootstrap-rt/action.yml`, and the structural `tests/workflows.bats` #513 arm. - [x] **AC4 — Fail-closed error contract.** Missing sidecar/unsigned manifest, missing or invalid public key, unverifiable or wrong-key signature, and a changed manifest each produce an attributable nonzero failure; no path warns, skips, or continues to asset use. Evidence: `tests/fetch-rt.bats` arms for tampered checksums, missing signature, missing trusted root, and wrong/unverifiable key all pass with the required early-failure assertions. - [x] **AC5 — Artifact-checkable controls.** The implementation includes a valid signed-manifest positive control, tampered-manifest/signature, missing-signature, missing-key, wrong-key, and overlapping-key-rotation controls. The pre-existing valid checksum/asset mismatch control remains covered, so authenticity does not weaken integrity verification. Evidence: `bats tests/fetch-rt.bats` reports `1..17` with all tests passing; the full suite reports `1..153` with all tests passing. - [x] **AC6 — Key-material hygiene.** The private signing key enters only through the operator secret, is written to a 0600 temporary file, is not committed, embedded in a release asset, passed as a command argument, or printed; the workflow unsets the secret before invoking GoReleaser. The repository tests use non-secret fake key fixtures only. Evidence: workflow and GoReleaser diff plus the staged-source scan report `none` for private-key material. Public-key delivery is external operator configuration, not repository content. - [x] **AC7 — Documentation and tracker linkage.** The threat model now states the precise same-origin authenticity protection for `checksums.txt` without claiming general supply-chain attestation. `action.yml` references `release-toolkit#513`; integration/ADR/Arc42 docs name the signature asset, trust-root delivery, rotation, fail-closed behavior, and the `alcatraz-infra#528` image dependency. Evidence: PR #1058 documentation changes and `action.yml` tracker comment. ## Verification evidence - `bats --print-output-on-failure tests/` → `1..153`, all tests passed. - `go run ./cmd/rt fragment-check changelog.d` → supported kinds/header/reference checks pass; density PASS. - `shellcheck scripts/fetch-rt.sh scripts/bootstrap-rt.sh scripts/release-assets.sh`, `bash -n ...`, and `git diff --check` → pass. - PyYAML parse of all workflow/config files → `YAML parsed: 23 files`. - `go run github.com/goreleaser/goreleaser/v2@v2.17.0 check --config .goreleaser.yaml` → `1 configuration file(s) validated`. - A real temporary minisign key pair signed `checksums.txt`; `scripts/fetch-rt.sh` verified the detached signature and checksum before installing the test asset → exit 0. ## Status Implemented in PR #1058 and merged at exact merge commit `cfffa822b188e361aafeff57ef163d176d81c670`; its reviewed PR head was `9e443cebe073927c8b09da0048c01e3af705f1eb`. Official Sentry review 6171 was exact-bound and current, and Forgejo CI was 25/25 successful. Current `main` matches the merge commit. The merged tree enforces minisign authenticity before checksum and asset use, with hard failures for missing, invalid, wrong-key, or unverifiable signatures. The operator-managed trust root and private signing secret remain deployment configuration; no key material is placed in the repository. All seven acceptance criteria are discharged. ## Not urgent Gated on the distribution model widening beyond private single-audience. The integrity check is the correct floor for now; this is the authenticity ceiling for when adoption justifies it. Refs: #502 · #511 (review 4531, S1) · ADR-0008 §4a lean-D + §6 R6 · ADR-0009 §9
Owner

OPERATOR RULING: minisign.

Not cosign, not GPG — and the deciding question was who verifies, not which is strongest.

minisign        one keypair, a short verification step, no infrastructure
cosign keyless  the supply-chain standard, wants an OIDC provider we do not have
GPG             ubiquitous, and key management is the whole cost

Today the verifier is us and any adopter on a private forge.

  • Sign release assets with minisign; publish the signature alongside checksums.txt
  • The composite action verifies the signature, not only the checksum
  • Write the threat model down in the same PR: "we sign so a compromised forge cannot silently swap an asset"not "we are supply-chain attested"

⚠️ The gap this closes, restated so the scope stays honest: the asset and its checksums.txt are same-origin, so anything able to rewrite one can rewrite both. Integrity yes, authenticity no — confirmed at source by @shipwright: action.yml carries 2 checksum references and 0 for any signing tool.

✅ **OPERATOR RULING: minisign.** Not cosign, not GPG — and the deciding question was **who verifies**, not which is strongest. ``` minisign one keypair, a short verification step, no infrastructure cosign keyless the supply-chain standard, wants an OIDC provider we do not have GPG ubiquitous, and key management is the whole cost ``` **Today the verifier is us and any adopter on a private forge.** - [ ] Sign release assets with minisign; publish the signature alongside `checksums.txt` - [ ] The composite action verifies the signature, not only the checksum - [ ] **Write the threat model down in the same PR**: *"we sign so a compromised forge cannot silently swap an asset"* — **not** *"we are supply-chain attested"* ⚠️ **The gap this closes, restated so the scope stays honest:** the asset and its `checksums.txt` are **same-origin**, so anything able to rewrite one can rewrite both. *Integrity yes, authenticity no* — confirmed at source by **@shipwright**: `action.yml` carries 2 checksum references and 0 for any signing tool.
Owner

Explicit acceptance criteria: minisign asset authenticity

No implementation changes are being made before these criteria are recorded.

  • AC1 — Trusted verification root and rotation. Define one operator-designated minisign public-key root for the release channel, delivered to the composite verifier through an explicit documented runtime/configuration path rather than learned from the release being verified. Document the key identifier/fingerprint, who controls delivery, and the rotation procedure (including how a new key becomes trusted and how the old key is retired). A missing or unusable root is an error, not an implicit trust fallback.
  • AC2 — Publish-side signature. At the release publish boundary, sign the exact generated checksums.txt with the operator-held minisign private key and publish the detached signature alongside it (for example checksums.txt.minisig). Signing or upload failure must fail the publish path; a release must not be reported complete without the signature.
  • AC3 — Verification ordering. In action.yml, download the detached signature and verify it with minisign and the trusted public key before parsing/using checksums.txt, before selecting an asset, and before any sha256 or cache acceptance. Only a valid signature permits the existing checksum and install path to continue.
  • AC4 — Fail-closed error contract. Missing signature, unsigned manifest, missing/invalid public key, unverifiable signature, wrong-key signature, and signature/checksum-manifest mismatch each produce a nonzero failure with an attributable diagnostic. None may be downgraded to a warning, skipped, or continued asset use.
  • AC5 — Artifact-checkable controls. Include a valid signed-manifest positive control; a tampered-manifest/signature control; a missing-signature control; and a missing-key or wrong-key control. Each inverse control must assert nonzero exit and prove that checksum parsing/asset use does not proceed. Preserve a valid checksum/asset mismatch control so signature verification does not weaken the existing integrity check.
  • AC6 — Key-material hygiene. The signing/private key is supplied only through the operator's secret mechanism and never committed, embedded in repository artifacts, placed in a generated release asset, exposed in command arguments, or printed to logs. Verification output may identify the public-key fingerprint/key id but must not disclose private key material or secret values. Add a repository/log scan or equivalent control for the implementation surface.
  • AC7 — Documentation and tracker linkage. Update the threat model in this same change to say precisely that minisign protects authenticity of checksums.txt against same-origin release rewriting; do not claim general supply-chain attestation. Add an inline reference to this tracker in action.yml, and document the trust-root delivery/rotation contract and signature asset name.

Verification evidence for the completed PR must include the exact publish/sign and composite verification commands, the positive and inverse-control outputs/exit codes, and the CI result.

## Explicit acceptance criteria: minisign asset authenticity No implementation changes are being made before these criteria are recorded. - [ ] **AC1 — Trusted verification root and rotation.** Define one operator-designated minisign public-key root for the release channel, delivered to the composite verifier through an explicit documented runtime/configuration path rather than learned from the release being verified. Document the key identifier/fingerprint, who controls delivery, and the rotation procedure (including how a new key becomes trusted and how the old key is retired). A missing or unusable root is an error, not an implicit trust fallback. - [ ] **AC2 — Publish-side signature.** At the release publish boundary, sign the exact generated `checksums.txt` with the operator-held minisign private key and publish the detached signature alongside it (for example `checksums.txt.minisig`). Signing or upload failure must fail the publish path; a release must not be reported complete without the signature. - [ ] **AC3 — Verification ordering.** In `action.yml`, download the detached signature and verify it with minisign and the trusted public key before parsing/using `checksums.txt`, before selecting an asset, and before any sha256 or cache acceptance. Only a valid signature permits the existing checksum and install path to continue. - [ ] **AC4 — Fail-closed error contract.** Missing signature, unsigned manifest, missing/invalid public key, unverifiable signature, wrong-key signature, and signature/checksum-manifest mismatch each produce a nonzero failure with an attributable diagnostic. None may be downgraded to a warning, skipped, or continued asset use. - [ ] **AC5 — Artifact-checkable controls.** Include a valid signed-manifest positive control; a tampered-manifest/signature control; a missing-signature control; and a missing-key or wrong-key control. Each inverse control must assert nonzero exit and prove that checksum parsing/asset use does not proceed. Preserve a valid checksum/asset mismatch control so signature verification does not weaken the existing integrity check. - [ ] **AC6 — Key-material hygiene.** The signing/private key is supplied only through the operator's secret mechanism and never committed, embedded in repository artifacts, placed in a generated release asset, exposed in command arguments, or printed to logs. Verification output may identify the public-key fingerprint/key id but must not disclose private key material or secret values. Add a repository/log scan or equivalent control for the implementation surface. - [ ] **AC7 — Documentation and tracker linkage.** Update the threat model in this same change to say precisely that minisign protects authenticity of `checksums.txt` against same-origin release rewriting; do not claim general supply-chain attestation. Add an inline reference to this tracker in `action.yml`, and document the trust-root delivery/rotation contract and signature asset name. Verification evidence for the completed PR must include the exact publish/sign and composite verification commands, the positive and inverse-control outputs/exit codes, and the CI result.
Owner

Artifact correction requested by Sentry review 6168: the exact tree has 17 tests in tests/fetch-rt.bats, so its exact-tree run is 1..17; only the full bats tests/ run is 1..153. Please correct AC5/evidence to those two counts. No code change or new review request is needed; preserve the existing exact head and review row.

Artifact correction requested by Sentry review 6168: the exact tree has 17 tests in `tests/fetch-rt.bats`, so its exact-tree run is `1..17`; only the full `bats tests/` run is `1..153`. Please correct AC5/evidence to those two counts. No code change or new review request is needed; preserve the existing exact head and review row.
Sign in to join this conversation.
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#513
No description provided.