From 59c8c150bd4984621cb64932edb5283ccb174ca2 Mon Sep 17 00:00:00 2001 From: Michael Sauer Date: Sun, 13 Sep 2026 19:47:51 +0200 Subject: [PATCH] feat(station): Migrationsimages beim Deploy signieren deploy.sh signiert pro Ziel das rohe _gz.img (nie das _ota.img) mit den Produktions- und Entwicklungsschluesseln aus $NINLIGHT_SIGN_DIR und legt die entstandenen _prod.img/_dev.img samt manifest.json unter migrate/ ab. Fehlt das Schluesselverzeichnis oder einzelne Schluessel, entfaellt nur die jeweilige Migrationsvariante mit einer Meldung auf stderr - die Rettungsimages werden unveraendert weiter ausgeliefert. migrate/ wird beim rsync-Abgleich auf dem Pi bewusst getrennt von den _ota.img behandelt: fehlt es lokal (kein Schluesselverzeichnis auf dem Entwicklungsrechner), bleibt ein vorhandenes migrate/ auf dem Pi unangetastet statt durch --delete geleert zu werden - derselbe Fehler, der frueher schon einmal fremde Geraetetypen weggeraeumt hat, diesmal auf die Migrationsimages angewendet. test_deploy.sh bekommt acht neue Szenarien fuer die sieben im Task-Brief verlangten Faelle plus den zusaetzlichen Schutz vor dem geleerten migrate/. Alle Szenarien erzeugen ihre eigenen Wegwerf-Testschluessel ueber nl_sign.generate_test_key() und setzen NINLIGHT_SIGN_DIR bei jedem Aufruf explizit (auch im "ohne Schluessel"-Fall) - der Default $HOME/workspace/fw-sign zeigt auf diesem Rechner auf echte Schluessel, und kein Testlauf darf sie je lesen. --- tools/station/deploy.sh | 108 ++++++++- tools/station/tests/test_deploy.sh | 337 +++++++++++++++++++++++++++++ 2 files changed, 443 insertions(+), 2 deletions(-) diff --git a/tools/station/deploy.sh b/tools/station/deploy.sh index eb03501..0539531 100755 --- a/tools/station/deploy.sh +++ b/tools/station/deploy.sh @@ -51,6 +51,7 @@ echo "== Images pruefen ==" STAGE=$(mktemp -d) trap 'rm -rf "$STAGE"' EXIT printf '%-26s %-10s %s\n' GERAET VERSION BYTES +declare -A NINL_VERSION=() for f in "${FILES[@]}"; do base=$(basename "$f") target=${base#ninlight_}; target=${target%_ota.img} @@ -61,9 +62,93 @@ for f in "${FILES[@]}"; do # shellcheck disable=SC2086 set -- $out printf '%-26s %-10s %s\n' "$1" "$2" "$3" + NINL_VERSION[$target]="$2" cp "$f" "$STAGE/$base" done +echo "== Migrationsimages signieren ==" +# Die Signierschluessel gehoeren nie ins Repo und nie auf den Pi - sie werden hier nur +# gelesen, unter $NINLIGHT_SIGN_DIR (Vorgabe ~/workspace/fw-sign auf DIESEM Rechner). +# Fehlt das Verzeichnis oder ein einzelner Schluessel, entfaellt die jeweilige +# Migrationsvariante mit einer Meldung - die Rettungsimages (_ota.img) werden trotzdem +# ausgeliefert (Exit-Code bleibt 0). +SIGN_DIR=${NINLIGHT_SIGN_DIR:-$HOME/workspace/fw-sign} +KEY_PROD="$SIGN_DIR/QCX_Device_CA/qcx_firmware_signing_key.pem" +KEY_DEV="$SIGN_DIR/fw_sign.pem" +NL_SIGN="$ROOT/tools/mkimage/nl_sign.py" + +if [ -d "$SIGN_DIR" ]; then + mkdir -p "$STAGE/migrate" + HAVE_PROD=0 + HAVE_DEV=0 + if [ -f "$KEY_PROD" ]; then + HAVE_PROD=1 + else + echo "Hinweis: Produktionsschluessel fehlt - keine _prod.img-Migrationsimages" >&2 + fi + if [ -f "$KEY_DEV" ]; then + HAVE_DEV=1 + else + echo "Hinweis: Entwicklungsschluessel fehlt - keine _dev.img-Migrationsimages" >&2 + fi + for f in "${FILES[@]}"; do + base=$(basename "$f") + target=${base#ninlight_}; target=${target%_ota.img} + # Signiert wird IMMER das rohe _gz.img, niemals das fertige _ota.img: beide belegen + # denselben 512-Byte-Block am Dateiende (Trailer bzw. Signatur), ein signiertes + # _ota.img waere ein Image, das die alte Lampe ablehnt oder halb annimmt. + gz="$ROOT/build/$target/ninlight_${target}_gz.img" + [ -f "$gz" ] || { + echo "Fehler: $gz fehlt - kein Migrationsimage fuer $target moeglich" >&2; exit 1; } + if [ "$HAVE_PROD" -eq 1 ]; then + python3 "$NL_SIGN" sign --key "$KEY_PROD" --in "$gz" --out "$STAGE/migrate/${target}_prod.img" \ + || { echo "Fehler: Signieren von $target (prod) fehlgeschlagen" >&2; exit 1; } + fi + if [ "$HAVE_DEV" -eq 1 ]; then + python3 "$NL_SIGN" sign --key "$KEY_DEV" --in "$gz" --out "$STAGE/migrate/${target}_dev.img" \ + || { echo "Fehler: Signieren von $target (dev) fehlgeschlagen" >&2; exit 1; } + fi + done + VERSION_ARGS=() + for t in "${!NINL_VERSION[@]}"; do + VERSION_ARGS+=("$t=${NINL_VERSION[$t]}") + done + # manifest.json aus den entstandenen Dateien - Groesse und sha256 werden hier neu + # berechnet statt aus dem Signierschritt uebernommen, damit die Datei die Wahrheit + # ueber das tatsaechlich abgelegte Image sagt. + python3 - "$STAGE/migrate" "${VERSION_ARGS[@]}" <<'PY' +import hashlib +import json +import os +import sys + +migrate_dir = sys.argv[1] +version_map = dict(a.split("=", 1) for a in sys.argv[2:]) +entries = [] +for name in sorted(os.listdir(migrate_dir)): + if name.endswith("_prod.img"): + target, variant = name[: -len("_prod.img")], "prod" + elif name.endswith("_dev.img"): + target, variant = name[: -len("_dev.img")], "dev" + else: + continue + with open(os.path.join(migrate_dir, name), "rb") as fh: + data = fh.read() + entries.append({ + "target": target, + "variant": variant, + "bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest(), + "ninlight_version": version_map.get(target, ""), + }) +with open(os.path.join(migrate_dir, "manifest.json"), "w") as fh: + json.dump({"images": entries}, fh, indent=2, sort_keys=True) + fh.write("\n") +PY +else + echo "Hinweis: Schluesselverzeichnis $SIGN_DIR nicht gefunden - keine Migrationsimages erzeugt" >&2 +fi + echo "== uebertragen ==" # -c (Inhaltsvergleich per Checksumme statt Groesse+Zeitstempel): ein neues # Image kann zufaellig dieselbe Byte-Groesse wie das alte haben, und bei @@ -105,12 +190,31 @@ if [ "$FULL_SYNC" -eq 1 ]; then else echo " voller Abgleich - keine Images werden entfernt" fi - sudo rsync -a -c --delete /tmp/ninlight-images/ "$ROOT_DIR/images/" + # migrate/ wird gleich in einem eigenen Schritt behandelt (siehe unten) - hier + # ausklammern, sonst wuerde --delete es leeren, wenn dieser Deploy ohne + # Schluesselverzeichnis lief und /tmp/ninlight-images/migrate deshalb gar nicht + # existiert. + sudo rsync -a -c --delete --exclude=/migrate /tmp/ninlight-images/ "$ROOT_DIR/images/" else # Gezielter Deploy: nur die uebertragenen Dateien ersetzen/ergaenzen, alle # anderen Geraetetypen im Zielverzeichnis bleiben erhalten (kein --delete). echo " gezielter Deploy - andere Geraetetypen im Zielverzeichnis bleiben erhalten" - sudo rsync -a -c /tmp/ninlight-images/ "$ROOT_DIR/images/" + sudo rsync -a -c --exclude=/migrate /tmp/ninlight-images/ "$ROOT_DIR/images/" +fi +# Migrationsimages getrennt behandeln: fehlt migrate/ in der Uebertragung (weil auf +# dem Entwicklungsrechner kein Schluesselverzeichnis vorhanden war), darf ein +# vorhandenes migrate/ auf dem Pi NICHT geleert werden - sonst zerstoert ein Deploy +# ohne Schluesselzugriff die Bestueckung frueherer Deploys (derselbe Fehler wie +# frueher bei den _ota.img, siehe oben, nur diesmal beim Fehlen statt beim Ersetzen). +if [ -d /tmp/ninlight-images/migrate ]; then + sudo install -d -m 0755 "$ROOT_DIR/images/migrate" + if [ "$FULL_SYNC" -eq 1 ]; then + sudo rsync -a -c --delete /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/" + else + sudo rsync -a -c /tmp/ninlight-images/migrate/ "$ROOT_DIR/images/migrate/" + fi +else + echo " kein Schluesselverzeichnis auf dem Entwicklungsrechner - migrate/ auf dem Pi bleibt unangetastet" fi sudo install -m 0755 /tmp/server.py "$ROOT_DIR/server.py" sudo install -m 0644 /tmp/products.py "$ROOT_DIR/products.py" diff --git a/tools/station/tests/test_deploy.sh b/tools/station/tests/test_deploy.sh index 85e3d56..3a9b977 100755 --- a/tools/station/tests/test_deploy.sh +++ b/tools/station/tests/test_deploy.sh @@ -14,6 +14,7 @@ set -euo pipefail HERE=$(cd "$(dirname "$0")" && pwd) STATION_DIR=$(cd "$HERE/.." && pwd) +MKIMAGE_DIR=$(cd "$STATION_DIR/../mkimage" && pwd) REAL_RSYNC=$(command -v rsync) TMPROOT=$(mktemp -d) @@ -30,6 +31,10 @@ setup_sandbox() { mkdir -p "$sandbox/tools/station" "$sandbox/tools/mkimage" "$sandbox/build" cp "$STATION_DIR/deploy.sh" "$sandbox/tools/station/deploy.sh" chmod +x "$sandbox/tools/station/deploy.sh" + # Das ECHTE Signierwerkzeug wird gebraucht (nicht gestubbt): die Migrations-Szenarien + # pruefen echtes Signieren/die echten Formatchecks (Mindestlaenge, kein Trailer, 4096 + # Bit). Nur die Schluessel selbst sind Wegwerf-Testschluessel, nie echte. + cp "$MKIMAGE_DIR/nl_sign.py" "$sandbox/tools/mkimage/nl_sign.py" # Inhalt irrelevant, die Dateien muessen nur existieren (werden "uebertragen"). echo "# dummy" >"$sandbox/tools/station/server.py" echo "# dummy" >"$sandbox/tools/station/products.py" @@ -114,14 +119,75 @@ run_deploy() { # Auf dem echten Pi existieren /usr/local/bin und /etc/systemd/system schon; # in der Sandbox muessen sie fuer "install" eigens angelegt werden. mkdir -p "$SANDBOX_STATE/usr/local/bin" "$SANDBOX_STATE/etc/systemd/system" + # NINLIGHT_SIGN_DIR IMMER explizit setzen: deploy.sh faellt sonst auf + # $HOME/workspace/fw-sign zurueck, und auf Entwicklungsrechnern liegen dort die + # ECHTEN Signierschluessel. Ein Test darf die niemals auch nur lesen. Szenarien, + # die ein Schluesselverzeichnis brauchen, ueberschreiben die Variable gezielt VOR + # dem Aufruf von run_deploy (siehe unten); dieser Default zeigt auf einen Pfad, + # der in der Sandbox garantiert nicht existiert. PATH="$STUBBIN:$PATH" \ NINLIGHT_SSH_BIN="$STUBBIN/ssh" \ NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \ NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \ NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \ + NINLIGHT_SIGN_DIR="${NINLIGHT_SIGN_DIR:-$sandbox/no-such-sign-dir}" \ "$sandbox/tools/station/deploy.sh" "$@" } +# --- Hilfsmittel fuer die Migrations-Szenarien --- + +# Erzeugt Wegwerf-Testschluessel (nl_sign.generate_test_key(), nie echte Schluessel, +# nie geschrieben ausser in dieses Sandbox-Verzeichnis) unter $keydir. with_prod/ +# with_dev steuern, welche der beiden Dateien entsteht. +make_test_keys() { + local keydir=$1 with_prod=$2 with_dev=$3 + mkdir -p "$keydir/QCX_Device_CA" + PYTHONPATH="$MKIMAGE_DIR" 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) +if with_dev: + k = nl_sign.generate_test_key() + with open(keydir + "/fw_sign.pem", "wb") as fh: + fh.write(k.private_pem) +PY +} + +# Erzeugt eine gueltige rohe _gz.img-Attrappe: >= 1024 Byte, durch 4 teilbar, ohne +# NINL-Trailer - genau das, was nl_sign.py als Eingabe akzeptiert. +make_gz_file() { + local path=$1 fill=${2:-A} + python3 -c " +import sys +path, fill = sys.argv[1], sys.argv[2].encode() +data = bytearray((fill * 2048)[:2048]) +data[-512:-508] = b'\\x00\\x00\\x00\\x00' # nie zufaellig wie 'NINL' aussehen +with open(path, 'wb') as fh: + fh.write(bytes(data)) +" "$path" "$fill" +} + +# Erzeugt eine _ota.img-Attrappe mit ANDEREM Inhalt als das zugehoerige _gz.img und +# einem echten NINL-Trailer - damit ein versehentliches Signieren des _ota.img statt +# des _gz.img (Fall 7) zweifelsfrei auffliegt: weder Inhalt noch Groesse passen dann. +make_ota_file() { + local path=$1 + python3 -c " +import sys +path = sys.argv[1] +body = b'B' * 2048 +trailer = bytearray(b'\\xff' * 512) +trailer[0:4] = b'NINL' +with open(path, 'wb') as fh: + fh.write(body + bytes(trailer)) +" "$path" +} + echo "=== Szenario 1: Image besteht die Trailer-Pruefung nicht -> nichts uebertragen ===" SANDBOX="$TMPROOT/s1"; STUBBIN="$TMPROOT/bin1"; LOG="$TMPROOT/s1.log"; SANDBOX_STATE="$TMPROOT/s1-state" setup_sandbox "$SANDBOX" @@ -238,6 +304,7 @@ OUT=$(FAKE_SYSTEMCTL_ACTIVE_EXIT=0 FAKE_SYSTEMCTL_RESTART_EXIT=1 \ NINLIGHT_STATION_ROOT="$SANDBOX_STATE/opt/ninlight-station" \ NINLIGHT_STATION_BIN="$SANDBOX_STATE/usr/local/bin/ninlight-station" \ NINLIGHT_STATION_UNIT="$SANDBOX_STATE/etc/systemd/system/ninlight-station.service" \ + NINLIGHT_SIGN_DIR="$SANDBOX/no-such-sign-dir" \ "$SANDBOX/tools/station/deploy.sh" pi-test foo 2>&1) RC=$? set -e @@ -357,6 +424,276 @@ else fail "Ausgabe meldet nicht, dass nichts entfernt wird: $OUT" fi +echo +echo "=== Vorbereitung: Wegwerf-Testschluessel fuer die Migrations-Szenarien ===" +# Nur ueber nl_sign.generate_test_key() erzeugt, nie ausserhalb dieses Testlaufs +# verwendet und nie in ein Repository- oder Log-File jenseits dieser TMPROOT-Sandbox +# geschrieben. +KEYDIR_FULL="$TMPROOT/keys-full" +make_test_keys "$KEYDIR_FULL" 1 1 +KEYDIR_DEV_ONLY="$TMPROOT/keys-dev-only" +make_test_keys "$KEYDIR_DEV_ONLY" 0 1 +pass "Testschluessel erzeugt" + +echo +echo "=== Szenario 10: Mit vorhandenem Schluesselverzeichnis entstehen zwei Migrationsimages je Ziel (Fall 1) ===" +SANDBOX="$TMPROOT/s10"; STUBBIN="$TMPROOT/bin10"; LOG="$TMPROOT/s10.log"; SANDBOX_STATE="$TMPROOT/s10-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(NINLIGHT_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi +GZ_SIZE=$(wc -c <"$SANDBOX/build/foo/ninlight_foo_gz.img" | tr -d ' ') +PROD_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" +DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" +if [ -f "$PROD_IMG" ] && [ -f "$DEV_IMG" ]; then + pass "beide Migrationsimages (prod, dev) wurden angelegt" +else + fail "mindestens ein Migrationsimage fehlt: $OUT" +fi +for img in "$PROD_IMG" "$DEV_IMG"; do + [ -f "$img" ] || continue + size=$(wc -c <"$img" | tr -d ' ') + expected=$((GZ_SIZE + 512)) + if [ "$size" -eq "$expected" ]; then + pass "$(basename "$img") ist genau 512 Byte groesser als das _gz.img ($size Byte)" + else + fail "$(basename "$img") hat $size Byte, erwartet $expected" + fi +done + +echo +echo "=== Szenario 11: manifest.json listet jedes ausgelieferte Migrationsimage (Fall 2) ===" +SANDBOX="$TMPROOT/s11"; STUBBIN="$TMPROOT/bin11"; LOG="$TMPROOT/s11.log"; SANDBOX_STATE="$TMPROOT/s11-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(NINLIGHT_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +MANIFEST="$SANDBOX_STATE/opt/ninlight-station/images/migrate/manifest.json" +if [ "$RC" -eq 0 ] && [ -f "$MANIFEST" ]; then + pass "manifest.json wurde angelegt" +else + fail "manifest.json fehlt oder Exit-Code $RC: $OUT" +fi +set +e +CHECK=$(python3 -c " +import json +with open('$MANIFEST') as fh: + data = json.load(fh) +entries = data.get('images', []) +by_variant = {e.get('variant'): e for e in entries if e.get('target') == 'foo'} +ok = ( + len(entries) == 2 + and set(by_variant) == {'prod', 'dev'} + and all(isinstance(e.get('bytes'), int) and e['bytes'] > 0 for e in entries) + and all(isinstance(e.get('sha256'), str) and len(e['sha256']) == 64 for e in entries) +) +print('OK' if ok else 'FAIL: ' + json.dumps(entries)) +" 2>&1) +set -e +if [ "$CHECK" = "OK" ]; then + pass "manifest.json listet target/variant/bytes/sha256 fuer beide Images" +else + fail "manifest.json-Inhalt unerwartet: $CHECK" +fi + +echo +echo "=== Szenario 12: Ohne NINLIGHT_SIGN_DIR entfallen die Migrationsimages, das Rettungsimage kommt trotzdem an (Fall 3) ===" +SANDBOX="$TMPROOT/s12"; STUBBIN="$TMPROOT/bin12"; LOG="$TMPROOT/s12.log"; SANDBOX_STATE="$TMPROOT/s12-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi +if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/ninlight_foo_ota.img" ]; then + pass "Rettungsimage (_ota.img) wurde trotzdem ausgeliefert" +else + fail "Rettungsimage fehlt: $OUT" +fi +if [ ! -d "$SANDBOX_STATE/opt/ninlight-station/images/migrate" ] \ + || [ -z "$(ls -A "$SANDBOX_STATE/opt/ninlight-station/images/migrate" 2>/dev/null)" ]; then + pass "keine Migrationsimages entstanden" +else + fail "Migrationsimages entstanden, obwohl kein Schluesselverzeichnis vorhanden war" +fi +if echo "$OUT" | grep -q "Schluesselverzeichnis"; then + pass "Meldung ueber fehlendes Schluesselverzeichnis vorhanden" +else + fail "keine Meldung ueber das fehlende Schluesselverzeichnis: $OUT" +fi + +echo +echo "=== Szenario 13: Schluesselverzeichnis ohne Produktionsschluessel - nur dev-Images entstehen (Fall 4) ===" +SANDBOX="$TMPROOT/s13"; STUBBIN="$TMPROOT/bin13"; LOG="$TMPROOT/s13.log"; SANDBOX_STATE="$TMPROOT/s13-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(NINLIGHT_SIGN_DIR="$KEYDIR_DEV_ONLY" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi +if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" ]; then + pass "dev-Migrationsimage wurde angelegt" +else + fail "dev-Migrationsimage fehlt: $OUT" +fi +if [ ! -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" ]; then + pass "prod-Migrationsimage entfiel wie erwartet" +else + fail "prod-Migrationsimage wurde trotz fehlendem Produktionsschluessel angelegt" +fi +if echo "$OUT" | grep -q "Produktionsschluessel fehlt"; then + pass "Meldung ueber fehlenden Produktionsschluessel vorhanden" +else + fail "keine Meldung ueber den fehlenden Produktionsschluessel: $OUT" +fi + +echo +echo "=== Szenario 14: gezielter Deploy erzeugt nur die Migrationsimages des angegebenen Ziels (Fall 5, 4a-Fehler fuer migrate/) ===" +SANDBOX="$TMPROOT/s14"; STUBBIN="$TMPROOT/bin14"; LOG="$TMPROOT/s14.log"; SANDBOX_STATE="$TMPROOT/s14-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" "$SANDBOX/build/bar" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" A +make_gz_file "$SANDBOX/build/bar/ninlight_bar_gz.img" C +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +echo "bar-ota" >"$SANDBOX/build/bar/ninlight_bar_ota.img" +mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate" +echo "vorhandenes-bar-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_prod.img" +echo "vorhandenes-bar-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_dev.img" +: >"$LOG" +set +e +OUT=$(NINLIGHT_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi +if [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" ] \ + && [ -f "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" ]; then + pass "Migrationsimages fuer das angegebene Ziel (foo) wurden angelegt" +else + fail "Migrationsimages fuer foo fehlen: $OUT" +fi +if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_prod.img" 2>/dev/null)" = "vorhandenes-bar-prod" ] \ + && [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/bar_dev.img" 2>/dev/null)" = "vorhandenes-bar-dev" ]; then + pass "Migrationsimages des NICHT ausgewaehlten Geraetetyps (bar) blieben unangetastet" +else + fail "Migrationsimages von bar wurden veraendert oder geloescht - 4a-Fehler fuer migrate/" +fi + +echo +echo "=== Szenario 15: Kein privater Schluessel und kein Schluesselpfad-Inhalt in der Ausgabe (Fall 6) ===" +SANDBOX="$TMPROOT/s15"; STUBBIN="$TMPROOT/bin15"; LOG="$TMPROOT/s15.log"; SANDBOX_STATE="$TMPROOT/s15-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(NINLIGHT_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi +if echo "$OUT" | grep -q -- "-----BEGIN"; then + fail "PEM-Header (Schluesselinhalt) taucht in der Ausgabe auf" +else + pass "kein PEM-Header in der Ausgabe" +fi +# Eine Zeile aus der Mitte des privaten Produktionsschluessels darf unter keinen +# Umstaenden in der Ausgabe erscheinen. +KEY_LINE=$(sed -n '3p' "$KEYDIR_FULL/QCX_Device_CA/qcx_firmware_signing_key.pem") +if [ -n "$KEY_LINE" ] && echo "$OUT" | grep -qF "$KEY_LINE"; then + fail "eine Zeile des privaten Produktionsschluessels taucht in der Ausgabe auf" +else + pass "keine Schluesselzeile in der Ausgabe" +fi + +echo +echo "=== Szenario 16: Signiert wird das _gz.img, niemals das _ota.img (Fall 7, gefaehrlichster Fehler) ===" +SANDBOX="$TMPROOT/s16"; STUBBIN="$TMPROOT/bin16"; LOG="$TMPROOT/s16.log"; SANDBOX_STATE="$TMPROOT/s16-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" +make_gz_file "$SANDBOX/build/foo/ninlight_foo_gz.img" A +make_ota_file "$SANDBOX/build/foo/ninlight_foo_ota.img" +: >"$LOG" +set +e +OUT=$(NINLIGHT_SIGN_DIR="$KEYDIR_FULL" run_deploy "$SANDBOX" pi-test foo 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi +DEV_IMG="$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" +if [ -f "$DEV_IMG" ]; then + set +e + python3 -c " +import sys +with open(sys.argv[1], 'rb') as fh: + signed = fh.read() +with open(sys.argv[2], 'rb') as fh: + gz = fh.read() +sys.exit(0 if signed[:-512] == gz else 1) +" "$DEV_IMG" "$SANDBOX/build/foo/ninlight_foo_gz.img" + PAYLOAD_MATCH=$? + set -e + if [ "$PAYLOAD_MATCH" -eq 0 ]; then + pass "die signierte Nutzlast ist byteidentisch mit dem _gz.img" + else + fail "die signierte Nutzlast entspricht NICHT dem _gz.img - das _ota.img wurde signiert!" + fi +else + fail "dev-Migrationsimage wurde nicht angelegt: $OUT" +fi + +echo +echo "=== Szenario 17: Ohne Schluesselverzeichnis bleibt ein vorhandenes migrate/ beim Voll-Deploy unangetastet ===" +SANDBOX="$TMPROOT/s17"; STUBBIN="$TMPROOT/bin17"; LOG="$TMPROOT/s17.log"; SANDBOX_STATE="$TMPROOT/s17-state" +setup_sandbox "$SANDBOX" +setup_stubbin "$STUBBIN" "$LOG" +mkdir -p "$SANDBOX/build/foo" +echo "foo-ota" >"$SANDBOX/build/foo/ninlight_foo_ota.img" +mkdir -p "$SANDBOX_STATE/opt/ninlight-station/images/migrate" +echo "vorhandenes-foo-prod" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" +echo "vorhandenes-foo-dev" >"$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" +: >"$LOG" +set +e +OUT=$(run_deploy "$SANDBOX" pi-test 2>&1) +RC=$? +set -e +if [ "$RC" -eq 0 ]; then pass "Exit-Code 0"; else fail "Exit-Code $RC, Ausgabe: $OUT"; fi +if [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_prod.img" 2>/dev/null)" = "vorhandenes-foo-prod" ] \ + && [ "$(cat "$SANDBOX_STATE/opt/ninlight-station/images/migrate/foo_dev.img" 2>/dev/null)" = "vorhandenes-foo-dev" ]; then + pass "vorhandenes migrate/ blieb beim Voll-Deploy ohne Schluesselverzeichnis unangetastet" +else + fail "migrate/ wurde geleert, obwohl kein Schluesselverzeichnis vorhanden war - zerstoert die Bestueckung" +fi +if echo "$OUT" | grep -q "migrate/ auf dem Pi bleibt unangetastet"; then + pass "Ausgabe kuendigt an, dass migrate/ unangetastet bleibt" +else + fail "Ausgabe erwaehnt nicht, dass migrate/ unangetastet bleibt: $OUT" +fi + echo if [ "$FAILED" -eq 0 ]; then echo "Alle Tests bestanden."