Skip to content

Part 6 – Using the Private Key from the TPM: Two C Examples

In the previous part, we modified the Python client to use a private key stored in the TPM. We referenced that key using a TSS2 (TPM Software Stack 2.0) private key file. The same approach works in C, and we will first show the C equivalent.

We will then show an alternative approach that is not (yet) directly available in Python: referencing a persistent TPM object via a handle.

Both approaches were introduced in Programming Languages and TPM2.0 - different techniques, different approaches, so feel free to revisit that section for background and trade-offs.

Using a TSS2 Private Key File in a C Application

This example mirrors the Python implementation from Part 5. We use OpenSSL's provider mechanism to reference a TPM-backed private key through a TSS2 key file.

We assume you have already completed the preparation steps described in Preparation - Createing the TSS2 private key file.

At this point you should have:

  • client.tss2key: TSS2 private key reference file (bound to this TPM)
  • client-tpm.crt: client certificate signed by your CA
  • server_ca.pem: CA certificate used to validate the server

Because the C implementation is significantly longer than the Python one, we only show the relevant excerpts here. The full source is available at: client.c.

    /* OpenSSL 3.x init */
    if (OPENSSL_init_ssl(0, NULL) != 1) {
        openssl_die("OPENSSL_init_ssl failed");
    }

    ctx = SSL_CTX_new(TLS_client_method());
    if (!ctx) {
        openssl_die("SSL_CTX_new failed");
    }

    /* Client and server authentication */
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);

    if (SSL_CTX_load_verify_locations(ctx, SERVER_CA, NULL) != 1) {
        openssl_die("Failed to load server CA file (%s)", SERVER_CA);
    }

    /* Present client certificate */
    if (SSL_CTX_use_certificate_file(ctx, CLIENT_CERT, SSL_FILETYPE_PEM) != 1) {
        openssl_die("Failed to load client certificate (%s)", CLIENT_CERT);
    }

    /* Load TPM-backed client key */
    if (SSL_CTX_use_PrivateKey_file(ctx, CLIENT_KEY, SSL_FILETYPE_PEM) != 1) {
        openssl_die("Failed to load client private key (%s) via providers", CLIENT_KEY);
    }

    if (SSL_CTX_check_private_key(ctx) != 1) {
        openssl_die("Client certificate and private key do not match");
    }

    /* TCP connect */
    fd = tcp_connect(server_host, SERVER_PORT);

    /* Wrap in TLS and set SNI */
    ssl = SSL_new(ctx);
    if (!ssl) {
        openssl_die("SSL_new failed");
    }

    if (SSL_set_fd(ssl, fd) != 1) {
        openssl_die("SSL_set_fd failed");
    }

    if (SSL_set_tlsext_host_name(ssl, server_host) != 1) {
        openssl_die("Failed to set SNI");
    }

    if (SSL_connect(ssl) != 1) {
        openssl_die("TLS handshake (SSL_connect) failed");
    }

    /* Ensure verification succeeded */
    vr = SSL_get_verify_result(ssl);
    if (vr != X509_V_OK) {
        fprintf(stderr, "Server certificate verification failed: %s\n",
                X509_verify_cert_error_string(vr));
        SSL_free(ssl);
        close(fd);
        SSL_CTX_free(ctx);
        return 2;
    }

    /* send one command, read one response */
    handle_command(ssl, "GET_SECRET\n");

    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(fd);
    SSL_CTX_free(ctx);

This establishes a TLS connection with:

  • server authentication (verify server certificate against server_ca.pem)
  • client authentication (present a client certificate)
  • a TPM-backed private key referenced via a TSS2 key file

In this version, CLIENT_KEY is client.tss2key, which causes OpenSSL to perform private-key operations via the TPM provider.

Building the example

You have three common options to build the program:

  • Build on the target: useful for quick prototyping, but undesirable for production images.
  • Integrate into Yocto: create a recipe and include it in your image (recommended for product deployment)
  • Cross-compile using an SDK: compile outside of Yocto using the SDK environment

We will use the third approach for this example.

