@@ -98,60 +98,6 @@ def bool_from_env(key, default=False) -> bool:
98
98
VERSION_FORMAT = "<major>.<minor>[.<patch>][-rc[0-9]][-platform]"
99
99
VERSION_EXAMPLE = "22.0-x86_64 or 0.21.0-rc2-osx"
100
100
101
- parser = argparse .ArgumentParser (description = __doc__ )
102
- parser .add_argument (
103
- 'version' , type = str , help = (
104
- f'version of the bitcoin release to download; of the format '
105
- f'{ VERSION_FORMAT } . Example: { VERSION_EXAMPLE } ' )
106
- )
107
- parser .add_argument (
108
- '-v' , '--verbose' , action = 'store_true' ,
109
- default = bool_from_env ('BINVERIFY_VERBOSE' ),
110
- )
111
- parser .add_argument (
112
- '-q' , '--quiet' , action = 'store_true' ,
113
- default = bool_from_env ('BINVERIFY_QUIET' ),
114
- )
115
- parser .add_argument (
116
- '--cleanup' , action = 'store_true' ,
117
- default = bool_from_env ('BINVERIFY_CLEANUP' ),
118
- help = 'if specified, clean up files afterwards'
119
- )
120
- parser .add_argument (
121
- '--import-keys' , action = 'store_true' ,
122
- default = bool_from_env ('BINVERIFY_IMPORTKEYS' ),
123
- help = 'if specified, ask to import each unknown builder key'
124
- )
125
- parser .add_argument (
126
- '--require-all-hosts' , action = 'store_true' ,
127
- default = bool_from_env ('BINVERIFY_REQUIRE_ALL_HOSTS' ),
128
- help = (
129
- f'If set, require all hosts ({ HOST1 } , { HOST2 } ) to provide signatures. '
130
- '(Sometimes bitcoin.org lags behind bitcoincore.org.)' )
131
- )
132
- parser .add_argument (
133
- '--min-good-sigs' , type = int , action = 'store' , nargs = '?' ,
134
- default = int (os .environ .get ('BINVERIFY_MIN_GOOD_SIGS' , 3 )),
135
- help = (
136
- 'The minimum number of good signatures to require successful termination.' ),
137
- )
138
- parser .add_argument (
139
- '--keyserver' , action = 'store' , nargs = '?' ,
140
- default = os .environ .get ('BINVERIFY_KEYSERVER' , 'hkp://keyserver.ubuntu.com' ),
141
- help = 'which keyserver to use' ,
142
- )
143
- parser .add_argument (
144
- '--trusted-keys' , action = 'store' , nargs = '?' ,
145
- default = os .environ .get ('BINVERIFY_TRUSTED_KEYS' , '' ),
146
- help = 'A list of trusted signer GPG keys, separated by commas. Not "trusted keys" in the GPG sense.' ,
147
- )
148
- parser .add_argument (
149
- '--json' , action = 'store_true' ,
150
- default = bool_from_env ('BINVERIFY_JSON' ),
151
- help = 'If set, output the result as JSON' ,
152
- )
153
-
154
-
155
101
def parse_version_string (version_str ):
156
102
if version_str .startswith (VERSIONPREFIX ): # remove version prefix
157
103
version_str = version_str [len (VERSIONPREFIX ):]
@@ -386,7 +332,7 @@ def join_url(host: str) -> str:
386
332
return ReturnCode .SUCCESS
387
333
388
334
389
- def check_multisig (sigfilename : str , args : argparse .Namespace ):
335
+ def check_multisig (sigfilename : Path , args : argparse .Namespace ) -> t . Tuple [ int , str , t . List [ SigData ], t . List [ SigData ], t . List [ SigData ]] :
390
336
# check signature
391
337
#
392
338
# We don't write output to a file because this command will almost certainly
@@ -423,61 +369,15 @@ def prompt_yn(prompt) -> bool:
423
369
got = input (prompt ).lower ()
424
370
return got == 'y'
425
371
372
+ def verify_shasums_signature (
373
+ signature_file_path : str , sums_file_path : str , args : argparse .Namespace
374
+ ) -> t .Tuple [
375
+ ReturnCode , t .List [SigData ], t .List [SigData ], t .List [SigData ], t .List [SigData ]
376
+ ]:
377
+ min_good_sigs = args .min_good_sigs
378
+ gpg_allowed_codes = [0 , 2 ] # 2 is returned when untrusted signatures are present.
426
379
427
- def main (args ):
428
- args = parser .parse_args ()
429
- if args .quiet :
430
- log .setLevel (logging .WARNING )
431
-
432
- WORKINGDIR = Path (tempfile .gettempdir ()) / f"bitcoin_verify_binaries.{ args .version } "
433
-
434
- def cleanup ():
435
- log .info ("cleaning up files" )
436
- os .chdir (Path .home ())
437
- shutil .rmtree (WORKINGDIR )
438
-
439
- # determine remote dir dependent on provided version string
440
- try :
441
- version_base , version_rc , os_filter = parse_version_string (args .version )
442
- version_tuple = [int (i ) for i in version_base .split ('.' )]
443
- except Exception as e :
444
- log .debug (e )
445
- log .error (f"unable to parse version; expected format is { VERSION_FORMAT } " )
446
- log .error (f" e.g. { VERSION_EXAMPLE } " )
447
- return ReturnCode .BAD_VERSION
448
-
449
- remote_dir = f"/bin/{ VERSIONPREFIX } { version_base } /"
450
- if version_rc :
451
- remote_dir += f"test.{ version_rc } /"
452
- remote_sigs_path = remote_dir + SIGNATUREFILENAME
453
- remote_sums_path = remote_dir + SUMS_FILENAME
454
-
455
- # create working directory
456
- os .makedirs (WORKINGDIR , exist_ok = True )
457
- os .chdir (WORKINGDIR )
458
-
459
- hosts = [HOST1 , HOST2 ]
460
-
461
- got_sig_status = get_files_from_hosts_and_compare (
462
- hosts , remote_sigs_path , SIGNATUREFILENAME , args .require_all_hosts )
463
- if got_sig_status != ReturnCode .SUCCESS :
464
- return got_sig_status
465
-
466
- # Multi-sig verification is available after 22.0.
467
- if version_tuple [0 ] >= 22 :
468
- min_good_sigs = args .min_good_sigs
469
- gpg_allowed_codes = [0 , 2 ] # 2 is returned when untrusted signatures are present.
470
-
471
- got_sums_status = get_files_from_hosts_and_compare (
472
- hosts , remote_sums_path , SUMS_FILENAME , args .require_all_hosts )
473
- if got_sums_status != ReturnCode .SUCCESS :
474
- return got_sums_status
475
-
476
- gpg_retval , gpg_output , good , unknown , bad = check_multisig (SIGNATUREFILENAME , args )
477
- else :
478
- log .error ("Version too old - single sig not supported. Use a previous "
479
- "version of this script from the repo." )
480
- return ReturnCode .BAD_VERSION
380
+ gpg_retval , gpg_output , good , unknown , bad = check_multisig (signature_file_path , args )
481
381
482
382
if gpg_retval not in gpg_allowed_codes :
483
383
if gpg_retval == 1 :
@@ -490,8 +390,7 @@ def cleanup():
490
390
log .critical (f"unexpected GPG exit code ({ gpg_retval } )" )
491
391
492
392
log .error (f"gpg output:\n { indent (gpg_output )} " )
493
- cleanup ()
494
- return ReturnCode .INTEGRITY_FAILURE
393
+ return (ReturnCode .INTEGRITY_FAILURE , [], [], [], [])
495
394
496
395
# Decide which keys we trust, though not "trust" in the GPG sense, but rather
497
396
# which pubkeys convince us that this sums file is legitimate. In other words,
@@ -503,7 +402,7 @@ def cleanup():
503
402
504
403
# Tally signatures and make sure we have enough goods to fulfill
505
404
# our threshold.
506
- good_trusted = { sig for sig in good if sig .trusted or sig .key in trusted_keys }
405
+ good_trusted = [ sig for sig in good if sig .trusted or sig .key in trusted_keys ]
507
406
good_untrusted = [sig for sig in good if sig not in good_trusted ]
508
407
num_trusted = len (good_trusted ) + len (good_untrusted )
509
408
log .info (f"got { num_trusted } good signatures" )
@@ -520,7 +419,7 @@ def cleanup():
520
419
"not enough trusted sigs to meet threshold "
521
420
f"({ num_trusted } vs. { min_good_sigs } )" )
522
421
523
- return ReturnCode .NOT_ENOUGH_GOOD_SIGS
422
+ return ( ReturnCode .NOT_ENOUGH_GOOD_SIGS , [], [], [], [])
524
423
525
424
for sig in good_trusted :
526
425
log .info (f"GOOD SIGNATURE: { sig } " )
@@ -537,10 +436,93 @@ def cleanup():
537
436
for sig in unknown :
538
437
log .warning (f"UNKNOWN SIGNATURE: { sig } " )
539
438
439
+ return (ReturnCode .SUCCESS , good_trusted , good_untrusted , unknown , bad )
440
+
441
+
442
+ def parse_sums_file (sums_file_path : Path , filename_filter : str ) -> t .List [t .List [str ]]:
540
443
# extract hashes/filenames of binaries to verify from hash file;
541
444
# each line has the following format: "<hash> <binary_filename>"
542
- with open (SUMS_FILENAME , 'r' , encoding = 'utf8' ) as hash_file :
543
- hashes_to_verify = [line .split ()[:2 ] for line in hash_file if os_filter in line ]
445
+ with open (sums_file_path , 'r' , encoding = 'utf8' ) as hash_file :
446
+ return [line .split ()[:2 ] for line in hash_file if filename_filter in line ]
447
+
448
+
449
+ def verify_binary_hashes (hashes_to_verify : t .List [t .List [str ]]) -> t .Tuple [ReturnCode , t .Dict [str , str ]]:
450
+ offending_files = []
451
+ files_to_hashes = {}
452
+
453
+ for hash_expected , binary_filename in hashes_to_verify :
454
+ with open (binary_filename , 'rb' ) as binary_file :
455
+ hash_calculated = sha256 (binary_file .read ()).hexdigest ()
456
+ if hash_calculated != hash_expected :
457
+ offending_files .append (binary_filename )
458
+ else :
459
+ files_to_hashes [binary_filename ] = hash_calculated
460
+
461
+ if offending_files :
462
+ joined_files = '\n ' .join (offending_files )
463
+ log .critical (
464
+ "Hashes don't match.\n "
465
+ f"Offending files:\n { joined_files } " )
466
+ return (ReturnCode .INTEGRITY_FAILURE , files_to_hashes )
467
+
468
+ return (ReturnCode .SUCCESS , files_to_hashes )
469
+
470
+
471
+ def verify_published_handler (args : argparse .Namespace ) -> ReturnCode :
472
+ WORKINGDIR = Path (tempfile .gettempdir ()) / f"bitcoin_verify_binaries.{ args .version } "
473
+
474
+ def cleanup ():
475
+ log .info ("cleaning up files" )
476
+ os .chdir (Path .home ())
477
+ shutil .rmtree (WORKINGDIR )
478
+
479
+ # determine remote dir dependent on provided version string
480
+ try :
481
+ version_base , version_rc , os_filter = parse_version_string (args .version )
482
+ version_tuple = [int (i ) for i in version_base .split ('.' )]
483
+ except Exception as e :
484
+ log .debug (e )
485
+ log .error (f"unable to parse version; expected format is { VERSION_FORMAT } " )
486
+ log .error (f" e.g. { VERSION_EXAMPLE } " )
487
+ return ReturnCode .BAD_VERSION
488
+
489
+ remote_dir = f"/bin/{ VERSIONPREFIX } { version_base } /"
490
+ if version_rc :
491
+ remote_dir += f"test.{ version_rc } /"
492
+ remote_sigs_path = remote_dir + SIGNATUREFILENAME
493
+ remote_sums_path = remote_dir + SUMS_FILENAME
494
+
495
+ # create working directory
496
+ os .makedirs (WORKINGDIR , exist_ok = True )
497
+ os .chdir (WORKINGDIR )
498
+
499
+ hosts = [HOST1 , HOST2 ]
500
+
501
+ got_sig_status = get_files_from_hosts_and_compare (
502
+ hosts , remote_sigs_path , SIGNATUREFILENAME , args .require_all_hosts )
503
+ if got_sig_status != ReturnCode .SUCCESS :
504
+ return got_sig_status
505
+
506
+ # Multi-sig verification is available after 22.0.
507
+ if version_tuple [0 ] < 22 :
508
+ log .error ("Version too old - single sig not supported. Use a previous "
509
+ "version of this script from the repo." )
510
+ return ReturnCode .BAD_VERSION
511
+
512
+ got_sums_status = get_files_from_hosts_and_compare (
513
+ hosts , remote_sums_path , SUMS_FILENAME , args .require_all_hosts )
514
+ if got_sums_status != ReturnCode .SUCCESS :
515
+ return got_sums_status
516
+
517
+ # Verify the signature on the SHA256SUMS file
518
+ sigs_status , good_trusted , good_untrusted , unknown , bad = verify_shasums_signature (SIGNATUREFILENAME , SUMS_FILENAME , args )
519
+ if sigs_status != ReturnCode .SUCCESS :
520
+ if sigs_status == ReturnCode .INTEGRITY_FAILURE :
521
+ cleanup ()
522
+ return sigs_status
523
+
524
+ # Extract hashes and filenames
525
+ hashes_to_verify = parse_sums_file (SUMS_FILENAME , os_filter )
544
526
remove_files ([SUMS_FILENAME ])
545
527
if not hashes_to_verify :
546
528
log .error ("no files matched the platform specified" )
@@ -570,23 +552,10 @@ def cleanup():
570
552
return ReturnCode .BINARY_DOWNLOAD_FAILED
571
553
572
554
# verify hashes
573
- offending_files = []
574
- files_to_hashes = {}
555
+ hashes_status , files_to_hashes = verify_binary_hashes (hashes_to_verify )
556
+ if hashes_status != ReturnCode .SUCCESS :
557
+ return hashes_status
575
558
576
- for hash_expected , binary_filename in hashes_to_verify :
577
- with open (binary_filename , 'rb' ) as binary_file :
578
- hash_calculated = sha256 (binary_file .read ()).hexdigest ()
579
- if hash_calculated != hash_expected :
580
- offending_files .append (binary_filename )
581
- else :
582
- files_to_hashes [binary_filename ] = hash_calculated
583
-
584
- if offending_files :
585
- joined_files = '\n ' .join (offending_files )
586
- log .critical (
587
- "Hashes don't match.\n "
588
- f"Offending files:\n { joined_files } " )
589
- return ReturnCode .INTEGRITY_FAILURE
590
559
591
560
if args .cleanup :
592
561
cleanup ()
@@ -609,5 +578,71 @@ def cleanup():
609
578
return ReturnCode .SUCCESS
610
579
611
580
581
+ def main ():
582
+ parser = argparse .ArgumentParser (description = __doc__ )
583
+ parser .add_argument (
584
+ '-v' , '--verbose' , action = 'store_true' ,
585
+ default = bool_from_env ('BINVERIFY_VERBOSE' ),
586
+ )
587
+ parser .add_argument (
588
+ '-q' , '--quiet' , action = 'store_true' ,
589
+ default = bool_from_env ('BINVERIFY_QUIET' ),
590
+ )
591
+ parser .add_argument (
592
+ '--import-keys' , action = 'store_true' ,
593
+ default = bool_from_env ('BINVERIFY_IMPORTKEYS' ),
594
+ help = 'if specified, ask to import each unknown builder key'
595
+ )
596
+ parser .add_argument (
597
+ '--min-good-sigs' , type = int , action = 'store' , nargs = '?' ,
598
+ default = int (os .environ .get ('BINVERIFY_MIN_GOOD_SIGS' , 3 )),
599
+ help = (
600
+ 'The minimum number of good signatures to require successful termination.' ),
601
+ )
602
+ parser .add_argument (
603
+ '--keyserver' , action = 'store' , nargs = '?' ,
604
+ default = os .environ .get ('BINVERIFY_KEYSERVER' , 'hkp://keyserver.ubuntu.com' ),
605
+ help = 'which keyserver to use' ,
606
+ )
607
+ parser .add_argument (
608
+ '--trusted-keys' , action = 'store' , nargs = '?' ,
609
+ default = os .environ .get ('BINVERIFY_TRUSTED_KEYS' , '' ),
610
+ help = 'A list of trusted signer GPG keys, separated by commas. Not "trusted keys" in the GPG sense.' ,
611
+ )
612
+ parser .add_argument (
613
+ '--json' , action = 'store_true' ,
614
+ default = bool_from_env ('BINVERIFY_JSON' ),
615
+ help = 'If set, output the result as JSON' ,
616
+ )
617
+
618
+ subparsers = parser .add_subparsers (title = "Commands" , required = True , dest = "command" )
619
+
620
+ pub_parser = subparsers .add_parser ("pub" , help = "Verify a published release." )
621
+ pub_parser .set_defaults (func = verify_published_handler )
622
+ pub_parser .add_argument (
623
+ 'version' , type = str , help = (
624
+ f'version of the bitcoin release to download; of the format '
625
+ f'{ VERSION_FORMAT } . Example: { VERSION_EXAMPLE } ' )
626
+ )
627
+ pub_parser .add_argument (
628
+ '--cleanup' , action = 'store_true' ,
629
+ default = bool_from_env ('BINVERIFY_CLEANUP' ),
630
+ help = 'if specified, clean up files afterwards'
631
+ )
632
+ pub_parser .add_argument (
633
+ '--require-all-hosts' , action = 'store_true' ,
634
+ default = bool_from_env ('BINVERIFY_REQUIRE_ALL_HOSTS' ),
635
+ help = (
636
+ f'If set, require all hosts ({ HOST1 } , { HOST2 } ) to provide signatures. '
637
+ '(Sometimes bitcoin.org lags behind bitcoincore.org.)' )
638
+ )
639
+
640
+ args = parser .parse_args ()
641
+ if args .quiet :
642
+ log .setLevel (logging .WARNING )
643
+
644
+ return args .func (args )
645
+
646
+
612
647
if __name__ == '__main__' :
613
- sys .exit (main (sys . argv [ 1 :] ))
648
+ sys .exit (main ())
0 commit comments