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
Callable,
IO,
Optional,
+ Sequence,
Union)
import pefile # type: ignore
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)
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:
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',
# 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 = [
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}']
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 certificate 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
return (section_name, key, value)
-VERBS = ('build',)
+VERBS = ('build', 'genkey')
CONFIG_ITEMS = [
ConfigItem(
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',
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,
# 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:
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'
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__':