Signed packets. A GeoNetworking Basic Header NextHeader of 2 means a TS 103 097 (IEEE 1609.2) envelope follows, with the Common Header inside it. gn_unwrap_its rejected all of these, and most real traffic is signed: the 2026-08-17 capture holds 157 signed frames from 15 source MACs against 2 unsecured stations. It now opens a COER-encoded signedData, or a bare unsecuredData, and parses the inner packet as before. The inner packet comes first inside tbsData, so the certificate and signature are never parsed, and the signature is not verified - the firmware has no trust store. Such messages reach the phone with the new V2X_RX flags bit1, signed but not verified. The app reads only bit0 and is unaffected until it learns the flag. Encrypted payloads, nested signing and the legacy v1.2.1 envelope are still rejected. All 157 recorded signed frames have the layout this reads, in all three COER length forms, and asn1tools decodes every envelope to the same inner packet. Payload bounds. Every frame recorded through the ESP32-C5's promiscuous RX, about 15 000 of them, ends in 8 bytes that are not part of the 802.11 frame and not a valid FCS. obu-firmware reads frames through the same API and took the rest of the frame as the message, so it forwarded those 8 bytes to the phone after every message. UPER decoders stop where the message ends, so nothing visibly broke, but the bytes cost serial bandwidth and 8 bytes of the DENM's headroom, and they stayed attached wherever raw payloads were stored or passed on. The payload is now exactly what the Common Header's payload-length field declares, which is also what separates a signed message from its signature. A frame longer than main.c's 800-byte capture buffer is now reported as truncated instead of being forwarded cut off, and counted as an oversize drop through the new serial_link_note_oversize_drop, as it was when the cut-off frame failed serial_link's size check. Host tests in obu-firmware/test/host build the firmware sources unmodified with MSYS2 gcc; `make` runs all three. - test_chain: frames from the firmware's TX code checked byte by byte against EN 302 636-4-1 and parsed back, including hand-built signed frames, the payload-length rule, the RX trailer, and every truncation length against a no-access guard page. 1731 checks, 0 failures. - test_replay and check_replay.py: all 15 145 recorded frames through gn_unwrap_its, cut to 800 bytes as on the board, and re-derived independently in Python with the envelope decoded by asn1tools. They agree on every record; 15 131 accepted, 157 of them signed. 11 043 of the 11 106 distinct messages re-encode byte-identically. The other 63 fail the same way with the old 8 bytes put back, so the boundary is not the cause: 5 are our own CAMs from before the 2026-08-20 yawRateConfidence fix, and the rest, from other stations, are a follow-up in TODO.md. - fuzz_gn_unwrap: random edits of every recorded frame, each run against the guard page. 50 000 000 iterations, no crash. obu-firmware/test/pcap_gn_tally.py tallies GeoNetworking header fields per station over captures; it is how the other stations' lifetimes were measured. TODO.md collects what is still open, including the on-air check for this change: it builds on IDF 6.1 but has not been flashed.
152 lines
7.7 KiB
Markdown
152 lines
7.7 KiB
Markdown
# Host-side tests for obu-firmware
|
|
|
|
**Status (2026-09-11):** the chain test, the capture replay and the fuzzer are written and pass;
|
|
results under "Running". Toolchain: MSYS2 UCRT64 gcc, Option B below (chosen and
|
|
installed 2026-09-11).
|
|
|
|
`geonet.c`, `dot11p.c` and `gn_unwrap.c` include nothing but standard C headers, so they compile
|
|
unmodified on a PC. That makes three things possible without a board: a TX -> RX round trip
|
|
through our own code, replaying real captures through the RX parser, and fuzzing the parser that
|
|
reads untrusted radio bytes. ESP-IDF only builds `main/` (and `components/`), so nothing under
|
|
`test/` ever affects the firmware image.
|
|
|
|
## Layout
|
|
|
|
```
|
|
obu-firmware/
|
|
├── main/ firmware sources; the tests compile these directly, never copies
|
|
└── test/
|
|
├── pcap_gn_tally.py GN header fields per station over .pcap captures (Python only)
|
|
└── host/
|
|
├── README.md this file
|
|
├── Makefile `make`: builds ../../main/{geonet,dot11p,gn_unwrap}.c + tests, runs them
|
|
├── test_chain.c geonet_wrap_shb -> dot11p_build_frame -> gn_unwrap_its, byte-checked
|
|
├── test_util.c/.h shared: guard page, crash report, pcap read/write
|
|
├── test_replay.c every recorded frame through gn_unwrap_its, results to a TSV
|
|
├── check_replay.py re-derives each result independently, then asn1tools on the messages
|
|
├── fuzz_gn_unwrap.c mutation fuzzer; each input ends against a no-access guard page, so
|
|
│ an over-read faults (no ASan on MinGW)
|
|
└── build/ compiler output; already ignored by the repo's `build/` rule
|
|
```
|
|
|
|
The repo's `vanetza/` folder is a gitignored reading copy only; it is not built. `check_replay.py`
|
|
reads the IEEE 1609.2 ASN.1 modules from it (they carry no licence header, so they are not copied
|
|
into `asn1/`). Without it, signed frames are still replayed but not checked independently.
|
|
|
|
## Running
|
|
|
|
From PowerShell, with MSYS2 on PATH for the session (Option B step 3):
|
|
|
|
```powershell
|
|
$env:PATH = "C:\msys64\ucrt64\bin;C:\msys64\usr\bin;$env:PATH"
|
|
cd C:\Users\Ashin\AndroidStudioProjects\MicrOBU\obu-firmware\test\host
|
|
make
|
|
```
|
|
|
|
`make` runs three things (`make chain`, `make replay`, `make fuzz` run one). A non-zero exit, or a
|
|
`CRASH ... during: <what>` line (from the fuzzer, followed by the input as hex), is a failure.
|
|
|
|
- **chain** (`test_chain.c`): frames built by the firmware's own TX code, checked byte by byte
|
|
against EN 302 636-4-1 and parsed back. Covers the CAM layout (non-QoS and QoS), Source Position
|
|
Vector edges, output-buffer bounds, a 512-byte payload through `main.c`'s buffer sizes,
|
|
hand-built GeoBroadcast frames in all three shapes, signed frames in all three COER length
|
|
forms plus a top-level unsecuredData, one-byte mutations that must be rejected or accepted, the
|
|
Common Header's payload length as the message boundary, the 8-byte RX trailer, and every
|
|
truncation length of each frame against the guard page. `pcap_gn_tally.py` then reads the
|
|
frames back as a second parser; `build/test_chain.pcap` opens in Wireshark too.
|
|
2026-09-11: 1731 checks, 0 failed.
|
|
- **replay** (`test_replay.c` + `check_replay.py`): every record in
|
|
`its-g5-receiver-firmware/recordings/*.pcap` through `gn_unwrap_its`, each cut to `main.c`'s
|
|
800-byte capture buffer as on the board. `check_replay.py` re-derives each result on its own
|
|
(its own GN/BTP parse; the security envelope decoded by asn1tools from the IEEE 1609.2 modules),
|
|
compares record by record, then decodes and re-encodes every distinct message with asn1tools -
|
|
a byte-identical re-encode is only possible when the message was cut at exactly the right byte.
|
|
Needs `py -3.11` with asn1tools. 2026-09-11: 15 145 records, 15 131 accepted (10 831 CAM,
|
|
4 300 DENM; 157 of them signed); C and Python agree on every record; 11 043 of the 11 106
|
|
distinct messages re-encode byte-identically. The other 63 fail the same way with the old 8
|
|
trailing bytes put back, so the boundary is not the cause: 5 are our own CAMs from before the
|
|
2026-08-20 yawRateConfidence fix, 56 come from the CiT One and 1 from another station (see
|
|
`TODO.md`), and 1 uses an extension asn1tools cannot re-encode.
|
|
- **fuzz** (`fuzz_gn_unwrap.c`): random edits of every recorded frame, each run against the guard
|
|
page; an over-read crashes, an accepted payload outside its input fails. Default 2 000 000
|
|
iterations (about 2 s); `make fuzz FUZZ_ITER=50000000 FUZZ_SEED=7` for a longer run.
|
|
2026-09-11: 50 000 000 iterations, no crash.
|
|
|
|
Not covered: `main.c` (serial prefix parsing, queues) and `serial_link.c`, which need ESP-IDF;
|
|
and the phone's encoder, whose bytes are opaque here (asn1tools and the app's golden test cover
|
|
it).
|
|
|
|
## Option A (not used): WSL2 + Ubuntu 24.04
|
|
|
|
Kept as the fallback if AddressSanitizer or libFuzzer are ever needed; Option B has neither.
|
|
|
|
1. In **PowerShell as Administrator**:
|
|
|
|
```powershell
|
|
wsl --install -d Ubuntu-24.04
|
|
```
|
|
|
|
Reboot if it asks. Ubuntu then opens and asks for a Linux username and password (separate from
|
|
the Windows account). Checked 2026-09-11: Hyper-V is already running on this PC, so no BIOS
|
|
change should be needed. If the install says virtualization is disabled, enable Intel VT-x /
|
|
AMD SVM in the BIOS.
|
|
|
|
2. Confirm it is WSL **2**: `wsl -l -v` should list `Ubuntu-24.04` with VERSION `2`.
|
|
|
|
3. Inside Ubuntu, the compilers:
|
|
|
|
```bash
|
|
sudo apt update
|
|
sudo apt install -y build-essential clang cmake ninja-build git pkg-config python3
|
|
```
|
|
|
|
4. Smoke test: the firmware sources compile on the host (expect no output):
|
|
|
|
```bash
|
|
cd /mnt/c/Users/Ashin/AndroidStudioProjects/MicrOBU/obu-firmware/test/host
|
|
cc -std=c11 -Wall -Wextra -fsyntax-only ../../main/geonet.c ../../main/dot11p.c ../../main/gn_unwrap.c
|
|
```
|
|
|
|
## Option B (chosen): native Windows gcc via MSYS2
|
|
|
|
Enough for the round-trip test, the capture replay and the guard-page fuzzer. No libFuzzer and no
|
|
AddressSanitizer with MinGW gcc, which is why the fuzzer uses a guard page instead.
|
|
|
|
1. In PowerShell: `winget install -e --id MSYS2.MSYS2` (installs to `C:\msys64`).
|
|
2. Open **MSYS2 UCRT64** from the Start menu and run `pacman -Syu`. If the window closes, reopen
|
|
it and run `pacman -Syu` again. Then:
|
|
|
|
```bash
|
|
pacman -S --needed mingw-w64-ucrt-x86_64-gcc make
|
|
```
|
|
|
|
3. **`C:\msys64\ucrt64\bin` must be on PATH.** Calling `C:\msys64\ucrt64\bin\gcc.exe` by its full
|
|
path alone exits 1 with no message, because gcc's compiler stages load their DLLs from that
|
|
folder. `make` lives on the MSYS side, in `C:\msys64\usr\bin`. Either work inside the
|
|
**MSYS2 UCRT64** shell, which has both, or put them on PATH for the current session:
|
|
|
|
```powershell
|
|
$env:PATH = "C:\msys64\ucrt64\bin;C:\msys64\usr\bin;$env:PATH" # PowerShell
|
|
```
|
|
|
|
```bash
|
|
export PATH=/c/msys64/ucrt64/bin:/c/msys64/usr/bin:$PATH # Git Bash
|
|
```
|
|
|
|
Adding them to the user PATH permanently also works; it was deliberately not done by setup.
|
|
4. Smoke test (expect no output):
|
|
|
|
```powershell
|
|
cd C:\Users\Ashin\AndroidStudioProjects\MicrOBU\obu-firmware\test\host
|
|
gcc -std=c11 -Wall -Wextra -fsyntax-only ../../main/geonet.c ../../main/dot11p.c ../../main/gn_unwrap.c
|
|
```
|
|
|
|
Installed on this PC 2026-09-11: MSYS2 20260611, gcc 16.2.0 (UCRT64), GNU Make 4.4.1. All three
|
|
firmware sources compile with `-std=c11 -O2 -Wall -Wextra -Wpedantic` and no warnings.
|
|
|
|
## Line endings
|
|
|
|
The repo runs with `core.autocrlf=true`, so Windows checkouts have CRLF line endings. C compilers
|
|
don't care; shell scripts run from WSL do (`bash: $'\r': command not found`). When the first `.sh`
|
|
file lands here, add `*.sh text eol=lf` to a root `.gitattributes` (none exists yet).
|