#!/bin/bash

REPO=lightningnetwork
PROJECT=lnd

RELEASE_URL=https://github.com/$REPO/$PROJECT/releases
API_URL=https://api.github.com/repos/$REPO/$PROJECT/releases
MANIFEST_SELECTOR=". | select(.name | test(\"manifest-v.*(\\\\.txt)$\")) | .name"
SIGNATURE_SELECTOR=". | select(.name | test(\"manifest-.*(\\\\.sig)$\")) | .name"
HEADER_JSON="Accept: application/json"
HEADER_GH_JSON="Accept: application/vnd.github.v3+json"
MIN_REQUIRED_SIGNATURES=5

# All keys that can sign lnd releases. The key must be added as a file to the
# keys directory, for example: scripts/keys/<username>.asc
# The username in the key file must match the username used for signing a
# manifest (manifest-<username>-v0.xx.yy-beta.sig), otherwise the signature
# won't be counted.
# NOTE: Reviewers of this file must make sure that both the key IDs and
# usernames in the list below are unique!
KEYS=()
KEYS+=("F4FC70F07310028424EFC20A8E4256593F177720 guggero")
KEYS+=("15E7ECF257098A4EF91655EB4CA7FE54A6213C91 carlaKC")
KEYS+=("E4D85299674B2D31FAA1892E372CBD7633C61696 roasbeef")
KEYS+=("729E9D9D92C75A5FBFEEE057B5DD717BEF7CA5B1 wpaulino")
KEYS+=("7E81EF6B9989A9CC93884803118759E83439A9B1 Crypt-iQ")
KEYS+=("9FC6B0BFD597A94DBF09708280E5375C094198D8 bhandras")
KEYS+=("E97A1AB6C77A1D2B72F50A6F90E00CCB1C74C611 arshbot")
KEYS+=("EB13A98091E8D67CDD7FC5A7E9FE7FE00AD163A4 positiveblue")
KEYS+=("26984CB69EB8C4A26196F7A4D7D916376026F177 ellemouton")
KEYS+=("FE5E159A70C436D6AF4D2887B1F8848557AA29D2 ffranr")
KEYS+=("4DC235556B18694E08518DBB671103D881A5F0E4 sputn1ck")
KEYS+=("187F6ADD93AE3B0CF335AA6AB984570980684DCC ViktorTigerstrom")
KEYS+=("E85497D2DBA0EB9ADB0024279BCD95C4FF296868 yyforyongyu")
KEYS+=("32F7EA1E7A0339F7D37164B9F82D456EA023C9BF hieblmi")
KEYS+=("5295A477FFC8064D7057B191FA7E65C951F12439 proofofkeags")
KEYS+=("3E9BD4436C288039CA827A9200C9E2BC2E45666F suheb")
KEYS+=("5F75437E11695F86D50C11BB1AFF9C4DCED6D666 ziggie1984")

TEMP_DIR=$(mktemp -d /tmp/lnd-sig-verification-XXXXXX)

function check_command() {
  echo -n "Checking if $1 is installed... "
  if ! command -v "$1"; then
    echo "ERROR: $1 is not installed or not in PATH!"
    exit 1
  fi
}

function verify_version() {
  version_regex="^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]"
  if [[ ! "$1" =~ $version_regex ]]; then
    echo "ERROR: Invalid expected version detected: $1"
    exit 1
  fi
  echo "Expected version for binaries: $1"
}

function import_keys() {
  # A trick to get the absolute directory where this script is located, no
  # matter how or from where it was called. We'll need it to locate the key
  # files which are located relative to this script.
  DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"

  # Import all the signing keys. We'll create a key ring for each user and use
  # that exact key ring when verifying a user's signature. That way we can make
  # sure one user cannot just upload multiple signatures to reach the 5/7
  # required sigs.
  for key in "${KEYS[@]}"; do
    KEY_ID=$(echo $key | cut -d' ' -f1)
    USERNAME=$(echo $key | cut -d' ' -f2)
    IMPORT_FILE="keys/$USERNAME.asc"
    KEY_FILE="$DIR/$IMPORT_FILE"
    KEYRING_UNTRUSTED="$USERNAME.pgp-untrusted"
    KEYRING_TRUSTED="$USERNAME.pgp"

    # Because a key file could contain multiple keys, we need to be careful. To
    # make sure we only import and use the key with the hard coded key ID of
    # this script, we first import the file into a temporary untrusted keyring
    # and then only export the specific key with the given ID into our final,
    # trusted keyring that we later use for verification. This is exactly what
    # https://github.com/Kixunil/sqck does but we didn't want to add another
    # binary dependency to this script so we re-implemented it in the following
    # few lines.
    echo ""
    echo "Importing key(s) from $KEY_FILE into temporary keyring $KEYRING_UNTRUSTED"
    gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING_UNTRUSTED" \
      --import < "$KEY_FILE"

    echo ""
    echo "Exporting key $KEY_ID from untrusted keyring to trusted keyring $KEYRING_TRUSTED"
    gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING_UNTRUSTED" \
      --export "$KEY_ID" | \
      gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING_TRUSTED" --import

  done
}

