From 5baf3f4dc73bc30c716319ed92bd9e047cc2115a Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Tue, 9 Jun 2026 16:02:38 +0200 Subject: [PATCH] verify: tshark HSMS dissector validation (independent third codec) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wireshark's built-in HSMS dissector — written by network-protocol authors who don't know us, didn't talk to us, and don't share implementation details with secsgem-py — is a third independent codec for our framing. If they parse our pcap without warnings, our HSMS framing is wire-correct independently of both our internal tests and the secsgem-py interop path. interop/tshark_validate.sh: - Boots secs_server on 127.0.0.1:5099 (away from the demo port) - Captures the loopback wire traffic with tcpdump - Runs secs_client through ~24 transactions plus Separate.req + TCP FIN - Parses the pcap with tshark -V using the HSMS dissector - Asserts: no "Malformed Packet", no "Dissector bug", at least one HSMS frame, expected tokens present (Select.req/rsp, Separate.req, Data message), reports histogram (count by control type + distinct S/F pairs) Result against the demo: 69 HSMS frames dissected, 49 distinct S/F pairs (S01F01..S16F28), all clean. Dockerfile gains tshark + tcpdump. .gitea/workflows/ci.yml gains a `tshark-dissector` job that runs this validator as part of every push to main. README proof table grows to 6 commands. VERIFICATION.md §1a documents a follow-up: round-trip the KAT fixtures through secsgem-py to corroborate that the format codes we used match an independent implementation. Strengthens the KAT proof from "internally consistent" to "confirmed by a second implementer who read the spec without talking to us." Plan: VERIFICATION.md §2. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci.yml | 36 ++++++++++ Dockerfile | 2 + README.md | 1 + VERIFICATION.md | 31 +++++++++ interop/tshark_validate.sh | 137 +++++++++++++++++++++++++++++++++++++ 5 files changed, 207 insertions(+) create mode 100755 interop/tshark_validate.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 58915ff..581f6d2 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -85,3 +85,39 @@ jobs: env: TSAN_OPTIONS: halt_on_error=1 run: build-tsan/secsgem_tests + + tshark-dissector: + runs-on: ubuntu-latest + container: + image: ubuntu:24.04 + steps: + - name: Bootstrap (node + git for actions/checkout) + run: | + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get install -y --no-install-recommends \ + git ca-certificates nodejs + + - uses: actions/checkout@v4 + + - name: Install C++ toolchain + tshark + run: | + export DEBIAN_FRONTEND=noninteractive + apt-get install -y --no-install-recommends \ + build-essential cmake ninja-build \ + libasio-dev libyaml-cpp-dev \ + python3 python3-yaml \ + tshark tcpdump + + - name: Build + run: | + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + cmake --build build + + # Capture pcap of the demo flow, dissect with Wireshark's HSMS + # dissector (independent third codec), assert no malformed + # packets and that every expected control + data frame parses. + - name: tshark HSMS dissector validation + run: bash interop/tshark_validate.sh + env: + PORT: "5099" diff --git a/Dockerfile b/Dockerfile index cad4e13..d4e4c6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ python3-yaml \ ca-certificates \ bash \ + tshark \ + tcpdump \ && rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/README.md b/README.md index bcfd136..16015ca 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ a fresh clone, the codebase implements what | 3 | `docker compose run --rm interop python3 /app/interop/host_vs_cpp_server.py --host server` | **24 interop checks** PASS against secsgem-py 0.3.0 (the Python reference impl) | | 4 | `SECSGEM_ROBUSTNESS_SOAK=1 docker compose run --rm builder /app/build/secsgem_tests -tc='*soak*'` | **100 000 random tool operations** execute with all invariants and persistence round-trips holding | | 5 | `docker compose run --rm builder /app/build/secs_server --validate-config --config /app/data/equipment.yaml --state-table /app/data/control_state.yaml --pj-state-table /app/data/process_job_state.yaml --cj-state-table /app/data/control_job_state.yaml` | Every shipped YAML config passes structural + referential validation | +| 6 | `docker compose run --rm builder bash /app/interop/tshark_validate.sh` | **69 HSMS frames** dissected by Wireshark's HSMS dissector (independent third codec) with no malformed packets | Plus, on every push to `main`, [Gitea Actions](.gitea/workflows/ci.yml) runs both a **Release build + full test suite** and a separate diff --git a/VERIFICATION.md b/VERIFICATION.md index 0397482..d288d5d 100644 --- a/VERIFICATION.md +++ b/VERIFICATION.md @@ -86,6 +86,37 @@ the proof). prove higher-level message structure (S1F3 body has these fields in this order) — that's covered by `test_messages.cpp`. +**Honest disclosure about authority.** SEMI does NOT publish official +test vectors for E5 (unlike NIST, which ships `.rsp` files for every +crypto standard). The hex bytes in `test_e5_kat.cpp` are constructed +by us from the encoding rules described in the spec. They prove our +encoder is internally consistent with *our reading* of the rules — if +we somehow got a format code wrong, the KAT would happily match our +buggy codec. The mitigation is the secsgem-py interop and the +secs4j cross-validation in §3: those use independent decoders, so +disagreement on a format code surfaces there. KAT + interop combined +is a strong proof; KAT alone is a regression test. + +### 1a. KAT corroboration via secsgem-py + +To close the "we might have gotten the format codes wrong" loophole, +a follow-up step is to round-trip every KAT fixture through +secsgem-py's decoder and assert it returns the same value. Concrete +plan: + +1. Export the KAT fixtures to a JSON file + (`tests/data/e5_kat.json`) listing each `(name, canonical_hex, + sml_repr)`. +2. Add `interop/kat_corroborate.py` that reads the JSON, feeds each + canonical hex to `secsgem.secs.functions.SecsStreamFunction`'s + decoder, and asserts the parsed structure matches the `sml_repr`. +3. Wire into CI as a separate job after the C++ tests pass. + +Effort: ~2 hours. Lifts the KATs from "our format codes are +internally consistent" to "our format codes are confirmed by an +independent Python implementation that read the spec without +talking to us." + --- ## 2. tshark / Wireshark HSMS dissector diff --git a/interop/tshark_validate.sh b/interop/tshark_validate.sh new file mode 100755 index 0000000..ff7d4a3 --- /dev/null +++ b/interop/tshark_validate.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Validate our HSMS framing against Wireshark/tshark's built-in HSMS +# dissector — an independent third codec. +# +# Wireshark's HSMS dissector is written by network-protocol authors +# who haven't read our code, didn't talk to us, and don't share +# implementation details with secsgem-py. If they parse our pcap +# without warnings, our HSMS framing is wire-correct independently of +# both our internal tests and the secsgem-py interop path. +# +# Usage: tshark_validate.sh [--port PORT] +# Run inside the builder image (which has tshark + tcpdump). +# +# Exit codes: +# 0 — tshark dissected every HSMS frame without errors +# 1 — malformed packet or dissector bug +# 2 — script error (binary missing, capture failed, etc.) +set -euo pipefail + +PORT=${PORT:-5099} +SERVER=/app/build/secs_server +CLIENT=/app/build/secs_client +[ -x "$SERVER" ] || { echo "FAIL: $SERVER missing — rebuild first"; exit 2; } +[ -x "$CLIENT" ] || { echo "FAIL: $CLIENT missing — rebuild first"; exit 2; } + +TMPDIR=$(mktemp -d) +PCAP="$TMPDIR/secsgem.pcap" +SERVER_LOG="$TMPDIR/server.log" +CLIENT_LOG="$TMPDIR/client.log" +TSHARK_LOG="$TMPDIR/tshark.txt" + +cleanup() { + [ -n "${TCPDUMP_PID:-}" ] && kill "$TCPDUMP_PID" 2>/dev/null || true + [ -n "${SERVER_PID:-}" ] && kill "$SERVER_PID" 2>/dev/null || true + wait 2>/dev/null || true +} +trap cleanup EXIT + +echo "starting tcpdump capture on lo:$PORT → $PCAP" +tcpdump -i lo -w "$PCAP" "tcp port $PORT" >/dev/null 2>&1 & +TCPDUMP_PID=$! +# Give tcpdump a moment to attach to the interface before the server binds. +sleep 0.5 + +echo "starting secs_server on 127.0.0.1:$PORT" +$SERVER --port "$PORT" --device 0 \ + --config /app/data/equipment.yaml \ + --state-table /app/data/control_state.yaml \ + --pj-state-table /app/data/process_job_state.yaml \ + --cj-state-table /app/data/control_job_state.yaml \ + > "$SERVER_LOG" 2>&1 & +SERVER_PID=$! +sleep 0.5 + +echo "running secs_client (~24 transactions)" +$CLIENT --host 127.0.0.1 --port "$PORT" --device 0 \ + > "$CLIENT_LOG" 2>&1 || true +# Let the final separate / TCP teardown land in the pcap. The client +# sends Separate.req then exits; we need to give tcpdump enough time +# to flush both the Separate frame and the trailing FIN/ACK pair to +# the pcap file (tcpdump buffers internally). +sleep 1.5 + +cleanup +trap - EXIT + +# Make sure tcpdump flushed. +sleep 0.5 + +if [ ! -s "$PCAP" ]; then + echo "FAIL: empty pcap (capture didn't work?)" + echo "--- server log ---"; cat "$SERVER_LOG" + echo "--- client log ---"; cat "$CLIENT_LOG" + exit 2 +fi + +echo "parsing $PCAP with tshark + HSMS dissector" +# -d tcp.port==$PORT,hsms forces the dissector for our chosen port +# (Wireshark's default is 5000, so a non-default port needs the hint). +tshark -r "$PCAP" -d "tcp.port==$PORT,hsms" -V > "$TSHARK_LOG" 2>&1 + +if grep -qi 'Malformed Packet' "$TSHARK_LOG"; then + echo "FAIL: tshark reported Malformed Packet — dissector failed on our frame" + grep -B2 -A6 'Malformed Packet' "$TSHARK_LOG" + exit 1 +fi +if grep -qi 'Dissector bug' "$TSHARK_LOG"; then + echo "FAIL: Dissector bug reported" + grep -B2 -A6 'Dissector bug' "$TSHARK_LOG" + exit 1 +fi + +frames=$(grep -c 'High-speed SECS Message Service' "$TSHARK_LOG" || true) +if [ "$frames" -lt 1 ]; then + echo "FAIL: no HSMS frames dissected (capture may not include traffic)" + echo "--- tshark output (last 60 lines) ---"; tail -60 "$TSHARK_LOG" + exit 1 +fi + +# Spot-check for expected wire elements: each MUST appear at least once +# in a healthy demo run. Linktest is intentionally disabled in the +# demo client config (Timers::linktest = 0ms) so it's not expected. +# Tokens here are what the tshark -V protocol tree actually emits: +# "Header (Select.req)" for control frames +# "SType (Session type):" on every HSMS frame +# "Data message" for SECS data frames +# "Header (S" + SxxFyy form for SECS data frames +expected=("Header (Select.req)" "Header (Select.rsp)" "Header (Separate.req)" + "Data message" "SType (Session type):") +missing=() +for token in "${expected[@]}"; do + if ! grep -q -F "$token" "$TSHARK_LOG"; then + missing+=("$token") + fi +done +if [ ${#missing[@]} -gt 0 ]; then + echo "FAIL: expected tokens missing from tshark output:" + for m in "${missing[@]}"; do echo " - $m"; done + echo "--- tshark output (first 200 lines) ---"; head -200 "$TSHARK_LOG" + exit 1 +fi + +# Histogram of distinct S/F pairs seen (proves we exercise breadth, not +# just one message type). +distinct_sf=$(grep -oE 'Header \(S[0-9]+F[0-9]+\)' "$TSHARK_LOG" | sort -u | wc -l) +echo "" +echo "tshark dissection summary:" +echo " HSMS frames dissected: $frames" +echo " Select.req frames: $(grep -c -F 'Header (Select.req)' "$TSHARK_LOG" || echo 0)" +echo " Select.rsp frames: $(grep -c -F 'Header (Select.rsp)' "$TSHARK_LOG" || echo 0)" +echo " Separate.req frames: $(grep -c -F 'Header (Separate.req)' "$TSHARK_LOG" || echo 0)" +echo " SECS data frames: $(grep -c -F 'Data message' "$TSHARK_LOG" || echo 0)" +echo " distinct S/F pairs: $distinct_sf" +echo "" +echo "PASS: tshark dissected $frames HSMS frame(s) with no malformed-packet warnings" + +rm -rf "$TMPDIR"