feature(keyringctl): add verify command to check certificate expectation

This command checks certain expectations using sq and hokey, prints the
results to stdout and potentially exists non successfully.
This commit is contained in:
Levente Polyak 2021-10-24 21:49:55 +02:00
parent 94c3b4c8e9
commit 9733fbafd8
No known key found for this signature in database
GPG Key ID: FC1B547C8D8172C8
4 changed files with 94 additions and 5 deletions

View File

@ -18,6 +18,10 @@ from the provided data structure and to install it:
* python * python
* sequoia-sq * sequoia-sq
Optional:
* hopenpgp-tools (verify)
* sq-keyring-linter (verify)
## Usage ## Usage
### Build ### Build
@ -85,6 +89,13 @@ Only inspect a specific main key
./keyringctl inspect --main <username_or_fingerprint_or_directory...> ./keyringctl inspect --main <username_or_fingerprint_or_directory...>
``` ```
### Verify
Verify certificates against modern expectations and assumptions
```bash
./keyringctl verify <username_or_fingerprint_or_directory...>
```
## Installation ## Installation
To install archlinux-keyring system-wide use the included `Makefile`: To install archlinux-keyring system-wide use the included `Makefile`:

View File

@ -14,6 +14,7 @@ from .keyring import convert
from .keyring import export from .keyring import export
from .keyring import inspect_keyring from .keyring import inspect_keyring
from .keyring import list_keyring from .keyring import list_keyring
from .keyring import verify
from .util import absolute_path from .util import absolute_path
from .util import cwd from .util import cwd
@ -97,8 +98,24 @@ inspect_parser.add_argument(
type=absolute_path, type=absolute_path,
) )
verify_parser = subcommands.add_parser(
"verify",
help="verify certificates against modern expectations",
)
verify_parser.add_argument(
"source",
nargs="*",
help="username, fingerprint or directories containing certificates",
type=absolute_path,
)
verify_parser.add_argument("--no-lint-hokey", dest="lint_hokey", action="store_false", help="Do not run hokey lint")
verify_parser.add_argument(
"--no-lint-sq-keyring", dest="lint_sq_keyring", action="store_false", help="Do not run sq-keyring-linter"
)
verify_parser.set_defaults(lint_hokey=True, lint_sq_keyring=True)
def main() -> None:
def main() -> None: # noqa: ignore=C901
args = parser.parse_args() args = parser.parse_args()
if args.verbose: if args.verbose:
@ -165,6 +182,14 @@ def main() -> None:
), ),
end="", end="",
) )
elif "verify" == args.subcommand:
verify(
working_dir=working_dir,
keyring_root=keyring_root,
sources=args.source,
lint_hokey=args.lint_hokey,
lint_sq_keyring=args.lint_sq_keyring,
)
else: else:
parser.print_help() parser.print_help()

View File

@ -9,6 +9,8 @@ from re import escape
from re import match from re import match
from re import sub from re import sub
from shutil import copytree from shutil import copytree
from subprocess import PIPE
from subprocess import Popen
from tempfile import mkdtemp from tempfile import mkdtemp
from tempfile import mkstemp from tempfile import mkstemp
from typing import Dict from typing import Dict
@ -27,6 +29,7 @@ from .sequoia import packet_split
from .types import Fingerprint from .types import Fingerprint
from .types import Uid from .types import Uid
from .types import Username from .types import Username
from .util import system
def is_pgp_fingerprint(string: str) -> bool: def is_pgp_fingerprint(string: str) -> bool:
@ -977,3 +980,49 @@ def inspect_keyring(working_dir: Path, keyring_root: Path, sources: Optional[Lis
certifications=True, certifications=True,
fingerprints=fingerprints, fingerprints=fingerprints,
) )
def verify(
working_dir: Path,
keyring_root: Path,
sources: Optional[List[Path]],
lint_hokey: bool = True,
lint_sq_keyring: bool = True,
) -> None:
"""Verify certificates against modern expectations using sq-keyring-linter and hokey
Parameters
----------
working_dir: A directory to use for temporary files
keyring_root: The keyring root directory to look up username shorthand sources
sources: A list of username, fingerprint or directories from which to read PGP packet information
(defaults to `keyring_root`)
lint_hokey: Whether to run hokey lint
lint_sq_keyring: Whether to run sq-keyring-linter
"""
if not sources:
sources = [keyring_root]
# transform shorthand paths to actual keyring paths
transform_username_to_keyring_path(keyring_dir=keyring_root / "packager", paths=sources)
transform_fingerprint_to_keyring_path(keyring_root=keyring_root, paths=sources)
cert_paths: Set[Path] = get_cert_paths(sources)
for certificate in sorted(cert_paths):
print(f"Verify {certificate.name} owned by {certificate.parent.name}")
keyring = Path(
mkstemp(dir=working_dir, prefix=f"{certificate.parent.name}-{certificate.name}", suffix=".asc")[1]
).absolute()
export(
working_dir=working_dir,
keyring_root=keyring_root,
sources=[certificate],
output=keyring,
)
if lint_hokey:
keyring_fd = Popen(("sq", "dearmor", f"{str(keyring)}"), stdout=PIPE)
print(system(["hokey", "lint"], _stdin=keyring_fd.stdout), end="")
if lint_sq_keyring:
print(system(["sq-keyring-linter", f"{str(keyring)}"]), end="")

View File

@ -7,13 +7,16 @@ from os import chdir
from os import getcwd from os import getcwd
from pathlib import Path from pathlib import Path
from re import split from re import split
from subprocess import PIPE from subprocess import STDOUT
from subprocess import CalledProcessError from subprocess import CalledProcessError
from subprocess import check_output from subprocess import check_output
from sys import exit from sys import exit
from sys import stderr from sys import stderr
from traceback import print_stack from traceback import print_stack
from typing import IO
from typing import AnyStr
from typing import List from typing import List
from typing import Optional
from typing import Union from typing import Union
@ -77,12 +80,13 @@ def natural_sort_path(_list: Iterable[Path]) -> Iterable[Path]:
return sorted(_list, key=alphanum_key) return sorted(_list, key=alphanum_key)
def system(cmd: List[str], exit_on_error: bool = False) -> str: def system(cmd: List[str], _stdin: Optional[IO[AnyStr]] = None, exit_on_error: bool = False) -> str:
"""Execute a command using check_output """Execute a command using check_output
Parameters Parameters
---------- ----------
cmd: A list of strings to be fed to check_output 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) exit_on_error: Whether to exit the script when encountering an error (defaults to False)
Raises Raises
@ -95,9 +99,9 @@ def system(cmd: List[str], exit_on_error: bool = False) -> str:
""" """
try: try:
return check_output(cmd, stderr=PIPE).decode() return check_output(cmd, stderr=STDOUT, stdin=_stdin).decode()
except CalledProcessError as e: except CalledProcessError as e:
stderr.buffer.write(e.stderr) stderr.buffer.write(e.stdout)
print_stack() print_stack()
if exit_on_error: if exit_on_error:
exit(e.returncode) exit(e.returncode)