Secure APT Key Storage using ARM TrustZone (OP-TEE)

Introduction

This document provides a comprehensive guide and example implementation for securely storing and retrieving APT authentication keys using ARM TrustZone, specifically leveraging the Open Portable Trusted Execution Environment (OP-TEE). The goal is to enhance the security of APT package authentication from a local APT server by ensuring that the critical signing keys are protected within the Trusted Execution Environment (TEE), making them resilient against attacks originating from the Normal World (e.g., a compromised Linux kernel or user-space applications).

Traditional APT key management often involves storing GPG keys directly on the file system, where they are vulnerable to various software-based attacks. By moving these sensitive keys into the TEE’s secure storage, we can achieve a higher level of assurance regarding their confidentiality and integrity. This approach ensures that the keys are never exposed in plaintext to the Normal World and that operations involving these keys (like signing or verification) are performed in an isolated and trusted environment.

This guide will cover the following aspects:

  1. TEE-based Secure Key Storage Example: Detailing the development of a Trusted Application (TA) for key management within OP-TEE and a corresponding Client Application (CA) in the Normal World to interact with the TA.
  2. APT Authentication Integration: Explaining how to integrate the TEE-retrieved key into the APT package authentication process.
  3. Yocto Integration Guidance: Providing instructions on how to incorporate the developed components into a Yocto-based embedded Linux project.

1. TEE-based Secure Key Storage Example

To demonstrate secure key storage, we will develop a pair of applications: a Trusted Application (TA) that resides in the Secure World and manages the key, and a Client Application (CA) that runs in the Normal World and requests key operations from the TA. The TA will utilize OP-TEE’s secure persistent storage to store the APT signing key.

1.1. Trusted Application (TA) for Key Management

The Trusted Application is the core component responsible for handling the sensitive APT key. It will expose functions to store a key, retrieve a key, and potentially perform cryptographic operations (like signing) using the key without ever exposing the key material to the Normal World. For this example, we will focus on storing and retrieving the key.

1.1.1. TA UUID and Command IDs

Every Trusted Application is uniquely identified by a Universally Unique Identifier (UUID). This UUID is used by the Client Application to specify which TA it wants to communicate with. Additionally, we define command IDs to differentiate between various operations that the TA can perform.

Let’s define a UUID and some command IDs for our apt_key_ta.

// apt_key_ta/include/apt_key_ta.h

#ifndef APT_KEY_TA_H
#define APT_KEY_TA_H

#define TA_APT_KEY_UUID \
    { 0x12345678, 0x1234, 0x1234, \
      { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef} }

/*
 * Command IDs for the Trusted Application
 */
#define TA_APT_KEY_CMD_STORE_KEY        0
#define TA_APT_KEY_CMD_RETRIEVE_KEY     1

#endif /* APT_KEY_TA_H */

Explanation:

1.1.2. TA Implementation (apt_key_ta/ta/apt_key_ta.c)

The TA implementation will handle the lifecycle of the Trusted Application and the secure storage operations. It will implement the mandatory entry points (TA_CreateEntryPoint, TA_DestroyEntryPoint, TA_OpenSessionEntryPoint, TA_CloseSessionEntryPoint, TA_InvokeCommandEntryPoint) and the logic for storing and retrieving the key.

We will use OP-TEE’s secure persistent object API (TEE_CreatePersistentObject, TEE_WriteObjectData, TEE_ReadObjectData, TEE_CloseObject, TEE_OpenPersistentObject) to store the key. This API ensures that the data is encrypted and integrity-protected by the TEE, and stored in a location accessible only by the TEE.

// apt_key_ta/ta/apt_key_ta.c

#include <tee_internal_api.h>
#include <tee_internal_api_extensions.h>
#include <string.h>

#include <apt_key_ta.h>

#define APT_KEY_OBJECT_ID   


0x4150544B // "APTKEY" in ASCII

/*
 * Called when the instance of the TA is created. This is the first call in
 * the TA.
 */
TEE_Result TA_CreateEntryPoint(void)
{
    DMSG("has been called");

    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)
{
    DMSG("has been called");
}

/*
 * 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.
 */
TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types,
                        TEE_Param params[4],
                        void **sess_ctx)
{
    uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_NONE,
                               TEE_PARAM_TYPE_NONE,
                               TEE_PARAM_TYPE_NONE,
                               TEE_PARAM_TYPE_NONE);

    DMSG("has been called");

    if (param_types != exp_param_types) {
        EMSG("Bad parameters -> %x", param_types);
        return TEE_ERROR_BAD_PARAMETERS;
    }

    /* Unused parameters */
    (void)&params;
    (void)&sess_ctx;

    /* If return value != TEE_SUCCESS the session will not be created. */
    return TEE_SUCCESS;
}

/*
 * Called when a session is closed, sess_ctx hold the value that was
 * assigned by TA_OpenSessionEntryPoint().
 */
void TA_CloseSessionEntryPoint(void *sess_ctx)
{
    (void)&sess_ctx; /* Unused parameter */
    DMSG("has been called");
}

static TEE_Result store_key(uint32_t param_types, TEE_Param params[4])
{
    uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT,
                                               TEE_PARAM_TYPE_NONE,
                                               TEE_PARAM_TYPE_NONE,
                                               TEE_PARAM_TYPE_NONE);
    TEE_ObjectHandle object;
    TEE_Result res;
    char *key_data;
    uint32_t key_len;

    DMSG("has been called");

    if (param_types != exp_param_types) {
        EMSG("Bad parameters -> %x", param_types);
        return TEE_ERROR_BAD_PARAMETERS;
    }

    key_data = (char *)params[0].memref.buffer;
    key_len = params[0].memref.size;

    res = TEE_CreatePersistentObject(TEE_STORAGE_PRIVATE,
                                     (void *)&APT_KEY_OBJECT_ID, sizeof(APT_KEY_OBJECT_ID),
                                     TEE_DATA_FLAG_ACCESS_WRITE_META, TEE_HANDLE_NULL,
                                     key_data, key_len, &object);
    if (res != TEE_SUCCESS) {
        EMSG("TEE_CreatePersistentObject failed 0x%x", res);
        return res;
    }

    TEE_CloseObject(object);

    return res;
}

static TEE_Result retrieve_key(uint32_t param_types, TEE_Param params[4])
{
    uint32_t exp_param_types = TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_OUTPUT,
                                               TEE_PARAM_TYPE_NONE,
                                               TEE_PARAM_TYPE_NONE,
                                               TEE_PARAM_TYPE_NONE);
    TEE_ObjectHandle object;
    TEE_Result res;
    uint32_t key_len;

    DMSG("has been called");

    if (param_types != exp_param_types) {
        EMSG("Bad parameters -> %x", param_types);
        return TEE_ERROR_BAD_PARAMETERS;
    }

    res = TEE_OpenPersistentObject(TEE_STORAGE_PRIVATE,
                                   (void *)&APT_KEY_OBJECT_ID, sizeof(APT_KEY_OBJECT_ID),
                                   TEE_DATA_FLAG_ACCESS_READ, &object);
    if (res != TEE_SUCCESS) {
        EMSG("TEE_OpenPersistentObject failed 0x%x", res);
        return res;
    }

    res = TEE_ReadObjectData(object, params[0].memref.buffer, params[0].memref.size, &key_len);
    if (res == TEE_SUCCESS)
        params[0].memref.size = key_len;

    TEE_CloseObject(object);

    return res;
}

/*
 * Called when a TA is invoked. sess_ctx hold that value that was
 * assigned by TA_OpenSessionEntryPoint(). The rest of the paramters
 * comes from normal world.
 */
TEE_Result TA_InvokeCommandEntryPoint(void *sess_ctx,
                            uint32_t cmd_id,
                            uint32_t param_types,
                            TEE_Param params[4])
{
    (void)&sess_ctx; /* Unused parameter */

    switch (cmd_id) {
    case TA_APT_KEY_CMD_STORE_KEY:
        return store_key(param_types, params);
    case TA_APT_KEY_CMD_RETRIEVE_KEY:
        return retrieve_key(param_types, params);
    default:
        return TEE_ERROR_BAD_PARAMETERS;
    }
}

Explanation of TA Implementation:

1.1.3. TA Build System (apt_key_ta/ta/Makefile)

