2019-07-21 22:41:20 +00:00
#!/usr/bin/env python3
import argparse
import os
import subprocess
import glob
2019-07-23 08:18:29 +00:00
import sys
2019-07-21 22:41:20 +00:00
2019-11-26 06:31:45 +00:00
GIT = os . getenv ( ' GIT ' , ' git ' )
GPG = os . getenv ( ' GPG ' , ' gpg ' )
GITIAN_PUBKEYS_DIR = os . getenv ( ' GITIAN_PUBKEYS_DIR ' , ' gitian-pubkeys ' )
2019-07-21 22:41:20 +00:00
def verify ( ) :
global args , workdir
2019-07-23 08:18:29 +00:00
if args . import_keys :
2019-11-26 06:31:45 +00:00
import_gpg_keys ( )
2019-07-23 08:18:29 +00:00
if args . refresh_keys :
2019-11-26 06:31:45 +00:00
refresh_gpg_keys ( )
2019-12-09 21:31:57 +00:00
# Shell glob pattern for specific version or all builds:
ver_pattern = args . version if args . version else ' v0* '
sig_file_paths = set ( glob . glob ( ver_pattern + ' -*/*/*.assert.sig ' ) )
assert_files = get_assert_file_list ( ver_pattern )
user_names = get_user_names_from_keys ( )
verify_file_path_naming ( assert_files , sig_file_paths , user_names )
verify_gpg_sigs ( sig_file_paths )
2019-11-26 06:31:45 +00:00
verify_checksums ( assert_files )
print ( ' All checks passed. ' )
os . chdir ( workdir )
def main ( ) :
global args , workdir
args = get_parsed_args ( )
workdir = os . getcwd ( )
if args . pull_id != None :
pull_request ( )
else :
verify ( )
def get_parsed_args ( ) :
parser = argparse . ArgumentParser ( usage = ' %(prog)s [options] ' , description = ' Use this script to verify the signatures of existing gitian assert files and / or assert files in a specific pull request. ' )
parser . add_argument ( ' -p ' , ' --pull_id ' , dest = ' pull_id ' , help = ' GitHub Pull request id to check ' )
parser . add_argument ( ' -r ' , ' --remote ' , dest = ' remote ' , default = ' upstream ' , help = ' The git remote repository ' )
parser . add_argument ( ' -t ' , ' --target-branch ' , dest = ' target_branch ' , default = ' master ' , help = ' Remote repository merge into branch ' )
parser . add_argument ( ' -m ' , ' --merge ' , action = ' store_true ' , dest = ' merge ' , help = ' Merge the given pull request id ' )
parser . add_argument ( ' -k ' , ' --refresh-keys ' , action = ' store_true ' , dest = ' refresh_keys ' , help = ' Refresh all public keys that are currently in the gpg keyring. ' )
parser . add_argument ( ' -i ' , ' --import-keys ' , action = ' store_true ' , dest = ' import_keys ' , help = ' Import all public keys in the gitian-pubkeys directory to the gpg keyring. ' )
parser . add_argument ( ' -o ' , ' --no-verify ' , action = ' store_true ' , dest = ' no_verify ' , help = ' Do not run any signature verification ' )
parser . add_argument ( ' -v ' , ' --version ' , dest = ' version ' , help = ' Version number of sigs to be verified (defaults to all versions if not specified). ' )
return parser . parse_args ( )
def pull_request ( ) :
global args
# Get branch from remote pull request and compare
head_branch = args . pull_id + ' _head '
subprocess . check_call ( [ GIT , ' fetch ' , args . remote ] )
subprocess . check_call ( [ GIT , ' checkout ' , args . remote + ' / ' + args . target_branch ] )
subprocess . check_call ( [ GIT , ' fetch ' , ' -q ' , args . remote , ' pull/ ' + args . pull_id + ' /head: ' + head_branch ] )
subprocess . check_call ( [ GIT , ' checkout ' , ' -f ' , head_branch ] )
if args . merge :
# Hard reset the target branch to the remote's state and merge the pull request's head branch into it
subprocess . check_call ( [ GIT , ' checkout ' , args . target_branch ] )
subprocess . check_call ( [ GIT , ' reset ' , ' --hard ' , args . remote + ' / ' + args . target_branch ] )
print ( ' Merging and signing pull request # ' + args . pull_id + ' , if you are using a smartcard, confirm the signature now. ' )
subprocess . check_call ( [ GIT , ' merge ' , ' -q ' , ' --commit ' , ' --no-edit ' , ' -m ' , ' Merge pull request # ' + args . pull_id + ' into ' + args . target_branch , ' --no-ff ' , ' --gpg-sign ' , head_branch ] )
if not args . no_verify :
verify ( )
subprocess . check_call ( [ GIT , ' checkout ' , ' master ' ] )
subprocess . check_call ( [ GIT , ' branch ' , ' -D ' , head_branch ] )
def refresh_gpg_keys ( ) :
print ( ' Refreshing pubkeys... ' )
subprocess . check_call ( [ GPG , ' --refresh ' ] )
def import_gpg_keys ( ) :
os . chdir ( GITIAN_PUBKEYS_DIR )
print ( ' Importing gpg pubkeys... ' )
2019-12-09 21:31:57 +00:00
keys = glob . glob ( ' *.asc ' )
2019-11-26 06:31:45 +00:00
for key in keys :
subprocess . check_call ( [ GPG , ' --import ' , key ] )
os . chdir ( ' ../ ' )
2019-12-09 21:31:57 +00:00
def get_assert_file_list ( ver_pattern ) :
2019-11-26 06:31:45 +00:00
assert_files = [ ]
for assert_file in sorted ( glob . glob ( ver_pattern + ' -*/*/*.assert ' ) ) :
pieces = assert_file . split ( ' / ' )
release_full = pieces [ 0 ] # eg v0.15.0.1-linux
release_num , platform = release_full . split ( ' - ' )
2019-12-09 21:31:57 +00:00
version_major = release_num . split ( ' . ' ) [ 1 ]
2019-11-26 06:31:45 +00:00
assert_files . append ( {
' release_full ' : release_full ,
' release_num ' : release_num ,
' platform ' : platform ,
' path ' : assert_file ,
2019-12-09 21:31:57 +00:00
' user ' : pieces [ 1 ] ,
' version_major ' : version_major } )
2019-11-26 06:31:45 +00:00
return assert_files
2019-12-09 21:31:57 +00:00
def verify_gpg_sigs ( sig_file_paths ) :
2019-11-19 02:50:03 +00:00
print ( ' Verifying signatures: ' )
is_verification_error = False
2019-12-09 21:31:57 +00:00
for sig_file in sig_file_paths :
2019-11-19 02:50:03 +00:00
print ( ' - ' + ' { message: < {fill} } ' . format ( message = sig_file , fill = ' 72 ' ) , end = ' ' )
2019-11-26 06:31:45 +00:00
result = verify_gpg_sig ( sig_file )
2019-11-19 02:50:03 +00:00
if result . returncode != 0 :
is_verification_error = True
print ( ' \n ' )
sys . stderr . write ( ' ERROR: \n ' + result . stderr + ' - ' * 80 + ' \n ' )
else :
print ( ' [OK] ' )
if is_verification_error :
sys . stderr . write ( ' ERROR: One or more signatures failed verification. \n ' )
exit ( 1 )
2019-11-19 17:07:29 +00:00
print ( ' All signatures verified correctly. \n ' )
2019-11-26 06:31:45 +00:00
2019-12-09 21:31:57 +00:00
def verify_file_path_naming ( assert_files , sig_file_paths , user_names ) :
path_pattern = ' {release_num} - {platform} / {user} /monero- {platform} -0. {version_major} -build.assert '
print ( ' Verifying file path naming... ' )
# Check that every sig has an assert:
if len ( sig_file_paths ) > len ( assert_files ) :
sys . stderr . write ( " ERROR: One or more sig files doesn ' t have a matching assert file: \n " )
assert_file_paths = [ a [ ' path ' ] for a in assert_files ]
extra_sigs = [ s for s in sig_file_paths if os . path . splitext ( s ) [ 0 ] not in assert_file_paths ]
for extra_sig in extra_sigs :
sys . stderr . write ( " - {0} \n " . format ( extra_sig ) )
exit ( 1 )
for assert_file in assert_files :
# Check assert file has a sig file:
if ( assert_file [ ' path ' ] + ' .sig ' ) not in sig_file_paths :
sys . stderr . write ( ' ERROR: Assert file found without corresponding sig file: \n ' + assert_file [ ' path ' ] + ' \n ' )
exit ( 1 )
# Check assert user corresponds with a known GPG pubkey:
if assert_file [ ' user ' ] not in user_names :
sys . stderr . write ( " ERROR: User ' {user} ' doesn ' t have a matching PGP key. Expected {folder} / {user} .asc \n " . format ( user = assert_file [ ' user ' ] , folder = GITIAN_PUBKEYS_DIR ) )
sys . stderr . write ( " * Found in path: {path} \n " . format ( path = assert_file [ ' path ' ] ) )
exit ( 1 )
# Check overall format of path (version num, platform, folder and file names):
expected_path = path_pattern . format ( * * assert_file )
if expected_path != assert_file [ ' path ' ] :
sys . stderr . write ( ' ERROR: File path appears to be incorrect: \n {actual} \n Expected: \n {expected} \n ' . format ( actual = assert_file [ ' path ' ] , expected = expected_path ) )
exit ( 1 )
print ( ' All file paths seem to be correct. \n ' )
def get_user_names_from_keys ( ) :
os . chdir ( GITIAN_PUBKEYS_DIR )
user_names = [ os . path . splitext ( key ) [ 0 ] for key in glob . glob ( ' *.asc ' ) ]
os . chdir ( ' ../ ' )
return user_names
2019-11-26 06:31:45 +00:00
def verify_gpg_sig ( sig_file ) :
2019-12-09 21:31:57 +00:00
# TODO: Verify correct user created the signature.
2019-11-26 06:31:45 +00:00
return subprocess . run ( [ GPG , ' --verify ' , sig_file ] , capture_output = True , encoding = ' utf-8 ' )
def verify_checksums ( assert_files ) :
print ( ' Beginning binary checksum comparison... \n ' )
2019-11-19 23:32:50 +00:00
# Check that the contents between the assertion signers match.
# This is meant for quick verification, not for validation of their contents.
2019-11-19 17:07:29 +00:00
# TODO: prevent false positives related to filenames / whitespace / formatting.
2019-11-26 06:31:45 +00:00
prev_release_num = ' '
prev_release_full = ' '
prev_platform = ' '
for assert_file in assert_files :
release_full = assert_file [ ' release_full ' ]
if release_full != prev_release_full :
first_user = assert_file [ ' user ' ]
first_file = assert_file [ ' path ' ]
prev_release_full = release_full
if prev_release_num != assert_file [ ' release_num ' ] :
print ( ' ' + assert_file [ ' release_num ' ] )
prev_release_num = assert_file [ ' release_num ' ]
f = open ( first_file , ' r ' )
first_file_contents = f . readlines ( )
2019-11-19 17:07:29 +00:00
f . close ( )
2019-11-26 06:31:45 +00:00
continue
platform = assert_file [ ' platform ' ]
if platform != prev_platform :
prev_platform = platform
print ( ' ' + platform )
print ( ' ' + first_user )
print ( ' ' + assert_file [ ' user ' ] )
assert_file_handle = open ( assert_file [ ' path ' ] , ' r ' )
assert_file_contents = assert_file_handle . readlines ( )
assert_file_handle . close ( )
for i in range ( len ( assert_file_contents ) ) :
# Compare each line in the assertion file until base_manifests:
if assert_file_contents [ i ] == ' - base_manifests: !!omap \n ' :
break
# The OSX SDK may change from time to time:
if ' sdk ' in assert_file_contents [ i ] :
continue
2020-12-14 00:28:05 +00:00
if assert_file_contents [ i ] . strip ( ) == ' \' ' :
continue
2019-11-26 06:31:45 +00:00
if assert_file_contents [ i ] != first_file_contents [ i ] :
sys . stderr . write ( ' ERROR: Found conflicting contents on line: ' + str ( i ) + ' of file ' )
sys . stderr . write ( assert_file [ ' path ' ] + ' : \n ' + assert_file_contents [ i ] )
sys . stderr . write ( first_file + ' : \n ' + first_file_contents [ i ] )
exit ( 1 )
2019-11-19 17:07:29 +00:00
print ( ' No discrepancies found in assertion files. ' )
2019-07-21 22:41:20 +00:00
if __name__ == ' __main__ ' :
main ( )