diff --git a/man/ukify.xml b/man/ukify.xml
index 2c20ca7106..b1f39d2494 100644
--- a/man/ukify.xml
+++ b/man/ukify.xml
@@ -144,6 +144,10 @@
PCRPublicKey=/, and
SecureBootPrivateKey=/ below.
+ The generated certificate's name and lifetime are set with
+ SecureBootCertificateCommonName=/ and
+ SecureBootCertificateValidity=/.
+
The output files must not exist.
@@ -591,6 +595,18 @@
+
+ SecureBootCertificateCommonName=CN
+
+
+ The common name and issuer name (certificate is self-signed) of a
+ certificate created by genkey. Defaults to SecureBoot signing key on host
+ HOST 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).
+
+
+
+
SecureBootCertificateDir=SB_PATH
diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py
index d61a7924ff..fbd70386a8 100755
--- a/src/ukify/test/test_ukify.py
+++ b/src/ukify/test/test_ukify.py
@@ -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:
diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py
index 82d0c8676e..ab5e29706c 100755
--- a/src/ukify/ukify.py
+++ b/src/ukify/ukify.py
@@ -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',