diff options
author | Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl> | 2023-06-14 17:57:24 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-14 17:57:24 +0200 |
commit | 2b8628c7049bf99466e3e446552f81abcfa87aee (patch) | |
tree | e681ea029384e03e009fcdc0a2bf20b7dc0bd6fe | |
parent | coverage: fix build with g++ (diff) | |
parent | ukify: make the certficate validity configurable (diff) | |
download | systemd-2b8628c7049bf99466e3e446552f81abcfa87aee.tar.xz systemd-2b8628c7049bf99466e3e446552f81abcfa87aee.zip |
Merge pull request #27946 from keszybz/ukify-genkey-verb
Add 'genkey' verb to ukify
-rw-r--r-- | man/uki.conf.example | 14 | ||||
-rw-r--r-- | man/ukify.xml | 164 | ||||
-rwxr-xr-x | src/kernel-install/60-ukify.install.in | 2 | ||||
-rwxr-xr-x | src/ukify/test/test_ukify.py | 70 | ||||
-rwxr-xr-x | src/ukify/ukify.py | 259 |
5 files changed, 408 insertions, 101 deletions
diff --git a/man/uki.conf.example b/man/uki.conf.example new file mode 100644 index 0000000000..84a9f77b8d --- /dev/null +++ b/man/uki.conf.example @@ -0,0 +1,14 @@ +[UKI] +SecureBootPrivateKey=/etc/kernel/secure-boot.key.pem +SecureBootCertificate=/etc/kernel/secure-boot.cert.pem + +[PCRSignature:initrd] +Phases=enter-initrd +PCRPrivateKey=/etc/kernel/pcr-initrd.key.pem +PCRPublicKey=/etc/kernel/pcr-initrd.pub.pem + +[PCRSignature:system] +Phases=enter-initrd:leave-initrd enter-initrd:leave-initrd:sysinit + enter-initrd:leave-initrd:sysinit:ready +PCRPrivateKey=/etc/kernel/pcr-system.key.pem +PCRPublicKey=/etc/kernel/pcr-system.pub.pem diff --git a/man/ukify.xml b/man/ukify.xml index 098dacfb99..06ae550530 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -25,6 +25,7 @@ <command>/usr/lib/systemd/ukify</command> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg choice="plain">build</arg> + <arg choice="plain">genkey</arg> </cmdsynopsis> </refsynopsisdiv> @@ -34,60 +35,83 @@ <para>Note: this command is experimental for now. While it is intended to become a regular component of systemd, it might still change in behaviour and interface.</para> - <para><command>ukify</command> is a tool that combines components (usually a kernel, an initrd, and a - UEFI boot stub) to create a + <para><command>ukify</command> is a tool whose primary purpose is to combine components (usually a + kernel, an initrd, and a UEFI boot stub) to create a <ulink url="https://uapi-group.org/specifications/specs/unified_kernel_image/">Unified Kernel Image (UKI)</ulink> — a PE binary that can be executed by the firmware to start the embedded linux kernel. See <citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry> for details about the stub.</para> + </refsect1> + + <refsect1> + <title>Commands</title> + + <para>The following commands are understood:</para> + + <refsect2> + <title><command>build</command></title> + + <para>This command creates a Unified Kernel Image. The two primary options that should be specified for + the <command>build</command> verb are <varname>Linux=</varname>/<option>--linux=</option>, and + <varname>Initrd=</varname>/<option>--initrd=</option>. <varname>Initrd=</varname> accepts multiple + whitespace-separated paths and <option>--initrd=</option> can be specified multiple times.</para> + + <para>Additional sections will be inserted into the UKI, either automatically or only if a specific + option is provided. See the discussions of + <varname>Cmdline=</varname>/<option>--cmdline=</option>, + <varname>OSRelease=</varname>/<option>--os-release=</option>, + <varname>DeviceTree=</varname>/<option>--devicetree=</option>, + <varname>Splash=</varname>/<option>--splash=</option>, + <varname>PCRPKey=</varname>/<option>--pcrpkey=</option>, + <varname>Uname=</varname>/<option>--uname=</option>, + <varname>SBAT=</varname>/<option>--sbat=</option>, + and <option>--section=</option> + below.</para> + + <para><command>ukify</command> can also be used to assemble a PE binary that is not executable but + contains auxiliary data, for example additional kernel command line entries.</para> + + <para>If PCR signing keys are provided via the + <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option> and + <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> options, PCR values that will be seen + after booting with the given kernel, initrd, and other sections, will be calculated, signed, and embedded + in the UKI. + <citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry> is + used to perform this calculation and signing.</para> + + <para>The calculation of PCR values is done for specific boot phase paths. Those can be specified with + the <varname>Phases=</varname>/<option>--phases=</option> option. If not specified, the default provided + by <command>systemd-measure</command> is used. It is also possible to specify the + <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>, + <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option>, and + <varname>Phases=</varname>/<option>--phases=</option> arguments more than once. Signatures will then be + performed with each of the specified keys. On the command line, when both <option>--phases=</option> and + <option>--pcr-private-key=</option> are used, they must be specified the same number of times, and then + the n-th boot phase path set will be signed by the n-th key. This can be used to build different trust + policies for different phases of the boot. In the config file, <varname>PCRPrivateKey=</varname>, + <varname>PCRPublicKey=</varname>, and <varname>Phases=</varname> are grouped into separate sections, + describing separate boot phases.</para> + + <para>If a SecureBoot signing key is provided via the + <varname>SecureBootPrivateKey=</varname>/<option>--secureboot-private-key=</option> option, the resulting + PE binary will be signed as a whole, allowing the resulting UKI to be trusted by SecureBoot. Also see the + discussion of automatic enrollment in + <citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry>. + </para> + </refsect2> + + <refsect2> + <title><command>genkey</command></title> - <para>The two primary options that should be specified for the <command>build</command> verb are - <varname>Linux=</varname>/<option>--linux=</option>, and - <varname>Initrd=</varname>/<option>--initrd=</option>. <varname>Initrd=</varname> accepts multiple - whitespace-separated paths and <option>--initrd=</option> can be specified multiple times.</para> - - <para>Additional sections will be inserted into the UKI, either automatically or only if a specific - option is provided. See the discussions of - <varname>Cmdline=</varname>/<option>--cmdline=</option>, - <varname>OSRelease=</varname>/<option>--os-release=</option>, - <varname>DeviceTree=</varname>/<option>--devicetree=</option>, - <varname>Splash=</varname>/<option>--splash=</option>, - <varname>PCRPKey=</varname>/<option>--pcrpkey=</option>, - <varname>Uname=</varname>/<option>--uname=</option>, - <varname>SBAT=</varname>/<option>--sbat=</option>, - and <option>--section=</option> - below.</para> - - <para><command>ukify</command> can also be used to assemble a PE binary that is not executable but - contains auxiliary data, for example additional kernel command line entries.</para> - - <para>If PCR signing keys are provided via the - <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option> and - <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> options, PCR values that will be seen - after booting with the given kernel, initrd, and other sections, will be calculated, signed, and embedded - in the UKI. - <citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry> is - used to perform this calculation and signing.</para> - - <para>The calculation of PCR values is done for specific boot phase paths. Those can be specified with - the <varname>Phases=</varname>/<option>--phases=</option> option. If not specified, the default provided - by <command>systemd-measure</command> is used. It is also possible to specify the - <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>, - <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option>, and - <varname>Phases=</varname>/<option>--phases=</option> arguments more than once. Signatures will then be - performed with each of the specified keys. On the command line, when both <option>--phases=</option> and - <option>--pcr-private-key=</option> are used, they must be specified the same number of times, and then - the n-th boot phase path set will be signed by the n-th key. This can be used to build different trust - policies for different phases of the boot. In the config file, <varname>PCRPrivateKey=</varname>, - <varname>PCRPublicKey=</varname>, and <varname>Phases=</varname> are grouped into separate sections, - describing separate boot phases.</para> - - <para>If a SecureBoot signing key is provided via the - <varname>SecureBootPrivateKey=</varname>/<option>--secureboot-private-key=</option> option, the resulting - PE binary will be signed as a whole, allowing the resulting UKI to be trusted by SecureBoot. Also see the - discussion of automatic enrollment in - <citerefentry><refentrytitle>systemd-boot</refentrytitle><manvolnum>7</manvolnum></citerefentry>. - </para> + <para>This command creates the keys for PCR signing and the key and certificate used for SecureBoot + signing. The same configuration options that determine what keys and in which paths will be needed for + signing when <command>build</command> is used, here determine which keys will be created. See the + discussion of <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>, + <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option>, and + <varname>SecureBootPrivateKey=</varname>/<option>--secureboot-private-key=</option> below.</para> + + <para>The output files must not exist.</para> + </refsect2> </refsect1> <refsect1> @@ -306,6 +330,14 @@ </varlistentry> <varlistentry> + <term><varname>SecureBootCertificateValidity=<replaceable>DAYS</replaceable></varname></term> + <term><option>--secureboot-certificate-validity=<replaceable>DAYS</replaceable></option></term> + + <listitem><para>Period of validity (in days) for a certificate created by + <command>genkey</command>. Defaults to 3650, i.e. 10 years.</para></listitem> + </varlistentry> + + <varlistentry> <term><varname>SigningEngine=<replaceable>ENGINE</replaceable></varname></term> <term><option>--signing-engine=<replaceable>ENGINE</replaceable></option></term> @@ -415,7 +447,7 @@ <example> <title>All the bells and whistles</title> - <programlisting># /usr/lib/systemd/ukify build \ + <programlisting>$ /usr/lib/systemd/ukify build \ --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \ --initrd=early_cpio \ --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \ @@ -472,7 +504,7 @@ Phases=enter-initrd:leave-initrd enter-initrd:leave-initrd:sysinit enter-initrd:leave-initrd:sysinit:ready -# /usr/lib/systemd/ukify -c ukify.conf build \ +$ /usr/lib/systemd/ukify -c ukify.conf build \ --linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \ --initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img </programlisting> @@ -498,6 +530,36 @@ Phases=enter-initrd:leave-initrd <para>This creates a signed PE binary that contains the additional kernel command line parameter <literal>debug</literal> with SBAT metadata referring to the owner of the addon.</para> </example> + + <example> + <title>Decide signing policy and create certificate and keys</title> + + <para>First, let's create an config file that specifies what signatures shall be made:</para> + + <programlisting># cat >/etc/kernel/uki.conf <<EOF +<xi:include href="uki.conf.example" parse="text" />EOF</programlisting> + + <para>Next, we can generate the certificate and keys:</para> + <programlisting># /usr/lib/systemd/ukify genkey --config=/etc/kernel/uki.conf +Writing SecureBoot private key to /etc/kernel/secure-boot.key.pem +Writing SecureBoot certicate to /etc/kernel/secure-boot.cert.pem +Writing private key for PCR signing to /etc/kernel/pcr-initrd.key.pem +Writing public key for PCR signing to /etc/kernel/pcr-initrd.pub.pem +Writing private key for PCR signing to /etc/kernel/pcr-system.key.pem +Writing public key for PCR signing to /etc/kernel/pcr-system.pub.pem +</programlisting> + + <para>(Both operations need to be done as root to allow write access + to <filename>/etc/kernel/</filename>.)</para> + + <para>Subsequent invocations of using the config file + (<command>/usr/lib/systemd/ukify build --config=/etc/kernel/uki.conf</command>) + will use this certificate and key files. Note that the + <citerefentry><refentrytitle>kernel-install</refentrytitle><manvolnum>8</manvolnum></citerefentry> + plugin <filename>60-ukify.install</filename> uses <filename>/etc/kernel/uki.conf</filename> + by default, so after this file has been created, installations of kernels that create a UKI on the + local machine using <command>kernel-install</command> would perform signing using this config.</para> + </example> </refsect1> <refsect1> diff --git a/src/kernel-install/60-ukify.install.in b/src/kernel-install/60-ukify.install.in index 0927bd7a2e..96ca2482b0 100755 --- a/src/kernel-install/60-ukify.install.in +++ b/src/kernel-install/60-ukify.install.in @@ -186,7 +186,7 @@ def call_ukify(opts): # Create "empty" namespace. We want to override just a few settings, so it # doesn't make sense to configure everything. We pretend to parse an empty # argument set to prepopulate the namespace with the defaults. - opts2 = ukify['create_parser']().parse_args(()) + opts2 = ukify['create_parser']().parse_args(['build']) opts2.config = config_file_location() opts2.uname = opts.kernel_version diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py index eae82c7f88..a6778bb694 100755 --- a/src/ukify/test/test_ukify.py +++ b/src/ukify/test/test_ukify.py @@ -4,6 +4,7 @@ # pylint: disable=missing-docstring,redefined-outer-name,invalid-name # pylint: disable=unused-import,import-outside-toplevel,useless-else-on-loop # pylint: disable=consider-using-with,wrong-import-position,unspecified-encoding +# pylint: disable=protected-access import base64 import json @@ -87,7 +88,7 @@ def test_apply_config(tmp_path): Phases = {':'.join(ukify.KNOWN_PHASES)} ''')) - ns = ukify.create_parser().parse_args(()) + ns = ukify.create_parser().parse_args(['build']) ns.linux = None ns.initrd = [] ukify.apply_config(ns, config) @@ -106,7 +107,7 @@ def test_apply_config(tmp_path): assert ns.signing_engine == 'engine1' assert ns.sb_key == 'some/path5' assert ns.sb_cert == 'some/path6' - assert ns.sign_kernel == False + assert ns.sign_kernel is False assert ns._groups == ['NAME'] assert ns.pcr_private_keys == [pathlib.Path('some/path7')] @@ -129,7 +130,7 @@ def test_apply_config(tmp_path): assert ns.signing_engine == 'engine1' assert ns.sb_key == 'some/path5' assert ns.sb_cert == 'some/path6' - assert ns.sign_kernel == False + assert ns.sign_kernel is False assert ns._groups == ['NAME'] assert ns.pcr_private_keys == [pathlib.Path('some/path7')] @@ -447,7 +448,7 @@ def test_sections(kernel_initrd, tmpdir): for sect in 'text osrel cmdline linux initrd uname test'.split(): assert re.search(fr'^\s*\d+\s+.{sect}\s+0', dump, re.MULTILINE) -def test_addon(kernel_initrd, tmpdir): +def test_addon(tmpdir): output = f'{tmpdir}/addon.efi' args = [ 'build', @@ -459,7 +460,7 @@ def test_addon(kernel_initrd, tmpdir): args += [f'--stub={stub}'] expected_exceptions = () else: - expected_exceptions = FileNotFoundError, + expected_exceptions = (FileNotFoundError,) opts = ukify.parse_args(args) try: @@ -588,7 +589,7 @@ def test_pcr_signing(kernel_initrd, tmpdir): '--uname=1.2.3', '--cmdline=ARG1 ARG2 ARG3', '--os-release=ID=foobar\n', - '--pcr-banks=sha1', # use sha1 as that is most likely to be supported + '--pcr-banks=sha1', # use sha1 because it doesn't really matter f'--pcrpkey={pub.name}', f'--pcr-public-key={pub.name}', f'--pcr-private-key={priv.name}', @@ -655,7 +656,7 @@ def test_pcr_signing2(kernel_initrd, tmpdir): '--uname=1.2.3', '--cmdline=ARG1 ARG2 ARG3', '--os-release=ID=foobar\n', - '--pcr-banks=sha1', # use sha1 as that is most likely to be supported + '--pcr-banks=sha1', f'--pcrpkey={pub2.name}', f'--pcr-public-key={pub.name}', f'--pcr-private-key={priv.name}', @@ -698,5 +699,60 @@ def test_pcr_signing2(kernel_initrd, tmpdir): assert list(sig.keys()) == ['sha1'] assert len(sig['sha1']) == 6 # six items for six phases paths +def test_key_cert_generation(tmpdir): + opts = ukify.parse_args([ + 'genkey', + f"--pcr-public-key={tmpdir / 'pcr1.pub.pem'}", + f"--pcr-private-key={tmpdir / 'pcr1.priv.pem'}", + '--phases=enter-initrd enter-initrd:leave-initrd', + f"--pcr-public-key={tmpdir / 'pcr2.pub.pem'}", + f"--pcr-private-key={tmpdir / 'pcr2.priv.pem'}", + '--phases=sysinit ready', + f"--secureboot-private-key={tmpdir / 'sb.priv.pem'}", + f"--secureboot-certificate={tmpdir / 'sb.cert.pem'}", + ]) + assert opts.verb == 'genkey' + ukify.check_cert_and_keys_nonexistent(opts) + + pytest.importorskip('cryptography') + + ukify.generate_keys(opts) + + if not shutil.which('openssl'): + return + + for key in (tmpdir / 'pcr1.priv.pem', + tmpdir / 'pcr2.priv.pem', + tmpdir / 'sb.priv.pem'): + out = subprocess.check_output([ + 'openssl', 'rsa', + '-in', key, + '-text', + '-noout', + ], text = True) + assert 'Private-Key' in out + assert '2048 bit' in out + + for pub in (tmpdir / 'pcr1.pub.pem', + tmpdir / 'pcr2.pub.pem'): + out = subprocess.check_output([ + 'openssl', 'rsa', + '-pubin', + '-in', pub, + '-text', + '-noout', + ], text = True) + assert 'Public-Key' in out + assert '2048 bit' in out + + out = subprocess.check_output([ + 'openssl', 'x509', + '-in', tmpdir / 'sb.cert.pem', + '-text', + '-noout', + ], text = True) + assert 'Certificate' in out + assert 'Issuer: CN = SecureBoot signing key on host' in out + if __name__ == '__main__': sys.exit(pytest.main(sys.argv)) diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index a9c21601df..3db2bac384 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -25,17 +25,21 @@ import argparse import configparser +import contextlib import collections import dataclasses +import datetime import fnmatch import itertools import json import os import pathlib import pprint +import pydoc import re import shlex import shutil +import socket import subprocess import sys import tempfile @@ -43,6 +47,7 @@ from typing import (Any, Callable, IO, Optional, + Sequence, Union) import pefile # type: ignore @@ -88,6 +93,15 @@ def guess_efi_arch(): return efi_arch +def page(text: str, enabled: Optional[bool]) -> None: + if enabled: + # Initialize less options from $SYSTEMD_LESS or provide a suitable fallback. + os.environ['LESS'] = os.getenv('SYSTEMD_LESS', 'FRSXMK') + pydoc.pager(text) + else: + print(text) + + def shell_join(cmd): # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path. return ' '.join(shlex.quote(str(x)) for x in cmd) @@ -345,6 +359,17 @@ def check_inputs(opts): check_splash(opts.splash) +def check_cert_and_keys_nonexistent(opts): + # Raise if any of the keys and certs are found on disk + paths = itertools.chain( + (opts.sb_key, opts.sb_cert), + *((priv_key, pub_key) + for priv_key, pub_key, _ in key_path_groups(opts))) + for path in paths: + if path and path.exists(): + raise ValueError(f'{path} is present') + + def find_tool(name, fallback=None, opts=None): if opts and opts.tools: for d in opts.tools: @@ -370,6 +395,19 @@ def combine_signatures(pcrsigs): return json.dumps(combined) +def key_path_groups(opts): + if not opts.pcr_private_keys: + return + + n_priv = len(opts.pcr_private_keys) + pub_keys = opts.pcr_public_keys or [None] * n_priv + pp_groups = opts.phase_path_groups or [None] * n_priv + + yield from zip(opts.pcr_private_keys, + pub_keys, + pp_groups) + + def call_systemd_measure(uki, linux, opts): measure_tool = find_tool('systemd-measure', '/usr/lib/systemd/systemd-measure', @@ -403,10 +441,6 @@ def call_systemd_measure(uki, linux, opts): # PCR signing if opts.pcr_private_keys: - n_priv = len(opts.pcr_private_keys or ()) - pp_groups = opts.phase_path_groups or [None] * n_priv - pub_keys = opts.pcr_public_keys or [None] * n_priv - pcrsigs = [] cmd = [ @@ -420,9 +454,7 @@ def call_systemd_measure(uki, linux, opts): for bank in banks), ] - for priv_key, pub_key, group in zip(opts.pcr_private_keys, - pub_keys, - pp_groups): + for priv_key, pub_key, group in key_path_groups(opts): extra = [f'--private-key={priv_key}'] if pub_key: extra += [f'--public-key={pub_key}'] @@ -711,6 +743,119 @@ def make_uki(opts): print(f"Wrote {'signed' if sign_args_present else 'unsigned'} {opts.output}") +ONE_DAY = datetime.timedelta(1, 0, 0) + + +@contextlib.contextmanager +def temporary_umask(mask: int): + # Drop <mask> bits from umask + old = os.umask(0) + os.umask(old | mask) + try: + yield + finally: + os.umask(old) + + +def generate_key_cert_pair( + common_name: str, + valid_days: int, + keylength: int = 2048, +) -> tuple[bytes]: + + from cryptography import x509 + import cryptography.hazmat.primitives as hp + + # We use a keylength of 2048 bits. That is what Microsoft documents as + # supported/expected: + # https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-secure-boot-key-creation-and-management-guidance?view=windows-11#12-public-key-cryptography + + now = datetime.datetime.utcnow() + + key = hp.asymmetric.rsa.generate_private_key( + public_exponent=65537, + key_size=keylength, + ) + cert = x509.CertificateBuilder( + ).subject_name( + x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, common_name)]) + ).issuer_name( + x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, common_name)]) + ).not_valid_before( + now, + ).not_valid_after( + now + ONE_DAY * valid_days + ).serial_number( + x509.random_serial_number() + ).public_key( + key.public_key() + ).add_extension( + x509.BasicConstraints(ca=False, path_length=None), + critical=True, + ).sign( + private_key=key, + algorithm=hp.hashes.SHA256(), + ) + + cert_pem = cert.public_bytes( + encoding=hp.serialization.Encoding.PEM, + ) + key_pem = key.private_bytes( + encoding=hp.serialization.Encoding.PEM, + format=hp.serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=hp.serialization.NoEncryption(), + ) + + return key_pem, cert_pem + + +def generate_priv_pub_key_pair(keylength : int = 2048) -> tuple[bytes]: + import cryptography.hazmat.primitives as hp + + key = hp.asymmetric.rsa.generate_private_key( + public_exponent=65537, + key_size=keylength, + ) + priv_key_pem = key.private_bytes( + encoding=hp.serialization.Encoding.PEM, + format=hp.serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=hp.serialization.NoEncryption(), + ) + pub_key_pem = key.public_key().public_bytes( + encoding=hp.serialization.Encoding.PEM, + format=hp.serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + return priv_key_pem, pub_key_pem + + +def generate_keys(opts): + # This will generate keys and certificates and write them to the paths that + # are specified as input paths. + if opts.sb_key or opts.sb_cert: + fqdn = socket.getfqdn() + cn = f'SecureBoot signing key on host {fqdn}' + key_pem, cert_pem = generate_key_cert_pair( + common_name=cn, + valid_days=opts.sb_cert_validity, + ) + print(f'Writing SecureBoot private key to {opts.sb_key}') + with temporary_umask(0o077): + opts.sb_key.write_bytes(key_pem) + print(f'Writing SecureBoot certicate to {opts.sb_cert}') + opts.sb_cert.write_bytes(cert_pem) + + for priv_key, pub_key, _ in key_path_groups(opts): + priv_key_pem, pub_key_pem = generate_priv_pub_key_pair() + + print(f'Writing private key for PCR signing to {priv_key}') + with temporary_umask(0o077): + priv_key.write_bytes(priv_key_pem) + if pub_key: + print(f'Writing public key for PCR signing to {pub_key}') + pub_key.write_bytes(pub_key_pem) + + @dataclasses.dataclass(frozen=True) class ConfigItem: @staticmethod @@ -843,7 +988,7 @@ class ConfigItem: return (section_name, key, value) -VERBS = ('build',) +VERBS = ('build', 'genkey') CONFIG_ITEMS = [ ConfigItem( @@ -1011,6 +1156,14 @@ uki.addon,1,UKI Addon,uki.addon,1,https://www.freedesktop.org/software/systemd/m help = 'required by --signtool=pesign. pesign needs a certificate nickname of nss certificate database entry to use for PE signing', config_key = 'UKI/SecureBootCertificateName', ), + ConfigItem( + '--secureboot-certificate-validity', + metavar = 'DAYS', + dest = 'sb_cert_validity', + default = 365 * 10, + help = "period of validity (in days) for a certificate created by 'genkey'", + config_key = 'UKI/SecureBootCertificateValidity', + ), ConfigItem( '--sign-kernel', @@ -1128,12 +1281,25 @@ def config_example(): yield f'{key} = {value}' +class PagerHelpAction(argparse._HelpAction): # pylint: disable=protected-access + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Union[str, Sequence[Any], None] = None, + option_string: Optional[str] = None + ) -> None: + page(parser.format_help(), True) + parser.exit() + + def create_parser(): p = argparse.ArgumentParser( description='Build and sign Unified Kernel Images', allow_abbrev=False, + add_help=False, usage='''\ -ukify [options…] [LINUX INITRD…] +ukify [options…] VERB ''', epilog='\n '.join(('config file:', *config_example())), formatter_class=argparse.RawDescriptionHelpFormatter, @@ -1145,10 +1311,42 @@ ukify [options…] [LINUX INITRD…] # Suppress printing of usage synopsis on errors p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n') + # Make --help paged + p.add_argument( + '-h', '--help', + action=PagerHelpAction, + help='show this help message and exit', + ) + return p def finalize_options(opts): + # Figure out which syntax is being used, one of: + # ukify verb --arg --arg --arg + # ukify linux initrd… + if len(opts.positional) == 1 and opts.positional[0] in VERBS: + opts.verb = opts.positional[0] + elif opts.linux or opts.initrd: + raise ValueError('--linux/--initrd options cannot be used with positional arguments') + else: + print("Assuming obsolete commandline syntax with no verb. Please use 'build'.") + if opts.positional: + opts.linux = pathlib.Path(opts.positional[0]) + # If we have initrds from parsing config files, append our positional args at the end + opts.initrd = (opts.initrd or []) + [pathlib.Path(arg) for arg in opts.positional[1:]] + opts.verb = 'build' + + # Check that --pcr-public-key=, --pcr-private-key=, and --phases= + # have either the same number of arguments are are not specified at all. + n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys) + n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys) + n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups) + if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv: + raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=') + if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv: + raise ValueError('--phases= specifications must match --pcr-private-key=') + if opts.cmdline and opts.cmdline.startswith('@'): opts.cmdline = pathlib.Path(opts.cmdline[1:]) elif opts.cmdline: @@ -1190,7 +1388,7 @@ def finalize_options(opts): if opts.sign_kernel and not opts.sb_key and not opts.sb_cert_name: raise ValueError('--sign-kernel requires either --secureboot-private-key= and --secureboot-certificate= (for sbsign) or --secureboot-certificate-name= (for pesign) to be specified') - if opts.output is None: + if opts.verb == 'build' and opts.output is None: if opts.linux is None: raise ValueError('--output= must be specified when building a PE addon') suffix = '.efi' if opts.sb_key or opts.sb_cert_name else '.unsigned.efi' @@ -1206,45 +1404,22 @@ def finalize_options(opts): def parse_args(args=None): - p = create_parser() - opts = p.parse_args(args) - - # Figure out which syntax is being used, one of: - # ukify verb --arg --arg --arg - # ukify linux initrd… - if len(opts.positional) == 1 and opts.positional[0] in VERBS: - opts.verb = opts.positional[0] - elif opts.linux or opts.initrd: - raise ValueError('--linux/--initrd options cannot be used with positional arguments') - else: - print("Assuming obsolete commandline syntax with no verb. Please use 'build'.") - if opts.positional: - opts.linux = pathlib.Path(opts.positional[0]) - opts.initrd = [pathlib.Path(arg) for arg in opts.positional[1:]] - opts.verb = 'build' - - # Check that --pcr-public-key=, --pcr-private-key=, and --phases= - # have either the same number of arguments are are not specified at all. - n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys) - n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys) - n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups) - if n_pcr_pub is not None and n_pcr_pub != n_pcr_priv: - raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=') - if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv: - raise ValueError('--phases= specifications must match --pcr-private-key=') - + opts = create_parser().parse_args(args) apply_config(opts) - finalize_options(opts) - return opts def main(): opts = parse_args() - check_inputs(opts) - assert opts.verb == 'build' - make_uki(opts) + if opts.verb == 'build': + check_inputs(opts) + make_uki(opts) + elif opts.verb == 'genkey': + check_cert_and_keys_nonexistent(opts) + generate_keys(opts) + else: + assert False if __name__ == '__main__': |