OTA Firmware Update Server

Complete Project Documentation & Architecture Guide

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/
server.py - FastAPI web server (main application)
patch_utils.py - Binary patch generation using bsdiff
crypto_utils.py - Encryption and signature functions
requirements.txt - Python dependencies
firmware_v1.bin - Sample firmware binary
storage/ - File storage directory
firmware/ - Stores firmware binaries
firmware_1.0.0.bin
patches/ - Stores binary patches (.bsdiff files)
signatures/ - Stores ECC signatures (.sig files)
src/ - ESP32 source code
main.cpp - ESP32 main application
lib/ - ESP32 libraries
bspatch_stub/
bspatch_stub.h - Patch application header
bspatch_stub.cpp - Patch application implementation
data/ - ESP32 LittleFS data files
venv/ - Python virtual environment
__pycache__/ - Python bytecode cache

/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}.bin
Example: firmware_1.0.0.bin
patches/ Binary delta patch files (bsdiff format) patch_{old_version}_to_{new_version}.bsdiff
Example: patch_1.0.0_to_1.0.1.bsdiff
Encrypted: patch_1.0.0_to_1.0.1.bsdiff.enc
signatures/ ECC digital signatures for patches patch_{old_version}_to_{new_version}.sig
Example: 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

1. Firmware Upload
Administrator uploads new firmware version to server via /upload_firmware/ endpoint
2. Patch Generation
Server generates binary delta patch using bsdiff comparing old and new firmware versions via /make_patch/ endpoint
3. Cryptographic Processing
Server signs patch with ECC private key and encrypts with device-specific AES-256 key
4. Update Instruction
Administrator queues update for specific device via /instruct_update/ endpoint
5. Device Polling
ESP32 device periodically checks /check_update/ endpoint to see if update is available
6. Patch Download
Device downloads encrypted patch file from /get_patch/{old}/{new}/{device_id} endpoint
7. Signature Download
Device downloads signature file from /get_signature/{old}/{new} endpoint
8. Decryption
Device decrypts patch using its per-device AES-256 key (AES-CCM mode)
9. Verification
Device verifies patch signature using server's public ECC key stored in LittleFS
10. Patch Application
Device applies patch to current firmware using bspatch algorithm (currently stub implementation)
11. OTA Write
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 bsdiff command-line tool
  • Creates patch file containing only differences between old and new firmware
  • Significantly reduces file size compared to full firmware binary
Requirement: The 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
Security Note: The 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 files
  • aes_ccm_decrypt_file() - Decrypts encrypted patch files using mbedTLS
  • verify_signature_file() - Verifies ECC signatures using mbedTLS
  • run_update_flow() - Orchestrates complete update process
  • apply_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
Note: For testing purposes, the current stub works if you upload a full firmware image as the "patch". For production, a real bspatch implementation is required.

API Endpoints

Server Endpoints

POST /upload_firmware/

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"
}
POST /make_patch/

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.

POST /instruct_update/

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 version
  • new_version (form data) - Target version to update to

Response:

{
  "status": "update_queued"
}
GET /check_update/

Purpose: Check if an update is available for a device

Parameters:

  • device_id (query) - Device identifier
  • version (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
}
GET /get_patch/{old}/{new}/{device_id}

Purpose: Download encrypted patch file for a specific device

Parameters:

  • old (path) - Old version
  • new (path) - New version
  • device_id (path) - Device identifier

Response: Binary file (encrypted patch)

Format: [nonce(13 bytes)][tag(16 bytes)][ciphertext]

GET /get_signature/{old}/{new}

Purpose: Download digital signature for a patch

Parameters:

  • old (path) - Old version
  • new (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_KEY in server.py (or use key management)
  • Ensure ecc_private.pem is 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
};
Important: The 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
  1. Open Arduino IDE
  2. Go to File → Preferences
  3. Add this URL to "Additional Board Manager URLs":
    https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  4. Go to Tools → Board → Boards Manager
  5. Search for "ESP32" and install "esp32 by Espressif Systems"
Step 2: Install Required Libraries
  1. Go to Sketch → Include Library → Manage Libraries
  2. 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:

  1. Install "ESP32 Filesystem Uploader" plugin from Arduino Library Manager
  2. Create data/server_pub.pem with your server's public key
  3. 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

1. Check Serial Monitor
Open Serial Monitor (115200 baud) and you should see:
WiFi connecting...
WiFi connected
PUBKEY found
Starting update flow demo...
2. Verify WiFi Connection
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.
3. Test Server Connection
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
4. Check for Errors
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 uploadfs again
  • 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 settings
  • server.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

Step 1: Upload Firmware
curl -X POST "http://your-server:8000/upload_firmware/" \
  -F "version=1.0.1" \
  -F "file=@firmware_1.0.1.bin"
Step 2: Generate Patch
curl -X POST "http://your-server:8000/make_patch/" \
  -F "old_version=1.0.0" \
  -F "new_version=1.0.1"
Step 3: Queue Update
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"
Step 4: Device Updates
Device polls /check_update/, downloads patch and signature, decrypts, verifies, applies, and reboots.

Testing

Testing the Update Flow:

  1. Upload firmware version 1.0.0 and 1.0.1
  2. Generate patch from 1.0.0 to 1.0.1
  3. Queue update for your device
  4. Power on device - it should automatically check for updates
  5. Monitor Serial output and OLED displays for progress
  6. Device should download, decrypt, verify, apply patch, and reboot