condorcore-keyring/libkeyringctl/sequoia.py

225 lines
6.8 KiB
Python
Raw Normal View History

# SPDX-License-Identifier: GPL-3.0-or-later
from collections.abc import Iterable
from datetime import datetime
from functools import reduce
from pathlib import Path
from re import sub
from tempfile import mkdtemp
from typing import Dict
from typing import List
from typing import Optional
from .types import Fingerprint
from .types import Username
from .util import cwd
from .util import natural_sort_path
from .util import system
def keyring_split(working_dir: Path, keyring: Path, preserve_filename: bool = False) -> Iterable[Path]:
"""Split a file containing a PGP keyring into separate certificate files
The original keyring filename is preserved if the split only yields a single certificate.
If preserve_filename is True, all keyrings are placed into separate directories while preserving
the filename.
The file is split using sq.
Parameters
----------
working_dir: The path of the working directory below which to create the output files
keyring: The path of a file containing a PGP keyring
preserve_filename: If True, all keyrings are placed into separate directories while preserving the filename
Returns
-------
An iterable over the naturally sorted list of certificate files derived from a keyring
"""
keyring_dir = Path(mkdtemp(dir=working_dir, prefix="keyring-")).absolute()
with cwd(keyring_dir):
system(["sq", "keyring", "split", "--prefix", "", str(keyring)])
keyrings: List[Path] = list(natural_sort_path(keyring_dir.iterdir()))
if 1 == len(keyrings) or preserve_filename:
for index, key in enumerate(keyrings):
keyring_sub_dir = Path(mkdtemp(dir=keyring_dir, prefix=f"{keyring.name}-")).absolute()
keyrings[index] = key.rename(keyring_sub_dir / keyring.name)
return keyrings
def keyring_merge(certificates: List[Path], output: Optional[Path] = None, force: bool = False) -> str:
"""Merge multiple certificates into a keyring
Parameters
----------
certificates: List of paths to certificates to merge into a keyring
output: Path to a file which the keyring is written, return the result instead if None
force: Whether to force overwriting existing files (defaults to False)
Returns
-------
The result if no output file has been used
"""
cmd = ["sq", "keyring", "merge"]
if force:
cmd.insert(1, "--force")
if output:
cmd += ["--output", str(output)]
cmd += [str(cert) for cert in sorted(certificates)]
return system(cmd)
def packet_split(working_dir: Path, certificate: Path) -> Iterable[Path]:
"""Split a file containing a PGP certificate into separate packet files
The files are split using sq
Parameters
----------
working_dir: The path of the working directory below which to create the output files
certificate: The absolute path of a file containing one PGP certificate
Returns
-------
An iterable over the naturally sorted list of packet files derived from certificate
"""
packet_dir = Path(mkdtemp(dir=working_dir, prefix="packet-")).absolute()
with cwd(packet_dir):
system(["sq", "packet", "split", "--prefix", "", str(certificate)])
return natural_sort_path(packet_dir.iterdir())
def packet_join(packets: List[Path], output: Optional[Path] = None, force: bool = False) -> str:
"""Join PGP packet data in files to a single output file
Parameters
----------
packets: A list of paths to files that contain PGP packet data
output: Path to a file to which all PGP packet data is written, return the result instead if None
force: Whether to force overwriting existing files (defaults to False)
Returns
-------
The result if no output file has been used
"""
cmd = ["sq", "packet", "join"]
if force:
cmd.insert(1, "--force")
packets_str = list(map(lambda path: str(path), packets))
cmd.extend(packets_str)
cmd.extend(["--output", str(output)])
return system(cmd)
def inspect(
packet: Path, certifications: bool = True, fingerprints: Optional[Dict[Fingerprint, Username]] = None
) -> str:
"""Inspect PGP packet data and return the result
Parameters
----------
packet: Path to a file that contain PGP data
certifications: Whether to print third-party certifications
fingerprints: Optional dict of fingerprints to usernames to enrich the output with
Returns
-------
The result of the inspection
"""
cmd = ["sq", "inspect"]
if certifications:
cmd.append("--certifications")
cmd.append(str(packet))
result: str = system(cmd)
if fingerprints:
for fingerprint, username in fingerprints.items():
result = sub(f"{fingerprint}", f"{fingerprint} {username}", result)
result = sub(f" {fingerprint[24:]}", f" {fingerprint[24:]} {username}", result)
return result
def packet_dump(packet: Path) -> str:
"""Dump a PGP packet to string
The `sq packet dump` command is used to retrieve a dump of information from a PGP packet
Parameters
----------
packet: The path to the PGP packet to retrieve the value from
Returns
-------
The contents of the packet dump
"""
return system(["sq", "packet", "dump", str(packet)])
def packet_dump_field(packet: Path, field: str) -> str:
"""Retrieve the value of a field from a PGP packet
Parameters
----------
packet: The path to the PGP packet to retrieve the value from
field: The name of the field
Raises
------
Exception: If the field is not found in the PGP packet
Returns
-------
The value of the field found in packet
"""
dump = packet_dump(packet)
lines = [line.strip() for line in dump.splitlines()]
lines = list(filter(lambda line: line.strip().startswith(f"{field}: "), lines))
if not lines:
raise Exception(f'Packet has no field "{field}"')
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,
)