Open-TEE Virtual Trusted Execution Environment

From RidgeRun Developer Wiki

Intro

Some ARM based family of devices supports trustzone execution via Open TEE. This is a open source effort to make a TEE api/project that can be used crossplatform extracting some complexity from a bare metal implementation.

Requirements

Provided by meta-arm/meta-arm/recipes-security

  • optee-os
  • optee-client
  • optee-os-tadevkit

This also needs to be extended by the meta layer of the target device manufacturer.

Kernel config

  • CONFIG_TEE=y
  • CONFIG_OPTEE=y

Enabled by default by the manufacturer and ARM recipes/layers

Project structure

A OpTEE project needs a set structure:

hello_world/
├── host
    ├── Makefile
    ├── hello_world.c
└── ta
    ├── Makefile                  BINARY=<uuid>
    ├── Android.mk                Android way to invoke the Makefile
    ├── sub.mk                    srcs-y += hello_world_ta.c
    ├── include
    │   └── hello_world_ta.h      Header exported to non-secure: TA commands API
    ├── hello_world_ta.c          Implementation of TA entry points
    └── user_ta_header_defines.h  TA_UUID, TA_FLAGS, TA_DATA/STACK_SIZE, ...

Mainly there are two sections the TA and the client secure app.

TA

The TA can be seen like a api where the main functionality and interactions with the secure world(TEE api) are. This app is compiled and has a unique UUID associated with it and as for the Jacinto J7, goes on the /lib/optee_armtz path. The client application uses is associated with the api via the UUID,It does not know the specifics of the api methods, only the param types it requires and the method types. For example, we have a do_stmg(param1, param2), defined on the api. The UUID is defined on hello_world_ta.h, like:

/*
 * This UUID is generated with uuidgen
 * the ITU-T UUID generator at http://www.itu.int/ITU-T/asn1/uuid.html
 */
#define TA_EXAMPLE_CUSTOM_UUID \
	{ 0x6ceb6fc1, 0x9e49, 0x41fb, \
		{ 0xa9, 0x63, 0x53, 0x6a, 0x65, 0x5f, 0x9a, 0x21} }

Regarding the api methods, the client does not knows about the name, and invokes it indirectly via TEEC_InvokeCommand, that takes the current opened tee session by the client, the method enum name, defined on the hello_world_ta.h, like:

#define TA_HELLO_WORLD 1

The invoke command goes to a defined entry point on the TA api, this method needs to be defined and has the control of how each method constant name maps to each actual method inside the api, for example:

TEE_Result TA_InvokeCommandEntryPoint(void __unused *session,
				      uint32_t command,
				      uint32_t param_types,
				      TEE_Param params[4])
{
	EMSG("TA_InvokeCommandEntryPoint super example tee app");
	switch (command) {
	case TA_TEE_EXAMPLE_CMD_DO_SMTG:
		return do_smtg(param_types, params);
	default:
		EMSG("Command ID 0x%x is not supported", command);
		return TEE_ERROR_NOT_SUPPORTED;
	}
}

This is a security feature on OPTee. The client never calls or uses directly anything that is defined on the TA api. It also needs the params for each call, but the params are predefined structures, they are not common c types.


typedef struct
{
 uint32_t a;
 uint32_t b;
} TEEC_Value;

typedef struct
{
 void* buffer;
 size_t size;
} TEEC_TempMemoryReference;

typedef struct
{
 TEEC_SharedMemory* parent;
 size_t size;
 size_t offset;
} TEEC_RegisteredMemoryReference;

typedef union
{
 TEEC_TempMemoryReference tmpref;
 TEEC_RegisteredMemoryReference memref;
 TEEC_Value value;
} TEEC_Parameter;

typedef struct
{
 uint32_t started;
 uint32_t paramTypes;
 TEEC_Parameter params[4];
 <Implementation-Defined Type> imp;
} TEEC_Operation;

The client API and the TA app use the TEEC_Operation as method signature for any of the methods that the api is exposing. And also the struct type for each param is controlled, and defined on the api method, using the paramTypes flag, with a combination of the following flags:

  • client app:
TEEC_NONE
TEEC_VALUE_INPUT
TEEC_VALUE_OUTPUT
TEEC_VALUE_INOUT
TEEC_MEMREF_TEMP_INPUT
TEEC_MEMREF_TEMP_OUTPUT
TEEC_MEMREF_TEMP_INOUT
TEEC_MEMREF_WHOLE
TEEC_MEMREF_PARTIAL_INPUT
TEEC_MEMREF_PARTIAL_OUTPUT
TEEC_MEMREF_PARTIAL_INOUT
</pre>
* api:
<pre>
TEE_PARAM_TYPE_NONE
TEE_PARAM_TYPE_VALUE_INPUT
TEE_PARAM_TYPE_VALUE_OUTPUT
TEE_PARAM_TYPE_VALUE_INOUT
TEE_PARAM_TYPE_MEMREF_TEMP_INPUT
TEE_PARAM_TYPE_MEMREF_TEMP_OUTPUT
TEE_PARAM_TYPE_MEMREF_TEMP_INOUT
TEE_PARAM_TYPE_MEMREF_WHOLE
TEE_PARAM_TYPE_MEMREF_PARTIAL_INPUT
TEE_PARAM_TYPE_MEMREF_PARTIAL_OUTPUT
TEE_PARAM_TYPE_MEMREF_PARTIAL_INOUT

The client defines the type of params and on each api method and the first thing is to check is for the param types:

static TEE_Result do_smtg(uint32_t param_types, TEE_Param params[4])
{
	const uint32_t exp_param_types =
		TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT,
				TEE_PARAM_TYPE_MEMREF_INPUT,
				TEE_PARAM_TYPE_NONE,
				TEE_PARAM_TYPE_NONE);
   if (param_types != exp_param_types)
		return TEE_ERROR_BAD_PARAMETERS;

This is one of the security checks that has to be done on a OPTee TA API. Another important thing that the TA api methods is that they do not use the param directly, they are always only to take the values in case of input types, and write values in the case of the output type. Always work with local variables, like:

    /* Input params */
	size_t obj_id_sz = params[0].memref.size;
	char* local_buff = TEE_Malloc(obj_id_sz, 0);
	if (!obj_id)
		return TEE_ERROR_OUT_OF_MEMORY;

	TEE_MemMove(local_buff, params[0].memref.buffer, obj_id_sz);
    /* Output params */
    /* do something to generate or operate on local_buff */
    size_t obj_out_sz = params[1].memref.size;
    TEE_MemMove(local_buff, &params[1].memref.buffer, obj_out_sz);

This way we can ensure all operations and sensible data is operated on the secure TA environment. Besides this, the TA api needs other mandatory methods:

TEE_Result TA_CreateEntryPoint(void)
{
	/* Nothing to do */
	return TEE_SUCCESS;
}

/*
 * Called when the instance of the TA is destroyed if the TA has not
 * crashed or panicked. This is the last call in the TA.
 */
void TA_DestroyEntryPoint(void)
{
	/* Nothing to do */
}

/*
 * Called when a new session is opened to the TA. *sess_ctx can be updated
 * with a value to be able to identify this session in subsequent calls to the
 * TA. In this function you will normally do the global initialization for the
 * TA.
 */
TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types,
		TEE_Param __maybe_unused params[4],
		void __maybe_unused **sess_ctx)
{
	/* Nothing to do */
	return TEE_SUCCESS;
}

/*
 * Called when a session is closed, sess_ctx hold the value that was
 * assigned by TA_OpenSessionEntryPoint().
 */
void TA_CloseSessionEntryPoint(void __maybe_unused *sess_ctx)
{
	/* Nothing to do */
}

That covers the most important notes about the TA api.

user_ta_header_defines.h

