diff --git a/.github/workflows/build_py_tools.yml b/.github/workflows/build_py_tools.yml index e22d8df5eff..8e32d5c2582 100644 --- a/.github/workflows/build_py_tools.yml +++ b/.github/workflows/build_py_tools.yml @@ -8,6 +8,7 @@ on: - "tools/espota.py" - "tools/gen_esp32part.py" - "tools/gen_insights_package.py" + - "tools/bin_signing.py" permissions: contents: write @@ -44,6 +45,7 @@ jobs: tools/espota.py tools/gen_esp32part.py tools/gen_insights_package.py + tools/bin_signing.py - name: List all changed files shell: bash @@ -108,7 +110,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pyinstaller requests + pip install pyinstaller requests cryptography - name: Build with PyInstaller shell: bash diff --git a/CMakeLists.txt b/CMakeLists.txt index d9b295dfa70..546c39c5c29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -229,7 +229,8 @@ set(ARDUINO_LIBRARY_Ticker_SRCS libraries/Ticker/src/Ticker.cpp) set(ARDUINO_LIBRARY_Update_SRCS libraries/Update/src/Updater.cpp - libraries/Update/src/HttpsOTAUpdate.cpp) + libraries/Update/src/HttpsOTAUpdate.cpp + libraries/Update/src/Updater_Signing.cpp) set(ARDUINO_LIBRARY_USB_SRCS libraries/USB/src/USBHID.cpp diff --git a/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino b/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino index 934789a52bf..4dba9351ebf 100644 --- a/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino +++ b/libraries/ArduinoOTA/examples/BasicOTA/BasicOTA.ino @@ -38,9 +38,6 @@ void setup() { // Hostname defaults to esp3232-[MAC] // ArduinoOTA.setHostname("myesp32"); - // No authentication by default - // ArduinoOTA.setPassword("admin"); - // Password can be set with plain text (will be hashed internally) // The authentication uses PBKDF2-HMAC-SHA256 with 10,000 iterations // ArduinoOTA.setPassword("admin"); diff --git a/libraries/ArduinoOTA/examples/SignedOTA/README.md b/libraries/ArduinoOTA/examples/SignedOTA/README.md new file mode 100644 index 00000000000..0cab5de93a7 --- /dev/null +++ b/libraries/ArduinoOTA/examples/SignedOTA/README.md @@ -0,0 +1,314 @@ +# SignedOTA - Secure OTA Updates with Signature Verification + +This example demonstrates how to perform secure OTA updates with cryptographic signature verification using the ArduinoOTA library. + +## Overview + +**SignedOTA** adds an extra layer of security to Arduino OTA updates by requiring all firmware to be cryptographically signed with your private key. This protects against: + +- ✅ Unauthorized firmware updates +- ✅ Man-in-the-middle attacks +- ✅ Compromised networks +- ✅ Firmware tampering +- ✅ Supply chain attacks + +Even if an attacker gains access to your network, they **cannot** install unsigned firmware on your devices. + +## Features + +- **RSA & ECDSA Support**: RSA-2048/3072/4096 and ECDSA-P256/P384 +- **Multiple Hash Algorithms**: SHA-256, SHA-384, SHA-512 +- **Arduino IDE Compatible**: Works with standard Arduino OTA workflow +- **Optional Password Protection**: Add password authentication in addition to signature verification +- **Easy Integration**: Just a few lines of code + +## Requirements + +- **ESP32 Arduino Core 3.3.0+** +- **Python 3.6+** with `cryptography` library +- **OTA-capable partition scheme** (e.g., "Minimal SPIFFS (1.9MB APP with OTA)") + +## Quick Start Guide + +### 1. Generate Cryptographic Keys + +```bash +# Navigate to Arduino ESP32 tools directory +cd /tools + +# Install Python dependencies +pip install cryptography + +# Generate RSA-2048 key pair (recommended) +python bin_signing.py --generate-key rsa-2048 --out private_key.pem + +# Extract public key +python bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +``` + +**⚠️ IMPORTANT: Keep `private_key.pem` secure! Anyone with this key can sign firmware for your devices.** + +### 2. Setup the Example + +1. Copy `public_key.h` (generated in step 1) to this sketch directory +2. Open `SignedOTA.ino` in Arduino IDE +3. Configure WiFi credentials: + ```cpp + const char *ssid = "YourWiFiSSID"; + const char *password = "YourWiFiPassword"; + ``` +4. Select appropriate partition scheme: + - **Tools → Partition Scheme → "Minimal SPIFFS (1.9MB APP with OTA)"** + +### 3. Upload Initial Firmware + +1. Connect your ESP32 via USB +2. Upload the sketch normally +3. Open Serial Monitor (115200 baud) +4. Note the device IP address + +### 4. Build & Sign Firmware for OTA Update Example + +**Option A: Using Arduino IDE** + +```bash +# Export compiled binary +# In Arduino IDE: Sketch → Export Compiled Binary + +# Sign the firmware +cd /tools +python bin_signing.py \ + --bin /path/to/SignedOTA.ino.bin \ + --key private_key.pem \ + --out firmware_signed.bin +``` + +**Option B: Using arduino-cli** + +```bash +# Compile and export +arduino-cli compile --fqbn esp32:esp32:esp32 --export-binaries SignedOTA + +# Sign the firmware +cd /tools +python bin_signing.py \ + --bin build/esp32.esp32.esp32/SignedOTA.ino.bin \ + --key private_key.pem \ + --out firmware_signed.bin +``` + +### 5. Upload Signed Firmware via OTA + +Upload the signed firmware using `espota.py`: + +```bash +python /tools/espota.py -i -f firmware_signed.bin +``` + +The device will automatically: +1. Receive the signed firmware (firmware + signature) +2. Hash only the firmware portion +3. Verify the signature +4. Install if valid, reject if invalid + +**Note**: You can also use the Update library's `Signed_OTA_Update` example for HTTP-based OTA updates. + +## Configuration Options + +### Hash Algorithms + +Choose one in `SignedOTA.ino`: + +```cpp +#define USE_SHA256 // Default, fastest +// #define USE_SHA384 +// #define USE_SHA512 +``` + +**Must match** the `--hash` parameter when signing: + +```bash +python bin_signing.py --bin firmware.bin --key private.pem --out signed.bin --hash sha256 +``` + +### Signature Algorithms + +Choose one in `SignedOTA.ino`: + +```cpp +#define USE_RSA // For RSA keys +// #define USE_ECDSA // For ECDSA keys +``` + +### Optional Password Protection + +Add password authentication **in addition to** signature verification: + +```cpp +const char *ota_password = "yourpassword"; // Set password +// const char *ota_password = nullptr; // Disable password +``` + +## How It Works + +``` +┌─────────────────┐ +│ Build Firmware │ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ Sign Firmware │ ← Uses your private key +│ (bin_signing) │ +└────────┬────────┘ + │ + ▼ +┌─────────────────────────┐ +│ firmware_signed.bin │ +│ [firmware][signature] │ +└────────┬────────────────┘ + │ + ▼ OTA Upload +┌─────────────────────────┐ +│ ESP32 Device │ +│ ┌──────────────────┐ │ +│ │ Verify Signature │ │ ← Uses your public key +│ │ ✓ or ✗ │ │ +│ └──────────────────┘ │ +│ │ │ +│ ✓ Valid? │ +│ ├─ Yes: Install │ +│ └─ No: Reject │ +└─────────────────────────┘ +``` + +## Troubleshooting + +### "Begin Failed" Error + +**Cause**: Signature verification setup failed, or partition scheme issue + +**Solutions**: +1. Check partition scheme (use "Minimal SPIFFS (1.9MB APP with OTA)") +2. Verify `public_key.h` is in the sketch directory +3. Check hash and signature algorithm match your key type + +### "End Failed" Error + +**Cause**: Signature verification failed + +**Solutions**: +1. Ensure firmware was signed with the **correct private key** +2. Verify hash algorithm matches (SHA-256, SHA-384, SHA-512) +3. Check firmware wasn't corrupted during signing/transfer +4. Confirm you signed the **correct** `.bin` file + +### "Receive Failed" Error + +**Cause**: Network timeout or connection issue + +**Solutions**: +1. Check WiFi signal strength +2. Ensure device is reachable on the network +3. Try increasing timeout: `ArduinoOTA.setTimeout(5000)` + +### Upload Fails + +**Issue**: OTA upload fails or times out + +**Solutions**: +1. Verify device is on the same network +2. Check firewall settings aren't blocking port 3232 +3. Ensure WiFi signal strength is adequate +4. If using password protection, ensure the password is correct +5. Try: `python /tools/espota.py -i -f firmware_signed.bin -d` + +## Security Considerations + +### Best Practices + +✅ **Keep private key secure**: Never commit to git, store encrypted +✅ **Use strong keys**: RSA-2048+ or ECDSA-P256+ +✅ **Use HTTPS when possible**: For additional transport security +✅ **Add password authentication**: Extra layer of protection +✅ **Rotate keys periodically**: Generate new keys every 1-2 years + +### What This Protects Against + +- ✅ Unsigned firmware installation +- ✅ Firmware signed with wrong key +- ✅ Tampered/corrupted firmware +- ✅ Network-based attacks (when combined with password) + +### What This Does NOT Protect Against + +- ❌ Physical access (USB flashing still works) +- ❌ Downgrade attacks (no version checking by default) +- ❌ Replay attacks (no timestamp/nonce by default) +- ❌ Key compromise (if private key is stolen) + +### Additional Security + +For production deployments, consider: + +1. **Add version checking** to prevent downgrades +2. **Add timestamp validation** to prevent replay attacks +3. **Use secure boot** for additional protection +4. **Store keys in HSM** or secure key management system +5. **Implement key rotation** mechanism + +## Advanced Usage + +### Using ECDSA Instead of RSA + +ECDSA keys are smaller and faster: + +```bash +# Generate ECDSA-P256 key +python bin_signing.py --generate-key ecdsa-p256 --out private_key.pem +python bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +``` + +In `SignedOTA.ino`: + +```cpp +#define USE_SHA256 +#define USE_ECDSA // Instead of USE_RSA +``` + +### Using SHA-384 or SHA-512 + +For higher security: + +```bash +# Sign with SHA-384 +python bin_signing.py --bin firmware.bin --key private.pem --out signed.bin --hash sha384 +``` + +In `SignedOTA.ino`: + +```cpp +#define USE_SHA384 // Instead of USE_SHA256 +#define USE_RSA +``` + +### Custom Partition Label + +To update a specific partition: + +```cpp +ArduinoOTA.setPartitionLabel("my_partition"); +``` + +## Support + +For issues and questions: + +- Update Library README: `libraries/Update/README.md` +- ESP32 Arduino Core: https://github.com/espressif/arduino-esp32 +- Forum: https://github.com/espressif/arduino-esp32/discussions + +## License + +This library is part of the Arduino-ESP32 project and is licensed under the Apache License 2.0. + diff --git a/libraries/ArduinoOTA/examples/SignedOTA/SignedOTA.ino b/libraries/ArduinoOTA/examples/SignedOTA/SignedOTA.ino new file mode 100644 index 00000000000..c1c04517c27 --- /dev/null +++ b/libraries/ArduinoOTA/examples/SignedOTA/SignedOTA.ino @@ -0,0 +1,205 @@ +/* + * SignedOTA Example - Secure OTA Updates with Signature Verification + * + * This example demonstrates how to perform OTA updates with cryptographic + * signature verification using ArduinoOTA library. + * + * IMPORTANT: This example requires firmware to be signed with bin_signing.py + * + * NOTE: Signature verification support is enabled via the build_opt.h file + * in this directory. + * + * Setup: + * 1. Generate keys: + * python /tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem + * python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem + * + * 2. Copy public_key.h to this sketch directory + * + * 3. Configure WiFi credentials below + * + * 4. Upload this sketch to your device + * + * 5. Build your firmware and sign it: + * arduino-cli compile --fqbn esp32:esp32:esp32 --export-binaries SignedOTA + * python /tools/bin_signing.py --bin build/.bin --key private_key.pem --out firmware_signed.bin + * + * 6. Upload signed firmware using espota.py or Arduino IDE (after modifying espota.py to handle signed binaries) + * python /tools/espota.py -i -f firmware_signed.bin + * + * For more information, see the Update library's Signed_OTA_Update example + * and README.md in the Update library folder. + * + * Created by lucasssvaz + */ + +#include +#include +#include +#include + +// Include your public key (generated with bin_signing.py) +#include "public_key.h" + +// ==================== CONFIGURATION ==================== + +// WiFi credentials +const char *ssid = ".........."; +const char *password = ".........."; + +// Optional: Set a password for OTA authentication +// This is in ADDITION to signature verification +// ArduinoOTA password protects the OTA connection +// Signature verification ensures firmware authenticity +const char *ota_password = nullptr; // Set to nullptr to disable, or "yourpassword" to enable + +// Choose hash algorithm (must match what you use with bin_signing.py --hash) +// Uncomment ONE of these: +#define USE_SHA256 // Default, recommended +// #define USE_SHA384 +// #define USE_SHA512 + +// Choose signature algorithm (must match your key type) +// Uncomment ONE of these: +#define USE_RSA // Recommended (works with rsa-2048, rsa-3072, rsa-4096) +// #define USE_ECDSA // Works with ecdsa-p256, ecdsa-p384 + +// ======================================================= + +uint32_t last_ota_time = 0; + +void setup() { + Serial.begin(115200); + Serial.println("\n\n================================="); + Serial.println("SignedOTA - Secure OTA Updates"); + Serial.println("=================================\n"); + Serial.println("Booting..."); + + // Connect to WiFi + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + while (WiFi.waitForConnectResult() != WL_CONNECTED) { + Serial.println("Connection Failed! Rebooting..."); + delay(5000); + ESP.restart(); + } + + Serial.println("WiFi Connected!"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + + // ==================== SIGNATURE VERIFICATION SETUP ==================== + + // Select hash algorithm +#ifdef USE_SHA256 + int hashType = HASH_SHA256; + Serial.println("Using SHA-256 hash"); +#elif defined(USE_SHA384) + int hashType = HASH_SHA384; + Serial.println("Using SHA-384 hash"); +#elif defined(USE_SHA512) + int hashType = HASH_SHA512; + Serial.println("Using SHA-512 hash"); +#else +#error "Please define a hash algorithm (USE_SHA256, USE_SHA384, or USE_SHA512)" +#endif + + // Create verifier object +#ifdef USE_RSA + static UpdaterRSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, hashType); + Serial.println("Using RSA signature verification"); +#elif defined(USE_ECDSA) + static UpdaterECDSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, hashType); + Serial.println("Using ECDSA signature verification"); +#else +#error "Please define a signature type (USE_RSA or USE_ECDSA)" +#endif + + // Install signature verification BEFORE ArduinoOTA.begin() + ArduinoOTA.setSignature(&sign); + Serial.println("✓ Signature verification enabled"); + + // ======================================================================= + + // Optional: Set hostname + // ArduinoOTA.setHostname("myesp32"); + + // Optional: Set OTA password (in addition to signature verification) + if (ota_password != nullptr) { + ArduinoOTA.setPassword(ota_password); + Serial.println("✓ OTA password protection enabled"); + } + + // Configure OTA callbacks + ArduinoOTA + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) { + type = "sketch"; + } else { // U_SPIFFS + type = "filesystem"; + } + Serial.println("\n================================="); + Serial.println("OTA Update Starting: " + type); + Serial.println("================================="); + Serial.println("⚠️ Signature will be verified!"); + }) + .onEnd([]() { + Serial.println("\n================================="); + Serial.println("✅ OTA Update Complete!"); + Serial.println("✅ Signature Verified!"); + Serial.println("================================="); + Serial.println("Rebooting..."); + }) + .onProgress([](unsigned int progress, unsigned int total) { + if (millis() - last_ota_time > 500) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + last_ota_time = millis(); + } + }) + .onError([](ota_error_t error) { + Serial.println("\n================================="); + Serial.println("❌ OTA Update Failed!"); + Serial.println("================================="); + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) { + Serial.println("Authentication Failed"); + Serial.println("Check your OTA password"); + } else if (error == OTA_BEGIN_ERROR) { + Serial.println("Begin Failed"); + Serial.println("This could be:"); + Serial.println("- Signature verification setup failed"); + Serial.println("- Not enough space for update"); + Serial.println("- Invalid partition"); + } else if (error == OTA_CONNECT_ERROR) { + Serial.println("Connect Failed"); + } else if (error == OTA_RECEIVE_ERROR) { + Serial.println("Receive Failed"); + } else if (error == OTA_END_ERROR) { + Serial.println("End Failed"); + Serial.println("This could be:"); + Serial.println("- ❌ SIGNATURE VERIFICATION FAILED!"); + Serial.println("- Firmware not signed with correct key"); + Serial.println("- Firmware corrupted during transfer"); + Serial.println("- MD5 checksum mismatch"); + } + Serial.println("================================="); + }); + + // Start ArduinoOTA service + ArduinoOTA.begin(); + + Serial.println("\n================================="); + Serial.println("✓ OTA Server Ready"); + Serial.println("================================="); + Serial.printf("Hostname: %s.local\n", ArduinoOTA.getHostname().c_str()); + Serial.printf("IP: %s\n", WiFi.localIP().toString().c_str()); + Serial.println("Port: 3232"); + Serial.println("\n⚠️ Only signed firmware will be accepted!"); + Serial.println("=================================\n"); +} + +void loop() { + ArduinoOTA.handle(); +} + diff --git a/libraries/ArduinoOTA/examples/SignedOTA/build_opt.h b/libraries/ArduinoOTA/examples/SignedOTA/build_opt.h new file mode 100644 index 00000000000..1b328fa2487 --- /dev/null +++ b/libraries/ArduinoOTA/examples/SignedOTA/build_opt.h @@ -0,0 +1 @@ +-DUPDATE_SIGN diff --git a/libraries/ArduinoOTA/examples/SignedOTA/ci.yml b/libraries/ArduinoOTA/examples/SignedOTA/ci.yml new file mode 100644 index 00000000000..006e6e07dda --- /dev/null +++ b/libraries/ArduinoOTA/examples/SignedOTA/ci.yml @@ -0,0 +1,3 @@ +requires_any: + - CONFIG_SOC_WIFI_SUPPORTED=y + - CONFIG_ESP_WIFI_REMOTE_ENABLED=y diff --git a/libraries/ArduinoOTA/examples/SignedOTA/public_key.h b/libraries/ArduinoOTA/examples/SignedOTA/public_key.h new file mode 100644 index 00000000000..b3c0de10114 --- /dev/null +++ b/libraries/ArduinoOTA/examples/SignedOTA/public_key.h @@ -0,0 +1,41 @@ +// Public key for OTA signature verification +// Include this in your Arduino sketch + +// ⚠️ THIS IS A TEST KEY - DO NOT USE IN PRODUCTION! +// Generate your own keys using: +// python /tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem +// python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +// +// Then replace this file with the generated public_key.h + +// Test RSA-2048 Public Key (PEM format) +const uint8_t PUBLIC_KEY[] PROGMEM = { + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, + 0x43, 0x20, 0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x42, 0x49, + 0x6a, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, + 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4d, 0x49, 0x49, 0x42, 0x43, + 0x67, 0x4b, 0x43, 0x41, 0x51, 0x45, 0x41, 0x73, 0x35, 0x35, 0x66, 0x4f, 0x74, 0x51, 0x64, 0x69, + 0x70, 0x39, 0x58, 0x6f, 0x49, 0x61, 0x6c, 0x52, 0x5a, 0x4b, 0x6c, 0x4a, 0x0a, 0x52, 0x62, 0x55, + 0x73, 0x49, 0x30, 0x4c, 0x48, 0x5a, 0x74, 0x2b, 0x50, 0x58, 0x35, 0x4b, 0x58, 0x43, 0x79, 0x54, + 0x64, 0x63, 0x78, 0x71, 0x6c, 0x6f, 0x44, 0x45, 0x2b, 0x63, 0x38, 0x43, 0x6f, 0x32, 0x50, 0x77, + 0x37, 0x6f, 0x66, 0x63, 0x66, 0x30, 0x47, 0x41, 0x38, 0x4a, 0x55, 0x65, 0x6e, 0x6d, 0x45, 0x46, + 0x6b, 0x57, 0x6a, 0x50, 0x53, 0x48, 0x4c, 0x55, 0x55, 0x79, 0x44, 0x0a, 0x63, 0x65, 0x4b, 0x63, + 0x2b, 0x71, 0x45, 0x47, 0x54, 0x58, 0x72, 0x59, 0x39, 0x56, 0x6f, 0x4d, 0x38, 0x6f, 0x42, 0x58, + 0x39, 0x67, 0x48, 0x41, 0x64, 0x4b, 0x4f, 0x51, 0x48, 0x33, 0x50, 0x4d, 0x70, 0x4a, 0x69, 0x56, + 0x51, 0x71, 0x4e, 0x43, 0x36, 0x37, 0x31, 0x44, 0x37, 0x54, 0x45, 0x76, 0x4e, 0x52, 0x43, 0x67, + 0x6e, 0x4f, 0x41, 0x37, 0x77, 0x62, 0x77, 0x6f, 0x78, 0x4e, 0x0a, 0x63, 0x75, 0x59, 0x30, 0x49, + 0x6e, 0x51, 0x4e, 0x30, 0x64, 0x6b, 0x42, 0x43, 0x4f, 0x63, 0x34, 0x4e, 0x66, 0x31, 0x56, 0x42, + 0x76, 0x35, 0x64, 0x71, 0x55, 0x57, 0x41, 0x62, 0x66, 0x43, 0x57, 0x68, 0x5a, 0x37, 0x31, 0x72, + 0x4a, 0x56, 0x32, 0x53, 0x68, 0x79, 0x35, 0x48, 0x42, 0x48, 0x48, 0x52, 0x4e, 0x43, 0x78, 0x4f, + 0x67, 0x58, 0x68, 0x4f, 0x6c, 0x66, 0x6c, 0x66, 0x0a, 0x72, 0x49, 0x57, 0x56, 0x71, 0x66, 0x51, + 0x4b, 0x2b, 0x75, 0x54, 0x4d, 0x62, 0x39, 0x4a, 0x4c, 0x51, 0x67, 0x76, 0x4a, 0x66, 0x70, 0x4c, + 0x61, 0x65, 0x35, 0x35, 0x61, 0x61, 0x4e, 0x77, 0x63, 0x72, 0x62, 0x59, 0x38, 0x58, 0x67, 0x53, + 0x79, 0x31, 0x64, 0x6c, 0x58, 0x76, 0x4e, 0x37, 0x4d, 0x33, 0x75, 0x4c, 0x52, 0x72, 0x4b, 0x79, + 0x61, 0x75, 0x34, 0x59, 0x0a, 0x39, 0x51, 0x53, 0x71, 0x76, 0x4a, 0x71, 0x67, 0x52, 0x61, 0x36, + 0x66, 0x47, 0x51, 0x2f, 0x4d, 0x41, 0x63, 0x6c, 0x48, 0x59, 0x33, 0x6d, 0x4b, 0x64, 0x6e, 0x64, + 0x68, 0x51, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x45, 0x4e, + 0x44, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, 0x2d, + 0x2d, 0x0a, 0x00, +}; +const size_t PUBLIC_KEY_LEN = 451; + diff --git a/libraries/ArduinoOTA/keywords.txt b/libraries/ArduinoOTA/keywords.txt index 9774de881ea..9c8a81df3ae 100644 --- a/libraries/ArduinoOTA/keywords.txt +++ b/libraries/ArduinoOTA/keywords.txt @@ -29,6 +29,7 @@ setPartitionLabel KEYWORD2 getPartitionLabel KEYWORD2 setRebootOnSuccess KEYWORD2 setMdnsEnabled KEYWORD2 +setSignature KEYWORD2 getCommand KEYWORD2 setTimeout KEYWORD2 diff --git a/libraries/ArduinoOTA/src/ArduinoOTA.cpp b/libraries/ArduinoOTA/src/ArduinoOTA.cpp index a5b0d09de58..3bc59a5dc98 100644 --- a/libraries/ArduinoOTA/src/ArduinoOTA.cpp +++ b/libraries/ArduinoOTA/src/ArduinoOTA.cpp @@ -28,7 +28,13 @@ ArduinoOTAClass::ArduinoOTAClass() : _port(0), _initialized(false), _rebootOnSuccess(true), _mdnsEnabled(true), _state(OTA_IDLE), _size(0), _cmd(0), _ota_port(0), _ota_timeout(1000), - _start_callback(NULL), _end_callback(NULL), _error_callback(NULL), _progress_callback(NULL) {} + _start_callback(NULL), _end_callback(NULL), _error_callback(NULL), _progress_callback(NULL) +#ifdef UPDATE_SIGN + , + _sign(NULL) +#endif /* UPDATE_SIGN */ +{ +} ArduinoOTAClass::~ArduinoOTAClass() { end(); @@ -136,6 +142,18 @@ ArduinoOTAClass &ArduinoOTAClass::setMdnsEnabled(bool enabled) { return *this; } +#ifdef UPDATE_SIGN +ArduinoOTAClass &ArduinoOTAClass::setSignature(UpdaterVerifyClass *sign) { + if (_state == OTA_IDLE && sign) { + _sign = sign; + int hashType = sign->getHashType(); + [[maybe_unused]] const char *hashName = (hashType == HASH_SHA256) ? "SHA-256" : (hashType == HASH_SHA384) ? "SHA-384" : "SHA-512"; + log_i("Signature verification enabled for ArduinoOTA (hash: %s)", hashName); + } + return *this; +} +#endif /* UPDATE_SIGN */ + void ArduinoOTAClass::begin() { if (_initialized) { log_w("already initialized"); @@ -297,6 +315,21 @@ void ArduinoOTAClass::_onRx() { } void ArduinoOTAClass::_runUpdate() { +#ifdef UPDATE_SIGN + // Install signature verification if enabled + if (_sign) { + if (!Update.installSignature(_sign)) { + log_e("Failed to install signature verification"); + if (_error_callback) { + _error_callback(OTA_BEGIN_ERROR); + } + _state = OTA_IDLE; + return; + } + log_i("Signature verification installed for OTA update"); + } +#endif /* UPDATE_SIGN */ + const char *partition_label = _partition_label.length() ? _partition_label.c_str() : NULL; if (!Update.begin(_size, _cmd, -1, LOW, partition_label)) { diff --git a/libraries/ArduinoOTA/src/ArduinoOTA.h b/libraries/ArduinoOTA/src/ArduinoOTA.h index a946388c4aa..a63754dfb74 100644 --- a/libraries/ArduinoOTA/src/ArduinoOTA.h +++ b/libraries/ArduinoOTA/src/ArduinoOTA.h @@ -67,6 +67,14 @@ class ArduinoOTAClass { //Sets if the device should advertise itself to Arduino IDE. Default true ArduinoOTAClass &setMdnsEnabled(bool enabled); +#ifdef UPDATE_SIGN + //Install signature verification for OTA updates + //Must be called before begin() + //sign: Signature verifier to use (e.g., UpdaterRSAVerifier or UpdaterECDSAVerifier) + // The hash type is determined from the verifier's configuration + ArduinoOTAClass &setSignature(UpdaterVerifyClass *sign); +#endif /* UPDATE_SIGN */ + //This callback will be called when OTA connection has begun ArduinoOTAClass &onStart(THandlerFunction fn); @@ -116,6 +124,10 @@ class ArduinoOTAClass { THandlerFunction_Error _error_callback; THandlerFunction_Progress _progress_callback; +#ifdef UPDATE_SIGN + UpdaterVerifyClass *_sign; +#endif /* UPDATE_SIGN */ + void _runUpdate(void); void _onRx(void); int parseInt(void); diff --git a/libraries/Update/README.md b/libraries/Update/README.md new file mode 100644 index 00000000000..0cf5727cd30 --- /dev/null +++ b/libraries/Update/README.md @@ -0,0 +1,432 @@ +# ESP32 Arduino Update Library + +The Update library provides functionality for Over-The-Air (OTA) firmware updates on ESP32 devices. It supports secure updates with signature verification, encrypted updates, and various update sources. + +## Features + +- **OTA Updates**: Update firmware over WiFi +- **Signature Verification**: RSA and ECDSA signature verification for secure updates (optional, must be enabled with `UPDATE_SIGN`) +- **Image Encryption**: Support for encrypted firmware updates (optional, can be disabled with `UPDATE_NOCRYPT`) +- **Multiple Sources**: HTTP, HTTPS, SD card, and custom sources +- **Progress Callbacks**: Monitor update progress +- **MD5 Verification**: Optional MD5 checksum verification + +## Quick Start + +### Basic OTA Update + +```cpp +#include + +WiFiClient client; +size_t updateSize = client.available(); + +if (Update.begin(updateSize)) { + Update.writeStream(client); + if (Update.end()) { + Serial.println("Update successful!"); + ESP.restart(); + } else { + Serial.println("Update failed!"); + } +} +``` + +### Signed OTA Update (Recommended) + +To enable signature verification, add `-DUPDATE_SIGN` to your build flags (e.g., in `build_opt.h`): +``` +-DUPDATE_SIGN +``` + +Then in your sketch: +```cpp +#include + +// Include your public key (generated with bin_signing.py) +#include "public_key.h" + +// Create verifier object (defaults to SHA-256) +UpdaterRSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN); + +// Install signature verification BEFORE Update.begin() +Update.installSignature(&sign); + +// Now perform the update as usual +if (Update.begin(updateSize)) { + Update.writeStream(client); + if (Update.end()) { + // Signature was verified successfully! + Serial.println("Signed update successful!"); + ESP.restart(); + } else { + if (Update.getError() == UPDATE_ERROR_SIGN) { + Serial.println("Signature verification failed!"); + } + } +} +``` + +## Signature Verification + +### Overview + +Code signing ensures that only firmware signed with your private key will be accepted by your devices. This protects against: + +- Unauthorized firmware updates +- Man-in-the-middle attacks +- Compromised update servers +- Supply chain attacks + +### Supported Algorithms + +**Signature Schemes:** +- RSA-2048, RSA-3072, RSA-4096 +- ECDSA-P256, ECDSA-P384 + +**Hash Algorithms:** +- SHA-256, SHA-384, SHA-512 + +### Setup + +1. **Generate Key Pair:** + +```bash +# RSA-2048 (recommended) +python /tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem +python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem + +# ECDSA-P256 (smaller, faster) +python /tools/bin_signing.py --generate-key ecdsa-p256 --out private_key.pem +python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +``` + +2. **Include Public Key in Sketch:** + +```cpp +#include "public_key.h" // Generated by bin_signing.py +``` + +3. **Install Signature Verification:** + +Enable the feature by adding to `build_opt.h`: +``` +-DUPDATE_SIGN +``` + +Then in your sketch: +```cpp +// For RSA with SHA-256 +UpdaterRSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, HASH_SHA256); +Update.installSignature(&sign); + +// For ECDSA with SHA-384 +UpdaterECDSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, HASH_SHA384); +Update.installSignature(&sign); +``` + +4. **Sign Your Firmware:** + +```bash +python /tools/bin_signing.py --bin --key private_key.pem --out firmware_signed.bin --hash +``` + +5. **Upload Signed Application Firmware:** + +The signed firmware includes the signature appended to the binary. Upload the newly created signed firmware instead of the original application binary. + +### Security Best Practices + +1. **Protect Your Private Key:** + - Never commit it to version control + - Store it in secure, encrypted storage + - Limit access to authorized personnel only + - Consider using HSM for production + +2. **Use HTTPS:** + - While signature verification protects integrity, HTTPS protects confidentiality + +## API Reference + +### UpdateClass Methods + +#### begin() +```cpp +bool begin(size_t size = UPDATE_SIZE_UNKNOWN, + int command = U_FLASH, + int ledPin = -1, + uint8_t ledOn = LOW, + const char *label = NULL) +``` +Starts an update operation. + +**Parameters:** +- `size`: Size of the update in bytes (including signature if using signed updates) +- `command`: Update type (U_FLASH, U_SPIFFS, U_FATFS, U_LITTLEFS) +- `ledPin`: Optional LED pin to indicate progress +- `ledOn`: LED on state (LOW or HIGH) +- `label`: Optional partition label + +**Returns:** `true` on success, `false` on failure + +#### installSignature() +```cpp +bool installSignature(UpdaterVerifyClass *sign) +``` +Installs signature verification. Must be called before `begin()`. + +**Parameters:** +- `sign`: Signature verifier (UpdaterRSAVerifier or UpdaterECDSAVerifier) + +**Returns:** `true` on success, `false` on failure + +#### write() +```cpp +size_t write(uint8_t *data, size_t len) +``` +Writes data to the update. + +**Parameters:** +- `data`: Data buffer +- `len`: Length of data + +**Returns:** Number of bytes written + +#### writeStream() +```cpp +size_t writeStream(Stream &data) +``` +Writes data from a stream. + +**Parameters:** +- `data`: Input stream + +**Returns:** Number of bytes written + +#### end() +```cpp +bool end(bool evenIfRemaining = false) +``` +Completes the update and verifies signature if enabled. + +**Parameters:** +- `evenIfRemaining`: Complete even if not all data was written + +**Returns:** `true` if update succeeded and signature is valid, `false` otherwise + +#### abort() +```cpp +void abort() +``` +Aborts the current update. + +#### setMD5() +```cpp +bool setMD5(const char *expected_md5) +``` +Sets expected MD5 hash for verification. + +**Parameters:** +- `expected_md5`: MD5 hash as hex string (32 characters) + +**Returns:** `true` on success, `false` on failure + +#### getError() +```cpp +uint8_t getError() +``` +Returns the last error code. + +**Returns:** Error code (see Error Codes below) + +#### errorString() +```cpp +const char *errorString() +``` +Returns a human-readable error message. + +**Returns:** Error message string + +### Hash Classes (from Hash Library) + +The Update library uses the Hash library for hashing. Simply use the builders from that library: + +```cpp +#include + +SHA256Builder hash256; // SHA-256 +SHA384Builder hash384; // SHA-384 +SHA512Builder hash512; // SHA-512 +``` + +See the [Hash library documentation](../Hash/README.md) for more details. + +### Signature Verifier Classes + +#### UpdaterRSAVerifier +RSA signature verifier. + +```cpp +UpdaterRSAVerifier(const uint8_t *pubkey, size_t pubkeyLen, int hashType = HASH_SHA256) +``` + +**Parameters:** +- `pubkey`: Public key in PEM format +- `pubkeyLen`: Length of public key +- `hashType`: Hash algorithm (`HASH_SHA256`, `HASH_SHA384`, or `HASH_SHA512`). Defaults to `HASH_SHA256`. + +#### UpdaterECDSAVerifier +ECDSA signature verifier. + +```cpp +UpdaterECDSAVerifier(const uint8_t *pubkey, size_t pubkeyLen, int hashType = HASH_SHA256) +``` + +**Parameters:** +- `pubkey`: Public key in PEM format +- `pubkeyLen`: Length of public key +- `hashType`: Hash algorithm (`HASH_SHA256`, `HASH_SHA384`, or `HASH_SHA512`). Defaults to `HASH_SHA256`. + +### Error Codes + +| Code | Name | Description | +|------|------|-------------| +| 0 | UPDATE_ERROR_OK | No error | +| 1 | UPDATE_ERROR_WRITE | Flash write failed | +| 2 | UPDATE_ERROR_ERASE | Flash erase failed | +| 3 | UPDATE_ERROR_READ | Flash read failed | +| 4 | UPDATE_ERROR_SPACE | Not enough space | +| 5 | UPDATE_ERROR_SIZE | Bad size given | +| 6 | UPDATE_ERROR_STREAM | Stream read timeout | +| 7 | UPDATE_ERROR_MD5 | MD5 check failed | +| 8 | UPDATE_ERROR_MAGIC_BYTE | Wrong magic byte | +| 9 | UPDATE_ERROR_ACTIVATE | Could not activate firmware | +| 10 | UPDATE_ERROR_NO_PARTITION | Partition not found | +| 11 | UPDATE_ERROR_BAD_ARGUMENT | Bad argument | +| 12 | UPDATE_ERROR_ABORT | Aborted | +| 13 | UPDATE_ERROR_DECRYPT | Decryption error | +| 14 | UPDATE_ERROR_SIGN | Signature verification failed | + +## Examples + +- **Signed_OTA_Update**: Demonstrates signed OTA updates with RSA/ECDSA +- **HTTPS_OTA_Update**: HTTPS OTA update +- **HTTP_Client_AES_OTA_Update**: Encrypted OTA update +- **SD_Update**: Update from SD card + +See the `examples/` directory for complete examples. + +## Tools + +### bin_signing.py + +Python script for key generation and firmware signing. Located in `/tools/bin_signing.py`. + +**Requirements:** +```bash +pip install cryptography +``` + +**Usage:** +```bash +# Generate keys +python /tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem +python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem + +# Sign firmware (defaults to SHA-256) +python /tools/bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin + +# Sign firmware with SHA-384 +python /tools/bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin --hash sha384 + +# Verify signature +python /tools/bin_signing.py --verify firmware_signed.bin --pubkey public_key.pem +``` + +See `/tools/bin_signing.py --help` for more options. + +## Troubleshooting + +### "Signature verification failed" + +- Ensure firmware was signed with correct private key +- Verify public key in sketch matches private key +- Check signature scheme and hash algorithm match +- Verify signed binary wasn't corrupted + +### "Failed to install signature verification" + +- Call `installSignature()` before `Update.begin()` +- Ensure hash and sign objects are properly initialized + +### "Update failed" with no specific error + +- Check firmware size is correct (including signature) +- Ensure enough space in target partition +- Verify magic byte (0xE9) at start of firmware + +### Memory Issues + +- Signature verification requires ~2KB of heap +- RSA-4096 uses more memory than ECDSA-P256 +- Ensure sufficient free heap before starting update + +## Compile-Time Options + +The Update library supports compile-time configuration to reduce code size if certain features are not needed: + +### UPDATE_SIGN + +Enable signature verification support (disabled by default). + +Add to your project's `build_opt.h`: +``` +-DUPDATE_SIGN +``` + +Or add to your build flags in `platformio.ini`: +```ini +build_flags = -DUPDATE_SIGN +``` + +**Effects:** +- Enables signature verification classes and methods +- Adds RSA and ECDSA signature verification support +- `installSignature()` method becomes available +- Increases code size due to mbedtls cryptographic functions + +### UPDATE_NOCRYPT + +Disable encryption/decryption support: + +```cpp +#define UPDATE_NOCRYPT +#include +``` + +**Effects:** +- Removes AES encryption support +- Reduces code size +- `setupCrypt()` and related methods will not be available + +**Note:** To enable signature verification while disabling encryption, add to `build_opt.h`: +``` +-DUPDATE_SIGN +-DUPDATE_NOCRYPT +``` + +## License + +This library is part of the Arduino-ESP32 project and is licensed under the Apache License 2.0. + +## Contributing + +Contributions are welcome! Please submit issues and pull requests on GitHub: +https://github.com/espressif/arduino-esp32 + +## Support + +- Documentation: https://docs.espressif.com/ +- Forum: https://esp32.com/ +- GitHub Issues: https://github.com/espressif/arduino-esp32/issues + diff --git a/libraries/Update/examples/Signed_OTA_Update/README.md b/libraries/Update/examples/Signed_OTA_Update/README.md new file mode 100644 index 00000000000..1fd3cb0eb76 --- /dev/null +++ b/libraries/Update/examples/Signed_OTA_Update/README.md @@ -0,0 +1,202 @@ +# Signed OTA Update Example + +This example demonstrates how to perform secure OTA (Over-The-Air) updates with signature verification on ESP32 devices using Arduino. + +## Overview + +Code signing ensures that only firmware signed with your private key will be accepted by your devices. This protects against unauthorized firmware updates, even if an attacker gains access to your update server. + +## Features + +- **RSA Signature Verification**: Supports RSA-2048, RSA-3072, and RSA-4096 +- **ECDSA Signature Verification**: Supports ECDSA-P256 and ECDSA-P384 +- **Multiple Hash Algorithms**: SHA-256, SHA-384, and SHA-512 +- **Automatic Signature Verification**: Signatures are verified automatically during OTA update +- **Secure by Default**: Update fails if signature verification fails + +## Prerequisites + +1. **Python 3** with the `cryptography` package: + ```bash + pip install cryptography + ``` + +2. **ESP32 Arduino Core** with Update library + +## Quick Start Guide + +### Step 1: Generate Key Pair + +Generate an RSA-2048 key pair (recommended): +```bash +python /tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem +python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +``` + +Or generate an ECDSA-P256 key pair (smaller, faster): +```bash +python /tools/bin_signing.py --generate-key ecdsa-p256 --out private_key.pem +python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +``` + +Where `` is your ESP32 Arduino installation path (e.g., `~/Arduino/hardware/espressif/esp32/`). + +**IMPORTANT**: Keep `private_key.pem` secure! Anyone with access to it can sign firmware for your devices. + +### Step 2: Update the Example Sketch + +1. Copy the generated `public_key.h` to the example directory +2. Open `Signed_OTA_Update.ino` +3. Update WiFi credentials: + ```cpp + const char *ssid = "YOUR_SSID"; + const char *password = "YOUR_PASSWORD"; + ``` +4. Update firmware URL: + ```cpp + const char *firmwareUrl = "http://your-server.com/firmware_signed.bin"; + ``` +5. Uncomment the appropriate key type (RSA or ECDSA) +6. Uncomment the appropriate hash algorithm (SHA-256, SHA-384, or SHA-512) + +### Step 3: Build and Upload Initial Firmware + +1. Compile and upload the sketch to your ESP32 +2. Open Serial Monitor to verify it's running + +### Step 4: Build and Sign New Firmware + +1. Make changes to your sketch (e.g., add a version number) +2. Build the sketch and export the binary: + - Arduino IDE: `Sketch` → `Export Compiled Binary` + - Find the application `.bin` file in the `build` folder of your sketch folder. For example `build/espressif.esp32.esp32c6/Signed_OTA_Update.ino.bin`. + +3. Sign the binary: + ```bash + python /tools/bin_signing.py --bin --key private_key.pem --out firmware_signed.bin + ``` + + For other hash algorithms (for example SHA-384): + ```bash + python /tools/bin_signing.py --bin --key private_key.pem --out firmware_signed.bin --hash sha384 + ``` + +### Step 5: Host the Signed Firmware + +Upload `firmware_signed.bin` to your web server and make it accessible at the URL you configured. + +### Step 6: Perform OTA Update + +Reset your ESP32. It will: +1. Connect to WiFi +2. Download the signed firmware +3. Verify the signature +4. Apply the update if signature is valid +5. Reboot with the new firmware + +## Security Considerations + +### Private Key Management + +- **NEVER** commit your private key to version control +- Store it securely (encrypted storage, HSM, etc.) +- Limit access to authorized personnel only +- Consider using separate keys for development and production + +### Recommended Practices + +1. **Use HTTPS**: While signature verification protects firmware integrity, HTTPS protects against MitM attacks +2. **Key Rotation**: Periodically rotate keys (requires firmware update to include new public key) + +## Signature Schemes Comparison + +| Scheme | Key Size | Signature Size | Verification Speed | Security | +|--------|----------|----------------|-------------------|----------| +| RSA-2048 | 2048 bits | 256 bytes | Medium | High | +| RSA-3072 | 3072 bits | 384 bytes | Slower | Very High | +| RSA-4096 | 4096 bits | 512 bytes | Slowest | Maximum | +| ECDSA-P256 | 256 bits | 64 bytes | Fast | High | +| ECDSA-P384 | 384 bits | 96 bytes | Fast | Very High | + +**Recommendation**: RSA-2048 or ECDSA-P256 provide good security with reasonable performance. + +## Hash Algorithms Comparison + +| Algorithm | Output Size | Speed | Security | +|-----------|-------------|-------|----------| +| SHA-256 | 32 bytes | Fast | High | +| SHA-384 | 48 bytes | Medium | Very High | +| SHA-512 | 64 bytes | Medium | Very High | + +**Recommendation**: SHA-256 is sufficient for most applications. + +## Troubleshooting + +### "Signature verification failed" + +- Ensure the firmware was signed with the correct private key +- Verify that the public key in the sketch matches the private key used for signing +- Check that the signature scheme (RSA/ECDSA) and hash algorithm match between signing and verification +- Ensure the signed binary wasn't corrupted during transfer + +### "Failed to install signature verification" + +- Check that `installSignature()` is called before `Update.begin()` +- Ensure hash and sign objects are properly initialized + +### "Public key parsing failed" + +- Verify the public key PEM format is correct +- Ensure PUBLIC_KEY_LEN matches the actual key length + +## Advanced Usage + +### Verifying a Signed Binary + +You can verify a signed binary without flashing it: + +```bash +python bin_signing.py --verify firmware_signed.bin --pubkey public_key.pem +``` + +### Using Different Hash Algorithms + +Match the hash algorithm between signing and verification: + +**Signing with SHA-384:** +```bash +python bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin --hash sha384 +``` + +**Sketch configuration:** +```cpp +#define USE_SHA384 +``` + +## API Reference + +### Classes + +- **UpdaterRSAVerifier**: RSA signature verifier +- **UpdaterECDSAVerifier**: ECDSA signature verifier + +### Methods + +```cpp +// Install signature verification (call before Update.begin()) +bool Update.installSignature(UpdaterVerifyClass *sign); +``` + +### Error Codes + +- `UPDATE_ERROR_SIGN (14)`: Signature verification failed + +## License + +This example is part of the Arduino-ESP32 project and is licensed under the Apache License 2.0. + +## Support + +For issues and questions: +- GitHub: https://github.com/espressif/arduino-esp32/issues +- Documentation: https://docs.espressif.com/ diff --git a/libraries/Update/examples/Signed_OTA_Update/Signed_OTA_Update.ino b/libraries/Update/examples/Signed_OTA_Update/Signed_OTA_Update.ino new file mode 100644 index 00000000000..327cd9ee50b --- /dev/null +++ b/libraries/Update/examples/Signed_OTA_Update/Signed_OTA_Update.ino @@ -0,0 +1,232 @@ +/* + Signed OTA Update Example + + This example demonstrates how to perform a secure OTA update with signature verification. + Only firmware signed with the correct private key will be accepted. + + NOTE: This example requires signature verification support to be enabled. + This is done automatically via the build_opt.h file in this directory. + + Steps to use this example: + 1. Generate a key pair (see instructions below) + 2. Include the public key in this sketch (see public_key.h) + 3. Build and upload this sketch to your ESP32 + 4. Build your new firmware binary + 5. Sign the binary with the private key (see instructions below) + 6. Upload the signed firmware via OTA (HTTP/HTTPS server) + + Generating keys: + ------------------ + RSA (recommended for maximum compatibility): + python bin_signing.py --generate-key rsa-2048 --out private_key.pem + python bin_signing.py --extract-pubkey private_key.pem --out public_key.pem + + ECDSA (smaller keys, faster verification): + python bin_signing.py --generate-key ecdsa-p256 --out private_key.pem + python bin_signing.py --extract-pubkey private_key.pem --out public_key.pem + + Signing firmware: + ----------------- + python bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin + + IMPORTANT: Keep your private_key.pem secure! Anyone with access to it can + sign firmware that will be accepted by your devices. + + Created by lucasssvaz +*/ + +#include +#include +#include +#include + +// WiFi credentials +const char *ssid = "YOUR_SSID"; +const char *password = "YOUR_PASSWORD"; + +// URL to the signed firmware binary +const char *firmwareUrl = "http://your-server.com/firmware_signed.bin"; + +// Public key for signature verification +// Generated with: python bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +// This will create a public_key.h file that you should include below +#include "public_key.h" + +// Uncomment the key type you're using: +#define USE_RSA // RSA signature verification +//#define USE_ECDSA // ECDSA signature verification + +// Uncomment the hash algorithm you're using (must match the one used for signing): +#define USE_SHA256 // SHA-256 (recommended and default) +//#define USE_SHA384 // SHA-384 +//#define USE_SHA512 // SHA-512 + +void performOTAUpdate() { + HTTPClient http; + + Serial.println("Starting OTA update..."); + Serial.print("Firmware URL: "); + Serial.println(firmwareUrl); + + http.begin(firmwareUrl); + int httpCode = http.GET(); + + if (httpCode != HTTP_CODE_OK) { + Serial.printf("HTTP GET failed, error: %s\n", http.errorToString(httpCode).c_str()); + http.end(); + return; + } + + int contentLength = http.getSize(); + Serial.printf("Firmware size: %d bytes\n", contentLength); + + if (contentLength <= 0) { + Serial.println("Invalid content length"); + http.end(); + return; + } + + // The signed firmware includes the signature (512 bytes padding) + // The actual firmware size is contentLength - 512 + const size_t signatureSize = 512; + size_t firmwareSize = contentLength - signatureSize; + + Serial.printf("Actual firmware size: %d bytes\n", firmwareSize); + Serial.printf("Signature size: %d bytes\n", signatureSize); + + // Select hash algorithm +#ifdef USE_SHA256 + int hashType = HASH_SHA256; + Serial.println("Using SHA-256 hash"); +#elif defined(USE_SHA384) + int hashType = HASH_SHA384; + Serial.println("Using SHA-384 hash"); +#elif defined(USE_SHA512) + int hashType = HASH_SHA512; + Serial.println("Using SHA-512 hash"); +#else +#error "Please define a hash algorithm (USE_SHA256, USE_SHA384, or USE_SHA512)" +#endif + + // Create verifier object +#ifdef USE_RSA + UpdaterRSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, hashType); + Serial.println("Using RSA signature verification"); +#elif defined(USE_ECDSA) + UpdaterECDSAVerifier sign(PUBLIC_KEY, PUBLIC_KEY_LEN, hashType); + Serial.println("Using ECDSA signature verification"); +#else +#error "Please define a signature scheme (USE_RSA or USE_ECDSA)" +#endif + + // Install signature verification BEFORE calling Update.begin() + if (!Update.installSignature(&sign)) { + Serial.println("Failed to install signature verification"); + http.end(); + return; + } + Serial.println("Signature verification installed"); + + // Begin update with the TOTAL size (firmware + signature) + if (!Update.begin(contentLength)) { + Serial.printf("Update.begin failed: %s\n", Update.errorString()); + http.end(); + return; + } + + // Get the stream + WiFiClient *stream = http.getStreamPtr(); + + // Write firmware data + Serial.println("Writing firmware..."); + size_t written = 0; + uint8_t buff[1024]; + int progress = 0; + + while (http.connected() && (written < contentLength)) { + size_t available = stream->available(); + + if (available) { + int bytesRead = stream->readBytes(buff, min(available, sizeof(buff))); + + if (bytesRead > 0) { + size_t bytesWritten = Update.write(buff, bytesRead); + + if (bytesWritten > 0) { + written += bytesWritten; + + // Print progress + int newProgress = (written * 100) / contentLength; + if (newProgress != progress && newProgress % 10 == 0) { + progress = newProgress; + Serial.printf("Progress: %d%%\n", progress); + } + } else { + Serial.printf("Update.write failed: %s\n", Update.errorString()); + break; + } + } + } + delay(1); + } + + Serial.printf("Written: %d bytes\n", written); + + // End the update - this will verify the signature + if (Update.end()) { + Serial.println("OTA update completed successfully!"); + Serial.println("Signature verified!"); + + if (Update.isFinished()) { + Serial.println("Update successfully completed. Rebooting..."); + delay(1000); + ESP.restart(); + } else { + Serial.println("Update not finished? Something went wrong!"); + } + } else { + Serial.printf("Update.end failed: %s\n", Update.errorString()); + + // Check if it was a signature verification failure + if (Update.getError() == UPDATE_ERROR_SIGN) { + Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + Serial.println("SIGNATURE VERIFICATION FAILED!"); + Serial.println("The firmware was not signed with the"); + Serial.println("correct private key or is corrupted."); + Serial.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); + } + } + + http.end(); +} + +void setup() { + Serial.begin(115200); + Serial.println("\n\nSigned OTA Update Example"); + Serial.println("=========================\n"); + + // Connect to WiFi + Serial.printf("Connecting to WiFi: %s\n", ssid); + WiFi.begin(ssid, password); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + + Serial.println("\nWiFi connected!"); + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + + // Wait a bit before starting OTA + delay(2000); + + // Perform OTA update + performOTAUpdate(); +} + +void loop() { + // Nothing to do here + delay(1000); +} + diff --git a/libraries/Update/examples/Signed_OTA_Update/build_opt.h b/libraries/Update/examples/Signed_OTA_Update/build_opt.h new file mode 100644 index 00000000000..1b328fa2487 --- /dev/null +++ b/libraries/Update/examples/Signed_OTA_Update/build_opt.h @@ -0,0 +1 @@ +-DUPDATE_SIGN diff --git a/libraries/Update/examples/Signed_OTA_Update/ci.yml b/libraries/Update/examples/Signed_OTA_Update/ci.yml new file mode 100644 index 00000000000..006e6e07dda --- /dev/null +++ b/libraries/Update/examples/Signed_OTA_Update/ci.yml @@ -0,0 +1,3 @@ +requires_any: + - CONFIG_SOC_WIFI_SUPPORTED=y + - CONFIG_ESP_WIFI_REMOTE_ENABLED=y diff --git a/libraries/Update/examples/Signed_OTA_Update/public_key.h b/libraries/Update/examples/Signed_OTA_Update/public_key.h new file mode 100644 index 00000000000..28b926da6b0 --- /dev/null +++ b/libraries/Update/examples/Signed_OTA_Update/public_key.h @@ -0,0 +1,43 @@ +// Public key for OTA signature verification +// Include this in your Arduino sketch + +// ⚠️ THIS IS A TEST KEY - DO NOT USE IN PRODUCTION! +// Generate your own keys using: +// python /tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem +// python /tools/bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +// +// Then replace this file with the generated public_key.h + +// Test RSA-2048 Public Key (PEM format) +const uint8_t PUBLIC_KEY[] PROGMEM = { + 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, + 0x43, 0x20, 0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x0a, 0x4d, 0x49, 0x49, 0x42, 0x49, + 0x6a, 0x41, 0x4e, 0x42, 0x67, 0x6b, 0x71, 0x68, 0x6b, 0x69, 0x47, 0x39, 0x77, 0x30, 0x42, 0x41, + 0x51, 0x45, 0x46, 0x41, 0x41, 0x4f, 0x43, 0x41, 0x51, 0x38, 0x41, 0x4d, 0x49, 0x49, 0x42, 0x43, + 0x67, 0x4b, 0x43, 0x41, 0x51, 0x45, 0x41, 0x36, 0x42, 0x33, 0x52, 0x67, 0x34, 0x39, 0x6b, 0x4e, + 0x47, 0x72, 0x44, 0x2b, 0x50, 0x46, 0x6e, 0x39, 0x64, 0x69, 0x4b, 0x0a, 0x57, 0x50, 0x34, 0x65, + 0x42, 0x59, 0x4d, 0x2f, 0x49, 0x79, 0x6b, 0x55, 0x4b, 0x4d, 0x34, 0x39, 0x63, 0x6a, 0x65, 0x56, + 0x56, 0x4f, 0x39, 0x42, 0x4f, 0x30, 0x66, 0x6c, 0x47, 0x47, 0x6e, 0x47, 0x71, 0x79, 0x34, 0x50, + 0x72, 0x69, 0x4e, 0x71, 0x32, 0x62, 0x4a, 0x4a, 0x6a, 0x7a, 0x68, 0x38, 0x46, 0x32, 0x42, 0x53, + 0x4f, 0x75, 0x74, 0x48, 0x77, 0x75, 0x7a, 0x6d, 0x62, 0x45, 0x52, 0x6f, 0x0a, 0x30, 0x38, 0x51, + 0x72, 0x32, 0x30, 0x4e, 0x61, 0x52, 0x72, 0x7a, 0x6e, 0x71, 0x6e, 0x59, 0x4e, 0x57, 0x4e, 0x69, + 0x6e, 0x43, 0x67, 0x7a, 0x34, 0x34, 0x49, 0x4e, 0x50, 0x50, 0x78, 0x70, 0x45, 0x55, 0x65, 0x68, + 0x61, 0x32, 0x66, 0x6d, 0x6d, 0x39, 0x77, 0x5a, 0x67, 0x57, 0x31, 0x69, 0x31, 0x67, 0x31, 0x77, + 0x70, 0x68, 0x56, 0x51, 0x6c, 0x5a, 0x30, 0x49, 0x63, 0x72, 0x6d, 0x5a, 0x5a, 0x0a, 0x42, 0x61, + 0x33, 0x49, 0x64, 0x6a, 0x78, 0x63, 0x52, 0x67, 0x51, 0x6c, 0x69, 0x32, 0x4b, 0x74, 0x78, 0x72, + 0x41, 0x4a, 0x67, 0x33, 0x4a, 0x47, 0x43, 0x54, 0x2f, 0x39, 0x6d, 0x7a, 0x52, 0x31, 0x70, 0x37, + 0x59, 0x34, 0x50, 0x34, 0x65, 0x71, 0x30, 0x6b, 0x2b, 0x78, 0x2b, 0x45, 0x72, 0x6f, 0x35, 0x73, + 0x47, 0x69, 0x49, 0x7a, 0x33, 0x44, 0x67, 0x61, 0x50, 0x43, 0x54, 0x41, 0x37, 0x52, 0x0a, 0x4b, + 0x69, 0x75, 0x6e, 0x2f, 0x67, 0x64, 0x56, 0x71, 0x34, 0x35, 0x2f, 0x75, 0x62, 0x64, 0x53, 0x58, + 0x65, 0x62, 0x50, 0x46, 0x43, 0x73, 0x36, 0x66, 0x46, 0x73, 0x52, 0x39, 0x6d, 0x43, 0x6f, 0x37, + 0x70, 0x43, 0x4b, 0x74, 0x45, 0x55, 0x51, 0x78, 0x34, 0x4d, 0x68, 0x55, 0x4e, 0x5a, 0x48, 0x48, + 0x31, 0x49, 0x33, 0x62, 0x79, 0x57, 0x35, 0x7a, 0x39, 0x36, 0x49, 0x6a, 0x46, 0x44, 0x68, 0x0a, + 0x54, 0x2f, 0x64, 0x5a, 0x71, 0x32, 0x6d, 0x44, 0x54, 0x64, 0x76, 0x59, 0x2b, 0x6d, 0x5a, 0x75, + 0x51, 0x4d, 0x37, 0x6c, 0x72, 0x31, 0x4d, 0x4e, 0x6a, 0x35, 0x36, 0x79, 0x74, 0x41, 0x56, 0x4a, + 0x39, 0x56, 0x7a, 0x74, 0x44, 0x75, 0x35, 0x4f, 0x6a, 0x48, 0x32, 0x76, 0x6f, 0x32, 0x6b, 0x59, + 0x46, 0x4f, 0x72, 0x52, 0x49, 0x57, 0x70, 0x5a, 0x4c, 0x56, 0x35, 0x6c, 0x47, 0x79, 0x7a, 0x45, + 0x0a, 0x33, 0x77, 0x49, 0x44, 0x41, 0x51, 0x41, 0x42, 0x0a, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x45, + 0x4e, 0x44, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, + 0x2d, 0x2d, 0x0a, 0x00, +}; +const size_t PUBLIC_KEY_LEN = 452; diff --git a/libraries/Update/keywords.txt b/libraries/Update/keywords.txt index 53544dbaf6c..5ddd5d46c91 100644 --- a/libraries/Update/keywords.txt +++ b/libraries/Update/keywords.txt @@ -1,5 +1,5 @@ ####################################### -# Syntax Coloring Map For Ultrasound +# Syntax Coloring Map For Update ####################################### ####################################### @@ -7,6 +7,9 @@ ####################################### Update KEYWORD1 +UpdaterVerifyClass KEYWORD1 +UpdaterRSAVerifier KEYWORD1 +UpdaterECDSAVerifier KEYWORD1 ####################################### # Methods and Functions (KEYWORD2) @@ -17,7 +20,44 @@ end KEYWORD2 write KEYWORD2 writeStream KEYWORD2 printError KEYWORD2 +installSignature KEYWORD2 +setMD5 KEYWORD2 +md5String KEYWORD2 +canRollBack KEYWORD2 +rollBack KEYWORD2 +onProgress KEYWORD2 +abort KEYWORD2 +setupCrypt KEYWORD2 +setCryptKey KEYWORD2 +setCryptMode KEYWORD2 ####################################### # Constants (LITERAL1) ####################################### + +UPDATE_ERROR_OK LITERAL1 +UPDATE_ERROR_WRITE LITERAL1 +UPDATE_ERROR_ERASE LITERAL1 +UPDATE_ERROR_READ LITERAL1 +UPDATE_ERROR_SPACE LITERAL1 +UPDATE_ERROR_SIZE LITERAL1 +UPDATE_ERROR_STREAM LITERAL1 +UPDATE_ERROR_MD5 LITERAL1 +UPDATE_ERROR_MAGIC_BYTE LITERAL1 +UPDATE_ERROR_ACTIVATE LITERAL1 +UPDATE_ERROR_NO_PARTITION LITERAL1 +UPDATE_ERROR_BAD_ARGUMENT LITERAL1 +UPDATE_ERROR_ABORT LITERAL1 +UPDATE_ERROR_DECRYPT LITERAL1 +UPDATE_ERROR_SIGN LITERAL1 +U_FLASH LITERAL1 +U_FLASHFS LITERAL1 +U_SPIFFS LITERAL1 +U_FATFS LITERAL1 +U_LITTLEFS LITERAL1 +SIGN_NONE LITERAL1 +SIGN_RSA LITERAL1 +SIGN_ECDSA LITERAL1 +HASH_SHA256 LITERAL1 +HASH_SHA384 LITERAL1 +HASH_SHA512 LITERAL1 diff --git a/libraries/Update/src/Update.h b/libraries/Update/src/Update.h index 5f52e6b3b73..86a1a5f1a26 100644 --- a/libraries/Update/src/Update.h +++ b/libraries/Update/src/Update.h @@ -11,6 +11,9 @@ #include #include #include "esp_partition.h" +#ifdef UPDATE_SIGN +#include "Updater_Signing.h" +#endif /* UPDATE_SIGN */ #define UPDATE_ERROR_OK (0) #define UPDATE_ERROR_WRITE (1) @@ -26,6 +29,7 @@ #define UPDATE_ERROR_BAD_ARGUMENT (11) #define UPDATE_ERROR_ABORT (12) #define UPDATE_ERROR_DECRYPT (13) +#define UPDATE_ERROR_SIGN (14) #define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF @@ -168,6 +172,16 @@ class UpdateClass { return _md5.getBytes(result); } +#ifdef UPDATE_SIGN + /* + Install signature verification for the update + Call this before begin() to enable signature verification + sign: Signature verifier to use (e.g., UpdaterRSAVerifier or UpdaterECDSAVerifier) + The hash type is determined from the verifier's configuration + */ + bool installSignature(UpdaterVerifyClass *sign); +#endif /* UPDATE_SIGN */ + //Helpers uint8_t getError() { return _error; @@ -287,6 +301,14 @@ class UpdateClass { size_t _cryptAddress; uint8_t _cryptCfg; #endif /* UPDATE_NOCRYPT */ + +#ifdef UPDATE_SIGN + SHA2Builder *_hash; + UpdaterVerifyClass *_sign; + uint8_t *_signatureBuffer; + size_t _signatureSize; + int _hashType; +#endif /* UPDATE_SIGN */ }; #if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE) diff --git a/libraries/Update/src/Updater.cpp b/libraries/Update/src/Updater.cpp index 3d3569f019e..e3d905a5f25 100644 --- a/libraries/Update/src/Updater.cpp +++ b/libraries/Update/src/Updater.cpp @@ -44,6 +44,10 @@ static const char *_err2str(uint8_t _error) { } else if (_error == UPDATE_ERROR_DECRYPT) { return ("Decryption error"); #endif /* UPDATE_NOCRYPT */ +#ifdef UPDATE_SIGN + } else if (_error == UPDATE_ERROR_SIGN) { + return ("Signature Verification Failed"); +#endif /* UPDATE_SIGN */ } return ("UNKNOWN"); } @@ -80,6 +84,10 @@ UpdateClass::UpdateClass() , _cryptMode(U_AES_DECRYPT_AUTO), _cryptAddress(0), _cryptCfg(0xf) #endif /* UPDATE_NOCRYPT */ +#ifdef UPDATE_SIGN + , + _hash(NULL), _sign(NULL), _signatureBuffer(NULL), _signatureSize(0), _hashType(-1) +#endif /* UPDATE_SIGN */ { } @@ -95,6 +103,17 @@ void UpdateClass::_reset() { if (_skipBuffer) { delete[] _skipBuffer; } +#ifdef UPDATE_SIGN + if (_signatureBuffer) { + delete[] _signatureBuffer; + _signatureBuffer = nullptr; + } + if (_hash && _hashType >= 0) { + // Clean up internally-created hash object + delete _hash; + _hash = nullptr; + } +#endif /* UPDATE_SIGN */ #ifndef UPDATE_NOCRYPT _cryptBuffer = nullptr; @@ -105,6 +124,9 @@ void UpdateClass::_reset() { _progress = 0; _size = 0; _command = U_FLASH; +#ifdef UPDATE_SIGN + _signatureSize = 0; +#endif /* UPDATE_SIGN */ if (_ledPin != -1) { digitalWrite(_ledPin, !_ledOn); // off @@ -127,6 +149,33 @@ bool UpdateClass::rollBack() { return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition); } +#ifdef UPDATE_SIGN +bool UpdateClass::installSignature(UpdaterVerifyClass *sign) { + if (_size > 0) { + log_w("Update already running"); + return false; + } + if (!sign) { + log_e("Invalid verifier"); + return false; + } + + int hashType = sign->getHashType(); + if (hashType != HASH_SHA256 && hashType != HASH_SHA384 && hashType != HASH_SHA512) { + log_e("Invalid hash type: %d", hashType); + return false; + } + + _sign = sign; + _hashType = hashType; + _signatureSize = 512; // Fixed signature size (padded to 512 bytes) + + [[maybe_unused]] const char *hashName = (hashType == HASH_SHA256) ? "SHA-256" : (hashType == HASH_SHA384) ? "SHA-384" : "SHA-512"; + log_i("Signature verification installed (hash: %s, signature size: %u bytes)", hashName, _signatureSize); + return true; +} +#endif /* UPDATE_SIGN */ + bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) { (void)label; @@ -143,11 +192,49 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con _target_md5 = emptyString; _md5 = MD5Builder(); +#ifdef UPDATE_SIGN + // Create and initialize signature hash if signature verification is enabled + if (_sign && _hashType >= 0) { + // Create the appropriate hash builder based on hashType + switch (_hashType) { + case HASH_SHA256: + _hash = new SHA256Builder(); + break; + case HASH_SHA384: + _hash = new SHA384Builder(); + break; + case HASH_SHA512: + _hash = new SHA512Builder(); + break; + default: + log_e("Invalid hash type"); + return false; + } + + if (_hash) { + _hash->begin(); + log_i("Signature hash initialized"); + } else { + log_e("Failed to create hash builder"); + return false; + } + } +#endif /* UPDATE_SIGN */ + if (size == 0) { _error = UPDATE_ERROR_SIZE; return false; } +#ifdef UPDATE_SIGN + // Validate size is large enough to contain firmware + signature + if (_signatureSize > 0 && size < _signatureSize) { + _error = UPDATE_ERROR_SIZE; + log_e("Size too small for signature: %u < %u", size, _signatureSize); + return false; + } +#endif /* UPDATE_SIGN */ + if (command == U_FLASH) { _partition = esp_ota_get_next_update_partition(NULL); if (!_partition) { @@ -462,6 +549,23 @@ bool UpdateClass::_writeBuffer() { #ifndef UPDATE_NOCRYPT } #endif /* UPDATE_NOCRYPT */ + +#ifdef UPDATE_SIGN + // Add data to signature hash if signature verification is enabled + // Only hash firmware bytes, not the signature bytes at the end + if (_hash && _signatureSize > 0) { + size_t firmwareSize = _size - _signatureSize; + if (_progress < firmwareSize) { + // Calculate how many bytes of this buffer are firmware (not signature) + size_t bytesToHash = _bufferLen; + if (_progress + _bufferLen > firmwareSize) { + bytesToHash = firmwareSize - _progress; + } + _hash->add(_buffer, bytesToHash); + } + } +#endif /* UPDATE_SIGN */ + _progress += _bufferLen; _bufferLen = 0; if (_progress_callback) { @@ -547,6 +651,41 @@ bool UpdateClass::end(bool evenIfRemaining) { } } +#ifdef UPDATE_SIGN + // Verify signature if signature verification is enabled + if (_hash && _sign && _signatureSize > 0) { + log_i("Verifying signature..."); + _hash->calculate(); + + // Allocate buffer for signature (max 512 bytes for RSA-4096) + const size_t maxSigSize = 512; + _signatureBuffer = new (std::nothrow) uint8_t[maxSigSize]; + if (!_signatureBuffer) { + log_e("Failed to allocate signature buffer"); + _abort(UPDATE_ERROR_SIGN); + return false; + } + + // Read signature from partition (last 512 bytes of what was written) + size_t firmwareSize = _size - _signatureSize; + log_d("Reading signature from offset %u (firmware size: %u, total size: %u)", firmwareSize, firmwareSize, _size); + if (!ESP.partitionRead(_partition, firmwareSize, (uint32_t *)_signatureBuffer, maxSigSize)) { + log_e("Failed to read signature from partition"); + _abort(UPDATE_ERROR_SIGN); + return false; + } + + // Verify the signature + if (!_sign->verify(_hash, _signatureBuffer, maxSigSize)) { + log_e("Signature verification failed"); + _abort(UPDATE_ERROR_SIGN); + return false; + } + + log_i("Signature verified successfully"); + } +#endif /* UPDATE_SIGN */ + return _verifyEnd(); } diff --git a/libraries/Update/src/Updater_Signing.cpp b/libraries/Update/src/Updater_Signing.cpp new file mode 100644 index 00000000000..47ba5ac492d --- /dev/null +++ b/libraries/Update/src/Updater_Signing.cpp @@ -0,0 +1,142 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifdef UPDATE_SIGN + +#include "Updater_Signing.h" +#include "mbedtls/pk.h" +#include "mbedtls/rsa.h" +#include "mbedtls/ecdsa.h" +#include "mbedtls/ecp.h" +#include "mbedtls/md.h" +#include "esp32-hal-log.h" + +// ==================== UpdaterRSAVerifier (using mbedtls) ==================== + +UpdaterRSAVerifier::UpdaterRSAVerifier(const uint8_t *pubkey, size_t pubkeyLen, int hashType) : _hashType(hashType), _valid(false) { + _ctx = new mbedtls_pk_context; + mbedtls_pk_init((mbedtls_pk_context *)_ctx); + + // Try to parse the public key + int ret = mbedtls_pk_parse_public_key((mbedtls_pk_context *)_ctx, pubkey, pubkeyLen); + if (ret != 0) { + log_e("Failed to parse RSA public key: -0x%04X", -ret); + return; + } + + // Verify it's an RSA key + if (mbedtls_pk_get_type((mbedtls_pk_context *)_ctx) != MBEDTLS_PK_RSA) { + log_e("Public key is not RSA"); + return; + } + + _valid = true; + log_i("RSA public key loaded successfully"); +} + +UpdaterRSAVerifier::~UpdaterRSAVerifier() { + if (_ctx) { + mbedtls_pk_free((mbedtls_pk_context *)_ctx); + delete (mbedtls_pk_context *)_ctx; + _ctx = nullptr; + } +} + +bool UpdaterRSAVerifier::verify(SHA2Builder *hash, const void *signature, size_t signatureLen) { + if (!_valid || !hash) { + log_e("Invalid RSA verifier or hash"); + return false; + } + + mbedtls_md_type_t md_type; + switch (_hashType) { + case HASH_SHA256: md_type = MBEDTLS_MD_SHA256; break; + case HASH_SHA384: md_type = MBEDTLS_MD_SHA384; break; + case HASH_SHA512: md_type = MBEDTLS_MD_SHA512; break; + default: + log_e("Invalid hash type"); + return false; + } + + // Get hash bytes from the builder + uint8_t hashBytes[64]; // Max hash size (SHA-512) + hash->getBytes(hashBytes); + + int ret = mbedtls_pk_verify((mbedtls_pk_context *)_ctx, md_type, hashBytes, hash->getHashSize(), (const unsigned char *)signature, signatureLen); + + if (ret == 0) { + log_i("RSA signature verified successfully"); + return true; + } else { + log_e("RSA signature verification failed: -0x%04X", -ret); + return false; + } +} + +// ==================== UpdaterECDSAVerifier (using mbedtls) ==================== + +UpdaterECDSAVerifier::UpdaterECDSAVerifier(const uint8_t *pubkey, size_t pubkeyLen, int hashType) : _hashType(hashType), _valid(false) { + _ctx = new mbedtls_pk_context; + mbedtls_pk_init((mbedtls_pk_context *)_ctx); + + // Try to parse the public key + int ret = mbedtls_pk_parse_public_key((mbedtls_pk_context *)_ctx, pubkey, pubkeyLen); + if (ret != 0) { + log_e("Failed to parse ECDSA public key: -0x%04X", -ret); + return; + } + + // Verify it's an ECDSA key + mbedtls_pk_type_t type = mbedtls_pk_get_type((mbedtls_pk_context *)_ctx); + if (type != MBEDTLS_PK_ECKEY && type != MBEDTLS_PK_ECDSA) { + log_e("Public key is not ECDSA"); + return; + } + + _valid = true; + log_i("ECDSA public key loaded successfully"); +} + +UpdaterECDSAVerifier::~UpdaterECDSAVerifier() { + if (_ctx) { + mbedtls_pk_free((mbedtls_pk_context *)_ctx); + delete (mbedtls_pk_context *)_ctx; + _ctx = nullptr; + } +} + +bool UpdaterECDSAVerifier::verify(SHA2Builder *hash, const void *signature, size_t signatureLen) { + if (!_valid || !hash) { + log_e("Invalid ECDSA verifier or hash"); + return false; + } + + mbedtls_md_type_t md_type; + switch (_hashType) { + case HASH_SHA256: md_type = MBEDTLS_MD_SHA256; break; + case HASH_SHA384: md_type = MBEDTLS_MD_SHA384; break; + case HASH_SHA512: md_type = MBEDTLS_MD_SHA512; break; + default: + log_e("Invalid hash type"); + return false; + } + + // Get hash bytes from the builder + uint8_t hashBytes[64]; // Max hash size (SHA-512) + hash->getBytes(hashBytes); + + int ret = mbedtls_pk_verify((mbedtls_pk_context *)_ctx, md_type, hashBytes, hash->getHashSize(), (const unsigned char *)signature, signatureLen); + + if (ret == 0) { + log_i("ECDSA signature verified successfully"); + return true; + } else { + log_e("ECDSA signature verification failed: -0x%04X", -ret); + return false; + } +} + +#endif // UPDATE_SIGN diff --git a/libraries/Update/src/Updater_Signing.h b/libraries/Update/src/Updater_Signing.h new file mode 100644 index 00000000000..907670ced9c --- /dev/null +++ b/libraries/Update/src/Updater_Signing.h @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ESP32UPDATER_SIGNING_H +#define ESP32UPDATER_SIGNING_H + +#ifdef UPDATE_SIGN + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// Signature schemes +#define SIGN_NONE 0 +#define SIGN_RSA 1 +#define SIGN_ECDSA 2 + +// Hash algorithms for signature verification +#define HASH_SHA256 0 +#define HASH_SHA384 1 +#define HASH_SHA512 2 + +// Signature sizes (in bytes) +#define RSA_2048_SIGNATURE_SIZE 256 +#define RSA_3072_SIGNATURE_SIZE 384 +#define RSA_4096_SIGNATURE_SIZE 512 +#define ECDSA_P256_SIGNATURE_SIZE 64 +#define ECDSA_P384_SIGNATURE_SIZE 96 + +// Hash sizes (in bytes) +#define SHA256_SIZE 32 +#define SHA384_SIZE 48 +#define SHA512_SIZE 64 + +#ifdef __cplusplus +} +#endif + +#ifdef __cplusplus + +class UpdaterVerifyClass { +public: + virtual bool verify(SHA2Builder *hash, const void *signature, size_t signatureLen) = 0; + virtual int getHashType() const = 0; + virtual ~UpdaterVerifyClass() {} +}; + +// Signature verifiers using mbedtls (required for public key cryptography) +class UpdaterRSAVerifier : public UpdaterVerifyClass { +public: + UpdaterRSAVerifier(const uint8_t *pubkey, size_t pubkeyLen, int hashType = HASH_SHA256); + ~UpdaterRSAVerifier(); + bool verify(SHA2Builder *hash, const void *signature, size_t signatureLen) override; + int getHashType() const override { + return _hashType; + } + +private: + void *_ctx; + int _hashType; + bool _valid; +}; + +class UpdaterECDSAVerifier : public UpdaterVerifyClass { +public: + UpdaterECDSAVerifier(const uint8_t *pubkey, size_t pubkeyLen, int hashType = HASH_SHA256); + ~UpdaterECDSAVerifier(); + bool verify(SHA2Builder *hash, const void *signature, size_t signatureLen) override; + int getHashType() const override { + return _hashType; + } + +private: + void *_ctx; + int _hashType; + bool _valid; +}; + +#endif // __cplusplus + +#endif // UPDATE_SIGN + +#endif // ESP32UPDATER_SIGNING_H diff --git a/tools/bin_signing.py b/tools/bin_signing.py new file mode 100755 index 00000000000..4367edcf300 --- /dev/null +++ b/tools/bin_signing.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +""" +OTA Update Signing Tool for ESP32 Arduino + +This script signs firmware binaries for secure OTA updates. +It supports both RSA and ECDSA signing schemes with various hash algorithms. + +Usage: + python bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin + python bin_signing.py --generate-key rsa-2048 --out private_key.pem + python bin_signing.py --extract-pubkey private_key.pem --out public_key.pem +""" + +import argparse +import sys +import os +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa, ec, padding +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import load_pem_private_key, load_pem_public_key + + +def generate_rsa_key(key_size, output_file): + """Generate an RSA private key""" + print(f"Generating RSA-{key_size} private key...") + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=key_size, + backend=default_backend() + ) + + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + with open(output_file, 'wb') as f: + f.write(pem) + + print(f"Private key saved to: {output_file}") + print("\nIMPORTANT: Keep this private key secure!") + print("Extract the public key with: python bin_signing.py --extract-pubkey", output_file) + + +def generate_ecdsa_key(curve_name, output_file): + """Generate an ECDSA private key""" + curves = { + 'p256': ec.SECP256R1(), + 'p384': ec.SECP384R1(), + } + + if curve_name not in curves: + print(f"Error: Unsupported curve. Supported curves: {', '.join(curves.keys())}") + sys.exit(1) + + print(f"Generating ECDSA-{curve_name.upper()} private key...") + private_key = ec.generate_private_key( + curves[curve_name], + backend=default_backend() + ) + + pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) + + with open(output_file, 'wb') as f: + f.write(pem) + + print(f"Private key saved to: {output_file}") + print("\nIMPORTANT: Keep this private key secure!") + print("Extract the public key with: python bin_signing.py --extract-pubkey", output_file) + + +def extract_public_key(private_key_file, output_file): + """Extract public key from private key""" + print(f"Extracting public key from {private_key_file}...") + + with open(private_key_file, 'rb') as f: + private_key = load_pem_private_key(f.read(), password=None, backend=default_backend()) + + public_key = private_key.public_key() + + pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ) + + with open(output_file, 'wb') as f: + f.write(pem) + + print(f"Public key saved to: {output_file}") + + # Also generate a C header file for embedding in Arduino sketch + header_file = os.path.splitext(output_file)[0] + '.h' + with open(header_file, 'w') as f: + f.write("// Public key for OTA signature verification\n") + f.write("// Include this in your Arduino sketch\n\n") + f.write("const uint8_t PUBLIC_KEY[] PROGMEM = {\n") + + # Add null terminator for mbedtls PEM parser + pem_bytes = pem + b'\x00' + for i in range(0, len(pem_bytes), 16): + chunk = pem_bytes[i:i+16] + hex_str = ', '.join(f'0x{b:02x}' for b in chunk) + f.write(f" {hex_str},\n") + + f.write("};\n") + f.write(f"const size_t PUBLIC_KEY_LEN = {len(pem_bytes)};\n") + + print(f"C header file saved to: {header_file}") + + +def sign_binary(binary_file, key_file, output_file, hash_algo='sha256'): + """Sign a binary file""" + print(f"Signing {binary_file} with {key_file}...") + + # Read the binary + with open(binary_file, 'rb') as f: + binary_data = f.read() + + print(f"Binary size: {len(binary_data)} bytes") + + # Load private key + with open(key_file, 'rb') as f: + private_key = load_pem_private_key(f.read(), password=None, backend=default_backend()) + + # Select hash algorithm + hash_algos = { + 'sha256': hashes.SHA256(), + 'sha384': hashes.SHA384(), + 'sha512': hashes.SHA512(), + } + + if hash_algo not in hash_algos: + print(f"Error: Unsupported hash algorithm. Supported: {', '.join(hash_algos.keys())}") + sys.exit(1) + + hash_obj = hash_algos[hash_algo] + + # Sign the binary + if isinstance(private_key, rsa.RSAPrivateKey): + print(f"Using RSA-PSS with {hash_algo.upper()}") + signature = private_key.sign( + binary_data, + padding.PSS( + mgf=padding.MGF1(hash_obj), + salt_length=padding.PSS.MAX_LENGTH + ), + hash_obj + ) + key_type = "RSA" + elif isinstance(private_key, ec.EllipticCurvePrivateKey): + print(f"Using ECDSA with {hash_algo.upper()}") + signature = private_key.sign( + binary_data, + ec.ECDSA(hash_obj) + ) + key_type = "ECDSA" + else: + print("Error: Unsupported key type") + sys.exit(1) + + print(f"Signature size: {len(signature)} bytes") + + # Pad signature to max size (512 bytes for RSA-4096) + max_sig_size = 512 + padded_signature = signature + b'\x00' * (max_sig_size - len(signature)) + + # Write signed binary (firmware + signature) + with open(output_file, 'wb') as f: + f.write(binary_data) + f.write(padded_signature) + + signed_size = len(binary_data) + len(padded_signature) + print(f"Signed binary saved to: {output_file}") + print(f"Signed binary size: {signed_size} bytes (firmware: {len(binary_data)}, signature: {len(padded_signature)})") + print(f"\nKey type: {key_type}") + print(f"Hash algorithm: {hash_algo.upper()}") + + +def verify_signature(binary_file, pubkey_file, hash_algo='sha256'): + """Verify a signed binary""" + print(f"Verifying signature of {binary_file} with {pubkey_file}...") + + # Read the signed binary + with open(binary_file, 'rb') as f: + signed_data = f.read() + + # The signature is the last 512 bytes (padded) + max_sig_size = 512 + if len(signed_data) < max_sig_size: + print("Error: File too small to contain signature") + sys.exit(1) + + binary_data = signed_data[:-max_sig_size] + signature = signed_data[-max_sig_size:] + + # Load public key + with open(pubkey_file, 'rb') as f: + public_key = load_pem_public_key(f.read(), backend=default_backend()) + + # Select hash algorithm + hash_algos = { + 'sha256': hashes.SHA256(), + 'sha384': hashes.SHA384(), + 'sha512': hashes.SHA512(), + } + + if hash_algo not in hash_algos: + print(f"Error: Unsupported hash algorithm. Supported: {', '.join(hash_algos.keys())}") + sys.exit(1) + + hash_obj = hash_algos[hash_algo] + + # Remove padding from signature + signature = signature.rstrip(b'\x00') + + # Verify the signature + try: + if isinstance(public_key, rsa.RSAPublicKey): + print(f"Verifying RSA-PSS signature with {hash_algo.upper()}") + public_key.verify( + signature, + binary_data, + padding.PSS( + mgf=padding.MGF1(hash_obj), + salt_length=padding.PSS.MAX_LENGTH + ), + hash_obj + ) + elif isinstance(public_key, ec.EllipticCurvePublicKey): + print(f"Verifying ECDSA signature with {hash_algo.upper()}") + public_key.verify( + signature, + binary_data, + ec.ECDSA(hash_obj) + ) + else: + print("Error: Unsupported key type") + sys.exit(1) + + print("✓ Signature verification SUCCESSFUL!") + return True + except Exception as e: + print(f"✗ Signature verification FAILED: {e}") + return False + + +def main(): + parser = argparse.ArgumentParser( + description='OTA Update Signing Tool for ESP32 Arduino', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + Generate RSA-2048 key: + python bin_signing.py --generate-key rsa-2048 --out private_key.pem + + Generate ECDSA-P256 key: + python bin_signing.py --generate-key ecdsa-p256 --out private_key.pem + + Extract public key: + python bin_signing.py --extract-pubkey private_key.pem --out public_key.pem + + Sign firmware: + python bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin + + Sign with SHA-384: + python bin_signing.py --bin firmware.bin --key private_key.pem --out firmware_signed.bin --hash sha384 + + Verify signed firmware: + python bin_signing.py --verify firmware_signed.bin --pubkey public_key.pem + ''' + ) + + parser.add_argument('--generate-key', metavar='TYPE', + help='Generate a new key (rsa-2048, rsa-3072, rsa-4096, ecdsa-p256, ecdsa-p384)') + parser.add_argument('--extract-pubkey', metavar='PRIVATE_KEY', + help='Extract public key from private key') + parser.add_argument('--bin', metavar='FILE', + help='Binary file to sign') + parser.add_argument('--key', metavar='FILE', + help='Private key file (PEM format)') + parser.add_argument('--pubkey', metavar='FILE', + help='Public key file for verification (PEM format)') + parser.add_argument('--out', metavar='FILE', + help='Output file') + parser.add_argument('--hash', default='sha256', choices=['sha256', 'sha384', 'sha512'], + help='Hash algorithm (default: sha256)') + parser.add_argument('--verify', metavar='FILE', + help='Verify a signed binary') + + args = parser.parse_args() + + if args.generate_key: + if not args.out: + print("Error: --out required for key generation") + sys.exit(1) + + key_type = args.generate_key.lower() + if key_type.startswith('rsa-'): + key_size = int(key_type.split('-')[1]) + generate_rsa_key(key_size, args.out) + elif key_type.startswith('ecdsa-'): + curve = key_type.split('-')[1] + generate_ecdsa_key(curve, args.out) + else: + print("Error: Invalid key type. Supported: rsa-2048, rsa-3072, rsa-4096, ecdsa-p256, ecdsa-p384") + sys.exit(1) + + elif args.extract_pubkey: + if not args.out: + print("Error: --out required for public key extraction") + sys.exit(1) + extract_public_key(args.extract_pubkey, args.out) + + elif args.verify: + if not args.pubkey: + print("Error: --pubkey required for verification") + sys.exit(1) + verify_signature(args.verify, args.pubkey, args.hash) + + elif args.bin and args.key: + if not args.out: + print("Error: --out required for signing") + sys.exit(1) + sign_binary(args.bin, args.key, args.out, args.hash) + + else: + parser.print_help() + sys.exit(1) + + +if __name__ == '__main__': + main() +