Protection de l’heure BLE contre la falsification

Pourquoi le propriétaire ne livre pas l’heure aujourd’hui

Dans la version 6.12, celle dont provient le journal du contrôleur, le code Android n’avait rien de tel — aucune synchronisation d’heure en arrière-plan depuis le téléphone du propriétaire. L’heure ne partait qu’en effet secondaire d’un fire du propriétaire (phase 3), et encore n’arrivait-elle pas : le contrôleur coupait la connexion juste après le déclenchement du relais.

La 6.13 a ajouté une connexion d’arrière-plan distincte : à chaque détection passive du contrôleur, le téléphone du propriétaire ouvre sa propre session GATT et écrit TIME. Mais la seule source d’heure reste le propriétaire.

Tout client de confiance devrait pouvoir livrer l’heure

Sinon voici le tableau : le propriétaire est en vacances, le contrôleur perd l’alimentation, redémarre avec rtc=0 — et les invités sont exclus jusqu’à son retour. On ne construit pas ainsi.

Un invité muni d’un jeton valide doit pouvoir signer TIME avec sa clé invité.

Comment empêcher la falsification — un cliquet monotone, réservé aux invités

L’idée est simple : pour un invité, l’heure ne peut qu’avancer. Le propriétaire est de confiance : il la déplace dans les deux sens sans limite — utile si quelqu’un a mis 2099 par erreur et qu’il faut revenir en arrière.

Le contrôleur garde la dernière heure connue en NVS (time_ratchet). Lors d’une écriture TIME d’invité :

Lors d’une écriture TIME du propriétaire, le cliquet est simplement écrasé par sa valeur, dans les deux sens.

Après un redémarrage, le contrôleur part de rtc = ratchet et non de 0. Le RTC compte à partir de là. Une réinitialisation d’usine efface aussi le cliquet, mais l’appairage y écrit aussitôt l’heure courante.

Pourquoi cela arrête un invité malveillant

L’invité attaquant veut prolonger un jeton dont valid_until = X). Que peut-il tenter ?

Option 1 — reculer l’heure (mettre l’horloge à hier pour que son jeton paraisse frais) :
→ le cliquet l’en empêche. Refusé.

Option 2 — avancer l’heure (avancer l’horloge d’un an) :
→ le contrôleur l’accepte, car l’heure a bien avancé.
→ MAIS le contrôleur compare désormais valid_until=X avec now = a year aheadX < now → et son propre jeton a expiré.
→ L’attaquant ne s’est nui qu’à lui-même.

Le seul geste honnête d’un invité est de mettre l’heure réelle. Toute falsification est soit rejetée, soit se retourne contre lui.

Qui signe TIME

Le contrôleur doit accepter une écriture TIME signée avec :

Deux octets sont ajoutés à la charge utile TIME : bleId. Ensuite le contrôleur essaie :

  1. le HMAC avec owner_secret — s’il correspond, on accepte.
  2. Sinon, dériver guest_key pour ce bleId et vérifier à nouveau.

Sans lot invité légitime délivré par le serveur, impossible de signer TIME — l’attaquant n’a pas la clé.

Protection supplémentaire : un verrou fire d’abord dans la fenêtre fraîche

Si la dernière synchronisation réussie date de moins d’une heure, le contrôleur juge son horloge fraîche et ne voit aucune raison de la mettre à jour sans motif. Dans cette fenêtre, une écriture TIME n’est acceptée que si le client a réalisé un FIRE réussi dans les 10 dernières secondes — preuve qu’il détient un jeton valide non expiré.

S’il s’est écoulé plus d’une heure le verrou fire se désactive et TIME est accepté librement, toujours plafonné à +24 h pour les invités.

Ce que cela ferme : un attaquant avec un lot volé mais expiré invité ne peut rien faire. Son FIRE est rejeté pour cause de validité, l’écriture TIME dans la fenêtre fraîche lui est donc inaccessible. Hors de cette fenêtre le décalage reste plafonné à 24 heures, et le propriétaire remet l’heure juste à sa prochaine visite.

En prime : aucune collision

Si le propriétaire et trois invités écrivent des heures différentes en même temps, chaque écriture avance le cliquet si elle est plus grande, ou est rejetée si elle est plus petite. Ni contention ni course ici.

Ce qu’il reste à faire

Micrologiciel du contrôleur :

Android:

Après cela :

NTP comme source principale (facultatif)

Si le contrôleur est à portée du Wi-Fi, il est plus robuste de retirer complètement la synchronisation d’heure BLE du chemin critique et de prendre l’heure directement d’un serveur NTP.

Implémentation dans le micrologiciel (NTP_ENABLED=1):

NTP avec la veille profonde

Sur piles, le contrôleur dort presque tout le temps et ne peut pas maintenir le Wi-Fi. La solution : les réveils ordinaires ne font que du BLE, dans une courte fenêtre d’environ 2,5 s, tandis que chaque DEEP_SLEEP_NTP_EVERY_N_WAKESréveil s’étire à 20 s et lève le Wi-Fi avec SNTP. Avec wake=3 s et N=7200, une synchronisation NTP a lieu environ toutes les 6 heures et le budget énergétique reste acceptable.

Constantes configurables du micrologiciel

Tous les paramètres de la politique d’heure, de la veille et de NTP se trouvent dans le bloc CONFIG en tête du sketch. Ils s’ajustent selon le site :