Skip to content

Telemetry to the TESAIoT Platform with Server-TLS

Companion videos

Watch on YouTube (opens in a new tab)

Videos by Thai Embedded Systems Association (TESA) · The whole series in the playlist AIoT Foundation

  1. Send telemetry to the platform over HTTPS or MQTTS with Server-TLS using the C example
  2. Explain what the CA certificate does when verifying the server

Who Server-TLS verifies, and what the device proves itself with

Section titled “Who Server-TLS verifies, and what the device proves itself with”

Server-TLS (one-way TLS) is a TLS handshake in which the server side always presents its own certificate for the device to check first. The device checks that the certificate is signed by a trusted CA and matches the hostname it intended to connect to — that is the only thing Server-TLS verifies: the server’s identity. The handshake itself does not verify the device’s identity. The device has to prove itself with a separate “secret” it holds, sent inside the tunnel once it is encrypted. In this example’s code (main.c), this shows up as tls.client_cert and tls.client_key always being set to NULL (no device-side certificate), while tls.verify_peer = 1U stays on the whole time — the device still checks the server, it just never presents a certificate of its own.

The two ways this example proves the device’s identity: an API key vs. username/password

Section titled “The two ways this example proves the device’s identity: an API key vs. username/password”

This example (device-servertls) can send data two ways, chosen through the COMM_MODE environment variable: HTTPS and MQTTS. Both are still Server-TLS (the server proves itself with a certificate either way), but the device proves itself differently on each path — HTTPS sends only the X-API-KEY header, read from api_key.txt (the code has a comment “omit Bearer”, meaning no Bearer token is sent as well), while MQTTS uses username = device_id with a password from mqtt_password.txt, whose hash the backend checks against its database. Either way, the secret (the API key or the password) always travels inside the already-encrypted TLS tunnel, never in the clear.

ca-chain.pem is the broker’s trust anchor, but this example’s HTTPS path uses the system trust store instead

Section titled “ca-chain.pem is the broker’s trust anchor, but this example’s HTTPS path uses the system trust store instead”

main.c sets up the initial TLS config with tls.ca_chain = file_exists(PATH_CA_CHAIN) ? PATH_CA_CHAIN : NULL; — if a ca-chain.pem file (from the bundle downloaded from the Admin Portal) exists in the certs_credentials/ folder, the code uses it as the trust anchor when connecting over MQTTS (also setting tls.sni_name = mqtt_host), to check that the broker’s certificate was issued by the platform’s own internal CA. But on entering the HTTPS branch, the code immediately overwrites this with tls.ca_chain = NULL;, with a source comment saying to use system trust — because the platform’s public HTTPS endpoint uses a certificate from a public CA the OS already trusts, so it does not need to carry its own ca-chain.pem to check again. This is a detail that differs by connection path within the same example, not a fixed rule that Server-TLS must always use ca-chain.pem.

Ports are split by authentication method, not by protocol

Section titled “Ports are split by authentication method, not by protocol”

The platform separates listeners by how the device proves itself. Server-TLS uses MQTT port 8884 and HTTPS port 443 (per the defaults in config.h: DEFAULT_MQTT_PORT = 8884U, DEFAULT_API_BASE_URL), while mTLS (the next lesson) uses different ports. If Server-TLS username/password credentials are used to connect to port 8883 (the mTLS port), TLS is closed right after CONNECT, because that listener expects the device’s certificate, not a username/password.

The order of steps, and what fails when one is missing

Section titled “The order of steps, and what fails when one is missing”

This example’s order is: (1) read credentials from files in certs_credentials/ — device_id, api_key or the MQTT username/password, and endpoints.json if present; (2) open the TLS handshake to the configured endpoint, always checking the server’s certificate (verify_peer = 1U is never turned off); (3) once the handshake succeeds, send the device’s credential inside the tunnel (a header, or the MQTT CONNECT packet); (4) send the JSON payload {device_id, timestamp, data} to the topic device/<device_id>/telemetry (MQTTS) or the /api/v1/telemetry endpoint (HTTPS). If step 1 is missing (no credential file), the program stops immediately with an error. If step 2 fails (for example the server’s certificate has expired, or the device’s clock is wrong enough that the certificate looks like it is not valid yet), the handshake always fails before step 3 is reached. If step 2 succeeds but step 3 is wrong (for example the password has expired), the broker answers with a rejection code (MQTT CONNACK code 5, “Not authorized”) — a different cause than a TLS problem. The example’s README clearly separates these two cases under Troubleshooting.

Before you run the example (checked 26 Sep 2026): at this commit the example’s config.h still 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. Set DEFAULT_API_BASE_URL to https://tesaiot.dev and DEFAULT_MQTT_HOST to mqtt.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 — the initial TLS config: no device-side certificate, but the server is still always checked:

/* Mongoose TLS conf */
iot_tls_conf_t tls; (void)memset(&tls, 0, sizeof(tls));
tls.ca_chain = file_exists(PATH_CA_CHAIN) ? PATH_CA_CHAIN : NULL;
tls.client_cert = NULL; tls.client_key = NULL; /* serverTLS */
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: uses the device’s own ca-chain.pem to check the broker, and proves itself with username/password:

if (is_mode_mqtts() != 0) {
/* MQTTS (Server‑TLS): username/password + CA verify */
char topic[MAX_TOPIC_SIZE]; (void)snprintf(topic, sizeof(topic), "device/%s/telemetry", dev_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 = dev_id;
mreq.username = (mqtt_user[0] != '\0') ? mqtt_user : dev_id; /* fallback to device_id */
mreq.password = (mqtt_pass[0] != '\0') ? mqtt_pass : NULL;
mreq.topic = topic; mreq.payload = json; mreq.payload_len = strlen(json);
mreq.qos = 1U; mreq.retain = 0U; mreq.keepalive_sec = 30U; mreq.timeout_ms = (uint32_t)(HTTP_TOTAL_TIMEOUT_SEC * 1000L);
tls.sni_name = mqtt_host;
const int rc = iot_mqtts_publish(&mreq, &tls);

main.c — the HTTPS branch: uses the system trust store instead of ca-chain.pem, and proves itself with X-API-KEY:

} else {
/* HTTPS (Server‑TLS): Bearer/X-API-KEY */
/* ... */
tls.ca_chain = NULL;
tls.sni_name = NULL;
iot_http_req_t hreq; (void)memset(&hreq, 0, sizeof(hreq));
hreq.url = url; hreq.body = json; hreq.body_len = strlen(json);
/* For Server‑TLS HTTPS, send only X-API-KEY (omit Bearer) */
hreq.api_key = api_key; /* include device API key header */
hreq.timeout_ms = (uint32_t)(HTTP_TOTAL_TIMEOUT_SEC * 1000L);
const int rc = iot_https_post(&hreq, &tls);
  • 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.
  • Assuming Server-TLS also verifies the device’s identity — Server-TLS only verifies the server side. The device must prove itself separately with an API key or username/password. Missing this can lead to under-protecting the API key/password, from the mistaken belief that the certificate already does that job.
  • Using the wrong port for the mode (8883 instead of 8884, or the reverse) — ports are split by how the device authenticates, not by whether TLS is used. Using the mTLS port with Server-TLS credentials closes the TLS connection right after CONNECT.
  • Seeing MQTT CONNACK Code 5 (Not authorized) and assuming it is a TLS/certificate problem — Code 5 only happens after the TLS handshake has already succeeded, so it is a problem at the device-authentication step (a wrong or not-yet-synced username/password), not a certificate problem.
  • Assuming ca-chain.pem must be used to verify every connection path — in this example, only MQTTS uses the device’s own ca-chain.pem; HTTPS uses the operating system’s trust store instead. That is a choice specific to this example, not a general rule for Server-TLS.
  1. 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.
  2. Change and run: build + flash, then compare against your guess. If it does not match, find which part you misunderstood.
  3. Extend: add one thing the example does not yet have, and keep a photo or video in your portfolio.
  • Whose identity does Server-TLS verify, and whose does it not verify?
  • If the device’s clock is wrong, why might TLS fail?

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.

Review questions

Answer on your own first, then open the answer.

  1. In this example's Server-TLS mode, who proves identity, and with what? (Objective 2)

    1. ทั้งสองฝั่งพิสูจน์ด้วยใบรับรอง
    2. อุปกรณ์พิสูจน์ด้วยใบรับรอง ส่วนเซิร์ฟเวอร์ไม่ต้องพิสูจน์
    3. เซิร์ฟเวอร์พิสูจน์ด้วยใบรับรองที่อุปกรณ์ตรวจกับ CA ระหว่าง handshake ส่วนอุปกรณ์พิสูจน์ด้วย credential ที่ส่งในอุโมงค์ TLS: API key สำหรับ HTTPS หรือ username/password สำหรับ MQTTS
    4. ไม่มีใครพิสูจน์ TLS แค่เข้ารหัส
    Show answer

    Answer: C. เซิร์ฟเวอร์พิสูจน์ด้วยใบรับรองที่อุปกรณ์ตรวจกับ CA ระหว่าง handshake ส่วนอุปกรณ์พิสูจน์ด้วย credential ที่ส่งในอุโมงค์ TLS: API key สำหรับ HTTPS หรือ username/password สำหรับ MQTTS

    main.c ตั้ง tls.client_cert และ tls.client_key เป็น NULL แต่ verify_peer = 1 จากนั้น HTTPS ส่ง header X-API-KEY และ MQTTS ใช้ username = device_id กับ password จาก bundle TLS จึงยืนยันตัวตนของเซิร์ฟเวอร์ ส่วนตัวตนของอุปกรณ์อยู่ที่ความลับที่มันถือ

  2. What role does ca-chain.pem play in the MQTTS connection? (Objective 2)

    1. เป็นจุดเริ่มของความเชื่อถือ (trust anchor) ที่อุปกรณ์ใช้ตรวจว่าใบรับรองของ broker ออกโดย CA ที่เชื่อถือ และตรงกับชื่อเครื่องที่ตั้งใจเชื่อมต่อ (SNI/hostname) จึงกันการปลอมตัวเป็นเซิร์ฟเวอร์
    2. ใช้เข้ารหัส payload ก่อนส่ง
    3. ใช้ระบุตัวตนของอุปกรณ์กับ broker
    4. เป็นความลับที่ต้องเก็บแบบเดียวกับรหัสผ่าน
    Show answer

    Answer: A. เป็นจุดเริ่มของความเชื่อถือ (trust anchor) ที่อุปกรณ์ใช้ตรวจว่าใบรับรองของ broker ออกโดย CA ที่เชื่อถือ และตรงกับชื่อเครื่องที่ตั้งใจเชื่อมต่อ (SNI/hostname) จึงกันการปลอมตัวเป็นเซิร์ฟเวอร์

    ในโหมด MQTTS โค้ดตั้ง tls.ca_chain เป็นไฟล์นี้และ tls.sni_name = mqtt_host ถ้าใบรับรองของเซิร์ฟเวอร์ไม่ได้ลงนามโดย CA ในไฟล์ handshake จะล้ม CA certificate เป็นข้อมูลสาธารณะ ไม่ใช่ความลับ ส่วนการเข้ารหัสข้อมูลใช้กุญแจชั่วคราวที่ตกลงกันระหว่าง handshake (ฝั่ง HTTPS ในตัวอย่างใช้ trust store ของระบบแทน)

  3. You use Server-TLS credentials (username/password) but set MQTT_PORT = 8883. What happens? (Objective 1)

    1. เชื่อมต่อได้ปกติ พอร์ตไหนก็ได้
    2. broker ตอบ Code 5 Not authorized เสมอ
    3. ได้ความเร็วสูงขึ้นเพราะเป็นพอร์ตมาตรฐาน
    4. TLS ถูกปิดหลัง CONNECT เพราะ 8883 เป็นพอร์ตของ mTLS ที่ต้องการใบรับรองของอุปกรณ์ Server-TLS ต้องใช้พอร์ต 8884
    Show answer

    Answer: D. TLS ถูกปิดหลัง CONNECT เพราะ 8883 เป็นพอร์ตของ mTLS ที่ต้องการใบรับรองของอุปกรณ์ Server-TLS ต้องใช้พอร์ต 8884

    หัวข้อ Troubleshooting ใน README ระบุว่าถ้าไป 8883 จะเจอ TLS ปิด connection หลัง CONNECT เพราะโหมดไม่ตรง แพลตฟอร์มแยกพอร์ตตามวิธีพิสูจน์ตัวตน: Server-TLS ใช้ MQTT 8884 และ HTTPS 443 ส่วน mTLS ใช้ 8883 และ 9444

  4. The connection to port 8884 works, but the broker answers Code 5 (Not authorized). What cause does the README name? (Objective 1)

    1. CA certificate หมดอายุ
    2. username ไม่ตรงกับ device_id หรือ password หมดอายุหรือยังไม่ได้รีเซ็ต ต้องดาวน์โหลด bundle ที่เลือก include_password แล้วซิงค์ใหม่
    3. payload ใหญ่เกินไป
    4. QoS ต้องเป็น 0
    Show answer

    Answer: B. username ไม่ตรงกับ device_id หรือ password หมดอายุหรือยังไม่ได้รีเซ็ต ต้องดาวน์โหลด bundle ที่เลือก include_password แล้วซิงค์ใหม่

    Code 5 เกิดหลัง TLS สำเร็จแล้ว จึงไม่ใช่ปัญหาใบรับรอง แต่เป็นขั้นพิสูจน์ตัวตนของอุปกรณ์ นอกจากนี้การ publish ต้องใช้ topic device/<device_id>/telemetry เท่านั้นตาม ACL ถ้าผิด topic จะถูกปฏิเสธเช่นกัน

  5. The device clock reads the year 2000 after power-up. Why might the TLS connection fail? (Objective 2)

    1. ใบรับรองของเซิร์ฟเวอร์มีช่วงเวลาที่ใช้ได้ (not before / not after) เมื่อเทียบกับนาฬิกาที่ผิด ใบรับรองจะดูเหมือนยังไม่ถึงเวลาใช้ การตรวจจึงล้ม ต้องตั้งเวลาให้ถูก (เช่นผ่าน SNTP) ก่อนเชื่อมต่อ
    2. TLS ใช้เวลาเป็นกุญแจ จึงถอดรหัสไม่ได้
    3. broker ปฏิเสธเพราะ timestamp ใน payload
    4. เวลาไม่เกี่ยวกับ TLS
    Show answer

    Answer: A. ใบรับรองของเซิร์ฟเวอร์มีช่วงเวลาที่ใช้ได้ (not before / not after) เมื่อเทียบกับนาฬิกาที่ผิด ใบรับรองจะดูเหมือนยังไม่ถึงเวลาใช้ การตรวจจึงล้ม ต้องตั้งเวลาให้ถูก (เช่นผ่าน SNTP) ก่อนเชื่อมต่อ

    การตรวจใบรับรองรวมถึงการตรวจว่าเวลาปัจจุบันอยู่ในช่วงอายุของใบรับรอง อุปกรณ์ฝังตัวมักเริ่มด้วยนาฬิกาที่ไม่ถูกต้อง ตัวอย่าง OPTIGA ในบทเรียน 5.3 จึงรอ NTP sync ก่อนเชื่อมต่อ ส่วน timestamp ใน payload ของตัวอย่างนี้ก็มาจาก time() เดียวกัน ถ้าเวลาเพี้ยน ข้อมูลบนแพลตฟอร์มก็จะเพี้ยนตาม

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.

"Telemetry to the TESAIoT Platform with Server-TLS" 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: "ส่งข้อมูลขึ้น TESAIoT Platform ด้วย Server-TLS" จาก 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/tesaiot-firmware-stack/m05-connect-to-platform/l01-server-tls/

This lesson adapts the source below; keep its credit too.
https://github.com/tesaiot/developer-hub/blob/d2ed42c4a31232f553b6b8cef9ee7373db348c21/examples/embedded-devices/entry/device-servertls · 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.

Full guide: how to cite TESA

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