HTTPS trên ESP32: nhúng chứng chỉ gốc đúng cách và vì sao setInsecure() nguy hiểm

HTTPS bảo vệ được gì, chuỗi tin cậy của chứng chỉ, nhúng chứng chỉ gốc ISRG Root X1 vào ESP32, vì sao setInsecure() mở đường cho tấn công xen giữa, chương trình thử ba trường hợp.

ESP32 gửi dữ liệu lên Google Sheets, Telegram, Firebase hay máy chủ riêng đều phải dùng HTTPS. Nhiều hướng dẫn trên mạng "cho chạy được" bằng một dòng client.setInsecure(), nhưng dòng đó tắt luôn phần quan trọng nhất của HTTPS: kiểm tra xem máy chủ có đúng là máy chủ thật không. Bài này giải thích HTTPS bảo vệ được gì, chuỗi chứng chỉ hoạt động thế nào, cách nhúng chứng chỉ gốc vào ESP32, vì sao setInsecure() nguy hiểm, và một chương trình thử ba trường hợp để thấy tận mắt.

1. HTTPS bảo vệ được gì

HTTPS là HTTP chạy bên trong TLS. TLS làm hai việc:

  • Mã hóa: người đứng giữa đường truyền (cùng mạng Wi-Fi, nhà mạng, router) không đọc được nội dung, kể cả mật khẩu, token API.
  • Xác thực máy chủ: ESP32 kiểm tra chứng chỉ của máy chủ để biết chắc mình đang nói chuyện với đúng máy chủ đó, không phải một máy giả mạo.

Mã hóa mà không xác thực thì gần như vô nghĩa: kẻ giả mạo chỉ cần đứng giữa, nhận kết nối mã hóa của ESP32 bằng chứng chỉ tự tạo, đọc hết rồi chuyển tiếp tới máy chủ thật. setInsecure() chính là bỏ phần xác thực.

2. Chuỗi tin cậy của chứng chỉ

Ba chứng chỉ: chứng chỉ máy chủ valid.x1.test-certs.letsencrypt.org hạn 90 ngày, chứng chỉ trung gian Let's Encrypt do ISRG Root X1 ký, chứng chỉ gốc ISRG Root X1 tự ký hết hạn 2035 nhúng sẵn trong ESP32; mũi tên ký từ gốc sang trung gian sang máy chủ; ba bước kiểm tra
ESP32 chỉ cần giữ chứng chỉ gốc; chứng chỉ máy chủ và trung gian do máy chủ gửi khi kết nối (hình minh họa).

Chứng chỉ của máy chủ được một tổ chức cấp chứng chỉ (CA) ký, qua một chứng chỉ trung gian. Chứng chỉ trên cùng là chứng chỉ gốc: nó tự ký và được hệ điều hành, trình duyệt tin sẵn. Trên ESP32 không có kho chứng chỉ như máy tính, nên chương trình phải tự đưa chứng chỉ gốc vào bằng setCACert().

Nên nhúng chứng chỉ gốc, không nhúng chứng chỉ máy chủ: chứng chỉ máy chủ của Let's Encrypt chỉ có hạn khoảng 90 ngày và đổi liên tục, còn ISRG Root X1 có hạn tới tháng 6/2035. Cách so "vân tay" (fingerprint) của chứng chỉ máy chủ bằng verify() cũng gặp đúng vấn đề này: mỗi lần máy chủ đổi chứng chỉ là ESP32 ngừng kết nối được.

Lấy chứng chỉ gốc của một trang web: mở trang bằng trình duyệt máy tính, bấm biểu tượng ổ khóa, xem chứng chỉ, chọn chứng chỉ trên cùng của chuỗi và xuất ra dạng Base64 (PEM), rồi chép vào chương trình như trong bài. Chứng chỉ gốc là dữ liệu công khai, không phải bí mật.

3. Vì sao setInsecure() nguy hiểm

Hai tình huống: với setCACert, ESP32 kết nối tới điểm phát Wi-Fi giả dùng chứng chỉ tự tạo bị chặn lại; với setInsecure, ESP32 kết nối thành công tới máy giả và gửi mật khẩu, token cho kẻ giả mạo
Kẻ giả mạo đứng giữa ESP32 và máy chủ thật (hình minh họa).

Với setInsecure(), ESP32 chấp nhận bất kỳ chứng chỉ nào, kể cả chứng chỉ kẻ giả mạo vừa tự tạo. Kẻ đó không cần phá mã gì cả: chỉ cần ESP32 kết nối qua một điểm phát Wi-Fi giả cùng tên, một router đã bị chiếm, hoặc một máy chủ DNS trả địa chỉ sai. Token Telegram, khóa API, mật khẩu trong yêu cầu gửi đi đều lộ, và kẻ đó còn trả về dữ liệu giả cho ESP32.

setInsecure() chỉ nên dùng lúc thử nghiệm trên bàn, với dữ liệu không có gì quan trọng, và phải bỏ trước khi mang thiết bị đi dùng thật.

4. Chương trình thử ba trường hợp

Chương trình nhúng chứng chỉ gốc ISRG Root X1 (của Let's Encrypt) rồi thử:

  1. Kết nối valid.x1.test-certs.letsencrypt.org, trang thử nghiệm chính thức của Let's Encrypt có chứng chỉ dẫn tới ISRG Root X1: phải thành công.
  2. Kết nối example.com (chứng chỉ của một CA khác) với cùng chứng chỉ gốc đó: phải bị từ chối. Trường hợp này giống hệt khi gặp kẻ giả mạo.
  3. Kết nối example.com với setInsecure(): thành công, nhưng ESP32 không hề biết đầu bên kia là ai.
// PYWORLD.VN - HTTPS trên ESP32: kiểm tra máy chủ bằng chứng chỉ gốc, thấy setInsecure() bỏ qua kiểm tra nguy hiểm thế nào
// Ba lần thử: máy chủ dùng chứng chỉ Let's Encrypt (khớp chứng chỉ gốc ISRG Root X1), máy chủ dùng chứng chỉ của hãng khác, và setInsecure()
// Thay TEN_WIFI, MAT_KHAU_WIFI. Chỉ dùng thư viện có sẵn trong gói esp32 (WiFi, WiFiClientSecure)

#include <WiFi.h>
#include <WiFiClientSecure.h>

const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";

// Chứng chỉ gốc ISRG Root X1 (của Let's Encrypt), hết hạn ngày 04/06/2035. Đây là dữ liệu công khai, không phải bí mật.
const char *ISRG_ROOT_X1 =
  "-----BEGIN CERTIFICATE-----\n"
  "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\n"
  "-----END CERTIFICATE-----\n";

// Mở kết nối HTTPS, gửi GET và in dòng trạng thái. caGoc = nullptr nghĩa là dùng setInsecure().
void thu(const char *tieuDe, const char *mayChu, const char *caGoc) {
  WiFiClientSecure ketNoi;
  if (caGoc) ketNoi.setCACert(caGoc);
  else ketNoi.setInsecure();             // KHÔNG kiểm tra máy chủ là ai: chỉ để thử nghiệm
  ketNoi.setHandshakeTimeout(15);
  Serial.printf("\n[%s] %s\n", tieuDe, mayChu);
  uint32_t batDau = millis();
  if (!ketNoi.connect(mayChu, 443)) {
    char loi[128];
    ketNoi.lastError(loi, sizeof(loi));
    Serial.printf("  Khong ket noi duoc: %s\n", loi);
    return;
  }
  Serial.printf("  Bat tay TLS xong sau %lu ms\n", (unsigned long)(millis() - batDau));
  ketNoi.printf("GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", mayChu);
  String dong = ketNoi.readStringUntil('\n');                  // dòng đầu: HTTP/1.1 200 OK
  Serial.printf("  May chu tra loi: %s\n", dong.c_str());
  ketNoi.stop();
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
  Serial.print("Dang ket noi Wi-Fi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nDia chi IP: %s, heap trong: %lu byte\n", WiFi.localIP().toString().c_str(), (unsigned long)ESP.getFreeHeap());

  thu("1. Dung chung chi goc", "valid.x1.test-certs.letsencrypt.org", ISRG_ROOT_X1);
  thu("2. Chung chi goc khong khop", "example.com", ISRG_ROOT_X1);
  thu("3. setInsecure", "example.com", nullptr);
}

void loop() {}

Mỗi dòng của chứng chỉ PEM được viết thành một chuỗi C kết thúc bằng \n; trình dịch tự nối các chuỗi đứng liền nhau thành một. Sai một ký tự là chứng chỉ hỏng, nên hãy chép nguyên khối, không gõ lại.

5. Kết quả mong đợi

Serial Monitor: lần 1 bắt tay TLS xong sau 1420 ms, máy chủ trả lời HTTP/1.1 200 OK; lần 2 không kết nối được X509 Certificate verification failed; lần 3 setInsecure bắt tay xong, HTTP/1.1 200 OK
Lần 2 bị từ chối đúng như mong muốn; lần 3 thành công nhưng không biết máy chủ có thật không (số liệu ví dụ).

Bắt tay TLS mất khoảng 1–2 giây và cần khoảng 40–50 KB RAM cho mỗi kết nối, vì vậy nên giữ kết nối dùng lại nhiều lần thay vì mở mới cho mỗi yêu cầu, và kiểm tra ESP.getFreeHeap() khi chương trình lớn. Câu lỗi "X509 - Certificate verification failed" là dấu hiệu chứng chỉ gốc không khớp: hoặc nhúng nhầm chứng chỉ, hoặc máy chủ đã đổi sang CA khác, hoặc đúng là có kẻ giả mạo.

6. Những lưu ý khi dùng lâu dài

  • Máy chủ có thể đổi CA. Thiết bị dùng nhiều năm nên có cách cập nhật chứng chỉ (cập nhật chương trình qua mạng, hoặc lưu chứng chỉ trong LittleFS để thay được), hoặc nhúng sẵn hai chứng chỉ gốc.
  • Bộ chứng chỉ gốc có sẵn: gói esp32 3.x được dựng kèm bộ chứng chỉ gốc phổ biến (bundle) như trình duyệt; dùng qua setCACertBundle() thì kết nối được hầu hết các trang mà không cần chọn chứng chỉ, đổi lại tốn thêm bộ nhớ flash.
  • Ngày hết hạn: với cấu hình mbedTLS của gói esp32 3.3.10 (không bật MBEDTLS_HAVE_TIME_DATE), ESP32 kiểm tra chữ ký và tên miền nhưng không kiểm tra ngày hết hạn của chứng chỉ. Đây thêm một lý do để chọn chứng chỉ gốc còn hạn lâu và theo dõi khi CA thông báo thay đổi.
  • Không để token trong mã nguồn chia sẻ: chứng chỉ gốc công khai được, còn mật khẩu Wi-Fi, token API thì không.

7. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
"X509 - Certificate verification failed"Chứng chỉ gốc không khớp chuỗi của máy chủXem chuỗi chứng chỉ trên trình duyệt, nhúng đúng chứng chỉ gốc
"X509 - The CRT/CRL/CSR format is invalid"PEM chép thiếu dòng, thiếu \n, sai ký tựChép lại nguyên khối từ dòng BEGIN tới END
Kết nối được vài tuần rồi hỏngNhúng chứng chỉ máy chủ thay vì chứng chỉ gốc; máy chủ đổi CANhúng chứng chỉ gốc; có cách cập nhật từ xa
Kết nối chậm, đôi khi treoMạng yếu, bắt tay TLS lâusetHandshakeTimeout(); giữ kết nối dùng lại
Khởi động lại khi mở kết nối HTTPSThiếu RAM (nhiều kết nối, chuỗi String lớn)Mỗi lúc một kết nối; giải phóng bộ nhớ; theo dõi heap
"Khong ket noi duoc" với mọi trangChưa có Wi-Fi, sai DNS, tường lửa chặn cổng 443Kiểm tra Wi-Fi, thử trang khác, thử mạng khác

8. Xem thêm

  • Dùng MQTT với ESP32: MQTT cũng có bản chạy trên TLS (cổng 8883) với cùng cách nhúng chứng chỉ gốc.
  • ESP32 làm web server: chiều ngược lại, ESP32 là máy chủ trong mạng nội bộ.
  • Các bài ESP32 với dịch vụ đám mây (Telegram, Google Sheets, Firebase) và bài LittleFS trong loạt ESP32 nâng cao: nơi dùng tới HTTPS và chỗ lưu chứng chỉ.

Tin tức liên quan

Mạch tăng áp XL6009: từ 3,7 V hoặc 5 V lên 12 V, dòng ra được bao nhiêu, chỉnh áp và lỗi hay gặp
Mạch tăng áp XL6009: từ 3,7 V hoặc 5 V lên 12 V, dòng ra được bao nhiêu, chỉnh áp và lỗi hay gặp

Mạch tăng áp XL6009 hoạt động thế nào, thông số IC và module, cách ước tính dòng ra thực tế khi nâng pin 18650, nguồn USB 5 V, ắc quy 12 V lên 12 V, 24 V, chỉnh áp và lỗi hay gặp.

C cho vi điều khiển: kiểu số cố định, phép toán bit, con trỏ thanh ghi, volatile, struct packed
C cho vi điều khiển: kiểu số cố định, phép toán bit, con trỏ thanh ghi, volatile, struct packed

Những phần C dùng nhiều nhất khi lập trình vi điều khiển: uint8_t và tràn số, phép toán bit với mặt nạ, con trỏ tới thanh ghi, volatile với ngắt, struct packed, thứ tự byte, const, static; ví dụ ESP32.

Mạch hạ áp, tăng áp DC-DC: buck, boost là gì và chọn LM2596, MP1584, XL6009 theo điện áp, dòng tải
Mạch hạ áp, tăng áp DC-DC: buck, boost là gì và chọn LM2596, MP1584, XL6009 theo điện áp, dòng tải

Ổn áp tuyến tính, mạch hạ áp buck, tăng áp boost khác nhau thế nào, chọn mạch theo điện áp và dòng tải, so sánh LM2596, MP1584, XL6009 và bảng chọn cho 12 V xuống 5 V, pin 18650 lên 12 V.

Cảm biến ánh sáng: quang trở, photodiode, BH1750 – cấu tạo, nguyên lý, chế tạo và đo lux bằng ESP32
Cảm biến ánh sáng: quang trở, photodiode, BH1750 – cấu tạo, nguyên lý, chế tạo và đo lux bằng ESP32

Cảm biến ánh sáng hoạt động thế nào: độ rọi và lux, quang trở CdS, photodiode, phototransistor, cảm biến số BH1750, cách chế tạo và đo lux bằng BH1750, quang trở với ESP32.

Quy tắc quyết định và tỉ số TUR: khi nào được kết luận đạt, dải bảo vệ và rủi ro chấp nhận sai
Quy tắc quyết định và tỉ số TUR: khi nào được kết luận đạt, dải bảo vệ và rủi ro chấp nhận sai

Quy tắc quyết định theo ISO/IEC 17025 và ILAC-G8: bốn trường hợp kết quả sát giới hạn, chấp nhận đơn giản và dải bảo vệ, rủi ro chấp nhận sai, tỉ số TUR 4:1 và cách chọn quy tắc.

Học lập trình từ nhỏ giúp trẻ phát triển tư duy thế nào? Đọc đúng bằng chứng từ các nghiên cứu
Học lập trình từ nhỏ giúp trẻ phát triển tư duy thế nào? Đọc đúng bằng chứng từ các nghiên cứu

Tóm tắt bằng chứng về việc học lập trình từ nhỏ giúp trẻ phát triển tư duy: tổng hợp 105 nghiên cứu, thí nghiệm với trẻ mẫu giáo và lớp 1, cách đọc con số và giới hạn.

KiCad cho người mới: từ sơ đồ nguyên lý tới mạch in 2 lớp, đổ đồng GND và xuất Gerber
KiCad cho người mới: từ sơ đồ nguyên lý tới mạch in 2 lớp, đổ đồng GND và xuất Gerber

Vẽ mạch in 2 lớp bằng KiCad: symbol và footprint, ERC, các lớp F.Cu, B.Cu, mask, silkscreen, đi dây, đổ đồng GND có cầu nhiệt, phím tắt hay dùng, xuất Gerber và tệp khoan.

Sơ đồ chân ESP32 DevKit V1 30 chân và 38 chân: chân nào dùng được, chân nào nên tránh
Sơ đồ chân ESP32 DevKit V1 30 chân và 38 chân: chân nào dùng được, chân nào nên tránh

Sơ đồ chân ESP32 DevKit V1 30 chân và 38 chân: số GPIO, ADC1, ADC2, cảm ứng, I2C, SPI, UART, chân khởi động, chân chỉ vào, chân nối flash và cách chọn chân đúng cho từng việc.


Bình luận
Đã thêm vào giỏ hàng