OTA con firma criptográfica: qué resuelve y por qué importa
La mayoría de los tutoriales de OTA en ESP32 cubren la mecánica (descargar un binario, escribirlo, reiniciar) pero ignoran la parte más importante: ¿cómo evitar que cualquiera empuje firmware malicioso a tu dispositivo?. Si tu ESP32 está expuesto a internet o comparte red con dispositivos que no controlas, una OTA sin verificación es una puerta abierta.
Esta guía construye un sistema OTA donde el ESP32 solo acepta firmware que tú firmaste personalmente con tu clave privada. Cualquier otro binario es rechazado antes de arrancar por el Lifecycle Manager (LCM). No llega ni a ejecutar. El sistema se compone de dos piezas:
Parte 1. Lifecycle Manager (LCM): un firmware "portero" que se instala primero. Gestiona WiFi, descarga actualizaciones desde GitHub y verifica firmas. Es lo único que decide qué firmware está autorizado a correr.
Parte 2. Tu firmware de aplicación: el código que realmente hace algo útil (LED, sensor, lo que sea). LCM lo descarga, verifica su firma y solo entonces lo instala.
La regla a recordar: el firmware debe estar firmado y la firma debe coincidir con la clave pública embebida en LCM. Si no coinciden, se rechaza. Eso no es un error, es exactamente el comportamiento que buscamos.
Términos que vas a ver:
- Clave privada (
ota_signing_private.pem): el archivo secreto que firma el binario. Si lo pierdes o se filtra, la seguridad cae. - Clave pública (
ota_signing_public.pem): se embebe en LCM. No es secreta: sirve para verificar. - Firma: hash criptográfico del binario, generado con la clave privada.
Hardware y software necesarios
Hardware:
- ESP32 WROOM 32D (placa con USB integrado, recomendada)

- Cable USB de datos
- 1 LED + resistencia de 220 Ω a 1 kΩ
- 1 pulsador (para reset físico)
- 2 jumpers macho-macho
- Protoboard
- WiFi 2.4 GHz
- iPhone/iPad con Apple Home
Software:
- Docker Desktop
- Git
- Python 3 + esptool.py
- OpenSSL (viene incluido en macOS/Linux; en Windows ver Git Bash o WSL)
- Pulsar (o cualquier editor de código)
Preparar el entorno ESP IDF
Si ya hiciste el tutorial de HomeKit para principiantes, este paso te lo sabes. Si no, en resumen:
- Instala Docker Desktop desde docker.com/products/docker-desktop y reinicia
- Descarga la imagen de ESP IDF:
docker pull espressif/idf:v5.4
- Instala Git y Python 3
- Instala esptool:
pip3 install esptool
- Verifica:
docker --version
git --version
python3 --version
esptool.py version
Listo.
Descargar el código del Lifecycle Manager
git clone https://github.com/AchimPieters/esp32-lifecycle-manager.git
cd esp32-lifecycle-manager
Generar tus claves de firma: el paso crítico
Esto va ahora, no después. Las claves se generan una sola vez y se reutilizan para todos tus dispositivos.
openssl ecparam -name prime256v1 -genkey -noout -out ota_signing_private.pem
openssl ec -in ota_signing_private.pem -pubout -out ota_signing_public.pem
Salida:
ota_signing_private.pem→ guárdalo en un lugar seguro (gestor de contraseñas, USB cifrado, no en GitHub)ota_signing_public.pem→ este lo vas a embeber en LCM
El algoritmo usado es ECDSA con curva prime256v1 (también conocida como P256), más eficiente que RSA en microcontroladores y suficientemente fuerte para este uso.
Embeber la clave pública en LCM
Abre el proyecto en Pulsar (o cualquier editor):
- Archivo → Abrir carpeta →
esp32-lifecycle-manager - Abre
ota_signing_public.pemy copia todo el texto - Abre
main/github_update.c - Busca
static const char OTA_PUBLIC_KEY_PEM[] - Reemplaza el contenido por tu clave pública

Si rompes algo, recuerda: el repo está intacto en GitHub. git checkout main/github_update.c deshace los cambios. No puedes hacer daño real explorando.
Compilar LCM
docker run -it -v ~/esp32-lifecycle-manager:/project -w /project espressif/idf:v5.4.2
Dentro del contenedor:
idf.py set-target esp32
idf.py build
Cuando termina, deja esta ventana abierta: vas a copiar el comando de flasheo desde la salida.
Compilar tu firmware de aplicación (ejemplo LED)
En otra terminal (sin cerrar la anterior):
docker run -it -v ~/esp32-lifecycle-manager:/project -w /project espressif/idf:v5.4
cd examples/led
idf.py set-target esp32
idf.py build
Resultado: build/main.bin.
Firmar tu firmware
En otra terminal nueva (no dentro de Docker):
cd ~/esp32-lifecycle-manager
./generate_sig.sh examples/led/build/main.bin ota_signing_private.pem
Esto genera:
main.bin(sin tocar)main.bin.sig(la firma)
Ambos archivos van juntos en cada actualización.
Subir a GitHub para que LCM lo descargue
- Crea un repositorio nuevo en tu cuenta de GitHub (ej:
firmware-mi-luz) - Crea un release con tag de versión (ej:
1.0.0) - Sube los dos archivos:
main.binymain.bin.sig
LCM descargará desde la URL pública del release, así que el repo debe ser público o accesible.

Cableado del LED + pulsador
GPIO2 → resistencia 220Ω → ánodo del LED → cátodo del LED → GND
GPIO0 → pulsador → GND
GPIO2 maneja el LED (mismo del ejemplo HomeKit). GPIO0 con pulsador permite el reset por software del LCM.

Erase + flash de LCM al ESP32
Sal de Docker (Ctrl+D) y desde el sistema:
esptool.py erase_flash
Anota el nombre del puerto serial (ej: /dev/cu.usbserial-01FD1166).
Vuelve a la terminal Docker donde compilaste LCM y copia el comando de flasheo que dejaste abierto. Algo así:
python -m esptool --chip esp32 -b 460800 \
--before default_reset --after hard_reset write_flash \
--flash_mode dio --flash_size 4MB --flash_freq 40m \
0x1000 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0xe000 build/ota_data_initial.bin \
0x20000 build/esp32-lifecycle-manager.bin
Pega y enter.
Verificar con monitor serial
screen /dev/cu.usbserial-XXXX 115200
Mensajes esperados al boot:
- Boot info (chip, memoria) → firmware corre
- "Starting AP interface" → LCM levantó un Access Point para configuración
- "Starting DNS server"
- "Starting HTTP server"
Salir: Ctrl+A, K, Y.
Configurar LCM por portal cautivo
LCM levanta un AP LCM-XXXXXX que aparece en las redes WiFi disponibles. En macOS puede demorar un par de minutos en aparecer.
- Conéctate al AP
LCM-XXXXXX - El portal cautivo se abre automáticamente
- Elige tu red WiFi de casa y escribe la contraseña
- Firmware source: pon tu repo (ej:
tu-usuario/firmware-mi-luz) - Activa "LED durante update" si quieres feedback visual (default GPIO2, nivel ON)
- Confirma

LCM va a:
- Descargar
main.bin+main.bin.sigdesde tu release - Verificar que la firma coincide con la clave pública embebida
- Si OK: escribir flash y reiniciar
- Si firma falla: rechazar, mantener firmware anterior, registrar el evento
Cómo updatear después
Cuando termines un cambio en tu firmware:
- Compila otra vez:
Bash
idf.py build - Firma:
Bash
./generate_sig.sh examples/led/build/main.bin ota_signing_private.pem - Crea un nuevo release en GitHub (ej:
1.0.1) y sube los dos archivos - Desde la app Eve en iOS, gatilla la actualización del accesorio HomeKit
LCM descarga, verifica la firma, instala y reinicia. Cero intervención física en el hardware.
Recuperación y resets
LCM soporta tres mecanismos:
| Método | Cómo se gatilla | Qué hace |
|---|---|---|
| Firmware update | App Eve → update | Descarga la última release del repo configurado |
| Software factory reset | API LCM o pulsador | Borra la config WiFi y vuelve al portal cautivo |
| Hardware factory reset | 10 ciclos de apagado y encendido | Si LCM detecta el patrón, espera ~11 s y borra la config |
El hardware reset es útil cuando el firmware quedó corrupto y ni el portal responde: desconecta y conecta el equipo 10 veces seguidas y LCM hace la recuperación automática.
Variantes y mejoras
OTA desde un servidor propio en lugar de GitHub: si no quieres que tus binarios estén públicos, monta un servidor HTTPS (un VPS pequeño con nginx sirve) y apunta
Firmware sourceen LCM ahttps://tu-server.cl/firmware/. Mantén un endpoint que devuelva la última versión disponible (puede ser simplemente un JSON con la URL del binario actual).Rotación de claves: si sospechas que tu
ota_signing_private.pemse filtró, no puedes simplemente generar una nueva clave: los dispositivos ya tienen embebida la pública vieja. Para resolverlo, agrega un mecanismo de "doble clave" en LCM: que acepte firmas de la pública A o de la pública B, y emite un firmware de transición que reemplaza la clave A por una nueva C, antes de descomisionar la A. Esto requiere modificargithub_update.c.Múltiples ESP32 con la misma clave: una sola clave privada puede firmar firmware para una flota de dispositivos. Útil si tienes 5 luces inteligentes en casa o si vas a fabricar varios accesorios DIY. La seguridad operativa pasa por mantener la clave privada en un solo equipo confiable.
Personalización para Chile
| Componente | Producto en MechatronicStore | SKU | Precio CLP |
|---|---|---|---|
| Placa ESP32 WROOM 32D | ESP32 WROOM 32 Tipo C | X2-10V2 | $7.990 |
| LED 5mm | LED 5mm rojo | GA1-5 | $100 |
| Resistencia 220 Ω | Resistencia 1/4W 220Ω | GK1-18 | $100 |
| Pulsador 12x12x8 | Boton pulsador switch 4 pines push button 12x12x8mm | GA6-16 | $290 |
| Protoboard 830 puntos | Breadboard MB102 830 puntos | C-302 | $3.790 |
| Cable USB Micro | Cable USB a Micro USB | X3-10 | $1.290 |
| Jumpers macho-macho 20cm | Cables macho-macho 40 piezas 20cm | C-411 | $1.990 |
Costo total estimado: ~$15.550 CLP.
Nota sobre OpenSSL en Windows: si trabajas en Windows nativo (sin WSL), instala OpenSSL desde slproweb.com/products/Win32OpenSSL.html o usa Git Bash, que ya lo incluye. Los comandos openssl ecparam y openssl ec funcionan igual.
Recursos
- Tutorial original (en inglés), en el que se basa esta versión: ESP32 Lifecycle Manager V2.0 (StudioPieters)
- Repo Lifecycle Manager: github.com/AchimPieters/esp32-lifecycle-manager
- Repo demo HomeKit: github.com/AchimPieters/esp32-homekit-demo
- App Eve para iOS (para gatillar OTA): App Store
- Documentación ECDSA: RFC 6979










