|
| 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