Two Policies to Unlock the Treasury — Having Fun with TPM Policies
Imagine that you have access to the castle's treasury, but only once the king and the treasurer have approved your request and confirmed that the castle is secure. This might involve having two locks on the door, with the keys only being given out by trusted personnel once the security protocol has been enforced. Now, let's move on from castles to the modern world. Can we solve this problem using a Trusted Platform Module (TPM), which is commonly found in modern hardware?
The short answer is yes, by using Extend Authorization (EA) policies. However, it is not as straightforward as it seems. Before we begin, there is already some excellent material available on TPMs and how policies work. If you are not familiar with TPMs, I recommend look these first:
- Imran Desai. Enhanced Authorization and TPM 2.0 Mutable Policy. TPM.dev 2012 Conference (video)
- Arthur, Will, u. a. A Practical Guide to TPM 2.0: Using the New Trusted Platform Module in the New Age of Security. Apress, 2015 doi: 10.1007/978-1-4302-6584-9
- OpenSecurityTraining2 Trusted Computing courses (Trusted Computing 1101, Trusted Computing 1102)
Now, let's take a deep dive into building this. All the examples are provided in the form of scripts using tpm2-tools and uses the Trusted Platform Module 2.0 Library, Version 184 as the specification. We assume a resource manager is used. To simplify some of the steps, we use the TPM to generate the values required for the policies. This is not necessary, however, and in one step we demonstrate how this can be done manually.
Extended Authorization Curiosities
When looking at the available policy commands, you will find TPM2_PolicyAuthorize, which is almost what we want to do: delegate authorization to an arbitrary policy digest signed by a specific key. Executing policies one after the other generally results in a logical AND, as the policy digest of the earlier policy is included in the next one. Therefore, it seems like we can just model our setup by using it once with a key from Party A and one from Party B, like so:
#Generate keys for Party A and B
openssl genrsa -out a.priv.pem 2048
openssl rsa -in a.priv.pem -out a.pub.pem -pubout
openssl genrsa -out b.priv.pem 2048
openssl rsa -in b.priv.pem -out b.pub.pem -pubout
# Get TPM names of the public keys
tpm2_loadexternal -G rsa -C o -u a.pub.pem -c a.ctx -n party_a.key.name -Q
tpm2_loadexternal -G rsa -C o -u b.pub.pem -c b.ctx -n party_b.key.name -Q
# Start a new TPM trail session
tpm2_startauthsession -S session.ctx
# Update policy digest for Party A
tpm2_policyauthorize -S session.ctx -n party_a.key.name -Q
# Update policy digest for Party B and save as authorized.policy
tpm2_policyauthorize -S session.ctx -n party_b.key.name -L authorized.policy -Q
tpm2_flushcontext session.ctx
# Protect key with this policy
tpm2_createprimary -C o -g sha256 -G ecc -c prim.ctx -Q
tpm2_create -C prim.ctx -g sha256 -G ecc -u key.pub -r key.priv -L authorized.policyHowever, this does not do what you expect; this only generates a policy where satisfying the signed policy of Party B is sufficient. If you rerun the commands from the trial session without the first policy authorize command, you will notice that the policy digest does not change.
# TPM trail session with A and B
tpm2_startauthsession -S session.ctx
# Update policy digest for Party A
tpm2_policyauthorize -S session.ctx -n party_a.key.name -Q
tpm2_policyauthorize -S session.ctx -n party_b.key.name -L authorized_ab.policy -Q
tpm2_flushcontext session.ctx
# TPM trail session with only B
tpm2_startauthsession -S session.ctx
tpm2_policyauthorize -S session.ctx -n party_b.key.name -L authorized_b.policy -Q
tpm2_flushcontext session.ctx
sha256sum authorized_ab.policy
sha256sum authorized_b.policy
Example code to show that only the last invocation of tpm2_policyauthorize does something
This actually makes sense when we consider how TPM policy authorization works. It unlocks the object if the current policy digest of the session matches the one specified in the object. If we allow for an arbitrary policy where the final digest is compared against a signature, the policy digest must be set to a value dependent only on the signature key and not the previous executed policy. TPM2_PolicyAuthorize does the following (Trusted Platform Module 2.0 Library Part 3: Commands, Section 23.16):
- Check whether the current policy digest matches the key specified in the policy via a ticket.
- If it succeeds, it resets the policy digest to a zero digest! This ensures that it is independent of the previous policies executed.
- Updates the policy digest using the command code and specified key name.
While executing most TPM policies sequentially does lead to a logical AND, for TPM2_PolicyAuthorize (and also TPM2_PolicyAuthorizeNV) this is only the case if it is the first policy in the chain and is not used twice. So we need to find another way of tying TPM2_PolicyAuthorize into our policies.
Idea
Examining the TPM policy commands, we find TPM2_PolicyNV, which only succeeds if a comparison operation on a given NV index is successful. Here, we can leverage the fact that the policy requires read access to the NV and specify an additional authorization as a parameter during policy execution. Our approach is therefore to create an NV index for each party containing their respective key within a TPM2_PolicyAuthorize policy, and then generate the final policy by combining these with a chain of TPM2_PolicyNV commands.
Completing the Puzzle
Now let's use our newly gained knowledge and the right building blocks to create a complete solution.
Signing Keys for Party A and B
Similarly to our example previously, first Party A and B need to create signing keys that are used to sign new the policy digests for TPM2_PolicyAuthorize.
# Generated by party A
openssl genrsa -out a.priv.pem 2048
openssl rsa -in a.priv.pem -out a.pub.pem -pubout
# Generated by party A
openssl genrsa -out b.priv.pem 2048
openssl rsa -in b.priv.pem -out b.pub.pem -puboutIn the next step, we will use a.pub.pem and b.pub.pem to obtain the TPM name of each and then include them in the respective policy.
Creating NV indices for A and B protected by Authorization Policies
The policies are TPM-independent, so A and B can generate them themselves and send them to C, where the object that needs to be protected by A and B lives. For convenience, we use a TPM to generate the policy instead of doing it by hand.
First, load the public keys in order to use them.
tpm2_loadexternal -G rsa -C o -u a.pub.pem -c a.key.ctx -n a.key.name
tpm2_loadexternal -G rsa -C o -u b.pub.pem -c b.key.ctx -n b.key.nameNext we create the two policies containing TPM2_PolicyAuthorize.
# Party A
tpm2_startauthsession -S session.ctx
tpm2_policyauthorize -S session.ctx -n a.key.name -L a.authorized.policy -Q
tpm2_flushcontext session.ctx
# Party B
tpm2_startauthsession -S session.ctx
tpm2_policyauthorize -S session.ctx -n b.key.name -L b.authorized.policy -Q
tpm2_flushcontext session.ctxNow, let's create the NV indices for which we will use the policies to authorize reading. To simplify policy computation once more, we will create them on a single TPM. This works because the name of the indices contains the authorization policy (authPolicy field), but does not contain any information about the TPM they are on, and therefore remains stable between TPMs. The indices just actually need to exist when executing the policy.
Note that we also allow the owner to write to them; this is necessary because we can only use an NV index in a policy if it has been written to. The value is irrelevant, as we only care about the required policy authorization for reading. We could extend the policy to enforce a write once rule, but that goes beyond the scope of this example. We also set the orderly attribute so that the data is only written to actual persistent memory on TPM shutdown.
HANDLE_A="0x1000001"
HANDLE_B="0x1000002"
tpm2_nvdefine -C o -s 1 -a "policyread|ownerwrite|orderly" $HANDLE_A -L a.authorized.policy
tpm2_nvdefine -C o -s 1 -a "policyread|ownerwrite|orderly" $HANDLE_B -L b.authorized.policyAfter that, we just write a zero byte to set the written attribute.
echo -n -e '\x0' | tpm2_nvwrite -C o $HANDLE_A -i-
echo -n -e '\x0' | tpm2_nvwrite -C o $HANDLE_B -i-Create Policy that requires A and B to provide a Policy
If we already have sessions that satisfy the signed policies of parties A and B, we could simply use the TPM to generate the policy digest.
tpm2_startauthsession -S session.ctx
echo -n -e '\x0' | tpm2_policynv -S session.ctx -i- $HANDLE_A bc -P <SESSION_AUTH_NV_A>
echo -n -e '\x0' | tpm2_policynv -S session.ctx -i- $HANDLE_B bc -P <SESSION_AUTH_NV_B> -L key.policy
tpm2_flushcontext session.ctxExample on how to generate it with sessions that satisfy the authorizations
During setup, this is unlikely to be the case. Instead, we use a simple script to derive the digest (using Trusted Platform Module 2.0 Library Part 3: Commands, Section 23.9).
import hashlib
import subprocess
import sys
from pathlib import Path
import yaml
# TPM NV Index Handles
HANDLE_A = "0x1000001"
HANDLE_B = "0x1000002"
# TPM Command Constants (pre-encoded as bytes)
POLICYNV_COMMAND = 0x00000149.to_bytes(4, byteorder="big") # TPM_CC_PolicyNV
NV_OPERAND_B = 0x0.to_bytes(1, byteorder="big")
NV_OFFSET = 0x0.to_bytes(2, byteorder="big")
NV_OPERATION = 0x000B.to_bytes(2, byteorder="big") # TPM_EO_BITCLEAR
POLICY_BASE_SIZE = 32 # SHA256 digest size
def get_nv_index_name(handle: str) -> bytes:
"""Retrieve the TPM name of an NV index using tpm2_nvreadpublic."""
result = subprocess.run(
["tpm2_nvreadpublic", handle], capture_output=True, check=True
)
nv_data = yaml.safe_load(result.stdout.decode("utf-8"))
handle_int = int(handle, base=16)
name_hex = nv_data[handle_int]["name"]
return bytes.fromhex(name_hex)
def extend_policy_nv(
policy_hash: hashlib._hashlib.HASH,
nv_index_name: bytes,
operand: bytes,
offset: bytes,
operation: bytes,
) -> None:
"""Extend a policy digest with a PolicyNV command."""
# Build argument hash: H(operand || offset || operation)
arg_hash = hashlib.sha256()
arg_hash.update(operand)
arg_hash.update(offset)
arg_hash.update(operation)
# Extend policy: H(policy || command || arg_hash || nv_name)
policy_hash.update(POLICYNV_COMMAND)
policy_hash.update(arg_hash.digest())
policy_hash.update(nv_index_name)
def calculate_policy_digest(name_a: bytes, name_b: bytes) -> bytes:
"""Calculate policy digest with two PolicyNV assertions."""
# Start with base policy: H(0x00 * 32)
policy_hash = hashlib.sha256(b"\x00" * POLICY_BASE_SIZE)
# Extend with first NV index
extend_policy_nv(policy_hash, name_a, NV_OPERAND_B, NV_OFFSET, NV_OPERATION)
# Rehash and extend for second NV index
policy_hash = hashlib.sha256(policy_hash.digest())
extend_policy_nv(policy_hash, name_b, NV_OPERAND_B, NV_OFFSET, NV_OPERATION)
return policy_hash.digest()
def main() -> bool:
"""Generate TPM policy digest and write to file."""
try:
# Retrieve NV index names
name_a = get_nv_index_name(HANDLE_A)
name_b = get_nv_index_name(HANDLE_B)
policy_digest = calculate_policy_digest(name_a, name_b)
output_path = Path("key.policy")
output_path.write_bytes(policy_digest)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
exit(1)
exit(0)
if __name__ == "__main__":
main()We finally have a policy called key.policy that we can use to protect any other TPM object. The hard part is done!
A great thing about this policy is that it is both TPM- and object-independent. This means that, as long as the NV indices exist on a TPM, we can reuse this policy. Please note that signed NV policies can be used to authorize access to any object with our final policy, so do this with caution! Unfortunately, due to the level of indirection, we cannot use TPM2_PolicyNameHash to solve this issue.
Creating a protected Key
Now, let's create a key to protect the policy.
# Protect key with this policy
tpm2_createprimary -C o -g sha256 -G ecc -c prim.ctx -Q
tpm2_create -C prim.ctx -g sha256 -G ecc -u key.pub -r key.priv -L key.policyExample to Satisfy the Policy
To provide an example, let us assume that Party A is concerned with a state measured in PCR0, while Party B is concerned with a state measured in PCR1.
Setup
First, let us use the TPM to generate the digest for the PCR policies.
# Policy for A
tpm2_pcrread -opcr0.sha256 sha256:0 -Q
tpm2_startauthsession -S session.ctx
tpm2_policypcr -S session.ctx -l sha256:0 -f pcr0.sha256 -L a.policy_desired -Q
tpm2_flushcontext session.ctx
# Policy for B
tpm2_pcrread -opcr1.sha256 sha256:1 -Q
tpm2_startauthsession -S session.ctx
tpm2_policypcr -S session.ctx -l sha256:1 -f pcr1.sha256 -L b.policy_desired -Q
tpm2_flushcontext session.ctxThe next step is using the respective private keys to sign the digests.
# Create signatures
openssl dgst -sha256 -sign a.priv.pem -out a.signature a.policy_desired
openssl dgst -sha256 -sign b.priv.pem -out b.signature b.policy_desiredNow we can use a.signature and b.signature with a.policy_desired and b.policy_desired to generate validation tickets to use in the policy later on.
Usage
In order to use our protected key, we first need to create two sessions for which we use as authorization for our NV indices.
# Generate tickets for signatures
tpm2_loadexternal -G rsa -C o -u a.pub.pem -c a.key.ctx -n a.key.name -Q
tpm2_verifysignature -c a.key.ctx -g sha256 -m a.policy_desired -s a.signature -t a.verification.tkt -f rsassa
tpm2_loadexternal -G rsa -C o -u b.pub.pem -c b.key.ctx -n b.key.name -Q
tpm2_verifysignature -c b.key.ctx -g sha256 -m b.policy_desired -s b.signature -t b.verification.tkt -f rsassa
# Create session to auth A
tpm2_startauthsession --policy-session -S a.session.ctx
tpm2_policypcr -S a.session.ctx -l sha256:0 -L a.policy_read -Q
tpm2_policyauthorize -S a.session.ctx -i a.policy_desired -n a.key.name -t a.verification.tkt -Q
# Create session to auth B
tpm2_startauthsession --policy-session -S b.session.ctx
tpm2_policypcr -S b.session.ctx -l sha256:1 -L b.policy_read -Q
tpm2_policyauthorize -S b.session.ctx -i b.policy_desired -n b.key.name -t b.verification.tkt -QNext, we create a session to authorize the key.
tpm2_startauthsession --policy-session -S session.ctx
echo -n -e '\x0' | tpm2_policynv -S session.ctx -i- $HANDLE_A bc -P session:a.session.ctx -Q
echo -n -e '\x0' | tpm2_policynv -S session.ctx -i- $HANDLE_B bc -P session:b.session.ctxFinally, let's load the key and sign the message.
# Recreate the primary key
tpm2_createprimary -C o -g sha256 -G ecc -c prim.ctx -Q
# Load the key into the TPM
tpm2_load -C prim.ctx -u key.pub -r key.priv -c key.ctx -n key.name -Q
# Sign a mesaage with the key!
echo "important message" > message.dat
tpm2_sign -c key.ctx -g sha256 -o message.sig message.dat -p session:session.ctx
# Cleanup sessions
tpm2_flushcontext session.ctx
tpm2_flushcontext a.session.ctx
tpm2_flushcontext b.session.ctxDone! We were successfully granted access to our key by satisfying the policies of two different entities.
(Optional) Attest that the Object has the Correct Policy
Now, both A and B may wish to confirm that an object has this policy. To achieve this, a few more things need to be done:
- Have an Attestation Key (AK)
- Use
TPM2_Certifyon the object (i.e the protected key) signed key - Verify the signature over the object name
- Check that the object name includes the correct public key and our policy.
We could even go the extra mile and compute the policy digest fully without using a TPM, but for simplicity we treat the above key.policy as a golden value.
In the first step, let us create an AK. Again, for simplicity's sake, we are not actually carrying out secure enrollment of the AK by checking its properties and linking its presence to the Endorsement Key (EK). Refer to the tpm.dev tutorial or Keylime's design on how to do this securely.
tpm2_createek -c ek.ctx -G ecc -u ek.pub
tpm2_createak -C ek.ctx -c ak.ctx -u ak.pub
Now we can generate a TPMS_ATTEST for the key we generated using TPM2_Certify and also immediately verify the signature.
# Generate TPMS_ATTEST structure which is signed by the AK
tpm2_certify -Q -c key.ctx -C ak.ctx -g sha256 -o key.attest -s key.sig
# Verify the siganture
tpm2_verifysignature -c ak.ctx -m key.attest -s key.sig
The only thing left to do is to check that the key in the TPMS_ATTEST structure is our key with the correct policy. This involves parsing a fair amount of data structures, but is otherwise relatively straightforward. First, we check the TPM2B_PUBLIC structure of our key to see if it includes our policy. Next, we calculate the name of the key and compare it with the name in the TPMS_ATTEST structure. The script below also checks some additional fields and verifies that the nonce is the default one.
import struct
import hashlib
import sys
from pathlib import Path
TPM_GENERATED_VALUE = 0xFF544347
TPM_ST_ATTEST_CERTIFY = 0x8017
TPM2_ALG_SHA256 = 0x000B
TPM2_ALG_ECC = 0x0023
NONCE = b"\x00\xff\x55\xaa" # Default nonce of tpm2_certify
# TPMS_ATTEST structure
TPMS_ATTEST_MAGIC_SIZE = 4
TPMS_ATTEST_TYPE_SIZE = 2
TPMS_ATTEST_QUALIFIED_SIGNER_SIZE = 34
TPMS_ATTEST_EXTRADATA_MAX_SIZE = 4
TPMS_ATTEST_CLOCK_INFO_SIZE = 17 # 8 (clock) + 4 (reset) + 4 (restart) + 1 (safe)
TPMS_ATTEST_FIRMWARE_VERSION_SIZE = 8
TPMS_ATTEST_NAME_SIZE = 34
# TPM2B_PUBLIC/TPMT_PUBLIC
TPM2B_PUBLIC_SIZE_FIELD = 2
TPMT_PUBLIC_TYPE_SIZE = 2
TPMT_PUBLIC_NAME_ALG_SIZE = 2
TPMT_PUBLIC_OBJECT_ATTR_SIZE = 4
# Digest size
TPM2_SHA256_DIGEST_SIZE = 32
def extract_name(attest_data: bytes) -> bytes:
"""Extract the certified key name from a TPMS_ATTEST structure.
Parses a TPMS_ATTEST structure and extracts the name field, which contains
the TPM name of the key that was certified. Also validates the structure's
magic constant, attestation type, and nonce.
"""
# Parse magic and attestation type
offset = 0
(magic, attest_type) = struct.unpack_from(">IH", attest_data, offset)
if magic != TPM_GENERATED_VALUE:
raise ValueError(
f"Invalid magic constant: expected 0x{TPM_GENERATED_VALUE:08X}, "
f"got 0x{magic:08X}"
)
if attest_type != TPM_ST_ATTEST_CERTIFY:
raise ValueError(
f"Invalid attestation type: expected 0x{TPM_ST_ATTEST_CERTIFY:04X}, "
f"got 0x{attest_type:04X}"
)
# Parse qualified signer (not used for validation)
offset += TPMS_ATTEST_MAGIC_SIZE + TPMS_ATTEST_TYPE_SIZE
(_qualified_signer_size, _qualified_signer) = struct.unpack_from(
f">H{TPMS_ATTEST_QUALIFIED_SIGNER_SIZE}s", attest_data, offset
)
# Could verify this matches the AK, but not required for basic validation
offset += 2 + TPMS_ATTEST_QUALIFIED_SIGNER_SIZE
# Parse and validate extra data (nonce)
(extradata_size, extradata) = struct.unpack_from(
f">H{TPMS_ATTEST_EXTRADATA_MAX_SIZE}s", attest_data, offset
)
if extradata_size != len(NONCE) or extradata[:extradata_size] != NONCE:
raise ValueError(
f"Nonce mismatch: expected {NONCE.hex()}, "
f"got {extradata[:extradata_size].hex()}"
)
offset += 2 + TPMS_ATTEST_EXTRADATA_MAX_SIZE
# Skip clock info
offset += TPMS_ATTEST_CLOCK_INFO_SIZE
# Skip firmware version
offset += TPMS_ATTEST_FIRMWARE_VERSION_SIZE
# Extract certified key name
(certified_name_size, certified_name) = struct.unpack_from(
f">H{TPMS_ATTEST_NAME_SIZE}s", attest_data, offset
)
return certified_name[:certified_name_size]
def calculate_name(key: bytes) -> bytes:
"""Calculate the TPM name of a public key.
The TPM name is calculated by hashing the TPMT_PUBLIC structure (the inner
structure within TPM2B_PUBLIC, excluding the 2-byte size prefix) and
prepending the hash algorithm identifier.
For SHA256, the name is: 0x000B (TPM2_ALG_SHA256) || SHA256(TPMT_PUBLIC)
"""
digest = hashlib.sha256(key[TPM2B_PUBLIC_SIZE_FIELD:]).digest()
return struct.pack(f">H{TPM2_SHA256_DIGEST_SIZE}s", TPM2_ALG_SHA256, digest)
def validate_key(key: bytes, key_policy: bytes) -> bool:
"""Validate key properties and policy.
Checks that the key uses:
- ECC algorithm for the key type
- SHA256 for the naming algorithm
- The expected policy digest
"""
# Parse key algorithm type
offset = TPM2B_PUBLIC_SIZE_FIELD
(key_type,) = struct.unpack_from(">H", key, offset)
if key_type != TPM2_ALG_ECC:
raise ValueError(
f"Expected ECC key (0x{TPM2_ALG_ECC:04X}), got 0x{key_type:04X}"
)
# Parse naming algorithm
offset += TPMT_PUBLIC_TYPE_SIZE
(name_alg,) = struct.unpack_from(">H", key, offset)
if name_alg != TPM2_ALG_SHA256:
raise ValueError(
f"Expected SHA256 naming algorithm (0x{TPM2_ALG_SHA256:04X}), "
f"got 0x{name_alg:04X}"
)
# Parse and validate policy digest (skip object attributes)
offset += TPMT_PUBLIC_NAME_ALG_SIZE + TPMT_PUBLIC_OBJECT_ATTR_SIZE
(policy_size, policy_digest) = struct.unpack_from(
f">H{TPM2_SHA256_DIGEST_SIZE}s", key, offset
)
if policy_digest[:policy_size] != key_policy:
raise ValueError(
f"Policy digest mismatch: expected {key_policy.hex()}, "
f"got {policy_digest[:policy_size].hex()}"
)
return True
def main():
try:
key = Path("key.pub").read_bytes()
attest_data = Path("key.attest").read_bytes()
key_policy = Path("key.policy").read_bytes()
validate_key(key, key_policy)
expected_name = calculate_name(key)
found_name = extract_name(attest_data)
if expected_name != found_name:
raise ValueError(
f"Name mismatch: expected {expected_name.hex()}, "
f"found {found_name.hex()}"
)
print("Attestation successful")
exit(0)
except FileNotFoundError as e:
print(f"Error: Required file not found: {e.filename}", file=sys.stderr)
exit(1)
except ValueError as e:
print(f"Validation failed: {e}", file=sys.stderr)
exit(1)
except Exception as e:
print(f"Unexpected error: {e}", file=sys.stderr)
exit(1)
if __name__ == "__main__":
main()Done! We can now check remotely whether a key uses our policy.
Alternatives
You might wonder why TPM2_PolicySecret cannot be used to implement this, as it allows to delegate the authentication to another objects authentication. Unfortunately it just ties to the authValue (i.e. password) and we cannot tie it to the authPolicy of an object. It would have been a great addition, but looking at the original use case of changing the authValue of group keys, it makes sense, because authPolicy is part of the name, while the authValue is not. This means the only time this would be useful was if the policy was pointing to an authorization policy. If one only cares about signatures and not additional policies, TPM_PolicySigned is definitely also an option. Just limit the ticket in time or to the session.
I hope you enjoyed exploring the intricacies of TPM EA policies with me and combining building blocks in unusual ways to solve a problem.