ukify: add naming a certificate created by genkey

Image builds create a key separate from the SecureBoot one for verity
signing. Its certificate ships inside the image as the sysext trust
anchor.

`genkey` certs had a hardcoded "SecureBoot signing key on host <fqdn of
the build machine>" CN, which describes neither that verity role nor a
meaningful hostname.

Make that name configurable with `SecureBootCertificateCommonName=`,
like mkosi's `--genkey-common-name`. That allows image build tools to
re-use `ukify genkey` for non-SB keys.

Unlike the implicit truncation for the default name, a specified one
which is too long is an error. Add a citation where that limit comes
frm.
This commit is contained in:
Martin Pitt
2026-08-25 07:22:47 +09:00
committed by Yu Watanabe
parent 2d31e784f1
commit b8bcae41ac
3 changed files with 103 additions and 5 deletions
+16
View File
@@ -144,6 +144,10 @@
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option>, and
<varname>SecureBootPrivateKey=</varname>/<option>--secureboot-private-key=</option> below.</para>
<para>The generated certificate's name and lifetime are set with
<varname>SecureBootCertificateCommonName=</varname>/<option>--secureboot-certificate-common-name=</option> and
<varname>SecureBootCertificateValidity=</varname>/<option>--secureboot-certificate-validity=</option>.</para>
<para>The output files must not exist.</para>
</refsect2>
@@ -591,6 +595,18 @@
<xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>SecureBootCertificateCommonName=<replaceable>CN</replaceable></varname></term>
<term><option>--secureboot-certificate-common-name=<replaceable>CN</replaceable></option></term>
<listitem><para>The common name and issuer name (certificate is self-signed) of a
certificate created by <command>genkey</command>. Defaults to <literal>SecureBoot signing key on host
<replaceable>HOST</replaceable></literal> with the fully qualified domain name of the machine it runs on.
Must not be empty or longer than 64 bytes (X.509 CN limit).</para>
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
</varlistentry>
<varlistentry>
<term><varname>SecureBootCertificateDir=<replaceable>SB_PATH</replaceable></varname></term>
<term><option>--secureboot-certificate-dir=<replaceable>SB_PATH</replaceable></option></term>
+59
View File
@@ -1245,6 +1245,65 @@ def test_key_cert_generation(tmp_path):
assert re.search(r'Issuer: CN\s?=\s?SecureBoot signing key on host', out)
def test_key_cert_generation_common_name(tmp_path):
opts = ukify.parse_args(
[
'genkey',
f'--secureboot-private-key={tmp_path / "sb.priv.pem"}',
f'--secureboot-certificate={tmp_path / "sb.cert.pem"}',
'--secureboot-certificate-common-name=ukify test key',
]
)
assert opts.verb == 'genkey'
pytest.importorskip('cryptography')
ukify.generate_keys(opts)
if not shutil.which('openssl'):
return
out = subprocess.check_output(
[
'openssl', 'x509',
'-in', tmp_path / 'sb.cert.pem',
'-text',
'-noout',
],
text=True,
) # fmt: skip
assert re.search(r'Subject: CN\s?=\s?ukify test key', out)
assert re.search(r'Issuer: CN\s?=\s?ukify test key', out)
def test_key_cert_generation_empty_common_name(tmp_path):
opts = ukify.parse_args(
[
'genkey',
f'--secureboot-private-key={tmp_path / "sb.priv.pem"}',
f'--secureboot-certificate={tmp_path / "sb.cert.pem"}',
'--secureboot-certificate-common-name=',
]
)
with pytest.raises(ValueError, match='--secureboot-certificate-common-name= must not be empty'):
ukify.generate_keys(opts)
def test_key_cert_generation_common_name_too_long(tmp_path):
opts = ukify.parse_args(
[
'genkey',
f'--secureboot-private-key={tmp_path / "sb.priv.pem"}',
f'--secureboot-certificate={tmp_path / "sb.cert.pem"}',
f'--secureboot-certificate-common-name={"x" * 65}',
]
)
with pytest.raises(ValueError, match='is longer than 64 bytes'):
ukify.generate_keys(opts)
@pytest.mark.skipif(not slow_tests, reason='slow')
def test_join_pcrsig(capsys, kernel_initrd, tmp_path):
if kernel_initrd is None:
+28 -5
View File
@@ -81,6 +81,9 @@ EFI_ARCHES: list[str] = sum(EFI_ARCH_MAP.values(), [])
DEFAULT_CONFIG_DIRS = ['/etc/systemd', '/run/systemd', '/usr/local/lib/systemd', '/usr/lib/systemd']
DEFAULT_CONFIG_FILE = 'ukify.conf'
# https://datatracker.ietf.org/doc/html/rfc5280 ub-common-name; bytes, not chars
COMMON_NAME_MAX_LEN = 64
class Style:
bold = '\033[0;1;39m' if sys.stderr.isatty() else ''
@@ -297,6 +300,7 @@ class UkifyConfig:
policy_digest: bool
profile: Optional[str]
sb_cert: Union[str, Path, None]
sb_cert_common_name: Optional[str]
sb_cert_name: Optional[str]
sb_cert_validity: int
sb_certdir: Path
@@ -1682,12 +1686,24 @@ def generate_keys(opts: UkifyConfig) -> None:
# This will generate keys and certificates and write them to the paths that
# are specified as input paths.
if opts.sb_key and opts.sb_cert:
fqdn = socket.getfqdn()
# The length of CN must not exceed 64 bytes
if opts.sb_cert_common_name is not None:
cn = opts.sb_cert_common_name
if len(cn) == 0:
raise ValueError('--secureboot-certificate-common-name= must not be empty')
cn = f'SecureBoot signing key on host {fqdn}'
if len(cn) > 64:
# The length of CN must not exceed 64 bytes
cn = cn[:61] + '...'
# Older cryptography versions (checked 41) don't check that by themselves.
# Newer (at least 49) do, but with a much less useful error message, so keep this extra check.
if len(cn.encode()) > COMMON_NAME_MAX_LEN:
raise ValueError(
f'--secureboot-certificate-common-name= is longer than {COMMON_NAME_MAX_LEN} bytes: {cn!r}'
)
else:
fqdn = socket.getfqdn()
cn = f'SecureBoot signing key on host {fqdn}'
if len(cn) > COMMON_NAME_MAX_LEN:
cn = cn[: COMMON_NAME_MAX_LEN - 3] + '...'
key_pem, cert_pem = generate_key_cert_pair(
common_name=cn,
@@ -2203,6 +2219,13 @@ CONFIG_ITEMS = [
),
config_key='UKI/SecureBootCertificate',
),
ConfigItem(
'--secureboot-certificate-common-name',
metavar='CN',
dest='sb_cert_common_name',
help="common name of a certificate created by 'genkey'",
config_key='UKI/SecureBootCertificateCommonName',
),
ConfigItem(
'--secureboot-certificate-dir',
dest='sb_certdir',