From 5cbf425e4999cb1f763786c7fd084263fada8f9b Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Thu, 24 Sep 2026 00:25:49 +0200 Subject: [PATCH] fix(release): Zugangsdatenpruefung entpackt die Abbilder, Teststub baut echte Die Pruefung vor dem Packen suchte die Werte aus config.local.cmake in den Rohbytes jeder Archivdatei. Ein Abbild ist aber 56 Byte Kopf, dann ein gzip-Strom, dann - je nach Bildart - 512 Byte Trailer bzw. Signatur; im komprimierten Strom steht kein Klartext. Derselbe Pruefkoerper meldete deshalb auch fuer einen Bildsatz aus build/, der die Zugangsdaten nachweislich traegt, "sauber (2 geprueft)". Er haette nie Alarm geschlagen. Jetzt wird jede Datei weiterhin roh geprueft (Kopf, Trailer und die Nicht-Abbilder wie README.md, onboard.sh und bruno/ sind unkomprimiert) und jedes Abbild zusaetzlich entpackt. Ein Abbild, das sich nicht entpacken laesst, bricht den Release ab, statt ungeprueft durchzugehen; das gilt auch fuer einen abgeschnittenen gzip-Strom, den decompressobj sonst stillschweigend annimmt. Entpackt wird ab Byte 56 ohne festes Abschneiden des Dateiendes. Anders als in tools/station/deploy.sh liegen hier drei Bildarten nebeneinander: flash/_gz.img traegt gar keinen Trailer, ota/_ota.img einen 512-Byte-Trailer, migrate/*.img eine 512-Byte-Signatur. Ein raw[56:-512] wie dort haette jedes _gz.img fuer unentpackbar gehalten und jeden Release abgebrochen. Der Teststub war der zweite Fehler und verdeckte den ersten: er legte die Zugangsdaten als Klartext in eine Datei, die er nur "..._ota.img" nannte. Damit fand die rohe Suche sie, und der Fall war gruen, obwohl er eine Welt prueft, die es nicht gibt. Der Stub baut jetzt Abbilder wie der echte Bau - 56 Byte Kopf, gzip des Rumpfes mit den Zugangsdaten INNERHALB des komprimierten Rumpfes, beim _ota.img 512 Byte Trailer mit NINL - und kontrolliert selbst, dass der Klartext nicht roh in der Datei steht. Der Rumpf ist gross und gleichfoermig genug, dass deflate ihn wirklich komprimiert statt ihn als "stored block" durchzureichen. Neu sind Szenarien fuer Klartext in einer Nicht-Abbild-Datei, ein nicht entpackbares Abbild und den Meldungstext; die Gegenprobe prueft jetzt auch den Aufbau des Stub-Abbilds. Szenario 6 (Zugangsdaten im komprimierten Rumpf) und 6c sind gegen den alten Pruefkoerper rot. Die Meldung nennt jetzt die Zahl der Dateien und der entpackten Abbilder statt der Zahl der Suchbegriffe. Gefundene Werte werden weiterhin nie ausgegeben, nur ihr Name und die Datei. --- tools/release.sh | 70 +++++++++++-- tools/tests/test_release.sh | 192 +++++++++++++++++++++++++++++++++--- 2 files changed, 241 insertions(+), 21 deletions(-) diff --git a/tools/release.sh b/tools/release.sh index 1e958da..4c1c2f0 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -254,15 +254,37 @@ README echo "== Auf Zugangsdaten pruefen ==" # Doppelter Boden zum Beiseitelegen von config.local.cmake: bevor irgendetwas gepackt -# wird, wird JEDE Datei des Archivs binaer gegen die echten Werte aus -# config.local.cmake geprueft. Lieber ein abgebrochener Release als ein Archiv mit -# Michaels WLAN-Passwort bei Dritten. +# wird, wird JEDE Datei des Archivs gegen die echten Werte aus config.local.cmake +# geprueft. Lieber ein abgebrochener Release als ein Archiv mit Michaels WLAN-Passwort +# bei Dritten - dieses Archiv geht an Fremde, und wiederholen laesst sich das nicht. +# +# Gesucht wird in ZWEI Sichten: +# roh - Kopf, Trailer und die Nicht-Abbilder (README.md, onboard.sh, bruno/) sind +# unkomprimiert; eine versehentlich notierte Zeile faellt nur so auf. +# entpackt- jedes Abbild ist 56 Byte Kopf, dann ein gzip-Strom, dann (beim _ota.img +# bzw. den signierten migrate/-Bildern) 512 Byte Trailer bzw. Signatur. Im +# komprimierten Strom steht kein Klartext. Die rohe Suche allein meldete +# deshalb IMMER "sauber" - auch fuer Abzuege aus build/, die die +# Zugangsdaten nachweislich tragen. Sie hat nie etwas geprueft. +# +# Entpackt wird ab Byte 56 OHNE das Dateiende abzuschneiden: die drei Bildarten im +# Archiv haben unterschiedliche Enden (flash/_gz.img traegt gar keinen Trailer, +# ota/_ota.img einen 512-Byte-Trailer, migrate/*.img eine 512-Byte-Signatur). Ein +# festes raw[56:-512] wie in tools/station/deploy.sh - wo nur Abbilder MIT Trailer +# vorkommen - wuerde hier jedes _gz.img fuer unentpackbar halten und jeden Release +# abbrechen. decompressobj hoert am Ende des gzip-Stroms von selbst auf; was danach +# kommt, ist ohnehin schon roh geprueft. +# +# Ein Abbild, das sich nicht entpacken laesst, ist ein FEHLER und bricht ab - nicht +# "dann eben ungeprueft durchlassen". Das gilt auch fuer einen abgeschnittenen +# gzip-Strom, den decompressobj sonst stillschweigend annimmt. LOCAL="$ROOT/config.local.cmake" if [ -f "$LOCAL" ]; then python3 - "$OUT" "$LOCAL" <<'PY' || exit 1 import os import re import sys +import zlib stage, local = sys.argv[1], sys.argv[2] text = open(local, encoding="utf-8", errors="replace").read() @@ -271,27 +293,63 @@ text = open(local, encoding="utf-8", errors="replace").read() # haette nur falsche Alarme erzeugt. SECRETS = ("NINLIGHT_WIFI_SSID", "NINLIGHT_WIFI_PASSWORD", "NINLIGHT_TOKEN") PUBLIC = {"", "ninlight", "aabbccddeeff0011223344", "ninlight-recovery", "change-me-please"} +# Kopflaenge des vom wm_tool erzeugten Abbilds; danach beginnt der gzip-Strom. +HEADER_LEN = 56 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: + raise ValueError("Datei ist kuerzer als der Kopf") + dec = zlib.decompressobj(zlib.MAX_WBITS | 16) + body = dec.decompress(raw[HEADER_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 = [] +broken = [] +checked = 0 +images = 0 for root, _dirs, files in os.walk(stage): for fn in sorted(files): path = os.path.join(root, fn) + rel = os.path.relpath(path, stage) with open(path, "rb") as fh: data = fh.read() + checked += 1 + views = [data] + if fn.endswith(".img"): + try: + views.append(unpack(data)) + except Exception as exc: + broken.append("%s (%s)" % (rel, exc)) + continue + images += 1 for name, needle in needles: - if needle in data: - hits.append("%s in %s" % (name, os.path.relpath(path, stage))) + 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 laesst sich nicht entpacken, also auch nicht auf Zugangsdaten " + "pruefen - nichts gepackt:\n " + "\n ".join(broken) + "\n") + sys.exit(1) if hits: sys.stderr.write( "Fehler: Zugangsdaten aus config.local.cmake im Archiv gefunden, nichts gepackt:\n " + "\n ".join(hits) + "\n") sys.exit(1) -print(" sauber (%d geprueft)" % len(needles)) +print(" sauber (%d Dateien geprueft, davon %d Abbilder entpackt)" % (checked, images)) PY fi diff --git a/tools/tests/test_release.sh b/tools/tests/test_release.sh index fe1ebf5..ed9dfcc 100755 --- a/tools/tests/test_release.sh +++ b/tools/tests/test_release.sh @@ -11,6 +11,13 @@ # 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 @@ -75,6 +82,9 @@ EOF # (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' @@ -115,22 +125,56 @@ fi if [ "${STUB_LEAK:-}" = "1" ]; then creds="$STUB_LEAK_VALUE" fi -CREDS="$creds" TARGET="$target" OUTDIR="$outdir" python3 - <<'PY' +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"] -body = bytearray(("firmware-%s-" % target).encode() * 200) -body = body[:4096] + b"\0" * (4096 - len(body[:4096])) + +# 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 -gz = bytes(body) + +# 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(gz) # roh, ohne NINL-Trailer - das signiert nl_sign.py + 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(gz + bytes(trailer)) + fh.write(ota_body + bytes(trailer)) PY STUB chmod +x "$stub" @@ -171,29 +215,50 @@ run_release() { 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 BINAER in jeder Datei unterhalb eines Verzeichnisses. +# 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 - SSID="$SECRET_SSID" PASS="$SECRET_PASS" TOKEN="$SECRET_TOKEN" python3 - "$dir" <<'PY' + 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 -root = sys.argv[1] +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 needle in data: - print("%s in %s" % (name, os.path.relpath(p, root))) + if any(needle in view for view in views): + print("%s in %s" % (name, rel)) PY } @@ -283,12 +348,12 @@ else fail "Pruefsummenpruefung fehlgeschlagen" fi -echo "--- Fall 5: keine Zugangsdaten im Archiv (binaer gesucht) ---" +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: + 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 @@ -312,6 +377,46 @@ if grep_secrets "$TMPROOT/probe" | grep -q SSID; then 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 @@ -413,7 +518,10 @@ else fail "andere Version schlaegt fehl: $OUT" fi -echo "=== Szenario 6: doppelter Boden - Zugangsdaten im Abzug brechen den Lauf ab ===" +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" @@ -433,6 +541,60 @@ 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"