diff --git a/man/ukify.xml b/man/ukify.xml
index b1f39d2494..5f2c86a2cd 100644
--- a/man/ukify.xml
+++ b/man/ukify.xml
@@ -249,7 +249,8 @@
systemd-cryptenroll1,
by using the option. The extra signatures reuse
the first PCRPrivateKey=/, hence one must be
- specified. Defaults to false.
+ specified, unless is used to generate the policies for offline
+ signing. Defaults to false.
@@ -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
@@ -996,7 +998,7 @@ ID=factory-reset' \
Then, sign the PCR digests offline and insert them in the JSON blob:#!/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()
diff --git a/src/ukify/test/test_ukify.py b/src/ukify/test/test_ukify.py
index fbd70386a8..5e34e2da3e 100755
--- a/src/ukify/test/test_ukify.py
+++ b/src/ukify/test/test_ukify.py
@@ -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
diff --git a/src/ukify/ukify.py b/src/ukify/ukify.py
index 9bfa1ecb78..fd00a7cbb4 100755
--- a/src/ukify/ukify.py
+++ b/src/ukify/ukify.py
@@ -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)