diff --git a/bootc-build/sign-and-publish/action.yml b/bootc-build/sign-and-publish/action.yml index 009b354b..660fe693 100644 --- a/bootc-build/sign-and-publish/action.yml +++ b/bootc-build/sign-and-publish/action.yml @@ -30,6 +30,22 @@ inputs: image-name: description: "Short image name for SBOM file path (e.g. bluefin)" default: "" + new-bundle-format: + description: > + Whether cosign writes signatures in the Sigstore "new bundle format" (OCI 1.1 + referrers) instead of the legacy `sha256-.sig` tag. + + Also controls `--use-signing-config`, which cosign requires to agree with it: + `--new-bundle-format=false` with the default `--use-signing-config=true` is + rejected outright ("must provide --new-bundle-format or --bundle where + applicable with --signing-config or --use-signing-config"). + + MUST stay "false" for bootc/podman consumers. containers/image — which backs + podman, skopeo and `bootc switch` — only discovers signatures via the legacy + `.sig` tag, so a policy.json `sigstoreSigned` entry cannot see new-format + signatures. cosign 3.x flipped this default to true, which silently produced + images that `cosign verify` accepts but podman rejects. See projectbluefin/common#977. + default: "false" certificate-identity-regexp: description: > Regexp for cosign verify --certificate-identity-regexp. Defaults to known signing @@ -80,18 +96,20 @@ runs: if: inputs.signing-mode == 'keyless' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 env: + NEW_BUNDLE_FORMAT: ${{ inputs.new-bundle-format }} IMAGE: ${{ inputs.image }} DIGEST: ${{ inputs.digest }} with: timeout_minutes: 5 max_attempts: 3 retry_wait_seconds: 30 - command: cosign sign -y "${IMAGE}@${DIGEST}" + command: cosign sign -y --new-bundle-format="${NEW_BUNDLE_FORMAT}" --use-signing-config="${NEW_BUNDLE_FORMAT}" "${IMAGE}@${DIGEST}" - name: Sign container image (key-based) if: inputs.signing-mode == 'key' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 env: + NEW_BUNDLE_FORMAT: ${{ inputs.new-bundle-format }} IMAGE: ${{ inputs.image }} DIGEST: ${{ inputs.digest }} COSIGN_PRIVATE_KEY: ${{ inputs.signing-key }} @@ -99,7 +117,7 @@ runs: timeout_minutes: 5 max_attempts: 3 retry_wait_seconds: 30 - command: cosign sign -y --key env://COSIGN_PRIVATE_KEY "${IMAGE}@${DIGEST}" + command: cosign sign -y --new-bundle-format="${NEW_BUNDLE_FORMAT}" --use-signing-config="${NEW_BUNDLE_FORMAT}" --key env://COSIGN_PRIVATE_KEY "${IMAGE}@${DIGEST}" - name: Verify signature if: inputs.signing-mode == 'keyless' @@ -115,6 +133,31 @@ runs: --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ "${IMAGE}@${DIGEST}" + # `cosign verify` succeeds against BOTH the legacy `.sig` tag and the new + # OCI 1.1 bundle format, so it cannot detect a format regression on its own — + # that is exactly how cosign 3.x's default flip shipped months of images that + # verified cleanly but that podman/bootc could not validate. + # containers/image (podman, skopeo, `bootc switch`) resolves signatures ONLY + # via `sha256-.sig`, so assert that tag exists in the registry. + - name: Assert legacy .sig tag exists (podman/bootc compatibility) + if: inputs.new-bundle-format == 'false' + shell: bash + env: + IMAGE: ${{ inputs.image }} + DIGEST: ${{ inputs.digest }} + run: | + set -euo pipefail + sig_tag="${DIGEST/:/-}.sig" + if ! cosign download signature "${IMAGE}@${DIGEST}" >/dev/null 2>&1; then + echo "::error::No legacy signature tag ${IMAGE}:${sig_tag} in the registry." + echo "::error::cosign verify passed, but containers/image (podman, skopeo, bootc)" + echo "::error::only discovers signatures via the .sig tag, so this image would be" + echo "::error::rejected by a policy.json sigstoreSigned entry. Check that" + echo "::error::--new-bundle-format=false reached the cosign sign invocation." + exit 1 + fi + echo "Legacy signature tag present: ${IMAGE}:${sig_tag}" + - name: Install Syft if: inputs.generate-sbom == 'true' id: setup-syft @@ -193,18 +236,20 @@ runs: if: inputs.generate-sbom == 'true' && inputs.signing-mode == 'keyless' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 env: + NEW_BUNDLE_FORMAT: ${{ inputs.new-bundle-format }} IMAGE: ${{ inputs.image }} SBOM_DIGEST: ${{ steps.upload-sbom.outputs.sbom-digest }} with: timeout_minutes: 5 max_attempts: 3 retry_wait_seconds: 30 - command: cosign sign -y "${IMAGE}@${SBOM_DIGEST}" + command: cosign sign -y --new-bundle-format="${NEW_BUNDLE_FORMAT}" --use-signing-config="${NEW_BUNDLE_FORMAT}" "${IMAGE}@${SBOM_DIGEST}" - name: Sign SBOM artifact (key-based) if: inputs.generate-sbom == 'true' && inputs.signing-mode == 'key' uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4 env: + NEW_BUNDLE_FORMAT: ${{ inputs.new-bundle-format }} IMAGE: ${{ inputs.image }} SBOM_DIGEST: ${{ steps.upload-sbom.outputs.sbom-digest }} COSIGN_PRIVATE_KEY: ${{ inputs.signing-key }} @@ -212,7 +257,7 @@ runs: timeout_minutes: 5 max_attempts: 3 retry_wait_seconds: 30 - command: cosign sign -y --key env://COSIGN_PRIVATE_KEY "${IMAGE}@${SBOM_DIGEST}" + command: cosign sign -y --new-bundle-format="${NEW_BUNDLE_FORMAT}" --use-signing-config="${NEW_BUNDLE_FORMAT}" --key env://COSIGN_PRIVATE_KEY "${IMAGE}@${SBOM_DIGEST}" - name: GitHub Build Provenance Attestation if: inputs.push-attestation == 'true' diff --git a/docs/skills/composite-actions/action-reference.md b/docs/skills/composite-actions/action-reference.md index da7e2c04..99a885be 100644 --- a/docs/skills/composite-actions/action-reference.md +++ b/docs/skills/composite-actions/action-reference.md @@ -166,6 +166,22 @@ Two signing modes: - `keyless` (default): OIDC/Fulcio via `cosign sign -y`. **Requires** `id-token: write` in the calling job. Validated early — fails immediately if `ACTIONS_ID_TOKEN_REQUEST_URL` is unset. - `key`: `cosign sign -y --key env://COSIGN_PRIVATE_KEY`. Requires `inputs.signing-key` to be set. +**Signature format — `new-bundle-format` (default `"false"`). Do not change this.** + +All four `cosign sign` invocations pass `--new-bundle-format=false`. cosign 3.x flipped +this default to `true`, which writes the signature as an OCI 1.1 referrer under a +`sha256-` tag instead of the legacy `sha256-.sig` tag. + +`containers/image` — the library behind podman, skopeo and `bootc switch` — discovers +signatures **only** via the `.sig` tag. A new-format signature is therefore invisible to +a `policy.json` `sigstoreSigned` entry: the image is signed, `cosign verify` passes, and +podman still rejects it. Because `cosign verify` accepts both formats, nothing in CI +catches the regression, which is why the `Assert legacy .sig tag exists` step exists — +it queries the registry directly for the `.sig` tag and fails the build if it is absent. + +Only set `new-bundle-format: "true"` if every consumer of the image verifies with cosign +rather than with podman/bootc policy. + **Step order (important):** gen-sbom → GitHub SBOM attestation → ORAS attach → sign SBOM artifact → SLSA provenance attestation. **SBOM flow** (when `generate-sbom: true`): Syft generates SPDX JSON → `actions/attest` with `sbom-path` creates a GitHub-native SBOM attestation in the attestation store → ORAS attaches the same SPDX JSON as an OCI referrer artifact → cosign signs the ORAS artifact digest. Both are needed: ORAS serves OCI-native consumers; the GitHub attestation store serves `gh attestation verify` and GitHub-native consumers.