DEV Community

Cover image for How I Used TPM for Key Encryption in Rust on Linux (Hardware TPM & vTPM)
tsuruko
tsuruko

Posted on

How I Used TPM for Key Encryption in Rust on Linux (Hardware TPM & vTPM)

Following my Windows implementation, this time I implemented key wrapping using a TPM on Linux.
(the Windows version: How I Used TPM for Key Encryption in Rust (Using Windows APIs))

I used tss-esapi on Ubuntu (WSL) with a virtual TPM (vTPM).
For the vTPM, I used swtpm (v0.7.3).
By changing the connection target, the same code also works with a hardware TPM.

I’ll walk you through the key-wrapping implementation and explain how it works!

Table of Contents


Cargo.toml

[dependencies]
rand = "0.9"
tss-esapi = "7.6"
zeroize = "1.8"
Enter fullscreen mode Exit fullscreen mode

Requirements to Build tss-esapi

  • gcc (C compiler / build tool)
  • pkg-config (dependency discovery tool for builds)
  • libtss2-dev (TPM libraries)

The tss-esapi-sys README says it uses pkg-config at build time to locate tss2-esys, tss2-tctildr, and tss2-mu.
So make sure pkg-config, libtss2-dev (which includes those three), and gcc installed.

Install on Ubuntu

sudo apt install gcc pkg-config libtss2-dev
Enter fullscreen mode Exit fullscreen mode

Implementation

use std::convert::TryFrom;

use rand;
use tss_esapi::{
    Context,
    Result,
    attributes::{
        object::{ObjectAttributes, ObjectAttributesBuilder},
        session::SessionAttributesBuilder,
    },
    constants::{
        capabilities::CapabilityType, 
        session_type::SessionType,
        startup_type::StartupType,
        response_code::Tss2ResponseCodeKind,
    },
    handles::{ObjectHandle, PersistentTpmHandle, TpmHandle},
    interface_types::{
        algorithm::{HashingAlgorithm, PublicAlgorithm},
        key_bits::RsaKeyBits,
        resource_handles::{Hierarchy, Provision},
        session_handles::AuthSession,
    },
    structures::{
        Auth, CapabilityData, Data, HashScheme,
        Public, PublicBuilder, PublicKeyRsa,
        PublicRsaParameters, PublicRsaParametersBuilder, 
        RsaDecryptionScheme, RsaExponent, RsaScheme,
        SymmetricDefinition, SymmetricDefinitionObject,
    },
    tcti_ldr::{DeviceConfig, NetworkTPMConfig, TctiNameConf},
};
use zeroize::Zeroize;


// Note: This is a simplified version

const RSA_KEY_AUTH: &[u8] = b"AuthValue";

struct Tpm {
    ctx: Context,
    wrap_session: Option<AuthSession>,
    unwrap_session: Option<AuthSession>,
}

// With a hardware TPM, the platform often shuts the TPM down automatically.
// If that's the case in your environment, remove this block.
impl Drop for Tpm {
    fn drop(&mut self) {
        self.ctx.execute_without_session(|ctx| {
            if let Err(e) = ctx.shutdown(StartupType::Clear) {
                eprintln!("Failed to shut down the TPM: {e}");
            } 
        });
    }
}

// Encryption and hashing algorithms are chosen for broad compatibility
// and are widely used and recommended

impl Tpm {
    fn new(use_vtpm: bool) -> Result<Self> {
        let tcti = if use_vtpm {
            // By default, this connects to "localhost:2321"
            TctiNameConf::Swtpm(NetworkTPMConfig::default())
        } else {
            // By default, this connects to "/dev/tpm0"
            TctiNameConf::Device(DeviceConfig::default())
        };

        let mut ctx = Context::new(tcti)?;

        // With a hardware TPM, the platform often starts the TPM automatically.
        // If that's the case in your environment, remove this line.
        ctx.startup(StartupType::Clear)?;

        let mut s = Self {
            ctx,
            wrap_session: None,
            unwrap_session: None,
        };
        s.setup_sessions()?;

        Ok(s)
    }

    fn setup_sessions(&mut self) -> Result<()> {
        let mut decrypt = true;
        let mut encrypt = false;

        for i in 0..2 {
            // Create an HMAC session
            let hmac_session = self.ctx.start_auth_session(
                None,
                None, 
                None,
                SessionType::Hmac,
                SymmetricDefinition::AES_128_CFB,
                HashingAlgorithm::Sha256,
            )?;

            // Add attributes to the session
            let (attrs, mask) = SessionAttributesBuilder::new()
                .with_decrypt(decrypt)
                .with_encrypt(encrypt)
                .build();

            self.ctx.tr_sess_set_attributes(hmac_session.unwrap(), attrs, mask)?;

            if i == 0 {
                self.wrap_session = hmac_session;
                decrypt = false;
                encrypt = true;  
            } else {         
                self.unwrap_session = hmac_session;
            }
        }

        Ok(())
    }

    fn wrap_key(&mut self, mut target_key: [u8; 32]) -> Result<Vec<u8>> {
        let hrsa = self.ensure_rsa_key()?;

        // Wrap the target key
        let wrapped_key = self.ctx.execute_with_session(
            self.wrap_session,
            |ctx| {
                ctx.rsa_encrypt(
                    hrsa.into(),
                    PublicKeyRsa::try_from(target_key.as_slice())?,
                    RsaDecryptionScheme::Null,
                    Data::default(),
                )
            },
        )?;

        // Zeroize the plaintext key
        target_key.zeroize();

        Ok(wrapped_key.to_vec())
    }

    fn unwrap_key(&mut self, target_key: Vec<u8>) -> Result<Vec<u8>> {
        let hrsa = self.ensure_rsa_key()?;

        // Set the auth value for the key object
        self.ctx.tr_set_auth(hrsa, Auth::try_from(RSA_KEY_AUTH)?)?; 

        // Unwrap the target key
        let unwrapped_key = self.ctx.execute_with_session(
            self.unwrap_session,
            |ctx| {
                ctx.rsa_decrypt(
                    hrsa.into(),
                    PublicKeyRsa::try_from(target_key)?,
                    RsaDecryptionScheme::Null,
                    Data::default(),
                )            
            },
        )?;

        Ok(unwrapped_key.to_vec())
    }

