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,94 @@
// Copyright 2024 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <WiFi.h>
#include <ESPmDNS.h>
#include <NetworkUdp.h>
#include <ArduinoOTA.h>
const char *ssid = "..........";
const char *password = "..........";
uint32_t last_ota_time = 0;
void setup() {
Serial.begin(115200);
Serial.println("Booting");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println("Connection Failed! Rebooting...");
delay(5000);
ESP.restart();
}
// Port defaults to 3232
// ArduinoOTA.setPort(3232);
// Hostname defaults to esp3232-[MAC]
// ArduinoOTA.setHostname("myesp32");
// Password can be set with plain text (will be hashed internally)
// The authentication uses PBKDF2-HMAC-SHA256 with 10,000 iterations
// ArduinoOTA.setPassword("admin");
// Or set password with pre-hashed value (SHA256 hash of "admin")
// SHA256(admin) = 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
// ArduinoOTA.setPasswordHash("8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918");
ArduinoOTA
.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH) {
type = "sketch";
} else { // U_SPIFFS
type = "filesystem";
}
// NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
Serial.println("Start updating " + type);
})
.onEnd([]() {
Serial.println("\nEnd");
})
.onProgress([](unsigned int progress, unsigned int total) {
if (millis() - last_ota_time > 500) {
Serial.printf("Progress: %u%%\n", (progress / (total / 100)));
last_ota_time = millis();
}
})
.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) {
Serial.println("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
Serial.println("Begin Failed");
} 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");
}
});
ArduinoOTA.begin();
Serial.println("Ready");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void loop() {
ArduinoOTA.handle();
}
@@ -0,0 +1,3 @@
requires_any:
- CONFIG_SOC_WIFI_SUPPORTED=y
- CONFIG_ESP_WIFI_REMOTE_ENABLED=y
@@ -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;
+47
View File
@@ -0,0 +1,47 @@
#######################################
# Syntax Coloring Map For Ultrasound
#######################################
#######################################
# Datatypes (KEYWORD1)
#######################################
ArduinoOTA KEYWORD1
ArduinoOTAClass KEYWORD1
#######################################
# Methods and Functions (KEYWORD2)
#######################################
begin KEYWORD2
end KEYWORD2
handle KEYWORD2
onStart KEYWORD2
onEnd KEYWORD2
onError KEYWORD2
onProgress KEYWORD2
setPort KEYWORD2
setHostname KEYWORD2
getHostname KEYWORD2
setPassword KEYWORD2
setPasswordHash KEYWORD2
setPartitionLabel KEYWORD2
getPartitionLabel KEYWORD2
setRebootOnSuccess KEYWORD2
setMdnsEnabled KEYWORD2
setSignature KEYWORD2
getCommand KEYWORD2
setTimeout KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
OTA_IDLE LITERAL1
OTA_WAITAUTH LITERAL1
OTA_RUNUPDATE LITERAL1
OTA_AUTH_ERROR LITERAL1
OTA_BEGIN_ERROR LITERAL1
OTA_CONNECT_ERROR LITERAL1
OTA_RECEIVE_ERROR LITERAL1
OTA_END_ERROR LITERAL1
+9
View File
@@ -0,0 +1,9 @@
name=ArduinoOTA
version=3.3.7
author=Ivan Grokhotkov and Hristo Gochkov
maintainer=Hristo Gochkov <hristo@espressif.com>
sentence=Enables Over The Air upgrades, via wifi and espota.py UDP request/TCP download.
paragraph=With this library you can enable your sketch to be upgraded over network. Includes mdns announces to get discovered by the arduino IDE.
category=Communication
url=
architectures=esp32
+499
View File
@@ -0,0 +1,499 @@
// Copyright 2024 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef LWIP_OPEN_SRC
#define LWIP_OPEN_SRC
#endif
#include <functional>
#include "ArduinoOTA.h"
#include "NetworkClient.h"
#include "ESPmDNS.h"
#include "HEXBuilder.h"
#include "SHA2Builder.h"
#include "PBKDF2_HMACBuilder.h"
#include "Update.h"
// #define OTA_DEBUG Serial
ArduinoOTAClass::ArduinoOTAClass(UpdateClass *updater)
: _updater(updater), _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)
#ifdef UPDATE_SIGN
,
_sign(NULL)
#endif /* UPDATE_SIGN */
{
}
ArduinoOTAClass::~ArduinoOTAClass() {
end();
}
ArduinoOTAClass &ArduinoOTAClass::onStart(THandlerFunction fn) {
_start_callback = fn;
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::onEnd(THandlerFunction fn) {
_end_callback = fn;
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::onProgress(THandlerFunction_Progress fn) {
_progress_callback = fn;
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::onError(THandlerFunction_Error fn) {
_error_callback = fn;
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::setPort(uint16_t port) {
if (!_initialized && !_port && port) {
_port = port;
}
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::setHostname(const char *hostname) {
if (!_initialized && !_hostname.length() && hostname) {
_hostname = hostname;
}
return *this;
}
String ArduinoOTAClass::getHostname() {
return _hostname;
}
ArduinoOTAClass &ArduinoOTAClass::setPassword(const char *password) {
if (_state == OTA_IDLE && password) {
// Hash the password with SHA256 for storage (not plain text)
SHA256Builder pass_hash;
pass_hash.begin();
pass_hash.add(password);
pass_hash.calculate();
_password.clear();
_password = pass_hash.toString();
}
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::setPasswordHash(const char *password) {
if (_state == OTA_IDLE && password) {
size_t len = strlen(password);
bool is_hex = HEXBuilder::isHexString(password, len);
if (!is_hex) {
log_e("Invalid password hash. Expected hex string (0-9, a-f, A-F).");
return *this;
}
if (len == 32) {
// Warn if MD5 hash is detected (32 hex characters)
log_w("MD5 password hash detected. MD5 is deprecated and insecure.");
log_w("Please use setPassword() with plain text or setPasswordHash() with SHA256 hash (64 chars).");
log_w("To generate SHA256: echo -n 'yourpassword' | sha256sum");
} else if (len == 64) {
log_i("Using SHA256 password hash.");
} else {
log_e("Invalid password hash length. Expected 32 (deprecated MD5) or 64 (SHA256) characters.");
return *this;
}
// Store the pre-hashed password directly
_password.clear();
_password = password;
}
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::setPartitionLabel(const char *partition_label) {
if (_state == OTA_IDLE && partition_label) {
_partition_label.clear();
_partition_label = partition_label;
}
return *this;
}
String ArduinoOTAClass::getPartitionLabel() {
return _partition_label;
}
ArduinoOTAClass &ArduinoOTAClass::setRebootOnSuccess(bool reboot) {
_rebootOnSuccess = reboot;
return *this;
}
ArduinoOTAClass &ArduinoOTAClass::setMdnsEnabled(bool enabled) {
_mdnsEnabled = 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");
return;
}
if (!_port) {
_port = 3232;
}
if (!_udp_ota.begin(_port)) {
log_e("udp bind failed");
return;
}
if (!_hostname.length()) {
char tmp[20];
uint8_t mac[6];
Network.macAddress(mac);
sprintf(tmp, "esp32-%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
_hostname = tmp;
}
#ifdef CONFIG_MDNS_MAX_INTERFACES
if (_mdnsEnabled) {
MDNS.begin(_hostname.c_str());
MDNS.enableArduino(_port, (_password.length() > 0));
}
#endif
_initialized = true;
_state = OTA_IDLE;
log_i("OTA server at: %s.local:%u", _hostname.c_str(), _port);
}
int ArduinoOTAClass::parseInt() {
char data[INT_BUFFER_SIZE];
uint8_t index = 0;
char value;
while (_udp_ota.peek() == ' ') {
_udp_ota.read();
}
while (index < INT_BUFFER_SIZE - 1) {
value = _udp_ota.peek();
if (value < '0' || value > '9') {
data[index++] = '\0';
return atoi(data);
}
data[index++] = _udp_ota.read();
}
return 0;
}
String ArduinoOTAClass::readStringUntil(char end) {
String res = "";
int value;
while (true) {
value = _udp_ota.read();
if (value <= 0 || value == end) {
return res;
}
res += (char)value;
}
return res;
}
void ArduinoOTAClass::_onRx() {
if (_state == OTA_IDLE) {
int cmd = parseInt();
if (cmd != U_FLASH && cmd != U_FLASHFS) {
return;
}
_cmd = cmd;
_ota_port = parseInt();
_size = parseInt();
_udp_ota.read();
_md5 = readStringUntil('\n');
_md5.trim();
if (_md5.length() != 32) { // MD5 produces 32 character hex string for firmware integrity
log_e("bad md5 length");
return;
}
if (_password.length()) {
// Generate a random challenge (nonce)
SHA256Builder nonce_sha256;
nonce_sha256.begin();
nonce_sha256.add(String(micros()) + String(random(1000000)));
nonce_sha256.calculate();
_nonce = nonce_sha256.toString();
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
_udp_ota.printf("AUTH %s", _nonce.c_str());
_udp_ota.endPacket();
_state = OTA_WAITAUTH;
return;
} else {
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
_udp_ota.print("OK");
_udp_ota.endPacket();
_ota_ip = _udp_ota.remoteIP();
_state = OTA_RUNUPDATE;
}
} else if (_state == OTA_WAITAUTH) {
int cmd = parseInt();
if (cmd != U_AUTH) {
log_e("%d was expected. got %d instead", U_AUTH, cmd);
_state = OTA_IDLE;
return;
}
_udp_ota.read();
String cnonce = readStringUntil(' ');
String response = readStringUntil('\n');
if (cnonce.length() != 64 || response.length() != 64) { // SHA256 produces 64 character hex string
log_e("auth param fail");
_state = OTA_IDLE;
return;
}
// Verify the challenge/response using PBKDF2-HMAC-SHA256
// The client should derive a key using PBKDF2-HMAC-SHA256 with:
// - password: the OTA password (or its hash if using setPasswordHash)
// - salt: nonce + cnonce
// - iterations: 10000 (or configurable)
// Then hash the challenge with the derived key
String salt = _nonce + ":" + cnonce;
SHA256Builder sha256;
// Use the stored password hash for PBKDF2 derivation
PBKDF2_HMACBuilder pbkdf2(&sha256, _password, salt, 10000);
pbkdf2.begin();
pbkdf2.calculate();
String derived_key = pbkdf2.toString();
// Create challenge: derived_key + nonce + cnonce
String challenge = derived_key + ":" + _nonce + ":" + cnonce;
SHA256Builder challenge_sha256;
challenge_sha256.begin();
challenge_sha256.add(challenge);
challenge_sha256.calculate();
String expected_response = challenge_sha256.toString();
if (expected_response.equals(response)) {
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
_udp_ota.print("OK");
_udp_ota.endPacket();
_ota_ip = _udp_ota.remoteIP();
_state = OTA_RUNUPDATE;
} else {
_udp_ota.beginPacket(_udp_ota.remoteIP(), _udp_ota.remotePort());
_udp_ota.print("Authentication Failed");
log_w("Authentication Failed");
_udp_ota.endPacket();
if (_error_callback) {
_error_callback(OTA_AUTH_ERROR);
}
_state = OTA_IDLE;
}
}
}
void ArduinoOTAClass::_runUpdate() {
if (!_updater) {
log_e("UpdateClass is NULL!");
return;
}
#ifdef UPDATE_SIGN
// Install signature verification if enabled
if (_sign) {
if (!_updater->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 (!_updater->begin(_size, _cmd, -1, LOW, partition_label)) {
log_e("Begin ERROR: %s", _updater->errorString());
if (_error_callback) {
_error_callback(OTA_BEGIN_ERROR);
}
_state = OTA_IDLE;
return;
}
_updater->setMD5(_md5.c_str()); // Note: Update library still uses MD5 for firmware integrity, this is separate from authentication
if (_start_callback) {
_start_callback();
}
if (_progress_callback) {
_progress_callback(0, _size);
}
NetworkClient client;
if (!client.connect(_ota_ip, _ota_port)) {
if (_error_callback) {
_error_callback(OTA_CONNECT_ERROR);
}
_state = OTA_IDLE;
}
uint32_t written = 0, total = 0, tried = 0;
while (!_updater->isFinished() && client.connected()) {
size_t waited = _ota_timeout;
size_t available = client.available();
while (!available && waited) {
delay(1);
waited -= 1;
available = client.available();
}
if (!waited) {
if (written && tried++ < 3) {
log_i("Try[%u]: %u", tried, written);
if (!client.printf("%lu", written)) {
log_e("failed to respond");
_state = OTA_IDLE;
break;
}
continue;
}
log_e("Receive Failed");
if (_error_callback) {
_error_callback(OTA_RECEIVE_ERROR);
}
_state = OTA_IDLE;
_updater->abort();
return;
}
if (!available) {
log_e("No Data: %u", waited);
_state = OTA_IDLE;
break;
}
tried = 0;
static uint8_t buf[1460];
if (available > 1460) {
available = 1460;
}
size_t r = client.read(buf, available);
if (r != available) {
log_w("didn't read enough! %u != %u", r, available);
if ((int32_t)r < 0) {
delay(1);
continue; //let's not try to write 4 gigabytes when client.read returns -1
}
}
written = _updater->write(buf, r);
if (written > 0) {
if (written != r) {
log_w("didn't write enough! %u != %u", written, r);
}
if (!client.printf("%lu", written)) {
log_w("failed to respond");
}
total += written;
if (_progress_callback) {
_progress_callback(total, _size);
}
} else {
log_e("Write ERROR: %s", _updater->errorString());
}
}
if (_updater->end()) {
client.print("OK");
client.stop();
delay(10);
if (_end_callback) {
_end_callback();
}
if (_rebootOnSuccess) {
//let serial/network finish tasks that might be given in _end_callback
delay(100);
ESP.restart();
}
} else {
if (_error_callback) {
_error_callback(OTA_END_ERROR);
}
_updater->printError(client);
client.stop();
delay(10);
log_e("Update ERROR: %s", _updater->errorString());
_state = OTA_IDLE;
}
}
void ArduinoOTAClass::end() {
_initialized = false;
_udp_ota.stop();
#ifdef CONFIG_MDNS_MAX_INTERFACES
if (_mdnsEnabled) {
MDNS.end();
}
#endif
_state = OTA_IDLE;
log_i("OTA server stopped.");
}
void ArduinoOTAClass::handle() {
if (!_initialized) {
return;
}
if (_state == OTA_RUNUPDATE) {
_runUpdate();
_state = OTA_IDLE;
}
if (_udp_ota.parsePacket()) {
_onRx();
}
_udp_ota.clear(); // always clear, even zero length packets must be cleared.
}
int ArduinoOTAClass::getCommand() {
return _cmd;
}
void ArduinoOTAClass::setTimeout(int timeoutInMillis) {
_ota_timeout = timeoutInMillis;
}
ArduinoOTAClass &ArduinoOTAClass::setUpdaterInstance(UpdateClass *updater) {
_updater = updater;
return *this;
}
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_ARDUINOOTA)
ArduinoOTAClass ArduinoOTA;
#endif
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2024 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef __ARDUINO_OTA_H
#define __ARDUINO_OTA_H
#include "Network.h"
#include "Update.h"
#include <functional>
#define INT_BUFFER_SIZE 16
typedef enum {
OTA_IDLE,
OTA_WAITAUTH,
OTA_RUNUPDATE
} ota_state_t;
typedef enum {
OTA_AUTH_ERROR,
OTA_BEGIN_ERROR,
OTA_CONNECT_ERROR,
OTA_RECEIVE_ERROR,
OTA_END_ERROR
} ota_error_t;
class ArduinoOTAClass {
public:
typedef std::function<void(void)> THandlerFunction;
typedef std::function<void(ota_error_t)> THandlerFunction_Error;
typedef std::function<void(unsigned int, unsigned int)> THandlerFunction_Progress;
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
ArduinoOTAClass(UpdateClass *updater = &Update);
#else
ArduinoOTAClass(UpdateClass *updater = nullptr);
#endif
~ArduinoOTAClass();
//Sets the service port. Default 3232
ArduinoOTAClass &setPort(uint16_t port);
//Sets the device hostname. Default esp32-xxxxxx
ArduinoOTAClass &setHostname(const char *hostname);
String getHostname();
//Sets the password that will be required for OTA. Default NULL
ArduinoOTAClass &setPassword(const char *password);
//Sets the password as above but in the form SHA256(password). Default NULL
ArduinoOTAClass &setPasswordHash(const char *password);
//Sets the partition label to write to when updating SPIFFS. Default NULL
ArduinoOTAClass &setPartitionLabel(const char *partition_label);
String getPartitionLabel();
//Sets instance of UpdateClass to perform updating operations
ArduinoOTAClass &setUpdaterInstance(UpdateClass *updater);
//Sets if the device should be rebooted after successful update. Default true
ArduinoOTAClass &setRebootOnSuccess(bool reboot);
//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);
//This callback will be called when OTA has finished
ArduinoOTAClass &onEnd(THandlerFunction fn);
//This callback will be called when OTA encountered Error
ArduinoOTAClass &onError(THandlerFunction_Error fn);
//This callback will be called when OTA is receiving data
ArduinoOTAClass &onProgress(THandlerFunction_Progress fn);
//Starts the ArduinoOTA service
void begin();
//Ends the ArduinoOTA service
void end();
//Call this in loop() to run the service
void handle();
//Gets update command type after OTA has started. Either U_FLASH or U_SPIFFS
int getCommand();
void setTimeout(int timeoutInMillis);
private:
UpdateClass *_updater;
int _port;
String _password;
String _hostname;
String _partition_label;
String _nonce;
NetworkUDP _udp_ota;
bool _initialized;
bool _rebootOnSuccess;
bool _mdnsEnabled;
ota_state_t _state;
int _size;
int _cmd;
int _ota_port;
int _ota_timeout;
IPAddress _ota_ip;
String _md5;
THandlerFunction _start_callback;
THandlerFunction _end_callback;
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);
String readStringUntil(char end);
};
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_ARDUINOOTA)
extern ArduinoOTAClass ArduinoOTA;
#endif
#endif /* __ARDUINO_OTA_H */