Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

How do these systems 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!

Info

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

Warning

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:

signdecryptrestricted
010

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 🚀
Info

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:

  1. the seal: TPM2_CreatePrimary or TPM2_Create
  2. the unseal: TPM2_Unseal
Info

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:

signdecryptrestricted
000

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.

Info

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.

Warning

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.

What is a PRF?

It's a deterministic function that:

  1. Takes two inputs:
    • A secret key K (master key)
    • Some arbitrary data M (message/data)
  2. Produces an output:
    • A pseudorandom output of fixed length
    • Indistinguishable from a truly random function for an attacker who doesn't know K
  3. 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:

signdecryptrestricted
100

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 AlgoHMAC Length (in bytes)
SHA-25632
SHA-38448
SHA-51264

Table: HMAC length by hash algorithm

Go further

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 Pills should cover a specific topic which isn't in the roadmap, let's initiate a discussion

  1. TPM2_EncryptDecrypt2 adds the ability to encrypt in transit the content of the data to be encrypted or decrypted.

  2. concept defined in pill #4

  3. see the Wikipedia page

  4. I'm referring in particular to GCP Workload Identity, AWS STS or AD federated identity credentials