Skip to content

Commit 911b7b6

Browse files
committed
scripts: add verify-install.sh
1 parent 4cebaf1 commit 911b7b6

File tree

1 file changed

+324
-0
lines changed

1 file changed

+324
-0
lines changed

scripts/verify-install.sh

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
#!/bin/bash
2+
3+
REPO=lightninglabs
4+
PROJECT=taproot-assets
5+
6+
RELEASE_URL=https://github.com/$REPO/$PROJECT/releases
7+
API_URL=https://api.github.com/repos/$REPO/$PROJECT/releases
8+
MANIFEST_SELECTOR=". | select(.name | test(\"manifest-v.*(\\\\.txt)$\")) | .name"
9+
SIGNATURE_SELECTOR=". | select(.name | test(\"manifest-.*(\\\\.sig)$\")) | .name"
10+
HEADER_JSON="Accept: application/json"
11+
HEADER_GH_JSON="Accept: application/vnd.github.v3+json"
12+
MIN_REQUIRED_SIGNATURES=5
13+
14+
# All keys that can sign taproot-assets releases. The key must be added as a
15+
# file to the keys directory, for example: scripts/keys/<username>.asc
16+
# The username in the key file must match the username used for signing a
17+
# manifest (manifest-<username>-v0.xx.yy-beta.sig), otherwise the signature
18+
# won't be counted.
19+
# NOTE: Reviewers of this file must make sure that both the key IDs and
20+
# usernames in the list below are unique!
21+
KEYS=()
22+
KEYS+=("E4D85299674B2D31FAA1892E372CBD7633C61696 roasbeef")
23+
KEYS+=("F4FC70F07310028424EFC20A8E4256593F177720 guggero")
24+
KEYS+=("6D664242E706635868EB0C67D55307DC71F37212 jharveyb")
25+
KEYS+=("7D85EF2052C86F6F81D2C59112BF5261A78AFF3D georgetsagk")
26+
KEYS+=("FE5E159A70C436D6AF4D2887B1F8848557AA29D2 ffranr")
27+
28+
TEMP_DIR=$(mktemp -d /tmp/taproot-assets-sig-verification-XXXXXX)
29+
30+
function check_command() {
31+
echo -n "Checking if $1 is installed... "
32+
if ! command -v "$1"; then
33+
echo "ERROR: $1 is not installed or not in PATH!"
34+
exit 1
35+
fi
36+
}
37+
38+
function verify_version() {
39+
version_regex="^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]"
40+
if [[ ! "$1" =~ $version_regex ]]; then
41+
echo "ERROR: Invalid expected version detected: $1"
42+
exit 1
43+
fi
44+
echo "Expected version for binaries: $1"
45+
}
46+
47+
function import_keys() {
48+
# A trick to get the absolute directory where this script is located, no
49+
# matter how or from where it was called. We'll need it to locate the key
50+
# files which are located relative to this script.
51+
DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"
52+
53+
# Import all the signing keys. We'll create a key ring for each user and use
54+
# that exact key ring when verifying a user's signature. That way we can make
55+
# sure one user cannot just upload multiple signatures to reach the 5/7
56+
# required sigs.
57+
for key in "${KEYS[@]}"; do
58+
KEY_ID=$(echo $key | cut -d' ' -f1)
59+
USERNAME=$(echo $key | cut -d' ' -f2)
60+
IMPORT_FILE="keys/$USERNAME.asc"
61+
KEY_FILE="$DIR/$IMPORT_FILE"
62+
KEYRING_UNTRUSTED="$USERNAME.pgp-untrusted"
63+
KEYRING_TRUSTED="$USERNAME.pgp"
64+
65+
# Because a key file could contain multiple keys, we need to be careful. To
66+
# make sure we only import and use the key with the hard coded key ID of
67+
# this script, we first import the file into a temporary untrusted keyring
68+
# and then only export the specific key with the given ID into our final,
69+
# trusted keyring that we later use for verification. This is exactly what
70+
# https://github.com/Kixunil/sqck does but we didn't want to add another
71+
# binary dependency to this script so we re-implemented it in the following
72+
# few lines.
73+
echo ""
74+
echo "Importing key(s) from $KEY_FILE into temporary keyring $KEYRING_UNTRUSTED"
75+
gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING_UNTRUSTED" \
76+
--import < "$KEY_FILE"
77+
78+
echo ""
79+
echo "Exporting key $KEY_ID from untrusted keyring to trusted keyring $KEYRING_TRUSTED"
80+
gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING_UNTRUSTED" \
81+
--export "$KEY_ID" | \
82+
gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING_TRUSTED" --import
83+
84+
done
85+
}
86+
87+
function verify_signatures() {
88+
# Download the JSON of the release itself. That'll contain the release ID we
89+
# need for the next call.
90+
RELEASE_JSON=$(curl -L -s -H "$HEADER_JSON" "$RELEASE_URL/$VERSION")
91+
92+
TAG_NAME=$(echo $RELEASE_JSON | jq -r '.tag_name')
93+
RELEASE_ID=$(echo $RELEASE_JSON | jq -r '.id')
94+
echo "Release $TAG_NAME found with ID $RELEASE_ID"
95+
96+
# Now download the asset list and filter by the manifest and the signatures.
97+
ASSETS=$(curl -L -s -H "$HEADER_GH_JSON" "$API_URL/$RELEASE_ID" | jq -c '.assets[]')
98+
MANIFEST=$(echo $ASSETS | jq -r "$MANIFEST_SELECTOR")
99+
SIGNATURES=$(echo $ASSETS | jq -r "$SIGNATURE_SELECTOR")
100+
101+
# We need to make sure we have unique signature file names. Otherwise someone
102+
# could just upload the same signature multiple times (if GH allows it for
103+
# some reason). Just adding the same files under different names also won't
104+
# work because we parse the signing user's name from the file. If a random
105+
# username is chosen then a signing key won't be found for it.
106+
SIGNATURES=$(echo $ASSETS | jq -r "$SIGNATURE_SELECTOR" | sort | uniq)
107+
108+
# Download the main "manifest-*.txt" and all "manifest-*.sig" files containing
109+
# the detached signatures.
110+
echo "Downloading $MANIFEST"
111+
curl -L -s -o "$TEMP_DIR/$MANIFEST" "$RELEASE_URL/download/$VERSION/$MANIFEST"
112+
113+
for signature in $SIGNATURES; do
114+
echo "Downloading $signature"
115+
curl -L -s -o "$TEMP_DIR/$signature" "$RELEASE_URL/download/$VERSION/$signature"
116+
done
117+
118+
echo ""
119+
120+
# Before we even look at the content of the manifest, we first want to make sure
121+
# the signatures actually sign that exact manifest.
122+
NUM_CHECKS=0
123+
for signature in $SIGNATURES; do
124+
# Remove everything from the filename after the username. We start with
125+
# "manifest-USERNAME-v0.xx.yy-beta.sig" and have "manifest-USERNAME" after
126+
# this step.
127+
USERNAME=${signature%-$VERSION.sig}
128+
129+
# Remove the manifest- part before the username.
130+
USERNAME=${USERNAME##manifest-}
131+
132+
# If the user is known, they should have a key ring file with only their key.
133+
KEYRING="$USERNAME.pgp"
134+
if [[ ! -f "$TEMP_DIR/$KEYRING" ]]; then
135+
echo "User $USERNAME does not have a known key, skipping"
136+
continue
137+
fi
138+
139+
# We'll write the status of the verification to a special file that we can
140+
# then inspect.
141+
STATUS_FILE="$TEMP_DIR/$USERNAME.sign-status"
142+
143+
# Make sure we haven't yet tried to verify a signature for that user.
144+
if [[ -f "$STATUS_FILE" ]]; then
145+
echo "ERROR: A signature for user $USERNAME was already verified!"
146+
echo " Either file name $signature is wrong or multiple files of same "
147+
echo " user were uploaded."
148+
exit 1
149+
fi
150+
151+
# Run the actual verification.
152+
gpg --homedir "$TEMP_DIR" --no-default-keyring --keyring "$KEYRING" --status-fd=1 \
153+
--verify "$TEMP_DIR/$signature" "$TEMP_DIR/$MANIFEST" \
154+
> "$STATUS_FILE" 2>&1 || { echo "ERROR: Invalid signature!"; exit 1; }
155+
156+
echo "Verifying $signature of user $USERNAME against key ring $KEYRING"
157+
if grep -q "Good signature" "$STATUS_FILE"; then
158+
echo "Signature for $signature appears valid: "
159+
grep "VALIDSIG" "$STATUS_FILE"
160+
elif grep -q "No public key" "$STATUS_FILE"; then
161+
# Because we checked above if the user has a key, getting the "No public
162+
# key" error now means the key used for signing doesn't match the key we
163+
# have in our repo and is now a failure case.
164+
echo "ERROR: Unable to verify signature $signature, no key available"
165+
echo " The signature $signature was signed with a different key than was"
166+
echo " imported for user $USERNAME."
167+
exit 1
168+
else
169+
echo "ERROR: Did not get valid signature for $MANIFEST in $signature!"
170+
echo " The developer signature $signature disagrees on the expected"
171+
echo " release binaries in $MANIFEST. The release may have been faulty or"
172+
echo " was backdoored."
173+
exit 1
174+
fi
175+
176+
echo "Verified $signature against $MANIFEST"
177+
echo ""
178+
((NUM_CHECKS=NUM_CHECKS+1))
179+
done
180+
181+
# We want at least five signatures (out of seven public keys) that sign the
182+
# hashes of the binaries we have installed. If we arrive here without exiting,
183+
# it means no signature manifests were uploaded (yet) with the correct naming
184+
# pattern.
185+
if [[ $NUM_CHECKS -lt $MIN_REQUIRED_SIGNATURES ]]; then
186+
echo "ERROR: Not enough valid signatures found!"
187+
echo " Valid signatures found: $NUM_CHECKS"
188+
echo " Valid signatures required: $MIN_REQUIRED_SIGNATURES"
189+
echo
190+
echo " Make sure the release $VERSION contains the required "
191+
echo " number of signatures on the manifest, or wait until more "
192+
echo " signatures have been added to the release."
193+
exit 1
194+
fi
195+
}
196+
197+
function check_hash() {
198+
# Make this script compatible with both linux and *nix.
199+
SHA_CMD="sha256sum"
200+
if ! command -v "$SHA_CMD" > /dev/null; then
201+
if command -v "shasum"; then
202+
SHA_CMD="shasum -a 256"
203+
else
204+
echo "ERROR: no SHA256 sum binary installed!"
205+
exit 1
206+
fi
207+
fi
208+
SUM=$($SHA_CMD "$1" | cut -d' ' -f1)
209+
210+
# Make sure the hash was actually calculated by looking at its length.
211+
if [[ ${#SUM} -ne 64 ]]; then
212+
echo "ERROR: Invalid hash for $2: $SUM!"
213+
exit 1
214+
fi
215+
216+
echo "Verifying $1 as version $VERSION with SHA256 sum $SUM"
217+
218+
# If we're inside the docker image, there should be a shasums.txt file in the
219+
# root directory. If that's the case, we first want to make sure we still have
220+
# the same hash as we did when building the image.
221+
if [[ -f /shasums.txt ]]; then
222+
if ! grep -q "$SUM" /shasums.txt; then
223+
echo "ERROR: Hash $SUM for $2 not found in /shasums.txt: "
224+
cat /shasums.txt
225+
exit 1
226+
fi
227+
fi
228+
229+
if ! grep "^$SUM" "$TEMP_DIR/$MANIFEST" | grep -q "$VERSION"; then
230+
echo "ERROR: Hash $SUM for $2 not found in $MANIFEST: "
231+
cat "$TEMP_DIR/$MANIFEST"
232+
echo " The expected release binaries have been verified with the developer "
233+
echo " signatures. Your binary's hash does not match the expected release "
234+
echo " binary hashes. Make sure you're using an official binary."
235+
exit 1
236+
fi
237+
}
238+
239+
# By default we're picking up tapd and tapcli from the system $PATH.
240+
TAPD_BIN=$(which tapd)
241+
TAPCLI_BIN=$(which tapcli)
242+
243+
if [[ $# -eq 0 ]]; then
244+
echo "ERROR: missing expected version!"
245+
echo "Usage: verify-install.sh expected-version [path-to-tapd-binary-or-download-archive [path-to-tapcli-binary]]"
246+
exit 1
247+
fi
248+
249+
# The first argument should be the expected version of the binaries.
250+
VERSION=$1
251+
shift
252+
253+
# Verify that the expected version is well-formed.
254+
verify_version "$VERSION"
255+
256+
# Make sure we have all tools needed for the verification.
257+
check_command curl
258+
check_command jq
259+
check_command gpg
260+
261+
# If exactly two parameters are specified, we expect the first one to be tapd
262+
# and the second one to be tapcli. One parameter is either just a single binary
263+
# or a packaged release archive. No parameters means picking up tapd and tapcli
264+
# from the system path.
265+
if [[ $# -eq 2 ]]; then
266+
TAPD_BIN=$(realpath $1)
267+
TAPCLI_BIN=$(realpath $2)
268+
269+
# Make sure both files actually exist.
270+
if [[ ! -f $TAPD_BIN ]]; then
271+
echo "ERROR: $TAPD_BIN not found!"
272+
exit 1
273+
fi
274+
if [[ ! -f $TAPCLI_BIN ]]; then
275+
echo "ERROR: $TAPCLI_BIN not found!"
276+
exit 1
277+
fi
278+
279+
# Make sure both binaries can be found and are executable.
280+
check_command "$TAPD_BIN"
281+
check_command "$TAPCLI_BIN"
282+
283+
elif [[ $# -eq 1 ]]; then
284+
# We're verifying a single binary or a packaged release archive.
285+
PACKAGE_BIN=$(realpath $1)
286+
287+
elif [[ $# -eq 0 ]]; then
288+
# By default we're picking up tapd and tapcli from the system $PATH.
289+
TAPD_BIN=$(which tapd)
290+
TAPCLI_BIN=$(which tapcli)
291+
292+
# Make sure both binaries can be found and are executable.
293+
check_command "$TAPD_BIN"
294+
check_command "$TAPCLI_BIN"
295+
296+
else
297+
echo "ERROR: invalid number of parameters!"
298+
echo "Usage: verify-install.sh [tapd-binary tapcli-binary]"
299+
exit 1
300+
fi
301+
302+
# Import all the signing keys.
303+
import_keys
304+
305+
echo ""
306+
307+
# Verify and count the signatures.
308+
verify_signatures
309+
310+
# Then make sure that the hash of the installed binaries can be found in the
311+
# manifest that we now have verified the signatures for.
312+
if [[ "$PACKAGE_BIN" != "" ]]; then
313+
check_hash "$PACKAGE_BIN" "$PACKAGE_BIN"
314+
315+
echo ""
316+
echo "SUCCESS! Verified $PACKAGE_BIN against $MANIFEST signed by $NUM_CHECKS developers."
317+
318+
else
319+
check_hash "$TAPD_BIN" "tapd"
320+
check_hash "$TAPCLI_BIN" "tapcli"
321+
322+
echo ""
323+
echo "SUCCESS! Verified tapd and tapcli against $MANIFEST signed by $NUM_CHECKS developers."
324+
fi

0 commit comments

Comments
 (0)