diff --git a/docs-site/docs/guides/tls-cert-verification.md b/docs-site/docs/guides/tls-cert-verification.md new file mode 100644 index 0000000..8b89279 --- /dev/null +++ b/docs-site/docs/guides/tls-cert-verification.md @@ -0,0 +1,223 @@ +--- +title: TLS 证书验证 +sidebar_label: TLS 证书验证 +sidebar_position: 7 +--- + +# TLS 证书验证 + +agentic-kit 的所有云端通信(IoT-DNS、ATOP HTTPS、MQTT)默认走 TLS——IoT-DNS 与 ATOP HTTPS 使用端口 443,MQTT over TLS(mqtts,默认 `mqtts://a6.tuyacn.com:8883`)使用端口 8883。如果不在配置中提供任何证书校验信息,连接仍会加密,但**不会校验服务器证书**——存在中间人攻击(MITM)风险。 + +SDK 提供两种方式启用服务器证书验证: + +| 方式 | 适用场景 | 原理 | +|------|---------|------| +| `.cacert`(PEM 字符串) | POSIX(Linux/macOS)、有足够 RAM 存放完整 PEM 的设备 | 传入 CA 根证书的 PEM 文本,SDK 解析后用于 mbedTLS 证书链校验 | +| `.cert_bundle_attach`(平台证书包回调) | ESP-IDF 等 RTOS 平台,RAM 紧张或证书以编译后的二进制形式管理 | 传入平台 SDK 的证书包挂载函数(如 `esp_crt_bundle_attach`),直接操作 mbedTLS 的 `ssl_config` | + +两者可以同时不设(向后兼容,退化为不校验),但不建议在生产环境中这样做。 + +## 验证优先级 + +当两个字段同时提供时,`.cacert` 优先。底层 `tls_connect()` 的逻辑: + +``` +1. cacert 非空? → 用 cacert 解析 PEM,authmode = VERIFY_REQUIRED +2. cert_bundle_attach 非空? → 调用回调挂载平台证书包,authmode = VERIFY_REQUIRED +3. 两者都空? → authmode = cfg->verify +``` + +第 3 步的 `cfg->verify` 由各上层模块设定,不通过 iot-client 的公开配置暴露:**iot-client 固定传 `TLS_VERIFY_NONE`**(即两者都空时不校验、仅加密,并打印一条警告);而 RTC/TAI 通道传的是 `TLS_VERIFY_OPTIONAL`。所以对 iot-client 用户而言,“两者都空 = 不校验”成立;这也正是生产环境必须至少设置 `.cacert` 或 `.cert_bundle_attach` 之一的原因。 + +## 涉及的连接 + +配置生效后,以下**所有**连接都会使用证书验证: + +- IoT-DNS 查询(`h1.iot-dns.com:443`) +- ATOP HTTPS 请求(设备元数据、session token、OTA 查询等) +- MQTT 连接(`mqtt_disable_tls = false` 时) + +无需为每个连接单独设置——在 `iot_client_config_t` 或 `iot_on_boarding_config_t` 上设置一次即可。 + +--- + +## 方式一:`.cacert`(PEM 字符串) + +适用于 POSIX 平台或内存充裕、可以直接在代码中嵌入 PEM 文本的场景。 + +### 用法 + +```c +/* 根 CA 证书 PEM —— 指向静态字符串或堆分配的 PEM 文本均可, + 但必须在 client 整个生命周期内有效(SDK 不拷贝)。 */ +static const char *root_ca = + "-----BEGIN CERTIFICATE-----\n" + "MIIDx...(省略)...\n" + "-----END CERTIFICATE-----\n"; + +iot_client_config_t cfg = { + .devid = "...", + .secret_key = "...", + .local_key = "...", + .region = AY, + .env = PROD, + .cacert = root_ca, /* ← 设置 CA 证书 */ +}; +iot_client_t *client = iot_client_init(&cfg); +``` + +### 运行时获取 CA 证书 + +如果不想在固件中硬编码 PEM,可以通过 IoT-DNS 在运行时查询**某个目标端点**的 CA 证书。签名要求传入目标主机名与端口(`host` 不能为 NULL,否则返回 `OPRT_INVALID_PARAMETER`): + +```c +int iot_get_ca_certificate(iot_client_t *client, const char *host, + uint16_t port, char **ca_certificate); +``` + +```c +/* 查询目标端点(例如 ATOP/HTTPS 主机用 443,MQTT 用 8883)的 CA 证书。 */ +char *ca_cert = NULL; +int rc = iot_get_ca_certificate(client, "a1.tuyacn.com", 443, &ca_cert); +if (rc == OPRT_OK && ca_cert != NULL) { + /* ... 使用 / 持久化 ca_cert ... */ + + /* ca_cert 由 PAL 分配(pal_strdup),必须用同一个 PAL 的 free 释放, + 不能用 libc free();且需在 iot_client_deinit() 之前(client->pal 仍有效时)释放。 */ + client->pal->free(ca_cert); +} +``` + +> **引导阶段的“先有鸡还是先有蛋”问题:** `iot_get_ca_certificate()` 本身要先建立一次到 IoT-DNS 的 TLS 连接;若此时 `client->cacert` 仍为空,这次查询以不校验方式进行——存在引导阶段 MITM 风险。同理,`iot_client_init()` 在返回前就已完成 DNS/ATOP/MQTT 的**首批连接**,因此**在 init 之后再设 `client->cacert` 只对后续请求生效,无法追溯保护 init 期间的连接**。 +> +> 所以运行时获取 CA 更适合用来**取回一份 CA 加以持久化**,下次启动前通过 `cfg.cacert` 在 `iot_client_init()` **之前**传入,从第一条连接起即校验;对安全要求严格的场景,建议直接硬编码根 CA 或改用 `.cert_bundle_attach`。 + +### 内存注意事项 + +- 完整的 Mozilla CA 证书包约 200KB,在 RAM 紧凑的 MCU 上可能过大。 +- 单个 Tuya 云端 CA 证书约 1-2KB,可接受。 +- `.cacert` 指向的字符串不拷贝,调用方必须保证它在 `iot_client_deinit()` 之前一直有效。 + +--- + +## 方式二:`.cert_bundle_attach`(平台证书包回调) + +适用于 ESP-IDF 等 RTOS 平台。平台以编译后的二进制形式管理证书包(存储在 flash 分区或固件镜像中),不需要在 RAM 中持有 PEM 文本。 + +### 原理 + +ESP-IDF 提供 `esp_crt_bundle_attach()`,它将一个预编译的 CA 证书包(包含主流公共根 CA)直接挂载到 mbedTLS 的 `ssl_config` 上。SDK 在 TLS 握手前调用这个回调,握手时 mbedTLS 会用证书包中的 CA 校验服务器证书。 + +### ESP-IDF 用法 + +```c +#include "esp_crt_bundle.h" +#include "iot_client.h" + +iot_client_config_t cfg = { + .devid = "...", + .secret_key = "...", + .local_key = "...", + .region = AY, + .env = PROD, + .cert_bundle_attach = (tls_cert_bundle_attach_fn)esp_crt_bundle_attach, +}; +iot_client_t *client = iot_client_init(&cfg); +``` + +配网(on-boarding)场景同理: + +```c +iot_on_boarding_config_t ob_cfg = { + .uuid = "...", + .authkey = "...", + .product_key = "...", + .env = PROD, + .mqtt_disable_tls = false, + .cert_bundle_attach = (tls_cert_bundle_attach_fn)esp_crt_bundle_attach, +}; +iot_client_t *client = iot_client_init_on_boarding(&ob_cfg); +``` + +### ESP-IDF sdkconfig 配置 + +确保在 `sdkconfig` 中启用了证书包: + +```ini +# 启用 mbedTLS 证书包(默认包含主流公共根 CA) +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y +``` + +### 类型转换说明 + +`esp_crt_bundle_attach` 的签名是 `esp_err_t (*)(void *conf)`,而 SDK 定义的 `tls_cert_bundle_attach_fn` 是 `void (*)(void *ssl_config)`。两者签名不完全一致,需要通过 `(tls_cert_bundle_attach_fn)` 强制转换。这是安全的——回调的语义是"向 `mbedTLS_ssl_config` 挂载证书",ESP-IDF 的实现与 mbedTLS 的 `mbedtls_ssl_conf_verify` / CA 链操作兼容。 + +### RTC/TAI 连接(AI 对话通道) + +RTC TCP Client 同样支持 `cert_bundle_attach`,设置方式与 iot-client 一致: + +```c +tai_config_t tai_cfg = { + .host = cp.host, + .port = cp.port, + .cert_bundle_attach = (tls_cert_bundle_attach_fn)esp_crt_bundle_attach, + /* ... 其他字段 ... */ +}; +``` + +这样 iot-client 和 RTC/TAI 两个模块使用同一个证书包,所有云端连接都经过验证。 + +### 其他 RTOS 平台 + +如果目标平台不是 ESP-IDF,但提供了类似的证书包机制,可以实现一个签名匹配 `tls_cert_bundle_attach_fn` 的回调函数,在函数内将平台的证书包挂载到传入的 `mbedTLS_ssl_config *` 上。 + +--- + +## 哪些 API 受影响 + +以下公开 API 的配置结构体均支持 `.cacert` 和 `.cert_bundle_attach`: + +| API | 配置结构体 | 说明 | +|-----|-----------|------| +| `iot_client_init` | `iot_client_config_t` | 已激活设备初始化(DNS + ATOP + MQTT) | +| `iot_client_init_on_boarding` | `iot_on_boarding_config_t` | App 扫码激活 | +| `iot_client_init_on_boarding_with_token` | `iot_on_boarding_config_t` | Token 激活 | +| `iot_get_qrcode_info` | `iot_qrcode_request_t` | 获取配网二维码 URL(独立 API,不需要 client) | + +配置后,这些 API 内部发起的所有 TLS 连接都会使用证书验证。 + +--- + +## 如何确认验证是否生效 + +### 查看日志 + +SDK 在 TLS 握手时会输出日志。启用证书验证时: + +- **`cacert` 生效**:无警告日志,TLS 握手成功后输出 `[tls] connected ...` +- **`cert_bundle_attach` 生效**:同上,无 "verification disabled" 警告 +- **两者都未设置**:会输出警告(`LOG_WARN` 级别): + ``` + [tls] peer verification disabled (no CA certificate) + ``` + +如果看到上述警告,说明该连接未启用证书验证。 + +### 验证失败的错误 + +当证书验证启用但服务器证书不受信任时,TLS 握手会失败。MQTT / ATOP HTTPS 路径返回 `OPRT_TLS_HANDSHAKE_FAILED`(-7);而 **IoT-DNS 查询路径会把 TLS 失败归一为 `OPRT_COMMUNICATION_ERROR`(-1)**,因此排查 DNS/CA 获取阶段的失败时不要只匹配 -7。常见原因: + +- 服务器证书链无法追溯到所提供的 CA +- 证书包中缺少涂鸦云的根 CA +- 系统时间不正确(证书有 Not Before / Not After 时间窗口) + +--- + +## 最佳实践 + +1. **生产环境必须启用证书验证。** 无论是 `.cacert` 还是 `.cert_bundle_attach`,至少设置一个。 +2. **ESP-IDF 平台优先使用 `.cert_bundle_attach`。** 不占用 RAM 存放 PEM,证书包存储在 flash,支持主流公共 CA。 +3. **POSIX 平台使用 `.cacert`。** 从 IoT-DNS 动态获取或硬编码根 CA PEM。 +4. **不要在生产环境留空两个字段。** 连接虽加密但不校验,存在 MITM 风险。 +5. **确保系统时间正确。** 证书有有效期,时间偏差过大会导致验证失败。MCU 平台应在联网后通过 NTP 同步时间。 diff --git a/docs-site/docs/reference/iot-client.md b/docs-site/docs/reference/iot-client.md index aa7a37c..6875478 100644 --- a/docs-site/docs/reference/iot-client.md +++ b/docs-site/docs/reference/iot-client.md @@ -91,6 +91,7 @@ sidebar_position: 3 | `mqtt_disable_tls` | `bool` | `false`(默认)使用 MQTTS,`true` 使用明文 MQTT | | `mqtt_auto_connect` | `bool` | `true` 初始化后自动连接 MQTT;`false`(默认)需手动调用 | | `cacert` | `const char *` | CA 证书 PEM(用于 MQTT/HTTPS/IoT-DNS TLS,调用方持有,需在 client 生命周期内有效) | +| `cert_bundle_attach` | `tls_cert_bundle_attach_fn` | 平台证书包回调(如 ESP-IDF 的 `esp_crt_bundle_attach`),NULL 表示不使用。详见 [TLS 证书验证](../guides/tls-cert-verification.md) | | `message_callback` | `iot_message_callback_t` | MQTT 消息回调,可为 NULL | ### `iot_on_boarding_config_t` @@ -111,6 +112,7 @@ sidebar_position: 3 | `mqtt_disable_tls` | `bool` | TLS 开关 | | `mqtt_auto_connect` | `bool` | `true` 激活后自动连接 MQTT;`false`(默认)需手动调用 | | `cacert` | `const char *` | CA 证书 PEM(用于 MQTT/HTTPS/IoT-DNS TLS,调用方持有) | +| `cert_bundle_attach` | `tls_cert_bundle_attach_fn` | 平台证书包回调(如 ESP-IDF 的 `esp_crt_bundle_attach`),NULL 表示不使用。详见 [TLS 证书验证](../guides/tls-cert-verification.md) | | `message_callback` | `iot_message_callback_t` | MQTT 消息回调 | ### `iot_client_t`(返回实例) @@ -250,6 +252,7 @@ int iot_get_qrcode_info(const iot_qrcode_request_t *request, | `region` | `iot_region_t` | 数据中心区域 | | `env` | `iot_env_t` | 环境 | | `cacert` | `const char *` | CA 证书 PEM(用于 HTTPS/IoT-DNS TLS,调用方持有) | +| `cert_bundle_attach` | `tls_cert_bundle_attach_fn` | 平台证书包回调(如 ESP-IDF 的 `esp_crt_bundle_attach`),NULL 表示不使用。详见 [TLS 证书验证](../guides/tls-cert-verification.md) | **响应 `iot_qrcode_response_t`:** diff --git a/docs-site/sidebars.ts b/docs-site/sidebars.ts index ee870d5..cbacd49 100644 --- a/docs-site/sidebars.ts +++ b/docs-site/sidebars.ts @@ -52,6 +52,7 @@ const sidebars: SidebarsConfig = { 'guides/dp-persistence', 'guides/device-mcp', 'guides/ota-upgrade', + 'guides/tls-cert-verification', 'guides/porting-to-new-platform', ], }, diff --git a/examples/esp-idf/ai/rtc-tcp-client/main/main.c b/examples/esp-idf/ai/rtc-tcp-client/main/main.c index 902b767..1755a9a 100644 --- a/examples/esp-idf/ai/rtc-tcp-client/main/main.c +++ b/examples/esp-idf/ai/rtc-tcp-client/main/main.c @@ -318,6 +318,7 @@ void app_main(void) .region = AY, .env = PROD, .mqtt_disable_tls = false, + .cert_bundle_attach = (tls_cert_bundle_attach_fn)esp_crt_bundle_attach, .message_callback = NULL, .schema = NULL, /* restart path: fill from persisted storage if used */ .schema_id = NULL, diff --git a/examples/esp-idf/ota-demo/main/main.c b/examples/esp-idf/ota-demo/main/main.c index 3109c35..8992a69 100644 --- a/examples/esp-idf/ota-demo/main/main.c +++ b/examples/esp-idf/ota-demo/main/main.c @@ -298,6 +298,7 @@ void app_main(void) .env = DEFAULT_ENV, .mqtt_disable_tls = false, .mqtt_auto_connect = false, + .cert_bundle_attach = (tls_cert_bundle_attach_fn)esp_crt_bundle_attach, .message_callback = NULL, .schema = NULL, .schema_id = NULL, diff --git a/examples/esp-idf/pair/pair-by-ble/main/main.c b/examples/esp-idf/pair/pair-by-ble/main/main.c index db1f057..4028b81 100644 --- a/examples/esp-idf/pair/pair-by-ble/main/main.c +++ b/examples/esp-idf/pair/pair-by-ble/main/main.c @@ -21,6 +21,7 @@ #include "esp_event.h" #include "esp_netif.h" #include "nvs_flash.h" +#include "esp_crt_bundle.h" #include "app_config.h" #include "tuya_ble_nimble.h" #include "iot_client.h" @@ -166,6 +167,7 @@ static esp_err_t activate_device_with_ble_token(void) .timeout_ms = 120000, .env = PROD, .mqtt_disable_tls = false, + .cert_bundle_attach = (tls_cert_bundle_attach_fn)esp_crt_bundle_attach, }; ESP_LOGI(TAG, "Starting activation with BLE token: %s", s_wifi_creds.token); diff --git a/modules/iot-client/include/iot_client.h b/modules/iot-client/include/iot_client.h index 7595a95..f5e4085 100644 --- a/modules/iot-client/include/iot_client.h +++ b/modules/iot-client/include/iot_client.h @@ -7,6 +7,7 @@ #include "pal.h" #include "log.h" +#include "tls.h" #if defined(__GNUC__) && (__GNUC__ >= 4) #define IOT_API __attribute__((visibility("default"))) @@ -93,6 +94,7 @@ typedef struct { bool mqtt_disable_tls; // false = mqtts (TLS, default), true = mqtt (TCP) bool mqtt_auto_connect; // true = connect MQTT after init/activation; false (default) = caller invokes iot_client_message_connect() manually const char *cacert; // CA cert for all TLS (MQTT/HTTPS/IoT-DNS) (PEM, caller-owned, must outlive client) + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback (NULL = none) iot_message_callback_t message_callback; // MQTT message callback /* ---- DP layer restore (all caller-owned, may be NULL) ---- */ @@ -117,6 +119,7 @@ typedef struct { bool mqtt_disable_tls; // false = mqtts (TLS, default), true = mqtt (TCP) bool mqtt_auto_connect; // true = connect MQTT after init/activation; false (default) = caller invokes iot_client_message_connect() manually const char *cacert; // CA cert for all TLS (MQTT/HTTPS/IoT-DNS) (PEM, caller-owned, must outlive client) + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback (NULL = none) iot_message_callback_t message_callback; // MQTT message callback } iot_on_boarding_config_t; @@ -146,6 +149,7 @@ struct iot_dp_context; const pal_t *pal; // PAL adapter const char *cacert; // CA certificate for all TLS (MQTT/HTTPS/IoT-DNS) (caller-owned, points to user buffer/flash) + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback (borrowed, NULL = none) struct mqtt_client *mqtt; // Internal MQTT client handle iot_message_callback_t message_callback; // User callback for incoming messages @@ -257,6 +261,7 @@ typedef struct { iot_region_t region; // Region (AY = China, default) iot_env_t env; // Environment (PROD or PRE) const char *cacert; // CA cert for HTTPS/IoT-DNS TLS (PEM, caller-owned) + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback (NULL = none) } iot_qrcode_request_t; /** diff --git a/modules/iot-client/src/atop.c b/modules/iot-client/src/atop.c index dc00eed..8e0fb27 100644 --- a/modules/iot-client/src/atop.c +++ b/modules/iot-client/src/atop.c @@ -227,7 +227,8 @@ int atop_activate_request(const pal_t *pal, const activite_request_t *request, a .user_data = request->user_data, .host = request->host, .port = request->port, - .cacert = request->cacert}; + .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach}; /* ATOP service request send */ atop_base_response_t *atop_response = pal->malloc(sizeof(atop_base_response_t)); @@ -346,6 +347,7 @@ int atop_device_meta_save(const pal_t *pal, const device_meta_save_request_t *re .host = request->host, .port = request->port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; atop_base_response_t atop_response = {0}; @@ -432,6 +434,7 @@ int atop_qrcode_info_get(const pal_t *pal, const qrcode_info_request_t *request, .host = request->host, .port = request->port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; atop_base_response_t atop_response = {0}; @@ -535,6 +538,7 @@ int atop_ai_token_get(const pal_t *pal, const ai_token_request_t *request, ai_to .host = request->host, .port = request->port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; atop_base_response_t atop_response = {0}; @@ -639,6 +643,7 @@ int atop_schema_newest_get(const pal_t *pal, const schema_newest_request_t *requ .host = request->host, .port = request->port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; atop_base_response_t atop_response = {0}; @@ -771,6 +776,7 @@ int atop_upgrade_get(const pal_t *pal, const ota_upgrade_request_t *request, ota .host = request->host, .port = request->port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; atop_base_response_t atop_response = {0}; @@ -908,6 +914,7 @@ int atop_version_update(const pal_t *pal, const ota_version_update_request_t *re .host = request->host, .port = request->port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; atop_base_response_t atop_response = {0}; @@ -974,6 +981,7 @@ int atop_upgrade_status_update(const pal_t *pal, const ota_status_update_request .host = request->host, .port = request->port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; atop_base_response_t atop_response = {0}; diff --git a/modules/iot-client/src/atop.h b/modules/iot-client/src/atop.h index c478902..6e4a157 100644 --- a/modules/iot-client/src/atop.h +++ b/modules/iot-client/src/atop.h @@ -6,6 +6,7 @@ #include #include "iot_client.h" #include "iot_ota.h" /* iot_ota_status_t (shared between atop and public OTA API) */ +#include "tls.h" /** * @brief Device activation request structure @@ -28,6 +29,7 @@ const char *host; // Server host (optional, defaults to TUYA_DEFAULT_HOST) uint16_t port; // Server port (optional, defaults to TUYA_DEFAULT_PORT) const char *cacert; // CA certificate PEM content + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback } activite_request_t; typedef struct { @@ -77,6 +79,7 @@ typedef struct { const char *host; // Server host (optional, defaults to TUYA_DEFAULT_HOST) uint16_t port; // Server port (optional, defaults to TUYA_DEFAULT_PORT) const char *cacert; // CA certificate PEM content + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback } ai_token_request_t; /** @@ -102,6 +105,7 @@ typedef struct { const char *host; // Server host uint16_t port; // Server port const char *cacert; // CA certificate PEM content + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback } qrcode_info_request_t; /** @@ -121,6 +125,7 @@ typedef struct { const char *host; // Server host (optional, defaults to TUYA_DEFAULT_HOST) uint16_t port; // Server port (optional, defaults to TUYA_DEFAULT_PORT) const char *cacert; // CA certificate PEM content + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback } device_meta_save_request_t; /** @@ -162,6 +167,7 @@ typedef struct { const char *host; // Server host (optional, defaults to TUYA_DEFAULT_HOST) uint16_t port; // Server port (optional, defaults to TUYA_DEFAULT_PORT) const char *cacert; // CA certificate PEM content + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback } schema_newest_request_t; /** @@ -203,6 +209,7 @@ typedef struct { const char *host; uint16_t port; const char *cacert; + tls_cert_bundle_attach_fn cert_bundle_attach; } ota_upgrade_request_t; /** @@ -234,6 +241,7 @@ typedef struct { const char *host; uint16_t port; const char *cacert; + tls_cert_bundle_attach_fn cert_bundle_attach; } ota_version_update_request_t; /** @@ -248,6 +256,7 @@ typedef struct { const char *host; uint16_t port; const char *cacert; + tls_cert_bundle_attach_fn cert_bundle_attach; } ota_status_update_request_t; /** diff --git a/modules/iot-client/src/atop_base.c b/modules/iot-client/src/atop_base.c index faae4c3..3d607ef 100644 --- a/modules/iot-client/src/atop_base.c +++ b/modules/iot-client/src/atop_base.c @@ -569,6 +569,7 @@ static int atop_response_result_parse_cjson(const uint8_t *input, size_t ilen, a log_info("Connecting to server: %s:%d", server_host, server_port); http_status = http_client_request(&(const http_client_request_t){.cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, .host = server_host, .port = server_port, .method = "POST", diff --git a/modules/iot-client/src/atop_base.h b/modules/iot-client/src/atop_base.h index b745766..ac108b8 100644 --- a/modules/iot-client/src/atop_base.h +++ b/modules/iot-client/src/atop_base.h @@ -24,6 +24,7 @@ #include #include "cJSON.h" #include "iot_client.h" + #include "tls.h" typedef struct { const char *path; @@ -40,6 +41,7 @@ const char *host; // Server host (optional, defaults to TUYA_DEFAULT_HOST) uint16_t port; // Server port (optional, defaults to TUYA_DEFAULT_PORT) const char *cacert; // CA certificate PEM content for TLS verification + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback } atop_base_request_t; typedef struct { diff --git a/modules/iot-client/src/http_client_interface.c b/modules/iot-client/src/http_client_interface.c index 6122437..0a66ad1 100644 --- a/modules/iot-client/src/http_client_interface.c +++ b/modules/iot-client/src/http_client_interface.c @@ -109,9 +109,10 @@ static void *connect_tcp(const pal_t *pal, const char *host, uint16_t port, uint return handle; } -static int connect_tls(struct HTTPNetworkContext *ctx, const char *host, uint16_t port, const char *cacert) { +static int connect_tls(struct HTTPNetworkContext *ctx, const char *host, uint16_t port, + const char *cacert, tls_cert_bundle_attach_fn cert_bundle_attach) { bool has_cacert = (cacert && cacert[0] != '\0'); - if (!has_cacert) { + if (!has_cacert && !cert_bundle_attach) { log_warn("No CA certificate provided - server verification disabled"); } @@ -120,6 +121,7 @@ static int connect_tls(struct HTTPNetworkContext *ctx, const char *host, uint16_ .port = port, .sni = host, .cacert = has_cacert ? cacert : NULL, + .cert_bundle_attach = cert_bundle_attach, .verify = TLS_VERIFY_NONE, // no CA -> no verification (legacy behaviour) .force_tls12 = true, .ciphersuites = tls_ciphersuites_tuya_default(), @@ -175,7 +177,8 @@ http_client_status_t http_client_request(const http_client_request_t *request, // Determine if TLS should be used bool use_tls = (request->port == 443) || - (request->cacert != NULL && request->cacert[0] != '\0'); + (request->cacert != NULL && request->cacert[0] != '\0') || + (request->cert_bundle_attach != NULL); if (request->port == 80) { use_tls = false; } @@ -199,7 +202,7 @@ http_client_status_t http_client_request(const http_client_request_t *request, int connect_ret = OPRT_COMMUNICATION_ERROR; if (use_tls) { connect_ret = connect_tls(network_ctx, request->host, request->port, - request->cacert); + request->cacert, request->cert_bundle_attach); } else { network_ctx->tcp_handle = connect_tcp(network_ctx->pal, request->host, request->port, network_ctx->timeout_ms); diff --git a/modules/iot-client/src/http_client_interface.h b/modules/iot-client/src/http_client_interface.h index 1028a14..5796ce2 100644 --- a/modules/iot-client/src/http_client_interface.h +++ b/modules/iot-client/src/http_client_interface.h @@ -4,6 +4,7 @@ #include #include #include "iot_client.h" +#include "tls.h" typedef enum { HTTP_CLIENT_SUCCESS = 0, @@ -20,6 +21,7 @@ typedef struct { typedef struct { const char *cacert; // CA certificate PEM content for TLS verification + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback (NULL = none) const char *host; uint16_t port; const char *method; diff --git a/modules/iot-client/src/iot_client.c b/modules/iot-client/src/iot_client.c index 005d3dd..4be2685 100644 --- a/modules/iot-client/src/iot_client.c +++ b/modules/iot-client/src/iot_client.c @@ -110,6 +110,7 @@ static int iot_client_dns_resolve(iot_client_t *client) }; iot_dns_url_config_request_t dns_req = { .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, .host = NULL, .port = 0, .region = iot_region_to_string(client->region), @@ -207,6 +208,7 @@ IOT_API iot_client_t *iot_client_init(const iot_client_config_t *config) client->env = config->env; client->mqtt_disable_tls = config->mqtt_disable_tls; client->cacert = config->cacert; + client->cert_bundle_attach = config->cert_bundle_attach; client->message_callback = config->message_callback; /* DP layer: restore persisted schema_id / schema from config (restart path). @@ -250,6 +252,7 @@ IOT_API iot_client_t *iot_client_init(const iot_client_config_t *config) .host = host, .port = meta_port, .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, }; device_meta_save_response_t meta_resp = {0}; int ret = atop_device_meta_save(pal, &meta_req, &meta_resp); @@ -336,6 +339,7 @@ IOT_API iot_client_t *iot_client_init_on_boarding(const iot_on_boarding_config_t ob_cfg.env = config->env; ob_cfg.mqtt_disable_tls = config->mqtt_disable_tls; ob_cfg.cacert = config->cacert; + ob_cfg.cert_bundle_attach = config->cert_bundle_attach; ob_cfg.dns_host = NULL; ob_cfg.dns_port = 0; @@ -359,6 +363,7 @@ IOT_API iot_client_t *iot_client_init_on_boarding(const iot_on_boarding_config_t client_config.mqtt_disable_tls = config->mqtt_disable_tls; client_config.mqtt_auto_connect = config->mqtt_auto_connect; client_config.cacert = config->cacert; + client_config.cert_bundle_attach = config->cert_bundle_attach; client_config.message_callback = config->message_callback; /* schema / schema_id come from the activation response. iot_client_init() * copies them and rebuilds the DP registry from the schema (dp_state is then @@ -424,6 +429,7 @@ IOT_API iot_client_t *iot_client_init_on_boarding_with_token(const iot_on_boardi ob_cfg.env = config->env; ob_cfg.mqtt_disable_tls = config->mqtt_disable_tls; ob_cfg.cacert = config->cacert; + ob_cfg.cert_bundle_attach = config->cert_bundle_attach; ob_cfg.dns_host = NULL; ob_cfg.dns_port = 0; @@ -445,6 +451,7 @@ IOT_API iot_client_t *iot_client_init_on_boarding_with_token(const iot_on_boardi client_config.mqtt_disable_tls = config->mqtt_disable_tls; client_config.mqtt_auto_connect = config->mqtt_auto_connect; client_config.cacert = config->cacert; + client_config.cert_bundle_attach = config->cert_bundle_attach; client_config.message_callback = config->message_callback; /* schema / schema_id come from the activation response. iot_client_init() * copies them and rebuilds the DP registry from the schema (dp_state is then @@ -485,6 +492,7 @@ IOT_API int iot_client_get_session_token(iot_client_t *client, const char *agent .host = host, .port = parsed_port, .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, }; ai_token_response_t resp = {0}; @@ -532,6 +540,7 @@ IOT_API int iot_get_ca_certificate(iot_client_t *client, const char *host, uint1 .host = IOT_DNS_DEFAULT_HOST, .port = IOT_DNS_DEFAULT_PORT, .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, .target_host = host, .target_port = port, .public_key_algorithm = "ECDSA", @@ -576,6 +585,7 @@ IOT_API int iot_get_qrcode_info(const iot_qrcode_request_t *request, iot_qrcode_ }; iot_dns_url_config_request_t dns_req = { .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, .host = NULL, .port = 0, .region = region_str, @@ -614,6 +624,7 @@ IOT_API int iot_get_qrcode_info(const iot_qrcode_request_t *request, iot_qrcode_ .host = host, .port = port, .cacert = request->cacert, + .cert_bundle_attach = request->cert_bundle_attach, }; qrcode_info_response_t resp = {0}; diff --git a/modules/iot-client/src/iot_client_message.c b/modules/iot-client/src/iot_client_message.c index df2f695..f5a1be4 100644 --- a/modules/iot-client/src/iot_client_message.c +++ b/modules/iot-client/src/iot_client_message.c @@ -63,7 +63,8 @@ static int iot_client_message_try_connect(iot_client_t *client) return OPRT_COMMUNICATION_ERROR; } - mqtt_tls_config_t tls_cfg = { .cacert = client->cacert }; + mqtt_tls_config_t tls_cfg = { .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach }; mqtt_client_config_t mqtt_cfg = { .broker_url = client->mqtt_url, .client_id = client->devid, diff --git a/modules/iot-client/src/iot_dns.c b/modules/iot-client/src/iot_dns.c index c484f9b..4c1ea28 100644 --- a/modules/iot-client/src/iot_dns.c +++ b/modules/iot-client/src/iot_dns.c @@ -24,6 +24,7 @@ static void parse_ip_array(cJSON *arr, char out[][64], int *count, int max) { } static int dns_http_request(const pal_t *pal, const char *host, uint16_t port, const char *cacert, + tls_cert_bundle_attach_fn cert_bundle_attach, const char *method, const char *path, const char *body, cJSON **out_json) { http_client_header_t headers[] = { @@ -35,6 +36,7 @@ static int dns_http_request(const pal_t *pal, const char *host, uint16_t port, c http_client_status_t status = http_client_request( &(const http_client_request_t){ .cacert = cacert, + .cert_bundle_attach = cert_bundle_attach, .host = host, .port = port, .method = method, @@ -109,7 +111,7 @@ int iot_dns_query(const pal_t *pal, const iot_dns_query_request_t *request, if (!json_body) return OPRT_MALLOC_FAILED; cJSON *root = NULL; - int ret = dns_http_request(pal, host, port, request->cacert, + int ret = dns_http_request(pal, host, port, request->cacert, request->cert_bundle_attach, "POST", "/v1/dns_query", json_body, &root); cJSON_free(json_body); if (ret != OPRT_OK) return ret; @@ -196,7 +198,7 @@ int iot_dns_url_config(const pal_t *pal, const iot_dns_url_config_request_t *req if (!json_body) return OPRT_MALLOC_FAILED; cJSON *root = NULL; - int ret = dns_http_request(pal, host, port, request->cacert, + int ret = dns_http_request(pal, host, port, request->cacert, request->cert_bundle_attach, "POST", "/v2/url_config", json_body, &root); cJSON_free(json_body); if (ret != OPRT_OK) return ret; @@ -317,7 +319,7 @@ int iot_dns_get_ca_cert(const pal_t *pal, const iot_dns_ca_cert_request_t *reque } cJSON *root = NULL; - int ret = dns_http_request(pal, dns_host, dns_port, request->cacert, + int ret = dns_http_request(pal, dns_host, dns_port, request->cacert, request->cert_bundle_attach, "GET", path, NULL, &root); pal->free(path); if (ret != OPRT_OK) return ret; diff --git a/modules/iot-client/src/iot_dns.h b/modules/iot-client/src/iot_dns.h index 3777f26..8bfa704 100644 --- a/modules/iot-client/src/iot_dns.h +++ b/modules/iot-client/src/iot_dns.h @@ -5,6 +5,7 @@ #include #include #include "iot_config_defaults.h" +#include "tls.h" #define IOT_DNS_DEFAULT_HOST "h1.iot-dns.com" #define IOT_DNS_DEFAULT_PORT 443 @@ -35,6 +36,7 @@ typedef struct { const char *host; uint16_t port; const char *cacert; + tls_cert_bundle_attach_fn cert_bundle_attach; const iot_dns_domain_t *domains; int domain_count; } iot_dns_query_request_t; @@ -79,6 +81,7 @@ typedef struct { const char *host; uint16_t port; const char *cacert; + tls_cert_bundle_attach_fn cert_bundle_attach; const char *region; const char *env; const char *uuid; @@ -114,6 +117,7 @@ typedef struct { const char *host; // DNS service host (NULL = IOT_DNS_DEFAULT_HOST) uint16_t port; // DNS service port (0 = IOT_DNS_DEFAULT_PORT) const char *cacert; // CA cert for TLS to DNS service + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback const char *target_host; // Host to query CA certificate for (required) uint16_t target_port; // Target service port (0 = 443) const char *public_key_algorithm; // "RSA" or "ECDSA" (NULL = "RSA") diff --git a/modules/iot-client/src/iot_dp.c b/modules/iot-client/src/iot_dp.c index edd435d..911b0ab 100644 --- a/modules/iot-client/src/iot_dp.c +++ b/modules/iot-client/src/iot_dp.c @@ -892,6 +892,7 @@ int iot_dp_schema_check_update(iot_client_t *client) .host = host[0] ? host : NULL, .port = port, .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, }; schema_newest_response_t resp = {0}; int rt = atop_schema_newest_get(pal, &req, &resp); diff --git a/modules/iot-client/src/iot_on_boarding.c b/modules/iot-client/src/iot_on_boarding.c index 365125a..5c463f1 100644 --- a/modules/iot-client/src/iot_on_boarding.c +++ b/modules/iot-client/src/iot_on_boarding.c @@ -275,6 +275,7 @@ static int activate_device(const pal_t *pal, on_boarding_config_t *on_boarding, } } request.cacert = on_boarding->cacert; + request.cert_bundle_attach = on_boarding->cert_bundle_attach; log_info("Sending activation request with:"); log_info(" - Token: [%zu chars, prefix=%.4s...]", @@ -536,6 +537,7 @@ int on_boarding_with_qrcode(const pal_t *pal, on_boarding_config_t *on_boarding, .host = on_boarding->dns_host, .port = on_boarding->dns_port, .cacert = on_boarding->cacert, + .cert_bundle_attach = on_boarding->cert_bundle_attach, .env = on_boarding->env == PRE ? "pre" : "prod", .uuid = on_boarding->uuid, .config = dns_keys, @@ -583,6 +585,8 @@ int on_boarding_with_qrcode(const pal_t *pal, on_boarding_config_t *on_boarding, if (!on_boarding->mqtt_disable_tls) { if (on_boarding->cacert && on_boarding->cacert[0] != '\0') { tls_config.cacert = on_boarding->cacert; + } else if (on_boarding->cert_bundle_attach) { + tls_config.cert_bundle_attach = on_boarding->cert_bundle_attach; } else { log_warn("MQTT TLS without CA certificate - server verification disabled"); } diff --git a/modules/iot-client/src/iot_on_boarding.h b/modules/iot-client/src/iot_on_boarding.h index 400fb17..8e207f3 100644 --- a/modules/iot-client/src/iot_on_boarding.h +++ b/modules/iot-client/src/iot_on_boarding.h @@ -16,6 +16,7 @@ typedef struct { const char *skill_param; char firmware_key[64]; const char *cacert; // CA cert for all TLS (caller-owned) + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback int timeout_ms; iot_env_t env; bool mqtt_disable_tls; diff --git a/modules/iot-client/src/iot_ota.c b/modules/iot-client/src/iot_ota.c index bf9a186..6307682 100644 --- a/modules/iot-client/src/iot_ota.c +++ b/modules/iot-client/src/iot_ota.c @@ -35,6 +35,7 @@ int iot_ota_report_version(iot_client_t *client, const char *sw_ver) .host = host[0] ? host : NULL, .port = port, .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, }; return atop_version_update(client->pal, &req); @@ -61,6 +62,7 @@ int iot_ota_check_upgrade(iot_client_t *client, int channel, const char *sw_ver, .host = host[0] ? host : NULL, .port = port, .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, }; ota_upgrade_response_t resp = {0}; @@ -99,6 +101,7 @@ int iot_ota_report_status(iot_client_t *client, int channel, iot_ota_status_t st .host = host[0] ? host : NULL, .port = port, .cacert = client->cacert, + .cert_bundle_attach = client->cert_bundle_attach, }; return atop_upgrade_status_update(client->pal, &req); diff --git a/modules/iot-client/src/mqtt.c b/modules/iot-client/src/mqtt.c index 874187b..fe05eb2 100644 --- a/modules/iot-client/src/mqtt.c +++ b/modules/iot-client/src/mqtt.c @@ -65,6 +65,7 @@ struct mqtt_client { bool connected; bool use_tls; const char *cacert; + tls_cert_bundle_attach_fn cert_bundle_attach; const pal_t *pal; // SUBACK tracking @@ -190,9 +191,10 @@ static void *connect_to_broker(mqtt_client *client) { } // Returns: OPRT_OK on success, OPRT_TLS_HANDSHAKE_FAILED on failure. -static int connect_to_broker_tls(NetworkContext_t *network_ctx, const char *host, int port, const char *cacert) { +static int connect_to_broker_tls(NetworkContext_t *network_ctx, const char *host, int port, + const char *cacert, tls_cert_bundle_attach_fn cert_bundle_attach) { bool has_cacert = (cacert && cacert[0] != '\0'); - if (!has_cacert) { + if (!has_cacert && !cert_bundle_attach) { log_warn("No CA certificate provided - server verification disabled"); } @@ -201,6 +203,7 @@ static int connect_to_broker_tls(NetworkContext_t *network_ctx, const char *host .port = (uint16_t)port, .sni = host, .cacert = has_cacert ? cacert : NULL, + .cert_bundle_attach = cert_bundle_attach, .verify = TLS_VERIFY_NONE, // no CA -> no verification (legacy behaviour) .force_tls12 = true, .ciphersuites = tls_ciphersuites_tuya_default(), @@ -314,8 +317,14 @@ mqtt_client *mqtt_client_create_with_config(const mqtt_client_config_t *config) log_info("TLS enabled with CA cert PEM"); } else { client->cacert = NULL; - log_warn("TLS enabled without CA certificate - server verification disabled"); + if (config->tls_config && config->tls_config->cert_bundle_attach) { + log_info("TLS enabled with platform cert bundle"); + } else { + log_warn("TLS enabled without CA certificate - server verification disabled"); + } } + if (config->tls_config) + client->cert_bundle_attach = config->tls_config->cert_bundle_attach; } // Copy client configuration @@ -387,7 +396,8 @@ int mqtt_client_connect(mqtt_client *client) { // Establish connection (TCP or TLS) if (client->use_tls) { int tls_ret = connect_to_broker_tls(&client->network_context, client->broker_host, - client->broker_port, client->cacert); + client->broker_port, client->cacert, + client->cert_bundle_attach); if (tls_ret != 0) { release_mqtt_buffer(client); return tls_ret; diff --git a/modules/iot-client/src/mqtt.h b/modules/iot-client/src/mqtt.h index fb6e743..c1e66c5 100644 --- a/modules/iot-client/src/mqtt.h +++ b/modules/iot-client/src/mqtt.h @@ -5,6 +5,7 @@ #include #include #include "iot_client.h" +#include "tls.h" // MQTT message callback type typedef void (*mqtt_message_callback_t)(const char *topic, size_t topic_len, @@ -19,6 +20,7 @@ typedef struct mqtt_client mqtt_client; */ typedef struct { const char *cacert; // CA certificate PEM content + tls_cert_bundle_attach_fn cert_bundle_attach; // Platform cert-bundle callback (NULL = none) const char *client_cert; // Client certificate PEM content (NULL if not needed) const char *client_key; // Client key PEM content (NULL if not needed) bool verify_peer; // Whether to verify peer certificate