ข้ามไปยังเนื้อหา

ยืนยันตัวตนทั้งสองฝั่งด้วย mTLS

  1. ส่ง telemetry ด้วย mTLS โดยใช้ client certificate และ private key ของอุปกรณ์
  2. เปรียบเทียบ Server-TLS กับ mTLS ในด้านความปลอดภัยและการดูแลกุญแจ

mTLS คืออะไร: อุปกรณ์ต้องพิสูจน์ว่าถือ private key คู่กับใบรับรองของตัวเอง

หัวข้อที่มีชื่อว่า “mTLS คืออะไร: อุปกรณ์ต้องพิสูจน์ว่าถือ private key คู่กับใบรับรองของตัวเอง”

mTLS (mutual TLS หรือ TLS สองทาง) เพิ่มขั้นตอนเข้าไปใน handshake ปกติ: หลังจากเซิร์ฟเวอร์ยื่นใบรับรองของตัวเองให้อุปกรณ์ตรวจแล้ว (เหมือน Server-TLS ในบทเรียนก่อน) เซิร์ฟเวอร์จะขอใบรับรองของอุปกรณ์กลับด้วย และอุปกรณ์ต้องพิสูจน์ว่าถือ private key ที่จับคู่กับใบรับรองนั้นจริง (โดยเซ็นข้อมูลบางส่วนของ handshake ด้วยกุญแจนั้น) จุดสำคัญคือ private key ไม่เคยถูกส่งผ่านเครือข่ายเลย มีแต่ “ลายเซ็น” ที่พิสูจน์ว่าอุปกรณ์ถือกุญแจอยู่เท่านั้น — ต่างจาก Server-TLS ที่อุปกรณ์ต้องส่งความลับ (API key หรือ password) ผ่านอุโมงค์ทุกครั้งที่เชื่อมต่อ

ไฟล์ที่ต้องมี และโค้ดตรวจก่อนเชื่อมต่อเสมอ

หัวข้อที่มีชื่อว่า “ไฟล์ที่ต้องมี และโค้ดตรวจก่อนเชื่อมต่อเสมอ”

ตัวอย่างนี้ (device-mtls) ต้องมีไฟล์ client_cert.pem และ client_key.pem อยู่ใน certs_credentials/ เสมอ main.c เช็กด้วย file_exists(crt) && file_exists(key) ก่อนเริ่มส่งข้อมูลทุกครั้ง ถ้าไม่ครบจะพิมพ์ error แล้วออกจากโปรแกรมทันที ไม่มีการลองเชื่อมต่อแบบไม่มีใบรับรอง เมื่อไฟล์ครบ โค้ดตั้ง tls.client_cert = crt; tls.client_key = key; และยังคงเปิด tls.verify_peer = 1U เหมือนบทเรียนก่อน — อุปกรณ์ยังตรวจเซิร์ฟเวอร์เหมือนเดิม เพิ่มเติมคือต้องยื่นใบรับรองของตัวเองด้วย

อุปกรณ์แบบ CSR: กุญแจไม่เคยออกจากเครื่องที่สร้างมันเลย

หัวข้อที่มีชื่อว่า “อุปกรณ์แบบ CSR: กุญแจไม่เคยออกจากเครื่องที่สร้างมันเลย”

สำหรับอุปกรณ์ที่ขอใบรับรองผ่าน CSR (Certificate Signing Request) README ของตัวอย่างระบุว่า bundle ที่ดาวน์โหลดจาก Admin Portal จะไม่มี private key รวมมาด้วย เพราะกุญแจถูกสร้างขึ้นที่ฝั่งอุปกรณ์เองตอนสร้าง CSR (ด้วย scripts/generate_csr.sh ที่เรียก openssl แล้ว chmod 600 ทันที) แพลตฟอร์มได้รับแค่ CSR ที่มี public key ไปลงนามเป็นใบรับรอง กุญแจส่วนตัวจึงไม่เคยเดินทางออกจากเครื่องที่สร้างมันเลย ถึง bundle ที่ดาวน์โหลดจะรั่วก็ไม่ทำให้กุญแจรั่วตามไปด้วย ผู้ใช้ต้องคัดลอกกุญแจนี้มาวางเป็น certs_credentials/client_key.pem เองก่อนใช้งาน (มีสคริปต์ช่วย sync_csr_key.sh <DEVICE_ID>)

CA ที่ใช้ตรวจเซิร์ฟเวอร์ยังเป็น system trust เหมือนเดิม ไม่ใช้ ca-chain.pem ของ bundle

หัวข้อที่มีชื่อว่า “CA ที่ใช้ตรวจเซิร์ฟเวอร์ยังเป็น system trust เหมือนเดิม ไม่ใช้ ca-chain.pem ของ bundle”

ต่างจากบทเรียนก่อนที่สาขา MQTTS ใช้ ca-chain.pem ของอุปกรณ์ตรวจ broker ตัวอย่าง mTLS นี้ตั้ง tls.ca_chain = NULL; ในทั้งสองสาขา (HTTPS และ MQTTS) พร้อมคอมเมนต์ในซอร์สว่าให้ใช้ system trust และห้ามบังคับใช้ CA ของอุปกรณ์เอง เพราะปลายทางทั้งสองยังใช้ใบรับรองจาก public CA เหมือนเดิม สิ่งที่ mTLS เพิ่มเข้ามาไม่ใช่วิธีตรวจเซิร์ฟเวอร์ (เหมือนเดิมทุกประการ) แต่คือการที่อุปกรณ์ต้องยื่นใบรับรองของตัวเองกลับไปด้วย

แพลตฟอร์มแยก listener ของ mTLS ออกจาก Server-TLS ชัดเจน mTLS ใช้ HTTPS พอร์ต 9444 และ MQTTS พอร์ต 8883 (ตามค่า default ใน config.h: DEFAULT_API_BASE_URL ลงท้ายด้วย :9444, DEFAULT_MQTT_PORT = 8883U) ขณะที่ Server-TLS ใช้ 443 และ 8884 README เตือนไว้ว่าถ้าส่ง HTTPS mTLS ไปที่พอร์ต 443 แทน 9444 ปลายทางจะไม่ใช่ endpoint ของ mTLS และจะเจอปัญหา SAN/โดเมนไม่ตรงตามที่คาด

เปรียบเทียบ Server-TLS กับ mTLS: จุดแลกเปลี่ยนด้านความปลอดภัยกับภาระดูแลกุญแจ

หัวข้อที่มีชื่อว่า “เปรียบเทียบ Server-TLS กับ mTLS: จุดแลกเปลี่ยนด้านความปลอดภัยกับภาระดูแลกุญแจ”

ทั้งสองแบบเข้ารหัสช่องทางเท่ากันและอุปกรณ์ตรวจเซิร์ฟเวอร์เหมือนกัน (verify_peer = 1 เสมอ) ความต่างอยู่ที่วิธีพิสูจน์ตัวตนของอุปกรณ์ Server-TLS เริ่มต้นง่ายกว่าเพราะแค่มีความลับหนึ่งค่า แต่ความลับนั้นเดินทางผ่านเครือข่ายทุกครั้งที่เชื่อมต่อ (แม้จะอยู่ในอุโมงค์ที่เข้ารหัสแล้วก็ตาม) และถ้าความลับหลุด ผู้โจมตีก็ปลอมเป็นอุปกรณ์ได้ทันทีโดยไม่ต้องมีอะไรเพิ่ม ส่วน mTLS ไม่มีความลับเดินทางผ่านเครือข่ายระหว่างพิสูจน์ตัวตนเลย แต่แลกกับภาระดูแลใบรับรองและกุญแจ: ต้องมีระบบออกใบรับรอง ต่ออายุก่อนหมดอายุ และเก็บ private key ให้ปลอดภัย — ถ้า private key ของอุปกรณ์หนึ่งรั่ว ผู้โจมตีปลอมเป็นอุปกรณ์ตัวนั้นได้จนกว่าใบรับรองจะถูกเพิกถอนและออกคู่กุญแจใหม่ แต่ผลกระทบจำกัดอยู่ที่อุปกรณ์ตัวนั้นเพราะแต่ละตัวมีใบรับรองและ ACL ของ topic เป็นของตัวเอง (device/<device_id>/…) การไม่ให้กุญแจออกจากอุปกรณ์เลยตั้งแต่แรก อย่างที่ทำด้วย secure element ใน OPTIGA Trust M (บทเรียน 5.3) คือทางลดความเสี่ยงนี้ไปอีกขั้น

ก่อนรันตัวอย่าง (ตรวจเมื่อ 26 ก.ย. 2026): config.h ของตัวอย่างที่ commit นี้ยังตั้งค่าเริ่มต้นเป็นโดเมนเดิมของแพลตฟอร์มที่ลงท้ายด้วย .com ซึ่งย้ายไปเป็น tesaiot.dev แล้ว โดเมนเดิมของ API ไม่ resolve แล้ว ส่วนของ MQTT ยังใช้ได้ชั่วคราว ให้ตั้ง DEFAULT_API_BASE_URL เป็น https://tesaiot.dev:9444 และ DEFAULT_MQTT_HOST เป็น mqtt.tesaiot.dev (บันทึกไว้ที่ developer-hub issue #3)

ตัวอย่างนี้เป็นภาษา C ที่รันบนคอมพิวเตอร์ก่อน (GCC/Clang + OpenSSL, mbedTLS หรือ wolfSSL หรือ libcurl) เพื่อให้เห็นโปรโตคอลชัด แล้วจึงนำแนวคิดเดียวกันไปใช้บนบอร์ด โค้ดด้านล่างคัดลอกจากไฟล์จริง (Apache-2.0, tesaiot/developer-hub, commit d2ed42c)

main.c — ต้องมีทั้งใบรับรองและกุญแจของอุปกรณ์ก่อนเริ่มทำงาน:

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 — TLS conf ผูกใบรับรอง/กุญแจของอุปกรณ์ ยังตรวจเซิร์ฟเวอร์เหมือนเดิม:

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 — สาขา MQTTS: ไม่มี username/password เลย เพราะใบรับรองทำหน้าที่พิสูจน์ตัวตนแทน:

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);
  • ลืมว่า bundle ของอุปกรณ์แบบ CSR ไม่มี private key มาด้วย — ถ้าเห็นข้อความ error ว่าไม่มีกุญแจ ต้องรัน ./scripts/sync_csr_key.sh <DEVICE_ID> เพื่อคัดลอกกุญแจจากเครื่องที่สร้าง CSR เอง ไม่ใช่ไปขอกุญแจใหม่จากแพลตฟอร์ม เพราะแพลตฟอร์มไม่เคยมีกุญแจนี้ตั้งแต่แรก
  • ใช้พอร์ตของ Server-TLS กับ credential แบบ mTLS หรือกลับกัน — mTLS ใช้ 9444 (HTTPS) และ 8883 (MQTTS) ถ้าไปที่ 443 หรือ 8884 ปลายทางจะไม่ใช่ endpoint ของ mTLS และจะเจอปัญหา SAN/โดเมนไม่ตรง
  • คิดว่า mTLS ทำให้ไม่ต้องตรวจใบรับรองของเซิร์ฟเวอร์อีกต่อไป — tls.verify_peer = 1U ยังเปิดอยู่เหมือน Server-TLS ทุกประการ mTLS แค่เพิ่มการที่อุปกรณ์ต้องพิสูจน์ตัวเองด้วย ไม่ได้ลดการตรวจฝั่งเซิร์ฟเวอร์ลงเลย
  • เข้าใจว่า Server-TLS กับ mTLS ใช้ X-API-KEY เหมือนกัน — mTLS ไม่ใช้ API key เลย (hreq.api_key = NULL) ถ้าเจอ HTTPS ตอบรหัสผิดพลาดในโหมด mTLS ต้องตรวจใบรับรอง/กุญแจของอุปกรณ์ ไม่ใช่ไปตรวจ API key
  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
  • mTLS เพิ่มอะไรจาก Server-TLS
  • ถ้า private key ของอุปกรณ์รั่ว ผลกระทบคืออะไร

คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. mTLS เพิ่มอะไรจาก Server-TLS (เป้าหมายข้อ 2)

    1. เข้ารหัสข้อมูลแรงขึ้น
    2. ไม่ต้องตรวจใบรับรองของเซิร์ฟเวอร์อีก
    3. ใช้ API key สองชั้น
    4. ระหว่าง handshake เซิร์ฟเวอร์ขอใบรับรองของอุปกรณ์ และอุปกรณ์ต้องพิสูจน์ว่าถือ private key คู่กับใบรับรองนั้น จึงไม่ต้องส่งรหัสผ่านหรือ API key
    ดูเฉลย

    คำตอบ: D. ระหว่าง handshake เซิร์ฟเวอร์ขอใบรับรองของอุปกรณ์ และอุปกรณ์ต้องพิสูจน์ว่าถือ private key คู่กับใบรับรองนั้น จึงไม่ต้องส่งรหัสผ่านหรือ API key

    ใน main.c ของ mTLS tls.client_cert และ tls.client_key ชี้ไปยังไฟล์ของอุปกรณ์ ส่วน username และ password เป็น NULL และ verify_peer ยังเป็น 1 อุปกรณ์จึงยังตรวจเซิร์ฟเวอร์เหมือนเดิม ความแรงของการเข้ารหัสช่องทางไม่ได้ต่างกัน

  2. ทำไม bundle ของอุปกรณ์ที่สร้างจาก CSR จึงไม่มี private key มาด้วย (เป้าหมายข้อ 1)

    1. เพราะไฟล์ใหญ่เกินไป
    2. เพราะกุญแจถูกสร้างฝั่งอุปกรณ์ตอนทำ CSR และไม่เคยออกจากฝั่งนั้น แพลตฟอร์มได้แค่ CSR ที่มี public key ไปลงนาม bundle ที่รั่วจึงไม่ทำให้กุญแจรั่ว
    3. เพราะแพลตฟอร์มลืมใส่ ต้องขอเพิ่ม
    4. เพราะ mTLS ไม่ใช้ private key
    ดูเฉลย

    คำตอบ: 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

  3. ส่ง HTTPS แบบ mTLS ไปที่พอร์ต 443 แทน 9444 ปัญหาที่ README เตือนไว้คืออะไร (เป้าหมายข้อ 1)

    1. ไม่มีปัญหา แค่ช้ากว่า
    2. ต้องใส่ X-API-KEY เพิ่ม
    3. ปลายทางไม่ใช่ endpoint ของ mTLS: README ระบุว่า ingest แบบ mTLS ต้องใช้ :9444 ไม่ใช่ 443 และเตือนเรื่อง SAN/โดเมนไม่ตรง พอร์ต 443 เป็นของ Server-TLS ที่ใช้ API key
    4. ใบรับรองของอุปกรณ์หมดอายุทันที
    ดูเฉลย

    คำตอบ: 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

  4. ถ้า private key ของอุปกรณ์หนึ่งรั่ว ผลกระทบคืออะไร (เป้าหมายข้อ 2)

    1. ผู้โจมตีปลอมเป็นอุปกรณ์ตัวนั้นได้ (เชื่อมต่อและ publish ในนามของมัน) จนกว่าใบรับรองจะถูกเพิกถอนและออกคู่กุญแจใหม่ ผลจำกัดอยู่ที่อุปกรณ์ตัวนั้น เพราะแต่ละตัวมีใบรับรองและ topic ของตัวเอง
    2. อุปกรณ์ทุกตัวบนแพลตฟอร์มถูกปลอมได้
    3. ไม่มีผล เพราะยังต้องมี CA certificate
    4. ผู้โจมตีถอดรหัสข้อมูลของอุปกรณ์อื่นได้
    ดูเฉลย

    คำตอบ: A. ผู้โจมตีปลอมเป็นอุปกรณ์ตัวนั้นได้ (เชื่อมต่อและ publish ในนามของมัน) จนกว่าใบรับรองจะถูกเพิกถอนและออกคู่กุญแจใหม่ ผลจำกัดอยู่ที่อุปกรณ์ตัวนั้น เพราะแต่ละตัวมีใบรับรองและ topic ของตัวเอง

    ใบรับรองผูกกับ public key ของอุปกรณ์ ใครถือ private key คู่กันก็ผ่าน handshake ได้ ACL จำกัด topic เป็น device/<device_id>/… จึงจำกัดความเสียหาย การไม่ให้กุญแจออกจากชิปเลย (OPTIGA Trust M ในบทเรียน 5.3) คือทางลดความเสี่ยงนี้

  5. เปรียบเทียบ Server-TLS กับ mTLS ข้อใดถูก (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 2)

    1. Server-TLS: อุปกรณ์ต้องเก็บรหัสผ่านหรือ API key เป็นความลับ และส่งมันในอุโมงค์ TLS ทุกครั้งที่เชื่อมต่อ
    2. mTLS: ไม่มีความลับถูกส่งผ่านเครือข่ายระหว่างพิสูจน์ตัวตน อุปกรณ์แค่แสดงว่าถือกุญแจ
    3. mTLS ทำให้อุปกรณ์ไม่ต้องตรวจใบรับรองของเซิร์ฟเวอร์
    4. mTLS มีภาระดูแลกุญแจ: ใบรับรองหมดอายุ ต้องต่ออายุหรือหมุนเวียน และต้องเก็บ private key ให้ปลอดภัย
    5. Server-TLS เข้ารหัสข้อมูลได้น้อยกว่า mTLS
    ดูเฉลย

    คำตอบ: A. Server-TLS: อุปกรณ์ต้องเก็บรหัสผ่านหรือ API key เป็นความลับ และส่งมันในอุโมงค์ TLS ทุกครั้งที่เชื่อมต่อ · B. mTLS: ไม่มีความลับถูกส่งผ่านเครือข่ายระหว่างพิสูจน์ตัวตน อุปกรณ์แค่แสดงว่าถือกุญแจ · D. mTLS มีภาระดูแลกุญแจ: ใบรับรองหมดอายุ ต้องต่ออายุหรือหมุนเวียน และต้องเก็บ private key ให้ปลอดภัย

    ทั้งสองโหมดเข้ารหัสช่องทางเท่ากัน และอุปกรณ์ตรวจเซิร์ฟเวอร์เหมือนกัน (verify_peer = 1) ความต่างอยู่ที่วิธีพิสูจน์ตัวตนของอุปกรณ์ Server-TLS เริ่มง่ายแต่ความลับเดินทางทุกครั้ง mTLS แข็งแรงกว่าแต่ต้องมีระบบออกและต่ออายุใบรับรอง

อ้างอิงบทเรียนนี้

ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน

"ยืนยันตัวตนทั้งสองฝั่งด้วย mTLS" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

ข้อความอ้างอิงภาษาอังกฤษ: "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

ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/tesaiot-firmware-stack/m05-connect-to-platform/l02-mtls/

บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
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 ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA