42
42
import urllib .request
43
43
import enum
44
44
from hashlib import sha256
45
- from pathlib import PurePath
45
+ from pathlib import PurePath , Path
46
46
47
47
# The primary host; this will fail if we can't retrieve files from here.
48
48
HOST1 = "https://bitcoincore.org"
@@ -141,14 +141,19 @@ def verify_with_gpg(
141
141
signature_filename ,
142
142
output_filename : t .Optional [str ] = None
143
143
) -> t .Tuple [int , str ]:
144
- args = [
145
- 'gpg' , '--yes' , '--verify' , '--verify-options' , 'show-primary-uid-only' ,
146
- '--output' , output_filename if output_filename else '' , signature_filename , filename ]
144
+ with tempfile .NamedTemporaryFile () as status_file :
145
+ args = [
146
+ 'gpg' , '--yes' , '--verify' , '--verify-options' , 'show-primary-uid-only' , "--status-file" , status_file .name ,
147
+ '--output' , output_filename if output_filename else '' , signature_filename , filename ]
147
148
148
- env = dict (os .environ , LANGUAGE = 'en' )
149
- result = subprocess .run (args , stderr = subprocess .STDOUT , stdout = subprocess .PIPE , env = env )
150
- log .debug (f'Result from GPG ({ result .returncode } ): { result .stdout } ' )
151
- return result .returncode , result .stdout .decode ().rstrip ()
149
+ env = dict (os .environ , LANGUAGE = 'en' )
150
+ result = subprocess .run (args , stderr = subprocess .STDOUT , stdout = subprocess .PIPE , env = env )
151
+
152
+ gpg_data = status_file .read ().decode ().rstrip ()
153
+
154
+ log .debug (f'Result from GPG ({ result .returncode } ): { result .stdout .decode ()} ' )
155
+ log .debug (f"{ gpg_data } " )
156
+ return result .returncode , gpg_data
152
157
153
158
154
159
def remove_files (filenames ):
@@ -158,11 +163,14 @@ def remove_files(filenames):
158
163
159
164
class SigData :
160
165
"""GPG signature data as parsed from GPG stdout."""
161
- def __init__ (self , key : str , name : str , trusted : bool , status : str ):
162
- self .key = key
163
- self .name = name
164
- self .trusted = trusted
165
- self .status = status
166
+ def __init__ (self ):
167
+ self .key = None
168
+ self .name = ""
169
+ self .trusted = False
170
+ self .status = ""
171
+
172
+ def __bool__ (self ):
173
+ return self .key is not None
166
174
167
175
def __repr__ (self ):
168
176
return (
@@ -174,60 +182,60 @@ def parse_gpg_result(
174
182
output : t .List [str ]
175
183
) -> t .Tuple [t .List [SigData ], t .List [SigData ], t .List [SigData ]]:
176
184
"""Returns good, unknown, and bad signatures from GPG stdout."""
177
- good_sigs = []
178
- unknown_sigs = []
179
- bad_sigs = []
185
+ good_sigs : t . List [ SigData ] = []
186
+ unknown_sigs : t . List [ SigData ] = []
187
+ bad_sigs : t . List [ SigData ] = []
180
188
total_resolved_sigs = 0
181
- curr_key = None
182
189
183
190
# Ensure that all lines we match on include a prefix that prevents malicious input
184
191
# from fooling the parser.
185
192
def line_begins_with (patt : str , line : str ) -> t .Optional [re .Match ]:
186
- return re .match (r'^\s*(gpg:)?(\s+)' + patt , line )
187
-
188
- detected_name = ''
189
-
190
- for i , line in enumerate (output ):
191
- if line_begins_with (r"using (ECDSA|RSA) key (0x[0-9a-fA-F]{16}|[0-9a-fA-F]{40})$" , line ):
192
- if curr_key :
193
- raise RuntimeError (
194
- f"WARNING: encountered a new sig without resolving the last ({ curr_key } ) - "
195
- "this could mean we have encountered a bad signature! check GPG output!" )
196
- curr_key = line .split ('key ' )[- 1 ].strip ()
197
- assert len (curr_key ) == 40 or (len (curr_key ) == 18 and curr_key .startswith ('0x' ))
198
-
199
- if line_begins_with (r"Can't check signature: No public key$" , line ):
200
- if not curr_key :
201
- raise RuntimeError ("failed to detect signature being resolved" )
202
- unknown_sigs .append (SigData (curr_key , detected_name , False , '' ))
203
- detected_name = ''
204
- curr_key = None
205
-
206
- if line_begins_with (r'Good signature from (".+")(\s+)(\[.+\])$' , line ):
207
- if not curr_key :
208
- raise RuntimeError ("failed to detect signature being resolved" )
209
- name , status = parse_gpg_from_line (line )
210
-
211
- # It's safe to index output[i + 1] because if we saw a good sig, there should
212
- # always be another line
213
- trusted = (
214
- 'This key is not certified with a trusted signature' not in output [i + 1 ])
215
- good_sigs .append (SigData (curr_key , name , trusted , status ))
216
- curr_key = None
217
-
218
- if line_begins_with ("issuer " , line ):
219
- detected_name = line .split ("issuer " )[- 1 ].strip ('"' )
220
-
221
- if 'bad signature from' in line .lower ():
222
- if not curr_key :
223
- raise RuntimeError ("failed to detect signature being resolved" )
224
- name , status = parse_gpg_from_line (line )
225
- bad_sigs .append (SigData (curr_key , name , False , status ))
226
- curr_key = None
227
-
228
- # Track total signatures included
229
- if line_begins_with ('Signature made ' , line ):
193
+ return re .match (r'^(\[GNUPG:\])\s+' + patt , line )
194
+
195
+ curr_sigs = unknown_sigs
196
+ curr_sigdata = SigData ()
197
+
198
+ for line in output :
199
+ if line_begins_with (r"NEWSIG(?:\s|$)" , line ):
230
200
total_resolved_sigs += 1
201
+ if curr_sigdata :
202
+ curr_sigs .append (curr_sigdata )
203
+ curr_sigdata = SigData ()
204
+ newsig_split = line .split ()
205
+ if len (newsig_split ) == 3 :
206
+ curr_sigdata .name = newsig_split [2 ]
207
+
208
+ elif line_begins_with (r"GOODSIG(?:\s|$)" , line ):
209
+ curr_sigdata .key , curr_sigdata .name = line .split (maxsplit = 3 )[2 :4 ]
210
+ curr_sigs = good_sigs
211
+
212
+ elif line_begins_with (r"EXPKEYSIG(?:\s|$)" , line ):
213
+ curr_sigdata .key , curr_sigdata .name = line .split (maxsplit = 3 )[2 :4 ]
214
+ curr_sigs = good_sigs
215
+ curr_sigdata .status = "expired"
216
+
217
+ elif line_begins_with (r"REVKEYSIG(?:\s|$)" , line ):
218
+ curr_sigdata .key , curr_sigdata .name = line .split (maxsplit = 3 )[2 :4 ]
219
+ curr_sigs = good_sigs
220
+ curr_sigdata .status = "revoked"
221
+
222
+ elif line_begins_with (r"BADSIG(?:\s|$)" , line ):
223
+ curr_sigdata .key , curr_sigdata .name = line .split (maxsplit = 3 )[2 :4 ]
224
+ curr_sigs = bad_sigs
225
+
226
+ elif line_begins_with (r"ERRSIG(?:\s|$)" , line ):
227
+ curr_sigdata .key , _ , _ , _ , _ , _ = line .split ()[2 :8 ]
228
+ curr_sigs = unknown_sigs
229
+
230
+ elif line_begins_with (r"TRUST_(UNDEFINED|NEVER)(?:\s|$)" , line ):
231
+ curr_sigdata .trusted = False
232
+
233
+ elif line_begins_with (r"TRUST_(MARGINAL|FULLY|ULTIMATE)(?:\s|$)" , line ):
234
+ curr_sigdata .trusted = True
235
+
236
+ # The last one won't have been added, so add it now
237
+ assert curr_sigdata
238
+ curr_sigs .append (curr_sigdata )
231
239
232
240
all_found = len (good_sigs + bad_sigs + unknown_sigs )
233
241
if all_found != total_resolved_sigs :
@@ -238,19 +246,6 @@ def line_begins_with(patt: str, line: str) -> t.Optional[re.Match]:
238
246
return (good_sigs , unknown_sigs , bad_sigs )
239
247
240
248
241
- def parse_gpg_from_line (line : str ) -> t .Tuple [str , str ]:
242
- """Returns name and expiration status."""
243
- assert 'signature from' in line
244
-
245
- name_end = line .split (' from ' )[- 1 ]
246
- m = re .search (r'(?P<name>".+") \[(?P<status>\w+)\]' , name_end )
247
- assert m
248
- (name , status ) = m .groups ()
249
- name = name .strip ('"\' ' )
250
-
251
- return (name , status )
252
-
253
-
254
249
def files_are_equal (filename1 , filename2 ):
255
250
with open (filename1 , 'rb' ) as file1 :
256
251
contents1 = file1 .read ()
0 commit comments