Skip to content

概述

MQTTSMQTT over TLS:在明文 MQTT 之上增加 TLS 加密层(默认端口 8883),保证消息在传输中机密、完整、防篡改,并可通过数字证书(网络世界的「身份证」,证明服务器确实是它自己,不是冒牌的)验证服务器身份。本教程使用的官方示例还演示了更严格的双向认证(mTLS)——服务器验证设备(客户端证书+私钥),设备也验证服务器(CA 证书),常用于 AWS IoT 等要求设备级认证的云平台。证书文件随固件打包进 ROMFS 文件系统分区(固件里划出的一块只读存储区,证书等文件随固件一起烧进开发板),运行时加载。

用大白话讲:MQTTS 就是「加密的 MQTT」——像把要说的话先装进加密信封再寄出去:MQTT 管「说什么」(发消息),TLS 管「怎么安全地说」(加密),别人就算截获了信封也看不懂内容。它还要双向验明正身:设备出示自己的「身份证」(客户端证书)给服务器看,服务器也出示「身份证」(CA 证书)给设备验——谁都不能冒充谁,这就是 mTLS 双向认证。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/protocols/mqtt/ssl 编写,代码可在本地 SDK 中直接找到(官方 README 为中文版,本节提炼其要点)。

🎯本页目标从 ROMFS 加载证书,通过 axk_mqtt 的 TLS 配置连接 MQTTS Broker,理解单向/双向认证与证书字段配置。
🧰前置条件① Ai-WB2 开发板一块(Type-C 数据线)② 2.4GHz 路由器(能访问公网)③ 已按 [SDK 安装](../sdk/sdk_intro) 完成环境搭建,并完成 [连接 Wi-Fi](./wifi_connect) 与 [MQTT 通信](./mqtt)。
🔗相关章节明文 MQTT 见 [MQTT 通信](./mqtt);HTTPS/TLS 见官方 [https_mbedtls 示例](https://gitee.com/Ai-Thinker-Open/Ai-Thinker-WB2/tree/master/applications/protocols/https_mbedtls)。

进入示例工程

打开终端,进入官方 mqtt/ssl 示例工程目录:

cd ~/Ai-Thinker-WB2/applications/protocols/mqtt/ssl

说明:cd 是「进入目录」的命令,进入官方示例工程;后面所有 make 命令都要在这个目录下执行。

工程结构:

ssl/
├── cert/          ← 证书目录:rootcert.pem(CA)、ccert.crt(客户端证书)、ckey.key(客户端私钥)
├── ssl/
│   ├── demo.c     ← MQTT 客户端:证书加载 + mTLS 连接配置
│   ├── fs.c       ← ROMFS 文件系统读写(vfs 封装)
│   ├── main.c     ← Wi-Fi 联网框架(GOT_IP 后调用 mqtt_start())
└── proj_config.mk ← 已开启 CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1
修改路由器参数与服务器地址

打开 ssl/main.c,修改 SSID/密码(官方默认 specter,需改成自己的路由器):

#define ROUTER_SSID "your ssid"
#define ROUTER_PWD "your password"

打开 ssl/demo.c,修改 MQTT 服务器地址与账号:

axk_mqtt_client_config_t mqtt_cfg = {
    .uri = "mqtts://hostname.com:8883",   // ★ 改成你自己的 MQTTS 服务器
    ...
    .username = "123",
    .password = "12345678",
    .client_id = "11111111",
    ...
};

⚠️ 官方默认 hostname.com 是占位符,必须替换为真实服务器。公共测试建议用 EMQX 公共 Brokermqtts://broker.emqx.io:8883(单向认证,无需客户端证书);AWS IoT 等云平台则是 mTLS 双向认证(申请设备证书)。

准备证书并写入 cert 目录

本示例需要三份 PEM 格式(一种文本格式的证书文件,以 -----BEGIN CERTIFICATE----- 开头)文件(ssl/cert/ 目录下官方已放好占位证书):

文件 作用 用途
rootcert.pem CA 根证书.cert_pem 设备验证服务器身份(必填),防止连上冒牌服务器
ccert.crt 客户端证书.client_cert_pem 服务器验证设备(mTLS 双向认证),证明「我就是这台设备」
ckey.key 客户端私钥.client_key_pem 与客户端证书配对(mTLS 双向认证),私钥要保密不能泄露
  • 从你的云平台(AWS IoT:控制台 → 安全 → 策略/证书)下载/生成对应证书与私钥
  • 单向认证(如 EMQX 公共 Broker)可只填 cert_pem,去掉客户端证书字段
  • 证书文件在编译时被构建系统打包进固件的 ROMFS 分区,运行时从 /romfs/ 路径读取

💡 证书 vs 账号密码:TLS 证书验证的是"设备/服务器是谁"(身份),MQTT .username/.password 验证的是"允许做什么"(权限)——两者叠加才是完整的设备接入安全方案。

编写代码

打开 ssl/demo.c,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/protocols/mqtt/ssl/ssl/demo.c)完全一致。

