Custom Service DevelopmentΒΆ
OverviewΒΆ
A custom service is a process that runs in Bunker OS and extends the Bunkers platform with application-specific logic requiring privileged access, hardware interfaces, cryptographic coprocessors, regulated storage, or proprietary algorithms that Open World must not reach directly. It connects to the Turmux daemon, registers one or more method names, and handles incoming requests from Open World over the vsock channel. From Open World, calling a custom service is identical to calling any built-in Bunker OS service: a Turmux client invokes a named method and receives a response. The difference lies entirely on the Bunker OS side, where the service has access to resources that are architecturally isolated from the untrusted environment.
The security boundary between Open World and Bunker OS is enforced by the hypervisor. A Open World process cannot connect to any Bunker OS resource except through the vsock interface, and the Turmux daemon only dispatches calls to methods that have been explicitly registered by a provider. This means the attack surface of a custom service is limited to its registered method names and the validation it performs on those inputs. Even a fully compromised Open World environment cannot interact with a service beyond what that service was designed to accept.
This page describes the full development workflow for a custom service, using a DRM license verification service as the worked example. The scenario is an Open World application that must verify digital licenses for protected content. The verification requires a device-specific private key that must never be exposed to Open World. A Bunker OS service implements a proprietary license verification algorithm and holds a persistent OP-TEE session to perform cryptographic operations using the device key. Open World applications request license verification over vsock without any knowledge of the key, the algorithm internals, or the underlying OP-TEE infrastructure.
ArchitectureΒΆ
A custom service participates in a layered stack. At the bottom, a privileged resource. In this example an OP-TEE Trusted Application running in ARM TrustZone Secure World holds the device-specific private key and performs cryptographic operations. Above that, a Host Application in Bunker OS acts as a bridge: it is simultaneously a Turmux provider (registering methods and handling the RPC message loop) and an OP-TEE client (opening sessions and invoking TA commands via libteec). The Host Application implements the proprietary license verification algorithm, calling into the TA to use the private key for verification. The Turmux daemon dispatches calls arriving from Open World over vsock to the Host Application. At the top, the Open World application uses a Turmux client binding to request license verification.
graph TD
subgraph ML["Open World"]
App["Media Player Application (Python)"]
TC["Turmux Client (turmux-python / turmux-c)"]
App --> TC
end
TC -->|"virtio-vsock transport"| Daemon
subgraph LC["Bunker OS"]
Daemon["Turmux daemon (turmux-daemon)"]
HA["DRM License Service (Turmux provider + libteec + proprietary algorithm)"]
TEEDrv["OP-TEE Driver (/dev/tee0)"]
Daemon --> HA
HA --> TEEDrv
end
TEEDrv -->|"Secure Monitor Call (SMC)"| OPTEE
subgraph SW["Secure World β ARM TrustZone"]
OPTEE["OP-TEE OS"]
TA["DRM Trusted Application cryptographic verification"]
SS["Secure Storage HUK-encrypted, device-bound private key"]
OPTEE --> TA
TA --> SS
end
The vsock channel carries Protobuf-framed Turmux messages between Open World and the daemon. The daemon inspects only the method field of each request envelope before forwarding the opaque payload bytes to the registered provider. The Host Application holds a persistent OP-TEE session for the process lifetime; each incoming Turmux request results in a single TEEC_InvokeCommand call into Secure World. The Trusted Application accesses the encryption key exclusively through the OP-TEE Secure Storage API: the key material exists in plaintext only inside the TEE and only while an operation is in progress. Open World has no access to /dev/tee0 and cannot reach the TA directly.
Worked Example: DRM License Verification ServiceΒΆ
Context and Design DecisionsΒΆ
A media player running in Open World receives digital content and associated license files. The licenses are digitally signed to prove they were issued by the license authority and have not been tampered with. If Open World is fully compromised, it cannot be trusted to verify licenses using a shared secret or private key. The solution is to keep the device-specific private key in OP-TEE Secure Storage and delegate license verification to a trusted Bunker OS service.
The service exposes one primary method: drm.verify_license accepts a license blob and device identifier, internally invokes cryptographic operations in the OP-TEE TA using the private key, and returns a verification result with license metadata (expiry, content ID, etc.). The algorithm that interprets the license structure and derives the verification data is proprietary and runs only in Bunker OS; Open World never sees the algorithm or the key.
The service contract between Open World and Bunker OS is the Protobuf schema. Both the Host Application (compiled for Bunker OS) and the Open World client are built from the same .proto file. Changing the schema requires coordinated redeployment of both sides.
Protocol DefinitionΒΆ
File: ``bunkers/drm/v1/license.proto``
syntax = "proto3";
package bunkers.drm.v1;
// Verify a digital license. The TA uses the device-specific private key
// to perform cryptographic verification. Returns the license validity,
// expiry timestamp, and associated content identifiers.
message VerifyLicenseRequest {
string device_id = 1; // Expected device identifier
bytes license_blob = 2; // Signed license data (proprietary format)
}
message VerifyLicenseResponse {
bool valid = 1; // License is valid and not expired
uint64 expiry_time = 2; // Unix timestamp when license expires
string content_id = 3; // Associated content identifier
string issuer = 4; // License issuer name
}
VerifyLicense accepts a license blob (signed binary data in a proprietary format) and the expected device identifier. It returns a response containing the verification result and license metadata. The Host Application deserializes the license blob, extracts the signature, and calls into the OP-TEE TA to verify it using the device-specific private key. If the signature is valid and the license is not expired, the response contains valid=true and the license metadata; otherwise valid=false. The TA never exposes the private key or intermediate verification data to Open World.
The Trusted ApplicationΒΆ
The OP-TEE Trusted Application runs inside TrustZone Secure World. It is compiled to a signed .ta binary, loaded by OP-TEE OS when the Host Application opens a session, and unloaded when the session closes. It implements a single command that performs license verification: loading the device-specific private key from Secure Storage, extracting the signature and license data from the blob supplied by the Host Application, and verifying the signature cryptographically. The private key material lives exclusively in Secure World: it is pre-provisioned during device manufacturing and persisted in OP-TEE Secure Storage encrypted under the Hardware Unique Key.
TA Entry PointsΒΆ
Every OP-TEE TA exposes five fixed entry points. The command dispatcher TA_InvokeCommandEntryPoint validates the command ID and delegates to the appropriate handler.
#include <tee_internal_api.h>
#include <tee_internal_api_extensions.h>
#include "drm_ta.h"
#define TA_DRM_CMD_VERIFY_LICENSE 0
#define RSA_KEY_BITS 2048
#define SHA256_HASH_SIZE 32
TEE_Result TA_CreateEntryPoint(void) { return TEE_SUCCESS; }
void TA_DestroyEntryPoint(void) {}
TEE_Result TA_OpenSessionEntryPoint(uint32_t param_types,
TEE_Param params[4],
void **sess_ctx)
{
(void)param_types; (void)params; (void)sess_ctx;
return TEE_SUCCESS;
}
void TA_CloseSessionEntryPoint(void *sess_ctx) { (void)sess_ctx; }
TEE_Result TA_InvokeCommandEntryPoint(void *sess_ctx,
uint32_t cmd_id,
uint32_t param_types,
TEE_Param params[4])
{
(void)sess_ctx;
switch (cmd_id) {
case TA_DRM_CMD_VERIFY_LICENSE: return cmd_verify_license(param_types, params);
default: return TEE_ERROR_BAD_PARAMETERS;
}
}
License VerificationΒΆ
cmd_verify_license performs the core DRM operation: it retrieves the device-specific RSA private key from Secure Storage, extracts the license structure and signature from the input blob (a proprietary format defined by the issuer), and verifies the signature using RSA-PSS over SHA-256. The verification fails explicitly if the signature does not match, if the license is expired, or if the device ID in the license does not match the expected device ID. The Host Application propagates the verification result (valid/invalid) and license metadata to Open World.
The Host ApplicationΒΆ
The Host Application is a C process in Bunker OS that holds two simultaneous roles: a Turmux provider, registering the drm.verify_license method and running the RPC event loop; and an OP-TEE client, maintaining a persistent OP-TEE session and translating each Turmux request into a TEEC_InvokeCommand call. The Host Application also implements the proprietary license format parser and additional application-level logic (e.g., caching, rate limiting, audit logging). The OP-TEE TA performs only the cryptographic verification; all other interpretation is handled here.
Initialising the OP-TEE SessionΒΆ
The OP-TEE context and session are opened once at startup and kept alive for the process lifetime. Session creation involves a Secure Monitor Call into Secure World and triggers TA loading; keeping the session persistent ensures that per-call latency is dominated by the RSA verification rather than session setup overhead.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <tee_client_api.h>
#include <turmux.h>
#include "drm_ta.h" /* TA UUID and command constants */
#include "bunkers/drm/v1/license.pb-c.h"
static TEEC_Context tee_ctx;
static TEEC_Session tee_sess;
static int init_optee(void)
{
const TEEC_UUID uuid = TA_DRM_UUID;
uint32_t err_origin;
TEEC_Result res;
res = TEEC_InitializeContext(NULL, &tee_ctx);
if (res != TEEC_SUCCESS) {
fprintf(stderr, "TEEC_InitializeContext: 0x%x\n", res);
return -1;
}
res = TEEC_OpenSession(&tee_ctx, &tee_sess, &uuid,
TEEC_LOGIN_PUBLIC, NULL, NULL, &err_origin);
if (res != TEE_SUCCESS) {
fprintf(stderr, "TEEC_OpenSession: 0x%x (origin 0x%x)\n",
res, err_origin);
TEEC_FinalizeContext(&tee_ctx);
return -1;
}
return 0;
}
Provider Event LoopΒΆ
After connecting to the Turmux daemon and registering the drm.verify_license method name, the provider enters a blocking loop: read the next request, deserialize the protobuf, construct a license blob in the format expected by the TA, call TEEC_InvokeCommand to invoke the TA, and serialize the response back into a protobuf. The handle_verify_license implementation below illustrates the pattern.
static void handle_verify_license(turmux_client_t *client,
uint32_t msg_id,
const uint8_t *payload, uint32_t payload_len)
{
Bunkers__Drm__V1__VerifyLicenseRequest *req =
bunkers__drm__v1__verify_license_request__unpack(NULL, payload_len, payload);
if (!req) { turmux_send_error(client, msg_id, "invalid payload"); return; }
/* Assemble license blob in proprietary format:
[device_id_len (2B) || device_id || expiry (8B) || content_id || signature]
This is the format the TA expects. The Host Application can add any
application-specific verification here (rate limiting, caching, etc.). */
size_t blob_size = 2 + req->device_id.len + 8 + strlen(req->content_id) + 256;
uint8_t *blob = malloc(blob_size);
/* Copy device_id_len, device_id, expiry_time into blob (simplified layout) */
uint16_t *dev_id_len = (uint16_t *)blob;
*dev_id_len = req->device_id.len;
TEE_MemMove(blob + 2, req->device_id.data, req->device_id.len);
uint64_t *exp_ptr = (uint64_t *)(blob + 2 + req->device_id.len);
*exp_ptr = req->expiry_time;
/* ... rest of blob assembly ... */
TEEC_Operation op = {0};
op.paramTypes = TEEC_PARAM_TYPES(TEEC_MEMREF_TEMP_INPUT,
TEEC_MEMREF_TEMP_INPUT,
TEEC_MEMREF_TEMP_OUTPUT,
TEEC_NONE);
op.params[0].tmpref.buffer = (void *)req->device_id.data;
op.params[0].tmpref.size = req->device_id.len;
op.params[1].tmpref.buffer = (void *)req->license_blob.data;
op.params[1].tmpref.size = req->license_blob.len;
op.params[2].tmpref.buffer = malloc(64);
op.params[2].tmpref.size = 64;
uint32_t err_origin;
TEEC_Result res = TEEC_InvokeCommand(&tee_sess, TA_DRM_CMD_VERIFY_LICENSE,
&op, &err_origin);
bunkers__drm__v1__verify_license_request__free_unpacked(req, NULL);
Bunkers__Drm__V1__VerifyLicenseResponse resp =
BUNKERS__DRM__V1__VERIFY_LICENSE_RESPONSE__INIT;
if (res == TEE_SUCCESS) {
resp.valid = 1;
resp.expiry_time = *exp_ptr;
resp.content_id = "content_123";
resp.issuer = "license_authority";
} else {
resp.valid = 0;
resp.expiry_time = 0;
}
size_t resp_len = bunkers__drm__v1__verify_license_response__get_packed_size(&resp);
uint8_t *resp_buf = malloc(resp_len);
bunkers__drm__v1__verify_license_response__pack(&resp, resp_buf);
turmux_send_response(client, msg_id, resp_buf, resp_len);
free(resp_buf);
free(blob);
free(op.params[2].tmpref.buffer);
}
int main(void)
{
if (init_optee() != 0) return 1;
turmux_client_t *client = turmux_connect_default();
if (!client) {
fprintf(stderr, "cannot connect to Turmux daemon\n");
return 1;
}
turmux_register(client, "drm.verify_license");
fprintf(stdout, "DRM License Service ready\n");
char method[128];
uint8_t *payload = NULL;
uint32_t payload_len = 0;
uint32_t msg_id = 0;
for (;;) {
int rc = turmux_read_request(client, method, sizeof(method),
&payload, &payload_len, &msg_id);
if (rc != TURMUX_OK) break;
if (strncmp(method, "drm.verify_license", 20) == 0)
handle_verify_license(client, msg_id, payload, payload_len);
else
turmux_send_error(client, msg_id, "unknown method");
turmux_free(payload);
}
TEEC_CloseSession(&tee_sess);
TEEC_FinalizeContext(&tee_ctx);
turmux_close(client);
return 0;
}
static TEE_Result cmd_verify_license(uint32_t param_types, TEE_Param params[4])
{
TEE_Result res;
TEE_ObjectHandle key = TEE_HANDLE_NULL;
TEE_OperationHandle op = TEE_HANDLE_NULL;
uint32_t exp_pt = TEE_PARAM_TYPES(TEE_PARAM_TYPE_MEMREF_INPUT,
TEE_PARAM_TYPE_MEMREF_INPUT,
TEE_PARAM_TYPE_MEMREF_OUTPUT,
TEE_PARAM_TYPE_NONE);
if (param_types != exp_pt) return TEE_ERROR_BAD_PARAMETERS;
const char *device_id = params[0].memref.buffer;
size_t device_id_len = params[0].memref.size;
uint8_t *license_blob = params[1].memref.buffer;
size_t blob_len = params[1].memref.size;
uint8_t *out_buf = params[2].memref.buffer;
size_t out_size = params[2].memref.size;
if (out_size < 64) /* Minimum output: packed protobuf response */
return TEE_ERROR_SHORT_BUFFER;
/* Load device-specific private key from Secure Storage */
res = TEE_OpenPersistentObject(TEE_STORAGE_PRIVATE, "device_key", 10,
TEE_DATA_FLAG_ACCESS_READ, &key);
if (res != TEE_SUCCESS) return res;
/* Allocate RSA-PSS verification operation */
res = TEE_AllocateOperation(&op, TEE_ALG_RSASSA_PKCS1_PSS_MGF1_SHA256,
TEE_MODE_VERIFY, RSA_KEY_BITS);
if (res != TEE_SUCCESS) goto out;
res = TEE_SetOperationKey(op, key);
if (res != TEE_SUCCESS) goto out;
/* Parse license blob: first 2 bytes = device_id length, then device_id,
then expiry_time (8 bytes), content_id (null-terminated string),
then SHA256 hash of the above, then RSA-PSS signature */
uint16_t *blob_device_id_len = (uint16_t *)license_blob;
uint8_t *blob_device_id = license_blob + 2;
size_t pos = 2 + *blob_device_id_len;
if (pos + 8 + SHA256_HASH_SIZE > blob_len) {
res = TEE_ERROR_BAD_PARAMETERS;
goto out;
}
/* Verify device ID matches */
if (*blob_device_id_len != device_id_len ||
TEE_MemCompare(blob_device_id, device_id, device_id_len) != 0) {
res = TEE_ERROR_ACCESS_DENIED; /* Device ID mismatch */
goto out;
}
/* Extract expiry time and verify it's not expired */
uint64_t *expiry_time_ptr = (uint64_t *)(license_blob + pos);
uint64_t expiry_time = *expiry_time_ptr;
uint64_t current_time = TEE_GetSystemTime();
if (current_time > expiry_time) {
res = TEE_ERROR_ACCESS_DENIED; /* License expired */
goto out;
}
pos += 8;
/* The data to verify is everything up to (but not including) the signature */
size_t sig_size = 256; /* RSA-2048 produces 256-byte signature */
if (pos + sig_size > blob_len) {
res = TEE_ERROR_BAD_PARAMETERS;
goto out;
}
size_t data_len = blob_len - sig_size;
uint8_t *signature = license_blob + data_len;
/* Verify RSA-PSS signature over the license data */
res = TEE_VerifySignature(op, NULL, 0, license_blob, data_len,
signature, sig_size);
if (res != TEE_SUCCESS) {
res = TEE_ERROR_SIGNATURE_INVALID;
goto out;
}
/* Signature valid and license not expired. Return success. */
params[2].memref.size = 1; /* Output: single byte indicating success */
((uint8_t *)out_buf)[0] = 1;
out:
if (op) TEE_FreeOperation(op);
if (key) TEE_CloseObject(key);
return res;
}
Calling from Open WorldΒΆ
The Open World application uses turmux-python to call the license verification service. When the media player receives a content license, it submits it to Bunker OS for verification. The verification result determines whether playback is allowed.
import turmux_python
from bunkers.drm.v1 import license_pb2
import time
BUNKER_OS_CID = 3
TURMUX_SERVICE_PORT = 1024
DEVICE_ID = "device_12345abc"
def _client():
return turmux_python.TurmuxClient.connect_vsock(BUNKER_OS_CID, TURMUX_SERVICE_PORT)
def verify_license(license_blob: bytes, device_id: str) -> dict:
"""Verify a digital license with the DRM service in Bunker OS."""
req = license_pb2.VerifyLicenseRequest(
device_id=device_id,
license_blob=license_blob
)
raw = _client().call("drm.verify_license", req.SerializeToString())
resp = license_pb2.VerifyLicenseResponse()
resp.ParseFromString(raw)
return {
"valid": resp.valid,
"expiry_time": resp.expiry_time,
"content_id": resp.content_id,
"issuer": resp.issuer
}
if __name__ == "__main__":
# Load a license file received from the license authority
with open("/opt/licenses/content.lic", "rb") as f:
license_blob = f.read()
# Ask Bunker OS to verify the license
result = verify_license(license_blob, DEVICE_ID)
if result["valid"]:
current_time = int(time.time())
if current_time < result["expiry_time"]:
print(f"License valid for content: {result['content_id']}")
print(f"Issued by: {result['issuer']}")
print(f"Expires at: {result['expiry_time']}")
# Proceed with playback
else:
print("License has expired")
else:
print("License verification failed - playback denied")
Build and DeploymentΒΆ
The workflow described below follows the standard OP-TEE build and integration pattern. The Bunker for Linux distribution includes pre-made Yocto meta-layers and recipes that automate much of this process. For detailed information on available meta-layers, build configuration, and device-specific customizations, see Meta Layers Reference.
Building the TAΒΆ
The Trusted Application is built with the OP-TEE TA Development Kit. The resulting .ta binary is signed during the build using the TA signing key configured in the OP-TEE OS build; unsigned binaries are rejected by OP-TEE OS at load time.
# TA_DEV_KIT_DIR must point to the optee_os export directory
# for your target platform (e.g. NXP i.MX8 or AMD Ultrascale+)
make -C ta/ \
TA_DEV_KIT_DIR=/path/to/optee_os/export-ta_arm64 \
CROSS_COMPILE=aarch64-none-linux-gnu-
# Output: ta/<UUID>.ta (ELF, signed TA binary)
Building the Host ApplicationΒΆ
The Host Application links against libteec (from the optee_client package) and libturmux_c (from the Turmux workspace). The protobuf C stubs are generated from the shared schema file.
# Generate protobuf C stubs from the shared schema
protoc --c_out=. bunkers/drm/v1/license.proto
# Generates: bunkers/drm/v1/license.pb-c.c, license.pb-c.h
# Cross-compile for the target (aarch64 Bunker OS)
aarch64-none-linux-gnu-gcc \
-o drm_host \
drm_host.c \
bunkers/drm/v1/license.pb-c.c \
-I/path/to/optee_client/public \
-I/path/to/turmux/crates/turmux-c/include \
-L/path/to/optee_client/out/export/usr/lib \
-L/path/to/turmux/target/aarch64-unknown-linux-gnu/release \
-lteec -lturmux_c -lprotobuf-c
Installing into the ImageΒΆ
Both artefacts are integrated through the Yocto build. The TA binary goes into the OP-TEE TA store on the Secure World filesystem; the Host Application binary goes into the Bunker OS rootfs with a systemd unit so it starts automatically after turmuxd.
# TA β installed to the OP-TEE dynamic TA store
install -m 0444 ta/<UUID>.ta \
${D}${nonarch_base_libdir}/optee_armtz/
# Host Application binary and systemd unit
install -m 0755 drm_host ${D}/usr/bin/
install -m 0644 drm-license-service.service \
${D}${systemd_system_unitdir}/
A minimal systemd unit for the Host Application. The Requires and After directives ensure the Turmux daemon is accepting connections before the Host Application attempts to register its methods.
[Unit]
Description=Bunkers DRM License Service (OP-TEE bridge)
After=turmuxd.service
Requires=turmuxd.service
[Service]
ExecStart=/usr/bin/drm_host
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
Security ConsiderationsΒΆ
The device-specific RSA private key is generated and provisioned during device manufacturing, stored exclusively in OP-TEE Secure Storage encrypted under the Hardware Unique Key (HUK), and never exposed to Normal World in any form. The Host Application and the Open World client handle only the opaque device ID and license blob; they cannot observe, derive, or export the private key material under any circumstances.
The primary trust boundary is the inputs arriving over vsock from Open World. The device_id field should be validated as a printable ASCII string of bounded length with no path-separator characters. The license_blob field should be checked against a maximum size before the Host Application allocates intermediate buffers. If license blobs exceed 1 MiB, the Open World client should negotiate a higher Turmux message size limit with $/setMaxMsgSize immediately after connecting and before sending any large payload.
The TA validates param_types at the entry point of every command before touching any parameter. This is an OP-TEE hard requirement: a compromised Host Application could pass unexpected parameter type combinations, and the TA must reject them explicitly rather than relying on Normal World to behave correctly. The RSA-PSS signature verification in cmd_verify_license provides the cryptographic guarantee: even if the license blob is tampered with in transit between Open World and Bunker OS, the signature verification will fail with TEE_ERROR_SIGNATURE_INVALID before any output is produced.
TestingΒΆ
The Bunker platform includes a complete QEMU-based emulation environment that allows you to test custom services end-to-end without physical hardware. This environment is pre-configured for supported platforms, allowing you to validate your service implementation, test protocol contracts, and experiment with different configurations. See Porting to New Platforms for the list of supported platforms and instructions on setting up the QEMU environment for your target architecture.
Unit testing the TA logic is done with the OP-TEE xtest framework, which can run TAs in a software emulation layer on a development machine. For integration testing with the full Turmux stack, a QEMU-based OP-TEE environment allows the complete path β Open World client, vsock, Turmux daemon, Host Application, OP-TEE emulation, and TA β to be exercised on a Linux host without physical hardware.
For CI environments that lack QEMU, the Host Application can be built with a -DMOCK_OPTEE compile-time flag that replaces TEEC_InvokeCommand with a stub returning a valid verification. This allows the protobuf encoding, Turmux framing, message routing, and provider event loop to be verified independently of OP-TEE.
Next StepsΒΆ
The pattern described here generalises to any privileged resource that must be isolated from Open World. Replace the TEEC_InvokeCommand calls with calls to a TPM, a hardware security module, a kernel driver for a hardware accelerator, or any other interface accessible only from Bunker OS. The Turmux provider event loop and the Protobuf schema design remain the same regardless of what the Host Application does behind the provider layer.
For reference material on the components used in this page:
Communication Layer (vsock) β vsock transport, message framing, and the protocol stack
Reference RPC Implementation: Turmux β the full Turmux API reference including all language bindings
Services API Reference β the built-in Bunker OS services (logger, storage, telemetry, monitoring)