1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
|
# SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
# SPDX-License-Identifier: GPL-2.0-or-later
"""Run a stateless OpenPGP command-line tool."""
from __future__ import annotations
import subprocess # noqa: S404
import typing
from testsigs import defs
from . import base
if typing.TYPE_CHECKING:
import pathlib
from typing import Final
class SOPCli(base.PGPCli):
"""Invoke a stateless OpenPGP command-line tool."""
def generate_keys(self, uid: str) -> defs.KeyFiles:
"""Generate an OpenPGP key to sign with."""
keydir: Final = self.cfg.path.home / "keys"
keydir.mkdir(mode=0o755)
keys: Final = defs.KeyFiles(
secret=keydir / "secret.asc",
secret_bin=keydir / "secret.gpg",
public=keydir / "public.asc",
public_bin=keydir / "public.gpg",
)
secret_text: Final = subprocess.check_output( # noqa: S603
[self.cfg.prog.sop, "generate-key", "--profile=rfc4880", uid],
cwd=self.cfg.path.work,
encoding="UTF-8",
env=self.cfg.env,
)
if not secret_text.startswith("-----BEGIN PGP PRIVATE KEY BLOCK-----"):
raise RuntimeError(repr((self.cfg.prog.sop, secret_text)))
keys.secret.write_text(secret_text, encoding="UTF-8")
# This is not UTF-8 text; it may contain arbitrary byte values.
secret_bin: Final = subprocess.check_output( # noqa: S603
[self.cfg.prog.sop, "dearmor"],
cwd=self.cfg.path.work,
env=self.cfg.env,
input=secret_text.encode("UTF-8"),
)
keys.secret_bin.write_bytes(secret_bin)
public_text: Final = subprocess.check_output( # noqa: S603
[self.cfg.prog.sop, "extract-cert"],
cwd=self.cfg.path.work,
encoding="UTF-8",
env=self.cfg.env,
input=secret_text,
)
if not public_text.startswith("-----BEGIN PGP PUBLIC KEY BLOCK-----"):
raise RuntimeError(repr((self.cfg.prog.sop, public_text)))
keys.public.write_text(public_text, encoding="UTF-8")
# This is not UTF-8 text; it may contain arbitrary byte values.
public_bin: Final = subprocess.check_output( # noqa: S603
[self.cfg.prog.sop, "dearmor"],
cwd=self.cfg.path.work,
env=self.cfg.env,
input=public_text.encode("UTF-8"),
)
keys.public_bin.write_bytes(public_bin)
return keys
def import_secret_key(self, keys: defs.KeyFiles, uid: str) -> defs.PublicKey:
"""Import a secret key, return the parts of the public key we care about."""
# We don't need that yet, and we cannot parse an OpenPGP key's structure by ourselves.
raise NotImplementedError
def sign_detached(self, keys: defs.KeyFiles, datafile: pathlib.Path) -> pathlib.Path:
"""Create an ASCII-armored detached signature, return the path to the new file."""
signature: Final = datafile.with_name(f"{datafile.name}.asc")
sig_contents: Final = subprocess.check_output( # noqa: S603
[self.cfg.prog.sop, "sign", keys.secret],
cwd=self.cfg.path.work,
env=self.cfg.env,
input=datafile.read_bytes(),
)
signature.write_bytes(sig_contents)
return signature
def verify_detached(
self,
keys: defs.KeyFiles,
pub_key: defs.PublicKey,
datafile: pathlib.Path,
signature: pathlib.Path,
) -> None:
"""Verify an OpenPGP detached signature in a file."""
sig_report: Final = subprocess.check_output( # noqa: S603
[self.cfg.prog.sop, "verify", signature, keys.public],
cwd=self.cfg.path.work,
env=self.cfg.env,
input=datafile.read_bytes(),
).decode()
if not any(
line.split()[2] in {pub_key.key_id, pub_key.fpr} and line.split()[3] == "mode:binary"
for line in sig_report.splitlines()
):
raise RuntimeError(repr((pub_key, sig_report)))
|