]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
ukify: add --policy-digest option
authorLuca Boccassi <luca.boccassi@gmail.com>
Mon, 20 Jan 2025 00:30:48 +0000 (00:30 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 21 Jan 2025 08:19:49 +0000 (09:19 +0100)
Uses the newly added policy-digest verb of systemd-measure, for the same
purpose: build a UKI and get digests for the .pcrsig section out, so
that they can be offline signed and reattached

man/ukify.xml
src/ukify/test/test_ukify.py
src/ukify/ukify.py

index 13c14a45c843aedb55ca754a6d7445a58a25c2f9..dfd74d59270da2bb0fc634ddc1dae0a6f712bc40 100644 (file)
           <xi:include href="version-info.xml" xpointer="v253"/></listitem>
         </varlistentry>
 
+        <varlistentry>
+          <term><option>--policy-digest</option></term>
+          <term><option>--no-policy-digest</option></term>
+
+          <listitem><para>Enable or disable a call to
+          <citerefentry><refentrytitle>systemd-measure</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+          to print pre-calculated TPM2 policy digests. Useful for offline signing of PCR policies.
+          Defaults to false.</para>
+
+          <xi:include href="version-info.xml" xpointer="v258"/></listitem>
+        </varlistentry>
+
         <varlistentry>
           <term><option>--section=<replaceable>NAME</replaceable>:<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
           <term><option>--section=<replaceable>NAME</replaceable>:text|binary<optional>@<replaceable>PATH</replaceable></optional></option></term>
index 3ed21fc0ace5043795a2689740230acec1d812b3..0f974b3558b3f130e41d5590cd18952895f9a738 100755 (executable)
@@ -242,6 +242,8 @@ def test_parse_args_many():
          '--output=OUTPUT',
          '--measure',
          '--no-measure',
+         '--policy-digest',
+         '--no-policy-digest',
          ])
     assert opts.linux == pathlib.Path('/ARG1')
     assert opts.initrd == [pathlib.Path('/ARG2'), pathlib.Path('/ARG3 WITH SPACE')]
@@ -262,6 +264,7 @@ def test_parse_args_many():
     assert opts.tools == [pathlib.Path('TOOLZ/')]
     assert opts.output == pathlib.Path('OUTPUT')
     assert opts.measure is False
+    assert opts.policy_digest is False
 
 def test_parse_sections():
     opts = ukify.parse_args(
index b3673a032aabbd62d0faeb5be804949e7b0fe405..eaeaa542024f02dce57b29b5024c4949022826a6 100755 (executable)
@@ -255,6 +255,7 @@ class UkifyConfig:
     pcr_public_keys: list[str]
     pcrpkey: Optional[Path]
     phase_path_groups: Optional[list[str]]
+    policy_digest: bool
     profile: Union[str, Path, None]
     sb_cert: Union[str, Path, None]
     sb_cert_name: Optional[str]
@@ -751,7 +752,7 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) ->
 
             unique_to_measure[section.name] = section
 
-    if opts.measure:
+    if opts.measure or opts.policy_digest:
         to_measure = unique_to_measure.copy()
 
         for dtbauto in dtbauto_to_measure:
@@ -762,7 +763,7 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) ->
 
             cmd = [
                 measure_tool,
-                'calculate',
+                'calculate' if opts.measure else 'policy-digest',
                 '--json',
                 opts.json,
                 *(f'--{s.name.removeprefix(".")}={s.content}' for s in to_measure.values()),
@@ -772,6 +773,11 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) ->
                 *(f'--phase={phase_path}' for phase_path in itertools.chain.from_iterable(pp_groups)),
             ]
 
+            # 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]}']
+
             print('+', shell_join(cmd), file=sys.stderr)
             subprocess.check_call(cmd)
 
@@ -1940,6 +1946,11 @@ CONFIG_ITEMS = [
         action=argparse.BooleanOptionalAction,
         help='print systemd-measure output for the UKI',
     ),
+    ConfigItem(
+        '--policy-digest',
+        action=argparse.BooleanOptionalAction,
+        help='print systemd-measure policy digests for the UKI',
+    ),
     ConfigItem(
         '--json',
         choices=('pretty', 'short', 'off'),
@@ -2108,10 +2119,16 @@ 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.
+    # But allow a single public key, for offline PCR signing, to pre-populate the JSON object
+    # with the certificate's fingerprint.
     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 n_pcr_pub is not None and n_pcr_pub != n_pcr_priv:
+    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 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_phase_path_groups is not None and n_phase_path_groups != n_pcr_priv:
         raise ValueError('--phases= specifications must match --pcr-private-key=')