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:

  1. Copy tutorials/mqtt/mqtt-client/mongoose_mqtt.c into the project directory
  2. Add it to the Makefile:
SOURCES = main.c hal.c mongoose_mqtt.c
  1. In main.c, call mg_mqtt_init() after mg_mgr_init(), and mg_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.

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: