mirror of
https://github.com/systemd/systemd.git
synced 2025-03-01 08:58:29 +03:00
ukify: add 'genkey' verb
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.
This commit is contained in:
parent
b09a5315f5
commit
a1c80efddc
@ -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>
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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 <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__':
|
||||
|
Loading…
x
Reference in New Issue
Block a user