Skip to content

Commit 16b784d

Browse files
committed
Merge #20689: contrib: replace binary verification script verify.sh with python rewrite
c86b9a6 contrib: remove verify.sh (Sebastian Falbesoner) c84838e contrib: binary verification script verify.sh rewritten in python (Sebastian Falbesoner) Pull request description: The rationale for the PR is the same as for #18132: > Most of our test scripts are written in python. We don't have enough reviewers for bash scripts and they tend to be clumsy anyway. Especially when it comes to argument parsing. Note that there are still a lot of things that could be improved in this replacement (e.g. using regexps for version string parsing, adding type annotations, dividing up into more functions, getting a pylint score closer to 10, etc.), but I found the original shell script quite hard to read, so it's possibly still a good first step for an improvement. ~Not sure though if it's worth the reviewers time, and if it's even continued to be used long-term (maybe there are plans to merge it with `get_previous_releases.py`, which partly does the same?), so chasing for Concept ACKs right now.~ ACKs for top commit: laanwj: Tested and code review ACK c86b9a6 Tree-SHA512: f7949eead4ef7e5913fe273923ae5c5299408db485146cf996cdf6f8ad8c0ee4f4b30bb6b08a5964000d97b2ae2e7a1bdc88d11c613c16d2d135d80b444e3b16
2 parents c8b8351 + c86b9a6 commit 16b784d

File tree

3 files changed

+190
-184
lines changed

3 files changed

+190
-184
lines changed

contrib/verifybinaries/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,21 @@ The script returns 0 if everything passes the checks. It returns 1 if either the
2121

2222

2323
```sh
24-
./verify.sh bitcoin-core-0.11.2
25-
./verify.sh bitcoin-core-0.12.0
26-
./verify.sh bitcoin-core-0.13.0-rc3
24+
./verify.py bitcoin-core-0.11.2
25+
./verify.py bitcoin-core-0.12.0
26+
./verify.py bitcoin-core-0.13.0-rc3
2727
```
2828

2929
If you only want to download the binaries of certain platform, add the corresponding suffix, e.g.:
3030

3131
```sh
32-
./verify.sh bitcoin-core-0.11.2-osx
33-
./verify.sh 0.12.0-linux
34-
./verify.sh bitcoin-core-0.13.0-rc3-win64
32+
./verify.py bitcoin-core-0.11.2-osx
33+
./verify.py 0.12.0-linux
34+
./verify.py bitcoin-core-0.13.0-rc3-win64
3535
```
3636

3737
If you do not want to keep the downloaded binaries, specify anything as the second parameter.
3838

3939
```sh
40-
./verify.sh bitcoin-core-0.13.0 delete
40+
./verify.py bitcoin-core-0.13.0 delete
4141
```

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:]))

contrib/verifybinaries/verify.sh

Lines changed: 0 additions & 177 deletions
This file was deleted.

0 commit comments

Comments
 (0)