SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches

Functions

void trustm_update_state (trustm_state_t new_state, const char *status_code, const char *detail)
 A setter with a timestamp, not a dispatcher; set WAITING_* before the transport starts.
void trustm_reset_state (void)
 Back to IDLE, correlation id zeroed — this is what ends a run; call it at every exit.
const char * trustm_current_correlation_id (void)
 The in-flight run's id, or NULL; NULL means discard the inbound bundle — the replay defence.
uint16_t trustm_requested_target_oid (void)
 The target OID the last request named; weak — NULL-check and fall back to 0xE0E1.
uint16_t trustm_requested_anchor_oid (void)
 The trust anchor the last request named; weak — same pattern, default 0xE0E8.

Detailed Description

Five functions. One writes the state machine, one ends a run, three read what the in-flight request named. Four of the five are weak-consumed (The six weak-consumed symbols — NULL-check contract); trustm_update_state() is the exception.

Variant
mtb-mpy and mtb-only

Function Documentation

◆ trustm_update_state()

void trustm_update_state ( trustm_state_t new_state,
const char * status_code,
const char * detail )

A setter with a timestamp, not a dispatcher; set WAITING_* before the transport starts.

Move the state machine, and put it back at IDLE. status_code and detail are surfaced on the provisioning screen.

Contract
A setter with a timestamp, not a dispatcher — it writes the state variable and a tick stamp (tesaiot_optiga_trust_m.c:377-388); there is no switch on trustm_state_t anywhere in either tree. Needs no lock and no chip access. Not weak. Set a TRUSTM_STATE_WAITING_* state before the transport starts, or the subscriber can process the reply before the state moves (the only place this is written down is a shipped reference file that is not compiled, so it is stated here rather than quoted). Every early exit gets a *_FAILED transition with a detail string. The compiled ingest walks PROCESSING_JSON_BUNDLE (tesaiot_pu_ingest.c:663) → WRITING_TRUST_ANCHOR (:978) → VERIFYING_MANIFEST (:1736) → PROTECTED_UPDATE_SUCCESS (:1913), with PROTECTED_UPDATE_FAILED on the error branches. Four enumerators are declared but dead — no writer in either tree: APPLYING_UPDATE, WAITING_FOR_CERTIFICATE, COMPLETE, APPLYING_FRAGMENTS. It does not end a run: only trustm_reset_state() clears the correlation id.
Variant
mtb-mpy and mtb-only
/* ...context: inside the Protected Update bundle ingest ... */
// State update: Processing JSON bundle
#if 0 /* TRACE messages disabled */
printf("%s [TRACE-2] State updated\n", LABEL_SUBSCRIBER);
fflush(stdout);
#endif
if (!subscriber_q_data.data) {
#if TESAIOT_DEBUG_VERBOSE_ENABLED
printf("%s ERROR: NULL bundle data\n", LABEL_SUBSCRIBER);
#endif /* TESAIOT_DEBUG_VERBOSE_ENABLED */
g_protected_update_active = false; // Reset flag on early exit
goto pu_done;
}

◆ trustm_reset_state()

void trustm_reset_state ( void )

Back to IDLE, correlation id zeroed — this is what ends a run; call it at every exit.

Contract
Puts the machine back at IDLE, zeroes the correlation id and resets the payload version — this, not trustm_update_state(IDLE, …), is what ends a run. Run it after the chip is released and before the completion counter bumps, so a waiter that wakes on the counter never sees an armed id. Call it at every exit of a request — success, failure and timeout: "a run that timed out leaves an id armed just as surely as one that succeeded" (ipc_hsm_handler.c:1964-1965). The ingest disarms at the one point every caller passes through, because two of the three callers forgot to; the handler additionally resets around every exit of prov_run_locked() as belt-and-braces — weak at that site, so NULL-check the pointer (The six weak-consumed symbols — NULL-check contract).
Variant
mtb-mpy and mtb-only
/* ...context: the single pu_done exit of the bundle ingest ... */
pu_done:
/* Announce completion here, not at the end of the write.
*
* The increment used to sit beside the success flag, roughly ten seconds
* before this function releases the chip: the acknowledgement publish and the
* 0xE0E1 read-back are both still ahead of it. The waiting provisioning task
* woke on that increment and went straight into optiga_verify_cert_key_pair(),
* which then queued behind this task's own chip gate - and if that wait
* exceeded its ten second ceiling the pair check ran anyway, alongside this
* task, which is the 0x0102 collision the gate exists to prevent.
*
* After the release, and after the touch hold, is the only point at which this
* task is genuinely finished with the chip. It matches what the certificate
* path already does. */
if (pu_handled_for_us)
{
/* Disarm before announcing. The request has been answered, so nothing is
* outstanding, and trustm_current_correlation_id() must stop matching.
*
* The platform RETAINS the last bundle, so one is redelivered on every
* subsequent connect. While the id stayed armed for the rest of the boot
* each of those redeliveries matched and was executed: a Protected Update
* nobody asked for, rewriting the target, re-locking its access condition
* on top of an unlock the operator had just performed, and consuming
* enough of the C heap that the MQTT publisher task could not be created -
* which then surfaced as an unrelated "could not publish" three layers
* away. Measured 2026-08-08.
*
* Disarming here rather than in each caller is deliberate. The callers -
* the HSM Security screens, tesaiot.protected_update() from MicroPython,
* and the reference menu loop - each have to remember otherwise, and two
* of the three did not. This is the one point every one of them passes
* through, and it is the point at which the statement "a request is
* outstanding" stops being true. */
g_optiga_ingest_events++;
}
/* ...context: tail of prov_run(), after prov_run_locked() returns ... */
/* Disarm before letting go. Nothing is outstanding once this returns.
*
* trustm_reset_state() clears the correlation id, and the ingest treats a
* NULL id as "nobody asked for this" - which is the only thing standing
* between the board and a replay. The platform retains the last Protected
* Update bundle, so one is delivered on every single connect; while the id
* stayed armed for the whole boot, that retained bundle matched, and the
* board silently re-ran a Protected Update nobody had asked for. Measured
* on 2026-08-08: an Enrol immediately after a successful Protect connected,
* received the retained bundle, re-locked 0xE0E1 on top of the unlock the
* operator had just performed, and exhausted the C heap far enough that the
* publisher task could not be created - so the enrolment then failed with
* "could not publish", three layers away from the cause.
*
* The function existed and did exactly this. It had no caller anywhere in
* the tree: the reference project drives it from its own menu loop, and
* that call was not carried across when these operations became screens.
*
* It goes here, around every exit of prov_run_locked() including the early
* failures, because a run that timed out leaves an id armed just as surely
* as one that succeeded. */
if (trustm_reset_state != NULL) {
}
prov_close_held();

◆ trustm_current_correlation_id()

const char * trustm_current_correlation_id ( void )

The in-flight run's id, or NULL; NULL means discard the inbound bundle — the replay defence.

Contract
The id that correlates the platform's reply with the in-flight request; NULL when nothing is outstanding. Set by publish_csr() and tesaiot_publish_protected_update(); cleared by trustm_reset_state(). NULL ⇒ discard the inbound bundle — the replay defence, not an optional check: the platform retains the last bundle and delivers it on every connect; before the check, every connect applied it (rewrote the target, re-locked its access condition, burned a step of the anti-rollback counter), observed three times on 2026-08-07. Comparison is exact and case-sensitive. At the weak-consuming handler site there are two NULL checks: the function pointer, then the returned string.
Variant
mtb-mpy and mtb-only
/* ...context: inside the Protected Update bundle ingest, after JSON parse ... */
const char *expected_corr_id = trustm_current_correlation_id();
/* No outstanding request means nothing on this device asked for this. The
* platform retains the last bundle, so one is delivered on every connect —
* and until now every connect applied it. That is a replay: it rewrites the
* target, re-locks its access condition, and burns a step of the chip's
* anti-rollback counter, all without anyone asking. Observed three times on
* 2026-08-07, each one silently redoing the previous run. */
if (NULL == expected_corr_id) {
printf("%s Ignoring a Protected Update bundle nobody asked for. Retained "
"bundles arrive on every connect; call tesaiot.protected_update() "
"to arm one.\n", LABEL_SUBSCRIBER);
fflush(stdout);
g_protected_update_active = false;
vPortFree(json_copy);
goto pu_done;
}
if (expected_corr_id && parse_ctx.correlation_id) {
// Compare correlation_id (case-sensitive, exact match required)
bool match = (parse_ctx.correlation_id_len == strlen(expected_corr_id)) &&
(strncmp(parse_ctx.correlation_id, expected_corr_id, parse_ctx.correlation_id_len) == 0);
/* ...context: inside the provisioning status reply builder ... */
const char *c = trustm_current_correlation_id();
if (c) strncpy((char *)&resp->data[HSM_PROV_CORR_OFF], c, HSM_PROV_CORR_MAX - 1U);
}

◆ trustm_requested_target_oid()

uint16_t trustm_requested_target_oid ( void )

The target OID the last request named; weak — NULL-check and fall back to 0xE0E1.

The OIDs the in-flight request named, and the id that correlates the platform's reply with it. NULL when nothing is in flight.

Contract
The target OID the last request named (set at tesaiot_optiga_trust_m.c:1426); only meaningful between the publish and trustm_reset_state(). Weak: NULL-check the pointer and fall back to 0xE0E1 (the certificate slot, the platform's default target). Wrap it in a local accessor rather than calling it at each use site, as the ingest does.
Variant
mtb-mpy and mtb-only
/* Which object this bundle is for — see the note on the definition. Weak so a
* build without the MQTT request path still links. */
extern uint16_t trustm_requested_target_oid(void) __attribute__((weak));
extern uint16_t trustm_requested_anchor_oid(void) __attribute__((weak));
/* The anchor the last request named. Everything below used to bake 0xE0E8 in,
* including the target's Change access condition — so asking for a different
* anchor was accepted, stored, and then quietly ignored, and the chip would
* verify the manifest against an object the platform had not signed for. That
* is 0x800F with no diagnostic, the same failure the target OID caused before
* it was made to follow the request. */
static uint16_t pu_anchor_oid(void)
{
: 0xE0E8U;
}
static uint16_t pu_target_oid(void)
{
return (trustm_requested_target_oid != NULL)
: 0xE0E1U; /* certificate slot — the platform's default target */
}

◆ trustm_requested_anchor_oid()

uint16_t trustm_requested_anchor_oid ( void )

The trust anchor the last request named; weak — same pattern, default 0xE0E8.

Contract
The trust anchor the last request named (set at tesaiot_optiga_trust_m.c:1427); same lifetime and the same weak pattern, default 0xE0E8. The cautionary tale is in the lifted comment: everything used to bake 0xE0E8 in, including the target's Change access condition, so asking for a different anchor was accepted, stored and quietly ignored — the chip then verified the manifest against an object the platform had not signed for, 0x800F with no diagnostic. The mTLS OID map in mqtt_mtls_setup.c is the companion table of which slot holds what.
Variant
mtb-mpy and mtb-only
/* Which object this bundle is for — see the note on the definition. Weak so a
* build without the MQTT request path still links. */
extern uint16_t trustm_requested_target_oid(void) __attribute__((weak));
extern uint16_t trustm_requested_anchor_oid(void) __attribute__((weak));
/* The anchor the last request named. Everything below used to bake 0xE0E8 in,
* including the target's Change access condition — so asking for a different
* anchor was accepted, stored, and then quietly ignored, and the chip would
* verify the manifest against an object the platform had not signed for. That
* is 0x800F with no diagnostic, the same failure the target OID caused before
* it was made to follow the request. */
static uint16_t pu_anchor_oid(void)
{
: 0xE0E8U;
}
static uint16_t pu_target_oid(void)
{
return (trustm_requested_target_oid != NULL)
: 0xE0E1U; /* certificate slot — the platform's default target */
}
/* ...context: inside mqtt_mtls_setup() ... */
/* Bootstrap on the Infineon factory pair, as the reference firmware does.
*
* This used to try the device pair (0xE0E1/0xE0F1) first and fall back to
* the factory pair if the read came back empty. That fallback can never
* run: reading an unprovisioned slot does not return zero bytes, it never
* completes, and the wait inside read_certificate_from_optiga() spins
* without a timeout — so a board that has not been enrolled yet hangs
* CM33_NS instead of falling back. Observed 2026-08-04 on a Dev Kit whose
* 0xE0E1 is empty.
*
* official_pse84_trustm_mTLS_tesaiot reads 0xE0E0 unconditionally at boot
* (main.c:493) for the same reason, and tesaiot_select_mqtt_certificate()
* there (tesaiot_optiga_trust_m.c:1349-1374) forces the factory pair even
* when a device cert exists, because after a reset the key in 0xE0F1 may no
* longer match the certificate in 0xE0E1 — which is exactly what happens
* once a CSR has generated a fresh key. Until enrolment installs a matching
* pair and something proves the match, the factory pair is the only one
* that can be trusted to work.
*
* Per the Infineon pre-provisioning map, 0xE0E0/0xE0F0 are the IFX-
* provisioned certificate and key; 0xE0E1/0xE0F1 are TESAIoT's device pair;
* 0xE0E9 holds the TESA CA. */
uint16_t cert_oid = 0xE0E0; /* IFX-provisioned factory certificate */
uint16_t key_oid = 0xE0F0; /* IFX-provisioned factory key */