forked from monero-project/gitian.sigs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
verify-merge.py
executable file
·199 lines (185 loc) · 9.39 KB
/
verify-merge.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#!/usr/bin/env python3
import argparse
import os
import subprocess
import glob
import sys
GIT = os.getenv('GIT', 'git')
GPG = os.getenv('GPG', 'gpg')
GITIAN_PUBKEYS_DIR = os.getenv('GITIAN_PUBKEYS_DIR', 'gitian-pubkeys')
def verify():
global args, workdir
if args.import_keys:
import_gpg_keys()
if args.refresh_keys:
refresh_gpg_keys()
# 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)
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...')
keys = glob.glob('*.asc')
for key in keys:
subprocess.check_call([GPG, '--import', key])
os.chdir('../')
def get_assert_file_list(ver_pattern):
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('-')
version_major = release_num.split('.')[1]
assert_files.append({
'release_full': release_full,
'release_num': release_num,
'platform': platform,
'path': assert_file,
'user': pieces[1],
'version_major': version_major})
return assert_files
def verify_gpg_sigs(sig_file_paths):
print('Verifying signatures:')
is_verification_error = False
for sig_file in sig_file_paths:
print(' - ' + '{message: <{fill}}'.format(message=sig_file, fill='72'), end='')
result = verify_gpg_sig(sig_file)
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)
print('All signatures verified correctly.\n')
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}\nExpected:\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
def verify_gpg_sig(sig_file):
# TODO: Verify correct user created the signature.
return subprocess.run([GPG, '--verify', sig_file], capture_output=True, encoding='utf-8')
def verify_checksums(assert_files):
print('Beginning binary checksum comparison...\n')
# Check that the contents between the assertion signers match.
# This is meant for quick verification, not for validation of their contents.
# TODO: prevent false positives related to filenames / whitespace / formatting.
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()
f.close()
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
if assert_file_contents[i].strip() == '\'':
continue
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)
print('No discrepancies found in assertion files.')
if __name__ == '__main__':
main()