Yocto SDK creation is described in the Variscite Developer Center article: Yocto toolchain installation for out of Yocto builds.

We assume that you have built (or downloaded) an SDK.

The first step is to activate the SDK build environment:

$ . /opt/fsl-imx-xwayland/6.6-scarthgap/environment-setup-armv8a-poky-linux

Then cross-compile:

$ $CC -Wall -Wextra -O2 client.c -o client -lssl -lcrypto

Running the Application

Just like in Part 5, OpenSSL must load the TPM provider. The simplest way is via an OpenSSL configuration file:

root@imx8mp-var-dart:~# cat openssl-tpm2.cnf
# openssl-tpm2.cnf
openssl_conf = openssl_init

[openssl_init]
providers = provider_sect

[provider_sect]
default = default_sect
tpm2 = tpm2_sect

[default_sect]
activate = 1

[tpm2_sect]
activate = 1
# module path varies by distro/build:
module = /usr/lib/ossl-modules/tpm2.so

Then run the client with OPENSSL_CONF set:

root@imx8mp-var-dart:~# OPENSSL_CONF=openssl-tpm2.cnf ./client 192.168.178.2
Sending: GET_SECRET
Received: THIS_IS_THE_SECRET

The server output (Part 3 server):

$ ./server.py
TLS server listening on 0.0.0.0:5000
[+] TLS connection from ('192.168.178.34', 44778)
 Cipher: ('TLS_AES_256_GCM_SHA384', 'TLSv1.3', 256)
 Client cert subject: ((('commonName', 'trusted-client'),),)

Loading OpenSSL Providers in Code (Optional)

In this example we have used an OpenSSL config file to load the OpenSSL provider which "points" to the TPM device. This mimics the Python code where using a configuration file is not an option but mandatory.

Hard-coding the OpenSSL provider means you no longer have to provide a suitable configuration file. The downside is that you lose the flexibility that we can build an application and either run it with a local private key file or a private key from a TPM.

The following changes will need to be made to reference the TPM provider in the source code:

    /* OpenSSL 3.x init */
    if (OPENSSL_init_ssl(0, NULL) != 1) {
        openssl_die("OPENSSL_init_ssl failed");
    }
+   OSSL_PROVIDER *prov_default;
+   OSSL_PROVIDER *prov_tpm2;

+   prov_default = OSSL_PROVIDER_load(NULL, "default");
+   prov_tpm2    = OSSL_PROVIDER_load(NULL, "tpm2");
+   if (!prov_default || !prov_tpm2) {
+       openssl_die("Failed to load OpenSSL providers (default/tpm2)");
+   }
    ctx = SSL_CTX_new(TLS_client_method());
    if (!ctx) {
        openssl_die("SSL_CTX_new failed");
    }

You should also unload the provider at the end of the application:

    close(fd);
    SSL_CTX_free(ctx);

    /* unload providers */
+   OSSL_PROVIDER_unload(prov_tpm2);
+   OSSL_PROVIDER_unload(prov_default);

    return 0;
}

Using hard-coded OpenSSL providers you can run the application without the need for an environment variable and configuration file:

root@imx8mp-var-dart:~# ./client 192.168.178.2
Sending: GET_SECRET
Received: THIS_IS_THE_SECRET

Using a TPM Persistent Handle in a C Application

In C, OpenSSL also allows you to reference a TPM-backed private key directly via a persistent handle.

A persistent key can be:

  • imported into the TPM (less ideal - the key exists outside the TPM at some point), or
  • generated inside the TPM (preferred — the private key never leaves the TPM)

The initial setup is the same for both: we create a primary object (the Storage Root Key / SRK) and use it as the parent for child keys.

root@imx8mp-var-dart:~# tpm2_createprimary -C o -g sha256 -G ecc_nist_p256 -c primary.ctx

This prints the public portion and properties of the primary key. Importantly, the SRK is derived from device-internal state, so repeated calls recreate the same SRK.

If you want to generate the private key inside the TPM (recommended), you can skip the import section and continue with Letting the TPM Generate the Private Key

