From 178ba652f9e67f14f31227b51ba8c4437ba0be68 Mon Sep 17 00:00:00 2001 From: Zach Hoeken Date: Wed, 26 Nov 2025 21:55:39 -1000 Subject: [PATCH 1/2] added certificate bundle support --- README.md | 16 +++++++++ library.json | 2 +- library.properties | 2 +- src/esp32FOTA.cpp | 86 ++++++++++++++++++++++++++++++++++++---------- src/esp32FOTA.hpp | 5 +++ 5 files changed, 91 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8bdc025..b276447 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,22 @@ $ gzip -c esp32-fota-http-2.bin > esp32-fota-http-2.bin.gz ### Root Certificates +#### Certificate Bundles + +Arduino 3.x now supports bundled root certificates, which means 99% of sites (including github.com) will work over https and you don't need to maintain a custom certificate on your firmware. + +To enable this functionality, simply call ```esp32FOTA.useBundledCerts();``` during your setup. + +If you are using Platformio / PIOArduino, the certificates are not automatically bundled and you will need to download them from [CURL](https://curl.se/docs/caextract.html). + +Save that file to your project root directory and then add this line to your platformio.ini: + +```board_build.embed_txtfiles=ca_cert_bundle``` + +Make sure it is named exactly ca_cert_bundle with no extension and located in the top level of your project. + +#### Custom Certificates + Certificates and signatures can be stored in different places: any fs::FS filesystem or progmem as const char*. Filesystems: diff --git a/library.json b/library.json index fbf6d51..a56b4a6 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "esp32FOTA", - "version": "0.2.9", + "version": "0.3.0", "keywords": "firmware, OTA, Over The Air Updates, ArduinoOTA", "description": "Allows for firmware to be updated from a webserver, the device can check for updates at any time. Uses a simple JSON file to outline if a new firmware is avaiable.", "examples": "examples/*/*.ino", diff --git a/library.properties b/library.properties index 98e8870..d7807ef 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=esp32FOTA -version=0.2.9 +version=0.3.0 author=Chris Joyce maintainer=Chris Joyce sentence=A simple library for firmware OTA updates diff --git a/src/esp32FOTA.cpp b/src/esp32FOTA.cpp index 601383f..876f8ad 100644 --- a/src/esp32FOTA.cpp +++ b/src/esp32FOTA.cpp @@ -191,6 +191,7 @@ void esp32FOTA::setConfig( FOTAConfig_t cfg ) _cfg.signature_len = cfg.signature_len; _cfg.allow_reuse = cfg.allow_reuse; _cfg.use_http10 = cfg.use_http10; + _cfg.use_bundled_certs = cfg.use_bundled_certs; } @@ -239,7 +240,15 @@ void esp32FOTA::setupCryptoAssets() } - +/* Enable ESP-IDF bundled certs */ +void esp32FOTA::useBundledCerts(bool enable) +{ + _cfg.use_bundled_certs = enable; + if (enable) { + _cfg.unsafe = false; // strict + _cfg.root_ca = nullptr; // disable custom CA + } +} void esp32FOTA::handle() { if ( execHTTPcheck() ) { @@ -370,34 +379,75 @@ bool esp32FOTA::setupHTTP( const char* url ) _http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); _http.setReuse(_cfg.allow_reuse); _http.useHTTP10(_cfg.use_http10); - log_i("Connecting to: %s", url ); - if( String(url).startsWith("https") ) { - if (!_cfg.unsafe) { - if( !_cfg.root_ca ) { - log_e("A strict security context has been set but no RootCA was provided"); - return false; + + bool is_https = String(url).startsWith("https"); + + if (is_https) { + + bool https_initialized = false; + + /* ================================ + 1. Bundled ESP-IDF CA certificate + ================================ */ +#if ESP_ARDUINO_VERSION_MAJOR >= 3 + if (!https_initialized && _cfg.use_bundled_certs) { + __attribute__((weak)) extern const uint8_t ca_cert_bundle_start[] asm("_binary_ca_cert_bundle_start"); + __attribute__((weak)) extern const uint8_t ca_cert_bundle_end[] asm("_binary_ca_cert_bundle_end"); + + if (ca_cert_bundle_start != nullptr && ca_cert_bundle_end != nullptr) { + size_t bundle_size = ca_cert_bundle_end - ca_cert_bundle_start; + log_i("Using built-in ESP-IDF certificate bundle (%u bytes)", bundle_size); + + _client.setCACertBundle(ca_cert_bundle_start, bundle_size); + _http.begin(_client, url); + https_initialized = true; + } else { + log_w("Bundled certs requested, but CA bundle not linked. Falling back."); } - if( _cfg.root_ca->size() == 0 ) { - log_e("A strict security context has been set but an empty RootCA was provided"); + } +#endif + + /* ================================ + 2. Custom root CA certificate + ================================ */ + if (!https_initialized && !_cfg.unsafe && _cfg.root_ca) { + if (_cfg.root_ca->size() == 0) { + log_e("Strict HTTPS but empty RootCA provided"); return false; } rootcastr = _cfg.root_ca->get(); - if( !rootcastr ) { - log_e("Unable to get RootCA, aborting"); - log_e("rootcastr=%s", rootcastr); + if (!rootcastr) { + log_e("Strict HTTPS but failed to get RootCA contents"); return false; } - log_d("Loading root_ca.pem"); - _client.setCACert( rootcastr ); - } else { - // We're downloading from a secure URL, but we don't want to validate the root cert. + log_i("Using custom RootCA for TLS"); + _client.setCACert(rootcastr); + _http.begin(_client, url); + https_initialized = true; + } + + /* ================================ + 3. Insecure HTTPS + ================================ */ + if (!https_initialized && _cfg.unsafe) { + log_w("Insecure HTTPS enabled"); _client.setInsecure(); + _http.begin(_client, url); + https_initialized = true; } - _http.begin( _client, url ); + + /* ================================ + 4. No CA available (strict) + ================================ */ + if (!https_initialized) { + log_e("Strict HTTPS enabled but no CA source available (neither bundled nor custom)"); + return false; + } + } else { - _http.begin( url ); + _http.begin(url); } if( extraHTTPHeaders.size() > 0 ) { diff --git a/src/esp32FOTA.hpp b/src/esp32FOTA.hpp index 2ad05e6..d92e947 100644 --- a/src/esp32FOTA.hpp +++ b/src/esp32FOTA.hpp @@ -222,6 +222,7 @@ struct FOTAConfig_t size_t signature_len {FW_SIGNATURE_LENGTH}; bool allow_reuse { true }; bool use_http10 { false }; // Use HTTP 1.0 (WARNING: setting to 'true' disables chunked transfers) + bool use_bundled_certs { false }; // use built-in ESP-IDF CA bundle FOTAConfig_t() = default; }; @@ -287,6 +288,10 @@ class esp32FOTA void setCertFileSystem( fs::FS *cert_filesystem = nullptr ); // this is passed to Update.onProgress() + + // enable bundled CA certificates + void useBundledCerts(bool enable = true); + typedef std::function ProgressCallback_cb; // size_t progress, size_t size void setProgressCb(ProgressCallback_cb fn) { onOTAProgress = fn; } // callback setter From 110b971639460f710ed6f5f9bbcc7914215006f0 Mon Sep 17 00:00:00 2001 From: Zach Hoeken Date: Wed, 26 Nov 2025 22:06:26 -1000 Subject: [PATCH 2/2] Update README.md put code on its own line --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b276447..6cd1dec 100644 --- a/README.md +++ b/README.md @@ -332,7 +332,11 @@ $ gzip -c esp32-fota-http-2.bin > esp32-fota-http-2.bin.gz Arduino 3.x now supports bundled root certificates, which means 99% of sites (including github.com) will work over https and you don't need to maintain a custom certificate on your firmware. -To enable this functionality, simply call ```esp32FOTA.useBundledCerts();``` during your setup. +To enable this functionality, simply call this during your setup: + +```C++ +esp32FOTA.useBundledCerts(); +``` If you are using Platformio / PIOArduino, the certificates are not automatically bundled and you will need to download them from [CURL](https://curl.se/docs/caextract.html).