diff --git a/.github/workflows/build-ai-cluster-iso.yml b/.github/workflows/build-ai-cluster-iso.yml index c8c0d9c011..aa62f738da 100644 --- a/.github/workflows/build-ai-cluster-iso.yml +++ b/.github/workflows/build-ai-cluster-iso.yml @@ -71,6 +71,12 @@ on: permissions: contents: read + id-token: write # B-0853.1: sigstore/cosign keyless OIDC signing. Fulcio CA + # mints a short-lived cert from the GitHub-issued OIDC token; + # Rekor transparency log records the signature. No private + # key material is handled in this workflow. The token-issuance + # scope is workflow-bound; granted permission cannot be + # exfiltrated to mint signatures for other workflows. concurrency: group: build-ai-cluster-iso-${{ github.workflow }}-${{ github.ref }} @@ -300,6 +306,63 @@ jobs: echo "to flash the stick. (ISO is ~1.5–2 GiB.)" } >> "$GITHUB_STEP_SUMMARY" + # B-0853.1 — sigstore/cosign keyless OIDC signing of the ISO blob. + # Composes with B-0853 (sigstore artifact signing free-stuff substrate). + # + # Keyless model: GitHub OIDC token (from id-token: write permission at + # workflow level) is exchanged with Fulcio CA for a short-lived cert + # bound to this workflow's identity. cosign then signs the ISO blob + # with that cert + records the signature in the Rekor transparency log. + # No long-lived private key material in this workflow. + # + # Verification (any consumer): + # cosign verify-blob \ + # --certificate .pem \ + # --signature .sig \ + # --certificate-identity-regexp '^https://github.com/Lucent-Financial-Group/Zeta' \ + # --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + # + # + # Security: inputs to cosign are filesystem paths from steps.iso.outputs + # of THIS workflow (no github.event.* interpolation). Same discipline + # as the QEMU boot-test step above. + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + # Pin verified via gh API 2026-05-27: + # gh api repos/sigstore/cosign-installer/releases/latest + # tag: v4.1.2; published: 2026-05-07T01:27:27Z + # Per .claude/rules/dep-pin-search-first-authority.md + + - name: Sign ISO with cosign (keyless OIDC + Fulcio + Rekor) + env: + # Pass ISO path via env (not direct ${{ }} interpolation in run:) + # per the workflow's existing security discipline. + ISO_PATH: ${{ steps.iso.outputs.path }} + ISO_NAME: ${{ steps.iso.outputs.name }} + run: | + set -euo pipefail + # --yes: skip interactive Fulcio confirmation (CI is non-interactive) + # Output files emitted alongside the ISO: + # .sig — signature (base64; small) + # .pem — short-lived Fulcio cert (the verifier's anchor) + cosign sign-blob --yes \ + --output-signature "${ISO_PATH}.sig" \ + --output-certificate "${ISO_PATH}.pem" \ + "${ISO_PATH}" + # Sanity: both artifacts present and non-empty. + test -s "${ISO_PATH}.sig" || { echo "::error::Empty signature"; exit 1; } + test -s "${ISO_PATH}.pem" || { echo "::error::Empty certificate"; exit 1; } + { + echo "## ISO signed via sigstore (cosign keyless OIDC)" + echo "" + echo "| Artifact | Path |" + echo "|---|---|" + echo "| Signature | \`${ISO_NAME}.sig\` |" + echo "| Certificate | \`${ISO_NAME}.pem\` |" + echo "" + echo "Verification: see workflow run logs for the canonical \`cosign verify-blob\` command." + } >> "$GITHUB_STEP_SUMMARY" + - name: Upload ISO as workflow artifact # Available for download from the workflow run page for ~90 days. uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 @@ -309,3 +372,16 @@ jobs: if-no-files-found: error retention-days: 90 compression-level: 0 # ISO is already compressed; re-zipping wastes time + + - name: Upload cosign signature + certificate as workflow artifact + # B-0853.1 sibling upload — sig + pem alongside the ISO for consumers. + # Separate artifact (not bundled with ISO) so consumers downloading + # just the ISO don't pay the (small) cost; verifiers can grab both. + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: ${{ steps.iso.outputs.name }}.cosign + path: | + ${{ steps.iso.outputs.path }}.sig + ${{ steps.iso.outputs.path }}.pem + if-no-files-found: error + retention-days: 90