diff --git a/Dockerfile b/Dockerfile index 40640f4e..c9d747e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,15 +31,19 @@ FROM alpine as final # Define a root volume for data persistence. VOLUME /root/.lnd -# Add bash, jq and ca-certs, for quality of life and SSL-related reasons. +# Add utilities for quality of life and SSL-related reasons. We also require +# curl and gpg for the signature verification script. RUN apk --no-cache add \ bash \ jq \ - ca-certificates + ca-certificates \ + gnupg \ + curl # Copy the binaries from the builder image. COPY --from=builder /go/bin/lncli /bin/ COPY --from=builder /go/bin/lnd /bin/ +COPY --from=builder /go/src/github.com/lightningnetwork/lnd/scripts/verify-install.sh / # Store the SHA256 hash of the binaries that were just produced for later # verification. diff --git a/scripts/verify-install.sh b/scripts/verify-install.sh new file mode 100755 index 00000000..cf7806f1 --- /dev/null +++ b/scripts/verify-install.sh @@ -0,0 +1,148 @@ +#!/bin/bash + +REPO=lightningnetwork +PROJECT=lnd + +RELEASE_URL=https://github.com/$REPO/$PROJECT/releases +API_URL=https://api.github.com/repos/$REPO/$PROJECT/releases +SIGNATURE_SELECTOR=". | select(.name | test(\"manifest-.*(\\\\.txt\\\\.asc)$\")) | .name" +HEADER_JSON="Accept: application/json" +HEADER_GH_JSON="Accept: application/vnd.github.v3+json" + +# All keys that can sign lnd releases. The key must be downloadable/importable +# from the URL given after the space. +KEYS=() +KEYS+=("F4FC70F07310028424EFC20A8E4256593F177720 https://keybase.io/guggero/pgp_keys.asc") +KEYS+=("15E7ECF257098A4EF91655EB4CA7FE54A6213C91 https://keybase.io/carlakirkcohen/pgp_keys.asc") +KEYS+=("9C8D61868A7C492003B2744EE7D737B67FA592C7 https://keybase.io/bitconner/pgp_keys.asc") +KEYS+=("E4D85299674B2D31FAA1892E372CBD7633C61696 https://keybase.io/roasbeef/pgp_keys.asc") +KEYS+=("729E9D9D92C75A5FBFEEE057B5DD717BEF7CA5B1 https://keybase.io/wpaulino/pgp_keys.asc") +KEYS+=("7E81EF6B9989A9CC93884803118759E83439A9B1 https://keybase.io/eugene_/pgp_keys.asc") + +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 +} + +check_command curl +check_command jq +check_command gpg +check_command lnd +check_command lncli + +LND_BIN=$(which lnd) +LNCLI_BIN=$(which lncli) + +LND_VERSION=$(lnd --version | cut -d'=' -f2) +LNCLI_VERSION=$(lncli --version | cut -d'=' -f2) +LND_SUM=$(sha256sum $LND_BIN | cut -d' ' -f1) +LNCLI_SUM=$(sha256sum $LNCLI_BIN | cut -d' ' -f1) + +echo "Detected lnd version $LND_VERSION with SHA256 sum $LND_SUM" +echo "Detected lncli version $LNCLI_VERSION with SHA256 sum $LNCLI_SUM" + +# Make sure lnd and lncli are installed with the same version. +if [[ "$LNCLI_VERSION" != "$LND_VERSION" ]]; then + echo "ERROR: Version $LNCLI_VERSION of lncli does not match $LND_VERSION of lnd!" + exit 1 +fi + +# 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 "$LND_SUM" /shasums.txt; then + echo "ERROR: Hash $LND_SUM for lnd not found in /shasums.txt: " + cat /shasums.txt + exit 1 + fi + if ! grep -q "$LNCLI_SUM" /shasums.txt; then + echo "ERROR: Hash $LNCLI_SUM for lnd not found in /shasums.txt: " + cat /shasums.txt + exit 1 + fi +fi + +# Import all the signing keys. +for key in "${KEYS[@]}"; do + KEY_ID=$(echo $key | cut -d' ' -f1) + IMPORT_URL=$(echo $key | cut -d' ' -f2) + echo "Downloading and importing key $KEY_ID from $IMPORT_URL" + curl -L -s $IMPORT_URL | gpg --import + + # Make sure we actually imported the correct key. + if ! gpg --list-key "$KEY_ID"; then + echo "ERROR: Imported key from $IMPORT_URL doesn't match ID $KEY_ID." + fi +done + +echo "" + +# 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/$LND_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 manifests and signatures. +ASSETS=$(curl -L -s -H "$HEADER_GH_JSON" "$API_URL/$RELEASE_ID" | jq -c '.assets[]') +SIGNATURES=$(echo $ASSETS | jq -r "$SIGNATURE_SELECTOR") + +# Download all "manifest-*.txt.asc" as those contain both the hashes that were +# signed and the signature itself (=detached sig). +TEMP_DIR=$(mktemp -d /tmp/lnd-sig-verification-XXXXXX) +for signature in $SIGNATURES; do + echo "Downloading $signature" + curl -L -s -o "$TEMP_DIR/$signature" "$RELEASE_URL/download/$LND_VERSION/$signature" +done + +echo "" +cd $TEMP_DIR || exit 1 + +NUM_CHECKS=0 +for signature in $SIGNATURES; do + # First make sure the downloaded signature file is valid. + echo "Verifying $signature" + if ! gpg --verify "$signature" 2>&1 | grep -q "Good signature"; then + echo "ERROR: Did not get valid signature for $signature!" + exit 1 + fi + + echo "Signature for $signature checks out: " + gpg --verify "$signature" 2>&1 | grep "using" + + echo "" + + # Then make sure that the hash of the installed binaries can be found in the + # signed list of hashes. + if ! grep -q "$LND_SUM" "$signature"; then + echo "ERROR: Hash $LND_SUM for lnd not found in $signature: " + cat "$signature" + exit 1 + fi + + if ! grep -q "$LNCLI_SUM" "$signature"; then + echo "ERROR: Hash $LNCLI_SUM for lncli not found in $signature: " + cat "$signature" + exit 1 + fi + + echo "Verified lnd and lncli hashes against $signature" + ((NUM_CHECKS=NUM_CHECKS+1)) +done + +# We want at least one signature that signs 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 1 ]]; then + echo "ERROR: No valid signatures found!" + echo "Make sure the release $LND_VERSION contains any signed manifests." + exit 1 +fi + +echo "" +echo "SUCCESS! Verified lnd and lncli against $NUM_CHECKS signature(s)."