    fn ensure_rsa_key(&mut self) -> Result<ObjectHandle> {
        self.ctx.clear_sessions();

        // Create a new SRK and RSA key if they don't exist

        // Persistent handle values are in the range 0x81000000–0x81FFFFFF
        let mut value = 0x81000000;
        let mut srk = None;

        loop {
            let (data, more) = self.ctx.get_capability(
                CapabilityType::Handles, 
                value, 
                8,
            )?;

            if let CapabilityData::Handles(h) = data {
                let handles = h.into_inner();
                if handles.is_empty() {
                    // Create a new SRK and RSA key if there are no registered handles
                    let srk = self.create_srk()?;
                    return Ok(self.create_rsa(srk)?)
                }

                let last_value = handles.last().unwrap();
                value = u32::from(*last_value) + 1;

                for handle in handles {
                    if let TpmHandle::Persistent(_) = handle {
                        let target_handle = self.ctx.tr_from_tpm_public(handle)?;
                        // Read the target handle's public area
                        let (public, _, _) = self.ctx.read_public(target_handle.into())?;

                        if let Public::Rsa {
                            object_attributes, ..
                        } = public
                        {
                            let attrs = object_attributes;

                            // Find a handle with attributes matching the RSA wrapping key
                            if !attrs.restricted()
                                && attrs.fixed_tpm()
                                && attrs.fixed_parent()
                                && attrs.sensitive_data_origin()
                                && attrs.user_with_auth()
                                && attrs.decrypt()
                            {
                                return Ok(target_handle)
                            } else if attrs.restricted()
                                && attrs.decrypt()
                                && attrs.fixed_tpm()
                                && attrs.fixed_parent()
                                && attrs.sensitive_data_origin()
                                && attrs.no_da()
                            {
                                srk = Some(target_handle);
                            }
                        }
                    }
                }
            }

            if !more {
                // If there are no more handles
                if let None = srk {
                    let srk = self.create_srk()?;
                    return Ok(self.create_rsa(srk)?)
                } else {
                    return Ok(self.create_rsa(srk.unwrap())?)
                }
            }
        }
    }

    fn make_persistent(&mut self, handle: impl Into<ObjectHandle>) -> Result<ObjectHandle> {
        let mut value = 0x81000001;
        let handle = handle.into();

        loop {
            // Make the target object persistent
            match self.ctx.evict_control(
                Provision::Owner,
                handle,
                PersistentTpmHandle::new(value)?.into(),
            ) {
                Ok(h) => {
                    println!("Registered the object at handle {value:#x}.");

                    self.ctx.execute_without_session(|ctx| {
                        // Remove the transient object
                        ctx.flush_context(handle)
                    })?;  
                    return Ok(h)
                },
                Err(e) => {
                    match e {
                        tss_esapi::Error::Tss2Error(code) => {
                            // If the target handle value is already in use
                            if let Some(Tss2ResponseCodeKind::NvDefined) = code.kind() {
                                value += 1;
                                continue
                            } else {
                                return Err(e)
                            }
                        }
                        _ => return Err(e),
                    }
                },
            }
        }
    }

    fn create_rsa(&mut self, hsrk: ObjectHandle) -> Result<ObjectHandle> {
        let attrs = ObjectAttributesBuilder::new()
            .with_fixed_tpm(true)
            .with_fixed_parent(true)
            .with_user_with_auth(true)
            .with_sensitive_data_origin(true)
            .with_decrypt(true)
            .build()?;

        let hash_algo = HashScheme::new(HashingAlgorithm::Sha256);
        let params = PublicRsaParametersBuilder::new()
            .with_scheme(RsaScheme::Oaep(hash_algo))
            .with_key_bits(RsaKeyBits::Rsa2048)
            .with_exponent(RsaExponent::ZERO_EXPONENT)
            .build()?;

        // Set a password session
        self.ctx.set_sessions((Some(AuthSession::Password), None, None));

        // Create a new RSA key
        let key_result = self.ctx.create(
            hsrk.into(),
            build_public(attrs, params)?,
            Some(Auth::try_from(RSA_KEY_AUTH)?),
            None,
            None,
            None,
        )?;
        // Load the created RSA key into the TPM
        let hrsa = self.ctx.load(
            hsrk.into(), 
            key_result.out_private, 
            key_result.out_public,
        )?;  

        Ok(self.make_persistent(hrsa)?)
    }

    fn create_srk(&mut self) -> Result<ObjectHandle> {
        let attrs = ObjectAttributesBuilder::new()
            .with_fixed_tpm(true)
            .with_fixed_parent(true)
            .with_no_da(true)
            .with_restricted(true)
            .with_user_with_auth(true)
            .with_sensitive_data_origin(true)
            .with_decrypt(true)
            .build()?;

        let params = PublicRsaParametersBuilder::new_restricted_decryption_key(
            SymmetricDefinitionObject::AES_128_CFB,
            RsaKeyBits::Rsa2048,
            RsaExponent::ZERO_EXPONENT,
        )
        .build()?;

        // Set a password session
        self.ctx.set_sessions((Some(AuthSession::Password), None, None));

        // Create a new SRK
        let key_result = self.ctx.create_primary(
            Hierarchy::Owner, 
            build_public(attrs, params)?, 
            None, 
            None, 
            None, 
            None,
        )?;

        Ok(self.make_persistent(key_result.key_handle)?)
    }
}

fn build_public(attrs: ObjectAttributes, params: PublicRsaParameters) -> Result<Public> {
    let public = PublicBuilder::new()
        .with_public_algorithm(PublicAlgorithm::Rsa)
        .with_object_attributes(attrs)
        .with_name_hashing_algorithm(HashingAlgorithm::Sha256)
        .with_rsa_parameters(params)
        .with_rsa_unique_identifier(PublicKeyRsa::default())
        .build()?;

    Ok(public)
}
Enter fullscreen mode Exit fullscreen mode

This implementation scans the TPM's persistent handles each time to look for the wrapping key created earlier. It verifies the key by checking its attributes. If it can’t find a match, it creates a new RSA key.

For authorization, it sets up password sessions or HMAC sessions depending on the function. Some functions don’t allow password sessions, or restrict which session attributes can be used, so the appropriate session type should be used for each case.

tr_from_tpm_public(), which constructs an ESYS object, is used to operate on an existing TPM object.

Since this is a simplified version, the password used as the RSA key’s auth value is defined as a const.
In real-world use, you should manage it securely!

To confirm it works, try running the main function below:

fn main() {
    // Connect to vTPM
    let mut tpm = match Tpm::new(true) {
        Ok(t) => t,
        Err(e) => {
            eprintln!("Failed to connect to the TPM: {e}");
            return
        },  
    };

    // Create a key to be wrapped (AES-256)
    let key: [u8; 32] = rand::random();
    println!("Plaintext Key: {:?}", key);

    let wrapped_key = match tpm.wrap_key(key) {
        Ok(k) => {
            println!("Wrapped Key: {:?}", k);
            k
        },
        Err(e) => {
            eprintln!("Failed to wrap the key: {e}");
            return
        },  
    };

    match tpm.unwrap_key(wrapped_key) {
        Ok(k) => println!("Unwrapped Key: {:?}", k),
        Err(e) => eprintln!("Failed to unwrap the key: {e}"),  
    }
}
Enter fullscreen mode Exit fullscreen mode

If you want to delete the key you created:

fn delete_key(&mut self, value: u32) -> Result<()> {
    // `value` is the handle value to remove
    let handle = self.ctx.tr_from_tpm_public(TpmHandle::try_from(value)?)?;
    self.ctx.set_sessions((Some(AuthSession::Password), None, None));

    // Remove the persistent handle
    let _ = self.ctx.evict_control(
        Provision::Owner,
        handle,
        PersistentTpmHandle::new(value)?.into(),
    )?;

    println!("Removed the object at handle {value:#x}.");
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

evict_control() behaves differently depending on whether the target is a transient object or a persistent one.

If the target is a transient object, it’s registered under the specified persistent handle.
If the target is already a persistent object, it's removed instead.

If you run it in the terminal:

# Specify the connection target using an environment variable
export TPM2TOOLS_TCTI="swtpm:host=localhost,port=2321"  # vTPM(swtpm)
export TPM2TOOLS_TCTI="device:/dev/tpm0"                # hardware TPM

# Remove the persistent object at handle 0x81000002
# If the hierarchy has an auth value, specify it with "-P" ("--auth")
tpm2_evictcontrol -c 0x81000002
Enter fullscreen mode Exit fullscreen mode

You can also easily list the current TPM handles:

# List persistent handles
tpm2_getcap handles-persistent
# List the known supported capability names
tpm2_getcap -l
Enter fullscreen mode Exit fullscreen mode

Explanation

I referred to the following resources to learn about TPM 2.0, including its architecture and function parameters:

I also used this resource as a reference for tpm2-tools command usage:

In this section, I’ll explain the key-wrapping workflow and the relevant functions and parameters based on the references above.


1. Connect to the TPM

If you use a hardware TPM, make sure it’s enabled.

To connect to swtpm, you need to start the vTPM server in the terminal.
In this implementation, the connection uses a TCP socket.

# Sample command
swtpm socket \
--tpm2 \
--server type=tcp,port=2321 \
--ctrl type=tcp,port=2322 \
--tpmstate dir=/path/to/dir \
--flags startup-clear
Enter fullscreen mode Exit fullscreen mode

--tpm2

--tpm2

This option selects TPM 2.0.
If not specified, TPM 1.2 is used.

--server

--server [type=tcp][,port=<port>[,bindaddr=<address> [,ifname=<ifname>]]][,fd=<fd>][,disconnect]

This option accepts TCP connections on the given port.

If you specify a port (port), you can also specify a bind address (bindaddr).
The default bind address is 127.0.0.1.
if not specified, you must specify a file descriptor (fd).

If you use a link-local IPv6 address, you must specify the interface name (ifname) to bind to.

Unless disconnect is specified, the connection remains persistent.

--ctrl

--ctrl type=[unixio|tcp][,path=<path>] [,port=<port>[,bindaddr=<address>[,ifname=<ifname>]]][,fd=<filedescriptor>|clientfd=<filedescriptor>] [,mode=<0...>][,uid=<uid>][,gid=<gid>]

This option adds a control channel to the TPM.

You can choose one of the following connection methods:

  • Unix domain socket
    • Connect using either a path (path) or a file descriptor (fd).
  • TCP socket
    • Connect using either a port (port) or a file descriptor (clientfd).

If you specify a port (port), you can also specify a bind address (bindaddr).
The default bind address is 127.0.0.1.

--tpmstate

--tpmstate dir=<dir>[,mode=<0...>]|backend-uri=<uri>

This option stores the TPM’s state in the specified directory.

When the state is saved you’ll see files like:

  • tpm2-00.permall (non-volatile data)
  • tpm2-00.volatilestate (volatile data)
  • tpm-00.savestate (TPM 1.2 only)

The directory can also be specified using the TPM_PATH.
If both are set, this option is used instead of TPM_PATH.

export TPM_PATH=/path/to/dir
Enter fullscreen mode Exit fullscreen mode

--flags

--flags [not-need-init] [,startup-clear|startup-state|startup-deactivated|startup-none]

This option controls the behavior of the TPM_Startup / TPM2_Startup command.

It automatically sends the Startup command unless you use startup-none, or use not-need-init on its own.

  • startup-clear: Normal initialization (resets PCRs and volatile state)
  • startup-state: Restore volatile state (except PCRs)
  • startup-deactivated: TPM 1.2 only (start in deactivated mode)
  • startup-none: Do not send any Startup command

These options except startup-none imply not-need-init.
The not-need-init flag enables the TPM to accept commands without an INIT being sent.

startup-none seems intended for cases where you send both the INIT and Startup commands yourself.

If none of these options are specified, startup-clear is used by default.

Note:
I tested whether startup-state actually saves the volatile state, but it didn't. I’m not sure if this is unsupported in TPM 2.0.
However, when I ran swtpm_ioctl with the -v option to save the state to a file manually, it worked.

To use this tool, install swtpm-tools.

# Specify the port for the control channel with `--tcp`
# The defaults are server "127.0.0.1" and port "6545"
swtpm_ioctl --tcp :2322 -v

Other Options

If you want to daemonize the process, specify -d (--daemon).

If you want to enable logging, specify --log.
--log [fd=<fd>|file=<path>][,level=<n>][,prefix=<prefix>][,truncate]

You can specify the log file path (file), and the log level (level).
If the log level is 5 or higher, debug tracing for libtpms is enabled.

For other options, refer to the swtpm manual:

Note:
As a TPM 2.0-specific caution, the TPM_PT_LOCKOUT_COUNTER (dictionary-attack lockout counter) may cause the TPM to become locked out once it reaches its limit.

For example, this counter can increase when you enter an incorrect auth value, or if you disconnect without calling TPM2_Shutdown() after accessing a command that requires authorization.
The TPM checks during TPM2_Startup() whether the previous shutdown was orderly.

Once the TPM is locked out, commands that require authorization will fail with an error on the next attempt.
To prevent this, always call TPM2_Shutdown() at the end.

In swtpm v0.8 and later, TPM2_Shutdown() is sent automatically unless you set disable-auto-shutdown. In earlier versions, you need to do it manually (this behavior is documented in the CHANGES file).

If you want to disable this counter for a specific object, you can set with_no_da(true) when creating that object.

If you do get locked out, you can either wait for the recovery time (the delay until the next auth attempt is allowed) or clear the lockout state using tpm2-tools commands.

# Specify the connection target using an environment variable

# Example: connect to a vTPM (swtpm)
# If not specified, the default IP address is "127.0.0.1" and the port is "2321"
export TPM2TOOLS_TCTI="swtpm:host=localhost,port=2321"

# Example: connect to a physical TPM
# If not specified, the default device path is "/dev/tpm0"
export TPM2TOOLS_TCTI="device:/dev/tpm0"

# Clear the lockout state with "-c"
tpm2_dictionarylockout -c

# Or specify the connection target along with the command
tpm2_dictionarylockout -c -T swtpm:host=localhost,port=2321

🔗 Reference:


Next, you need to configure the library.

let tcti = if use_vtpm {
    // By default, this connects to "localhost:2321"
    TctiNameConf::Swtpm(NetworkTPMConfig::default())
} else {
    // By default, this connects to "/dev/tpm0"
    TctiNameConf::Device(DeviceConfig::default())
};
Enter fullscreen mode Exit fullscreen mode

In this implementation, I use these defaults.
If you want to connect to a custom port or path, use from_str():

use std::str::FromStr;

// Custom port/host
let tcti = TctiNameConf::from_str("swtpm:port=1234,host=127.0.0.1")?;
// Custom TPM device path
let tcti = TctiNameConf::from_str("device:/dev/tpmrm0")?;
Enter fullscreen mode Exit fullscreen mode

To load the connection settings from an environment variable:

// The lookup order is: TPM2TOOLS_TCTI → TCTI → TEST_TCTI
let tcti = TctiNameConf::from_environment_variable()?;
Enter fullscreen mode Exit fullscreen mode

Other connection options include mssim (Microsoft TPM simulator) and tabrmd (the tpm2-abrmd daemon), but I won’t cover them in this article.


2. Create a Context

Use Context::new() to create an ESYS (Enhanced System API) context from the TCTI you choose.
A Context mainly manages ESAPI state, such as sessions and TPM resources.

For tcti_name_conf, pass the TCTI configuration you defined earlier.

let tcti = if use_vtpm {
    // By default, this connects to "localhost:2321"
    TctiNameConf::Swtpm(NetworkTPMConfig::default())
} else {
    // By default, this connects to "/dev/tpm0"
    TctiNameConf::Device(DeviceConfig::default())
};

// Create a Context with the specified TCTI
let mut ctx = Context::new(tcti)?;
Enter fullscreen mode Exit fullscreen mode

If you want to use the resource manager (tpm2-abrmd), you can create the context with new_with_tabrmd().


3. Start the TPM

After creating a context, may you need to call startup().

With a hardware TPM, the platform often sends TPM2_Startup() during boot, so calling it yourself can run it twice and may cause an error.

(Reference: TCG PC Client Platform TPM Profile Specification for TPM 2.0 (p.31 Power Management))
With swtpm, calling it twice didn’t cause an error in my tests.

let mut ctx = Context::new(tcti)?;

ctx.startup(StartupType::Clear)?;
// Run this function after creating a context.
// But if the platform or swtpm already starts it automatically,
// you can skip it.
Enter fullscreen mode Exit fullscreen mode

For startup_type, pass the startup mode to Clear or State.
Whether the previous volatile state can be restored depends on the argument used in the previous shutdown().

Reset:
Previous: shutdown(StartupType::Clear) → Next: startup(StartupType::Clear)

Most values are initialized to their default values or to new random values.
Persistent data stored in NV (non-volatile) memory is not affected.
If you call startup(StartupType::State), there is no saved state to restore, so it fails.

Restart:
Previous: shutdown(StartupType::State) → Next: startup(StartupType::Clear)

Volatile state (except PCRs) is restored, but PCRs are cleared (initialized).
This allows the next boot measurements to be recorded correctly.

Resume:
Previous: shutdown(StartupType::State) → Next: startup(StartupType::State)

All volatile state saved by shutdown(StartupType::State) is restored, including PCRs.
However, PCRs that aren't saved are initialized.

If you want to do this in the terminal:

# Start with State
tpm2_startup
# Start with Clear
tpm2_startup -c
Enter fullscreen mode Exit fullscreen mode

tpm2_startup(1) - tpm2-tools

What are PCRs?

PCRs (Platform Configuration Registers) are dedicated registers used to validate the platform’s state.

During system boot (and other security-relevant events), measurements are recorded in a log. The measurement itself (or its hash) is sent to the TPM and extended into a PCR, so the PCR value is accumulated as a chain of hashes.
Using this value, you can verify whether the measurement log has been tampered with.

It’s possible to use a single PCR, but in practice, measured boot assigns measurements to specific PCRs, so they’re usually spread across multiple PCRs.

Note:
The number of PCRs and their attributes depend on the platform.

🔗 References:


4. Get the SRK Handle

To create a wrapping key (an RSA key), you need its parent key, the SRK.
On a vTPM, there is often no persisted SRK yet, so you may need to create one and make it persistent.

It’s common to persist the SRK at 0x81000001, so you’ll often find it there.

To read the TPM capabilities, use get_capability().
This function only allows the audit session attribute, so make sure other session attributes are disabled.

// The return value includes the retrieved data and a bool indicating
// whether more data is available
let (data, more) = self.ctx.get_capability(
    CapabilityType::Handles, 
    value, 
    8,
)?;
Enter fullscreen mode Exit fullscreen mode

For capability, pass the type of data you want to retrieve.

For property, it depends on the capability type.
For handles, pass the handle value to start from.

For property_count, pass how many entries to request at once.
Depending on the TPM implementation, it may return fewer entries than requested.
Also, even if you repeat the request with the same parameters, the number of entries returned may vary from call to call.

If you want to do this in the terminal:

# List transient handles
tpm2_getcap handles-transient
# List persistent handles
tpm2_getcap handles-persistent
Enter fullscreen mode Exit fullscreen mode

tpm2_getcap(1) - tpm2-tools

ESYS Object Construction

To operate on an object in the TPM, you need to construct an ESYS object using tr_from_tpm_public().
This function only allows the audit and encrypt session attributes, so make sure other session attributes are disabled.

For tpm_handle, pass the handle you want to read.

Reading the Public Area

read_public() reads the public area of the specified handle.
This function only allows the audit and encrypt session attributes, so make sure other session attributes are disabled.

For key_handle, pass the object handle whose public area you want to read.
Since From<KeyHandle> is implemented for ObjectHandle, you can convert it with into().

// Convert the TPM handle to an ESYS object (handle)
let target_handle = self.ctx.tr_from_tpm_public(handle)?;
// Read the object's public area
let (public, _, _) = self.ctx.read_public(target_handle.into())?;
Enter fullscreen mode Exit fullscreen mode

If you want to do this in the terminal:

# Read the public area from the context file path (or handle value) with "-c"
tpm2_readpublic -c rsa.ctx
Enter fullscreen mode Exit fullscreen mode

tpm2_readpublic(1) - tpm2-tools


5. Create and Persist an RSA Key

Once you have the SRK, you can create an RSA key for key wrapping.
In this section, I’ll also explain how to create an SRK.

In this implementation, it looks for a previously created RSA key on each run. In real-world use, it’s more efficient to store its persistent handle value.

Before calling the key-creation functions, make sure your authorization settings are in place, since these functions require authorization.

Authorization Settings

To set the auth value for an object handle, use tr_set_auth().
If the auth value is an empty byte string, you can skip this call.
The official documentation states that the default auth value for hierarchy handles (and similar handles) is an empty byte string.
(Reference: TCG TPM v2.0 Provisioning Guidance (p.19, Owner Authorizations), Trusted Platform Module 2.0 Library Part 1: Architecture (p.74, Owner Controls))

To configure authorization sessions, use set_sessions().
By default, a session is created with with_continue_session(true), so it remain active even after the function succeeds.
If you want to use HMAC or policy authorization, you need to create a session with start_auth_session().

For session_handles, set the sessions to use for authorization.

You can set up to three sessions.
In most cases, one session is enough, but some functions require multiple authorizations, or you want to combine an auth value with a policy session. In those cases, you need to specify multiple sessions at the same time.

Password is used for simple password authorization only.

HmacSession derives an HMAC key by combining the session key and the object’s auth value. If both are empty, the HMAC key remains an empty byte string.
Even if both are empty, authorization still happens, but using an HMAC session is basically pointless for authorization alone in this case.
Using Password is usually a better choice here because it has lower communication overhead.
(Reference: Trusted Platform Module Library Part 1: Architecture (p.125, No HMAC Authorization))

// Set a password session
self.ctx.set_sessions((Some(AuthSession::Password), None, None));
Enter fullscreen mode Exit fullscreen mode

Note:
The call order of tr_set_auth() and set_sessions() doesn’t matter.

Key Creation

Use create_primary() for the SRK and create() for the RSA key.

// Create a new SRK
let key_result = self.ctx.create_primary(
    Hierarchy::Owner,
    build_public(attrs, params)?,
    None,
    None,
    None,
    None,
)?;

// Create a new RSA key
let key_result = self.ctx.create(
    hsrk.into(),
    build_public(attrs, params)?,
    Some(Auth::try_from(RSA_KEY_AUTH)?),
    None,
    None,
    None,
)?;
Enter fullscreen mode Exit fullscreen mode

For primary_handle / parent_handle, pass the hierarchy handle / parent handle.
These functions require authorization for those handles.
For the SRK, pass the hierarchy you want to create it under (commonly the Owner hierarchy).
For the RSA key, pass the parent SRK handle.

For public, pass an object’s public area as a Public.
You can choose the object type from Rsa, KeyedHash, Ecc, or SymCipher.

For auth_value, pass the auth value for an object as a byte array.
If you pass None, it becomes an empty byte string.
In that code, Auth::try_from() accepts &[u8] and Vec<u8>.

For initial_data / sensitive_data, pass sensitive data as a byte array.
For a symmetric key, this value becomes the key material.
For an asymmetric key, you can’t provide the sensitive data from outside, so pass None.
Also, if you set with_sensitive_data_origin(true) in the object attributes, the sensitive data is generated inside the TPM (except the auth value), so pass None.

For outside_info, pass extra data to include in the creation data as a byte array.
If you don’t need it, pass None.

For creation_pcrs, pass PCR indices to include in the creation data.
You can use this to record the environment at creation time.
If you don’t need it, pass None.

If you want to do this in the terminal:

# Default values are used for the object attributes, 
# the Name hash algorithm, etc

# Create an SRK
# Save the created key to a context file with "-c"
tpm2_createprimary -c srk.ctx

# Create and load an RSA key
# Specify the parent object (context file path or handle) with "-C"
# Set the object attributes with "-a"
# Set the algorithm with "-G" (<type>:<scheme>:<symmetric-details>)
# Set the auth value for the object with "-p"
# Load the created object and output it to a context file with "-c"
tpm2_create \
  -C srk.ctx \
  -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|decrypt" \
  -G rsa2048:oaep-sha256 \
  -p AuthValue \
  -c rsa.ctx

# Using "-c" performs the same flow as tpm2_createloaded.
# If the TPM doesn't support it, the command returns an error.
Enter fullscreen mode Exit fullscreen mode

Key Loading

After creating the RSA key, you need to load it into the TPM.
However, create_primary() creates and loads a key, so you can skip this step.

// Load the created key into the TPM
let hrsa = self.ctx.load(
    hsrk.into(),
    key_result.out_private,
    key_result.out_public,
)?;  
Enter fullscreen mode Exit fullscreen mode

For parent_handle, pass the parent handle used to load the created key.
The function requires authorization for this handle.
Pass the same handle you used as parent_handle in create().

For private and public, pass a created object's private and public parts.
You can take them from the return values of create():

For private, use out_private, for public, use out_public.

If you want to do this in the terminal:

# Create an RSA key
# Save the public part to a file with "-u"
# Save the private part to a file with "-r"
tpm2_create \
  -C srk.ctx \
  -u rsa.pub \
  -r rsa.priv \
  -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|decrypt" \
  -G rsa2048:oaep-sha256 \
  -p AuthValue

# Load the created key into the TPM
# Specify the saved public file with "-u"
# Specify the saved private file with "-r"
# Save the context file with "-c"
tpm2_load \
  -C srk.ctx \
  -u rsa.pub \
  -r rsa.priv \
  -c rsa.ctx
Enter fullscreen mode Exit fullscreen mode

tpm2_load(1) - tpm2-tools

Key Persistence

To make a key persistent, use evict_control().
The transient object is copied to a persistent handle.
This function requires authorization, but I skip the authorization settings here since they were already set earlier.

// Make the target object persistent
match self.ctx.evict_control(
    Provision::Owner,
    handle,
    PersistentTpmHandle::new(value)?.into(),
) {
Enter fullscreen mode Exit fullscreen mode

For auth, pass the hierarchy authorization, Owner or Platform.
This function requires authorization for this handle.
If you use Owner, the persistent handle value must be in 0x81000000–0x817FFFFF.
If you use Platform, it must be in 0x81800000–0x81FFFFFF.

For object_handle, pass the object handle to persist.
After creating a key, you can get a KeyHandle from the return value and convert it to an ObjectHandle with into().
For create_primary(), it’s available as the key_handle field in the return value.

For persistent, pass the destination persistent handle value (0x81000000–0x81FFFFFF).
If object_handle is a transient object, it's persisted under this handle value.
If it's already a persistent object, it's removed instead.

If you want to do this in the terminal:

# Persist an object
# Specify the context file path (or handle value) of the target object with "-c"
# Specify the destination persistent handle as the last argument
tpm2_evictcontrol -c rsa.ctx 0x81000002

# If you don’t specify a persistent handle value, it's stored under an available handle value
Enter fullscreen mode Exit fullscreen mode

tpm2_evictcontrol(1) - tpm2-tools

Removing Transient Objects

Even after you make an object persistent, the original transient object remains loaded.
Because the number of transient objects that can be loaded at the same time is limited, it’s best to remove any you no longer need.
With swtpm, I was able to keep up to three transient objects at once (this may vary depending on the environment or version).

flush_context() flushes the context associated with a loaded handle (e.g., a transient object or a session).
This function doesn't allow any authorization sessions, so make sure to disable them before calling it.

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        

self.ctx.execute_without_session(|ctx| {
    // Remove the transient object
    ctx.flush_context(handle)
})?;   
Enter fullscreen mode Exit fullscreen mode

For handle, pass the handle of the object (or session) you want to remove.

If you want to do this in the terminal:

# Specify the object to be removed as the last argument
tpm2_flushcontext 0x80000001

# Remove all transient objects with "-t"
tpm2_flushcontext -t
Enter fullscreen mode Exit fullscreen mode

tpm2_flushcontext(1) - tpm2-tools

Public Struct

In this implementation, I use Rsa as the object type.

You can build a Public with PublicBuilder by setting the required fields.

// Build a Public struct
let public = PublicBuilder::new()
    .with_public_algorithm(PublicAlgorithm::Rsa)
    .with_object_attributes(attrs)
    .with_name_hashing_algorithm(HashingAlgorithm::Sha256)
    .with_rsa_parameters(params)
    .with_rsa_unique_identifier(PublicKeyRsa::default())
    .build()?;
Enter fullscreen mode Exit fullscreen mode
object_attributes

For object_attributes, pass the object attributes with ObjectAttributes.

// SRK attributes
let attrs = ObjectAttributesBuilder::new()
    .with_fixed_tpm(true)
    .with_fixed_parent(true)
    .with_no_da(true)
    .with_restricted(true)
    .with_user_with_auth(true)
    .with_sensitive_data_origin(true)
    .with_decrypt(true)
    .build()?;

// RSA key attributes
let attrs = ObjectAttributesBuilder::new()
    .with_fixed_tpm(true)
    .with_fixed_parent(true)
    .with_user_with_auth(true)
    .with_sensitive_data_origin(true)
    .with_decrypt(true)
    .build()?;
Enter fullscreen mode Exit fullscreen mode

In this code, the SRK attributes follow the guidance for a shared SRK.
Since it’s intended to be usable by multiple applications, the guidance recommends setting noDA and leaving the auth policy empty (and typically using an all-zero auth value).

When creating an asymmetric key, you need to set with_sensitive_data_origin(true), and you can't provide sensitive data from outside the TPM.
(Reference: Trusted Platform Module Library Part 1: Architecture (p.183 sensitiveDataOrigin))

Note:
If with_user_with_auth(false) is set, you can’t use password or HMAC authorization for that object.
(Reference: Trusted Platform Module Library Part 1: Architecture (p.184 userWithAuth))

name_hashing_algorithm

For name_hashing_algorithm, pass the hash algorithm used to compute the object’s Name.

For now, Sha256 is widely supported and recommended from a security standpoint.

auth_policy

For auth_policy, pass the policy digest to set on the object as a byte array.

You can use it to restrict how the object can be used.
If you don’t need a policy, pass Digest::default().

parameters

For parameters, pass the internal parameters for the selected object type as PublicRsaParameters.

// SRK parameters
let params = PublicRsaParametersBuilder::new_restricted_decryption_key(
    SymmetricDefinitionObject::AES_128_CFB,
    RsaKeyBits::Rsa2048,
    RsaExponent::ZERO_EXPONENT,
)
.build()?;

// RSA key parameters
let hash_algo = HashScheme::new(HashingAlgorithm::Sha256);
let params = PublicRsaParametersBuilder::new()
    .with_scheme(RsaScheme::Oaep(hash_algo))
    .with_key_bits(RsaKeyBits::Rsa2048)
    .with_exponent(RsaExponent::ZERO_EXPONENT)
    .build()?;
Enter fullscreen mode Exit fullscreen mode

new_restricted_decryption_key() provides parameters typically used for an SRK.

For symmetric, pass the symmetric algorithm to wrap (protect) child keys.

You can also choose one of the constants AES_128_CFB, AES_256_CFB, or SM4_128_CFB.
For most use cases, AES_128_CFB is a reasonable default.

For key_bits, pass a key size (in bits).

For now, Rsa2048 or higher is recommended.

For exponent, pass a public exponent.
ZERO_EXPONENT means you pass 0, which the TPM treats as 65537 (2^16 + 1).

For an RSA key, you set the scheme (padding) instead of a symmetric algorithm.
You can choose from Oaep, RsaEs, or Null.
Oaep is recommended, and in that case you also need to set a hash algorithm.

unique

For unique, pass bytes that uniquely identify the public area.

When creating a key, this field is filled with a value generated inside the TPM (for an asymmetric key, it’s the public key), so any value you pass here is ignored.
You need to set it when loading an external key, for example.
If you don’t need it, use PublicKeyRsa::default().

🔗 References:


6. Wrap the Key

Now we’re finally at the key-wrapping step!

In this implementation, the HMAC session has the decrypt attribute set when wrapping a key.
Only if the first parameter is a sized buffer will it be encrypted on the host side and sent to the TPM.

HMAC session creation

To create an HMAC session, use start_auth_session().

// Create an HMAC session
let hmac_session = self.ctx.start_auth_session(
    None,
    None,
    None,
    SessionType::Hmac,
    SymmetricDefinition::AES_128_CFB, // symmetric algorithm used for the decrypt/encrypt session attributes
    HashingAlgorithm::Sha256,
)?;
Enter fullscreen mode Exit fullscreen mode

For tpm_key, pass the key handle used to encrypt the salt.
This is used to derive the session key.
If you pass None, it's treated as an empty byte string.

For bind, pass the handle that provides an auth value.
This is used to derive the session key.
If you don’t need it, pass None.

For nonce, pass a nonce.
This is used to derive the session key.
If you provide one, it must be at least 16 bytes.
Even if you reuse the same session, the internal values are updated on each call, so the same nonce won’t be reused as-is.
If you pass None, ESAPI generates a nonce of an appropriate length.

For session_type, pass one of Hmac, Policy, or Trial.
Trial is like a Policy session, but it’s for computing the policy digest only (it can’t be used for authorization).

For symmetric, pass the symmetric algorithm used when the decrypt or encrypt session attributes are set.

You can also choose from the constants AES_128_CFB, AES_256_CFB, or SM4_128_CFB.

For auth_hash, pass a hash algorithm used by the session.

Sha256 is widely supported and is recommended from a security standpoint.

Note:
If both tpm_key and bind are None, the session key is an empty byte string.

After creating the session, you can set attributes on it.

For session, pass the AuthSession from the Option returned by start_auth_session().

For attributes and mask, pass the values returned by SessionAttributesBuilder::build().

// Add attributes to the session
let (attrs, mask) = SessionAttributesBuilder::new()
    .with_decrypt(decrypt)
    .with_encrypt(encrypt)
    .build();

self.ctx.tr_sess_set_attributes(hmac_session.unwrap(), attrs, mask)?;
Enter fullscreen mode Exit fullscreen mode

By default, with_continue_session(true) is set, so the session stays active after a successful call.
If it’s false, the session is flushed on success.

with_decrypt(true) encrypts the first command parameter on the host side, sends it to the TPM, and the TPM decrypts it after the HMAC computations successfully complete.

with_encrypt(true) encrypts the first response parameter on the TPM before it performs the HMAC computations, returns it encrypted, and then the host decrypts it.

These behaviors apply only when the first command/response parameter is a sized buffer (a TPM2B type).
During key wrapping, you end up sending plaintext to the TPM, so I set the decrypt attribute to protect it in transit.

If you want to do this in the terminal:

# Create an HMAC session
# Save the session context to a file with "-S"
tpm2_startauthsession --hmac-session -S hmacsession.ctx

# Add attributes to the session
# Update the session context file in place
tpm2_sessionconfig hmacsession.ctx --enable-encrypt hmacsession.ctx
Enter fullscreen mode Exit fullscreen mode

Note:
If a session isn't used for authorization, you must set at least one session attribute.
(Here, "session attributes" refers only to decrypt, encrypt, and audit.)
Depending on the function, the session may not be usable, or only specific attributes may be allowed. For functions that don't require authorization, you can't use a password session.

In this implementation, I use execute_with_session() to run the closure with the session applied.

For session_handle, pass the value returned by start_auth_session().

For f, pass a closure that takes &mut Context.

Key Wrapping

To encrypt (wrap) with an RSA key, use rsa_encrypt().

// Wrap the target key
let wrapped_key = self.ctx.execute_with_session(
    self.wrap_session,
    |ctx| {
        ctx.rsa_encrypt(
            hrsa.into(),
            PublicKeyRsa::try_from(target_key.as_slice())?,
            RsaDecryptionScheme::Null,
            Data::default(),
        )
    },
)?;
Enter fullscreen mode Exit fullscreen mode

For key_handle, pass the handle of the key object used for encryption.

For message, pass the data you want to encrypt as a byte array.

For in_scheme, pass the padding scheme.
If the object specified by key_handle has a scheme other than Null, pass either the same scheme or Null. If the object’s scheme is Null, the scheme you pass here will be used.

For label, pass the data to put into OAEP’s L parameter as a byte array.
If you provide a non-empty label, the last byte must be 0.
If you don’t need it, pass Data::default().

If you want to do this in the terminal:

# Specify the key context file (or handle value) with "-c"
# Specify the padding scheme with "-s" (default: RSAES_PKCSV1.5)
# Save the encrypted output to a file with "-o" (otherwise it goes to stdout)
# Specify the file to be encrypted as the last argument
tpm2_rsaencrypt \
  -c rsa.ctx \
  -s null \
  -o msg.enc \
  msg.ptext
Enter fullscreen mode Exit fullscreen mode

tpm2_rsaencrypt(1) - tpm2-tools

🔗 References:


Unwrap the Key

To decrypt (unwrap) with an RSA key, use rsa_decrypt().

This function requires authorization, so you need to configurate authorization in place before calling it.

Authorization Settings

Authorization is required for the RSA key used for decryption.
To set its auth value, use tr_set_auth(). You can skip this if the auth value is an empty byte string.

// Set the auth value for the object
self.ctx.tr_set_auth(hrsa, Auth::try_from(RSA_KEY_AUTH)?)?;

// The auth value isn't validated here.
// It's verified when you run a function that requires authorization.
Enter fullscreen mode Exit fullscreen mode

For object_handle, pass the object handle you want to set the auth value for.

For auth, pass the auth value for the target object as a byte array.

Just like in the encryption step, I use an HMAC session.
For this step, I set the encrypt attribute.

Key Unwrapping

For cipher_text, pass the data you want to decrypt as a byte array.

For the rest of the parameters, pass the same values you used in the encryption step.

// Unwrap the target key
let unwrapped_key = self.ctx.execute_with_session(
    self.unwrap_session,
    |ctx| {
        ctx.rsa_decrypt(
            hrsa.into(),
            PublicKeyRsa::try_from(target_key)?,
            RsaDecryptionScheme::Null,
            Data::default(),
        )
    },
)?;
Enter fullscreen mode Exit fullscreen mode

If you want to do this in the terminal:

# Specify the key context file (or handle value) with "-c"
# Set the auth value for the specified key with "-p"
# (you can also combine it with a session using the "session:" prefix)
# Specify the padding scheme with "-s" (default: RSAES_PKCSV1.5)
# Save the decrypted data to a file with "-o" (otherwise it goes to stdout)
# Specify the file to be decrypted as the last argument
tpm2_rsadecrypt \
  -c rsa.ctx \
  -p session:hmacsession.ctx+AuthValue \
  -s null \
  -o msg.ptext \
  msg.enc
Enter fullscreen mode Exit fullscreen mode

tpm2_rsadecrypt(1) - tpm2-tools

🔗 Reference:


Shut Down the TPM

To shut down the TPM, use shutdown().
With a hardware TPM, the platform often sends TPM2_Shutdown() as part of power-state transitions, so you may not need to call it yourself.
(Reference: TCG PC Client Platform TPM Profile Specification for TPM 2.0 (p.31 Power Management))

// Shut down the TPM with Clear
ctx.shutdown(StartupType::Clear)
Enter fullscreen mode Exit fullscreen mode

For shutdown_type, pass either Clear or State.
With Clear, the TPM discards the current volatile state.
With State, the TPM saves the current volatile state, and how it is restored depends on the argument you pass to startup() next time.

If you want to do this in the terminal:

# Shut down with State
tpm2_shutdown
# Shut down with Clear
tpm2_shutdown -c
Enter fullscreen mode Exit fullscreen mode

tpm2_shutdown(1) - tpm2-tools

🔗 Reference:


Remove a Persistent Key

To remove a persistent key, use evict_control().

You also use this function to make a key persistent, but for removal you specify the persistent object you want to remove, and you pass the same persistent handle value.

// Remove the persistent object
let _ = self.ctx.evict_control(
    Provision::Owner,
    handle,
    PersistentTpmHandle::new(value)?.into(),
)?;
Enter fullscreen mode Exit fullscreen mode

Clear the TPM

To clear the TPM state — including objects stored under the Storage/Endorsement hierarchies — use clear().
This function requires authorization, so make sure your authorization settings are in place before calling it.

fn clear_tpm(&mut self) -> Result<()> {
    // Set a password session
    self.ctx.set_sessions((Some(AuthSession::Password), None, None));

    // If the auth value is non-empty, set it with `tr_set_auth()`.

    // Clear the TPM
    self.ctx.clear(AuthHandle::Lockout)?;

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

For auth_handle, pass the authorization handle, either Lockout or Platform.
If you use Platform, physical presence may be required depending on the platform/firmware configuration.

If you want to do this in the terminal:

# This example uses the Platform hierarchy for authorization

# Specify the hierarchy used for authorization with "-c" (default: lockout)
# Provide the auth value for specified hierarch as the last argument
tpm2_clear -c p <platformAuth>
Enter fullscreen mode Exit fullscreen mode

tpm2_clear(1) - tpm2-tools


About tpm2-tss Logs

If you see log messages from tpm2-tss (included in libtss2-dev), you can adjust the log level using environment variables.

# Disable all log output
export TSS2_LOG=all+NONE
# Show only ERROR and above
export TSS2_LOG=all+ERROR
# Write logs to a file
export TSS2_LOGFILE=tss.log
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

It was my first time using Ubuntu.
It made it really easy to set up a Linux environment.

For this implementation, tss-esapi is a much lower-level API than NCrypt, which I used on Windows, so it was much more challenging.

While writing this explanation, I didn’t just read the docs. I also tried a lot of things myself along the way.
Because of that, it ended up taking me more than a month to finish this article...

It was tough, but it was also more fun to write than I expected!

Thanks for reading!

Top comments (0)