diff --git a/man/ukify.xml b/man/ukify.xml
index b2e7f82d8fe..283d58b3b05 100644
--- a/man/ukify.xml
+++ b/man/ukify.xml
@@ -25,6 +25,7 @@
/usr/lib/systemd/ukify
OPTIONS
build
+ genkey
diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py
index 3ca9b531c24..ac39a719402 100755
--- a/src/ukify/test/test_ukify.py
+++ b/src/ukify/test/test_ukify.py
@@ -698,5 +698,57 @@ 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)
+ 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 9abaefec9ae..4fc3ce2e192 100755
--- a/src/ukify/ukify.py
+++ b/src/ukify/ukify.py
@@ -25,8 +25,10 @@
import argparse
import configparser
+import contextlib
import collections
import dataclasses
+import datetime
import fnmatch
import itertools
import json
@@ -37,6 +39,7 @@ import pydoc
import re
import shlex
import shutil
+import socket
import subprocess
import sys
import tempfile
@@ -356,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:
@@ -385,7 +399,7 @@ def key_path_groups(opts):
if not opts.pcr_private_keys:
return
- n_priv = len(opts.pcr_private_keys or ())
+ 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
@@ -729,6 +743,116 @@ 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 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,
+ keylength: int = 2048,
+ valid_days: int = 365 * 10, # TODO: can we drop the expiration date?
+) -> 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)
+ 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
@@ -861,7 +985,7 @@ class ConfigItem:
return (section_name, key, value)
-VERBS = ('build',)
+VERBS = ('build', 'genkey')
CONFIG_ITEMS = [
ConfigItem(
@@ -1253,7 +1377,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'
@@ -1277,9 +1401,14 @@ def parse_args(args=None):
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__':