ukify: generate initrd policy digests for offline signing
When --sign-initrd-pcrs is used with --policy-digest, generate the
extra enter-initrd policy with the "initrd" policy reference. This
matches the policy produced when ukify signs with a private key.
Fixes building UKIs with offline signing (e.g.: on OBS) and using
NVPCRs.
Follow-up for f0f3687700
This commit is contained in:
committed by
Zbigniew Jędrzejewski-Szmek
parent
786fcb493f
commit
8cceaab8f5
+8
-4
@@ -249,7 +249,8 @@
|
||||
<citerefentry><refentrytitle>systemd-cryptenroll</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
|
||||
by using the <option>--tpm2-public-key-policyref=initrd</option> option. The extra signatures reuse
|
||||
the first <varname>PCRPrivateKey=</varname>/<option>--pcr-private-key=</option>, hence one must be
|
||||
specified. Defaults to false.</para>
|
||||
specified, unless <option>--policy-digest</option> is used to generate the policies for offline
|
||||
signing. Defaults to false.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v262"/></listitem>
|
||||
</varlistentry>
|
||||
@@ -989,6 +990,7 @@ ID=factory-reset' \
|
||||
--cmdline='quiet rw' \
|
||||
--pcr-public-key=tpm2-pcr-initrd-public-key.pem \
|
||||
--policy-digest \
|
||||
--sign-initrd-pcrs \
|
||||
--json=short \
|
||||
--output=base.efi >base.pcrs
|
||||
</programlisting>
|
||||
@@ -996,7 +998,7 @@ ID=factory-reset' \
|
||||
<para>Then, sign the PCR digests offline and insert them in the JSON blob:</para>
|
||||
|
||||
<programlisting>#!/usr/bin/python3
|
||||
import base64, json, subprocess
|
||||
import base64, hashlib, json, subprocess
|
||||
|
||||
priv_key = '/home/zbyszek/src/systemd/tpm2-pcr-private.pem'
|
||||
base_file = 'base.pcrs'
|
||||
@@ -1005,9 +1007,11 @@ base = json.load(open(base_file))
|
||||
|
||||
for bank,policies in base.items():
|
||||
for policy in policies:
|
||||
pol = base64.b16decode(policy['pol'].upper())
|
||||
to_sign = base64.b16decode(policy['pol'].upper())
|
||||
if policyref := policy.get('ref'):
|
||||
to_sign += hashlib.sha256(policyref.encode()).digest()
|
||||
call = subprocess.run(['openssl', 'dgst', f'-{bank}', '-sign', priv_key],
|
||||
input=pol,
|
||||
input=to_sign,
|
||||
check=True,
|
||||
capture_output=True)
|
||||
sig = base64.b64encode(call.stdout).decode()
|
||||
|
||||
@@ -299,6 +299,26 @@ def test_parse_args_many():
|
||||
assert opts.sign_initrd_pcrs is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'signing_args',
|
||||
(
|
||||
('--pcr-private-key=PKEY',),
|
||||
('--policy-digest', '--pcr-public-key=PKEY'),
|
||||
),
|
||||
ids=('private-key', 'policy-digest'),
|
||||
)
|
||||
def test_parse_args_sign_initrd_pcrs(signing_args):
|
||||
args = ['build', '--linux=LINUX', '--output=OUTPUT', '--sign-initrd-pcrs']
|
||||
opts = ukify.parse_args([*args, *signing_args])
|
||||
assert opts.sign_initrd_pcrs is True
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match='--sign-initrd-pcrs requires --pcr-private-key= or --policy-digest',
|
||||
):
|
||||
ukify.parse_args(args)
|
||||
|
||||
|
||||
def test_parse_sections():
|
||||
opts = ukify.parse_args(
|
||||
[
|
||||
@@ -1129,7 +1149,9 @@ def test_pcr_signing3(kernel_initrd, tmp_path):
|
||||
shutil.rmtree(tmp_path)
|
||||
|
||||
|
||||
def test_pcr_signing_initrd_pcrs(kernel_initrd, tmp_path):
|
||||
@pytest.mark.skipif(not slow_tests, reason='slow')
|
||||
@pytest.mark.parametrize('policy_digest', (False, True), ids=('sign', 'policy-digest'))
|
||||
def test_pcr_signing_initrd_pcrs(kernel_initrd, tmp_path, policy_digest):
|
||||
if kernel_initrd is None:
|
||||
pytest.skip('linux+initrd not found')
|
||||
try:
|
||||
@@ -1150,7 +1172,7 @@ def test_pcr_signing_initrd_pcrs(kernel_initrd, tmp_path):
|
||||
'--cmdline=ARG1 ARG2 ARG3',
|
||||
'--os-release=ID=foobar\n',
|
||||
'--pcr-banks=sha384', # sha1 might not be allowed, use something else
|
||||
f'--pcr-private-key={priv.name}',
|
||||
*(['--policy-digest'] if policy_digest else [f'--pcr-private-key={priv.name}']),
|
||||
f'--pcr-public-key={pub.name}',
|
||||
'--sign-initrd-pcrs',
|
||||
] + arg_tools
|
||||
@@ -1170,13 +1192,15 @@ def test_pcr_signing_initrd_pcrs(kernel_initrd, tmp_path):
|
||||
|
||||
sig = json.loads(open(tmp_path / 'out.pcrsig').read())
|
||||
assert list(sig.keys()) == ['sha384']
|
||||
assert len(sig['sha384']) == 5 # five items for five phase paths
|
||||
assert 'ref' not in sig['sha384'][0]
|
||||
assert 'ref' not in sig['sha384'][1]
|
||||
assert 'ref' not in sig['sha384'][2]
|
||||
assert 'ref' not in sig['sha384'][3]
|
||||
assert 'ref' in sig['sha384'][4]
|
||||
assert sig['sha384'][4]['ref'] == 'initrd'
|
||||
policies = sig['sha384']
|
||||
assert len(policies) == 5 # five items for five phase paths
|
||||
assert policies[0]['pol'] == policies[4]['pol']
|
||||
assert all('ref' not in policy for policy in policies[:4])
|
||||
assert policies[4]['ref'] == 'initrd'
|
||||
if policy_digest:
|
||||
assert all('pkfp' in policy and 'sig' not in policy for policy in policies)
|
||||
else:
|
||||
assert all('sig' in policy for policy in policies)
|
||||
|
||||
shutil.rmtree(tmp_path)
|
||||
|
||||
@@ -1324,6 +1348,7 @@ def test_join_pcrsig(capsys, kernel_initrd, tmp_path):
|
||||
f'--pcr-public-key={pub.name}',
|
||||
'--json=short',
|
||||
'--policy-digest',
|
||||
'--sign-initrd-pcrs',
|
||||
] + arg_tools
|
||||
opts = ukify.parse_args(args)
|
||||
try:
|
||||
@@ -1335,7 +1360,7 @@ def test_join_pcrsig(capsys, kernel_initrd, tmp_path):
|
||||
pcrs = json.loads(capsys.readouterr().out)
|
||||
for bank, sigs in pcrs.items():
|
||||
for sig in sigs:
|
||||
sig['sig'] = 'a' * int(bank[3:])
|
||||
sig['sig'] = ('b' if sig.get('ref') else 'a') * int(bank[3:])
|
||||
|
||||
opts = ukify.parse_args(['inspect', str(output)])
|
||||
ukify.inspect_sections(opts)
|
||||
@@ -1360,6 +1385,16 @@ def test_join_pcrsig(capsys, kernel_initrd, tmp_path):
|
||||
|
||||
ukify.make_uki(opts)
|
||||
|
||||
pcrsig = tmp_path / 'out.pcrsig'
|
||||
subprocess.check_call(
|
||||
['objcopy', f'--dump-section=.pcrsig={pcrsig}', output_sig, tmp_path / 'dummy'],
|
||||
text=True,
|
||||
)
|
||||
attached = json.loads(pcrsig.read_text())
|
||||
for bank, sigs in attached.items():
|
||||
for sig in sigs:
|
||||
assert sig['sig'] == ('b' if sig.get('ref') else 'a') * int(bank[3:])
|
||||
|
||||
opts = ukify.parse_args(['inspect', str(output_sig)])
|
||||
ukify.inspect_sections(opts)
|
||||
text = capsys.readouterr().out
|
||||
|
||||
+43
-35
@@ -83,6 +83,7 @@ DEFAULT_CONFIG_FILE = 'ukify.conf'
|
||||
|
||||
# https://datatracker.ietf.org/doc/html/rfc5280 ub-common-name; bytes, not chars
|
||||
COMMON_NAME_MAX_LEN = 64
|
||||
INITRD_PCR_POLICY: tuple[list[str], str] = (['enter-initrd'], 'initrd')
|
||||
|
||||
|
||||
class Style:
|
||||
@@ -771,14 +772,13 @@ def key_path_groups(
|
||||
)
|
||||
|
||||
# When requested, emit an extra signing group that reuses the first PCR signing key to produce
|
||||
# a signed PCR policyy that can only be used from the initrd.
|
||||
# a signed PCR policy that can only be used from the initrd.
|
||||
if opts.sign_initrd_pcrs and include_extra:
|
||||
yield (
|
||||
opts.pcr_private_keys[0],
|
||||
pub_keys[0] if pub_keys else None,
|
||||
certs[0] if certs else None,
|
||||
['enter-initrd'],
|
||||
'initrd',
|
||||
*INITRD_PCR_POLICY,
|
||||
)
|
||||
|
||||
|
||||
@@ -839,39 +839,47 @@ def call_systemd_measure(uki: UKI, opts: UkifyConfig, profile_start: int = 0) ->
|
||||
if dtbauto is not None:
|
||||
to_measure[dtbauto.name] = dtbauto
|
||||
|
||||
pp_groups = opts.phase_path_groups or []
|
||||
|
||||
cmd = [
|
||||
measure_tool,
|
||||
'calculate' if opts.measure else 'policy-digest',
|
||||
'--json',
|
||||
opts.json,
|
||||
*(f'--{s.name.removeprefix(".")}={s.content}' for s in to_measure.values()),
|
||||
*(f'--bank={bank}' for bank in banks),
|
||||
# For measurement, the keys are not relevant, so we can lump all the phase paths
|
||||
# into one call to systemd-measure calculate.
|
||||
*(f'--phase={phase_path}' for phase_path in itertools.chain.from_iterable(pp_groups)),
|
||||
policy_groups: list[tuple[list[str], str]] = [
|
||||
(list(itertools.chain.from_iterable(opts.phase_path_groups or [])), ''),
|
||||
]
|
||||
if opts.policy_digest and opts.sign_initrd_pcrs:
|
||||
policy_groups += [INITRD_PCR_POLICY]
|
||||
|
||||
# The JSON object will be used for offline signing, include the public key
|
||||
# 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}']
|
||||
for phase_paths, policyref in policy_groups:
|
||||
cmd = [
|
||||
measure_tool,
|
||||
'calculate' if opts.measure else 'policy-digest',
|
||||
'--json',
|
||||
opts.json,
|
||||
*(f'--{s.name.removeprefix(".")}={s.content}' for s in to_measure.values()),
|
||||
*(f'--bank={bank}' for bank in banks),
|
||||
# For measurement, the keys are not relevant, so we can lump all the phase paths
|
||||
# into one call to systemd-measure calculate.
|
||||
*(f'--phase={phase_path}' for phase_path in phase_paths),
|
||||
]
|
||||
|
||||
print('+', shell_join(cmd), file=sys.stderr)
|
||||
output = subprocess.check_output(cmd, text=True) # type: ignore
|
||||
# The JSON object will be used for offline signing, include the public key
|
||||
# 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 += [f'--public-key={opts.pcr_public_keys[0]}']
|
||||
elif opts.pcr_certificates:
|
||||
cmd += [f'--certificate={opts.pcr_certificates[0]}']
|
||||
if opts.certificate_provider:
|
||||
cmd += [f'--certificate-source=provider:{opts.certificate_provider}']
|
||||
|
||||
if opts.policy_digest:
|
||||
pcrsig = json.loads(output)
|
||||
pcrsigs += [pcrsig]
|
||||
else:
|
||||
print(output)
|
||||
if policyref:
|
||||
cmd += [f'--policyref={policyref}']
|
||||
|
||||
print('+', shell_join(cmd), file=sys.stderr)
|
||||
output = subprocess.check_output(cmd, text=True) # type: ignore
|
||||
|
||||
if opts.policy_digest:
|
||||
pcrsig = json.loads(output)
|
||||
pcrsigs += [pcrsig]
|
||||
else:
|
||||
print(output)
|
||||
|
||||
if opts.policy_digest:
|
||||
combined = combine_signatures(pcrsigs)
|
||||
@@ -1134,7 +1142,7 @@ def pe_add_sections(opts: UkifyConfig, uki: UKI, output: str) -> None:
|
||||
if input_bank != bank:
|
||||
continue
|
||||
for sig, input_sig in itertools.product(sigs, input_sigs):
|
||||
if sig['pol'] == input_sig['pol']:
|
||||
if (sig['pol'], sig.get('ref')) == (input_sig['pol'], input_sig.get('ref')):
|
||||
sig['sig'] = input_sig['sig']
|
||||
|
||||
encoded = json.dumps(j).encode()
|
||||
@@ -2528,8 +2536,8 @@ def finalize_options(opts: argparse.Namespace) -> None:
|
||||
raise ValueError('--phases= specifications must match --pcr-private-key=')
|
||||
if n_policyrefs is not None and n_policyrefs != n_pcr_priv:
|
||||
raise ValueError('--policyref= specifications must match --pcr-private-key=')
|
||||
if opts.sign_initrd_pcrs and not n_pcr_priv:
|
||||
raise ValueError('--sign-initrd-pcrs requires at least one --pcr-private-key=')
|
||||
if opts.sign_initrd_pcrs and not (n_pcr_priv or opts.policy_digest):
|
||||
raise ValueError('--sign-initrd-pcrs requires --pcr-private-key= or --policy-digest')
|
||||
|
||||
opts.cmdline = resolve_at_path(opts.cmdline)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user