4242import urllib .request
4343import enum
4444from hashlib import sha256
45- from pathlib import PurePath
45+ from pathlib import PurePath , Path
4646
4747# The primary host; this will fail if we can't retrieve files from here.
4848HOST1 = "https://bitcoincore.org"
@@ -141,14 +141,19 @@ def verify_with_gpg(
141141 signature_filename ,
142142 output_filename : t .Optional [str ] = None
143143) -> 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 ]
147148
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
152157
153158
154159def remove_files (filenames ):
@@ -158,11 +163,14 @@ def remove_files(filenames):
158163
159164class SigData :
160165 """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
166174
167175 def __repr__ (self ):
168176 return (
@@ -174,60 +182,60 @@ def parse_gpg_result(
174182 output : t .List [str ]
175183) -> t .Tuple [t .List [SigData ], t .List [SigData ], t .List [SigData ]]:
176184 """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 ] = []
180188 total_resolved_sigs = 0
181- curr_key = None
182189
183190 # Ensure that all lines we match on include a prefix that prevents malicious input
184191 # from fooling the parser.
185192 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 ):
230200 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 )
231239
232240 all_found = len (good_sigs + bad_sigs + unknown_sigs )
233241 if all_found != total_resolved_sigs :
@@ -238,19 +246,6 @@ def line_begins_with(patt: str, line: str) -> t.Optional[re.Match]:
238246 return (good_sigs , unknown_sigs , bad_sigs )
239247
240248
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-
254249def files_are_equal (filename1 , filename2 ):
255250 with open (filename1 , 'rb' ) as file1 :
256251 contents1 = file1 .read ()
0 commit comments