This commit is contained in:
2026-05-22 21:52:50 +03:00
commit be7c60e4dd
1854 changed files with 583428 additions and 0 deletions
@@ -0,0 +1,323 @@
# 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
<!-- vale Espressif-latest.Units = NO -->
- **OTA-capable partition scheme** (e.g., "Minimal SPIFFS (1.9MB APP with OTA)")
<!-- vale Espressif-latest.Units = YES -->
## Quick Start Guide
### 1. Generate Cryptographic Keys
```bash
# Navigate to Arduino ESP32 tools directory
cd <ARDUINO_ROOT>/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
<!-- vale Espressif-latest.TermsSingleCorrectSpelling = NO -->
3. Configure WiFi credentials:
```cpp
const char *ssid = "YourWiFiSSID";
const char *password = "YourWiFiPassword";
```
<!-- vale Espressif-latest.TermsSingleCorrectSpelling = YES -->
<!-- vale Espressif-latest.Units = NO -->
4. Select appropriate partition scheme:
- **Tools → Partition Scheme → "Minimal SPIFFS (1.9MB APP with OTA)"**
<!-- vale Espressif-latest.Units = YES -->
### 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 <ARDUINO_ROOT>/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 <ARDUINO_ROOT>/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 <ARDUINO_ROOT>/tools/espota.py -i <device-ip> -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**:
<!-- vale Espressif-latest.Units = NO -->
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
<!-- vale Espressif-latest.Units = YES -->
### "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 Wi-Fi 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 Wi-Fi signal strength is adequate
4. If using password protection, ensure the password is correct
5. Try: `python <ARDUINO_ROOT>/tools/espota.py -i <device-ip> -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
<!-- vale Espressif-latest.Cursing = NO -->
- ❌ 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)
<!-- vale Espressif-latest.Cursing = YES -->
### 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.
@@ -0,0 +1,204 @@
/*
* 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 <ARDUINO_ROOT>/tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem
* python <ARDUINO_ROOT>/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 <ARDUINO_ROOT>/tools/bin_signing.py --bin build/<file>.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 <ARDUINO_ROOT>/tools/espota.py -i <device-ip> -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 <WiFi.h>
#include <ESPmDNS.h>
#include <NetworkUdp.h>
#include <ArduinoOTA.h>
// 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();
}
@@ -0,0 +1 @@
-DUPDATE_SIGN
@@ -0,0 +1,3 @@
requires_any:
- CONFIG_SOC_WIFI_SUPPORTED=y
- CONFIG_ESP_WIFI_REMOTE_ENABLED=y
@@ -0,0 +1,30 @@
// 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 <ARDUINO_ROOT>/tools/bin_signing.py --generate-key rsa-2048 --out private_key.pem
// python <ARDUINO_ROOT>/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;