1
0
mirror of https://github.com/systemd/systemd.git synced 2025-08-25 13:49:55 +03:00

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.
This commit is contained in:
Luca Boccassi
2025-02-08 13:17:22 +00:00
parent 5dbd751ccc
commit 2ac8fcf656
2 changed files with 83 additions and 20 deletions

View File

@ -85,7 +85,8 @@
<para>If PCR signing keys are provided via the <para>If PCR signing keys are provided via the
<varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option> and <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option> and
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> options, PCR values that will be seen <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> or
<varname>PCRCertificate=</varname>/<option>--pcr-certificate=</option> options, PCR values that will be seen
after booting with the given kernel, initrd, and other sections, will be calculated, signed, and embedded after booting with the given kernel, initrd, and other sections, will be calculated, signed, and embedded
in the UKI. in the UKI.
<citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry> is <citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry> is
@ -95,7 +96,8 @@
the <varname>Phases=</varname>/<option>--phases=</option> option. If not specified, the default provided the <varname>Phases=</varname>/<option>--phases=</option> option. If not specified, the default provided
by <command>systemd-measure</command> is used. It is also possible to specify the by <command>systemd-measure</command> is used. It is also possible to specify the
<varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>, <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>,
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option>, and <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> or
<varname>PCRCertificate=</varname>/<option>--pcr-certificate=</option>, and
<varname>Phases=</varname>/<option>--phases=</option> arguments more than once. Signatures will then be <varname>Phases=</varname>/<option>--phases=</option> arguments more than once. Signatures will then be
performed with each of the specified keys. On the command line, when both <option>--phases=</option> and performed with each of the specified keys. On the command line, when both <option>--phases=</option> and
<option>--pcr-private-key=</option> are used, they must be specified the same number of times, and then <option>--pcr-private-key=</option> are used, they must be specified the same number of times, and then
@ -478,8 +480,9 @@
<listitem><para>A path to a public key to embed in the <literal>.pcrpkey</literal> section. If not <listitem><para>A path to a public key to embed in the <literal>.pcrpkey</literal> section. If not
specified, and there's exactly one specified, and there's exactly one
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> argument, that key will be used. <varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> or
Otherwise, the section will not be present.</para> <varname>PCRCertificate=</varname>/<option>--pcr-certificate=</option> argument, that key will be
used. Otherwise, the section will not be present.</para>
<xi:include href="version-info.xml" xpointer="v253"/></listitem> <xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry> </varlistentry>
@ -652,11 +655,27 @@
<para>On the command line, this option may be specified more than once, similarly to the <para>On the command line, this option may be specified more than once, similarly to the
<option>--pcr-private-key=</option> option. If not present, the public keys will be extracted from <option>--pcr-private-key=</option> 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 the private keys. On the command line, if present, this option must be specified the same number of
times as the <option>--pcr-private-key=</option> option.</para> times as the <option>--pcr-private-key=</option> option. Cannot be specified if
<option>--pcr-certificate=</option> is used.</para>
<xi:include href="version-info.xml" xpointer="v253"/></listitem> <xi:include href="version-info.xml" xpointer="v253"/></listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term><varname>PCRCertificate=<replaceable>PATH</replaceable></varname></term>
<term><option>--pcr-certificate=<replaceable>PATH</replaceable></option></term>
<listitem><para>An X.509 certificate to use for signing PCR policies.</para>
<para>On the command line, this option may be specified more than once, similarly to the
<option>--pcr-private-key=</option> 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>--pcr-private-key=</option> option. Cannot be specified if
<option>--pcr-public-key=</option> is used.</para>
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term><varname>Phases=<replaceable>LIST</replaceable></varname></term> <term><varname>Phases=<replaceable>LIST</replaceable></varname></term>
<term><option>--phases=<replaceable>LIST</replaceable></option></term> <term><option>--phases=<replaceable>LIST</replaceable></option></term>

View File

