scripts/sign-release.sh signs with a key file or the PEM key from RELEASE_SIGNING_KEY (private temporary file, never printed), verifies with openssl pkeyutl -rawin and has a selftest with a throwaway key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
3.9 KiB
Bash
Executable File
132 lines
3.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Signs and verifies release checksums with Ed25519 (ADR 0024, docs/RELEASE.md).
|
|
# The signature is the raw 64-byte output of
|
|
# `openssl pkeyutl -sign -rawin` over SHA256SUMS, stored as SHA256SUMS.sig.
|
|
#
|
|
# Usage:
|
|
# scripts/sign-release.sh sign SUMS [KEY_FILE]
|
|
# writes SUMS.sig; without KEY_FILE the PEM private key is taken from
|
|
# the environment variable RELEASE_SIGNING_KEY (it is written to a
|
|
# private temporary file and removed again, never printed)
|
|
# scripts/sign-release.sh verify SUMS SIG PUBLIC_KEY_FILE
|
|
# exits 0 only if SIG is a valid signature of SUMS
|
|
# scripts/sign-release.sh public-key KEY_FILE
|
|
# prints the PEM public key that belongs to a private key
|
|
# scripts/sign-release.sh selftest
|
|
# signs and verifies with a throwaway key (CI); a tampered file and a
|
|
# foreign key must fail
|
|
#
|
|
# Requires OpenSSL 3.
|
|
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
sed -n '4,21p' "$0" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
die() {
|
|
echo "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
command -v openssl >/dev/null || die "openssl is required"
|
|
|
|
# sign SUMS KEY_FILE
|
|
sign_with_file() {
|
|
local sums="$1" key="$2"
|
|
[[ -f "$sums" ]] || die "no such file: $sums"
|
|
[[ -f "$key" ]] || die "no such key file: $key"
|
|
openssl pkeyutl -sign -rawin -inkey "$key" -in "$sums" -out "${sums}.sig"
|
|
[[ "$(wc -c <"${sums}.sig")" -eq 64 ]] || die "unexpected signature size (is the key an Ed25519 key?)"
|
|
echo "signed ${sums} -> ${sums}.sig"
|
|
}
|
|
|
|
cmd_sign() {
|
|
(($# >= 1 && $# <= 2)) || {
|
|
usage >&2
|
|
exit 2
|
|
}
|
|
if (($# == 2)); then
|
|
sign_with_file "$1" "$2"
|
|
return
|
|
fi
|
|
[[ -n "${RELEASE_SIGNING_KEY:-}" ]] || die "RELEASE_SIGNING_KEY is not set and no key file was given"
|
|
local dir
|
|
dir="$(umask 077 && mktemp -d)"
|
|
# shellcheck disable=SC2064 # expand now: $dir is local
|
|
trap "rm -rf '$dir'" EXIT
|
|
(umask 077 && printf '%s\n' "$RELEASE_SIGNING_KEY" >"$dir/key.pem")
|
|
sign_with_file "$1" "$dir/key.pem"
|
|
rm -rf "$dir"
|
|
trap - EXIT
|
|
}
|
|
|
|
cmd_verify() {
|
|
(($# == 3)) || {
|
|
usage >&2
|
|
exit 2
|
|
}
|
|
local sums="$1" sig="$2" pub="$3"
|
|
for f in "$sums" "$sig" "$pub"; do
|
|
[[ -f "$f" ]] || die "no such file: $f"
|
|
done
|
|
if openssl pkeyutl -verify -pubin -inkey "$pub" -rawin -in "$sums" -sigfile "$sig" >/dev/null 2>&1; then
|
|
echo "signature of ${sums} is valid"
|
|
else
|
|
die "signature of ${sums} is NOT valid"
|
|
fi
|
|
}
|
|
|
|
cmd_public_key() {
|
|
(($# == 1)) || {
|
|
usage >&2
|
|
exit 2
|
|
}
|
|
openssl pkey -in "$1" -pubout
|
|
}
|
|
|
|
cmd_selftest() {
|
|
local dir
|
|
dir="$(umask 077 && mktemp -d)"
|
|
# shellcheck disable=SC2064 # expand now: $dir is local
|
|
trap "rm -rf '$dir'" EXIT
|
|
openssl genpkey -algorithm ed25519 -out "$dir/key.pem" 2>/dev/null
|
|
openssl pkey -in "$dir/key.pem" -pubout -out "$dir/pub.pem"
|
|
openssl genpkey -algorithm ed25519 -out "$dir/other.pem" 2>/dev/null
|
|
openssl pkey -in "$dir/other.pem" -pubout -out "$dir/other-pub.pem"
|
|
printf '%s example_v0.0.0_linux_amd64.tar.gz\n' "$(printf 'x' | sha256sum | cut -d' ' -f1)" >"$dir/SHA256SUMS"
|
|
|
|
# Signing from the environment, as the release workflow does.
|
|
RELEASE_SIGNING_KEY="$(cat "$dir/key.pem")" bash "$0" sign "$dir/SHA256SUMS" >/dev/null
|
|
cmd_verify "$dir/SHA256SUMS" "$dir/SHA256SUMS.sig" "$dir/pub.pem" >/dev/null ||
|
|
die "selftest: valid signature rejected"
|
|
if bash "$0" verify "$dir/SHA256SUMS" "$dir/SHA256SUMS.sig" "$dir/other-pub.pem" >/dev/null 2>&1; then
|
|
die "selftest: signature accepted with a foreign key"
|
|
fi
|
|
printf 'tampered\n' >>"$dir/SHA256SUMS"
|
|
if bash "$0" verify "$dir/SHA256SUMS" "$dir/SHA256SUMS.sig" "$dir/pub.pem" >/dev/null 2>&1; then
|
|
die "selftest: tampered file accepted"
|
|
fi
|
|
echo "selftest passed: sign, verify, foreign key and tampered file rejected"
|
|
}
|
|
|
|
(($# >= 1)) || {
|
|
usage >&2
|
|
exit 2
|
|
}
|
|
cmd="$1"
|
|
shift
|
|
case "$cmd" in
|
|
sign) cmd_sign "$@" ;;
|
|
verify) cmd_verify "$@" ;;
|
|
public-key) cmd_public_key "$@" ;;
|
|
selftest) cmd_selftest "$@" ;;
|
|
-h | --help | help) usage ;;
|
|
*)
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|