obu-firmware builds against the vanetza-idf C-ITS library, which until now came from the colleague's microbu-esp32c5 tree beside the repository and was not tracked here, so a clone of this repository could not build the firmware it ships. The library alone is now part of obu-firmware, as obu-firmware/external/vanetza-idf: their external/vanetza-idf at commit cf4b99f, unchanged (9775 files; see its PROVENANCE.md). CMake takes it from there by default; -DVANETZA_IDF_DIR still points the build elsewhere. The rest of the colleague's tree (their own VAM firmware, PKI tooling, station-link Python tools, the V2X2MAP bridge) stays out of this repository and gitignored; nothing is pushed to their repository. NOTES.md, docs/06, TODO.md and the pcap verifier's usage line point at the new location.
142 lines
9.0 KiB
Markdown
142 lines
9.0 KiB
Markdown
# 06 – Signed ITS messages, VAM and the BLE link (2026-09-23)
|
||
|
||
What changed when the ESP32-C5 OBU moved onto the colleague's vanetza-idf station, how the pieces
|
||
fit together, how it was verified, and what is still open. Hardware checks still to do are in
|
||
`TODO.md` ("Signed-TX firmware ...").
|
||
|
||
## Summary
|
||
|
||
- **Signing lives on the ESP32-C5.** The authorization ticket's private key is in the board's NVS;
|
||
vanetza-idf's security entity signs every secured message there (IEEE 1609.2 / ETSI TS 103 097,
|
||
ECDSA NIST P-256). The phone never holds a key and never signs.
|
||
- **The phone decides what to send and when.** It builds CAM or VAM (UPER) from its own GNSS/IMU,
|
||
hands each message to the board with the flag "signed" or "unsigned", and keeps the board's clock
|
||
and position current.
|
||
- **Two links, one protocol.** USB-C (native USB Serial/JTAG) or Bluetooth LE, chosen in Settings.
|
||
Both carry the colleague's station-link protocol v1 plus one MicrOBU extension for reception.
|
||
- **Reception is unchanged for the app.** Every ITS message heard on air reaches the phone, signed
|
||
or not, verifiable or not, exactly as with the previous firmware.
|
||
- **Demo PKI, not the EU trust list.** Signed messages carry a throwaway chain. Receivers that
|
||
verify against the EU trust list drop them; unsigned sending remains available.
|
||
|
||
## Who does what
|
||
|
||
| | Phone (app) | ESP32-C5 (obu-firmware) |
|
||
|---|---|---|
|
||
| CAM / VAM content and UPER encoding | yes | – |
|
||
| Send cadence (CAM 1 Hz baseline; VAM per TS 103 300-3 clause 6.4) | yes | – |
|
||
| Pseudonym (station ID + MAC, rotated together) | yes | uses the MAC it is configured with |
|
||
| Time and position (PoTi) | yes, per new GNSS fix | keeps an ITS clock from it |
|
||
| GeoNetworking + BTP headers | – | yes |
|
||
| Signing (TS 103 097), certificate handling | – | yes |
|
||
| Credentials | ships the demo bundle, provisions it once | stores it in NVS |
|
||
| 802.11p radio at 5 900 MHz | – | yes |
|
||
| Reception: unwrap GN/BTP, forward | decodes CAM / DENM / SPATEM | yes (all frames) |
|
||
|
||
## Firmware (obu-firmware)
|
||
|
||
obu-firmware is now a port of `microbu-esp32c5/firmware`, from the colleague's own repository
|
||
(not part of this one; nothing is pushed there). The C-ITS library it needs is copied into this
|
||
repository as `obu-firmware/external/vanetza-idf` (their commit cf4b99f, unchanged), so
|
||
obu-firmware builds from a plain clone. It builds
|
||
with **ESP-IDF 6.0.2 only**: the raw-TX path uses private Wi-Fi driver structures that vanetza-idf
|
||
pins to that version. The previous C firmware (IDF 6.1) is backed up as a full flash image in
|
||
`firmware-backups/` (gitignored, restore command in its README.txt); its sources stay on disk,
|
||
unbuilt. Setup and flashing: `obu-firmware/FLASHING.md`. Design notes and every deviation from the
|
||
colleague's code (`MicrOBU:` in the sources): `obu-firmware/NOTES.md`.
|
||
|
||
Main changes against the colleague's firmware:
|
||
|
||
- **Raw receive path kept.** vanetza-idf decapsulates strictly and would drop unsigned frames (the
|
||
bench car) and anything not signed under the demo root (every RSU). Every captured frame also
|
||
goes through the previous firmware's `gn_unwrap.c` and reaches the phone as link opcode
|
||
`V2X_RX` (0x85), whose body is the old `SERIAL_MSG_V2X_RX` payload.
|
||
- **Unsigned sending kept.** The colleague's station refuses unsecured requests; here they go out
|
||
with the previous firmware's `geonet.c` header.
|
||
- **Console on UART0** (CH343, COM3 on the bench); the native USB port carries only link frames.
|
||
- **BLE pauses advertising while USB is in use** (BLE and ITS-G5 share one RF front end).
|
||
- **NVS 80 KB instead of 24 KB**, app at 0x20000. At 24 KB the BLE bond could not be stored and the
|
||
phone had to pair on every connection.
|
||
- Fixes found on the bench: radio queue drained before the first PoTi (no RX, ~177 queue drops
|
||
before); station loop waited 0 ticks at 100 Hz and starved the idle task; 2.4 KB RX buffer moved
|
||
off the Wi-Fi task stack; no silent truncation of BLE notifications; ATT MTU 517; serial writes
|
||
skipped when no USB host is present.
|
||
|
||
## Link protocol
|
||
|
||
Station-link v1 (colleague's repository, `station-link/README.md`): `[opcode][flags][sequence LE][body]`,
|
||
little-endian, at most 512 octets. Over USB each message is one `0xAA55` frame of type `0x10`
|
||
(the old framing and CRC). Over BLE each message is one GATT value on service
|
||
`0000C175-BA5E-4C17-8000-00805F9B34FB` (the README describes a different, Nordic-UART layout; the
|
||
firmware is what counts).
|
||
|
||
| Direction | Message | Used for |
|
||
|---|---|---|
|
||
| phone → board | `STATION_CONFIGURE` | pseudonym MAC, station type, channel 180, 20 dBm; starts the radio |
|
||
| phone → board | `CREDENTIALS_PROVISION` | the demo bundle, once, when the board reports no ticket |
|
||
| phone → board | `POTI_UPDATE` | position and ITS time, once per new GNSS fix |
|
||
| phone → board | `BTP_DATA_REQUEST` | one CAM (port 2001, psid 36) or VAM (port 2018, psid 638), signed or not |
|
||
| board → phone | `RESULT` | answer to a request |
|
||
| board → phone | `STATUS` | every second: counters, tickets, signed/refused counts |
|
||
| board → phone | `V2X_RX` (0x85, MicrOBU) | every ITS message heard on air |
|
||
|
||
The app side is `Esp32Link.kt` (session), `StationLink.kt` (codec, pinned by unit tests to bytes
|
||
from the colleague's Python implementation), `UsbSerialTransport.kt` and `BleLinkTransport.kt`.
|
||
A board still on the previous firmware is recognised by its old heartbeat and keeps working for
|
||
CAM over USB.
|
||
|
||
## Redundancy and recovery
|
||
|
||
| Situation | What notices | What happens |
|
||
|---|---|---|
|
||
| USB link dead (board hung, cable) | app watchdog: no frame for 3.5 s (the board's `STATUS` comes every second) | link marked ERROR on the card |
|
||
| USB unplugged | Android detach broadcast | port closed; Connect again after re-plugging |
|
||
| BLE link lost | BLE supervision timeout (4 s) | app reconnects by itself: 1 s after a drop, then backing off to 30 s if attempts fail |
|
||
| Board reset / power cycle | first `STATUS` says "not configured" | app reconfigures (and re-provisions if needed) without user action |
|
||
| App closed and reopened | new session | app configures the board again; BLE reconnects with the stored bond, no passkey (confirmed) |
|
||
| Phone clock or GNSS time jumping | app tracks the board's clock | PoTi never moves it backwards (except a real correction of ≥ 60 s), so the board does not restart its stack |
|
||
| Board firmware wedged | ESP task watchdog (30 s, logs on COM3) | the phone sees it as a dead link (above) |
|
||
|
||
## App changes
|
||
|
||
- Settings > Connection > ESP32-C5: **link** USB-C / Bluetooth, **transmit** CAM / VAM, **sign
|
||
outgoing messages** (on by default). The connection card, top bar and dashboard show the link in
|
||
use, the pairing passkey when needed, and signing counters.
|
||
- VAM encoder (`VamUperCodec.kt`, TS 103 300-3 V2.3.1, checked against asn1tools) and the VAM
|
||
generation rules (`VamGenerationRules.kt`).
|
||
- `GnssTimeSource` keeps the last measured phone-clock error while GNSS time drops out indoors. The
|
||
bench phone's clock was 14 minutes fast; falling back to it made every transmitted timestamp
|
||
jump by 14 minutes.
|
||
- Bluetooth permissions (Android 12+) requested at start-up.
|
||
|
||
## Credentials (demo PKI)
|
||
|
||
`app/src/main/assets/demo-chain.vcr`, generated 2026-09-23 with the colleague's `vidf_issue`: root
|
||
`6E7D0374FB021901` → AA `B3312F29844299E0` → AT `B80B49387A4C12EB` (two years; psid 36 SSP `010000`,
|
||
psid 638 SSP `01`). It is throwaway and not EU-registered; its private key ships with the app on
|
||
purpose. The colleague's own demo chain only grants psid 638 and cannot sign CAMs.
|
||
|
||
## Verification
|
||
|
||
- **Unit tests** (103): VAM bytes against asn1tools, station-link messages against the colleague's
|
||
Python encoder, VAM generation rules.
|
||
- **Signatures on air**: `obu-firmware/test/verify_signed_pcap.py` checks a pcap with asn1tools
|
||
and OpenSSL, sharing no code with the firmware. Pinger capture of 2026-09-23: 12/12 signed CAMs,
|
||
psid 36, signer the demo AT, all signatures valid, chain valid.
|
||
- **Third-party stack**: the CiT One receives the signed CAMs (~1 Hz on `v2x/rx/cam`), so its
|
||
stack unwraps our envelope. Its MQTT interface exposes no security information, and it forwards
|
||
unsigned and unknown-root messages alike, so it cannot tell whether it verified the signature.
|
||
- **V2X2MAP (COM10)**: now the colleague's v2x2map 0.3.0 bridge from source with a new
|
||
`verify.py` and `--trust demo-chain.vcr`; it shows "signature verified", "SIGNATURE INVALID" or
|
||
"not verified" per packet. Signed CAMs and signed VAMs verified live; a one-bit change in a
|
||
signed CAM comes out invalid. Launcher: `micrOBU_workspace/v2x-obu-esp32c5/start-v2x2map-signed.bat`.
|
||
|
||
## Open
|
||
|
||
- BLE/ITS-G5 coexistence is not measured: does an active BLE connection cost 5.9 GHz reception?
|
||
- `time_regression` standing still indoors for several minutes, and board reset recovery over BLE,
|
||
after the last fixes.
|
||
- The signature's generationTime follows the app's UTC-based `ItsTime`; the colleague's VBS adds
|
||
the 5 leap seconds (TAI). Which is right is the open question in `ItsTime.kt`.
|
||
- Real EU PKI enrolment/authorisation (TS 102 941) instead of the demo chain.
|