SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (ModusToolbox)
Loading...
Searching...
No Matches
D2 — การลงทะเบียนและ Protected Update ตั้งแต่ต้นจนจบ

เป้าหมายของหัวข้อนี้

ปุ่ม → IPC → worker → ชิป → broker → ingest → ผลตัดสิน พร้อมจุดที่แน่นอนซึ่ง secure element (ชิปนิรภัยแยกส่วน) — ไม่ใช่ host — เป็นผู้ตรวจลายเซ็นดิจิทัลของแพลตฟอร์ม ระหว่างทางมีข้อเท็จจริง 3 ข้อที่ header ของ SDK เองอาจทำให้เข้าใจผิดได้:

  • trustm_state_t เป็น ตัวแปรที่มี timestamp ไม่ใช่ state machine ไม่มี switch บนตัวแปรนี้อยู่ที่ใดเลย
  • ตัวสมาชิกของ enum 4 ตัวเป็น ตัวตาย: TRUSTM_STATE_APPLYING_UPDATE, TRUSTM_STATE_WAITING_FOR_CERTIFICATE, TRUSTM_STATE_COMPLETE, TRUSTM_STATE_APPLYING_FRAGMENTS ไม่มีผู้เขียนค่าเหล่านี้ทั้งในทรีที่ส่งมอบและในซอร์สของ archive
  • แพลตฟอร์ม เก็บค้าง (retained) bundle ของ Protected Update ชุดล่าสุดไว้ และส่งซ้ำในทุกครั้งที่เชื่อมต่อ correlation id (รหัสจับคู่คำขอกับคำตอบ) คือสิ่งเดียวที่ขวางอยู่ระหว่างบอร์ดกับการ replay

ลำดับการทำงานจริงของเฟิร์มแวร์

1. ปุ่ม

/* ...context: button event callbacks - GFX task context ... */
static void enrol_btn_clicked_cb(lv_event_t *e) { (void)e; hsm_enrol_open(); }
static void protect_btn_clicked_cb(lv_event_t *e) { (void)e; hsm_protect_open(); }

hsm_enrol_open() / hsm_protect_open() เป็นฟังก์ชันของ cm55_core ที่ถูกส่งออก ส่วน overlay ที่เปิดขึ้น (hsm_provision_ui.c) ไม่ได้ส่งมอบมาเป็นซอร์ส เพราะอยู่ใน libbento_cm55.a header บอกข้อกำหนดการเรียกใช้ไว้ว่า IPC_CMD_HSM_PROVISION คืนค่าทันที และมี lv_timer เป็นตัววนถาม หน้าที่เป็นเจ้าของต้องจับคู่การเปิดนี้กับการรื้อใน destroy callback ของตน:

void page_hsm_destroy(void)
{
memset(&s_ctx, 0, sizeof(s_ctx));
s_cert_overlay = NULL;
s_bench_overlay = NULL;
/* The screen (and every PIN widget under it) is being deleted by the
* page manager — cancel the pending one-shot timer and drop all widget
* pointers so a late callback cannot touch freed LVGL objects. */
if (s_pin.timer) {
lv_timer_delete(s_pin.timer);
s_pin.timer = NULL;
}
s_pin.overlay = NULL;
s_pin.icon_lbl = NULL;
s_pin.title_lbl = NULL;
s_pin.msg_lbl = NULL;
for (int i = 0; i < PIN_LENGTH; i++) s_pin.dots[i] = NULL;
/* Any provisioning overlay and its poll timer go with the page. A timer
* that fires after these widgets are freed dereferences them. */

ต้องเป็นคำสั่งสุดท้ายของ destroy_cb หลังจากลบตัวจับเวลาของหน้านั้นแล้ว มิฉะนั้นตัวจับเวลาที่วนถาม provisioning จะยิงหลังจากที่ widget ถูกคืนหน่วยความจำไปแล้ว

2. รูปแบบข้อมูลบนสาย

IPC_CMD_HSM_PROVISION มีค่า 0xBE (ipc_communication.h:184) ปฏิบัติการที่มี: 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 สถานะมี IDLE/BUSY/DONE/FAILED ส่วนขั้นตอนมี KEYGEN 1, CSR 2, PUBLISH 3, WAIT 4, INSTALL 5, VERIFY 6 (:253-271) และมีรหัสเหตุการณ์หนึ่งตัว:

TESAIOT_PU_CHIP_VERIFIED_MANIFEST คือช่วงเวลาที่ secure element ตรวจลายเซ็นดิจิทัล ของแพลตฟอร์มเทียบกับ trust anchor ของตนเองด้วยตัวมันเอง เป็นขั้นเดียวในกระบวนการนี้ที่ host ซึ่งถูกเจาะแล้วปลอมไม่ได้ และเป็นเหตุผลที่ทำให้ทั้งหมดนี้มีค่ามากกว่าการเขียน ธรรมดา (ipc_communication.h:273-277)

3. CM33_NS: ล็อกค่า OID ไว้ แล้วส่งต่อให้ worker

handle_hsm_provision() ปฏิเสธด้วย HSM_PROV_REJECTED_BUSY หากมีรอบงานทำอยู่ ตั้งค่าปริยายของ target_oid เป็น 0xE0E1 และ anchor_oid เป็น 0xE0E8 แล้วตั้ง pending_op ส่วน prov_task วนถามค่านั้นทุก 50 ms และเรียก prov_run(op)

4. กรอบครอบ open/close และการปลดสถานะรอ

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

prov_open_held() — เรียก optiga_manager_init() แล้ว optiga_trust_open_application() ภายใต้ touch hold (บท D1) — จากนั้น prov_run_locked(op) แล้ว trustm_reset_state() บนทุกเส้นทางขาออก แล้วจึง prov_close_held() การรีเซ็ตนี้เป็นการกันไว้ 2 ชั้น เพราะตัว ingest ปลดสถานะรอของตนเองอยู่แล้ว (ขั้นที่ 10) แต่รอบงานที่หมดเวลาก็ทิ้ง id ค้างอยู่ในสถานะรอไม่ต่างจากรอบงานที่สำเร็จ และจุดนี้คือจุดเดียวที่ทุกรอบงานผ่าน

5. ไปให้ถึงแพลตฟอร์มก่อนแตะชิป

prov_run_locked ตัดการเชื่อมต่อแล้วเริ่ม MQTT ใหม่ รอ mqtt_is_started() (ไม่ใช่ mqtt_is_connected()) ผลัก "Connecting to the platform" ไปยังหน้าจอ ลองเชื่อมต่อ 2 ครั้ง ครั้งละ 15 s แล้วรอให้ data_received_event_group มีอยู่จริง — task ของผู้รับสมัครสมาชิกเป็นผู้สร้างมันขึ้น และการที่มันมีอยู่คือหลักฐานเดียวที่หาได้ ณ จุดนี้ว่ามีบางสิ่งกำลังรอฟังคำตอบอยู่ (ipc_hsm_handler.c:2023-2114) ข้อความเมื่อล้มเหลว: "No answer from the platform after 30 seconds."

6–8. ปฏิเสธ slot ที่ล็อกไว้ สร้างกุญแจและ CSR แล้ว publish

/* ...context: inside prov_run_locked() ... */
if (op != HSM_PROV_OP_PU) {
if (prov_manifest_anchor_held(s_prov.target_oid) != 0U) {
/* Say what is true. The manifest requirement is a metadata field,
* not a fuse: writing D0 back to E1 FC 07 clears it, which this
* firmware already does to key slots on every key generation, and
* which was measured on this board on 2026-08-08: D0 on 0xE0E1
* read 21 e0 e8 before the write and e1 fc 07 after. Calling it
* permanent would teach the operator something false about their
* own hardware. */
prov_say(HSM_PROV_STATE_FAILED, HSM_PROV_STEP_NONE,
"This slot takes signed manifests only. Use Protect, or "
"clear the requirement first. Nothing was changed.");
return;
}
if (publish_csr == NULL) {
prov_say(HSM_PROV_STATE_FAILED, HSM_PROV_STEP_NONE,
"CSR enrolment is not built into this firmware");
return;
}
if (!prov_make_csr_held(key_oid)) return;
}
char t[8], a[8];
(void)snprintf(t, sizeof(t), "%04X", s_prov.target_oid);
(void)snprintf(a, sizeof(a), "%04X", s_prov.anchor_oid);
/* Read the completion counter before publishing. Anything that finishes
* after this point is an answer to this request; anything that finished
* before it is not, no matter what a flag says. */
uint32_t events_before = g_optiga_ingest_events;
if (op == HSM_PROV_OP_PU) {
prov_say(HSM_PROV_STATE_FAILED, HSM_PROV_STEP_PUBLISH,
"Protected Update is not built into this firmware");
return;
}
prov_say(HSM_PROV_STATE_BUSY, HSM_PROV_STEP_PUBLISH,
"Asking the platform for a signed manifest");
events_before = g_optiga_ingest_events;
if (prov_publish_pu_held(t, a) != 0) {
prov_say(HSM_PROV_STATE_FAILED, HSM_PROV_STEP_PUBLISH,
"Could not publish the request - is the broker connected?");
return;
}
} else {
/* Plain enrolment publishes the CSR and the platform answers on
* commands/certificate, which the subscriber installs with an ordinary
* write. Once Protected Update has run against this object the chip
* refuses ordinary writes for good, so that path is simply gone - and
* saying so is more use than letting it fail inside the vendor library
* with a bare status code. */
prov_say(HSM_PROV_STATE_BUSY, HSM_PROV_STEP_PUBLISH,
"Sending the request to the platform");
events_before = g_optiga_ingest_events;
if (publish_csr((uint8_t *)s_prov.csr, strlen(s_prov.csr),
s_prov.target_oid, s_prov.anchor_oid, 1U) != 0) {
prov_say(HSM_PROV_STATE_FAILED, HSM_PROV_STEP_PUBLISH,
"Could not publish - is the broker connected?");
return;
}
}

