DEV Community

Cover image for How I Used TPM for Key Encryption in Rust (Using Windows APIs)
tsuruko
tsuruko

Posted on • Edited on

How I Used TPM for Key Encryption in Rust (Using Windows APIs)

I implemented TPM-based key wrapping on Windows using the windows-sys crate.
(Linux version: How I Used TPM for Key Encryption in Rust on Linux (Hardware TPM & vTPM))

At first, I tried key wrapping with tss-esapi, but since it's designed for Linux, using it on Windows would have required a more complex setup.
So I chose windows-sys for this implementation.

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

Table of Contents


Cargo.toml

[dependencies]
windows-sys = { version = "0.61", features = [
    "Win32_Security_Cryptography",
    "Win32_Foundation",
] }
rand = "0.9"
zeroize = "1.8"
Enter fullscreen mode Exit fullscreen mode

Implementation

This crate uses FFI, so you'll need to call the Windows APIs inside an unsafe block.

use std::{
    ffi::{OsStr, c_void},
    os::windows::ffi::OsStrExt,
    ptr,
};

use windows_sys::{
    core::HRESULT,
    Win32::{
        Foundation::NTE_BAD_KEYSET, 
        Security::Cryptography::*,
    },
};
use zeroize::Zeroize;

const KEY_NAME: &str = "RSA_KEY";

fn open_provider() -> Result<NCRYPT_PROV_HANDLE, HRESULT> {
    unsafe{
        let mut hprov: NCRYPT_PROV_HANDLE = 0;

        let status = NCryptOpenStorageProvider(
            &mut hprov,
            MS_PLATFORM_CRYPTO_PROVIDER,
            0,
        );
        if status != 0 {
            Err(status)
        } else {
            Ok(hprov)
        }        
    }
}

fn create_padding_info() -> BCRYPT_OAEP_PADDING_INFO {
    BCRYPT_OAEP_PADDING_INFO {
        pszAlgId: BCRYPT_SHA256_ALGORITHM,
        pbLabel: ptr::null_mut(),
        cbLabel: 0,
    }
}

fn to_utf16(s: &str) -> Vec<u16> {
    let mut utf16: Vec<u16> = OsStr::new(s).encode_wide().collect();
    utf16.push(0);

    utf16
}

// The target key is 32 bytes (AES-256), which is generally recommended

// The status is an i32 (HRESULT) value, 
// and 0 means the function was successful

fn wrap_key(mut target_key: [u8; 32]) -> Result<Vec<u8>, HRESULT> {
    unsafe {
        let hprov = open_provider()?;

        let mut hkey: NCRYPT_KEY_HANDLE = 0;
        let key_name = to_utf16(KEY_NAME);

        // Get the key registered with the TPM
        let status = NCryptOpenKey(
            hprov,
            &mut hkey,
            key_name.as_ptr(),
            0,
            0,
        );
        if status != 0 {
            match status {
                NTE_BAD_KEYSET => {
                    // The key with this name doesn't exist yet

                    // Create a persistent RSA key with the given name
                    let status_2 = NCryptCreatePersistedKey(
                        hprov,
                        &mut hkey,
                        BCRYPT_RSA_ALGORITHM,
                        key_name.as_ptr(),
                        0,
                        0,
                    );
                    if status_2 != 0 {
                        NCryptFreeObject(hprov);
                        return Err(status_2)
                    }

                    let status_2 = NCryptFinalizeKey(
                        hkey,
                        0,
                    );
                    if status_2 != 0 {
                        NCryptFreeObject(hkey);
                        NCryptFreeObject(hprov);
                        return Err(status_2)
                    }
                },
                _ => {
                    NCryptFreeObject(hprov);
                    return Err(status)
                },
            }
        } 

        // Get encrypted data size (not needed if you already know it)
        let mut size: u32 = 0;
        let padding_info = create_padding_info();

        let status = NCryptEncrypt(
            hkey,
            target_key.as_ptr(),
            target_key.len() as u32,
            &padding_info as *const _ as *const c_void,
            ptr::null_mut(),
            0,
            &mut size,
            NCRYPT_PAD_OAEP_FLAG,
        );
        if status != 0 {
            NCryptFreeObject(hkey);
            NCryptFreeObject(hprov);
            return Err(status)
        }

        // Wrap the target key
        let mut wrapped_key = vec![0u8; size as usize];

        let status = NCryptEncrypt(
            hkey,
            target_key.as_ptr(),
            target_key.len() as u32,
            &padding_info as *const _ as *const c_void,
            wrapped_key.as_mut_ptr(),
            size,
            &mut size,
            NCRYPT_PAD_OAEP_FLAG,
        );
        if status != 0 {
            NCryptFreeObject(hkey);
            NCryptFreeObject(hprov);
            return Err(status)
        }

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

        // Truncate the buffer to the actual encrypted-data size
        wrapped_key.truncate(size as usize);
        NCryptFreeObject(hkey);
        NCryptFreeObject(hprov);

        Ok(wrapped_key)
    }
}

