chore(keyringctl): increase test coverage and fix trust expectations
This commit is contained in:
@ -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}")
|
||||
|
@ -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())):
|
||||
|
@ -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.
|
||||
|
||||
|
Reference in New Issue
Block a user