MQTT 通信(tcp 版)的差异

差异点 说明
.uri = "mqtts://hostname.com:8883" 协议 mqtt://mqtts://,端口 1883 → 8883;服务器靠它知道要走加密连接,端口必须与服务器 TLS 端口一致
load_romfs_file() + aos_open/read 从 ROMFS 读取证书(而非硬编码在代码里),以后换证书不用重新编译代码
.cert_pem + .cert_len=0 CA 证书;长度传 0 由库自动计算(PEM 以 \0 结尾),手算字节数极易出错
.client_cert_pem / .client_key_pem 客户端证书 + 私钥(双向认证),缺了服务器不认这台设备
.username / .password / .client_id MQTT 账号密码与客户端 ID;证书验「是谁」,账号验「能做什么」
MQTT_EVENT_ERROR 更详细 打印 axk_tls_cert_verify_flags(证书校验标志)、connect_return_code;证书出错时靠它定位原因

💡 单向认证(最常见,如公共 Broker)只需 .uri + .cert_pem(CA),不需要客户端证书/私钥。双向认证(mTLS,如 AWS IoT)才需要三件套:CA + 客户端证书 + 私钥,服务器通过客户端证书识别设备。

编译与烧录

在工程目录执行编译:

make -j8

说明:make 是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8 表示用 8 个 CPU 核并行编译,更快。

编译成功后生成固件 build_out/ssl.bin——证书已被构建系统打包进 ROMFS 区域,随固件一起烧录(proj_config.mk 已开启 CONFIG_ENABLE_VFS_ROMFS=1CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1)。

烧录(make flash 与普通工程相同,ROMFS 分区随固件一并写入):

make flash p=/dev/ttyUSB0 b=921600

说明:make flash 是「烧录」命令,把编译好的固件下载进开发板;p= 后面是串口设备号(Linux 下常为 /dev/ttyUSB0,Windows 下是 COM3 之类,以你电脑实际为准),b=921600 是烧录波特率(串口传数据的速度),保持默认即可。

⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式。若使用官方 BLDevCube 图形烧录工具,需额外导入 partition_cfg_*.toml 分区表,并确认 ROMFS 分区已勾选烧录。

运行验证

烧录完成后开发板自动重启运行,串口(波特率 921600,串口传数据的「语速」,两边必须一致)打印:

[APP] [EVT] GOT IP 5594
load_romfs_file success ca_len:1100, cli_len:800, key_len:900
[MQTT] axk_mqtt_client_init OK, starting client...
MQTT_EVENT_CONNECTED
sent subscribe successful, msg_id=1
sent publish successful, msg_id=2
MQTT_EVENT_SUBSCRIBED, msg_id=1
sent publish successful, msg_id=3
MQTT_EVENT_PUBLISHED, msg_id=2
MQTT_EVENT_PUBLISHED, msg_id=3

出现 MQTT_EVENT_CONNECTED 且无 MQTT_EVENT_ERROR(无证书校验失败),即 TLS 握手成功。如果一直停在 axk_mqtt_client_init OK 之后没有 CONNECTED,先确认开发板已联网(前面先看到 GOT IP)、电脑能连上同一个 MQTTS 服务器(8883 端口可达),再检查证书与账号配置,见文末 FAQ。

电脑端验证:MQTTX 新建连接时启用 TLS(端口 8883,开启证书校验并导入同一 CA 根证书),订阅 test/echo 主题,能收到开发板发布的 data_3data 消息——且这些消息全程加密传输

💡 抓包对比:明文 MQTT(1883)用 Wireshark 可直接看到消息内容;MQTTS(8883)只能看到 TLS 加密报文——这就是加密的意义。若只想验证加密链路,用 openssl s_client -connect host:8883 亦可看到握手过程。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

axk_mqtt_client_init(&config)

创建 MQTT 客户端(配置 uri/证书/事件回调),返回句柄。

参数

  • configaxk_mqtt_client_config_t 结构体指针,.urimqtts://host:8883)、证书字段与 .event_handle 必填

返回值:成功返回客户端句柄;失败返回 NULL

axk_mqtt_client_start(client)

启动客户端并异步建立 TLS 连接。

参数

  • clientaxk_mqtt_client_init 返回的客户端句柄

返回值:成功返回 0;失败返回负值错误码

axk_mqtt_client_subscribe(client, topic, qos)

订阅主题。

参数

  • client:客户端句柄
  • topic:主题字符串(官方示例 "test/echo"
  • qos:QoS 等级(0/1/2

返回值:成功返回消息 ID(msg_id);失败返回负值

axk_mqtt_client_publish(client, topic, data, len, qos, retain)

发布消息到主题。

参数

  • client:客户端句柄
  • topic:主题字符串
  • data:消息数据指针
  • len:消息长度
  • qos:QoS 等级
  • retain:是否保留消息

返回值:成功返回消息 ID(msg_id);失败返回负值

axk_mqtt_client_config_t

客户端配置结构体(TLS 新增字段)。

参数

  • .urimqtts://host:8883(TLS 用 mqtts:// 协议)
  • .cert_pem / .cert_len:CA 证书;长度传 0 由库按 PEM 文本自动计算
  • .client_cert_pem:客户端证书(双向认证需要)
  • .client_key_pem:客户端私钥(双向认证需要)
  • .username / .password:MQTT 账号密码
  • .client_id:客户端 ID

返回值:无(结构体,非函数)

aos_open(path, 0)

打开 ROMFS 文件(证书打包在 ROMFS 分区)。

参数

  • path:文件路径,如 "/romfs/rootcert.pem"
  • flags:打开标志,只读传 0

返回值:成功返回文件描述符;失败返回负值

aos_lseek(fd, off, SEEK_END/SET)

定位文件(先取文件大小再回到开头)。

参数

  • fd:文件描述符
  • off:偏移量
  • whenceSEEK_END(文件末尾)/ SEEK_SET(文件开头)

返回值:成功返回新偏移位置;失败返回负值

aos_read(fd, buf, len)

读取文件内容。

参数

  • fd:文件描述符
  • buf:输出缓冲区
  • len:读取长度

返回值:成功返回读取字节数;0 = 文件末尾;失败返回负值

aos_close(fd)

关闭文件。

参数

  • fd:文件描述符

返回值:成功返回 0;失败返回负值

MQTT_EVENT_ERROR

MQTT_EVENT_ERROR 事件扩展字段(排错关键)。

参数

  • error_typeMQTT_ERROR_TYPE_TCP_TRANSPORT(TCP 传输层)/ CONNECTION_REFUSED(连接被拒)
  • axk_tls_cert_verify_flags:证书校验失败标志
  • connect_return_code:Broker 返回的连接码

返回值:无(事件字段)

📌 证书长度传 0:库按 PEM 文本(\0 结尾)自动计算长度,避免手算字节数出错(README 明确指出这是 mbedtls_x509_crt_parse 失败的常见原因)。证书缓冲区必须在客户端生命周期内保持有效(示例用 malloc 且不 free,注意堆开销 3×2048 字节)。


完整代码

以下为 ssl/demo.c 完整源码,与官方示例(applications/protocols/mqtt/ssl/ssl/demo.c)完全一致:

📜 点击展开 ssl/demo.c 完整代码
c
#include "blog.h"
#include <FreeRTOS.h>
#include <mqtt_client.h>
#include <stdio.h>
#include <task.h>
#include <vfs.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define ROOTCERT_PATH "/romfs/rootcert.pem"
#define CLI_CERT_PATH "/romfs/ccert.crt"
#define CLI_KEY_PATH "/romfs/ckey.key"  

#define PUB_TOPIC "test/echo"
#define SUB_TOPIC "test/echo"


static int load_romfs_file(int index, char *out_buf, size_t *out_len)
{
    int fd = -1;
    switch (index) {
        case 0:
            fd = aos_open(ROOTCERT_PATH, 0);
            break;
        case 1:
            fd = aos_open(CLI_CERT_PATH, 0);
            break;
        case 2:
            fd = aos_open(CLI_KEY_PATH, 0);
            break;
        default:
            return -1;
    }

    if (fd < 0) {   
        blog_error("aos_open path[%d] failed", index);
        return -1;
    }

    ssize_t size = aos_lseek(fd, 0, SEEK_END);
    if (size < 0) {
        blog_error("aos_lseek SEEK_END failed");
        aos_close(fd);
        return -1;
    }
    aos_lseek(fd, 0, SEEK_SET);

    if (aos_read(fd, out_buf, size) != size) {

        aos_close(fd);
        return -1;
    }
    out_buf[size] = '\0';
    aos_close(fd);
    *out_len = size;
    return 0;
}

static void log_error_if_nonzero(const char *message, int error_code)
{
    if (error_code != 0)
    {
        blog_error("Last error %s: 0x%x", message, error_code);
    }
}

static axk_err_t event_cb(axk_mqtt_event_handle_t event)
{
    int32_t event_id;
    axk_mqtt_client_handle_t client = event->client;

    event_id = event->event_id;
    blog_debug("Event dispatched, event_id=%d", event_id);
    int msg_id;
    switch ((axk_mqtt_event_id_t)event_id)
    {
    case MQTT_EVENT_CONNECTED:
        blog_info("MQTT_EVENT_CONNECTED");
        msg_id = axk_mqtt_client_subscribe(client, SUB_TOPIC, 0);
        blog_info("sent subscribe successful, msg_id=%d", msg_id);

        msg_id = axk_mqtt_client_publish(client, PUB_TOPIC, "data_3", 0, 1, 0);
        blog_info("sent publish successful, msg_id=%d", msg_id);
        break;
    case MQTT_EVENT_DISCONNECTED:
        blog_info("MQTT_EVENT_DISCONNECTED");
        break;

    case MQTT_EVENT_SUBSCRIBED:
        blog_info("MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
        msg_id = axk_mqtt_client_publish(client, PUB_TOPIC, "data", 0, 0, 0);
        blog_info("sent publish successful, msg_id=%d", msg_id);
        break;
    case MQTT_EVENT_UNSUBSCRIBED:
        blog_info("MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
        break;
    case MQTT_EVENT_PUBLISHED:
        blog_info("MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
        break;
    case MQTT_EVENT_DATA:
        blog_info("MQTT_EVENT_DATA");
        printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
        printf("DATA=%.*s\r\n", event->data_len, event->data);
        break;
    case MQTT_EVENT_ERROR:
        blog_info("MQTT_EVENT_ERROR");
        if (event->error_handle) {
            blog_info("error_type=%d, sock_errno=%d, tls_err=%d, tls_stack_err=%d, cert_verify=0x%x",
                      event->error_handle->error_type,
                      event->error_handle->axk_transport_sock_errno,
                      event->error_handle->axk_tls_last_axk_err,
                      event->error_handle->axk_tls_stack_err,
                      event->error_handle->axk_tls_cert_verify_flags);
            if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
                log_error_if_nonzero("reported from axk-tls", event->error_handle->axk_tls_last_axk_err);
                log_error_if_nonzero("reported from tls stack", event->error_handle->axk_tls_stack_err);
                log_error_if_nonzero("captured as transport's socket errno", event->error_handle->axk_transport_sock_errno);
                blog_info("Last errno string (%s)", strerror(event->error_handle->axk_transport_sock_errno));
            } else if (event->error_handle->error_type == MQTT_ERROR_TYPE_CONNECTION_REFUSED) {
                blog_info("Connection refused, return_code=%d", event->error_handle->connect_return_code);
            }
        } else {
            blog_info("error_handle is NULL");
        }
        break;
    default:
        blog_info("Other event id:%d", event->event_id);
        break;
    }
    return AXK_OK;
}

void mqtt_start(void)
{
    char *ca_buf = malloc( sizeof(char)*2048);
    char *cli_buf = malloc( sizeof(char)*2048);
    char *key_buf = malloc( sizeof(char)*2048);
    memset(ca_buf, 0, sizeof(char)*2048);
    memset(cli_buf, 0, sizeof(char)*2048);
    memset(key_buf, 0, sizeof(char)*2048);
    size_t ca_len = 0, cli_len = 0, key_len = 0;
    if (load_romfs_file(0, ca_buf, &ca_len) != 0 ||
        load_romfs_file(1, cli_buf, &cli_len) != 0 ||
        load_romfs_file(2, key_buf, &key_len) != 0) {
        blog_error("Failed to load certificates from MEDIA");
        return;
    }
    blog_error("load_romfs_file success ca_len:%d, cli_len:%d, key_len:%d\r\n", ca_len, cli_len, key_len);
    axk_mqtt_client_config_t mqtt_cfg = {
        .uri = "mqtts://hostname.com:8883",
        .cert_pem = ca_buf,
        .cert_len = 0,
        .client_cert_pem = cli_buf,     // 双向认证:客户端证书
        .client_cert_len = 0,
        .client_key_pem = key_buf,      // 双向认证:客户端私钥
        .client_key_len = 0,
        .username = "123",
        .password = "12345678",
        .client_id = "11111111",
        .event_handle = event_cb,
    };

    axk_mqtt_client_handle_t client = axk_mqtt_client_init(&mqtt_cfg);
    if (client == NULL) {
        blog_error("[MQTT] axk_mqtt_client_init returned NULL! Check config/URI.");
        return;
    }
    blog_info("[MQTT] axk_mqtt_client_init OK, starting client...");
    axk_mqtt_client_start(client);

    // 注意:ca_buf / cli_buf / key_buf 需要在 client 生命周期内保持有效
    //(示例中未 free,实际可做 static 或在 deinit 时释放)
}

常见问题与踩坑提示

⚠️ 串口打印 aos_open path[0] failed
原因:ROMFS 中找不到证书文件——CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1 未开启,或证书未打包进固件/未烧录 ROMFS 分区
解决:检查 proj_config.mkCONFIG_ENABLE_VFS_ROMFS:=1;确认 cert/ 目录三个文件存在并重新编译;用图形烧录工具时确认 ROMFS 分区一并烧录

⚠️ MQTT_EVENT_ERROR,cert_verify 非 0(证书校验失败)
原因:CA 证书与服务器不匹配、证书过期、主机名与证书 CN/SAN 不符、或单向/双向认证配置颠倒
解决:看 error_typecert_verify 标志位;换用与服务器同一签发链的 CA 根证书;公共 Broker 用其官方公布的 CA;AWS IoT 用亚马逊根 CA(Amazon Root CA 1)

⚠️ mbedtls_x509_crt_parse 失败
原因:PEM 文件未以 \0 结尾、缓冲区过小(官方 2048 字节,证书更大时截断)、或长度字段未设 0
解决:把 .cert_len 等长度字段设为 0;证书较大时把 malloc(2048) 调大(如 4096)

⚠️ 服务器拒绝连接(Connection refused)
原因:服务器不支持双向认证(公共 Broker 一般只收客户端证书?不,是不收客户端证书)、账号密码错误、或端口非 8883
解决:连接公共 Broker(如 broker.emqx.io:8883)时去掉 .client_cert_pem/.client_key_pem 改为单向认证;核对 .username/.password/.client_id;确认服务器 8883 端口可达

⚠️ 与 tcp 版相同的其它问题
说明:GOT_IP 后再启动客户端、主题/QoS 不匹配等
解决:参照 MQTT 通信 的踩坑提示处理

⚠️ 一直打印 Connecting,始终没有 GOT IP(连不上路由器)
原因:SSID/密码填错、路由器是 5GHz、或信号太弱
解决:核对 ssl/main.cROUTER_SSID/ROUTER_PWD 与路由器完全一致;确认路由器是 2.4GHz(开发板不支持 5GHz);把开发板靠近路由器再试

⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或数据线只能充电不能传数据
解决:Linux 用 lsusb/dmesg 查看设备,权限不足可 sudo chmod 666 /dev/ttyUSB0;Windows 装驱动后到设备管理器查 COM 口;换一根能传数据的线

⚠️ 烧录一直等待 / 失败
原因:未进入下载模式、波特率不对、或串口号填错
解决:烧录时按提示长按 EN 键进入下载模式;确认 p=/dev/ttyUSB0 换成你实际的串口;用 BLDevCube 图形工具时确认 ROMFS 分区已勾选;换 USB 口或数据线重试

运行自检

串口出现 MQTT_EVENT_CONNECTED 且无 cert_verify 错误,MQTTX(开启 TLS)能收到 test/echo 消息,即 MQTTS 验证通过。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23