From: Luca Boccassi Date: Sat, 8 Feb 2025 13:17:22 +0000 (+0000) Subject: ukify: add --pcr-certificate= parameter X-Git-Tag: v258-rc1~1371 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2ac8fcf6562f3dbec42b0d720cd8b5c9d2a231ab;p=thirdparty%2Fsystemd.git ukify: add --pcr-certificate= parameter Public keys and certificates are not the same, as the latter embeds more information that the former, and other tools like sd-measure have distinct parameters for each of them. Add a new --pcr-certificate= parameter to ukify, and use it to pass certs down to sd-measure, as an alternative to --pcr-public-key=. Do not allow specifying both. --- diff --git a/man/ukify.xml b/man/ukify.xml index f68ef0a8d0a..a0e58ab693b 100644 --- a/man/ukify.xml +++ b/man/ukify.xml @@ -85,7 +85,8 @@ If PCR signing keys are provided via the PCRPrivateKey=/ and - PCRPublicKey=/ options, PCR values that will be seen + PCRPublicKey=/ or + PCRCertificate=/ options, PCR values that will be seen after booting with the given kernel, initrd, and other sections, will be calculated, signed, and embedded in the UKI. systemd-measure1 is @@ -95,7 +96,8 @@ the Phases=/ option. If not specified, the default provided by systemd-measure is used. It is also possible to specify the PCRPrivateKey=/, - PCRPublicKey=/, and + PCRPublicKey=/ or + PCRCertificate=/, and Phases=/ arguments more than once. Signatures will then be performed with each of the specified keys. On the command line, when both and are used, they must be specified the same number of times, and then @@ -478,8 +480,9 @@ A path to a public key to embed in the .pcrpkey section. If not specified, and there's exactly one - PCRPublicKey=/ argument, that key will be used. - Otherwise, the section will not be present. + PCRPublicKey=/ or + PCRCertificate=/ argument, that key will be + used. Otherwise, the section will not be present. @@ -652,11 +655,27 @@ On the command line, this option may be specified more than once, similarly to the option. If not present, the public keys will be extracted from the private keys. On the command line, if present, this option must be specified the same number of - times as the option. + times as the option. Cannot be specified if + is used. + + PCRCertificate=PATH + + + An X.509 certificate to use for signing PCR policies. + + On the command line, this option may be specified more than once, similarly to the + option. If not present, the public keys will be extracted from + the private keys. On the command line, if present, this option must be specified the same number of + times as the option. Cannot be specified if + is used. + + + + Phases=LIST diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py index 7a6ffd29956..9013e64b62d 100755 --- a/src/ukify/ukify.py +++ b/src/ukify/ukify.py @@ -274,6 +274,7 @@ class UkifyConfig: pcr_banks: list[str] pcr_private_keys: list[str] pcr_public_keys: list[str] + pcr_certificates: list[str] pcrpkey: Optional[Path] pcrsig: Union[str, Path, None] join_pcrsig: Optional[Path] @@ -683,7 +684,7 @@ def check_cert_and_keys_nonexistent(opts: UkifyConfig) -> None: # Raise if any of the keys and certs are found on disk paths: Iterator[Union[str, Path, None]] = itertools.chain( (opts.sb_key, opts.sb_cert), - *((priv_key, pub_key) for priv_key, pub_key, _ in key_path_groups(opts)), + *((priv_key, pub_key, cert) for priv_key, pub_key, cert, _ in key_path_groups(opts)), ) for path in paths: if path and Path(path).exists(): @@ -721,17 +722,19 @@ def combine_signatures(pcrsigs: list[dict[str, str]]) -> str: return json.dumps(combined) -def key_path_groups(opts: UkifyConfig) -> Iterator[tuple[str, Optional[str], Optional[str]]]: +def key_path_groups(opts: UkifyConfig) -> Iterator[tuple[str, Optional[str], Optional[str], Optional[str]]]: if not opts.pcr_private_keys: return n_priv = len(opts.pcr_private_keys) pub_keys = opts.pcr_public_keys or [] + certs = opts.pcr_certificates or [] pp_groups = opts.phase_path_groups or [] yield from itertools.zip_longest( opts.pcr_private_keys, pub_keys[:n_priv], + certs[:n_priv], pp_groups[:n_priv], fillvalue=None, ) @@ -809,9 +812,15 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) -> ] # The JSON object will be used for offline signing, include the public key - # so that the fingerprint is included too. - if opts.policy_digest and opts.pcr_public_keys: - cmd += [f'--public-key={opts.pcr_public_keys[0]}'] + # so that the fingerprint is included too. In case a certificate is passed, use the + # right parameter so that systemd-measure can extract the public key from it. + if opts.policy_digest: + if opts.pcr_public_keys: + cmd += ['--public-key', opts.pcr_public_keys[0]] + elif opts.pcr_certificates: + cmd += ['--certificate', opts.pcr_certificates[0]] + if opts.certificate_provider: + cmd += ['--certificate-source', f'provider:{opts.certificate_provider}'] print('+', shell_join(cmd), file=sys.stderr) output = subprocess.check_output(cmd, text=True) # type: ignore @@ -848,16 +857,24 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) -> *(f'--bank={bank}' for bank in banks), ] - for priv_key, pub_key, group in key_path_groups(opts): + for priv_key, pub_key, cert, group in key_path_groups(opts): extra = [f'--private-key={priv_key}'] if opts.signing_engine is not None: - assert pub_key - extra += [f'--private-key-source=engine:{opts.signing_engine}'] - extra += [f'--certificate={pub_key}'] + assert pub_key or cert + # Backward compatibility, we used to pass the public key as the certificate + # as there was no --pcr-certificate= parameter + extra += [ + f'--private-key-source=engine:{opts.signing_engine}', + f'--certificate={pub_key or cert}', + ] elif opts.signing_provider is not None: - assert pub_key - extra += [f'--private-key-source=provider:{opts.signing_provider}'] - extra += [f'--certificate={pub_key}'] + assert pub_key or cert + extra += [ + f'--private-key-source=provider:{opts.signing_provider}', + f'--certificate={pub_key or cert}', + ] + elif cert: + extra += [f'--certificate={cert}'] elif pub_key: extra += [f'--public-key={pub_key}'] @@ -1316,6 +1333,13 @@ def make_uki(opts: UkifyConfig) -> None: pcrpkey = subprocess.check_output(cmd) else: pcrpkey = Path(opts.pcr_public_keys[0]) + elif opts.pcr_certificates and len(opts.pcr_certificates) == 1: + cmd += ['--certificate', opts.pcr_certificates[0]] + if opts.certificate_provider: + cmd += ['--certificate-source', f'provider:{opts.certificate_provider}'] + + print('+', shell_join(cmd), file=sys.stderr) + pcrpkey = subprocess.check_output(cmd) elif opts.pcr_private_keys and len(opts.pcr_private_keys) == 1: cmd += ['--private-key', Path(opts.pcr_private_keys[0])] @@ -1594,7 +1618,7 @@ def generate_keys(opts: UkifyConfig) -> None: work = True - for priv_key, pub_key, _ in key_path_groups(opts): + 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}') @@ -2099,6 +2123,15 @@ CONFIG_ITEMS = [ config_key='PCRSignature:/PCRPublicKey', config_push=ConfigItem.config_set_group, ), + ConfigItem( + '--pcr-certificate', + dest='pcr_certificates', + metavar='PATH', + action='append', + help='certificate part of the keypair or engine/provider designation for signing PCR signatures', + config_key='PCRSignature:/PCRCertificate', + config_push=ConfigItem.config_set_group, + ), ConfigItem( '--phases', dest='phase_path_groups', @@ -2298,17 +2331,27 @@ def finalize_options(opts: argparse.Namespace) -> None: # Check that --pcr-public-key=, --pcr-private-key=, and --phases= # have either the same number of arguments or are not specified at all. + # Also check that --pcr-public-key= and --pcr-certificate= are not set at the same time. # But allow a single public key, for offline PCR signing, to pre-populate the JSON object # with the certificate's fingerprint. + n_pcr_cert = None if opts.pcr_certificates is None else len(opts.pcr_certificates) n_pcr_pub = None if opts.pcr_public_keys is None else len(opts.pcr_public_keys) n_pcr_priv = None if opts.pcr_private_keys is None else len(opts.pcr_private_keys) n_phase_path_groups = None if opts.phase_path_groups is None else len(opts.phase_path_groups) if opts.policy_digest and n_pcr_priv is not None: raise ValueError('--pcr-private-key= cannot be specified with --policy-digest') - if opts.policy_digest and (n_pcr_pub is None or n_pcr_pub != 1): - raise ValueError('--policy-digest requires exactly one --pcr-public-key=') + if ( + opts.policy_digest + and (n_pcr_pub is None or n_pcr_pub != 1) + and (n_pcr_cert is None or n_pcr_cert != 1) + ): + raise ValueError('--policy-digest requires exactly one --pcr-public-key= or --pcr-certificate=') if n_pcr_pub is not None and n_pcr_priv is not None and n_pcr_pub != n_pcr_priv: raise ValueError('--pcr-public-key= specifications must match --pcr-private-key=') + if n_pcr_cert is not None and n_pcr_priv is not None and n_pcr_cert != n_pcr_priv: + raise ValueError('--pcr-certificate= specifications must match --pcr-private-key=') + if n_pcr_pub is not None and n_pcr_cert is not None: + raise ValueError('--pcr-public-key= and --pcr-certificate= cannot be used at the same time') if n_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv: raise ValueError('--phases= specifications must match --pcr-private-key=') @@ -2405,6 +2448,7 @@ def finalize_options(opts: argparse.Namespace) -> None: or opts.devicetree_auto or opts.pcr_private_keys or opts.pcr_public_keys + or opts.pcr_certificates ): raise ValueError('--pcrsig and --join-pcrsig cannot be used with other sections') if opts.pcrsig: