Android System Securing, Hardening, & Privacy

Developing Custom TrustZone Trusted Applications (TAs): An Advanced Guide for Android Security

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to Android TrustZone and Trusted Applications

The Android ecosystem, with its vast attack surface, heavily relies on hardware-backed security features. Central among these is ARM TrustZone, a hardware isolation technology that enables the creation of a Trusted Execution Environment (TEE). Within this TEE, sensitive operations like cryptographic key management, secure boot, and DRM are performed by Trusted Applications (TAs), isolated from the potentially compromised Rich Execution Environment (REE) where Android runs. This advanced guide delves into the intricacies of developing custom TrustZone TAs, providing a roadmap for enhancing Android device security.

TrustZone partitions the system into two worlds: the Secure World and the Normal World. The Normal World hosts the operating system (e.g., Android) and its applications, while the Secure World runs a minimal Trusted OS and TAs. Access to Secure World resources is strictly controlled by hardware, ensuring that even a fully compromised Android OS cannot directly access or manipulate data within the TEE.

Understanding the TrustZone Architecture

At the heart of TrustZone lies the Secure Monitor, a small piece of code responsible for switching between the Normal and Secure Worlds. When an application in the Normal World needs to perform a secure operation, it invokes a Secure Monitor Call (SMC), which then transitions the CPU to the Secure World. Here, the Trusted OS (e.g., OP-TEE, Trusty) dispatches the request to the appropriate TA.

Key Components:

  • Trusted Execution Environment (TEE): The isolated environment for secure operations.
  • Trusted OS: A lightweight operating system running in the Secure World, managing TAs. Popular examples include OP-TEE, Trusty, and Kinibi.
  • Trusted Applications (TAs): Specific applications loaded into the TEE to perform sensitive tasks.
  • Client Applications (CAs): Standard Android applications or services running in the Normal World that communicate with TAs via the TEE driver.
  • GlobalPlatform TEE Specification: A set of standards defining the TEE architecture, APIs for TAs (TEE Internal API), and CAs (TEE Client API).

Setting Up Your Development Environment

Developing TAs typically involves cross-compilation for ARM-based targets. For this guide, we’ll primarily reference OP-TEE, a widely adopted open-source TEE implementation compatible with the GlobalPlatform API.

Prerequisites:

  • A Linux-based development machine.
  • ARM GCC Cross-Toolchain (e.g., arm-linux-gnueabihf- for 32-bit, aarch64-linux-gnu- for 64-bit).
  • Git for cloning repositories.
  • A build environment with necessary packages (build-essential, uuid-dev, etc.).

Steps for OP-TEE Development Environment Setup:

  1. Clone OP-TEE Repositories:

    mkdir optee-dev && cd optee-devgit clone https://github.com/OP-TEE/optee_os.gitgit clone https://github.com/OP-TEE/optee_client.gitgit clone https://github.com/OP-TEE/optee_examples.gitgit clone https://github.com/OP-TEE/build.git
  2. Configure Toolchain: Ensure your toolchain path is correctly set. For example, add the following to your ~/.bashrc or equivalent:

    export PATH=$PATH:/path/to/your/gcc-arm-toolchain/bin
  3. Build OP-TEE OS and Client: Navigate to the build directory and use its scripts. This process can be complex and often targets a specific platform (e.g., QEMU or a specific development board).

    cd buildmake -f Makefile.optee-qemu all

    This command will build the OP-TEE OS for QEMU, along with the client libraries and example TAs/CAs. Adjust the makefile target if you are using a different platform.

Developing a Trusted Application (TA)

TAs are written in C and adhere to the GlobalPlatform TEE Internal API. They expose a set of entry points that the Trusted OS calls during their lifecycle and command invocations.

TA Structure and Entry Points:

  • TA_CreateEntryPoint: Called when the first session to the TA is opened. Initializes TA-specific resources.
  • TA_DestroyEntryPoint: Called when the last session to the TA is closed. Cleans up resources.
  • TA_OpenSessionEntryPoint: Called each time a new session is opened by a CA.
  • TA_CloseSessionEntryPoint: Called when a session is closed.
  • TA_InvokeCommandEntryPoint: The core entry point for handling commands from CAs.

Example: Secure Key Storage TA

Let’s outline a TA that securely stores and retrieves a cryptographic key using TEE persistent storage.

#include <tee_internal_api.h>#include <tee_internal_api_ext.h>#include <string.h>#define TA_UUID { 0x12345678, 0x9012, 0x3456, { 0x78, 0x90, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45 } }#define TA_CMD_STORE_KEY 0#define TA_CMD_GET_KEY 1static TEE_ObjectHandle key_handle = TEE_HANDLE_NULL;TEE_Result TA_CreateEntryPoint(void) {    DMSG("TA_CreateEntryPoint called");    return TEE_SUCCESS;}void TA_DestroyEntryPoint(void) {    DMSG("TA_DestroyEntryPoint called");}TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types,    TEE_Param params[4],    void **sess_ctx) {    DMSG("TA_OpenSessionEntryPoint called");    (void)&param_types; (void)&params; (void)&sess_ctx;    return TEE_SUCCESS;}void TA_CloseSessionEntryPoint(void *sess_ctx) {    DMSG("TA_CloseSessionEntryPoint called");    (void)&sess_ctx;}TEE_Result TA_InvokeCommandEntryPoint(void *sess_ctx,    uint32_t cmd_id,    uint32_t param_types,    TEE_Param params[4]) {    DMSG("TA_InvokeCommandEntryPoint called");    (void)&sess_ctx;    switch (cmd_id) {        case TA_CMD_STORE_KEY: {            // Expected: MEMREF_INPUT for key data            if (TEE_PARAM_TYPE_GET(param_types, 0) != TEE_PARAM_TYPE_MEMREF_INPUT) {                return TEE_ERROR_BAD_PARAMETERS;            }            char *key_data = (char *)params[0].memref.buffer;            size_t key_size = params[0].memref.size;            TEE_ObjectStorageConstants storage_flags = TEE_DATA_FLAG_OVERWRITE | TEE_DATA_FLAG_ACCESS_WRITE;            TEE_Attribute attrs[2];            TEE_InitRefAttribute(&attrs[0], TEE_ATTR_SECRET_VALUE, key_data, key_size);            TEE_Result res = TEE_CreatePersistentObject(TEE_STORAGE_PRIVATE_RPMB, "my_secure_key",                strlen("my_secure_key"), storage_flags, TEE_HANDLE_NULL, attrs, 1, &key_handle);            if (res != TEE_SUCCESS) {                EMSG("Failed to create persistent object: %x", res);                return res;            }            DMSG("Key stored successfully.");            break;        }        case TA_CMD_GET_KEY: {            // Expected: MEMREF_OUTPUT for key data            if (TEE_PARAM_TYPE_GET(param_types, 0) != TEE_PARAM_TYPE_MEMREF_OUTPUT) {                return TEE_ERROR_BAD_PARAMETERS;            }            if (key_handle == TEE_HANDLE_NULL) {                return TEE_ERROR_ITEM_NOT_FOUND;            }            TEE_ObjectInfo object_info;            TEE_Result res = TEE_GetObjectInfo1(key_handle, &object_info);            if (res != TEE_SUCCESS) {                EMSG("Failed to get object info: %x", res);                return res;            }            if (object_info.dataSize > params[0].memref.size) {                return TEE_ERROR_SHORT_BUFFER;            }            uint32_t read_bytes = 0;            res = TEE_ReadObjectData(key_handle, params[0].memref.buffer, object_info.dataSize, &read_bytes);            if (res != TEE_SUCCESS) {                EMSG("Failed to read object data: %x", res);                return res;            }            params[0].memref.size = read_bytes;            DMSG("Key retrieved successfully.");            break;        }        default:            EMSG("Command ID %d is not supported", cmd_id);            return TEE_ERROR_NOT_SUPPORTED;    }    return TEE_SUCCESS;}

This example demonstrates using TEE_CreatePersistentObject and TEE_ReadObjectData to interact with the TEE’s secure storage. The TEE_STORAGE_PRIVATE_RPMB flag indicates storage on Replay Protected Memory Block (RPMB), if available, offering robust replay protection.

Developing a Client Application (CA)

CAs communicate with TAs using the GlobalPlatform TEE Client API. This API allows CAs to discover TAs, open/close sessions, and invoke commands, passing parameters securely between the Normal and Secure Worlds.

Example: Android CA Interacting with Key Storage TA

An Android CA, typically written in Java or C++ (via JNI), would use the TEE Client API (libteec.so) to interact with our key storage TA.

#include <err.h>#include <string.h>#include <stdlib.h>#include <stdio.h>#include <tee_client_api.h>// Defined TA UUID (must match TA's UUID)#define TA_CUSTOM_KEY_STORAGE_UUID     { 0x12345678, 0x9012, 0x3456,       { 0x78, 0x90, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45 } }#define TA_CMD_STORE_KEY 0#define TA_CMD_GET_KEY 1int main() {    TEEC_Context ctx;    TEEC_Session sess;    TEEC_Result res;    TEEC_UUID uuid = TA_CUSTOM_KEY_STORAGE_UUID;    uint32_t err_origin;    TEEC_Operation op;    char key_to_store[] = "ThisIsMySuperSecretKey";    char retrieved_key[256];    // Initialize a context for communication with 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 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);    // Prepare operation for storing key    memset(&op, 0, sizeof(op));    op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT,                                     TEEC_NONE, TEEC_NONE, TEEC_NONE);    op.params[0].tmpref.buffer = key_to_store;    op.params[0].tmpref.size = strlen(key_to_store) + 1; // Include null terminator    // Invoke command to store key    res = TEEC_InvokeCommand(&sess, TA_CMD_STORE_KEY, &op, &err_origin);    if (res != TEEC_SUCCESS)        errx(1, "TEEC_InvokeCommand(STORE_KEY) failed with code 0x%x origin 0x%x", res, err_origin);    printf("Key stored successfully.n");    // Prepare operation for retrieving key    memset(&op, 0, sizeof(op));    op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_OUTPUT,                                     TEEC_NONE, TEEC_NONE, TEEC_NONE);    op.params[0].tmpref.buffer = retrieved_key;    op.params[0].tmpref.size = sizeof(retrieved_key);    // Invoke command to get key    res = TEEC_InvokeCommand(&sess, TA_CMD_GET_KEY, &op, &err_origin);    if (res != TEEC_SUCCESS)        errx(1, "TEEC_InvokeCommand(GET_KEY) failed with code 0x%x origin 0x%x", res, err_origin);    printf("Key retrieved: %sn", retrieved_key);    // Clean up    TEEC_CloseSession(&sess);    TEEC_FinalizeContext(&ctx);    return 0;}

This C client application demonstrates the basic flow: initializing context, opening a session to the TA (identified by its UUID), preparing parameters for an operation, invoking the command, and finally cleaning up.

Building, Deploying, and Debugging TAs

Building:

TAs are typically compiled using the `make` system provided by the TEE SDK (e.g., OP-TEE client SDK). The output is a `.ta` file, which is an ELF executable packaged for the TEE.

cd optee-dev/optee_examples/host/hello_world# Or your custom TA directory (often a sub-directory in optee_examples/ta/)make -f ../mk/Makefile.ta CFG_TA_UUID="12345678-9012-3456-7890-ABCDEF012345"

Deployment:

The compiled `.ta` files need to be placed in a specific directory on the Android file system, usually `/data/tee` or `/vendor/lib/optee_armtz/`. The exact path depends on the specific TEE implementation and device integration.

adb push <your_ta>.ta /data/tee/

The CA (`libteec.so`) also needs to be available to the client application, either linked statically or placed in a searchable library path (e.g., `/vendor/lib64` for 64-bit Android).

Debugging:

Debugging TAs is challenging due to the isolation. Techniques include:

  • Print statements: The simplest method, using `DMSG()` and `EMSG()` in OP-TEE TAs, which print to the TEE console (often accessible via serial console or `dmesg`).
  • GDB with QEMU: For development on QEMU, GDB can be attached to the Secure World, allowing breakpoints and step-by-step execution.
  • JTAG/SWD Debugging: On actual hardware, JTAG/SWD interfaces provide low-level access for debugging the Secure World. This requires specialized hardware debuggers.

Security Considerations and Best Practices

  • Minimalism: Keep TAs as small and focused as possible to minimize the attack surface.
  • Input Validation: Strictly validate all parameters received from the Normal World. Assume all input is malicious.
  • Secure Coding: Adhere to secure coding principles (e.g., avoid buffer overflows, use secure cryptographic practices).
  • Least Privilege: TAs should only have access to the resources absolutely necessary for their function.
  • Memory Management: Be cautious with dynamic memory allocation within TAs, as TEEs often have limited memory and less robust memory protection mechanisms than full OSes.
  • Replay Protection: For sensitive data like keys, utilize features like RPMB or TEE-specific monotonic counters to prevent rollback attacks.

Conclusion

Developing custom TrustZone Trusted Applications is a complex but powerful way to enhance the security posture of Android devices. By leveraging the hardware isolation capabilities of the TEE, developers can create robust solutions for sensitive operations, significantly raising the bar for attackers. Understanding the underlying architecture, mastering the TEE Internal and Client APIs, and adhering to rigorous security practices are crucial for successful TA development and deployment in the challenging landscape of mobile security.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner