Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

crates/dwarves-slsa/scripts/sign-release.sh

#!/bin/bash
# sign-release.sh — sign ONE release statement on the YubiKey (the owner's step, per release).
#
#   sign-release.sh <release.statement.json> [<out.intoto.jsonl>]
#
# Run it on the machine the YubiKey is plugged into (your laptop, or dwarf's HOST; never
# inside a build container). Its only input is the statement file the release runner
# wrote; its output is the signed bundle (one DSSE envelope, one line), which goes back
# to the run directory on dwarf as release.intoto.jsonl.
#
# It shows what you sign (release, builder, sources at their commits, every artifact and
# its sha256) and asks you to type the release name. Then the YubiKey asks for its PIN
# (typed into yubico-piv-tool or cosign, never into this script) and blinks: touch it.
# The signature is checked against the release public key before the bundle is written.
#
# Environment (all optional):
#   SLSA_KEY     the release public key (PEM). Default: ./release-signing-key.pub, then
#                ~/slsa-release-key/release-signing-key.pub (what the setup script wrote),
#                then ../keys/release-signing-key.pub beside this script (the committed key).
#   SLSA_SIGNER  piv (default: yubico-piv-tool) or cosign (a cosign 2.x "pivkey" build:
#                cosign sign-blob --sk).
#   SLSA_SLOT    the PIV slot (default 9c, the signature slot the setup script used).
#   SLSA_TEST_KEY  TESTS ONLY: a software P-256 private key (PEM) to sign with instead of
#                the YubiKey. A bundle signed this way does not verify with the release key.
#
# Needs: bash, openssl, base64, sha256sum (or shasum), and yubico-piv-tool (Ubuntu/Debian:
# `apt install yubico-piv-tool`; macOS: `brew install yubico-piv-tool`) or cosign.
# python3 is optional (a nicer summary).
set -euo pipefail

