Schutz der BLE-Zeit vor Fälschung

Warum der Besitzer die Zeit heute nicht liefert

In Version 6.12, aus der das Controller-Log stammt, gab es im Android-Code nichts dergleichen — keine Hintergrund-Zeitsynchronisation vom Telefon des Besitzers. Die Zeit ging nur als Nebeneffekt eines Besitzer-FIRE hinaus (Phase 3), und selbst dann kam sie nicht an: Der Controller trennte die Verbindung direkt nach dem Schalten des Relais.

In 6.13 kam eine eigene Hintergrundverbindung hinzu: Bei jedem passiven Erkennen des Controllers öffnet das Telefon des Besitzers eine eigene GATT-Sitzung und schreibt TIME. Doch die Zeitquelle bleibt allein der Besitzer.

Die Zeit liefern sollte jeder vertrauenswürdige Client können

Sonst sieht es so aus: Der Besitzer ist im Urlaub, der Controller verliert die Stromversorgung, startet mit rtc=0 neu — und die Gäste bleiben bis zu seiner Rückkehr ausgesperrt. So darf man das nicht bauen.

Einem Gast mit gültigem Token muss erlaubt sein, TIME mit seinem Gastschlüssel zu signieren.

Wie man Fälschung verhindert — eine monotone Ratsche, nur für Gäste

Die Idee ist einfach: Beim Gast kann die Zeit nur vorwärts gehen. Dem Besitzer wird vertraut: Er verschiebt sie ohne Grenzen in beide Richtungen — nötig, wenn jemand versehentlich das Jahr 2099 gesetzt hat und es zurückgedreht werden muss.

Der Controller hält die zuletzt bekannte Zeit im NVS (time_ratchet). Bei einem Gast-TIME-Schreibvorgang:

Bei einem Besitzer-TIME-Schreibvorgang wird die Ratsche einfach mit dessen Wert überschrieben, in beide Richtungen.

Nach einem Neustart startet der Controller von rtc = ratchet statt von 0. Die RTC zählt von dort weiter. Ein Werksreset löscht auch die Ratsche, doch die Kopplung schreibt sofort die aktuelle Zeit hinein.

Warum das einen böswilligen Gast stoppt

Der angreifende Gast will ein Token verlängern, dessen valid_until = X). Was kann er versuchen?

Variante 1 — die Zeit zurückdrehen (die Uhr auf gestern stellen, damit das Token frisch wirkt):
→ die Ratsche lässt es nicht zu. Abgelehnt.

Variante 2 — die Zeit vorstellen (die Uhr ein Jahr vorstellen):
→ der Controller nimmt es an, denn die Zeit ist tatsächlich vorgerückt.
→ ABER der Controller vergleicht nun valid_until=X mit now = a year aheadX < now → und sein eigenes Token ist abgelaufen.
→ Der Angreifer hat nur sich selbst geschadet.

Der einzige ehrliche Zug eines Gastes ist, die echte aktuelle Zeit zu setzen. Jede Fälschung wird entweder abgelehnt oder fällt auf ihn zurück.

Wer TIME signiert

Der Controller soll einen TIME-Schreibvorgang annehmen, der signiert ist mit:

Der TIME-Nutzlast werden zwei Bytes hinzugefügt: bleId. Danach versucht der Controller:

  1. den HMAC mit owner_secret — passt er, wird angenommen.
  2. Andernfalls guest_key für diese bleId ableiten und erneut prüfen.

Ohne ein legitimes, über den Server ausgestelltes Gast-Bundle lässt sich TIME nicht signieren — der Angreifer hat keinen Schlüssel.

Zusätzlicher Schutz: ein FIRE-zuerst-Gate im frischen Fenster

Liegt die letzte erfolgreiche Synchronisation weniger als eine Stunde zurück, hält der Controller seine Uhr für frisch und sieht keinen Grund, sie grundlos zu aktualisieren. In diesem Fenster wird ein TIME-Schreibvorgang nur angenommen, wenn der Client in den letzten 10 Sekunden ein erfolgreiches FIRE ausgeführt hat — Beweis für ein gültiges Token mit nicht abgelaufener Frist.

Ist es mehr als eine Stunde her ist das FIRE-Gate aus und TIME wird frei angenommen, weiterhin mit der Obergrenze +24 h für Gäste.

Was das schließt: Ein Angreifer mit einem gestohlenen, aber abgelaufen Gast-Bundle kann nichts ausrichten. Sein FIRE scheitert an der Frist, ein TIME-Schreibvorgang im frischen Fenster bleibt also unerreichbar. Außerhalb des Fensters ist die Verschiebung ohnehin auf 24 Stunden begrenzt, und der Besitzer stellt beim nächsten Besuch die richtige Zeit.

Als Zugabe: keine Kollisionen

Schreiben der Besitzer und drei Gäste gleichzeitig verschiedene Zeiten, so schiebt jeder Schreibvorgang die Ratsche weiter, wenn er größer ist, oder wird abgelehnt, wenn er kleiner ist. Es gibt hier weder Streit noch Wettlauf.

Was zu tun ist

Controller-Firmware:

Android:

Danach:

NTP als primäre Quelle (optional)

Steht der Controller in Wi-Fi-Reichweite, ist es robuster, die BLE-Zeitsynchronisation ganz vom kritischen Pfad zu nehmen und die Zeit direkt von einem NTP-Server zu holen.

Umsetzung in der Firmware (NTP_ENABLED=1):

NTP zusammen mit Deep Sleep

Im Batteriebetrieb schläft der Controller fast durchgehend und kann kein WLAN halten. Die Lösung: gewöhnliche Weckzyklen laufen nur über BLE, in einem kurzen Fenster von etwa 2,5 s, während jeder DEEP_SLEEP_NTP_EVERY_N_WAKES-te Weckzyklus auf 20 s verlängert wird und WLAN mit SNTP hochfährt. Bei wake=3 s und N=7200 erfolgt eine NTP-Synchronisation etwa alle 6 Stunden, und der Energiehaushalt bleibt vertretbar.

Konfigurierbare Konstanten in der Firmware

Alle Parameter der Zeitpolitik, des Schlafs und von NTP stehen im CONFIG-Block am Anfang des Sketches. Sie lassen sich je Standort anpassen: