Last updated: 2026-04-03
Applies to: Ed25519 signing keys used for firmware image verification in the eBootloader secure boot chain.
eBootloader uses Ed25519 (RFC 8032) digital signatures to verify firmware image authenticity and integrity. This document defines the full lifecycle of the signing keypair — from generation through rotation, revocation, and emergency response.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Generate │────>│ Deploy │────>│ Verify │
│ Keypair │ │ Public Key │ │ at Boot │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
│ ┌─────┴─────┐ │
│ │ Key Slot 0│ │
│ │ Key Slot 1│ │
│ └───────────┘ │
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Rotate │ │ Revoke │
│ (new key) │ │ (counter) │
└──────────────┘ └──────────────┘
| Parameter | Value |
|---|---|
| Algorithm | Ed25519 (EdDSA over Curve25519) |
| Private key size | 32 bytes |
| Public key size | 32 bytes |
| Signature size | 64 bytes |
| Hash function | SHA-512 (internal to Ed25519) |
| Reference | RFC 8032, Section 5.1 |
Prerequisites:
- Air-gapped workstation running a hardened OS (no network interfaces active)
- Hardware random number generator (TRNG) or
/dev/randomwith sufficient entropy - OpenSSL ≥ 1.1.1 or
ed25519-keygenfrom the eBootloader tools
Steps:
# 1. Generate Ed25519 private key (PEM format)
openssl genpkey -algorithm Ed25519 -out eboot_signing_key.pem
# 2. Extract public key
openssl pkey -in eboot_signing_key.pem -pubout -out eboot_signing_pub.pem
# 3. Export raw 32-byte public key for embedding
openssl pkey -in eboot_signing_key.pem -pubout -outform DER | \
tail -c 32 > eboot_signing_pub.raw
# 4. Generate SHA-256 hash of public key (for TLV embedding)
sha256sum eboot_signing_pub.raw > eboot_signing_pub.sha256
# 5. Verify the keypair
echo "test" | openssl pkeyutl -sign -inkey eboot_signing_key.pem | \
openssl pkeyutl -verify -pubin -inkey eboot_signing_pub.pemAlternative — using eBootloader tooling:
# Generate keypair and C header in one step
python3 tools/sign_image.py --genkey \
--key-out keys/production_key.pem \
--pub-header include/eos_signing_key.h| Storage Location | Content | Access Control |
|---|---|---|
| HSM or air-gapped vault | Private key (eboot_signing_key.pem) |
Two-person access; encrypted at rest |
| CI/CD signing server | Private key (encrypted) | Build system service account only |
| Source repository | Never — private keys must never be committed | N/A |
The public key is compiled directly into the stage-1 bootloader binary. This is the default for devices without OTP/eFuse capability.
Generated header (include/eos_signing_key.h):
// SPDX-License-Identifier: MIT
// Auto-generated by sign_image.py — do not edit manually
#ifndef EOS_SIGNING_KEY_H
#define EOS_SIGNING_KEY_H
#include <stdint.h>
/* Primary signing key (slot 0) */
static const uint8_t eos_signing_pubkey_0[32] = {
0x3b, 0x6a, 0x27, 0xbc, /* ... 28 more bytes ... */
};
/* Backup signing key (slot 1) */
static const uint8_t eos_signing_pubkey_1[32] = {
0x9d, 0x61, 0xb1, 0x9d, /* ... 28 more bytes ... */
};
#define EOS_SIGNING_KEY_COUNT 2
#endif /* EOS_SIGNING_KEY_H */Verification logic:
int eos_image_verify_signature(const eos_image_header_t *hdr) {
for (int i = 0; i < EOS_SIGNING_KEY_COUNT; i++) {
const uint8_t *pubkey = (i == 0)
? eos_signing_pubkey_0
: eos_signing_pubkey_1;
int rc = eos_crypto_verify_signature(
hdr->hash, EOS_SHA256_DIGEST_SIZE,
hdr->signature, hdr->sig_len,
pubkey, 32);
if (rc == EOS_OK) return EOS_OK;
}
return EOS_ERR_SIGNATURE;
}For devices with one-time-programmable memory:
| Field | OTP Address | Size | Description |
|---|---|---|---|
pubkey_slot_0 |
OTP base + 0x00 | 32 bytes | Primary public key |
pubkey_slot_1 |
OTP base + 0x20 | 32 bytes | Backup public key |
key_revoke_mask |
OTP base + 0x40 | 4 bytes | Bit mask — bit N=1 revokes slot N |
security_version |
OTP base + 0x44 | 4 bytes | Monotonic counter for anti-rollback |
Advantages over compiled-in:
- Public key cannot be modified by flash reprogramming
- Key revocation is permanent (OTP bits are one-way)
- Survives complete flash erase
For high-security deployments using an external secure element (e.g., ATECC608B, OPTIGA Trust M):
| Operation | API |
|---|---|
| Store public key | se_write_slot(slot_id, pubkey, 32) |
| Verify signature | se_verify_ed25519(slot_id, hash, sig) |
| Read key hash | se_read_slot_hash(slot_id, hash_out) |
The secure element performs the signature verification internally — the public key is protected from modification by the SE's tamper-resistant hardware.
eBootloader supports two key slots (primary and backup) to enable seamless key rotation without bricking deployed devices.
┌─────────────────────────────────┐
│ Image Header │
│ sig_type = ED25519 │
│ signature[64] │
│ (signed with either key) │
└──────────────┬──────────────────┘
│
┌──────┴──────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Slot 0 │ │ Slot 1 │
│ Primary │ │ Backup │
│ pubkey │ │ pubkey │
└──────────┘ └──────────┘
The bootloader tries each non-revoked key slot in order:
- Check
key_revoke_mask— skip any revoked slots - Attempt signature verification with slot 0 public key
- If slot 0 fails, attempt with slot 1 public key
- If all slots fail →
EOS_ERR_SIGNATURE
| Slot | Purpose | Typical Usage |
|---|---|---|
| Slot 0 | Primary production key | Signs all production firmware releases |
| Slot 1 | Backup / rotation target | Receives new key during rotation; becomes new primary |
Key rotation replaces the active signing key without disrupting deployed devices. The dual-slot architecture ensures that devices can verify firmware signed with either the old or new key during the transition window.
Phase 1: Prepare Phase 2: Transition Phase 3: Revoke
(1 release cycle) (2 release cycles) (after full fleet update)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Generate new │ │ Sign firmware │ │ Revoke old key │
│ keypair │ │ with NEW key │ │ (set revoke bit)│
│ │ │ │ │ │
│ Install new │ │ Old key still │ │ Increment │
│ pubkey in │ │ accepted for │ │ security version│
│ slot 1 │ │ verification │ │ │
│ │ │ │ │ Sign firmware │
│ Continue signing│ │ Fleet updates │ │ with new key │
│ with OLD key │ │ propagate │ │ only │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Phase 1 — Prepare (release N):
- Generate new Ed25519 keypair on air-gapped workstation (see §2.2)
- Update
eos_signing_pubkey_1ininclude/eos_signing_key.hwith new public key - Build and sign stage-1 bootloader with existing (old) key
- Deploy bootloader update — devices now have both public keys
- Continue signing application firmware with old key (slot 0)
Phase 2 — Transition (releases N+1, N+2):
- Begin signing new application firmware with the new key (slot 1)
- Devices that received the bootloader update verify with slot 1
- Devices still on old bootloader verify with slot 0 (old firmware still deployed)
- Monitor fleet update telemetry — track percentage of devices on new bootloader
Phase 3 — Revoke (release N+3, after fleet convergence):
- Verify ≥99% of fleet has updated bootloader with both key slots
- Set
key_revoke_maskbit 0 to revoke old key (slot 0) - Move new key to slot 0; generate next rotation key in slot 1 (optional)
- Increment
security_versionmonotonic counter - Securely destroy old private key material
- New keypair generated on air-gapped workstation
- New public key tested in CI with
sign_image.py --verify - Bootloader binary with new public key deployed and confirmed on test fleet
- Fleet telemetry confirms ≥99% adoption of new bootloader
- Old key revocation applied (OTP bit or compiled-in flag)
- Old private key material securely destroyed
- Key inventory updated in HSM management system
- Security counter incremented
The security version is a monotonic counter that can only increment. It is stored in:
| Storage | Mechanism | Reversibility |
|---|---|---|
| OTP/eFuse | One-time programmable bits | Irreversible |
| Dedicated flash sector | Counter with anti-tearing write | Reversible (with flash access) |
| Secure element | SE-managed counter | Irreversible |
Image header: security_counter = 5
Device OTP: security_version = 5 → ACCEPT (5 ≥ 5)
Image header: security_counter = 4
Device OTP: security_version = 5 → REJECT (4 < 5, rollback attempt)
Image header: security_counter = 6
Device OTP: security_version = 5 → ACCEPT (6 ≥ 5)
Update OTP to 6
- Never decrement. The counter is monotonic — any attempt to write a lower value is silently ignored.
- Increment on key rotation. Every key rotation bumps the security version by 1.
- Increment on critical CVE. A security-critical fix may bump the counter to prevent rollback to vulnerable versions.
- Coordinate with fleet. Before incrementing, ensure the new firmware is available for all devices.
| Environment | Key Purpose | Storage | Signing Authority |
|---|---|---|---|
| Development | Local builds, unit tests, CI | Plaintext PEM in developer workspace | Any developer |
| Staging | Pre-production validation | Encrypted PEM on staging build server | Release engineer |
| Production | Release firmware | HSM-protected; two-person authorization | Release manager + security officer |
#if defined(EOS_BUILD_PRODUCTION)
/* Production keys — compiled from HSM-exported header */
#include "eos_signing_key_production.h"
#elif defined(EOS_BUILD_STAGING)
#include "eos_signing_key_staging.h"
#else
/* Development key — well-known test key */
#include "eos_signing_key_dev.h"
#endif| Rule | Rationale |
|---|---|
| Development private key is committed to the repository | Enables any developer to build and test signed images locally |
| Development key is never used in production | Production builds fail if development key header is detected |
| Production key never appears in source control | Only the public key is embedded; private key stays in HSM |
| CI pipeline uses staging key for integration tests | Tests signature verification without exposing production key |
The development key is intentionally public and must never be used for production:
Development Private Key (Ed25519, PEM):
MC4CAQAwBQYDK2VwBCIEIJ+DYvh6SEqVTm50DFtMDoQikQ2Ig35R6DIxQ/BV1Cxz
Development Public Key SHA-256:
9f836af87a484a954e6e74c5b4c0e842291d8883be51e832314bf055d42c73...
⚠️ THIS KEY IS PUBLIC. Images signed with this key are NOT authenticated.
| Severity | Scenario | Response Time |
|---|---|---|
| P0 — Confirmed compromise | Private key material exposed publicly or to unauthorized party | Immediate (< 4 hours) |
| P1 — Suspected compromise | Unauthorized access to signing infrastructure detected | < 24 hours |
| P2 — Precautionary | Personnel change, infrastructure migration, policy violation | < 7 days |
┌─ HOUR 0 ──────────────────────────────────────────────┐
│ 1. Notify security team: security@embeddedos.org │
│ 2. Revoke compromised key in CI/CD pipeline │
│ 3. Halt all firmware signing with compromised key │
└───────────────────────────────────────────────────────┘
│
┌─ HOURS 1-4 ───────────────────────────────────────────┐
│ 4. Generate emergency replacement keypair (§2.2) │
│ 5. Build emergency bootloader with new public key │
│ 6. Sign emergency bootloader with backup key (slot 1) │
│ 7. Push emergency OTA to all connected devices │
└───────────────────────────────────────────────────────┘
│
┌─ HOURS 4-24 ──────────────────────────────────────────┐
│ 8. Increment security version counter │
│ 9. Revoke compromised key slot (OTP/eFuse) │
│ 10. Monitor fleet for devices still on old key │
│ 11. Publish security advisory │
└───────────────────────────────────────────────────────┘
│
┌─ DAYS 1-7 ────────────────────────────────────────────┐
│ 12. Root cause analysis │
│ 13. Update key management procedures │
│ 14. Rotate all related credentials │
│ 15. Post-incident review │
└───────────────────────────────────────────────────────┘
- Firmware images signed with the production key that were not produced by the authorized build pipeline
- Unauthorized access logs on the HSM or signing server
- Public disclosure of key material (paste sites, repositories, social media)
- Anomalous device behavior consistent with unauthorized firmware
- Compiled-in keys: Devices with compiled-in public keys require a bootloader update to rotate keys. If both key slots are compromised and the device cannot receive OTA updates, physical UART recovery is required.
- OTP/eFuse keys: Once all OTP key slots are revoked, the device cannot verify any firmware. This is a permanent brick condition — plan key slots carefully.
- Offline devices: Devices not connected to the network cannot receive emergency key rotation. They remain vulnerable until physically recovered.
- Secure Boot Chain
- Threat Model
- Security Model
- Architecture
- RFC 8032 — Edwards-Curve Digital Signature Algorithm (EdDSA)