fix(keyringctl): prioritize latest certification per issuer
When importing a non reduced keyring the certifications were not deterministic for keys that have multiple certifications per issuer. This was for example the case for self certifications to extend the expiry time. Before this commit a random certification could remain the final one which would lead to a non up to date keyring and a potentially expired key.
This commit is contained in:
parent
32469720f8
commit
f90e860d16
87
keyringctl
87
keyringctl
@ -7,6 +7,8 @@ from collections import defaultdict
|
|||||||
from collections.abc import Iterable
|
from collections.abc import Iterable
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import reduce
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from logging import DEBUG
|
from logging import DEBUG
|
||||||
from logging import basicConfig
|
from logging import basicConfig
|
||||||
@ -242,18 +244,20 @@ def convert_certificate( # noqa: ignore=C901
|
|||||||
# root packets
|
# root packets
|
||||||
certificate_fingerprint: Optional[Fingerprint] = None
|
certificate_fingerprint: Optional[Fingerprint] = None
|
||||||
pubkey: Optional[Path] = None
|
pubkey: Optional[Path] = None
|
||||||
|
# TODO: direct key certifications are not yet selecting the latest sig, owner may have multiple
|
||||||
|
# TODO: direct key certifications are not yet single packet per file
|
||||||
direct_sigs: Dict[Fingerprint, List[Path]] = defaultdict(list)
|
direct_sigs: Dict[Fingerprint, List[Path]] = defaultdict(list)
|
||||||
direct_revocations: Dict[Fingerprint, List[Path]] = defaultdict(list)
|
direct_revocations: Dict[Fingerprint, List[Path]] = defaultdict(list)
|
||||||
|
|
||||||
# subkey packets
|
# subkey packets
|
||||||
subkeys: Dict[Fingerprint, Path] = {}
|
subkeys: Dict[Fingerprint, Path] = {}
|
||||||
subkey_bindings: Dict[Fingerprint, Path] = {}
|
subkey_bindings: Dict[Fingerprint, List[Path]] = defaultdict(list)
|
||||||
subkey_revocations: Dict[Fingerprint, Path] = {}
|
subkey_revocations: Dict[Fingerprint, List[Path]] = defaultdict(list)
|
||||||
|
|
||||||
# uid packets
|
# uid packets
|
||||||
uids: Dict[Uid, Path] = {}
|
uids: Dict[Uid, Path] = {}
|
||||||
certifications: Dict[Uid, List[Path]] = defaultdict(list)
|
certifications: Dict[Uid, Dict[Fingerprint, List[Path]]] = defaultdict(lambda: defaultdict(list))
|
||||||
revocations: Dict[Uid, List[Path]] = defaultdict(list)
|
revocations: Dict[Uid, Dict[Fingerprint, List[Path]]] = defaultdict(lambda: defaultdict(list))
|
||||||
|
|
||||||
# intermediate variables
|
# intermediate variables
|
||||||
current_packet_mode: Optional[str] = None
|
current_packet_mode: Optional[str] = None
|
||||||
@ -309,11 +313,11 @@ def convert_certificate( # noqa: ignore=C901
|
|||||||
raise Exception('missing current packet uid for "{packet.name}"')
|
raise Exception('missing current packet uid for "{packet.name}"')
|
||||||
|
|
||||||
if signature_type == "CertificationRevocation":
|
if signature_type == "CertificationRevocation":
|
||||||
revocations[current_packet_uid].append(packet)
|
revocations[current_packet_uid][issuer].append(packet)
|
||||||
elif signature_type.endswith("Certification"):
|
elif signature_type.endswith("Certification"):
|
||||||
if fingerprint_filter is not None and any([fp.endswith(issuer) for fp in fingerprint_filter]):
|
if fingerprint_filter is not None and 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.")
|
debug(f"The certification by issuer {issuer} is appended as it is found in the filter.")
|
||||||
certifications[current_packet_uid].append(packet)
|
certifications[current_packet_uid][issuer].append(packet)
|
||||||
else:
|
else:
|
||||||
debug(f"The certification by issuer {issuer} is not appended because it is not in the filter")
|
debug(f"The certification by issuer {issuer} is not appended because it is not in the filter")
|
||||||
else:
|
else:
|
||||||
@ -323,9 +327,9 @@ def convert_certificate( # noqa: ignore=C901
|
|||||||
raise Exception('missing current packet fingerprint for "{packet.name}"')
|
raise Exception('missing current packet fingerprint for "{packet.name}"')
|
||||||
|
|
||||||
if signature_type == "SubkeyBinding":
|
if signature_type == "SubkeyBinding":
|
||||||
subkey_bindings[current_packet_fingerprint] = packet
|
subkey_bindings[current_packet_fingerprint].append(packet)
|
||||||
elif signature_type == "SubkeyRevocation":
|
elif signature_type == "SubkeyRevocation":
|
||||||
subkey_revocations[certificate_fingerprint] = packet
|
subkey_revocations[certificate_fingerprint].append(packet)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"unknown signature type: {signature_type}")
|
raise Exception(f"unknown signature type: {signature_type}")
|
||||||
else:
|
else:
|
||||||
@ -462,7 +466,7 @@ def persist_subkeys(
|
|||||||
|
|
||||||
def persist_subkey_bindings(
|
def persist_subkey_bindings(
|
||||||
key_dir: Path,
|
key_dir: Path,
|
||||||
subkey_bindings: Dict[Fingerprint, Path],
|
subkey_bindings: Dict[Fingerprint, List[Path]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Persist all SubkeyBinding of a root key file to file(s)
|
"""Persist all SubkeyBinding of a root key file to file(s)
|
||||||
|
|
||||||
@ -472,7 +476,8 @@ def persist_subkey_bindings(
|
|||||||
subkey_bindings: The SubkeyBinding signatures of a Public-Subkey
|
subkey_bindings: The SubkeyBinding signatures of a Public-Subkey
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for fingerprint, subkey_binding in subkey_bindings.items():
|
for fingerprint, bindings in subkey_bindings.items():
|
||||||
|
subkey_binding = latest_certification(bindings)
|
||||||
issuer = packet_dump_field(subkey_binding, "Issuer")
|
issuer = packet_dump_field(subkey_binding, "Issuer")
|
||||||
output_file = key_dir / "subkey" / fingerprint / "certification" / f"{issuer}.asc"
|
output_file = key_dir / "subkey" / fingerprint / "certification" / f"{issuer}.asc"
|
||||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@ -482,7 +487,7 @@ def persist_subkey_bindings(
|
|||||||
|
|
||||||
def persist_subkey_revocations(
|
def persist_subkey_revocations(
|
||||||
key_dir: Path,
|
key_dir: Path,
|
||||||
subkey_revocations: Dict[Fingerprint, Path],
|
subkey_revocations: Dict[Fingerprint, List[Path]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Persist the SubkeyRevocations of all Public-Subkeys of a root key to file(s)
|
"""Persist the SubkeyRevocations of all Public-Subkeys of a root key to file(s)
|
||||||
|
|
||||||
@ -492,7 +497,8 @@ def persist_subkey_revocations(
|
|||||||
subkey_revocations: The SubkeyRevocations of PublicSubkeys of a key
|
subkey_revocations: The SubkeyRevocations of PublicSubkeys of a key
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for fingerprint, revocation in subkey_revocations.items():
|
for fingerprint, revocations in subkey_revocations.items():
|
||||||
|
revocation = latest_certification(revocations)
|
||||||
issuer = packet_dump_field(revocation, "Issuer")
|
issuer = packet_dump_field(revocation, "Issuer")
|
||||||
output_file = key_dir / "subkey" / fingerprint / "revocation" / f"{issuer}.asc"
|
output_file = key_dir / "subkey" / fingerprint / "revocation" / f"{issuer}.asc"
|
||||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@ -540,7 +546,7 @@ def persist_direct_key_revocations(
|
|||||||
|
|
||||||
|
|
||||||
def persist_uid_certifications(
|
def persist_uid_certifications(
|
||||||
certifications: Dict[Uid, List[Path]],
|
certifications: Dict[Uid, Dict[Fingerprint, List[Path]]],
|
||||||
key_dir: Path,
|
key_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Persist the certifications of a root key to file(s)
|
"""Persist the certifications of a root key to file(s)
|
||||||
@ -555,19 +561,18 @@ def persist_uid_certifications(
|
|||||||
key_dir: The root directory below which certifications are persisted
|
key_dir: The root directory below which certifications are persisted
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key, current_certifications in certifications.items():
|
for uid, uid_certifications in certifications.items():
|
||||||
for certification in current_certifications:
|
for issuer, issuer_certifications in uid_certifications.items():
|
||||||
certification_dir = key_dir / "uid" / key / "certification"
|
certification_dir = key_dir / "uid" / uid / "certification"
|
||||||
certification_dir.mkdir(parents=True, exist_ok=True)
|
certification_dir.mkdir(parents=True, exist_ok=True)
|
||||||
issuer = packet_dump_field(certification, "Issuer")
|
certification = latest_certification(issuer_certifications)
|
||||||
|
|
||||||
output_file = certification_dir / f"{issuer}.asc"
|
output_file = certification_dir / f"{issuer}.asc"
|
||||||
debug(f"Writing file {output_file} from {certification}")
|
debug(f"Writing file {output_file} from {certification}")
|
||||||
packet_join(packets=[certification], output=output_file, force=True)
|
packet_join(packets=[certification], output=output_file, force=True)
|
||||||
|
|
||||||
|
|
||||||
def persist_uid_revocations(
|
def persist_uid_revocations(
|
||||||
revocations: Dict[Uid, List[Path]],
|
revocations: Dict[Uid, Dict[Fingerprint, List[Path]]],
|
||||||
key_dir: Path,
|
key_dir: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Persist the revocations of a root key to file(s)
|
"""Persist the revocations of a root key to file(s)
|
||||||
@ -581,12 +586,11 @@ def persist_uid_revocations(
|
|||||||
key_dir: The root directory below which revocations will be persisted
|
key_dir: The root directory below which revocations will be persisted
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for key, current_revocations in revocations.items():
|
for uid, uid_revocations in revocations.items():
|
||||||
for revocation in current_revocations:
|
for issuer, issuer_revocations in uid_revocations.items():
|
||||||
revocation_dir = key_dir / "uid" / key / "revocation"
|
revocation_dir = key_dir / "uid" / uid / "revocation"
|
||||||
revocation_dir.mkdir(parents=True, exist_ok=True)
|
revocation_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
revocation = latest_certification(issuer_revocations)
|
||||||
issuer = packet_dump_field(revocation, "Issuer")
|
|
||||||
output_file = revocation_dir / f"{issuer}.asc"
|
output_file = revocation_dir / f"{issuer}.asc"
|
||||||
debug(f"Writing file {output_file} from {revocation}")
|
debug(f"Writing file {output_file} from {revocation}")
|
||||||
packet_join(packets=[revocation], output=output_file, force=True)
|
packet_join(packets=[revocation], output=output_file, force=True)
|
||||||
@ -628,10 +632,41 @@ def packet_dump_field(packet: Path, field: str) -> str:
|
|||||||
|
|
||||||
dump = packet_dump(packet)
|
dump = packet_dump(packet)
|
||||||
lines = [line.strip() for line in dump.splitlines()]
|
lines = [line.strip() for line in dump.splitlines()]
|
||||||
lines = list(filter(lambda line: line.startswith(f"{field}: "), lines))
|
lines = list(filter(lambda line: line.strip().startswith(f"{field}: "), lines))
|
||||||
if not lines:
|
if not lines:
|
||||||
raise Exception(f'Packet has no field "{field}"')
|
raise Exception(f'Packet has no field "{field}"')
|
||||||
return lines[0].split(maxsplit=1)[1]
|
return lines[0].split(sep=": ", maxsplit=1)[1]
|
||||||
|
|
||||||
|
|
||||||
|
def packet_signature_creation_time(packet: Path) -> datetime:
|
||||||
|
"""Retrieve the signature creation time field as datetime
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
packet: The path to the PGP packet to retrieve the value from
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The signature creation time as datetime
|
||||||
|
"""
|
||||||
|
return datetime.strptime(packet_dump_field(packet, "Signature creation time"), "%Y-%m-%d %H:%M:%S %Z")
|
||||||
|
|
||||||
|
|
||||||
|
def latest_certification(certifications: Iterable[Path]) -> Path:
|
||||||
|
"""Returns the latest certification based on the signature creation time from a list of packets.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
certifications: List of certification from which to choose the latest from
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
The latest certification from a list of packets
|
||||||
|
"""
|
||||||
|
return reduce(
|
||||||
|
lambda a, b: a if packet_signature_creation_time(a) > packet_signature_creation_time(b) else b,
|
||||||
|
certifications,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def keyring_split(working_dir: Path, keyring: Path, preserve_filename: bool = False) -> Iterable[Path]:
|
def keyring_split(working_dir: Path, keyring: Path, preserve_filename: bool = False) -> Iterable[Path]:
|
||||||
|
Loading…
Reference in New Issue
Block a user