@ -274,6 +274,7 @@ class UkifyConfig:
pcr_banks: list[str] pcr_banks: list[str]
pcr_private_keys: list[str] pcr_private_keys: list[str]
pcr_public_keys: list[str] pcr_public_keys: list[str]
pcr_certificates: list[str]
pcrpkey: Optional[Path] pcrpkey: Optional[Path]
pcrsig: Union[str, Path, None] pcrsig: Union[str, Path, None]
join_pcrsig: Optional[Path] 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 # Raise if any of the keys and certs are found on disk
paths: Iterator[Union[str, Path, None]] = itertools.chain( paths: Iterator[Union[str, Path, None]] = itertools.chain(
(opts.sb_key, opts.sb_cert), (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: for path in paths:
if path and Path(path).exists(): if path and Path(path).exists():
@ -721,17 +722,19 @@ def combine_signatures(pcrsigs: list[dict[str, str]]) -> str:
return json.dumps(combined) 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: if not opts.pcr_private_keys:
return return
n_priv = len(opts.pcr_private_keys) n_priv = len(opts.pcr_private_keys)
pub_keys = opts.pcr_public_keys or [] pub_keys = opts.pcr_public_keys or []
certs = opts.pcr_certificates or []
pp_groups = opts.phase_path_groups or [] pp_groups = opts.phase_path_groups or []
yield from itertools.zip_longest( yield from itertools.zip_longest(
opts.pcr_private_keys, opts.pcr_private_keys,
pub_keys[:n_priv], pub_keys[:n_priv],
certs[:n_priv],
pp_groups[:n_priv], pp_groups[:n_priv],
fillvalue=None, 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 # The JSON object will be used for offline signing, include the public key
# so that the fingerprint is included too. # so that the fingerprint is included too. In case a certificate is passed, use the
if opts.policy_digest and opts.pcr_public_keys: # right parameter so that systemd-measure can extract the public key from it.
cmd += [f'--public-key={opts.pcr_public_keys[0]}'] 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) print('+', shell_join(cmd), file=sys.stderr)
output = subprocess.check_output(cmd, text=True) # type: ignore 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), *(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}'] extra = [f'--private-key={priv_key}']
if opts.signing_engine is not None: if opts.signing_engine is not None:
assert pub_key assert pub_key or cert
extra += [f'--private-key-source=engine:{opts.signing_engine}'] # Backward compatibility, we used to pass the public key as the certificate
extra += [f'--certificate={pub_key}'] # 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: elif opts.signing_provider is not None:
assert pub_key assert pub_key or cert
extra += [f'--private-key-source=provider:{opts.signing_provider}'] extra += [
extra += [f'--certificate={pub_key}'] f'--private-key-source=provider:{opts.signing_provider}',
f'--certificate={pub_key or cert}',
]
elif cert:
extra += [f'--certificate={cert}']
elif pub_key: elif pub_key:
extra += [f'--public-key={pub_key}'] extra += [f'--public-key={pub_key}']
@ -1316,6 +1333,13 @@ def make_uki(opts: UkifyConfig) -> None:
pcrpkey = subprocess.check_output(cmd) pcrpkey = subprocess.check_output(cmd)
else: else:
pcrpkey = Path(opts.pcr_public_keys[0]) 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: elif opts.pcr_private_keys and len(opts.pcr_private_keys) == 1:
cmd += ['--private-key', Path(opts.pcr_private_keys[0])] cmd += ['--private-key', Path(opts.pcr_private_keys[0])]
@ -1594,7 +1618,7 @@ def generate_keys(opts: UkifyConfig) -> None:
work = True 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() priv_key_pem, pub_key_pem = generate_priv_pub_key_pair()
print(f'Writing private key for PCR signing to {priv_key}') print(f'Writing private key for PCR signing to {priv_key}')
@ -2099,6 +2123,15 @@ CONFIG_ITEMS = [
config_key='PCRSignature:/PCRPublicKey', config_key='PCRSignature:/PCRPublicKey',
config_push=ConfigItem.config_set_group, 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( ConfigItem(
'--phases', '--phases',
dest='phase_path_groups', 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= # Check that --pcr-public-key=, --pcr-private-key=, and --phases=
# have either the same number of arguments or are not specified at all. # 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 # But allow a single public key, for offline PCR signing, to pre-populate the JSON object
# with the certificate's fingerprint. # 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_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_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) 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: if opts.policy_digest and n_pcr_priv is not None:
raise ValueError('--pcr-private-key= cannot be specified with --policy-digest') 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): if (
raise ValueError('--policy-digest requires exactly one --pcr-public-key=') 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: 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=') 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: 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=') 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.devicetree_auto
or opts.pcr_private_keys or opts.pcr_private_keys
or opts.pcr_public_keys or opts.pcr_public_keys
or opts.pcr_certificates
): ):
raise ValueError('--pcrsig and --join-pcrsig cannot be used with other sections') raise ValueError('--pcrsig and --join-pcrsig cannot be used with other sections')
if opts.pcrsig: if opts.pcrsig: