From c482194503ca480d77b327158ddc57d4832dfdfa Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Tue, 15 Sep 2026 03:57:08 +0200 Subject: [PATCH] fix(station): deploy.sh prueft Bildsatz 'ohne-wlan' auf Zugangsdaten release.sh prueft sein fertiges Archiv binaer gegen die Werte aus config.local.cmake und bricht ab, wenn es etwas findet. deploy.sh signierte dagegen ungeprueft, was in build-nocreds/ lag - und genau dieser Satz ist die sichere Vorgabe, die im Automatikmodus an fremde Lampen in Funkreichweite geht. Der Weg dorthin ist Bedienfehler, nicht Programmfehler: ein von Hand konfiguriertes build-nocreds/, ein gesetztes NINLIGHT_NOCREDS_BUILD_REL=build. Das Ergebnis waere, dass das Panel "ohne WLAN" anzeigt, waehrend das WLAN-Passwort des Bedieners unwiderruflich in fremde Lampen wandert. Derselbe Pruefkoerper wie in release.sh, angewandt auf $STAGE/migrate-nocreds und VOR dem Uebertragen - bei einem Treffer geht kein Byte an den Pi. Szenario 33 stellt den undichten Abzug nach (mit Gegenprobe, dass der Abzug die Zugangsdaten wirklich traegt), Szenario 34 grenzt ab, dass migrate-wlan sie weiterhin tragen darf. Mutationsproben: Pruefblock entfernt = 6 Zusagen rot (die Zugangsdaten landen beim Ziel); Pruefung auf den ganzen STAGE ausgeweitet = 4 Zusagen rot. --- tools/station/deploy.sh | 58 +++++++++++ tools/station/tests/test_deploy.sh | 148 +++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) diff --git a/tools/station/deploy.sh b/tools/station/deploy.sh index ee64044..f7ca8cd 100755 --- a/tools/station/deploy.sh +++ b/tools/station/deploy.sh @@ -241,6 +241,64 @@ else echo "Hinweis: Schluesselverzeichnis $SIGN_DIR nicht gefunden - keine Migrationsimages erzeugt" >&2 fi +# Der Bildsatz "ohne-wlan" ist die SICHERE VORGABE: er geht im Automatikmodus an jede +# alte Klyqa-Lampe in Funkreichweite, auch an fremde, und das Flashen ist nicht +# umkehrbar. tools/release.sh prueft sein fertiges Archiv deshalb binaer gegen die Werte +# aus config.local.cmake (dort "Auf Zugangsdaten pruefen") - deploy.sh signierte +# dagegen ungeprueft, was in build-nocreds/ lag. +# +# Der Weg dorthin ist Bedienfehler, nicht Programmfehler: ein von Hand konfiguriertes +# build-nocreds/, oder ein gesetztes NINLIGHT_NOCREDS_BUILD_REL=build. Das +# Ergebnis waere, dass das Panel "ohne WLAN" anzeigt, waehrend das WLAN-Passwort des +# Bedieners unwiderruflich in fremde Lampen wandert - der teure, nicht rueckholbare +# Fehler. Lieber ein abgebrochener Deploy. +# +# Geprueft wird VOR dem Uebertragen, es ist also noch nichts beim Pi angekommen. Nur +# dieser eine Satz: migrate-wlan/ SOLL die Zugangsdaten enthalten, das ist sein Zweck. +# Der Pruefkoerper ist bis auf die Meldung derselbe wie in tools/release.sh. +if [ -d "$STAGE/migrate-nocreds" ]; then + echo "== Bildsatz 'ohne-wlan' auf Zugangsdaten pruefen ==" + LOCAL="$ROOT/config.local.cmake" + if [ -f "$LOCAL" ]; then + python3 - "$STAGE/migrate-nocreds" "$LOCAL" <<'CHECKPY' || exit 1 +import os +import re +import sys + +stage, local = sys.argv[1], sys.argv[2] +text = open(local, encoding="utf-8", errors="replace").read() +# Nur die drei Werte, die wirklich geheim sind. NINLIGHT_AP_PASSWORD und der +# Vorgabetoken stehen oeffentlich im Repo (README, Bruno) - sie hier mitzupruefen +# haette nur falsche Alarme erzeugt. +SECRETS = ("NINLIGHT_WIFI_SSID", "NINLIGHT_WIFI_PASSWORD", "NINLIGHT_TOKEN") +PUBLIC = {"", "ninlight", "aabbccddeeff0011223344", "ninlight-recovery", "change-me-please"} +needles = [] +for name in SECRETS: + m = re.search(r'^\s*set\s*\(\s*%s\s+"([^"]*)"' % name, text, re.M) + if m and m.group(1) not in PUBLIC: + needles.append((name, m.group(1).encode())) + +hits = [] +for root, _dirs, files in os.walk(stage): + for fn in sorted(files): + path = os.path.join(root, fn) + with open(path, "rb") as fh: + data = fh.read() + for name, needle in needles: + if needle in data: + hits.append("%s in %s" % (name, os.path.relpath(path, stage))) +if hits: + sys.stderr.write( + "Fehler: Zugangsdaten aus config.local.cmake im Bildsatz 'ohne-wlan' gefunden, " + "nichts uebertragen:\n " + "\n ".join(hits) + "\n") + sys.exit(1) +print(" sauber (%d geprueft)" % len(needles)) +CHECKPY + else + echo " uebersprungen - $LOCAL nicht vorhanden" >&2 + fi +fi + echo "== uebertragen ==" # -c (Inhaltsvergleich per Checksumme statt Groesse+Zeitstempel): ein neues # Image kann zufaellig dieselbe Byte-Groesse wie das alte haben, und bei diff --git a/tools/station/tests/test_deploy.sh b/tools/station/tests/test_deploy.sh index c5fc25d..3b65822 100755 --- a/tools/station/tests/test_deploy.sh +++ b/tools/station/tests/test_deploy.sh @@ -247,6 +247,61 @@ with open(path, 'wb') as fh: " "$path" } +# Wegwerf-Zugangsdaten fuer die Pruefung des Bildsatzes "ohne-wlan" - niemals die +# echten aus dem Repo. Dieselbe Rolle wie SECRET_* in tools/tests/test_release.sh. +SECRET_SSID="TestWLAN-9c2e-geheim" +SECRET_PASS="testpasswort-9c2e" +SECRET_TOKEN="0badc0de9c2e0badc0de9c2e" + +# Legt config.local.cmake im Sandbox-Repo an - genau die Datei, gegen die deploy.sh den +# Bildsatz "ohne-wlan" prueft. +write_local_cmake() { + local sandbox=$1 + cat >"$sandbox/config.local.cmake" < nichts uebertragen ===" SANDBOX="$TMPROOT/s1"; STUBBIN="$TMPROOT/bin1"; LOG="$TMPROOT/s1.log"; SANDBOX_STATE="$TMPROOT/s1-state" setup_sandbox "$SANDBOX" @@ -1271,6 +1326,99 @@ grep -Fq "systemctl enable ninlight-poweroff" "$LOG" \ [ ! -e /tmp/ninlight-poweroff.service ] && [ ! -e /tmp/ninlight-reboot.service ] \ && pass "die Ablage wurde aufgeraeumt" || fail "Unit-Dateien blieben unter /tmp liegen" +echo +echo "=== Szenario 33 (Abschlussreview, I3): Zugangsdaten im Bildsatz 'ohne-wlan' brechen den Deploy ab ===" +# Der Satz "ohne-wlan" ist die SICHERE VORGABE - im Automatikmodus geht er an jede alte +# Klyqa-Lampe in Funkreichweite, auch an fremde, und das Flashen ist nicht umkehrbar. +# release.sh prueft sein Archiv binaer gegen config.local.cmake; deploy.sh signierte +# bisher ungeprueft, was in build-nocreds/ lag. Ein von Hand konfiguriertes +# build-nocreds/ genuegt, damit das Panel "ohne WLAN" anzeigt, waehrend das +# WLAN-Passwort des Bedieners unwiderruflich in fremde Lampen wandert. +SANDBOX="$TMPROOT/s33"; STUBBIN="$TMPROOT/bin33"; LOG="$TMPROOT/s33.log"; SANDBOX_STATE="$TMPROOT/s33-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +write_local_cmake "$SANDBOX" +mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" W +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +# Der undichte Abzug: enthaelt SSID, Passwort und Token. +make_gz_file_with_creds "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N +# GEGENPROBE (ohne sie waere dieses Szenario wertlos): der "Compiler" dieser Suite muss +# die Zugangsdaten wirklich einbauen. Genau diese Gegenprobe gibt es in test_release.sh +# fuer den Docker-Stub - dort hat sie belegt, dass Fall 5 nicht nur deshalb gruen ist, +# weil nie jemand etwas einbaut. +if tree_contains "$SANDBOX/build-nocreds" "$SECRET_PASS"; then + pass "Gegenprobe: der Abzug in build-nocreds/ enthaelt das WLAN-Passwort wirklich" +else + fail "Gegenprobe fehlgeschlagen - der Abzug enthaelt gar keine Zugangsdaten, Szenario 33 waere wertlos" +fi +# Der Pi hat schon einen (sauberen) Bestand aus einem frueheren Deploy - er darf durch +# den Abbruch weder ersetzt noch geloescht werden. +mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds" +echo "alter-sauberer-abzug" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img" +: >"$LOG" +set +e +OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +[ "$RC" -ne 0 ] && pass "Exit-Code != 0 ($RC)" || fail "Exit-Code war 0 - der undichte Bildsatz waere ausgeliefert worden: $OUT" +echo "$OUT" | grep -q "Zugangsdaten" \ + && pass "Fehlermeldung nennt die gefundenen Zugangsdaten" \ + || fail "keine Fehlermeldung zu Zugangsdaten: $OUT" +echo "$OUT" | grep -q "NINLIGHT_WIFI_PASSWORD" \ + && pass "Fehlermeldung nennt den betroffenen Schluessel" \ + || fail "Fehlermeldung nennt den Schluessel nicht: $OUT" +if ! grep -q "STUB ssh\|STUB rsync\|STUB scp" "$LOG"; then + pass "kein ssh/rsync/scp aufgerufen - nichts wurde uebertragen" +else + fail "es wurde trotz gefundener Zugangsdaten uebertragen: $(cat "$LOG")" +fi +[ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img")" = "alter-sauberer-abzug" ] \ + && pass "der Bestand auf dem Pi blieb unangetastet" \ + || fail "der Bestand auf dem Pi wurde veraendert" +if ! tree_contains "$SANDBOX_STATE" "$SECRET_PASS"; then + pass "kein Zugangsdatum hat es in die Sandbox-Zielablage geschafft" +else + fail "Zugangsdaten sind trotz Abbruch beim Ziel gelandet" +fi + +echo +echo "=== Szenario 34 (Abgrenzung zu 33): ein sauberer Satz 'ohne-wlan' geht raus, migrate-wlan DARF die Zugangsdaten tragen ===" +# Die Pruefung muss genau einen Satz betreffen. migrate-wlan/ enthaelt die Zugangsdaten +# absichtlich - das ist sein ganzer Zweck ("Lampe haengt sofort im eigenen WLAN"). Eine +# Pruefung, die auch ihn abweist, waere unbenutzbar und wuerde nach kurzer Zeit +# abgeschaltet. +SANDBOX="$TMPROOT/s34"; STUBBIN="$TMPROOT/bin34"; LOG="$TMPROOT/s34.log"; SANDBOX_STATE="$TMPROOT/s34-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +write_local_cmake "$SANDBOX" +mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build-nocreds/foo" +# build/ MIT Zugangsdaten (wie im echten Betrieb), build-nocreds/ ohne. +make_gz_file_with_creds "$SANDBOX/build/foo/ninlight_foo_gz.img" W +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +make_gz_file "$SANDBOX/build-nocreds/foo/ninlight_foo_gz.img" N +: >"$LOG" +set +e +OUT=$(TEST_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +[ "$RC" -eq 0 ] && pass "Exit-Code 0" || fail "Exit-Code $RC - die Pruefung weist einen sauberen Satz ab: $OUT" +[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds/foo_dev.img" ] \ + && pass "der saubere Satz 'ohne-wlan' kam an" || fail "der saubere Satz 'ohne-wlan' fehlt: $OUT" +[ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan/foo_dev.img" ] \ + && pass "migrate-wlan kam trotz einkompilierter Zugangsdaten an" \ + || fail "migrate-wlan wurde faelschlich mit abgewiesen: $OUT" +if tree_contains "$SANDBOX_STATE/opt/ninlight-station/images/migrate-wlan" "$SECRET_PASS"; then + pass "Gegenprobe: migrate-wlan traegt die Zugangsdaten wirklich - die Pruefung haette anschlagen KOENNEN" +else + fail "Gegenprobe fehlgeschlagen - migrate-wlan traegt gar keine Zugangsdaten, die Abgrenzung waere wertlos" +fi +if ! tree_contains "$SANDBOX_STATE/opt/ninlight-station/images/migrate-nocreds" "$SECRET_PASS"; then + pass "und der Satz 'ohne-wlan' auf dem Pi ist frei davon" +else + fail "der Satz 'ohne-wlan' auf dem Pi enthaelt Zugangsdaten" +fi + echo if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden."