Firmware signé & OTA sécurisé¶
Signer le firmware côté build et vérifier la signature à l'embarquement — la seule protection fiable contre l'injection de code malveillant via les mises à jour Over-The-Air.
Pourquoi signer le firmware¶
Une mise à jour OTA non authentifiée est une porte dérobée permanente. Sans signature, un attaquant qui accède au réseau de mise à jour — ou au serveur OTA — peut pousser un firmware malveillant sur l'ensemble du parc connecté. La signature asymétrique résout ce problème : seul celui qui détient la clé privée peut signer, et n'importe qui peut vérifier avec la clé publique embarquée.
Les propriétés à garantir sont :
- Authenticité : le firmware a bien été signé par l'OEM légitime
- Intégrité : aucun octet n'a été modifié depuis la signature
- Fraîcheur : le firmware est plus récent que celui installé (anti-downgrade)
- Atomicité : l'application partielle d'une mise à jour échoue proprement
Choix de l'algorithme : ECDSA P-256¶
ECDSA avec la courbe P-256 (secp256r1) est le standard de facto pour les systèmes embarqués contraints :
| Algorithme | Taille clé | Taille signature | Mémoire (vérif) | Sécurité |
|---|---|---|---|---|
| RSA-2048 | 2048 bits | 256 octets | ~30 Ko RAM | 112 bits |
| RSA-4096 | 4096 bits | 512 octets | ~60 Ko RAM | 140 bits |
| ECDSA P-256 | 256 bits | 64 octets | ~8 Ko RAM | 128 bits |
| ECDSA P-384 | 384 bits | 96 octets | ~12 Ko RAM | 192 bits |
| Ed25519 | 256 bits | 64 octets | ~4 Ko RAM | 128 bits |
ECDSA P-256 offre le meilleur compromis performance/empreinte mémoire/sécurité pour les MCU Cortex-M. Ed25519 est légèrement plus rapide en vérification sur MCU modernes — MCUboot 2.x supporte les deux.
Structure du manifest signé¶
Un manifest signé encapsule les métadonnées de la mise à jour. Le device vérifie d'abord le manifest, puis utilise les informations du manifest pour vérifier le firmware.
{
"manifest_version": 1,
"sequence_number": 1042,
"update_description": "Correctif CVE-2025-1234 — stack overflow BLE",
"firmware": {
"version": "2.4.1",
"size_bytes": 524288,
"sha256": "a3f8c2d1e4b5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
"target_hardware": "EXT-SENSOR-V2",
"min_hw_revision": 3
},
"anti_downgrade": {
"monotonic_counter": 42,
"min_version": "2.0.0"
},
"signature": {
"algorithm": "ECDSA-P256-SHA256",
"value": "3045022100...base64..."
}
}
Champs critiques :
| Champ | Rôle | Protection |
|---|---|---|
sequence_number | Ordre absolu des mises à jour | Anti-replay |
sha256 | Hash du binaire firmware | Intégrité |
monotonic_counter | Compteur non décrémentable en OTP | Anti-downgrade |
min_version | Version minimale acceptable | Anti-downgrade complémentaire |
target_hardware | Identifiant du modèle cible | Anti-bricking cross-model |
signature | ECDSA sur le hash du manifest | Authenticité + intégrité manifest |
Flux de signature et vérification OTA¶
sequenceDiagram
participant DEV as Développeur
participant CI as Pipeline CI/CD
participant HSM as HSM (clé privée OEM)
participant SRV as Serveur OTA
participant GW as Gateway IoT
participant DEV2 as Device embarqué
DEV->>CI: git push tag v2.4.1
CI->>CI: Compilation firmware.bin<br/>sha256(firmware.bin) = H
CI->>HSM: Signer manifest JSON<br/>(contient H + metadata)
HSM-->>CI: Signature ECDSA P-256
CI->>CI: Packager: firmware.bin + manifest.json.sig
CI->>SRV: Publier artefact signé
Note over SRV: Le serveur OTA ne détient pas<br/>la clé privée — il distribue seulement
GW->>SRV: Requête mise à jour (device ID, version actuelle)
SRV-->>GW: Manifest.json + manifest.json.sig + firmware.bin
GW->>GW: Vérifier signature manifest<br/>(clé publique OEM)
GW->>GW: Vérifier sha256(firmware.bin) == H
GW->>DEV2: Transférer firmware vérifié (chunk par chunk)
DEV2->>DEV2: 1. Vérifier signature manifest
DEV2->>DEV2: 2. Vérifier hash firmware
DEV2->>DEV2: 3. Vérifier monotonic counter > actuel
DEV2->>DEV2: 4. Écrire en slot secondaire (A/B)
DEV2->>DEV2: 5. Marquer slot comme "pending test"
DEV2->>DEV2: 6. Reboot sur nouveau firmware
DEV2->>DEV2: 7. Self-test (30s)
DEV2-->>GW: Confirmer succès (marquer permanent)
Note over DEV2: Si self-test échoue → rollback auto sur slot A Mécanisme anti-downgrade avec compteur monotone¶
Le compteur monotone (monotonic counter) est stocké dans un registre OTP qui ne peut qu'être incrémenté, jamais décrémenté. Même si un attaquant parvient à pousser un manifest signé d'une version plus ancienne, le device refuse l'installation si le compteur du manifest est inférieur au compteur courant.
stateDiagram-v2
[*] --> Vérification_Manifest
Vérification_Manifest --> Signature_Invalide : signature KO
Vérification_Manifest --> Hash_Invalide : hash firmware KO
Vérification_Manifest --> Check_Compteur : signature + hash OK
Signature_Invalide --> Rejet : Abandon mise à jour
Hash_Invalide --> Rejet : Abandon mise à jour
Check_Compteur --> Downgrade_Détecté : counter_manifest < counter_device
Check_Compteur --> Installation : counter_manifest >= counter_device
Downgrade_Détecté --> Rejet : Abandon + log sécurité
Installation --> Boot_Test_Slot
Boot_Test_Slot --> Self_Test
Self_Test --> Rollback : échec (watchdog / assertion)
Self_Test --> Confirmer : succès (30s stable)
Confirmer --> Incrémenter_Compteur_OTP
Incrémenter_Compteur_OTP --> [*]
Rollback --> Boot_Ancien_Slot
Boot_Ancien_Slot --> [*] Mise à jour A/B (dual-bank)¶
La mise à jour A/B est le mécanisme de rollback sûr standard. Le device dispose de deux partitions firmware :
graph TD
subgraph FLASH["FLASH STORAGE"]
direction LR
A["Slot A (actif)<br/>firmware v2.3<br/>status : PERM"]
B["Slot B (mise à jour)<br/>firmware v2.4.1 (pending test)<br/>status : TEST"]
end
subgraph BOOT["Bootloader MCUboot"]
C["Sélectionne le slot<br/>selon flags + vérification"]
end
FLASH --> BOOT Les états d'un slot MCUboot :
| Statut | Description | Action du bootloader |
|---|---|---|
GOOD | Firmware vérifié, permanent | Boot direct |
REVERT | Rollback demandé | Boot sur l'autre slot |
TEST | Premier boot post-update | Boot + timer watchdog |
INVALID | Hash invalide | Skip, boot sur l'autre slot |
EMPTY | Slot vide | Skip |
Sécurisation du canal OTA¶
La signature protège l'intégrité du firmware même si le canal est compromis. Pour autant, le canal de distribution doit aussi être sécurisé :
| Couche | Mécanisme | Pourquoi |
|---|---|---|
| Transport | TLS 1.3 + certificate pinning | Confidentialité, anti-MitM |
| Authentification device | mTLS (certificat X.509 device) | Seuls les devices légitimes reçoivent les MAJ |
| Autorisation | RBAC sur le serveur OTA | Contrôle par flotte/groupe |
| Rate limiting | Max X tentatives/heure par device | Protection DoS sur l'OTA backend |
| Delta updates | BSDiff / ZBSDIFF | Réduction bande passante (critique sur LoRa) |
Cas des MCU très contraints¶
Sur les MCU avec moins de 64 Ko de flash, la mise à jour A/B complète peut être impossible. Alternatives :
Mise à jour in-place avec hash pré-vérification. Le nouveau firmware est téléchargé en RAM (ou partition temporaire EEPROM externe), vérifié, puis écrit sur la flash. Risque : si la coupure de courant survient pendant l'écriture, le device est briqué. Mitigation : checksum partiel + écriture sectorielle.
Incremental patching. Seuls les secteurs modifiés sont transférés. Réduit la bande passante et le temps d'exposition. Nécessite un composant de patch embarqué (JanPatch, HDiffPatch).
Bootloader en ROM immuable. Certains MCU (STM32, ESP32) disposent d'un bootloader ROM incorruptible qui peut toujours restaurer un firmware via UART — filet de sécurité ultime en production.
Ce qu'il faut retenir¶
- ECDSA P-256 est le standard embarqué : petit, rapide, 128 bits de sécurité.
- Le manifest signé doit inclure : hash du binaire, version, compteur monotone, cible matérielle.
- Le compteur monotone en OTP est la seule protection solide contre le downgrade.
- La mise à jour A/B avec self-test et rollback automatique est la norme pour les déploiements industriels.
- La clé privée de signature ne doit jamais quitter le HSM — ni les serveurs CI, ni les workstations développeur.
Chapitre suivant : Segmentation réseau — isoler les devices IoT du réseau IT pour contenir la propagation d'une compromission.