Interfacing with OP-TEE
Overview
NuttX supports basic interfacing with OP-TEE OS through three different
transports: local network, RPMsg, and native Secure Monitor Calls (SMCs)
on arm. Tasks can interface with the OP-TEE driver (and in turn with the
OP-TEE OS) via IOCTLs on the TEE (/dev/tee#
) character device. This
interface should allow use-of/integration-with libteec, although this is
not officially supported by NuttX, and is out of the scope of this guide.
The driver supports opening and closing sessions, allocating and registering shared memory, and invoking functions on OP-TEE Trusted Applications (TAs). It does not (yet) support reverse direction commands (TA -> Normal World) to something like a TEE supplicant.
Enabling the OP-TEE Driver
The driver is enabled using one of:
CONFIG_DEV_OPTEE_LOCAL
CONFIG_DEV_OPTEE_RPMSG
CONFIG_DEV_OPTEE_SMC
All of the above require also CONFIG_ALLOW_BSD_COMPONENTS
and
CONFIG_LIBC_MEMFD_SHMFS
, which in turn requires CONFIG_FS_SHMFS
. So,
at a bare minimum, to enable the driver one would need something like the
following:
CONFIG_ALLOW_BSD_COMPONENTS=y
CONFIG_DEV_OPTEE_SMC=y
CONFIG_FS_SHMFS=y
CONFIG_LIBC_MEMFD_SHMFS=y
Each implementation (local, RPMsg, or SMC) may have further dependencies
(e.g. RPMsg requires CONFIG_NET_RPMSG
and more) and may have further
parameters to configure (e.g. RPMsg remote CPU name through
CONFIG_OPTEE_REMOTE_CPU_NAME
).
Warning
CONFIG_DEV_OPTEE_SMC
has only been tested on arm64. Also, please note
that in configurations with CONFIG_ARM*_DCACHE_DISABLE=y
you might
encounter issues with shared memory depending on the state of the data
cache in Secure World.
Successful registration of the driver can be verified by looking into
/dev/tee0
. For instance, incompatibility with the TEE OS running in the
system, will prevent the /dev/tee0
character device from being
registered.
IOCTLs supported
All IOCTLs return negative error codes on failure. All of them return 0
on success unless otherwise specified (see TEE_IOC_SHM_ALLOC
).
TEE_IOC_VERSION
: Query the version and capabilities of the TEE driver.Use the
struct tee_ioctl_version_data
to get the version and capabilities. This driver supports OP-TEE so you should expect to receive onlyTEE_IMPL_ID_OPTEE
in.impl_id
andTEE_OPTEE_CAP_TZ
in.impl_caps
. The driver is GlobalPlatform compliant, and you should always expect to receiveTEE_GEN_CAP_GP | TEE_GEN_CAP_MEMREF_NULL
in.gen_caps
. If using the SMC implementation, the driver supports also shared memory registration, so you can expect alsoTEE_GEN_CAP_REG_MEM
in.gen_caps
.
TEE_IOC_OPEN_SESSION
: Open a session with a Trusted Application.Expects a
struct tee_ioctl_buf_data
pointer, pointing to astruct tee_ioctl_open_session_arg
instance with at minimum, the.uuid
set. You can typically useuuid_enc_be()
to encode auuid_t
struct to the raw byte buffer expected in the.uuid
field. After a successful call, you can expect to get a session identifier back in the.session
field.
TEE_IOC_CLOSE_SESSION
: Close a session with a Trusted Application.Expects a pointer to a
struct tee_ioctl_close_session_arg
with the.session
field set to the identifier of the session to close.
TEE_IOC_INVOKE
: Invoke a function on a previously opened session to a Trusted Application.Expects a
struct tee_ioctl_buf_data
pointer, pointing to astruct tee_ioctl_invoke_arg
instance. You can use theTEE_IOCTL_PARAM_SIZE()
macro to calculate the size of the variable-length array ofstruct tee_ioctl_param
parameters in the invoke arguments struct. At minimum, the interface expects the fields.func
,.session
,.num_params
, and.params
to be set..cancel_id
can be optionally set to enable later canceling of this command if needed. You might notice thatstruct tee_ioctl_param
has rather obscure field names (.a
,.b
,.c
). This can be improved with a union in the future, but until then, please refer toinclude/nuttx/tee.h
for details. In short, for shared memory references,.a
is the offset into the shared memory buffer,.b
is the size of the buffer, and.c
is the the shared memory identifier (.addr
field used inTEE_IOC_SHM_REGISTER
).
TEE_IOC_CANCEL
: Cancel a currently invoked command.Expects a
struct tee_ioctl_cancel_arg
pointer with the.session
and.cancel_id
fields set.
TEE_IOC_SHM_ALLOC
: Allocate shared memory between the user space and the secure OS.Expects a
struct tee_ioctl_shm_alloc_data
pointer with the.size
field set, and ignoring the.flags
field. Upon successful return, it returns the memory file descriptor one can usemmap()
on (withMAP_SHARED
). It also returns an identifier for the allocation in.id
.
TEE_IOC_SHM_REGISTER
: Register a shared memory reference with the secure OS.Expects a pointer to a
struct tee_ioctl_shm_register_data
instance with all fields set except.id
..flags
can be any combination ofTEE_SHM_REGISTER
andTEE_SHM_SEC_REGISTER
but notTEE_SHM_ALLOC
.TEE_SHM_REGISTER
registers the memory with the driver for automatic cleanup (not freeing!) during/dev/tee#
character device close.TEE_SHM_SEC_REGISTER
registers the memory with the secure OS for later use in memrefs and is automatically de-registered during driver close ifTEE_SHM_REGISTER
is also specified..addr
shall point to the (user) memory to register and.size
shall indicate its size. The identifier used to register shared memory with the secure OS is the value of the.addr
field (what one needs to specify intee_ioctl_param.c
during aTEE_IOC_INVOKE
).
Typical usage
Include the necessary headers:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <nuttx/tee.h> #include <uuid.h>
Open the TEE character device
int fd = open("/dev/tee0", O_RDONLY | O_NONBLOCK);
Check the version and capabilities
struct tee_ioctl_version_data ioc_ver; int ret = ioctl(fd, TEE_IOC_VERSION, (unsigned long)&ioc_ver); if (ret < 0) { printf("Failed to query TEE driver version and caps: %d, %s\n", ret, strerror(errno)); return ret; } [...check ioc_ver accordingly...]
Open a session with a Trusted Application
const uuid_t *uuid = [...]; struct tee_ioctl_open_session_arg ioc_opn = { 0 }; struct tee_ioctl_buf_data ioc_buf; uuid_enc_be(&ioc_opn.uuid, uuid); ioc_buf.buf_ptr = (uintptr_t)&ioc_opn; ioc_buf.buf_len = sizeof(struct tee_ioctl_open_session_arg); ret = ioctl(fd, TEE_IOC_OPEN_SESSION, (unsigned long)&ioc_buf); if (ret < 0) { return ret; } [...use ioc_opn.session returned...]
Invoke a function of the Trusted Application
const size_t num_params = 1; struct tee_ioctl_invoke_arg *ioc_args; struct tee_ioctl_buf_data ioc_buf; size_t ioc_args_len; ioc_args_len = sizeof(struct tee_ioctl_invoke_arg) + TEE_IOCTL_PARAM_SIZE(num_params); ioc_args = (struct tee_ioctl_invoke_arg *)calloc(1, ioc_args_len); if (!ioc_args) { return -ENOMEM; } ioc_args->func = <SOME_FUNCTION_ID>; ioc_args->session = ioc_opn.session; ioc_args->num_params = num_params; ioc_args->params[0].attr = TEE_IOCTL_PARAM_ATTR_TYPE_MEMREF_OUTPUT; ioc_buf.buf_ptr = (uintptr_t)ioc_args; ioc_buf.buf_len = ioc_args_len; ret = ioctl(fd, TEE_IOC_INVOKE, (unsigned long)&ioc_buf); if (ret < 0) { goto err_with_args; } [...use result (if any) in ioc_args->params...]
Allocate shared memory through the driver
struct tee_ioctl_shm_alloc_data ioc_alloc = { 0 }; int memfd; void *shm; ioc_alloc.size = 1024; memfd = ioctl(fd, TEE_IOC_SHM_ALLOC, (unsigned long)&ioc_alloc); if (memfd < 0) { return memfd; } shm = mmap(NULL, ioc_alloc.size, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0); if (shm == MAP_FAILED) { close(memfd); return -ENOMEM; }
Register shared memory with the driver and the secure OS
/* The following will fail if TEE_GEN_CAP_REG_MEM is not reported in * the returned `ioc_ver.gen_caps` in step 1 above */ struct tee_ioctl_shm_register_data ioc_reg = { 0 }; ioc_reg.addr = (uintptr_t)ptr; ioc_reg.length = ioc_alloc.size; ioc_reg.flags = TEE_SHM_REGISTER | TEE_SHM_SEC_REGISTER; ret = ioctl(fd, TEE_IOC_SHM_REGISTER, (unsigned long)&ioc_reg); if (ret < 0) { munmap(shm, ioc_alloc.size); close(memfd); return ret; }
Use the registered shared memory in an invocation
const size_t num_params = 1; struct tee_ioctl_invoke_arg *ioc_args; struct tee_ioctl_buf_data ioc_buf; size_t ioc_args_len; ioc_args_len = sizeof(struct tee_ioctl_invoke_arg) + TEE_IOCTL_PARAM_SIZE(num_params); ioc_args = (struct tee_ioctl_invoke_arg *)calloc(1, ioc_args_len); if (!ioc_args) { return -ENOMEM; } ioc_args->func = <SOME_FUNCTION_ID>; ioc_args->session = ioc_opn.session; ioc_args->num_params = num_params; ioc_args->params[0].attr = TEE_IOCTL_PARAM_ATTR_TYPE_MEMREF_OUTPUT; ioc_args->params[0].a = 0; ioc_args->params[0].b = ioc_alloc.size; ioc_args->params[0].a = (uintptr_t)shm; ioc_buf.buf_ptr = (uintptr_t)ioc_args; ioc_buf.buf_len = ioc_args_len; ret = ioctl(fd, TEE_IOC_INVOKE, (unsigned long)&ioc_buf); if (ret < 0) { goto err_with_args; } [...use result (if any) in ioc_args->params...]