Skip to content

ADR-003: Ed25519 for Allow-List Signing

Status: Accepted Date: 2026-02-25

Context

The ESP32 fetches an allow-list from the backend over HTTPS. Even with TLS, we want a second layer of protection: the allow-list must be cryptographically signed so that the ESP32 can reject tampered or forged payloads. This protects against backend compromise, MITM attacks (if TLS is somehow bypassed), and replay attacks.

Options Considered

A) HMAC-SHA256 (symmetric)

  • Pro: Simple, fast, small output (32 bytes)
  • Con: Shared secret — if extracted from ESP32 flash, attacker can forge allow-lists
  • Con: Key rotation requires updating all ESP32 devices simultaneously

B) RSA-2048 / RSA-4096

  • Pro: Widely supported, well-understood
  • Con: Large signatures (256 / 512 bytes)
  • Con: Slow verification on ESP32 (~100ms for RSA-2048, ~400ms for RSA-4096)
  • Con: Large key sizes

C) ECDSA (P-256)

  • Pro: Smaller keys and signatures than RSA
  • Con: Nonce generation is critical — bad nonce = leaked private key
  • Con: More complex implementation than Ed25519

D) Ed25519

  • Pro: Fast verification (~2ms on ESP32 with libsodium)
  • Pro: Small signature (64 bytes), small public key (32 bytes)
  • Pro: Deterministic signatures — no nonce issues
  • Pro: Available in libsodium (ESP32) and PyNaCl (Python)
  • Pro: No patents, modern design (Daniel J. Bernstein)
  • Con: Less widely deployed than RSA (but fully mature)

Decision

Ed25519 (Option D).

It provides the best combination of security, performance, and simplicity for our use case. The ESP32 verifies signatures in ~2ms using libsodium. The deterministic nature eliminates nonce-related vulnerabilities. PyNaCl on the backend and libsodium on the firmware use the same underlying implementation (libsodium), ensuring compatibility.

Security Properties

  • Asymmetric: The ESP32 only holds the public key. Extracting it from flash does not allow forging allow-lists.
  • Monotonic versioning: The ESP32 rejects any allow-list with version <= current_version, preventing replay of older lists.
  • Validity window: valid_until is set to generated_at + 24 hours, bounding the trust period of stale lists.
  • Signature coverage: The Ed25519 signature covers the header (including version and valid_until) and all entries, preventing partial modification.

Key Management

  • Private key generated offline with tools/keygen.py
  • Private key stored as environment variable on the backend server (OPENLATCH_SIGNING_KEY_BASE64)
  • Public key provisioned to ESP32 NVS during initial setup
  • Keys are not stored in the repository or database

Consequences

  • Backend depends on PyNaCl (pip install pynacl)
  • Firmware depends on libsodium (included in espressif32 platform)
  • Cross-platform test vectors validate that Python-signed payloads verify in C++
  • Key rotation requires updating the ESP32's NVS (physical access or future OTA)