11 KiB
Rust Wrapper
The wolftpm crate provides safe Rust bindings for wolfTPM. The raw FFI is generated with bindgen and kept in the sys module. The rest of the crate is a safe API: functions return Result, TPM handles are released when they go out of scope, and all unsafe stays inside the crate. The crate lives in wrapper/rust/wolftpm in the wolfTPM source tree.
Requirements
- Rust and Cargo, stable, rustc 1.81 or newer.
- A built wolfTPM C library (
libwolftpm) and its wolfSSL dependency (libwolfssl). The crate links a prebuilt library. It does not build the C code itself. - For the tests, a software TPM. wolfTPM's own
fwtpm_serverworks, or any other TPM emulator or software TPM. See fwTPM and SWTPM.
Step 1: Build the C Library
Run these from the wolfTPM repository root:
./autogen.sh
./configure --enable-swtpm --enable-fwtpm
make
This produces src/.libs/libwolftpm. With --enable-fwtpm it also builds the software TPM server at src/fwtpm/fwtpm_server.
Step 2: Build the Crate
cd wrapper/rust/wolftpm
cargo build
The build finds the libraries in this order:
- If
WOLFTPM_PREFIXorWOLFSSL_PREFIXare set, it uses$PREFIX/includeand$PREFIX/libof an installed copy. - Otherwise it uses the in-tree build: headers from the repo root, libraries from
src/.libs. wolfSSL is located withpkg-config, then a local./wolfsslor a sibling../wolfsslcheckout.
Shared libraries are preferred. Static is used when no shared library is present.
The library is no_std and uses alloc for owned keys, blobs, and output buffers. Bare-metal applications must provide a global allocator. The build script translates Rust cross-compilation targets such as riscv32imac-unknown-none-elf to clang's target spelling when generating the FFI bindings.
The wrapper/rust directory also has a Makefile for the common tasks. After building the C library, make -C wrapper/rust builds, lints, and documents the crate, and make -C wrapper/rust test runs the tests (a software TPM must be listening on localhost:2321).
Step 3: Run the Tests
The integration tests talk to a software TPM over a socket. They are behind the swtpm-tests feature, so a plain cargo test does not need a running server.
Start a server on port 2321 (from the repo root):
./src/fwtpm/fwtpm_server --clear --port 2321 --platform-port 2322 &
Run the tests against it:
cd wrapper/rust/wolftpm
TPM2_SWTPM_HOST=localhost TPM2_SWTPM_PORT=2321 \
cargo test --features swtpm-tests -- --test-threads=1
Use --test-threads=1 because the software TPM serves one client at a time.
Run a single test file, for example the signing tests:
cargo test --features swtpm-tests --test sign -- --test-threads=1
The default host and port are localhost:2321, so the environment variables can be omitted when the server is on that address.
What the Tests Check
Each file under tests/ exercises one area against the software TPM:
| File | Checks |
|---|---|
tests/smoke.rs |
Random bytes differ across draws, and a primary key loads. |
tests/keys.rs |
Create and load a child key, blob round-trip, short-auth blob round-trip, and an auth-protected parent. |
tests/sign.rs |
Sign a digest and verify it, and reject a tampered signature. |
tests/seal.rs |
Seal a secret and unseal it, and fail with the wrong auth. |
tests/seal_pcr.rs |
PCR-bound seal and unseal, unseal fails after a PCR changes, and an invalid PCR selection is rejected. |
tests/nv.rs |
Define an NV index, write and read it, then delete it. |
tests/pcr.rs |
Read a PCR, extend it, and confirm the value changed. |
tests/certify.rs |
An attestation key (ECC and RSA) certifies another key. |
tests/quote.rs |
Quote PCRs with an ECC and RSA AIK, and reject a bad PCR selection. |
tests/credential.rs |
MakeCredential then ActivateCredential round-trip. |
tests/ek.rs |
Create the endorsement key and export its public part. |
tests/persist.rs |
Persist a key, read it back, and evict it. |
tests/rsa.rs |
RSA-OAEP encrypt and decrypt, and an explicit OAEP-SHA1 round-trip. |
tests/hmac.rs |
Raw-key HMAC and a TPM-resident keyed-hash key HMAC. |
tests/caps.rs |
Self-test and capability query. |
tests/ecdh.rs |
ECDH generate then recover the same shared secret. |
tests/symmetric.rs |
AES-CFB encrypt and decrypt round-trip. |
tests/import.rs |
Import an external RSA and ECC private key. |
Example Test Output
running 2 tests
test certify_with_ecc_aik ... ok
test certify_with_rsa_aik ... ok
test result: ok. 2 passed; 0 failed; 0 ignored
running 4 tests
test create_and_load_child ... ok
test key_blob_roundtrip_then_load ... ok
test key_blob_roundtrip_preserves_short_auth ... ok
test auth_protected_parent_loads_child ... ok
test result: ok. 4 passed; 0 failed; 0 ignored
running 2 tests
test sign_then_verify ... ok
test verify_rejects_tampered_signature ... ok
test result: ok. 2 passed; 0 failed; 0 ignored
running 2 tests
test rsa_oaep_roundtrip ... ok
test rsa_oaep_sha1_roundtrip ... ok
test result: ok. 2 passed; 0 failed; 0 ignored
running 1 test
test make_and_activate_credential ... ok
test result: ok. 1 passed; 0 failed; 0 ignored
Step 4: Run the Examples
There are two examples. The first is minimal:
cargo run --example create_primary
It opens the software TPM, reads random bytes, and creates an RSA and an ECC storage root key.
The second runs the whole API in one pass:
cargo run --example full_flow
Expected output. The random bytes and handle values differ between runs:
device connected to software TPM
get_random a91b37b1838d002453f1632ba2c0adbc
create_primary ECC SRK handle 0x80000000
create_and_load signing key handle 0x80000001
sign/verify 64 byte signature, verified
seal/unseal recovered "my secret"
key blob 257 bytes, reloaded as handle 0x80000002
pcr read/extend PCR16 000000000000.. -> debb3e7acfff..
nv define/rw 32 bytes at index 0x01500100
certify 157 byte attestation
done all operations succeeded
If the wolfTPM C library was built with debug output, you will also see verbose TPM2_* traces from the library. A normal build prints only the lines above.
Using the Wrapper
use wolftpm::{Device, HashAlg, Hierarchy, KeyAlg, KeyBlob, Template};
fn main() -> Result<(), wolftpm::TpmError> {
// Connect to a software TPM. Use Device::open() for the platform default.
let dev = Device::open_swtpm()?;
// Random bytes from the TPM.
let mut nonce = [0u8; 32];
dev.get_random(&mut nonce)?;
// Storage root key under the owner hierarchy.
let srk = dev.create_primary(Hierarchy::Owner, KeyAlg::EccP256, None)?;
// Signing key under the SRK, then sign and verify a digest.
let signer = dev.create_and_load(&srk, &Template::signing(KeyAlg::EccP256)?, None)?;
let digest = [0x11u8; 32];
let sig = signer.sign_hash(&digest)?;
signer.verify_hash(&digest, &sig)?;
// Seal a secret to the TPM and read it back.
let sealed = dev.seal(&srk, b"my secret", None)?;
let _secret = dev.unseal(sealed, &srk, None)?;
// Persist a key as bytes, then load it again. Scope the reloaded key so it
// releases its TPM handle before more transient objects are created below
// (many TPMs allow only three transient objects at once).
let bytes = {
let blob = dev.create_key(&srk, &Template::signing(KeyAlg::EccP256)?, None)?;
blob.to_bytes()?
};
{
let restored = KeyBlob::from_bytes(&dev, &bytes)?;
let _loaded = restored.load(&srk, None)?;
}
// Read and extend a PCR.
let _value = dev.pcr_read(16, HashAlg::Sha256)?;
dev.pcr_extend(16, HashAlg::Sha256, &[0xAB; 32])?;
// Define, write, read, and delete an NV index.
let mut slot = dev.nv_create(0x0150_0100, 32, None)?;
dev.nv_write(&mut slot, b"metadata", 0)?;
let mut buf = [0u8; 32];
dev.nv_read(&mut slot, &mut buf, 0)?;
dev.nv_delete(0x0150_0100)?;
// Attest that a key lives in this TPM, signed by an attestation key.
let aik = dev.create_and_load(&srk, &Template::attestation(KeyAlg::EccP256)?, None)?;
let _attestation = dev.certify(&signer, &aik, &nonce)?;
Ok(())
}
Keys and the device release their TPM handles automatically when they drop.
What the Crate Covers
- Device open and cleanup, TPM random numbers, self-test, and capability query.
- Primary and child keys. Templates for storage, signing, attestation, EK, RSA decrypt, keyed-hash HMAC, symmetric AES, and ECDH keys.
- Key blob serialize and load for persistence, and external RSA and ECC key import.
- Persistent key handles: store, read back, and evict.
- Sign and verify.
- RSA-OAEP encrypt and decrypt, including an explicit label hash (SHA-1 for Microsoft enrollment interop).
- Symmetric AES-CFB encrypt and decrypt.
- ECDH key agreement.
- HMAC, both raw-key and with a TPM-resident keyed-hash key.
- Seal and unseal, plain and bound to a PCR policy.
- PCR read and extend.
- NV define, write, read, delete, and certificate read (EK certificate).
- Attestation: certify a key, and quote PCRs.
- Credential activation: MakeCredential and ActivateCredential.
Recovered secrets (unseal, RSA decrypt, ECDH, AES decrypt, credential activation) are returned in a Secret that zeroizes its buffer on drop.
Transport Security
For confidentiality on the TPM transport, start a parameter-encryption session with Device::start_encrypted_session before the secret-bearing operations:
let srk = dev.create_primary(Hierarchy::Owner, KeyAlg::EccP256, None)?;
let _session = dev.start_encrypted_session(&srk)?; // salted HMAC + AES-CFB
let sealed = dev.seal(&srk, b"secret", None)?; // command param encrypted
let plain = dev.unseal(sealed, &srk, None)?; // response param encrypted
While the session is alive its salted HMAC session occupies auth slot 1, so wolfTPM encrypts the sensitive command and response parameters of seal/unseal, RSA and AES encrypt/decrypt, HMAC, NV, ECDH, and key create/load. Only one session is allowed at a time. The attestation commands (certify, quote, activate_credential) need the same auth slot and are refused while a session is active, so drop the session before calling them.
!!! warning
Without a session, parameters cross the transport in the clear. Either use a session or run over a trusted local transport (the Linux kernel device or a local socket) rather than a remote TPM2_SWTPM_HOST or an observable physical bus.
License
GPLv3, or a commercial wolfSSL license, matching wolfTPM.