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.
659 lines
28 KiB
Bash
Executable File
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)
|
|
|
|
# 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" = "8" ] && pass "ota/ enthaelt 8 Dateien" || fail "ota/ enthaelt $N_OTA statt 8"
|
|
[ "$N_FLASH" = "8" ] && pass "flash/ enthaelt 8 Dateien" || fail "flash/ enthaelt $N_FLASH statt 8"
|
|
[ "$N_MIG" = "16" ] && pass "migrate/ enthaelt 16 Dateien" || fail "migrate/ enthaelt $N_MIG statt 16"
|
|
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 ' ')" = "8" ]; 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"
|