SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
D3 — Weak symbols, ENABLE_OPTIGA_CLM, and the consumer contract

Learning goal

Six of the eighteen tesaiot_hsm functions are declared weak by every shipped caller and must be NULL-checked before the call. Why that is, what ENABLE_OPTIGA_CLM controls, what the archive demands from you (consumer_must_provide.txt), and how to see the contract enforce itself by building with CLM off.

The real firmware sequence

What the flag does

proj_cm33_ns/Makefile:143-161:

ENABLE_OPTIGA_CLM ?= 1
ifeq ($(ENABLE_OPTIGA_CLM),1)
SOURCES+=$(BENTO_LIBS_DIR)/kit-pse84-ai/modules/tesaiot/tesaiot_pu_ingest.c
DEFINES+=ENABLE_OPTIGA_CLM=1
endif

and :464-475: libbento_hsm.a is on LDLIBS unconditionally — "an archive member is pulled only when something already references it, so with <tt>ENABLE_OPTIGA_CLM</tt> off nothing is extracted." With the flag off, the ingest is not compiled, nothing references the archive's MQTT-side members, and the six symbols below simply do not exist in the link. The shipped callers still compile because they declare them weak.

The six weak-consumed symbols

dist/tesaiot_hsm/overridable.txt is empty — nothing in the archive is weak. The weakness is on the consumer side: shipped callers declare these six __attribute__((weak)) so that a build without the MQTT enrolment path still links:

Symbol Declared weak at Guarded call
publish_csr ipc_hsm_handler.c:1691-1693 ipc_hsm_handler.c if (publish_csr == NULL)
tesaiot_publish_protected_update ipc_hsm_handler.c:1687-1689; modtesaiot.c both guarded
trustm_reset_state ipc_hsm_handler.c:1715 if (trustm_reset_state != NULL)
trustm_current_correlation_id ipc_hsm_handler.c:1721 two NULL checks — pointer, then result
trustm_requested_target_oid tesaiot_pu_ingest.c wrapped in pu_target_oid() with 0xE0E1 fallback
trustm_requested_anchor_oid tesaiot_pu_ingest.c wrapped in pu_anchor_oid() with 0xE0E8 fallback

The declaration:

extern int publish_csr(uint8_t *csr, size_t csr_length, uint16_t target_oid,
uint16_t trust_anchor_oid, uint32_t payload_version)
__attribute__((weak));

The double-NULL form for a function that returns a pointer:

/* ...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);
}

First the function pointer (is it linked?), then the returned string (is a request outstanding?). Skipping the first dereferences address zero on a CLM-off build; skipping the second turns "nothing outstanding" into a replay (chapter D2).

The wrapped-accessor form with a default:

/* 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 */
}

Wrap every weak accessor in a local function that supplies the default. Do not scatter != NULL ? f() : default at call sites.

The MicroPython binding does the same, raising instead of returning — 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):

mp_raise_msg(&mp_type_OSError,
MP_ERROR_TEXT("Protected Update not built in "
"(needs ENABLE_OPTIGA_CLM=1)"));
}
int tesaiot_publish_protected_update(const char *target_oid, const char *trust_anchor_oid, uint32_t payload_version, bool with_csr)
Ask the platform for a Protected Update (OIDs as hex strings); weak — NULL-check first.

What the archive demands from you

template/lib/tesaiot_hsm/consumer_must_provide.txt is the machine-derived list of symbols the archive references but does not define. Grouped:

  • MQTT client objects — cy_mqtt_publish, mqtt_connection, mqtt_device_id, publisher_task_q, tesaiot_mqtt_client_id, tesaiot_mqtt_username. The archive owns the request envelope; you own the connection it publishes on.
  • OPTIGA primitives — optiga_util_create/destroy, optiga_util_open_application/close_application, optiga_util_read_data/read_metadata, optiga_util_protected_update_start/final, optiga_crypt_*, optiga_generate_device_keypair, optiga_generate_csr_pem, optiga_read_factory_uid, optiga_slot_info, optiga_check_certificate_validity. The template's optiga_trust_helpers.c provides these.
  • Touch IPC — ipc_hsm_touch_pause, ipc_hsm_touch_pause_reason, ipc_hsm_touch_resume. Chapter D1's discipline depends on these being the counted implementations from ipc_hsm_handler.c.
  • Ingest handshake — g_protected_update_just_completed, certificate_sync_success, sync_certificate_response_*, upload_certificate_response_*, check_certificate_response_*, platform_has_certificate, tesaiot_is_licensed, tesaiot_read_lcso, tesaiot_read_data, tesaiot_read_metadata.
  • libc / FreeRTOS — mbedtls_base64_encode/decode, malloc, free, pvPortMalloc, printf, scanf, snprintf, strcpy, memcpy, …

If you replace the template's HSM handler or MQTT module, every name on that list must still resolve, with the same semantics. The touch IPC trio in particular: substituting a non-counted resume reintroduces the 0x0102 signature from chapter D1.

Step-by-step

Step 1 — Build with CLM on (the default) and confirm the symbols exist

cd proj_cm33_ns && make build -j
arm-none-eabi-nm build/*/Release/proj_cm33_ns.elf | grep -E ' (publish_csr|tesaiot_publish_protected_update|trustm_reset_state|trustm_current_correlation_id|trustm_requested_target_oid|trustm_requested_anchor_oid)$'

What you should observe. Six T symbols. (Glob the ELF path; the TARGET directory name differs between kits.)

Step 2 — Build with CLM off

cd proj_cm33_ns && make build -j ENABLE_OPTIGA_CLM=0

What you should observe. The build succeeds — that is the weak declarations working. Run the same nm line: the six names are absent (U/w at most, no T), tesaiot_pu_ingest.o is not in the link, and the [PU-Ingest] strings are gone from the ELF.

Step 3 — Flash it and press the buttons

Power-cycle after flashing. Home → HSM Security → Enrol Certificate.

What you should observe. The screen shows, without touching the chip:

CSR enrolment is not built into this firmware

(ipc_hsm_handler.c, the publish_csr == NULL branch in prov_run_locked.) Then Protected Update:

Protected Update is not built into this firmware

(the tesaiot_publish_protected_update == NULL branch). On mtb-mpy, tesaiot.protected_update(...) from the REPL raises:

OSError: Protected Update not built in (needs ENABLE_OPTIGA_CLM=1)

(modtesaiot.c:739-743). Those three strings are the verification for this chapter: each is a NULL-guard refusing, exactly where the table above says it would.

What you should not observe: a HardFault, a dark screen, or a [MQTT] reconnect. The guards run before the platform is reached.

Step 4 — Confirm the rest of the HSM surface still works with CLM off

Read a credential slot (WiFi page saved list, or optiga.read_metadata() on mtb-mpy).

What you should observe. It works. optiga_manager_*, optiga_chip_* and the credential IPC do not depend on the flag — the ten D1 functions are pulled from the archive because ipc_hsm_handler.c references them unconditionally. Only the MQTT-side enrolment path is gone.

Step 5 — Rebuild with CLM on

cd proj_cm33_ns && make build -j

Flash, power-cycle, and run chapter D2's Step 2 to confirm Enrol proceeds past the guard.

Traps

Trap 1 — Calling a weak-consumed symbol without the NULL check.
On a CLM-off build the pointer is zero. Every shipped caller checks; so must yours. (Appendix X, item 19.)
Trap 2 — Declaring the symbol strong in your own file.
If any translation unit declares publish_csr without weak, a CLM-off build fails at link with an undefined reference — the opposite of the graceful refusal above. Copy the shipped declaration.
Trap 3 — Expecting overridable.txt to list these.
It is empty. Weak-*consumed* is not weak-*defined*: the archive's definitions are strong; the consumers' declarations are weak. Nothing in libbento_hsm.a can be overridden.
Trap 4 — Replacing the touch IPC trio with a raw resume.
consumer_must_provide.txt names ipc_hsm_touch_resume; the archive expects the counted semantics chapter D1 describes. A "simpler" implementation that sends IPC_CMD_TOUCH_RESUME unconditionally gives you 0x0102.
Trap 5 — tesaiot_run_protected_update_isolated_test().
The eighteenth exported function has no shipped call site (its only real caller is in a file that does not ship). It is an interactive self-test: it blocks on scanf() over polled stdin, requires optiga_manager_init() and an opened application first, and returns only when its menu's exit option is chosen. Never call it from a task another subsystem waits on. Any example of it in these docs is authored, not lifted.
Trap 6 — Reading ENABLE_OPTIGA_CLM as "OPTIGA on/off".
It gates the MQTT enrolment protocol and the bundle ingest only. mTLS (chapter C4), credential storage (C2) and the chip-access discipline (D1) are unaffected.

Variant

Variant
mtb-mpy and mtb-only

The flag, the six weak declarations, the guards and the refusal strings on the screen are identical on both variants. The OSError refusal is mtb-mpy only — modtesaiot.c is one of the ten files the mtb-only package drops. consumer_must_provide.txt is the same file in both zips; on mtb-only the printf/scanf entries resolve to retarget-io exactly as on mtb-mpy.