To build the Trusted Application, you’ll need a Makefile that leverages the OP-TEE build system. This Makefile will specify the source files, include paths, and the UUID of the TA.

# apt_key_ta/ta/Makefile

# The UUID for the TA (must match the one in apt_key_ta.h)
# Use uuidgen to generate a unique UUID for your TA
# For example: 12345678-1234-1234-1234-567890abcdef
# Convert to the format used in apt_key_ta.h
# Example: { 0x12345678, 0x1234, 0x1234, { 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef } }

# TA_UUID is defined in apt_key_ta.h, so we just need to include it
# This is typically handled by the OP-TEE build system including the common.mk

# Source files for the TA
SRCS = apt_key_ta.c

# Include path for the TA header
# This assumes apt_key_ta.h is in ../include
CFLAGS += -I../include

# Name of the TA binary
TA_NAME ?= apt_key_ta

# Inherit the common OP-TEE TA build rules
include $(TA_DEV_KIT_DIR)/mk/ta_dev_kit.mk

Explanation of TA Makefile:

1.2. Client Application (CA) for Key Interaction

The Client Application runs in the Normal World (Linux) and acts as the interface between the user/system and the Trusted Application. It will use the OP-TEE Client API (libteec) to open a session with the TA, invoke commands, and exchange data.

1.2.1. CA Implementation (apt_key_ta/host/main.c)

The Client Application will provide functions to store and retrieve the APT key by communicating with the TA. For simplicity, this example will take the key data as a command-line argument for storing and print the retrieved key.

// apt_key_ta/host/main.c

#include <err.h>
#include <stdio.h>
#include <string.h>

// OP-TEE Client API header
#include <tee_client_api.h>

// TA UUID and command IDs
#include <apt_key_ta.h>

#define MAX_KEY_SIZE 4096 // Example max size for the key

int main(int argc, char *argv[])
{
    TEEC_Result res;
    TEEC_Context ctx;
    TEEC_Session sess;
    TEEC_Operation op;
    TEEC_UUID uuid = TA_APT_KEY_UUID;
    uint32_t err_origin;
    char key_buffer[MAX_KEY_SIZE];

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <store|retrieve> [key_data]\n", argv[0]);
        return 1;
    }

    /* 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 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);

    memset(&op, 0, sizeof(op)); // Clear the operation structure

    if (strcmp(argv[1], "store") == 0) {
        if (argc < 3) {
            fprintf(stderr, "Usage: %s store <key_data>\n", argv[0]);
            TEEC_CloseSession(&sess);
            TEEC_FinalizeContext(&ctx);
            return 1;
        }

        // Prepare parameters for storing the key
        op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT, TEEC_NONE, TEEC_NONE, TEEC_NONE);
        op.params[0].memref.buffer = argv[2];
        op.params[0].memref.size = strlen(argv[2]);

        if (op.params[0].memref.size > MAX_KEY_SIZE) {
            fprintf(stderr, "Key data too large (max %d bytes)\n", MAX_KEY_SIZE);
            TEEC_CloseSession(&sess);
            TEEC_FinalizeContext(&ctx);
            return 1;
        }

        printf("Storing key: \"%s\" (size: %zu)\n", (char *)op.params[0].memref.buffer, op.params[0].memref.size);

        // Invoke the store command in the TA
        res = TEEC_InvokeCommand(&sess, TA_APT_KEY_CMD_STORE_KEY, &op, &err_origin);
        if (res != TEEC_SUCCESS)
            err(1, "TEEC_InvokeCommand(STORE_KEY) failed with code 0x%x origin 0x%x", res, err_origin);

        printf("Key stored successfully.\n");

    } else if (strcmp(argv[1], "retrieve") == 0) {
        // Prepare parameters for retrieving the key
        op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_OUTPUT, TEEC_NONE, TEEC_NONE, TEEC_NONE);
        op.params[0].memref.buffer = key_buffer;
        op.params[0].memref.size = MAX_KEY_SIZE;

        // Invoke the retrieve command in the TA
        res = TEEC_InvokeCommand(&sess, TA_APT_KEY_CMD_RETRIEVE_KEY, &op, &err_origin);
        if (res != TEEC_SUCCESS)
            err(1, "TEEC_InvokeCommand(RETRIEVE_KEY) failed with code 0x%x origin 0x%x", res, err_origin);

        // Print only the raw key data to stdout
        // Ensure no newline is added if the key itself contains one, or if it's binary
        fprintf(stdout, "%.*s", (int)op.params[0].memref.size, (char *)op.params[0].memref.buffer);

    } else {
        fprintf(stderr, "Invalid command: %s. Use 'store' or 'retrieve'.\n", argv[1]);
        TEEC_CloseSession(&sess);
        TEEC_FinalizeContext(&ctx);
        return 1;
    }

    /* We're done with the TA, close the session */
    TEEC_CloseSession(&sess);

    /* And destroy the context */
    TEEC_FinalizeContext(&ctx);

    return 0;
}

Explanation of CA Implementation:

1.2.2. CA Build System (apt_key_ta/host/Makefile)

To build the Client Application, you’ll need a Makefile that links against libteec.

# apt_key_ta/host/Makefile

# Name of the host application binary
BIN ?= apt_key_client

# Source files for the host application
SRCS = main.c

# Include path for the TA header
# This assumes apt_key_ta.h is in ../include
CFLAGS += -I../include

# Link against the OP-TEE Client library
LDADD += -lteec

# Inherit the common OP-TEE host application build rules
include $(OPTEE_CLIENT_DIR)/mk/host_lib.mk

Explanation of CA Makefile:

1.3. Project Structure

The complete project structure for this example would look like this:

apt_key_ta/
├── host/
│   ├── main.c
│   └── Makefile
├── include/
│   └── apt_key_ta.h
└── ta/
    ├── apt_key_ta.c
    └── Makefile

1.4. Building and Testing the Example

To build and test this example, you would typically need an OP-TEE development environment set up, which usually involves a cross-compilation toolchain and the OP-TEE build system. The general steps are:

  1. Set up OP-TEE Development Environment: This involves cloning the OP-TEE repositories (optee_os, optee_client, build, etc.) and setting up the toolchain. This is often done via a Yocto build or by following the OP-TEE documentation for a specific platform.
  2. Build the TA: Navigate to apt_key_ta/ta and run make. This will produce the apt_key_ta.ta binary.
  3. Build the CA: Navigate to apt_key_ta/host and run make. This will produce the apt_key_client executable.
  4. Deploy: Copy the apt_key_ta.ta to the TEE’s TA deployment directory on your target device (e.g., /lib/optee_armtz/ or /usr/lib/optee_armtz/). Copy the apt_key_client executable to your Normal World filesystem.
  5. Run: On the target device, you can then run:

“my_secret_apt_key_data”to store a key. *./apt_key_client retrieve` to retrieve and print the stored key.

Important Considerations for Secure Storage:

This concludes the first phase of setting up the TEE-based secure key storage. The next step is to integrate this with the APT authentication process.

2. APT Authentication Integration

APT (Advanced Package Tool) relies on cryptographic signatures to ensure the authenticity and integrity of packages. When you add a new APT repository, you typically import a GPG (GNU Privacy Guard) public key associated with that repository. This key is then used by APT to verify the signatures of the Release files and subsequently the packages themselves. This section will delve into how APT manages these keys and how we can integrate our TEE-protected key into this process.

2.1. Understanding APT’s Key Management and Authentication Process

APT’s authentication mechanism is built around OpenPGP (GPG) signatures. Here’s a simplified overview of the process:

  1. Repository Configuration: An APT repository is typically configured in /etc/apt/sources.list or a file within /etc/apt/sources.list.d/. This configuration specifies the URL of the repository and the distribution components.
  2. Key Import: For a new repository, its public GPG key must be imported into APT’s keyring. Historically, this was done using apt-key add <keyfile>, but this method is deprecated due to security concerns [1]. The recommended modern approach is to store the key as a separate file in /etc/apt/trusted.gpg.d/ or reference it directly in the sources.list entry using the signed-by option [2].
  3. Release File Download: When apt update is run, APT downloads the Release file (and potentially Release.gpg and InRelease) from each configured repository. The Release file contains metadata about the packages in the repository, including checksums of other index files.
  4. Signature Verification: APT verifies the digital signature of the Release file using the public key(s) it has in its keyring. If the signature is valid, it confirms that the Release file has not been tampered with and originates from a trusted source.
  5. Checksum Verification: After verifying the Release file’s signature, APT uses the checksums listed within it to verify the integrity of other index files (e.g., Packages.gz, Sources.gz).
  6. Package Download and Verification: When a package is downloaded, its checksum is verified against the checksum listed in the Packages file, which was itself verified via the Release file.

Key Storage Locations for APT:

2.2. Challenges and Integration Strategy

The primary challenge in integrating a TEE-protected key with APT is that APT expects a GPG public key file to be present on the Normal World filesystem. Our TEE-based solution, however, stores the private key securely within the TEE and only allows its retrieval (or use for signing) through the retrieve_key command. APT itself does not have a direct mechanism to interact with a TEE to fetch a key or request a signature.

Therefore, our integration strategy will involve:

  1. Storing the Public Key in TEE: As demonstrated in Phase 1, the APT signing public key will be securely stored in the TA. This is the key that the client (embedded device) will use to verify packages.
  2. Providing the Public Key to APT: The corresponding APT signing public key must still be made available to APT in the Normal World. This public key does not need to be protected by the TEE, as its purpose is public verification. However, we will use the TEE to retrieve this public key, ensuring that APT always gets the correct, untampered public key from a trusted source.
  3. Custom APT Method (Conceptual): While APT doesn’t directly interact with a TEE, we can create a custom APT method or a wrapper script that uses our Client Application to retrieve the public key from the TEE and provide it to APT. This ensures that the public key used for verification is always sourced from the TEE.

Let’s clarify the use case: if the local APT server is signing packages, then the private key is on the server. If the embedded device is verifying packages, it needs the public key. The request is to authenticate APT packages, which implies verification on the client side. Therefore, the TEE will store the public key that APT uses for verification, or a secret that helps verify the public key. For this example, we will assume the TEE stores the public key directly.st

2.3. Developing a Mechanism to Integrate the TEE-Retrieved Key with APT

Since APT expects a public key file on the filesystem, we need an intermediary step. We will create a simple wrapper script that utilizes our apt_key_client (the Client Application from Phase 1) to retrieve the public key from the TEE and then writes it to a temporary file. This temporary file can then be referenced by APT using the signed-by option.

2.3.1. Wrapper Script (get_apt_key_from_tee.sh)

This script will call our apt_key_client to retrieve the key and then output it to standard output, which can be redirected to a file.

#!/bin/bash

# Path to your apt_key_client executable
APT_KEY_CLIENT="/usr/local/bin/apt_key_client" # Adjust this path as needed

# Check if the client exists
if [ ! -f "$APT_KEY_CLIENT" ]; then
    echo "Error: APT key client not found at $APT_KEY_CLIENT" >&2
    exit 1
fi

# Retrieve the key from the TEE and print it to stdout
# IMPORTANT: The apt_key_client (main.c) should be modified to ONLY print the raw key data
# without any 


the key with a prefix, so we need to extract just the key data.
# For simplicity, we assume the key is ASCII text. For binary keys, this would need adjustment.

# Execute the client and capture its output
CLIENT_OUTPUT=$("$APT_KEY_CLIENT" retrieve 2>&1)

# Extract the key data. Assumes the client outputs 


additional messages (like "Retrieving key..." or "Retrieved key:").

"$APT_KEY_CLIENT" retrieve

exit $?

Note on apt_key_client modification:

For the get_apt_key_from_tee.sh script to work correctly, the apt_key_client/host/main.c file needs a slight modification. Specifically, when retrieving the key, it should only print the raw key data to stdout and nothing else. Remove the printf statements that output

Explanation of Wrapper Script:

2.3.2. Integrating with APT using signed-by

Now, we can use this wrapper script to provide the public key to APT. The recommended way to do this is by using the signed-by option in your sources.list entry. This option allows you to specify a file containing the public key that should be used to verify the repository.

First, you need to ensure the get_apt_key_from_tee.sh script is executable and placed in a location accessible by APT (e.g., /usr/local/bin/).

sudo cp get_apt_key_from_tee.sh /usr/local/bin/
sudo chmod +x /usr/local/bin/get_apt_key_from_tee.sh

Next, you will modify your APT sources.list entry (e.g., in /etc/apt/sources.list.d/my_local_repo.list) to use the output of our script as the signing key. APT supports reading keys from a pipe, which is perfect for our use case.

# /etc/apt/sources.list.d/my_local_repo.list

deb [signed-by=/usr/local/bin/get_apt_key_from_tee.sh] http://your-local-apt-server/debian stable main

Explanation:

2.4. Testing the APT Authentication with the TEE-Provided Key

To test this setup, you will need a local APT repository that is signed with the same private key whose public counterpart is stored in your TEE. You would typically set up a local APT server (e.g., using apt-mirror or reprepro) and sign its Release files with your GPG private key.

Steps to Test:

  1. Generate a GPG Key Pair: If you don’t already have one, generate a GPG key pair. This will be used to sign your local APT repository. bash gpg --batch --gen-key <<EOF Key-Type: RSA Key-Length: 2048 Subkey-Type: RSA Subkey-Length: 2048 Name-Real: APT Signing Key Name-Comment: Local APT Repository Key Name-Email: apt@example.com Expire-Date: 0 %no-protection %commit EOF Note: %no-protection is used for simplicity in this example. In a real scenario, you would protect your private key with a passphrase.

  2. Export the Public Key: Export the public key from your GPG keyring. This is the key you will store in the TEE. bash gpg --armor --export apt@example.com > apt_public_key.asc

  3. Store the Public Key in the TEE: Use your apt_key_client to store the content of apt_public_key.asc into the TEE. bash ./apt_key_client store "$(cat apt_public_key.asc)"

  4. Set up a Local APT Repository: Configure a local APT repository and sign its Release files with the private key corresponding to the public key you just stored in the TEE. This step is outside the scope of this document but involves tools like reprepro or apt-ftparchive.

  5. Configure APT sources.list: Create or modify a file in /etc/apt/sources.list.d/ (e.g., my_local_repo.list) with the signed-by option pointing to your wrapper script, as shown in Section 2.3.2.

  6. Run apt update: Execute sudo apt update on your embedded device. APT should now attempt to fetch the Release file from your local repository and use the key retrieved from the TEE (via get_apt_key_from_tee.sh) to verify its signature.

    If successful, you should see output similar to:

    Get:1 http://your-local-apt-server/debian stable InRelease [X B]
    Reading package lists... Done

    If there are issues with the key or signature, APT will report an error, such as:

    W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: http://your-local-apt-server/debian stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY <KEY_ID>

This setup ensures that the public key used for APT package authentication is always sourced from the secure storage within the TEE, providing a strong integrity check for your software updates.

References:

[1] Debian Wiki: apt-key is deprecated https://wiki.debian.org/DebianRepository/UseDeprecatedAptKey

[2] Debian Wiki: SecureApt https://wiki.debian.org/SecureApt

3. Yocto Integration Guidance

Integrating OP-TEE and your custom Trusted Applications and Client Applications into a Yocto-based embedded Linux project involves several steps. Yocto provides a robust framework for building custom Linux distributions, and OP-TEE is well-supported through dedicated Yocto layers. This section will guide you through the necessary configurations and additions to your Yocto build.

To incorporate OP-TEE into your Yocto build, you will primarily need the meta-security layer, which contains the meta-optee layer. Additionally, you might need meta-arm or meta-arm-toolchain for specific ARM-related configurations and toolchains.

Here are the essential layers and their typical roles:

Adding Layers to bblayers.conf:

Your build/conf/bblayers.conf file needs to include these layers. An example snippet might look like this:

# POKY_BBLAYERS_CONF_VERSION is increased each time build/conf/bblayers.conf
# changes in incompatible ways
POKY_BBLAYERS_CONF_VERSION = "2"

BBPATH = "${TOPDIR}"
BBFILES ?= ""

BBLAYERS ?= " \
  ${TOPDIR}/../poky/meta \
  ${TOPDIR}/../poky/meta-poky \
  ${TOPDIR}/../poky/meta-yocto-bsp \
  ${TOPDIR}/../meta-openembedded/meta-oe \
  ${TOPDIR}/../meta-openembedded/meta-python \
  ${TOPDIR}/../meta-openembedded/meta-networking \
  ${TOPDIR}/../meta-security/meta-security \
  ${TOPDIR}/../meta-security/meta-optee \
  ${TOPDIR}/../meta-arm/meta-arm \
  ${TOPDIR}/../meta-arm/meta-arm-toolchain \
  "

Note: The paths (${TOPDIR}/../poky/meta, etc.) assume a specific directory structure where your poky, meta-openembedded, meta-security, and meta-arm repositories are siblings to your build directory. Adjust these paths according to your actual setup.

3.2. Kernel and U-Boot Configuration for TEE Support

For OP-TEE to function correctly, the Linux kernel and U-Boot (or other bootloaders) need to be configured to support ARM TrustZone and the specific secure monitor used (TF-A).

3.2.1. Kernel Configuration

Your Linux kernel must be built with the necessary configurations to enable TrustZone support and the OP-TEE driver. These are typically enabled by default when using the meta-optee layer, but it’s good to be aware of them.

Key kernel configuration options include:

These options are usually set in your kernel’s .config file or via kernel fragments in your Yocto recipes. You can add a .bbappend file for your kernel recipe (e.g., linux-yocto_%.bbappend) to include a kernel configuration fragment:

# project-specific/recipes-kernel/linux/linux-yocto_%.bbappend

FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"

SRC_URI += "file://optee.cfg"

And in project-specific/recipes-kernel/linux/linux-yocto/optee.cfg:

CONFIG_ARM_TRUSTZONE=y
CONFIG_OPTEE=y
CONFIG_OPTEE_SHM_NUM_BUFFERS=y
CONFIG_TEE=y

3.2.2. U-Boot Configuration

U-Boot needs to be configured to load and hand off control to ARM Trusted Firmware-A (TF-A), which then initializes the Secure World (including OP-TEE) before jumping to the Normal World Linux kernel. The meta-arm layer provides recipes for TF-A and U-Boot that are typically pre-configured for TrustZone.

Key U-Boot configurations often involve:

Example device tree snippet (conceptual, actual details depend on your SoC):

/ {
    #address-cells = <2>;
    #size-cells = <2>;

    reserved-memory {
        #address-cells = <2>;
        #size-cells = <2>;
        ranges;

        optee_shm: optee-shm@...
            compatible = "optee-shm";
            reg = <0x0 0xXXXXXXXX 0x0 0xYYYYYYYY>; // Shared memory region
            no-map;
        };
    };

    firmware {
        optee {
            compatible = "linaro,optee-tz";
            method = "smc";
            shm-res = <&optee_shm>;
        };
    };
};

In Yocto, you typically manage device tree overlays or modifications through .bbappend files for your machine configuration or kernel recipe.

3.3. Integrating the TA/CA into the Yocto Build

Now, let’s integrate your apt_key_ta and apt_key_client into the Yocto build. This involves creating custom recipes for both the Trusted Application and the Client Application.

3.3.1. Recipe for the Trusted Application (apt-key-ta_%.bb)

You will create a new recipe for your Trusted Application. This recipe will define how to fetch, build, and install your TA binary.

Create a directory structure like project-specific/recipes-security/apt-key-ta/ and place your recipe file there.

# project-specific/recipes-security/apt-key-ta/apt-key-ta_%.bb

SUMMARY = "Trusted Application for secure APT key storage"
DESCRIPTION = "OP-TEE Trusted Application to securely store and retrieve APT public keys."
LICENSE = "CLOSED" # Or your chosen license

# Point to your TA source code
SRC_URI = "file://apt_key_ta.tar.gz"

# Inherit optee-ta-common to get the necessary build environment for TAs
inherit optee-ta-common

# Specify the UUID of your TA (must match apt_key_ta.h)
OPTEE_TA_UUID = "12345678-1234-1234-1234-567890abcdef"

# Specify the TA name (must match TA_NAME in apt_key_ta/ta/Makefile)
OPTEE_TA_NAME = "apt_key_ta"

# Where to install the TA binary on the target rootfs
# This is the default location for OP-TEE TAs
OPTEE_TA_INSTALL_DIR = "${nonarch_base_libdir}/optee_armtz"

# Ensure the TA is built for the secure world
OPTEE_TA_BUILD_TYPE = "ta"

# Define the source directory within the tarball
S = "${WORKDIR}/apt_key_ta/ta"

# Override do_install to ensure correct installation
do_install() {
    install -d ${D}${OPTEE_TA_INSTALL_DIR}
    install -m 0755 ${B}/${OPTEE_TA_NAME}.ta ${D}${OPTEE_TA_INSTALL_DIR}/
}

# Add dependencies if your TA needs specific libraries from the secure world
# RDEPENDS_${PN} += "optee-os"

Explanation of TA Recipe:

3.3.2. Recipe for the Client Application (apt-key-client_%.bb)

Similarly, you will create a recipe for your Client Application. This recipe will define how to fetch, build, and install your CA executable.

Create a directory structure like project-specific/recipes-security/apt-key-client/ and place your recipe file there.

# project-specific/recipes-security/apt-key-client/apt-key-client_%.bb

SUMMARY = "Client Application for secure APT key interaction"
DESCRIPTION = "Normal World application to interact with the APT key TA."
LICENSE = "CLOSED" # Or your chosen license

# Point to your CA source code
SRC_URI = "file://apt_key_ta.tar.gz"

# Inherit autotools or cmake depending on your host Makefile structure
# For simple Makefiles, you might use 'make' directly in do_compile/do_install
inherit autotools # Assuming a simple Makefile that can be adapted to autotools

# Add dependencies on optee-client (for libteec) and your TA (to ensure it's built)
RDEPENDS_${PN} += "optee-client apt-key-ta"

# Define the source directory within the tarball for the host part
S = "${WORKDIR}/apt_key_ta/host"

# Override do_configure and do_compile if not using autotools/cmake
# do_configure() { :; }
# do_compile() {
#    oe_runmake
# }

# Override do_install to ensure correct installation
do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${B}/apt_key_client ${D}${bindir}/
}

Explanation of CA Recipe:

3.3.3. Recipe for the Wrapper Script (get-apt-key-from-tee-script_%.bb)

Finally, you need a recipe for your get_apt_key_from_tee.sh wrapper script.

Create a directory structure like project-specific/recipes-security/get-apt-key-from-tee-script/ and place your recipe file there.

# project-specific/recipes-security/get-apt-key-from-tee-script/get-apt-key-from-tee-script_%.bb

SUMMARY = "Wrapper script to retrieve APT key from TEE"
DESCRIPTION = "A shell script to call the apt_key_client and output the APT key."
LICENSE = "CLOSED" # Or your chosen license

# Point to your script file
SRC_URI = "file://get_apt_key_from_tee.sh"

# No special inheritance needed for a simple script

# Add dependency on the client application
RDEPENDS_${PN} += "apt-key-client"

# Override do_install to ensure correct installation and permissions
do_install() {
    install -d ${D}${bindir}
    install -m 0755 ${WORKDIR}/get_apt_key_from_tee.sh ${D}${bindir}/
}

Explanation of Wrapper Script Recipe:

3.4. Adding to Your Image

To include these components in your final Yocto image, you need to add them to your local.conf or your custom image recipe (e.g., core-image-minimal.bbappend or my-custom-image.bb).

# build/conf/local.conf or project-specific/recipes-images/images/my-custom-image.bbappend

IMAGE_INSTALL_append = " \
    optee-client \
    apt-key-ta \
    apt-key-client \
    get-apt-key-from-tee-script \
    "

This will ensure that all necessary OP-TEE components, your Trusted Application, Client Application, and the wrapper script are included in your final root filesystem image.

3.5. Building the Yocto Image

After setting up all the layers and recipes, you can build your Yocto image:

source oe-init-build-env
bitbake my-custom-image # Or core-image-minimal, etc.

This will compile everything, including the kernel, U-Boot, OP-TEE, and your custom applications, and create a bootable image for your ARM64 target. Once the image is deployed to your device, you should be able to use the apt_key_client and the get_apt_key_from_tee.sh script as described in the previous sections.

Important Yocto Considerations:

This concludes the Yocto integration guidance. The final phase will be to consolidate all the code and documentation into a deliverable package.

Color format