diff --git a/.github/workflows/daemonless-build.yaml b/.github/workflows/daemonless-build.yaml index 24cb84c..b40d732 100644 --- a/.github/workflows/daemonless-build.yaml +++ b/.github/workflows/daemonless-build.yaml @@ -641,10 +641,15 @@ jobs: git pull --rebase origin main git push - # Sign the SBOMs onto the pushed images as cosign attestations. - # Runs on Linux (keyless needs the GitHub OIDC token) against the already-pushed - # ghcr images -- keyless (GitHub OIDC), so no keys to manage. Consumers: - # cosign verify-attestation --type cyclonedx ghcr.io/daemonless/: + # Sign the pushed images, and sign the SBOMs onto them as cosign + # attestations. Runs on Linux (keyless needs the GitHub OIDC token) against + # the already-pushed ghcr images -- keyless (GitHub OIDC), so no keys to + # manage. A signature is on the image's digest, so every tag of that + # digest (the versioned ones push.py adds too) verifies. Consumers: + # cosign verify ghcr.io/daemonless/: \ + # --certificate-identity-regexp 'github.com/daemonless/' \ + # --certificate-oidc-issuer https://token.actions.githubusercontent.com + # cosign verify-attestation --type cyclonedx ghcr.io/daemonless/: ... attest-sbom: needs: build if: success() && github.event_name != 'pull_request' @@ -668,12 +673,13 @@ jobs: pattern: sbom-* path: sbom-artifacts/ - - name: Attest SBOMs onto pushed images + - name: Sign pushed images and attest SBOMs onto them env: IMAGE_NAME: ${{ inputs.image_name }} run: | set -euo pipefail shopt -s nullglob + signed="" # actions/download-artifact only nests matched artifacts under a # per-artifact subdirectory when 2+ artifacts match the pattern -- # with exactly one match (single-variant images) it flattens the @@ -691,10 +697,57 @@ jobs: tag="${rest%-spdx.json}"; predicate_type="spdxjson" fi ref="ghcr.io/daemonless/${IMAGE_NAME}:${tag}" + # Once per tag: both SBOM formats name the same image. + if [[ " $signed " != *" $tag "* ]]; then + echo "=== Signing ${ref} ===" + cosign sign --yes "$ref" + signed="$signed $tag" + fi echo "=== Attesting ${predicate_type} SBOM onto ${ref} (${f}) ===" cosign attest --yes --type "$predicate_type" --predicate "$f" "$ref" done + # A multi-arch image's bare tag is a manifest list assembled after the + # builds; it has a digest of its own, so it is signed once it exists. + sign-manifests: + needs: [attest-sbom, create-manifests] + if: success() && github.event_name != 'pull_request' + runs-on: ubuntu-latest + permissions: + packages: write + id-token: write + contents: read + steps: + - name: Install cosign + uses: sigstore/cosign-installer@v4.1.2 + + - name: Login to GHCR + run: cosign login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} + + - name: Download SBOM artifacts + uses: actions/download-artifact@v8 + with: + pattern: sbom-* + path: sbom-artifacts/ + + - name: Sign the manifest lists + env: + IMAGE_NAME: ${{ inputs.image_name }} + run: | + set -euo pipefail + # The bare tag is the arch-suffixed one the SBOM is named for, + # less its suffix (see dbuild/config.py arch_tag_suffix). + signed="" + for f in $(find sbom-artifacts -name "${IMAGE_NAME}-*-cyclonedx.json"); do + tag="$(basename "$f")"; tag="${tag#"${IMAGE_NAME}"-}"; tag="${tag%-cyclonedx.json}" + bare="${tag%-amd64}"; bare="${bare%-aarch64}" + [[ "$bare" != "$tag" && " $signed " != *" $bare "* ]] || continue + ref="ghcr.io/daemonless/${IMAGE_NAME}:${bare}" + echo "=== Signing manifest list ${ref} ===" + cosign sign --yes "$ref" + signed="$signed $bare" + done + # Trigger status page update after successful builds notify-status: needs: [build, test-compose, commit-sbom, create-manifests]