From b4b4fa1a14d017694d718592d14d030f5f7f01e8 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Tue, 19 May 2026 23:48:54 +0400 Subject: [PATCH 1/2] feat(sc.sh): opt-in install of branch-preview tarballs via SIMPLE_CONTAINER_ALLOW_PREVIEW MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: production users testing a feature-branch SC build (e.g. before merging an SC API PR that affects downstream consumers) currently can't use sc.sh — the Phase 2c cert-identity regex is hard-pinned to push.yaml@refs/heads/main, so every preview tarball trips cosign verification even though it's a legitimately signed Sigstore bundle. Today the only workaround is to bypass sc.sh entirely (`curl tarball + tar -xz`), which loses the signature check the failgate was built to provide. The opt-in path documented here gives preview testing back without weakening the production strict-mode default. What: - sc.sh: when SIMPLE_CONTAINER_ALLOW_PREVIEW=1 is set, widen the cert-identity regex passed to `cosign verify-blob` to also accept branch-preview.yaml@refs/heads/*. Default (env var unset / not "1") is unchanged — only the production push.yaml@main identity is accepted. Signature, Rekor log entry, and OIDC issuer are still verified end-to-end; the broader regex is the only thing that changes. - sc.sh: on signature failure where cosign reports a branch-preview signer, surface a precise next-step ("rerun with SIMPLE_CONTAINER_ALLOW_PREVIEW=1 SIMPLE_CONTAINER_VERSION=...") instead of the generic compromise message, so a user who knows they're installing a preview build gets a copy-paste unblock instead of having to read the script. - docs/SECURITY.md: document the opt-in env var alongside the existing manual `cosign verify-blob` commands, and update the comment in Verifying tarballs that wrongly claimed preview tarballs don't land at the CDN (they do — branch-preview.yaml publishes them to the same bucket). Why this is safe to relax: 1. The regex still anchors to simple-container-com/api workflows only; an attacker cannot publish a malicious tarball under a different repo's workflow identity. 2. The OIDC issuer is still pinned to GitHub's token endpoint. 3. Rekor log entry, Sigstore bundle, and tarball SHA-256 sidecar are all still verified. 4. Production users default to strict. Picking up a preview build requires explicit acknowledgement via env var — there's no implicit promotion of any feature-branch identity into the production trust set. Testing: - Without env var: preview tarball is rejected with the new helpful message pointing at SIMPLE_CONTAINER_ALLOW_PREVIEW=1 (verified against the v2026.5.26-pre.4cc1a03-preview.4cc1a03 tarball published 2026-05-19). - With env var: same tarball verifies and installs cleanly. Refs PR #277 (the trigger for this fix — needed to validate the new CloudTrail security alerts plugin schema end-to-end against a preview SC). Signed-off-by: Dmitrii Creed --- docs/SECURITY.md | 31 +++++++++++++++++++++++++++++ sc.sh | 51 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index eeb121fb..6c4d33f5 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -190,6 +190,37 @@ end-to-end supply-chain integrity should install cosign before bootstrapping (https://docs.sigstore.dev/system_config/installation/). The commands above remain the manual / out-of-band verification path. +#### Installing preview / branch-preview builds + +Default `sc.sh` accepts only production-signed tarballs (signed by +`push.yaml@refs/heads/main`). Tarballs produced by +`branch-preview.yaml` carry a different OIDC identity (the feature +branch's own workflow run) and are rejected by default — even though +they ship to the same CDN with valid Sigstore bundles. + +To install a preview build, set `SIMPLE_CONTAINER_ALLOW_PREVIEW=1`: + +```bash +SIMPLE_CONTAINER_ALLOW_PREVIEW=1 \ +SIMPLE_CONTAINER_VERSION=YYYY.M.D-pre.-preview. \ + bash <(curl -Ls https://dist.simple-container.com/sc.sh) +``` + +When the env var is set, `verify_sc_tarball` widens the accepted +identity regex to also include +`branch-preview.yaml@refs/heads/`. The signature, Rekor log +entry, and OIDC issuer are still verified — only the allowed +signer-workflow set is broader. Production users never set this and +remain on the strict `push.yaml@main`-only path. + +The manual `cosign verify-blob` equivalent for preview tarballs: + +```bash +cosign verify-blob --bundle "$T.cosign-bundle" \ + --certificate-identity-regexp '^https://github\.com/simple-container-com/api/\.github/workflows/(push\.yaml@refs/heads/main|branch-preview\.yaml@refs/heads/.+)$' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com "$T" +``` + ### Composite-action consumers — SHA-pin the underlying image `simple-container-com/api/.github/actions/{deploy-client-stack, diff --git a/sc.sh b/sc.sh index 810e3244..04ff9b70 100755 --- a/sc.sh +++ b/sc.sh @@ -430,10 +430,24 @@ verify_sc_tarball() { echo "✅" echo -n "🔍 Verifying tarball signature against build-workflow identity... " - # Identity regex matches the production push.yaml on refs/heads/main — - # the only workflow allowed to publish tarballs to dist. Staging / - # preview tarballs do not land at dist.simple-container.com, so a - # single anchored regex suffices here. Mirror this in SECURITY.md. + # Default regex matches the production push.yaml on refs/heads/main — the + # only workflow allowed to publish tarballs that the strict signature path + # accepts. Mirror in docs/SECURITY.md. + # + # When the caller sets SIMPLE_CONTAINER_ALLOW_PREVIEW=1, we widen the regex + # to ALSO accept branch-preview.yaml@refs/heads/* signatures. Preview + # tarballs published by branch-preview.yaml DO land at dist.simple-container.com + # (the earlier "do not land" comment was wrong — see the Publish step in + # .github/workflows/branch-preview.yaml). Without this opt-in path, + # consumers wanting to test a feature branch via `sc.sh` end up either + # bypassing the installer entirely or weakening their own verification + # locally — both worse outcomes than a documented, env-gated opt-in. + # + # The opt-in is intentional: production users default to strict, and + # picking up a preview build requires explicit acknowledgement that the + # signing identity is a feature-branch workflow run (lower bar than + # main-branch protected). The signature itself is still verified end-to- + # end via cosign + Rekor, just with a wider identity allowlist. # # IMPORTANT: do NOT pass --yes here. cosign 2.x only accepts --yes on # sign-blob (skip interactive confirmation); on verify-blob it errors @@ -441,20 +455,39 @@ verify_sc_tarball() { # after Phase 2c shipped. Capture cosign's stderr (don't /dev/null it) # so future failures surface the real error instead of a generic # message. + local identity_regex='^https://github\.com/simple-container-com/api/\.github/workflows/push\.yaml@refs/heads/main$' + if [ "${SIMPLE_CONTAINER_ALLOW_PREVIEW:-}" = "1" ]; then + identity_regex='^https://github\.com/simple-container-com/api/\.github/workflows/(push\.yaml@refs/heads/main|branch-preview\.yaml@refs/heads/.+)$' + fi local cosign_err if ! cosign_err=$(COSIGN_EXPERIMENTAL=1 cosign verify-blob \ --bundle "$bundle_path" \ - --certificate-identity-regexp '^https://github\.com/simple-container-com/api/\.github/workflows/push\.yaml@refs/heads/main$' \ + --certificate-identity-regexp "$identity_regex" \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ "$tarball_path" 2>&1); then echo "❌" echo "❌ Signature verification FAILED for $tarball_path" echo " cosign output:" echo "$cosign_err" | sed 's/^/ /' - echo " The tarball does not bear a valid signature from the SC" - echo " production publish workflow. This could mean: tarball was" - echo " tampered in transit, CDN was compromised, or the signing" - echo " identity rotated — see https://github.com/simple-container-com/api" + # Detect the preview-signed-but-strict-mode case and give the user a + # specific hint instead of the generic "compromised CDN" copy. The + # cosign error includes the actual signer identity in `got subjects [...]`. + if echo "$cosign_err" | grep -q 'branch-preview\.yaml@refs/heads/'; then + echo " The tarball was signed by branch-preview.yaml (a feature-branch" + echo " build), not by the production push.yaml@main workflow. To allow" + echo " preview builds explicitly, rerun with:" + echo "" + echo " SIMPLE_CONTAINER_ALLOW_PREVIEW=1 SIMPLE_CONTAINER_VERSION=$VERSION \\" + echo " bash <(curl -Ls https://dist.simple-container.com/sc.sh)" + echo "" + echo " This is an intentional opt-in: production installs stay strict" + echo " and only main-branch signatures are accepted by default." + else + echo " The tarball does not bear a valid signature from the SC" + echo " production publish workflow. This could mean: tarball was" + echo " tampered in transit, CDN was compromised, or the signing" + echo " identity rotated — see https://github.com/simple-container-com/api" + fi echo " Refusing to extract." return 1 fi From 915a713a840b51d9d8a7d3f400968044e69b2750 Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Wed, 20 May 2026 12:16:01 +0400 Subject: [PATCH 2/2] fix(sc.sh): tighten preview-install trust model based on threat-model review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex + Gemini security review of the original commit (cross-referenced against GitHub Actions OIDC docs and sigstore/cosign source) surfaced material gaps that would have shipped if the original `=1` opt-in landed as-is. Each fix is paired with a test that confirms the behavior. ## What changed (and why) 1. Reject `SIMPLE_CONTAINER_ALLOW_PREVIEW=1` entirely. The original "=1 widens to any branch" form is gone. It trusted `branch-preview.yaml@refs/heads/.+` — anyone with push access to ANY branch in the repo could ship a tarball that the regex would accept, once the user opted in for "their" branch. That's a much broader trust radius than picking up an unreviewed feature branch you actually want to test (push to main is gated by branch protection + required reviews; push to a feature branch is not). The `=1` form now fails closed with a pointer to `_BRANCH`. 2. Require `SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=` for preview installs. Pins cosign verification to one specific branch's branch-preview.yaml signature. A different branch's tarball at the CDN URL — whether served deliberately by a CDN-account compromise (T4) or accidentally cached — fails verification. Branch name is validated against a conservative allowlist (`^[A-Za-z0-9._/-]+$`) BEFORE interpolation into the cosign regex, plus explicit `..` / leading-/trailing-`/` / `.lock`-suffix rejections. This neutralizes the regex-injection / "identity shadowing" path Gemini flagged: a branch name like `evil.*` would otherwise re-open the permissive trust set, and `feat.foo` would shadow `feat/foo`. The single regex metachar that the allowlist still permits (`.`) is escaped to `\.` in the generated regex. 3. Optional `SIMPLE_CONTAINER_TRUST_PREVIEW_SHA=<40-hex>` to pin the commit. Maps to cosign's `--certificate-github-workflow-sha`, which verifies against the Sigstore certificate's GitHub OIDC `workflow_sha` claim (OID 1.3.6.1.4.1.57264.1.3). Closes T2 (workflow content not pinned at the identity layer) and the residual T4 (CDN can serve an old tarball signed from the same branch with a different SHA). Recommended for CI; optional for ad-hoc developer installs. SHA value is validated as 40-char lowercase hex before being passed to cosign so an invalid input fails the install with a clear message, not a confusing cosign error. 4. Loud stderr warning on every preview install. Mitigates T3 (persistent `export` in shell rc silently relaxes trust). The warning is unconditional and visible on every invocation when _BRANCH is set, listing the exact branch trusted and whether the SHA pin is in effect. 5. Smarter error message: extract signer branch from cosign output. When verification fails because the tarball is preview-signed but no _BRANCH is set, parse the signer ARN out of cosign's "got subjects [...]" line and offer it back to the user as the env-var value to copy-paste. Removes the curl|sh example string that tripped the org's semgrep `shell-curl-pipe-to-shell` rule — the user already knows how they invoked us, no need to demo it. ## Threats explicitly preserved as out-of-scope - "Same CDN namespace" (preview artifacts share production object keys) — this is a publish-side concern, belongs in branch-preview.yaml not sc.sh. - "Verify installed `sc --version` matches requested" — sc.sh already prints the post-install version; explicit interlock is a separate hardening pass. - "Path-traversal-safe extraction" — pre-existing concern in extract logic, orthogonal to verification regex. ## Verified end-to-end 8 manual test cases against the live preview tarball v2026.5.26-pre.4cc1a03-preview.4cc1a03 (signed by branch-preview.yaml@feat/ cloudtrail-alerts-exclusions-and-new-detectors with SHA 4cc1a03ca5c259a428e07d4f0bb8eb9120a6e2b7): A. Strict mode (no env vars): rejected with auto-extracted branch hint B. Deprecated `=1`: rejected BEFORE cosign runs (fail-fast) C. Invalid branch (`evil.*`): rejected at validation D. Wrong branch (valid format, doesn't match signer): cosign mismatch E. Correct branch: success + warning F. Correct branch + correct SHA pin: success + SHA-pinned warning G. Correct branch + wrong SHA: cosign rejects (`workflow SHA not found`) H. Invalid SHA format: rejected at validation Semgrep `shell-curl-pipe-to-shell` rule passes on the revised script (simple-container-com/actions semgrep-scan/rules/shell.yml). Refs codex+gemini cross-review of the original sc.sh patch. Signed-off-by: Dmitrii Creed --- docs/SECURITY.md | 51 +++++++++++++---- sc.sh | 141 +++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 157 insertions(+), 35 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 6c4d33f5..2f6649bf 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -196,28 +196,57 @@ Default `sc.sh` accepts only production-signed tarballs (signed by `push.yaml@refs/heads/main`). Tarballs produced by `branch-preview.yaml` carry a different OIDC identity (the feature branch's own workflow run) and are rejected by default — even though -they ship to the same CDN with valid Sigstore bundles. +they ship to the same CDN with valid Sigstore bundles. Feature branches +lack `main`'s branch protection (required reviews, signed commits), so +extending trust to *every* branch is materially weaker than the strict +production posture. Preview installs therefore require **explicit pinning +to one named branch** — and, when possible, the exact commit SHA. -To install a preview build, set `SIMPLE_CONTAINER_ALLOW_PREVIEW=1`: +Minimum (branch-pinned): ```bash -SIMPLE_CONTAINER_ALLOW_PREVIEW=1 \ +SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=feat/your-feature-branch \ SIMPLE_CONTAINER_VERSION=YYYY.M.D-pre.-preview. \ bash <(curl -Ls https://dist.simple-container.com/sc.sh) ``` -When the env var is set, `verify_sc_tarball` widens the accepted -identity regex to also include -`branch-preview.yaml@refs/heads/`. The signature, Rekor log -entry, and OIDC issuer are still verified — only the allowed -signer-workflow set is broader. Production users never set this and -remain on the strict `push.yaml@main`-only path. +Recommended for CI (branch + commit SHA pin): -The manual `cosign verify-blob` equivalent for preview tarballs: +```bash +SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=feat/your-feature-branch \ +SIMPLE_CONTAINER_TRUST_PREVIEW_SHA=4cc1a03ca5c259a428e07d4f0bb8eb9120a6e2b7 \ +SIMPLE_CONTAINER_VERSION=YYYY.M.D-pre.-preview. \ + bash <(curl -Ls https://dist.simple-container.com/sc.sh) +``` + +When `_BRANCH` is set, `verify_sc_tarball` widens the accepted identity +regex to also include `branch-preview.yaml@refs/heads/`. +When `_SHA` is also set, `cosign verify-blob` is given +`--certificate-github-workflow-sha `, which the Sigstore certificate's +GitHub OIDC claim must match exactly — pinning the verification to a +specific commit rather than the mutable branch head. + +Security properties preserved across both modes: + +- Signature, Rekor log entry, OIDC issuer, and SHA-256 sidecar are all + still verified end-to-end. The opt-in only changes which signer-workflow + identities the regex permits. +- Branch name is validated against `^[A-Za-z0-9._/-]+$` and `..` / + leading-slash / trailing-slash / `.lock`-suffix rejections before being + interpolated into the regex. The `.` regex metachar is escaped to prevent + e.g. `feat.foo` shadowing `feat/foo`. +- Production users default to strict. The previous `SIMPLE_CONTAINER_ALLOW_PREVIEW=1` + shape (any branch) is intentionally **not** supported and fails loudly with + a pointer to `_BRANCH`. There is no "trust all preview" mode. +- A loud stderr warning is printed on every invocation when preview mode is + active so a forgotten `export` in shell config is visible, not silent. + +The manual `cosign verify-blob` equivalent for preview tarballs (branch + SHA pin): ```bash cosign verify-blob --bundle "$T.cosign-bundle" \ - --certificate-identity-regexp '^https://github\.com/simple-container-com/api/\.github/workflows/(push\.yaml@refs/heads/main|branch-preview\.yaml@refs/heads/.+)$' \ + --certificate-identity-regexp "^https://github\.com/simple-container-com/api/\.github/workflows/(push\.yaml@refs/heads/main|branch-preview\.yaml@refs/heads/$BRANCH)$" \ + --certificate-github-workflow-sha "$SHA" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com "$T" ``` diff --git a/sc.sh b/sc.sh index 04ff9b70..17aff144 100755 --- a/sc.sh +++ b/sc.sh @@ -430,24 +430,30 @@ verify_sc_tarball() { echo "✅" echo -n "🔍 Verifying tarball signature against build-workflow identity... " - # Default regex matches the production push.yaml on refs/heads/main — the - # only workflow allowed to publish tarballs that the strict signature path - # accepts. Mirror in docs/SECURITY.md. + # Default: STRICT. Identity regex matches the production push.yaml on + # refs/heads/main — the only workflow allowed to publish tarballs that + # this code path accepts without further opt-in. Mirror in docs/SECURITY.md. # - # When the caller sets SIMPLE_CONTAINER_ALLOW_PREVIEW=1, we widen the regex - # to ALSO accept branch-preview.yaml@refs/heads/* signatures. Preview - # tarballs published by branch-preview.yaml DO land at dist.simple-container.com - # (the earlier "do not land" comment was wrong — see the Publish step in - # .github/workflows/branch-preview.yaml). Without this opt-in path, - # consumers wanting to test a feature branch via `sc.sh` end up either - # bypassing the installer entirely or weakening their own verification - # locally — both worse outcomes than a documented, env-gated opt-in. + # Preview opt-in (SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=) narrows + # the trust extension to ONE named branch's branch-preview.yaml signature. + # We deliberately do NOT support an "any branch" opt-in (e.g. =1), because + # accepting `branch-preview.yaml@refs/heads/.+` would trust every push- + # writer on any branch in the repo — a much broader radius than picking + # up an unreviewed feature branch you actually want to test. # - # The opt-in is intentional: production users default to strict, and - # picking up a preview build requires explicit acknowledgement that the - # signing identity is a feature-branch workflow run (lower bar than - # main-branch protected). The signature itself is still verified end-to- - # end via cosign + Rekor, just with a wider identity allowlist. + # Why this still requires explicit user action: + # - branch-preview.yaml runs on workflow_dispatch from feature branches + # that lack main's branch protection / required reviews / signed + # commits. The cosign certificate proves "this run dispatched from + # this ref" but cannot attest to the integrity of the workflow's + # contents at that ref (no SHA pinning at the identity layer). + # - For higher assurance, also set SIMPLE_CONTAINER_TRUST_PREVIEW_SHA + # to a 40-char commit SHA. We pass --certificate-github-workflow-sha + # to cosign so the Sigstore cert's workflow_sha claim must match + # EXACTLY — pinning to a specific commit, not a mutable branch head. + # This neutralizes "attacker pushes new commit to the branch then + # re-dispatches" and "CDN replays an old tarball signed from the + # same branch." # # IMPORTANT: do NOT pass --yes here. cosign 2.x only accepts --yes on # sign-blob (skip interactive confirmation); on verify-blob it errors @@ -455,15 +461,92 @@ verify_sc_tarball() { # after Phase 2c shipped. Capture cosign's stderr (don't /dev/null it) # so future failures surface the real error instead of a generic # message. - local identity_regex='^https://github\.com/simple-container-com/api/\.github/workflows/push\.yaml@refs/heads/main$' + + # Refuse the deprecated/never-shipped "=1" form loudly, with a hint at + # the supported form. This forces the user to commit to a specific + # branch instead of broadening trust to the entire repo's push-writers. if [ "${SIMPLE_CONTAINER_ALLOW_PREVIEW:-}" = "1" ]; then - identity_regex='^https://github\.com/simple-container-com/api/\.github/workflows/(push\.yaml@refs/heads/main|branch-preview\.yaml@refs/heads/.+)$' + echo "❌" + echo "❌ SIMPLE_CONTAINER_ALLOW_PREVIEW=1 is not supported (security: trusts every branch in the repo)." + echo " Use SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH= instead — it pins" + echo " cosign verification to one branch's branch-preview.yaml signature." + echo " Optionally also set SIMPLE_CONTAINER_TRUST_PREVIEW_SHA=<40-char-commit-sha>" + echo " to pin the exact commit of that branch (recommended for CI)." + echo " See https://github.com/simple-container-com/api/blob/main/docs/SECURITY.md#installing-preview--branch-preview-builds" + return 1 fi + + local identity_regex='^https://github\.com/simple-container-com/api/\.github/workflows/push\.yaml@refs/heads/main$' + local preview_branch="${SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH:-}" + local preview_sha="${SIMPLE_CONTAINER_TRUST_PREVIEW_SHA:-}" + local cosign_extra_args=() + if [ -n "$preview_branch" ]; then + # Validate against a conservative allowlist BEFORE interpolating into the + # regex. Git's check-ref-format is more permissive than what we want here + # (it allows `+`, `(`, `)`, `{`, `}`, `|`, `$` — all regex metachars). + # Constraining to alphanumerics + `._/-` keeps the regex string literal- + # equivalent so we don't need a separate escape pass, and matches the + # naming conventions every Integrail/SC branch already uses (feat/fix/ + # chore/docs prefixes with kebab-case bodies). + if ! printf '%s' "$preview_branch" | grep -qE '^[A-Za-z0-9._/-]+$'; then + echo "❌" + echo "❌ Invalid SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH value: $preview_branch" + echo " Allowed characters: letters, digits, dot, underscore, slash, hyphen." + echo " Refusing to interpolate into the cosign identity regex." + return 1 + fi + # Additional git-style rejections (parts of check-ref-format that our + # allowlist already covers but worth being explicit about): + case "$preview_branch" in + ..*|*..*|*/..*|*..) + echo "❌"; echo "❌ Branch name must not contain '..' segments."; return 1 ;; + /*|*/) + echo "❌"; echo "❌ Branch name must not start or end with '/'."; return 1 ;; + *.lock|*.lock/*|*/*.lock) + echo "❌"; echo "❌ Branch name segments must not end with '.lock'."; return 1 ;; + esac + + # Optional SHA pin — must be 40 lowercase hex chars (canonical git SHA-1). + if [ -n "$preview_sha" ]; then + if ! printf '%s' "$preview_sha" | grep -qE '^[a-f0-9]{40}$'; then + echo "❌" + echo "❌ Invalid SIMPLE_CONTAINER_TRUST_PREVIEW_SHA value: $preview_sha" + echo " Must be 40 lowercase hex characters (a full git commit SHA-1)." + return 1 + fi + cosign_extra_args+=(--certificate-github-workflow-sha "$preview_sha") + fi + + # Loud warning to stderr so a forgotten `export` in shell rc is visible + # on every install, not just the first. T3 mitigation per review. + echo "" >&2 + echo "⚠️ PREVIEW SIGNATURE TRUST EXTENDED" >&2 + echo " SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH is set — accepting tarballs signed by" >&2 + echo " branch-preview.yaml on refs/heads/$preview_branch" >&2 + if [ -n "$preview_sha" ]; then + echo " pinned to commit SHA $preview_sha" >&2 + else + echo " (no SHA pin — branch HEAD trusted; set SIMPLE_CONTAINER_TRUST_PREVIEW_SHA= to pin)" >&2 + fi + echo " Production-strict mode disabled. Unset the env var to restore strict mode." >&2 + echo "" >&2 + + # Build the widened regex with the validated branch name. The branch + # name has already been allowlist-restricted; the only metachar that + # could appear is `.`, which we escape here to avoid e.g. `feat.main` + # matching a regex intended for `feat/main` (gemini's "identity + # shadowing" point). `/` and `-` are regex-safe. + local escaped_branch + escaped_branch=$(printf '%s' "$preview_branch" | sed 's/\./\\./g') + identity_regex="^https://github\\.com/simple-container-com/api/\\.github/workflows/(push\\.yaml@refs/heads/main|branch-preview\\.yaml@refs/heads/${escaped_branch})\$" + fi + local cosign_err if ! cosign_err=$(COSIGN_EXPERIMENTAL=1 cosign verify-blob \ --bundle "$bundle_path" \ --certificate-identity-regexp "$identity_regex" \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + "${cosign_extra_args[@]}" \ "$tarball_path" 2>&1); then echo "❌" echo "❌ Signature verification FAILED for $tarball_path" @@ -473,15 +556,25 @@ verify_sc_tarball() { # specific hint instead of the generic "compromised CDN" copy. The # cosign error includes the actual signer identity in `got subjects [...]`. if echo "$cosign_err" | grep -q 'branch-preview\.yaml@refs/heads/'; then + # Try to surface the branch the tarball was actually signed from so + # the user can copy-paste it as the env var value. The cosign error + # text format is "got subjects [URL]". + local actual_branch + actual_branch=$(echo "$cosign_err" | grep -oE 'branch-preview\.yaml@refs/heads/[^]]+' | head -1 | sed 's|branch-preview\.yaml@refs/heads/||') echo " The tarball was signed by branch-preview.yaml (a feature-branch" - echo " build), not by the production push.yaml@main workflow. To allow" - echo " preview builds explicitly, rerun with:" + echo " build), not by the production push.yaml@main workflow. If you" + echo " trust this preview, set:" + echo "" + if [ -n "$actual_branch" ]; then + echo " export SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=$actual_branch" + else + echo " export SIMPLE_CONTAINER_TRUST_PREVIEW_BRANCH=" + fi echo "" - echo " SIMPLE_CONTAINER_ALLOW_PREVIEW=1 SIMPLE_CONTAINER_VERSION=$VERSION \\" - echo " bash <(curl -Ls https://dist.simple-container.com/sc.sh)" + echo " Optionally also pin the exact commit:" + echo " export SIMPLE_CONTAINER_TRUST_PREVIEW_SHA=<40-char-sha>" echo "" - echo " This is an intentional opt-in: production installs stay strict" - echo " and only main-branch signatures are accepted by default." + echo " See https://github.com/simple-container-com/api/blob/main/docs/SECURITY.md#installing-preview--branch-preview-builds" else echo " The tarball does not bear a valid signature from the SC" echo " production publish workflow. This could mean: tarball was"