Skip to content

Commit 6cd6d6b

Browse files
committed
ukify: add --pcrsig and --join-pcrsig arguments to append offline signature
Add a build parameter to take an existing UKI and attach a .pcrsig section to it. This allows one to create a UKI with a .pcrpkey section with --policy-digest to get the json output from sd-measure, sign the digest offline, and attach the .pcrsig section with the signature later.
1 parent 7ebe910 commit 6cd6d6b

File tree

3 files changed

+255
-16
lines changed

3 files changed

+255
-16
lines changed

man/ukify.xml

+64
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,20 @@
269269
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
270270
</varlistentry>
271271

272+
<varlistentry>
273+
<term><option>--join-pcrsig=<replaceable>PATH</replaceable></option></term>
274+
<term><option>--pcrsig=<replaceable>TEXT</replaceable>|<replaceable>@PATH</replaceable></option></term>
275+
276+
<listitem><para><option>--join-pcrsig=</option> takes a path to an existing PE file containing a
277+
previously built UKI. <option>--pcrsig=</option> takes a path to an existing pcrsig JSON blob, or
278+
a verbatim inline blob. They must be used together, and without specifying any other UKI section
279+
parameters. <command>ukify</command> will attach the pcrsig JSON blob to the UKI. This is useful
280+
in combination with <option>--policy-digest</option> to create a UKI and then sign the TPM2 policy
281+
digests offline.</para>
282+
283+
<xi:include href="version-info.xml" xpointer="v258"/></listitem>
284+
</varlistentry>
285+
272286
<varlistentry>
273287
<term><option>--tools=<replaceable>DIRS</replaceable></option></term>
274288

@@ -839,6 +853,56 @@ ID=factory-reset' \
839853
<para>The resulting UKI <filename>base-with-profile-0-1-2.efi</filename> will now contain three profiles.</para>
840854
</example>
841855

856+
<example>
857+
<title>Offline signing of pcrsig section</title>
858+
859+
<para>First, create a UKI and save the PCR JSON blob:</para>
860+
861+
<programlisting>$ ukify build \
862+
--linux=/lib/modules/6.0.9-300.fc37.x86_64/vmlinuz \
863+
--initrd=/some/path/initramfs-6.0.9-300.fc37.x86_64.img \
864+
--cmdline='quiet rw' \
865+
--pcr-public-key=tpm2-pcr-public-key-initrd.pem \
866+
--policy-digest \
867+
--json=short \
868+
--output=base.efi >base.pcrs
869+
</programlisting>
870+
871+
<para>Then, sign the PCR digests offline and insert them in the JSON blob:</para>
872+
873+
<programlisting>#!/usr/bin/python3
874+
import base64, json, subprocess
875+
876+
priv_key = '/home/zbyszek/src/systemd/tpm2-pcr-private.pem'
877+
base_file = 'base.pcrs'
878+
879+
base = json.load(open(base_file))
880+
881+
for bank,policies in base.items():
882+
for policy in policies:
883+
pol = base64.b16decode(policy['pol'].upper())
884+
call = subprocess.run(['openssl', 'dgst', f'-{bank}', '-sign', priv_key],
885+
input=pol,
886+
check=True,
887+
capture_output=True)
888+
sig = base64.b64encode(call.stdout).decode()
889+
policy['sig'] = sig
890+
891+
print(json.dumps(base))
892+
</programlisting>
893+
894+
<para>Finally, attach the updated JSON blob to the UKI:</para>
895+
896+
<programlisting>$ ukify build \
897+
--join-pcrsig=base.efi \
898+
899+
--json=short \
900+
--output=base-signed.efi
901+
</programlisting>
902+
903+
<para>The resulting UKI <filename>base-signed.efi</filename> will now contain the signed PCR digests.</para>
904+
</example>
905+
842906
</refsect1>
843907

844908
<refsect1>

src/ukify/test/test_ukify.py

+65
Original file line numberDiff line numberDiff line change
@@ -891,5 +891,70 @@ def test_key_cert_generation(tmp_path):
891891
assert 'Certificate' in out
892892
assert re.search(r'Issuer: CN\s?=\s?SecureBoot signing key on host', out)
893893

894+
@pytest.mark.skipif(not slow_tests, reason='slow')
895+
def test_join_pcrsig(capsys, kernel_initrd, tmp_path):
896+
if kernel_initrd is None:
897+
pytest.skip('linux+initrd not found')
898+
try:
899+
systemd_measure()
900+
except ValueError:
901+
pytest.skip('systemd-measure not found')
902+
903+
ourdir = pathlib.Path(__file__).parent
904+
pub = unbase64(ourdir / 'example.tpm2-pcr-public.pem.base64')
905+
906+
output = tmp_path / 'basic.efi'
907+
args = [
908+
'build',
909+
*kernel_initrd,
910+
f'--output={output}',
911+
f'--pcr-public-key={pub.name}',
912+
'--json=short',
913+
'--policy-digest',
914+
] + arg_tools
915+
opts = ukify.parse_args(args)
916+
try:
917+
ukify.check_inputs(opts)
918+
except OSError as e:
919+
pytest.skip(str(e))
920+
921+
ukify.make_uki(opts)
922+
pcrs = json.loads(capsys.readouterr().out)
923+
for bank, sigs in pcrs.items():
924+
for sig in sigs:
925+
sig['sig'] = 'a' * int(bank[3:])
926+
927+
opts = ukify.parse_args(['inspect', str(output)])
928+
ukify.inspect_sections(opts)
929+
text = capsys.readouterr().out
930+
assert re.search(r'\.pcrpkey', text, re.MULTILINE)
931+
assert re.search(r'\.pcrsig', text, re.MULTILINE)
932+
assert not re.search(r'"sig":', text, re.MULTILINE)
933+
934+
output_sig = tmp_path / 'pcrsig.efi'
935+
args = [
936+
'build',
937+
f'--output={output_sig}',
938+
f'--join-pcrsig={output}',
939+
f'--pcrsig={json.dumps(pcrs)}',
940+
'--json=short',
941+
] + arg_tools
942+
opts = ukify.parse_args(args)
943+
try:
944+
ukify.check_inputs(opts)
945+
except OSError as e:
946+
pytest.skip(str(e))
947+
948+
ukify.make_uki(opts)
949+
950+
opts = ukify.parse_args(['inspect', str(output_sig)])
951+
ukify.inspect_sections(opts)
952+
text = capsys.readouterr().out
953+
assert re.search(r'\.pcrpkey', text, re.MULTILINE)
954+
assert re.search(r'\.pcrsig', text, re.MULTILINE)
955+
assert re.search(r'"sig":', text, re.MULTILINE)
956+
957+
shutil.rmtree(tmp_path)
958+
894959
if __name__ == '__main__':
895960
sys.exit(pytest.main(sys.argv))

0 commit comments

Comments
 (0)