verify: tshark HSMS dissector validation (independent third codec)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+137
@@ -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"
|
||||
Reference in New Issue
Block a user