Featured image of post How to use TPM 2.0 to secure private keys

How to use TPM 2.0 to secure private keys

A practical guide to generating, storing, and using private keys with hardware-backed protection

Trusted Platform Module (TPM) is a secure cryptoprocessor for many security applications requiring strong device identity, data protection, and platform integrity. Some uses include:

  • Device identity and attestation
  • Secure boot
  • Key storage
  • Disk encryption
  • Digital signatures

TPM 1.2 was standardized in 2009 as ISO/IEC 11889:2009. TPM 2.0, the most common today, came out in 2015 with the ISO/IEC 11889:2015 standard. The link is for part 1 of the standard (out of 4).

Although many people think of TPM as a hardware chip, it has many possible implementations. They are:

  • A dedicated TPM chip
  • Integrated TPM as part of another chip (e.g., an ARM-based SoC)
  • Firmware TPMs (fTPMs) that run in the CPU’s trusted execution environment
  • Virtual TPMs (vTPMs) are provided by hypervisors to provide security to virtual environments
  • Software TPMs are emulators of TPMs. They are helpful for development purposes

In this article, we will focus on the use case of storing a private key in TPM 2.0.

TPM architecture

  graph TD
    subgraph TPM
        subgraph Memory
            VM[<b>Volatile Memory</b><br/>Temporary keys, Sessions,<br/>Buffers]
            NV[<b>Non-Volatile Memory</b><br/>EK, SRK, Policies,<br/>NV indexes]
        end

        subgraph Crypto engine
            CC[Cryptographic Coprocessor]
            RNG[Random Number Generator]
            KGEN[Key Generation Logic]
            CC -->|Uses| RNG
            KGEN -->|Uses| RNG
            CC -->|Performs| ENC[Encryption/Decryption]
            CC -->|Performs| SIG[Digital Signature / Hashing]
        end

        subgraph Control logic
            PCR["Platform Configuration<br/>Registers<br/>(PCRs)"]
            MM[Command Processing<br/>Engine /<br/>State Machine]
        end

    end

  style TPM fill:#5c2d91,stroke:#fff,stroke-width:2px,color:#fff
  style CC fill:#1f4e79,stroke:#fff,color:#fff
  style KGEN fill:#1f4e79,stroke:#fff,color:#fff
  style RNG fill:#2e7d32,stroke:#fff,color:#fff
  style NV fill:#5d4037,stroke:#fff,color:#fff
  style VM fill:#5d4037,stroke:#fff,color:#fff
  style PCR fill:#7b1fa2,stroke:#fff,color:#fff
  style MM fill:#37474f,stroke:#fff,color:#fff
  style ENC fill:#455a64,stroke:#fff,stroke-dasharray: 5 5,color:#fff
  style SIG fill:#455a64,stroke:#fff,stroke-dasharray: 5 5,color:#fff

This TPM architecture diagram illustrates the internal components of a Trusted Platform Module, highlighting its secure cryptographic engine, non-volatile and volatile memory, and control subsystems. Key elements include the cryptographic coprocessor, random number generator, and key generation logic, all operating within a hardware-isolated boundary. The platform configuration registers (PCRs) and command processing engine manage system state and policy enforcement, while non-volatile memory stores persistent keys and metadata. Non-volatile storage includes the Endorsement Key (EK), a unique, factory-installed identity key, and the Storage Root Key (SRK), which anchors the TPM’s key hierarchy.

TPM key storage

TPM defines four main authorization hierarchies, each rooted in a different seed and intended for various use cases:

HierarchySeed UsedPurpose
OwnerStorage SeedStorage keys, general-purpose keys
EndorsementEndorsement SeedIdentity, attestation (e.g. EK)
PlatformPlatform SeedFirmware-level trust & control
Null(None)Ephemeral keys not tied to any seed

Each hierarchy has:

  • Its own seed
  • Its own authorization policy (e.g., owner password)
  • Its own logical namespace for creating keys

We will focus on the Owner hierarchy.

While TPMs are capable of securely storing cryptographic keys, most applications avoid storing keys directly in the TPM in practice. This is because the amount of available non-volatile storage varies significantly between TPM models and is often limited. Instead, keys are typically generated or loaded temporarily into the TPM or stored externally in encrypted form and only used inside the TPM when needed.

TPM key hierarchy model

In addition to the authorization hierarchies, the TPM organizes keys in a hierarchy. This model helps balance performance, security, and the TPM’s limited storage.

Storage Seed โ†’ Parent Key โ†’ Child Key

Storage seed (Owner)

The seed is a non-exportable, hardware-internal value that acts as the TPM’s true root key. You can’t access it, but you can use it indirectly. The storage seed cannot be modified.

Parent key

Parent keys are stored persistently in TPM non-volatile memory or reloaded as needed. A parent key is any key used to encrypt (wrap) one or more child keys. The parent key must be already loaded in the TPM to load/use any of its children.

When creating a parent key, the TPM does not randomly generate the key unless you explicitly ask it to. Instead, if you provide:

  • The same hierarchy (e.g., the Owner ties it to the storage seed)
  • The same key template (same attributes, algorithms, policy)
  • The same authorization (e.g., null password)

… then the TPM will derive the exact same key every time.

The TPM uses a deterministic KDF (key derivation function). This determinism means the application does not need to store the parent key explicitly. It can be recreated when needed.

Create a parent key example

tpm2-tools is the official CLI toolset for interacting with TPM 2.0 via the TPM2 Software Stack (tss). Install it on Ubuntu/Debian like:

sudo apt update
sudo apt install tpm2-tools

The tpm2-tss should already be installed or pulled in as part of tpm2-tools installation. You can check for these libraries with: dpkg -l | grep libtss2

To interact with the TPM device using tpm2-tools, the user must either be root or a member of the tss group, which has access to /dev/tpmrm0. To add the user to the tss group, you can:

sudo usermod -aG tss $USER

Then log out and log back in for the group change to take effect.

Our examples use tpm2-tools version 5.6. See the tpm2-tools documentation for details. To create a transient parent key in TPM 2.0 using TSS (tpm2-tools) CLI, use the tpm2_createprimary command:

tpm2_createprimary \
  --hierarchy=owner \
  --key-algorithm=rsa \
  --hash-algorithm=sha256 \
  --attributes="fixedtpm|fixedparent|sensitivedataorigin|userwithauth|decrypt|restricted" \
  --key-context=parent.ctx

This command creates the parent.ctx context file, which can be used in later commands.

Below is an equivalent example of creating the parent key using the go-tpm library:

// Create a parent key template with the required attributes
parentTemplate := tpm2.New2B(tpm2.TPMTPublic{
  Type:    tpm2.TPMAlgRSA,
  NameAlg: tpm2.TPMAlgSHA256,
  ObjectAttributes: tpm2.TPMAObject{
    FixedTPM:            true, // bound to TPM that created it
    FixedParent:         true, // Required
    SensitiveDataOrigin: true, // key material generated internally
    UserWithAuth:        true, // Required, even if we use nil password
    Decrypt:             true, // Allows key to be used for decryption/unwrapping
    Restricted:          true, // Limits use to decryption of child keys
  },
  Parameters: tpm2.NewTPMUPublicParms(
    tpm2.TPMAlgRSA,
    &tpm2.TPMSRSAParms{
      KeyBits: 2048,
      Symmetric: tpm2.TPMTSymDefObject{
        Algorithm: tpm2.TPMAlgAES,
        KeyBits: tpm2.NewTPMUSymKeyBits(
          tpm2.TPMAlgAES,
          tpm2.TPMKeyBits(128),
        ),
        Mode: tpm2.NewTPMUSymMode(
          tpm2.TPMAlgAES,
          tpm2.TPMAlgCFB,
        ),
      },
    },
  ),
})

primaryKey, err := tpm2.CreatePrimary{
  PrimaryHandle: tpm2.TPMRHOwner,
  InPublic:      parentTemplate,
}.Execute(t.device)
if err != nil {
  return err
}

Child key

A child key in TPM 2.0 is created under a parent key (which must already exist or be loaded). The child key is wrapped (encrypted) by the parent key and is not usable on its ownโ€”it must be unwrapped (loaded) by the TPM using the correct parent key.

To create a child ECC key using tpm2-tools, use the tpm2-create command, like:

tpm2_create \
  --parent-context=parent.ctx \
  --key-algorithm=ecc_nist_p256 \
  --hash-algorithm=sha256 \
  --attributes="fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign|decrypt" \
  --public=child.pub \
  --private=child.priv

This command creates the child.pub and child.priv files which can be used in later commands.

To load the child key into the TPM using tpm2-tools, use the tpm2_load command, like:

tpm2_load \
  --parent-context=parent.ctx \
  --public=child.pub \
  --private=child.priv \
  --key-context=child.ctx

This command creates the child.ctx context file which can be used later.

Below is an equivalent example of creating the child key using the go-tpm library, loading the key into the TPM, and saving the context for subsequent reuse:

// Create an ECC key template for the child key
eccTemplate := tpm2.New2B(tpm2.TPMTPublic{
  Type:    tpm2.TPMAlgECC,
  NameAlg: tpm2.TPMAlgSHA256,
  ObjectAttributes: tpm2.TPMAObject{
    FixedTPM:            true,
    FixedParent:         true,
    SensitiveDataOrigin: true,
    UserWithAuth:        true, // Required even if the password is nil
    SignEncrypt:         true,
    Decrypt:             true,
  },
  Parameters: tpm2.NewTPMUPublicParms(
    tpm2.TPMAlgECC,
    &tpm2.TPMSECCParms{
      CurveID: curveID,
    },
  ),
})

// Create the key under the transient parent
createKey, err := tpm2.Create{
  ParentHandle: parentKeyHandle,
  InPublic:     eccTemplate,
}.Execute(t.device)
if err != nil {
  return err
}

// Load the key
loadedKey, err := tpm2.Load{
  ParentHandle: parentKeyHandle,
  InPrivate:    createKey.OutPrivate,
  InPublic:     createKey.OutPublic,
}.Execute(t.device)
if err != nil {
  return err
}

// Save the key context
keyContext, err := tpm2.ContextSave{
  SaveHandle: loadedKey.ObjectHandle,
}.Execute(t.device)
if err != nil {
  return err
}

๐Ÿ“ Where can a child key be persisted?

The most common approach is to persist the child keys outside the TPM.

  • You store the child’s public key and private key blob on a disk, in a secure database, or in a file system.
  • When you need to use the key, you load it into the TPM

This storage approach is safe because:

  • The private key is always encrypted with the TPM’s parent key
  • Only the correct TPM with the correct parent key can decrypt/use it

There is also a de facto standard for storing TPM 2.0 key files using ASN.1 structure. OpenConnect VPN and several other tools use this standard.

TPM signing

TPM can sign data using the child key. The output signature is in raw binary.

To sign using tpm2-tools, use the tpm2_sign command, like:

# Create dummy data and a dummy digest
echo "hello world" > data.in.raw
cat data.in.raw | openssl dgst -sha256 -binary > digest.bin
# Sign the digest
tpm2_sign \
  --key-context=child.ctx \
  --hash-algorithm=sha256 \
  --digest \
  --format=plain \
  --signature=signature.plain digest.bin

# Now we will verify the signature using openssl.
# First, we need the child public key in PEM format.
tpm2_readpublic \
  --object-context=child.ctx \
  --output=child.pem \
  --format=pem
# Now, we can verify the signature.
openssl dgst \
  -sha256 \
  -verify=child.pem \
  -keyform=pem \
  -signature=signature.plain \
  data.in.raw

Below is an example of signing using the go-tpm library:

sign := tpm2.Sign{
  KeyHandle: childKeyHandle,
  Digest: tpm2.TPM2BDigest{
    Buffer: digest,
  },
  InScheme: tpm2.TPMTSigScheme{
    Scheme: tpm2.TPMAlgECDSA,
    Details: tpm2.NewTPMUSigScheme(
      tpm2.TPMAlgECDSA,
      &tpm2.TPMSSchemeHash{
        HashAlg: hashAlg,
      },
    ),
  },
  Validation: tpm2.TPMTTKHashCheck{
    Tag: tpm2.TPMSTHashCheck,
  },
}

rsp, err := sign.Execute(tpm)
if err != nil {
  return err
}

// Get the ECDSA signature
ecdsaSig, err := rsp.Signature.Signature.ECDSA()
if err != nil {
  return err
}

Further reading

Watch the explanation of securing private keys with TPM