Overview
MQTTS is MQTT over TLS: a TLS encryption layer on top of plaintext MQTT (default port 8883), guaranteeing that messages are confidential, complete and tamper-proof in transit, and that the server identity can be verified via a digital certificate (the "ID card" of the network world, proving the server really is itself, not an impostor). The official example used in this tutorial also demonstrates stricter two-way authentication (mTLS) — the server verifies the device (client certificate + private key), and the device verifies the server (CA certificate) — commonly required by cloud platforms such as AWS IoT for device-level authentication. The certificate files are packaged into the ROMFS filesystem partition of the firmware (a read-only storage area carved out of the firmware; files such as certificates are flashed into the board together with the firmware) and loaded at runtime.
In plain words: MQTTS is "encrypted MQTT" — like putting what you want to say into an encrypted envelope before mailing it: MQTT handles "what to say" (sending messages), TLS handles "how to say it safely" (encryption); even if someone intercepts the envelope, they can't read the content. It also verifies identity both ways: the device presents its "ID card" (client certificate) to the server, and the server presents its "ID card" (CA certificate) to the device — nobody can impersonate anybody else; that's mTLS two-way authentication.
This tutorial is written based on the official example
applications/protocols/mqtt/sslof the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, versionrelease_bl_iot_sdk_1.6.40); the code can be found directly in the local SDK (the official README is in Chinese — this section extracts its key points).
Open the terminal and enter the official mqtt/ssl example project directory:
cd ~/Ai-Thinker-WB2/applications/protocols/mqtt/ssl
Note:
cdis the “change directory” command, entering the official example project; all subsequentmakecommands must run in this directory.
Project structure:
ssl/
├── cert/ ← certificate directory: rootcert.pem (CA), ccert.crt (client cert), ckey.key (client private key)
├── ssl/
│ ├── demo.c ← MQTT client: certificate loading + mTLS connection config
│ ├── fs.c ← ROMFS filesystem read/write (vfs wrapper)
│ ├── main.c ← Wi-Fi networking framework (calls mqtt_start() after GOT_IP)
└── proj_config.mk ← CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1 already enabled
Open ssl/main.c and change the SSID/password (the official default is specter — change it to your own router):
#define ROUTER_SSID "your ssid"
#define ROUTER_PWD "your password"
Open ssl/demo.c and change the MQTT server address and account:
axk_mqtt_client_config_t mqtt_cfg = {
.uri = "mqtts://hostname.com:8883", // ★ change to your own MQTTS server
...
.username = "123",
.password = "12345678",
.client_id = "11111111",
...
};
⚠️ The official default
hostname.comis a placeholder — it must be replaced with a real server. For public testing, use the EMQX public Broker:mqtts://broker.emqx.io:8883(one-way authentication, no client certificate needed); cloud platforms like AWS IoT use mTLS two-way authentication (apply for a device certificate).
This example needs three PEM-format (a text-format certificate file starting with -----BEGIN CERTIFICATE-----) files (the official placeholders are already in ssl/cert/):
| File | Purpose | Use |
|---|---|---|
rootcert.pem |
CA root certificate (.cert_pem) |
the device verifies the server identity (required), preventing connection to an impostor server |
ccert.crt |
client certificate (.client_cert_pem) |
the server verifies the device (mTLS two-way auth), proving “I really am this device” |
ckey.key |
client private key (.client_key_pem) |
paired with the client certificate (mTLS two-way auth); the private key must be kept secret and never leaked |
- Download/generate the matching certificates and private key from your cloud platform (AWS IoT: console → Security → Policies/certificates)
- For one-way auth (e.g. the EMQX public Broker), only
cert_pemneeds filling; drop the client certificate fields - The certificate files are packaged into the firmware’s ROMFS partition by the build system at compile time, read at runtime from the
/romfs/path
💡 Certificates vs account/password: TLS certificates verify “who the device/server is” (identity); MQTT
.username/.passwordverifies “what it’s allowed to do” (permission) — together they form a complete device access security solution.
Open ssl/demo.c. The full code for this step has been moved to the end of this page:
📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand, identical to the official example (
applications/protocols/mqtt/ssl/ssl/demo.c).
Differences from MQTT Communication (the tcp version):
| Difference | Description |
|---|---|
.uri = "mqtts://hostname.com:8883" |
Protocol mqtt:// → mqtts://, port 1883 → 8883; the server knows from it to use an encrypted connection, and the port must match the server’s TLS port |
load_romfs_file() + aos_open/read |
Reads the certificates from ROMFS (instead of hardcoding them in code); changing certificates later needs no code recompile |
.cert_pem + .cert_len=0 |
CA certificate; passing length 0 lets the library compute it automatically (PEM ends with \0); counting bytes by hand is error-prone |
.client_cert_pem / .client_key_pem |
Client certificate + private key (two-way auth); without them the server doesn’t recognize this device |
.username / .password / .client_id |
MQTT account/password and client ID; certificates verify “who you are”, the account verifies “what you can do” |
More detailed MQTT_EVENT_ERROR |
Prints axk_tls_cert_verify_flags (certificate verification flags) and connect_return_code; certificate errors are located through them |
💡 One-way auth (most common, e.g. public Brokers) only needs
.uri+.cert_pem(CA), no client certificate/private key. Two-way auth (mTLS, e.g. AWS IoT) needs the full set of three: CA + client certificate + private key; the server identifies the device through the client certificate.
Build in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (the program flashed into the board) the board can run;-j8builds with 8 parallel cores, faster.
On success a firmware build_out/ssl.bin is generated — the certificates have been packaged into the ROMFS area by the build system, flashed together with the firmware (proj_config.mk has CONFIG_ENABLE_VFS_ROMFS=1 and CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1 enabled).
Flash (make flash is the same as normal projects; the ROMFS partition is written together with the firmware):
make flash p=/dev/ttyUSB0 b=921600
Note:
make flashis the “flash” command, writing the compiled firmware into the board. Afterp=comes the serial device (often/dev/ttyUSB0on Linux,COM3-like on Windows — use your computer’s actual one),b=921600is the flash baud rate (serial transfer speed), keep the default.
⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode. If using the official BLDevCube GUI flashing tool, additionally import the
partition_cfg_*.tomlpartition table and make sure the ROMFS partition is selected for flashing.
After flashing, the board automatically restarts and runs. The serial (baud rate 921600, the “speech rate” of serial transfer — both sides must agree) prints:
[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 appearing with no MQTT_EVENT_ERROR (no certificate verification failure) means the TLS handshake succeeded. If it stays stuck after axk_mqtt_client_init OK without CONNECTED, first confirm the board is online (you saw GOT IP earlier), your computer can reach the same MQTTS server (port 8883 reachable), then check the certificate and account config — see the FAQ at the end.
Verification on the computer: when creating a new connection in MQTTX, enable TLS (port 8883, enable certificate verification and import the same CA root certificate), subscribe to test/echo, and you’ll receive the board’s data_3 and data messages — and those messages are encrypted end to end.
💡 Packet-capture comparison: with plaintext MQTT (1883), Wireshark can read the message content directly; with MQTTS (8883) only TLS-encrypted packets are visible — that’s the point of encryption. If you only want to verify the encrypted link,
openssl s_client -connect host:8883shows the handshake process too.
API Summary for This Tutorial
axk_mqtt_client_init(&config)
Creates an MQTT client (configuring uri/certificates/event callback), returns a handle.
Parameters:
config:axk_mqtt_client_config_tstructure pointer;.uri(mqtts://host:8883), the certificate fields and.event_handleare required
Return: client handle on success; NULL on failure
axk_mqtt_client_start(client)
Starts the client and asynchronously establishes the TLS connection.
Parameters:
client: the client handle returned byaxk_mqtt_client_init
Return: 0 on success; negative error code on failure
axk_mqtt_client_subscribe(client, topic, qos)
Subscribes to a topic.
Parameters:
client: the client handletopic: topic string (the official example uses"test/echo")qos: QoS level (0/1/2)
Return: message ID (msg_id) on success; negative on failure
axk_mqtt_client_publish(client, topic, data, len, qos, retain)
Publishes a message to a topic.
Parameters:
client: the client handletopic: topic stringdata: message data pointerlen: message lengthqos: QoS levelretain: whether to retain the message
Return: message ID (msg_id) on success; negative on failure
axk_mqtt_client_config_t
The client configuration structure (TLS-added fields).
Parameters:
.uri:mqtts://host:8883(TLS uses themqtts://protocol).cert_pem/.cert_len: CA certificate; pass length0and the library computes it from the PEM text.client_cert_pem: client certificate (needed for two-way auth).client_key_pem: client private key (needed for two-way auth).username/.password: MQTT account and password.client_id: client ID
Return: none (a structure, not a function)
aos_open(path, 0)
Opens a ROMFS file (certificates are packaged in the ROMFS partition).
Parameters:
path: file path, e.g."/romfs/rootcert.pem"flags: open flags, pass0for read-only
Return: file descriptor on success; negative on failure
aos_lseek(fd, off, SEEK_END/SET)
Positions the file (gets the file size first, then returns to the start).
Parameters:
fd: file descriptoroff: offsetwhence:SEEK_END(file end) /SEEK_SET(file start)
Return: new offset position on success; negative on failure
aos_read(fd, buf, len)
Reads file content.
Parameters:
fd: file descriptorbuf: output bufferlen: read length
Return: bytes read on success; 0 = end of file; negative on failure
aos_close(fd)
Closes a file.
Parameters:
fd: file descriptor
Return: 0 on success; negative on failure
MQTT_EVENT_ERROR
MQTT_EVENT_ERROR event extended fields (key for troubleshooting).
Parameters:
error_type:MQTT_ERROR_TYPE_TCP_TRANSPORT(TCP transport layer) /CONNECTION_REFUSED(connection refused)axk_tls_cert_verify_flags: certificate verification failure flagsconnect_return_code: the connection code returned by the Broker
Return: none (event fields)
📌 Pass certificate length 0: the library computes the length from the PEM text (ends with
\0), avoiding hand-counting errors (the README explicitly calls this a common cause ofmbedtls_x509_crt_parsefailures). The certificate buffers must stay valid for the client's lifetime (the example usesmallocand doesn't free; note the heap cost of 3×2048 bytes).
Full Code
Below is the complete ssl/demo.c source, identical to the official example (applications/protocols/mqtt/ssl/ssl/demo.c):
📜 Click to expand the full ssl/demo.c code
#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 时释放)
}FAQ & Troubleshooting
⚠️ Serial prints aos_open path[0] failed
Cause: the certificate file isn't found in ROMFS — CONFIG_SYS_USER_VFS_ROMFS_ENABLE=1 not enabled, or the certificates weren't packaged into the firmware / the ROMFS partition wasn't flashed
Fix: check CONFIG_ENABLE_VFS_ROMFS:=1 in proj_config.mk; confirm the three files in cert/ exist and rebuild; when using a GUI flashing tool, make sure the ROMFS partition is flashed along
⚠️ MQTT_EVENT_ERROR, cert_verify non-zero (certificate verification failed)
Cause: the CA certificate doesn't match the server, the certificate is expired, the hostname doesn't match the cert's CN/SAN, or the one-way/two-way auth config is swapped
Fix: look at error_type and the cert_verify flags; use a CA root certificate from the same signing chain as the server; for public Brokers use their officially published CA; for AWS IoT use the Amazon root CA (Amazon Root CA 1)
⚠️ mbedtls_x509_crt_parse failed
Cause: the PEM file doesn't end with \0, the buffer is too small (official 2048 bytes, truncated with larger certificates), or the length fields aren't set to 0
Fix: set length fields such as .cert_len to 0; enlarge malloc(2048) (e.g. 4096) for larger certificates
⚠️ Server refuses the connection (Connection refused)
Cause: the server doesn't support two-way auth (public Brokers generally don't accept client certificates), wrong account/password, or a non-8883 port
Fix: when connecting to a public Broker (e.g. broker.emqx.io:8883), remove .client_cert_pem/.client_key_pem and switch to one-way auth; verify .username/.password/.client_id; confirm the server's 8883 port is reachable
⚠️ Other issues same as the tcp version
Note: starting the client after GOT_IP, topic/QoS mismatch, etc.
Fix: follow the pitfalls in MQTT Communication
⚠️ Keeps printing Connecting, never GOT IP (can't connect to the router)
Cause: wrong SSID/password, the router is on 5GHz, or the signal is too weak
Fix: verify ROUTER_SSID/ROUTER_PWD in ssl/main.c match the router exactly; make sure the router is 2.4GHz (the board doesn't support 5GHz); move the board closer to the router and retry
⚠️ Serial device not found / can't open
Cause: USB-to-serial driver not installed, insufficient permission, or the cable only charges and can't transfer data
Fix: on Linux check the device with lsusb/dmesg; if permission denied run sudo chmod 666 /dev/ttyUSB0; on Windows install the driver and check the COM port in Device Manager; try a data-capable cable
⚠️ Flashing keeps waiting / fails
Cause: download mode wasn't entered, wrong baud rate, or a wrong serial number
Fix: press and hold EN during flashing to enter download mode as prompted; change p=/dev/ttyUSB0 to your actual serial port; when using the BLDevCube GUI tool, make sure the ROMFS partition is selected; try another USB port or cable
Self-Check
MQTT_EVENT_CONNECTED appears on the serial with no cert_verify error, and MQTTX (with TLS enabled) receives the test/echo messages — MQTTS is verified.

