Enrolment with a CSR
Companion videos
Watch on YouTube (opens in a new tab)
-
Overall TESAIoT Secure Platform Thai Embedded Systems Association (TESA)
Videos by Thai Embedded Systems Association (TESA) · The whole series in the playlist AIoT Foundation
Module 5 · Secure device provisioning · Module overview · Course home
A device leaves the factory with a certificate from Infineon that can only prove it is a genuine Trust M (lesson 3.1).
Enrolment means obtaining a certificate that our platform issues, tied to that specific device’s device_id, without the private key ever leaving the chip even once.
Objectives
Section titled “Objectives”By the end of this lesson you will:
- Explain what a CSR contains, and why the private key does not need to leave the chip
- Track a request by correlation id and target OIDs, as the SDK’s example does
- State the buffer contract the CSR-publishing function imposes on its caller
Before you start
Section titled “Before you start”- Already covered: Lesson 4.2: Protected Update, and certificates from lesson 1.2
- On your computer:
openssl, and a freshlab_key.pemyou can regenerate following lab 1.2, step 5 - On the board: the SDK’s template, already building. The optional lab that really enrols needs a device that can already reach the platform (lesson 3.2), and the instructor’s permission.
- Read alongside: Lesson 5.3 of TESAIoT Firmware Stack: PSoC Edge E84 with the OPTIGA™ Trust M
See it work first
Section titled “See it work first”When you press HSM Security → Enrol Certificate on the screen, chapter D2 of the SDK docs says the screen shows these seven sentences, in order.
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 namesGuess first: the third sentence says the request gets “signed.” If the request already carries a public key, why sign it at all, and who checks that signature?
Concepts
Section titled “Concepts”1. What a CSR is, and why the private key does not need to leave the chip
Section titled “1. What a CSR is, and why the private key does not need to leave the chip”A CSR (certificate signing request), per RFC 2986 (PKCS #10), has three main parts: a subject — the name being requested for the certificate, a public key being requested for certification, and a signature the requester makes over the first two, with the matching private key.
The answer to the guess is that signature. It is a proof of possession. The issuer verifies the signature using the public key inside the request; if it verifies, the requester really does hold that pair’s private key. The issuer therefore never needs to see the private key at all. The certificate it issues carries the same subject and public key, plus an issuer and a validity period, signed by the CA.
On our board, chapter D2 states the subject is CN=<mqtt username>,O=TESAIoT. The key pair is generated inside the chip, and the request is signed with a key that never leaves it.
The full steps, per the header comment of 05_csr_enrolment.c:
- The device generates a key pair inside the chip, then builds a PEM CSR from the public key (this part is the consumer’s job, per the
consumer_must_provide.txtlist) publish_csr()wraps the CSR as JSON and publishes it todevice/<id>/commands/csron the already-open MQTT session- The platform signs it and replies on a different topic — per the SDK’s PU contract,
commands/certificatedirectly, with no manifest involved - The device’s subscriber receives the certificate, writes it to the slot, then verifies it matches the key inside the chip (
optiga_verify_cert_key_pair)
The SDK’s CSR_SUBMISSION_CONTRACT.md opens with rules that have “already cost someone a debugging session.”
- The topic always uses
device_id(a UUID), never the Trust M’s UID. The UID is the MQTT client id; if you use it in the topic, the broker refuses while the connection itself still looks fine, so the message just seems to vanish. - Subscribe to
device/<id>/commands/#before publishing the CSR. Replies are not retained; a reply that arrives before the subscription succeeds is gone forever. - Someone must tend the session. With no PINGREQ, the broker closes the session at 90 seconds, and any later reply is lost too.
- A rejected CSR gets silence. Nothing replies on the topic at all; the reason lives in the platform’s log, and a CSR must be longer than 100 bytes.
- There is also an HTTPS path for administrators or back-end systems holding a JWT, but a device still using its factory certificate uses the MQTT path.
2. Tracking a request by correlation id and OID
Section titled “2. Tracking a request by correlation id and OID”05_csr_enrolment.c explains three readers, two of which are traps.
trustm_current_correlation_id()is a new id thatpublish_csr()generates from the TRNG every time it is called. The platform’s reply is matched against this id;NULLmeans nothing is outstanding.trustm_requested_target_oid()andtrustm_requested_anchor_oid()are the OID pair the most recent Protected Update request named.publish_csr()does not set either of these. They are written only bytesaiot_publish_protected_update(), and read as0xE0E1/0xE0E8after a reset. After callingpublish_csr()alone, these two values still describe the previous PU request, or their defaults — never this CSR. Never use them as the sole label for a CSR enrolment.
The reset rule: trustm_reset_state() clears the correlation id. If called while a reply is still pending, an incoming certificate has nothing left to match against, and gets discarded unread.
Reset when you are done, or when whatever timeout you decided on has elapsed — never “just to be tidy.” And if you plan to wait for the result with a counter, capture the counter’s value before publishing, per chapter D2’s pitfall 4 —
otherwise a job that had already finished earlier gets counted as this request’s answer.
Chapter D2 has one more warning: trustm_state_t is only a variable with a timestamp — it is not a state machine, and no switch is driven by it.
Four of its values are never written on the normal path (APPLYING_UPDATE, WAITING_FOR_CERTIFICATE, COMPLETE, APPLYING_FRAGMENTS); a screen waiting for COMPLETE will wait forever.
3. The buffer contract of publish_csr()
Section titled “3. The buffer contract of publish_csr()”The function’s signature, per the tesaiot_hsm_api docs:
int publish_csr(uint8_t *csr, size_t csr_length, uint16_t target_oid, uint16_t trust_anchor_oid, uint32_t payload_version);The first parameter is uint8_t *, not const uint8_t *, and example 05 says this is not a typo: publish_csr() builds JSON in place, on top of your own buffer, to avoid allocating a second large block. The consequences:
- The buffer must be writable. A
constPEM string that lives in flash will fault. - The buffer must be larger than the CSR. It has to hold
{"device_id":"<id>","csr":"<CSR with \n escaped>","correlation_id":"<uuid>"}— that is, the CSR plus one extra byte per newline, plus roughly 45 bytes of fixed text, plus the device id and correlation id. The example recommends adding 256 bytes of headroom and not worrying further. csr_lengthis the length of the CSR, not the size of the buffer.- Your CSR is gone once the function returns. The buffer now holds JSON. Keep a copy first if you still need the CSR afterwards.
trust_anchor_oid and payload_version are accepted but not yet used (reserved). The example still recommends sending the real intended values, so the code stays correct on the day they start to matter.
Worked example
Section titled “Worked example”Taken from 05_csr_enrolment.c, lines 158–172 and 178–184 (TESAIoT PSE84 Dev Kit SDK, © Thai Embedded Systems Association, Apache-2.0)
/* Refuse rather than publish a fabricated CSR. A CSR the platform signs is * a certificate on a real device; the wrong one is worse than none. */ if (s_csr_len == 0u) { printf(" publish_csr() NOT called: s_csr is empty. Build a CSR first —\r\n" " generate the keypair in the chip, then produce the PEM. The\r\n" " archive cannot do this for you (optiga_generate_csr_pem is\r\n" " in consumer_must_provide.txt).\r\n"); return SDK_EX_NO_DATA; } if (s_csr_len + 256u > sizeof(s_csr)) { printf(" publish_csr() NOT called: %u-byte CSR in a %u-byte buffer " "leaves no room for the JSON envelope built in place\r\n", (unsigned)s_csr_len, (unsigned)sizeof(s_csr)); return SDK_EX_REFUSED; } int rc = publish_csr(s_csr, s_csr_len, (uint16_t)EXAMPLE_TARGET_OID, (uint16_t)EXAMPLE_ANCHOR_OID, (uint32_t)EXAMPLE_PAYLOAD_VER);
/* s_csr now holds the JSON envelope, not the CSR. Do not reuse it as a CSR. */ s_csr_len = 0u;Three things worth copying into your own work:
- Never send a fabricated CSR. A CSR the platform signs becomes a certificate on a real device — the wrong one is worse than none.
- Check the headroom before calling, using the CSR + 256 bytes rule; the example uses a
staticbuffer of 1280 bytes. - Zero the length immediately after calling, so no one later mistakes the now-JSON buffer for a CSR again.
This example has publishing disabled by default. You need a real MQTT session connected to the platform, and a real CSR built from a key inside the chip, before you enable DEFINES+=EXAMPLE_HSM_PUBLISH_CSR=1.
The comment gives the reason: a request missing either of those is just noise on someone else’s broker.
Practice
Section titled “Practice”Using example 05’s rules, decide for each case whether it is safe to call publish_csr() or must be fixed first, and how you would fix it.
static uint8_t buf[1280];holding a 620-byte CSR, then callingpublish_csr(buf, 620, 0xE0E1, 0xE0E8, 1)____static const char csr[] = "-----BEGIN CERTIFICATE REQUEST-----\n...";then callingpublish_csr((uint8_t *)csr, strlen(csr), ...)____static uint8_t buf[700];holding a 620-byte CSR ____publish_csr(buf, sizeof(buf), 0xE0E1, 0xE0E8, 1)wherebufholds a 620-byte CSR ____- After
publish_csr()returns0, the program printsbuf“to see the CSR that was sent” ____ - The reply has not arrived yet, but the program calls
trustm_reset_state()“to start with a clean state” ____
Solution
- Safe to call. 620 + 256 = 876, which does not exceed 1280.
- Must be fixed. A
constbuffer lives in flash and cannot be written;publish_csr()will fault while building JSON on top of it. Copy it into a writable buffer first. - Must be fixed. 620 + 256 = 876, which exceeds 700 — there is no room for the JSON built in place.
- Must be fixed.
csr_lengthmust be 620, not the buffer’s size. - Must be fixed. By then,
bufalready holds JSON. Keep a copy before calling if you want to see the CSR afterwards. - Must be fixed. Resetting clears the correlation id; a certificate arriving afterwards would be discarded. Wait until it is done, or until whatever timeout you actually intended has elapsed.
Check your understanding
Section titled “Check your understanding”The questions below are part of the full set in quiz.yaml, which the automated grader uses.
-
Why can the platform issue a certificate without ever seeing the device’s private key? (objective 1)
- a) Because the platform can guess the private key from the public key
- b) Because the CSR carries the public key, and the signature on the CSR proves the requester holds the matching private key
- c) Because the private key is sent encrypted
- d) Because certificates have nothing to do with keys
Solution
b. This is proof of possession. The issued certificate carries the same public key, so the private key stays inside the chip the whole time.
-
After calling only
publish_csr(), what doestrustm_requested_target_oid()tell you? (objective 2)- a) The slot this CSR’s certificate will be written to
- b) The OID from the previous Protected Update request, or its default
0xE0E1— it says nothing about this CSR - c) The chip’s UID
- d) The correlation id
Solution
b. This value is written only by
tesaiot_publish_protected_update(). What tracks a CSR is the correlation id. -
Which of these correctly states
publish_csr()’s buffer contract? (objective 3)- a) The buffer can be
const, andcsr_lengthis the buffer’s size - b) The buffer must be writable, larger than the CSR by enough room for the JSON built on top of it;
csr_lengthis the CSR’s length; and the CSR is gone after the call - c) The function allocates a fresh buffer for you
- d) The CSR must be sent as DER only
Solution
b. Example 05 spells this out in four points, and recommends 256 bytes of headroom.
- a) The buffer can be
Main lab: track a request without sending it, and read a CSR by eye
- Build example 05 without enabling publishing.
Record the lines
Terminal window make build ENABLE_PAGE_EXAMPLES=1 SDK_EXAMPLE_CM33=cm33/security/05_csr_enrolmentmake program BENTO_WORKSPACE="$(cd .. && pwd)"correlation id on entry,requested target OID,requested anchor OID, thetrustm_update_stateandtrustm_reset_statelines, and theSKIPPED publish_csr()message. Explain why the example dares to calltrustm_reset_state()right there (look at the condition just before it). - Build a CSR on your computer from the test key from lesson 1.2, using an obviously fake UUID, then read every part of it.
Find the subject, the public key and the signature algorithm in the output. The last line must say self-signature verify OK — this is the proof of possession a CA verifies.
Terminal window openssl ecparam -name prime256v1 -genkey -noout -out lab_key.pemopenssl req -new -key lab_key.pem -subj "/CN=00000000-0000-0000-0000-000000000000/O=TESAIoT" -out lab.csropenssl req -in lab.csr -noout -textopenssl req -in lab.csr -noout -verify - Count its size with
wc -c < lab.csrand its line count withgrep -c '' lab.csr, then compute how largepublish_csr()’s buffer would need to be by example 05’s formula, and compare it against the +256-byte rule. Delete the test key file afterwards. - Draw a sequence diagram of enrolment, showing every relevant MQTT topic (
commands/#,commands/csr,commands/certificate), where proof of possession is checked, and where the device verifies the certificate matches its key.
Optional lab: real enrolment — only with the instructor’s permission
Enrolling creates a new key pair inside the chip; the old key in that slot is gone permanently, and it writes the new certificate to slot 0xE0E1 with a plain write. If that slot is already locked by a Protected Update, the button refuses before doing anything.
- With the device connected to the platform in its current mode (lesson 3.2), press HSM Security → Enrol Certificate, and record all seven on-screen sentences against “See it work first.”
- Record the
[CSR] Using DIRECT PUBLISH ...and[Subscriber] Certificate from platform (N bytes)UART lines, per chapter D2. - Switch to
tls_mode=mtlsand reconnect. Look for the line[mTLS] device pair verified — using TESAIoT identityfrom lesson 3.1, which means the firmware has chosen to use the certificate and key you just enrolled.
Going further
Section titled “Going further”Enrolment takes several seconds and can wait on the platform for up to a minute. The next lesson looks at how the on-device screen handles work this long without freezing and without going silent.
Next lesson: Lesson 5.2: On-device provisioning screens
Reflect
Section titled “Reflect”- In your own system, where does “resetting state to be clean” cause a later reply to be discarded?
- If a rejected CSR gets no reply at all, how would your team know, and where would you look for evidence?
- Which functions in your own API write over the caller’s buffer in a way that the parameter’s name or type does not warn you about?
References
Section titled “References”- SDK: cm33/security/05_csr_enrolment.c
- SDK: CSR_SUBMISSION_CONTRACT.md
- SDK: PROTECTED_UPDATE_CONTRACT.md, section 4.3 (certificates from the CSR path)
- SDK: tesaiot_hsm_api docs
- D2 — Enrolment and Protected Update end to end (SDK docs built from commit ef72c1b)
- RFC 2986: PKCS #10 Certification Request Syntax
- Examples on the Developer Hub: PSoC Edge E84 + OPTIGA Trust M: TESAIoT MQTT Client (Cypress EULA, link only) · device-mtls, whose README notes that a bundle from CSR enrolment carries no private key
Review questions
Answer on your own first, then open the answer.
-
Why can the platform issue a certificate without ever seeing the device's private key? (Objective 1)
- เพราะแพลตฟอร์มเดากุญแจลับจากกุญแจสาธารณะได้
- เพราะ CSR มีกุญแจสาธารณะ และลายเซ็นบน CSR พิสูจน์ว่าผู้ขอถือกุญแจลับคู่นั้น
- เพราะกุญแจลับถูกส่งไปแบบเข้ารหัส
- เพราะใบรับรองไม่เกี่ยวกับกุญแจ
Show answer
Answer: B. เพราะ CSR มีกุญแจสาธารณะ และลายเซ็นบน CSR พิสูจน์ว่าผู้ขอถือกุญแจลับคู่นั้น
นี่คือ proof of possession ตาม PKCS
-
Select everything a CSR contains. (Objective 1)
- subject ที่ขอให้ใส่ในใบ
- กุญแจสาธารณะ
- ลายเซ็นที่ลงด้วยกุญแจลับคู่กัน
- กุญแจลับ
- ลายเซ็นของ CA
Show answer
Answer: A. subject ที่ขอให้ใส่ในใบ · B. กุญแจสาธารณะ · C. ลายเซ็นที่ลงด้วยกุญแจลับคู่กัน
CSR ไม่มีกุญแจลับ และยังไม่มีลายเซ็นของ CA ลายเซ็นของ CA อยู่ในใบรับรองที่ออกมาภายหลัง
-
After a bare publish_csr(), what does trustm_requested_target_oid() tell you? (Objective 2)
- ช่องที่ใบรับรองจาก CSR นี้จะถูกเขียน
- OID ของคำขอ Protected Update ครั้งก่อน หรือค่าเริ่มต้น 0xE0E1 ไม่ได้บอกเรื่อง CSR นี้
- UID ของชิป
- correlation id
Show answer
Answer: B. OID ของคำขอ Protected Update ครั้งก่อน หรือค่าเริ่มต้น 0xE0E1 ไม่ได้บอกเรื่อง CSR นี้
ค่านี้ถูกเขียนโดย tesaiot_publish_protected_update() เท่านั้น ตัวติดตามคำขอ CSR คือ correlation id
-
The platform's reply has not arrived yet, but the program calls trustm_reset_state(). What happens? (Objective 2)
- ไม่มีผล
- correlation id ถูกล้าง ใบรับรองที่มาถึงทีหลังไม่มีอะไรให้จับคู่และถูกทิ้ง
- ชิปสร้างกุญแจใหม่
- แพลตฟอร์มยกเลิกคำขอ
Show answer
Answer: B. correlation id ถูกล้าง ใบรับรองที่มาถึงทีหลังไม่มีอะไรให้จับคู่และถูกทิ้ง
ตัวอย่าง 05 ให้รีเซ็ตเมื่อเสร็จ หรือเมื่อหมดเวลาที่ตัดสินใจเอง ไม่ใช่เพื่อความสะอาด
-
Which statement is the buffer contract of publish_csr()? (Objective 3)
- บัฟเฟอร์เป็น const ได้ และ csr_length คือขนาดบัฟเฟอร์
- บัฟเฟอร์ต้องเขียนได้ ใหญ่กว่า CSR พอให้ JSON ที่สร้างทับ csr_length คือความยาว CSR และ CSR หายไปหลังเรียก
- ฟังก์ชันจองบัฟเฟอร์ใหม่ให้เอง
- ต้องส่ง CSR แบบ DER เท่านั้น
Show answer
Answer: B. บัฟเฟอร์ต้องเขียนได้ ใหญ่กว่า CSR พอให้ JSON ที่สร้างทับ csr_length คือความยาว CSR และ CSR หายไปหลังเรียก
publish_csr() สร้าง JSON ทับในบัฟเฟอร์ของผู้เรียก ตัวอย่าง 05 แนะนำให้เผื่อ 256 ไบต์เหนือขนาด CSR
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"Enrolment with a CSR" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
Thai attribution: "ลงทะเบียนด้วย CSR" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/secure-iot-optiga/m05-provisioning/l01-csr-enrolment/
This lesson adapts the source below; keep its credit too.
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK security examples and docs are linked at this commit; lesson pages quote short excerpts with attribution and copy no files.
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA