Code Sample: Secret key provisioning and retrieval in C using Intel® Platform Trust Technology

Published: 07/31/2020

By Bryon S Nevis

File(s): Download
License: BSD-3-Clause

 

Optimized for...  
Hardware: Platforms including Intel® Platform Trust Technology or a discrete TPM 2.0 with verified and measured boot enabled and TPM drivers present in the operating system
Software: (Programming Language, tool, IDE, Framework) C (ISO C99)
Prerequisites: OpenSSL executable, headers and libraries; GNU autotools, C compiler and libraries


Introduction

A common problem that arises in IoT edge scenarios is confidentiality of secrets in the presence of physical threat vectors such as the cloning or theft of boot media. This code sample demonstrates the use of Intel® Platform Trust Technology and local attestation along with the tpm2-software stack to provision and later retrieve a secret key. (Usage of said key is not shown in this sample.) This code sample is statically linked and writes directly to the TPM device, making it easy to use in containerized environments. More importantly, critical pieces of the sample are written in both shell script and in C to illustrate how to use the TPM2 TSS C language APIs.

Background

Intel® Platform Trust Technology

Intel® Platform Trust Technology (Intel® PTT) is an integrated TPM 2.0 implementation on select Intel platforms. Intel PTT runs on the Intel® Management Engine (Intel® ME) and maintains its state separate and isolated from the host CPU and hence the host software. Intel PTT uses the platform SPI flash for persistent storage and protects its content with part-unique encryption key.

Verified Boot

Platforms based on Intel® Core™ processors use a technology called Intel® Boot Guard to implement a hardware root of trust. At the platform manufacturer's discretion, Boot Guard verifies the initial boot block in the SPI flash using keys programmed into hardware by the manufacturer. The initial boot block is then responsible for verifying—and then executing—the next boot component. This verification process is completed numerous times in succession, so that when the operating system finally boots, all the code leading up to operating system boot has been verified. Beyond this point, the operating system is responsible to prevent execution of untrusted code.

Measured Boot

Verified boot requires calculation of a digital fingerprint ("hash") of the component that is about to be executed in order to validate digital signatures. Measured boot is an extension of that. At the platform manufacturer's discretion, these measurements can be "extended" into special registers in the TPM called platform configuration registers (PCRs). At the conclusion of boot, the TPM PCRs contain a unique fingerprint of all code that was loaded and executed as part of the boot process. These PCRs can be used to attest the state of the system and can be associated with policies that govern the use of TPM-protected data.


Overview

This code sample conceptually does only two things:

  1. Generates a random secret key and encrypts it into the TPM.
  2. Retrieves that secret key if and only if allowed by policy.

The policy implemented by the sample is that the administrator will have the ability to define sets of PCR values that permit releasing of the secret key. These sets of PCR values will be signed using digital signature. Intel Platform Trust Technology will provide a TPM 2.0. Verified boot will ensure that measurements sent to the TPM cannot be forged. Measured boot will ensure that tampering with the firmware, OS loader, OS kernel, or custom-measured components will be detected, even if these components pass digital signature verification. The digital signature on the PCR values will ensure that authorized changes can be made to the firmware, OS loader, OS kernel, or custom-measured components without losing access to the secret.

There are four steps involved in enabling the use case in this sample:

  1. Bootstrapping: generating some initial secrets that are central to the use case.
  2. Policy signing: generating data files that will allow for authorized release of the secret.
  3. Provisioning: generating a new random secret and associating it with an authorization policy.
  4. Retrieval: proving to the TPM that the authorization policy is satisfied and retrieving the secret.

These steps will be explained in detail later.

There are two important decisions that need to be made up front. The first decision is how the secret is to be stored. Available options are "filesystem" and "TPM NVRAM". When using the TPM NVRAM for secret storage, the Registry of reserved TPM 2.0 handles and localities can be consulted to learn which NVRAM indexes are reserved for system use and which are available for general purpose usage. The second decision is the list of PCRs to be used to protect the secret key.

Which PCRs do I use?

"It depends" is a good answer here. If one is using a desktop-class PC, one of the best informative guides is the TCG PC Client Platform Firmware Profile Specification. This specification goes into detail regarding what goes into the first eight PCR's on the system. A summary table, reproduced from the specification, is below:

 

PCR Index PCR Usage
0 SRTM, BIOS, Host Platform Extensions, Embedded Option ROMs and PI Drivers
1 Host Platform Configuration
2 UEFI driver and application Code
3 UEFI driver and application Configuration and Data
4 UEFI Boot Manager Code (usually the MBR) and Boot Attempts
5 Boot Manager Code Configuration and Data (for use by the Boot Manager Code) and GPT/Partition Table
6 Host Platform Manufacturer Specific
7 Secure Boot Policy

 

Note that PCRs 0, 2, and 4 measure code, while PCRs 1, 3, and 5 measure data. PCR[7] deserves special mention because it is designed to work with UEFI secure boot. Whereas PCR[0] through PCR[6] contain exact measurements of code and data processed during system boot that can change due to system reconfiguration or firmware upgrades, PCR[7] contains references to keys that are utilized to verify UEFI secure boot that change rarely, rather than actual code measurements that can change frequently. Microsoft* BitLocker* documentation contains extensive documentation on the PCR registers that Microsoft* Windows* uses by default for the UEFI secure boot enabled and UEFI secure boot disabled cases.

Some modern Linux distributions record useful things to PCRs. PCR[8], for example, may contain a measurement of all GRUB commands that were executed plus the kernel command line, and PCR[9] may contain a measurement of the kernel and initramfs binaries.

PCR[16] is reserved as a debug PCR and it is resettable by software. It is especially useful for scenario testing as it can be reset without rebooting the system.

PCR selection is very subjective, use-case dependent, and depends on the software stack being used.

Compiling the Sample

Prerequisites

This code sample assumes that the underlying platform and operating system enable verified and measured boot using a virtual (Intel PTT) or discrete TPM 2.0. A further assumption is made that the SHA-256 PCR measurement bank has been enabled and is being used by the system firmware and operating system. This code sample also assumes that the appropriate TPM device drivers have been loaded (/dev/tpm0 device is visible) and that the firmware and software stack has been hardened to prevent an attacker from altering the boot flow. Lack of verified and measured boot support should not prevent the sample from executing, but it would render TPM protections ineffective in production environments since tampering with the boot flow would be undetectable.

This code sample assumes that the OpenSSL executables, headers, and libraries have been installed. This code sample produces a static executable, meaning that the resulting executable will be subject to the dual OpenSSL and SSLeay license. Additionally, the GNU autotools and the GNU C compiler must be installed in order to build the executable.

This code sample statically links to code in the TPM2 software stack, notably:

A script to fetch and build tpm2-tss and tpm2-tools in the manner required for this sample is included with the sample.

Compiling the prerequisites

Note that in the following commands "$ " or "# " represents the shell prompt. The command that should be typed is after the shell prompt.

Compiling tpm2-tss

Run the following command (a small continuous integration helper script) from the root of the sample:

$ .ci/build-tpm2-tss.sh

The command will fetch a specific version of tpm2-tss from GitHub, run the configure script, build, and install the libraries into the "local" folder of the sample. This code sample utilizes the enhanced system API (Esys_) from this package to issue TPM commands. In the event that compilation fails due to missing dependencies, install the missing dependencies, remove the tpm2-tss folder, and try again.

Compiling tpm2-tools

Run the following command (a small continuous integration helper script) from the root of the sample:

$ .ci/build-tpm2-tools.sh

The command will fetch a specific version of tpm2-tools from GitHub, run the configure script, build, and install the tpm2-tools into the "local" folder of the sample. This code sample relies on some functions defined in the "lib" folder (in libcommon.a) to parse the PCR register specification and turn it into a binary format. Also, not all of this sample is written in C—bootstrapping and policy signing continue to be done using the tpm2-tools shell commands. In the event that compilation fails due to missing dependencies, install the missing dependencies, remove the tpm2-tools folder, and try again.

Compiling the sample

Run the following commands from the root of the sample.

$ ./configure --prefix=`pwd`/local PKG_CONFIG_PATH=`pwd`/local/lib/pkgconfig
$ make

The sample, when compiled, creates the file "tools/tpmseed", which is a statically-linked C binary.

Running the Sample

Required environment variables

The samples require access to the TPM device, which is normally owned by root. This means that many of the commands below must be run as root. This is denoted by the shell prompt. "$" means the command can be run at user privilege. "#" means the command must be run as root. (It is recommended to get a long-lived root shell with "sudo bash -l" or "sudo su -" and then set the PATH and LD_LIBRARY_PATH below from that shell, as PATH and LD_LIBRARY_PATH do not inherit through sudo.) These samples are written against the version of the tpm2-tools that was locally compiled but not installed in the previous step. Please ensure the following environment variables are set to allow access to the locally compiled tools (run these commands in the root directory of the sample):

export PATH="`pwd`/local/bin:$PATH"
export LD_LIBRARY_PATH="`pwd`/local/lib:$LD_LIBRARY_PATH"

In the examples below, "target machine" refers to an IoT device that is in a configuration that would be deployed in the field. "Administrative machine" refers to a back-office machine with physical access controls that will not be deployed in the field. Since this is a code sample, the development machine can be used for both purposes for the purposes of experimentation.

Bootstrapping

The bootstrapping step creates some initial secrets that are central to the use case. Bootstrapping consists of two steps that are done just once by the administrator.

Step 1 is to create a public/private keypair. The private key will be needed to sign the PCR policy. The public key will be used in the "policy signing" step and is also deployed to the entire fleet of IoT devices for use in the "retrieval" step. The private key must be safeguarded—preferably stored offline under physical security—to prevent its disclosure.

Step 2 is to create a sealing policy that defines the conditions under which the secret can be released. The sealing policy is just some metadata plus a SHA-256 hash of the public key that signed the PCR policy. The sealing policy is later used in the "provisioning" step and is created in a standardized way that is independent of the TPM manufacturer.

Bootstrapping should be done on an administrative machine that will not be deployed in the field where tpm2-tools have been installed.

# ./bootstrap_policy.sh 
Current settings:
TPM2TOOLS_TCTI=device:/dev/tpm0
XDG_RUNTIME_DIR=/run/user/1000
POLICY_FOLDER=policies
DB_FOLDER=db
PRIVATE_FOLDER=private
Generating RSA key pair...
............................................+++++
...............................................................................................................................................+++++
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
Enter pass phrase for private/signing_key_private.pem:
Please specify PCR list against which to seal. (e.g.: "sha256:7"):
sha256:0,2,4
Creating authorized.policy...
Bootstrapping completed.

The bootstrapping script may be found in the "scripts" directory of the sample.

Policy Signing

The policy signing step is used to certify a valid system configuration in which release of the secret will be authorized. This sample places no protections on generation of the secret (for example, it can be maliciously overwritten): the only protections are around retrieval of the secret.

Three things are needed to sign a policy that authorizes release of the TPM-sealed secret:

First is the list of PCR registers that are used in the policy. This register list is hashed into the PCR policy to prevent an attacker from substituting an alternative list of PCR registers when retrieving the secret.

Second is the expected values of the above PCR registers. These register values are also hashed and extended into the existing hash, resulting in a policy that is simply a hash that has been created in a deterministic manufacturer-independent way.

Third is the private key generated in the bootstrapping phase. This private key is used to generate a signature on the PCR policy. This signature is needed in the retrieval step to associate the PCR digest to a public key that authorizes release of the TPM-bound secret.

In this code sample, the PCR register list is fixed in order to use a single-step algorithm to determine which signature to present to the TPM when retrieving the secret.

On the target machine, run the tpm2_pcrread command with the PCR list specified in the provisioning step. This will output the current PCR values for those registers:

# tpm2_pcrread sha256:0,2,4
sha256:
  0 : 0x13887470D949D717AF4FCE2811E1BCDB2531F26D3E4D6868E7579044FEF922F5
  2 : 0x3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969
  4 : 0x719B0ABD7D31A9F7BE55D10F97994AAEB7112458DC98E0A20D761E942758472B

These register values will be used in the next step.

Next, on the administrative machine, execute the sign_policy.sh script. This script is also found in the "scripts" directory of the sample.

# ./sign_policy.sh 
ERROR: ./sign_policy.sh: Policy argument is required.

Usage: ./sign_policy.sh <policy>

The script will complain that the policy needs to be named. Run it again with the name of a policy. This sample just calls it "my" policy for simplicity.

# ./sign_policy.sh my
ERROR: ./sign_policy.sh: Expected policies/my/pcr.values containg hex-coded PCR values.
ERROR: ./sign_policy.sh: Please create PCR values to be signed and try again.
Example:
echo "38E84479AB806FF4F87A4E41EF0B2F9E261C35A88B39C9168CBBB77FC536BFDC" > "policies/my/pcr.values"
echo "6E404A7FE2C05BF3F5BAE5D6B66A8794F957FFCBA6CEAE181C1EF2774926DFB9" >> "policies/my/pcr.values"
echo "0000000000000000000000000000000000000000000000000000000000000000" >> "policies/my/pcr.values"

This time the script will complain that it doesn't know what PCR values it needs to sign. Copy the values from the tpm2_pcrread command (without the 0x prefix) above into the text file as requested by the script:

# echo "13887470D949D717AF4FCE2811E1BCDB2531F26D3E4D6868E7579044FEF922F5" > policies/my/pcr.values
# echo "3D458CFE55CC03EA1F443F1562BEEC8DF51C75E14A9FCF9A7234A13F198E7969" >> policies/my/pcr.values
# echo "719B0ABD7D31A9F7BE55D10F97994AAEB7112458DC98E0A20D761E942758472B " >> policies/my/pcr.values

One last time:

# ./sign_policy.sh my
Creating signed PCR policy...
Enter pass phrase for private/signing_key_private.pem:
PCR policy signed.

The output of the above step will be the creation of a special file, such as db/66308a14c6a09f096cde46e8b6b8825cfd38c03a25c93c024453fdf8f31b1d01.signature that is required to authorize releasing of the secret. This file is the signature on the PCR policy constructed from the above PCR values. To prove that this is true:

# openssl dgst -verify db/signing_key_public.pem -signature db/66308a14c6a09f096cde46e8b6b8825cfd38c03a25c93c024453fdf8f31b1d01.signature policies/my/pcr.policy
Verified OK

Provisioning – Shell Script (Filesystem variant)

The purpose of the provisioning step is to generate a new random device-unique secret and associate it with a policy that restricts the conditions under which the secret can be later retrieved. This policy was created in the bootstrapping stage.

This code sample provides two different mechanisms storing the secret:

The first method, secret sealing, stores the secret in an on-disk data blob that is wrapped by a device-unique TPM-bound key. This method takes the output of TPM2_GetRandom and feeds it to TPM2_Create. Internally, TPM2_Create creates a key that wraps the random number, and that key is in turn protected by the key created by TPM2_CreatePrimary command, which generates a device-unique key that never leaves the TPM. In this way, the random secret can only ever be decrypted on the device that created it, and only if the authorization policy is satisfied.

The second method simply stores the secret in the TPM NVRAM. It is conceptually the same as the first method, but the retrieval flow is made simpler by not having to load the encrypted data blob into the TPM. This second method will also work if access to writable disk is not available at the time of provisioning at the cost of using up limited TPM NVRAM resources.

To provision a filesystem-based secret, run the following command on the target machine:

# ./provision_seed.sh
Provisioning random seed using authorized PCR policy...
Random seed is provisioned.

The command will create some files in the "data" folder that contain an encrypted copy of the secret:

# ls -l data
total 8
-rw-r--r-- 1 root root 128 Jun  5 12:22 seed.priv
-rw-r--r-- 1 root root  80 Jun  5 12:22 seed.pub

Provisioning – Shell Script (NVRAM variant)

To provision an NVRAM-based secret, run the following command on the target machine. Note that the choice of nv-index is arbitrary—refer to the guidance in the Overview section for advice in choosing a suitable value.

# ./provision_seed_nv.sh 
Provisioning random seed using authorized PCR policy...
nv-index: 0x1800002
Random seed is provisioned.

To prove that the NVRAM was used to provision the secret (note that authorization policy is the same value was the authorized.policy file):

# tpm2_getcap handles-nv-index
- 0x1800002
# tpm2_nvreadpublic 0x1800002
0x1800002:
  name: 000ba0207f3f3cb3c36af750264596274c092dd193f503d05d2c091ce383b10e2737
  hash algorithm:
    friendly: sha256
    value: 0xB
  attributes:
    friendly: authwrite|writeall|policyread|written
    value: 0x4100820
  size: 32
  authorization policy: 07B47A51A2B793096306BD011A0DD1D6558D18ACC207BE7FF7443C69BC5F727D


Note: to un-provision an NVRAM index, run:

# tpm2_nvundefine 0x1800002

Retrieval – Shell Script (Filesystem variant)

The retrieval step is complicated and warrants a more detailed explanation.

The goal of the retrieval step is to obtain access to the random secret that was generated in the provisioning step. A blind attempt to simply read the value will result in failure, as the secret is bound to a policy that governs under what conditions the secret can be read. Authorization is granted to the TPM to release the secret by passing a handle to an policy authorization session to the TPM2_NV_Read command (for an NVRAM-based secret) or TPM2_Unseal command (for an on-disk secret).

The authorization that is needed is a hash of the public key that signed a set of PCR values. This authorization is granted by the TPM2_PolicyAuthorize command. As evidence, TPM2_PolicyAuthorize requires the current PCR values, a verification ticket on a signature of those values, and a reference to the public key used to validate the signature. If the verification ticket is valid and the PCR values match what was signed, the authorization is changed from the hash of the actual PCR values to a hash of the public key. This authorization value will then match what was set in the policy and the secret will be released.

The verification ticket is generated by the TPM2_VerifySignature command. This command verifies an RSA signature in the normal way: it uses an asymmetric decryption to recover a hash value from a signature and compares that hash value to the hash of an input message—in this case, a PCR digest. If the two hashes match, the TPM generates a verification ticket that is built from a secret known only to the TPM, plus the hash that was signed, and the key used in verification.

The next question, of course, is which signature to use for TPM2_VerifySignature? If the administrator has authorized multiple sets of PCRs, how does the code sample know which one to submit to the TPM for authorization? Simple. The code sample simply re-creates the PCR digest from the current PCR values, converts the digest to a hexadecimal string, and looks for a file on disk named PCRDIGESTINHEX.signature. This file—generated in the policy signing step—contains the only data that will work to authorize the release of the secret.

The above steps are identical regardless of whether the secret is stored in a file or in the TPM NVRAM. The only differences are that when the secret is stored in a file, the TPM2_Load command must be called to load the secret blob into the TPM memory and the TPM2_Unseal command is used to extract the secret. Otherwise, a simple TPM2_NV_Read command is used.

To retrieve the filesystem-based secret, run the following command on the target machine:

# ./retrieve_seed.sh 
INFO: ./retrieve_seed.sh: Successfully verified PCR policy signature.
INFO: ./retrieve_seed.sh: Successfully loaded encrypted seed.
52eb15414374aa9aba632ff6f7bead4034913f4b714eb158057bceeefd9ee533

The hex-encoded output can be passed via a pipe or other IPC mechanism to a consuming process.

Retrieval – Shell Script (NVRAM variant)

To retrieve the NVRAM-based secret, run the following command on the target machine. (Note that provisioning creates a new random secret every time: it is expected that the NVRAM-based secret will be different from the filesystem-based secret.)

# ./retrieve_seed_nv.sh 
INFO: ./retrieve_seed_nv.sh: Successfully verified PCR policy signature.
cbcc457aa4f71ed59414b128217594a3dad95473a6c7a5e2ce9f54b3c57feb5e

The hex-encoded output can be passed via a pipe or other IPC mechanism to a consuming process.

Provisioning/Retrieval – C Program (Filesystem variant)

The C program does both provisioning and retrieval based on whether the file passed to the "private" option exists and is readable. If the file is readable, the program will attempt to decrypt the secret. Otherwise, it will provision a new secret. Note that the argument to the "--signature" option contains the literal value "{hash}" with the curly braces: the program will replace this value with PCR digest in order to find the correct signature file that matches the current PCRs.

# tpmseed --sealing-policy=db/authorized.policy --private=data/seed2.priv --public=data/seed2.pub --signature-key=db/signing_key_public.pem --signature=db/{hash}.signature --pcr-list=db/pcr.list -n 32 -T /dev/tpm0
7d854d7b8dcb528f339919031cf27c4f87aad41a5288e741ddcda8b79de3110a

Running the command a second time will yield the same result, returning the previously provisioned seed:

# tpmseed --sealing-policy=db/authorized.policy --private=data/seed2.priv --public=data/seed2.pub --signature-key=db/signing_key_public.pem --signature=db/{hash}.signature --pcr-list=db/pcr.list -n 32 -T /dev/tpm0
7d854d7b8dcb528f339919031cf27c4f87aad41a5288e741ddcda8b79de3110a

The hex-encoded output can be passed via a pipe or other IPC mechanism to a consuming process.

Provisioning/Retrieval – C Program (NVRAM variant)

The C program can also use the TPM NVRAM to store its data; in this case, it should be passed the "--nvindex" option specifying the NVRAM index to use. If the NVRAM index exists, it will be used to retrieve the secret; otherwise the NVRAM index will be allocated and a new secret provisioned. As above, note that the argument to the "--signature" option contains the literal value "{hash}" (with curly braces intact) – the program will replace this value with PCR digest in order to find the correct signature file that matches the current PCRs.

# tpmseed --sealing-policy=db/authorized.policy -nvindex=0x1800002 --signature-key=db/signing_key_public.pem --signature=db/{hash}.signature --pcr-list=db/pcr.list -n 32 -T /dev/tpm0
79dbd5fbe0ec16dacc80473cf7d5427d709e6c0ac00a7708e2318e4532db6a52

Retrieve the previously provisioned seed:

# tpmseed --sealing-policy=db/authorized.policy -nvindex=0x1800002 --signature-key=db/signing_key_public.pem --signature=db/{hash}.signature --pcr-list=db/pcr.list -n 32 -T /dev/tpm0
79dbd5fbe0ec16dacc80473cf7d5427d709e6c0ac00a7708e2318e4532db6a52

The hex-encoded output can be passed via a pipe or other IPC mechanism to a consuming process.

Code Walkthrough

How the Sample is Organized

This code sample is based on the tpm2-tools project and is organized into several directories:

The "lib" folder contains common subroutines that form a "seed API" that can potentially be reused.

The "scripts" folder contains shell scripts that use the tpm2-tools software distribution to perform all four steps of the use case.

The "tests" folder contains an integration test for the "tpmseed" executable.

The "tools" folder contains the source code for the "tpmseed" executable.

The "tpm2-tss" and "tpm2-tools" are placeholders for tpm2-software GitHub source that are statically linked while producing the tpmseed executable.

Other directories contain support collateral for the sample.

Bootstrapping

The bootstrapping code is implemented in the "bootstrap_policy.sh" script in the "scripts" folder. It starts by creating a (password-protected) RSA keypair:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -aes256 -out "${PRIVATE_FOLDER}/signing_key_private.pem"
openssl rsa -pubout -in "${PRIVATE_FOLDER}/signing_key_private.pem" -out "${DB_FOLDER}/signing_key_public.pem"

Next, the script prompts for a PCR register specification and saves it to a file; it will be used as input for the remaining phases.

read pcr_list
echo "${pcr_list}" > "${DB_FOLDER}/pcr.list"

Next, the script will take care of some preliminary work: loading the public signing key and saving its cryptographic identifier ("name"). It will also create a primary object that will be used to encrypt the TPM trial session for use in the next command batch.

tpm2_loadexternal --quiet --hierarchy=o --key-algorithm=rsa "--name=${tmp_signing_key_name}" "--key-context=${tmp_signing_key_ctx}" "--public=${DB_FOLDER}/signing_key_public.pem"
tpm2_createprimary --quiet "--key-context=${tmp_primary_ctx}"
tpm2_startauthsession --quiet "--key-context=${tmp_primary_ctx}" "--session=${tmp_session}"

The next two steps create an authorization policy for the release of the TPM bound secret. A PCR policy is created based on the current PCR values, and then the policy is switched to a policy based on the public key. This policy is saved to disk to be used later in the provisioning phase. The current PCR values are thrown away since these represent the administrative machine not the target machine.

tpm2_policypcr --quiet "--session=${tmp_session}" "--policy=${tmp_dummy_policy}" "--pcr-list=${pcr_list}"
tpm2_policyauthorize --quiet "--session=${tmp_session}" "--name=${tmp_signing_key_name}" "--input=${tmp_dummy_policy}" "--policy=${DB_FOLDER}/authorized.policy"

Lastly, clean up the session.

tpm2_flushcontext "${tmp_session}" && rm -f "${tmp_session}"

Policy Signing

The bootstrapping code is implemented in the "sign_policy.sh" script in the "scripts" folder. The interesting code starts halfway into the main() function. The first line takes the hexadecimal representation of the PCR values copied from the target machine and turns them into binary:

cat "${POLICY_FOLDER}/${target_policy}/pcr.values" | xxd -r -p - > "${POLICY_FOLDER}/${target_policy}/pcr.values.bin"

As in the bootstrapping script, the next step is to start an integrity-protected trial session with the TPM for issuing the next set of commands.

tpm2_createprimary --quiet "--key-context=${tmp_primary_ctx}"
tpm2_startauthsession --quiet "--key-context=${tmp_primary_ctx}" "--session=${tmp_session}"

The next step generates a PCR policy based on the user-supplied PCR values of the target rather than the administrative machine. This policy also includes information regarding the PCRs that were used to generate the policy. This policy is manufacturer independent. The session is closed afterwards.

tpm2_policypcr --quiet "--session=${tmp_session}" "--pcr=${POLICY_FOLDER}/${target_policy}/pcr.values.bin" "--policy=${POLICY_FOLDER}/${target_policy}/pcr.policy" "--pcr-list=${pcr_list}"
tpm2_flushcontext "${tmp_session}" && rm -f "${tmp_session}"

Lastly, in order to simplify the retrieval step, the contents of the PCR policy are translated into hexadecimal and used as the stub filename for the PCR signature. When the PCR policy is recreated on a target machine, this same algorithm can be used to locate the PCR policy signature. A digital signature on the PCR policy is then computed using the private key and written to that file.

signature_file="${DB_FOLDER}/`xxd -c 131072 -ps ${POLICY_FOLDER}/${target_policy}/pcr.policy`.signature"
openssl dgst -sign "${PRIVATE_FOLDER}/signing_key_private.pem" -out "${signature_file}" "${POLICY_FOLDER}/${target_policy}/pcr.policy"

Over time, repeated applications of the policy signing step will result in an on-disk database of authorized system configurations that will permit releasing of the TPM-bound secret. The resulting database is public information for which there is no disclosure risk. However, since the resulting signatures are needed at runtime to recover the secret, availability and integrity threats are in scope.

Deciding between provisioning and retrieval

For a filesystem-based secret, this step is easy — if the private portion of the secret does not exist, the provisioning flow is executed. For an NVRAM-based secret, the following code in "lib/seed.c" (Seed_Is_NV_Provisioned) is used:

TPMS_CAPABILITY_DATA *capability_data = NULL;

TSS2_RC rval = Esys_GetCapability(seed_context->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, TPM2_CAP_HANDLES, nv_index, 1, NULL, &capability_data);
if (rval != TSS2_RC_SUCCESS) {
    LOG_PERR(Esys_GetCapability, rval);
    return false;
}

*out_provisioned = (capability_data->data.handles.count > 0 && capability_data->data.handles.handle[0] == nv_index );

It is an error to read metadata about an NVRAM location that does not exist. Therefore, it is necessary to query the TPM for allocated NVRAM handles (TPM2_CAP_HANDLES) and see if the requested handle exists. If it does exist, it will be returned in the list of handles, and the code assumes that the secret has been provisioned. A more robust check would follow up with a call to TPM2_NV_ReadPublic on the index and check to see if the "written" attribute has been set. The code sample assumes that allocating and writing the NVRAM happens together and thus no need to check them separately.

Provisioning

The files involved in provisioning are:

  • scripts/provision_seed.sh (filesystem-based)
  • scripts/provision_seed_nv.sh (NVRAM-based)
  • lib/seed.c (C language port of the scripts above)

Provisioning in script looks like

tpm2_createprimary --quiet "--key-context=${tmp_primary_ctx}" --unique-data "${XDG_RUNTIME_DIR}/unique.dat"
tpm2_getrandom "${SEED_LENGTH}" | tpm2_create --quiet "--parent-context=${tmp_primary_ctx}" "--public=${DATA_FOLDER}/seed.pub" --private="${DATA_FOLDER}/seed.priv" "--policy=${DB_FOLDER}/authorized.policy" --sealing-input=-

for provisioning a secret using file system storage, or like

tpm2_nvdefine -s "${SEED_LENGTH}" -a "writeall|policyread|authwrite" "--policy=${DB_FOLDER}/authorized.policy" "${SEED_NVHANDLE}"
tpm2_getrandom "${SEED_LENGTH}" | tpm2_nvwrite --quiet -i - "${SEED_NVHANDLE}"

for provisioning a secret using TPM NVRAM storage.

In the C program, the Seed_Initialize() function is responsible for initializing the Esys session and creating the primary object (which is only used in the filesystem flow):

tpm2_createprimary --quiet "--key-context=${tmp_primary_ctx}" --unique-data "${XDG_RUNTIME_DIR}/unique.dat"

becomes

TPM2B_SENSITIVE_CREATE in_sensitive = { .size = 0 };
TPML_PCR_SELECTION creation_pcr = { .count = 0 };

rval = Esys_CreatePrimary(c->esys_context, ESYS_TR_RH_OWNER, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, &in_sensitive, &SRK_template, NULL, &creation_pcr, &c->primary_handle, NULL, NULL, NULL, NULL);

Both flows generate a random number, which is straightforward:

TSS2_RC rval = Esys_GetRandom(seed_context->esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, random_seed_length, (TPM2B_DIGEST**) out_seed);

 

Writing to NVRAM

If an NVRAM index has been provided, owner authorization is used to define the NV index. The index is defined as normal storage, with index authorization required for writing, and a policy authorization required for reading. Additionally, the entire value must be written—no partial updates are allowed:

tpm2_nvdefine -s "${SEED_LENGTH}" -a "writeall|policyread|authwrite" "--policy=${DB_FOLDER}/authorized.policy" "${SEED_NVHANDLE}"

Becomes the following:

TPM2B_NV_PUBLIC publicInfo = { .size = sizeof(publicInfo.nvPublic) };
TPM2B_AUTH null_auth = { .size = 0 };
ESYS_TR out_nvhandle = ESYS_TR_NONE;

publicInfo.nvPublic.attributes = TPM2_NT_ORDINARY | TPMA_NV_WRITEALL | TPMA_NV_AUTHWRITE | TPMA_NV_POLICYREAD;
publicInfo.nvPublic.authPolicy = *sealing_policy;
publicInfo.nvPublic.dataSize = (*out_seed)->size;
publicInfo.nvPublic.nameAlg = TPM2_ALG_SHA256;
publicInfo.nvPublic.nvIndex = nv_index;

rval = Esys_NV_DefineSpace(seed_context->esys_context, ESYS_TR_RH_OWNER, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, &null_auth, &publicInfo, &out_nvhandle);

This is followed by an NV_Write

tpm2_nvwrite --quiet -i - "${SEED_NVHANDLE}"

which translates to two separate calls to the Esys API. The first turns the NVRAM index into a handle required by the API:

ESYS_TR nvHandle = ESYS_TR_NONE;
rval = Esys_TR_FromTPMPublic(seed_context->esys_context, nv_index, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &nvHandle);

The second call writes the NVRAM index. Note that the nvHandle is used both for authorization as well as to specify which NVRAM location to write:

rval = Esys_NV_Write(seed_context->esys_context, nvHandle, nvHandle, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, (TPM2B_MAX_NV_BUFFER*) *out_seed, 0);

Writing to a file

In the case of writing to a file, this is done by asking the TPM to create a key with some sealed data:

tpm2_create --quiet "--parent-context=${tmp_primary_ctx}" "--public=${DATA_FOLDER}/seed.pub" --private="${DATA_FOLDER}/seed.priv" "--policy=${DB_FOLDER}/authorized.policy" --sealing-input=-

In C, the random secret is passed in the in_sensitive parameter.

TPM2B_SENSITIVE_CREATE in_sensitive = { .size = sizeof(in_sensitive.sensitive) };
in_sensitive.sensitive.data.size = sizeof(in_sensitive.sensitive.data.buffer);
if ((*out_seed)->size < in_sensitive.sensitive.data.size) {
    in_sensitive.sensitive.data.size = (*out_seed)->size;
}
for (int i = 0; i < in_sensitive.sensitive.data.size; i++) {
    in_sensitive.sensitive.data.buffer[i] = (*out_seed)->buffer[i];
}

For sealing data to the TPM, we must create a wrapping key and attach the policy that governs use of the key / releasing of the secret:

TPM2B_PUBLIC in_public = {
    .size = sizeof(TPMT_PUBLIC),
    .publicArea = {
        .type = TPM2_ALG_KEYEDHASH,
        .nameAlg = TPM2_ALG_SHA256,
        .objectAttributes = TPMA_OBJECT_FIXEDTPM | TPMA_OBJECT_FIXEDPARENT,
        .parameters = {
                .keyedHashDetail = {
                        .scheme = TPM2_ALG_NULL
                }
        },
        .unique = {
                .keyedHash = {
                        .size = 32
                }
        }
    }
};

in_public.publicArea.authPolicy.size = sizeof(in_public.publicArea.authPolicy.buffer);
if (sealing_policy->size < in_public.publicArea.authPolicy.size) {
    in_public.publicArea.authPolicy.size = sealing_policy->size;
}
for (int i = 0; i < in_public.publicArea.authPolicy.size; i++) {
    in_public.publicArea.authPolicy.buffer[i] = sealing_policy->buffer[i];
}

TPML_PCR_SELECTION creation_pcr = { .count = 0 };


Finally, call TPM2_Create with all the parameters previously defined. The TPM will return a sealed blob with a public portion containing metadata, and a private portion that was encrypted by the TPM. These will be saved to disk and re-loaded in the seed retrieval step.

rval = Esys_Create(seed_context->esys_context, seed_context->primary_handle, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, &in_sensitive, &in_public, NULL, &creation_pcr, out_private, out_public, NULL, NULL, NULL);

Now, on to seed retrieval:

Retrieval

The files involved in retrieval are:

  • scripts/retrieve_seed.sh (filesystem-based)
  • scripts/retreive_seed_nv.sh (NVRAM-based)
  • lib/seed.c (C language port of the scripts above)

This is the most complicated flow as it brings together all the information in the previous three steps.

Before getting started in earnest, one of the first things that needs to be done is to locate the PCR policy signature that is needed to release the TPM-bound secret. This requires re-doing the same steps what were done in the "Policy Signing" part of the workflow, but this time with the live PCR values on the target system. Recall:

tpm2_policypcr --quiet "--session=${tmp_session}" "--policy=${tmp_policy}" "--pcr-list=${pcr_list}"
policy_signature="db/`xxd -c 131072 -ps ${tmp_policy}`.signature"

The equivalent C code is in the Seed_Get_PCR_Digest() function (error checks removed for brevity):

TPML_DIGEST *pcr_values = NULL;
rval = Esys_PCR_Read(esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, pcrs, NULL, NULL, &pcr_values);

TPM2B_AUTH null_auth = { .size = 0 };
rval = Esys_HashSequenceStart(esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &null_auth, TPM2_ALG_SHA256, &sequence_handle);

for (UINT32 i = 0 ; i < pcr_values->count ; i++) {
    rval = Esys_SequenceUpdate(esys_context, sequence_handle, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, (TPM2B_MAX_BUFFER*)&pcr_values->digests[i]);
}

rval = Esys_SequenceComplete(esys_context, sequence_handle, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, NULL, TPM2_RH_NULL, &intermediate_digest, NULL);

if (!seed_start_session(seed_context, TPM2_SE_TRIAL)) {
    LOG_ERR("%s: couldn't start trial session", __func__);
    goto cleanup;
}
ESYS_TR session_handle = seed_context->session_handle;

rval = Esys_PolicyPCR(esys_context, session_handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, intermediate_digest, pcrs);

rval = Esys_PolicyGetDigest(esys_context, session_handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, pcr_digest);

To summarize, the above code retrieves the current PCR values, hashes them together, starts a trial session, asks the TPM to compute the PCR policy based on the PCR set and the supplied hash, and then reads that value back out. As in the script, the output value is used to look for a matching signature.

The next step is to load the public key and signature and generate the verification ticket:

tpm2_loadexternal --quiet --hierarchy=o "--name=${tmp_signing_key_name}" "--key-context=${tmp_signing_key_ctx}" "--key-algorithm=${signing_key_algorithm}" "--public=${verification_key}"
tpm2_verifysignature --quiet "--key-context=${tmp_signing_key_ctx}" "--ticket=${tmp_verification_tkt}" "--hash-algorithm=${signing_key_hash_algorithm}" "--message=${tmp_policy}" "--signature=${policy_signature}" --format=rsassa


And the equivalent C code (with error checks removed):

TPM2B_PUBLIC public_key = *in_public_key;
public_key.publicArea.nameAlg = TPM2_ALG_SHA256;
public_key.publicArea.objectAttributes = TPMA_OBJECT_DECRYPT | TPMA_OBJECT_SIGN_ENCRYPT | TPMA_OBJECT_USERWITHAUTH;
/* Note: any variation on publicArea will change public_key_name hash and cause PolicyAuthorize to fail */

rval = Esys_LoadExternal(esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, NULL, &public_key, TPM2_RH_OWNER, &seed_context->public_key_handle);

rval = Esys_TR_GetName(esys_context, seed_context->public_key_handle, &public_key_name);

rval = Esys_Hash(esys_context, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, (TPM2B_MAX_BUFFER*) signed_policy, TPM2_ALG_SHA256, TPM2_RH_NULL, &pcr_policy_hash, NULL);

rval = Esys_VerifySignature(esys_context, seed_context->public_key_handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, pcr_policy_hash, policy_signature, &verification_ticket);

One of the more notable points about the above code is that the attributes of the loaded public key form part of the cryptographic identity of the key; changing these attributes will cause the PolicyAuthorize command to fail. Most cryptographic APIs will take an arbitrary length input buffer when performing signature verification functions; the TPM VerifySignature command is unusual in that the input must be pre-hashed.

If the secret is stored on the file system, it must be loaded into the TPM first:

tpm2_load "--parent-context=${tmp_primary_ctx}" "--name=${tmp_seed_name}" "--key-context=${tmp_seed_ctx}" "--public=${seed_public_file}" --private="${seed_private_file}"

In C:

rval = Esys_Load(esys_context, seed_context->primary_handle, ESYS_TR_PASSWORD, ESYS_TR_NONE, ESYS_TR_NONE, blob_private, blob_public, &seed_context->sealed_object_handle);

With a verification ticket in hand and the encrypted secret loaded into the TPM, it is now time start a policy authorization session. Step 1 is to start a policy session, step 2 is to read in the current PCR values, and step 3 is to switch to the public key based policy:

tpm2_startauthsession --quiet "--key-context=${tmp_primary_ctx}" "--session=${tmp_session}" --policy-session
tpm2_policypcr --quiet "--session=${tmp_session}" --pcr-list="${pcr_list}"
if ! tpm2_policyauthorize --quiet "--session=${tmp_session}" "--input=${tmp_policy}" "--name=${tmp_signing_key_name}" "--ticket=${tmp_verification_tkt}"; then
  echo "WARN: $0: PCR policy failed to authorize (PCR values do not match authorized policy?)" >&2
  return 1
fi


The C language equivalent is nearly identical to the shell script version:

if (!seed_start_session(seed_context, TPM2_SE_POLICY)) {
    LOG_ERR("%s: couldn't start auth session", __func__);
    goto cleanup;
}
ESYS_TR session_handle = seed_context->session_handle;

TPM2B_DIGEST empty_digest = { .size = 0 };
rval = Esys_PolicyPCR(esys_context, session_handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &empty_digest, pcrs);

TPM2B_NONCE policyRef = { .size = 0 };
rval = Esys_PolicyAuthorize(esys_context, session_handle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, signed_policy, &policyRef, public_key_name, verification_ticket);

Finally, it is time to retrieve the secret. In shell:

seed_value=`tpm2_nvread "--auth=session:${tmp_session}" "${SEED_NVHANDLE}" | xxd -c 131072 -ps`

or

seed_value=`tpm2_unseal "--auth=session:${tmp_session}" "--object-context=${tmp_seed_ctx}" | xxd -c 131072 -ps`


The NVRAM retrieval flow looks similar to the NVRAM provisioning flow, with the requirement to create a handle for the NVRAM index and then pass that into the NV_Read command, after first calling NV_ReadPublic to determine the data size:

ESYS_TR nvHandle = ESYS_TR_NONE;

rval = Esys_TR_FromTPMPublic(seed_context->esys_context, nv_index, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &nvHandle);

rval = Esys_NV_ReadPublic(seed_context->esys_context, nvHandle, ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE, &nvPublic, NULL);

rval = Esys_NV_Read(seed_context->esys_context, nvHandle, nvHandle, session_handle, ESYS_TR_NONE, ESYS_TR_NONE, nvPublic->nvPublic.dataSize, 0, (TPM2B_MAX_NV_BUFFER**) out_seed);

The filesystem-based retrieval flow is straightforward as well:

rval = Esys_Unseal(esys_context, seed_context->sealed_object_handle, session_handle, ESYS_TR_NONE, ESYS_TR_NONE, (TPM2B_SENSITIVE_DATA**) out_seed);

All that remains (not shown) is to hex encode and output the secret so that it can be used for some special purpose.

Conclusion

Intel Platform Trust Technology (Intel PTT) is a useful technology based on the TCG TPM 2.0 specifications that enables protection of secret data and keys. This code sample presented a use case where Intel PTT can be used to protect a secret key using flexible PCR policy in a manner that can be easily used in a containerized environment where a TPM resource broker is not running. More importantly, this sample is a concrete example of translating a tpm2-tools script into a C language program targeting the TSS Enhanced System API.

Limitations

This is only a sample that has been designed to illustrate a TPM 2.0 use case. The use of TPM 2.0 alone is insufficient to fully secure a working system. This sample makes optimistic assumptions regarding the extent to which a manufacturer has implemented verified and measured boot and does not fully explore complex system behaviors that may affect the security of the final solution such as those that occur after kernel execution. Even though the platform manufacturer may enable verified boot, many older Linux distributions require UEFI secure boot to be disabled. Even for newer distributions, UEFI secure boot carries additional costs and procedural details that some distributions may not be able to absorb. In this case, verified boot terminates prior to invoking the Linux boot loader, allowing for interception of the boot flow at that point. The situation for measured boot is similar. The patches to boot loaders to enable measured boot have not been accepted into the upstream open source, and it is up to each operating system vendor to carry the patch set that enables measured boot. Even if verified and measured boot is implemented for the operating system kernel, unless Linux Integrity Measurement Architecture (Linux IMA) has been enabled in the kernel, protections are not carried into the post-boot environment. Linux IMA also has known unresolved issues with PCR determinism since PCR values are influenced by execution order.

The suggested mitigation for protecting the private signing key—storing the key offline—is weak. A much better solution would be to use a TPM- or HSM-based signing key. A software-based key can be disclosed by software-based attacks. Use of a TPM or HSM to protect an asymmetric key implies physical possession of a device. A robust production deployment would use either of these two methods to protect the signing key.

This sample does not address threats where the secret is deleted (via file deletion or deallocation of the NVRAM index), nor does it address malicious overwrite of the secret (neither the file nor NVRAM index is protected from writing), nor does it attempt to lock the NVRAM index from further reads or writes after being used. A suggested hardening step would be to prevent the secret from being read more than once per boot by either locking the NVRAM location or extending a PCR that is part of the protection policy after the secret is read.

The 4.1 release of tpm2-tools only supports encrypted policy sessions. The remainder of the time, password authorization (using the default password) is used when issuing TPM commands. Password sessions are susceptible to interception by kernel or physical bus attackers. A future release of tpm2-tools is adding support for encrypted HMAC sessions to address some of these concerns and additional changes would similarly be needed to utilize these features.

Feedback

To report a vulnerability with an Intel branded product or technology such as Intel Platform Trust Technology please follow the Intel vulnerability reporting process at https://www.intel.com/content/www/us/en/security-center/default.html.

To report a vulnerability in this open source code sample, please contact secure-opensource@01.org.

Please contact iot-security-samples@intel.com if you have feedback, questions, or wish to report non-security issues with this sample.

References


Export Classification: 5D002TSU

Product and Performance Information

1

Performance varies by use, configuration and other factors. Learn more at www.Intel.com/PerformanceIndex.