From: Joerg Behrmann Date: Mon, 18 Jan 2021 18:27:16 +0000 (+0100) Subject: mkosi: add command genkey X-Git-Tag: v10~74^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=refs%2Fpull%2F633%2Fhead;p=thirdparty%2Fmkosi.git mkosi: add command genkey --- diff --git a/.gitignore b/.gitignore index 970c32fd4..cf0626733 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ /mkosi.nspawn /mkosi.rootpw /mkosi.default +/mkosi.secure-boot.key +/mkosi.secure-boot.crt __pycache__ diff --git a/NEWS.md b/NEWS.md index c73ea6381..79012dc1f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,10 @@ - `--source-file-transfer` and `--source-file-transfer-final` now accept the empty string as an argument which can be used to override a previously set value. +- Add a new command `genkey` to mkosi that can be used to generate secure boot + keys for usage with mkosi's `--secure-boot` options. The number of days the + keys should remain valid can be specified via `--secure-boot-valid-days=` and + their CN via `--secure-boot-common-name=` ## v9 diff --git a/mkosi.md b/mkosi.md index de1ef349e..14ef7bff8 100644 --- a/mkosi.md +++ b/mkosi.md @@ -296,6 +296,17 @@ details see the table below. : Path to the X.509 file containing the certificate for the signed UEFI kernel image, if `--secure-boot` is used. +`--secure-boot-common-name=` + +: Common name to be used when generating SecureBoot keys via mkosi's `genkey` + command. Defaults to `mkosi of %u`, where `%u` expands to the username of the + user invoking mkosi. + +`--secure-boot-valid-days=` + +: Number of days that the keys should remain valid when generating SecureBoot + keys via mkosi's `genkey` command. Defaults to two years (730 days). + `--read-only` : Make root file system read-only. Only applies to `gpt_ext4`, @@ -807,6 +818,8 @@ which settings file options. | `--secure-boot` | `[Output]` | `SecureBoot=` | | `--secure-boot-key=` | `[Output]` | `SecureBootKey=` | | `--secure-boot-certificate=` | `[Output]` | `SecureBootCertificate=` | +| `--secure-boot-valid-days=` | `[Output]` | `SecureBootValidDays=` | +| `--secure-boot-common-name=` | `[Output]` | `SecureBootCommonName=` | | `--read-only` | `[Output]` | `ReadOnly=` | | `--encrypt=` | `[Output]` | `Encrypt=` | | `--verity=` | `[Output]` | `Verity=` | diff --git a/mkosi/__init__.py b/mkosi/__init__.py index e582415a0..e72e4b2a6 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -8,6 +8,7 @@ import copy import crypt import ctypes import ctypes.util +import datetime import enum import errno import fcntl @@ -76,7 +77,7 @@ else: MKOSI_COMMANDS_CMDLINE = ("build", "shell", "boot", "qemu", "ssh") MKOSI_COMMANDS_NEED_BUILD = ("shell", "boot", "qemu") MKOSI_COMMANDS_SUDO = ("build", "clean", "shell", "boot", "qemu") -MKOSI_COMMANDS = ("build", "clean", "help", "summary") + MKOSI_COMMANDS_CMDLINE +MKOSI_COMMANDS = ("build", "clean", "help", "summary", "genkey") + MKOSI_COMMANDS_CMDLINE DRACUT_SYSTEMD_EXTRAS = [ "/usr/lib/systemd/systemd-veritysetup", @@ -4258,6 +4259,9 @@ class ArgumentParserMkosi(argparse.ArgumentParser): continue # replace arguments referencing files with the file content try: + # This used to use configparser.ConfigParser before, but + # ConfigParser's interpolation clashes with systemd style + # specifier, e.g. %u for user, since both use % as a sigil. config = configparser.RawConfigParser(delimiters="=") config.optionxform = str # type: ignore with open(arg_string[1:]) as args_file: @@ -4367,6 +4371,18 @@ def create_parser() -> ArgumentParserMkosi: ) group.add_argument("--secure-boot-key", help="UEFI SecureBoot private key in PEM format", metavar="PATH") group.add_argument("--secure-boot-certificate", help="UEFI SecureBoot certificate in X509 format", metavar="PATH") + group.add_argument( + "--secure-boot-valid-days", + help="Number of days UEFI SecureBoot keys should be valid when generating keys", + metavar="DAYS", + default="730", + ) + group.add_argument( + "--secure-boot-common-name", + help="Template for the UEFI SecureBoot CN when generating keys", + metavar="CN", + default="mkosi of %u", + ) group.add_argument( "--read-only", action=BooleanAction, @@ -6334,6 +6350,57 @@ def run_ssh(args: CommandLineArguments) -> None: ) +def generate_secure_boot_key(args: CommandLineArguments) -> NoReturn: + """Generate secure boot keys using openssl""" + args.secure_boot_key = args.secure_boot_key or "./mkosi.secure-boot.key" + args.secure_boot_certificate = args.secure_boot_certificate or "./mkosi.secure-boot.crt" + + keylength = 2048 + expiration_date = datetime.date.today() + datetime.timedelta(int(args.secure_boot_valid_days)) + cn = expand_specifier(args.secure_boot_common_name) + + for f in (args.secure_boot_key, args.secure_boot_certificate): + if os.path.exists(f) and not args.force: + die( + dedent( + f"""\ + {f} already exists. + If you are sure you want to generate new secure boot keys + remove {args.secure_boot_key} and {args.secure_boot_certificate} first. + """ + ) + ) + + MkosiPrinter.print_step(f"Generating secure boot keys rsa:{keylength} for CN `{cn}`.") + MkosiPrinter.info( + dedent( + f""" + The keys will expire in {args.secure_boot_valid_days} days ({expiration_date:%A %d. %B %Y}). + Remember to roll them over to new ones before then. + """ + ) + ) + + cmd = [ + "openssl", + "req", + "-new", + "-x509", + "-newkey", + f"rsa:{keylength}", + "-keyout", + args.secure_boot_key, + "-out", + args.secure_boot_certificate, + "-days", + str(args.secure_boot_valid_days), + "-subj", + f"/CN={cn}/", + ] + + os.execvp(cmd[0], cmd) + + def expand_paths(paths: List[str]) -> List[str]: if not paths: return [] @@ -6370,6 +6437,12 @@ def prepend_to_environ_path(paths: List[str]) -> None: os.environ["PATH"] = new_path + ":" + original_path +def expand_specifier(s: str) -> str: + user = os.getenv("SUDO_USER") or os.getenv("USER") + assert user is not None + return s.replace("%u", user) + + def needs_build(args: CommandLineArguments) -> bool: return args.verb == "build" or (not os.path.exists(args.output) and args.verb in MKOSI_COMMANDS_NEED_BUILD) @@ -6379,6 +6452,9 @@ def run_verb(args: CommandLineArguments) -> None: prepend_to_environ_path(args.extra_search_paths) + if args.verb == "genkey": + generate_secure_boot_key(args) + if args.verb in MKOSI_COMMANDS_SUDO: check_root() unlink_output(args) diff --git a/tests/test_config_parser.py b/tests/test_config_parser.py index f85f141f0..a41503905 100644 --- a/tests/test_config_parser.py +++ b/tests/test_config_parser.py @@ -96,6 +96,8 @@ class MkosiConfig(object): "secure_boot": False, "secure_boot_certificate": None, "secure_boot_key": None, + "secure_boot_common_name": "mkosi of %u", + "secure_boot_valid_days": "730", "sign": False, "skeleton_trees": [], "source_file_transfer": None, @@ -210,6 +212,10 @@ class MkosiConfig(object): self.reference_config[job_name]["secure_boot_key"] = mk_config_output["SecureBootKey"] if "SecureBootCertificate" in mk_config_output: self.reference_config[job_name]["secure_boot_certificate"] = mk_config_output["SecureBootCertificate"] + if "SecureBootCommonName" in mk_config_output: + self.reference_config[job_name]["secure_boot_common_name"] = mk_config_output["SecureBootCommonName"] + if "SecureBootValidDays" in mk_config_output: + self.reference_config[job_name]["secure_boot_valid_days"] = mk_config_output["SecureBootValidDays"] if "ReadOnly" in mk_config_output: self.reference_config[job_name]["read_only"] = mk_config_output["ReadOnly"] if "Encrypt" in mk_config_output: @@ -490,6 +496,8 @@ class MkosiConfigManyParams(MkosiConfigOne): "SecureBoot": False, "SecureBootKey": "/foo.pem", "SecureBootCertificate": "bar.crt", + "SecureBootCommonName": "mkosi for %u", + "SecureBootValidDays": "730", "ReadOnly": False, "Encrypt": "all", "Verity": False,