Skip to content

Commit 37c9fb7

Browse files
jamesobryanofskywillcl-ark
authored andcommitted
contrib: verifybinaries: allow multisig verification
This commit adds the functionality necessary to transition from doing binary verification on the basis of a single signature to requiring a minimum threshold of trusted signatures. A signature can appear as "good" from GPG output, but it may not come from an identity the user trusts. We call these "good, untrusted" signatures. We report bad signatures but do not necessarily fail in their presence, since a bad signature might coexist with enough good, trusted signatures to fulfill our criteria. If "--import-keys" is enabled, we will prompt the user to optionally try to retrieve unknown keys. Marking them as trusted locally is a WIP, but keys which are retrieved successfully and appear on the builder-keys list will immediately count as being useful towards fulfilling the threshold. Logging is improved and an option to output JSON that summarizes the whole sum signature and binary verification processes has been added. Co-authored-by: Russ Yanofsky <[email protected]> Co-authored-by: willcl-ark <[email protected]>
1 parent e352f5a commit 37c9fb7

File tree

3 files changed

+645
-112
lines changed

3 files changed

+645
-112
lines changed

contrib/verifybinaries/README.md

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,77 @@
11
### Verify Binaries
22

3-
#### Usage:
3+
#### Preparation
44

5-
This script attempts to download the signature file `SHA256SUMS.asc` from https://bitcoin.org.
5+
As of Bitcoin Core v22.0, releases are signed by a number of public keys on the basis
6+
of the [guix.sigs repository](https://github.com/bitcoin-core/guix.sigs/). When
7+
verifying binary downloads, you (the end user) decide which of these public keys you
8+
trust and then use that trust model to evaluate the signature on a file that contains
9+
hashes of the release binaries. The downloaded binaries are then hashed and compared to
10+
the signed checksum file.
611

7-
It first checks if the signature passes, and then downloads the files specified in the file, and checks if the hashes of these files match those that are specified in the signature file.
12+
First, you have to figure out which public keys to recognize. Browse the [list of frequent
13+
builder-keys](https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys) and
14+
decide which of these keys you would like to trust. For each key you want to trust, you
15+
must obtain that key for your local GPG installation.
816

9-
The script returns 0 if everything passes the checks. It returns 1 if either the signature check or the hash check doesn't pass. If an error occurs the return value is 2.
17+
You can obtain these keys by
18+
- through a browser using a key server (e.g. keyserver.ubuntu.com),
19+
- manually using the `gpg --keyserver <url> --recv-keys <key>` command, or
20+
- you can run the packaged `verifybinaries.py ... --import-keys` script to
21+
have it automatically retrieve unrecognized keys.
1022

23+
#### Usage
1124

25+
This script attempts to download the checksum file (`SHA256SUMS`) and corresponding
26+
signature file `SHA256SUMS.asc` from https://bitcoincore.org and https://bitcoin.org.
27+
28+
It first checks if the checksum file is valid based upon a plurality of signatures, and
29+
then downloads the release files specified in the checksum file, and checks if the
30+
hashes of the release files are as expected.
31+
32+
If we encounter pubkeys in the signature file that we do not recognize, the script
33+
can prompt the user as to whether they'd like to download the pubkeys. To enable
34+
this behavior, use the `--import-keys` flag.
35+
36+
The script returns 0 if everything passes the checks. It returns 1 if either the
37+
signature check or the hash check doesn't pass. An exit code of >2 indicates an error.
38+
39+
See the `Config` object for various options.
40+
41+
#### Examples
42+
43+
Validate releases with default settings:
44+
```sh
45+
./contrib/verifybinaries/verify.py 22.0
46+
./contrib/verifybinaries/verify.py 22.0-rc2
47+
./contrib/verifybinaries/verify.py bitcoin-core-23.0
48+
./contrib/verifybinaries/verify.py bitcoin-core-23.0-rc1
49+
```
50+
51+
Get JSON output and don't prompt for user input (no auto key import):
52+
53+
```sh
54+
./contrib/verifybinaries/verify.py 22.0-x86 --json
55+
```
56+
57+
Don't trust builder-keys by default, and rely only on local GPG state and manually
58+
specified keys, while requiring a threshold of at least 10 trusted signatures:
1259
```sh
13-
./verify.py bitcoin-core-0.11.2
14-
./verify.py bitcoin-core-0.12.0
15-
./verify.py bitcoin-core-0.13.0-rc3
60+
./contrib/verifybinaries/verify.py 22.0-x86 \
61+
--no-trust-builder-keys \
62+
--trusted-keys 74E2DEF5D77260B98BC19438099BAD163C70FBFA,9D3CC86A72F8494342EA5FD10A41BDC3F4FAFF1C \
63+
--min-trusted-sigs 10
1664
```
1765

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

2068
```sh
21-
./verify.py bitcoin-core-0.11.2-osx
22-
./verify.py 0.12.0-linux
23-
./verify.py bitcoin-core-0.13.0-rc3-win64
69+
./contrib/verifybinaries/verify.py bitcoin-core-22.0-osx
70+
./contrib/verifybinaries/verify.py bitcoin-core-22.0-rc2-win64
2471
```
2572

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

2875
```sh
29-
./verify.py bitcoin-core-0.13.0 delete
76+
./contrib/verifybinaries/verify.py bitcoin-core-22.0 delete
3077
```

contrib/verifybinaries/test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import sys
5+
import subprocess
6+
from pathlib import Path
7+
8+
9+
def main():
10+
"""Tests ordered roughly from faster to slower."""
11+
expect_code(run_verify('0.32'), 4, "Nonexistent version should fail")
12+
expect_code(run_verify('0.32.awefa.12f9h'), 11, "Malformed version should fail")
13+
expect_code(run_verify('22.0 --min-good-sigs 20'), 9, "--min-good-sigs 20 should fail")
14+
15+
print("- testing multisig verification (22.0)", flush=True)
16+
_220 = run_verify('22.0 --json')
17+
try:
18+
result = json.loads(_220.stdout.decode())
19+
except Exception:
20+
print("failed on 22.0 --json:")
21+
print_process_failure(_220)
22+
raise
23+
24+
expect_code(_220, 0, "22.0 should succeed")
25+
v = result['verified_binaries']
26+
assert result['good_trusted_sigs']
27+
assert v['bitcoin-22.0-aarch64-linux-gnu.tar.gz'] == 'ac718fed08570a81b3587587872ad85a25173afa5f9fbbd0c03ba4d1714cfa3e'
28+
assert v['bitcoin-22.0-osx64.tar.gz'] == '2744d199c3343b2d94faffdfb2c94d75a630ba27301a70e47b0ad30a7e0155e9'
29+
assert v['bitcoin-22.0-x86_64-linux-gnu.tar.gz'] == '59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16'
30+
31+
32+
def run_verify(extra: str) -> subprocess.CompletedProcess:
33+
maybe_here = Path.cwd() / 'verify.py'
34+
path = maybe_here if maybe_here.exists() else Path.cwd() / 'contrib' / 'verifybinaries' / 'verify.py'
35+
36+
return subprocess.run(
37+
f"{path} --cleanup {extra}",
38+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
39+
40+
41+
def expect_code(completed: subprocess.CompletedProcess, expected_code: int, msg: str):
42+
if completed.returncode != expected_code:
43+
print(f"{msg!r} failed: got code {completed.returncode}, expected {expected_code}")
44+
print_process_failure(completed)
45+
sys.exit(1)
46+
else:
47+
print(f"✓ {msg!r} passed")
48+
49+
50+
def print_process_failure(completed: subprocess.CompletedProcess):
51+
print(f"stdout:\n{completed.stdout.decode()}")
52+
print(f"stderr:\n{completed.stderr.decode()}")
53+
54+
55+
if __name__ == '__main__':
56+
main()

0 commit comments

Comments
 (0)