3.3.7
This commit is contained in:
@@ -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;
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user