diff --git a/libkeyringctl/keyring.py b/libkeyringctl/keyring.py index 5c918df..a29cc33 100644 --- a/libkeyringctl/keyring.py +++ b/libkeyringctl/keyring.py @@ -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}") diff --git a/libkeyringctl/trust.py b/libkeyringctl/trust.py index 6dcb83f..d7fa7ad 100644 --- a/libkeyringctl/trust.py +++ b/libkeyringctl/trust.py @@ -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())): diff --git a/libkeyringctl/util.py b/libkeyringctl/util.py index f3c5c2b..b43eff0 100644 --- a/libkeyringctl/util.py +++ b/libkeyringctl/util.py @@ -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. diff --git a/tests/conftest.py b/tests/conftest.py index 9043a0b..c00c306 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,9 @@ from collections import defaultdict from functools import wraps from pathlib import Path from shutil import copytree +from subprocess import PIPE +from subprocess import Popen +from tempfile import NamedTemporaryFile from tempfile import TemporaryDirectory from typing import Any from typing import Callable @@ -14,6 +17,7 @@ from typing import Set from pytest import fixture from libkeyringctl.keyring import convert_certificate +from libkeyringctl.keyring import export from libkeyringctl.keyring import simplify_user_id from libkeyringctl.sequoia import certify from libkeyringctl.sequoia import key_extract_certificate @@ -24,12 +28,15 @@ from libkeyringctl.types import Fingerprint from libkeyringctl.types import Uid from libkeyringctl.types import Username from libkeyringctl.util import cwd +from libkeyringctl.util import system test_keys: Dict[Username, List[Path]] = defaultdict(list) test_key_revocation: Dict[Username, List[Path]] = defaultdict(list) test_certificates: Dict[Username, List[Path]] = defaultdict(list) +test_certificate_uids: Dict[Username, List[List[Uid]]] = defaultdict(list) test_keyring_certificates: Dict[Username, List[Path]] = defaultdict(list) test_main_fingerprints: Set[Fingerprint] = set() +test_all_fingerprints: Set[Fingerprint] = set() @fixture(autouse=True) @@ -37,8 +44,10 @@ def reset_storage() -> None: test_keys.clear() test_key_revocation.clear() test_certificates.clear() + test_certificate_uids.clear() test_keyring_certificates.clear() test_main_fingerprints.clear() + test_all_fingerprints.clear() def create_certificate( @@ -50,16 +59,14 @@ def create_certificate( def decorator(decorated_func: Callable[..., None]) -> Callable[..., Any]: @wraps(decorated_func) def wrapper(working_dir: Path, *args: Any, **kwargs: Any) -> None: - print(username) - - key_directory = working_dir / "secret" / f"{id}" + key_directory = working_dir / "secret" / f"{username}" key_directory.mkdir(parents=True, exist_ok=True) key_file: Path = key_directory / f"{username}.asc" key_generate(uids=uids, outfile=key_file) test_keys[username].append(key_file) - certificate_directory = working_dir / "certificate" / f"{id}" + certificate_directory = working_dir / "certificate" / f"{username}" certificate_directory.mkdir(parents=True, exist_ok=True) keyring_root: Path = working_dir / "keyring" @@ -68,6 +75,7 @@ def create_certificate( key_extract_certificate(key=key_file, output=certificate_file) test_certificates[username].append(certificate_file) + test_certificate_uids[username].append(uids) key_revocation_packet = key_file.parent / f"{key_file.name}.rev" key_revocation_joined = key_file.parent / f"{key_file.name}.joined.rev" @@ -88,8 +96,10 @@ def create_certificate( copytree(src=user_dir, dst=(target_dir / user_dir.name), dirs_exist_ok=True) test_keyring_certificates[username].append(target_dir / user_dir.name / decomposed_path.name) + certificate_fingerprint: Fingerprint = Fingerprint(decomposed_path.name) if "main" == keyring_type: - test_main_fingerprints.add(Fingerprint(decomposed_path.name)) + test_main_fingerprints.add(certificate_fingerprint) + test_all_fingerprints.add(certificate_fingerprint) decorated_func(working_dir=working_dir, *args, **kwargs) @@ -169,6 +179,95 @@ def create_key_revocation( return decorator(func) +def create_signature_revocation( + issuer: Username, certified: Username, uid: Uid, func: Optional[Callable[[Any], None]] = None +) -> Callable[..., Any]: + def decorator(decorated_func: Callable[..., None]) -> Callable[..., Any]: + @wraps(decorated_func) + def wrapper(working_dir: Path, *args: Any, **kwargs: Any) -> None: + + issuer_key: Path = test_keys[issuer][0] + keyring_root: Path = working_dir / "keyring" + + keyring_certificate: Path = test_keyring_certificates[certified][0] + certified_fingerprint = keyring_certificate.name + + with NamedTemporaryFile(dir=str(working_dir), prefix=f"{certified}", suffix=".asc") as certificate: + certificate_path: Path = Path(certificate.name) + export( + working_dir=working_dir, + keyring_root=keyring_root, + sources=[keyring_certificate], + output=certificate_path, + ) + + with TemporaryDirectory(prefix="gnupg") as gnupg_home: + env = {"GNUPGHOME": gnupg_home} + + print( + system( + [ + "gpg", + "--no-auto-check-trustdb", + "--import", + f"{str(issuer_key)}", + f"{str(certificate_path)}", + ], + env=env, + ) + ) + + uid_confirmations = "" + for cert_uid in test_certificate_uids[certified][0]: + if uid == cert_uid: + uid_confirmations += "y\n" + else: + uid_confirmations += "n\n" + + commands = Popen(["echo", "-e", f"{uid_confirmations}y\n0\ny\n\ny\ny\nsave\n"], stdout=PIPE) + system( + [ + "gpg", + "--no-auto-check-trustdb", + "--command-fd", + "0", + "--expert", + "--yes", + "--batch", + "--edit-key", + f"{certified_fingerprint}", + "revsig", + "save", + ], + _stdin=commands.stdout, + env=env, + ) + + revoked_certificate = system(["gpg", "--armor", "--export", f"{certified_fingerprint}"], env=env) + certificate.truncate(0) + certificate.seek(0) + certificate.write(revoked_certificate.encode()) + certificate.flush() + + target_dir = keyring_root / "packager" + decomposed_path: Path = convert_certificate( + working_dir=working_dir, + certificate=certificate_path, + keyring_dir=target_dir, + ) + user_dir = decomposed_path.parent + (target_dir / user_dir.name).mkdir(parents=True, exist_ok=True) + copytree(src=user_dir, dst=(target_dir / user_dir.name), dirs_exist_ok=True) + + decorated_func(working_dir=working_dir, *args, **kwargs) + + return wrapper + + if not func: + return decorator + return decorator(func) + + @fixture(scope="function") def working_dir() -> Generator[Path, None, None]: with TemporaryDirectory(prefix="arch-keyringctl-test-") as tempdir: diff --git a/tests/test_sequoia.py b/tests/test_sequoia.py index ee0065c..eb90f48 100644 --- a/tests/test_sequoia.py +++ b/tests/test_sequoia.py @@ -55,26 +55,22 @@ def test_keyring_split(mkdtemp_mock: Mock, system_mock: Mock, create_subdir: boo @mark.parametrize( - "force, output", + "output", [ - (True, None), - (False, None), - (True, Path("output")), - (False, Path("output")), + None, + Path("output"), ], ) @patch("libkeyringctl.sequoia.system") -def test_keyring_merge(system_mock: Mock, force: bool, output: Optional[Path]) -> None: +def test_keyring_merge(system_mock: Mock, output: Optional[Path]) -> None: certificates = [Path("foo"), Path("bar")] system_mock.return_value = "return" - assert sequoia.keyring_merge(certificates=certificates, output=output, force=force) == "return" + assert sequoia.keyring_merge(certificates=certificates, output=output) == "return" name, args, kwargs = system_mock.mock_calls[0] for cert in certificates: assert str(cert) in args[0] - if force: - assert "--force" == args[0][1] if output: assert "--output" in args[0] and str(output) in args[0] diff --git a/tests/test_trust.py b/tests/test_trust.py index 8bceae4..e336ccd 100644 --- a/tests/test_trust.py +++ b/tests/test_trust.py @@ -1,22 +1,68 @@ from pathlib import Path +from typing import List +from unittest.mock import Mock +from unittest.mock import patch + +from pytest import mark +from pytest import raises from libkeyringctl.trust import certificate_trust +from libkeyringctl.trust import certificate_trust_from_paths +from libkeyringctl.trust import format_trust_label +from libkeyringctl.trust import trust_color +from libkeyringctl.trust import trust_icon +from libkeyringctl.types import Color +from libkeyringctl.types import Fingerprint from libkeyringctl.types import Trust from libkeyringctl.types import Uid from libkeyringctl.types import Username from .conftest import create_certificate from .conftest import create_key_revocation +from .conftest import create_signature_revocation from .conftest import create_uid_certification +from .conftest import test_all_fingerprints from .conftest import test_keyring_certificates from .conftest import test_main_fingerprints +@mark.parametrize( + "sources", + [ + ([Path("foobar")]), + ([Path("foobar"), Path("quxdoo")]), + ], +) +@patch("libkeyringctl.trust.certificate_trust") +def test_certificate_trust_from_paths( + certificate_trust_mock: Mock, + sources: List[Path], +) -> None: + certificate_trust_mock.return_value = Trust.full + for source in sources: + source.mkdir(parents=True, exist_ok=True) + cert = source / "foo.asc" + cert.touch() + + trusts = certificate_trust_from_paths( + sources=sources, main_keys=test_main_fingerprints, all_fingerprints=test_all_fingerprints + ) + for i, source in enumerate(sources): + name, args, kwargs = certificate_trust_mock.mock_calls[i] + assert kwargs["certificate"] == source + assert kwargs["main_keys"] == test_main_fingerprints + assert kwargs["all_fingerprints"] == test_all_fingerprints + fingerprint = Fingerprint(source.name) + assert Trust.full == trusts[fingerprint] + assert len(trusts) == len(sources) + + @create_certificate(username=Username("foobar"), uids=[Uid("foobar ")], keyring_type="main") def test_certificate_trust_main_key_has_full_trust(working_dir: Path, keyring_dir: Path) -> None: trust = certificate_trust( test_keyring_certificates[Username("foobar")][0], test_main_fingerprints, + test_all_fingerprints, ) assert Trust.full == trust @@ -27,16 +73,46 @@ def test_certificate_trust_main_key_revoked(working_dir: Path, keyring_dir: Path trust = certificate_trust( test_keyring_certificates[Username("foobar")][0], test_main_fingerprints, + test_all_fingerprints, ) assert Trust.revoked == trust +@create_certificate(username=Username("foobar"), uids=[Uid("foobar ")], keyring_type="main") +@create_key_revocation(username=Username("foobar"), keyring_type="main") +def test_certificate_trust_main_key_revoked_unknown_fingerprint_lookup(working_dir: Path, keyring_dir: Path) -> None: + fingerprint = Fingerprint(test_keyring_certificates[Username("foobar")][0].name) + revocation = list((keyring_dir / "main" / "foobar" / fingerprint / "revocation").iterdir())[0] + revocation.rename(revocation.parent / "12341234.asc") + with raises(Exception): + certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + {Fingerprint("12341234")}, + ) + + +@create_certificate(username=Username("foobar"), uids=[Uid("foobar ")], keyring_type="main") +@create_key_revocation(username=Username("foobar"), keyring_type="main") +def test_certificate_trust_main_key_revoked_unknown_self_revocation(working_dir: Path, keyring_dir: Path) -> None: + fingerprint = Fingerprint(test_keyring_certificates[Username("foobar")][0].name) + revocation = list((keyring_dir / "main" / "foobar" / fingerprint / "revocation").iterdir())[0] + revocation.rename(revocation.parent / "12341234.asc") + with raises(Exception): + certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + set(), + ) + + @create_certificate(username=Username("main"), uids=[Uid("main ")]) @create_certificate(username=Username("foobar"), uids=[Uid("foobar ")]) def test_certificate_trust_no_signature_is_unknown(working_dir: Path, keyring_dir: Path) -> None: trust = certificate_trust( test_keyring_certificates[Username("foobar")][0], test_main_fingerprints, + test_all_fingerprints, ) assert Trust.unknown == trust @@ -48,6 +124,7 @@ def test_certificate_trust_one_signature_is_marginal(working_dir: Path, keyring_ trust = certificate_trust( test_keyring_certificates[Username("foobar")][0], test_main_fingerprints, + test_all_fingerprints, ) assert Trust.marginal == trust @@ -60,6 +137,7 @@ def test_certificate_trust_one_none_main_signature_gives_no_trust(working_dir: P trust = certificate_trust( test_keyring_certificates[Username("foobar")][0], test_main_fingerprints, + test_all_fingerprints, ) assert Trust.unknown == trust @@ -75,16 +153,176 @@ def test_certificate_trust_three_main_signature_gives_full_trust(working_dir: Pa trust = certificate_trust( test_keyring_certificates[Username("foobar")][0], test_main_fingerprints, + test_all_fingerprints, ) assert Trust.full == trust @create_certificate(username=Username("main"), uids=[Uid("main ")], keyring_type="main") @create_certificate(username=Username("foobar"), uids=[Uid("foobar ")]) -@create_key_revocation(username=Username("foobar"), keyring_type="packager") +@create_key_revocation(username=Username("foobar")) def test_certificate_trust_revoked_key(working_dir: Path, keyring_dir: Path) -> None: trust = certificate_trust( test_keyring_certificates[Username("foobar")][0], test_main_fingerprints, + test_all_fingerprints, ) assert Trust.revoked == trust + + +@create_certificate(username=Username("main"), uids=[Uid("main ")], keyring_type="main") +@create_certificate(username=Username("foobar"), uids=[Uid("foobar ")]) +@create_uid_certification(issuer=Username("main"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_signature_revocation(issuer=Username("main"), certified=Username("foobar"), uid=Uid("foobar ")) +def test_certificate_trust_one_signature_revoked(working_dir: Path, keyring_dir: Path) -> None: + trust = certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + test_all_fingerprints, + ) + assert Trust.revoked == trust + + +@create_certificate(username=Username("main1"), uids=[Uid("main1 ")], keyring_type="main") +@create_certificate(username=Username("main2"), uids=[Uid("main2 ")], keyring_type="main") +@create_certificate(username=Username("main3"), uids=[Uid("main3 ")], keyring_type="main") +@create_certificate(username=Username("foobar"), uids=[Uid("foobar ")]) +@create_uid_certification(issuer=Username("main1"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_uid_certification(issuer=Username("main2"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_uid_certification(issuer=Username("main3"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_signature_revocation(issuer=Username("main3"), certified=Username("foobar"), uid=Uid("foobar ")) +def test_certificate_trust_revoked_if_below_full(working_dir: Path, keyring_dir: Path) -> None: + trust = certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + test_all_fingerprints, + ) + assert Trust.revoked == trust + + +@create_certificate(username=Username("main1"), uids=[Uid("main1 ")], keyring_type="main") +@create_certificate(username=Username("main2"), uids=[Uid("main2 ")], keyring_type="main") +@create_certificate(username=Username("main3"), uids=[Uid("main3 ")], keyring_type="main") +@create_certificate(username=Username("main4"), uids=[Uid("main4 ")], keyring_type="main") +@create_certificate(username=Username("foobar"), uids=[Uid("foobar ")]) +@create_uid_certification(issuer=Username("main1"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_uid_certification(issuer=Username("main2"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_uid_certification(issuer=Username("main3"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_uid_certification(issuer=Username("main4"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_signature_revocation(issuer=Username("main4"), certified=Username("foobar"), uid=Uid("foobar ")) +def test_certificate_trust_full_remains_if_enough_sigs_present(working_dir: Path, keyring_dir: Path) -> None: + trust = certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + test_all_fingerprints, + ) + assert Trust.full == trust + + +@create_certificate(username=Username("main1"), uids=[Uid("main1 ")], keyring_type="main") +@create_certificate(username=Username("main2"), uids=[Uid("main2 ")], keyring_type="main") +@create_certificate(username=Username("main3"), uids=[Uid("main3 ")], keyring_type="main") +@create_certificate(username=Username("foobar"), uids=[Uid("foobar "), Uid("old ")]) +@create_uid_certification(issuer=Username("main1"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_uid_certification(issuer=Username("main2"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_signature_revocation(issuer=Username("foobar"), certified=Username("foobar"), uid=Uid("old ")) +def test_certificate_trust_not_revoked_if_only_one_uid_is_self_revoked(working_dir: Path, keyring_dir: Path) -> None: + trust = certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + test_all_fingerprints, + ) + assert Trust.marginal == trust + + +@create_certificate(username=Username("foobar"), uids=[Uid("foobar "), Uid("old ")]) +@create_signature_revocation(issuer=Username("foobar"), certified=Username("foobar"), uid=Uid("old ")) +def test_certificate_trust_unknown_if_only_contains_self_revoked(working_dir: Path, keyring_dir: Path) -> None: + trust = certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + test_all_fingerprints, + ) + assert Trust.unknown == trust + + +@create_certificate(username=Username("main1"), uids=[Uid("main1 ")], keyring_type="main") +@create_certificate(username=Username("foobar"), uids=[Uid("foobar "), Uid("old ")]) +@create_uid_certification(issuer=Username("main1"), certified=Username("foobar"), uid=Uid("foobar ")) +def test_certificate_trust_missing_signature_fingerprint_lookup(working_dir: Path, keyring_dir: Path) -> None: + with raises(Exception): + certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + set(), + ) + + +@create_certificate(username=Username("foobar"), uids=[Uid("old ")]) +@create_signature_revocation(issuer=Username("foobar"), certified=Username("foobar"), uid=Uid("old ")) +def test_certificate_trust_missing_revocation_fingerprint_lookup(working_dir: Path, keyring_dir: Path) -> None: + with raises(Exception): + certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + set(), + ) + + +@create_certificate(username=Username("main1"), uids=[Uid("main1 ")], keyring_type="main") +@create_certificate(username=Username("foobar"), uids=[Uid("foobar ")]) +@create_certificate(username=Username("packager"), uids=[Uid("packager ")]) +@create_uid_certification(issuer=Username("main1"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_uid_certification(issuer=Username("packager"), certified=Username("foobar"), uid=Uid("foobar ")) +@create_signature_revocation(issuer=Username("packager"), certified=Username("foobar"), uid=Uid("foobar ")) +def test_certificate_trust_ignore_3rd_party_revocation(working_dir: Path, keyring_dir: Path) -> None: + trust = certificate_trust( + test_keyring_certificates[Username("foobar")][0], + test_main_fingerprints, + test_all_fingerprints, + ) + assert Trust.marginal == trust + + +@mark.parametrize( + "trust, result", + [ + (Trust.revoked, Color.RED), + (Trust.full, Color.GREEN), + (Trust.marginal, Color.YELLOW), + (Trust.unknown, Color.YELLOW), + ], +) +def test_trust_color(trust: Trust, result: Color) -> None: + assert trust_color(trust) == result + + +@mark.parametrize( + "trust, result", + [ + (Trust.revoked, "✗"), + (Trust.full, "✓"), + (Trust.marginal, "~"), + (Trust.unknown, "~"), + (None, "?"), + ], +) +def test_trust_icon(trust: Trust, result: str) -> None: + assert trust_icon(trust) == result + + +@mark.parametrize( + "trust", + [ + Trust.revoked, + Trust.full, + Trust.marginal, + Trust.unknown, + ], +) +@patch("libkeyringctl.trust.trust_icon") +@patch("libkeyringctl.trust.trust_color") +def test_format_trust_label(trust_color_mock: Mock, trust_icon_mock: Mock, trust: Trust) -> None: + trust_icon_mock.return_value = "ICON" + trust_color_mock.return_value = Color.GREEN + assert f"{Color.GREEN.value}ICON {trust.name}{Color.RST.value}" == format_trust_label(trust) diff --git a/tests/test_util.py b/tests/test_util.py index ff7c200..14df128 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,6 +3,7 @@ from os import getcwd from pathlib import Path from tempfile import NamedTemporaryFile from tempfile import TemporaryDirectory +from typing import Dict from typing import List from unittest.mock import Mock from unittest.mock import patch @@ -12,6 +13,7 @@ from pytest import raises from libkeyringctl import util from libkeyringctl.types import Fingerprint +from libkeyringctl.types import Trust def test_cwd() -> None: @@ -93,7 +95,7 @@ def test_get_cert_paths() -> None: cert2 = cert_dir2 / "cert2.asc" cert2.touch() - assert util.get_cert_paths(paths=[tmp_dir]) == set([cert_dir1, cert_dir2]) + assert util.get_cert_paths(paths=[tmp_dir]) == {cert_dir1, cert_dir2} def test_get_parent_cert_paths() -> None: @@ -112,7 +114,7 @@ def test_get_parent_cert_paths() -> None: cert2 = cert_dir2 / "cert2.asc" cert2.touch() - assert util.get_parent_cert_paths(paths=[cert1, cert2]) == set([cert_dir1]) + assert util.get_parent_cert_paths(paths=[cert1, cert2]) == {cert_dir1} @mark.parametrize( @@ -132,3 +134,50 @@ def test_get_parent_cert_paths() -> None: ) def test_contains_fingerprint(fingerprints: List[Fingerprint], fingerprint: Fingerprint, result: bool) -> None: assert util.contains_fingerprint(fingerprints=fingerprints, fingerprint=fingerprint) is result + + +@mark.parametrize( + "fingerprints, fingerprint, result", + [ + ([Fingerprint("blahfoo"), Fingerprint("blahbar")], Fingerprint("foo"), Fingerprint("blahfoo")), + ([Fingerprint("blahfoo"), Fingerprint("blahbar")], Fingerprint("blahfoo"), Fingerprint("blahfoo")), + ( + [Fingerprint("bazfoo"), Fingerprint("bazbar")], + Fingerprint("baz"), + None, + ), + ], +) +def test_get_fingerprint_from_partial(fingerprints: List[Fingerprint], fingerprint: Fingerprint, result: bool) -> None: + assert util.get_fingerprint_from_partial(fingerprints=fingerprints, fingerprint=fingerprint) is result + + +@mark.parametrize( + "trusts, trust, result", + [ + ( + {Fingerprint("foo"): Trust.full, Fingerprint("bar"): Trust.marginal}, + Trust.full, + [Fingerprint("foo")], + ), + ( + {Fingerprint("foo"): Trust.full, Fingerprint("bar"): Trust.full}, + Trust.full, + [Fingerprint("foo"), Fingerprint("bar")], + ), + ( + {Fingerprint("foo"): Trust.full, Fingerprint("bar"): Trust.marginal}, + Trust.unknown, + [], + ), + ( + {}, + Trust.unknown, + [], + ), + ], +) +def test_filter_fingerprints_by_trust( + trusts: Dict[Fingerprint, Trust], trust: Trust, result: List[Fingerprint] +) -> None: + assert util.filter_fingerprints_by_trust(trusts=trusts, trust=trust) == result