Files
ninlight-w600/tools/tests/test_release.sh
T
Michael Sauer e10147b70f Merge remote-tracking branch 'origin/montana-black-shield' into merge-probe
* origin/montana-black-shield:
  docs(bulb-map): Montana-Black-Shield (f960) ergaenzt
  chore(tools): qconnex-Migrationswerkzeug und HA-Integration ins Repo
  feat(station): Montana-Black-Produkt-ID -> Ziel ledshield_rgb
  feat(board): ledshield_rgb — Montana Black RGB-LED-Shield (analoge RGB-Streifen)
  build: tools/podman.sh — docker.sh-Pendant für podman (gleiches Image, gleicher Mount)
  Überträgt lokale Änderungen (nl_crc8 unsigned-shift, docker.sh cd -P) in neuen Top-Level-Tree und entfernt altes ninlight-w600/
  Initial commit
2026-09-26 17:18:27 +02:00

659 lines
28 KiB
Bash
Executable File

#!/usr/bin/env bash
# Stub-Tests fuer tools/release.sh (und tools/build_nocreds.sh) - ohne Docker, ohne
# echten Firmwarebau ausfuehrbar.
#
# Gestubbt wird nur die UNTERSTE Ebene: NINLIGHT_DOCKER zeigt auf ein Skript, das sich
# wie der Bau im Container verhaelt (relative Pfade ab Repo-Wurzel, legt _gz.img und
# _ota.img ab). Alles darueber - das Beiseitelegen von config.local.cmake, das
# Signieren, README/SHA256SUMS, das Packen - laeuft echt. Wichtig dabei: der Stub
# verhaelt sich wie ein Compiler und SCHREIBT die Werte aus config.local.cmake in die
# Images, wenn die Datei zum Bauzeitpunkt noch da ist. Nur so pruefen die Faelle 5/6
# wirklich das Skript und nicht den Stub: vergisst release.sh/build_nocreds.sh das
# Beiseitelegen, landen die Zugangsdaten tatsaechlich im Abzug.
#
# Und zwar an der Stelle, an der sie beim echten Bau auch landen: IM KOMPRIMIERTEN
# RUMPF. Ein Abbild ist 56 Byte Kopf, dann ein gzip-Strom, dann - beim _ota.img - 512
# Byte Trailer. Frueher schrieb der Stub reinen Klartext in eine Datei, die er nur
# "..._ota.img" nannte; damit fand die rohe Bytesuche in release.sh die Werte, und Fall
# 5 war gruen, obwohl der Pruefkoerper an echten Abbildern nie irgendetwas gefunden
# haette. Ein Testartefakt, das dem Pruefling recht gibt, ist schlimmer als kein Test.
#
# Aufruf: tools/tests/test_release.sh
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd)
TOOLS_DIR=$(cd "$HERE/.." && pwd)
REPO=$(cd "$TOOLS_DIR/.." && pwd)
# Sicherheitsnetz (wie in tools/station/tests/test_deploy.sh): NINLIGHT_SIGN_DIR zeigt
# auf diesem Rechner auf ECHTE Signierschluessel. Sie darf nicht aus der Umgebung in
# diese Suite durchsickern - jedes Szenario setzt sie hart, Testschluessel kommen ueber
# die separate Variable TEST_SIGN_DIR.
unset NINLIGHT_SIGN_DIR
unset TEST_SIGN_DIR
unset NINLIGHT_NOCREDS_BUILD_REL
unset NINLIGHT_DOCKER
# GNU vor BSD: auf dem Pi/Debian gibt es sha256sum, auf dem Mac nur shasum.
if command -v sha256sum >/dev/null 2>&1; then
SHA_CHECK=(sha256sum -c)
else
SHA_CHECK=(shasum -a 256 -c)
fi
TMPROOT=$(mktemp -d)
trap 'rm -rf "$TMPROOT"' EXIT
FAILED=0
pass() { echo "ok - $1"; }
fail() { echo "FEHLER - $1" >&2; FAILED=1; }
TARGETS=(evk e27_rgb e14_rgb gu10_rgb e27_cw e14_cw gu10_cw g95_cw ledshield_rgb)
# Wegwerf-Zugangsdaten - niemals die echten aus dem Repo.
SECRET_SSID="TestWLAN-4b7f-geheim"
SECRET_PASS="testpasswort-4b7f"
SECRET_TOKEN="0badc0de4b7f0badc0de4b7f"
# --- Sandbox-Repo: release.sh, build_nocreds.sh, nl_sign.py, bruno/, config.local.cmake ---
setup_sandbox() {
local sandbox=$1
rm -rf "$sandbox"
mkdir -p "$sandbox/tools/mkimage" "$sandbox/bruno" "$sandbox/dist"
cp "$TOOLS_DIR/release.sh" "$sandbox/tools/release.sh"
cp "$TOOLS_DIR/build_nocreds.sh" "$sandbox/tools/build_nocreds.sh"
chmod +x "$sandbox/tools/release.sh" "$sandbox/tools/build_nocreds.sh"
# Das ECHTE Signierwerkzeug, nur die Schluessel sind Wegwerf-Testschluessel.
cp "$TOOLS_DIR/mkimage/nl_sign.py" "$sandbox/tools/mkimage/nl_sign.py"
# Die ECHTE Bruno-Collection: die Zusage ist, dass sie im Archiv liegt - mit einer
# erfundenen Attrappe wuerde der Test nur sich selbst pruefen.
cp "$REPO/bruno/ninlight.yml" "$sandbox/bruno/ninlight.yml"
cat >"$sandbox/config.local.cmake" <<EOF
set(NINLIGHT_WIFI_SSID "$SECRET_SSID")
set(NINLIGHT_WIFI_PASSWORD "$SECRET_PASS")
set(NINLIGHT_TOKEN "$SECRET_TOKEN")
EOF
}
# --- Docker-Stub: verhaelt sich wie der Bau im Container ---
# Steuerbar ueber:
# STUB_FAIL_TARGET bricht beim Bau dieses Ziels mit Exit 1 ab
# STUB_TERM_TARGET schickt beim Bau dieses Ziels SIGTERM an den Aufrufer
# (build_nocreds.sh) - erzwungener Abbruch mitten im Lauf
# STUB_LEAK=1 schreibt die Zugangsdaten IMMER ins Image, auch wenn
# config.local.cmake beiseite liegt (prueft den doppelten Boden)
# STUB_TRUNC_TARGET schneidet beim _ota.img dieses Ziels den gzip-Strom ab - das
# Abbild laesst sich dann nicht entpacken. Das _gz.img bleibt heil,
# damit das Signieren davor nicht schon abbricht.
setup_stub_docker() {
local stub=$1 sandbox=$2
cat >"$stub" <<'STUB'
#!/usr/bin/env bash
# Erwartet "cmake -S . -B <rel>/<ziel> ..." bzw. "cmake --build <rel>/<ziel>".
set -euo pipefail
cd "$NINLIGHT_STUB_ROOT"
mode=""
outdir=""
args=("$@")
for ((i = 0; i < ${#args[@]}; i++)); do
case "${args[$i]}" in
--build) mode=build; outdir=${args[$((i + 1))]} ;;
-B) mode=${mode:-configure}; outdir=${args[$((i + 1))]} ;;
esac
done
[ -n "$outdir" ] || { echo "Stub: kein Build-Verzeichnis in '$*'" >&2; exit 1; }
target=$(basename "$outdir")
mkdir -p "$outdir"
[ "$mode" = "build" ] || exit 0
if [ "${STUB_TERM_TARGET:-}" = "$target" ]; then
kill -TERM "$PPID"
sleep 5
exit 1
fi
if [ "${STUB_FAIL_TARGET:-}" = "$target" ]; then
echo "Stub: Bau von $target fehlgeschlagen" >&2
exit 1
fi
# "Compiler": die Zugangsdaten aus config.local.cmake landen im Abzug - genau wie beim
# echten Bau, wo CMakeLists.txt sie per set() einkompiliert.
creds=""
if [ -f config.local.cmake ]; then
creds=$(sed -n 's/.*set(NINLIGHT_[A-Z_]* "\(.*\)").*/\1/p' config.local.cmake | tr '\n' ' ')
fi
if [ "${STUB_LEAK:-}" = "1" ]; then
creds="$STUB_LEAK_VALUE"
fi
CREDS="$creds" TARGET="$target" OUTDIR="$outdir" TRUNC="${STUB_TRUNC_TARGET:-}" python3 - <<'PY'
import gzip
import io
import os
target, outdir, creds = os.environ["TARGET"], os.environ["OUTDIR"], os.environ["CREDS"]
# Der Rumpf ist ~100 kB gleichfoermiger Text - so wie echte Firmware gross genug, dass
# deflate ihn WIRKLICH komprimiert. Zwei Klippen liegen hier dicht nebeneinander:
# zu kleiner Rumpf -> gzip-Strom unter 1024 Byte, und nl_sign.py lehnt ab;
# unkomprimierbarer Rumpf (etwa Zufallsbytes) -> deflate legt ihn als "stored block"
# UNVERAENDERT ab, der Klartext stuende wieder roh in der Datei und der Test prueft
# erneut eine Welt, die es nicht gibt.
body = bytearray(
b"".join(b"ninlight-%s-block-%06d\n" % (target.encode(), i) for i in range(4000)))
tag = ("firmware-%s-" % target).encode()
body[0:len(tag)] = tag
blob = creds.encode()
if blob:
# Die Zugangsdaten liegen IM Rumpf - also nach dem Packen im komprimierten Strom
# und nicht im Klartext in der Datei. Genau wie beim echten Compiler.
body[100:100 + len(blob)] = blob
# Aufbau wie beim echten Bau: 56 Byte Kopf (wm_tool), dann der gzip-Strom.
header = bytearray(b"\x9f\xff\xff\xa0" + b"\x00" * 52)
header[4:8] = b"STUB"
buf = io.BytesIO()
# mtime=0: sonst haengt der Dateiinhalt an der Uhrzeit und Vergleiche werden wacklig.
with gzip.GzipFile(fileobj=buf, mode="wb", mtime=0) as gf:
gf.write(bytes(body))
stream = buf.getvalue()
raw = bytes(header) + stream
# nl_sign.py verlangt eine durch 4 teilbare Laenge von mindestens 1024 Byte. Die
# Fuellbytes stehen hinter dem gzip-Strom und stoeren das Entpacken nicht.
raw += b"\x00" * (-len(raw) % 4)
assert len(raw) >= 1024, "Stub-Abbild zu kurz fuer nl_sign.py"
# Selbstkontrolle gegen genau den Fehler, der diese Suite jahrelang wertlos machte.
assert not blob or blob not in raw, \
"Zugangsdaten stehen roh in der Datei - der Rumpf wurde nicht wirklich komprimiert"
with open(os.path.join(outdir, "ninlight_%s_gz.img" % target), "wb") as fh:
fh.write(raw) # roh, ohne NINL-Trailer - das signiert nl_sign.py
trailer = bytearray(b"\xff" * 512)
trailer[0:4] = b"NINL"
ota_body = raw
if os.environ.get("TRUNC") == target:
# Abgeschnittener gzip-Strom: decompressobj liefert dafuer stillschweigend nur den
# lesbaren Teil, wenn niemand dec.eof abfragt.
ota_body = bytes(header) + stream[:len(stream) // 2]
with open(os.path.join(outdir, "ninlight_%s_ota.img" % target), "wb") as fh:
fh.write(ota_body + bytes(trailer))
PY
STUB
chmod +x "$stub"
}
# Wegwerf-Testschluessel ueber nl_sign.generate_test_key() - nie echte Schluessel.
make_test_keys() {
local keydir=$1 with_prod=$2 with_dev=$3
mkdir -p "$keydir/QCX_Device_CA"
PYTHONPATH="$TOOLS_DIR/mkimage" python3 - "$keydir" "$with_prod" "$with_dev" <<'PY'
import sys
import nl_sign
keydir, with_prod, with_dev = sys.argv[1], sys.argv[2] == "1", sys.argv[3] == "1"
if with_prod:
k = nl_sign.generate_test_key()
with open(keydir + "/QCX_Device_CA/qcx_firmware_signing_key.pem", "wb") as fh:
fh.write(k.private_pem)
with open(keydir + "/prod_pub.der", "wb") as fh:
fh.write(k.public_der)
if with_dev:
k = nl_sign.generate_test_key()
with open(keydir + "/fw_sign.pem", "wb") as fh:
fh.write(k.private_pem)
with open(keydir + "/dev_pub.der", "wb") as fh:
fh.write(k.public_der)
PY
}
# Ruft release.sh in der Sandbox auf. NINLIGHT_SIGN_DIR wird hier HART gesetzt, nie aus
# der geerbten Umgebung uebernommen (siehe Sicherheitsnetz oben).
run_release() {
local sandbox=$1 version=$2
local sign_dir="${TEST_SIGN_DIR:-$sandbox/kein-schluesselverzeichnis}"
env NINLIGHT_SIGN_DIR="$sign_dir" \
NINLIGHT_DOCKER="$sandbox/stub-docker" \
NINLIGHT_STUB_ROOT="$sandbox" \
STUB_FAIL_TARGET="${STUB_FAIL_TARGET:-}" \
STUB_TERM_TARGET="${STUB_TERM_TARGET:-}" \
STUB_LEAK="${STUB_LEAK:-}" \
STUB_TRUNC_TARGET="${STUB_TRUNC_TARGET:-}" \
STUB_LEAK_VALUE="$SECRET_SSID" \
"$sandbox/tools/release.sh" "$version"
}
# Sucht die Zugangsdaten in jeder Datei unterhalb eines Verzeichnisses - in den
# Rohbytes UND im entpackten Rumpf jedes Abbilds. Nur roh zu suchen hiesse, die
# Zusage "keine Zugangsdaten im Archiv" gar nicht zu pruefen: im gzip-Strom steht
# kein Klartext. Ein Abbild, das sich nicht entpacken laesst, wird hier gemeldet und
# nicht verschwiegen - sonst haette die Gegenprobe ein stilles Loch.
# $2 (optional) = "roh": nur die Rohbytes durchsuchen, nichts entpacken.
grep_secrets() {
local dir=$1 mode=${2:-entpackt}
SSID="$SECRET_SSID" PASS="$SECRET_PASS" TOKEN="$SECRET_TOKEN" \
python3 - "$dir" "$mode" <<'PY'
import os
import sys
import zlib
HEADER_LEN = 56
root, mode = sys.argv[1], sys.argv[2]
needles = {k: os.environ[k].encode() for k in ("SSID", "PASS", "TOKEN")}
for dirpath, _dirs, files in os.walk(root):
for fn in sorted(files):
p = os.path.join(dirpath, fn)
rel = os.path.relpath(p, root)
if os.path.islink(p):
continue
with open(p, "rb") as fh:
data = fh.read()
views = [data]
if mode != "roh" and fn.endswith(".img"):
dec = zlib.decompressobj(zlib.MAX_WBITS | 16)
try:
plain = dec.decompress(data[HEADER_LEN:])
if not dec.eof:
raise ValueError("gzip-Strom unvollstaendig")
views.append(plain)
except Exception as exc:
print("NICHT ENTPACKBAR %s (%s)" % (rel, exc))
for name, needle in needles.items():
if any(needle in view for view in views):
print("%s in %s" % (name, rel))
PY
}
sha_of() { python3 -c "
import hashlib,sys
print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$1"; }
echo "=== Szenario 1: Erfolgslauf mit beiden Schluesseln ==="
SANDBOX="$TMPROOT/s1"
setup_sandbox "$SANDBOX"
setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX"
KEYS_FULL="$TMPROOT/keys-full"
make_test_keys "$KEYS_FULL" 1 1
CONF_BEFORE=$(sha_of "$SANDBOX/config.local.cmake")
set +e
OUT=$(TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC: $OUT"; fi
TGZ="$SANDBOX/dist/ninlight-0.9.9.tar.gz"
if [ -f "$TGZ" ]; then pass "Archiv entstanden"; else fail "kein Archiv unter $TGZ"; fi
EXTRACT="$TMPROOT/x1"
mkdir -p "$EXTRACT"
tar xzf "$TGZ" -C "$EXTRACT"
A="$EXTRACT/ninlight-0.9.9"
if [ -d "$A" ]; then pass "Archiv entpackt nach ninlight-0.9.9/"; else fail "Verzeichnis ninlight-0.9.9/ fehlt"; fi
echo "--- Fall 1/2/3: Verzeichnisse und Dateizahlen ---"
for d in ota migrate flash bruno; do
if [ -d "$A/$d" ]; then pass "$d/ vorhanden"; else fail "$d/ fehlt"; fi
done
for f in SHA256SUMS README.md onboard.sh bruno/ninlight.yml; do
if [ -f "$A/$f" ]; then pass "$f vorhanden"; else fail "$f fehlt"; fi
done
if [ -x "$A/onboard.sh" ]; then pass "onboard.sh ist ausfuehrbar"; else fail "onboard.sh ist nicht ausfuehrbar"; fi
N_OTA=$(find "$A/ota" -type f | wc -l | tr -d ' ')
N_FLASH=$(find "$A/flash" -type f | wc -l | tr -d ' ')
N_MIG=$(find "$A/migrate" -type f | wc -l | tr -d ' ')
[ "$N_OTA" = "${#TARGETS[@]}" ] && pass "ota/ enthaelt ${#TARGETS[@]} Dateien" || fail "ota/ enthaelt $N_OTA statt ${#TARGETS[@]}"
[ "$N_FLASH" = "${#TARGETS[@]}" ] && pass "flash/ enthaelt ${#TARGETS[@]} Dateien" || fail "flash/ enthaelt $N_FLASH statt ${#TARGETS[@]}"
[ "$N_MIG" = "$(( 2 * ${#TARGETS[@]} ))" ] && pass "migrate/ enthaelt $(( 2 * ${#TARGETS[@]} )) Dateien" || fail "migrate/ enthaelt $N_MIG statt $(( 2 * ${#TARGETS[@]} ))"
MISS=""
for t in "${TARGETS[@]}"; do
[ -f "$A/ota/ninlight_${t}_ota.img" ] || MISS="$MISS ota/$t"
[ -f "$A/flash/ninlight_${t}_gz.img" ] || MISS="$MISS flash/$t"
[ -f "$A/migrate/${t}_prod.img" ] || MISS="$MISS migrate/${t}_prod"
[ -f "$A/migrate/${t}_dev.img" ] || MISS="$MISS migrate/${t}_dev"
done
[ -z "$MISS" ] && pass "je Ziel genau die erwarteten Dateinamen" || fail "fehlende Dateien:$MISS"
echo "--- Migrationsimages sind echt signiert (und zwar das _gz.img) ---"
if python3 "$TOOLS_DIR/mkimage/nl_sign.py" verify --pub "$KEYS_FULL/prod_pub.der" \
--in "$A/migrate/evk_prod.img" >/dev/null 2>&1; then
pass "evk_prod.img traegt eine gueltige Produktionssignatur"
else
fail "evk_prod.img besteht die Signaturpruefung nicht"
fi
if python3 "$TOOLS_DIR/mkimage/nl_sign.py" verify --pub "$KEYS_FULL/dev_pub.der" \
--in "$A/migrate/g95_cw_dev.img" >/dev/null 2>&1; then
pass "g95_cw_dev.img traegt eine gueltige Entwicklungssignatur"
else
fail "g95_cw_dev.img besteht die Signaturpruefung nicht"
fi
# Die Signatur belegt denselben 512-Byte-Block wie der Trailer: signiert werden muss das
# rohe _gz.img. Ein versehentlich signiertes _ota.img waere 512 Byte laenger.
GZ_SIZE=$(python3 -c "import os,sys;print(os.path.getsize(sys.argv[1]))" "$A/flash/ninlight_evk_gz.img")
MIG_SIZE=$(python3 -c "import os,sys;print(os.path.getsize(sys.argv[1]))" "$A/migrate/evk_prod.img")
if [ "$MIG_SIZE" -eq $((GZ_SIZE + 512)) ]; then
pass "Migrationsimage = _gz.img + 512 Byte Signatur (nicht das _ota.img signiert)"
else
fail "Migrationsimage hat $MIG_SIZE Byte, erwartet $((GZ_SIZE + 512))"
fi
echo "--- Fall 4: SHA256SUMS deckt jede Datei ab und stimmt ---"
LISTED=$(awk '{print $2}' "$A/SHA256SUMS" | sed 's|^\*||' | sort)
PRESENT=$( (cd "$A" && find . -type f ! -name SHA256SUMS | sed 's|^\./||' | sort) )
if [ "$LISTED" = "$PRESENT" ]; then
pass "SHA256SUMS listet genau die Dateien des Archivs ($(echo "$PRESENT" | wc -l | tr -d ' '))"
else
fail "SHA256SUMS und Archivinhalt weichen ab:
$(diff <(echo "$LISTED") <(echo "$PRESENT") || true)"
fi
if (cd "$A" && "${SHA_CHECK[@]}" SHA256SUMS >/dev/null 2>&1); then
pass "Pruefsummen stimmen"
else
fail "Pruefsummenpruefung fehlgeschlagen"
fi
echo "--- Fall 5: keine Zugangsdaten im Archiv (roh UND im entpackten Rumpf gesucht) ---"
HITS=$(grep_secrets "$A")
if [ -z "$HITS" ]; then
pass "weder SSID noch Passwort noch Token in irgendeiner Datei des Archivs"
else
fail "Zugangsdaten im Archiv gefunden (oder ein Abbild liess sich nicht entpacken):
$HITS"
fi
# Auch im gepackten Archiv selbst (tar speichert Inhalte ungefiltert, gzip ist keine
# Verschleierung - aber eine unkomprimierbare Kopie wuerde hier auffallen).
if python3 -c "
import gzip,sys
data = gzip.open(sys.argv[1],'rb').read()
sys.exit(0 if not any(s.encode() in data for s in sys.argv[2:]) else 1)" \
"$TGZ" "$SECRET_SSID" "$SECRET_PASS" "$SECRET_TOKEN"; then
pass "auch im entpackten tar-Strom keine Zugangsdaten"
else
fail "Zugangsdaten im tar-Strom des Archivs"
fi
# Der Stub verhaelt sich nur dann wie ein Compiler, wenn er die Werte ueberhaupt
# einbauen KANN - sonst waere Fall 5 wertlos. Gegenprobe: mit vorhandener
# config.local.cmake landen sie im Abzug.
mkdir -p "$TMPROOT/probe"
env NINLIGHT_STUB_ROOT="$SANDBOX" "$SANDBOX/stub-docker" cmake --build "$TMPROOT/probe/evk" >/dev/null
if grep_secrets "$TMPROOT/probe" | grep -q SSID; then
pass "Gegenprobe: der Stub baut die Zugangsdaten ein, solange config.local.cmake da ist"
else
fail "Gegenprobe fehlgeschlagen - der Stub baut nie Zugangsdaten ein, Fall 5 waere wertlos"
fi
# Und zwar realistisch: IM komprimierten Rumpf, nicht als Klartext in der Datei. Faende
# die rohe Suche sie, prueften alle Faelle oben eine Welt, die es nicht gibt - an echten
# Abbildern findet eine rohe Suche nie etwas.
if grep_secrets "$TMPROOT/probe" roh | grep -q .; then
fail "Gegenprobe: der Stub legt die Zugangsdaten im Klartext ab - unrealistisches Abbild"
else
pass "Gegenprobe: im Rohbyteinhalt des Stub-Abbilds steht kein Klartext (nur im gzip-Rumpf)"
fi
# Und das Abbild hat wirklich den Aufbau des echten: 56 Byte Kopf, gzip, 512 Byte
# Trailer mit NINL am Anfang.
if python3 - "$TMPROOT/probe/evk" <<'PY'
import os
import sys
import zlib
d = sys.argv[1]
ota = open(os.path.join(d, "ninlight_evk_ota.img"), "rb").read()
gz = open(os.path.join(d, "ninlight_evk_gz.img"), "rb").read()
assert ota[-512:][:4] == b"NINL", "kein NINL am Trailer-Anfang"
assert ota[:-512] == gz, "_ota.img ist nicht _gz.img + 512 Byte Trailer"
assert gz[56:58] == b"\x1f\x8b", "kein gzip-Strom ab Byte 56"
for raw in (ota, gz):
dec = zlib.decompressobj(zlib.MAX_WBITS | 16)
dec.decompress(raw[56:])
assert dec.eof, "gzip-Strom unvollstaendig"
PY
then
pass "Gegenprobe: Stub-Abbild hat den echten Aufbau (56 Byte Kopf, gzip, NINL-Trailer)"
else
fail "Gegenprobe: Stub-Abbild hat nicht den Aufbau eines echten Abbilds"
fi
echo "--- Meldung der Zugangsdatenpruefung nennt Dateien und entpackte Abbilder ---"
# 8 ota + 8 flash + 16 migrate + bruno/ninlight.yml + README.md + onboard.sh = 35,
# davon 32 Abbilder. SHA256SUMS entsteht erst nach der Pruefung.
if echo "$OUT" | grep -qF "sauber (35 Dateien geprueft, davon 32 Abbilder entpackt)"; then
pass "Meldung nennt Dateizahl und Zahl der entpackten Abbilder"
else
fail "Meldung der Zugangsdatenpruefung unerwartet: $(echo "$OUT" | grep -i sauber || echo '(keine sauber-Zeile)')"
fi
echo "--- Fall 6a: config.local.cmake nach dem Erfolgslauf unveraendert ---"
if [ -f "$SANDBOX/config.local.cmake" ] && [ "$(sha_of "$SANDBOX/config.local.cmake")" = "$CONF_BEFORE" ]; then
pass "config.local.cmake liegt unveraendert an seinem Platz"
else
fail "config.local.cmake fehlt oder wurde veraendert"
fi
echo "--- README nennt die Vertrauensgrenze und die Verzeichnisse ---"
for phrase in "kein kryptografisches Signaturverfahren" "ohne TLS" "onboard.sh" "migrate/" "flash/"; do
if grep -qF "$phrase" "$A/README.md"; then pass "README nennt '$phrase'"; else fail "README nennt '$phrase' nicht"; fi
done
echo "=== Szenario 2: Bau bricht mittendrin ab (Fall 6) ==="
SANDBOX="$TMPROOT/s2"
setup_sandbox "$SANDBOX"
setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX"
CONF_BEFORE=$(sha_of "$SANDBOX/config.local.cmake")
set +e
OUT=$(STUB_FAIL_TARGET=e27_cw TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi
if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein halbes Archiv zurueckgelassen"; else fail "Archiv trotz Abbruch entstanden"; fi
if [ -f "$SANDBOX/config.local.cmake" ] && [ "$(sha_of "$SANDBOX/config.local.cmake")" = "$CONF_BEFORE" ]; then
pass "config.local.cmake nach dem Abbruch unveraendert an seinem Platz"
else
fail "config.local.cmake nach dem Abbruch verschwunden oder veraendert"
fi
echo "=== Szenario 3: Abbruch per Signal (SIGTERM mitten im Bau) ==="
SANDBOX="$TMPROOT/s3"
setup_sandbox "$SANDBOX"
setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX"
CONF_BEFORE=$(sha_of "$SANDBOX/config.local.cmake")
set +e
OUT=$(STUB_TERM_TARGET=gu10_rgb TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Exit-Code war 0"; fi
if [ -f "$SANDBOX/config.local.cmake" ] && [ "$(sha_of "$SANDBOX/config.local.cmake")" = "$CONF_BEFORE" ]; then
pass "config.local.cmake nach dem Signalabbruch unveraendert an seinem Platz"
else
fail "config.local.cmake nach dem Signalabbruch verschwunden oder veraendert"
fi
if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein Archiv nach dem Signalabbruch"; else fail "Archiv trotz Signalabbruch entstanden"; fi
echo "=== Szenario 4: ohne Schluesselverzeichnis (Fall 3) ==="
SANDBOX="$TMPROOT/s4"
setup_sandbox "$SANDBOX"
setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX"
set +e
OUT=$(run_release "$SANDBOX" 0.9.9 2>&1) # kein TEST_SIGN_DIR -> Verzeichnis gibt es nicht
RC=$?
set -e
if [ "$RC" -eq 0 ]; then pass "Exit-Code 0 - das Archiv entsteht trotzdem"; else fail "Exit-Code $RC: $OUT"; fi
if echo "$OUT" | grep -q "Schluesselverzeichnis"; then pass "deutliche Meldung zum fehlenden Schluesselverzeichnis"; else fail "keine Meldung zum fehlenden Schluesselverzeichnis"; fi
EXTRACT="$TMPROOT/x4"; mkdir -p "$EXTRACT"
tar xzf "$SANDBOX/dist/ninlight-0.9.9.tar.gz" -C "$EXTRACT"
A4="$EXTRACT/ninlight-0.9.9"
if [ ! -d "$A4/migrate" ]; then pass "migrate/ liegt nicht vor"; else fail "migrate/ trotzdem vorhanden"; fi
if [ -d "$A4/ota" ] && [ "$(find "$A4/ota" -type f | wc -l | tr -d ' ')" = "${#TARGETS[@]}" ]; then
pass "ota/ trotzdem vollstaendig"
else
fail "ota/ unvollstaendig"
fi
if grep -q "migrate/\` fehlt in diesem Archiv" "$A4/README.md"; then
pass "README.md sagt, warum migrate/ fehlt"
else
fail "README.md erklaert das fehlende migrate/ nicht"
fi
if (cd "$A4" && "${SHA_CHECK[@]}" SHA256SUMS >/dev/null 2>&1); then
pass "SHA256SUMS stimmt auch ohne migrate/"
else
fail "SHA256SUMS stimmt ohne migrate/ nicht"
fi
echo "=== Szenario 5: zweiter Lauf mit derselben Version bricht ab (Fall 7) ==="
BEFORE=$(sha_of "$SANDBOX/dist/ninlight-0.9.9.tar.gz")
set +e
OUT=$(run_release "$SANDBOX" 0.9.9 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "zweiter Lauf war erfolgreich"; fi
if echo "$OUT" | grep -q "existiert bereits"; then pass "Meldung 'existiert bereits'"; else fail "keine Meldung zum vorhandenen Archiv: $OUT"; fi
if [ "$(sha_of "$SANDBOX/dist/ninlight-0.9.9.tar.gz")" = "$BEFORE" ]; then
pass "vorhandenes Archiv unveraendert"
else
fail "vorhandenes Archiv wurde ueberschrieben"
fi
# Und eine andere Version laeuft weiterhin durch.
set +e
OUT=$(run_release "$SANDBOX" 0.9.10 2>&1)
RC=$?
set -e
if [ "$RC" -eq 0 ] && [ -f "$SANDBOX/dist/ninlight-0.9.10.tar.gz" ]; then
pass "andere Version laeuft normal durch"
else
fail "andere Version schlaegt fehl: $OUT"
fi
echo "=== Szenario 6: doppelter Boden - Zugangsdaten im KOMPRIMIERTEN Rumpf brechen den Lauf ab ==="
# Das ist der Kern: der Stub legt die SSID im gzip-Rumpf jedes Abbilds ab, wie es der
# echte Compiler taete. Ein Pruefkoerper, der nur die Rohbytes durchsucht, laesst diesen
# Lauf anstandslos durch und packt Michaels WLAN-Daten ins Archiv.
SANDBOX="$TMPROOT/s6"
setup_sandbox "$SANDBOX"
setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX"
set +e
OUT=$(STUB_LEAK=1 TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Lauf war trotz Zugangsdaten im Abzug erfolgreich"; fi
if echo "$OUT" | grep -q "Zugangsdaten aus config.local.cmake im Archiv gefunden"; then
pass "Meldung benennt den Fund"
else
fail "keine Meldung zum Fund: $OUT"
fi
if echo "$OUT" | grep -qF "$SECRET_SSID"; then
fail "die Meldung druckt den geheimen Wert selbst"
else
pass "die Meldung druckt den Wert nicht, nur den Namen"
fi
if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein Archiv gepackt"; else fail "Archiv trotz Fund gepackt"; fi
if echo "$OUT" | grep -q "NINLIGHT_WIFI_SSID in ota/ninlight_evk_ota.img"; then
pass "Meldung nennt den Namen des Wertes und die Datei"
else
fail "Meldung nennt Name und Datei nicht: $OUT"
fi
echo "=== Szenario 6b: Klartext in einer Nicht-Abbild-Datei bricht den Lauf ab ==="
# Nicht jede Datei im Archiv ist ein Abbild: README, onboard.sh und die Bruno-Collection
# sind Text. Fuer sie bleibt die rohe Bytesuche die einzige Pruefung - sie darf durch das
# Entpacken nicht verlorengehen.
SANDBOX="$TMPROOT/s6b"
setup_sandbox "$SANDBOX"
setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX"
printf '\n# versehentlich notiert: %s\n' "$SECRET_PASS" >>"$SANDBOX/bruno/ninlight.yml"
set +e
OUT=$(TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Lauf war trotz Klartext in bruno/ninlight.yml erfolgreich"; fi
if echo "$OUT" | grep -q "NINLIGHT_WIFI_PASSWORD in bruno/ninlight.yml"; then
pass "Meldung nennt NINLIGHT_WIFI_PASSWORD und die Datei"
else
fail "Meldung nennt den Fund in bruno/ninlight.yml nicht: $OUT"
fi
if echo "$OUT" | grep -qF "$SECRET_PASS"; then
fail "die Meldung druckt das Passwort selbst"
else
pass "die Meldung druckt das Passwort nicht, nur den Namen"
fi
if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein Archiv gepackt"; else fail "Archiv trotz Fund gepackt"; fi
echo "=== Szenario 6c: ein nicht entpackbares Abbild bricht den Lauf ab ==="
# Ein abgeschnittener gzip-Strom ist der heimtueckische Fall: decompressobj liefert
# stillschweigend nur den lesbaren Teil. Was dahinter steht, bliebe ungeprueft - und
# genau dort koennte das Passwort liegen. Ungeprueft durchlassen ist keine Option.
SANDBOX="$TMPROOT/s6c"
setup_sandbox "$SANDBOX"
setup_stub_docker "$SANDBOX/stub-docker" "$SANDBOX"
set +e
OUT=$(STUB_TRUNC_TARGET=e14_cw TEST_SIGN_DIR="$KEYS_FULL" run_release "$SANDBOX" 0.9.9 2>&1)
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "Exit-Code != 0 ($RC)"; else fail "Lauf mit unentpackbarem Abbild war erfolgreich"; fi
if echo "$OUT" | grep -q "ota/ninlight_e14_cw_ota.img"; then
pass "Meldung nennt die betroffene Datei"
else
fail "Meldung nennt die betroffene Datei nicht: $OUT"
fi
if echo "$OUT" | grep -q "laesst sich nicht entpacken"; then
pass "Meldung sagt, dass das Abbild nicht entpackbar ist"
else
fail "Meldung erklaert den Grund nicht: $OUT"
fi
if [ ! -e "$SANDBOX/dist/ninlight-0.9.9.tar.gz" ]; then pass "kein Archiv gepackt"; else fail "Archiv trotz unentpackbarem Abbild gepackt"; fi
echo "=== Szenario 7: onboard.sh spricht die richtige Schnittstelle an ==="
ONB="$TMPROOT/x1/ninlight-0.9.9/onboard.sh"
STUBBIN="$TMPROOT/onb-bin"
mkdir -p "$STUBBIN"
CURLLOG="$TMPROOT/curl.log"
cat >"$STUBBIN/curl" <<'CURL'
#!/usr/bin/env bash
{ for a in "$@"; do printf '%s\n' "$a"; done; echo "--"; } >>"$CURL_LOG"
echo '{}'
CURL
chmod +x "$STUBBIN/curl"
: >"$CURLLOG"
CURL_LOG="$CURLLOG" PATH="$STUBBIN:$PATH" NINLIGHT_ONBOARD_WAIT=0 "$ONB" "MeinWLAN" "geheim123" >/dev/null
if grep -q "^http://192.168.4.1:3333/api/v1/system/settings$" "$CURLLOG"; then
pass "PUT geht an system/settings im Onboarding-AP"
else
fail "falsche URL: $(cat "$CURLLOG")"
fi
if grep -q '^PUT$' "$CURLLOG"; then pass "Methode PUT"; else fail "keine PUT-Methode"; fi
if grep -qF '{"type": "wifi", "ssid": "MeinWLAN", "password": "geheim123"}' "$CURLLOG"; then
pass "Rumpf entspricht der Bruno-Anfrage 'Set WiFi'"
else
fail "falscher Rumpf: $(cat "$CURLLOG")"
fi
if grep -q "^http://192.168.4.1:3333/api/v1/system/info$" "$CURLLOG"; then
pass "danach wird system/info abgefragt"
else
fail "system/info wird nicht abgefragt"
fi
if grep -q "^Authorization:" "$CURLLOG"; then
fail "ohne --token wird trotzdem ein Authorization-Header geschickt"
else
pass "ohne --token kein Authorization-Header (offener Onboarding-AP)"
fi
: >"$CURLLOG"
CURL_LOG="$CURLLOG" PATH="$STUBBIN:$PATH" NINLIGHT_ONBOARD_WAIT=0 "$ONB" --token abc12345 "MeinWLAN" "geheim123" >/dev/null
if grep -q "^Authorization: abc12345$" "$CURLLOG"; then
pass "mit --token wird der Header geschickt (Fallback-AP)"
else
fail "--token kommt nicht als Authorization-Header an"
fi
HELP=$("$ONB" --help)
if echo "$HELP" | grep -qi "von hand" && echo "$HELP" | grep -q "ninlight-<mac4>"; then
pass "Hilfetext nennt die manuellen Schritte und den AP-Namen"
else
fail "Hilfetext erklaert die manuellen Schritte nicht: $HELP"
fi
set +e
"$ONB" >/dev/null 2>&1
RC=$?
set -e
if [ "$RC" -ne 0 ]; then pass "ohne SSID bricht onboard.sh ab"; else fail "ohne SSID lief onboard.sh durch"; fi
echo
if [ "$FAILED" -eq 0 ]; then
echo "alle Zusagen erfuellt"
else
echo "es gab Fehler" >&2
fi
exit "$FAILED"