Files
MicrOBU/TODO.md
T
Ashin Walpola 75d6d3b85c Receive signed ITS messages and forward each at its declared length
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.
2026-09-11 20:19:40 +02:00

6.3 KiB

TODO

Engineering to-do list. The reviewer-facing open items live in docs/01-requirements-traceability.md ("Open items"); this file is the working list behind them.

Waiting on hardware

Over-the-air check of the GN lifetime fix (added 2026-09-11)

geonet.c now writes GN lifetime 0x05 (1 s) instead of 0x83, which decoded to 3200 s. Changed in both obu-firmware and obu-cam-transmistter. Both still build (IDF 6.1 / 5.5.4), and the compiled geonet_wrap_shb stores the new byte, but it has not been seen on air yet. Nothing else reads this byte (gn_unwrap.c ignores it, the app never sees GN headers), so the app does not need updating alongside the firmware.

Needs: the phone with the app, the OBU ESP32-C5, and a second ESP32-C5 running its-g5-receiver-firmware to capture with.

  • Flash obu-firmware (see obu-firmware/FLASHING.md).
  • Connect the phone, let it send CAMs, and confirm the CAM Pinger's tx fail counter stays 0.
  • Capture with the receiver into its-g5-receiver-firmware/recordings/.
  • Run python obu-firmware/test/pcap_gn_tally.py its-g5-receiver-firmware/recordings/<capture>.pcap. The rows for the phone's pseudonym MACs must show SHB, port 2001, lifetime 0x05, exactly like every other station's CAMs.
  • While the phone is connected: real-station CAMs/DENMs still reach the app (RX path unchanged).

Partial check possible with one board and no phone: flash it, idf.py -p COMx monitor, and look for OCB @ 5900 MHz - TX/RX armed. That proves the new build boots and brings the radio up, not that it transmits correctly.

obu-cam-transmistter yawRateConfidence fix (added 2026-09-11)

Its cam.c (compiled into that firmware) wrote yawRateConfidence as 3 bits / 7 instead of 4 bits / unavailable(8), the bug the app fixed on 2026-08-20. Fixed in it and in obu-firmware's reference copy; asn1tools now decodes the CAM and re-encodes it byte-identically, and it builds on IDF 5.5.4. No board runs this firmware right now (the production OBU runs obu-firmware), so this only matters if it is flashed again:

  • After flashing it: capture, run pcap_gn_tally.py, and decode the CAM payload with asn1tools (py -3.11, modules in asn1/).

Signed-message reception and exact payloads (added 2026-09-11)

obu-firmware's gn_unwrap.c now unwraps TS 103 097 signed packets (signature not verified, reported as V2X_RX flags bit1) and cuts every message to the length its header declares, dropping the 8 bytes the chip's RX appends to each frame, which were forwarded to the phone until now. Verified on the host (obu-firmware/test/host: chain, replay of all recordings against asn1tools, 50M-iteration fuzz) and built on IDF 6.1, but not flashed: the production OBU still runs the 2026-09-10 build. Needs the OBU with this build, the phone, and signed traffic - real vehicles or RSUs, since the bench CiT One sends unsigned. A second ESP32 running the receiver firmware is optional, but shows what was on air at the time.

  • Flash obu-firmware (this also carries the GN lifetime fix above).
  • Near signed traffic: signed CAMs/DENMs appear in the app, and a simultaneous capture shows them on air (pcap_gn_tally.py lists them as secured).
  • Unsigned bench traffic still decodes in the app as before (messages now arrive 8 bytes shorter).
  • The heartbeat's oversize counter still counts over-long messages (e.g. road SPATEMs).

Set up host testing

  • Install MSYS2 UCRT64 gcc (done 2026-09-11: gcc 16.2.0, GNU Make 4.4.1; chosen over WSL, vanetza is not going to be built). Setup and the PATH gotcha: obu-firmware/test/host/README.md.
  • Host round-trip test obu-firmware/test/host/test_chain.c (geonet_wrap_shb -> dot11p_build_frame -> gn_unwrap_its, byte-checked against the standard). Done 2026-09-11: 491 checks, 0 failed. Run make in that folder before flashing any firmware fix.
  • Replay of the recorded captures (test_replay.c + check_replay.py, independent asn1tools check). Done 2026-09-11: C and Python agree on all 15 145 records.
  • Mutation fuzzer fuzz_gn_unwrap.c, inputs against a no-access guard page. Done 2026-09-11: 50 000 000 iterations, no crash. make runs a 2 000 000-iteration pass every time.

Firmware ideas from the vanetza review (2026-09-11, not started)

Suggested order after the host tests exist:

  • Read secured packets (GN NextHeader=2) without verifying them. Done 2026-09-11 in gn_unwrap.c, host-verified; flagged to the phone as V2X_RX flags bit1. On-air check under "Waiting on hardware".
  • Forward the full GeoBroadcast area: shape (circle/rectangle/ellipse), DistanceB, angle, appended to the V2X_RX prefix behind a capability bit. Port vanetza's geonet/areas.cpp inside_or_at_border to the app, which currently treats every area as a circle.
  • RX filtering before the serial link: duplicate detection for GBC (last 8 sequence numbers per source, as vanetza does), drop our own frames, reject GN version != 1.
  • Read the DCC-MCO field (the 4 "reserved" bytes of an SHB header): neighbours' channel busy ratio for free.
  • Minimum TX gap in firmware as a DCC safety net (vanetza reactive table: 60 ms relaxed ... 460 ms restrictive), with a CBR estimate in the heartbeat.
  • Generic V2X_TX message (BTP port, SHB/GBC, traffic class, lifetime, area) so the phone can send DENM and VAM without reflashing. Consider QoS Data frames: vanetza's Cohda receive path drops non-QoS ones.

Dropped: building vanetza as a GN/BTP oracle. Real captures (pcap_gn_tally.py), the host round-trip test and asn1tools for UPER cover what it would have checked.

Follow-ups found 2026-09-11

  • App: show the signed flag. V2xRxFrame.parse in SerialFrame.kt only reads bit0 of the flags byte; read bit1 (signed, not verified) and show it where messages are listed.
  • Messages that do not decode with asn1tools. In the recordings, 56 from the CiT One (aa:f8:76:7d:bd:ad: 54 CAMs of 245 bytes, 2 DENMs of 402 bytes) and one 218-byte CAM from 6e:94:03:1b:05:26 fail against cam_1_4_1/denm_1_3_1 + cdd_1_3_1_1, with or without the old trailing bytes. A newer module version on the sender, or a sender bug; check what the app's decoders make of them (check_replay.py lists the records).