#!/bin/bash # sign.sh @ : sign a published image and attach what it was judged by. # # The reference and every platform image behind it get a cosign signature made # with the key in COSIGN_PRIVATE_KEY_B64 (its password in COSIGN_PASSWORD). Each # platform image gets its CIS result (/cis-.json, image_tools/cis.py) # and its CycloneDX SBOM (/sbom-.cdx.json, image_tools/sbom.py) as # in-toto attestations. This takes the place of Docker Content Trust (CIS Docker # 4.5), as cosign does for Docker's Hardened Images. # # The platforms are whatever the reference holds (image_tools/platforms.py): an # index of amd64 and arm64, or one manifest. EXPECT_PLATFORMS ("amd64 arm64") # makes any other set an error. ATTEST_PLATFORMS ("arm64") attests only those: # a release index joins arm64 to an amd64 image its dev build already attested. # # Nothing goes to a public transparency log: cosign-signing.json names none, so # the names in this registry stay inside. Everything is verified against # cosign.pub before it returns. set -euo pipefail : "${COSIGN_PRIVATE_KEY_B64:?is empty: the secret is missing}" "${COSIGN_PASSWORD:?is empty: the secret is missing}" # The key comes base64 in one line: the runner prints a step's env and masks a # secret only where it appears whole, and a PEM key's lines did not (2026-10-22). COSIGN_PRIVATE_KEY=$(printf '%s' "$COSIGN_PRIVATE_KEY_B64" | base64 -d) export COSIGN_PRIVATE_KEY ref=$1 dir=$(cd "$2" && pwd) cd "$(dirname "$0")/.." CIS_TYPE=https://netork.io/attestations/cis/v1 # quietly : cosign's chatter only when it fails. quietly() { local out; out=$("$@" 2>&1) || { echo "$out" >&2; return 1; }; } case $ref in *@sha256:*) ;; *) echo "$ref is not pinned by digest" >&2; exit 1 ;; esac image=${ref%@*} case ${image##*/} in *:*) image=${image%:*} ;; esac # the tag, not a registry port quietly cosign sign --yes --recursive --key env://COSIGN_PRIVATE_KEY \ --signing-config cosign-signing.json "$ref" quietly cosign verify --key cosign.pub --insecure-ignore-tlog "$ref" raw=$(mktemp) config=$(mktemp) trap 'rm -f "$raw" "$config"' EXIT docker buildx imagetools inspect "$ref" --raw >"$raw" docker buildx imagetools inspect "$ref" --format '{{json .Image}}' >"$config" platforms=$(python3 -m image_tools.platforms "${ref#*@}" "$raw" "$config") if [ -n "${EXPECT_PLATFORMS:-}" ]; then got=$(echo "$platforms" | cut -d' ' -f1 | sort | xargs) want=$(echo "$EXPECT_PLATFORMS" | tr ' ' '\n' | sort | xargs) [ "$got" = "$want" ] || { echo "expected $want in $ref, found $got" >&2; exit 1; } fi while read -r arch digest; do case " ${ATTEST_PLATFORMS:-$arch} " in *" $arch "*) ;; *) echo "signed: $image@$digest ($arch)"; continue ;; esac for attestation in "$CIS_TYPE cis-$arch.json" "cyclonedx sbom-$arch.cdx.json"; do read -r type file <<<"$attestation" quietly cosign attest --yes --key env://COSIGN_PRIVATE_KEY --signing-config cosign-signing.json \ --type "$type" --predicate "$dir/$file" "$image@$digest" quietly cosign verify-attestation --key cosign.pub --insecure-ignore-tlog \ --type "$type" "$image@$digest" done echo "signed and attested: $image@$digest ($arch)" done <<<"$platforms"