diff options
author | Andrew Chow <github@achow101.com> | 2023-03-23 00:56:12 -0400 |
---|---|---|
committer | Cory Fields <cory-nospam-@coryfields.com> | 2023-03-28 22:16:05 +0000 |
commit | 7a6e7ffd066a42c5fbb7d69effbe074fb982936b (patch) | |
tree | ab82aff32f5222ef2df35dc7e0a1bc25a600952d /contrib | |
parent | 6b2cebfa2f1526f7eae31eb645c71712f0a69e97 (diff) | |
download | bitcoin-7a6e7ffd066a42c5fbb7d69effbe074fb982936b.tar.xz |
contrib: Use machine parseable GPG output in verifybinaries
GPG has an option to provide machine parseable output. Use that instead
of trying to parse the human readable output.
Diffstat (limited to 'contrib')
-rwxr-xr-x | contrib/verifybinaries/verify.py | 143 |
1 files changed, 69 insertions, 74 deletions
diff --git a/contrib/verifybinaries/verify.py b/contrib/verifybinaries/verify.py index 76986aa090..83370fbede 100755 --- a/contrib/verifybinaries/verify.py +++ b/contrib/verifybinaries/verify.py @@ -42,7 +42,7 @@ import textwrap import urllib.request import enum from hashlib import sha256 -from pathlib import PurePath +from pathlib import PurePath, Path # The primary host; this will fail if we can't retrieve files from here. HOST1 = "https://bitcoincore.org" @@ -141,14 +141,19 @@ def verify_with_gpg( signature_filename, output_filename: t.Optional[str] = None ) -> t.Tuple[int, str]: - args = [ - 'gpg', '--yes', '--verify', '--verify-options', 'show-primary-uid-only', - '--output', output_filename if output_filename else '', signature_filename, filename] + with tempfile.NamedTemporaryFile() as status_file: + args = [ + 'gpg', '--yes', '--verify', '--verify-options', 'show-primary-uid-only', "--status-file", status_file.name, + '--output', output_filename if output_filename else '', signature_filename, filename] - env = dict(os.environ, LANGUAGE='en') - result = subprocess.run(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env) - log.debug(f'Result from GPG ({result.returncode}): {result.stdout}') - return result.returncode, result.stdout.decode().rstrip() + env = dict(os.environ, LANGUAGE='en') + result = subprocess.run(args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env) + + gpg_data = status_file.read().decode().rstrip() + + log.debug(f'Result from GPG ({result.returncode}): {result.stdout.decode()}') + log.debug(f"{gpg_data}") + return result.returncode, gpg_data def remove_files(filenames): @@ -158,11 +163,14 @@ def remove_files(filenames): class SigData: """GPG signature data as parsed from GPG stdout.""" - def __init__(self, key: str, name: str, trusted: bool, status: str): - self.key = key - self.name = name - self.trusted = trusted - self.status = status + def __init__(self): + self.key = None + self.name = "" + self.trusted = False + self.status = "" + + def __bool__(self): + return self.key is not None def __repr__(self): return ( @@ -174,60 +182,60 @@ def parse_gpg_result( output: t.List[str] ) -> t.Tuple[t.List[SigData], t.List[SigData], t.List[SigData]]: """Returns good, unknown, and bad signatures from GPG stdout.""" - good_sigs = [] - unknown_sigs = [] - bad_sigs = [] + good_sigs: t.List[SigData] = [] + unknown_sigs: t.List[SigData] = [] + bad_sigs: t.List[SigData] = [] total_resolved_sigs = 0 - curr_key = None # Ensure that all lines we match on include a prefix that prevents malicious input # from fooling the parser. def line_begins_with(patt: str, line: str) -> t.Optional[re.Match]: - return re.match(r'^\s*(gpg:)?(\s+)' + patt, line) - - detected_name = '' - - for i, line in enumerate(output): - if line_begins_with(r"using (ECDSA|RSA) key (0x[0-9a-fA-F]{16}|[0-9a-fA-F]{40})$", line): - if curr_key: - raise RuntimeError( - f"WARNING: encountered a new sig without resolving the last ({curr_key}) - " - "this could mean we have encountered a bad signature! check GPG output!") - curr_key = line.split('key ')[-1].strip() - assert len(curr_key) == 40 or (len(curr_key) == 18 and curr_key.startswith('0x')) - - if line_begins_with(r"Can't check signature: No public key$", line): - if not curr_key: - raise RuntimeError("failed to detect signature being resolved") - unknown_sigs.append(SigData(curr_key, detected_name, False, '')) - detected_name = '' - curr_key = None - - if line_begins_with(r'Good signature from (".+")(\s+)(\[.+\])$', line): - if not curr_key: - raise RuntimeError("failed to detect signature being resolved") - name, status = parse_gpg_from_line(line) - - # It's safe to index output[i + 1] because if we saw a good sig, there should - # always be another line - trusted = ( - 'This key is not certified with a trusted signature' not in output[i + 1]) - good_sigs.append(SigData(curr_key, name, trusted, status)) - curr_key = None - - if line_begins_with("issuer ", line): - detected_name = line.split("issuer ")[-1].strip('"') - - if 'bad signature from' in line.lower(): - if not curr_key: - raise RuntimeError("failed to detect signature being resolved") - name, status = parse_gpg_from_line(line) - bad_sigs.append(SigData(curr_key, name, False, status)) - curr_key = None - - # Track total signatures included - if line_begins_with('Signature made ', line): + return re.match(r'^(\[GNUPG:\])\s+' + patt, line) + + curr_sigs = unknown_sigs + curr_sigdata = SigData() + + for line in output: + if line_begins_with(r"NEWSIG(?:\s|$)", line): total_resolved_sigs += 1 + if curr_sigdata: + curr_sigs.append(curr_sigdata) + curr_sigdata = SigData() + newsig_split = line.split() + if len(newsig_split) == 3: + curr_sigdata.name = newsig_split[2] + + elif line_begins_with(r"GOODSIG(?:\s|$)", line): + curr_sigdata.key, curr_sigdata.name = line.split(maxsplit=3)[2:4] + curr_sigs = good_sigs + + elif line_begins_with(r"EXPKEYSIG(?:\s|$)", line): + curr_sigdata.key, curr_sigdata.name = line.split(maxsplit=3)[2:4] + curr_sigs = good_sigs + curr_sigdata.status = "expired" + + elif line_begins_with(r"REVKEYSIG(?:\s|$)", line): + curr_sigdata.key, curr_sigdata.name = line.split(maxsplit=3)[2:4] + curr_sigs = good_sigs + curr_sigdata.status = "revoked" + + elif line_begins_with(r"BADSIG(?:\s|$)", line): + curr_sigdata.key, curr_sigdata.name = line.split(maxsplit=3)[2:4] + curr_sigs = bad_sigs + + elif line_begins_with(r"ERRSIG(?:\s|$)", line): + curr_sigdata.key, _, _, _, _, _ = line.split()[2:8] + curr_sigs = unknown_sigs + + elif line_begins_with(r"TRUST_(UNDEFINED|NEVER)(?:\s|$)", line): + curr_sigdata.trusted = False + + elif line_begins_with(r"TRUST_(MARGINAL|FULLY|ULTIMATE)(?:\s|$)", line): + curr_sigdata.trusted = True + + # The last one won't have been added, so add it now + assert curr_sigdata + curr_sigs.append(curr_sigdata) all_found = len(good_sigs + bad_sigs + unknown_sigs) if all_found != total_resolved_sigs: @@ -238,19 +246,6 @@ def parse_gpg_result( return (good_sigs, unknown_sigs, bad_sigs) -def parse_gpg_from_line(line: str) -> t.Tuple[str, str]: - """Returns name and expiration status.""" - assert 'signature from' in line - - name_end = line.split(' from ')[-1] - m = re.search(r'(?P<name>".+") \[(?P<status>\w+)\]', name_end) - assert m - (name, status) = m.groups() - name = name.strip('"\'') - - return (name, status) - - def files_are_equal(filename1, filename2): with open(filename1, 'rb') as file1: contents1 = file1.read() |