function verify_signatures() {
  # Download the JSON of the release itself. That'll contain the release ID we
  # need for the next call.
  RELEASE_JSON=$(curl -L -s -H "$HEADER_JSON" "$RELEASE_URL/$VERSION")

  TAG_NAME=$(echo $RELEASE_JSON | jq -r '.tag_name')
  RELEASE_ID=$(echo $RELEASE_JSON | jq -r '.id')
  echo "Release $TAG_NAME found with ID $RELEASE_ID"

  # Now download the asset list and filter by the manifest and the signatures.
  ASSETS=$(curl -L -s -H "$HEADER_GH_JSON" "$API_URL/$RELEASE_ID" | jq -c '.assets[]')
  MANIFEST=$(echo $ASSETS | jq -r "$MANIFEST_SELECTOR")
  SIGNATURES=$(echo $ASSETS | jq -r "$SIGNATURE_SELECTOR")

  # We need to make sure we have unique signature file names. Otherwise someone
  # could just upload the same signature multiple times (if GH allows it for
  # some reason). Just adding the same files under different names also won't
  # work because we parse the signing user's name from the file. If a random
  # username is chosen then a signing key won't be found for it.
  SIGNATURES=$(echo $ASSETS | jq -r "$SIGNATURE_SELECTOR" | sort | uniq)

  # Download the main "manifest-*.txt" and all "manifest-*.sig" files containing
  # the detached signatures.
  echo "Downloading $MANIFEST"
  curl -L -s -o "$TEMP_DIR/$MANIFEST" "$RELEASE_URL/download/$VERSION/$MANIFEST"

  for signature in $SIGNATURES; do
    echo "Downloading $signature"
    curl -L -s -o "$TEMP_DIR/$signature" "$RELEASE_URL/download/$VERSION/$signature"
  done

  echo ""

  # Before we even look at the content of the manifest, we first want to make sure
  # the signatures actually sign that exact manifest.
  NUM_CHECKS=0
  for signature in $SIGNATURES; do
    # Remove everything from the filename after the username. We start with
    # "manifest-USERNAME-v0.xx.yy-beta.sig" and have "manifest-USERNAME" after
    # this step. 
    USERNAME=${signature%-$VERSION.sig}

    # Remove the manifest- part before the username.
    USERNAME=${USERNAME##manifest-}

    # If the user is known, they should have a key ring file with only their key.
    KEYRING="$USERNAME.pgp"
    if [[ ! -f "$TEMP_DIR/$KEYRING" ]]; then
      echo "User $USERNAME does not have a known key, skipping"
      continue
    fi

    # We'll write the status of the verification to a special file that we can
    # then inspect.
    STATUS_FILE="$TEMP_DIR/$USERNAME.sign-status"

    # Make sure we haven't yet tried to verify a signature for that user.
    if [[ -f "$STATUS_FILE" ]]; then
      echo "ERROR: A signature for user $USERNAME was already verified!"
      echo "  Either file name $signature is wrong or multiple files of same "
      echo "  user were uploaded."
      exit 1
    fi

    # Run the actual verification.
    gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING" --status-fd=1 \
      --verify "$TEMP_DIR/$signature" "$TEMP_DIR/$MANIFEST" \
      > "$STATUS_FILE" 2>&1 || { echo "ERROR: Invalid signature!"; exit 1; } 

    echo "Verifying $signature of user $USERNAME against key ring $KEYRING"
    if grep -q "Good signature" "$STATUS_FILE"; then
      echo "Signature for $signature appears valid: "
      grep "VALIDSIG" "$STATUS_FILE"
    elif grep -q "No public key" "$STATUS_FILE"; then
      # Because we checked above if the user has a key, getting the "No public
      # key" error now means the key used for signing doesn't match the key we
      # have in our repo and is now a failure case.
      echo "ERROR: Unable to verify signature $signature, no key available"
      echo "  The signature $signature was signed with a different key than was"
      echo "  imported for user $USERNAME."
      exit 1
    else
      echo "ERROR: Did not get valid signature for $MANIFEST in $signature!"
      echo "  The developer signature $signature disagrees on the expected"
      echo "  release binaries in $MANIFEST. The release may have been faulty or"
      echo "  was backdoored."
      exit 1
    fi

    echo "Verified $signature against $MANIFEST"
    echo ""
    ((NUM_CHECKS=NUM_CHECKS+1))
  done

  # We want at least five signatures (out of seven public keys) that sign the
  # hashes of the binaries we have installed. If we arrive here without exiting,
  # it means no signature manifests were uploaded (yet) with the correct naming
  # pattern.
  if [[ $NUM_CHECKS -lt $MIN_REQUIRED_SIGNATURES ]]; then
    echo "ERROR: Not enough valid signatures found!"
    echo "  Valid signatures found: $NUM_CHECKS"
    echo "  Valid signatures required: $MIN_REQUIRED_SIGNATURES"
    echo
    echo "  Make sure the release $VERSION contains the required "
    echo "  number of signatures on the manifest, or wait until more "
    echo "  signatures have been added to the release."
    exit 1
  fi
}

function check_hash() {
  # Make this script compatible with both linux and *nix.
  SHA_CMD="sha256sum"
  if ! command -v "$SHA_CMD" > /dev/null; then
    if command -v "shasum"; then
      SHA_CMD="shasum -a 256"
    else
      echo "ERROR: no SHA256 sum binary installed!"
      exit 1
    fi
  fi
  SUM=$($SHA_CMD "$1" | cut -d' ' -f1)

  # Make sure the hash was actually calculated by looking at its length.
  if [[ ${#SUM} -ne 64 ]]; then
    echo "ERROR: Invalid hash for $2: $SUM!"
    exit 1
  fi

  echo "Verifying $1 as version $VERSION with SHA256 sum $SUM"

  # If we're inside the docker image, there should be a shasums.txt file in the
  # root directory. If that's the case, we first want to make sure we still have
  # the same hash as we did when building the image.
  if [[ -f /shasums.txt ]]; then
    if ! grep -q "$SUM" /shasums.txt; then
      echo "ERROR: Hash $SUM for $2 not found in /shasums.txt: "
      cat /shasums.txt
      exit 1
    fi
  fi

  if ! grep "^$SUM" "$TEMP_DIR/$MANIFEST" | grep -q "$VERSION"; then
    echo "ERROR: Hash $SUM for $2 not found in $MANIFEST: "
    cat "$TEMP_DIR/$MANIFEST"
    echo "  The expected release binaries have been verified with the developer "
    echo "  signatures. Your binary's hash does not match the expected release "
    echo "  binary hashes. Make sure you're using an official binary."
    exit 1
  fi
}

# By default we're picking up lnd and lncli from the system $PATH.
LND_BIN=$(which lnd)
LNCLI_BIN=$(which lncli)

if [[ $# -eq 0 ]]; then
  echo "ERROR: missing expected version!"
  echo "Usage: verify-install.sh expected-version [path-to-lnd-binary-or-download-archive [path-to-lncli-binary]]"
  exit 1
fi

# The first argument should be the expected version of the binaries.
VERSION=$1
shift

# Verify that the expected version is well-formed.
verify_version "$VERSION"

# Make sure we have all tools needed for the verification.
check_command curl
check_command jq
check_command gpg

# If exactly two parameters are specified, we expect the first one to be lnd and
# the second one to be lncli. One parameter is either just a single binary or a
# packaged release archive. No parameters means picking up lnd and lncli from
# the system path.
if [[ $# -eq 2 ]]; then
  LND_BIN=$(realpath $1)
  LNCLI_BIN=$(realpath $2)

  # Make sure both files actually exist.
  if [[ ! -f $LND_BIN ]]; then
    echo "ERROR: $LND_BIN not found!"
    exit 1
  fi
  if [[ ! -f $LNCLI_BIN ]]; then
    echo "ERROR: $LNCLI_BIN not found!"
    exit 1
  fi

  # Make sure both binaries can be found and are executable.
  check_command "$LND_BIN"
  check_command "$LNCLI_BIN"

elif [[ $# -eq 1 ]]; then
  # We're verifying a single binary or a packaged release archive.
  PACKAGE_BIN=$(realpath $1)

elif [[ $# -eq 0 ]]; then
  # By default we're picking up lnd and lncli from the system $PATH.
  LND_BIN=$(which lnd)
  LNCLI_BIN=$(which lncli)

  # Make sure both binaries can be found and are executable.
  check_command "$LND_BIN"
  check_command "$LNCLI_BIN"

else
  echo "ERROR: invalid number of parameters!"
  echo "Usage: verify-install.sh [lnd-binary lncli-binary]"
  exit 1
fi

# Import all the signing keys.
import_keys

echo ""

# Verify and count the signatures.
verify_signatures

# Then make sure that the hash of the installed binaries can be found in the
# manifest that we now have verified the signatures for.
if [[ "$PACKAGE_BIN" != "" ]]; then
  check_hash "$PACKAGE_BIN" "$PACKAGE_BIN"

  echo ""
  echo "SUCCESS! Verified $PACKAGE_BIN against $MANIFEST signed by $NUM_CHECKS developers."

else
  check_hash "$LND_BIN" "lnd"
  check_hash "$LNCLI_BIN" "lncli"

  echo ""
  echo "SUCCESS! Verified lnd and lncli against $MANIFEST signed by $NUM_CHECKS developers."
fi
