How to connect STM32 to AWS IoT Core
We take an STM32 Nucleo-H723ZG, add an MQTT client to it, and connect it to AWS IoT Core over mutual TLS, using an elliptic curve (EC) device certificate signed by Amazon. No RTOS, no lwIP, no mbedTLS. Mongoose does the TCP/IP, TLS and MQTT parts.
The project we start from is nucleo-h723zg/minimal, a plain Makefile build with CMSIS headers. The steps are the same for the nucleo-h723zg/cubemx project, and for any other board Mongoose supports.
The setup
The board has two cables going to my workstation. An Ethernet cable goes into a USB-Ethernet dongle, and a USB cable goes to the on-board ST-Link. The ST-Link is wired to USART3 on the STM32H7, so a serial console on the workstation shows the firmware log.
On the workstation I turned on internet sharing from Wi-Fi to that dongle. The workstation then runs a DHCP server on the dongle interface and sets up routing, so the board gets an IP address when it boots and can reach the internet. A plain cable to your office router works too.
Step 1: Build and flash the minimal project
Prepare your build environment, then clone the repo and flash the minimal tutorial:
git clone https://github.com/cesanta/mongoose
cd mongoose/tutorials/stm32/nucleo-h723zg/minimal
make flash
Start the serial console first, so you catch the boot log. make flash
fetches the CMSIS headers for Arm and the H7, builds the firmware and flashes
it. In the log you should see the board boot and get an IP address over DHCP.
Open that address in a browser and you get a response from the built-in web
server.
main.c does the hardware init, creates a Mongoose event manager, starts an
HTTP listener and then falls into a superloop with two tasks, the network and
the blinky:
struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL);
for (;;) {
mg_mgr_poll(&mgr, 0);
blink_task();
}
You can reach this point by integrating Mongoose in your existing STM32 CubeMX project: just follow the excellent Ethernet Getting Started Guide by ST's Bruno Montanari, and follow the rest of this article.
With Mongoose in the firmware, the rest of its features are right there: HTTP, MQTT, Modbus TCP, OTA firmware updates and so on. Mongoose is cross-platform, so adding one of them is mostly copy-pasting code from an existing tutorial. We need MQTT.
Step 2: Add the MQTT client
The MQTT client tutorial
keeps all its logic in one file, mongoose_mqtt.c. The README says: copy the
file, add it to the build, add two function calls. So:
- Copy
tutorials/mqtt/mqtt-client/mongoose_mqtt.cinto the project directory - Add it to the
Makefile:
SOURCES = main.c hal.c mongoose_mqtt.c
- In
main.c, callmg_mqtt_init()aftermg_mgr_init(), andmg_mqtt_poll()in the main loop:
struct mg_mgr mgr;
mg_mgr_init(&mgr);
mg_mqtt_init(&mgr);
mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL);
for (;;) {
mg_mgr_poll(&mgr, 0);
mg_mqtt_poll(&mgr);
blink_task();
}
Step 3: Check it against HiveMQ
The client connects to the broker, subscribes to one topic, and echoes every message it gets there to another topic. By default it uses the public HiveMQ broker:
#define MQTT_SERVER_URL "mqtt://broker.hivemq.com:1883"
#define MQTT_CLIENT_ID "d3"
#define MQTT_USER MQTT_CLIENT_ID
#define MQTT_PASS ""
#define MQTT_PUBLISH_TOPIC "mg/" MQTT_CLIENT_ID "/tx"
#define MQTT_SUBSCRIBE_TOPIC "mg/" MQTT_CLIENT_ID "/rx"
The client ID d3 plays the role of a device ID. The device listens on
mg/d3/rx (receive) and answers on mg/d3/tx (transmit). Rebuild with
make flash, and the log shows the board connected and subscribed.
Open the HiveMQ WebSocket client,
click Connect and subscribe to mg/d3/#. The # is a wildcard, it matches
everything below mg/d3/. Publish hi to mg/d3/rx. The board gets the
message and sends a response, and you see both in the HiveMQ console.
MQTT works. Anything that breaks from now on is the AWS side or the TLS setup, not the client.
Step 4: Get the AWS IoT endpoint
Log in to the AWS console and search for IoT. There's a "Connect device" button with a wizard, but we skip it. The wizard generates an RSA certificate, and we want an EC one, which is cheaper for a microcontroller to work with. So we follow the steps from the tutorial README instead.
On the left menu go to Connect, Domain configurations. The domain name there
is your MQTT broker address. Copy it into mongoose_mqtt.c:
#define MQTT_SERVER_URL "mqtts://YOUR_DOMAIN_NAME:8883"
#define MQTT_CLIENT_ID "d1"
Note the mqtts:// scheme and port 8883. That's MQTT over TLS, and the
scheme is what tells Mongoose to do the TLS handshake.
Step 5: Create an EC certificate signing request
On the workstation, generate a P-256 private key and a certificate signing
request (CSR) for device d1:
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -pkeyopt ec_param_enc:named_curve -nodes -keyout device.key -out device.csr -subj /CN=d1
You get two files. device.key is the device private key, it stays with
you and goes into the firmware. device.csr goes to Amazon.
Step 6: Register the device as a thing
In AWS IoT terms a device is a "thing". On the left menu click Manage, All devices, Things, then Create things, Create single thing.
- Thing name:
d1 - No shadow, click Next
- Certificate: choose "Upload CSR" and give it
device.csr, click Next - Policy: create a policy named
AllowAll, effect Allow, action*, resource* - Attach
AllowAllto the certificate and click Create thing
Authentication vs authorization
It's worth stopping here for a minute, because this is where AWS differs from a plain MQTT broker.
MQTT has a username and password in the protocol. AWS IoT doesn't use them. It authenticates devices with certificates, using mutual TLS: both the server and the client prove who they are. When the device connects, AWS asks for its certificate, and that certificate must be signed by Amazon. We gave Amazon our CSR, Amazon signed it, and the result is a certificate AWS trusts. That's authentication: "let me in".
Authorization is what you can do once you're in. In AWS IoT it's a policy, a JSON document attached to the certificate. For MQTT that means which topics the device may publish to and subscribe to. When a device authenticates, AWS looks up the policy attached to its certificate and allows or denies each topic based on it.
Why all this? Because MQTT itself has no access control. Any client that
gets in can subscribe to # and read every message from every device. It can
also publish to any topic, so if devices listen for commands on some topics,
that client can send commands to all of them. MQTT is not very secure by
design, and that's why cloud vendors like Amazon added their own,
non-standard mechanisms on top.
AllowAll is fine for a demo. In production, make the policy fine-grained,
so each device can only touch its own topics.
Step 7: Activate and download the certificate
Go to Things, d1, Certificates. Click the certificate, activate it, and
download it. Save it as device.crt. Now you have three files: device.key,
device.csr and device.crt.
Step 8: Put the certificates into the firmware
mongoose_mqtt.c has three TLS settings, all C strings:
#define TLS_CA "" // Use https://mongoose.ws/tls/
#define TLS_KEY ""
#define TLS_CRT ""
TLS_CA is the CA the device uses to verify the AWS server. Open the
Mongoose TLS helper, enter your domain name with
port 8883 (YOUR_DOMAIN_NAME:8883), and click "Get CA Certificate". Tick
"Show as C/C++ constant", copy the result and paste it as TLS_CA.
TLS_KEY and TLS_CRT are the device key and the certificate from
Amazon. These commands turn the PEM files into C string literals:
sed 's/\r$//; s/.*/ "&\\n"/; $!s/$/ \\/' device.key
sed 's/\r$//; s/.*/ "&\\n"/; $!s/$/ \\/' device.crt
Paste the outputs into TLS_KEY and TLS_CRT.
Step 9: Test it
Run make flash. The log shows the connection to AWS IoT with status 0,
which means success, and a subscription to mg/d1/rx.
In the AWS console go to Test, MQTT test client. Subscribe to mg/#, then
publish hi to mg/d1/rx. The board gets the message and sends the
response to mg/d1/tx, and the test client shows both. The STM32 is talking
to AWS IoT.
What's in the firmware
There's no Cube framework here, no RTOS, no lwIP and no separate TLS library. It's CMSIS headers, a small HAL for clocks and pins, and Mongoose. The firmware is bare metal, and the same code runs under an RTOS like FreeRTOS or ThreadX.
FAQ
Does AWS IoT Core use the MQTT username and password? No. AWS IoT authenticates devices with X.509 client certificates over mutual TLS. Access to topics is controlled by a policy attached to the certificate.
Why not use the "Connect device" wizard? It generates an RSA certificate. We create our own EC key and CSR, and let Amazon sign it. EC is more efficient on a microcontroller.
What port and URL does the device use?
mqtts:// plus the domain name from Connect, Domain configurations, port
8883.
Do I need lwIP or mbedTLS for this?
No. Mongoose has its own TCP/IP stack, TLS 1.3 and MQTT client. The whole
example is mongoose.c, mongoose.h and mongoose_mqtt.c on top of CMSIS.
Does it work on other STM32 boards?
Yes. The MQTT client is the same file on every platform Mongoose supports.
Start from your board's tutorial in the
STM32 tutorials
and add mongoose_mqtt.c the same way.
Links:
- MQTT client tutorial, with short AWS, Azure and EMQX instructions in the README
- STM32 tutorials
- Mongoose TLS helper
- How to connect STM32 to EMQX, the same board and client with username/password auth
- How to connect STM32 to Azure IoT Hub, the same board and client talking to Azure with mutual TLS
- MQTT on a microcontroller, for background on MQTT itself