Files
Christian Manivong e7ef8fe073
CI / Tests, lint (push) Successful in 9s
feat(sign): attest only the platforms a release adds
netork's release joins an arm64 image to the amd64 one its dev build
already signed and attested, under a new index. ATTEST_PLATFORMS (the sign
action's attest-platforms) signs the new index and every platform behind
it, and attests only the ones named.
2026-10-22 18:17:10 +02:00

63 lines
3.2 KiB
Bash
Executable File

#!/bin/bash
# sign.sh <image>@<digest> <dir>: 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 (<dir>/cis-<arch>.json, image_tools/cis.py)
# and its CycloneDX SBOM (<dir>/sbom-<arch>.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 <command>: 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"