Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 32 additions & 20 deletions .github/workflows/prune-release-assets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ name: prune-release-assets
# Every release publishes ~6 archives at ~15.8 MB of binary each plus 4
# .rpm/.deb packages; that is the entire storage cost. Alongside them it
# publishes a handful of kilobyte-sized files: checksums.txt, its keyless cosign
# signature (checksums.txt.sig) and certificate (checksums.txt.pem), and one
# signature (checksums.txt.bundle — and, on releases up to v0.22.0, the earlier
# detached checksums.txt.sig / checksums.txt.pem pair), and one
# SPDX SBOM per archive/package. Those small files are the integrity and
# supply-chain record an air-gapped / FSTEC-regulated consumer may have to
# produce years after the binaries are gone — and because builds in this repo
Expand All @@ -20,12 +21,15 @@ name: prune-release-assets
# were kilobytes.
#
# So this workflow deletes ONLY *.tar.gz, *.zip, *.rpm, *.deb, and it keeps
# checksums.txt, *.sig, *.pem, the *_sboms.tar.gz bundle and any legacy
# *.spdx.json unconditionally (belt-and-braces:
# there is both a delete-list and an explicit always-keep list below). Do NOT
# checksums.txt, its signature (*.bundle, plus the legacy *.sig / *.pem pair),
# the *_sboms.tar.gz bundle and any legacy *.spdx.json unconditionally
# (belt-and-braces: there is both a delete-list and an explicit always-keep list
# below). Both signature spellings are on the keep-list because the two eras of
# releases carry different ones and this workflow runs across all of them. Do NOT
# "simplify" this later into deleting every asset — that trades the entire audit
# trail for zero additional bytes and is the one thing the design exists to
# prevent.
# prevent. Whenever the `signs:` block in .goreleaser.yaml changes the name it
# writes, the keep-list below has to learn the new suffix in the same commit.
#
# It never deletes a release and never touches a tag: tags and release notes are
# the project's history, v* tags are immutable (a tag ruleset enforces it), and
Expand Down Expand Up @@ -153,25 +157,32 @@ jobs:
echo "protected tags: $(jq -r 'join(", ")' "$TMP/protected.json")"
echo

# Heavy build outputs only. The always-keep list is redundant with the
# NOTE the SBOM bundle is koc_<ver>_sboms.tar.gz — it DOES end in
# .tar.gz, so unlike the other retained files it is not naturally
# excluded by the suffix test and the always_keep guard is what saves
# it. Deleting it would leave a release whose checksums.txt still lists
# an SBOM nobody can fetch. Releases cut before the bundle landed have
# loose per-artifact documents instead, hence both patterns.
# (A per-artifact SBOM is named koc_X_os_arch.tar.gz.spdx.json,
# which does NOT end in .tar.gz) and that redundancy is deliberate:
# if someone ever loosens the delete-list, the audit-trail files still
# survive.
# Heavy build outputs only, with the always_keep list below as a second
# guard.
#
# For most retained files that guard is redundant with the suffix test:
# a per-artifact SBOM is named koc_X_os_arch.tar.gz.spdx.json, which
# does NOT end in .tar.gz, so `heavy` never matches it in the first
# place. The redundancy is deliberate — if someone later loosens the
# delete-list, the audit-trail files still survive.
#
# The SBOM bundle is the one exception: koc_<ver>_sboms.tar.gz DOES end
# in .tar.gz, so always_keep is the only thing standing between it and
# deletion. Losing it would leave a release whose checksums.txt lists
# an SBOM nobody can fetch.
#
# Two spellings appear for both the SBOM and the signature because this
# workflow runs across every release: the bundle forms (_sboms.tar.gz,
# checksums.txt.bundle) from v0.23.0, and the loose forms (.spdx.json,
# checksums.txt.sig/.pem) on everything before it.
jq -r --slurpfile protected "$TMP/protected.json" '
def heavy:
endswith(".tar.gz") or endswith(".zip")
or endswith(".rpm") or endswith(".deb");
def always_keep:
. == "checksums.txt" or endswith(".spdx.json")
or endswith("_sboms.tar.gz")
or endswith(".sig") or endswith(".pem");
or endswith(".bundle") or endswith(".sig") or endswith(".pem");
($protected[0]) as $keepTags
| .[]
| select(.draft | not) # never touch drafts
Expand All @@ -196,8 +207,8 @@ jobs:
echo
echo "> Pruned versions can no longer be installed via \`brew install ftarasenko/tap/koc\`,"
echo "> and any mirror that downloads koc binaries from GitHub on demand will 404 for them."
echo "> Tags, release notes, \`checksums.txt\`(+\`.sig\`/\`.pem\`) and the SPDX SBOMs are retained,"
echo "> and builds are byte-reproducible from the tag."
echo "> Tags, release notes, \`checksums.txt\`(+ its \`.bundle\`, or \`.sig\`/\`.pem\` on older"
echo "> releases) and the SPDX SBOMs are retained, and builds are byte-reproducible from the tag."
fi
echo
echo "Keep window: **$KEEP** most recent non-draft, non-prerelease releases; latest (\`${LATEST_TAG:-none}\`) always kept."
Expand Down Expand Up @@ -295,7 +306,8 @@ jobs:
printf '> **Build artifacts pruned to save storage.** The archives and packages\n'
printf '> (`.tar.gz`, `.zip`, `.rpm`, `.deb`) for this release have been deleted. The tag,\n'
printf '> these release notes, `checksums.txt` with its cosign signature\n'
printf '> (`checksums.txt.sig`/`.pem`) and the SPDX SBOMs are retained. Builds are\n'
printf '> (`checksums.txt.bundle`, or `.sig`/`.pem` on releases up to v0.22.0) and the\n'
printf '> SPDX SBOMs are retained. Builds are\n'
printf '> byte-reproducible from tag `%s`, so the binaries can be rebuilt and verified\n' "$tag"
printf '> against the retained `checksums.txt`. Consequence: `brew install` and any\n'
printf '> on-demand mirror will no longer find binaries for this version — use a newer\n'
Expand Down
23 changes: 15 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,16 @@ jobs:

# cosign and syft are required by the `signs:` and `sboms:` blocks in
# .goreleaser.yaml. Both are official installers from the projects
# themselves.
# Pinned to full commit SHAs per repo convention. Both are still on their
# original major here; Dependabot's github-actions ecosystem proposes major
# bumps as reviewable PRs (cosign-installer v4 and sbom-action have both
# moved on), which is the right place to verify changed inputs.
# themselves, pinned to full commit SHAs per repo convention.
#
# cosign-installer v4 installs cosign v3 by default, and v3 turns the
# standardized Sigstore bundle on by default — which is why the `signs:`
# block writes checksums.txt.bundle rather than a detached .sig/.pem pair.
# The version is deliberately NOT pinned via `cosign-release`: the two
# move together, and letting the installer choose keeps the tool current
# with Dependabot's reviewable bumps. If that default ever has to be
# pinned, the floor is v3 — the v2 flags this repo used before are ignored
# under the new bundle format (see the note in .goreleaser.yaml).
- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

Expand All @@ -121,9 +126,11 @@ jobs:
# proves what produced them. Verify with:
# gh attestation verify koc_<v>_linux_amd64.tar.gz \
# --repo ftarasenko/go-openstackclient
# Pinned to a full commit SHA per repo convention. Held at v2 because the
# inputs below were written against it; v4 is current, and Dependabot will
# propose that bump as a reviewable PR.
# Pinned to a full commit SHA per repo convention. On v4, which upstream
# has reduced to a thin wrapper over actions/attest — `subject-path` still
# behaves as it did on v2, which is the only input used here. New work
# should reach for actions/attest directly; this stays until there is a
# reason to move it.
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/supply-chain.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,9 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Pinned to a full commit SHA per repo convention. Held on v4 because the
# inputs below were written against it; v5 is current, and Dependabot will
# propose that bump as a reviewable PR.
# Pinned to a full commit SHA per repo convention. On v5, whose only
# breaking change was the node24 runtime (needs runner >= v2.327.1, which
# ubuntu-latest satisfies); both inputs below are unchanged from v4.
- name: Dependency review
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
Expand Down
27 changes: 18 additions & 9 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,29 +145,38 @@ sboms:

# Keyless cosign signature over checksums.txt. checksums.txt alone is not an
# integrity story — whoever can swap an artifact in the release can swap the
# checksums with it — but a Sigstore-backed detached signature over it is: the
# signature roots the trust and the checksums chain to every artifact.
# checksums with it — but a Sigstore-backed signature over it is: the signature
# roots the trust and the checksums chain to every artifact.
#
# Verify (needs the cert, the sig and the file; no key distribution):
# cosign verify-blob checksums.txt \
# --certificate checksums.txt.pem --signature checksums.txt.sig \
# Verify (the bundle carries signature, certificate and transparency-log
# inclusion proof, so it is the only extra file needed; no key distribution):
# cosign verify-blob checksums.txt --bundle checksums.txt.bundle \
# --certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \
# --certificate-oidc-issuer https://token.actions.githubusercontent.com
# sha256sum -c checksums.txt
#
# --new-bundle-format is cosign v3's default, but it is named explicitly here on
# purpose: the previous config passed --output-signature/--output-certificate,
# and when that default flipped from false to true in v3 those flags became
# silently IGNORED ("deprecated when using --new-bundle-format and will be
# ignored") rather than an error. The release then produced no .sig/.pem at all,
# and because release.yml only runs on a v* tag, nothing would have caught it
# before the tag was already burned. Stating the format keeps this block correct
# under either default, and it is what makes an air-gapped `--trusted-root`
# verification possible (that flag requires the new format) — see SECURITY.md.
#
# Needs `id-token: write` in the release workflow (the OIDC token is what
# Fulcio signs) plus cosign on PATH — both wired in release.yml.
signs:
- id: cosign-checksums
cmd: cosign
artifacts: checksum
signature: "${artifact}.sig"
certificate: "${artifact}.pem"
signature: "${artifact}.bundle"
output: true
args:
- sign-blob
- "--output-signature=${signature}"
- "--output-certificate=${certificate}"
- "--new-bundle-format"
- "--bundle=${signature}"
- "${artifact}"
- "--yes"

Expand Down
23 changes: 20 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,19 +373,36 @@ weakened by a check that happens to need a proxy.
- `.github/workflows/prune-release-assets.yml` — dispatch (`dry_run` defaults to
true) to reclaim storage by deleting `.tar.gz`/`.zip`/`.rpm`/`.deb` from
releases outside a keep window, while **retaining** `checksums.txt`, its
signature/certificate and the SBOM bundle — note the bundle ends in `.tar.gz`,
so its always-keep guard, not the suffix filter, is what protects it. Because
builds are byte-reproducible from
cosign bundle (plus the older detached signature/certificate) and the SBOM
bundle — note the SBOM bundle ends in `.tar.gz`, so its always-keep guard, not
the suffix filter, is what protects it. Because builds are byte-reproducible from
the tag, a pruned release stays verifiable and re-derivable; deleting the
checksums would end that, which is why the retain list is redundant with the
delete list on purpose. Pruned versions stop installing via the Homebrew cask.
Its keep-list is keyed on asset **suffix**, so changing what `signs:` writes
means editing that jq filter in the same commit.

`checksums.txt` on its own is not an integrity story: whoever can swap an artifact
in a release can swap the checksums with it. The cosign signature is what roots
the trust, and the checksums then chain to every artifact. The verification
command (no key distribution needed) is in `.goreleaser.yaml` next to the `signs:`
block and in README "Prebuilt binaries".

**The signature is a Sigstore bundle (`checksums.txt.bundle`), not a detached
`.sig`/`.pem` pair.** cosign v3 made the standardized bundle the default, and the
migration was not optional: under that default the `--output-signature` /
`--output-certificate` flags the old config passed are *silently ignored* rather
than rejected, so the release would have published no signature at all — and
since `release.yml` only runs on a `v*` tag, no PR check could have caught it.
The bundle also carries the transparency-log inclusion proof, which is what makes
the air-gapped `--trusted-root` verification in SECURITY.md possible; cosign v3
deprecated `--offline` in favour of exactly that. Three consequences to keep in
mind when touching this: verifying needs cosign v3+ (or v2.6+ with an explicit
`--new-bundle-format`); releases up to v0.22.0 still carry the old pair and a v3
client falls back to it automatically; and `.goreleaser.yaml` names
`--new-bundle-format` explicitly so a future default flip cannot repeat the
failure silently.

**GoReleaser has no `before:` hooks, on purpose.** Hooks run inside the same
workflow step as GoReleaser, and that step carries `HOMEBREW_TAP_TOKEN` — a PAT
with write access to a second repository. A `go run ./cmd/koc completion …` hook
Expand Down
28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,13 @@ darwin/amd64, darwin/arm64, windows/amd64, windows/arm64** with a
Alongside them: **`.rpm` and `.deb` packages** for the two linux architectures
(for a local yum/apt mirror on an air-gapped node — these do install the shell
completions), an **SPDX SBOM bundle** (`koc_<ver>_sboms.tar.gz`, one
`*.spdx.json` per artifact inside), a keyless **cosign
signature** over `checksums.txt` (`checksums.txt.sig` + `.pem`), and a GitHub
`*.spdx.json` per artifact inside), a keyless **cosign signature** over
`checksums.txt` (`checksums.txt.bundle`), and a GitHub
**build-provenance attestation**. Builds are byte-reproducible, so the checksums
can be re-derived independently from the tag.

```sh
cosign verify-blob checksums.txt \
--certificate checksums.txt.pem --signature checksums.txt.sig \
cosign verify-blob checksums.txt --bundle checksums.txt.bundle \
--certificate-identity-regexp 'https://github.com/ftarasenko/go-openstackclient/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum -c checksums.txt
Expand All @@ -98,12 +97,23 @@ The two identity flags are the part that matters — without them you have check
only that *somebody* signed the file. Verify the signature first, then let
`sha256sum -c` carry that trust to each artifact.

`checksums.txt.bundle` is a [Sigstore bundle][sigstore-bundle]: it carries the
signature, the signing certificate and the transparency-log inclusion proof in
one file, so nothing else has to be downloaded alongside it. Reading it needs
**cosign v3 or newer** (where the format is the default), or cosign v2.6+ with an
explicit `--new-bundle-format`. Releases up to and including **v0.22.0** instead
carry a detached `checksums.txt.sig` + `.pem` pair; a v3 client still verifies
those — pass `--certificate`/`--signature` in place of `--bundle` and it falls
back to the legacy path automatically.

Both commands reach the network by default (`cosign` consults the public Rekor
log, `gh` calls the GitHub API), so on an isolated network capture a
`--bundle` on a connected host and verify from it with `cosign verify-blob
--bundle … --offline`. `sha256sum -c checksums.txt` is fully offline and is what
you gate the install on. See [SECURITY.md](SECURITY.md) for the air-gapped
procedure, the supported-version policy, and how to report a vulnerability.
log, `gh` calls the GitHub API). On an isolated network, verify the bundle
against a `--trusted-root` file captured on a connected host — no Rekor call, no
`--offline` flag. `sha256sum -c checksums.txt` is fully offline and is what you
gate the install on. See [SECURITY.md](SECURITY.md) for that procedure, the
supported-version policy, and how to report a vulnerability.

[sigstore-bundle]: https://blog.sigstore.dev/cosign-3-0-available/

## Build

Expand Down
Loading
Loading