#!/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" <"$stub" <<'STUB' #!/usr/bin/env bash # Erwartet "cmake -S . -B / ..." bzw. "cmake --build /". 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-"; 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"