11 KiB
wolfTPM with the Linux Kernel TPM Device (/dev/tpmX)
On Linux the kernel's TPM driver stack exposes a TPM through a character device, and wolfTPM can use it directly instead of driving SPI or I2C itself. This is the right transport whenever the kernel already owns the TPM: a discrete chip bound to a kernel driver, a Windows-style firmware TPM, or a TEE-resident firmware TPM such as the one on NVIDIA Jetson platforms.
With --enable-devtpm there is no TIS layer and no HAL IO callback: hal/tpm_io.c is compiled out entirely and TPM2_IoCb is NULL (see hal/tpm_io.h), so pass NULL for the callback argument of TPM2_Init / wolfTPM2_Init.
With --enable-autodetect this is not the case. The TIS/SPI HAL stays compiled in on purpose - it is the fallback - and TPM2_IoCb is a real function. Keep passing it, or the SPI fallback that build exists to provide is unreachable.
Two device nodes
The kernel presents up to two nodes per TPM:
/dev/tpm0- the raw device. One user at a time, no resource management. Whatever you send reaches the TPM./dev/tpmrm0- the in-kernel resource manager (kernel 4.12+, practical from 5.12+). It virtualizes handles, swaps transient objects and sessions in and out as needed, and flushes everything belonging to a connection when that connection closes.
wolfTPM prefers /dev/tpmrm0 and falls back to /dev/tpm0. The resource manager is the better default: a TPM has very few transient object slots, and without it a program that leaks a handle wedges the TPM for everything else on the system.
Build-time overrides, honored by both --enable-devtpm and --enable-autodetect:
-DWOLFTPM_USE_TPMRM- use/dev/tpmrm0only, with no fallback to the raw device.CFLAGS='-DTPM2_LINUX_DEV="/dev/tpm1"'- use a specific node. The inner quotes are required: the macro is used directly as a C string literal, so an unquoted value does not compile.
Startup, shutdown, and shared state
The TPM is started by firmware long before Linux runs, and on the resource manager it is shared with every other process on the system. Restarting or shutting it down is therefore not an individual caller's decision, so wolfTPM stays out of the way on this transport:
wolfTPM2_Initskips the startup and self-test sequence.wolfTPM2_ResetandwolfTPM2_Shutdownsend no TPM command and returnNOT_COMPILED_IN(-174), the same waywolfTPM2_SetLocalitydoes on this transport. AwolfTPM2_Reset(dev, 0, 0)that asked for neither a shutdown nor a startup still returnsTPM_RC_SUCCESS, since nothing was declined. TreatNOT_COMPILED_INhere as "the OS owns this", not as a failure.wolfTPM2_SetLocalityreturnsNOT_COMPILED_IN- the kernel owns the locality.
Be aware that the kernel does not reliably stop you here. Command filtering on /dev/tpmrm0 is primarily about handle isolation, not about blocking global state changes, and behavior varies by kernel version and TPM implementation. On Linux 5.15 with the Jetson OP-TEE fTPM, a TPM2_Shutdown(TPM_SU_CLEAR) sent through the resource manager is passed straight through and returns success - both from wolfTPM and from tpm2_shutdown. So this is a case where the library declining to send the command is what protects other users of the TPM, rather than the kernel doing it for you.
If you genuinely need to control TPM startup state, you need /dev/tpm0 and exclusive use of the TPM, or direct SPI access with wolfTPM's own TIS driver.
What the native API does on autodetect builds
Two behaviors worth knowing if you use TPM2_Init / TPM2_Init_ex directly rather than the wolfTPM2_* wrapper.
The kernel device wins over your callback. If /dev/tpmrm0 or /dev/tpm0 opens, every command is routed there and the HAL IO callback you passed is never invoked. On a host that has both a kernel-bound TPM and a discrete SPI part, that means you now talk to a different TPM than a pre-autodetect build did. Pin the part you want with --enable-devtpm, --enable-spi / --enable-<vendor>, or -DTPM2_LINUX_DEV.
Init now acquires a descriptor. TPM2_Init* opens the device on autodetect builds, and TPM2_Cleanup() is what closes it. Native callers that skipped cleanup previously leaked nothing; now they leak a descriptor per context. This matters most on hosts exposing only the raw /dev/tpm0, which permits a single open - a context that merely initialized holds the TPM exclusively for its lifetime, and a second context in the same process falls through to a different transport.
TPM2_Init_minimal() is unaffected: it performs no IO and still succeeds with no device present.
Transient handles do not outlive a process
This is the difference most likely to break an existing application.
On /dev/tpmrm0 the kernel gives each open file description its own handle space. Transient object handles are virtualized - the value the TPM assigned is not the value you get back - and everything in that space is flushed when the file descriptor closes. So a transient key created by one process is gone by the time a second process runs, and the handle number it printed is meaningless to anyone else.
Creating a primary key on the Jetson fTPM through the resource manager returns:
Create Primary Handle: 0x80ffffff
not the 0x80000000 a raw device would report. Query the transient handles from a separate process afterwards and the list is empty:
tpm2_getcap handles-transient # no output - the space was torn down
Two practical consequences:
- A "create a key, keep it, use it from the next command" workflow does not work across processes. Do the whole sequence in one process, or make the object persistent with
TPM2_EvictControlso it gets a stable0x81xxxxxxhandle that does survive. - Passing a hard-coded transient handle such as
0x80000000on a command line will fail. The kernel rejects the reference before it reaches the TPM, and because that happens at the file-descriptor layer the error surfaces as anerrno 22 = Invalid argumentonread(), which wolfTPM reports asTPM_RC_FAILURErather than as a handle error. If you seeTPM_RC_FAILUREalongsideFailed to read from /dev/tpmrm0 ... errno 22, suspect a stale or cross-process transient handle before suspecting the TPM.
wolfTPM's own examples/run_examples.sh hits exactly this: its provisioning section creates IAK and IDevID primaries with -keep in one process and then references 0x80000000 / 0x80000001 from another. That block cannot pass on the resource manager by construction. Everything either side of it is unaffected. Use /dev/tpm0 with exclusive access if you need to run it as written.
Building
./autogen.sh
./configure --enable-devtpm
make
--enable-devtpm uses the kernel node only. Use --enable-autodetect instead if you want wolfTPM to try /dev/tpmrm0, then /dev/tpm0, and finally fall back to probing SPI - useful for one binary that has to run on several boards.
Only one transport can be enabled at a time. --enable-devtpm conflicts with --enable-swtpm and --enable-winapi, and configure will stop if you ask for more than one.
The x86_64 / aarch64 default
A bare ./configure on Linux x86_64 or aarch64 does not produce a build that talks to /dev/tpmX. On those hosts wolfTPM auto-enables the software TPMs (swTPM and fwTPM) so that make check passes with no hardware attached, and defining WOLFTPM_SWTPM suppresses the kernel-device autodetect path. The result talks to a simulator on TCP port 2321.
Selecting any hardware path explicitly turns that default back off - --enable-autodetect, --enable-devtpm, or any --enable-<vendor>. Configure prints a notice when the software default is taken, so check the tail of its output if a build unexpectedly fails to find your TPM.
This bites hardest on single-board aarch64 machines with a firmware TPM, where the kernel device is the only transport there is.
Permissions
The TPM character devices are not world-accessible. On a typical system they are mode 0660 owned by group tss:
crw-rw---- 1 tss root 10, 224 /dev/tpm0
crw-rw---- 1 tss tss 252, 65536 /dev/tpmrm0
wolfTPM detects EACCES and reports it plainly:
Permission denied on /dev/tpm0
Use sudo or add tss group to user.
The fix is to put your user in the owning group and start a new login session:
sudo usermod -aG tss $USER
Note that the tss group is created by tpm2-tss, and on distributions that ship it the group frequently exists with no members - so this step is required even though the group looks correctly set up.
To use a group of your own instead, add a udev rule:
- Create the group and add your user:
sudo addgroup wolftpm
sudo adduser [username] wolftpm
- Create
/etc/udev/rules.d/wolftpm-udev.rulescontaining:
KERNEL=="tpm[0-9]*", TAG+="systemd", MODE="0660", GROUP="wolftpm"
- Reload the rules:
sudo udevadm control -R, then re-plug or reboot.
NVIDIA Jetson Orin (Tegra234) firmware TPM
Jetson Orin platforms carry a TPM 2.0 implemented in firmware, running as a trusted application inside OP-TEE rather than as a discrete package on a bus. Linux reaches it through the tpm_ftpm_tee driver, which speaks to the TA over the TEE interface and registers an ordinary TPM chip - so from wolfTPM's point of view it is just another /dev/tpmrm0.
Confirm the device is present before building:
lsmod | grep tpm_ftpm_tee
ls -l /dev/tpm*
cat /sys/class/tpm/tpm0/tpm_version_major # expect 2
If the module is missing, try sudo modprobe tpm_ftpm_tee and check that the kernel was configured with CONFIG_TCG_FTPM_TEE. On NVIDIA's Jetson Linux (L4T) images the driver is present and an fTPM Device Provisioning Service systemd unit runs at boot; you can see it complete in the boot log.
Note that an OP-TEE boot message about silicon-identity fTPM provisioning not being enabled refers to a separate NVIDIA feature and does not mean the TPM 2.0 device is unavailable.
Build as above with --enable-devtpm or --enable-autodetect, then confirm with:
./examples/wrap/caps
Because this is a firmware TPM, expect two differences from a discrete part. There is no TIS bus, so the TPM2: Caps/Did/Vid/Rid values do not exist and the device is identified purely from TPM2_GetCapability properties. Under --enable-devtpm the DEBUG_WOLFTPM line is still printed but reads all zeros; under --enable-autodetect wolfTPM2_Init_ex returns as soon as the kernel device opens, before that printf, so the line is absent entirely. And a firmware TPM's algorithm coverage is set by its firmware build rather than by a datasheet, so it is worth checking rather than assuming; where an operation is absent the benchmark reports it as unsupported rather than failing. The Jetson Orin fTPM supports every operation the benchmark exercises - see the README results.
See the main README.md for this platform's identification values and benchmark results.
Testing
The examples run unchanged on this transport:
./examples/wrap/caps
./examples/native/native_test
./examples/wrap/wrap_test
./examples/bench/bench
./examples/run_examples.sh
run_examples.sh already skips the locality test on backends that do not support it.
CI coverage
Both --enable-devtpm and --enable-autodetect are build-tested in CI, but not run - GitHub-hosted runners have no /dev/tpm* node. Runtime coverage of this transport requires a self-hosted runner with a real TPM bound to the kernel driver.