guix-attest raw

   1  #!/usr/bin/env bash
   2  export LC_ALL=C
   3  set -e -o pipefail
   4  
   5  # Source the common prelude, which:
   6  #   1. Checks if we're at the top directory of the Limenka repository
   7  #   2. Defines a few common functions and variables
   8  #
   9  # shellcheck source=libexec/prelude.bash
  10  source "$(dirname "${BASH_SOURCE[0]}")/libexec/prelude.bash"
  11  
  12  
  13  ###################
  14  ## Sanity Checks ##
  15  ###################
  16  
  17  ################
  18  # Required non-builtin commands should be invokable
  19  ################
  20  
  21  check_tools cat env basename mkdir diff sort
  22  
  23  if [ -z "$NO_SIGN" ]; then
  24      # make it possible to override the gpg binary
  25      GPG=${GPG:-gpg}
  26  
  27      # $GPG can contain extra arguments passed to the binary
  28      # so let's check only the existence of arg[0]
  29      # shellcheck disable=SC2206
  30      GPG_ARRAY=($GPG)
  31      check_tools "${GPG_ARRAY[0]}"
  32  fi
  33  
  34  ################
  35  # Required env vars should be non-empty
  36  ################
  37  
  38  cmd_usage() {
  39  cat <<EOF
  40  Synopsis:
  41  
  42      env GUIX_SIGS_REPO=<path/to/guix.sigs> \\
  43          SIGNER=GPG_KEY_NAME[=SIGNER_NAME] \\
  44          [ NO_SIGN=1 ]
  45        ./contrib/guix/guix-attest
  46  
  47  Example w/o overriding signing name:
  48  
  49      env GUIX_SIGS_REPO=/home/achow101/guix.sigs \\
  50          SIGNER=achow101 \\
  51        ./contrib/guix/guix-attest
  52  
  53  Example overriding signing name:
  54  
  55      env GUIX_SIGS_REPO=/home/dongcarl/guix.sigs \\
  56          SIGNER=0x96AB007F1A7ED999=dongcarl \\
  57        ./contrib/guix/guix-attest
  58  
  59  Example w/o signing, just creating SHA256SUMS:
  60  
  61      env GUIX_SIGS_REPO=/home/achow101/guix.sigs \\
  62          SIGNER=achow101 \\
  63          NO_SIGN=1 \\
  64        ./contrib/guix/guix-attest
  65  
  66  EOF
  67  }
  68  
  69  if [ -z "$GUIX_SIGS_REPO" ] || [ -z "$SIGNER" ]; then
  70      cmd_usage
  71      exit 1
  72  fi
  73  
  74  ################
  75  # GUIX_SIGS_REPO should exist as a directory
  76  ################
  77  
  78  if [ ! -d "$GUIX_SIGS_REPO" ]; then
  79  cat << EOF
  80  ERR: The specified GUIX_SIGS_REPO is not an existent directory:
  81  
  82      '$GUIX_SIGS_REPO'
  83  
  84  Hint: Please clone the guix.sigs repository and point to it with the
  85        GUIX_SIGS_REPO environment variable.
  86  
  87  EOF
  88  cmd_usage
  89  exit 1
  90  fi
  91  
  92  ################
  93  # The key specified in SIGNER should be usable
  94  ################
  95  
  96  IFS='=' read -r gpg_key_name signer_name <<< "$SIGNER"
  97  if [ -z "${signer_name}" ]; then
  98      signer_name="$gpg_key_name"
  99  fi
 100  
 101  if [ -z "$NO_SIGN" ] && ! ${GPG} --dry-run --list-secret-keys "${gpg_key_name}" >/dev/null 2>&1; then
 102      echo "ERR: GPG can't seem to find any key named '${gpg_key_name}'"
 103      exit 1
 104  fi
 105  
 106  ################
 107  # We should be able to find at least one output
 108  ################
 109  
 110  echo "Looking for build output SHA256SUMS fragments in ${OUTDIR_BASE}"
 111  
 112  shopt -s nullglob
 113  sha256sum_fragments=( "$OUTDIR_BASE"/*/SHA256SUMS.part ) # This expands to an array of directories...
 114  shopt -u nullglob
 115  
 116  noncodesigned_fragments=()
 117  codesigned_fragments=()
 118  
 119  if (( ${#sha256sum_fragments[@]} )); then
 120      echo "Found build output SHA256SUMS fragments:"
 121      for outdir in "${sha256sum_fragments[@]}"; do
 122          echo "    '$outdir'"
 123          case "$outdir" in
 124              "$OUTDIR_BASE"/*-codesigned/SHA256SUMS.part)
 125                  codesigned_fragments+=("$outdir")
 126                  ;;
 127              *)
 128                  noncodesigned_fragments+=("$outdir")
 129                  ;;
 130          esac
 131      done
 132      echo
 133  else
 134      echo "ERR: Could not find any build output SHA256SUMS fragments in ${OUTDIR_BASE}"
 135      exit 1
 136  fi
 137  
 138  ##############
 139  ##  Attest  ##
 140  ##############
 141  
 142  # Usage: out_name $outdir
 143  #
 144  #   HOST: The output directory being attested
 145  #
 146  out_name() {
 147      basename "$(dirname "$1")"
 148  }
 149  
 150  shasum_already_exists() {
 151  cat <<EOF
 152  --
 153  
 154  ERR: An ${1} file already exists for '${VERSION}' and attests
 155       differently. You likely previously attested to a partial build (e.g. one
 156       where you specified the HOST environment variable).
 157  
 158       See the diff above for more context.
 159  
 160  Hint: You may wish to remove the existing attestations and their signatures by
 161        invoking:
 162  
 163            rm '${PWD}/${1}'{,.asc}
 164  
 165        Then try running this script again.
 166  
 167  EOF
 168  }
 169  
 170  echo "Attesting to build outputs for version: '${VERSION}'"
 171  echo ""
 172  
 173  # Given a SHA256SUMS file as stdin that has lines like:
 174  #     0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6  a/b/d/c/d/s/limenka-22.0rc2-riscv64-linux-gnu.tar.gz
 175  #     ...
 176  #
 177  # Replace each line's file name with its basename:
 178  #     0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6  limenka-22.0rc2-riscv64-linux-gnu.tar.gz
 179  #     ...
 180  #
 181  basenameify_SHA256SUMS() {
 182      sed -E 's@(^[[:xdigit:]]{64}[[:space:]]+).+/([^/]+$)@\1\2@'
 183  }
 184  
 185  outsigdir="$GUIX_SIGS_REPO/$VERSION/$signer_name"
 186  mkdir -p "$outsigdir"
 187  (
 188      cd "$outsigdir"
 189  
 190      temp_noncodesigned="$(mktemp)"
 191      trap 'rm -rf -- "$temp_noncodesigned"' EXIT
 192  
 193      if (( ${#noncodesigned_fragments[@]} )); then
 194          cat "${noncodesigned_fragments[@]}" \
 195              | sort -u \
 196              | sort -k2 \
 197              | basenameify_SHA256SUMS \
 198                  > "$temp_noncodesigned"
 199          if [ -e noncodesigned.SHA256SUMS ]; then
 200              # The SHA256SUMS already exists, make sure it's exactly what we
 201              # expect, error out if not
 202              if diff -u noncodesigned.SHA256SUMS "$temp_noncodesigned"; then
 203                  echo "A noncodesigned.SHA256SUMS file already exists for '${VERSION}' and is up-to-date."
 204              else
 205                  shasum_already_exists noncodesigned.SHA256SUMS
 206                  exit 1
 207              fi
 208          else
 209              mv "$temp_noncodesigned" noncodesigned.SHA256SUMS
 210          fi
 211      else
 212          echo "ERR: No noncodesigned outputs found for '${VERSION}', exiting..."
 213          exit 1
 214      fi
 215  
 216      temp_all="$(mktemp)"
 217      trap 'rm -rf -- "$temp_all"' EXIT
 218  
 219      if (( ${#codesigned_fragments[@]} )); then
 220          # Note: all.SHA256SUMS attests to all of $sha256sum_fragments, but is
 221          #       not needed if there are no $codesigned_fragments
 222          cat "${sha256sum_fragments[@]}" \
 223              | sort -u \
 224              | sort -k2 \
 225              | basenameify_SHA256SUMS \
 226                  > "$temp_all"
 227          if [ -e all.SHA256SUMS ]; then
 228              # The SHA256SUMS already exists, make sure it's exactly what we
 229              # expect, error out if not
 230              if diff -u all.SHA256SUMS "$temp_all"; then
 231                  echo "An all.SHA256SUMS file already exists for '${VERSION}' and is up-to-date."
 232              else
 233                  shasum_already_exists all.SHA256SUMS
 234                  exit 1
 235              fi
 236          else
 237              mv "$temp_all" all.SHA256SUMS
 238          fi
 239      else
 240          # It is fine to have the codesigned outputs be missing (perhaps the
 241          # detached codesigs have not been published yet), just print a log
 242          # message instead of erroring out
 243          echo "INFO: No codesigned outputs found for '${VERSION}', skipping..."
 244      fi
 245  
 246      if [ -z "$NO_SIGN" ]; then
 247          echo "Signing SHA256SUMS to produce SHA256SUMS.asc"
 248          for i in *.SHA256SUMS; do
 249              if [ ! -e "$i".asc ]; then
 250                  ${GPG} --detach-sign \
 251                         --digest-algo sha256 \
 252                         --local-user "$gpg_key_name" \
 253                         --armor \
 254                         --output "$i".asc "$i"
 255              else
 256                  echo "Signature already there"
 257              fi
 258          done
 259      else
 260          echo "Not signing SHA256SUMS as \$NO_SIGN is not empty"
 261      fi
 262      echo ""
 263  )
 264