measure: expose offline signing data in policy-digest output
The PolicyPCR digest is not always the complete input to an offline
signature. PolicyAuthorize also includes the hashed policy reference.
This is now required for NVPCRs to work in the initrd.
Expose the complete data as "tbs" (to-be-signed) and use it when ukify
matches offline signatures. Keep "pol" as the compatibility fallback.
Fixes: https://github.com/systemd/systemd/issues/43635
Follow-up for f0f3687700
This commit is contained in:
+12
-8
@@ -113,6 +113,10 @@
|
||||
TPM2 policy and print its digest. This will write a JSON object to standard output that contains
|
||||
the policy digests for all specified PCR banks (see the <option>--bank=</option> option below),
|
||||
so that it may be signed offline, for the cases where the private key is not directly accessible.
|
||||
Each policy contains a <literal>pol</literal> field with the approved PolicyPCR digest and a
|
||||
<literal>tbs</literal> field with the data to sign. The two fields have the same value when no
|
||||
policy reference is specified. With <option>--policyref=</option>, <literal>tbs</literal> is the
|
||||
concatenation of <literal>pol</literal> and the digest of the policy reference.
|
||||
If <option>--public-key=</option> or <option>--certificate=</option> are specified, the JSON object
|
||||
will also contain the key fingerprint.</para>
|
||||
|
||||
@@ -280,14 +284,14 @@
|
||||
<varlistentry>
|
||||
<term><option>--policyref=<replaceable>STRING</replaceable></option></term>
|
||||
|
||||
<listitem><para>When generating a PCR JSON signature (via the <command>sign</command> command), bind
|
||||
the generated signatures to the specified policy reference. The policy reference is an opaque string
|
||||
that is included in the data that is signed, and is also incorporated into the authorization policy of
|
||||
any TPM2 resource that is locked against these signatures (via the
|
||||
<constant>TPM2_PolicyAuthorize</constant> assertion). As a result, a signed policy is only accepted
|
||||
for a resource whose authorization policy was created with the same policy reference. Signed policies
|
||||
created with a different policy reference are rejected, even if they are produced with the same
|
||||
signing key.</para>
|
||||
<listitem><para>When generating a PCR JSON signature or policy digest (via the
|
||||
<command>sign</command> or <command>policy-digest</command> command), bind the generated signatures
|
||||
to the specified policy reference. The policy reference is an opaque string that is included in the
|
||||
data that is signed, and is also incorporated into the authorization policy of any TPM2 resource that
|
||||
is locked against these signatures (via the <constant>TPM2_PolicyAuthorize</constant> assertion). As
|
||||
a result, a signed policy is only accepted for a resource whose authorization policy was created with
|
||||
the same policy reference. Signed policies created with a different policy reference are rejected,
|
||||
even if they are produced with the same signing key.</para>
|
||||
|
||||
<para>This can be used together with <option>--phase=</option> and <option>--append=</option> to bind
|
||||
signatures for different boot phases to different policy references, so that a single signing key can
|
||||
|
||||
+4
-6
@@ -998,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, hashlib, json, subprocess
|
||||
import base64, json, subprocess
|
||||
|
||||
priv_key = '/home/zbyszek/src/systemd/tpm2-pcr-private.pem'
|
||||
base_file = 'base.pcrs'
|
||||
@@ -1007,11 +1007,9 @@ base = json.load(open(base_file))
|
||||
|
||||
for bank,policies in base.items():
|
||||
for policy in policies:
|
||||
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=to_sign,
|
||||
tbs = base64.b16decode(policy['tbs'].upper())
|
||||
call = subprocess.run(['openssl', 'dgst', '-sha256', '-sign', priv_key],
|
||||
input=tbs,
|
||||
check=True,
|
||||
capture_output=True)
|
||||
sig = base64.b64encode(call.stdout).decode()
|
||||
|
||||
+11
-10
@@ -1009,17 +1009,17 @@ static int build_policy_digest(bool sign) {
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Could not calculate PolicyPCR digest: %m");
|
||||
|
||||
_cleanup_(iovec_done) struct iovec tbs_data = {};
|
||||
r = tpm2_make_policy_authorize_tbs_data(&pcr_policy_digest, arg_policyref, &tbs_data);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_free_ void *sig = NULL;
|
||||
size_t ss = 0;
|
||||
if (privkey) {
|
||||
/* We always use SHA256 for signing currently. Regardless of the bank. */
|
||||
const EVP_MD *sha256 = ASSERT_PTR(sym_EVP_get_digestbyname("sha256"));
|
||||
|
||||
_cleanup_(iovec_done) struct iovec tbs_data = {};
|
||||
r = tpm2_make_policy_authorize_tbs_data(&pcr_policy_digest, arg_policyref, &tbs_data);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = digest_and_sign(sha256, privkey, tbs_data.iov_base, tbs_data.iov_len, &sig, &ss);
|
||||
if (r == -EADDRNOTAVAIL)
|
||||
return log_error_errno(r, "Hash algorithm '%s' not available while signing. (Maybe OS security policy disables this algorithm?)", sym_EVP_MD_get0_name(p->md));
|
||||
@@ -1042,11 +1042,12 @@ static int build_policy_digest(bool sign) {
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *bv = NULL;
|
||||
r = sd_json_buildo(&bv,
|
||||
SD_JSON_BUILD_PAIR_VARIANT("pcrs", a), /* PCR mask */
|
||||
SD_JSON_BUILD_PAIR_CONDITION(pubkey_fp_size > 0, "pkfp", SD_JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)), /* SHA256 fingerprint of public key (DER) used for the signature */
|
||||
SD_JSON_BUILD_PAIR_CONDITION(!isempty(arg_policyref), "ref", SD_JSON_BUILD_STRING(arg_policyref)), /* TPM2 policy reference */
|
||||
SD_JSON_BUILD_PAIR_HEX("pol", pcr_policy_digest.buffer, pcr_policy_digest.size), /* TPM2 policy hash that is signed */
|
||||
SD_JSON_BUILD_PAIR_CONDITION(ss > 0, "sig", SD_JSON_BUILD_BASE64(sig, ss))); /* signature data */
|
||||
SD_JSON_BUILD_PAIR_VARIANT("pcrs", a), /* PCR mask */
|
||||
SD_JSON_BUILD_PAIR_CONDITION(pubkey_fp_size > 0, "pkfp", SD_JSON_BUILD_HEX(pubkey_fp, pubkey_fp_size)), /* SHA256 fingerprint of public key (DER) used for the signature */
|
||||
SD_JSON_BUILD_PAIR_CONDITION(!isempty(arg_policyref), "ref", SD_JSON_BUILD_STRING(arg_policyref)), /* TPM2 policy reference */
|
||||
SD_JSON_BUILD_PAIR_HEX("pol", pcr_policy_digest.buffer, pcr_policy_digest.size), /* TPM2 PolicyPCR digest */
|
||||
SD_JSON_BUILD_PAIR_CONDITION(!sign, "tbs", SD_JSON_BUILD_HEX(tbs_data.iov_base, tbs_data.iov_len)), /* data to sign offline */
|
||||
SD_JSON_BUILD_PAIR_CONDITION(ss > 0, "sig", SD_JSON_BUILD_BASE64(sig, ss))); /* signature data */
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to build JSON object: %m");
|
||||
|
||||
|
||||
@@ -1328,6 +1328,34 @@ def test_key_cert_generation_common_name_too_long(tmp_path):
|
||||
ukify.generate_keys(opts)
|
||||
|
||||
|
||||
def test_apply_pcr_signatures() -> None:
|
||||
pcrsig = {
|
||||
'sha256': [
|
||||
{'pol': 'same', 'tbs': 'first'},
|
||||
{'pol': 'same', 'tbs': 'second'},
|
||||
{'pol': 'legacy', 'ref': 'first'},
|
||||
{'pol': 'legacy', 'ref': 'second'},
|
||||
]
|
||||
}
|
||||
signatures = {
|
||||
'sha256': [
|
||||
{'pol': 'same', 'tbs': 'second', 'sig': 'second-signature'},
|
||||
{'pol': 'same', 'tbs': 'first', 'sig': 'first-signature'},
|
||||
{'pol': 'legacy', 'ref': 'second', 'sig': 'legacy-second-signature'},
|
||||
{'pol': 'legacy', 'ref': 'first', 'sig': 'legacy-first-signature'},
|
||||
]
|
||||
}
|
||||
|
||||
ukify.apply_pcr_signatures(pcrsig, signatures)
|
||||
|
||||
assert [policy['sig'] for policy in pcrsig['sha256']] == [
|
||||
'first-signature',
|
||||
'second-signature',
|
||||
'legacy-first-signature',
|
||||
'legacy-second-signature',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not slow_tests, reason='slow')
|
||||
def test_join_pcrsig(capsys, kernel_initrd, tmp_path):
|
||||
if kernel_initrd is None:
|
||||
|
||||
+20
-8
@@ -749,6 +749,25 @@ def combine_signatures(pcrsigs: list[dict[str, str]]) -> str:
|
||||
return json.dumps(combined)
|
||||
|
||||
|
||||
def apply_pcr_signatures(pcrsig: dict[str, Any], signatures: dict[str, Any]) -> None:
|
||||
for (bank, policies), (input_bank, input_policies) in itertools.product(
|
||||
pcrsig.items(), signatures.items()
|
||||
):
|
||||
if input_bank != bank:
|
||||
continue
|
||||
for policy, input_policy in itertools.product(policies, input_policies):
|
||||
if 'tbs' in policy and 'tbs' in input_policy:
|
||||
match = policy['tbs'] == input_policy['tbs']
|
||||
else:
|
||||
match = (policy['pol'], policy.get('ref')) == (
|
||||
input_policy['pol'],
|
||||
input_policy.get('ref'),
|
||||
)
|
||||
|
||||
if match:
|
||||
policy['sig'] = input_policy['sig']
|
||||
|
||||
|
||||
def key_path_groups(
|
||||
opts: UkifyConfig,
|
||||
include_extra: bool,
|
||||
@@ -1136,14 +1155,7 @@ def pe_add_sections(opts: UkifyConfig, uki: UKI, output: str) -> None:
|
||||
.rstrip(b'\x00')
|
||||
.decode()
|
||||
)
|
||||
for (bank, sigs), (input_bank, input_sigs) in itertools.product(
|
||||
j.items(), signatures.items()
|
||||
):
|
||||
if input_bank != bank:
|
||||
continue
|
||||
for sig, input_sig in itertools.product(sigs, input_sigs):
|
||||
if (sig['pol'], sig.get('ref')) == (input_sig['pol'], input_sig.get('ref')):
|
||||
sig['sig'] = input_sig['sig']
|
||||
apply_pcr_signatures(j, signatures)
|
||||
|
||||
encoded = json.dumps(j).encode()
|
||||
if len(encoded) > section.SizeOfRawData:
|
||||
|
||||
@@ -83,17 +83,18 @@ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "/tmp/pcrsign-
|
||||
openssl rsa -pubout -in "/tmp/pcrsign-private.pem" -out "/tmp/pcrsign-public.pem"
|
||||
|
||||
# Verify that the offline signature obtained via policy-digests is the same as an online signature created
|
||||
# with the same key by systemd-measure
|
||||
digest="$("$SD_MEASURE" policy-digest --json=short --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha256 --public-key="/tmp/pcrsign-public.pem" --phase=:)"
|
||||
signed_digest="$("$SD_MEASURE" sign --json=short --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=:)"
|
||||
echo "$digest" | jq -r '.sha256 | to_entries[] | .value.pol' | while read -r pol; do
|
||||
# with the same key and policy reference by systemd-measure
|
||||
digest="$("$SD_MEASURE" policy-digest --json=short --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha256 --public-key="/tmp/pcrsign-public.pem" --phase=: --policyref=foo)"
|
||||
signed_digest="$("$SD_MEASURE" sign --json=short --linux=/tmp/tpmdata1 --initrd=/tmp/tpmdata2 --bank=sha256 --private-key="/tmp/pcrsign-private.pem" --public-key="/tmp/pcrsign-public.pem" --phase=: --policyref=foo)"
|
||||
echo "$digest" | jq -r '.sha256 | to_entries[] | [.value.pol, .value.tbs] | @tsv' | while read -r pol tbs; do
|
||||
# Note: basenc before coreutils 9.5 refuses lowercase hex input strings
|
||||
offline_sig="$(echo -n "$pol" | \
|
||||
offline_sig="$(echo -n "$tbs" | \
|
||||
tr '[:lower:]' '[:upper:]' | \
|
||||
basenc --base16 --decode | \
|
||||
openssl dgst -sign /tmp/pcrsign-private.pem -sha256 | \
|
||||
base64 -w0)"
|
||||
online_sig="$(echo "$signed_digest" | jq -r --arg pol "$pol" '.sha256[] | select(.pol == $pol) | .sig')"
|
||||
test "$tbs" != "$pol"
|
||||
test -n "$offline_sig"
|
||||
test -n "$online_sig"
|
||||
test "$offline_sig" = "$online_sig"
|
||||
|
||||
Reference in New Issue
Block a user