|
|
|
#!/usr/bin/env bash
|
|
|
|
export LC_ALL=C
|
|
|
|
set -e -o pipefail
|
|
|
|
|
|
|
|
# Source the common prelude, which:
|
|
|
|
# 1. Checks if we're at the top directory of the Bitcoin Core repository
|
|
|
|
# 2. Defines a few common functions and variables
|
|
|
|
#
|
|
|
|
# shellcheck source=libexec/prelude.bash
|
|
|
|
source "$(dirname "${BASH_SOURCE[0]}")/libexec/prelude.bash"
|
|
|
|
|
|
|
|
|
|
|
|
###################
|
|
|
|
## Sanity Checks ##
|
|
|
|
###################
|
|
|
|
|
|
|
|
################
|
|
|
|
# Required non-builtin commands should be invokable
|
|
|
|
################
|
|
|
|
|
|
|
|
check_tools cat env basename mkdir diff sort
|
|
|
|
if [ -z "$NO_SIGN" ]; then
|
|
|
|
check_tools gpg
|
|
|
|
fi
|
|
|
|
|
|
|
|
################
|
|
|
|
# Required env vars should be non-empty
|
|
|
|
################
|
|
|
|
|
|
|
|
cmd_usage() {
|
|
|
|
cat <<EOF
|
|
|
|
Synopsis:
|
|
|
|
|
|
|
|
env GUIX_SIGS_REPO=<path/to/guix.sigs> \\
|
|
|
|
SIGNER=GPG_KEY_NAME[=SIGNER_NAME] \\
|
|
|
|
[ NO_SIGN=1 ]
|
|
|
|
./contrib/guix/guix-attest
|
|
|
|
|
|
|
|
Example w/o overriding signing name:
|
|
|
|
|
|
|
|
env GUIX_SIGS_REPO=/home/achow101/guix.sigs \\
|
|
|
|
SIGNER=achow101 \\
|
|
|
|
./contrib/guix/guix-attest
|
|
|
|
|
|
|
|
Example overriding signing name:
|
|
|
|
|
|
|
|
env GUIX_SIGS_REPO=/home/dongcarl/guix.sigs \\
|
|
|
|
SIGNER=0x96AB007F1A7ED999=dongcarl \\
|
|
|
|
./contrib/guix/guix-attest
|
|
|
|
|
|
|
|
Example w/o signing, just creating SHA256SUMS:
|
|
|
|
|
|
|
|
env GUIX_SIGS_REPO=/home/achow101/guix.sigs \\
|
|
|
|
SIGNER=achow101 \\
|
|
|
|
NO_SIGN=1 \\
|
|
|
|
./contrib/guix/guix-attest
|
|
|
|
|
|
|
|
EOF
|
|
|
|
}
|
|
|
|
|
|
|
|
if [ -z "$GUIX_SIGS_REPO" ] || [ -z "$SIGNER" ]; then
|
|
|
|
cmd_usage
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
################
|
|
|
|
# GUIX_SIGS_REPO should exist as a directory
|
|
|
|
################
|
|
|
|
|
|
|
|
if [ ! -d "$GUIX_SIGS_REPO" ]; then
|
|
|
|
cat << EOF
|
|
|
|
ERR: The specified GUIX_SIGS_REPO is not an existent directory:
|
|
|
|
|
|
|
|
'$GUIX_SIGS_REPO'
|
|
|
|
|
|
|
|
Hint: Please clone the guix.sigs repository and point to it with the
|
|
|
|
GUIX_SIGS_REPO environment variable.
|
|
|
|
|
|
|
|
EOF
|
|
|
|
cmd_usage
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
################
|
|
|
|
# The key specified in SIGNER should be usable
|
|
|
|
################
|
|
|
|
|
|
|
|
IFS='=' read -r gpg_key_name signer_name <<< "$SIGNER"
|
|
|
|
if [ -z "${signer_name}" ]; then
|
|
|
|
signer_name="$gpg_key_name"
|
|
|
|
fi
|
|
|
|
|
|
|
|
if [ -z "$NO_SIGN" ] && ! gpg --dry-run --list-secret-keys "${gpg_key_name}" >/dev/null 2>&1; then
|
|
|
|
echo "ERR: GPG can't seem to find any key named '${gpg_key_name}'"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
################
|
|
|
|
# We should be able to find at least one output
|
|
|
|
################
|
|
|
|
|
|
|
|
echo "Looking for build output SHA256SUMS fragments in ${OUTDIR_BASE}"
|
|
|
|
|
|
|
|
shopt -s nullglob
|
|
|
|
sha256sum_fragments=( "$OUTDIR_BASE"/*/SHA256SUMS.part ) # This expands to an array of directories...
|
|
|
|
shopt -u nullglob
|
|
|
|
|
|
|
|
noncodesigned_fragments=()
|
|
|
|
codesigned_fragments=()
|
|
|
|
|
|
|
|
if (( ${#sha256sum_fragments[@]} )); then
|
|
|
|
echo "Found build output SHA256SUMS fragments:"
|
|
|
|
for outdir in "${sha256sum_fragments[@]}"; do
|
|
|
|
echo " '$outdir'"
|
|
|
|
case "$outdir" in
|
|
|
|
"$OUTDIR_BASE"/*-codesigned/SHA256SUMS.part)
|
|
|
|
codesigned_fragments+=("$outdir")
|
|
|
|
;;
|
|
|
|
*)
|
|
|
|
noncodesigned_fragments+=("$outdir")
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
done
|
|
|
|
echo
|
|
|
|
else
|
|
|
|
echo "ERR: Could not find any build output SHA256SUMS fragments in ${OUTDIR_BASE}"
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
##############
|
|
|
|
## Attest ##
|
|
|
|
##############
|
|
|
|
|
|
|
|
# Usage: out_name $outdir
|
|
|
|
#
|
|
|
|
# HOST: The output directory being attested
|
|
|
|
#
|
|
|
|
out_name() {
|
|
|
|
basename "$(dirname "$1")"
|
|
|
|
}
|
|
|
|
|
|
|
|
shasum_already_exists() {
|
|
|
|
cat <<EOF
|
|
|
|
--
|
|
|
|
|
|
|
|
ERR: An ${1} file already exists for '${VERSION}' and attests
|
|
|
|
differently. You likely previously attested to a partial build (e.g. one
|
|
|
|
where you specified the HOST environment variable).
|
|
|
|
|
|
|
|
See the diff above for more context.
|
|
|
|
|
|
|
|
Hint: You may wish to remove the existing attestations and their signatures by
|
|
|
|
invoking:
|
|
|
|
|
|
|
|
rm '${PWD}/${1}'{,.asc}
|
|
|
|
|
|
|
|
Then try running this script again.
|
|
|
|
|
|
|
|
EOF
|
|
|
|
}
|
|
|
|
|
|
|
|
# Given a document with unix line endings (just <LF>) in stdin, make all lines
|
|
|
|
# end in <CR><LF> and make sure there's no trailing <LF> at the end of the file.
|
|
|
|
#
|
|
|
|
# This is necessary as cleartext signatures are calculated on text after their
|
|
|
|
# line endings are canonicalized.
|
|
|
|
#
|
|
|
|
# For more information:
|
|
|
|
# 1. https://security.stackexchange.com/a/104261
|
|
|
|
# 2. https://datatracker.ietf.org/doc/html/rfc4880#section-7.1
|
|
|
|
#
|
|
|
|
rfc4880_normalize_document() {
|
|
|
|
sed 's/$/\r/' | head -c -2
|
|
|
|
}
|
|
|
|
|
|
|
|
echo "Attesting to build outputs for version: '${VERSION}'"
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
outsigdir="$GUIX_SIGS_REPO/$VERSION/$signer_name"
|
|
|
|
mkdir -p "$outsigdir"
|
|
|
|
(
|
|
|
|
cd "$outsigdir"
|
|
|
|
|
|
|
|
temp_noncodesigned="$(mktemp)"
|
|
|
|
trap 'rm -rf -- "$temp_noncodesigned"' EXIT
|
|
|
|
|
|
|
|
if (( ${#noncodesigned_fragments[@]} )); then
|
|
|
|
cat "${noncodesigned_fragments[@]}" \
|
|
|
|
| sort -u \
|
|
|
|
| sort -k2 \
|
|
|
|
| rfc4880_normalize_document \
|
|
|
|
> "$temp_noncodesigned"
|
|
|
|
if [ -e noncodesigned.SHA256SUMS ]; then
|
|
|
|
# The SHA256SUMS already exists, make sure it's exactly what we
|
|
|
|
# expect, error out if not
|
|
|
|
if diff -u noncodesigned.SHA256SUMS "$temp_noncodesigned"; then
|
|
|
|
echo "A noncodesigned.SHA256SUMS file already exists for '${VERSION}' and is up-to-date."
|
|
|
|
else
|
|
|
|
shasum_already_exists noncodesigned.SHA256SUMS
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
else
|
|
|
|
mv "$temp_noncodesigned" noncodesigned.SHA256SUMS
|
|
|
|
fi
|
|
|
|
else
|
|
|
|
echo "ERR: No noncodesigned outputs found for '${VERSION}', exiting..."
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
|
|
|
|
temp_codesigned="$(mktemp)"
|
|
|
|
trap 'rm -rf -- "$temp_codesigned"' EXIT
|
|
|
|
|
|
|
|
if (( ${#codesigned_fragments[@]} )); then
|
|
|
|
# Note: all.SHA256SUMS attests to all of $sha256sum_fragments, but is
|
|
|
|
# not needed if there are no $codesigned_fragments
|
|
|
|
cat "${sha256sum_fragments[@]}" \
|
|
|
|
| sort -u \
|
|
|
|
| sort -k2 \
|
|
|
|
| sed 's/$/\r/' \
|
|
|
|
| rfc4880_normalize_document \
|
|
|
|
> "$temp_codesigned"
|
|
|
|
if [ -e codesigned.SHA256SUMS ]; then
|
|
|
|
# The SHA256SUMS already exists, make sure it's exactly what we
|
|
|
|
# expect, error out if not
|
|
|
|
if diff -u all.SHA256SUMS "$temp_codesigned"; then
|
|
|
|
echo "An all.SHA256SUMS file already exists for '${VERSION}' and is up-to-date."
|
|
|
|
else
|
|
|
|
shasum_already_exists all.SHA256SUMS
|
|
|
|
exit 1
|
|
|
|
fi
|
|
|
|
else
|
|
|
|
mv "$temp_codesigned" codesigned.SHA256SUMS
|
|
|
|
fi
|
|
|
|
else
|
|
|
|
# It is fine to have the codesigned outputs be missing (perhaps the
|
|
|
|
# detached codesigs have not been published yet), just print a log
|
|
|
|
# message instead of erroring out
|
|
|
|
echo "INFO: No codesigned outputs found for '${VERSION}', skipping..."
|
|
|
|
fi
|
|
|
|
|
|
|
|
if [ -z "$NO_SIGN" ]; then
|
|
|
|
echo "Signing SHA256SUMS to produce SHA256SUMS.asc"
|
|
|
|
for i in *.SHA256SUMS; do
|
|
|
|
if [ ! -e "$i".asc ]; then
|
|
|
|
gpg --detach-sign \
|
|
|
|
--digest-algo sha256 \
|
|
|
|
--local-user "$gpg_key_name" \
|
|
|
|
--armor \
|
|
|
|
--output "$i".asc "$i"
|
|
|
|
else
|
|
|
|
echo "Signature already there"
|
|
|
|
fi
|
|
|
|
done
|
|
|
|
else
|
|
|
|
echo "Not signing SHA256SUMS as \$NO_SIGN is not empty"
|
|
|
|
fi
|
|
|
|
echo ""
|
|
|
|
)
|