From: Luca Boccassi Date: Sat, 10 Feb 2024 12:27:11 +0000 (+0000) Subject: SecureBoot: add support for signing with an hardware token X-Git-Tag: v21~12^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8a8f65424224b4b9134ae56eec164609b02eb171;p=thirdparty%2Fmkosi.git SecureBoot: add support for signing with an hardware token Use ukify/sbsigntools native support for engines/providers Co-authored-by: Daan De Meyer --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index ababf3adf..ebaf49906 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -33,6 +33,7 @@ from mkosi.config import ( ConfigFeature, DocFormat, JsonEncoder, + KeySource, ManifestFormat, Network, OutputFormat, @@ -858,22 +859,28 @@ def sign_efi_binary(context: Context, input: Path, output: Path) -> None: find_binary("sbsign", root=context.config.tools()) is not None ): with open(output, "wb") as f: + cmd: list[PathString] = [ + "sbsign", + "--key", context.config.secure_boot_key, + "--cert", context.config.secure_boot_certificate, + "--output", "/dev/stdout", + ] + options: list[PathString] = [ + "--ro-bind", context.config.secure_boot_certificate, context.config.secure_boot_certificate, + "--ro-bind", input, input, + ] + if context.config.secure_boot_key_source.type == KeySource.Type.engine: + cmd += ["--engine", context.config.secure_boot_key_source.source] + if context.config.secure_boot_key.exists(): + options += ["--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key] + cmd += [input] run( - [ - "sbsign", - "--key", context.config.secure_boot_key, - "--cert", context.config.secure_boot_certificate, - "--output", "/dev/stdout", - input, - ], + cmd, stdout=f, sandbox=context.sandbox( - options=[ - "--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key, - "--ro-bind", context.config.secure_boot_certificate, context.config.secure_boot_certificate, - "--ro-bind", input, input, - ] - ), + options=options, + devices=context.config.secure_boot_key_source.type != KeySource.Type.file, + ) ) elif ( context.config.secure_boot_sign_tool == SecureBootSignTool.pesign or @@ -991,26 +998,31 @@ def install_systemd_boot(context: Context) -> None: # We reuse the key for all secure boot databases to keep things simple. for db in ["PK", "KEK", "db"]: with umask(~0o600), open(keys / f"{db}.auth", "wb") as f: + cmd: list[PathString] = [ + "sbvarsign", + "--attr", + "NON_VOLATILE,BOOTSERVICE_ACCESS,RUNTIME_ACCESS,TIME_BASED_AUTHENTICATED_WRITE_ACCESS", + "--key", context.config.secure_boot_key, + "--cert", context.config.secure_boot_certificate, + "--output", "/dev/stdout", + ] + options: list[PathString] = [ + "--ro-bind", + context.config.secure_boot_certificate, + context.config.secure_boot_certificate, + "--ro-bind", context.workspace / "mkosi.esl", context.workspace / "mkosi.esl", + ] + if context.config.secure_boot_key_source.type == KeySource.Type.engine: + cmd += ["--engine", context.config.secure_boot_key_source.source] + if context.config.secure_boot_key.exists(): + options += ["--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key] + cmd += [db, context.workspace / "mkosi.esl"] run( - [ - "sbvarsign", - "--attr", - "NON_VOLATILE,BOOTSERVICE_ACCESS,RUNTIME_ACCESS,TIME_BASED_AUTHENTICATED_WRITE_ACCESS", - "--key", context.config.secure_boot_key, - "--cert", context.config.secure_boot_certificate, - "--output", "/dev/stdout", - db, - context.workspace / "mkosi.esl", - ], + cmd, stdout=f, sandbox=context.sandbox( - options=[ - "--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key, - "--ro-bind", - context.config.secure_boot_certificate, - context.config.secure_boot_certificate, - "--ro-bind", context.workspace / "mkosi.esl", context.workspace / "mkosi.esl", - ], + options=options, + devices=context.config.secure_boot_key_source.type != KeySource.Type.file, ), ) @@ -1887,9 +1899,12 @@ def build_uki( context.config.secure_boot_certificate, ] options += [ - "--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key, "--ro-bind", context.config.secure_boot_certificate, context.config.secure_boot_certificate, ] + if context.config.secure_boot_key_source.type == KeySource.Type.engine: + cmd += ["--signing-engine", context.config.secure_boot_key_source.source] + if context.config.secure_boot_key.exists(): + options += ["--ro-bind", context.config.secure_boot_key, context.config.secure_boot_key] else: pesign_prepare(context) cmd += [ @@ -1924,7 +1939,13 @@ def build_uki( options += ["--ro-bind", initrd, initrd] with complete_step(f"Generating unified kernel image for kernel version {kver}"): - run(cmd, sandbox=context.sandbox(options=options)) + run( + cmd, + sandbox=context.sandbox( + options=options, + devices=context.config.secure_boot_key_source.type != KeySource.Type.file, + ), + ) def want_efi(config: Config) -> bool: diff --git a/mkosi/config.py b/mkosi/config.py index 40e647707..119516172 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -482,6 +482,13 @@ def parse_path(value: str, return path +def config_parse_key(value: Optional[str], old: Optional[str]) -> Optional[Path]: + if not value: + return None + + return parse_path(value, secret=True) if Path(value).exists() else Path(value) + + def make_tree_parser(absolute: bool = True) -> Callable[[str], ConfigTree]: def parse_tree(value: str) -> ConfigTree: src, sep, tgt = value.partition(':') @@ -994,6 +1001,32 @@ def config_parse_minimum_version(value: Optional[str], old: Optional[GenericVers return max(old, new) +@dataclasses.dataclass(frozen=True) +class KeySource: + class Type(StrEnum): + file = enum.auto() + engine = enum.auto() + + type: Type + source: str = "" + + def __str__(self) -> str: + return f"{self.type}:{self.source}" if self.source else str(self.type) + + +def config_parse_key_source(value: Optional[str], old: Optional[KeySource]) -> Optional[KeySource]: + if not value: + return old + + typ, _, source = value.partition(":") + try: + type = KeySource.Type(typ) + except ValueError: + die(f"'{value}' is not a valid key source") + + return KeySource(type=type, source=source) + + @dataclasses.dataclass(frozen=True) class ConfigSetting: dest: str @@ -1290,6 +1323,7 @@ class Config: secure_boot: bool secure_boot_auto_enroll: bool secure_boot_key: Optional[Path] + secure_boot_key_source: KeySource secure_boot_certificate: Optional[Path] secure_boot_sign_tool: SecureBootSignTool verity_key: Optional[Path] @@ -2309,11 +2343,19 @@ SETTINGS = ( ), ConfigSetting( dest="secure_boot_key", - metavar="PATH", + metavar="KEY", section="Validation", - parse=config_make_path_parser(secret=True), + parse=config_parse_key, paths=("mkosi.key",), - help="UEFI SecureBoot private key in PEM format", + help="UEFI SecureBoot private key", + ), + ConfigSetting( + dest="secure_boot_key_source", + section="Validation", + metavar="SOURCE[:ENGINE]", + parse=config_parse_key_source, + default=KeySource(type=KeySource.Type.file), + help="The source to use to retrieve the secure boot signing key", ), ConfigSetting( dest="secure_boot_certificate", @@ -3603,6 +3645,7 @@ def summary(config: Config) -> str: UEFI SecureBoot: {yes_no(config.secure_boot)} UEFI SecureBoot AutoEnroll: {yes_no(config.secure_boot_auto_enroll)} SecureBoot Signing Key: {none_to_none(config.secure_boot_key)} + SecureBoot Signing Key Source: {config.secure_boot_key_source} SecureBoot Certificate: {none_to_none(config.secure_boot_certificate)} SecureBoot Sign Tool: {config.secure_boot_sign_tool} Verity Signing Key: {none_to_none(config.verity_key)} @@ -3746,6 +3789,10 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[ ) -> Optional[GenericVersion]: return GenericVersion(version) if version is not None else None + def key_source_transformer(keysource: dict[str, Any], fieldtype: type[KeySource]) -> KeySource: + assert "type" in keysource + return KeySource(type=KeySource.Type(keysource["type"]), source=keysource.get("source", "")) + transformers = { Path: path_transformer, Optional[Path]: optional_path_transformer, @@ -3772,6 +3819,7 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[ GenericVersion: generic_version_transformer, Cacheonly: enum_transformer, Network: enum_transformer, + KeySource: key_source_transformer, } def json_transformer(key: str, val: Any) -> Any: diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 68fed222a..f8eb980db 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -1336,7 +1336,13 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, `SecureBootKey=`, `--secure-boot-key=` : Path to the PEM file containing the secret key for signing the - UEFI kernel image, if `SecureBoot=` is used. + UEFI kernel image, if `SecureBoot=` is used. When `SecureBootKeySource=` is specified, the input + type depends on the source. + +`SecureBootKeySource=`, `--secure-boot-key-source=` + +: Source of `SecureBootKey=`, to support OpenSSL engines. E.g.: + `--secure-boot-key-source=engine:pkcs11` `SecureBootCertificate=`, `--secure-boot-certificate=` diff --git a/tests/test_json.py b/tests/test_json.py index 001a116b4..12c1a01cc 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -19,6 +19,7 @@ from mkosi.config import ( ConfigFeature, ConfigTree, DocFormat, + KeySource, ManifestFormat, Network, OutputFormat, @@ -260,6 +261,10 @@ def test_config() -> None: "SecureBootAutoEnroll": true, "SecureBootCertificate": null, "SecureBootKey": "/path/to/keyfile", + "SecureBootKeySource": { + "source": "", + "type": "file" + }, "SecureBootSignTool": "pesign", "Seed": "7496d7d8-7f08-4a2b-96c6-ec8c43791b60", "ShimBootloader": "none", @@ -411,6 +416,7 @@ def test_config() -> None: secure_boot_auto_enroll = True, secure_boot_certificate = None, secure_boot_key = Path("/path/to/keyfile"), + secure_boot_key_source = KeySource(type=KeySource.Type.file), secure_boot_sign_tool = SecureBootSignTool.pesign, seed = uuid.UUID("7496d7d8-7f08-4a2b-96c6-ec8c43791b60"), selinux_relabel = ConfigFeature.disabled,