Skip to content

feat: sign releases and publish SBOMs#33

Merged
hudsonaikins merged 1 commit into
mainfrom
codex/release-trust
Apr 12, 2026
Merged

feat: sign releases and publish SBOMs#33
hudsonaikins merged 1 commit into
mainfrom
codex/release-trust

Conversation

@hudsonaikins
Copy link
Copy Markdown
Contributor

@hudsonaikins hudsonaikins commented Apr 12, 2026

Summary

  • generate SPDX JSON SBOMs for release archives and source snapshots
  • sign release archives, SBOMs, and checksum manifests with Cosign
  • publish verification assets and document the verification flow

Testing

  • env GOCACHE=/tmp/profitctl-go-build GOTMPDIR=/tmp/profitctl-go-tmp go test ./...
  • bash -n scripts/release/install-tool.sh
  • bash -n scripts/release/generate-sboms.sh
  • bash -n scripts/release/sign-release-assets.sh
  • bash -n scripts/release/verify-release-assets.sh
  • bash -n scripts/release/publish-github-release.sh
  • bash -n scripts/release/smoke-published-release.sh
  • bash scripts/release/install-tool.sh syft /tmp/profitctl-tooltest-net/syft
  • bash scripts/release/install-tool.sh cosign /tmp/profitctl-tooltest-net/cosign
  • bash scripts/release/build-artifacts.sh v0.1.3-test
  • bash scripts/release/create-checksums.sh v0.1.3-test
  • bash scripts/release/generate-sboms.sh v0.1.3-test
  • bash scripts/release/sign-release-assets.sh v0.1.3-test
  • bash scripts/release/verify-release-assets.sh v0.1.3-test dist/v0.1.3-test keys/profitctl-release-cosign.pub

Closes #27

Greptile Summary

This PR introduces SBOM generation (via Syft, SPDX JSON format) for each release archive and the source tree, detached Cosign signatures for archives, SBOMs, and SHA256SUMS, and publishes the release public key (profitctl-release-cosign.pub) to GitHub Releases. The Woodpecker pipeline gains generate-sboms.sh, sign-release-assets.sh, and a smoke-published-release stage that downloads and re-verifies the published artifacts end-to-end.

Confidence Score: 4/5

Do not merge until the cosign bootstrap verification and the curl | sh signing-step risk are addressed.

Two P1 security findings: the cosign binary is used to sign every release artifact yet is itself fetched without any integrity check, and the Doppler CLI installer runs as an unverified remote script immediately before signing secrets are injected. Either issue could silently compromise the release signing chain. The remaining finding is P2 style.

scripts/release/install-tool.sh (cosign download, lines 50–54) and .woodpecker.yml (curl | sh in publish and smoke steps)

Security Review

  • Unverified binary execution (install-tool.sh, lines 50–54): The cosign binary is downloaded from GitHub and made executable without checksum verification. Compromise of this binary would silently invalidate the entire signing chain.
  • curl | sh in privileged CI steps (.woodpecker.yml, lines 33 and ~53): The Doppler CLI installer runs as an unverified remote script immediately before signing secrets are injected, creating a PATH-shim / hook injection vector for credential exfiltration.

Important Files Changed

Filename Overview
scripts/release/install-tool.sh Downloads syft and cosign binaries without verifying their checksums or signatures before use — bootstrapping risk for the security toolchain.
.woodpecker.yml Tag-triggered pipeline adds SBOM, signing, and smoke stages; the signing and smoke steps install Doppler via `curl
scripts/release/publish-github-release.sh Publishes all release assets via gh release upload; two early-exit error messages are missing the >&2 redirect, inconsistent with the rest of the codebase.
scripts/release/sign-release-assets.sh Signs archives, SBOMs, and SHA256SUMS with Cosign using key-from-env; correctly isolates HOME to avoid contaminating system config.
scripts/release/verify-release-assets.sh Verifies Cosign signatures and SHA256 checksums for archive, SBOM, and checksum manifest; handles both BSD and GNU shasum formats correctly.
scripts/release/generate-sboms.sh Generates SPDX JSON SBOMs for each release archive and the source tree using Syft; correctly excludes .git and dist directories.
scripts/release/smoke-published-release.sh Downloads the published release from GitHub, re-verifies signatures/checksums, extracts the binary, and runs functional smoke tests.
keys/profitctl-release-cosign.pub ECDSA P-256 public key committed for release-signature verification; correct approach for a key-pair signing workflow.

Sequence Diagram

sequenceDiagram
    participant WP as Woodpecker CI
    participant BA as build-and-package step
    participant PV as publish-release-and-vps step
    participant SM as smoke-published-release step
    participant GH as GitHub Releases

    WP->>BA: trigger on tag v*.*.*
    BA->>BA: install-tool.sh syft
    BA->>BA: build-artifacts.sh
    BA->>BA: create-checksums.sh (SHA256SUMS)
    BA->>BA: generate-sboms.sh (*.spdx.json)

    WP->>PV: artifacts passed via dist/TAG/
    PV->>PV: install-tool.sh cosign
    PV->>PV: doppler run → sign-release-assets.sh
    Note over PV: signs *.tar.gz, *.zip, *.spdx.json, SHA256SUMS

    PV->>GH: publish-github-release.sh

    WP->>SM: after publish
    SM->>SM: install-tool.sh cosign
    SM->>GH: gh release download
    SM->>SM: verify-release-assets.sh
    SM->>SM: extract + smoke test binary
    SM-->>WP: smoke passed
Loading

Comments Outside Diff (1)

  1. scripts/release/publish-github-release.sh, line 6-12 (link)

    P2 Error messages not redirected to stderr

    Both early-exit echo calls write to stdout instead of stderr, which is inconsistent with every other error message in the release scripts and breaks any caller that redirects stdout.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: scripts/release/publish-github-release.sh
    Line: 6-12
    
    Comment:
    **Error messages not redirected to stderr**
    
    Both early-exit `echo` calls write to stdout instead of stderr, which is inconsistent with every other error message in the release scripts and breaks any caller that redirects stdout.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: scripts/release/install-tool.sh
Line: 50-54

Comment:
**cosign binary downloaded without integrity verification**

The cosign binary is fetched over HTTPS and made executable immediately, with no checksum or signature check. If the GitHub release asset is tampered with or the CDN is compromised, the binary used to sign every subsequent release artifact would be malicious — undermining the entire signing chain. Sigstore publishes `cosign-checksums.txt` alongside each release; verifying the SHA-256 of the downloaded binary against that file (or a pinned hash in this repo) would close this gap.

```suggestion
  cosign)
    VERSION="${COSIGN_VERSION:-v3.0.6}"
    CHECKSUM_URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/cosign-checksums.txt"
    BINARY="cosign-${OS_NAME}-${ARCH_NAME}"
    URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/${BINARY}"
    download "${URL}" "${TMP_DIR}/${BINARY}"
    download "${CHECKSUM_URL}" "${TMP_DIR}/cosign-checksums.txt"
    (cd "${TMP_DIR}" && grep "${BINARY}" cosign-checksums.txt | sha256sum --check --status)
    cp "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/cosign"
    chmod +x "${INSTALL_DIR}/cosign"
    ;;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: .woodpecker.yml
Line: 33

Comment:
**`curl | sh` installer runs before signing secrets are injected**

`curl -Ls https://cli.doppler.com/install.sh | sh` executes an unverified remote script in the same shell session that subsequently calls `doppler run ... -- bash scripts/release/sign-release-assets.sh`. A compromised or MITMed installer could plant a `doppler` shim on `PATH` or create bash hooks that exfiltrate `COSIGN_PRIVATE_KEY` and `COSIGN_PASSWORD` the moment `doppler run` injects them. The same pattern appears in the `smoke-published-release` step (line ~53). Pinning the Doppler CLI to a known version and verifying its checksum before execution, or pre-baking it into the CI image, would remove this risk.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/release/publish-github-release.sh
Line: 6-12

Comment:
**Error messages not redirected to stderr**

Both early-exit `echo` calls write to stdout instead of stderr, which is inconsistent with every other error message in the release scripts and breaks any caller that redirects stdout.

```suggestion
if [[ -z "${TAG}" ]]; then
  echo "TAG is required" >&2
  exit 1
fi

if [[ -z "${GITHUB_TOKEN_RELEASE:-}" ]]; then
  echo "GITHUB_TOKEN_RELEASE is required" >&2
  exit 1
fi
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: sign releases and publish sboms" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@hudsonaikins hudsonaikins merged commit 76284e3 into main Apr 12, 2026
3 checks passed
@hudsonaikins hudsonaikins deleted the codex/release-trust branch April 12, 2026 17:27
Comment on lines +50 to +54
cosign)
VERSION="${COSIGN_VERSION:-v3.0.6}"
URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/cosign-${OS_NAME}-${ARCH_NAME}"
download "${URL}" "${INSTALL_DIR}/cosign"
chmod +x "${INSTALL_DIR}/cosign"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security cosign binary downloaded without integrity verification

The cosign binary is fetched over HTTPS and made executable immediately, with no checksum or signature check. If the GitHub release asset is tampered with or the CDN is compromised, the binary used to sign every subsequent release artifact would be malicious — undermining the entire signing chain. Sigstore publishes cosign-checksums.txt alongside each release; verifying the SHA-256 of the downloaded binary against that file (or a pinned hash in this repo) would close this gap.

Suggested change
cosign)
VERSION="${COSIGN_VERSION:-v3.0.6}"
URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/cosign-${OS_NAME}-${ARCH_NAME}"
download "${URL}" "${INSTALL_DIR}/cosign"
chmod +x "${INSTALL_DIR}/cosign"
cosign)
VERSION="${COSIGN_VERSION:-v3.0.6}"
CHECKSUM_URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/cosign-checksums.txt"
BINARY="cosign-${OS_NAME}-${ARCH_NAME}"
URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/${BINARY}"
download "${URL}" "${TMP_DIR}/${BINARY}"
download "${CHECKSUM_URL}" "${TMP_DIR}/cosign-checksums.txt"
(cd "${TMP_DIR}" && grep "${BINARY}" cosign-checksums.txt | sha256sum --check --status)
cp "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/cosign"
chmod +x "${INSTALL_DIR}/cosign"
;;
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/release/install-tool.sh
Line: 50-54

Comment:
**cosign binary downloaded without integrity verification**

The cosign binary is fetched over HTTPS and made executable immediately, with no checksum or signature check. If the GitHub release asset is tampered with or the CDN is compromised, the binary used to sign every subsequent release artifact would be malicious — undermining the entire signing chain. Sigstore publishes `cosign-checksums.txt` alongside each release; verifying the SHA-256 of the downloaded binary against that file (or a pinned hash in this repo) would close this gap.

```suggestion
  cosign)
    VERSION="${COSIGN_VERSION:-v3.0.6}"
    CHECKSUM_URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/cosign-checksums.txt"
    BINARY="cosign-${OS_NAME}-${ARCH_NAME}"
    URL="https://github.com/sigstore/cosign/releases/download/${VERSION}/${BINARY}"
    download "${URL}" "${TMP_DIR}/${BINARY}"
    download "${CHECKSUM_URL}" "${TMP_DIR}/cosign-checksums.txt"
    (cd "${TMP_DIR}" && grep "${BINARY}" cosign-checksums.txt | sha256sum --check --status)
    cp "${TMP_DIR}/${BINARY}" "${INSTALL_DIR}/cosign"
    chmod +x "${INSTALL_DIR}/cosign"
    ;;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread .woodpecker.yml
- apk add --no-cache bash curl openssh-client rsync git gnupg github-cli
- apk add --no-cache bash curl openssh-client rsync git github-cli
- curl -Ls https://cli.doppler.com/install.sh | sh
- |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security curl | sh installer runs before signing secrets are injected

curl -Ls https://cli.doppler.com/install.sh | sh executes an unverified remote script in the same shell session that subsequently calls doppler run ... -- bash scripts/release/sign-release-assets.sh. A compromised or MITMed installer could plant a doppler shim on PATH or create bash hooks that exfiltrate COSIGN_PRIVATE_KEY and COSIGN_PASSWORD the moment doppler run injects them. The same pattern appears in the smoke-published-release step (line ~53). Pinning the Doppler CLI to a known version and verifying its checksum before execution, or pre-baking it into the CI image, would remove this risk.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .woodpecker.yml
Line: 33

Comment:
**`curl | sh` installer runs before signing secrets are injected**

`curl -Ls https://cli.doppler.com/install.sh | sh` executes an unverified remote script in the same shell session that subsequently calls `doppler run ... -- bash scripts/release/sign-release-assets.sh`. A compromised or MITMed installer could plant a `doppler` shim on `PATH` or create bash hooks that exfiltrate `COSIGN_PRIVATE_KEY` and `COSIGN_PASSWORD` the moment `doppler run` injects them. The same pattern appears in the `smoke-published-release` step (line ~53). Pinning the Doppler CLI to a known version and verifying its checksum before execution, or pre-baking it into the CI image, would remove this risk.

How can I resolve this? If you propose a fix, please make it concise.

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.

Publish signed artifacts and SBOMs in the release pipeline

1 participant