diff --git a/Makefile b/Makefile index 08f4b1f..ac819df 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,8 @@ # make library-net-smoke on-target mqtt.library end-to-end API test (real broker) # make volamos-library-net-smoke same check via volamos - faster local loop, not a CI substitute # make library-reconnect-smoke on-target mco_AutoReconnect test (broker restart mid-run) +# make fetch-amissl-sdk fetch the AmiSSL v5 SDK (needed for Amiga-side TLS support) +# make library-tls-smoke on-target mco_TLS test - local-only, needs an amibake image # make clean # # The core is portable C99, so `test` and `cli` build with any host compiler. @@ -386,6 +388,25 @@ libreconn-m68k: library-headers | $(BUILD)/.dir library-reconnect-smoke: library libreconn-m68k cli sh tests/library/reconn-run.sh +# --- m68k: on-target mco_TLS end-to-end smoke test (issue #3 Phase 3) --- +# Same shape as libnet-m68k (a normal CLI program against the generated +# caller-side headers only - no AmiSSL headers needed by the caller) - see +# tests/library/libtls.c. +libtls-m68k: library-headers | $(BUILD)/.dir + $(M68K_CC) $(M68K_CFLAGS) -I$(LIB_INCDIR) tests/library/libtls.c -o $(BUILD)/libtls + +# Local-only, NOT a CI/release gate (unlike library-net-smoke): needs a +# real AmigaOS 3.2.2 + AmiSSL boot image built by amibake +# (github.com/sidick/amibake), since AmiSSL needs real Devs:/Libs: install +# layout that the plain bundled-AROS/HostSocket boot the other +# library-*-smoke targets use doesn't have - see +# tests/library/README.md's "TLS smoke test" section for setup. Also +# needs `library` built with AmiSSL support (`make fetch-amissl-sdk` +# first - see M68K_HAS_AMISSL above), and $MIDGE_TLS_AMIGA_IMAGE pointing +# at the amibake output directory. +library-tls-smoke: library libtls-m68k cli + sh tests/library/tls-run.sh + # --- m68k: on-target check that examples/pubexample.c actually works --- # Real Copperline boot (CI/release gate): stages build/pubexample and # build/mqtt.library into a throwaway boot volume with Copperline's diff --git a/tests/library/README.md b/tests/library/README.md new file mode 100644 index 0000000..e1ac6f5 --- /dev/null +++ b/tests/library/README.md @@ -0,0 +1,71 @@ +# tests/library/ — on-target mqtt.library smoke tests + +`run.sh`, `net-run.sh`, and `reconn-run.sh` (and their `volamos-*` +counterparts) all boot Copperline's bundled AROS Kickstart replacement with +the HostSocket board fitted (see `../net/README.md`'s "Why this needs no +machine-specific assets") - no Kickstart ROM, no Workbench image, nothing +beyond `copperline`/`mosquitto` on `PATH`. They're CI/release gates +(`.github/workflows/ci.yml`). + +## TLS smoke test + +`tls-run.sh` (`make library-tls-smoke`) is different: it's **local-only**, +not a CI/release gate, because AmiSSL needs things the bundled-AROS boot +doesn't have: + +- A real `AmigaOS` `Devs:`/`Libs:` install layout - the `amissl` package's + own install step assigns `AmiSSL:` to `SYS:Devs/AmiSSL` (where its cert + store lives), which only exists on a real OS boot. +- A real Kickstart ROM - AmiSSL requires actual AmigaOS 3.0+, not the + bundled-AROS Kickstart replacement the other tests use. + +Neither is something CI can be handed (a Kickstart ROM is Cloanto/Hyperion +licensed media; a full OS install image is large and itself +proprietary-derived) - same reasoning `../copperline/`'s optional ROM and +`../net/`'s original (superseded) Roadshow-image design ran into. + +### Building the image + +Use [amibake](https://github.com/sidick/amibake) (a separate, +manifest-driven Amiga image builder) to build a real AmigaOS 3.2.2 + +AmiSSL 5.27 image: + +```sh +# in a checkout of amibake, with your own licensed AmigaOS 3.2 install +# media + Kickstart ROM under assets/ (see amibake's own docs/limits.md) +cat > manifests/midge-library-tls.toml <<'EOF' +base = "os3.2.2" +machine = { cpu = "68020", ram = "chip:1M,fast:8M" } +packages = ["amissl = 5.27"] +output = ["dir"] +emit = ["copperline"] +EOF +.venv/bin/amibake resolve manifests/midge-library-tls.toml +.venv/bin/amibake build manifests/midge-library-tls.toml --assets assets +``` + +This writes `manifests/midge-library-tls/` (the boot image) and +`manifests/midge-library-tls.copperline.toml` (which names the Kickstart +ROM amibake resolved against - `tls-run.sh` re-derives its own machine +config, but that file tells you which ROM path to point +`MIDGE_TLS_KICKSTART` at if it isn't amibake's own default location). + +### Running it + +```sh +export MIDGE_TLS_AMIGA_IMAGE=/path/to/amibake/manifests/midge-library-tls +export MIDGE_TLS_KICKSTART=/path/to/amibake/assets/roms/kickstart-47.7.rom +make fetch-amissl-sdk # once, populates AMISSL_SDK_DIR for `make library` +make library-tls-smoke +``` + +`tls-run.sh` stages `build/libtls` + `build/mqtt.library` into the image +and overwrites its `S/User-Startup` on every run (see the script's own +banner for exactly what it does and why) - point `MIDGE_TLS_AMIGA_IMAGE` at +an image you don't mind that happening to, not a real Workbench install you +use for anything else. + +See [tests/copperline/amissl-spike/](../copperline/amissl-spike/) for the +earlier de-risking investigation this test's approach (amibake image, +`AmiSSL:` assign trap, real-hardware CPU-speed caveat) is drawn from - +issue #3 has the full writeup. diff --git a/tests/library/libtls.c b/tests/library/libtls.c new file mode 100644 index 0000000..dc1040f --- /dev/null +++ b/tests/library/libtls.c @@ -0,0 +1,136 @@ +/* libtls.c — on-target (m68k/AmigaOS) end-to-end TLS smoke test for + * mqtt.library's mco_TLS/mco_TLSInsecure (issue #3 Phase 3): OpenLibrary, + * CreateClient with mco_TLS set, Connect, Subscribe, GetMessage, Publish, + * Disconnect, DeleteClient, CloseLibrary, all against a real TLS Mosquitto + * reachable over Copperline's HostSocket board - mqtt.library's TLS + * counterpart to libnet.c (see that file's banner for the shared shape; + * this one differs only in needing AmiSSL and a real AmigaOS boot instead + * of the bundled-AROS/HostSocket-only boot libnet.c uses - see + * tests/library/README.md's "TLS smoke test" section for why). + * + * Same PASS/FAIL/RESULT/END serial contract as libnet.c/libsmoke.c/ + * codec_selftest.c, via exec/RawPutChar rather than stdio. + */ + +#include +#include +#include +#include + +#include +#include + +#include + +/* See libnet.c's banner: a normal CLI program must define + fill this in + * itself, unlike a library's own base. */ +struct Library *MqttBase; + +static void raw_put(char c) +{ + void *SysBase = *(void **)4UL; + register long d0 __asm__("d0") = (unsigned char)c; + register void *a6 __asm__("a6") = SysBase; + __asm__ volatile("jsr -516(%%a6)" : : "r"(d0), "r"(a6) + : "d1", "a0", "a1", "cc", "memory"); +} + +static void raw_str(const char *s) +{ + while (*s) + raw_put(*s++); +} + +static int g_fails; + +static void check(int cond, const char *name) +{ + if (cond) { + raw_str("PASS "); + } else { + raw_str("FAIL "); + g_fails++; + } + raw_str(name); + raw_str("\r\n"); +} + +/* Must match tls-run.sh's own PORT/topic/payload constants exactly. */ +#define TEST_HOST "127.0.0.1" +#define TEST_PORT 18884 +#define TOPIC_IN "midge/lib/tls/in" +#define TOPIC_OUT "midge/lib/tls/out" +#define RETAINED_PAYLOAD "hello-from-host-retained-tls" +#define OUT_PAYLOAD "hello-from-mqtt-library-tls" + +/* Poll budget for the retained message - see libnet.c's own comment. A + * real TLS handshake costs more than a plaintext connect, so this budget + * is a little more generous than libnet.c's. */ +#define POLL_TRIES 150 +#define POLL_TICKS 10 + +int main(void) +{ + struct MqttConnectOpts opts; + APTR client = NULL; + struct MqttMessage *msg = NULL; + int i; + + raw_str("BEGIN\r\n"); + + MqttBase = OpenLibrary((STRPTR) "mqtt.library", 0); + check(MqttBase != NULL, "openlibrary-nonnull"); + if (!MqttBase) + goto done; + + memset(&opts, 0, sizeof(opts)); + opts.mco_ClientID = (STRPTR) "midge-libtls"; + opts.mco_KeepAlive = 30; + opts.mco_CleanSession = TRUE; + opts.mco_TLS = TRUE; + /* Self-signed test broker (tls-run.sh) - not a production posture, see + * mco_TLSInsecure's own doc comment in . */ + opts.mco_TLSInsecure = TRUE; + + client = MQTT_CreateClient((STRPTR) TEST_HOST, TEST_PORT, &opts); + check(client != NULL, "createclient-nonnull"); + if (!client) + goto close_lib; + + check(MQTT_Connect(client) == 0, "connect-tls-ok"); + check(MQTT_Subscribe(client, (STRPTR) TOPIC_IN, 1) == 0, "subscribe-ok"); + + for (i = 0; i < POLL_TRIES; i++) { + msg = MQTT_GetMessage(client); + if (msg) + break; + Delay(POLL_TICKS); + } + check(msg != NULL, "getmessage-received"); + + if (msg) { + check(strcmp((const char *)msg->mm_Topic, TOPIC_IN) == 0, + "message-topic"); + check(strcmp((const char *)msg->mm_Payload, RETAINED_PAYLOAD) == 0, + "message-payload"); + MQTT_FreeMessage(client, msg); + } else { + check(0, "message-topic"); + check(0, "message-payload"); + } + + check(MQTT_Publish(client, (STRPTR) TOPIC_OUT, (APTR) OUT_PAYLOAD, + (ULONG) strlen(OUT_PAYLOAD), 0, 1) == 0, + "publish-qos1-ok"); + + MQTT_Disconnect(client); + MQTT_DeleteClient(client); + +close_lib: + CloseLibrary(MqttBase); + +done: + raw_str(g_fails == 0 ? "RESULT=OK\r\n" : "RESULT=FAIL\r\n"); + raw_str("END\r\n"); + return g_fails; +} diff --git a/tests/library/tls-run.sh b/tests/library/tls-run.sh new file mode 100755 index 0000000..74acffc --- /dev/null +++ b/tests/library/tls-run.sh @@ -0,0 +1,156 @@ +#!/bin/sh +# tls-run.sh — on-target Copperline end-to-end TLS test for mqtt.library's +# mco_TLS/mco_TLSInsecure (issue #3 Phase 3). Modeled on net-run.sh (broker +# setup, staging, serial capture, PASS/FAIL/RESULT/END markers) but, unlike +# every other tests/library/*-run.sh, this one is LOCAL-ONLY and NOT a +# CI/release gate - see tests/library/README.md's "TLS smoke test" section +# for the full explanation. In short: AmiSSL needs a real AmigaOS +# Devs:/Libs: install (its own recipe assigns AmiSSL: to SYS:Devs/AmiSSL), +# which the bundled-AROS/HostSocket-only boot every other library-*-smoke +# target uses doesn't have - so this test boots a real AmigaOS 3.2.2 + +# AmiSSL image built by amibake (github.com/sidick/amibake) instead, via a +# real Kickstart ROM, both of which are local developer assets, not +# something CI can be given. +# +# Sequence (matching libtls.c's PORT/topic/payload constants exactly): +# 1. start a scratch TLS Mosquitto (self-signed cert), wait for it +# 2. retained-publish RETAINED_PAYLOAD to TOPIC_IN over TLS (host +# mqtt_pub-host -s -S -r) +# 3. boot Copperline running C:libtls (staged into the amibake image, +# alongside a fresh S:User-Startup that does the AmiSSL: assign a +# real OS boot doesn't inject on its own - see amissl-spike/run.sh's +# own comment on this same trap) - libtls OpenLibrary("mqtt.library"), +# MQTT_CreateClient(mco_TLS=TRUE, mco_TLSInsecure=TRUE)/Connect/ +# Subscribe(TOPIC_IN)/GetMessage/Publish(TOPIC_OUT)/Disconnect/ +# DeleteClient/CloseLibrary +# 4. assert the guest's serial PASS/FAIL/RESULT/END markers AND that a +# host TLS observer actually saw OUT_PAYLOAD on TOPIC_OUT +# +# Usage: sh tests/library/tls-run.sh (invoked via `make library-tls-smoke`, +# which cross-builds libtls + mqtt.library - with AmiSSL support, see +# `make fetch-amissl-sdk` - first) +# +# Environment: +# MIDGE_TLS_AMIGA_IMAGE amibake-built image dir (default +# $HOME/src/amibake/manifests/AmiSSLSpike) +# MIDGE_TLS_KICKSTART Kickstart ROM path (default +# $HOME/src/amibake/assets/roms/kickstart-47.7.rom) +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$HERE/../.." && pwd) + +COPPERLINE=${COPPERLINE:-copperline} +BIN=${LIBTLS_M68K:-$ROOT/build/libtls} +LIB=${MQTT_LIBRARY_M68K:-$ROOT/build/mqtt.library} +MQTT_PUB=${MQTT_PUB:-$ROOT/build/mqtt_pub-host} +IMAGE=${MIDGE_TLS_AMIGA_IMAGE:-$HOME/src/amibake/manifests/AmiSSLSpike} +ROM=${MIDGE_TLS_KICKSTART:-$HOME/src/amibake/assets/roms/kickstart-47.7.rom} +PORT=18884 # must match libtls.c's TEST_PORT +TOPIC_IN=midge/lib/tls/in +TOPIC_OUT=midge/lib/tls/out +RETAINED_PAYLOAD=hello-from-host-retained-tls +OUT_PAYLOAD=hello-from-mqtt-library-tls +BENCH=${BENCH:-300} # a real OS3.2.2 boot costs much more than AROS's ~10s + +command -v "$COPPERLINE" >/dev/null || { echo "FAIL: $COPPERLINE not found" >&2; exit 2; } +command -v mosquitto >/dev/null || { echo "FAIL: mosquitto not on PATH" >&2; exit 2; } +command -v openssl >/dev/null || { echo "FAIL: openssl not on PATH" >&2; exit 2; } +[ -e "$BIN" ] || { echo "FAIL: missing $BIN - run 'make libtls-m68k' first" >&2; exit 2; } +[ -e "$LIB" ] || { echo "FAIL: missing $LIB - run 'make fetch-amissl-sdk && make library' first" >&2; exit 2; } +[ -x "$MQTT_PUB" ] || { echo "FAIL: missing $MQTT_PUB - run 'make cli' first" >&2; exit 2; } +[ -d "$IMAGE" ] || { + echo "FAIL: MIDGE_TLS_AMIGA_IMAGE=$IMAGE not found - see" >&2 + echo " tests/library/README.md's \"TLS smoke test\" section to build one" >&2 + exit 2 +} +[ -f "$ROM" ] || { echo "FAIL: MIDGE_TLS_KICKSTART=$ROM not found" >&2; exit 2; } + +cp "$BIN" "$IMAGE/C/libtls" +cp "$LIB" "$IMAGE/Libs/mqtt.library" +cat > "$IMAGE/S/User-Startup" <<'EOF' +; Written by tests/library/tls-run.sh on every run - see that script. +Assign AmiSSL: SYS:Devs/AmiSSL +libtls +echo "LIBTLS: ALL-DONE" +EOF + +OUTDIR=$(mktemp -d) +MACHINE_TOML="$OUTDIR/tls-machine.toml" +OUT=$(mktemp) +MOSQ_PID= +trap 'kill "$MOSQ_PID" 2>/dev/null || true; rm -f "$OUT"; rm -rf "$OUTDIR"' EXIT + +cat > "$MACHINE_TOML" </dev/null + +cat > "$OUTDIR/mosquitto.conf" < "$OUTDIR/mosquitto.log" 2>&1 & +MOSQ_PID=$! + +i=0 +while ! grep -q "mosquitto version .* running" "$OUTDIR/mosquitto.log" 2>/dev/null; do + i=$((i + 1)) + [ "$i" -ge 50 ] && { echo "FAIL: mosquitto did not start" >&2; cat "$OUTDIR/mosquitto.log" >&2; exit 1; } + sleep 0.1 +done + +"$MQTT_PUB" -h 127.0.0.1 -p "$PORT" -S -t "$TOPIC_IN" -m "$RETAINED_PAYLOAD" -r + +mosquitto_sub -h 127.0.0.1 -p "$PORT" --cafile "$OUTDIR/server.crt" \ + -t "$TOPIC_OUT" -C 1 -W "$BENCH" > "$OUTDIR/sub.out" 2>"$OUTDIR/sub.err" & +SUB_PID=$! + +set -- --config "$MACHINE_TOML" --noaudio --serial stdout --benchmark-until "$BENCH" +( cd "$IMAGE" && "$COPPERLINE" "$@" ) >"$OUT" 2>/dev/null \ + || { echo "FAIL: $COPPERLINE exited non-zero" >&2; cat "$OUT" >&2; exit 3; } + +tr -d '\r' <"$OUT" >"$OUT.n" && mv "$OUT.n" "$OUT" +echo "----- serial capture -----"; cat "$OUT"; echo "--------------------------" + +wait "$SUB_PID" || true +echo "----- host TLS observer (topic $TOPIC_OUT) -----"; cat "$OUTDIR/sub.out"; echo "--------------------------" + +grep -q '^END' "$OUT" 2>/dev/null || { echo "FAIL: no END marker (raise BENCH?)" >&2; exit 1; } +grep -q '^FAIL ' "$OUT" && { echo "FAIL: one or more mqtt.library TLS checks failed on-target" >&2; exit 1; } +grep -q '^RESULT=OK$' "$OUT" || { echo "FAIL: no RESULT=OK marker" >&2; exit 1; } +grep -qF "$OUT_PAYLOAD" "$OUTDIR/sub.out" || { + echo "FAIL: host TLS observer never saw \"$OUT_PAYLOAD\" on $TOPIC_OUT" >&2 + exit 1 +} + +echo "PASS: mqtt.library's mco_TLS works end-to-end on 68020 (real AmiSSL codepath)"