diff --git a/.github/templates/firmware.test-suite.json b/.github/templates/firmware.test-suite.json new file mode 100644 index 0000000..d472e05 --- /dev/null +++ b/.github/templates/firmware.test-suite.json @@ -0,0 +1,38 @@ +[{ + "type": "FIRMWARE_TYPE", + "version": 1, + "host": "FIRMWARE_HOST", + "port": FIRMWARE_PORT, + "bin": "FIRMWARE_PATH/1.nosecurity.ino.bin?raw=true" +}, +{ + "type": "FIRMWARE_TYPE", + "version": 2, + "host": "FIRMWARE_HOST", + "port": FIRMWARE_PORT, + "bin": "FIRMWARE_PATH/2.cert-in-spiffs.ino.bin?raw=true", + "spiffs": "FIRMWARE_PATH/2.cert-in-spiffs.spiffs.bin?raw=true" +}, +{ + "type": "FIRMWARE_TYPE", + "version": 3, + "host": "FIRMWARE_HOST", + "port": FIRMWARE_PORT, + "bin": "FIRMWARE_PATH/3.cert-in-progmem.ino.bin?raw=true" +}, +{ + "type": "FIRMWARE_TYPE", + "version": 4, + "host": "FIRMWARE_HOST", + "port": FIRMWARE_PORT, + "bin": "FIRMWARE_PATH/4.cert-in-littlefs.ino.bin?raw=true", + "littlefs": "FIRMWARE_PATH/4.cert-in-littlefs.littlefs.bin?raw=true" +}, +{ + "type": "FIRMWARE_TYPE", + "version": 5, + "host": "FIRMWARE_HOST", + "port": FIRMWARE_PORT, + "bin": "FIRMWARE_PATH/5.final-stage.ino.bin?raw=true" +}] + diff --git a/.github/workflows/gen-test-suite.yml b/.github/workflows/gen-test-suite.yml new file mode 100644 index 0000000..30a26d2 --- /dev/null +++ b/.github/workflows/gen-test-suite.yml @@ -0,0 +1,186 @@ +name: Dispatchable build +on: + workflow_dispatch: + inputs: + firmware_type: + description: 'Firmware Type' + default: 'esp32-fota-http' + required: true + type: string + manifest_url: + description: 'Complete URL (where the JSON Manifest will be hosted)' + required: true + default: 'https://github.com/[user]/[repo]/raw/[branch]/[path to manifest]/firmware.json' + type: string + manifest_host: + description: 'Hostname (where the binaries will be hosted)' + required: true + default: 'github.com' + type: string + manifest_port: + description: 'Port (e.g 80 or 443)' + required: true + default: '443' + type: string + manifest_bin_path: + description: 'Path (no trailing slash)' + required: true + default: '/[user]/[repo]/blob/[branch]/[path to binaries]' + type: string + board_fqbn: + description: 'Board FQBN (for arduino-cli)' + required: true + default: 'esp32:esp32:esp32' + type: string + partition_scheme: + description: 'Partition scheme' + required: true + type: choice + options: + - default + - min_spiffs + - large_spiffs + default: 'default' + +jobs: + + matrix_build: + + name: ${{ matrix.sketch }} + runs-on: ubuntu-latest + + strategy: + matrix: + + sketch: + - 1.nosecurity.ino + - 2.cert-in-spiffs.ino + - 3.cert-in-progmem.ino + - 4.cert-in-littlefs.ino + - 5.final-stage.ino + + steps: + + - name: Checkout + uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - name: Inject Manifest URL + run: | + full_ino_path=`find /home/runner/work/ | grep "${{ matrix.sketch }}"` + echo -e "#define FOTA_URL \"${{ inputs.manifest_url }}\"\n$(cat $full_ino_path)" > $full_ino_path + + + - name: Inject TLS Cert if applicable + if: (inputs.manifest_port == '443' || inputs.manifest_port == '4433') + run: | + full_rootca_path=`find /home/runner/work/ | grep "progmem/root_ca.h"` + ssl_host="${{ inputs.manifest_host }}" + prefix="const char* root_ca =\\" + suffix="\"\";" + echo $prefix > $full_rootca_path + openssl s_client -showcerts -connect $ssl_host:${{ inputs.manifest_port }} > $full_rootca_path + echo $suffix >> $full_rootca_path + cat $full_rootca_path + + + - name: ${{ matrix.sketch }} + uses: ArminJo/arduino-test-compile@v3 + with: + platform-url: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_dev_index.json + arduino-board-fqbn: ${{ inputs.board_fqbn }}:PartitionScheme=${{inputs.partition_scheme}} + required-libraries: ArduinoJson + extra-arduino-lib-install-args: --no-deps + extra-arduino-cli-args: "--warnings default " # see https://github.com/ArminJo/arduino-test-compile/issues/28 + sketch-names: ${{ matrix.sketch }} + set-build-path: true + # build-properties: ${{ toJson(matrix.build-properties) }} + # arduino-platform: esp32:esp32@${{ matrix.sdk-version }} + # extra-arduino-cli-args: ${{ matrix.extra-arduino-cli-args }} + # debug-install: true + + - name: Save compiled binaries + run: | + mkdir -p /home/runner/builds + ls /home/runner/builds -la + cd /home/runner/work/ + full_ino_bin_path=`find . | grep "build/${{ matrix.sketch }}.bin"` + echo "Bin path: $full_ino_bin_path" + cp "$full_ino_bin_path" /home/runner/builds/${{ matrix.sketch }}.bin + + - name: Prepare data folder if applicable + if: (inputs.manifest_port == '443' || inputs.manifest_port == '4433') + run: | + full_rootca_path=`find /home/runner/work/ | grep "anyFS/data/root_ca.pem"` + ssl_host="${{ inputs.manifest_host }}" + openssl s_client -showcerts -connect $ssl_host:${{ inputs.manifest_port }} $full_rootca_path + cat $full_rootca_path + + - name: Create filesystem images + run: | + # ooh that's dirty :-) + default_size=0x170000 + large_spiffs_size=0x6F0000 + min_spiffs_size=0x30000 + + mkspiffs_esp32=~/.arduino15/packages/esp32*/tools/mkspiffs/*/mkspiffs + mklittlefs_esp32=~/.arduino15/packages/esp32*/tools/mklittlefs/*/mklittlefs + echo "mkspiffs path: $mkspiffs_esp32" + echo "mklittlefs path: $mklittlefs_esp32" + $mkspiffs_esp32 -c examples/anyFS/data/ -p 256 -b 4096 -s $${{inputs.partition_scheme}}_size /home/runner/builds/2.cert-in-spiffs.spiffs.bin + $mklittlefs_esp32 -c examples/anyFS/data/ -p 256 -b 4096 -s $${{inputs.partition_scheme}}_size /home/runner/builds/4.cert-in-littlefs.littlefs.bin + ls /home/runner/builds -la + + - name: Upload artifact for Stage ${{ matrix.test-stage }} + uses: actions/upload-artifact@v2 + with: + name: TestSuite + path: | + /home/runner/builds/** + + + post_build: + name: Gather Artefacts + runs-on: ubuntu-latest + # wait until matrix jobs are all finished + needs: matrix_build + steps: + + - name: Checkout + uses: actions/checkout@v2 + + - name: Create artifacts dir + run: mkdir -p /home/runner/builds + + #- name: Download artifacts + #uses: actions/download-artifact@v2 + #with: + #path: /home/runner/builds + + - name: Polulate JSON Manifest + run: | + firmware_json_path=/home/runner/builds/firmware.json + cp .github/templates/firmware.test-suite.json $firmware_json_path + sed -i -e 's/FIRMWARE_TYPE/${{ inputs.firmware_type }}/g' $firmware_json_path + sed -i -e 's/FIRMWARE_HOST/${{ inputs.manifest_host }}/g' $firmware_json_path + sed -i -e 's/FIRMWARE_PORT/${{ inputs.manifest_port }}/g' $firmware_json_path + sed -i -e 's~FIRMWARE_PATH~${{ inputs.manifest_bin_path }}~g' $firmware_json_path + cat $firmware_json_path + + - name: Update artifacts with new JSON Manifest + uses: actions/upload-artifact@v2 + with: + name: TestSuite + path: | + /home/runner/builds/** + + + - name: Release check + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + /home/runner/builds/** + + diff --git a/.github/workflows/platformio.yml b/.github/workflows/platformio.yml index d576f3d..4b1ee92 100644 --- a/.github/workflows/platformio.yml +++ b/.github/workflows/platformio.yml @@ -8,6 +8,7 @@ on: pull_request: branches: - master + workflow_dispatch: jobs: build: @@ -30,4 +31,4 @@ jobs: - name: Build test run: | pio run - + diff --git a/.gitignore b/.gitignore index 4ff737d..2cec19e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vscode/ _Notes .pio +test/stage1 diff --git a/README.md b/README.md index 22af45d..f8e392a 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,15 @@ A simple library to add support for Over-The-Air (OTA) updates to your project. - [x] Web update (requires web server) - [x] Batch firmware sync -- [x] Force firmware update [issues 8] -- [x] https support [#26][i26] ( Thanks to @fbambusi ) -- [x] Signature check of downloaded firmware-image [issue 65] +- [x] Force firmware update [#8] +- [x] https support [#26] ( Thanks to @fbambusi ) +- [x] Signature check of downloaded firmware-image [#65] - [x] https or https - [x] Signature verification - [x] Semantic versioning support -- [ ] Checking for update via bin headers [issue 15] +- [x] Any fs::FS support (SPIFFS/LITTLEFS/SD) for cert/signature storage [#79], [#74], [#91], [#92] (thanks to all participants) +- [x] SPIFFS/LittleFS partition Update [#25], [#47], [#60], [#92] (thanks to all participants) +- [ ] Checking for update via bin headers [#15] ## How it works @@ -76,6 +78,46 @@ A single JSON file can provide information on multiple firmware types by combini ``` +A single JSON file can also contain several versions of a single firmware type: + +```json +[ + { + "type":"esp32-fota-http", + "version":"0.0.2", + "url":"http://192.168.0.100/fota/esp32-fota-0.0.2.bin" + }, + { + "type":"esp32-fota-http", + "version":"0.0.3", + "url":"http://192.168.0.100/fota/esp32-fota-0.0.3.bin" + } +] +``` + + +#### Filesystem image (spiffs/littlefs) + +Adding `spiffs` key to the JSON entry will end up with the filesystem being updated first, then the firmware. + +Obviously don't use the filesystem you're updating to store certificates needed by the update, spiffs partition +doesn't have redundancy like OTA0/OTA1 and won't recover from a failed update without a restart and format. + +```json +{ + "type": "esp32-fota-http", + "version": 2, + "host": "192.168.0.100", + "port": 80, + "bin": "/fota/esp32-fota-http-2.bin", + "spiffs": "/fota/default_spiffs.bin" +} +``` + +Other accepted keys for filesystems are `spiffs`, `littlefs` and `fatfs`. +Picking one or another doesn't make any difference yet. + + #### Firmware types Types are used to compare with the current loaded firmware, this is used to make sure that when loaded, the device will still do the intended job. @@ -135,6 +177,69 @@ void loop() delay(2000); } ``` + + +### Root Certificates + +Certificates and signatures can be stored in different places: any fs::FS filesystem or progmem as const char*. + +Filesystems: + +```C++ +CryptoFileAsset *MyRootCA = new CryptoFileAsset( "/root_ca.pem", &SPIFFS ); +``` + +```C++ +CryptoFileAsset *MyRootCA = new CryptoFileAsset( "/root_ca.pem", &LittleFS ); +``` + +```C++ +CryptoFileAsset *MyRootCA = new CryptoFileAsset( "/root_ca.pem", &SD ); +``` + +Progmem: + +```C++ +const char* root_ca = R"ROOT_CA( +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +)ROOT_CA"; + +CryptoMemAsset *MyRootCA = new CryptoMemAsset("Root CA", root_ca, strlen(root_ca)+1 ); +``` + +Then later in the `setup()`: + +```C++ +void setup() +{ + esp32FOTA.checkURL = "http://server/fota/fota.json"; + esp32FOTA.setRootCA( MyRootCA ); +} + +``` + + ### Verified images via signature You can now sign your firmware image with an RSA public/private key pair and have the ESP32 check if the signature is correct before @@ -162,18 +267,31 @@ Last step, create an SPIFFS partition with your `rsa_key.pub` in it. The OTA upd On the next update-check the ESP32 will download the `firmware.img` extract the first 512 bytes with the signature and check it together with the public key against the new image. If the signature check runs OK, it'll reset into the new firmware. -[issue 15]: https://github.com/chrisjoyce911/esp32FOTA/issues/15 -[issues 8]: https://github.com/chrisjoyce911/esp32FOTA/issues/8 -[issue 65]: https://github.com/chrisjoyce911/esp32FOTA/issues/65 + +[#8]: https://github.com/chrisjoyce911/esp32FOTA/issues/8 +[#15]: https://github.com/chrisjoyce911/esp32FOTA/issues/15 +[#25]: https://github.com/chrisjoyce911/esp32FOTA/issues/25 +[#26]: https://github.com/chrisjoyce911/esp32FOTA/issues/26 +[#60]: https://github.com/chrisjoyce911/esp32FOTA/issues/60 +[#65]: https://github.com/chrisjoyce911/esp32FOTA/issues/65 +[#74]: https://github.com/chrisjoyce911/esp32FOTA/issues/74 +[#47]: https://github.com/chrisjoyce911/esp32FOTA/pull/47 +[#79]: https://github.com/chrisjoyce911/esp32FOTA/pull/79 +[#91]: https://github.com/chrisjoyce911/esp32FOTA/pull/91 +[#92]: https://github.com/chrisjoyce911/esp32FOTA/pull/92 + + ### Libraries This relies on [semver.c by h2non](https://github.com/h2non/semver.c) for semantic versioning support. semver.c is licensed under [MIT](https://github.com/h2non/semver.c/blob/master/LICENSE). -### Thanks to +### Thanks to * @nuclearcat * @thinksilicon -* @nuclearcat -* @hpsaturn +* @nuclearcat +* @hpsaturn +* @tobozo + diff --git a/examples/anyFS/anyFS.ino b/examples/anyFS/anyFS.ino new file mode 100644 index 0000000..eb3b8c7 --- /dev/null +++ b/examples/anyFS/anyFS.ino @@ -0,0 +1,92 @@ +/** + esp32 firmware OTA + + Purpose: Perform an OTA update from a bin located on a webserver (HTTPS) + + Setup: + Step 1 : Set your WiFi (ssid & password) + Step 2 : set esp32fota() + Step 3 : Provide SPIFFS filesystem with root_ca.pem of your webserver + + Upload: + Step 1 : Menu > Sketch > Export Compiled Library. The bin file will be saved in the sketch folder (Menu > Sketch > Show Sketch folder) + Step 2 : Upload it to your webserver + Step 3 : Update your firmware JSON file ( see firwmareupdate ) + +*/ + +// declare filesystem first ! + +//#include +//#include +//#include +#include +//#include + +#include // fota pulls WiFi library + +CryptoFileAsset *MyRootCA = new CryptoFileAsset( "/root_ca.pem", &LittleFS ); + +//CryptoMemAsset *MyRSAKey = new CryptoMemAsset("RSA Public Key", rsa_key_pub, strlen(rsa_key_pub)+1 ); +//CryptoMemAsset *MyRootCA = new CryptoMemAsset("Certificates Chain", root_ca, strlen(root_ca)+1 ); + + +// Change to your WiFi credentials +const char *ssid = ""; +const char *password = ""; + +// esp32fota esp32fota("", , , ); +esp32FOTA esp32FOTA("esp32-fota-http", 1, false ); + +void setup_wifi() +{ + delay(10); + Serial.print("Connecting to WiFi "); + Serial.println( WiFi.macAddress() ); + //Serial.println(ssid); + + WiFi.begin(/*ssid, password*/); + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println(WiFi.localIP()); + + esp32FOTA.setRootCA( MyRootCA ); + +} + + +void setup() +{ + Serial.begin(115200); + // Provide filesystem with root_ca.pem to validate server certificate + if( ! LittleFS.begin( false ) ) { + Serial.println("LittleFS Mounting failed, aborting!"); + while(1) vTaskDelay(1); + } + // use this when more than one filesystem is used in the sketch + // esp32FOTA.setCertFileSystem( &SD ); + + esp32FOTA.checkURL = "http://server/fota/fota.json"; + + + setup_wifi(); +} + +void loop() +{ + + bool updatedNeeded = esp32FOTA.execHTTPcheck(); + if (updatedNeeded) + { + esp32FOTA.execOTA(); + } + + delay(20000); +} + diff --git a/examples/anyFS/data/root_ca.pem b/examples/anyFS/data/root_ca.pem new file mode 100644 index 0000000..342ecfe --- /dev/null +++ b/examples/anyFS/data/root_ca.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- diff --git a/examples/anyFS/test/1.nosecurity/1.nosecurity.ino b/examples/anyFS/test/1.nosecurity/1.nosecurity.ino new file mode 100644 index 0000000..55f5681 --- /dev/null +++ b/examples/anyFS/test/1.nosecurity/1.nosecurity.ino @@ -0,0 +1,90 @@ +/** + esp32 firmware OTA + + Purpose: Perform an OTA update to both firmware and filesystem from binaries located + on a webserver (HTTPS) without checking for certificate validity + + Usage: If the ESP32 had a previous successful WiFi connection, then no need to set the ssid/password + to run this sketch, the credentials are still cached :-) + Sketch 1 will FOTA to Sketch 2, then Sketch 3, and so on until all versions in firmware.json are + exhausted. + + +*/ + +#include + +// esp32fota settings +const int firmware_version = 1; +#if defined FOTA_URL + const char* fota_url = FOTA_URL; +#else + const char* fota_url = "https://github.com/chrisjoyce911/esp32FOTA/raw/tests/examples/anyFS/test/stage1/firmware.json"; +#endif +const char* firmware_name = "esp32-fota-http"; +const bool check_signature = false; +const bool disable_security = true; +// for debug only +const char* description = "Basic example with no security and no filesystem"; + +const char* fota_debug_fmt = R"DBG_FMT( + +***************** STAGE %i ***************** + + Description : %s + Firmware type : %s + Firmware version : %i + Signature check : %s + TLS Cert check : %s + +******************************************** + +)DBG_FMT"; + + +// esp32fota esp32fota("", , , ); +esp32FOTA esp32FOTA( String(firmware_name), firmware_version, check_signature, disable_security ); + +void setup_wifi() +{ + delay(10); + //Serial.print("Connecting to WiFi "); + //Serial.println( ssid ); + Serial.print("MAC Address "); + Serial.println( WiFi.macAddress() ); + + WiFi.begin(); // no WiFi creds in this demo :-) + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println(WiFi.localIP()); + +} + + +void setup() +{ + Serial.begin(115200); + Serial.printf( fota_debug_fmt, firmware_version, description, firmware_name, firmware_version, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + + esp32FOTA.checkURL = fota_url; + + setup_wifi(); +} + +void loop() +{ + + bool updatedNeeded = esp32FOTA.execHTTPcheck(); + if (updatedNeeded) + { + esp32FOTA.execOTA(); + } + + delay(20000); +} diff --git a/examples/anyFS/test/2.cert-in-spiffs/2.cert-in-spiffs.ino b/examples/anyFS/test/2.cert-in-spiffs/2.cert-in-spiffs.ino new file mode 100644 index 0000000..6454b00 --- /dev/null +++ b/examples/anyFS/test/2.cert-in-spiffs/2.cert-in-spiffs.ino @@ -0,0 +1,95 @@ +/** + esp32 firmware OTA + + Purpose: Perform an OTA update firmware from a bin located on a webserver (HTTPS) + while using filesystem to check for certificate validity + +*/ + +#include // include filesystem **before** esp32fota !! +#include + + +// esp32fota settings +const int firmware_version = 2; +#if defined FOTA_URL + const char* fota_url = FOTA_URL; +#else + const char* fota_url = "https://github.com/chrisjoyce911/esp32FOTA/raw/tests/examples/anyFS/test/stage1/firmware.json"; +#endif +const char* firmware_name = "esp32-fota-http"; +const bool check_signature = false; +const bool disable_security = false; +// for debug only +const char* description = "SPIFFS example with security"; + +const char* fota_debug_fmt = R"DBG_FMT( + +***************** STAGE %i ***************** + + Description : %s + Firmware type : %s + Firmware version : %i + Signature check : %s + TLS Cert check : %s + +******************************************** + +)DBG_FMT"; + +// esp32fota esp32fota("", , , ); +esp32FOTA esp32FOTA( String(firmware_name), firmware_version, check_signature, disable_security ); + +// create an abstraction of the root_ca file +CryptoFileAsset *GithubRootCA = new CryptoFileAsset( "/root_ca.pem", &SPIFFS ); + +void setup_wifi() +{ + delay(10); + Serial.print("MAC Address "); + Serial.println( WiFi.macAddress() ); + + WiFi.begin(); // no WiFi creds in this demo :-) + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println(WiFi.localIP()); +} + + +void setup() +{ + Serial.begin(115200); + Serial.printf( fota_debug_fmt, firmware_version, description, firmware_name, firmware_version, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + // Provide filesystem with root_ca.pem to validate server certificate + if( ! SPIFFS.begin( false ) ) { + Serial.println("SPIFFS Mounting failed, aborting!"); + while(1) vTaskDelay(1); + } + + esp32FOTA.checkURL = fota_url; + esp32FOTA.setRootCA( GithubRootCA ); + + setup_wifi(); + +} + +void loop() +{ + + bool updatedNeeded = esp32FOTA.execHTTPcheck(); + if (updatedNeeded) + { + esp32FOTA.execOTA(); + } + + delay(20000); +} + + + diff --git a/examples/anyFS/test/2.cert-in-spiffs/data/root_ca.pem b/examples/anyFS/test/2.cert-in-spiffs/data/root_ca.pem new file mode 100644 index 0000000..342ecfe --- /dev/null +++ b/examples/anyFS/test/2.cert-in-spiffs/data/root_ca.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- diff --git a/examples/anyFS/test/3.cert-in-progmem/3.cert-in-progmem.ino b/examples/anyFS/test/3.cert-in-progmem/3.cert-in-progmem.ino new file mode 100644 index 0000000..dcd3911 --- /dev/null +++ b/examples/anyFS/test/3.cert-in-progmem/3.cert-in-progmem.ino @@ -0,0 +1,91 @@ +/** + esp32 firmware OTA + + Purpose: Perform an OTA update to both firmware and filesystem from binaries located + on a webserver (HTTPS) while using progmem to check for certificate validity + + +*/ + +#include + +#include "root_ca.h" + +// esp32fota settings +const int firmware_version = 3; +#if defined FOTA_URL + const char* fota_url = FOTA_URL; +#else + const char* fota_url = "https://github.com/chrisjoyce911/esp32FOTA/raw/tests/examples/anyFS/test/stage1/firmware.json"; +#endif +const char* firmware_name = "esp32-fota-http"; +const bool check_signature = false; +const bool disable_security = false; +// for debug only +const char* description = "PROGMEM example with security"; + +const char* fota_debug_fmt = R"DBG_FMT( + +***************** STAGE %i ***************** + + Description : %s + Firmware type : %s + Firmware version : %i + Signature check : %s + TLS Cert check : %s + +******************************************** + +)DBG_FMT"; + +// esp32fota esp32fota("", , , ); +esp32FOTA esp32FOTA( String(firmware_name), firmware_version, check_signature, disable_security ); + +// create an abstraction of the root_ca file +CryptoMemAsset *GithubRootCA = new CryptoMemAsset("Root CA", /*github_*/root_ca, strlen(/*github_*/root_ca)+1 ); + +void setup_wifi() +{ + delay(10); + Serial.print("Connecting to WiFi "); + Serial.println( WiFi.macAddress() ); + //Serial.println(ssid); + + WiFi.begin(); // no WiFi creds in this demo :-) + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println(WiFi.localIP()); + + esp32FOTA.setRootCA( GithubRootCA ); + +} + + +void setup() +{ + Serial.begin(115200); + Serial.printf( fota_debug_fmt, firmware_version, description, firmware_name, firmware_version, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + + esp32FOTA.checkURL = fota_url; + + setup_wifi(); +} + +void loop() +{ + + bool updatedNeeded = esp32FOTA.execHTTPcheck(); + if (updatedNeeded) + { + esp32FOTA.execOTA(); + } + + delay(20000); +} + diff --git a/examples/anyFS/test/3.cert-in-progmem/root_ca.h b/examples/anyFS/test/3.cert-in-progmem/root_ca.h new file mode 100644 index 0000000..60b1c9f --- /dev/null +++ b/examples/anyFS/test/3.cert-in-progmem/root_ca.h @@ -0,0 +1,25 @@ + +const char* /*github_*/root_ca = R"ROOT_CA( +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +)ROOT_CA"; diff --git a/examples/anyFS/test/4.cert-in-littlefs/4.cert-in-littlefs.ino b/examples/anyFS/test/4.cert-in-littlefs/4.cert-in-littlefs.ino new file mode 100644 index 0000000..1230e49 --- /dev/null +++ b/examples/anyFS/test/4.cert-in-littlefs/4.cert-in-littlefs.ino @@ -0,0 +1,95 @@ +/** + esp32 firmware OTA + + Purpose: Perform an OTA update firmware from a bin located on a webserver (HTTPS) + while using filesystem to check for certificate validity + +*/ + +#include // include filesystem **before** esp32fota !! +#include + +// esp32fota settings +const int firmware_version = 4; +#if defined FOTA_URL + const char* fota_url = FOTA_URL; +#else + const char* fota_url = "https://github.com/chrisjoyce911/esp32FOTA/raw/tests/examples/anyFS/test/stage1/firmware.json"; +#endif +const char* firmware_name = "esp32-fota-http"; +const bool check_signature = false; +const bool disable_security = false; +// for debug only +const char* description = "LittleFS example with security"; + +const char* fota_debug_fmt = R"DBG_FMT( + +***************** STAGE %i ***************** + + Description : %s + Firmware type : %s + Firmware version : %i + Signature check : %s + TLS Cert check : %s + +******************************************** + +)DBG_FMT"; + +// esp32fota esp32fota("", , , ); +esp32FOTA esp32FOTA( String(firmware_name), firmware_version, check_signature, disable_security ); + +// create an abstraction of the root_ca file +CryptoFileAsset *GithubRootCA = new CryptoFileAsset( "/root_ca.pem", &LittleFS ); + +void setup_wifi() +{ + delay(10); + + Serial.print("MAC Address "); + Serial.println( WiFi.macAddress() ); + + WiFi.begin(); // no WiFi creds in this demo :-) + + while (WiFi.status() != WL_CONNECTED) + { + delay(500); + Serial.print("."); + } + + Serial.println(""); + Serial.println(WiFi.localIP()); +} + + +void setup() +{ + Serial.begin(115200); + Serial.printf( fota_debug_fmt, firmware_version, description, firmware_name, firmware_version, check_signature?"Enabled":"Disabled", disable_security?"Disabled":"Enabled" ); + // Provide filesystem with root_ca.pem to validate server certificate + if( ! LittleFS.begin( false ) ) { + Serial.println("LittleFS Mounting failed, aborting!"); + while(1) vTaskDelay(1); + } + + esp32FOTA.checkURL = fota_url; + esp32FOTA.setRootCA( GithubRootCA ); + + setup_wifi(); + +} + +void loop() +{ + + bool updatedNeeded = esp32FOTA.execHTTPcheck(); + if (updatedNeeded) + { + esp32FOTA.execOTA(); + } + + delay(20000); +} + + + diff --git a/examples/anyFS/test/4.cert-in-littlefs/data/root_ca.pem b/examples/anyFS/test/4.cert-in-littlefs/data/root_ca.pem new file mode 100644 index 0000000..342ecfe --- /dev/null +++ b/examples/anyFS/test/4.cert-in-littlefs/data/root_ca.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- diff --git a/examples/anyFS/test/5.final-stage/5.final-stage.ino b/examples/anyFS/test/5.final-stage/5.final-stage.ino new file mode 100644 index 0000000..b064943 --- /dev/null +++ b/examples/anyFS/test/5.final-stage/5.final-stage.ino @@ -0,0 +1,18 @@ + + +void setup() +{ + Serial.begin( 115200 ); + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("**************************"); + Serial.println("Test suite COMPLETE :-)"); + Serial.println(); + Serial.println(); +} + +void loop() +{ + +} diff --git a/library.json b/library.json index d05552e..2c98e57 100644 --- a/library.json +++ b/library.json @@ -1,24 +1,24 @@ -{ - "name": "esp32FOTA", - "version": "0.1.6", - "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", - "repository": { - "type": "git", - "url": "https://github.com/chrisjoyce911/esp32FOTA.git" - }, - "authors": [ - { - "name": "Chris Joyce", - "email": "chris@joyce.id.au", - "url": "https://github.com/chrisjoyce911", - "maintainer": true - } - ], - "frameworks": "arduino", - "platforms": [ - "esp32", - "espressif32" - ] -} \ No newline at end of file +{ + "name": "esp32FOTA", + "version": "0.2.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", + "repository": { + "type": "git", + "url": "https://github.com/chrisjoyce911/esp32FOTA.git" + }, + "authors": [ + { + "name": "Chris Joyce", + "email": "chris@joyce.id.au", + "url": "https://github.com/chrisjoyce911", + "maintainer": true + } + ], + "frameworks": "arduino", + "platforms": [ + "esp32", + "espressif32" + ] +} diff --git a/library.properties b/library.properties index 9523bf2..ca8fc9a 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=esp32FOTA -version=0.1.6 +version=0.2.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 cb0b9db..727a391 100644 --- a/src/esp32fota.cpp +++ b/src/esp32fota.cpp @@ -8,93 +8,154 @@ Author: Moritz Meintker Remarks: Re-written/removed a bunch of functions around HTTPS. The library is now URL-agnostic. This means if you provide an https://-URL it will - use the root_ca.pem (needs to be provided via SPIFFS) to verify the - server certificate and then download the ressource through an encrypted - connection unless you set the allow_insecure_https option. + use the root_ca.pem (needs to be provided via PROGMEM/SPIFFS/LittleFS or SD) + to verify the server certificate and then download the ressource through an + encrypted connection unless you set the allow_insecure_https option. Otherwise it will just use plain HTTP which will still offer to sign your firmware image. + + Date: 2022-09-12 + Author: tobozo + Changes: + - Abstracted away filesystem + - Refactored some code blocks + - Added spiffs/littlefs/fatfs updatability + - Made crypto assets (pub key, rootca) loadable from multiple sources + Roadmap: + - Firmware/FlashFS update order (SPIFFS/LittleFS first or last?) + - Archive support for gz/targz formats + - firmware.gz + spiffs.gz in manifest + - bundle.tar.gz [ firmware + filesystem ] in manifest + - Update from Stream (e.g deported update via SD, http or gzupdater) + */ #include "esp32fota.h" -#include -#include -#include -#include -#include "ArduinoJson.h" -#include -#include - #include "mbedtls/pk.h" #include "mbedtls/md.h" #include "mbedtls/md_internal.h" #include "esp_ota_ops.h" -#include +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + +// Filesystem helper for signature check and pem validation +// This is abstracted away to allow storage alternatives such +// as PROGMEM, SD, SPIFFS, LittleFS or FatFS + +bool CryptoFileAsset::fs_read_file() +{ + File file = fs->open( path ); + size_t fsize = file.size(); + // if( file->size() > ESP.getFreeHeap() ) return false; + if( !file ) { + log_e( "Failed to open %s for reading", path ); + return false; + } + contents = ""; // make sure the output bucket is empty + while( file.available() ) { + contents.push_back( file.read() ); + } + file.close(); + return contents.size()>0 && fsize==contents.size(); +} + + +size_t CryptoFileAsset::size() +{ + if( len > 0 ) { // already stored, no need to access filesystem + return len; + } + if( fs ) { // fetch file contents + if( ! fs_read_file() ) { + log_w("Invalid contents!"); + return 0; + } + len = contents.size(); + } else { + log_e("No filesystem was set for %s!", path); + return 0; + } + return len; +} + + + -esp32FOTA::esp32FOTA(String firmwareType, int firmwareVersion, boolean validate, boolean allow_insecure_https) +esp32FOTA::esp32FOTA(String firmwareType, int firmwareVersion, bool validate, bool allow_insecure_https) { _firmwareType = firmwareType; _firmwareVersion = semver_t{firmwareVersion}; _check_sig = validate; _allow_insecure_https = allow_insecure_https; useDeviceID = false; - - char version_no[256] = {'\0'}; // If we are passed firmwareVersion as an int, we're assuming it's a major version - semver_render(&_firmwareVersion, version_no); - log_i("Current firmware version: %s", version_no ); - + setupCryptoAssets(); + debugSemVer("Current firmware version", &_firmwareVersion ); } -esp32FOTA::esp32FOTA(String firmwareType, String firmwareSemanticVersion, boolean validate, boolean allow_insecure_https) + +esp32FOTA::esp32FOTA(String firmwareType, String firmwareSemanticVersion, bool validate, bool allow_insecure_https) { if (semver_parse(firmwareSemanticVersion.c_str(), &_firmwareVersion)) { log_e( "Invalid semver string %s passed to constructor. Defaulting to 0", firmwareSemanticVersion.c_str() ); _firmwareVersion = semver_t {0}; } - _firmwareType = firmwareType; _check_sig = validate; _allow_insecure_https = allow_insecure_https; useDeviceID = false; - - char version_no[256] = {'\0'}; - semver_render(&_firmwareVersion, version_no); - log_i("Current firmware version: %s", version_no ); - + setupCryptoAssets(); + debugSemVer("Current firmware version", &_firmwareVersion ); } -esp32FOTA::~esp32FOTA() { +esp32FOTA::~esp32FOTA() +{ semver_free(&_firmwareVersion); semver_free(&_payloadVersion); } -// Check file signature + +void esp32FOTA::setCertFileSystem( fs::FS *cert_filesystem ) +{ + _fs = cert_filesystem; + setupCryptoAssets(); +} + + +// Used for legacy behaviour when SPIFFS and RootCa/PubKey had default values +// New recommended method is to use setPubKey() and setRootCA() with CryptoMemAsset ot CryptoFileAsset objects. +void esp32FOTA::setupCryptoAssets() +{ + if( _fs ) { + PubKey = (CryptoAsset*)(new CryptoFileAsset( rsa_key_pub_default_path, _fs )); + RootCA = (CryptoAsset*)(new CryptoFileAsset( root_ca_pem_default_path, _fs )); + } +} + + + +// SHA-Verify the OTA partition after it's been written // https://techtutorialsx.com/2018/05/10/esp32-arduino-mbed-tls-using-the-sha-256-algorithm/ // https://github.com/ARMmbed/mbedtls/blob/development/programs/pkey/rsa_verify.c -bool esp32FOTA::validate_sig( unsigned char *signature, uint32_t firmware_size ) { +bool esp32FOTA::validate_sig( unsigned char *signature, uint32_t firmware_size ) +{ int ret = 1; + size_t pubkeylen = PubKey ? PubKey->size()+1 : 0; + const char* pubkeystr = PubKey->get(); + + if( pubkeylen <= 1 ) { + return false; + } + mbedtls_pk_context pk; mbedtls_md_context_t rsa; + mbedtls_pk_init( &pk ); - { // Open RSA public key: - File public_key_file = SPIFFS.open( "/rsa_key.pub" ); - if( !public_key_file ) { - log_e( "Failed to open rsa_key.pub for reading" ); - return false; - } - std::string public_key = ""; - while( public_key_file.available() ){ - public_key.push_back( public_key_file.read() ); - } - public_key_file.close(); - - mbedtls_pk_init( &pk ); - if( ( ret = mbedtls_pk_parse_public_key( &pk, (unsigned char *)public_key.c_str(), public_key.length() +1 ) ) != 0 ) { - log_e( "Reading public key failed\n ! mbedtls_pk_parse_public_key %d\n\n", ret ); - return false; - } + if( ( ret = mbedtls_pk_parse_public_key( &pk, (const unsigned char*)pubkeystr, pubkeylen ) ) != 0 ) { + log_e( "Reading public key failed\n ! mbedtls_pk_parse_public_key %d\n\n", ret ); + return false; } if( !mbedtls_pk_can_do( &pk, MBEDTLS_PK_RSA ) ) { @@ -102,9 +163,8 @@ bool esp32FOTA::validate_sig( unsigned char *signature, uint32_t firmware_size ) return false; } - const esp_partition_t* partition = esp_ota_get_next_update_partition(NULL); - + if( !partition ) { log_e( "Could not find update partition!" ); return false; @@ -114,7 +174,7 @@ bool esp32FOTA::validate_sig( unsigned char *signature, uint32_t firmware_size ) mbedtls_md_init( &rsa ); mbedtls_md_setup( &rsa, mdinfo, 0 ); mbedtls_md_starts( &rsa ); - + int bytestoread = SPI_FLASH_SEC_SIZE; int bytesread = 0; int size = firmware_size; @@ -124,41 +184,44 @@ bool esp32FOTA::validate_sig( unsigned char *signature, uint32_t firmware_size ) log_e( "malloc failed" ); return false; } + //Serial.printf( "Reading partition (%i sectors, sec_size: %i)\r\n", size, bytestoread ); while( bytestoread > 0 ) { - //Serial.printf( "Left: %i (%i) \r", size, bytestoread ); - - if( ESP.partitionRead( partition, bytesread, (uint32_t*)_buffer, bytestoread ) ) { - // Debug output for the purpose of comparing with file - /*for( int i = 0; i < bytestoread; i++ ) { - if( ( i % 16 ) == 0 ) { - Serial.printf( "\r\n0x%08x\t", i + bytesread ); - } - Serial.printf( "%02x ", (uint8_t*)_buffer[i] ); - }*/ - - mbedtls_md_update( &rsa, (uint8_t*)_buffer, bytestoread ); - - bytesread = bytesread + bytestoread; - size = size - bytestoread; - - if( size <= SPI_FLASH_SEC_SIZE ) { - bytestoread = size; + //Serial.printf( "Left: %i (%i) \r", size, bytestoread ); + + if( ESP.partitionRead( partition, bytesread, (uint32_t*)_buffer, bytestoread ) ) { + // Debug output for the purpose of comparing with file + /*for( int i = 0; i < bytestoread; i++ ) { + if( ( i % 16 ) == 0 ) { + Serial.printf( "\r\n0x%08x\t", i + bytesread ); + } + Serial.printf( "%02x ", (uint8_t*)_buffer[i] ); + }*/ + + mbedtls_md_update( &rsa, (uint8_t*)_buffer, bytestoread ); + + bytesread = bytesread + bytestoread; + size = size - bytestoread; + + if( size <= SPI_FLASH_SEC_SIZE ) { + bytestoread = size; + } + } else { + log_e( "partitionRead failed!" ); + return false; } - } else { - log_e( "partitionRead failed!" ); - return false; - } } + free( _buffer ); unsigned char *hash = (unsigned char*)malloc( mdinfo->size ); + if(!hash){ + log_e( "malloc failed" ); + return false; + } mbedtls_md_finish( &rsa, hash ); - ret = mbedtls_pk_verify( &pk, MBEDTLS_MD_SHA256, - hash, mdinfo->size, - (unsigned char*)signature, 512 - ); + ret = mbedtls_pk_verify( &pk, MBEDTLS_MD_SHA256, hash, mdinfo->size, (unsigned char*)signature, 512 ); free( hash ); mbedtls_md_free( &rsa ); @@ -166,69 +229,114 @@ bool esp32FOTA::validate_sig( unsigned char *signature, uint32_t firmware_size ) if( ret == 0 ) { return true; } - // overwrite the frist few bytes so this partition won't boot! + + // validation failed, overwrite the first few bytes so this partition won't boot! ESP.partitionEraseRange( partition, 0, ENCRYPTED_BLOCK_SIZE); return false; } + + // OTA Logic void esp32FOTA::execOTA() { + if( _flashFileSystemUrl != "" ) { // handle the spiffs partition first + if( _fs ) { // Possible risk of overwriting certs and signatures, cancel flashing! + log_e("Cowardly refusing to overwrite U_SPIFFS. Use setCertFileSystem(nullptr) along with setPubKey()/setCAPem() to enable this feature."); + } else { + log_i("Will update U_SPIFFS"); + execOTA( U_SPIFFS, false ); + } + } else { + log_i("This update is for U_FLASH only"); + } + // handle the application partition and restart on success + execOTA( U_FLASH, true ); +} + + +void esp32FOTA::execOTA( int partition, bool restart_after ) +{ + String UpdateURL = ""; + + switch( partition ) { + case U_SPIFFS: // spiffs/littlefs/fatfs partition + if( _flashFileSystemUrl == "" ) { + log_i("[SKIP] No spiffs/littlefs/fatfs partition was speficied"); + return; + } + UpdateURL = _flashFileSystemUrl; + break; + case U_FLASH: // app partition (default) + default: + partition = U_FLASH; + UpdateURL = _firmwareUrl; + break; + } + int contentLength = 0; bool isValidContentType = false; + const char* rootcastr = nullptr; HTTPClient http; WiFiClientSecure client; //http.setConnectTimeout( 1000 ); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - - log_i("Connecting to: %s\r\n", _firmwareUrl.c_str() ); - if( _firmwareUrl.substring( 0, 5 ) == "https" ) { + + log_i("Connecting to: %s\r\n", UpdateURL.c_str() ); + if( UpdateURL.substring( 0, 5 ) == "https" ) { if (!_allow_insecure_https) { - // If we're downloading from secure URL use WifiClientSecure instead - // and provide the root_ca.pem log_i( "Loading root_ca.pem" ); - //WiFiClientSecure client; - File root_ca_file = SPIFFS.open( "/root_ca.pem" ); - if( !root_ca_file ) { - log_e( "Could not open root_ca.pem" ); + if( !RootCA || RootCA->size() == 0 ) { + log_e("A strict security context has been set but no RootCA was provided"); return; } - { - std::string root_ca = ""; - while( root_ca_file.available() ){ - root_ca.push_back( root_ca_file.read() ); - } - root_ca_file.close(); - http.begin( _firmwareUrl, root_ca.c_str() ); + rootcastr = RootCA->get(); + if( !rootcastr ) { + log_e("Unable to get RootCA, aborting"); + return; } + client.setCACert( rootcastr ); } else { // We're downloading from a secure URL, but we don't want to validate the root cert. client.setInsecure(); - http.begin(client, _firmwareUrl); } + http.begin( client, UpdateURL ); } else { - http.begin( _firmwareUrl ); + http.begin( UpdateURL ); } + if( extraHTTPHeaders.size() > 0 ) { + // add custom headers provided by user e.g. http.addHeader("Authorization", "Basic " + auth) + for( const auto &header : extraHTTPHeaders ) { + http.addHeader(header.first, header.second); + } + } + + // TODO: add more watched headers e.g. Authorization: Signature keyId="rsa-key-1",algorithm="rsa-sha256",signature="Base64(RSA-SHA256(signing string))" const char* get_headers[] = { "Content-Length", "Content-type" }; http.collectHeaders( get_headers, 2 ); int httpCode = http.GET(); - + if( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) { contentLength = http.header( "Content-Length" ).toInt(); String contentType = http.header( "Content-type" ); if( contentType == "application/octet-stream" ) { isValidContentType = true; - + } else if( contentType == "application/gzip" ) { + // was gzipped by the server, needs decompression + // TODO: use gzStreamUpdater + } else if( contentType == "application/tar+gz" ) { + // was packaged and compressed, may contain more than one file + // TODO: use tarGzStreamUpdater } } else { // Connect to webserver failed // May be try? // Probably a choppy network? - log_i( "Connection to %s failed. Please check your setup", _firmwareUrl ); + log_i( "Connection to %s failed with httpCode %i. Please check your setup", UpdateURL, httpCode ); // retry?? // execOTA(); } @@ -237,103 +345,112 @@ void esp32FOTA::execOTA() log_i("contentLength : %i, isValidContentType : %s", contentLength, String(isValidContentType)); // check contentLength and content type - if( contentLength && isValidContentType ) { - WiFiClient& client = http.getStream(); + if( !contentLength || !isValidContentType ) { + Serial.println("There was no content in the http response"); + http.end(); + return; + } - if( _check_sig ) { - // If firmware is signed, extract signature and decrease content-length by 512 bytes for signature - contentLength = contentLength - 512; - } - // Check if there is enough to OTA Update - bool canBegin = Update.begin(contentLength); - - // If yes, begin - if( canBegin ) { - unsigned char signature[512]; - if( _check_sig ) { - client.readBytes( signature, 512 ); - } - Serial.println("Begin OTA. This may take 2 - 5 mins to complete. Things might be quiet for a while.. Patience!"); - // No activity would appear on the Serial monitor - // So be patient. This may take 2 - 5mins to complete - size_t written = Update.writeStream(client); - - if (written == contentLength) - { - Serial.println("Written : " + String(written) + " successfully"); - } - else - { - Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?"); - // retry?? - // execOTA(); - } + Stream& stream = http.getStream(); - if (Update.end()) - { - if( _check_sig ) { - if( !validate_sig( signature, contentLength ) ) { - - const esp_partition_t* partition = esp_ota_get_running_partition(); - esp_ota_set_boot_partition( partition ); - - log_e( "Signature check failed!" ); - http.end(); - ESP.restart(); - return; - } else { - log_i( "Signature OK" ); - } - } - Serial.println("OTA done!"); - if (Update.isFinished()) - { - Serial.println("Update successfully completed. Rebooting."); - http.end(); - ESP.restart(); - } - else - { - Serial.println("Update not finished? Something went wrong!"); - } - } - else - { - Serial.println("Error Occurred. Error #: " + String(Update.getError())); + if( _check_sig ) { + // If firmware is signed, extract signature and decrease content-length by 512 bytes for signature + contentLength = contentLength - 512; + } + // Check if there is enough available space on the partition to perform the Update + bool canBegin = Update.begin( contentLength, partition ); + + if( !canBegin ) { + Serial.println("Not enough space to begin OTA"); + http.end(); + return; + } + + if( _ota_progress_callback ) { + Update.onProgress( _ota_progress_callback ); + } else { + Update.onProgress( [](size_t progress, size_t size) { + if( progress == size || progress == 0 ) Serial.println(); + Serial.print("."); + }); + } + + unsigned char signature[512]; + if( _check_sig ) { + stream.readBytes( signature, 512 ); + } + Serial.printf("Begin %s OTA. This may take 2 - 5 mins to complete. Things might be quiet for a while.. Patience!", partition==U_FLASH?"Firmware":"Filesystem"); + // No activity would appear on the Serial monitor + // So be patient. This may take 2 - 5mins to complete + size_t written = Update.writeStream( stream ); + + if (written == contentLength) { + Serial.println("Written : " + String(written) + " successfully"); + } else { + Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?"); + // retry?? + // execOTA(); + } + + if (!Update.end()) { + Serial.println("Error Occurred. Error #: " + String(Update.getError())); + return; + } + + if( _check_sig ) { // check signature + if( !validate_sig( signature, contentLength ) ) { + if( partition == U_FLASH ) { // partition was marked as bootable, but signature validation failed, undo! + const esp_partition_t* partition = esp_ota_get_running_partition(); + esp_ota_set_boot_partition( partition ); + } else if( partition == U_SPIFFS ) { // bummer! + // SPIFFS/LittleFS partition was already overwritten and unlike U_FLASH (has OTA0/OTA1) this can't be rolled back. + // TODO: onValidationFail decision tree with [erase-partition, mark-unsafe, keep-as-is] } - } - else - { - // not enough space to begin OTA - // Understand the partitions and - // space availability - Serial.println("Not enough space to begin OTA"); + Serial.println( "Signature check failed!" ); http.end(); + if( restart_after ) { + Serial.println("Rebooting."); + ESP.restart(); + } + return; + } else { + log_i( "Signature OK" ); } } - else - { - log_e("There was no content in the response"); + Serial.println("OTA done!"); + if (Update.isFinished()) { + Serial.println("Update successfully completed."); http.end(); + if( restart_after ) { + Serial.println("Rebooting."); + ESP.restart(); + } + return; + } else { + Serial.println("Update not finished? Something went wrong!"); } } -bool esp32FOTA::checkJSONManifest(JsonVariant JSONDocument) { - if(strcmp(JSONDocument["type"].as(), _firmwareType.c_str()) != 0) { - log_i("Payload type in manifest %s doesn't match current firmware %s", JSONDocument["type"].as(), _firmwareType.c_str() ); +bool esp32FOTA::checkJSONManifest(JsonVariant doc) +{ + if(strcmp(doc["type"].as(), _firmwareType.c_str()) != 0) { + log_i("Payload type in manifest %s doesn't match current firmware %s", doc["type"].as(), _firmwareType.c_str() ); log_i("Doesn't match type: %s", _firmwareType.c_str() ); return false; // Move to the next entry in the manifest } - log_i("Payload type in manifest %s matches current firmware %s", JSONDocument["type"].as(), _firmwareType.c_str() ); + log_i("Payload type in manifest %s matches current firmware %s", doc["type"].as(), _firmwareType.c_str() ); semver_free(&_payloadVersion); - if(JSONDocument["version"].is()) { - log_i("JSON version: %d (int)", JSONDocument["version"].as()); - _payloadVersion = semver_t {JSONDocument["version"].as()}; - } else if (JSONDocument["version"].is()) { - log_i("JSON version: %s (semver)", JSONDocument["version"].as() ); - if (semver_parse(JSONDocument["version"].as(), &_payloadVersion)) { + + if(doc["version"].is()) { + uint16_t v = doc["version"].as(); + log_i("JSON version: %d (int)", v); + _payloadVersion = semver_t {v}; + } else if (doc["version"].is()) { + const char* c = doc["version"].as(); + log_i("JSON version: %s (semver)", c ); + if (semver_parse(c, &_payloadVersion)) { log_e( "Invalid semver string received in manifest. Defaulting to 0" ); _payloadVersion = semver_t {0}; } @@ -342,28 +459,53 @@ bool esp32FOTA::checkJSONManifest(JsonVariant JSONDocument) { _payloadVersion = semver_t {0}; } - char version_no[256] = {'\0'}; - semver_render(&_payloadVersion, version_no); - log_i("Payload firmware version: %s", version_no ); - + debugSemVer("Payload firmware version", &_payloadVersion ); + + // Memoize some values to help with the decision tree + bool has_url = doc.containsKey("url") && doc["url"].is(); + bool has_firmware = doc.containsKey("bin") && doc["bin"].is(); + bool has_hostname = doc.containsKey("host") && doc["host"].is(); + bool has_port = doc.containsKey("port") && doc["port"].is(); + uint16_t portnum = has_port ? doc["port"].as() : 0; + bool has_tls = has_port ? (portnum == 443 || portnum == 4433) : false; + bool has_spiffs = doc.containsKey("spiffs") && doc["spiffs"].is(); + bool has_littlefs = doc.containsKey("littlefs") && doc["littlefs"].is(); + bool has_fatfs = doc.containsKey("fatfs") && doc["fatfs"].is(); + bool has_filesystem = has_littlefs || has_spiffs || has_fatfs; + + String protocol = has_tls ? "https" : "http"; + String flashFSPath = + has_filesystem + ? ( + has_littlefs + ? doc["littlefs"].as() + : has_spiffs + ? doc["spiffs"].as() + : doc["fatfs"].as() + ) + : ""; + + log_i("JSON manifest provided keys: url=%s, host: %s, port: %s, bin: %s, fs: [%s]", + has_url?"true":"false", + has_hostname?"true":"false", + has_port?"true":"false", + has_firmware?"true":"false", + flashFSPath.c_str() + ); - if(JSONDocument["url"].is()) { - // We were provided a complete URL in the JSON manifest - use it - _firmwareUrl = JSONDocument["url"].as(); - if(JSONDocument["host"].is()) // If the manifest provides both, warn the user + if( has_url ) { // Basic scenario: a complete URL was provided in the JSON manifest, all other keys will be ignored + _firmwareUrl = doc["url"].as(); + if( has_hostname ) { // If the manifest provides both, warn the user log_w("Manifest provides both url and host - Using URL"); - } else if (JSONDocument["host"].is() && JSONDocument["port"].is() && JSONDocument["bin"].is()){ - // We were provided host/port/bin format - Build the URL - if( JSONDocument["port"].as() == 443 || JSONDocument["port"].as() == 4433 ) - _firmwareUrl = String( "https://"); - else - _firmwareUrl = String( "http://" ); - - _firmwareUrl += JSONDocument["host"].as() + ":" + String( JSONDocument["port"].as() ) + JSONDocument["bin"].as(); - - } else { - // JSON was malformed - no firmware target was provided - log_e("JSON manifest was missing both 'url' and 'host'/'port'/'bin' keys"); + } + } else if( has_firmware && has_hostname && has_port ) { // Precise scenario: Hostname, Port and Firmware Path were provided + _firmwareUrl = protocol + "://" + doc["host"].as() + ":" + String( portnum ) + doc["bin"].as(); + if( has_filesystem ) { // More complex scenario: the manifest also provides a [spiffs, littlefs or fatfs] partition + _flashFileSystemUrl = protocol + "://" + doc["host"].as() + ":" + String( portnum ) + flashFSPath; + } + } else { // JSON was malformed - no firmware target was provided + log_e("JSON manifest was missing one of the required keys :(" ); + serializeJsonPretty(doc, Serial); return false; } @@ -373,17 +515,16 @@ bool esp32FOTA::checkJSONManifest(JsonVariant JSONDocument) { return false; } + bool esp32FOTA::execHTTPcheck() { String useURL; + const char* rootcastr = nullptr; - if (useDeviceID) - { + if (useDeviceID) { // String deviceID = getDeviceID() ; useURL = checkURL + "?id=" + getDeviceID(); - } - else - { + } else { useURL = checkURL; } @@ -400,70 +541,75 @@ bool esp32FOTA::execHTTPcheck() if( useURL.substring( 0, 5 ) == "https" ) { if (!_allow_insecure_https) { - // If the checkURL is https load the root-CA and connect with that - log_i( "Loading root_ca.pem" ); - File root_ca_file = SPIFFS.open( "/root_ca.pem" ); - if( !root_ca_file ) { - log_e( "Could not open root_ca.pem" ); + if( !RootCA || RootCA->size() == 0 ) { + log_e("A strict security context has been set but no RootCA was provided"); return false; } - { - std::string root_ca = ""; - while( root_ca_file.available() ){ - root_ca.push_back( root_ca_file.read() ); - } - root_ca_file.close(); - http.begin( useURL, root_ca.c_str() ); + rootcastr = RootCA->get(); + if( !rootcastr ) { + log_e("Unable to get RootCA, aborting"); + return false; } + log_i( "Loading root_ca.pem" ); + client.setCACert( rootcastr ); } else { // We're downloading from a secure port, but we don't want to validate the root cert. client.setInsecure(); - http.begin(client, useURL); } + http.begin(client, useURL); } else { http.begin(useURL); //Specify the URL } + + if( extraHTTPHeaders.size() > 0 ) { + // add custom headers provided by user e.g. http.addHeader("Authorization", "Basic " + auth) + for( const auto &header : extraHTTPHeaders ) { + http.addHeader(header.first, header.second); + } + } + int httpCode = http.GET(); //Make the request - if( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) { //Check is a file was returned + // only handle 200/301, fail on everything else + if( httpCode != HTTP_CODE_OK && httpCode != HTTP_CODE_MOVED_PERMANENTLY ) { + log_e("Error on HTTP request (httpCode=%i)", httpCode); + http.end(); + return false; + } - String payload = http.getString(); + String payload = http.getString(); - int str_len = payload.length() + 1; - char JSONMessage[str_len]; - payload.toCharArray(JSONMessage, str_len); + // TODO: use payload.length() to speculate on JSONResult buffer size + #define JSON_FW_BUFF_SIZE 2048 + DynamicJsonDocument JSONResult( JSON_FW_BUFF_SIZE ); - DynamicJsonDocument JSONResult(2048); - DeserializationError err = deserializeJson(JSONResult, JSONMessage); + DeserializationError err = deserializeJson( JSONResult, payload.c_str() ); - http.end(); // We're done with HTTP - free the resources + http.end(); // We're done with HTTP - free the resources - if (err) { //Check for errors in parsing - log_e("Parsing failed"); - return false; - } + if (err) { // Check for errors in parsing, or JSON length may exceed buffer size + log_e("JSON Parsing failed (err #%d, in=%d bytes, buff=%d bytes):\n%s\n", err, payload.length(), JSON_FW_BUFF_SIZE, payload.c_str() ); + return false; + } - if (JSONResult.is()) { - // We already received an array of multiple firmware types - JsonArray arr = JSONResult.as(); - for (JsonVariant JSONDocument : arr) { - if(checkJSONManifest(JSONDocument)) { - return true; - } - } - } else if (JSONResult.is()) { - if(checkJSONManifest(JSONResult.as())) + if (JSONResult.is()) { + // Although improbable given the size on JSONResult buffer, we already received an array of multiple firmware types + JsonArray arr = JSONResult.as(); + for (JsonVariant JSONDocument : arr) { + if(checkJSONManifest(JSONDocument)) { + // TODO: filter "highest vs next" version number for JSON with only one firmware type but several version numbers return true; + } } - - return false; // We didn't get a hit against the above, return false - } else { - log_e("Error on HTTP request"); - http.end(); - return false; + } else if (JSONResult.is()) { + if(checkJSONManifest(JSONResult.as())) + return true; } + + return false; // We didn't get a hit against the above, return false } + String esp32FOTA::getDeviceID() { char deviceid[21]; @@ -474,34 +620,38 @@ String esp32FOTA::getDeviceID() return thisID; } + // Force a firmware update regardless on current version -void esp32FOTA::forceUpdate(String firmwareURL, boolean validate ) +void esp32FOTA::forceUpdate(String firmwareURL, bool validate ) { _firmwareUrl = firmwareURL; - _check_sig = validate; + _check_sig = validate; execOTA(); } -void esp32FOTA::forceUpdate(String firmwareHost, uint16_t firmwarePort, String firmwarePath, boolean validate ) + +void esp32FOTA::forceUpdate(String firmwareHost, uint16_t firmwarePort, String firmwarePath, bool validate ) { String firmwareURL; - if( firmwarePort == 443 || firmwarePort == 4433 ) + if( firmwarePort == 443 || firmwarePort == 4433 ) { firmwareURL = String( "https://"); - else + } else { firmwareURL = String( "http://" ); + } firmwareURL += firmwareHost + ":" + String( firmwarePort ) + firmwarePath; forceUpdate(firmwareURL, validate); } -void esp32FOTA::forceUpdate(boolean validate ) + +void esp32FOTA::forceUpdate(bool validate ) { // Forces an update from a manifest, ignoring the version check if(!execHTTPcheck()) { if (!_firmwareUrl) { // execHTTPcheck returns false if either the manifest is malformed or if the version isn't - // an upgrade. If _firmwareUrl isn't set, however, we can't force an upgrade. + // an upgrade. If _firmwareUrl isn't set, however, we can't force an upgrade. log_e("forceUpdate called, but unable to get _firmwareUrl from manifest via execHTTPcheck."); return; } @@ -519,6 +669,16 @@ int esp32FOTA::getPayloadVersion(){ return _payloadVersion.major; } + void esp32FOTA::getPayloadVersion(char * version_string){ semver_render(&_payloadVersion, version_string); } + + +void esp32FOTA::debugSemVer( const char* label, semver_t* version ) { + char version_no[256] = {'\0'}; + semver_render(version, version_no); + log_i("%s: %s", label, version_no ); +} + +#pragma GCC diagnostic pop diff --git a/src/esp32fota.h b/src/esp32fota.h index 6052b20..c672977 100644 --- a/src/esp32fota.h +++ b/src/esp32fota.h @@ -8,30 +8,126 @@ Author: Moritz Meintker Remarks: Re-written/removed a bunch of functions around HTTPS. The library is now URL-agnostic. This means if you provide an https://-URL it will - use the root_ca.pem (needs to be provided via SPIFFS) to verify the - server certificate and then download the ressource through an encrypted - connection. + use the root_ca.pem (needs to be provided via PROGMEM/SPIFFS/LittleFS or SD) + to verify the server certificate and then download the ressource through an + encrypted connection unless you set the allow_insecure_https option. Otherwise it will just use plain HTTP which will still offer to sign your firmware image. + + Date: 2022-09-12 + Author: tobozo + Changes: + - Abstracted away filesystem + - Refactored some code blocks + - Added spiffs/littlefs/fatfs updatability + - Made crypto assets (pub key, rootca) loadable from multiple sources + Roadmap: + - Firmware/FlashFS update order (SPIFFS/LittleFS first or last?) + - Archive support for gz/targz formats + - firmware.gz + spiffs.gz in manifest + - bundle.tar.gz [ firmware + filesystem ] in manifest + - Update from Stream (e.g deported update via SD, http or gzupdater) */ #ifndef esp32fota_h #define esp32fota_h -#include +extern "C" { + #include "semver/semver.h" +} + +#include +#include +#include +#include #include -#include "semver/semver.h" +#include + +// inherit includes from sketch, detect SPIFFS first for legacy support +#if __has_include() || defined _SPIFFS_H_ + #pragma message "Using SPIFFS for certificate validation" + #include + #define FOTA_FS &SPIFFS +#elif __has_include() || defined _LiffleFS_H_ + #pragma message "Using LittleFS for certificate validation" + #include + #define FOTA_FS &LittleFS +#elif __has_include() || defined _SD_H_ + #pragma message "Using SD for certificate validation" + #include + #define FOTA_FS &SD +#elif __has_include() || defined _SD_MMC_H_ + #pragma message "Using SD_MMC for certificate validation" + #include + #define FOTA_FS &SD_MMC +#elif defined _LIFFLEFS_H_ // older externally linked, hard to identify and unsupported versions of SPIFFS + #pragma message "this version of LittleFS is unsupported, use #include instead, if using platformio add LittleFS(esp32)@^2.0.0 to lib_deps" +#elif defined _PSRAMFS_H_ + #pragma message "Using PSRamFS for certificate validation" + #include + #define FOTA_FS &PSRamFS +#else + // #pragma message "No filesystem provided, certificate validation will be unavailable (hint: include SD, SPIFFS or LittleFS before including this library)" + #define FOTA_FS nullptr +#endif + + +// Filesystem/memory helper for signature check and pem validation. +// This is abstracted away to allow storage alternatives such as +// PROGMEM, SD, SPIFFS, LittleFS or FatFS +// Intended to be used by esp32FOTA.setPubKey() and esp32FOTA.setRootCA() +class CryptoAsset +{ +public: + virtual size_t size() = 0; + virtual const char* get() = 0; +}; + +class CryptoFileAsset : public CryptoAsset +{ +public: + CryptoFileAsset( const char* _path, fs::FS* _fs ) : path(_path), fs(_fs), contents(""), len(0) { } + size_t size(); + const char* get() { return contents.c_str(); } +private: + const char* path; + fs::FS* fs; + std::string contents; + size_t len; + bool fs_read_file(/* fs::FS* fs, const char* path, std::string *out */); +}; + +class CryptoMemAsset : public CryptoAsset +{ +public: + CryptoMemAsset( const char* _name, const char* _bytes, size_t _len ) : name(_name), bytes(_bytes), len(_len) { } + size_t size() { return len; }; + const char* get() { return bytes; } +private: + const char* name; + const char* bytes; + size_t len; +}; + +// Main Class class esp32FOTA { public: - esp32FOTA(String firwmareType, int firwmareVersion, boolean validate = false, boolean allow_insecure_https = false ); - esp32FOTA(String firwmareType, String firmwareSemanticVersion, boolean validate = false, boolean allow_insecure_https = false ); + esp32FOTA(String firwmareType, int firwmareVersion, bool validate = false, bool allow_insecure_https = false ); + esp32FOTA(String firwmareType, String firmwareSemanticVersion, bool validate = false, bool allow_insecure_https = false ); ~esp32FOTA(); - void forceUpdate(String firmwareHost, uint16_t firmwarePort, String firmwarePath, boolean validate ); - void forceUpdate(String firmwareURL, boolean validate ); - void forceUpdate(boolean validate ); + + void setCertFileSystem( fs::FS *cert_filesystem = nullptr ); + + template void setPubKey( T* asset ) { PubKey = (CryptoAsset*)asset; _check_sig = true; } + template void setRootCA( T* asset ) { RootCA = (CryptoAsset*)asset; _allow_insecure_https = false; } + + void forceUpdate(String firmwareHost, uint16_t firmwarePort, String firmwarePath, bool validate ); + void forceUpdate(String firmwareURL, bool validate ); + void forceUpdate(bool validate ); void execOTA(); + void execOTA( int partition, bool restart_after = true ); bool execHTTPcheck(); int getPayloadVersion(); void getPayloadVersion(char * version_string); @@ -39,15 +135,41 @@ class esp32FOTA String checkURL; bool validate_sig( unsigned char *signature, uint32_t firmware_size ); + // this is passed to Update.onProgress() + typedef std::function ProgressCallback_cb; + void setProgressCb(ProgressCallback_cb fn) { _ota_progress_callback = fn; } + + // use this to set "Authorization: Basic" or other specific headers to be sent with the queries + void setExtraHTTPHeader( String name, String value ) { extraHTTPHeaders[name] = value; } + private: String getDeviceID(); String _firmwareType; - semver_t _firmwareVersion = {0}; - semver_t _payloadVersion = {0}; + semver_t _firmwareVersion = semver_t(); + semver_t _payloadVersion = semver_t(); String _firmwareUrl; - boolean _check_sig; - boolean _allow_insecure_https; + String _flashFileSystemUrl; + bool _check_sig; + bool _allow_insecure_https; bool checkJSONManifest(JsonVariant JSONDocument); + void debugSemVer( const char* label, semver_t* version ); + + fs::FS *_fs = FOTA_FS; // default filesystem for certificate validation + // This is kept for legacy behaviour, use setPubKey() and setRootCA() with + // CryptoMemAsset ot CryptoFileAsset instead + const char* rsa_key_pub_default_path = "/rsa_key.pub"; + const char* root_ca_pem_default_path = "/root_ca.pem"; + + CryptoAsset *PubKey = nullptr; + CryptoAsset *RootCA = nullptr; + + void setupCryptoAssets(); + + // custom progress callback provided by user + ProgressCallback_cb _ota_progress_callback; + + // this holds the extra http headers defined by the user + std::map extraHTTPHeaders; }; diff --git a/src/semver/semver.c b/src/semver/semver.c index 77a11ba..5cae0db 100644 --- a/src/semver/semver.c +++ b/src/semver/semver.c @@ -499,7 +499,7 @@ semver_free (semver_t *x) { */ static void -concat_num (char * str, int x, char * sep) { +concat_num (char * str, int x, const char * sep) { char buf[SLICE_SIZE] = {0}; if (sep == NULL) sprintf(buf, "%d", x); else sprintf(buf, "%s%d", sep, x); @@ -507,7 +507,7 @@ concat_num (char * str, int x, char * sep) { } static void -concat_char (char * str, char * x, char * sep) { +concat_char (char * str, char * x, const char * sep) { char buf[SLICE_SIZE] = {0}; sprintf(buf, "%s%s", sep, x); strcat(str, buf);