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
53 changes: 49 additions & 4 deletions bootc-build/sign-and-publish/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<digest>.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
Expand Down Expand Up @@ -80,26 +96,28 @@ 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 }}
with:
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'
Expand All @@ -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-<digest>.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
Expand Down Expand Up @@ -193,26 +236,28 @@ 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 }}
with:
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'
Expand Down
16 changes: 16 additions & 0 deletions docs/skills/composite-actions/action-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<digest>` tag instead of the legacy `sha256-<digest>.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.
Expand Down
Loading