"""Utilities for verifying files using gpg.""" from re import compile from pathlib import Path from subprocess import run, PIPE, STDOUT import gpg.exceptions as ex def assert_detached_signature_is_valid( path_to_input_file, path_to_signature_file ): """Verifies the input file using the specified detached gpg signature. The invoking user's local gpg key store is used for verification, the public key used to create the signature must be present in the invoking user's local gpg key store. Parameters ---------- path_to_input_file : str or pathlike object Path to the file which should be verified. path_to_signature_file : str or pathlike object Path to the file containing a detached gpg signature of the input file. Raises ------ FileNotFoundError If either of the input files do not exist. gpg.exceptions.MissingLocalKeyError If a key referenced by the signature file could not be found in the invoking user's local key store. gpg.exceptions.VerificationFailedError If the gpg verification detects a bad signature. """ if '~' in str(path_to_input_file): path_to_input_file = Path(path_to_input_file).expanduser() path_to_input_file = Path(path_to_input_file).resolve() if '~' in str(path_to_signature_file): path_to_signature_file = Path(path_to_signature_file).expanduser() path_to_signature_file = Path(path_to_signature_file).resolve() if not path_to_input_file.is_file(): raise FileNotFoundError( f"No such file: '{path_to_input_file}'." ) if not path_to_signature_file.is_file(): raise FileNotFoundError( f"No such file: '{path_to_signature_file}'." ) # execute a gpg verification as a shell command, redirecting stderr to # stdout process_result = run( [ "gpg", "--verify", path_to_signature_file, path_to_input_file, ], stdout=PIPE, stderr=STDOUT, text=True, ) output_lines = process_result.stdout.split("\n") if len(output_lines) < 3: raise ex.UnexpectedOutputException( f"Unexpected output during gpg verification:\n" f"{process_result.stdout}" ) if process_result.returncode == 2: # a missing local key causes return code 2 # and the following output on the third line: missing_key_regex = compile( r"^gpg: Can't check signature: No public key$" ) # an invalid detached signature file causes return code 2 # and the following output on the first line: invalid_signature_regex = compile( r"^gpg: no valid OpenPGP data found.$" ) if missing_key_regex.match(output_lines[2]): raise ex.MissingLocalKeyError( "Failed to verify gpg signature: no matching local key." ) elif invalid_signature_regex.match(output_lines[0]): raise ex.InvalidSignatureError( "Invalid signature file." ) else: raise ex.UnexpectedOutputException( f"Unexpected output during gpg verification:\n" f"{process_result.stdout}" ) elif process_result.returncode == 1: # failed verification causes return code 1 # and the following output on line 3: verification_failed_regex = compile( r"^gpg: BAD signature from .*$" ) if not verification_failed_regex.match(output_lines[2]): raise ex.UnexpectedOutputException( f"Unexpected output during gpg verification:\n" f"{process_result.stdout}" ) else: raise ex.VerificationFailedError( "gpg signature verification failed: BAD SIGNATURE!" ) elif process_result.returncode == 0: # successful verification causes return code 0 # and the following output on line 3: verification_successful_regex = compile( r"^gpg: (Good signature|Bonne signature) (from|de) .*$" ) signature_found = any(verification_successful_regex.match(line) for line in output_lines) if not signature_found: raise ex.UnexpectedOutputException( f"Unexpected output during gpg verification:\n" f"{process_result.stdout}" ) else: raise ex.UnexpectedOutputException( f"Unexpected return code during gpg verification:\n" f"{process_result.returncode}" f"{process_result.stdout}" )