1
0
mirror of https://github.com/systemd/systemd.git synced 2025-03-14 04:58:28 +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
<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
in the UKI.
<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
by <command>systemd-measure</command> is used. It is also possible to specify the
<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
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
@ -478,8 +480,9 @@
<listitem><para>A path to a public key to embed in the <literal>.pcrpkey</literal> section. If not
specified, and there's exactly one
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> argument, that key will be used.
Otherwise, the section will not be present.</para>
<varname>PCRPublicKey=</varname>/<option>--pcr-public-key=</option> or
<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>
</varlistentry>
@ -652,11 +655,27 @@
<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.</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>
</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>
<term><varname>Phases=<replaceable>LIST</replaceable></varname></term>
<term><option>--phases=<replaceable>LIST</replaceable></option></term>

View File

@ -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: