Project Overview
This is a comprehensive Over-The-Air (OTA) Firmware Update System designed for ESP32 embedded devices. The system enables secure, incremental firmware updates over HTTPS using binary delta patching (bsdiff algorithm), cryptographic signatures, and per-device encryption.
The project consists of three main parts:
- Server Component (FastAPI Python backend) - Handles firmware storage, patch generation, signing, and encryption
- Client Component (ESP32 Arduino C++ code) - Embedded firmware that checks for updates and applies patches
- Storage System - File-based storage for firmware binaries, patches, and signatures
Key Features
- Incremental Updates: Uses bsdiff binary patching to send only the differences between firmware versions, reducing bandwidth and update time
- Cryptographic Security: ECC (Elliptic Curve Cryptography) signatures ensure patch authenticity and integrity
- Per-Device Encryption: AES-256-CCM encryption with device-specific keys protects patches during transmission
- HTTPS Communication: Secure communication between devices and server
- Streaming Downloads: Efficient memory usage with streaming patch downloads and verification
- OLED Display Support: Visual feedback during update process with dual OLED displays
Project Structure
Root Directory Structure
/var/www/ota-server/ Directory
Main project directory containing the Python FastAPI server and ESP32 client code.
| File/Folder | Purpose | Type |
|---|---|---|
server.py |
FastAPI application with REST API endpoints for OTA updates | Python |
patch_utils.py |
Wrapper for bsdiff binary patching tool | Python |
crypto_utils.py |
AES encryption and ECC signature generation functions | Python |
requirements.txt |
Python package dependencies (FastAPI, pycryptodome, etc.) | Config |
src/main.cpp |
ESP32 Arduino application - handles WiFi, downloads, decryption, verification, and OTA updates | C++ |
lib/bspatch_stub/ |
Library for applying binary patches to firmware (currently a stub that writes full images) | C++ |
/var/www/ota-server/storage/ Directory
Centralized storage directory for all firmware-related files. Organized into subdirectories for different file types.
| Subdirectory | Contents | File Naming Convention |
|---|---|---|
firmware/ |
Complete firmware binary files | firmware_{version}.binExample: firmware_1.0.0.bin |
patches/ |
Binary delta patch files (bsdiff format) | patch_{old_version}_to_{new_version}.bsdiffExample: patch_1.0.0_to_1.0.1.bsdiffEncrypted: patch_1.0.0_to_1.0.1.bsdiff.enc |
signatures/ |
ECC digital signatures for patches | patch_{old_version}_to_{new_version}.sigExample: patch_1.0.0_to_1.0.1.sig |
/var/www/server/ Directory
Additional server directory containing reference code for cryptographic operations.
| File | Purpose |
|---|---|
main.py |
Reference implementation showing patch signing and encryption workflow (example code for understanding the process) |
System Architecture
High-Level Architecture
The OTA update system follows a client-server architecture where:
- Server: FastAPI Python application running on a VPS/cloud server
- Client: ESP32 microcontroller running Arduino-based firmware
- Communication: HTTPS REST API for all interactions
Update Flow Diagram
Administrator uploads new firmware version to server via
/upload_firmware/ endpoint
Server generates binary delta patch using bsdiff comparing old and new firmware versions via
/make_patch/ endpoint
Server signs patch with ECC private key and encrypts with device-specific AES-256 key
Administrator queues update for specific device via
/instruct_update/ endpoint
ESP32 device periodically checks
/check_update/ endpoint to see if update is available
Device downloads encrypted patch file from
/get_patch/{old}/{new}/{device_id} endpoint
Device downloads signature file from
/get_signature/{old}/{new} endpoint
Device decrypts patch using its per-device AES-256 key (AES-CCM mode)
Device verifies patch signature using server's public ECC key stored in LittleFS
Device applies patch to current firmware using bspatch algorithm (currently stub implementation)
New firmware is written to OTA partition and device reboots into new firmware
Component Details
Server Components
server.py - FastAPI Application
Purpose: Main web server providing REST API endpoints for OTA operations.
Key Components:
- Device Database: In-memory dictionary storing device versions and pending updates (in production, use a real database)
- Storage Paths: Configurable paths for firmware, patches, and signatures
- Device Keys: Per-device AES encryption keys (currently hardcoded - should be in secure storage)
patch_utils.py - Binary Patching
Purpose: Generates binary delta patches using the bsdiff algorithm.
Function: generate_patch(old_bin, new_bin, patch_path)
- Calls external
bsdiffcommand-line tool - Creates patch file containing only differences between old and new firmware
- Significantly reduces file size compared to full firmware binary
bsdiff and bspatch tools must be installed on the server system.
crypto_utils.py - Cryptography
Purpose: Provides encryption and digital signature functions.
Functions:
-
encrypt_patch(patch_bytes, device_key)- Uses AES-256-CCM mode encryption
- Generates random 13-byte nonce
- Produces 16-byte authentication tag
- Output format:
[nonce(13 bytes)][tag(16 bytes)][ciphertext]
-
sign_patch(patch_bytes)- Reads ECC private key from
ecc_private.pem - Computes SHA-256 hash of patch
- Signs hash using ECDSA (FIPS-186-3)
- Returns binary DER-encoded signature
- Reads ECC private key from
ecc_private.pem file must be kept secure and never exposed publicly.
Client Components (ESP32)
src/main.cpp - ESP32 Main Application
Purpose: ESP32 firmware that handles OTA update process on the device side.
Key Features:
- WiFi Management: Connects to configured WiFi network
- HTTPS Client: Secure communication with server using WiFiClientSecure
- File System: Uses LittleFS for storing patches, signatures, and server public key
- OLED Display: Dual OLED displays (0x3C and 0x3D) for status and progress feedback
- Update Flow: Complete update workflow from download to OTA installation
Key Functions:
https_download_to_file()- Streams HTTP/HTTPS downloads to LittleFS filesaes_ccm_decrypt_file()- Decrypts encrypted patch files using mbedTLSverify_signature_file()- Verifies ECC signatures using mbedTLSrun_update_flow()- Orchestrates complete update processapply_patch_and_write_ota()- Applies patch and writes to OTA partition
Configuration:
- WiFi SSID and password
- Server host and port
- Device ID
- Device AES encryption key (must match server)
lib/bspatch_stub/ - Patch Application Library
Purpose: Library for applying binary patches to firmware.
Current Implementation:
- Currently a stub implementation that treats the patch file as a complete firmware image
- Reads patch file and writes directly to OTA partition
- Does not actually apply binary delta patching yet
Future Implementation:
- Should implement streaming bspatch algorithm
- Read from current firmware partition
- Apply patch bytes to generate new firmware
- Stream output to OTA partition
API Endpoints
Server Endpoints
Purpose: Upload a new firmware binary to the server
Parameters:
version(form data) - Version string (e.g., "1.0.1")file(form data) - Firmware binary file
Response:
{
"status": "ok",
"stored": "storage/firmware/firmware_1.0.1.bin"
}
Purpose: Generate binary delta patch between two firmware versions
Parameters:
old_version(form data) - Source version (e.g., "1.0.0")new_version(form data) - Target version (e.g., "1.0.1")
Response:
{
"status": "ok",
"patch": "storage/patches/patch_1.0.0_to_1.0.1.bsdiff.enc",
"signature": "storage/signatures/patch_1.0.0_to_1.0.1.sig"
}
Process: This endpoint generates the patch, signs it, and encrypts it for the default device.
Purpose: Queue an update for a specific device
Parameters:
device_id(form data) - Device identifier (e.g., "esp32_01")old_version(form data) - Current device versionnew_version(form data) - Target version to update to
Response:
{
"status": "update_queued"
}
Purpose: Check if an update is available for a device
Parameters:
device_id(query) - Device identifierversion(query) - Current device version
Response (Update Available):
{
"update_available": true,
"old_version": "1.0.0",
"new_version": "1.0.1",
"patch_url": "/get_patch/1.0.0/1.0.1/esp32_01",
"signature_url": "/get_signature/1.0.0/1.0.1"
}
Response (No Update):
{
"update_available": false
}
Purpose: Download encrypted patch file for a specific device
Parameters:
old(path) - Old versionnew(path) - New versiondevice_id(path) - Device identifier
Response: Binary file (encrypted patch)
Format: [nonce(13 bytes)][tag(16 bytes)][ciphertext]
Purpose: Download digital signature for a patch
Parameters:
old(path) - Old versionnew(path) - New version
Response: Binary file (DER-encoded ECC signature)
Security Features
Cryptographic Protection
1. Digital Signatures (ECC-ECDSA)
- Algorithm: Elliptic Curve Digital Signature Algorithm (ECDSA)
- Key Type: ECC private/public key pair
- Hash: SHA-256
- Purpose: Ensures patch authenticity and integrity
- Process: Server signs patch hash with private key, device verifies with public key
2. Encryption (AES-256-CCM)
- Algorithm: Advanced Encryption Standard (AES) with CCM mode
- Key Size: 256 bits (32 bytes)
- Mode: CCM (Counter with CBC-MAC) - provides both encryption and authentication
- Nonce: 13-byte random nonce (per-patch, ensures uniqueness)
- MAC Tag: 16-byte authentication tag
- Purpose: Protects patches during transmission and storage
- Per-Device: Each device has its own encryption key
3. HTTPS Communication
- All server communication uses HTTPS
- Protects against man-in-the-middle attacks
- Currently uses
setInsecure()for demo - should use proper CA certificate in production
Security Best Practices
Current Implementation Notes:
- Private Keys: ECC private key is stored in
ecc_private.pem- ensure this file has restricted permissions (chmod 600) - Device Keys: AES keys are currently hardcoded - should be stored securely (database, key management service)
- HTTPS Certificates: ESP32 code uses
setInsecure()- replace with proper CA certificate validation - Device Database: Currently in-memory dictionary - use persistent database in production
- Public Key Distribution: Server public key must be securely distributed to devices (via secure boot, factory programming, or first secure connection)
Attack Mitigation
| Attack Vector | Mitigation |
|---|---|
| Malicious Patch Injection | ECC signatures ensure only server-signed patches are accepted |
| Patch Interception | AES-256-CCM encryption prevents unauthorized access to patch content |
| Replay Attacks | Random nonces in encryption ensure each encrypted patch is unique |
| Man-in-the-Middle | HTTPS with proper certificate validation (should be implemented) |
| Unauthorized Updates | Per-device encryption keys ensure patches are device-specific |
Setup & Usage Guide
Server Setup
1. Install Dependencies
cd /var/www/ota-server
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
2. Install System Dependencies
# Install bsdiff/bspatch tools
sudo apt-get update
sudo apt-get install bsdiff
# Or compile from source if not available in package manager
# https://github.com/mendsley/bsdiff
3. Generate Cryptographic Keys
# Generate ECC key pair for signing
openssl ecparam -genkey -name secp256r1 -noout -out ecc_private.pem
openssl ec -in ecc_private.pem -pubout -out ecc_public.pem
# Set secure permissions
chmod 600 ecc_private.pem
4. Configure Server
- Update
DEVICE_AES_KEYinserver.py(or use key management) - Ensure
ecc_private.pemis in the project directory - Create storage directories if they don't exist
5. Run Server
source venv/bin/activate
uvicorn server:app --host 0.0.0.0 --port 8000
Prerequisites:
- ESP32 development board (ESP32, ESP32-S2, ESP32-S3, etc.)
- USB cable to connect ESP32 to your computer
- WiFi network credentials (SSID and password)
- Server running and accessible (IP address or domain name)
- Optional: Two SSD1306 OLED displays (I2C addresses 0x3C and 0x3D) - code will work without them
Method 1: Using PlatformIO (Recommended)
Step 1: Install PlatformIO
# Install PlatformIO Core
pip install platformio
# Or install PlatformIO IDE (VS Code extension)
# https://platformio.org/install/ide?install=vscode
Step 2: Create platformio.ini Configuration
Create a platformio.ini file in /var/www/ota-server/:
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
adafruit/Adafruit SSD1306@^2.5.7
adafruit/Adafruit GFX Library@^1.11.9
upload_speed = 921600
monitor_speed = 115200
board_build.filesystem = littlefs
Step 3: Configure WiFi and Server Settings
Open src/main.cpp and update these configuration values (lines 17-32):
// ---------- CONFIG: set these ----------
const char* WIFI_SSID = "YourWiFiNetwork"; // Your WiFi network name
const char* WIFI_PASS = "YourWiFiPassword"; // Your WiFi password
const char* SERVER_HOST = "84.46.255.218"; // Your server IP or domain
const uint16_t SERVER_PORT = 8000; // Server port (usually 8000)
const char* DEVICE_ID = "esp32_01"; // Unique device identifier
// AES-256 per-device key (32 bytes) - MUST MATCH SERVER KEY
// This key must be the same in server.py DEVICE_AES_KEY
const uint8_t DEVICE_AES_KEY[32] = {
0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,
0x00,0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF
};
DEVICE_AES_KEY in src/main.cpp must
exactly match the DEVICE_AES_KEY in server.py (line 20).
Both devices and server must use the same key for encryption/decryption to work.
Step 4: Generate and Prepare Server Public Key
On your server, generate the public key file if you haven't already:
# Generate ECC key pair (if not done already)
cd /var/www/ota-server
openssl ecparam -genkey -name secp256r1 -noout -out ecc_private.pem
openssl ec -in ecc_private.pem -pubout -out ecc_public.pem
# Copy public key to data directory for ESP32
mkdir -p data
cp ecc_public.pem data/server_pub.pem
Step 5: Upload Files to ESP32 LittleFS
Upload the public key file to ESP32's LittleFS filesystem:
cd /var/www/ota-server
platformio run --target uploadfs
This uploads all files from the data/ directory to ESP32's LittleFS.
Step 6: Compile and Upload Firmware
cd /var/www/ota-server
platformio run --target upload
This will compile the code and upload it to your ESP32.
Step 7: Monitor Serial Output
platformio device monitor
Or use Arduino Serial Monitor at 115200 baud to see connection status and debug messages.
Method 2: Using Arduino IDE
Step 1: Install ESP32 Board Support
- Open Arduino IDE
- Go to File → Preferences
- Add this URL to "Additional Board Manager URLs":
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json - Go to Tools → Board → Boards Manager
- Search for "ESP32" and install "esp32 by Espressif Systems"
Step 2: Install Required Libraries
- Go to Sketch → Include Library → Manage Libraries
- Install these libraries:
- Adafruit SSD1306 by Adafruit
- Adafruit GFX Library by Adafruit
Step 3: Configure Board Settings
Go to Tools → Board and select your ESP32 board (e.g., "ESP32 Dev Module")
Configure these settings:
- Upload Speed: 921600
- CPU Frequency: 240MHz
- Flash Frequency: 80MHz
- Flash Mode: QIO
- Flash Size: 4MB (or your board's size)
- Partition Scheme: Default 4MB with spiffs (or OTA if available)
- Port: Select your ESP32's COM port
Step 4: Configure WiFi and Server
Same as PlatformIO Method - edit src/main.cpp lines 17-32 with your settings.
Step 5: Upload Server Public Key
For Arduino IDE, you'll need a SPIFFS/LittleFS uploader plugin:
- Install "ESP32 Filesystem Uploader" plugin from Arduino Library Manager
- Create
data/server_pub.pemwith your server's public key - Use Tools → ESP32 Sketch Data Upload to upload the data folder
Or manually copy the public key content and paste it into a file in the data directory.
Step 6: Compile and Upload
Click the Upload button (→) in Arduino IDE to compile and upload to ESP32.
Connection Testing
Open Serial Monitor (115200 baud) and you should see:
WiFi connecting...
WiFi connected
PUBKEY found
Starting update flow demo...
The OLED display (if connected) should show "WiFi OK" and display the ESP32's IP address. Serial monitor should show the IP address assigned by your router.
The ESP32 will automatically attempt to connect to your server. Check server logs:
# On server, monitor logs
tail -f /var/log/your-server.log
# Or if using uvicorn directly, you'll see HTTP requests in terminal
Common issues:
- WiFi failed: Check SSID and password
- HTTPS connect failed: Check SERVER_HOST and SERVER_PORT, ensure server is running
- PUBKEY not found: Run
platformio run --target uploadfsagain - Decrypt failed: Verify DEVICE_AES_KEY matches server key
- Verify failed: Check that server_pub.pem matches server's public key
Matching Server and Device Configuration
Critical Configuration Checklist:
| Setting | Server (server.py) | ESP32 (main.cpp) | Must Match? |
|---|---|---|---|
| Server Host | Running on IP/domain | SERVER_HOST |
✅ Yes |
| Server Port | Port 8000 (default) | SERVER_PORT |
✅ Yes |
| Device ID | DEVICES dict key |
DEVICE_ID |
✅ Yes |
| AES Key | DEVICE_AES_KEY (hex) |
DEVICE_AES_KEY (bytes) |
✅ CRITICAL |
| Public Key | ecc_public.pem |
data/server_pub.pem |
✅ Yes |
Network Requirements
- Firewall: Ensure port 8000 (or your chosen port) is open on the server
- HTTPS/SSL: Currently uses
setInsecure()for testing. For production, configure proper SSL certificates - WiFi: ESP32 must be on the same network or have internet access to reach the server
- Server Accessibility: Server must be accessible from ESP32's network (public IP, VPN, or local network)
Optional: Disable OLED Displays
If you don't have OLED displays, you can comment out OLED-related code or the code will handle missing displays gracefully. The Serial Monitor will still show all status information.
Quick Reference: Configuration Values
Files to Edit:
src/main.cpp- Lines 17-32 for WiFi, server, and encryption settingsserver.py- Line 20 for device AES key (must match ESP32)data/server_pub.pem- Server's ECC public key (upload to ESP32)
Quick Setup Commands:
# 1. Generate keys
openssl ecparam -genkey -name secp256r1 -noout -out ecc_private.pem
openssl ec -in ecc_private.pem -pubout -out ecc_public.pem
cp ecc_public.pem data/server_pub.pem
# 2. Edit src/main.cpp with your WiFi and server settings
# 3. Upload public key to ESP32
platformio run --target uploadfs
# 4. Upload firmware
platformio run --target upload
# 5. Monitor connection
platformio device monitor
Setup & Usage Guide
Usage Workflow
curl -X POST "http://your-server:8000/upload_firmware/" \
-F "version=1.0.1" \
-F "file=@firmware_1.0.1.bin"
curl -X POST "http://your-server:8000/make_patch/" \
-F "old_version=1.0.0" \
-F "new_version=1.0.1"
curl -X POST "http://your-server:8000/instruct_update/" \
-F "device_id=esp32_01" \
-F "old_version=1.0.0" \
-F "new_version=1.0.1"
Device polls
/check_update/, downloads patch and signature,
decrypts, verifies, applies, and reboots.
Testing
Testing the Update Flow:
- Upload firmware version 1.0.0 and 1.0.1
- Generate patch from 1.0.0 to 1.0.1
- Queue update for your device
- Power on device - it should automatically check for updates
- Monitor Serial output and OLED displays for progress
- Device should download, decrypt, verify, apply patch, and reboot