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).