0

With an ESP8266 it's pretty easy to pass a custom bundle; eg:

BearSSL::WiFiClientSecure client;
BearSSL::CertStore certStore;

int numCerts = certStore.initCertStore(FSTYPE, "/certs.idx", "/certs.ar");
client.setCertStore(&certStore);

Of course ESP32 doesn't use BearSSL. So I'm trying to work out the equivalent.

It appears I have to use setCACertBundle(), but I can't find an example of how to do this. There's a lot of Platform.io stuff but I'm not using that.

I also see hints that the libraries may already contain a built-in bundle, which may be sufficient, but, again, I can't see how to use this.

What is the ESP32 equivalent of the ESP8266 code?

It doesn't necessarily have to load from a filesystem; I can programatically generate constants to include in my code (indeed that'd be easier in some respects).

2 Answers 2

1

How to use CA Cert Bundle in Arduino ESP32?

The process for using the ESP32 setCACertBundle() was described in Arduino Library README.md of WiFiClientSecure Library for Arduino ESP32 framework release 2.x. For some reason it has been reduced to a simple vague paragraph in release 3.x README.md when the WiFiCleintSecure library is re-factored to NetworkClientSecure library. Espressif lately like to do that and sometime break the backward compatibility or remove something useful from its Github without explanation.

Create a CA Cert Bundle

In order to use a cert bundle, first you would need to generate a cert bundle. You could generate it yourself, or get CA certificates extracted from Mozilla from here. The download Mozilla CA certificate store cacert.pem is in PEM format and is around 200KB uncompressed.

Esp-idf has a python utility for generating a binary version of CA bundle which is only around 64kB. Download the gen_crt_bundle.py (you might need to install python package cryptography with pip3 install cryptography before you can run the python script).

Run the python script with the following command line to generate the binary file:

python3 gen_crt_bundle.py -i cacert.pem

This should generated a file called x509_crt_bundle, make a directory in your project directory and move the created bundle into the directory.

mkdir data data/cert
mv x509_crt_bundle data/cert/

Setup PlatformIO

The code that I'm going to shown work for PlatformIO, but does not work for Arduino IDE (I don't know why and in fact I don't really use Arduino IDE these days so I never spent the time to find out why).

Assuming you had PlatfromIO installed and have created an Arduino project. Add build flag in platformio.ini:

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
board_build.embed_files = data/cert/x509_crt_bundle
monitor_speed = 115200

Call setCACertBundle() before making an secure connection

The code for using the setCACertBundle() is actually quite simple and similar to the usage of WiFiClientSecure example, with only two differences:

  1. add extern const uint8_t rootca_crt_bundle_start[] asm("_binary_data_cert_x509_crt_bundle_bin_start"); to your sketch;
  2. call client.setCACertBundle(rootca_crt_bundle_start); before making a client connection.

Here is the complete example sketch that I used for testing, it access the httpbin.org test site (you could try other sites) and httpbin server would return a response in json format.

#include <WiFiClientSecure.h>

const char* ssid     = "wifi ssid";
const char* password = "wifi password";

const char*  server = "httpbin.org";

extern const uint8_t rootca_crt_bundle_start[] asm("_binary_data_cert_x509_crt_bundle_bin_start");

WiFiClientSecure client;

void setup() {
  Serial.begin(115200);
  delay(100);

  Serial.print("Connecting to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(1000);
  }
  Serial.println("Connected");

  // client.setCACert(ISRG_Root_X1_CA);
  client.setCACertBundle(rootca_crt_bundle_start);
  // client.setCertificate(test_client_cert); // for client verification
  // client.setPrivateKey(test_client_key);  // for client verification

  Serial.println("\nStarting connection to server...");
  if (!client.connect(server, 443))
    Serial.println("Connection failed!");
  else {
    Serial.println("Connected to server!");
    // Make a HTTP request:
    client.printf("GET https://%s/get HTTP/1.1\n", server);
    client.printf("Host: %s\n", server);
    client.println("Connection: close");
    client.println();

    while (client.connected()) {
      String line = client.readStringUntil('\n');
      if (line == "\r") {
        Serial.println("headers received");
        break;
      }
    }
    // read the response body
    while (client.available()) {
      char c = client.read();
      Serial.write(c);
    }

    client.stop();
  }
}

void loop() {
  // do nothing
}

The screen capture shown the overall directory structure of platformIO setup as well as the response received from the test server (httpbin.org). enter image description here

For reference, here is the system setting of my PlatformIO show during the compilation.

PLATFORM: Espressif 32 (6.9.0) > Espressif ESP32 Dev Module
HARDWARE: ESP32 240MHz, 320KB RAM, 4MB Flash
DEBUG: Current (cmsis-dap) External (cmsis-dap, esp-bridge, esp-prog, iot-bus-jtag, jlink, minimodule, olimex-arm-usb-ocd, olimex-arm-usb-ocd-h, olimex-arm-usb-tiny-h, olimex-jtag-tiny, tumpa)
PACKAGES: 
 - framework-arduinoespressif32 @ 3.20017.0 (2.0.17) 
 - tool-esptoolpy @ 1.40501.0 (4.5.1) 
 - tool-mkfatfs @ 2.0.1 
 - tool-mklittlefs @ 1.203.210628 (2.3) 
 - tool-mkspiffs @ 2.230.0 (2.30) 
 - toolchain-xtensa-esp32 @ 8.4.0+2021r2-patch5
5
  • This is all great, except the question says "There's a lot of Platform.io stuff but I'm not using that". I'm looking for something I can build with arduino-cli or (worst case) the Arduino IDE. In your solution what is rootca_crt_bundle_start? Is it the cert bundle itself or some sort of pointer into the flash filesystem? Commented Dec 11, 2024 at 12:42
  • It is a pointer to the binary CA cert bundle in data/cert Commented Dec 11, 2024 at 12:48
  • Let me know if you find something work for Arduino IDE. The second option described in the README that I linked should work for Arduino IDE, but It requires you to load the pen file from SPIFFS, but it will take up 200KB+ RAM space... Commented Dec 11, 2024 at 12:52
  • Unless I'm missing something, Store the bundle as a SPIFFS file, but then you have to load it into RAM in runtime and waste 64k of precious memory but neither the v2 nor v3 READMEs tell me what I then need to do with the bundle; do I just pass it directly to setCACertBundle() ? It seems everyone is using PlatformIO now, so I can't find any examples not using it!! Commented Dec 11, 2024 at 13:46
  • 1
    I think I worked it out. It didn't help that the data structures changed format in esp-idf and arduino-esp32 is using an older version! But it seems to work :-) Commented Dec 11, 2024 at 20:23
1

To use a cert collection with the ESP32 we need to provide the CAs in a specific format called a "bundle". This is a binary data structure with certs sorted in order and an "offset" header to allow for quick access to each cert.

Generating this object gets a litle complicated.

The Arduino ESP32 library v3.0.7 (latest version) is based on ESP-IDF 5.1.4

The format for a bundle in this is

  uint16_t num_certs
  uint16_t offset * num_certs
  <cert_data>

Note the offsets are 16bit so the complete bundle can't be more than 64k. Using https://curl.se/ca/cacert.pem it's too large (it generates a 69k bundle) so it's possible some CAs at the end of the list (in alphabetical order by CN) would not work. When providing a custom bundle ensure the generated file is under 64k.

So in esp-idf v5.4 the format has changed. It's now

  uint32_t offset * num_certs

The number of certs is determined by offset[0]/sizeof(uint32_t)

This means the gen_crt_bundle.py script in the master branch will not work with Arduino ESP32 library and you must use the one from the v5.1 branch.

Also it may mean that as the Arduino library updates to v5.4 or above then code will need changing.

To use a custom CA bundle we need to use the generator from v5.1; ie from

https://github.com/espressif/esp-idf/tree/release/v5.1/components/mbedtls/esp_crt_bundle

We can generate a bundle file with

python3 gen_crt_bundle.py -q -i *.pem

That will create x509_crt_bundle

This needs to be got into your program. This could be placed in SPIFFS or LittleFS and loaded into a variable at runtime, or we can embed it directly into our program as a const PROGMEM variable.

To embed it we can use a script similar to this:

eg

#!/bin/sh

F=x509_crt_bundle

echo 'static const uint8_t certs_bundle[] PROGMEM = {'
xxd -p $F | sed 's/\(..\)/0x\1,/g' | sed '$s/,$//'
echo '};'

s=`wc -c < $F`

echo "#define SIZE_OF_CERTS $s"

This will create output that looks like

static const uint8_t certs_bundle[] PROGMEM = {
0x00,0x8a,0x00,0x36,0x01,0x26,0x30,0x34,0x31,0x0b,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x46,0x52,0x31,0x12,0x30,0x10,0x06,0x03,0x55,0x04,0x0a,

....

0x4f,0x17,0x02,0x03,0x01,0x00,0x01

};
#define SIZE_OF_CERTS 64057

(Oh good; 64057 is below 65535 so all the offsets will be OK).

I called this output certs.h.

This can now be used

#include <WiFi.h>
#include <time.h>
#include <WiFiClientSecure.h>
#include "certs.h"

...
  WiFiClientSecure client;
  client.setCACertBundle(certs_bundle,SIZE_OF_CERTS);

Note this changed in 3.0.4 where you need to pass the size of the array; in 3.0.3 this would simply be client.setCACertBundle(certs_bundle) and would likely fail due to a bug.

Now esp-idf v5.1 has a cacrt_all.pem that is from Tue Jan 10 04:12:06 2023 GMT along with the DST ROOT CA X3 (cacrt_local.pem) which may no longer be needed but is for older letsencrypt cross-signing.

To use this built-in bundle that is embedded into libmbedtls.a requires a little bit of a kludge as well. As far as I can work it out,


extern const uint8_t x509_crt_imported_bundle_bin_start[] asm("_binary_x509_crt_bundle_start");
extern const uint8_t x509_crt_imported_bundle_bin_end[]   asm("_binary_x509_crt_bundle_end");

...
  client.setCACertBundle(x509_crt_imported_bundle_bin_start,x509_crt_imported_bundle_bin_end-x509_crt_imported_bundle_bin_start);

This allowed me to connect to servers with certs from common CAs (eg Amazon, Google, LetsEncrypt).

2
  • Glad that you solved this. Commented Dec 12, 2024 at 0:48
  • @hcheung Thanks for your answer; it helped lead me to the solution! Commented Dec 12, 2024 at 1:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.