Repository navigation
Add NVIDIA Jetson Orin fwTPM OP-TEE trusted application port #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dgarske
wants to merge
1
commit into
wolfSSL:main
Choose a base branch
from
dgarske:jetson_optee
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,448
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # Created by ta/prep.sh | ||
| ta/wc/ | ||
| ta/wt/ | ||
| ta/port/ | ||
| ta/inc-cfg | ||
| ta/inc-wolfssl | ||
| ta/inc-wolftpm | ||
| ta/uuid.mk | ||
| ta/uuid_macro.inc | ||
|
|
||
| # OP-TEE TA build output | ||
| ta/*.ta | ||
| ta/*.elf | ||
| ta/*.dmp | ||
| ta/*.map | ||
| ta/*.o | ||
| ta/.*.cmd | ||
| ta/.*.d | ||
| ta/dyn_list | ||
| ta/ta.lds | ||
|
|
||
| native/fwtpm_native | ||
| host/fwtpm_client |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,331 @@ | ||
| # fwTPM on the NVIDIA Jetson Orin, as an OP-TEE trusted application | ||
|
|
||
| The wolfTPM firmware TPM 2.0 running in the secure world on a Jetson Orin, as | ||
| an OP-TEE trusted application, with unmodified wolfTPM examples driving it | ||
| from Linux over `libteec`. | ||
|
|
||
| The Jetson already has a firmware TPM: NVIDIA ships Microsoft's TPM 2.0 | ||
| reference as an early trusted application, reached through `tpm_ftpm_tee` as | ||
| `/dev/tpmrm0`. It reports **TPM 2.0 spec revision 1.62**, and ML-DSA | ||
| (FIPS 204) and ML-KEM (FIPS 203) arrive in revision **1.85**, so it has no | ||
| post-quantum support until NVIDIA ships new firmware. This port puts a 1.85 | ||
| TPM on the same silicon alongside it. | ||
|
|
||
| ## Tested target | ||
|
|
||
| | Board | Status | Date | | ||
| |-------|--------|------| | ||
| | Jetson Orin Nano Engineering Reference Developer Kit Super, L4T R36.4.4, kernel 5.15.148-tegra | `Mfg WOLF (0), Vendor wolfTPM`, `TPM_PT_REVISION` 185. `caps`, `getrandom`, `native_test`, `wrap_test`, `pqc/mldsa_sign`, `pqc/mlkem_encap` and both PQC negative-control examples pass. NV persists across processes. | 2026-10-28 | | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| - `aarch64-linux-gnu-gcc`. Nothing else is needed on the build host. | ||
| - An OP-TEE TA devkit (`export-ta_arm64`), optee_os 4.4.0 verified. The arm64 | ||
| TA devkit targets the TEE Internal Core API, so the platform it was built | ||
| for does not matter for an ordinary trusted application. | ||
| - wolfTPM with `WOLFTPM_OPTEE` support, which supplies | ||
| `src/fwtpm/ports/optee/` and `wolftpm/fwtpm/fwtpm_optee.h`. | ||
| - wolfSSL with ML-DSA and ML-KEM (`--enable-mldsa --enable-mlkem | ||
| --enable-experimental`), and with the `WOLFSSL_OPTEE` heap and RNG support, | ||
| which supplies the allocator and entropy so this port needs no shims. Only | ||
| the sources are used for the trusted application; the install is for the | ||
| Linux-side examples. | ||
| - On the board, `nvidia-l4t-optee`, which ships `tee_client_api.h` and | ||
| `libteec`, so the client builds natively there. | ||
|
|
||
| `/dev/tee0` and `/dev/teepriv0` are mode `0600` and root owned, so **every | ||
| client runs under sudo**. | ||
|
|
||
| ## Signing | ||
|
|
||
| A filesystem-loaded trusted application has to be signed with a key the | ||
| platform's OP-TEE core trusts. Check what your image uses, against a trusted | ||
| application it already ships, before building anything: | ||
|
|
||
| ```sh | ||
| python3 scripts/sign_encrypt.py verify \ | ||
| --key keys/default_ta.pem \ | ||
| --uuid 5b9e0e40-2636-11e1-ad9e-0002a5d5c51b \ | ||
| --in 5b9e0e40-2636-11e1-ad9e-0002a5d5c51b.ta | ||
| ``` | ||
|
|
||
| Exit 0 means that key. **Run the negative control too**, or exit 0 tells you | ||
| nothing: repeat with a freshly generated key (`openssl genrsa -out wrong.pem | ||
| 2048`) and confirm it reports a verification failure. | ||
|
|
||
| On L4T R36.4.4 the shipped trusted applications verify against upstream | ||
| OP-TEE's `keys/default_ta.pem`, so the devkit's default key works unchanged. | ||
|
|
||
| ## Build and run | ||
|
|
||
| ```sh | ||
| export WOLFSSL=../../../../wolfssl | ||
| export WOLFTPM=../../../../wolftpm | ||
| export TA_DEV_KIT_DIR=/path/to/optee_os/out/.../export-ta_arm64 | ||
| ``` | ||
|
|
||
| ### 1. Build and sign the trusted application | ||
|
|
||
| ```sh | ||
| cd ta && WOLFSSL=$WOLFSSL WOLFTPM=$WOLFTPM ./prep.sh | ||
| make TA_DEV_KIT_DIR=$TA_DEV_KIT_DIR CROSS_COMPILE=aarch64-linux-gnu- | ||
| ``` | ||
|
|
||
| `prep.sh` symlinks the wolfCrypt, fwTPM and OP-TEE port sources plus the three | ||
| include roots into the build directory, and generates `uuid.mk` and | ||
| `uuid_macro.inc` from wolfTPM. The OP-TEE build resolves every `srcs-y` and | ||
| `global-incdirs-y` entry against the `sub.mk` directory, so nothing outside it | ||
| can be named directly. Re-run `prep.sh` after moving either tree. Signing is | ||
| automatic and uses the devkit key. | ||
|
|
||
| Configuration is one `user_settings.h` for both wolfCrypt and wolfTPM, reached | ||
| through `WOLFSSL_USER_SETTINGS` and `WOLFTPM_USER_SETTINGS`. There is no | ||
| generated `options.h` here, and that is also what keeps an installed | ||
| `/usr/local/include/wolftpm/options.h` out of the build. | ||
|
|
||
| ### 2. Deploy | ||
|
|
||
| ```sh | ||
| scp 9013be0e-39b0-4ed4-80e0-1242c0daf382.ta board:/tmp/ | ||
| ssh board "sudo install -m 0644 -o root -g root \ | ||
| /tmp/9013be0e-39b0-4ed4-80e0-1242c0daf382.ta /lib/optee_armtz/" | ||
| ``` | ||
|
|
||
| No service restart: `tee-supplicant` reads `/lib/optee_armtz/` per request. | ||
|
|
||
| **Before installing, run the client once and confirm it fails with | ||
| `TEEC_OpenSession 0xffff0008` (`TEEC_ERROR_ITEM_NOT_FOUND`), origin 3.** | ||
| Otherwise a later success may be a stale trusted application rather than the | ||
| one you just built. | ||
|
|
||
| ### 3. Drive it | ||
|
|
||
| The minimal `libteec` client, built on the board: | ||
|
|
||
| ```sh | ||
| make -C host && sudo ./host/fwtpm_client | ||
| ``` | ||
|
|
||
| Or unmodified wolfTPM examples, which is the point of the port: | ||
|
|
||
| ```sh | ||
| cd wolftpm | ||
| ./configure --enable-optee --disable-swtpm --enable-v185 \ | ||
| CFLAGS="-I$WOLFSSL_PREFIX/include" LDFLAGS="-L$WOLFSSL_PREFIX/lib" | ||
| make | ||
| sudo ./examples/wrap/caps | ||
| sudo ./examples/pqc/mldsa_sign | ||
| ``` | ||
|
|
||
| `--disable-swtpm` is required: the software TPM is the default on Linux | ||
| aarch64 and would win. Build the client **without** `--enable-fwtpm`; the | ||
| trusted application is the server. | ||
|
|
||
| ## The trusted application's UUID | ||
|
|
||
| OP-TEE identifies a trusted application by UUID, not by name, and the UUID is | ||
| not cosmetic: the TEE loads a filesystem trusted application from | ||
| `/lib/optee_armtz/<uuid>.ta`, and a client opens a session by UUID. So the | ||
| same value has to appear in three places that are built separately, and a | ||
| mismatch shows up only at run time as `TEEC_OpenSession` returning | ||
| `0xffff0008` (`TEEC_ERROR_ITEM_NOT_FOUND`). | ||
|
|
||
| There is one definition, `WOLFTPM_OPTEE_TA_UUID_STR` in wolfTPM's | ||
| `wolftpm/fwtpm/fwtpm_optee.h`, and everything here derives from it: | ||
|
|
||
| | Where | How it gets the UUID | | ||
| |-------|----------------------| | ||
| | `TA_UUID` in the trusted application | `ta/uuid_macro.inc`, generated by `prep.sh` | | ||
| | the signed binary's filename | `BINARY` in `ta/Makefile`, from `ta/uuid.mk`, generated by `prep.sh` | | ||
| | the client | `WOLFTPM_OPTEE_TA_UUID` in `wolftpm/tpm2_optee.h`, built from the same macros | | ||
|
|
||
| `ta/uuid.mk` and `ta/uuid_macro.inc` are generated, not checked in. This is | ||
| the UUID wolfTPM's fwTPM trusted application uses, so a stock client and a | ||
| stock trusted application interoperate with no configuration. To ship your own | ||
| identity, override `WOLFTPM_OPTEE_TA_UUID_STR` and the `_P1` to `_P4` macros | ||
| beside it; all three places then follow. | ||
|
|
||
| ## Developing or porting this | ||
|
|
||
| `native/` compiles the same source list with the same `user_settings.h` for | ||
| the host and drives `FWTPM_ProcessCommand` through a RAM NV HAL: | ||
|
|
||
| ```sh | ||
| make -C native && ./native/fwtpm_native | ||
| ``` | ||
|
|
||
| Nothing about running the fwTPM on a board needs it. It is for changing the | ||
| port: a configuration or source-list mistake surfaces in seconds there | ||
| instead of after a cross-compile, copy and deploy cycle, and it runs under a | ||
| host debugger and the sanitizers. | ||
|
|
||
| ## How it works | ||
|
|
||
| `FWTPM_ProcessCommand` takes a command buffer and returns a response buffer, | ||
| which is exactly the shape of `TA_InvokeCommandEntryPoint`, so one | ||
| `TEEC_InvokeCommand` carries one whole TPM command. There is no TIS register | ||
| emulation, which would cost a world switch per register access. | ||
|
|
||
| `FWTPM_IO_ServerLoop` and `FWTPM_TIS_ServerLoop` both block, which a trusted | ||
| application cannot do, so `fwtpm_io.c`, `fwtpm_tis.c`, `fwtpm_tis_shm.c` and | ||
| `fwtpm_main.c` are not part of this build. | ||
|
|
||
| NV lives in OP-TEE secure storage in the journal's default byte-addressable | ||
| mode; the append-only option is for write-once flash and is not used here. The | ||
| journal integrity key is derived from the hardware unique key through the | ||
| system PTA, falling back to a random key in its own storage object on | ||
| platforms built without that PTA. | ||
|
|
||
| `locality` is fixed and never taken from the client, because it is stored | ||
| verbatim into the command context without validation. | ||
|
|
||
| ## Sizes | ||
|
|
||
| Measured on this board, bounded from both sides, acceptance being | ||
| `native_test` plus `pqc/mldsa_sign`: | ||
|
|
||
| | Axis | Passes | Fails | | ||
| |------|--------|-------| | ||
| | `TA_DATA_SIZE` (heap) | 96 KB | 88 KB | | ||
| | `TA_STACK_SIZE` | 8 KB | 4 KB | | ||
|
|
||
| ML-DSA is the binding constraint: at 64 KB of heap `native_test` still passes | ||
| and only ML-DSA fails. The acceptance test signs with ML-DSA-65, so ML-DSA-87 | ||
| needs more. | ||
|
|
||
| `FWTPM_CTX` is a 161 KB static in `.bss` and does not come out of this heap, | ||
| which is why a 96 KB heap works at all. The trusted application load ceiling | ||
| on this board is between 47 and 48 MB. | ||
|
|
||
| Shipping defaults are 32 KB of stack and 512 KB of heap. `sweep.sh` re-measures | ||
| on another board; sweep until something fails, or you have not found a floor. | ||
|
|
||
| The heap and stack floors are the same with the assembly in or out, because | ||
| ML-DSA binds the heap and the single-precision code allocates its temporaries | ||
| rather than using stack. | ||
|
|
||
| ### Assembly, and what it costs | ||
|
|
||
| Both are on by default on aarch64 and each has an opt-out, so a target that | ||
| wants a smaller image or lacks the instructions can drop either: | ||
|
|
||
| | Build | `.text` | `.rodata` | TA image | | ||
| |-------|---------|-----------|----------| | ||
| | `FWTPM_TA_NO_SP_ASM` and `FWTPM_TA_NO_ARMASM` | 480624 | 34241 | 604872 | | ||
| | `FWTPM_TA_NO_ARMASM` only | 586772 | 530361 | 1215176 | | ||
| | default, both enabled | 641120 | 532841 | 1272600 | | ||
|
|
||
| The single-precision assembly is what costs the space, and nearly all of it is | ||
| `.rodata`: the ECC precomputed base-point tables, plus RSA 4096 and P-384 | ||
| support. It buys RSA-2048 private 10.9x and ECDSA P-256 sign 46x. The ARMv8 | ||
| crypto extensions add 4.7% for ML-KEM encapsulate +32% and decapsulate +48%, | ||
| ECC P-256 key generation +38%, AES +15% and SHA-256 +8%. | ||
|
|
||
| ML-DSA gains nothing measurable from either: there is no aarch64 ML-DSA | ||
| polynomial assembly, and Keccak is not where its time goes. | ||
|
|
||
| `FWTPM_TA_NO_ARMASM` also drops the `-march=armv8.2-a+crypto` requirement in | ||
| `ta/sub.mk`, which exists because the ML-KEM transform uses SQRDMLSH from | ||
| FEAT_RDM and the devkit names no architecture. | ||
|
|
||
| ## Benchmarks | ||
|
|
||
| `examples/bench/bench`, same tool and board as the vendor fTPM figures, second | ||
| run kept so the first pays the trusted application load and the frequency ramp. | ||
|
|
||
| | Operation | This port | NVIDIA vendor fTPM | | ||
| |-----------|----------:|-------------------:| | ||
| | RSA-2048 key gen | 1043 ms | 736 ms | | ||
| | RSA-2048 public | 3038 ops/sec | 1144 ops/sec | | ||
| | RSA-2048 private | 56.8 ops/sec | 84 ops/sec | | ||
| | ECDSA P-256 sign | 204 ops/sec | 22.2 ops/sec | | ||
| | ECDSA P-256 verify | 316 ops/sec | 31.5 ops/sec | | ||
| | ECDHE P-256 agree | 104 ops/sec | 11.2 ops/sec | | ||
| | AES-256-CBC | 15.6 MB/s | 2.9 MB/s | | ||
| | SHA-256 | 7.79 MB/s | 1.57 MB/s | | ||
| | ML-DSA-65 sign | 1012 ops/sec | not supported | | ||
| | ML-DSA-65 verify | 2072 ops/sec | not supported | | ||
| | ML-KEM-768 encapsulate | 6956 ops/sec | not supported | | ||
| | ML-KEM-768 decapsulate | 4684 ops/sec | not supported | | ||
| | SHA-1 | not supported | supported | | ||
| | AES-CTR | not supported | supported | | ||
|
|
||
| Faster than the vendor part on ECC, AES and SHA-256, **slower on RSA private | ||
| and RSA key generation**, and it does not implement SHA-1 or AES-CTR where the | ||
| vendor fTPM skips nothing. | ||
|
|
||
| **`bench` key generation rows for ML-DSA and ML-KEM are not key generation.** | ||
| The loop calls `wolfTPM2_CreatePrimaryKey` on an unchanging template and the | ||
| fwTPM has a four-entry primary cache, so every iteration after the first is a | ||
| cache hit. `bench -iter=100` shows it: ML-DSA-65 key gen min 0.247 ms, max | ||
| 72.8 ms, while ECC P-256 key gen stays within 5.065-5.150 ms because it uses | ||
| `TPM2_Create` and is not cached. A first, uncached ML-DSA-65 `CreatePrimary` | ||
| costs roughly 73-90 ms, part of which is the NV journal write to secure | ||
| storage. | ||
|
|
||
| ## Reaching it as /dev/tpm1, with standard Linux tooling | ||
|
|
||
| The libteec client above is not the only way in. Linux's `tpm_ftpm_tee` driver | ||
| binds a TPM chip to a trusted application, so a copy of it carrying this | ||
| UUID presents the fwTPM as `/dev/tpm1` and `/dev/tpmrm1`, alongside the | ||
| vendor part on `/dev/tpm0`. `tpm2-tools` and anything else speaking the | ||
| kernel TPM interface then work unmodified. | ||
|
|
||
| The driver is GPL-2.0 and is not vendored here. `kernel/tpm_wolftpm_tee.patch` | ||
| is a diff against `drivers/char/tpm/tpm_ftpm_tee.c` from Linux v5.15. Build it | ||
| out of tree on the board, which has the kernel headers: | ||
|
|
||
| ```sh | ||
| mkdir -p ~/wolftpm-drv && cd ~/wolftpm-drv | ||
| # from a Linux v5.15 source tree, or fetch the three files directly | ||
| cp <linux>/drivers/char/tpm/tpm_ftpm_tee.c tpm_wolftpm_tee.c | ||
| cp <linux>/drivers/char/tpm/tpm_ftpm_tee.h tpm.h . | ||
| patch -p1 < <this port>/kernel/tpm_wolftpm_tee.patch | ||
| printf 'obj-m += tpm_wolftpm_tee.o\n' > Makefile | ||
| make -C /lib/modules/$(uname -r)/build M=$PWD modules | ||
| sudo insmod ./tpm_wolftpm_tee.ko | ||
| ``` | ||
|
|
||
| The patch does three things: points both UUID sites at this trusted | ||
| application, renames the driver and module so they coexist with the vendor | ||
| driver rather than fighting over its device tree node, and registers the | ||
| platform device itself, since this trusted application has no device tree | ||
| node of its own. | ||
|
|
||
| What works through the kernel path, confirmed with `tpm2-tools`: | ||
|
|
||
| | Command | `/dev/tpmrm1` | `/dev/tpm1` | | ||
| |---------|---------------|-------------| | ||
| | `tpm2_getcap properties-fixed` | pass, reports `WOLF` at revision 1.85 | pass | | ||
| | `tpm2_getrandom`, `tpm2_pcrread`, `tpm2_selftest` | pass | pass | | ||
| | `tpm2_createprimary` | **fails** | pass | | ||
|
|
||
| Creating a transient object through the resource manager fails because | ||
| `tpm2-space` flushes the handle afterwards and the fwTPM answers | ||
| `TPM_RC_HANDLE` (139). The raw device has no resource manager and is | ||
| unaffected. That is fwTPM behaviour rather than anything about OP-TEE, so the | ||
| raw device is the working path until it is addressed. | ||
|
|
||
| Two traps if you try this: | ||
|
|
||
| - Do not add `TA_FLAG_DEVICE_ENUM_TEE_STORAGE_PRIVATE` to get onto the Linux | ||
| TEE bus. The devkit defines it but the OP-TEE core in L4T R36.4.4 is older | ||
| and rejects the unknown flag, after which the trusted application stops | ||
| loading at all and every session returns `TEEC_ERROR_BAD_FORMAT`. | ||
| - The vendor fTPM does not reach Linux over the TEE bus either. It is a | ||
| platform device from a `microsoft,ftpm` device tree node, which is why the | ||
| patch registers a platform device rather than relying on enumeration. | ||
|
|
||
| ## Limits | ||
|
|
||
| - The trusted application has its own UUID and **cannot appear as | ||
| `/dev/tpm0`**. The vendor fTPM owns that UUID as an early trusted | ||
| application compiled into `tos.img`, and an early one always beats a | ||
| filesystem one. Use the `WOLFTPM_OPTEE` transport, not `tpm_ftpm_tee`. | ||
| - The vendor fTPM keeps working throughout; the two are independent TPMs on | ||
| one SoC with separate state. | ||
| - A firmware TPM shares the SoC with the software it attests. Where the threat | ||
| model includes physical possession, a discrete part remains a different | ||
| proposition. | ||
| - Single instance by design: one instance is one TPM, since a second would be | ||
| a second TPM sharing one NV journal. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Normal-world client, built on the board: nvidia-l4t-optee ships | ||
| # tee_client_api.h and libteec there, so no cross toolchain is needed. | ||
| # | ||
| # The shared command IDs come from the wolfTPM tree. | ||
| WOLFTPM ?= ../../../../wolftpm | ||
|
|
||
| CFLAGS += -Wall -Wextra -O2 -I$(WOLFTPM) | ||
| LDLIBS += -lteec | ||
|
|
||
| fwtpm_client: main.c | ||
| $(CC) $(CFLAGS) -o $@ main.c $(LDLIBS) | ||
|
|
||
| clean: | ||
| rm -f fwtpm_client | ||
|
|
||
| .PHONY: clean |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The patch has paths like
drivers/char/tpm/*, so this fails with-p1, it should be-p4.