fix(station): Zugangsdatenpruefung entpackt die Abbilder und prueft beide Rettungssaetze
Die Pruefung des Bildsatzes "ohne-wlan" suchte den Klartext der Werte aus config.local.cmake in der Abbilddatei, wie sie auf der Platte liegt. Der Rumpf eines Abbilds ist aber gzip (ab Byte 56, die letzten 512 Byte sind Trailer bzw. Signatur) - im komprimierten Strom steht der Klartext nicht. Derselbe Pruefkoerper meldete deshalb auch fuer den Satz aus build/, der die Zugangsdaten nachweislich traegt, "sauber". Er haette nie Alarm geschlagen, egal was im Bildsatz lag. Jetzt wird jedes Abbild entpackt und erst dann durchsucht - im Rumpf wie in den Rohbytes, denn Kopf, Trailer und Beidateien wie manifest.json sind unkomprimiert. Ein Abbild, das sich nicht entpacken laesst, ist ein Fehler und bricht den Deploy ab, statt ungeprueft durchzugehen; das gilt auch fuer einen abgeschnittenen gzip-Strom, den decompressobj sonst stillschweigend annimmt. Zweitens hing der ganze Block an der Existenz von migrate-nocreds/. Der Rettungssatz rescue-nocreds/ lief ungeprueft durch, obwohl er der gefaehrlichere von beiden ist: er geht ohne Signatur an die Lampe. Geprueft werden jetzt beide Saetze "ohne-wlan"; migrate-wlan/ und rescue-wlan/ bleiben ungeprueft, die sollen die Daten enthalten. Die Meldung nennt jetzt Zahl der Abbilder und der Saetze statt der Zahl der Suchbegriffe. Gefundene Werte werden weiterhin nie ausgegeben, nur ihr Name und die Datei. Die Testattrappen haben jetzt den echten Aufbau (56 Byte Kopf, gzip-Rumpf, 512 Byte Trailer) und tragen die Zugangsdaten im komprimierten Rumpf; die Gegenproben-Hilfe tree_contains entpackt mit. Neu sind Szenarien fuer Zugangsdaten im Rettungssatz, die Abgrenzung zu rescue-wlan, ein nicht entpackbares Abbild und den Meldungstext. Szenario 33, 39, 41 und 42 sind gegen den alten Pruefkoerper rot.
This commit is contained in:
+85
-16
@@ -329,46 +329,115 @@ fi
|
||||
# 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 =="
|
||||
# Geprueft wird VOR dem Uebertragen, es ist also noch nichts beim Pi angekommen.
|
||||
# Geprueft werden BEIDE Saetze "ohne-wlan" - der Migrationssatz migrate-nocreds/ UND der
|
||||
# Rettungssatz rescue-nocreds/. Der Rettungssatz ist dabei der gefaehrlichere von beiden:
|
||||
# er geht ohne Signatur an die Lampe. Frueher hing dieser ganze Block allein an
|
||||
# migrate-nocreds/, der Rettungssatz lief ungeprueft durch.
|
||||
# migrate-wlan/ und rescue-wlan/ bleiben ungeprueft: die SOLLEN die Zugangsdaten
|
||||
# enthalten, das ist ihr Zweck.
|
||||
#
|
||||
# Gesucht wird im ENTPACKTEN Rumpf, nicht in der Datei, wie sie auf der Platte liegt:
|
||||
# jedes Abbild (_ota.img wie die signierten _prod.img/_dev.img) ist 56 Byte Kopf, dann
|
||||
# ein gzip-Strom, dann 512 Byte Trailer bzw. Signatur. Im komprimierten Strom steht der
|
||||
# Klartext des Passworts nicht - eine Suche im Rohbyteinhalt meldete deshalb IMMER
|
||||
# "sauber", auch fuer den Satz aus build/, der die Zugangsdaten nachweislich traegt. Die
|
||||
# Pruefung hat bis hierhin also nie irgendetwas geprueft.
|
||||
#
|
||||
# Ein Abbild, das sich nicht entpacken laesst, ist ein FEHLER und bricht ab - nicht
|
||||
# "dann eben ungeprueft durchlassen". Genau diese Nachsicht waere der Weg, auf dem das
|
||||
# Passwort am Ende doch hinausgeht.
|
||||
#
|
||||
# Der Pruefkoerper in tools/release.sh ist verwandt, aber nicht mehr identisch: dort
|
||||
# wird ein fertiges Archiv aus lauter verschiedenen Dateiarten geprueft, hier ein
|
||||
# Bildsatz aus Firmware-Abbildern.
|
||||
# Als if-Zweige und nicht als "[ -d … ] && CHECK_SETS+=(…)": unter "set -e" waere eine
|
||||
# solche Zeile, wenn das Verzeichnis fehlt, ein fehlgeschlagenes letztes Kommando und
|
||||
# risse den Deploy ab.
|
||||
declare -a CHECK_SETS=()
|
||||
if [ -d "$STAGE/migrate-nocreds" ]; then CHECK_SETS+=(migrate-nocreds); fi
|
||||
if [ -d "$STAGE/rescue-nocreds" ]; then CHECK_SETS+=(rescue-nocreds); fi
|
||||
if [ ${#CHECK_SETS[@]} -gt 0 ]; then
|
||||
echo "== Bildsaetze 'ohne-wlan' auf Zugangsdaten pruefen =="
|
||||
LOCAL="$ROOT/config.local.cmake"
|
||||
if [ -f "$LOCAL" ]; then
|
||||
python3 - "$STAGE/migrate-nocreds" "$LOCAL" <<'CHECKPY' || exit 1
|
||||
python3 - "$LOCAL" "$STAGE" "${CHECK_SETS[@]}" <<'CHECKPY' || exit 1
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
stage, local = sys.argv[1], sys.argv[2]
|
||||
local, stage, sets = sys.argv[1], sys.argv[2], sys.argv[3:]
|
||||
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"}
|
||||
# Aufbau eines Abbilds: 56 Byte Kopf (vom wm_tool erzeugt), dann der gzip-Strom mit der
|
||||
# eigentlichen Firmware, dann 512 Byte ninlight-Trailer bzw. RSA-Signatur.
|
||||
HEADER_LEN = 56
|
||||
TRAILER_LEN = 512
|
||||
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()))
|
||||
|
||||
|
||||
def unpack(raw):
|
||||
"""Entpackt den gzip-Rumpf eines Abbilds. Wirft bei allem, was nicht passt."""
|
||||
if len(raw) <= HEADER_LEN + TRAILER_LEN:
|
||||
raise ValueError("Datei ist kuerzer als Kopf + Trailer")
|
||||
dec = zlib.decompressobj(zlib.MAX_WBITS | 16)
|
||||
body = dec.decompress(raw[HEADER_LEN:-TRAILER_LEN])
|
||||
# Ohne diese Abfrage waere ein ABGESCHNITTENER gzip-Strom kein Fehler: decompressobj
|
||||
# liefert stillschweigend so viel, wie es lesen konnte. Der ungelesene Rest bliebe
|
||||
# ungeprueft - und genau dort koennte das Passwort stehen.
|
||||
if not dec.eof:
|
||||
raise ValueError("gzip-Strom ist unvollstaendig")
|
||||
return body
|
||||
|
||||
|
||||
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)))
|
||||
broken = []
|
||||
images = 0
|
||||
for image_set in sets:
|
||||
root_dir = os.path.join(stage, image_set)
|
||||
for root, _dirs, files in os.walk(root_dir):
|
||||
for fn in sorted(files):
|
||||
path = os.path.join(root, fn)
|
||||
rel = os.path.relpath(path, stage)
|
||||
with open(path, "rb") as fh:
|
||||
raw = fh.read()
|
||||
# Durchsucht werden beide Sichten: der entpackte Rumpf (dort steht die
|
||||
# Firmware) UND die Rohbytes (Kopf und Trailer werden nicht komprimiert,
|
||||
# und Beidateien wie manifest.json gibt es nur roh).
|
||||
views = [raw]
|
||||
if fn.endswith(".img"):
|
||||
images += 1
|
||||
try:
|
||||
views.append(unpack(raw))
|
||||
except Exception as exc:
|
||||
broken.append("%s (%s)" % (rel, exc))
|
||||
continue
|
||||
for name, needle in needles:
|
||||
if any(needle in view for view in views):
|
||||
# NUR der Name des Wertes und die Datei - niemals der Wert selbst.
|
||||
hits.append("%s in %s" % (name, rel))
|
||||
if broken:
|
||||
sys.stderr.write(
|
||||
"Fehler: Abbild im Bildsatz 'ohne-wlan' laesst sich nicht entpacken, also auch "
|
||||
"nicht auf Zugangsdaten pruefen - nichts uebertragen:\n "
|
||||
+ "\n ".join(broken) + "\n")
|
||||
sys.exit(1)
|
||||
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))
|
||||
print(" sauber (%d Abbilder in %d Saetzen geprueft: %s)"
|
||||
% (images, len(sets), ", ".join(sets)))
|
||||
CHECKPY
|
||||
else
|
||||
echo " uebersprungen - $LOCAL nicht vorhanden" >&2
|
||||
|
||||
Reference in New Issue
Block a user