Status: accepted Date: 2026-09-09 Supersedes parts of: ADR-001 §3 (single-library assumption)
Context
Platform_Protocol_SafeComm implements two ERTMS/ETCS safe-communication profiles that share almost all of their machinery:
- Subset-037 — EuroRadio FIS: EVC ↔ RBC over a mobile bearer.
- Subset-098 — RBC/RBC Safe Communication Interface: RBC ↔ RBC over a fixed network (companion to the Subset-039 handover application logic).
The reference application (RBC_GP) is a 2oo2 redundant RBC: two vital channels A and B cross-compare every decision, and a non-vital role-C Gateway (rte_gateway_c_*) concentrates and routes external traffic. The safe-communication stack has to be placed onto that existing process structure, and every runtime parameter has to come from configuration files, not code or long argv lists (workspace convention: packaging.conf, sim config.json).
Decision
1. Four modules, split on the vital / non-vital boundary
| Module | CMake target | SIL | Runs in | Responsibility |
| SFM — Safe Functional Module | safecomm::safety | vital, SIL 4 | A and B | AU1/AU2/AU3 authentication, KSMAC derivation, 3DES CBC-MAC per PDU (SS-037 §7.3), sequence numbering + replay window, connection-supervision timers, the SaF state machine, the 2oo2 compute-and-cross-check hook |
| CFM — Communication Functional Module | safecomm::cfm | vital context, deterministic interface up to SFM | A and B | bearer connection management (open / close / redial / retry / keep-alive), the SS-037-vs-SS-098 communication-layer differences; presents the SFM a whole-PDU safecomm_transport_t |
| ALE — Application Layer Entity | safecomm::ale | non-vital | Gateway | terminates external bearers, identifies the peer (NID_ENGINE / RBC-ID), looks it up in a peer map, relays the raw byte stream to both A and B, multiplexes many peers |
| config — file loader | safecomm::config | non-vital (values range-checked by consumers) | everywhere | parse one key = value file per node into a struct |
Dependency arrows only ever point non-vital → vital or to nothing:
safecomm::safety (vital, no deps)
safecomm::config (non-vital, no deps)
safecomm::cfm ──▶ safecomm::config (+ RteFramework, installed mode only)
safecomm::ale ──▶ safecomm::config
The vital safecomm::safety depends on nothing — it never links a non-vital library.
2. The SFM (and CFM) terminate inside the vital A/B core
Per direct instruction: SFM and CFM both link into A/B; only the ALE is in the Gateway. The EuroRadio safe session — authentication, session key, MAC chain, sequence state — is therefore held end-to-end in the vital 2oo2 core. The Gateway carries bytes it cannot interpret.
Consequence, accepted: the CFM's socket I/O introduces timing non-determinism. It is quarantined below a discrete interface: the CFM does its own I/O but hands the SFM only whole-PDU "received" / "send" events, drained once per RBC cycle. Bearer jitter never reaches the cross-compared path.
3. The ALE cannot forge an authenticated message
The Gateway ALE never holds KSMAC. A fault in it can drop or misroute a connection — the SFM's supervision timer then expires safe-side — but it cannot inject** a false MA or handover message, because A/B will reject any PDU whose MAC does not verify. This is what keeps the ALE outside the safety case.
4. 2oo2 EuroRadio endpoint rule
A and B both run the full SFM over the same logical connection:
- Both channels derive KSMAC and compute the MAC over the agreed plaintext (already cross-compared as application state).
- Before transmit, the nominated transmitter channel (config ab.transmitter) sends; the other compares its own computed frame byte-for-byte and raises a discrepancy if they differ.
- On receive, both channels verify the MAC independently; a mismatch on either drops the connection to IDLE (safe side).
5. Configuration is files, one per node
safecomm_config_load("<path>") reads a flat key = value file (# comments, inline # ... comments, whitespace-trimmed, no sections — keys are group.name). The loader does syntax only; every component range-checks the values it consumes at init and refuses to start on a bad one (safe). Sample files: etc/safecomm.A.conf.example, etc/safecomm.gateway.conf.example, etc/peers.map.example. KMAC is never inline — keys.kmac_file points at a mode-0400 file of 48 hex characters.
The stack
┌──────────────────────────────────────────────────────────────┐
VITAL │ ERTMS/ETCS application (RBC decision logic, cross-compared) │ A + B
SIL4 │ SS-026 messages (EVC) / SS-039 handover messages (RBC) │
├──────────────────────────────────────────────────────────────┤
VITAL │ SFM safecomm::safety │ A + B
SIL4 │ AU1/AU2/AU3 · KSMAC · 3DES CBC-MAC · seq/replay · │
│ supervision timers · SaF state machine · 2oo2 hook │
├──────────────────────────────────────────────────────────────┤
vital │ CFM safecomm::cfm (discrete whole-PDU interface upward) │ A + B
ctx │ bearer connect/redial/retry/keep-alive · SS-037 vs SS-098 │
│ comms-layer rules │
└───────────────┬──────────────────────────────────────────────┘
┌───────────────┴──────────────────────────────────────────────┐
NON- │ ALE safecomm::ale │ Gateway
VITAL │ external listener · peer id (NID_ENGINE / RBC-ID) · │
│ peers.map lookup · raw relay to BOTH A and B · mux │
├──────────────────────────────────────────────────────────────┤
│ bearer: SS-037 → GSM-R / PS / FRMCS (out of scope, stubbed) │
│ SS-098 → fixed TCP to the peer RBC │
└──────────────────────────────────────────────────────────────┘
config: safecomm::config — one file per node, feeds SFM+CFM (A/B) and ALE (Gateway)
A consumer-supplied bearer the SaF layer runs over.
Deployment onto the 2oo2 processes (one site shown)
EVC / neighbour RBC
│ bearer (GSM-R / FRMCS / fixed TCP)
▼
┌───────────────────┐
│ role-C GATEWAY │ rte_gateway_c_* NON-VITAL
│ ┌─────────────┐ │ links: safecomm::ale, safecomm::config
│ │ ALE │ │ config: safecomm.gateway.conf + peers.map
│ └──────┬──────┘ │
└────┬────────┬─────┘ raw MAC'd frames, fanned out
│ │
┌─────▼────┐ ┌─▼────────┐
│ RBC A │ │ RBC B │ rte_example VITAL 2oo2
│ ┌──────┐ │ │ ┌──────┐ │ links: safecomm::safety,
│ │ CFM │ │ │ │ CFM │ │ safecomm::cfm, safecomm::config
│ ├──────┤ │ │ ├──────┤ │ config: safecomm.A.conf / safecomm.B.conf
│ │ SFM │◀┼─┼▶│ SFM │ │ ── compute cross-check (both MAC, one sends)
│ ├──────┤ │ │ ├──────┤ │
│ │ app │◀┼─┼▶│ app │ │ ── existing 2oo2 cross-compare + checkpoint
│ └──────┘ │ │ └──────┘ │
└──────────┘ └──────────┘
SS-037 connect + first Sa-DATA (EVC ↔ RBC)
EVC GW/ALE A.CFM/SFM B.CFM/SFM
│ bearer conn │ │ │
│──────────────▶│ identify NID_ENGINE │ │
│ │ peers.map lookup │ │
│ │────── relay ──────────▶│ │
│ │────── relay ───────────┼───────────────────▶│
│ AU1 (RA) ────┼───────────────────────▶│ (both channels) │
│ │ │ verify, pick RB │
│◀── AU2 (RB, MAC) ──── (transmitter A) ─┤ B computes same, │
│ │ │ compares frame │
│ AU3 (MAC) ───┼───────────────────────▶│ both verify │
│ │ │ KSMAC ready → DATA │
│ Sa-DATA(seq,MAC,payload) ────────────▶│ both verify MAC │
│ │ │ + seq window │
│ │ │ → app (SS-026) │
A supervision-timer expiry, a MAC mismatch, or an A/B frame discrepancy at any step drops the connection to IDLE — safe side.
SS-098 connect (RBC ↔ neighbour RBC)
Identical SFM handshake; differences are all in the CFM and ALE:
RBC-hand-over RBC-accepting
A.CFM ── fixed TCP ────▶ ALE(GW) ── relay ──▶ A.SFM / B.SFM
cfm.profile = rbc_rbc ale.profile = rbc_rbc
peer id = RBC-ID peer map keyed by RBC-ID
no bearer dial; SS-039 handover messages ride as SaF user data
connection supervision only
Consequences
- The vital footprint is safecomm::safety (+ the deterministic slice of safecomm::cfm); it links no non-vital code and is unit-testable with no network and no framework checkout.
- EuroRadio session state (seq numbers, KSMAC, MAC chain) becomes vital state that the existing checkpoint / cross-compare machinery must carry — a real addition to the 2oo2 safety case, called out here so it is planned, not discovered.
- The Gateway stays non-vital: it is a byte relay for frames it cannot read.
- Config-driven: standing up A, B or the Gateway is an etc/*.conf edit, no rebuild.
- Bearer realism (GSM-R call drop / re-establishment) is still deferred; the CFM's safecomm_transport_t seam is where a real bearer later slots in without touching the SFM.