Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 25 additions & 16 deletions contrib/verify-binaries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

#### Preparation

As of Bitcoin Core v22.0, releases are signed by a number of public keys on the basis
of the [guix.sigs repository](https://github.com/bitcoin-core/guix.sigs/). When
As of Bitcoin Knots v21.x, releases are signed by a number of public keys on the basis
of the [guix.sigs repository](https://github.com/bitcoinknots/guix.sigs/). When
verifying binary downloads, you (the end user) decide which of these public keys you
trust and then use that trust model to evaluate the signature on a file that contains
hashes of the release binaries. The downloaded binaries are then hashed and compared to
the signed checksum file.

First, you have to figure out which public keys to recognize. Browse the [list of frequent
builder-keys](https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys) and
builder-keys](https://github.com/bitcoinknots/guix.sigs/tree/knots/builder-keys) and
decide which of these keys you would like to trust. For each key you want to trust, you
must obtain that key for your local GPG installation.

Expand All @@ -23,7 +23,8 @@ You can obtain these keys by
#### Usage

This script attempts to download the checksum file (`SHA256SUMS`) and corresponding
signature file `SHA256SUMS.asc` from https://bitcoincore.org and https://bitcoin.org.
signature file `SHA256SUMS.asc` from https://bitcoinknots.org and
https://github.com/bitcoinknots/bitcoin/releases.

It first checks if the checksum file is valid based upon a plurality of signatures, and
then downloads the release files specified in the checksum file, and checks if the
Expand All @@ -42,37 +43,45 @@ See the `Config` object for various options.

Validate releases with default settings:
```sh
./contrib/verify-binaries/verify.py pub 22.0
./contrib/verify-binaries/verify.py pub 22.0-rc3
./contrib/verify-binaries/verify.py pub 29.1.knots20250903
./contrib/verify-binaries/verify.py pub 27.1.knots20240801
```

Get JSON output and don't prompt for user input (no auto key import):

```sh
./contrib/verify-binaries/verify.py --json pub 22.0-x86
./contrib/verify-binaries/verify.py --json pub 23.0-rc5-linux-gnu
./contrib/verify-binaries/verify.py --json pub 29.1.knots20250903-x86
./contrib/verify-binaries/verify.py --json pub 27.1.knots20240801-win64
```

Require all hosts (bitcoinknots.org / github.com) to provide identical
checksums and signature files:

```sh
./contrib/verify-binaries/verify.py --json pub --require-all-hosts 29.1.knots20250903-x86
./contrib/verify-binaries/verify.py --json pub --require-all-hosts 27.1.knots20240801-win64
```

Rely only on local GPG state and manually specified keys, while requiring a
threshold of at least 10 trusted signatures:
```sh
./contrib/verify-binaries/verify.py \
--trusted-keys 74E2DEF5D77260B98BC19438099BAD163C70FBFA,9D3CC86A72F8494342EA5FD10A41BDC3F4FAFF1C \
--min-good-sigs 10 pub 22.0-linux
--trusted-keys 1A3E761F19D2CC7785C5502EA291A2C45D0C504A,F4FC70F07310028424EFC20A8E4256593F177720 \
--min-good-sigs 10 pub 29.1.knots20250903-linux
```

If you only want to download the binaries for a certain architecture and/or platform, add the corresponding suffix, e.g.:

```sh
./contrib/verify-binaries/verify.py pub 25.2-x86_64-linux
./contrib/verify-binaries/verify.py pub 24.1-rc1-darwin
./contrib/verify-binaries/verify.py pub 27.0-win64-setup.exe
./contrib/verify-binaries/verify.py pub 25.1.knots20231115-x86_64-linux
./contrib/verify-binaries/verify.py pub 23.0.knots20220529-darwin
./contrib/verify-binaries/verify.py pub 27.1.knots20240801-win64-setup.exe
```

If you do not want to keep the downloaded binaries, specify the cleanup option.

```sh
./contrib/verify-binaries/verify.py pub --cleanup 22.0
./contrib/verify-binaries/verify.py pub --cleanup 29.1.knots20250903
```

Use the bin subcommand to verify all files listed in a local checksum file
Expand All @@ -85,6 +94,6 @@ Verify only a subset of the files listed in a local checksum file

```sh
./contrib/verify-binaries/verify.py bin ~/Downloads/SHA256SUMS \
~/Downloads/bitcoin-24.0.1-x86_64-linux-gnu.tar.gz \
~/Downloads/bitcoin-24.0.1-arm-linux-gnueabihf.tar.gz
~/Downloads/bitcoin-23.0.knots20220529-x86_64-linux-gnu.tar.gz \
~/Downloads/bitcoin-23.0.knots20220529-arm-linux-gnueabihf.tar.gz
```
34 changes: 17 additions & 17 deletions contrib/verify-binaries/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,38 @@ def main():
"""Tests ordered roughly from faster to slower."""
expect_code(run_verify("", "pub", '0.32'), 4, "Nonexistent version should fail")
expect_code(run_verify("", "pub", '0.32.awefa.12f9h'), 11, "Malformed version should fail")
expect_code(run_verify('--min-good-sigs 20', "pub", "22.0"), 9, "--min-good-sigs 20 should fail")
expect_code(run_verify('--min-good-sigs 20', "pub", "29.1.knots20250903"), 9, "--min-good-sigs 20 should fail")

print("- testing verification (22.0-x86_64-linux-gnu.tar.gz)", flush=True)
_220_x86_64_linux_gnu = run_verify("--json", "pub", "22.0-x86_64-linux-gnu.tar.gz")
print("- testing verification (29.1.knots20250903-x86_64-linux-gnu.tar.gz)", flush=True)
_291knots_x86_64_linux_gnu = run_verify("--json", "pub --require-all-hosts", "29.1.knots20250903-x86_64-linux-gnu.tar.gz")
try:
result = json.loads(_220_x86_64_linux_gnu.stdout.decode())
result = json.loads(_291knots_x86_64_linux_gnu.stdout.decode())
except Exception:
print("failed on 22.0-x86_64-linux-gnu.tar.gz --json:")
print_process_failure(_220_x86_64_linux_gnu)
print("failed on 29.1.knots20250903-x86_64-linux-gnu.tar.gz --json:")
print_process_failure(_291knots_x86_64_linux_gnu)
raise

expect_code(_220_x86_64_linux_gnu, 0, "22.0-x86_64-linux-gnu.tar.gz should succeed")
expect_code(_291knots_x86_64_linux_gnu, 0, "29.1.knots20250903-x86_64-linux-gnu.tar.gz should succeed")
v = result['verified_binaries']
assert result['good_trusted_sigs']
assert len(v) == 1
assert v['bitcoin-22.0-x86_64-linux-gnu.tar.gz'] == '59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16'
assert v['bitcoin-29.1.knots20250903-x86_64-linux-gnu.tar.gz'] == '3752cf932309cd98734eb20ebb6c7aea4b8a10eb329b3d8d8fbd00098ea674fb'

print("- testing verification (22.0)", flush=True)
_220 = run_verify("--json", "pub", "22.0")
print("- testing verification (29.1.knots20250903)", flush=True)
_291knots = run_verify("--json", "pub", "29.1.knots20250903")
try:
result = json.loads(_220.stdout.decode())
result = json.loads(_291knots.stdout.decode())
except Exception:
print("failed on 22.0 --json:")
print_process_failure(_220)
print("failed on 29.1.knots20250903 --json:")
print_process_failure(_291knots)
raise

expect_code(_220, 0, "22.0 should succeed")
expect_code(_291knots, 0, "29.1.knots20250903 should succeed")
v = result['verified_binaries']
assert result['good_trusted_sigs']
assert v['bitcoin-22.0-aarch64-linux-gnu.tar.gz'] == 'ac718fed08570a81b3587587872ad85a25173afa5f9fbbd0c03ba4d1714cfa3e'
assert v['bitcoin-22.0-osx64.tar.gz'] == '2744d199c3343b2d94faffdfb2c94d75a630ba27301a70e47b0ad30a7e0155e9'
assert v['bitcoin-22.0-x86_64-linux-gnu.tar.gz'] == '59ebd25dd82a51638b7a6bb914586201e67db67b919b2a1ff08925a7936d1b16'
assert v['bitcoin-29.1.knots20250903-aarch64-linux-gnu.tar.gz'] == '505fe70d71609abc42d1d91c23fb0b4ac0c94bcb6fe6674bb6b936a4708576e9'
assert v['bitcoin-29.1.knots20250903-x86_64-apple-darwin.tar.gz'] == 'fd7b56abc611a5311dde3157346524ae4f03131df4f72948e1c9ba4d10d6cc33'
assert v['bitcoin-29.1.knots20250903-x86_64-linux-gnu.tar.gz'] == '3752cf932309cd98734eb20ebb6c7aea4b8a10eb329b3d8d8fbd00098ea674fb'


def run_verify(global_args: str, command: str, command_args: str) -> subprocess.CompletedProcess:
Expand Down
65 changes: 30 additions & 35 deletions contrib/verify-binaries/verify.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
#!/usr/bin/env python3
# Copyright (c) 2020-2021 The Bitcoin Core developers
# Modified for Bitcoin Knots Support
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Script for verifying Bitcoin Core release binaries.
"""Script for verifying Bitcoin Knots release binaries.

This script attempts to download the sum file SHA256SUMS and corresponding
signature file SHA256SUMS.asc from bitcoincore.org and bitcoin.org and
compares them.
signature file SHA256SUMS.asc from bitcoinknots.org and github.com and compares
them.

The sum-signature file is signed by a number of builder keys. This script
ensures that there is a minimum threshold of signatures from pubkeys that
Expand All @@ -15,7 +16,7 @@

The builder keys are available in the guix.sigs repo:

https://github.com/bitcoin-core/guix.sigs/tree/main/builder-keys
https://github.com/bitcoinknots/guix.sigs/tree/knots/builder-keys

If a minimum good, trusted signature threshold is met on the sum file, we then
download the files specified in SHA256SUMS, and check if the hashes of these
Expand Down Expand Up @@ -46,9 +47,9 @@
from pathlib import PurePath, Path

# The primary host; this will fail if we can't retrieve files from here.
HOST1 = "https://bitcoincore.org"
HOST2 = "https://bitcoin.org"
VERSIONPREFIX = "bitcoin-core-"
HOST1 = "https://bitcoinknots.org"
HOST2 = "https://github.com/bitcoinknots/bitcoin/releases"
VERSIONPREFIX = "bitcoin-"
SUMS_FILENAME = 'SHA256SUMS'
SIGNATUREFILENAME = f"{SUMS_FILENAME}.asc"

Expand Down Expand Up @@ -96,8 +97,8 @@ def bool_from_env(key, default=False) -> bool:
raise ValueError(f"Unrecognized environment value {key}={raw!r}")


VERSION_FORMAT = "<major>.<minor>[.<patch>][-rc[0-9]][-platform]"
VERSION_EXAMPLE = "22.0 or 23.1-rc1-darwin.dmg or 27.0-x86_64-linux-gnu"
VERSION_FORMAT = "<major>.<minor>[.<patch>].knots<date>[-rc[0-9]][-platform]"
VERSION_EXAMPLE = "29.1.knots20250903 or 29.1.knots20250903-arm64-apple-darwin or 27.1.knots20240801-x86_64-linux-gnu"

def parse_version_string(version_str):
# "<version>[-rcN][-platform]"
Expand All @@ -106,8 +107,9 @@ def parse_version_string(version_str):
if platform.startswith("rc"): # "<version>-rcN[-platform]"
rc, _, platform = platform.partition('-')
# else "<version>" or "<version>-platform"
version_base, _, version_date = version_base.partition('.knots')

return version_base, rc, platform
return version_base, version_date, rc, platform


def download_with_wget(remote_file, local_file):
Expand Down Expand Up @@ -259,7 +261,7 @@ def files_are_equal(filename1, filename2):


def get_files_from_hosts_and_compare(
hosts: list[str], path: str, filename: str, require_all: bool = False
hosts: list[str], paths: list[str], filename: str, require_all: bool = False
) -> ReturnCode:
"""
Retrieve the same file from a number of hosts and ensure they have the same contents.
Expand All @@ -271,12 +273,14 @@ def get_files_from_hosts_and_compare(
assert len(hosts) > 1
primary_host = hosts[0]
other_hosts = hosts[1:]
primary_path = paths[0]
other_paths = paths[1:]
got_files = []

def join_url(host: str) -> str:
def join_url(host: str, path: str) -> str:
return host.rstrip('/') + '/' + path.lstrip('/')

url = join_url(primary_host)
url = join_url(primary_host, primary_path)
success, output = download_with_wget(url, filename)
if not success:
log.error(
Expand All @@ -290,8 +294,8 @@ def join_url(host: str) -> str:
log.info(f"got file {url} as {filename}")
got_files.append(filename)

for i, host in enumerate(other_hosts):
url = join_url(host)
for i, (host, path) in enumerate(zip(other_hosts, other_paths)):
url = join_url(host, path)
fname = filename + f'.{i + 2}'
success, output = download_with_wget(url, fname)

Expand Down Expand Up @@ -462,19 +466,22 @@ def cleanup():

# determine remote dir dependent on provided version string
try:
version_base, version_rc, os_filter = parse_version_string(args.version)
version_base, version_date, version_rc, os_filter = parse_version_string(args.version)
version_tuple = [int(i) for i in version_base.split('.')]
except Exception as e:
log.debug(e)
log.error(f"unable to parse version; expected format is {VERSION_FORMAT}")
log.error(f" e.g. {VERSION_EXAMPLE}")
return ReturnCode.BAD_VERSION

remote_dir = f"/bin/{VERSIONPREFIX}{version_base}/"
major_version_dir = f"{version_tuple[0]}.x"
exact_version_dir=f"{version_base}.knots{version_date}"
host1_remote_dir = f"/files/{major_version_dir}/{exact_version_dir}/"
host2_remote_dir = f"download/v{exact_version_dir}/"
if version_rc:
remote_dir += f"test.{version_rc}/"
remote_sigs_path = remote_dir + SIGNATUREFILENAME
remote_sums_path = remote_dir + SUMS_FILENAME
remote_sigs_paths = [ host1_remote_dir + SIGNATUREFILENAME, host2_remote_dir + SIGNATUREFILENAME ]
remote_sums_paths = [ host1_remote_dir + SUMS_FILENAME, host2_remote_dir + SUMS_FILENAME ]

# create working directory
os.makedirs(WORKINGDIR, exist_ok=True)
Expand All @@ -483,7 +490,7 @@ def cleanup():
hosts = [HOST1, HOST2]

got_sig_status = get_files_from_hosts_and_compare(
hosts, remote_sigs_path, SIGNATUREFILENAME, args.require_all_hosts)
hosts, remote_sigs_paths, SIGNATUREFILENAME, args.require_all_hosts)
if got_sig_status != ReturnCode.SUCCESS:
return got_sig_status

Expand All @@ -494,7 +501,7 @@ def cleanup():
return ReturnCode.BAD_VERSION

got_sums_status = get_files_from_hosts_and_compare(
hosts, remote_sums_path, SUMS_FILENAME, args.require_all_hosts)
hosts, remote_sums_paths, SUMS_FILENAME, args.require_all_hosts)
if got_sums_status != ReturnCode.SUCCESS:
return got_sums_status

Expand All @@ -513,22 +520,11 @@ def cleanup():
log.error(f"No files matched the platform specified. Did you mean: {closest_match}")
return ReturnCode.NO_BINARIES_MATCH

# remove binaries that are known not to be hosted by bitcoincore.org

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you ripping out this functionality? just creates more unnecessary constraints on both the hosts,

we should leave the option to avoid these binaries fi we can since they arent the ones used by users..

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block was removed simply because it was ad-hoc code to handle bitcoincore.org. However it is a good feature, and this functionality could be re-introduced as a cli option, like so:

./verify.py --ignore "-unsigned","-debug",".zip" . . .

Which would let the user filter out files he doesn't need. This would only apply if you're running the script with just the version, for example, and all matching files get verified and downloaded. Files matching the ignored keywords would be dropped.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is better, or for now a simpler one that says -ignore-unused or something of that sort. I dont think ripping this functionality is good, it would add more constraints on hosts and make rebases harder. We should make the script more flexible rather than less.

fragments_to_remove = ['-unsigned', '-debug', '-codesignatures']
for fragment in fragments_to_remove:
nobinaries = [i for i in hashes_to_verify if fragment in i[1]]
if nobinaries:
remove_str = ', '.join(i[1] for i in nobinaries)
log.info(
f"removing *{fragment} binaries ({remove_str}) from verification "
f"since {HOST1} does not host *{fragment} binaries")
hashes_to_verify = [i for i in hashes_to_verify if fragment not in i[1]]

# download binaries
for _, binary_filename in hashes_to_verify:
log.info(f"downloading {binary_filename} to {WORKINGDIR}")
success, output = download_with_wget(
HOST1 + remote_dir + binary_filename, binary_filename)
HOST1 + host1_remote_dir + binary_filename, binary_filename)

if not success:
log.error(
Expand Down Expand Up @@ -685,8 +681,7 @@ def main():
'--require-all-hosts', action='store_true',
default=bool_from_env('BINVERIFY_REQUIRE_ALL_HOSTS'),
help=(
f'If set, require all hosts ({HOST1}, {HOST2}) to provide signatures. '
'(Sometimes bitcoin.org lags behind bitcoincore.org.)')
f'If set, require all hosts ({HOST1}, {HOST2}) to provide signatures.')
)

bin_parser = subparsers.add_parser("bin", help="Verify local binaries.")
Expand Down