Files
ninlight-w600/tools/station
Michael Sauer ad2f1b8486 fix(station): Manifest-Form, Teilschluessel-Faelle und Schluessel-Testisolation
Fixrunde 1 zu "Migrationsimages beim Deploy signieren":

- manifest.json traegt ninlight_version und ein neues built jetzt auf oberster
  Ebene statt pro Eintrag, dazu "file" statt der Redundanz einer Version pro
  Bild - genau die Form aus der Spec (Abschnitt 4) und aus server.py's
  Station.manifest_version(), die vorher immer "unbekannt" gelesen hat, weil
  sie ausschliesslich auf oberster Ebene sucht (C1). Ein Testfall schickt das
  Manifest jetzt zusaetzlich durch genau diese Funktion statt nur durch
  json.load.

- Ein Schluesselverzeichnis, das existiert, aber weder Produktions- noch
  Entwicklungsschluessel enthaelt, legt kein STAGE/migrate mehr an - das
  vorherige Verhalten liess dafuer trotzdem ein (leeres) migrate/ mit
  Manifest entstehen, das der volle rsync-Abgleich dann mit --delete gegen
  ein vorhandenes migrate/ auf dem Pi durchgesetzt und es geleert hat (C2).
  Aus demselben Grund war die _gz.img-Pflichtpruefung faelschlich
  unconditional: fehlte sie, brach sogar ein Deploy ab, der gar nicht
  signieren wollte, und nahm die Rettungsimages gleich mit (C3).

- Ein neues MIGRATE_COMPLETE-Flag (nur 1, wenn dieser Lauf mit BEIDEN
  Schluesseln signiert hat) entscheidet jetzt, ob der Abgleich auf dem Pi
  --delete benutzen und das Manifest ersetzen darf. Bei nur einem der beiden
  Schluessel oder einem gezielten Deploy synct migrate/ ohne --delete und
  merged das Manifest serverseitig mit dem vorhandenen zusammen, statt es zu
  ueberschreiben - vorher raeumte ein Voll-Deploy mit nur dem dev-Schluessel
  saemtliche _prod.img weg (C2, Teilfall), und ein gezielter Deploy liess ein
  Manifest zurueck, das die unveraendert gebliebenen Ziele nicht mehr
  erwaehnte (I1).

- ${HOME:-} statt $HOME beim Ermitteln von SIGN_DIR, damit ein Aufruf unter
  `set -u` ohne gesetztes HOME (cron, env -i) nicht unnoetig abbricht, obwohl
  nur die Rettungsimages gebraucht werden.

- tests/test_deploy.sh: die Sicherheitsabsicherung aus dem letzten Durchgang
  hatte selbst eine Luecke (C4) - "${NINLIGHT_SIGN_DIR:-default}" in
  run_deploy() greift nur, wenn die Variable NICHT gesetzt ist, und genau die
  ist es, mit der man auf ~/workspace/fw-sign (das ECHTE Schluesselverzeichnis
  auf diesem Rechner) zeigt. run_deploy() setzt NINLIGHT_SIGN_DIR jetzt hart
  und liest dafuer nur noch die eigene Testvariable TEST_SIGN_DIR, die
  deploy.sh nicht kennt und die deshalb nie von aussen vorbelegt sein kann;
  am Dateikopf werden zur Sicherheit beide Variablen ge-unset. Verifiziert:
  die Suite bleibt gruen, wenn NINLIGHT_SIGN_DIR vorher exportiert ist wie in
  einem echten Entwicklerprofil.

- Szenario 15 vergleicht jetzt ALLE Zeilen beider privater Testschluessel
  gegen Ausgabe UND Log, nicht nur eine einzelne Zeile des Produktions-
  schluessels gegen die Ausgabe.

Vier neue/erweiterte Testfaelle (18-20) plus die Ergaenzung an Szenario 14:
Verzeichnis vorhanden aber leer, Voll-Deploy mit nur dev-Schluessel, echter
Server-Leser fuer das Manifest, gezielter Deploy fuehrt das Manifest zusammen
statt es zu verkuerzen. Jeder der vier Critical-Befunde wurde vor dem Fix per
Rot-Probe am kaputten Code bestaetigt.
2026-09-13 20:14:19 +02:00
..

ninlight-Recovery-Station

Ein Raspberry Pi spannt den Zugangspunkt ninlight-recovery auf und liefert unter http://192.168.8.1/ninlight_<target>_ota.img Firmware-Images aus.

Jede ninlight-Lampe sucht beim Booten bis zu drei Sekunden nach dieser Station. Findet sie sie, lädt sie das Image für ihren Gerätetyp und flasht es — ohne dass die Lampe im WLAN erreichbar sein muss. Das ist der Rettungsweg für Lampen, an die man sonst nur noch mit dem Lötkolben käme.

Trägt das angebotene Image dieselbe Version, die bereits läuft, lehnt die Lampe es ab und bootet normal weiter. Eine angelassene Station flasht also nicht in einer Endlosschleife.

Voraussetzung

Der SSH-Benutzer auf dem Pi braucht passwortloses sudo — beide Skripte und die CLI (ninlight-station on|off|status) rufen sudo nicht-interaktiv auf; ein Passwort-Prompt würde dort nur mit einer Fehlermeldung enden.

Bedienung

tools/station/setup_pi.sh <pi-host>        einmalig einrichten
tools/station/deploy.sh <pi-host> [target] Images ausliefern
ssh <pi-host> ninlight-station on          einschalten
ssh <pi-host> ninlight-station status      Zustand und letzte Auslieferungen
ssh <pi-host> ninlight-station off         ausschalten

SSID und Passwort stehen in /etc/ninlight-station.conf auf dem Pi und müssen zu den Build-Parametern NINLIGHT_RECOVERY_SSID und NINLIGHT_RECOVERY_PASSWORD aus config.local.cmake passen. Weicht eines ab, findet die Lampe die Station nicht.

Vertrauensgrenze

Der Trailer eines Images prüft Gerätetyp und SHA256. Er ist keine kryptografische Signatur, und der Download läuft über http:// ohne TLS. Die Station schützt allein ein WPA2-Passwort, dessen Default im Repo steht. Wer in Funkreichweite ist und es kennt, kann jeder Lampe beim nächsten Boot beliebige Firmware unterschieben.

Das ist gewollt: Die Signaturprüfung der alten Firmware auszuhebeln ist der Zweck dieses Projekts. Daraus folgen zwei Regeln.

Setz ein eigenes Passwort. In config.local.cmake und in /etc/ninlight-station.conf — an beiden Stellen denselben Wert.

Lass die Station nur an, solange du sie brauchst. Nicht nur wegen des Fremdzugriffs: Die Firmware löscht den Power-Cycle-Zähler, bevor sie einen Recovery-Versuch startet. Solange eine Station in Reichweite ist, funktioniert der Werksreset per fünf Stromzyklen bei keiner Lampe in Reichweite mehr.