Mutual authentication with mTLS
Objectives
Section titled “Objectives”- Send telemetry with mTLS using the device client certificate and private key
- Compare Server-TLS and mTLS for security and key handling
Concepts
Section titled “Concepts”What mTLS is: the device must prove it holds the private key that matches its own certificate
Section titled “What mTLS is: the device must prove it holds the private key that matches its own certificate”mTLS (mutual TLS, or two-way TLS) adds a step to the ordinary handshake: after the server presents its own certificate for the device to check (as with Server-TLS in the previous lesson), the server also asks for the device’s certificate back, and the device must prove it holds the private key paired with that certificate (by signing part of the handshake with that key). The key point is that the private key is never sent over the network at all — only a “signature” proving the device holds the key ever travels. This is different from Server-TLS, where the device has to send a secret (an API key or a password) through the tunnel every time it connects.
The files that must be present, and the code’s check before connecting
Section titled “The files that must be present, and the code’s check before connecting”This example (device-mtls) always needs client_cert.pem and client_key.pem in certs_credentials/. main.c checks with file_exists(crt) && file_exists(key) before it ever tries to send data. If either is missing, it prints an error and exits immediately — there is no attempt to connect without a certificate. Once both files are present, the code sets tls.client_cert = crt; tls.client_key = key; and still keeps tls.verify_peer = 1U on, as in the previous lesson — the device still checks the server exactly as before; it now also has to present its own certificate.
CSR-based devices: the key never leaves the machine that created it
Section titled “CSR-based devices: the key never leaves the machine that created it”For devices that request a certificate through a CSR (Certificate Signing Request), the example’s README states that the bundle downloaded from the Admin Portal does not include the private key, because the key is generated on the device’s own side while creating the CSR (with scripts/generate_csr.sh, which calls openssl and then immediately chmod 600s the key file). The platform only ever receives the CSR, which carries the public key, to sign into a certificate — the private key itself never travels off the machine that created it. Even if the downloaded bundle leaks, the key does not leak with it. Users must copy this key into certs_credentials/client_key.pem themselves before use (a helper script, sync_csr_key.sh <DEVICE_ID>, is provided for this).
The CA used to check the server is still the system trust store — not the bundle’s ca-chain.pem
Section titled “The CA used to check the server is still the system trust store — not the bundle’s ca-chain.pem”Unlike the previous lesson, where the MQTTS branch used the device’s own ca-chain.pem to check the broker, this mTLS example sets tls.ca_chain = NULL; on both branches (HTTPS and MQTTS), with a source comment saying to use system trust and never force the device’s own CA — because both endpoints still use certificates from a public CA, exactly as before. What mTLS adds is not a different way of checking the server (that stays identical); it is that the device must also present its own certificate back.
mTLS uses different ports from Server-TLS
Section titled “mTLS uses different ports from Server-TLS”The platform clearly separates the mTLS listeners from the Server-TLS ones: mTLS uses HTTPS port 9444 and MQTTS port 8883 (per the defaults in config.h: DEFAULT_API_BASE_URL ending in :9444, DEFAULT_MQTT_PORT = 8883U), while Server-TLS uses 443 and 8884. The README warns that sending mTLS HTTPS to port 443 instead of 9444 lands on the wrong endpoint for mTLS, and runs into the SAN/domain mismatch it warns about.
Comparing Server-TLS and mTLS: the trade-off between security and key-management burden
Section titled “Comparing Server-TLS and mTLS: the trade-off between security and key-management burden”Both encrypt the channel equally, and the device checks the server the same way in both (verify_peer = 1 always). The difference is in how the device proves itself. Server-TLS is simpler to start with, because it only needs one secret — but that secret travels across the network every time the device connects (even though it is inside an encrypted tunnel), and if it leaks, an attacker can impersonate the device immediately with nothing else needed. mTLS never sends a secret across the network during authentication at all, but in exchange it carries the burden of managing certificates and keys: there must be a system to issue certificates, renew them before they expire, and keep the private key safe. If one device’s private key leaks, an attacker can impersonate that device until its certificate is revoked and a new keypair is issued — but the impact is limited to that one device, because each device has its own certificate and its own topic ACL (device/<device_id>/…). Never letting the key leave the device at all, the way OPTIGA Trust M’s secure element does (lesson 5.3), is a further step down that same risk.
Worked example
Section titled “Worked example”Before you run the example (checked 26 Sep 2026): at this commit the example’s
config.hstill defaults to the platform’s former domain ending in .com, which has moved to tesaiot.dev. The former API domain no longer resolves; the former MQTT name still works for now. SetDEFAULT_API_BASE_URLtohttps://tesaiot.dev:9444andDEFAULT_MQTT_HOSTtomqtt.tesaiot.dev(tracked in developer-hub issue #3).
This example is written in C and runs on a computer first (GCC/Clang with OpenSSL, mbedTLS or wolfSSL, or libcurl) so the protocol is easy to see, before you carry the same ideas over to the board. The excerpts below are copied from the actual files (Apache-2.0, tesaiot/developer-hub, commit d2ed42c).
main.c — both the device’s certificate and its key must exist before anything starts:
char ca[512], crt[512], key[512]; join_path(ca, sizeof(ca), certs_dir, FILE_CA_CHAIN); join_path(crt, sizeof(crt), certs_dir, FILE_CLIENT_CERT); join_path(key, sizeof(key), certs_dir, FILE_CLIENT_KEY); if (!(file_exists(crt) && file_exists(key))) { (void)fprintf(stderr, "mTLS requires client_cert.pem and client_key.pem in %s\n", certs_dir); return 1; }main.c — the TLS config binds the device’s certificate/key, and still checks the server exactly as before:
iot_tls_conf_t tls; (void)memset(&tls, 0, sizeof(tls)); tls.client_cert = crt; tls.client_key = key; tls.verify_peer = 1U; tls.connect_timeout_ms = (uint32_t)(HTTP_CONNECT_TIMEOUT_SEC * 1000L); tls.total_timeout_ms = (uint32_t)(HTTP_TOTAL_TIMEOUT_SEC * 1000L);main.c — the MQTTS branch: no username/password at all, because the certificate does that job instead:
if (is_mode_mqtts() != 0) { /* Send via MQTTS (mTLS) */ /* ... */ tls.ca_chain = NULL; tls.sni_name = mqtt_host; char topic[MAX_TOPIC_SIZE]; (void)snprintf(topic, sizeof(topic), "device/%s/telemetry", device_id); iot_mqtt_req_t mreq; (void)memset(&mreq, 0, sizeof(mreq)); mreq.host = mqtt_host; mreq.port = (uint16_t)mqtt_port; mreq.client_id = device_id; mreq.username = NULL; mreq.password = NULL; mreq.topic = topic; mreq.payload = json_buf; mreq.payload_len = strlen(json_buf); mreq.qos = 1U; mreq.retain = 0U; mreq.keepalive_sec = 30U; mreq.timeout_ms = (uint32_t)(HTTP_TOTAL_TIMEOUT_SEC * 1000L);- Example README · code folder · commit
d2ed42c - You need device credentials from the TESAIoT Platform, following the steps in the README. Never commit real credentials to a public repository.
Common mistakes
Section titled “Common mistakes”- Forgetting that a CSR-based device’s bundle has no private key in it — if you see an error about a missing key, run
./scripts/sync_csr_key.sh <DEVICE_ID>to copy the key from the machine that created the CSR. Do not ask the platform for a new key — it never had this key in the first place. - Using a Server-TLS port with mTLS credentials, or the reverse — mTLS uses 9444 (HTTPS) and 8883 (MQTTS). Going to 443 or 8884 lands on an endpoint that is not mTLS, and runs into a SAN/domain mismatch.
- Assuming mTLS means the server’s certificate no longer needs to be checked —
tls.verify_peer = 1Uis still on, exactly as with Server-TLS. mTLS only adds the device having to prove itself too; it does not reduce checking the server side at all. - Assuming Server-TLS and mTLS both use
X-API-KEY— mTLS uses no API key at all (hreq.api_key = NULL). If HTTPS returns an error in mTLS mode, check the device’s certificate/key, not the API key.
Try a change
Section titled “Try a change”- Guess before you change anything: pick one value the example’s README explains in the How section, and write down what you expect to change on the screen or in the log.
- Change and run: build + flash, then compare against your guess. If it does not match, find which part you misunderstood.
- Extend: add one thing the example does not yet have, and keep a photo or video in your portfolio.
Check your understanding
Section titled “Check your understanding”- What does mTLS add on top of Server-TLS?
- If a device’s private key leaks, what is the impact?
The answers are in the example’s README and in the code. If you cannot answer one, go back and read the Why / What / How section again.
References
Section titled “References”- Example on the Developer Hub
- The example lives in tesaiot/developer-hub (Apache-2.0) and is referenced by link
Review questions
Answer on your own first, then open the answer.
-
What does mTLS add over Server-TLS? (Objective 2)
- เข้ารหัสข้อมูลแรงขึ้น
- ไม่ต้องตรวจใบรับรองของเซิร์ฟเวอร์อีก
- ใช้ API key สองชั้น
- ระหว่าง handshake เซิร์ฟเวอร์ขอใบรับรองของอุปกรณ์ และอุปกรณ์ต้องพิสูจน์ว่าถือ private key คู่กับใบรับรองนั้น จึงไม่ต้องส่งรหัสผ่านหรือ API key
Show answer
Answer: D. ระหว่าง handshake เซิร์ฟเวอร์ขอใบรับรองของอุปกรณ์ และอุปกรณ์ต้องพิสูจน์ว่าถือ private key คู่กับใบรับรองนั้น จึงไม่ต้องส่งรหัสผ่านหรือ API key
ใน main.c ของ mTLS tls.client_cert และ tls.client_key ชี้ไปยังไฟล์ของอุปกรณ์ ส่วน username และ password เป็น NULL และ verify_peer ยังเป็น 1 อุปกรณ์จึงยังตรวจเซิร์ฟเวอร์เหมือนเดิม ความแรงของการเข้ารหัสช่องทางไม่ได้ต่างกัน
-
Why does the bundle for a CSR-based device not include the private key? (Objective 1)
- เพราะไฟล์ใหญ่เกินไป
- เพราะกุญแจถูกสร้างฝั่งอุปกรณ์ตอนทำ CSR และไม่เคยออกจากฝั่งนั้น แพลตฟอร์มได้แค่ CSR ที่มี public key ไปลงนาม bundle ที่รั่วจึงไม่ทำให้กุญแจรั่ว
- เพราะแพลตฟอร์มลืมใส่ ต้องขอเพิ่ม
- เพราะ mTLS ไม่ใช้ private key
Show answer
Answer: B. เพราะกุญแจถูกสร้างฝั่งอุปกรณ์ตอนทำ CSR และไม่เคยออกจากฝั่งนั้น แพลตฟอร์มได้แค่ CSR ที่มี public key ไปลงนาม bundle ที่รั่วจึงไม่ทำให้กุญแจรั่ว
scripts/generate_csr.sh สร้างกุญแจ (ค่าเริ่มต้น ECDSA) ด้วย openssl แล้ว chmod 600 ก่อนสร้าง CSR README เรียกสิ่งนี้ว่า security requirement และให้นำกุญแจจากเครื่องที่สร้างมาวางเองก่อนใช้งาน ถ้าไม่มีไฟล์กุญแจ โปรแกรมจะหยุดพร้อมข้อความ mTLS requires client_cert.pem and client_key.pem
-
You send mTLS HTTPS to port 443 instead of 9444. What problem does the README warn about? (Objective 1)
- ไม่มีปัญหา แค่ช้ากว่า
- ต้องใส่ X-API-KEY เพิ่ม
- ปลายทางไม่ใช่ endpoint ของ mTLS: README ระบุว่า ingest แบบ mTLS ต้องใช้ :9444 ไม่ใช่ 443 และเตือนเรื่อง SAN/โดเมนไม่ตรง พอร์ต 443 เป็นของ Server-TLS ที่ใช้ API key
- ใบรับรองของอุปกรณ์หมดอายุทันที
Show answer
Answer: C. ปลายทางไม่ใช่ endpoint ของ mTLS: README ระบุว่า ingest แบบ mTLS ต้องใช้ :9444 ไม่ใช่ 443 และเตือนเรื่อง SAN/โดเมนไม่ตรง พอร์ต 443 เป็นของ Server-TLS ที่ใช้ API key
แพลตฟอร์มแยก listener ตามวิธีพิสูจน์ตัวตน mTLS ใช้ HTTPS 9444 และ MQTTS 8883 ส่วน Server-TLS ใช้ HTTPS 443 และ MQTTS 8884 เมื่อเชื่อมผิดพอร์ต ปลายทางจะไม่ขอหรือไม่ตรวจใบรับรองของอุปกรณ์ตามที่คาด การแก้ที่ถูกคือแก้ปลายทาง ไม่ใช่ปิดการตรวจ TLS
-
If one device's private key leaks, what is the impact? (Objective 2)
- ผู้โจมตีปลอมเป็นอุปกรณ์ตัวนั้นได้ (เชื่อมต่อและ publish ในนามของมัน) จนกว่าใบรับรองจะถูกเพิกถอนและออกคู่กุญแจใหม่ ผลจำกัดอยู่ที่อุปกรณ์ตัวนั้น เพราะแต่ละตัวมีใบรับรองและ topic ของตัวเอง
- อุปกรณ์ทุกตัวบนแพลตฟอร์มถูกปลอมได้
- ไม่มีผล เพราะยังต้องมี CA certificate
- ผู้โจมตีถอดรหัสข้อมูลของอุปกรณ์อื่นได้
Show answer
Answer: A. ผู้โจมตีปลอมเป็นอุปกรณ์ตัวนั้นได้ (เชื่อมต่อและ publish ในนามของมัน) จนกว่าใบรับรองจะถูกเพิกถอนและออกคู่กุญแจใหม่ ผลจำกัดอยู่ที่อุปกรณ์ตัวนั้น เพราะแต่ละตัวมีใบรับรองและ topic ของตัวเอง
ใบรับรองผูกกับ public key ของอุปกรณ์ ใครถือ private key คู่กันก็ผ่าน handshake ได้ ACL จำกัด topic เป็น device/<device_id>/… จึงจำกัดความเสียหาย การไม่ให้กุญแจออกจากชิปเลย (OPTIGA Trust M ในบทเรียน 5.3) คือทางลดความเสี่ยงนี้
-
Comparing Server-TLS and mTLS, which statements are true? (choose all that apply) (Objective 2)
- Server-TLS: อุปกรณ์ต้องเก็บรหัสผ่านหรือ API key เป็นความลับ และส่งมันในอุโมงค์ TLS ทุกครั้งที่เชื่อมต่อ
- mTLS: ไม่มีความลับถูกส่งผ่านเครือข่ายระหว่างพิสูจน์ตัวตน อุปกรณ์แค่แสดงว่าถือกุญแจ
- mTLS ทำให้อุปกรณ์ไม่ต้องตรวจใบรับรองของเซิร์ฟเวอร์
- mTLS มีภาระดูแลกุญแจ: ใบรับรองหมดอายุ ต้องต่ออายุหรือหมุนเวียน และต้องเก็บ private key ให้ปลอดภัย
- Server-TLS เข้ารหัสข้อมูลได้น้อยกว่า mTLS
Show answer
Answer: A. Server-TLS: อุปกรณ์ต้องเก็บรหัสผ่านหรือ API key เป็นความลับ และส่งมันในอุโมงค์ TLS ทุกครั้งที่เชื่อมต่อ · B. mTLS: ไม่มีความลับถูกส่งผ่านเครือข่ายระหว่างพิสูจน์ตัวตน อุปกรณ์แค่แสดงว่าถือกุญแจ · D. mTLS มีภาระดูแลกุญแจ: ใบรับรองหมดอายุ ต้องต่ออายุหรือหมุนเวียน และต้องเก็บ private key ให้ปลอดภัย
ทั้งสองโหมดเข้ารหัสช่องทางเท่ากัน และอุปกรณ์ตรวจเซิร์ฟเวอร์เหมือนกัน (verify_peer = 1) ความต่างอยู่ที่วิธีพิสูจน์ตัวตนของอุปกรณ์ Server-TLS เริ่มง่ายแต่ความลับเดินทางทุกครั้ง mTLS แข็งแรงกว่าแต่ต้องมีระบบออกและต่ออายุใบรับรอง
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.
"Mutual authentication with mTLS" 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: "ยืนยันตัวตนทั้งสองฝั่งด้วย mTLS" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
This lesson adapts the source below; keep its credit too.
https://github.com/tesaiot/developer-hub/blob/d2ed42c4a31232f553b6b8cef9ee7373db348c21/examples/embedded-devices/intermediate/device-mtls · Code stays in the Developer Hub and is linked at pinned commits, never copied: the episodes, practice codes and main-branch examples are Apache-2.0; the master template and the OPTIGA client carry Infineon/Cypress EULAs.
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