die() { echo "RED  $*" >&2; exit 1; }
[ $# -ge 1 ] && [ $# -le 2 ] || { sed -n '2,30p' "$0"; exit 2; }
ST=$1
[ -f "$ST" ] || die "no statement file $ST"
OUT=${2:-$(dirname "$ST")/release.intoto.jsonl}
[ ! -e "$OUT" ] || die "$OUT exists already; move it away first (a release is signed once)"
SIGNER=${SLSA_SIGNER:-piv}
SLOT=${SLSA_SLOT:-9c}
KEY=${SLSA_KEY:-}
if [ -z "$KEY" ]; then
  for k in ./release-signing-key.pub "$HOME/slsa-release-key/release-signing-key.pub" \
           "$(dirname "$0")/../keys/release-signing-key.pub"; do [ -f "$k" ] && { KEY=$k; break; }; done
fi
[ -n "$KEY" ] && [ -f "$KEY" ] || die "no release public key: set SLSA_KEY=<release-signing-key.pub>"

sha256() { if command -v sha256sum >/dev/null; then sha256sum | cut -d' ' -f1; else shasum -a 256 | cut -d' ' -f1; fi; }
b64() { base64 | tr -d '\n'; }
for t in openssl base64; do command -v $t >/dev/null || die "$t is missing"; done

# ── what is signed ──
ST_SHA=$(sha256 < "$ST")
echo "statement  $ST"
echo "sha256     $ST_SHA"
if command -v python3 >/dev/null; then
  python3 -I - "$ST" <<'PY'
import json, sys
s = json.load(open(sys.argv[1]))
assert s.get("_type") == "https://in-toto.io/Statement/v1", "not an in-toto Statement v1"
assert s.get("predicateType") == "https://slsa.dev/provenance/v1", "not SLSA Provenance v1"
p = s["predicate"]; bd = p["buildDefinition"]; rd = p["runDetails"]; x = bd.get("externalParameters", {})
print(f"release    {x.get('release')}")
print(f"builder    {rd['builder']['id']}")
print(f"buildType  {bd['buildType']}")
print(f"invocation {rd['metadata'].get('invocationId')}")
print(f"built      {rd['metadata'].get('startedOn')} .. {rd['metadata'].get('finishedOn')}")
print(f"image      {bd.get('internalParameters', {}).get('image')}")
print("sources:")
for r in x.get("repositories", []):
    print(f"  {r['name']:<16} {r['commit']}  {r['repository']}")
print(f"artifacts ({len(s['subject'])}):")
for t in s["subject"]:
    print(f"  {t['digest']['sha256']}  {t['name']}")
PY
else
  grep -o '"release": *"[^"]*"' "$ST" | head -1
  echo "subjects: $(grep -c '"sha256":' "$ST") digests (install python3 for the full list)"
fi
REL=$(grep -o '"release": *"[^"]*"' "$ST" | head -1 | sed 's/.*: *"//; s/"$//')
[ -n "$REL" ] || die "the statement names no release"
echo
read -r -p "Type the release name ($REL) to sign it: " ANSWER
[ "$ANSWER" = "$REL" ] || die "not signed (you typed '$ANSWER')"

# ── the PAE, then the YubiKey ──
T=application/vnd.in-toto+json
W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
{ printf 'DSSEv1 %d %s %d ' ${#T} "$T" "$(wc -c < "$ST" | tr -d ' ')"; cat "$ST"; } > "$W/pae.bin"
if [ -n "${SLSA_TEST_KEY:-}" ]; then
  echo "TEST signer: software key $SLSA_TEST_KEY (not the YubiKey)"
  openssl dgst -sha256 -sign "$SLSA_TEST_KEY" -out "$W/sig.der" "$W/pae.bin"
elif [ "$SIGNER" = piv ]; then
  command -v yubico-piv-tool >/dev/null || die "yubico-piv-tool is missing (apt install yubico-piv-tool), or SLSA_SIGNER=cosign"
  echo "YubiKey: enter the PIV PIN when asked, then TOUCH the key when it blinks."
  yubico-piv-tool -a verify-pin --sign -s "$SLOT" -A ECCP256 -H SHA256 -i "$W/pae.bin" -o "$W/sig.out"
  # Raw DER is what the card returns; accept a base64 file too.
  if openssl asn1parse -inform DER -in "$W/sig.out" >/dev/null 2>&1; then mv "$W/sig.out" "$W/sig.der"
  else base64 -d < "$W/sig.out" > "$W/sig.der" 2>/dev/null || die "yubico-piv-tool wrote no DER signature"; fi
elif [ "$SIGNER" = cosign ]; then
  command -v cosign >/dev/null || die "cosign is missing"
  case "$SLOT" in 9a) CS=authentication;; 9c) CS=signature;; 9d) CS=key-management;; 9e) CS=card-authentication;; *) die "cosign knows slots 9a 9c 9d 9e";; esac
  echo "YubiKey (cosign --sk): enter the PIN when asked, then TOUCH the key when it blinks."
  cosign sign-blob --sk --slot "$CS" --tlog-upload=false --output-signature "$W/sig.b64" -y "$W/pae.bin"
  base64 -d < "$W/sig.b64" > "$W/sig.der"
else
  die "SLSA_SIGNER=$SIGNER: piv or cosign"
fi

# ── the signature against the release key, before anything is written ──
if [ -z "${SLSA_TEST_KEY:-}" ]; then
  openssl dgst -sha256 -verify "$KEY" -signature "$W/sig.der" "$W/pae.bin" >/dev/null \
    || die "the signature does not verify with $KEY (another slot or another YubiKey?)"
  echo "OK   the signature verifies with the release key $KEY"
fi
KEYID=$(openssl pkey -pubin -in "$KEY" -outform DER 2>/dev/null | sha256)
printf '{"payloadType":"%s","payload":"%s","signatures":[{"keyid":"%s","sig":"%s"}]}\n' \
  "$T" "$(b64 < "$ST")" "$KEYID" "$(b64 < "$W/sig.der")" > "$OUT"
echo "signed     $OUT (sha256 $(sha256 < "$OUT"))"
echo "next       copy it to the run directory on dwarf as release.intoto.jsonl (if it is not there already)"