fn unwrap_key(target_key: Vec<u8>) -> Result<Vec<u8>, HRESULT> {
    // The process is almost the same as encryption
    unsafe {
        let hprov = open_provider()?;

        let mut hkey: NCRYPT_KEY_HANDLE = 0;
        let key_name = to_utf16(KEY_NAME);

        let status = NCryptOpenKey(
            hprov,
            &mut hkey,
            key_name.as_ptr(),
            0,
            0,
        );
        if status != 0 {
            NCryptFreeObject(hprov);
            return Err(status)
        }

        // Get decrypted data size (not needed if you already know it)
        let mut size: u32 = 0;
        let padding_info = create_padding_info();

        let status = NCryptDecrypt(
            hkey,
            target_key.as_ptr(),
            target_key.len() as u32,
            &padding_info as *const _ as *const c_void,
            ptr::null_mut(),
            0,
            &mut size,
            NCRYPT_PAD_OAEP_FLAG,
        );
        if status != 0 {
            NCryptFreeObject(hkey);
            NCryptFreeObject(hprov);
            return Err(status)      
        }

        // Unwrap the target key
        let mut unwrapped_key = vec![0u8; size as usize];

        let status = NCryptDecrypt(
            hkey,
            target_key.as_ptr(),
            target_key.len() as u32,
            &padding_info as *const _ as *const c_void,
            unwrapped_key.as_mut_ptr(),
            size,
            &mut size,
            NCRYPT_PAD_OAEP_FLAG,
        );
        if status != 0 {
            NCryptFreeObject(hkey);
            NCryptFreeObject(hprov);
            return Err(status)      
        }

        // Truncate the buffer to the actual decrypted-data size
        unwrapped_key.truncate(size as usize);
        NCryptFreeObject(hkey);
        NCryptFreeObject(hprov);

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

Try running it in the main function:

use rand;

fn main() {
    let key: [u8; 32] = rand::random();
    println!("Plaintext Key: {:?}", key);

    let wrapped_key = match wrap_key(key) {
        Ok(k) => {
            println!("Wrapped Key: {:?}", k);
            k
        },
        Err(e) => {
            println!("Error: {e}");
            return
        }, 
    };

    match unwrap_key(wrapped_key) {
        Ok(k) => println!("Unwrapped Key: {:?}", k),
        Err(e) => println!("Error: {e}"), 
    }
}
Enter fullscreen mode Exit fullscreen mode

If you want to delete the registered key:

fn delete_key() -> Result<(), HRESULT> {
    unsafe{
        let hprov = open_provider()?;

        let mut hkey: NCRYPT_KEY_HANDLE = 0;
        let key_name = to_utf16(KEY_NAME);

        let status = NCryptOpenKey(
            hprov,
            &mut hkey,
            key_name.as_ptr(),
            0,
            0,
        );
        if status != 0 {
            NCryptFreeObject(hprov);
            return Err(status)
        }

        // Delete the registered key
        let status = NCryptDeleteKey(
            hkey,
            0,
        );
        if status != 0 {
            NCryptFreeObject(hkey);
            NCryptFreeObject(hprov);
            Err(status)
        } else {
            // The key handle is automatically freed by NCryptDeleteKey() 
            NCryptFreeObject(hprov);
            Ok(())
        } 
    }
}
Enter fullscreen mode Exit fullscreen mode

Error Codes

This is a list of the main error codes, their corresponding constants, and descriptions.

Code (i32) Code (hex) Name Description
0 0x00000000 ERROR_SUCCESS Operation succeeded
-2146893802 0x80090006 NTE_BAD_KEYSET The key does not exist or is not registered with the TPM
-2146893809 0x8009000F NTE_EXISTS A key with the same name already exists
-2146893786 0x80090026 NTE_INVALID_HANDLE The handle is invalid (provider or key)
-2146893785 0x80090027 NTE_INVALID_PARAMETER One or more parameters are invalid
-2146893810 0x8009000E NTE_NO_MEMORY Not enough memory to complete the operation
-2146893808 0x80090010 NTE_PERM Access denied or operation not permitted
-2146893792 0x80090020 NTE_FAIL An internal error occurred
-2146893783 0x80090029 NTE_NOT_SUPPORTED The algorithm or option is not supported
-2146893815 0x80090009 NTE_BAD_FLAGS Invalid flags specified in dwFlags
-2146893784 0x80090028 NTE_BUFFER_TOO_SMALL Output buffer is too small
-2147479534 0x80090032 NTE_INVALID_STATE Object is in an invalid state (e.g. not finalized)
-2146893816 0x80090008 NTE_BAD_ALGID Invalid algorithm identifier
-2146893814 0x8009000A NTE_BAD_TYPE The key or object type is invalid

Explanation

I referred to the official Microsoft documentation to choose the right functions and understand each parameter, and I used the windows-sys docs to check the parameter types!

References:

In this section, I'll explain each function and parameter used in the implementation, based on these official documents.


1. Open the TPM Provider

First, you need to open the provider using NCryptOpenStorageProvider().

Microsoft Documentation:

windows-sys Documentation:

For phprovider, pass a variable (&mut usize) to receive the provider handle.

For pszprovidername, pass the alias of a key storage provider to load.
If you pass std::ptr::null(), the default key storage provider is loaded.

I use MS_PLATFORM_CRYPTO_PROVIDER, but if a TPM isn’t available, you can use MS_KEY_STORAGE_PROVIDER instead.

The dwflags parameter is reserved, so it should always be set to 0 for now.

let mut hprov: NCRYPT_PROV_HANDLE = 0;

let status = NCryptOpenStorageProvider(
    &mut hprov,
    MS_PLATFORM_CRYPTO_PROVIDER,
    0,
);
Enter fullscreen mode Exit fullscreen mode

Some of the return codes:

Note:
If the function fails, don't use or free the provider handle.

It’s described in the Microsoft documentation:


2. Create a Persistent Key (If It Doesn't Exist)

NCryptCreatePersistedKey() can create a persistent key with a given name.

Microsoft Documentation:

windows-sys Documentation:

For phkey, pass a variable (&mut usize) to receive the key handle.

For pszalgid, pass a null-terminated UTF-16 string that specifies the cryptographic algorithm.

You can find the standard CNG Algorithm Identifiers here:
CNG Algorithm Identifiers
I use BCRYPT_RSA_ALGORITHM for key wrapping.

For pszkeyname, pass a pointer to a null-terminated UTF-16 string containing the key name.
If the parameter is set to std::ptr::null(), this function will create an ephemeral key that won't be stored.

fn to_utf16(s: &str) -> Vec<u16> {
    let mut utf16: Vec<u16> = OsStr::new(s).encode_wide().collect();
    utf16.push(0);

    utf16
}
Enter fullscreen mode Exit fullscreen mode

encode_wide() converts an &OsStr to UTF-16.
Since pszkeyname needs a null-terminated string, a trailing 0 is appended.

For dwlegacykeyspec, pass a legacy identifier that specifies the key type.

In typical CNG key creation, pass 0, since the other values are for CryptoAPI(CAPI)/CSP compatibility.
If you set it to anything other than 0, it may fail with an error (NTE_NOT_SUPPORTED).

For dwflags, pass flags that modify the behavior of this function.
If you don't need it, pass 0.

But these values aren't commonly used for typical TPM use cases.

let mut hkey: NCRYPT_KEY_HANDLE = 0;
let key_name = to_utf16(KEY_NAME);

let status_2 = NCryptCreatePersistedKey(
    hprov,
    &mut hkey,
    BCRYPT_RSA_ALGORITHM,
    key_name.as_ptr(),
    0,
    0,
);
if status_2 != 0 {
    NCryptFreeObject(hprov); // Free the provider handle
    return Err(status_2)
}
Enter fullscreen mode Exit fullscreen mode

Note:
If the function fails, you should call NCryptFreeObject() to free the provider handle.

Some of the return codes:


You can use NCryptSetProperty() to set key properties, but I’m skipping it here.

After creating the key, you need to call NCryptFinalizeKey() to use it.

Microsoft Documentation:

windows-sys Documentation:

For dwflags, pass flags that modify the behavior of this function.
If you don't need it, pass 0.

NCRYPT_SILENT_FLAG prevents any user interface from being shown.
Since a TPM doesn't have one, it usually doesn’t change anything.

let status_2 = NCryptFinalizeKey(
    hkey,
    0,
);
if status_2 != 0 {
    NCryptFreeObject(hkey);
    NCryptFreeObject(hprov);
    return Err(status_2)
}
Enter fullscreen mode Exit fullscreen mode

Note:
If the function fails, you should call NCryptFreeObject() to free both the provider and key handles.

Some of the return codes:


2. Get a Key from the Provider (If It Already Exists)

If the key already exists, you can retrieve it from the provider using NCryptOpenKey().

Microsoft Documentation:

windows-sys Documentation:

These parameters are almost the same as those of NCryptCreatePersistedKey().

let mut hkey: NCRYPT_KEY_HANDLE = 0;
let key_name = to_utf16(KEY_NAME);

let status = NCryptOpenKey(
    hprov,
    &mut hkey,
    key_name.as_ptr(),
    0,
    0,
);
Enter fullscreen mode Exit fullscreen mode

If the specified key name doesn't exist, it'll cause NTE_BAD_KEYSET error.
You can handle return codes with match or if statements by comparing the constant with the returned value.

if status != 0 {
    match status {
        NTE_BAD_KEYSET => {
            // The key with this name doesn't exist yet

            // Create a persisted RSA key with the given name
            let status_2 = NCryptCreatePersistedKey(
                hprov,
                &mut hkey,
                BCRYPT_RSA_ALGORITHM,
                key_name.as_ptr(),
                0,
                0,
            );
            if status_2 != 0 {
                NCryptFreeObject(hprov);
                return Err(status_2)
            }

            let status_2 = NCryptFinalizeKey(
                hkey,
                0,
            );
            if status_2 != 0 {
                NCryptFreeObject(hprov);
                return Err(status_2)
            }
        },
        _ => {
            NCryptFreeObject(hprov);
            return Err(status)
        },
    }
} 
Enter fullscreen mode Exit fullscreen mode

Note:
If the function fails, you should call NCryptFreeObject() to free the provider handle.

Some of the return codes:


3. Get the Encrypted Data Size

You need to get the size of the encrypted data before encryption.
If you already know it, you can skip this step.

Microsoft Documentation:

windows-sys Documentation:

For pbinput, pass a pointer to the buffer to encrypt.

For cdinput, pass the size (in bytes) of the data (pbinput).

For ppaddinginfo, pass a pointer to a BCRYPT_OAEP_PADDING_INFO struct that contains padding information.
If you don’t pass NCRYPT_PAD_OAEP_FLAG in dwflags, or if you’re using a symmetric key, pass std::ptr::null().

For pboutput, pass a pointer to the output buffer for the encrypted data.
If you only want to get the encrypted data size, pass std::ptr::null_mut().

For cboutput, pass the buffer size (pboutput).
If pboutput is std::ptr::null_mut(), this parameter is ignored.
However, in my test, passing a non-zero value caused an NTE_INVALID_PARAMETER error.

For pcbresult, pass a variable (&mut u32) that receives the output size for pboutput.
If pboutput is std::ptr::null_mut(), this parameter receives the required output size.

For dwflags, pass a flag that modifies the behavior of this function.
The allowed flags depend on the key type specified by hkey.

NCRYPT_PAD_OAEP_FLAG is recommended for modern security.
When using this flag, you need to pass a pointer to a BCRYPT_OAEP_PADDING_INFO struct in ppaddinginfo.
For symmetric keys, pass 0.

let mut size: u32 = 0;
let padding_info = create_padding_info();

let status = NCryptEncrypt(
    hkey,
    target_key.as_ptr(),
    target_key.len() as u32,
    &padding_info as *const _ as *const c_void,
    ptr::null_mut(), // Pass null when getting the data size
    0,               // Pass 0 when getting the data size
    &mut size,
    NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
    NCryptFreeObject(hkey);
    NCryptFreeObject(hprov);
    return Err(status)
}
Enter fullscreen mode Exit fullscreen mode

Note:
If the function fails, you should call NCryptFreeObject() to free both the provider handle and the key handle.

Some of the return codes:

BCRYPT_OAEP_PADDING_INFO

Microsoft Documentation:

windows-sys Documentation:

For pszAlgId, pass the hashing algorithm used to create the padding.
You can find the list here: CNG Algorithm Identifiers

For pbLabel, pass a pointer to a buffer that contains label data used for OAEP padding.
If you don't need it, pass std::ptr::null_mut().

For cbLabel, pass the size of the pbLabel buffer.
If pbLabel is std::ptr::null_mut(), pass 0.

fn create_padding_info() -> BCRYPT_OAEP_PADDING_INFO {
    BCRYPT_OAEP_PADDING_INFO {
        pszAlgId: BCRYPT_SHA256_ALGORITHM,
        pbLabel: ptr::null_mut(),
        cbLabel: 0,
    }
}
Enter fullscreen mode Exit fullscreen mode

In this code, I use BCRYPT_SHA256_ALGORITHM as the hash algorithm.


4. Wrap the Key

Finally, wrap the key using NCryptEncrypt()!

let mut wrapped_key = vec![0u8; size as usize];

let status = NCryptEncrypt(
    hkey,
    target_key.as_ptr(),
    target_key.len() as u32,
    &padding_info as *const _ as *const c_void,
    wrapped_key.as_mut_ptr(),
    size,
    &mut size,
    NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
    NCryptFreeObject(hkey);
    NCryptFreeObject(hprov);
    return Err(status)
}
Enter fullscreen mode Exit fullscreen mode

To get the encrypted data, prepare a buffer with the required size, and pass a pointer to that buffer in pboutput.
If you queried the size with this function, allocate the buffer using the size stored in the variable you passed to pcbresult.

See the previous section for an explanation of NCryptEncrypt().

Note:
If the function fails, you should call NCryptFreeObject() to free both the provider handle and the key handle.

Lastly, truncate the output buffer to the actual size written, as returned in pcbresult.

wrapped_key.truncate(size as usize);
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
Enter fullscreen mode Exit fullscreen mode

Note:
Make sure to call NCryptFreeObject() to free both the provider handle and the key handle before returning from the function.


Unwrap the Key

Decrypting the wrapped key is almost the same as encryption — just replace NCryptEncrypt() with NCryptDecrypt().

// Get decrypted data size (not needed if you already know it)
let mut size: u32 = 0;
let padding_info = create_padding_info();

let status = NCryptDecrypt(
    hkey,
    target_key.as_ptr(),
    target_key.len() as u32,
    &padding_info as *const _ as *const c_void,
    ptr::null_mut(),
    0,
    &mut size,
    NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
    NCryptFreeObject(hkey);
    NCryptFreeObject(hprov);
    return Err(status)      
}

// Unwrap the target key
let mut unwrapped_key = vec![0u8; size as usize];

let status = NCryptDecrypt(
    hkey,
    target_key.as_ptr(),
    target_key.len() as u32,
    &padding_info as *const _ as *const c_void,
    unwrapped_key.as_mut_ptr(),
    size,
    &mut size,
    NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
    NCryptFreeObject(hkey);
    NCryptFreeObject(hprov);
    return Err(status)      
}
Enter fullscreen mode Exit fullscreen mode

Some of the return codes:


Delete Registered Key

You can delete the registered key using NCryptDeleteKey(), and the process is the same up to retrieving the key.

Microsoft Documentation:

windows-sys Documentation:

For dwflags, pass a flag that modifies the behavior of this function.
The only supported value is NCRYPT_SILENT_FLAG.
If you don't need it, pass 0.

let status = NCryptDeleteKey(
    hkey,
    0,
);
if status != 0 {
    NCryptFreeObject(hkey);
    NCryptFreeObject(hprov);
    Err(status)
} else {
    // The key handle is automatically freed by NCryptDeleteKey()
    NCryptFreeObject(hprov);
    Ok(())
} 
Enter fullscreen mode Exit fullscreen mode

Note:
If the function succeeds, it automatically frees the key handle.
Before returning from the function, you only need to call NCryptFreeObject() to free the provider handle.

It’s described in the Microsoft documentation:

Some of the return codes:


Final Thoughts

At first, since I didn’t know much about C++, I struggled to read the function signatures...
But through this implementation, I came to understand them better, and I learned how TPM works.

Thanks for reading!

Top comments (0)