Perform cryptographic operations with symmetric keys
After introducing asymmetric keys in the previous pill, it's time to move on to symmetric keys.
Like asymmetric keys, symmetric keys can be used to perform encryption/decryption or signature operations. These two types of operations allow us to address a number of use cases, the most well-known being probably the encryption function that enables Bitlocker (on Windows) and systemd (on Linux) to work.
During an onboarding phase, a secret (aka symmetric key) is produced to encrypt and decrypt the disk with a good security/speed ratio. However, a question remains: how to securely store this secret at rest? The TPM is the component (co-located on the machine) that carries this responsibility. Subsequently, during the early boot, the encrypted (sealed) secret is provided to the TPM which returns the plaintext value.
Now, let's take a closer look at how things work through a series of small examples!
The code of the CLI that you will see below is fully available here.
Note: if you want to use a real TPM in the examples, you can add --use-real-tpm flag in each command (except cleanup).
Encryption / Decryption vs. Seal / Unseal
It is necessary to distinguish between encryption and sealing. In one case we will use a key to perform cryptographic operations (e.g. AES or SM4), in the other we will want to protect a secret generally produced outside the TPM.
Encryption / Decryption
Most TPMs do not support symmetric encryption as indicated in the spec below:

For example, my machine's TPM does not support this function.
If you are on Linux, you can check support by running the following command:
tpm2_getcap commands | grep -i encryptdecrypt
Let's start by creating a symmetric key with the following characteristics:
| sign | decrypt | restricted |
|---|---|---|
| 0 | 1 | 0 |
Table: Key attributes
# Note: the key will be stored in the current directory
# with the name `key.tpm`
go run github.com/loicsikidi/tpm-pills/examples/06-pill create
# output: Ordinary key created successfully 🚀
The key is AES 128-bit type and the encryption mode is CFB (Cipher Feedback).
Why this mode?
Although far from current standards (e.g. lack of data authentication), it benefits from wide support.
Now, we can perform encryption / decryption:
go run github.com/loicsikidi/tpm-pills/examples/06-pill encrypt --message "Hello TPM Pills!" --output ./blob.enc
# output: Encrypted message saved to ./blob.enc 🚀
go run github.com/loicsikidi/tpm-pills/examples/06-pill decrypt --key ./key.tpm \
--in ./blob.enc
# output: Decrypted "Hello TPM Pills!" successfully 🚀
# clean up
go run github.com/loicsikidi/tpm-pills/examples/06-pill cleanup
rm -f ./key.tpm ./blob.enc
Under the hood, the CLI uses the TPM2_EncryptDecrypt2 command (which is the successor of TPM2_EncryptDecrypt as it adds a security element1)
rsp, _ := tpm2.EncryptDecrypt2{
KeyHandle: keyHandle, // reference to the key doing the job
// if the payload is bigger than TPM's buffer
// we can use pagination ⬇️
Message: tpm2.TPM2BMaxBuffer{
Buffer: block,
},
Mode: mode, // eg. CFB, GCM, etc.
Decrypt: decrypt, // this is a boolean
IV: tpm2.TPM2BIV{
Buffer: iv,
},
}.Execute(tpm)
Seal / Unseal
Since support for TPM2_EncryptDecrypt and TPM2_EncryptDecrypt2 is not widespread, you will mostly use sealing to encrypt secrets at rest.
$IMAGE
To do this, you have two steps:
- the seal:
TPM2_CreatePrimaryorTPM2_Create - the unseal:
TPM2_Unseal
In the implementations I've most often seen TPM2_Create command used for sealing under a Storage Key2.
Data to seal is nothing more and nothing less than a key with the following properties:
| sign | decrypt | restricted |
|---|---|---|
| 0 | 0 | 0 |
Table: Sealed data key attributes
In short, a key that technically can do nothing except return its content via the TPM2_Unseal command.
Let's see what it looks like in code:
# SEAL
createRsp, _ := tpm2.Create{
ParentHandle: parentHandle,
InPublic: tpm2.New2B(tpm2.TPMTPublic{
Type: tpm2.TPMAlgKeyedHash, // indicates that it's a shared secret
NameAlg: tpm2.TPMAlgSHA256,
ObjectAttributes: tpm2.TPMAObject{
FixedTPM: true,
FixedParent: true,
UserWithAuth: true,
SensitiveDataOrigin: false, // necessary because the value has been generated outside the TPM
SignEncrypt: false,
Decrypt: false,
Restricted: false,
},
}),
InSensitive: TPM2BSensitiveCreate{
Sensitive: &TPMSSensitiveCreate{
Data: NewTPMUSensitiveCreate(&TPM2BSensitiveData{
Buffer: []byte("VALUE TO SEAL"), // data blob
}),
},
},
}.Execute(tpm)
# UNSEAL
unsealRsp, _ := tpm2.Unseal{
ItemHandle: keyHandle, // key to unseal's handle
}.Execute(thetpm)
As we can see above, the seal consists of creating a symmetric key by providing it with the value. The TPM via the Primary Seed (i.e. TPM2_CreatePrimary) or the Storage Parent (i.e. TPM2_Create) is able to encrypt the key's content to store it securely.
The spec limits the secret size to 128 bytes so keep that in mind!

Please find below a concrete example using the CLI:
go run github.com/loicsikidi/tpm-pills/examples/06-pill seal \
--message "important secret" --output ./sealed_key.tpm
# output: Sealed message saved to ./sealed_key.tpm 🚀
go run github.com/loicsikidi/tpm-pills/examples/06-pill unseal --in ./sealed_key.tpm
# output: Unsealed message: "important secret" 🚀
# clean up
go run github.com/loicsikidi/tpm-pills/examples/06-pill cleanup
rm -f ./sealed_key.tpm
Signature
Here, HMAC3 is used to sign messages, most often for authentication purposes. For example, the S3 authentication protocol relies on this principle (i.e. AWS Signature v4) by sharing a shared secret with the client.
Whenever possible, try to base authentication on ephemeral identities through Identity Federation4 rather than long-term secrets even when stored in a TPM.
For this use case, this pill will not provide an example, however, here are two repos that show how to implement this in Go:
Deep dive: derives key (KDF)
In addition to signing, the HMAC function provides a quite interesting property, that of being able to derive other symmetric keys from a master key.

The idea is quite elegant in that:
- the master key is stored in a secure environment (i.e. TPM)
- the TPM is autonomous to generate the HMAC
The HMAC plays the role of Pseudorandom Function (PRF) to produce a seed that will be used by a Key Derivation Function (KDF) to generate a key with good entropy.
It's a deterministic function that:
- Takes two inputs:
- A secret key K (master key)
- Some arbitrary data M (message/data)
- Produces an output:
- A pseudorandom output of fixed length
- Indistinguishable from a truly random function for an attacker who doesn't know K
- Essential properties:
- Deterministic: PRF(K, M) always produces the same result
- Pseudorandom: Without knowing K, the output appears random
- One-way: Impossible to recover K or M from the output
- Collision resistant: Difficult to find M₁ ≠ M₂ such that PRF(K,M₁) = PRF(K,M₂)
Here's how this applies in a TPM context:
K: is an HMAC type key that is loaded in the TPM (key handle in the diagram below)M: is the data to be signed (data in the diagram below)

The example below shows how to generate an HMAC with a digest produced with SHA-256:
go run github.com/loicsikidi/tpm-pills/examples/06-pill hmac --data "secret"
# output: HMAC result: "bde701bc281f6d5e55ee29b30c08c59fb05425298442b5060238af88305964a0" 🚀
# value is deterministic
go run github.com/loicsikidi/tpm-pills/examples/06-pill hmac --data "secret"
# output: HMAC result: "bde701bc281f6d5e55ee29b30c08c59fb05425298442b5060238af88305964a0" 🚀
# clean up
go run github.com/loicsikidi/tpm-pills/examples/06-pill cleanup
Note: the output is formatted in hexadecimal for clarity but this doubles the payload size. Originally it is 32 bytes, in hex it is therefore 64 bytes.
The key that was produced has the following characteristics this time:
| sign | decrypt | restricted |
|---|---|---|
| 1 | 0 | 0 |
Table: HMAC key attributes
Since HMAC is a signature protocol, this is indeed a prerequisite.
The size of the seed varies depending on the hash function.
| Hash Algo | HMAC Length (in bytes) |
|---|---|
SHA-256 | 32 |
SHA-384 | 48 |
SHA-512 | 64 |
Table: HMAC length by hash algorithm
If you're interested in the subject, I recommend taking a look at the tpm-kdf repo which provides a KDF implementation based on Counter Mode. In parallel, I also recommend reading NIST SP 800-108 for all the security information.
Acknowledgement
You probably noticed that this pill mentions several repos belonging to salrashid123 who is a person whose contributions on TPM usage are invaluable.
Shout-out to him!
Bonus
The file 06-pill/concepts_test.go includes unit tests demonstrating some concepts that were discussed in this pill (e.g. maximum seal size, HMAC sizes, etc.)
Conclusion
In this pill, we introduced different use cases for symmetric keys. The key takeaways are as follows:
- support for symmetric encryption is very limited
- seal/unseal turns out to be the alternative
- HMAC is the first-class citizen algorithm for signing
Nevertheless, some might say "that's nice and all, but where is the authorization layer?". A little patience, this topic will be addressed as soon as the main concepts are mastered, but know that it is possible to set up more or less complex locks. For example, it is possible to:
- ask the user to provide a passphrase to authorize unsealing
- limit the unseal operation only during boot (not after in user-space)
- authorize unsealing if and only if the machine is in a trusted state at boot time (cf. measured boot)
Next Pill...
...we will focus on the two elements that allow referencing a key (the handle and the name) in a TPM.
🚧 TPM Pills is in beta 🚧
- if you encounter problems 🙏 please report them on the tpm-pills issue tracker
- if you think that
TPM Pillsshould cover a specific topic which isn't in the roadmap, let's initiate a discussion