Comprehensive documentation of security implementations, threat model, and best practices.
- Security Overview
- Threat Model
- Upload Security
- FOTA Security
- Key Management
- Attack Mitigations
- Security Best Practices
EcoWatt implements defense-in-depth security with multiple independent layers:
┌──────────────────────────────────────────────────┐
│ Security Layers │
├──────────────────────────────────────────────────┤
│ │
│ Layer 1: Transport Security (HTTPS/TLS) │
│ Layer 2: Anti-Replay Protection (Nonce) │
│ Layer 3: Message Authentication (HMAC-SHA256) │
│ Layer 4: Confidentiality (AES-128-CBC) │
│ Layer 5: Firmware Authenticity (RSA-2048) │
│ │
└──────────────────────────────────────────────────┘
| Goal | Implementation | Status |
|---|---|---|
| Confidentiality | AES-128-CBC encryption | ✅ Supported |
| Integrity | HMAC-SHA256 signatures | ✅ Active |
| Authenticity | Pre-shared keys | ✅ Active |
| Non-Repudiation | Logging & audit trail | ✅ Active |
| Availability | Fault tolerance & recovery | ✅ Active |
- Sensor Data: Solar inverter measurements
- Firmware: ESP32 application code
- Cryptographic Keys: HMAC keys, RSA keys
- Device Identity: Device credentials
- Command Authority: Remote control access
| Actor | Capability | Motivation |
|---|---|---|
| Script Kiddie | Low | Curiosity, vandalism |
| Malicious Insider | Medium | Data theft, sabotage |
| Competitor | High | IP theft, market intelligence |
| Nation State | Very High | Infrastructure disruption |
Internet
│
├─── Man-in-the-Middle ──────► Mitigated by HTTPS/TLS
│
├─── Replay Attack ──────────► Mitigated by Nonce
│
├─── Data Forgery ────────────► Mitigated by HMAC
│
├─── Malicious Firmware ──────► Mitigated by RSA signatures
│
└─── DoS Attack ───────────────► Mitigated by rate limiting
ESP32:
┌──────────────────────────────────────┐
│ 1. Load nonce from NVS │
│ nonce = 10001 │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 2. Compress data │
│ payload = compress(sensor_data) │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 3. (Optional) Encrypt payload │
│ encrypted = AES(payload, key, iv) │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 4. Base64 encode │
│ b64_payload = base64(encrypted) │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 5. Calculate HMAC │
│ mac = HMAC-SHA256(key, │
│ nonce+payload) │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 6. Create JSON packet │
│ {nonce, payload, mac, ...} │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 7. HTTPS POST to server │
└──────────────────────────────────────┘
Server:
┌──────────────────────────────────────┐
│ 8. Receive packet │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 9. Check nonce > last_nonce │
│ if not: reject as replay attack │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 10. Recalculate HMAC │
│ expected_mac = HMAC-SHA256(...) │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 11. Compare MACs │
│ if mac != expected: reject │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 12. Base64 decode payload │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 13. (Optional) Decrypt │
│ payload = AES_decrypt(...) │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 14. Decompress data │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 15. Store in database │
└────────────┬─────────────────────────┘
│
┌────────────▼─────────────────────────┐
│ 16. Update last_nonce │
└──────────────────────────────────────┘
ESP32 Implementation:
// nonce.h
#define NVS_NAMESPACE "ecowatt"
#define NVS_NONCE_KEY "nonce"
uint32_t load_nonce() {
nvs_handle_t handle;
uint32_t nonce = 0;
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READONLY, &handle));
nvs_get_u32(handle, NVS_NONCE_KEY, &nonce);
nvs_close(handle);
return nonce;
}
void save_nonce(uint32_t nonce) {
nvs_handle_t handle;
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &handle));
ESP_ERROR_CHECK(nvs_set_u32(handle, NVS_NONCE_KEY, nonce));
ESP_ERROR_CHECK(nvs_commit(handle));
nvs_close(handle);
}
uint32_t generate_next_nonce() {
uint32_t nonce = load_nonce();
nonce++;
save_nonce(nonce);
return nonce;
}Server Implementation:
# security_handler.py
def validate_nonce(device_id, nonce):
"""Validate nonce to prevent replay attacks"""
last_nonce = get_last_nonce_from_db(device_id)
if last_nonce is None:
# First communication from device
update_nonce_in_db(device_id, nonce)
return True
if nonce <= last_nonce:
log_security_event(device_id, "REPLAY_ATTACK", nonce)
raise SecurityException("Replay attack detected")
# Nonce is valid, update database
update_nonce_in_db(device_id, nonce)
return TrueKey Derivation:
# generate_keys.py
import secrets
import base64
def generate_hmac_key():
"""Generate 256-bit random key"""
key_bytes = secrets.token_bytes(32) # 256 bits
key_hex = key_bytes.hex()
key_b64 = base64.b64encode(key_bytes).decode()
return {
'bytes': key_bytes,
'hex': key_hex,
'base64': key_b64
}ESP32 HMAC Calculation:
// security.cpp
#include "mbedtls/md.h"
void calculate_hmac_sha256(
const uint8_t* key, size_t key_len,
const uint8_t* data, size_t data_len,
uint8_t* output
) {
mbedtls_md_context_t ctx;
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
mbedtls_md_hmac_starts(&ctx, key, key_len);
mbedtls_md_hmac_update(&ctx, data, data_len);
mbedtls_md_hmac_finish(&ctx, output);
mbedtls_md_free(&ctx);
}
// Usage
uint8_t hmac[32];
calculate_hmac_sha256(
hmac_key, 32,
message, message_len,
hmac
);Server HMAC Validation:
# security_handler.py
import hmac
import hashlib
def validate_hmac(key, nonce, payload, received_mac):
"""Validate HMAC-SHA256 signature"""
# Construct message (nonce + payload)
message = str(nonce).encode() + payload.encode()
# Calculate expected HMAC
expected_mac = hmac.new(
key.encode(),
message,
hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected_mac, received_mac)ESP32 Encryption:
// security.cpp
#include "mbedtls/aes.h"
int aes_encrypt_cbc(
const uint8_t* key, // 16 bytes
const uint8_t* iv, // 16 bytes
const uint8_t* input,
size_t input_len,
uint8_t* output
) {
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
// Set encryption key
mbedtls_aes_setkey_enc(&aes, key, 128);
// Encrypt in CBC mode
uint8_t iv_copy[16];
memcpy(iv_copy, iv, 16); // IV is modified during encryption
int ret = mbedtls_aes_crypt_cbc(
&aes,
MBEDTLS_AES_ENCRYPT,
input_len,
iv_copy,
input,
output
);
mbedtls_aes_free(&aes);
return ret;
}Server Decryption:
# security_handler.py
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
def aes_decrypt_cbc(key, iv, ciphertext):
"""Decrypt data using AES-128-CBC"""
cipher = Cipher(
algorithms.AES(key),
modes.CBC(iv),
backend=default_backend()
)
decryptor = cipher.decryptor()
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
# Remove PKCS7 padding
padding_len = plaintext[-1]
return plaintext[:-padding_len]Server:
┌───────────────────────────────────────┐
│ 1. Receive firmware.bin │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 2. Calculate SHA-256 hash │
│ hash = SHA256(firmware.bin) │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 3. Sign hash with RSA private key │
│ signature = RSA_sign(hash) │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 4. Generate random IV │
│ iv = random_bytes(16) │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 5. Encrypt firmware with AES-256-CBC │
│ encrypted = AES256(firmware, iv) │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 6. Split into 2KB chunks │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 7. Create manifest JSON │
│ {version, hash, signature, iv} │
└───────────────────────────────────────┘
ESP32:
┌───────────────────────────────────────┐
│ 8. Poll manifest endpoint │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 9. Compare version with current │
│ if new: proceed with update │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 10. Download chunks sequentially │
│ for each 2KB chunk: │
│ - Download │
│ - Decrypt │
│ - Write to flash partition │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 11. Calculate SHA-256 of decrypted FW │
│ calculated_hash = SHA256(...) │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 12. Compare with manifest hash │
│ if not match: rollback & abort │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 13. Verify RSA signature │
│ valid = RSA_verify(hash, sig, pk) │
│ if not valid: rollback & abort │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 14. Mark partition as bootable │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 15. Reboot into new firmware │
└────────────┬──────────────────────────┘
│
┌────────────▼──────────────────────────┐
│ 16. Verify boot success │
│ if fail: auto-rollback │
└───────────────────────────────────────┘
# Generate RSA-2048 private key
openssl genrsa -out private_key.pem 2048
# Extract public key
openssl rsa -in private_key.pem -pubout -out public_key.pem
# Convert public key to C header format
python scripts/generate_keys.pyGenerated keys.h:
// keys.h
#ifndef KEYS_H
#define KEYS_H
// RSA-2048 Public Key (for firmware verification)
const char* RSA_PUBLIC_KEY =
"-----BEGIN PUBLIC KEY-----\n"
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...\n"
"-----END PUBLIC KEY-----";
// HMAC-SHA256 Key (for data authentication)
const uint8_t HMAC_KEY[32] = {
0x5a, 0x7b, 0x3c, 0x9d, 0x2e, 0x4f, 0x1a, 0x6b,
// ... 32 bytes total
};
#endifServer-side:
# prepare_firmware.py
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.backends import default_backend
def sign_firmware(firmware_path, private_key_path):
# Read firmware
with open(firmware_path, 'rb') as f:
firmware_data = f.read()
# Calculate SHA-256 hash
digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
digest.update(firmware_data)
firmware_hash = digest.finalize()
# Load private key
with open(private_key_path, 'rb') as f:
private_key = serialization.load_pem_private_key(
f.read(),
password=None,
backend=default_backend()
)
# Sign hash
signature = private_key.sign(
firmware_hash,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return firmware_hash.hex(), signature.hex()ESP32-side:
// OTAManager.cpp
#include "mbedtls/rsa.h"
#include "mbedtls/sha256.h"
bool verify_firmware_signature(
const uint8_t* firmware_hash,
const uint8_t* signature,
size_t signature_len
) {
mbedtls_rsa_context rsa;
mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256);
// Load public key
int ret = load_rsa_public_key(&rsa, RSA_PUBLIC_KEY);
if (ret != 0) {
return false;
}
// Verify signature
ret = mbedtls_rsa_rsassa_pss_verify(
&rsa,
NULL, NULL,
MBEDTLS_RSA_PUBLIC,
MBEDTLS_MD_SHA256,
32, // Hash length
firmware_hash,
signature
);
mbedtls_rsa_free(&rsa);
return (ret == 0);
}| Key Type | Location | Protection |
|---|---|---|
| HMAC Key | ESP32 firmware | Embedded in code, flash encryption |
| RSA Private Key | Server filesystem | File permissions (600) |
| RSA Public Key | ESP32 firmware | Embedded in code |
| AES Encryption Key | Server config | Environment variable |
HMAC Key Rotation:
- Generate new key
- Distribute to all devices via secure channel
- Update server configuration
- Switch over at coordinated time
RSA Key Rotation:
- Generate new RSA key pair
- Rebuild firmware with new public key
- Deploy firmware update (signed with old key)
- After update, new firmware uses new key
Mitigation: Monotonically increasing nonce
Test:
# Test replay attack
packet = capture_valid_packet()
response1 = send_packet(packet) # Success
response2 = send_packet(packet) # Should fail with replay errorMitigation: HTTPS/TLS with certificate validation
ESP32 Configuration:
// Enable certificate validation
esp_http_client_config_t config = {
.url = "https://server.example.com",
.cert_pem = server_root_ca_pem_start,
.skip_cert_common_name_check = false
};Mitigation: HMAC-SHA256 signature
Test:
# Test data forgery
packet = {
'nonce': 10001,
'payload': 'forged_data',
'mac': 'invalid_mac'
}
response = send_packet(packet) # Should fail HMAC validationMitigation: RSA-2048 signature verification
Test:
// Attempt to install unsigned firmware
bool success = install_firmware("malicious.bin");
// Should fail signature verification and rollback- Never Hardcode Keys: Use environment variables or secure storage
- Rotate Keys Regularly: Minimum every 90 days
- Use Strong Random:
secretsmodule in Python, hardware RNG on ESP32 - Validate All Inputs: Never trust user input
- Log Security Events: Audit trail for forensics
- Enable Flash Encryption: Protect firmware in ESP32 flash
- Enable Secure Boot: Verify bootloader signature
- Use HTTPS: Always encrypt transport layer
- Restrict Access: Firewall rules, VPN access
- Monitor Logs: Detect anomalies
- Regular Updates: Apply security patches promptly
- Backup Keys: Secure, encrypted backups
- Incident Response: Documented procedure
- Security Audits: Regular third-party reviews
- Penetration Testing: Annual pen tests