chore(keyringctl): increase test coverage and fix trust expectations

This commit is contained in:
Levente Polyak
2021-11-04 19:26:11 +01:00
parent 7513e71b3f
commit cd585f4be2
7 changed files with 481 additions and 41 deletions

View File

@ -196,7 +196,9 @@ def convert_certificate( # noqa: ignore=C901
if signature_type == "CertificationRevocation":
revocations[current_packet_uid][issuer].append(packet)
elif signature_type.endswith("Certification"):
if fingerprint_filter is not None and any([fp.endswith(issuer) for fp in fingerprint_filter]):
# TODO: extend fp filter to all certifications
# TODO: use contains_fingerprint
if fingerprint_filter is None or any([fp.endswith(issuer) for fp in fingerprint_filter]):
debug(f"The certification by issuer {issuer} is appended as it is found in the filter.")
certifications[current_packet_uid][issuer].append(packet)
else:
@ -587,7 +589,7 @@ def convert(
return target_dir
def export_ownertrust(certs: List[Path], output: Path) -> List[Fingerprint]:
def export_ownertrust(certs: List[Path], keyring_root: Path, output: Path) -> List[Fingerprint]:
"""Export ownertrust from a set of keys and return the trusted and revoked fingerprints
The output file format is compatible with `gpg --import-ownertrust` and lists the main fingerprint ID of all
@ -598,6 +600,7 @@ def export_ownertrust(certs: List[Path], output: Path) -> List[Fingerprint]:
Parameters
----------
certs: The certificates to trust
keyring_root: The keyring root directory to get all accepted fingerprints from
output: The file path to write to
Returns
@ -605,7 +608,11 @@ def export_ownertrust(certs: List[Path], output: Path) -> List[Fingerprint]:
List of ownertrust fingerprints
"""
main_trusts = certificate_trust_from_paths(sources=certs, main_keys=get_fingerprints_from_paths(sources=certs))
main_trusts = certificate_trust_from_paths(
sources=certs,
main_keys=get_fingerprints_from_paths(sources=certs),
all_fingerprints=get_fingerprints_from_paths([keyring_root]),
)
trusted_certs: List[Fingerprint] = filter_fingerprints_by_trust(main_trusts, Trust.full)
with open(file=output, mode="w") as trusted_certs_file:
@ -616,7 +623,7 @@ def export_ownertrust(certs: List[Path], output: Path) -> List[Fingerprint]:
return trusted_certs
def export_revoked(certs: List[Path], main_keys: Set[Fingerprint], output: Path) -> None:
def export_revoked(certs: List[Path], keyring_root: Path, main_keys: Set[Fingerprint], output: Path) -> None:
"""Export the PGP revoked status from a set of keys
The output file contains the fingerprints of all self-revoked keys and all keys for which at least two revocations
@ -627,11 +634,16 @@ def export_revoked(certs: List[Path], main_keys: Set[Fingerprint], output: Path)
Parameters
----------
certs: A list of directories with keys to check for their revocation status
keyring_root: The keyring root directory to get all accepted fingerprints from
main_keys: A list of strings representing the fingerprints of (current and/or revoked) main keys
output: The file path to write to
"""
certificate_trusts = certificate_trust_from_paths(sources=certs, main_keys=main_keys)
certificate_trusts = certificate_trust_from_paths(
sources=certs,
main_keys=main_keys,
all_fingerprints=get_fingerprints_from_paths([keyring_root]),
)
revoked_certs: List[Fingerprint] = filter_fingerprints_by_trust(certificate_trusts, Trust.revoked)
with open(file=output, mode="w") as revoked_certs_file:
@ -839,10 +851,12 @@ def build(
trusted_main_keys = export_ownertrust(
certs=[keyring_root / "main"],
keyring_root=keyring_root,
output=target_dir / "archlinux-trusted",
)
export_revoked(
certs=[keyring_root],
keyring_root=keyring_root,
main_keys=set(trusted_main_keys),
output=target_dir / "archlinux-revoked",
)
@ -877,7 +891,9 @@ def list_keyring(keyring_root: Path, sources: Optional[List[Path]] = None, main_
for certificate in sources:
username: Username = Username(certificate.parent.name)
trust = certificate_trust(
certificate=certificate, main_keys=get_fingerprints_from_paths([keyring_root / "main"])
certificate=certificate,
main_keys=get_fingerprints_from_paths([keyring_root / "main"]),
all_fingerprints=get_fingerprints_from_paths([keyring_root]),
)
trust_label = format_trust_label(trust=trust)
print(f"{username:<{username_length}} {certificate.name} {trust_label}")

View File

@ -4,6 +4,7 @@ from logging import debug
from pathlib import Path
from typing import Dict
from typing import Iterable
from typing import Optional
from typing import Set
from .types import Color
@ -12,9 +13,12 @@ from .types import Trust
from .types import Uid
from .util import contains_fingerprint
from .util import get_cert_paths
from .util import get_fingerprint_from_partial
def certificate_trust_from_paths(sources: Iterable[Path], main_keys: Set[Fingerprint]) -> Dict[Fingerprint, Trust]:
def certificate_trust_from_paths(
sources: Iterable[Path], main_keys: Set[Fingerprint], all_fingerprints: Set[Fingerprint]
) -> Dict[Fingerprint, Trust]:
"""Get the trust status of all certificates in a list of paths given by main keys.
Uses `get_get_certificate_trust` to determine the trust status.
@ -23,6 +27,7 @@ def certificate_trust_from_paths(sources: Iterable[Path], main_keys: Set[Fingerp
----------
sources: Certificates to acquire the trust status from
main_keys: Fingerprints of trusted keys used to calculate the trust of the certificates from sources
all_fingerprints: Fingerprints of all certificates, packager and main, to look up key-ids to full fingerprints
Returns
-------
@ -34,11 +39,15 @@ def certificate_trust_from_paths(sources: Iterable[Path], main_keys: Set[Fingerp
for certificate in sorted(sources):
fingerprint = Fingerprint(certificate.name)
certificate_trusts[fingerprint] = certificate_trust(certificate=certificate, main_keys=main_keys)
certificate_trusts[fingerprint] = certificate_trust(
certificate=certificate, main_keys=main_keys, all_fingerprints=all_fingerprints
)
return certificate_trusts
def certificate_trust(certificate: Path, main_keys: Set[Fingerprint]) -> Trust: # noqa: ignore=C901
def certificate_trust( # noqa: ignore=C901
certificate: Path, main_keys: Set[Fingerprint], all_fingerprints: Set[Fingerprint]
) -> Trust:
"""Get the trust status of a certificates given by main keys.
main certificates are:
@ -67,6 +76,7 @@ def certificate_trust(certificate: Path, main_keys: Set[Fingerprint]) -> Trust:
----------
certificate: Certificate to acquire the trust status from
main_keys: Fingerprints of trusted keys used to calculate the trust of the certificates from sources
all_fingerprints: Fingerprints of all certificates, packager and main, to look up key-ids to full fingerprints
Returns
-------
@ -78,10 +88,13 @@ def certificate_trust(certificate: Path, main_keys: Set[Fingerprint]) -> Trust:
revocations: Set[Fingerprint] = set()
# TODO: what about direct key revocations/signatures?
for revocation in certificate.glob("revocation/*.asc"):
issuer: Fingerprint = Fingerprint(revocation.stem)
if fingerprint.endswith(issuer):
debug(f"Revoking {fingerprint} due to self-revocation")
revocations.add(fingerprint)
issuer: Optional[Fingerprint] = get_fingerprint_from_partial(all_fingerprints, Fingerprint(revocation.stem))
if not issuer:
raise Exception(f"Unknown issuer: {issuer}")
if not fingerprint.endswith(issuer):
raise Exception(f"Wrong root revocation issuer: {issuer}, expected: {fingerprint}")
debug(f"Revoking {fingerprint} due to self-revocation")
revocations.add(fingerprint)
if revocations:
return Trust.revoked
@ -92,26 +105,28 @@ def certificate_trust(certificate: Path, main_keys: Set[Fingerprint]) -> Trust:
return Trust.full
uid_trust: Dict[Uid, Trust] = {}
self_revoked_uids: Set[Uid] = set()
uids = certificate / "uid"
for uid_path in uids.iterdir():
uid: Uid = Uid(uid_path.name)
# TODO: convert key-id to fingerprint otherwise it may contain duplicates
revocations = set()
self_revoked = False
for revocation in uid_path.glob("revocation/*.asc"):
issuer = Fingerprint(revocation.stem)
issuer = get_fingerprint_from_partial(all_fingerprints, Fingerprint(revocation.stem))
if not issuer:
raise Exception(f"Unknown issuer: {issuer}")
# self revocation
if fingerprint.endswith(issuer):
self_revoked = True
self_revoked_uids.add(uid)
# main key revocation
elif contains_fingerprint(fingerprints=main_keys, fingerprint=issuer):
revocations.add(issuer)
# TODO: convert key-id to fingerprint otherwise it may contain duplicates
certifications: Set[Fingerprint] = set()
for certification in uid_path.glob("certification/*.asc"):
issuer = Fingerprint(certification.stem)
issuer = get_fingerprint_from_partial(all_fingerprints, Fingerprint(certification.stem))
if not issuer:
raise Exception(f"Unknown issuer: {issuer}")
# only take main key certifications into account
if not contains_fingerprint(fingerprints=main_keys, fingerprint=issuer):
continue
@ -121,7 +136,7 @@ def certificate_trust(certificate: Path, main_keys: Set[Fingerprint]) -> Trust:
certifications.add(issuer)
# self revoked uid
if self_revoked:
if uid in self_revoked_uids:
debug(f"Certificate {fingerprint} with uid {uid} is self-revoked")
uid_trust[uid] = Trust.revoked
continue
@ -152,8 +167,7 @@ def certificate_trust(certificate: Path, main_keys: Set[Fingerprint]) -> Trust:
if any(map(lambda t: Trust.full == t, uid_trust.values())):
trust = Trust.full
# no uid has full trust but at least one is revoked
# TODO: only revoked if it contains main key revocations, not just self-revocation
elif any(map(lambda t: Trust.revoked == t, uid_trust.values())):
elif any(map(lambda e: Trust.revoked == e[1] and e[0] not in self_revoked_uids, uid_trust.items())):
trust = Trust.revoked
# no uid has full trust or is revoked
elif any(map(lambda t: Trust.marginal == t, uid_trust.values())):

View File

@ -86,7 +86,12 @@ def natural_sort_path(_list: Iterable[Path]) -> Iterable[Path]:
return sorted(_list, key=alphanum_key)
def system(cmd: List[str], _stdin: Optional[IO[AnyStr]] = None, exit_on_error: bool = False) -> str:
def system(
cmd: List[str],
_stdin: Optional[IO[AnyStr]] = None,
exit_on_error: bool = False,
env: Optional[Dict[str, str]] = None,
) -> str:
"""Execute a command using check_output
Parameters
@ -94,6 +99,7 @@ def system(cmd: List[str], _stdin: Optional[IO[AnyStr]] = None, exit_on_error: b
cmd: A list of strings to be fed to check_output
_stdin: input fd used for the spawned process
exit_on_error: Whether to exit the script when encountering an error (defaults to False)
env: Optional environment vars for the shell invocation
Raises
------
@ -103,9 +109,11 @@ def system(cmd: List[str], _stdin: Optional[IO[AnyStr]] = None, exit_on_error: b
-------
The output of cmd
"""
if not env:
env = {}
try:
return check_output(cmd, stderr=STDOUT, stdin=_stdin).decode()
return check_output(cmd, stderr=STDOUT, stdin=_stdin, env=env).decode()
except CalledProcessError as e:
stderr.buffer.write(bytes(e.stdout, encoding="utf8"))
print_stack()
@ -215,6 +223,26 @@ def contains_fingerprint(fingerprints: Iterable[Fingerprint], fingerprint: Finge
return any(filter(lambda e: str(e).endswith(fingerprint), fingerprints))
def get_fingerprint_from_partial(
fingerprints: Iterable[Fingerprint], fingerprint: Fingerprint
) -> Optional[Fingerprint]:
"""Returns the full fingerprint looked up from a partial fingerprint like a key-id
Parameters
----------
fingerprints: Iteratable structure of fingerprints that should be searched
fingerprint: Partial fingerprint to search for
Returns
-------
The full fingerprint or None
"""
for fingerprint in filter(lambda e: str(e).endswith(fingerprint), fingerprints):
return fingerprint
return None
def filter_fingerprints_by_trust(trusts: Dict[Fingerprint, Trust], trust: Trust) -> List[Fingerprint]:
"""Filters a dict of Fingerprint to Trust by a passed Trust parameter and returns the matching fingerprints.