0
gpg/__init__.py
Normal file
0
gpg/__init__.py
Normal file
37
gpg/exceptions.py
Normal file
37
gpg/exceptions.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Exceptions which may be raised during execution of gpg wrapper functions."""
|
||||
|
||||
|
||||
class GpgProgrammingException(Exception):
|
||||
"""A general exception indicating a programming error."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GpgRuntimeError(RuntimeError):
|
||||
"""A general exception indicating a runtime error."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnexpectedOutputException(GpgProgrammingException):
|
||||
"""Raised when a gpg subprocess produces unexpected output."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingLocalKeyError(GpgRuntimeError):
|
||||
"""Raised when an expected key is missing from the local keystore."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSignatureError(GpgRuntimeError):
|
||||
"""Raised when an invalid gpg signature is encountered."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VerificationFailedError(GpgRuntimeError):
|
||||
"""Raised when a gpg verfication encounters a bad signature."""
|
||||
|
||||
pass
|
||||
112
gpg/keystore.py
Normal file
112
gpg/keystore.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Utilities for searching and importing GPG keys locally."""
|
||||
|
||||
from subprocess import run, STDOUT, PIPE
|
||||
from re import compile
|
||||
|
||||
|
||||
_DEBIAN_KEY_SERVER_HOSTNAME = "keyring.debian.org"
|
||||
_DEBIAN_CD_SIGNING_KEY_ID = "DA87E80D6294BE9B"
|
||||
|
||||
|
||||
def import_debian_signing_key():
|
||||
"""Imports the public debian CD signing key using gpg.
|
||||
|
||||
The key is imported from keyring.debian.org into the invoking user's
|
||||
GPG public key store using a shell command.
|
||||
"""
|
||||
|
||||
# execute a gpg key import as a shell command, redirecting stderr to stdout
|
||||
process_result = run(
|
||||
[
|
||||
"gpg", "--keyserver", _DEBIAN_KEY_SERVER_HOSTNAME,
|
||||
"--recv-key", _DEBIAN_CD_SIGNING_KEY_ID
|
||||
],
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# check shell return code
|
||||
if process_result.returncode != 0:
|
||||
if process_result.stdout:
|
||||
raise RuntimeError(
|
||||
f"Failed to import key using gpg:\n{process_result.stdout}"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("Failed to import key using gpg.")
|
||||
|
||||
# check shell output:
|
||||
# the first line of stdout should look like this
|
||||
expected_first_line = str(
|
||||
f"gpg: key {_DEBIAN_CD_SIGNING_KEY_ID}: public key "
|
||||
f"\"Debian CD signing key <debian-cd@lists.debian.org>\""
|
||||
f" imported"
|
||||
)
|
||||
|
||||
if not process_result.stdout.split("\n")[0] == expected_first_line:
|
||||
raise RuntimeError(
|
||||
f"Unexpected output while importing PGP public key:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
|
||||
|
||||
def debian_signing_key_is_imported():
|
||||
"""Checks whether the debian PGP signing key exists in the local key store.
|
||||
|
||||
The invoking user's GPG key store is checked using a shell command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
True : bool
|
||||
If the public PGP debian cd signing key exists in the invoking user's
|
||||
GPG key store.
|
||||
False : bool
|
||||
If the public PGP debian cd signing key does not exist in the invoking
|
||||
user's GPG key store.
|
||||
"""
|
||||
|
||||
# execute a local gpg key lookup as a shell command, redirecting stderr to
|
||||
# stdout
|
||||
# NOTE: this command returns 0 even if the key is not present
|
||||
process_result = run(
|
||||
["gpg", "--locate-keys", _DEBIAN_CD_SIGNING_KEY_ID],
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# check shell return code
|
||||
if process_result.returncode != 0:
|
||||
raise RuntimeError("Failed to search local keys using gpg.")
|
||||
|
||||
# no shell output means that the key does not exist locally
|
||||
if not process_result.stdout:
|
||||
return False
|
||||
|
||||
# verify existing key shell output using regex:
|
||||
# it should contain six lines in the following format
|
||||
expected_output_lines_regexes = [
|
||||
compile(r"^pub .*$"),
|
||||
compile(r"^ *[0-9A-F]{40}$"),
|
||||
compile(r"^uid .*$"),
|
||||
compile(r"^sub .*$"),
|
||||
compile(r"^$"),
|
||||
compile(r"^$"),
|
||||
]
|
||||
|
||||
actual_output_lines = process_result.stdout.split("\n")
|
||||
if not len(actual_output_lines) == len(expected_output_lines_regexes):
|
||||
raise RuntimeError(
|
||||
f"Unexpected line count in shell output while performing local"
|
||||
f"GPG key lookup:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
for i in range(4):
|
||||
if not expected_output_lines_regexes[i].match(actual_output_lines[i]):
|
||||
raise RuntimeError(
|
||||
f"Unexpected shell output format while performing local"
|
||||
f"GPG key lookup:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
|
||||
return True
|
||||
128
gpg/verify.py
Normal file
128
gpg/verify.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""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 from .*$"
|
||||
)
|
||||
if not verification_successful_regex.match(output_lines[2]):
|
||||
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}"
|
||||
)
|
||||
Reference in New Issue
Block a user