]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
ukify: add 'genkey' verb
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Tue, 6 Jun 2023 19:06:20 +0000 (21:06 +0200)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Wed, 14 Jun 2023 11:17:33 +0000 (13:17 +0200)
The idea is to make it easy to generate all the signing key and certs
that can be used for local signing. The verb is the modeled after
'mkosi genkey', but there are some important differences: we generate
the keys to the paths where they will be read from, both pcr signing
keys and the SecureBoot certificate+key.

If any of the outputs exist, operation is refused. Maybe we could add a
--force option in the future, but this operation should be rare, so I think
it's better to refuse to overwrite anything initially.

I'm only doing a token man page change here.
https://github.com/systemd/systemd/pull/27621 reworks the man page,
and the changes done here would conflict heavily with that work. I'll
submit a follow-up patch later.

man/ukify.xml
src/ukify/test/test_ukify.py
src/ukify/ukify.py

index b2e7f82d8fe8f176c9bf1109c7ef293f8049010e..283d58b3b05ef5cdd095a723ccb2e23f6fa746fe 100644 (file)
@@ -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>
 
index 3ca9b531c241613a8dcad153ec199a7e19c03734..ac39a719402b151aa281d4a25535e73cd963f18c 100755 (executable)
@@ -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))
index 9abaefec9aec928851075d8ebd65f1a0a7c07fbe..4fc3ce2e192c7298aebad179e216e86d5f76b37f 100755 (executable)
 
 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 <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,
+        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__':