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:

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.policy

However, 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):

  1. Check whether the current policy digest matches the key specified in the policy via a ticket.
  2. If it succeeds, it resets the policy digest to a zero digest! This ensures that it is independent of the previous policies executed.
  3. 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 -pubout

In 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.name

Next 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.ctx

Now, 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.policy

After 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.ctx

Example 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.policy

Example 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.ctx

The 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_desired

Now 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 -Q

Next, 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.ctx

Finally, 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.ctx

Done! 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_Certify on 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.