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:
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.
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.
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:
TA_APT_KEY_UUID
: This is a placeholder UUID. In a
real-world scenario, you should generate a unique UUID for your TA to
avoid conflicts. Tools like uuidgen
can be used for this purpose.TA_APT_KEY_CMD_STORE_KEY
: This command ID will be used by the Client Application to instruct the TA to store an APT key.TA_APT_KEY_CMD_RETRIEVE_KEY
: This command ID will be used to retrieve the stored APT key from the TA.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.
*/
void)
TEE_Result TA_CreateEntryPoint(
{"has been called");
DMSG(
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)
{"has been called");
DMSG(
}
/*
* 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.
*/
uint32_t param_types,
TEE_Result TA_OpenSessionEntryPoint(4],
TEE_Param params[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);
"has been called");
DMSG(
if (param_types != exp_param_types) {
"Bad parameters -> %x", param_types);
EMSG(return TEE_ERROR_BAD_PARAMETERS;
}
/* Unused parameters */
void)¶ms;
(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 */
("has been called");
DMSG(
}
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;
"has been called");
DMSG(
if (param_types != exp_param_types) {
"Bad parameters -> %x", param_types);
EMSG(return TEE_ERROR_BAD_PARAMETERS;
}
char *)params[0].memref.buffer;
key_data = (0].memref.size;
key_len = params[
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) {
"TEE_CreatePersistentObject failed 0x%x", res);
EMSG(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;
"has been called");
DMSG(
if (param_types != exp_param_types) {
"Bad parameters -> %x", param_types);
EMSG(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) {
"TEE_OpenPersistentObject failed 0x%x", res);
EMSG(return res;
}
0].memref.buffer, params[0].memref.size, &key_len);
res = TEE_ReadObjectData(object, params[if (res == TEE_SUCCESS)
0].memref.size = key_len;
params[
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.
*/
void *sess_ctx,
TEE_Result TA_InvokeCommandEntryPoint(uint32_t cmd_id,
uint32_t param_types,
4])
TEE_Param params[
{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:
APT_KEY_OBJECT_ID
: A unique identifier
for the persistent object that will store our APT key. This is a 32-bit
integer, but it’s often represented as a string or a more complex
structure. Here, we use a simple integer for demonstration.TA_CreateEntryPoint()
and TA_DestroyEntryPoint()
: These are standard lifecycle functions. For this simple example, they don’t perform any specific actions beyond logging.TA_OpenSessionEntryPoint()
and TA_CloseSessionEntryPoint()
:
These functions manage the session lifecycle. In this example, they
also don’t perform complex operations, but in a real-world scenario, you
might allocate session-specific resources or perform authentication
checks here.store_key(param_types, params)
: This function is responsible for taking the APT key data from the Normal World (via params[0].memref.buffer
) and storing it in secure persistent storage. It uses TEE_CreatePersistentObject
to create a new object with a specified ID (APT_KEY_OBJECT_ID
) and then writes the key data to it. If an object with the same ID already exists, TEE_CreatePersistentObject
will fail, preventing accidental overwrites. For simplicity, this
example assumes the key is stored once. In a more robust solution, you
might add logic to update an existing key or delete it first.
TEE_STORAGE_PRIVATE
: Specifies that the object should
be stored in the TA’s private persistent storage, meaning only this
specific TA can access it.TEE_DATA_FLAG_ACCESS_WRITE_META
: Allows writing to the object’s metadata (e.g., size) and data.retrieve_key(param_types, params)
: This function reads the APT key data from secure persistent storage and returns it to the Normal World. It uses TEE_OpenPersistentObject
to open the previously stored object and then TEE_ReadObjectData
to read its content into the buffer provided by the Normal World (params[0].memref.buffer
).
TEE_DATA_FLAG_ACCESS_READ
: Specifies that the object is opened for reading.TA_InvokeCommandEntryPoint()
: This is the main dispatcher. It receives the cmd_id
from the Client Application and calls the appropriate internal function (store_key
or retrieve_key
)
based on the command. It also performs basic parameter type checking to
ensure the Normal World is sending the correct data types.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:
SRCS
: Lists the C source files that make up your TA. In this case, it’s just apt_key_ta.c
.CFLAGS += -I../include
: Adds the ../include
directory to the compiler’s include path, so apt_key_ta.h
can be found.TA_NAME
: Defines the name of the resulting TA binary. This is important for the Client Application to find the correct TA.include $(TA_DEV_KIT_DIR)/mk/ta_dev_kit.mk
: This line
is crucial. It pulls in the standard build rules provided by the OP-TEE
Trusted Application Development Kit (TA-DEV-KIT). This kit provides all
the necessary toolchain configurations, linker scripts, and rules to
compile your TA for the Secure World.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.
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) {
"Usage: %s <store|retrieve> [key_data]\n", argv[0]);
fprintf(stderr, return 1;
}
/* Initialize a context connecting us to the TEE */
res = TEEC_InitializeContext(NULL, &ctx);if (res != TEEC_SUCCESS)
1, "TEEC_InitializeContext failed with code 0x%x", res);
errx(
/* Open a session to the TA */
res = TEEC_OpenSession(&ctx, &sess, &uuid, TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);if (res != TEEC_SUCCESS)
1, "TEEC_OpenSession failed with code 0x%x origin 0x%x", res, err_origin);
errx(
0, sizeof(op)); // Clear the operation structure
memset(&op,
if (strcmp(argv[1], "store") == 0) {
if (argc < 3) {
"Usage: %s store <key_data>\n", argv[0]);
fprintf(stderr,
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);0].memref.buffer = argv[2];
op.params[0].memref.size = strlen(argv[2]);
op.params[
if (op.params[0].memref.size > MAX_KEY_SIZE) {
"Key data too large (max %d bytes)\n", MAX_KEY_SIZE);
fprintf(stderr,
TEEC_CloseSession(&sess);
TEEC_FinalizeContext(&ctx);return 1;
}
"Storing key: \"%s\" (size: %zu)\n", (char *)op.params[0].memref.buffer, op.params[0].memref.size);
printf(
// Invoke the store command in the TA
res = TEEC_InvokeCommand(&sess, TA_APT_KEY_CMD_STORE_KEY, &op, &err_origin);if (res != TEEC_SUCCESS)
1, "TEEC_InvokeCommand(STORE_KEY) failed with code 0x%x origin 0x%x", res, err_origin);
err(
"Key stored successfully.\n");
printf(
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);0].memref.buffer = key_buffer;
op.params[0].memref.size = MAX_KEY_SIZE;
op.params[
// Invoke the retrieve command in the TA
res = TEEC_InvokeCommand(&sess, TA_APT_KEY_CMD_RETRIEVE_KEY, &op, &err_origin);if (res != TEEC_SUCCESS)
1, "TEEC_InvokeCommand(RETRIEVE_KEY) failed with code 0x%x origin 0x%x", res, err_origin);
err(
// Print only the raw key data to stdout
// Ensure no newline is added if the key itself contains one, or if it's binary
"%.*s", (int)op.params[0].memref.size, (char *)op.params[0].memref.buffer);
fprintf(stdout,
else {
} "Invalid command: %s. Use 'store' or 'retrieve'.\n", argv[1]);
fprintf(stderr,
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:
tee_client_api.h
for the OP-TEE Client API functions and apt_key_ta.h
for the TA’s UUID and command IDs.main
function: This is the entry point
of the Client Application. It parses command-line arguments to
determine whether to store or retrieve a key.TEEC_InitializeContext(NULL, &ctx)
: Establishes a connection to the TEE. The first argument NULL
means we’re using the default TEE instance.TEEC_OpenSession(&ctx, &sess, &uuid, ...)
: Opens a session with the Trusted Application identified by uuid
. TEEC_LOGIN_PUBLIC
indicates a public login, suitable for this example. In more complex scenarios, you might use secure authentication methods.TEEC_InvokeCommand(&sess, cmd_id, &op, &err_origin)
: This is the core function call to the TA. It takes the session handle, the command ID (TA_APT_KEY_CMD_STORE_KEY
or TA_APT_KEY_CMD_RETRIEVE_KEY
), an op
(operation) structure containing the parameters, and a pointer to err_origin
to get more details on errors.TEEC_Operation op
and op.paramTypes
: The TEEC_Operation
structure is used to pass parameters to and from the TA. op.paramTypes
defines the types of the four possible parameters. For storing, we use TEEC_MEMREF_TEMP_INPUT
to pass a temporary memory buffer as input. For retrieving, we use TEEC_MEMREF_TEMP_OUTPUT
to receive data into a temporary memory buffer.op.params[0].memref.buffer
and op.params[0].memref.size
: These fields within the TEEC_Operation
structure are used to point to the data buffer and specify its size. For input, size
is the actual data length. For output, size
is the maximum buffer capacity, and after the call, it will be updated with the actual data length received from the TA.errx
and err
for basic error reporting, which are common in simple C programs.TEEC_CloseSession(&sess)
and TEEC_FinalizeContext(&ctx)
: These functions are called at the end to clean up the session and context, releasing resources.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:
BIN
: Defines the name of the resulting Client Application executable.SRCS
: Lists the C source files for the Client Application.CFLAGS += -I../include
: Adds the ../include
directory to the compiler’s include path.LDADD += -lteec
: This is crucial for linking. It tells the linker to include the libteec
library, which provides the OP-TEE Client API functions.include $(OPTEE_CLIENT_DIR)/mk/host_lib.mk
: This line pulls in standard build rules for OP-TEE host applications, simplifying the build process.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
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:
apt_key_ta/ta
and run make
. This will produce the apt_key_ta.ta
binary.apt_key_ta/host
and run make
. This will produce the apt_key_client
executable.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.“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:
MAX_KEY_SIZE
in the CA is an example. Ensure it’s large enough to accommodate your actual APT signing keys (GPG keys can be quite large).TEEC_LOGIN_IDENTIFY
and passing client credentials during TEEC_OpenSession
.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.
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.
APT’s authentication mechanism is built around OpenPGP (GPG) signatures. Here’s a simplified overview of the process:
/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.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].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.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.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
).Packages
file, which was itself verified via the Release
file.Key Storage Locations for APT:
/etc/apt/trusted.gpg
: The traditional global keyring (deprecated for direct use)./etc/apt/trusted.gpg.d/
: Directory for individual trusted public keys (recommended for manually added keys).signed-by
option in sources.list
or sources.list.d/
files: This allows specifying a direct path to a public key file for a
specific repository, offering better isolation and security.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:
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
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.
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:
apt_key_client
with the retrieve
command.apt_key_client
has been modified to print only the raw key data to standard output when the retrieve
command is used. This is crucial for the script to function correctly as a key provider.2>&1
redirects stderr
to stdout
to capture any error messages from the client, though ideally, the client should only output the key on success.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:
deb [signed-by=/usr/local/bin/get_apt_key_from_tee.sh]
: This tells APT to execute the get_apt_key_from_tee.sh
script. APT will then use the standard output of this script as the
public key for verifying the repository. This means every time apt update
is run for this repository, the public key will be dynamically retrieved from the TEE via our client application.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:
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.
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
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)"
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
.
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.
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
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:
poky
: The core Yocto Project reference distribution. Provides the build system, OpenEmbedded Core, and essential recipes.
git://git.yoctoproject.org/poky
kirkstone
, dunfell
, honister
).meta-openembedded
: A collection of additional layers providing a vast array of software. You will likely need meta-oe
, meta-python
, meta-networking
, etc., depending on your project’s requirements.
git://git.openembedded.org/meta-openembedded
meta-security
: Contains security-related components, including the meta-optee
layer.
git://git.yoctoproject.org/meta-security
meta-arm
: Provides ARM-specific board
support packages (BSPs) and configurations, including recipes for ARM
Trusted Firmware-A (TF-A) and U-Boot for ARM platforms.
git://git.yoctoproject.org/meta-arm
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.
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).
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:
CONFIG_ARM_TRUSTZONE=y
: Enables generic ARM TrustZone support.CONFIG_OPTEE=y
: Enables the OP-TEE driver in the Linux kernel, allowing the Normal World to communicate with the TEE.CONFIG_OPTEE_SHM_NUM_BUFFERS=y
: Configures the shared memory buffers used for communication between the Normal World and Secure World.CONFIG_TEE=y
: Generic TEE subsystem support.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
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:
.dts
or .dtsi
files) plays a crucial role in partitioning resources (memory,
peripherals) between the Normal and Secure Worlds. You will need to
ensure your device tree includes the necessary nodes for OP-TEE, such
as:
optee
node specifying the shared memory regions.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.
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.
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:
SRC_URI
: Points to a tarball containing your apt_key_ta
source code (host, ta, include directories). You would create this tarball from your local development environment.inherit optee-ta-common
: This is crucial. It pulls in
the build system configurations and variables required to cross-compile a
Trusted Application for OP-TEE.OPTEE_TA_UUID
and OPTEE_TA_NAME
: These variables are used by the optee-ta-common
class to configure the build process and ensure the correct UUID and name are used.OPTEE_TA_INSTALL_DIR
: Specifies where the compiled TA binary (.ta
file) will be installed on the target root filesystem.S
: Sets the source directory for the build, pointing to the ta
subdirectory within your extracted tarball.do_install()
: Overrides the default install task to ensure your TA binary is copied to the correct location on the target rootfs.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:
SRC_URI
: Points to the same tarball as the TA recipe, as both share the same source repository.inherit autotools
: This is a common inheritance for user-space applications. If your host/Makefile
is very simple, you might need to override do_configure
and do_compile
to just call oe_runmake
.RDEPENDS_${PN} += "optee-client apt-key-ta"
: This is critical. It tells Yocto that your apt-key-client
recipe depends on optee-client
(which provides libteec
) and your apt-key-ta
(to ensure the TA is built and available).S
: Sets the source directory for the build, pointing to the host
subdirectory.do_install()
: Installs the compiled apt_key_client
executable to the standard binary directory (/usr/bin
) on the target rootfs.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:
SRC_URI
: Points directly to your get_apt_key_from_tee.sh
script.RDEPENDS_${PN} += "apt-key-client"
: Ensures that the apt-key-client
is built and installed before this script.do_install()
: Installs the script to /usr/bin
and sets it as executable.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.
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:
poky
, meta-openembedded
, meta-security
, meta-arm
) are compatible with each other and with your desired Yocto release.MACHINE
variable in local.conf
must be set correctly for your specific ARM64 board. This determines which BSPs and device trees are used.arm64
target.This concludes the Yocto integration guidance. The final phase will be to consolidate all the code and documentation into a deliverable package.