มี 3 เรื่องในบล็อกนั้น:

  • การปฏิเสธ slot ที่ล็อกไว้ (เฉพาะ Enrol) หาก metadata ของ slot เป้าหมายกำหนดให้ต้องใช้ manifest ที่ลงลายเซ็นแล้ว Enrol จะหยุดก่อนสร้างสิ่งใดขึ้นมา: "This slot takes signed manifests only. Use Protect, or clear the requirement first. Nothing was changed." คอมเมนต์ระมัดระวังไว้ว่า ข้อกำหนดนี้เป็นฟิลด์ metadata ไม่ใช่ fuse ตราบใดที่สถานะ life-cycle ของชิปยังอนุญาต Protect หรือ Unlock ก็ล้างข้อกำหนดนั้นได้
  • ตัวกัน (guard) ของ weak symbol if (publish_csr == NULL) และ if (tesaiot_publish_protected_update == NULL) — บท D3
  • การเก็บค่าตัวนับการทำงานสำเร็จเป็น snapshot events_before = g_optiga_ingest_events ซึ่งเก็บ ก่อนการ publish สิ่งใดที่ทำเสร็จหลังจุดนี้คือคำตอบของคำขอนี้ สิ่งใดที่เสร็จก่อนหน้านั้นไม่ใช่ "no matter what a flag says"

การสร้างกุญแจและ CSR ทำงานภายใน prov_make_csr_held() ภายใต้เหตุผล "Generating a key and signing the request" ส่วนหน้าจอได้รับข้อความ "Generating a key pair inside the secure element" แล้วตามด้วย "Signing the request with the key that never leaves the chip" (ipc_hsm_handler.c:1807-1840) ส่วน subject คือ CN=<mqtt username>,O=TESAIoT

การ publish มีสองกิ่ง:

  • Enrolpublish_csr(csr, len, target, anchor, 1) ภายใน archive (tesaiot_optiga_trust_m.c:895-1058) จะล้าง g_protected_update_just_completed สร้าง correlation id ใหม่จาก TRNG ตั้ง trustm_state = TRUSTM_STATE_PUBLISHING_CSR ด้วยการกำหนดค่าตรง ๆ ห่อ CSR ไว้ใน {"device_id":…,"csr":…,"correlation_id":…} แล้ว publish ไปยัง device/<id>/commands/csr โดยตรง ข้ามคิวของผู้เผยแพร่ ที่ QoS 0 จากนั้นเรียก trustm_update_state(WAITING_FOR_MANIFEST, "csr_sent", …)
  • Protectprov_publish_pu_held():
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;
}

tesaiot_publish_protected_update(target, anchor, version, with_csr) — สี่อาร์กิวเมนต์ โดย OID เป็น สตริงเลขฐานสิบหก ("E0E1", "E0E8" ผ่าน snprintf "%04X") ค่า with_csr = true ทำให้เกิดการสร้างคู่กุญแจบนชิป ซึ่งเป็นเหตุผลที่ wrapper ต้องจับ touch ไว้ ตัว archive บันทึก OID ที่ร้องขอไว้ (อ่านย้อนหลังได้ผ่าน trustm_requested_target_oid() / trustm_requested_anchor_oid()) ปฏิเสธหากชิปผูกเป้าหมายไว้กับ anchor คนละตัว อยู่แล้ว อ่านตัวนับ anti-rollback มาเป็น current_version แล้ว publish ไปยัง device/<id>/commands/request ส่วน binding ฝั่ง MicroPython เป็นผู้เรียกที่คอมไพล์แล้วรายที่สอง — tesaiot_protected_update_py() ที่ modtesaiot.c:730-768 marker [hsm_publish_pu_mpy_binding] (มีเฉพาะใน zip ของ mtb-mpy — ไฟล์นี้ไม่อยู่ในแพ็กเกจ mtb-only) มันตั้งค่าปริยายของ target/anchor เป็น "E0E1"/"E0E8" และเรียกด้วยสี่อาร์กิวเมนต์แบบเดียวกัน:

int rc = tesaiot_publish_protected_update(target, anchor,
(uint32_t)vals[ARG_version].u_int,
vals[ARG_csr].u_bool);
return mp_obj_new_bool(rc == 0);
int tesaiot_publish_protected_update(const char *target_oid, const char *trust_anchor_oid, uint32_t payload_version, bool with_csr)
ขอ Protected Update จากแพลตฟอร์ม (OID เป็น hex string); เป็น weak — ให้ตรวจ NULL ก่อน

หลังจาก publish ไม่ว่ากิ่งใด: บัฟเฟอร์ของ CSR จะถูกคืนหน่วยความจำ (1,600 ไบต์ที่ขั้นถัดไปต้องใช้) ข้อความ "Waiting for the platform" ไปขึ้นบนหน้าจอ และ worker วนถาม g_optiga_ingest_events != events_before เป็นเวลา 60 s โดยไม่จับ touch hold ค้างข้ามการรอนั้น

9a. เส้นทางขากลับ A — ใบรับรองมาที่ commands/certificate

เมื่อแพลตฟอร์มลงลายเซ็น CSR แล้ว มันจะ publish ใบรับรองตรงไปยัง device/<id>/commands/certificate ตัวแยกเส้นทางตาม suffix (บท C3) จะส่งต่อไปยัง tesaiot_pu_ingest_certificate ซึ่งเป็น weak:

void tesaiot_pu_ingest_certificate(char *cert_payload, uint16_t cert_size)
{
bool installed = false;
optiga_manager_touch_hold_reason("Installing the device certificate");
bool answered = pu_ingest_certificate_held(cert_payload, cert_size, &installed);
/* Announce completion here, once, for every path on which the platform
* actually answered — including the ones that failed to install.
*
* Setting it only on the success path left a waiter to burn its full 60
* seconds and then report "the platform did not deliver a bundle", which
* blames the wrong subsystem for a write the chip refused. Setting it
* inside, before the pair check, put two tasks into that check at once.
* After the hold is released and after every chip transaction this function
* makes is the only place that is both complete and safe. */
if (installed) {
g_protected_update_just_completed = true;
}
if (answered) {
/* Same disarm as the bundle path, for the same reason: the platform
* retains this topic too, and an armed id turns every later connect
* into a certificate install nobody asked for. */
g_optiga_ingest_events++; /* stop the waiter either way */
}
}

ติดตั้งภายใต้ touch hold หากติดตั้งแล้วให้ตั้ง g_protected_update_just_completed หากตอบแล้วให้เรียก trustm_reset_state() และเพิ่มค่า g_optiga_ingest_events การเพิ่มค่านั้นคือสิ่งที่ปลุก worker

9b. เส้นทางขากลับ B — การ ingest bundle ของ Protected Update

ตัวแยกเส้นทาง → tesaiot_pu_ingest_bundle() (tesaiot_pu_ingest.c ซอร์สที่ส่งมอบมาขนาดราว 90 KB เป็นบันได goto pu_done แบบเส้นตรง) touch hold ถูกจับไว้ตั้งแต่ต้นตลอดทั้งการ ingest (บท D1) จากนั้น:

/* ...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_update_state(PROCESSING_JSON_BUNDLE) เป็นเพียง setter ลำดับที่คอมไพล์ไว้ในไฟล์นี้คือ PROCESSING_JSON_BUNDLE → WRITING_TRUST_ANCHOR → VERIFYING_MANIFEST → PROTECTED_UPDATE_SUCCESS โดยมีการเปลี่ยนไปเป็น *_FAILED และสตริงรายละเอียดบนทุกเส้นทางคืนค่าเร็ว ไม่มีอะไรอ่านตัวแปรนี้เพื่อตัดสินว่าจะทำอะไรต่อ บันได goto ต่างหากที่ทำหน้าที่นั้น

จากนั้นคือการป้องกัน replay:

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

trustm_current_correlation_id() ที่คืนค่า NULL หมายความว่า ไม่มีสิ่งใดบนอุปกรณ์นี้ร้องขอ bundle นี้ ให้ทิ้ง bundle นั้น และนี่ไม่ใช่ทางเลือก: แพลตฟอร์มเก็บ bundle ชุดล่าสุดไว้และส่งซ้ำในทุกครั้งที่เชื่อมต่อ ก่อนจะมีการตรวจนี้ ทุกครั้งที่เชื่อมต่อจึงมีการนำ bundle มาใช้ — "a replay: it rewrites the target, re-locks its access condition, and burns a step of the chip's anti-rollback counter" ซึ่งสังเกตได้ 3 ครั้งเมื่อ 2026-08-07 ส่วน id ที่ไม่ใช่ NULL จะถูกเทียบกับ correlation_id ของ bundle แบบตรงตัวและแยกตัวพิมพ์ใหญ่-เล็ก

anchor และเป้าหมายที่ bundle นี้มุ่งไปหา มาจากคำขอ ผ่านตัวเข้าถึงชนิด weak ที่มีค่าปริยายตามเอกสาร:

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

(เรื่องเตือนใจอยู่ในคอมเมนต์: เดิม anchor ถูกฝังไว้ตายตัวเป็น 0xE0E8 ดังนั้นคำขอที่ระบุ anchor ตัวอื่นจึงถูกรับไว้แล้วละเลยไปอย่างเงียบ ๆ และชิปก็ตรวจ manifest เทียบกับ object ที่แพลตฟอร์มไม่ได้ลงลายเซ็นให้ — ได้ 0x800F โดยไม่มีข้อมูลวินิจฉัยใด ๆ)

จากนั้นคืองานฝั่งชิป: optiga_manager_acquire(); เขียน metadata ของ trust anchor (0xE8 0x01 0x11, Data Object Type = Trust Anchor); STEP 3 — ถอดรหัส base64 ของ fragment_0..2 แล้วต่อกัน; เขียน metadata MUD บนเป้าหมาย; STEP 4 — optiga_util_protected_update_start(me, 1, manifest, len) ซึ่งเป็นจุดที่ ชิป ตรวจลายเซ็นดิจิทัลของ manifest เทียบกับ trust anchor เมื่อสำเร็จ:

if (NULL != tesaiot_pu_progress) {
tesaiot_pu_progress(TESAIOT_PU_CHIP_VERIFIED_MANIFEST);
}

(tesaiot_pu_ingest.c อยู่ถัดจากบรรทัด [4.1] Manifest verification OK ทันที และถูกใช้โดย ipc_hsm_handler.c:2258-2260 เพื่อขับขั้น INSTALL บนหน้าจอ) จากนั้น fragment ถูกนำไปใช้ด้วย protected_update_final ตั้งแฟล็กความสำเร็จ ตั้งบิตของ event group publish ACK ไปยัง device/<id>/telemetry/system และท้ายที่สุด:

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

ปล่อยชิปและ touch hold ก่อน จากนั้นจึงเรียก trustm_reset_state() และเพิ่มค่าตัวนับ เฉพาะกรณีที่ bundle นี้เป็นของเราเท่านั้น ลำดับนี้มีความหมาย: เดิมการเพิ่มค่าอยู่ก่อนหน้านี้สิบวินาที worker ที่รออยู่จึงตื่นขึ้นเข้าสู่ optiga_verify_cert_key_pair() แล้วไปต่อคิวอยู่หลัง gate ของ task นี้เอง — เป็นการชนกันแบบ 0x0102 ที่ gate มีอยู่เพื่อป้องกัน

10. ผลตัดสิน

ipc_hsm_handler.c:2234-2262: "Checking the certificate against the key in the chip"optiga_verify_cert_key_pair(target, key) → ได้อย่างใดอย่างหนึ่งใน "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"

ทีละขั้น

สิ่งที่ต้องมีก่อน: WiFi (C1/C2) การเชื่อมต่อแบบ server TLS หรือ mTLS ที่ใช้งานได้ (C3/C4) device_id ของบอร์ดที่ลงทะเบียนไว้บนแพลตฟอร์มแล้ว และ build ที่ตั้ง ENABLE_OPTIGA_CLM=1 (เป็นค่าปริยาย — บท D3)

ขั้นที่ 1 — เก็บสภาพชิปเป็น snapshot ก่อนเริ่ม

บน mtb-mpy: import optiga; optiga.read_metadata(0xE0E1) ให้จดค่าแท็ก C0 (สถานะ life-cycle) และแท็ก D0 (เงื่อนไขการเข้าถึงสำหรับการเปลี่ยนแปลง) ทุกบอร์ดบนโต๊ะทดสอบควรอ่านได้ C0 = 01 (Creation) ห้ามเขียนแท็ก C0

สิ่งที่ควรสังเกต TLV ของ metadata บน REPL วินัยของบท D1 ทำงานอยู่ใต้การเรียกนี้

ขั้นที่ 2 — Enrol

Home → HSM Security → Enrol Certificate

สิ่งที่ควรสังเกต บนหน้าจอ: Connecting to the platformGenerating a key pair inside the secure elementSigning the request with the key that never leaves the chipSending the request to the platformWaiting for the platformChecking the certificate against the key in the chipThe device can prove it holds the key this certificate names ทั้งเจ็ดประโยคนี้ถูกผลักมาจาก CM33_NS (prov_say) และเป็นสตริงในซอร์สที่ตรวจสอบแล้ว บน UART พิมพ์จริง: ลำดับการเชื่อมต่อ [MQTT] ใหม่จากบท C3 จากนั้น [CSR] Using DIRECT PUBLISH (bypassing publisher_task queue) และ [DirectPub] Publishing u bytes to 's' (จาก tesaiot_optiga_trust_m.c ที่อยู่ใน archive ไม่มีการปิดเสียง) แล้วเมื่อได้รับคำตอบ [Subscriber] Certificate from platform (d bytes) (subscriber_task.c:217 รูปแบบ (printf) จึงพิมพ์จริง)

ขั้นที่ 3 — Protect

Home → HSM Security → Protected Update

สิ่งที่ควรสังเกต บนหน้าจอ: Connecting to the platformAsking the platform for a signed manifestWaiting for the platform → ขั้น INSTALL → ขั้น VERIFY และผลตัดสินของมัน บน UART พิมพ์จริง ตามลำดับ:

[Subscriber] Protected Update bundle (%d bytes)
[PU-Ingest] Fragment count: %u
[PU-Ingest] OPTIGA acquired: OK
[PU-Ingest] STEP 3: Processing fragments...
[PU-Ingest] STEP 4: Executing OPTIGA Trust M Protected Update
[PU-Ingest] [4.1] Manifest verification OK (Trust Anchor signature valid)
[PU-Ingest] PROTECTED UPDATE COMPLETED SUCCESSFULLY!
[PU-Ingest] [ACK] Certificate ACK published successfully

(subscriber_task.c:205 ผ่าน (printf); tesaiot_pu_ingest.c ไม่มีการปิดเสียง — LABEL_SUBSCRIBER มีค่าเป็น "[PU-Ingest]") บรรทัด [4.1] Manifest verification OK ตามมาด้วย tesaiot_pu_progress(TESAIOT_PU_CHIP_VERIFIED_MANIFEST) ในโค้ด — นี่คือ เหตุการณ์เดียวที่ปลอมไม่ได้ การตรวจลายเซ็นดิจิทัลเกิดขึ้นภายใน Trust M เทียบกับ trust anchor ของตัวมันเอง ทุกอย่างก่อนหน้านั้น host ที่ถูกเจาะแล้วพิมพ์ออกมาเองได้

ขั้นที่ 4 — อ่านชิปอีกครั้ง

optiga.read_metadata(0xE0E1)

สิ่งที่ควรสังเกต แท็ก D0 ตอนนี้กำหนดให้ต้องใช้ manifest ที่ลงลายเซ็นแล้ว (ค่ารูป 21 e0 e8 ซึ่งระบุ anchor) จากเดิมที่เป็น e1 fc 07 ส่วนแท็ก C0 ไม่เปลี่ยน คงเป็น 01 หาก C0 ขยับ ให้หยุดและรายงาน เพราะไม่มีอะไรในกระบวนการนี้เขียนค่านั้น และ optiga.write_metadata() ปฏิเสธ TLV ที่มีแท็กนั้นอยู่

ขั้นที่ 5 — เชื่อมต่อใหม่แล้วดูการป้องกัน replay

ตัดไฟแล้วจ่ายไฟใหม่ เชื่อมต่อ WiFi แล้วเรียก tesaiot.connect() (หรือใช้หน้าจอ) broker จะส่ง bundle ที่เก็บค้างไว้ซ้ำ

สิ่งที่ควรสังเกต [Subscriber] Protected Update bundle (d bytes) ตามด้วย [PU-Ingest] Ignoring a Protected Update bundle nobody asked for. … และไม่มี OPTIGA acquired ไม่มีบรรทัด STEP ใด ๆ ไม่มีการเขียนใดเกิดขึ้น หากกลับเห็นลำดับ STEP 3/STEP 4 ครบทั้งที่ไม่ได้กดปุ่มใด แสดงว่า correlation id ถูกตั้งค้างไว้รอคำตอบทั้งที่ไม่ควรเป็นเช่นนั้น

ขั้นที่ 6 — Enrol เข้า slot ที่ล็อกไว้

สั่ง Enrol อีกครั้ง ตอนที่ 0xE0E1 กำหนดให้ต้องใช้ manifest แล้ว

สิ่งที่ควรสังเกต บนหน้าจอ: This slot takes signed manifests only. Use Protect, or clear the requirement first. Nothing was changed. — โดยยังไม่มีการสร้างกุญแจใด ๆ บนบอร์ดที่อยู่ในสถานะ Creation การเลือก HSM Security → Unlock (HSM_PROV_OP_UNLOCK) จะล้างข้อกำหนดนั้นและ Enrol กลับมาใช้งานได้อีกครั้ง

กับดัก

กับดัก 1 — เชื่อว่า enum เป็นตัวขับ switch
trustm_update_state() กำหนดค่าให้ trustm_state และประทับ tick ไว้ (tesaiot_optiga_trust_m.c:377-388) ตัวกำหนดลำดับคือบันได goto pu_done ใน tesaiot_pu_ingest.c และ prov_run_locked() แบบเส้นตรง หากเขียน switch (trustm_state) ในโค้ดของตน นั่นคือการประดิษฐ์เครื่องจักรที่เฟิร์มแวร์ไม่มีอยู่
กับดัก 2 — ตัวสมาชิก enum ที่ตายแล้ว 4 ตัว
APPLYING_UPDATE, WAITING_FOR_CERTIFICATE, COMPLETE, APPLYING_FRAGMENTS ไม่เคยถูกเขียนค่า แดชบอร์ดที่รอ COMPLETE จะรอไปตลอดกาล สถานะที่แทนความสำเร็จคือ PROTECTED_UPDATE_SUCCESS
กับดัก 3 — ข้ามการตรวจ correlation id ใน ingest ที่เขียนขึ้นเอง
bundle ที่เก็บค้างไว้จะมาถึงในทุกครั้งที่เชื่อมต่อ id เป็น NULL → ให้ทิ้ง ภาคผนวก X ข้อ 12
กับดัก 4 — เก็บ snapshot ของ g_optiga_ingest_events หลังการ publish
publish_csr และ tesaiot_publish_protected_update เป็นตัวตั้ง ingest ให้อยู่ในสถานะรอ ให้เก็บ snapshot ก่อน มิฉะนั้นการทำงานสำเร็จที่วิ่งแข่งกับการ publish จะถูกนับเป็นคำตอบของเรา
กับดัก 5 — จบรอบงานด้วย trustm_update_state(IDLE, …)
มีเพียง trustm_reset_state() เท่านั้นที่ล้าง correlation id รอบงานที่จบโดยไม่ผ่านฟังก์ชันนั้นจะทิ้งบอร์ดไว้ในสถานะรอ bundle ที่เก็บค้างไว้ชุดถัดไป
กับดัก 6 — เรียกฟังก์ชัน publish โดยไม่ตรวจ NULL
ทั้ง 2 ตัวเป็น weak ดูบท D3
กับดัก 7 — อ้างอิง tesaiot_protected_update_workflow.c หรือ tesaiot_csr_workflow.c
ทั้ง 2 ไฟล์ส่งมอบมาเป็นซอร์ส แต่ไม่มีไฟล์ใดถูกคอมไพล์โดย proj_cm33_ns ไฟล์แรกเรียก tesaiot_publish_protected_update() ด้วย สามอาร์กิวเมนต์เทียบกับ prototype ที่รับ 4 ตัว จึงคอมไพล์ไม่ผ่าน ส่วนไฟล์ที่สองมี TODO stub สำหรับการตรวจสอบใบรับรอง และจบด้วยการรอแบบไม่รู้จบโดยเจตนาเพื่อให้รีเซ็ตด้วยฮาร์ดแวร์ prov_run_locked() คือเส้นทางที่ส่งมอบจริง
กับดัก 8 — ป้ายกำกับขั้นตอนบนหน้าจอ
การรัน strings(1) บน libbento_cm55.a แสดง Key pair made in the chip, Request signed by that key, Sent to the platform, Waiting for the platform, The chip verified that signature, Checked against the key, Certificate written, Key possession proved เหล่านี้เป็น หลักฐานระดับ strings(1) กล่าวคือมีอยู่จริงใน archive แต่การจับคู่แต่ละป้ายเข้ากับค่า HSM_PROV_STEP_* ตัวใดตัวหนึ่งยังพิสูจน์จากซอร์สไม่ได้ (hsm_provision_ui.c ไม่ได้ส่งมอบมา) ให้ถือว่าเจ็ดประโยคของ prov_say บน CM33 คือสิ่งที่สังเกตได้และตรวจสอบแล้ว ส่วนสายของป้ายกำกับให้ถือว่ายังไม่ยืนยันจนกว่าจะมีซอร์ส
กับดัก 9 — เขียนแท็ก C0
สถานะ life-cycle เดินทางเดียว ทุกกระบวนการในบทนี้ไม่แตะมันเลย

Variant

variant ที่ใช้ได้
mtb-mpy และ mtb-only

libbento_hsm.a ลิงก์ได้ทั้งสอง variant ส่วน ingest, helper และ handler คอมไพล์ได้ทั้งสอง variant เช่นกัน mtb-mpy เพิ่ม tesaiot.protected_update() และโมดูล optiga สำหรับขั้นที่ 1 และ 4 ส่วนบน mtb-only ให้อ่าน metadata ผ่าน optiga_util_read_metadata() จาก task ของตนเอง ภายใต้วินัยของบท D1 ปุ่ม Enrol/Protect และ overlay บน CM55 เหมือนกันทั้งสอง variant