pax_global_header00006660000000000000000000000064147663565370014540gustar00rootroot0000000000000052 comment=ab0779148842cd2ad7cd7241ab1b66cff225e449 sopv-gpgv-0.1.4/000077500000000000000000000000001476635653700134725ustar00rootroot00000000000000sopv-gpgv-0.1.4/.gitignore000066400000000000000000000000141476635653700154550ustar00rootroot00000000000000sopv-gpgv.1 sopv-gpgv-0.1.4/Changelog.md000066400000000000000000000010511476635653700157000ustar00rootroot00000000000000# Changelog for sopv-gpgv Maintainer: Daniel Kahn Gillmor ## Version 0.1.4 (2025-03-18) - correct version number ## Version 0.1.3 (2025-03-15) - Validate signatures from expired certificates as long as they were made when the certificate was not expired. ## Version 0.1.2 (2025-03-07) - support global `--debug` CLI option ## Version 0.1.1 (2024-11-25) - stop using gpgv --logger - bugfix: CERTS from file descriptors - bugfix: SIGNATURES from environment ## Version 0.1 (2024-07-21) - Initial release (Aiming for compliance with sopv 1.0) sopv-gpgv-0.1.4/LICENSE000066400000000000000000000017771476635653700145130ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sopv-gpgv-0.1.4/Makefile000066400000000000000000000026201476635653700151320ustar00rootroot00000000000000#!/usr/bin/make -f VERSION = $(lastword $(shell ./sopv-gpgv version)) COMPLETIONS = completions/bash/sopv-gpgv completions/fish/sopv-gpgv.fish completions/zsh/_sopv-gpgv Reported_version = $(shell ./sopv-gpgv version | cut -f2 -d\ ) Changelog_version = $(shell grep Version ./Changelog.md | head -n1 | cut -f3 -d\ ) all: sopv-gpgv.1 $(COMPLETIONS) sopv-gpgv.1: sopv-gpgv argparse-manpage --pyfile $< \ --project-name sopv-gpgv \ --author 'Daniel Kahn Gillmor ' \ --version $(VERSION) \ --function get_parser \ --description 'Stateless OpenPGP Signature Verification backed by gpgv' \ --output $@ completions/zsh/_sopv-gpgv: sopv-gpgv mkdir -p $(dir $@) register-python-argcomplete --shell zsh $< > $@.tmp mv $@.tmp $@ completions/bash/sopv-gpgv: sopv-gpgv mkdir -p $(dir $@) register-python-argcomplete --shell bash $< > $@.tmp mv $@.tmp $@ completions/fish/sopv-gpgv.fish: sopv-gpgv mkdir -p $(dir $@) register-python-argcomplete --shell fish $< > $@.tmp mv $@.tmp $@ check: typecheck spellcheck formatcheck versioncheck ./test sqop typecheck: mypy --strict sopv-gpgv spellcheck: codespell README.md sopv.gpgv test LICENSE formatcheck: black --check sopv-gpgv versioncheck: test $(Changelog_version) = $(Reported_version) clean: rm -f *.key *.cert *.cert.bin test.* sopv-gpgv.1 rm -rf completions .mypy_cache .PHONY: all clean typecheck spellcheck check formatcheck sopv-gpgv-0.1.4/README.md000066400000000000000000000057411476635653700147600ustar00rootroot00000000000000# An implementation of the `sopv` subset using `gpgv` the [Stateless OpenPGP Command Line Interface](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/) has a signature-verification-only subset called `sopv`. This python script provides the `sopv` interface by wrapping [g10code](https://g10code.com/)'s [`gpgv`](https://www.gnupg.org/documentation/manuals/gnupg/gpgv.html) implementation. ## Impedance mismatches There are some subtle differences and difficulties in mapping `sopv` to `gpgv`. The goal of this project is to be a complete `sopv` implementation that exposes all of the OpenPGP functionality of `gpgv`, without inheriting any of `gpgv`'s interface quirks. This means that if there is a particular cryptographic OpenPGP feature that `gpgv` can't support, `sopv-gpgv` won't be able to support them either; but if it's just an interface quirk, that should be smoothed over correctly. ### ASCII-armored keyrings `gpgv` expects that certificates used for verification are in binary "keyring" form. `sopv` permits ASCII-armored certificates. ### Whitespace at the end of Clearsigned Messages GnuPG [does not follow the OpenPGP standard for clearsigned messages](https://dev.gnupg.org/T7106). In particular, it changes the number of trailing newlines for most messages. `gpgv-sopv` does not address this problem, as `gpgv`'s non-compliance with the spec is not an invertible transformation. Other implementations of the `gpgv` interface, like [`gpgv-sq`](https://gitlab.com/sequoia-pgp/sequoia-chameleon-gnupg) do not have this problem. ### Filesystem Access `gpgv` typically uses filesystem access, but `sopv` is expected to work entirely in read-only mode. However, [`gpgv` cannot use keyrings from file descriptors](https://dev.gnupg.org/T4608). Combined with the need to translate from ASCII-armored keyrings (see above), this means that we have to use a tmpfile for some keyrings. The current implementation uses a tmpfile for CERTS objects that need this kind of rewriting. Given that the project is already necessarily reliant on tmpfiles, it also uses tmpfiles for `gpgv`'s `--status` output. If `gpgv` could use file descriptors for keyring input, then we might also want to rewrite `sopv-gpgv` to use asynchronous (or multithreaded concurrent) reads from the `--status` output. ## Author and License This project is released by Daniel Kahn Gillmor under an [MIT-style license](LICENSE). If you find any of it useful, either in implementing another `sopv` implementation, or in making sense of how to use `gpgv` safely, please reuse it where possible. ## Acknowledgements The regular expression for unarmoring OpenPGP ASCII armor was adapted from [PGPy](https://github.com/SecurityInnovation/PGPy). And this wrapper of course depends on the long-standing `gpgv` interface from [g10code](https://g10code.com/). ## Bug Reporting and Feedback Please report bugs or provide other feedback at [https://gitlab.com/dkg/sopv-gpgv](https://gitlab.com/dkg/sopv-gpgv), or by e-mail to the author. sopv-gpgv-0.1.4/sopv-gpgv000077500000000000000000000521511476635653700153540ustar00rootroot00000000000000#!/usr/bin/python3 """sopv-gpgv OpenPGP Stateless Command Line Interface Verification-Only Subset, backed by gpgv This implements the sopv 1.0 subset as defined in https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/, using gpgv as a backend. Usage: sopv-gpgv version [--extended|--backend|--sop-spec|--sopv] sopv-gpgv verify [--not-before=DATE] [--not-after=DATE] [--] SIGNATURES CERTS [CERTS…] < MESSAGE > VERIFICATIONS sopv-gpgv inline-verify [--not-before=DATE] [--not-after=DATE] [--verifications-out=VERIFICATIONS] [--] CERTS [CERTS…] < INLINE_SIGNED_MESSAGE > MESSAGE Author: Daniel Kahn Gillmor License: MIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ import subprocess import os import re import sys import logging from argparse import ArgumentParser, RawDescriptionHelpFormatter, Namespace, SUPPRESS from typing import Optional, Dict, List, Union from types import ModuleType from datetime import datetime, UTC from enum import IntEnum from io import ( BytesIO, FileIO, BufferedReader, BufferedIOBase, BufferedWriter, BufferedRandom, ) from base64 import b64decode from tempfile import NamedTemporaryFile, TemporaryFile, _TemporaryFileWrapper # from typing import List, Optional, Dict, Sequence, MutableMapping, Tuple, BinaryIO, TYPE_CHECKING argcomplete: Optional[ModuleType] try: import argcomplete except ImportError: argcomplete = None __version__ = "0.1.4" class Err(IntEnum): OK = 0 UNSPECIFIED_FAILURE = 1 NO_SIGNATURE = 3 UNSUPPORTED_ASYMMETRIC_ALGO = 13 CERT_CANNOT_ENCRYPT = 17 MISSING_ARG = 19 INCOMPLETE_VERIFICATION = 23 CANNOT_DECRYPT = 29 PASSWORD_NOT_HUMAN_READABLE = 31 UNSUPPORTED_OPTION = 37 BAD_DATA = 41 EXPECTED_TEXT = 53 OUTPUT_EXISTS = 59 MISSING_INPUT = 61 KEY_IS_PROTECTED = 67 UNSUPPORTED_SUBCOMMAND = 69 UNSUPPORTED_SPECIAL_PREFIX = 71 AMBIGUOUS_INPUT = 73 KEY_CANNOT_SIGN = 79 INCOMPATIBLE_OPTIONS = 83 UNSUPPORTED_PROFILE = 89 NO_HARDWARE_KEY_FOUND = 97 HARDWARE_KEY_FAILURE = 101 class SopException(Exception): "SOP-specific exception" def __init__(self, err: Err, msg: str) -> None: super().__init__(msg) self.err = err self.msg = msg def __str__(self) -> str: return f"SOP Error {self.err.name} ({int(self.err)}): {self.msg}" class SigMode(IntEnum): BINARY = 0 TEXT = 1 def __str__(self) -> str: return f"mode:{self.name.lower()}" class Fingerprint: def __init__(self, value: Union[str, bytes]) -> None: if isinstance(value, bytes): value = value.decode() value = value.replace(" ", "") if re.match("^[a-fA-F0-9]{40}$", value) is None: raise ValueError(f"Bad OpenPGP fingerprint string: {value}") self._value = value.upper() def __str__(self) -> str: return self._value def __repr__(self) -> str: return f"" def confirm_nonexistence(name: str, err: Err) -> None: "raise an exception if a file with this name exists" if os.path.exists(name): raise SopException(err, f"File '{name}' exists") class Signatures: """Doesn't need to be dearmored, can remain a simple file descriptor""" _fd: Optional[int] = None _tempfile: Optional[BufferedRandom] = None def __init__(self, name: str) -> None: self._name = name if name.startswith("@FD:"): confirm_nonexistence(name, Err.AMBIGUOUS_INPUT) self._fd = int(name[4:]) elif name.startswith("@ENV:"): confirm_nonexistence(name, Err.AMBIGUOUS_INPUT) self._tempfile = TemporaryFile() self._tempfile.write(os.environ[name[5:]].encode()) self._tempfile.flush() self._tempfile.seek(0) self._fd = self._tempfile.fileno() elif name.startswith("@"): raise SopException( Err.UNSUPPORTED_SPECIAL_PREFIX, f"No implementation for special prefix {name.split(":")[0]}", ) @property def fd(self) -> Optional[int]: return self._fd @property def label(self) -> str: if self._fd is None: return self._name return f"-&{self._fd}" class Certs: """Needs to be dearmored, cannot be a simple file descriptor""" _armor_matcher = re.compile( rb"""# armor header line; capture the variable part of the magic text ^-{5}BEGIN\ PGP\ (?P[A-Z0-9 ,]+)-{5}(?:\r?\n) # try to capture all the headers into one capture group # if this doesn't match, m['headers'] will be None (?P(^.+:\ .+(?:\r?\n))+)?(?:\r?\n)? # capture all lines of the body # including the newline, and the pad character(s) (?P([A-Za-z0-9+/]+={,2}(?:\r?\n))+) # optionally capture the armored CRC24 value (?:^=(?P[A-Za-z0-9+/]{4})(?:\r?\n))? # finally, capture the armor tail line, which must match the armor header line ^-{5}END\ PGP\ (?P=magic)-{5}(?:\r?\n)? """, flags=re.MULTILINE | re.VERBOSE, ) def __init__(self, name: str) -> None: self._name = name _rawfile: Union[BufferedReader, _TemporaryFileWrapper[bytes]] if name.startswith("@FD:"): confirm_nonexistence(name, Err.AMBIGUOUS_INPUT) _rawfile = NamedTemporaryFile() _rawfile.write(BufferedReader(FileIO(int(name[4:]), mode="rb")).read()) elif name.startswith("@ENV:"): confirm_nonexistence(name, Err.AMBIGUOUS_INPUT) _rawfile = NamedTemporaryFile() _rawfile.write(os.environ[name[5:]].encode()) elif name.startswith("@"): raise SopException( Err.UNSUPPORTED_SPECIAL_PREFIX, f"No implementation for special prefix {name.split(":")[0]}", ) else: _rawfile = open(name, "rb") _rawfile.seek(0) early = _rawfile.peek(15) if early.startswith(b"-----BEGIN PGP "): data = _rawfile.read() _rawfile = NamedTemporaryFile() _rawfile.write(self.unarmor(data)) self._file = _rawfile self._file.flush() def unarmor(self, data: bytes) -> bytes: m = self._armor_matcher.search(data) if m is None or m.groupdict()["body"] is None: raise SopException(Err.BAD_DATA, "Could not dearmor") body = m.groupdict()["body"] return b64decode(body) @property def name(self) -> str: 'work around gpgv bizarre expectation about file names with no "/" being found in the GnuPG homedir' if "/" not in self._file.name: return "./" + self._file.name return self._file.name class Verification: """- ISO-8601 UTC datestamp of the signature, to one second precision, using the `Z` suffix - Fingerprint of the signing key (may be a subkey) - Fingerprint of primary key of signing certificate (if signed by primary key, same as the previous field) - a string describing the mode of the signature, either `mode:text` or `mode:binary` - message describing the verification (free form) """ def __init__(self) -> None: self.ts: Optional[datetime] = None self.signingfpr: Optional[Fingerprint] = None self.primaryfpr: Optional[Fingerprint] = None self.mode: Optional[SigMode] = None self.msg: Optional[str] = None self.good: bool = False self.key_expired: Optional[datetime] = None @property def complete(self) -> bool: return ( self.ts is not None and ( self.good or (self.key_expired is not None and self.key_expired > self.ts) ) and self.signingfpr is not None and self.primaryfpr is not None and self.mode is not None and self.ts.tzinfo == UTC ) def __str__(self) -> str: if not self.complete or self.ts is None: raise ValueError("Verification Object is not complete") msg = "" if self.msg is not None: msg = f" {self.msg}" return f"{self.ts.strftime('%Y-%m-%dT%H:%m:%SZ')} {self.signingfpr} {self.primaryfpr} {self.mode}{msg}" @staticmethod def from_gpg_status_ts(part: bytes) -> datetime: if b"T" in part: return datetime.fromisoformat(part.decode()) else: return datetime.fromtimestamp(int(part.decode()), UTC) class GpgvSopv: _parser: Optional[ArgumentParser] = None _gpgv: str = os.environ.get("SOPV_GPGV", "gpgv") def timestamp(self, instr: str) -> Optional[datetime]: if instr == "-": return None elif instr == "now": return datetime.now(tz=UTC) else: return datetime.fromisoformat(instr) def indirect_output(self, outputname: str) -> BufferedWriter: if outputname.startswith("@FD:"): return BufferedWriter(FileIO(int(outputname[4:]), "wb")) elif outputname.startswith("@"): raise SopException( Err.UNSUPPORTED_SPECIAL_PREFIX, f"No implementation for special prefix {outputname.split(":")[0]}", ) else: confirm_nonexistence(outputname, Err.OUTPUT_EXISTS) return BufferedWriter(FileIO(outputname, "wb")) @property def parser(self) -> ArgumentParser: if self._parser is None: self._parser = ArgumentParser( prog="sopv-gpgv", description="Verify OpenPGP signatures using gpgv", formatter_class=RawDescriptionHelpFormatter, epilog="""This tool implements `sopv`, the verificaftion-only subset of https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/ It uses gpgv (from the GnuPG project) as its backend. The environment variable $SOPV_GPGV can be used to choose the name or path of the gpgv implementation used (defaults to "gpgv").""", allow_abbrev=False, ) self._parser.add_argument( "--debug", help="Provide debugging information", action="store_true", ) _cmds = self._parser.add_subparsers( title="Exactly one subcommand must be selected", required=True, metavar="SUBCOMMAND", dest="subcommand", ) _debug = ArgumentParser(add_help=False) _debug.add_argument( "--debug", help="Provide debugging information", default=SUPPRESS, action="store_true", ) _version = _cmds.add_parser( "version", help="emit version", parents=[_debug], description="Print version information to stdout", ) _version_type = _version.add_mutually_exclusive_group(required=False) _version_type.add_argument( "--backend", help="Show gpgv version", action="store_true", ) _version_type.add_argument( "--extended", help="Show extended version information", action="store_true", ) _version_type.add_argument( "--sop-spec", help="Show last known version of SOP, the Stateless OpenPGP specification", action="store_true", ) _version_type.add_argument( "--sopv", help="Show compliant version of the `sopv` verification only subset of SOP", action="store_true", ) _version.set_defaults(func=self.version) _date_range = ArgumentParser(add_help=False, parents=[_debug]) _date_range.add_argument( "--not-before", type=self.timestamp, help="Only accept signatures later than this (default: None)", default=None, metavar="TIMESTAMP", ) _date_range.add_argument( "--not-after", type=self.timestamp, help="Only accept signatures earlier than this (default: now)", default=datetime.now(UTC), metavar="TIMESTAMP", ) _verify = _cmds.add_parser( "verify", help="verify detached signatures", parents=[_date_range], description="Verify detached OpenPGP signatures", formatter_class=RawDescriptionHelpFormatter, epilog=f"""input/output: STDIN: Message to verify STDOUT: SOP-style VERIFICATIONS (if any) return value: 0 if at least one valid signature from an acceptable certificate, non-zero otherwise.""", ) _verify.add_argument( "SIGNATURES", type=Signatures, help="OpenPGP Signatures", ) _verify.add_argument( "CERTS", type=Certs, help="Acceptable OpenPGP Certificates for Signature Verification", nargs="+", ) _verify.set_defaults(func=self.verify) _inline_verify = _cmds.add_parser( "inline-verify", help="verify inline signatures", parents=[_date_range], description='Verify inline OpenPGP signatures (either CSF or "Signed Message" OpenPGP packets)', formatter_class=RawDescriptionHelpFormatter, epilog="""input/output: STDIN: Inline-signed OpenPGP message to verify STDOUT: If correctly signed, message without OpenPGP signature return value: 0 if at least one valid signature from an acceptable certificate, non-zero otherwise.""", ) _inline_verify.add_argument( "--verifications-out", type=self.indirect_output, help="Write SOP-style VERIFICATIONS here", metavar="VERIFICATIONS", ) _inline_verify.add_argument( "CERTS", type=Certs, help="Acceptable OpenPGP Certificates for Signature Verification", nargs="+", ) _inline_verify.set_defaults(func=self.inline_verify) return self._parser def get_gpgv_version(self) -> str: success = subprocess.run( [self._gpgv, "--version"], capture_output=True, ) return success.stdout.decode() def version(self, args: Namespace) -> None: if args.backend: print(f"gpgv {self.get_gpgv_version().split('\n')[0].split(' ')[-1]}") elif args.extended: print(f"sopv-gpgv {__version__}\n{self.get_gpgv_version().strip()}") elif args.sop_spec: print("~draft-dkg-openpgp-stateless-cli-10") elif args.sopv: print("1.0") else: print(f"sopv-gpgv {__version__}") def status_to_verifs( self, status: bytes, not_before: Optional[datetime], not_after: Optional[datetime], ) -> List[Verification]: """From DETAILS.gz, each NEWSIG status line delimits the evaluation of another signature. We are looking for a signature that emits both GOODSIG and VALIDSIG. We get the data from the associated VALIDSIG entry. """ verif = Verification() verifs: List[Verification] = [] for l in status.split(b"\n"): if l == b"[GNUPG:] NEWSIG": if verif.complete: verifs.append(verif) verif = Verification() continue if l.startswith(b"[GNUPG:] GOODSIG "): verif.good = True if l.startswith(b"[GNUPG:] KEYEXPIRED "): parts = l.split(b" ") verif.key_expired = Verification.from_gpg_status_ts(parts[2]) if l.startswith(b"[GNUPG:] VALIDSIG "): parts = l.split(b" ") ts = Verification.from_gpg_status_ts(parts[4]) if not_before is not None and ts < not_before: continue if not_after is not None and ts > not_after: continue verif.ts = ts verif.signingfpr = Fingerprint(parts[2].decode()) verif.primaryfpr = Fingerprint(parts[11].decode()) verif.mode = SigMode(int(parts[10])) if verif.complete: verifs.append(verif) return verifs def verify(self, args: Namespace) -> None: sigs: Signatures = args.SIGNATURES certs: List[Certs] = args.CERTS status = TemporaryFile() cmd = [ self._gpgv, "--enable-special-filenames", "--homedir=/dev/null", f"--status-fd={status.fileno()}", ] # add each certs as a --keyring argument -- gpgv does not accept -& arguments for keyrings: # https://dev.gnupg.org/T4608 for cert in certs: cmd += [f"--keyring={cert.name}"] logging.info(f"cert({cert._name}): {cert.name}") cmd += [ "--", sigs.label, "-", ] keep_fds = [0, status.fileno()] if sigs.fd is not None: keep_fds += [sigs.fd] res = subprocess.run( cmd, capture_output=True, pass_fds=keep_fds, ) status.seek(0) status_data = status.read() verifs = self.status_to_verifs(status_data, args.not_before, args.not_after) # we ignore returncode, because gpgv fails if any signature fails. if len(verifs) == 0: raise SopException(Err.NO_SIGNATURE, "No Valid Signature found") for verif in verifs: print(verif) def inline_verify(self, args: Namespace) -> None: certs: List[Certs] = args.CERTS status = TemporaryFile() output = TemporaryFile() cmd = [ self._gpgv, "--enable-special-filenames", "--homedir=/dev/null", f"--status-fd={status.fileno()}", f"--output=-&{output.fileno()}", ] # add each certs as a --keyring argument -- gpgv does not accept -& arguments for keyrings: # https://dev.gnupg.org/T4608 for cert in certs: cmd += [f"--keyring={cert.name}"] logging.info(f"cert({cert._name}): {cert.name}") cmd += [ "--", "-", ] keep_fds = [0, status.fileno(), output.fileno()] res = subprocess.run( cmd, capture_output=True, pass_fds=keep_fds, ) status.seek(0) status_data = status.read() verifs = self.status_to_verifs(status_data, args.not_before, args.not_after) if len(verifs) == 0: raise SopException(Err.NO_SIGNATURE, "No Valid Inline Signature Found") else: output.seek(0) # write message to stdout sys.stdout.buffer.write(output.read()) if args.verifications_out is not None: for verif in verifs: args.verifications_out.write((str(verif) + "\n").encode()) def get_parser() -> ArgumentParser: "This function is used by argparse-manpage to extract a manpage" return GpgvSopv().parser def main() -> None: sopv = GpgvSopv() if argcomplete: argcomplete.autocomplete(sopv.parser) elif "_ARGCOMPLETE" in os.environ: logging.error( 'Argument completion requested but the "argcomplete" module is not installed.' "It can be obtained at https://pypi.python.org/pypi/argcomplete" ) sys.exit(1) try: args = sopv.parser.parse_args(sys.argv[1:]) if args.debug: logging.basicConfig(level=logging.DEBUG) logging.debug(f"Subcommand: {args.subcommand}") args.func(args) except SopException as e: logging.error(e) sys.exit(e.err) if __name__ == "__main__": main() sys.exit(0) sopv-gpgv-0.1.4/test000077500000000000000000000040761476635653700144060ustar00rootroot00000000000000#!/bin/bash # Test sopv-gpgv against some SOP signing implementation # Author: Daniel Kahn Gillmor set -e set -x SOP="$1" if ! [ -n "$SOP" ] ; then printf >&2 'Usage: ./test SOP\n' exit 1 fi shift # the sopv implementation to test (set this explicitly to sopv-gpgv to # test the installed version) SOPV=${SOPV:-./sopv-gpgv} for keyname in x y z; do $SOP generate-key 'test '$keyname' key' > $keyname.key $SOP extract-cert < $keyname.key > $keyname.cert $SOP dearmor < $keyname.cert > $keyname.cert.bin done echo test > test.txt $SOP sign x.key < test.txt > test.txt.signatures $SOP sign x.key z.key < test.txt > test.txt.2signatures $SOP inline-sign x.key < test.txt > test.signed $SOP inline-sign x.key z.key < test.txt > test.2signed $SOP inline-sign --as=clearsigned x.key < test.txt > test.csf $SOP inline-sign --as=clearsigned x.key z.key < test.txt > test.2csf for x in '' --extended --backend --sop-spec --sopv; do printf "Version (%s)\n" "$x" $SOPV version $x done for kt in cert cert.bin; do for t in test.txt.signatures test.txt.2signatures; do $SOPV verify $t x.$kt < test.txt ! $SOPV verify $t y.$kt < test.txt if [ $t == test.txt.2signatures ] ; then $SOPV verify $t z.$kt < test.txt else ! $SOPV verify $t z.$kt < test.txt fi $SOPV verify $t x.$kt y.$kt < test.txt $SOPV verify $t y.$kt x.$kt < test.txt $SOPV verify $t x.$kt z.$kt < test.txt done for t in test.signed test.2signed test.csf test.2csf; do $SOPV inline-verify x.$kt < $t if [ $t == test.2signed -o $t == test.2csf ]; then $SOPV inline-verify z.$kt < $t else ! $SOPV inline-verify z.$kt < $t fi ! $SOPV inline-verify y.$kt < $t $SOPV inline-verify x.$kt z.$kt < $t done done # FIXME: inline-verify: need to also test --verifications-out # FIXME: should test @FD: and @ENV: special designators as inputs # FIXME: should test --not-before and --not-after echo "Tests completed successfully!"