mirror of
https://github.com/systemd/systemd.git
synced 2024-10-27 18:55:40 +03:00
Merge pull request #27946 from keszybz/ukify-genkey-verb
Add 'genkey' verb to ukify
This commit is contained in:
commit
2b8628c704
14
man/uki.conf.example
Normal file
14
man/uki.conf.example
Normal file
@ -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
|
154
man/ukify.xml
154
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>
|
||||
|
||||
<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>
|
||||
<refsect1>
|
||||
<title>Commands</title>
|
||||
|
||||
<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>The following commands are understood:</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>
|
||||
<refsect2>
|
||||
<title><command>build</command></title>
|
||||
|
||||
<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>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>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>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>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><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>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>
|
||||
@ -305,6 +329,14 @@
|
||||
This option is required by <varname>SecureBootSigningTool=pesign</varname>/<option>--signtool=pesign</option>.</para></listitem>
|
||||
</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>
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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__':
|
||||
|
Loading…
Reference in New Issue
Block a user