SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
C4 — mTLS: the OPTIGA-backed TLS identity

Learning goal

Where the private key lives (inside the Trust M, in a slot it never leaves), how a TLS handshake on CM33_NS reaches it (PSA opaque key → secure-element driver → trustm_ecdsa_sign), and what is patched in the ModusToolbox tree to make that possible — and how to apply the eleven patches that ship with this package and prove they went in correctly.

The real firmware sequence

The OID map

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

Two pairs and a CA:

OID Contents Used by
0xE0E0 / 0xE0F0 Infineon factory certificate / key this chapter — the TLS identity
0xE0E1 / 0xE0F1 TESAIoT device certificate / key enrolment and Protected Update — chapter D2
0xE0E9 TESA CA (trust anchor for the device pair) D2

mqtt_mtls_setup_optiga() now chooses for itself. It calls optiga_verify_cert_key_pair(0xE0E1, 0xE0F1) first: if the TESAIoT certificate and key are the same identity, that pair is used; otherwise it falls back to the factory pair 0xE0E0 / 0xE0F0.

That check answers the question the old fallback could not — are these two the same identity — and is safe on an unenrolled board, because it asks the chip a bounded question rather than reading a slot that may never return, which is what made the previous attempt hang.

Order matters.** The check must come after optiga_trust_init(). Called with the OPTIGA application closed, the read spins in a bare while (status == BUSY) with no timeout and CM33_NS never comes back — measured 2026-08-31 by placing it too early.

From a Dev Kit that day:

[mTLS] device pair verified — using TESAIoT identity
[PSA-SE] Setting signing key OID to 0xE0F1
[mTLS] Certificate read: 730 bytes PEM
[HTTPS] Response: 200

What the factory certificate proves, and what it does not.** The certificate in 0xE0E0 has subject CN=InfineonIoTNode, and that subject is the same on every OPTIGA Trust M part (decoded from a real board's board-factory-cert-cd16334d.pem with openssl x509, 2026-08-29: issuer Infineon OPTIGA(TM) Trust M CA 300, valid 2025-10-21 to 2045-10-21, no SAN and no device-identifying field). The only per-part fields are the serial number and the public key.

So the handshake proves the peer holds a genuine Infineon Trust M — not which device it is. Meanwhile the MQTT client id in mTLS mode comes from cfg.factory_uid and the subscribed command topic from cfg.device_id (mqtt_client_config.c:143-144, subscriber_task.c:92-93), and both are free-form strings settable from MicroPython with tesaiot.config_set(...) and persisted (tesaiot_config_store.c:174-176).

State the consequence exactly: at the firmware level nothing binds the identity a device asserts to the certificate it presents. The decisive control is broker-side — an ACL that grants device/{id}/# only to the client whose certificate is pinned to that id, pinned by public-key fingerprint or certificate serial, never by subject, which is identical across parts. Verify that ACL before putting the chapter D2 enrolment recipe into production use.

Setup, in order

mqtt_client_config_init() (chapter C3) calls mqtt_mtls_setup_optiga() when tls_mode == TESAIOT_MODE_MTLS. That function (mqtt_mtls_setup.c:61-310):

  1. Takes a touch hold with a reason — touch_pause_for("Preparing secure element for mTLS") → the chip and the touch controller share the same I2C block (chapter D1).
  2. optiga_trust_init() and opens the application.
  3. optiga_manager_init() — before any TLS work, because the handshake's signing path starts with optiga_manager_lock(), which fails outright when the manager was never initialised. Skipping this made every CertificateVerify fail:
/* ...context: inside mqtt_mtls_setup() ... */
printf("[mTLS] optiga_manager_init failed — signing would be impossible\n");
return false;
}
  1. Reads the certificate out of 0xE0E0 as PEM and hands it to cy_tls_set_client_cert() (+1 for the NUL, is_pem = 1).
  2. optiga_psa_register() then psa_crypto_init() — repeated here because a TLS teardown wipes the PSA key store (:235-247).
  3. Builds the opaque key attributes: ECC SECP256R1 key pair, 256 bits, PSA_KEY_USAGE_SIGN_HASH, PSA_ALG_ECDSA(PSA_ALG_ANY_HASH), lifetime volatile at PSA_KEY_LOCATION_OPTIGA ((psa_key_location_t)1, optiga_psa_se.c:17). psa_generate_key() does not generate anything on the chip — with that location it registers a handle that the SE driver maps to key_oid.
  4. cy_tls_set_optiga_key_id(s_psa_key_id) — the hand-off into the patched TLS stack.

The handshake reaches the chip

During the TLS handshake the patched cy_tls.c builds an opaque mbedtls_pk from the key id and installs it with mbedtls_ssl_conf_own_cert() (cy_tls.c:1755-1809 in the patched secure-sockets — see the caveat below). When the server asks for CertificateVerify, mbedTLS calls PSA, PSA dispatches to the registered SE driver, and the driver's optiga_psa_sign() (optiga_psa_se.c:315, registered at :151 and :235-236) calls trustm_ecdsa_sign(map->oid, hash, …). That function takes optiga_manager_lock() then optiga_manager_touch_hold() for the whole transaction (optiga_trust_helpers.c:1665, :1673) — chapter D1's discipline, exercised on every connect.

SHA-384 suites: the driver truncates the hash to its leftmost 256 bits (optiga_psa_se.c:332-343); that is what makes PSA_ALG_ANY_HASH work.

What is patched, and what ships

The cy_tls ↔ OPTIGA binding is not in the template — it lives in mtb_shared/, a third-party tree. So this package ships the whole of third_party_patches/: eleven diffs across five assets, a series file giving the order they apply in, and PATCHED.sha256 — eleven file paths under mtb_shared/ with the SHA-256 each must have, including secure-sockets/…/cy_tls.c, cy_tls_optiga_key.c and cy_tls_optiga_key.h.

Shipping the diffs was settled on 2026-08-28, on the practice Buildroot follows for packages it marks non-redistributable and that Infineon's own meta-freescale layer follows against EULA'd NXP sources: a patch carries the licence of the work it patches, and a package the customer cannot build is not a release.

You applied those patches in chapter A1/A2's patch step and verified them with shasum -a 256 -c PATCHED.sha256. If that check did not pass, mTLS cannot work: cy_tls_set_optiga_key_id() will not exist and the build fails at link — or, with an unpatched cy_tls.c that happens to link, the handshake will use no client certificate and the broker will reject it.

Step-by-step

Step 0 — Confirm the patch state

cd <template>/third_party_patches && shasum -a 256 -c PATCHED.sha256

What you should observe. Every line OK. Any FAILED means the tree is not the tree this firmware was built for; stop here.

Step 1 — Switch the config to mTLS

Set tls_mode=0 in /.tesaiot_config (chapter C3). Leave port= alone — it is unused; the mode selects 8883.

What you should observe. Nothing yet.

Step 2 — Connect

tesaiot.connect() or the page's Connect button.

What you should observe on the UART, live, in order:

[MQTT] Waiting for WiFi...
[MQTT] WiFi connected
[MQTT] Start request received
[MQTT-Config] Mode=0, Broker=%s:8883, Client=%s, User=%s, PassLen=%u
[mTLS] Setting up OPTIGA Trust M (cert=0xE0E0, key=0xE0F0)
[mTLS] Certificate read: %u bytes PEM
[mTLS] OPTIGA Trust M setup complete (key_id=%lu)
[MQTT] Instance created
[MQTT] Connecting to '%s:8883' as '%s'...
[PSA-Sign] Using Key OID 0xE0F0 for TLS CertificateVerify (slot=%lu)
[MQTT] Connected to broker

Client= in the [MQTT-Config] line is now the factory UID, not device_id.

The [PSA-Sign] line (optiga_psa_se.c:364-365) deserves precision. It prints before the signature is attempted — it marks "sign attempted with a resolved OID", not "sign succeeded". Success additionally requires the absence of the ERROR line that follows it in the same function:

[PSA-Sign] ERROR: trustm_ecdsa_sign status=0x%04X
optiga_lib_status_t trustm_ecdsa_sign(optiga_key_id_t oid, const uint8_t *digest, uint16_t digest_len, uint8_t *sig_raw, uint16_t *sig_raw_length)

