bitcoin/bitcoin

View on GitHub
contrib/guix/guix-attest

Summary

Maintainability
Test Coverage
#!/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
    # make it possible to override the gpg binary
    GPG=${GPG:-gpg}

    # $GPG can contain extra arguments passed to the binary
    # so let's check only the existence of arg[0]
    # shellcheck disable=SC2206
    GPG_ARRAY=($GPG)
    check_tools "${GPG_ARRAY[0]}"
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
}

echo "Attesting to build outputs for version: '${VERSION}'"
echo ""

# Given a SHA256SUMS file as stdin that has lines like:
#     0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6  a/b/d/c/d/s/bitcoin-22.0rc2-riscv64-linux-gnu.tar.gz
#     ...
#
# Replace each line's file name with its basename:
#     0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6  bitcoin-22.0rc2-riscv64-linux-gnu.tar.gz
#     ...
#
basenameify_SHA256SUMS() {
    sed -E 's@(^[[:xdigit:]]{64}[[:space:]]+).+/([^/]+$)@\1\2@'
}

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 \
            | basenameify_SHA256SUMS \
                > "$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_all="$(mktemp)"
    trap 'rm -rf -- "$temp_all"' 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 \
            | basenameify_SHA256SUMS \
                > "$temp_all"
        if [ -e all.SHA256SUMS ]; then
            # The SHA256SUMS already exists, make sure it's exactly what we
            # expect, error out if not
            if diff -u all.SHA256SUMS "$temp_all"; 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_all" all.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 ""
)