SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
D1 — The chip-access discipline: gate, lock, touch-hold

Learning goal

The OPTIGA Trust M is shared by every task that wants a credential, a signature, a certificate — and it shares its I2C block with the CM55 touch controller. Three mechanisms keep that from turning into bus collisions, and each has an exact pairing rule:

Mechanism API What it answers Pairing
Gate optiga_chip_enter() / optiga_chip_exit() "does another task hold the chip right now?" one exit per successful enter
Lock optiga_manager_lock() / optiga_manager_unlock() "is the manager up, and may I have it?" one unlock per successful lock; aliases onto the gate
Touch hold optiga_manager_touch_hold[_reason]() / optiga_manager_touch_release() "keep CM55 off the shared I2C block" counted; strict 1:1 on every exit path

Plus the acquire/release pair — optiga_manager_acquire() returns the optiga_util_t * you drive the chip with, and optiga_manager_release() is an alias for optiga_chip_exit().

The failure signature you are learning to recognise is OPTIGA_COMMS_ERROR (0x0102): the touch controller and the secure element were driven on the same SCB at the same time.

The real firmware sequence

Enter versus lock — the truth about what each checks

int optiga_verify_cert_key_pair(uint16_t cert_oid, uint16_t key_oid)
{
int rc;
/* Two tasks call this: the MQTT subscriber after installing a certificate,
* and the provisioning task when its wait ends. They share cert_pem - a 2 KB
* static - and the library's single global status. The gate is re-entrant, so
* the certificate read and the signature inside still take it, without
* deadlocking on this one. */
return -5; /* the chip is busy elsewhere; a verdict now would be a guess */
}
optiga_manager_touch_hold_reason("Checking certificate against its key");
rc = verify_cert_key_pair_locked(cert_oid, key_oid);
return rc;
}

optiga_chip_enter() is re-entrant per FreeRTOS task (nesting is free) and returns false for exactly one reason: another task holds the chip. That is fatal — return an error, never proceed. What it is not is an initialisation check. When the manager was never initialised it deliberately returns true (tesaiot_optiga_manager.c:157-169): "Reporting failure here is what made every call site fail open, because a caller could not tell 'no lock exists' from 'somebody else has it'." Succeeding when nothing exists and failing only on contention lets denial mean one thing.

optiga_manager_lock() is the opposite: it returns false when never initialised. It is the correct "is the manager up?" test. Skipping optiga_manager_init() before TLS made every CertificateVerify fail because trustm_ecdsa_sign() begins with optiga_manager_lock() (chapter C4, mqtt_mtls_setup.c:184-201). Getting enter-vs-lock backwards is the documented cause of several ungated-access defects.

One exit per successful enter

{
/* Closing under another task's transaction is worse than deferring the
* close: the reference stays, and the next balanced close will do it. */
optiga_lib_print_message("close deferred: the chip is busy elsewhere",
OPTIGA_UTIL_SERVICE, OPTIGA_UTIL_SERVICE_COLOR);
return;
}
optiga_trust_close_application_gated();
}

Never call optiga_chip_exit() after a failed enter — the deferred-close early return exists precisely for that branch. The gate is a depth counter per owning task; an unmatched exit corrupts the count for everyone.

Lock → touch-hold → work → release → unlock

/* ...context: inside trustm_ecdsa_sign() ... */
/* Lock OPTIGA mutex for thread-safe access */
printf("optiga_manager_lock failed\n");
return OPTIGA_LIB_BUSY;
}
/* Keep CM55 touch off SCB5 for the whole transaction — the secure element
* and the touch controller share the bus, and the TLS handshake signs after
* the mTLS setup has already resumed touch polling. */

This is trustm_ecdsa_sign(), the function every TLS handshake ends in. Lock first, then hold touch for the whole transaction — "the secure element and the touch controller share the bus, and the TLS handshake signs after the mTLS setup has already resumed touch polling."

/* ...context: inside trustm_ecdsa_sign(), the single exit path ... */

Release the hold before unlocking. The same order is used in the staged model verify:

/* ...context: inside optiga_verify_staged_model() ... */
return STAGE_SIG_CHIP_ERROR;
}
/* Same bus discipline every other transaction here uses: the secure
* element and the touch controller share SCB5. */

The clearest textbook shape of lock/unlock with every early exit balanced is in tesaiot_crypto.c — shipped reference, not compiled by proj_cm33_ns (it is in no SOURCES list). Read it as idiom, not as a call site:

/* shipped reference, not compiled by proj_cm33_ns */
/* ...context: inside the TRNG random_generate helper ... */
if (!optiga_manager_is_initialized()) {
return TESAIOT_ERROR_NOT_INITIALIZED;
}
return TESAIOT_ERROR_TIMEOUT;
}
volatile optiga_lib_status_t optiga_status = OPTIGA_LIB_BUSY;
optiga_crypt_t *crypt = optiga_manager_create_crypt(crypto_callback,
(void *)&optiga_status);
if (!crypt) {
return TESAIOT_ERROR_OPTIGA;
}
optiga_lib_status_t result = optiga_crypt_random(crypt,
OPTIGA_RNG_TYPE_TRNG,
buffer,
length);
int rc = TESAIOT_ERROR_OPTIGA;
if (result == OPTIGA_LIB_SUCCESS) {
rc = crypto_wait_for_completion(&optiga_status);
}
optiga_crypt_destroy(crypt);

One more ordering rule from the same file family: never open the OPTIGA application while holding the lock — open first, lock second (optiga_trust_helpers.c:4604-4622).

Acquire and release

/* ...context: inside the certificate read helper ... */
// Acquire OPTIGA instance (thread-safe)
optiga_util_t *me_util = optiga_manager_acquire();
if (!me_util) {
printf("%s ERROR: OPTIGA instance not available\n", LABEL_SUBSCRIBER);
vPortFree(cert_der);
return CY_RSLT_TYPE_ERROR;
}
// Perform async read
optiga_lib_status = OPTIGA_LIB_BUSY;
optiga_lib_status_t status = optiga_util_read_data(me_util, oid, 0, cert_der, &cert_len);
if (OPTIGA_LIB_SUCCESS != status) {
printf("%s ERROR: OPTIGA read failed (0x%04X)\n", LABEL_SUBSCRIBER, status);
vPortFree(cert_der);
return CY_RSLT_TYPE_ERROR;
}
// Wait for read completion (max 2 seconds)
TickType_t start = xTaskGetTickCount();
TickType_t timeout = pdMS_TO_TICKS(2000);
while (optiga_lib_status == OPTIGA_LIB_BUSY && (xTaskGetTickCount() - start) < timeout) {
vTaskDelay(pdMS_TO_TICKS(100));
}
if (optiga_lib_status != OPTIGA_LIB_SUCCESS) {
printf("%s ERROR: OPTIGA read timeout/failed (0x%04X)\n", LABEL_SUBSCRIBER, optiga_lib_status);
vPortFree(cert_der);
return CY_RSLT_TYPE_ERROR;
}

optiga_manager_acquire() returns NULL until optiga_manager_init() has run. On NULL, do not call release() — the implementation already exited the gate. On non-NULL, exactly one release() per exit path, errors included — the shipped function has three exits and releases on all three. The async pattern sets optiga_lib_status = OPTIGA_LIB_BUSY before the call and polls after.

Counted holds, for the whole conversation

/* ...context: inside the Protected Update bundle ingest ... */
/* Keep CM55 touch off the bus for the whole ingest.
*
* The secure element and the touch controller share SCB5 on this board.
* The carried-over reference has no touchscreen and so no notion of this;
* dropped in here unchanged, its longest writes lose the bus mid-transfer.
* Observed 2026-08-06 on correlation bento-pu-1: the 8-byte trust anchor
* metadata write succeeded, and the 580-byte certificate write that follows
* failed with 0x0102 — OPTIGA_COMMS_ERROR, the transport layer, not the
* chip refusing anything — after 57 seconds of comms retries.
*
* Held across the entire function rather than per operation: the ingest is
* one long conversation with the chip, and releasing between steps would
* reopen the same window. Counted, so the signature path inside
* trustm_ecdsa_sign() nests without resuming polling early. Every exit runs
* through pu_done, which releases it. */

The hold is counted (s_touch_holds) — nesting inside a callee that also holds is correct and required. The first hold sleeps 50 ms so an in-flight touch transfer can finish. Take it for the whole chip conversation, not per operation: this ingest learned that the hard way when a 580-byte certificate write lost the SCB5 bus mid-transfer and failed with 0x0102. Every exit funnels through one pu_done: label that releases.

The five *_held() wrappers

static bool prov_open_held(void)
{
bool ok;
optiga_manager_touch_hold_reason("Opening the secure element");
return ok;
}
static void prov_close_held(void)
{
optiga_manager_touch_hold_reason("Closing the secure element");
}
static bool prov_make_csr_held(uint16_t key_oid)
{
bool ok;
optiga_manager_touch_hold_reason("Generating a key and signing the request");
ok = prov_make_csr(key_oid);
return ok;
}
static uint16_t prov_manifest_anchor_held(uint16_t oid)
{
uint16_t anchor;
optiga_manager_touch_hold_reason("Reading the secure element");
return anchor;
}
static int prov_publish_pu_held(const char *target, const char *anchor)
{
int rc;
optiga_manager_touch_hold_reason("Generating a key and signing the request");
rc = tesaiot_publish_protected_update(target, anchor, 1U, true);
return rc;
}

prov_open_held, prov_close_held, prov_make_csr_held, prov_manifest_anchor_held, prov_publish_pu_held: each takes a hold with a reason string, does one thing, releases. The comment above them records why they exist: "of the eleven chip operations on this path only four" held touch themselves; key generation and the CSR signature — the two longest transactions — ran with CM55 polling the FT5406 on the same SCB, "which is what returns <tt>OPTIGA_COMMS_ERROR (0x0102)</tt> and then leaves the next call meeting <tt>OPTIGA_UTIL_ERROR_INSTANCE_IN_USE (0x0305)</tt>." And: "Nothing holds across the 60 second wait for the platform: no chip traffic happens there, and freezing the screen for a minute would be its own bug."

The reason strings are not decoration. They travel to CM55 and are shown on the panel while touch is disabled — "Opening the secure element", "Generating a key and signing the request", "Reading the secure element", "Closing the secure element".

A complete example with every early return balanced

/* ...context: inside the synchronous credential read ... */
/* Pause CM55 touch to get exclusive SCB0 I2C access */
optiga_manager_touch_hold_reason("Reading stored credentials");
/* These take s_optiga_mutex but never entered the gate, so they were the
* third mechanism that did not know about the other two. */
xSemaphoreGive(s_optiga_mutex);
return false;
}
bool ok = false;
if (optiga_open()) {
uint16_t len = read_oid(oid, buf, buf_len);
printf("[HSM] cred_read_sync: slot %u OID 0x%04X → %u bytes\r\n",
slot, oid, len);
if (len > 0) {
if (out_len) *out_len = len;
ok = true;
}
optiga_close();
} else {
printf("[HSM] cred_read_sync: optiga_open FAILED (slot %u)\r\n", slot);
}
/* Resume CM55 touch polling */

Hold with reason, then optiga_chip_enter(); on a failed enter, release the hold and give the mutex back before returning. mqtt_mtls_setup.c is the extended version of the same discipline — seven releases for one hold.

The raw IPC_CMD_TOUCH_RESUME ban

CM55 treats IPC_CMD_TOUCH_RESUME as an unconditional touch_disabled = false — it is not a counter (ipc_hsm_handler.c:1393-1405). A raw resume sent from any task cancels a hold that another task is relying on; the next touchpad_read() on CM55 disables and re-enables the shared SCB block under a transfer CM33_NS still has in flight. That is exactly the 0x0102 this page's own provisioning poll used to cause "about 150 times per enrolment, at 400 ms intervals". Always go through optiga_manager_touch_hold_reason() / optiga_manager_touch_release(); never send the raw command. (Appendix X, item 2.)

Step-by-step

These steps use the HSM page because it exercises all three mechanisms; the observables are the panel and the UART.

Step 1 — Watch a hold from the panel

Home → HSM Security → Enrol Certificate (or run any credential read).

What you should observe. The screen's touch goes dead and a reason string appears — one of the strings above, e.g. Opening the secure element then Generating a key and signing the request. When the wrapper releases, touch returns. There is no UART line for hold/release.

Step 2 — See the gate refuse

Start an enrolment and, while its reason string is on screen, request a credential read from the same board (a second HSM page action, or optiga-module call from the REPL on mtb-mpy).

What you should observe. The second operation reports failure without touching the chip — the gate returned false because another task holds it. On the UART, ipc_hsm_cred_read_sync prints nothing on that path; a mutex timeout would print [HSM] cred_read_sync: mutex timeout (slot u) (ipc_hsm_handler.c:2408). The first operation completes normally. This is "denial means one thing": contention, never "not initialised".

Step 3 — Recognise 0x0102

You are not asked to cause this — the shipped code is designed so you cannot from the UI. Recognise it: any chip operation returning OPTIGA_COMMS_ERROR (0x0102), frequently followed by OPTIGA_UTIL_ERROR_INSTANCE_IN_USE (0x0305) on the next call, means a chip transaction ran while CM55 was polling touch on the same SCB. Look for a missing hold, a raw IPC_CMD_TOUCH_RESUME, or an unbalanced release that dropped the count to zero early. On the mTLS path the symptom is [PSA-Sign] ERROR: trustm_ecdsa_sign status=0x0102 (chapter C4).

Step 4 — Recognise an unbalanced release

What you should observe. The panel stays dead after an operation finishes — "an unbalanced release leaves the panel dead". Count holds and releases on every exit path of the function that ran last.

Traps

Trap 1 — Using optiga_chip_enter() as an init check.
It returns true when nothing is initialised. Use optiga_manager_lock() for "is the manager up?". (Appendix X, item 3.)
Trap 2 — Calling exit() / release() after a failed enter() / NULL acquire().
The implementation already exited the gate. Calling exit again unbalances the depth counter for the task that actually holds it.
Trap 3 — Releasing touch per operation instead of per conversation.
Between two chip operations the touch controller gets the bus, and your next operation meets 0x0102. Hold across the whole conversation; funnel exits through one release label.
Trap 4 — Unlocking before releasing the hold.
Order on the way out is release-then-unlock, mirroring lock-then-hold on the way in.
Trap 5 — Holding touch across a long network wait.
Nothing holds across the 60 s wait for the platform in the shipped flow. A hold is a frozen screen; keep it to chip traffic.
Trap 6 — Calling optiga_manager_init() before the scheduler.
It reaches xTimerCreate() inside a critical section with no scheduler (ipc_hsm_handler.c:1356-1364). Task context only; it is idempotent, so calling it from more than one task is fine. Initialising is a separate step from optiga_trust_open_application().
Trap 7 — Drawing optiga_chip_enter/exit as separate steps in a flow diagram.
On the enrolment and mTLS journeys nothing calls them directly; they are reached through lock/unlock/acquire/release (tesaiot_optiga_manager.c:340-365). Show the alias, not two boxes.

Variant

Variant
mtb-mpy and mtb-only

All ten optiga_manager_* / optiga_chip_* functions are in libbento_hsm.a, which needs zero MicroPython symbols. optiga_trust_helpers.c, tesaiot_pu_ingest.c and ipc_hsm_handler.c ship as source and compile on both variants. The touch IPC underneath — ipc_hsm_touch_pause, ipc_hsm_touch_pause_reason, ipc_hsm_touch_resume — is on the archive's consumer_must_provide.txt list (chapter D3): the template provides it, and so must any consumer that replaces the template's HSM handler.