This is another necessary file, in here the UUID is added to the type OPTee wants and a really important definition is the stack and data memory allocations. The TA api cannot use more than that but also it cannot allocate big spaces, since people report issues with big allocations, like +1MB but also the dedicated space for TEE is not much, so its ideal that that value is tuned to each specific needs and only use whats needed and not more. Here is an example:

/*
 * The name of this file must not be modified
 */

#ifndef USER_TA_HEADER_DEFINES_H
#define USER_TA_HEADER_DEFINES_H

/* To get the TA UUID definition */
#include <example_custom_ta.h>

#define TA_UUID				TA_EXAMPLE_CUSTOM_UUID

/* application properties */
// TA_FLAG_SINGLE_INSTANCE, TA_FLAG_MULTI_SESSION and TA_FLAG_INSTANCE_KEEP_ALIVE
// TA_FLAG_USER_MODE, TA_FLAG_EXEC_DDR
#define TA_FLAGS			(TA_FLAG_EXEC_DDR | TA_FLAG_SINGLE_INSTANCE)


#define TA_STACK_SIZE			(2 * 1024)
#define TA_DATA_SIZE			(32 * 1024)

#define TA_CURRENT_TA_EXT_PROPERTIES \
    { "gp.ta.description", USER_TA_PROP_TYPE_STRING, \
        "Example of TA writing/reading data from its secure storage" }, \
    { "gp.ta.version", USER_TA_PROP_TYPE_U32, &(const uint32_t){ 0x0010 } }

#endif /* USER_TA_HEADER_DEFINES_H */

User app

This is the entry point, this the the executable that will consume the TA api and what will be executed as the entry point for the project. There is an important remark, that is that the application is responsible to create the secure session and has ownership over it, so when done it needs to close and destroy:

int main(void)
{
	TEEC_Result res;
	TEEC_Context ctx;
	TEEC_Session sess;
    uint32_t err_origin;
   TEEC_UUID uuid = TA_EXAMPLE_CUSTOM_UUID;

	/* Initialize a context connecting us to the TEE */
	res = TEEC_InitializeContext(NULL, &ctx);
	if (res != TEEC_SUCCESS)
		errx(1, "TEEC_InitializeContext failed with code 0x%x", res);

	/*
	 * Open a session to the "hello world" TA
	 */
	res = TEEC_OpenSession(&ctx, &sess, &uuid,
			       TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);
	if (res != TEEC_SUCCESS)
		errx(1, "TEEC_Opensession failed with code 0x%x origin 0x%x",
			res, err_origin);
	TEEC_CloseSession(&sess);

	TEEC_FinalizeContext(&ctx);

	return 0;
}

Note that the client knows the UUID of the TA api, and that is how it opens the session and connects to it. Note that nothing that is defined on the *ta.c is visible to the client api, that helps to further secure the application, and makes the usage of the TEEC_InvokeCommand, mandatory.

Secure storage

This is a way that OPTee applications can write persistent data and share it with other secure applications. This is a secure space to store data, the space is manager by the OPTee api/libs. To use it on our TA api we first create it with:

TEE_Result TEE_OpenPersistentObject(uint32_t storageID, void* objectID, size_t objectIDLen, uint32_t flags, TEE_ObjectHandle* object);
  • storageID: defines what storage to use the default values are TEE_STORAGE_PRIVATE or TEE_STORAGE_ILLEGAL_VALUE, other values are platform dependent.
  • objectID: The str name of the object.
  • objectIDLen: len of the str name.
  • flags, create flags:
    • TEE_DATA_FLAG_ACCESS_READ: The object is opened with the read access right. This allows the Trusted Application to call the function TEE_ReadObjectData.
    • TEE_DATA_FLAG_ACCESS_WRITE: The object is opened with the write access right. This allows the Trusted Application to call the functions TEE_WriteObjectData and
    • TEE_TruncateObjectData.
    • TEE_DATA_FLAG_ACCESS_WRITE_META: The object is opened with the write-meta access right. This allows the Trusted Application to call the functions
    • TEE_CloseAndDeletePersistentObject and TEE_RenamePersistentObject.
    • TEE_DATA_FLAG_SHARE_READ: The caller allows another handle on the object to be created with read access.
    • TEE_DATA_FLAG_SHARE_WRITE: The caller allows another handle on the object to be created with write access.
  • object the handle of the object

The other methods relates are similar to file operations, for example read, write and delete. To access and read it, first we need to open the resource with the read flags(TEE_DATA_FLAG_ACCESS_READ), then we use the following method to read 'size' number of bytes and move them to 'buffer', and at the end we will get the actual read bytes on the 'count'.

TEE_ReadObjectData(TEE_ObjectHandle object, void* buffer, size_t size, uint32_t* count);

To write is pretty similar, we need to open it first with the write flags(TEE_DATA_FLAG_ACCESS_READ | TEE_DATA_FLAG_ACCESS_WRITE), and then write to it:

TEE_WriteObjectData(TEE_ObjectHandle object, void* buffer, size_t size);

To delete it, we first need to open it using the correct flags(TEE_DATA_FLAG_ACCESS_READ | TEE_DATA_FLAG_ACCESS_WRITE_META) and then use:

TEE_CloseAndDeletePersistentObject1( TEE_ObjectHandle object );

To only close it after it has been opened, we use:

 TEE_CloseObject( TEE_ObjectHandle object );

Yocto

Here is an example recipe that builds a local repo and dumps the executable and *.ta on the build folder, this example is used for a TI device, so from the device to device it might vary.

SUMMARY = "OP-TEE example"
LICENSE = "CLOSED"
DEPENDS = "optee-client optee-os-tadevkit python3-cryptography-native"

inherit python3native
SRC_URI = "git:///home/tisdk/tisdk/sources/meta-example/recipes-security/optee/files/;protocol=file"

OPTEE_CLIENT_EXPORT = "${STAGING_DIR_HOST}${prefix}"
TEEC_EXPORT = "${STAGING_DIR_HOST}${prefix}"
TA_DEV_KIT_DIR = "${STAGING_INCDIR}/optee/export-user_ta"

EXTRA_OEMAKE += "TA_DEV_KIT_DIR=${TA_DEV_KIT_DIR} \
                 OPTEE_CLIENT_EXPORT=${OPTEE_CLIENT_EXPORT} \
                 TEEC_EXPORT=${TEEC_EXPORT} \
                 HOST_CROSS_COMPILE=${HOST_PREFIX} \
                 TA_CROSS_COMPILE=${HOST_PREFIX} \
                 -C ${S} OUTPUT_DIR=${B} \
               "

S = "${WORKDIR}/git"
B = "${WORKDIR}/build"

CFLAGS += "--sysroot=${STAGING_DIR_HOST}"

do_compile() {
    oe_runmake
}

do_install () {
    install -D -p -m0444 ${S}/host/optee_example_rw ${B}/optee_example_rw
    install -D -p -m0444 ${S}/ta/6ceb6fc1-9e49-41fb-a963-536a655f9a21.ta ${B}/6ceb6fc1-9e49-41fb-a963-536a655f9a21.ta
}

FILES_${PN} += "${nonarch_base_libdir}/optee_armtz/"

# Imports machine specific configs from staging to build
PACKAGE_ARCH = "${MACHINE_ARCH}"

# python3-cryptography needs the legacy provider, so set OPENSSL_MODULES to the
# right path until this is relocated automatically.
export OPENSSL_MODULES="${STAGING_LIBDIR_NATIVE}/ossl-modules"

There are two important notes:

  • The export OPENSSL_MODULES="${STAGING_LIBDIR_NATIVE}/ossl-modules" is mandatory otherwise the signing of the TA api binary will fail.
  • The TA_DEV_KIT_DIR = "${STAGING_INCDIR}/optee/export-user_ta" can change depending on the arch so check the examples that are targeted for your platform with the optee-examples layer.

Refs