Importing an Existing Private Key into the TPM

For these experiments, we reuse client.key from earlier parts. TPM tooling expects a compatible format:

$ openssl pkey -in client.key -out client_priv.pem

In most cases client_priv.pem and client.key will be completely identical.

We should check the result before continuing

$ openssl pkey -in client_priv.pem -check -noout
Key is valid

The next step is to import the existing key under a parent - given by the context we created earlier.

root@imx8mp-var-dart:~# tpm2_import -C primary.ctx -G ecc_nist_p256 -i client_priv.pem -u key.pub -r key.priv

This returns to files

  • key.pub: A TPM2B_PUBLIC blob, containing the public portion of the imported object
  • key.priv: A TPM2B_PRIVATE blob, containing the encrypted private key. It is not usable without this TPM device

We have these two files which can be used with the TPM, but they are not yet stored in the TPM. For this we will need the following call:

root@imx8mp-var-dart:~# tpm2_load -C primary.ctx -u key.pub -r key.priv -c key.ctx

This will load the blobs into the TPM into transient memory, making the keys accessible via the file key.ctx.

Let's do a simple signing test now. We don't yet use our client / server application but use applications from TPM2-tools instead:

root@imx8mp-var-dart:~# echo hello | openssl dgst -sha256 -binary > digest.bin
root@imx8mp-var-dart:~# tpm2_sign -c key.ctx -g sha256 -o sig.bin digest.bin
root@imx8mp-var-dart:~# ls -l sig.bin
-rw-rw---- 1 root root 72 Jan 28 19:06 sig.bin

ℹ️ In other tutorials you will rather see a call like

echo "hello" | sha256sum | cut -d' ' -f1 | xxd -r -p > digest.bin

We have used the call with openssl dgst for simplicity, but also because xxd might not be installed on your embedded system.

In the next section we will show an alternative to importing a private key: Letting the TPM generate the private key which has the advantage that it is never exposed during manufacturing or deployment. It is an alternative to importing an existing private key, so if you don't care about this you can skip this section and continue with Making the private key persistent.

Letting the TPM Generate the Private Key

If you want to ensure the private key never exists outside the TPM, generate it inside the TPM. We will assume that you already have a file called primary.ctx which is used for both importing a key and letting the TPM generate it.

The first step is to create an EC NIST P-256 child key under that primary context.

root@imx8mp-var-dart:~# tpm2_create \
  -C primary.ctx \
  -g sha256 \
  -G ecc_nist_p256 \
  -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign" \
  -u key.pub \
  -r key.priv

Let's take a quick look at individual commands / options: - primary.ctx: Object is bound to that parent - sha256: The internal naming of this object uses SHA256. This has no relevance beyond the internal naming - ecc_nist_p256: Create an EC NIST P-256 key - fixedtpm: Key is bound to the TPM and cannot be exported / migrated to another TPM - fixedparent: Key is bound to and encrypted with using the parent - sensitivedataorigin: Generate the key internally, consider it sensitive - userwithauth: this object may be authorized with an authValue. We don't use an authValue for now, but it has to be specified for some versions of tpm-tools - sign: enables signatures, CSR generation and TLS client auth

This command will create two files: - key.pub: A public key in the TPMT_PUBLIC format - key.priv: A TPM2B_PRIVATE structure

key.priv is basically "a sealed, encrypted blob that only this TPM and its parent can ever unwrap."

The next step is to load the key.

root@imx8mp-var-dart:~# tpm2_load -C primary.ctx -u key.pub -r key.priv -c key.ctx

This key is still transient after that operation but we can already use it for some test operations:

root@imx8mp-var-dart:~# echo hello | openssl dgst -sha256 -binary > digest.bin
root@imx8mp-var-dart:~# tpm2_sign -c key.ctx -g sha256 -o sig.bin digest.bin
root@imx8mp-var-dart:~# ls -l sig.bin
-rw-rw---- 1 root root 262 Jan 28 19:06 sig.bin

Making the Key Persistent

