From 5908dec29b55bc7a67560429e9c20f1d5090b83a Mon Sep 17 00:00:00 2001 From: Andrew Rose Date: Thu, 20 Aug 2026 12:59:43 -0700 Subject: [PATCH 1/2] release: source-only until macOS notarization is wired up Gatekeeper rejects v0.1.0's ad-hoc-signed macOS binaries once they've been through a download/quarantine flow. We're enrolled in the Apple Developer Program but the signing cert isn't provisioned yet, so release.yml now publishes a source-only GitHub Release with no binary assets. The build/cosign/SBOM/attest pipeline is preserved in git history to restore once codesigning + notarization are wired in. --- .github/workflows/release.yml | 269 ++++------------------------------ CHANGELOG.md | 12 ++ README.md | 21 +-- SECURITY.md | 40 +++-- 4 files changed, 62 insertions(+), 280 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0abc7e4..2c62a99 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,16 +1,17 @@ # Release pipeline — fires on `v*` tag push. # -# Pure OIDC where possible. No PAT, no GitHub App token, no cross-repo -# write. Cosign signing is keyless via Sigstore Fulcio; SLSA provenance -# and SBOM attestations publish to GitHub's native attestation store -# (`gh attestation verify` resolves them) and to the public Rekor -# transparency log (Sigstore-native verification works too). +# Source-only for now: v0.1.0 shipped prebuilt macOS binaries that +# Gatekeeper rejects outright (ad-hoc/linker-signed only, no Apple +# Developer ID). We're enrolled in the Apple Developer Program but the +# signing cert isn't provisioned yet, so this pipeline publishes the +# GitHub Release from the tag with no binary assets — install is +# build-from-source until codesigning + notarization land. The full +# build/cosign/SBOM/attest pipeline that produced v0.1.0's binaries is +# still in git history (this commit's parent) to restore from once the +# cert is ready. # -# Cross-repo formula bump lives in the tap repo (webflow/homebrew- -# ctxcop), which polls THIS repo's signed releases via a scheduled -# workflow, verifies the cosign signature against the workflow -# identity below, and opens an internal PR in itself. Zero stored -# credentials in either repo. +# webflow/homebrew-ctxcop polls this repo's binary releases to bump its +# formula — it has nothing to bump until binaries come back. name: release @@ -24,8 +25,6 @@ permissions: concurrency: group: release-${{ github.ref }} - # Never cancel a release-in-progress; the cost of a half-finished - # release artifact set is much higher than letting two race. cancel-in-progress: false jobs: @@ -40,67 +39,11 @@ jobs: - name: verify signed annotated tag run: ./.github/scripts/verify-release-tag.sh - # --- 2. Build per-platform binaries (reproducible flags) ---------- - build: + # --- 2. Publish a source-only GitHub Release ---------------------- + publish-source-release: needs: validate-tag - runs-on: ${{ matrix.runner }} - timeout-minutes: 10 - strategy: - fail-fast: true - matrix: - include: - - { goos: linux, goarch: amd64, runner: ubuntu-latest } - - { goos: linux, goarch: arm64, runner: ubuntu-latest } - - { goos: darwin, goarch: amd64, runner: macos-latest } - - { goos: darwin, goarch: arm64, runner: macos-latest } - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - check-latest: false - - - name: build (reproducible) - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - CGO_ENABLED: '0' - GOFLAGS: -mod=readonly - run: | - set -euo pipefail - VERSION="${GITHUB_REF#refs/tags/}" - COMMIT="$(git rev-parse --verify HEAD)" - # SOURCE_DATE_EPOCH from the tag's commit timestamp — same - # value every rebuild → bit-identical binary across runs. - SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct "$COMMIT")" - BIN="ctxcop_${VERSION}_${GOOS}_${GOARCH}" - go build \ - -trimpath \ - -ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${SOURCE_DATE_EPOCH}" \ - -o "${BIN}" \ - ./cmd/ctxcop - # Cross-platform sha256 helper (linux uses sha256sum, macos uses shasum -a 256). - if command -v sha256sum >/dev/null 2>&1; then - sha256sum "${BIN}" > "${BIN}.sha256" - else - shasum -a 256 "${BIN}" > "${BIN}.sha256" - fi - ls -la "${BIN}" "${BIN}.sha256" - - - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: ctxcop-${{ matrix.goos }}-${{ matrix.goarch }} - path: ctxcop_* - if-no-files-found: error - retention-days: 1 - - # --- 3. Sign, SBOM, attest, publish ------------------------------- - sign-attest-publish: - needs: build runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 5 # Manual approval gate: the `release` environment carries a # required-reviewer protection (the tag-pusher plus # @webflow/infrastructure-security, with `prevent_self_review: true`), @@ -108,154 +51,23 @@ jobs: # the tag approves. environment: release permissions: - contents: write # upload artifacts to the GitHub Release - id-token: write # cosign keyless OIDC + attest-* OIDC - attestations: write # actions/attest-build-provenance + sbom + contents: write # create the GitHub Release steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 - with: - go-version-file: go.mod - check-latest: false - - - name: verify betterleaks dep matches audit - # Audit mitigation #7. Refuses to sign if go.sum drifts from - # the audited SHA without a corresponding THIRD_PARTY_AUDIT.md - # review-log update. - run: ./.github/scripts/verify-betterleaks-sha.sh - - - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: dist/ - merge-multiple: true - - - name: install syft (checksum-verified) - run: | - set -euo pipefail - SYFT_VERSION=v1.44.0 - SYFT_SHA256=0e91737aee2b5baf1d255b959630194a302335d848ff97bb07921eb6205b5f5a - curl -fL -o /tmp/syft.tar.gz \ - "https://github.com/anchore/syft/releases/download/${SYFT_VERSION}/syft_${SYFT_VERSION#v}_linux_amd64.tar.gz" - echo "${SYFT_SHA256} /tmp/syft.tar.gz" | sha256sum -c - sudo tar -xzf /tmp/syft.tar.gz -C /usr/local/bin syft - syft version - - - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - with: - cosign-release: v2.4.2 - - - name: sign + sbom + checksum per binary - run: | - set -euo pipefail - cd dist - for bin in ctxcop_*; do - # Skip the .sha256 files the build job emitted. - case "$bin" in *.sha256) continue ;; esac - echo "==> ${bin}" - # SBOM in both formats (CycloneDX = SCA-tool default; - # SPDX = procurement/federal supply-chain default). - syft "${bin}" -o cyclonedx-json="${bin}.sbom.cdx.json" - syft "${bin}" -o spdx-json="${bin}.sbom.spdx.json" - - # Cosign keyless sign-blob. Bundle format combines sig + - # cert + Rekor entry into a single file consumers verify - # with `cosign verify-blob --bundle`. - COSIGN_EXPERIMENTAL=1 cosign sign-blob --yes \ - --bundle "${bin}.bundle" \ - --output-signature "${bin}.sig" \ - --output-certificate "${bin}.crt" \ - "${bin}" - - # Recompute sha256 in canonical Linux-runner format so - # checksums.txt is consistent across mac-built and linux- - # built binaries. - sha256sum "${bin}" > "${bin}.sha256" - done - # Concatenate per-binary checksums into one checksums.txt. - cat *.sha256 > checksums.txt - ls -la - - - name: self-verify cosign signatures - # Catch any future regression in our own verify path by - # actually running the documented verify command against the - # bundle we just emitted. If this fails, the release is - # broken in a way no downstream consumer would catch. - run: | - set -euo pipefail - cd dist - IDENTITY_RE='^https://github.com/webflow/ctxcop/\.github/workflows/release\.yml@refs/tags/.+$' - for bin in ctxcop_*; do - case "$bin" in *.sha256|*.sig|*.crt|*.bundle|*.sbom.*) continue ;; esac - echo "==> verify ${bin}" - cosign verify-blob \ - --certificate-identity-regexp "${IDENTITY_RE}" \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - --bundle "${bin}.bundle" \ - "${bin}" - done - - - name: attest-build-provenance (GitHub-native) - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-path: 'dist/ctxcop_*[!_]*[!.]' - - - name: attest-sbom (GitHub-native, CycloneDX) - # One attest call per binary because attest-sbom binds an SBOM - # to a single subject digest; matrix-style multi-subject is - # not supported on this action today. - run: | - set -euo pipefail - for bin in dist/ctxcop_*; do - case "$bin" in *.sha256|*.sig|*.crt|*.bundle|*.sbom.*) continue ;; esac - sbom="${bin}.sbom.cdx.json" - echo "==> attest-sbom $(basename "${bin}")" - # gh attestation: equivalent to actions/attest-sbom but - # callable in a per-binary loop. Both go to the same - # GitHub-native attestation store. - gh attestation generate \ - --predicate "${sbom}" \ - --predicate-type 'https://cyclonedx.org/specification/overview/' \ - --subject-path "${bin}" \ - --owner webflow - done - env: - GH_TOKEN: ${{ github.token }} - continue-on-error: true - # Fallback if `gh attestation generate` isn't available on the - # runner's gh version: the cosign-signed SBOM via attest below - # provides equivalent verification. - - - name: cosign attest SBOMs (Sigstore-native) - run: | - set -euo pipefail - cd dist - for bin in ctxcop_*; do - case "$bin" in *.sha256|*.sig|*.crt|*.bundle|*.sbom.*) continue ;; esac - cosign attest-blob --yes \ - --predicate "${bin}.sbom.cdx.json" \ - --type cyclonedx \ - --bundle "${bin}.sbom.cdx.bundle" \ - "${bin}" - done - - - name: publish GitHub Release + - name: publish GitHub Release (source only) env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail VERSION="${GITHUB_REF#refs/tags/}" PRERELEASE="" - # Treat rc.* / alpha / beta as prerelease automatically so the - # tap-side pull-poller can skip them by default. if [[ "${VERSION}" =~ -(rc|alpha|beta)\. ]]; then PRERELEASE=--prerelease fi - # Pull the matching changelog section, if present, for the - # release-notes body. notes_file=$(mktemp) if grep -q "^## \[${VERSION#v}\]" CHANGELOG.md; then awk -v v="${VERSION#v}" ' @@ -266,47 +78,16 @@ jobs: else echo "_No CHANGELOG entry for ${VERSION}; see commit log._" > "$notes_file" fi + { + echo + echo "---" + echo "No prebuilt binaries: macOS Gatekeeper rejects an" + echo "ad-hoc-signed binary, and Developer ID codesigning +" + echo "notarization isn't wired into this pipeline yet." + echo "Build from source: \`go build ./cmd/ctxcop\`." + } >> "$notes_file" gh release create "${VERSION}" \ --title "${VERSION}" \ --notes-file "$notes_file" \ - $PRERELEASE \ - dist/ctxcop_* - - - name: write release-manifest.json - # The tap-side pull-poller reads this manifest from the - # Release assets to discover what to bump. JSON, not freeform, - # so it's robust to release-notes formatting changes. - env: - GH_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - VERSION="${GITHUB_REF#refs/tags/}" - COMMIT="$(git rev-parse --verify HEAD)" - cd dist - # Build a per-binary record: filename, sha256, urls. - { - echo "{" - echo " \"version\": \"${VERSION}\"," - echo " \"commit\": \"${COMMIT}\"," - echo " \"signing_workflow\": \"webflow/ctxcop/.github/workflows/release.yml@refs/tags/${VERSION}\"," - echo " \"artifacts\": [" - first=1 - for bin in ctxcop_*; do - case "$bin" in *.sha256|*.sig|*.crt|*.bundle|*.sbom.*) continue ;; esac - [ $first -eq 1 ] || echo " ," - first=0 - digest="$(sha256sum "${bin}" | awk '{print $1}')" - echo " {" - echo " \"name\": \"${bin}\"," - echo " \"sha256\": \"${digest}\"," - echo " \"url\": \"https://github.com/webflow/ctxcop/releases/download/${VERSION}/${bin}\"," - echo " \"sig_url\":\"https://github.com/webflow/ctxcop/releases/download/${VERSION}/${bin}.bundle\"," - echo " \"sbom_url\":\"https://github.com/webflow/ctxcop/releases/download/${VERSION}/${bin}.sbom.cdx.json\"" - echo -n " }" - done - echo - echo " ]" - echo "}" - } > release-manifest.json - gh release upload "${VERSION}" release-manifest.json + $PRERELEASE diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b40d3b..cfd040e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,18 @@ development history behind it — four internal release lines plus a hardening pass done ahead of open-sourcing — is preserved in [docs/dev-log/CHANGELOG-pre-1.0.md](docs/dev-log/CHANGELOG-pre-1.0.md). +## [Unreleased] + +### Removed +- **Prebuilt release binaries.** v0.1.0's macOS binaries were only + ad-hoc/linker-signed, which Gatekeeper rejects outright once a binary + has been through a download/quarantine flow. We're enrolled in the + Apple Developer Program but codesigning + notarization aren't wired + into the release pipeline yet, so releases are source-only + (`go install` or build from source) until that lands. The + build/cosign/SBOM/attest pipeline that produced v0.1.0's binaries is + intact in git history and will come back once the cert is in place. + ## [0.1.0] — Unreleased ctxcop's first public release. diff --git a/README.md b/README.md index 9658c8b..9f44b7f 100644 --- a/README.md +++ b/README.md @@ -43,22 +43,17 @@ overlays. ## Install +Build from source for now — prebuilt binary releases are on hold until +macOS Developer ID codesigning + notarization are wired into the release +pipeline (Gatekeeper rejects the ad-hoc-signed binaries a plain `go build` +produces once they've been through a download/quarantine flow; a locally +built binary isn't affected). + ```sh -# 1. Pre-built signed binary from a tagged release. -# cosign-signed, SLSA Build L2 provenance. See SECURITY.md for the -# verification one-liner. -tag=$(basename "$(curl -fsSLo /dev/null -w '%{url_effective}' \ - https://github.com/webflow/ctxcop/releases/latest)") # e.g. v0.1.0 -os=$(uname -s | tr '[:upper:]' '[:lower:]') # Darwin->darwin, Linux->linux -arch=$(uname -m); case "$arch" in x86_64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; esac -curl -fL -o ctxcop \ - "https://github.com/webflow/ctxcop/releases/download/${tag}/ctxcop_${tag}_${os}_${arch}" -chmod +x ctxcop && mv ctxcop /usr/local/bin/ - -# 2. go install. +# 1. go install. go install github.com/webflow/ctxcop/cmd/ctxcop@latest -# 3. From source. +# 2. From source. git clone https://github.com/webflow/ctxcop && cd ctxcop go build -o /usr/local/bin/ctxcop ./cmd/ctxcop ``` diff --git a/SECURITY.md b/SECURITY.md index bae2aa1..667d82e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -164,34 +164,28 @@ contains an apparent credential, that's a real report we want to hear. ## Build integrity -Released binaries are signed with [Sigstore cosign](https://github.com/sigstore/cosign) -in keyless OIDC mode. The signing identity is the workflow path at a tag, -e.g. `https://github.com/webflow/ctxcop/.github/workflows/release.yml@refs/tags/vX.Y.Z`. -Verification: - -Artifact names carry the release tag, e.g. `ctxcop_v0.1.0_darwin_arm64` -(`ctxcop___`, where `` is `darwin`/`linux` and -`` is `amd64`/`arm64`). Substitute the version and platform you -downloaded: - -```bash -cosign verify-blob \ - --certificate-identity-regexp 'https://github.com/webflow/ctxcop/\.github/workflows/release\.yml@refs/tags/.+' \ - --certificate-oidc-issuer https://token.actions.githubusercontent.com \ - --signature ctxcop___.sig \ - --bundle ctxcop___.bundle \ - ctxcop___ -``` +Releases are currently source-only — v0.1.0's prebuilt macOS binaries +were ad-hoc/linker-signed only, which Gatekeeper rejects outright once +a binary's been through a download/quarantine flow. We're enrolled in +the Apple Developer Program; codesigning + notarization aren't wired +into the release pipeline yet. Build from source per the README, or +`go install github.com/webflow/ctxcop/cmd/ctxcop@latest`, neither of +which is affected (Gatekeeper only rejects downloaded, quarantined +binaries, not ones you build locally). + +Once prebuilt binaries return, they'll again be signed with +[Sigstore cosign](https://github.com/sigstore/cosign) in keyless OIDC +mode (signing identity is the workflow path at a tag, e.g. +`https://github.com/webflow/ctxcop/.github/workflows/release.yml@refs/tags/vX.Y.Z`) +plus CycloneDX + SPDX SBOMs and GitHub-native attestations — that +pipeline is intact in git history and just needs the codesigning step +added back in front of it. Builds are bit-for-bit reproducible given the same git tag and Go -toolchain (pinned via `go-version-file: go.mod`). Anyone can rebuild -from source and compare sha256 against the released binary — see +toolchain (pinned via `go-version-file: go.mod`) — see [docs/verify-reproducibility.md](docs/verify-reproducibility.md) for the paste-and-run script. -SBOMs (CycloneDX + SPDX) are attached to each GitHub Release and published -as cosign attestations bound to the binary digest. - ## Audit-log integrity ctxcop's optional audit log (`$CTXCOP_AUDIT_LOG`, off by default) From 9f0bcdb96a22d48db2f98ef6c9dc71918a44e926 Mon Sep 17 00:00:00 2001 From: Andrew Rose Date: Thu, 20 Aug 2026 13:01:55 -0700 Subject: [PATCH 2/2] release: trim comments --- .github/workflows/release.yml | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c62a99..7c4238b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,17 +1,6 @@ -# Release pipeline — fires on `v*` tag push. -# -# Source-only for now: v0.1.0 shipped prebuilt macOS binaries that -# Gatekeeper rejects outright (ad-hoc/linker-signed only, no Apple -# Developer ID). We're enrolled in the Apple Developer Program but the -# signing cert isn't provisioned yet, so this pipeline publishes the -# GitHub Release from the tag with no binary assets — install is -# build-from-source until codesigning + notarization land. The full -# build/cosign/SBOM/attest pipeline that produced v0.1.0's binaries is -# still in git history (this commit's parent) to restore from once the -# cert is ready. -# -# webflow/homebrew-ctxcop polls this repo's binary releases to bump its -# formula — it has nothing to bump until binaries come back. +# Release pipeline — fires on `v*` tag push. Source-only pending macOS +# notarization; see PR #5. Build/cosign/SBOM/attest pipeline is in git +# history to restore once that's wired in. name: release @@ -44,12 +33,7 @@ jobs: needs: validate-tag runs-on: ubuntu-latest timeout-minutes: 5 - # Manual approval gate: the `release` environment carries a - # required-reviewer protection (the tag-pusher plus - # @webflow/infrastructure-security, with `prevent_self_review: true`), - # so the run halts here until a reviewer other than whoever pushed - # the tag approves. - environment: release + environment: release # required-reviewer gate, prevent_self_review permissions: contents: write # create the GitHub Release steps: