|
SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
|
Button → IPC → worker → chip → broker → ingest → verdict, with the exact point at which the secure element — not the host — checks the platform's signature. Along the way, three facts the SDK's own headers can mislead you about:
hsm_enrol_open() / hsm_protect_open() are exported cm55_core functions; the overlay they open (hsm_provision_ui.c) is not shipped as source — it is in libbento_cm55.a. The header tells you the contract: IPC_CMD_HSM_PROVISION returns immediately and an lv_timer polls. The owning page must pair the open with a teardown in its destroy callback:
Last statement of destroy_cb, after the page's own timers are deleted — otherwise a provisioning poll timer fires after the widgets are freed.
IPC_CMD_HSM_PROVISION is 0xBE (ipc_communication.h:184). Ops: HSM_PROV_OP_POLL 0, HSM_PROV_OP_CSR 1 (Enrol), HSM_PROV_OP_PU 2 (Protect), HSM_PROV_OP_FETCH_CSR 3, HSM_PROV_OP_UNLOCK 4. States IDLE/BUSY/DONE/FAILED; steps KEYGEN 1, CSR 2, PUBLISH 3, WAIT 4, INSTALL 5, VERIFY 6 (:253-271). And one event id:
TESAIOT_PU_CHIP_VERIFIED_MANIFEST is the moment the secure element itself checked the platform's signature against its trust anchor — the one step in the flow that a compromised host cannot fake, and the reason any of this is worth more than a plain write. (ipc_communication.h:273-277)
handle_hsm_provision() rejects with HSM_PROV_REJECTED_BUSY if a run is in progress, defaults target_oid to 0xE0E1 and anchor_oid to 0xE0E8, and sets pending_op. prov_task polls it every 50 ms and calls prov_run(op).
prov_open_held() — optiga_manager_init() then optiga_trust_open_application() under a touch hold (chapter D1) — then prov_run_locked(op), then trustm_reset_state() on every exit, then prov_close_held(). The reset is belt-and-braces: the ingest disarms itself (step 10), but a run that timed out leaves an id armed just as surely as one that succeeded, and this is the one point every run passes through.
prov_run_locked disconnects and restarts MQTT, waits on mqtt_is_started() (not mqtt_is_connected()), pushes "Connecting to the platform" to the screen, tries two connects of 15 s each, then waits for data_received_event_group to exist — the subscriber task creates it, and its existence is the only proof available here that something is listening for the reply (ipc_hsm_handler.c:2023-2114). Failure text: "No answer from the platform after 30 seconds.".
Three things in that block:
Key generation and CSR run inside prov_make_csr_held() under the reason "Generating a key and signing the request"; the screen gets "Generating a key pair inside the secure element" then "Signing the request with the key that never leaves the chip" (ipc_hsm_handler.c:1807-1840). Subject is CN=<mqtt username>,O=TESAIoT.
Two publish branches:
tesaiot_publish_protected_update(target, anchor, version, with_csr) — four arguments, OIDs as hex strings ("E0E1", "E0E8" via snprintf "%04X"). with_csr = true generates a key pair on-chip, which is why the wrapper holds touch. The archive records the requested OIDs (readable later through trustm_requested_target_oid() / trustm_requested_anchor_oid()), refuses if the chip already binds the target to a different anchor, reads the anti-rollback counter for current_version, and publishes to device/<id>/commands/request. The MicroPython binding is the second compiled caller — tesaiot_protected_update_py() at modtesaiot.c:730-768, marker [hsm_publish_pu_mpy_binding] (mtb-mpy zip only — this file is not in the mtb-only package). It defaults target/anchor to "E0E1"/"E0E8" and makes the same four-argument call:
After either publish: the CSR buffer is freed (1,600 bytes the next step needs), "Waiting for the platform" goes to the screen, and the worker polls g_optiga_ingest_events != events_before for 60 s — no touch hold across that wait.
When the platform signs a CSR it publishes the certificate straight to device/<id>/commands/certificate. The suffix router (chapter C3) dispatches to the weak tesaiot_pu_ingest_certificate:
Install under a touch hold; if installed, set g_protected_update_just_completed; if answered, trustm_reset_state() and bump g_optiga_ingest_events. That bump is what wakes the worker.
Router → tesaiot_pu_ingest_bundle() (tesaiot_pu_ingest.c, ~90 KB of shipped source, the linear goto pu_done ladder). The touch hold is taken at the top for the entire ingest (chapter D1), then:
trustm_update_state(PROCESSING_JSON_BUNDLE) — a setter. The compiled progression through this file is PROCESSING_JSON_BUNDLE → WRITING_TRUST_ANCHOR → VERIFYING_MANIFEST → PROTECTED_UPDATE_SUCCESS, with a *_FAILED transition and a detail string on every early exit. Nothing reads the variable to decide what to do next; the goto ladder does.
Then the replay defence:
trustm_current_correlation_id() returning NULL means nothing on this device asked for this — discard the bundle. Not optional: the platform retains the last bundle and redelivers it on every connect; before this check every connect applied it — "a replay: it rewrites the target, re-locks its access condition, and burns a step of the chip's anti-rollback counter", observed three times on 2026-08-07. A non-NULL id is compared exactly, case-sensitively, with the bundle's correlation_id.
The anchor and target the bundle is for come from the request, through weak accessors with the documented defaults:
(The cautionary tale is in the comment: the anchor used to be baked in as 0xE0E8, so a request naming a different anchor was accepted and silently ignored, and the chip verified the manifest against an object the platform had not signed for — 0x800F with no diagnostic.)
Then the chip work: optiga_manager_acquire(); write the trust anchor's metadata (0xE8 0x01 0x11, Data Object Type = Trust Anchor); STEP 3 — base64-decode fragment_0..2 and concatenate; MUD metadata on the target; STEP 4 — optiga_util_protected_update_start(me, 1, manifest, len), which is where the chip verifies the manifest's signature against the trust anchor; on success:
(tesaiot_pu_ingest.c, immediately after the [4.1] Manifest verification OK line; consumed by ipc_hsm_handler.c:2258-2260 to drive the screen's INSTALL step.) Then fragments are applied with protected_update_final, the success flag is set, the event group bit is set, the ACK is published to device/<id>/telemetry/system, and finally:
Release the chip and the touch hold first; then, only if this bundle was for us, trustm_reset_state() and bump the counter. The order matters: the increment used to sit ten seconds earlier, the waiting worker woke into optiga_verify_cert_key_pair() and queued behind this task's own gate — the 0x0102 collision the gate exists to prevent.
ipc_hsm_handler.c:2234-2262: "Checking the certificate against the key in the chip" → optiga_verify_cert_key_pair(target, key) → one of "The device can prove it holds the key this certificate names" (DONE), "Installed, but the certificate does not belong to this chip's key" (FAILED), "Installed; the pair check could not run".
Prerequisites: WiFi (C1/C2), a working server-TLS or mTLS connection (C3/C4), the board's device_id registered on the platform, and a build with ENABLE_OPTIGA_CLM=1 (the default — chapter D3).
On mtb-mpy: import optiga; optiga.read_metadata(0xE0E1). Note tag C0 (life-cycle state) and tag D0 (change access condition). Every board on the bench should read C0 = 01 (Creation). Never write tag C0.
What you should observe. The metadata TLV on the REPL. Chapter D1's discipline runs underneath the call.
Home → HSM Security → Enrol Certificate.
What you should observe. Screen: Connecting to the platform → Generating a key pair inside the secure element → Signing the request with the key that never leaves the chip → Sending the request to the platform → Waiting for the platform → Checking the certificate against the key in the chip → The device can prove it holds the key this certificate names. Those seven sentences are pushed by CM33_NS (prov_say), and they are verified source strings. UART, live: the [MQTT] reconnect sequence from C3, then [CSR] Using DIRECT PUBLISH (bypassing publisher_task queue) and [DirectPub] Publishing u bytes to 's' (archived tesaiot_optiga_trust_m.c, no mute), then on the reply [Subscriber] Certificate from platform (d bytes) (subscriber_task.c:217, (printf) form — live).
Home → HSM Security → Protected Update.
What you should observe. Screen: Connecting to the platform → Asking the platform for a signed manifest → Waiting for the platform → the INSTALL step → the VERIFY step and its verdict. UART, live, in order:
(subscriber_task.c:205 via (printf); tesaiot_pu_ingest.c has no mute — LABEL_SUBSCRIBER is "[PU-Ingest]".) The [4.1] Manifest verification OK line is followed in the code by tesaiot_pu_progress(TESAIOT_PU_CHIP_VERIFIED_MANIFEST) — this is the one unfakeable event: the signature check happened inside the Trust M against its own trust anchor. Everything before it a compromised host could print.
optiga.read_metadata(0xE0E1).
What you should observe. Tag D0 now demands a signed manifest (21 e0 e8-style value naming the anchor) where it was e1 fc 07 before; tag C0 unchanged at 01. If C0 moved, stop and report — nothing in this flow writes it, and optiga.write_metadata() refuses TLVs containing it.
Power-cycle, connect WiFi, tesaiot.connect() (or the page). The broker redelivers the retained bundle.
What you should observe. [Subscriber] Protected Update bundle (d bytes) then [PU-Ingest] Ignoring a Protected Update bundle nobody asked for. … — and no OPTIGA acquired, no STEP lines. Nothing was written. If instead you see the full STEP 3/STEP 4 sequence with no button pressed, the correlation id was armed when it should not have been.
Enrol again, now that 0xE0E1 demands a manifest.
What you should observe. Screen: This slot takes signed manifests only. Use Protect, or clear the requirement first. Nothing was changed. — before any key generation. On a Creation-state board, HSM Security → Unlock (HSM_PROV_OP_UNLOCK) clears the requirement and Enrol works again.
libbento_hsm.a links on both variants; the ingest, helpers and handler compile on both. mtb-mpy adds tesaiot.protected_update() and the optiga module for Steps 1 and 4; on mtb-only, read the metadata through optiga_util_read_metadata() from your own task, under chapter D1's discipline. The Enrol/Protect buttons and the CM55 overlay are identical.