Previously we had only used the key by its context file. This is fine as long as we work with tpm2 command line tools. But if we want to use the key in our client / server application we need to make the key persistent.

Persistence means:

  • the key is stored in TPM non-volatile memory
  • it survives reboot/power loss
  • it can be referenced by a handle
  • it can still be deleted later

Make the key persistent:

root@imx8mp-var-dart:~# tpm2_evictcontrol -C o -c key.ctx 0x81000001
persistent-handle: 0x81000001
action: persisted

0x81000001 is for the first persistent object, so if you want to store multiple objects, you can use 0x81000002 for the next one.

Looking at the source code

Now that we have a persistent handle for our private key, we want to see how our C source code looks like. We will take a quick comparison to see what has changed compared to the previous approach.

#include <openssl/store.h>

#define CLIENT_CERT "client.crt"   // CA cert file from part3.md
#define CLIENT_KEY_URI "handle:0x81000001"

Instead of specifying a key file on the filesystem, we reference a TPM handle. We will also use the client.crt file that we already signed before, not the client-tpm.crt from the TSS2key example.

We have a completely different function for loading the provider-backed key:

/*
 * Obtain an EVP_PKEY that references a provider-backed key.
 *
 * For TPM usage with a persistent handle, the URI "handle:0x81000001" is resolved
 * by the TPM provider. The returned EVP_PKEY is an opaque object: it does not
 * contain extractable private key bytes. When TLS needs to sign, OpenSSL invokes
 * the provider implementation, which delegates the signing operation to the TPM.
 */
static EVP_PKEY *load_pkey_from_uri(const char *uri) {
    OSSL_STORE_CTX *store = NULL;
    OSSL_STORE_INFO *info = NULL;
    EVP_PKEY *pkey = NULL;
    int type;

    store = OSSL_STORE_open(uri, NULL, NULL, NULL, NULL);
    if (!store) {
        openssl_die("OSSL_STORE_open failed for '%s'", uri);
    }

    while (!OSSL_STORE_eof(store)) {
        info = OSSL_STORE_load(store);
        if (!info) {
            /* Could be EOF or an error; distinguish via OSSL_STORE_eof() above */
            break;
        }

        type = OSSL_STORE_INFO_get_type(info);
        if (type == OSSL_STORE_INFO_PKEY) {
            pkey = OSSL_STORE_INFO_get1_PKEY(info);
            OSSL_STORE_INFO_free(info);
            info = NULL;
            break;
        }

        OSSL_STORE_INFO_free(info);
        info = NULL;
    }

    if (!pkey && !OSSL_STORE_eof(store)) {
        /* Some error occurred while loading */
        OSSL_STORE_close(store);
        openssl_die("OSSL_STORE_load failed while reading '%s'", uri);
    }

    OSSL_STORE_close(store);

    if (!pkey) {
        openssl_die("No private key found in STORE URI '%s'", uri);
    }

    return pkey;
}

[..]

    /* Load TPM-backed client key from persistent handle */
    pkey = load_pkey_from_uri(CLIENT_KEY_URI);

    if (SSL_CTX_use_PrivateKey(ctx, pkey) != 1) {
        EVP_PKEY_free(pkey);
        openssl_die("SSL_CTX_use_PrivateKey failed for key URI '%s'", CLIENT_KEY_URI);
     }
    EVP_PKEY_free(pkey);
    pkey = NULL;

Building and Running

We name this version client-handle.c:

$ $CC -Wall -Wextra -O2 client-handle.c -o client-handle -lssl -lcrypto
Run it (either via OPENSSL_CONF or by hard-coding provider loading):

$ OPENSSL_CONF="openssl-tpm2.cnf" ./client-handle 192.168.178.2
Sending: GET_SECRET
Received: THIS_IS_THE_SECRET

Wrap-up

This concludes the C examples. You have now seen two common approaches:

  • TSS2 private key files (portable workflow across Python and C)
  • persistent TPM handles (more common in lower-level integrations)

In the next part we'll summarize where we are from a security standpoint. This conclusion applies equally to both the Python and C implementations.