]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
ukify: add --pcr-certificate= parameter
authorLuca Boccassi <luca.boccassi@gmail.com>
Sat, 8 Feb 2025 13:17:22 +0000 (13:17 +0000)
committerLuca Boccassi <luca.boccassi@gmail.com>
Sun, 9 Feb 2025 22:24:00 +0000 (22:24 +0000)
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.

man/ukify.xml
src/ukify/ukify.py

index f68ef0a8d0a00a3ddb941abcbf58f36a350d26a6..a0e58ab693bca12aac6f738ca04dd0ccf5970577 100644 (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
 
           <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>
           <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>
index 7a6ffd299567aeddc5827cb1b84cd7d57e11b139..9013e64b62da582ca04a92f1c061695b33a5d081 100755 (executable)
@@ -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: