Trusted Platform Modules

Trust Platform Modules: Concepts and capabilities #

This document serves as an introduction to the capabilities of Trusted Platform Modules (TPM). We will focus our discussion on version 2.0 of the TPM specification released by the Trusted Computing Group. The emphasis will be on the TPM’s authentication system and its capabilities beyond key management.

Each of the subsequent sections will link relevant parts of the Trusted Computing Group’s specifications. The main specification is split into four parts:

  1. Architecture,
  2. Structures,
  3. Commands, and
  4. Supporting Routines.

Only the first three parts are relevant for this document. We will refer to specification Family 2.0, Level 00, Revision 01.38, released September 29, 2018. Additionally, we will refer to the PC Client Platform Firmware Profile specification Family 2.0, Level 00, Version 1.05, Revision 23, released May 7, 2021 as “Firmware” and the PC Client Platform TPM Profile Specification for TPM 2.0 version 1.05, Revision 14, released September 2, 2020 as “Platform”.

Authentication #

There are two methods of authentication when interacting with the TPM: knowledge of an AuthValue or satisfying a Policy (Part 1, Section 19.1).

AuthValues secrets that the caller must prove knowledge of. This can either be done by sending the AuthValue as part of the command or by computing the HMAC of a TPM-provided nonce using the AuthValue as the key (Part 1, Section 19.4 & 19.6). Hierarchies, Ordinary Objects (keys), and NV Indices all have AuthValues. Except for hierarchies, the AuthValue is provided during creation and cannot be changed later. The TPM2_HierarchyChangeAuth (Part 3, Section 24.8) command can be used to change the AuthValue of a hierarchy. To do so, the caller needs to authenticate itself within that hierarchy.

Policies, also known as Enhanced Authorization (Part 1, Section 19.7; Part 3, Section 23) offer an alternative way to authenticate actions. A Policy is a hash checksum (and some auxiliary state, Part 1, Section 19.7.8) that is set during the creation of an object in the TPM. To authenticate using a policy, the caller initiates a policy session, which is an object residing in the TPM and maintaining a Policy Digest. This digest is initialized with zeros and updated by invoking Policy* commands. During authentication, the PolicyDigest maintained by the session is compared to the policy set in the object. For a successful authentication, they must be identical. The policy commands augment the policy digest with TPM state or verify specific conditions. Consequently, access to an object can be regulated by TPM state.

For instance, when the PolicyPCR (Part 3, Section 23.7) command receives a policy session’s handle and a set of PCR indices, it updates the Policy Digest of the session in the following manner:

$$ \mathit{policyDigest} \leftarrow \mathrm{H}(\mathit{policyDigest} || \mathtt{0x17e} || \mathit{pcrs} || \mathit{digest}) $$

Where $H$ is the hash function chosen when starting the policy session, $pcrs$ are the selected PCR indices, and $digest$ is the hash of the contents of the PCR selected in $pcrs$. To ensure that a key can only be used when PCR[0] has a specific state, the policy digest generated by PolicyPCR can be computed in advance and set as the key’s policy. If the key is then created with the userWithAuth attribute set to false, the only way to use it is to call PolicyPCR on the policy session on a TPM with PCR[0] set to the desired value.

Policy sessions also allow limiting the precise action that can be authenticated. The PolicyCommandCode (Part 3, Section 23.11) command updates the policy digest with a given command code and limits the session to only be used to authenticate this particular command. The PolicyCpHash (Part 3, Section 23.13) command further limits the command to a particular invocation by requiring the on-wire serialization of the command to have a predefined checksum (Part 1, Section 18.7). Both commands can only be called once per policy session.

Some Policy commands allow replacing the session’s policy digest with a new value if a certain check passes. One of them is PolicyAuthorize (Part 3, Section 23.16). This command requires the caller to provide a signature over

$$ \mathrm{H}(\mathit{currentPolicyDigest} || \mathit{policyRef}) $$

where $\mathit{policyRef}$ is a freeform string used to allow the same signing key to be used for different object’s policies. Without it, every policy that contains a call to PolicyAuthorize may potentially accept every signature made by a single signing key. To prevent this, each object would have a different $\mathit{policyRef}$ string. Because the string is included in the signed message, two PolicyAuthorize invocations with different $\mathit{policyRef}$ strings, but the same signing keys, produce different policyDigests.

The signature is validated by a key loaded in the TPM by the caller. If it is valid, PolicyAuthorize replaces the current PolicyDigest with

$$ \mathit{policyDigest} \leftarrow \mathrm{H}(\mathtt{0x16a} || \mathrm{H}(\mathit{public}) || \mathit{policyRef}) $$

where $\mathit{public}$ is the public part of the TPM resident key. This allows for later updates to a policy. The owner of an object like an NV index would create a signing key with a secret AuthValue. The NV index’s policy can then be set to the PolicyDigest computed by PolicyAuthorize. The owner would then choose a public but unique $\mathit{policyRef}$ and sign $\mathrm{H}(\mathit{approvedPolicy} || \mathit{policyRef})$ where $\mathit{approvedPolicy}$ is the Policy the owner currently wants to use for the NV index. For each new policy, a new checksum would be signed by the NV index owner. PolicyAuthorize leaves all other state of the policy session untouched.

Example Policies #

We will develop a static and a updatable authentication policy for a TPM-resident RSA encryption key. The key should be usable for decryption by any user as long as the UEFI has not been manipulated. All other operations on the key should require additional authorization. We could set the key’s AuthValue to an empty buffer but this would make the key usable regardless of the UEFI state. Additionally, this would allow users to also encrypt data with it. The solution is to use a policy. When creating the key we’ll make sure the userWithAuth attribute is cleared to disable AuthValue-based authentication for TPM2_RSA_Decrypt. We set the policy to the SHA-256 digest $p$ that is computed as follows.

$$ \def \H{\operatorname*{H}} \def \cc{\:\Vert\:} \begin{equation} \begin{split} p_{\it init} &= 0^{\lvert\H\rvert} \\ p_{\it pcr} &= \H(p_{\it init} \cc \mathtt{0x17F} \cc \mathtt{0x01} \cc h \cc \mathtt{0x01} \cc \mathtt{0x01} \cc \H(\mathrm{PCR}[0])) \\ p &= \H(p_{pcr} \cc \mathtt{0x16C} \cc \mathtt{0x159}) \\ \end{split} \end{equation} $$

The function $\operatorname{H}$ is SHA-256, and $0^{\lvert\operatorname{H}\rvert}$ represents a string of 32 zero bytes. The variable $h$ is the TPM’s 16-bit algorithm identifier for SHA-256. It is important to note that SHA-256 can be replaced with any cryptographic hash function that is implemented by the TPM.

The equations above describe the steps that a user needs to take in order to create the correct policy digest and how to precompute the policy digest when creating the key.

First, the digest is initialized with zeros. Then, it is hashed with the serialized list of the selected PCR and the PCR values themselves. Finally, the intermediate digest is hashed with the authorized command’s identifier, which is 0x159 for TPM2_RSA_Decrypt. The resulting value, denoted as $p$, is set as the key’s policy digest at the time of creation.

Later, any user can start a policy session and call TPM2_PolicyPCR with the same PCR selection that was used before (PCR[0]). The TPM will then hash that selection, the current PCR value, and the policy session’s policy digest, which was initialized to all zeros. The resulting digest becomes the new policy session digest.

Next, the user calls TPM2_PolicyCommandCode with the command code of TPM2_RSA_Decrypt as an argument. Once again, the TPM will hash the policy digest with the command code and a constant. The resulting digest replaces the policy digest of the session. Additionally, the session’s commandCode field is set to the supplied command code of TPM2_RSA_Decrypt.

Now, the user can call TPM2_RSA_Decrypt using the policy session for authorization. The TPM will then verify that the policyDigest of the policy session matches the one set for the RSA key and that the commandCode field matches the current authorizing command. Because all inputs into the policy digest computation are public constants, the digest is simply a function of the value of PCR[0].

$$ p = f(\rm PCR [0]) $$

If PCR[0] has the value that was assumed during the computation p, the authorization passes. Other commands cannot be authorized because of the check for ‘commandCode’.

The downside of this static policy is that the accepted value of PCR[0] cannot be changed afterwards. The policy digest of a key (or any TPM object) cannot be updated. A new key would need to be generated. To make policies updatable, the TPM2_PolicyAuthorize command can be used. This associates a signing key with the policy. This key can sign updates to the policy as illustrated below.

$$ \def \H{\operatorname*{H}} \def \cc{\:\Vert\:} \begin{array}{lrcll} \bf {Setup} & \it policyRef &=& \text{“PCR-BOUND-RSA-KEY”} \\ & (\it pub, \it priv) &\leftarrow& \operatorname*{Gen}(1^n) \\ & \it sig &=& \operatorname*{Sign}(\it priv, \H(p_{\it pcr}^{\prime} \cc \it policyRef)) \\ & \quad \\ \bf{Authorization} & p_{\it init} &=& 0^{\lvert\H\rvert} & \\ & p_{\it pcr} &=& \H(p_{\it init} \cc \tt 0x17F \cc 0x01 \cc {\bf h} \cc 0x01 \cc 0x01 \cc \H(\rm PCR [0])) \\ & p_{\it reset} &=& 0^{\lvert\H\rvert} & \iff \operatorname*{Verify}(\it pub, \it sig, \H(p_{\it pcr} \cc \it policyRef)) \\ & p_{\it auth} &=& \H(p_{\it reset} \cc \tt 0x16A \cc \H(\it pub) \cc \it policyRef) & \\ & p &=& \H(p_{\it auth} \cc \tt 0x16C \cc 0x159) & \\ \end{array} $$

Using TPM2_PolicyAuthorize begins with a setup phase where a new signing key is generated, and the unique but public $policyRef$ value is chosen. The key is then used to sign the hash of the expected policy digest of the updatable part, together with the $policyRef$. The key does not have to be generated by the TPM, but its public part needs to be loadable by it, i.e., it must be a key type that the TPM implements.

The signature, public key, and $policyRef$ are then used in the authorization phase. First, a new policy session is created, which is initialized with an all-zero policy digest. Then, the party wanting to use the key calls TPM2_PolicyPCR. Like before, this results in a PCR[0]-dependent policy digest. Second, TPM2_PolicyAuthorize is called with the loaded public signing key, the signature, and the $policyRef$ values of the setup phase. The TPM will now verify that the signature is valid for the hash of the current policy digest concatenated to the $policyRef$. If yes, the TPM will reset the policy digest to all zeros and then hash it together with the loaded signing key’s public key and the $policyRef$ value. This will be used as the new policy digest of the session. From here, the policy session resumes as above with a call to TPM2_PolicyCommandCode and TPM2_RSA_Decrypt.

Observe that the policy digest computed by TPM2_PolicyAuthorize is constant and no longer dependent on the value of PCR[0]; only the signature over the previous policy digest is.

$$ p = \bf g \iff \operatorname*{Verify}(\it pub, \it sig, f’(\rm PCR [0])) $$

For each new, permitted value of PCR[0], a new signature can be issued without affecting any TPM state. This scheme splits the policy into two parts: a dynamic prefix that is authorized using a signing key, and a static suffix that depends on a constant, precomputable value.

Hierarchies #

All TPM-managed objects exist in one of the following four hierarchies (Part 1, Section 13):

  1. Platform hierarchy, managed by the firmware.
  2. Endorsement hierarchy, managed by the operating system.
  3. Owner hierarchy, managed by the user.
  4. Null hierarchy, an ephemeral hierarchy also managed by the user.

Additionally, there is a Lockout hierarchy that is only used as an authentication target.

Hierarchies have an AuthValue and Policy associated with them. Both are initialized to the empty buffer when the TPM starts.

While hierarchies are isolated from each other, meaning that one hierarchy cannot access the objects of another hierarchy, the Platform hierarchy is more powerful than the others. The Platform hierarchy, along with the Lockout hierarchy, is the only one that can clear the TPM (Part 3, Section 24.6) or change the PCR allocation (Part 3, Section 22.5). To prevent later code from having Platform rights, the UEFI changes the Platform hierarchy’s AuthValue to a random value (Firmware, Section 8.3.3). The operating system should also do the same for the Endorsement hierarchy. Furthermore, most actions authenticated by the Platform hierarchy also require Physical Presence. This is an assertion that cannot be raised by a TPM command, but only through an out-of-band mechanism implemented by the platform firmware. The exact implementation is not specified, but it should require pressing a button or performing a similar non-automatable action (Part 1, Section 19.3).

Platform Configuration Registers #

One of the important features of Trusted Computing Modules is recording all code and data consumed during boot. The TPM maintains a set of registers called PCR (Part 1, Section 17) that store a hash over some part of the boot process. PCR are numbered from zero upwards. Each PCR is assigned another part of the boot process, with higher numbers generally reserved for later stages. PCR contents are reset to all zero bits on each power cycle, and PCR 17 and up can also be reset during TPM operation (Platform, Table 6).

During boot, the host CPU sends data, known as measurements, to the TPM’s PCR using either TPM2_PCR_Extend or TPM2_PCR_Event (Part 3, Sections 22.2 & 22.3). Generally, each boot stage measures the next stage before handing off execution. This way, malicious code in the boot flow can be detected. PCR cannot be written to directly; each new measurement is concatenated with the previous PCR contents and then hashed (Part 1, Section 17.2). The operation is called extension and looks like the following pseudo code.

$$ \mathit{PCR} \leftarrow \mathrm{H}(\mathit{PCR} || \mathit{data}) $$

Each PCR consists of multiple banks. Each bank is a different cryptographic hash algorithm implemented by the TPM. A PCR can have multiple banks active at the same time. Thus, the operation above is run multiple times for different $\mathrm{H}$ but the same $\mathit{data}$. The active banks can be configured using TPM2_PCR_Allocate (Part 3, Section 22.5), which requires authenticating using the Platform hierarchy.

Each PCR has an AuthValue and Policy hash associated with it, which can be changed (Part 3, Sections 22.6 & 22.7). In practice, the AuthValue is set to the empty buffer (Platform, Section 4.6), allowing everyone to extend into all PCRs.

Non-Volatile RAM #

The majority of TPM state is volatile and is reset after a power cycle. Keys need to be stored outside the TPM and loaded into it for each use, PCR, hierarchy AuthValues and Policies are reset each start. The only user accessible state that is maintained across reboots are NV indices (Part 1, Section 37). TPMs have at least 3 kB of non-volatile memory (Platform, Table 4) that can be partitioned using indices. Like keys, users can define as many indices as they want until all memory is used up. Each index has an authentication policy, an AuthValue, an Hierarchy and so on. When defining an index its type (Part 2, Section 13.2) has to be chosen from the following list:

  • Ordinary: The index occupies a constant amount of memory filled with arbitrary data.
  • Counter: The index is a 64 bit number that can only be read and incremented.
  • Bits: The index is a 64 bit number whose individual bits can be manipulated and queried.
  • Extend: The index is large enough for a digest of a user defined hash function and behaves like a PCR.

Depending on the type, only certain operations can be done on an NV index. Only ordinary indices can be overwritten with TPM2_NV_Write (Part 3, Section 31.7), only counters can be incremented with TPM2_NV_Increment (Part 3, Section 31.8), only bits type indices can have bits set with TPM2_NV_SetBits (Part 3, Section 31.10) and only extend type indices can be extended using TPM2_NV_Extend. All indices can be read using TPM2_NV_Read (Part 3, Section 31.13) and certified using TPM2_NV_Certify (Part 3, Section 31.16).

NV counters that do not have the TPMA_NV_ORDERLY (Part 3, Section 31.2) bit set are immediately written back to NVRAM after change. Indices that have the bit set are called Hybrid Indices. A Hybrid Index is kept in volatile memory and only written to NVRAM if the TPM is shutdown. In case of a power failure, the changes to the index are lost.

When creating an index, the caller defines how read and write access needs to be authenticated. The choices are:

  • the index’s AuthValue or Policy,
  • the Owner hierarchy’s AuthValue or Policy, or
  • the Platform hierarchy’s AuthValue or Policy.

The authentication source can be separately configured for read and write access.

NV indices can be locked. Each index can be individually read or write-locked (Part 3, Section 31.11 and 31.14), or globally write-locked (Part 3, Section 31.12).

In both cases, depending on the type of lock, read or write access will fail. Locking indices individually is done by sending the TPM2_NV_ReadLock or TPM2_NV_WriteLock command with a specific index’s handle. This will lock the individual index. The global write lock is engaged by sending the TPM2_GlobalWriteLock command, which will write lock all indices that have the TPMA_NV_GLOBALLOCK attribute set. This command works across hierarchies.

Locked NV indices can be configured to release the lock when the TPM is restarted (Part 1, Section 37.2.6.1) or remain locked.

Deleting an NV index can be done in two ways: either by deleting the individual index or by clearing the entire TPM. The former is done with the TPM2_UndefineSpace or TPM2_UndefineSpaceSpecial command (Part 3, Section 31.3 & 31.4) depending on whether the index requires an AuthValue or policy session for deletion. Clearing the TPM using the TPM2_Clear command (Part 3, Section 24.6) deletes all NV indices that were not created with the TPMA_NV_PLATFORM_CREATE attribute set. Setting this attribute requires authenticating with the Platform hierarchy. The clear command itself requires either Lockout or Platform hierarchy authentication.