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',