130 lines
4.6 KiB
Python
130 lines
4.6 KiB
Python
"""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}"
|
|
)
|