From 54aeac21c9559538d26486e1e7dba31c4935ed63 Mon Sep 17 00:00:00 2001 From: Joerg Behrmann Date: Mon, 3 Oct 2022 16:55:57 +0200 Subject: [PATCH] Make SignExpectedPCR tri-valued This makes SignEpectedPCR a tri-valued option using aspecial value "auto" in addition to boolean values, where it will check whether cryptography is importable and systemd-measure in the PATH and value True in that case and False else. For a True value it checks both conditions and fails hard if they are not met. The checks are kept in the CLI definition so that what comes out stays a clean boolean value and doesn't leak any decisions into layers further down. This unfortunately necesitates a custom default value in the tests, so that they are robust against what's installed on the system they run on and also needs to use a function for the argparse type=, since actions are not called for every value. --- mkosi.md | 8 ++--- mkosi/__init__.py | 61 +++++++++++++++++++++++++++++++------ tests/test_config_parser.py | 11 ++++++- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/mkosi.md b/mkosi.md index 76c29b492..8353e95bb 100644 --- a/mkosi.md +++ b/mkosi.md @@ -566,10 +566,10 @@ a boolean argument: either "1", "yes", or "true" to enable, or "0", : Measure the components of the unified kernel image (UKI) using `systemd-measure` and embed the PCR signature into the unified kernel - image. - - This option requires the [`cryptography`](https://cryptography.io/) - module. + image. This option takes a boolean value or the special value `auto`, + which is the default, which is equal to a true value if the + [`cryptography`](https://cryptography.io/) module is importable and + the `systemd-measure` binary is in `PATH`. `CompressFs=`, `--compress-fs=` diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 7a6175990..234a7f206 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -3992,14 +3992,8 @@ def install_unified_kernel( # systemd-measure binary around, then also include a # signature of expected PCR 11 values in the kernel image if state.config.secure_boot and state.config.sign_expected_pcr: - try: - from cryptography import x509 - from cryptography.hazmat.primitives import serialization - except ImportError: - die("Couldn't import the cryptography Python module. This is needed for the --sign-expected-pcr option.") - - if not shutil.which('systemd-measure'): - die("Couldn't find systemd-measure binary. It is needed for the --sign-expected-pcr option.") + from cryptography import x509 + from cryptography.hazmat.primitives import serialization with complete_step("Generating PCR 11 signature…"): # Extract the public key from the SecureBoot certificate @@ -4831,6 +4825,52 @@ class VerityAction(BooleanAction): super().__call__(parser, namespace, values, option_string) +def parse_sign_expected_pcr(value: Union[bool, str]) -> bool: + if isinstance(value, bool): + return value + + if value == "auto": + try: + # TODO: pyright stumbles over this with + # "import_module" is not a known member of module (reportGeneralTypeIssues) + # although importlib.import_module exists in Python 3.7 + # a regular import trips pyflakes, though and I haven't found a way + # to silence that + importlib.import_module("cryptography") # type: ignore + return True if shutil.which('systemd-measure') else False + except ImportError: + return False + + val = parse_boolean(value) + if val: + try: + importlib.import_module("cryptography") # type: ignore + except ImportError: + die("Couldn't import the cryptography Python module. This is needed for the --sign-expected-pcr option.") + + if not shutil.which('systemd-measure'): + die("Couldn't find systemd-measure binary. It is needed for the --sign-expected-pcr option.") + + return val + + +class SignExpectedPcrAction(BooleanAction): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None, bool], + option_string: Optional[str] = None, + ) -> None: + if values is None: + parsed = False + elif isinstance(values, bool) or isinstance(values, str): + parsed = parse_sign_expected_pcr(values) + else: + raise argparse.ArgumentError(self, f"Invalid argument for {option_string}: {values}") + setattr(namespace, self.dest, parsed) + + class CustomHelpFormatter(argparse.HelpFormatter): def _format_action_invocation(self, action: argparse.Action) -> str: if not action.option_strings or action.nargs == 0: @@ -5212,7 +5252,10 @@ def create_parser() -> ArgumentParserMkosi: ) group.add_argument( "--sign-expected-pcr", - action=BooleanAction, + metavar="BOOL", + default="auto", + action=SignExpectedPcrAction, + type=parse_sign_expected_pcr, help="Measure the components of the unified kernel image (UKI) and embed the PCR signature into the UKI", ) group.add_argument( diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index a3e33f0d7..68a41a297 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -3,7 +3,9 @@ import configparser import contextlib import copy +import importlib import os +import shutil from pathlib import Path from typing import Any, Dict, Generator, List, Mapping, Optional @@ -36,6 +38,13 @@ class MkosiConfig: self.cli_arguments = [] self.reference_config = {} + def local_sign_expected_pcr_default(self) -> bool: + try: + importlib.import_module("cryptography") + return True if shutil.which('systemd-measure') else False + except ImportError: + return False + def add_reference_config(self, job_name: str = DEFAULT_JOB_NAME) -> None: """create one initial reference configuration @@ -128,7 +137,7 @@ class MkosiConfig: "bios_size": None, "verb": Verb.build, "verity": False, - "sign_expected_pcr": False, + "sign_expected_pcr": self.local_sign_expected_pcr_default(), "with_docs": False, "with_network": False, "with_tests": True, -- 2.47.2