(optiga_psa_se.c:372). The pair — the Using Key OID line present, the ERROR: trustm_ecdsa_sign line absent, then [MQTT] Connected to broker — is the success signal. Any of the other four [PSA-Sign] ERROR forms (:328, :346, :352, :359) means the driver never reached the chip.

On screen: the TESAIoT page shows connected, and in this mode also optiga_state = PROVISIONED, cert_state = LOADED (tesaiot_mqtt.c:60-64).

If the patched secure-sockets was built with TLS_DIAG defined (cy_tls.c:168), two more lines appear between setup and the sign: [TLS-DIAG] optiga_key=lu cert_ready=d and [TLS-DIAG] mbedtls_pk_setup_opaque = d (-0x%04X). They live in the patched code and are off by default — useful if you turn them on, not required for the verification above.

Step 3 — Prove it is the chip that signed

Disconnect the Trust M's I2C (or, on a board with the debug header, hold the chip in reset) and reconnect.

What you should observe. [mTLS] Certificate read never appears; instead one of [mTLS] OPTIGA application would not open — aborting mTLS setup, [mTLS] optiga_manager_init failed — signing would be impossible, or [mTLS] No usable certificate in OID 0x%04X (got u bytes) (:182, :208, :223), followed by [MQTT-Config] mTLS OPTIGA setup failed and [MQTT] Configuration failed — not attempting to connect. No private key exists anywhere in flash to fall back to. That is the point.

Step 4 — Reconnect after a teardown

Disconnect from the broker and connect again without rebooting.

What you should observe. [mTLS] Re-binding existing OPTIGA key (key_id=lu, cert=u bytes) (:107) — or, if the teardown wiped the PSA key, [mTLS] PSA key lu no longer exists — the TLS teardown wiped … (:95) followed by a full setup. Both are normal; the second is why step 5 of the setup re-registers the driver every time.

Traps

Trap 1 — Assuming make getlibs applies the patches.
It does not. A fresh make getlibs overwrites mtb_shared/ with the unpatched upstream, and the build's checksum gate then refuses to compile until the eleven files match. Re-apply from third_party_patches/ in the order given by series, always with patch -p1 -F0, and re-check PATCHED.sha256.
Trap 2 — Reading [PSA-Sign] Using Key OID … as success.
It prints before the sign. Success = that line plus no [PSA-Sign] ERROR: trustm_ecdsa_sign status= after it plus [MQTT] Connected to broker.
Trap 3 — Skipping optiga_manager_init() before TLS.
The signing path's first instruction is optiga_manager_lock(), which returns false when the manager was never initialised. Every CertificateVerify then fails with a PSA hardware-failure status and the broker closes the socket. Chapter D1 explains why optiga_chip_enter() would not have caught this.
Trap 4 — Opening the OPTIGA application while holding the manager lock.
Open first, lock second (optiga_trust_helpers.c:4604-4622). The setup function follows this order; keep it if you reorder anything.
Trap 5 — cfg.port.
Still unused. mTLS is 8883 by the switch in mqtt_client_config.c, full stop.
Trap 6 — Expecting the TESAIoT pair to be used.
TLS uses 0xE0E0/0xE0F0 (factory). The 0xE0E1/0xE0F1 pair written by enrolment is not consulted unless you change the two lines in the OID map. The side effect is that the certificate shown to the broker carries no device-identifying field at all — see "What the factory certificate proves" above.

Variant

Variant
mtb-mpy and mtb-only

libbento_hsm.a needs zero MicroPython symbols (variants/mtb-only.mk:14); mqtt_mtls_setup.c, optiga_psa_se.c and optiga_trust_helpers.c compile on both variants. The only mtb-mpy-specific element in this chapter is the tesaiot.connect() trigger. The PSA driver registration order at boot — optiga_psa_register() then psa_crypto_init() — is in proj_cm33_ns/main.c:228-235 on both; its failure lines [BOOT] optiga_psa_register failed: d / [BOOT] psa_crypto_init failed: d are live, and success is silent.