Skip to content

Commit c84838e

Browse files
committed
contrib: binary verification script verify.sh rewritten in python
1 parent 143bd10 commit c84838e

File tree

1 file changed

+183
-0
lines changed

1 file changed

+183
-0
lines changed

contrib/verifybinaries/verify.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2020 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Script for verifying Bitoin Core release binaries
6+
7+
This script attempts to download the signature file SHA256SUMS.asc from
8+
bitcoincore.org and bitcoin.org and compares them.
9+
It first checks if the signature passes, and then downloads the files
10+
specified in the file, and checks if the hashes of these files match those
11+
that are specified in the signature file.
12+
The script returns 0 if everything passes the checks. It returns 1 if either
13+
the signature check or the hash check doesn't pass. If an error occurs the
14+
return value is >= 2.
15+
"""
16+
from hashlib import sha256
17+
import os
18+
import subprocess
19+
import sys
20+
from textwrap import indent
21+
22+
WORKINGDIR = "/tmp/bitcoin_verify_binaries"
23+
HASHFILE = "hashes.tmp"
24+
HOST1 = "https://bitcoincore.org"
25+
HOST2 = "https://bitcoin.org"
26+
VERSIONPREFIX = "bitcoin-core-"
27+
SIGNATUREFILENAME = "SHA256SUMS.asc"
28+
29+
30+
def parse_version_string(version_str):
31+
if version_str.startswith(VERSIONPREFIX): # remove version prefix
32+
version_str = version_str[len(VERSIONPREFIX):]
33+
34+
parts = version_str.split('-')
35+
version_base = parts[0]
36+
version_rc = ""
37+
version_os = ""
38+
if len(parts) == 2: # "<version>-rcN" or "version-platform"
39+
if "rc" in parts[1]:
40+
version_rc = parts[1]
41+
else:
42+
version_os = parts[1]
43+
elif len(parts) == 3: # "<version>-rcN-platform"
44+
version_rc = parts[1]
45+
version_os = parts[2]
46+
47+
return version_base, version_rc, version_os
48+
49+
50+
def download_with_wget(remote_file, local_file=None):
51+
if local_file:
52+
wget_args = ['wget', '-O', local_file, remote_file]
53+
else:
54+
# use timestamping mechanism if local filename is not explicitely set
55+
wget_args = ['wget', '-N', remote_file]
56+
57+
result = subprocess.run(wget_args,
58+
stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
59+
return result.returncode == 0, result.stdout.decode().rstrip()
60+
61+
62+
def files_are_equal(filename1, filename2):
63+
with open(filename1, 'rb') as file1:
64+
contents1 = file1.read()
65+
with open(filename2, 'rb') as file2:
66+
contents2 = file2.read()
67+
return contents1 == contents2
68+
69+
70+
def verify_with_gpg(signature_filename, output_filename):
71+
result = subprocess.run(['gpg', '--yes', '--decrypt', '--output',
72+
output_filename, signature_filename],
73+
stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
74+
return result.returncode, result.stdout.decode().rstrip()
75+
76+
77+
def remove_files(filenames):
78+
for filename in filenames:
79+
os.remove(filename)
80+
81+
82+
def main(args):
83+
# sanity check
84+
if len(args) < 1:
85+
print("Error: need to specify a version on the command line")
86+
return 3
87+
88+
# determine remote dir dependend on provided version string
89+
version_base, version_rc, os_filter = parse_version_string(args[0])
90+
remote_dir = f"/bin/{VERSIONPREFIX}{version_base}/"
91+
if version_rc:
92+
remote_dir += f"test.{version_rc}/"
93+
remote_sigfile = remote_dir + SIGNATUREFILENAME
94+
95+
# create working directory
96+
os.makedirs(WORKINGDIR, exist_ok=True)
97+
os.chdir(WORKINGDIR)
98+
99+
# fetch first signature file
100+
sigfile1 = SIGNATUREFILENAME
101+
success, output = download_with_wget(HOST1 + remote_sigfile, sigfile1)
102+
if not success:
103+
print("Error: couldn't fetch signature file. "
104+
"Have you specified the version number in the following format?")
105+
print(f"[{VERSIONPREFIX}]<version>[-rc[0-9]][-platform] "
106+
f"(example: {VERSIONPREFIX}0.21.0-rc3-osx)")
107+
print("wget output:")
108+
print(indent(output, '\t'))
109+
return 4
110+
111+
# fetch second signature file
112+
sigfile2 = SIGNATUREFILENAME + ".2"
113+
success, output = download_with_wget(HOST2 + remote_sigfile, sigfile2)
114+
if not success:
115+
print("bitcoin.org failed to provide signature file, "
116+
"but bitcoincore.org did?")
117+
print("wget output:")
118+
print(indent(output, '\t'))
119+
remove_files([sigfile1])
120+
return 5
121+
122+
# ensure that both signature files are equal
123+
if not files_are_equal(sigfile1, sigfile2):
124+
print("bitcoin.org and bitcoincore.org signature files were not equal?")
125+
print(f"See files {WORKINGDIR}/{sigfile1} and {WORKINGDIR}/{sigfile2}")
126+
return 6
127+
128+
# check signature and extract data into file
129+
retval, output = verify_with_gpg(sigfile1, HASHFILE)
130+
if retval != 0:
131+
if retval == 1:
132+
print("Bad signature.")
133+
elif retval == 2:
134+
print("gpg error. Do you have the Bitcoin Core binary release "
135+
"signing key installed?")
136+
print("gpg output:")
137+
print(indent(output, '\t'))
138+
remove_files([sigfile1, sigfile2, HASHFILE])
139+
return 1
140+
141+
# extract hashes/filenames of binaries to verify from hash file;
142+
# each line has the following format: "<hash> <binary_filename>"
143+
with open(HASHFILE, 'r', encoding='utf8') as hash_file:
144+
hashes_to_verify = [
145+
line.split()[:2] for line in hash_file if os_filter in line]
146+
remove_files([HASHFILE])
147+
if not hashes_to_verify:
148+
print("error: no files matched the platform specified")
149+
return 7
150+
151+
# download binaries
152+
for _, binary_filename in hashes_to_verify:
153+
print(f"Downloading {binary_filename}")
154+
download_with_wget(HOST1 + remote_dir + binary_filename)
155+
156+
# verify hashes
157+
offending_files = []
158+
for hash_expected, binary_filename in hashes_to_verify:
159+
with open(binary_filename, 'rb') as binary_file:
160+
hash_calculated = sha256(binary_file.read()).hexdigest()
161+
if hash_calculated != hash_expected:
162+
offending_files.append(binary_filename)
163+
if offending_files:
164+
print("Hashes don't match.")
165+
print("Offending files:")
166+
print('\n'.join(offending_files))
167+
return 1
168+
verified_binaries = [entry[1] for entry in hashes_to_verify]
169+
170+
# clean up files if desired
171+
if len(args) >= 2:
172+
print("Clean up the binaries")
173+
remove_files([sigfile1, sigfile2] + verified_binaries)
174+
else:
175+
print(f"Keep the binaries in {WORKINGDIR}")
176+
177+
print("Verified hashes of")
178+
print('\n'.join(verified_binaries))
179+
return 0
180+
181+
182+
if __name__ == '__main__':
183+
sys.exit(main(sys.argv[1:]))

0 commit comments

Comments
 (0)