docs/06-signed-its-vam-ble.md: who does what between phone and ESP32-C5 (signing lives on the board), the link protocol, recovery paths (USB heartbeat watchdog, BLE supervision timeout and auto-reconnect, board-reset reconfiguration, app restart), the demo PKI, and what is still open. obu-firmware/test/verify_signed_pcap.py checks the IEEE 1609.2 signatures in a pcap with asn1tools and OpenSSL, independent of the firmware. On a capture of the CAM pinger (2026-09-23) all 12 signed CAMs verify under the demo ticket, whose chain verifies too. The CiT One receives the same CAMs but its MQTT interface exposes no security information, so it cannot confirm the signature itself. The V2X2MAP bridge on COM10 now verifies against the demo chain as well (change in the colleague's repository); signed CAMs and VAMs show as verified. TODO.md: bench checks confirmed so far ticked; open are BLE/ITS-G5 coexistence, time_regression over a longer stationary run, and board reset recovery over BLE.
140 lines
8.8 KiB
Markdown
140 lines
8.8 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` (the colleague's repository, kept beside
|
||
this one, gitignored). vanetza-idf is taken from `microbu-esp32c5/external/vanetza-idf`. 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 (`microbu-esp32c5/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.
|