summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>2023-06-14 17:57:24 +0200
committerGitHub <noreply@github.com>2023-06-14 17:57:24 +0200
commit2b8628c7049bf99466e3e446552f81abcfa87aee (patch)
treee681ea029384e03e009fcdc0a2bf20b7dc0bd6fe
parentcoverage: fix build with g++ (diff)
parentukify: make the certficate validity configurable (diff)
downloadsystemd-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.example14
-rw-r--r--man/ukify.xml164
-rwxr-xr-xsrc/kernel-install/60-ukify.install.in2
-rwxr-xr-xsrc/ukify/test/test_ukify.py70
-rwxr-xr-xsrc/ukify/ukify.py259
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 &lt;&lt;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__':