Repository navigation
cell M6: UE keys for RACH-joined UEs — a joiner hands over under per-cell keys - #541
Conversation
…keys A joiner has no home (cell, UE) identity, so under per-cell keys (#540) the CU could not wrap a handover context for it and it never handed over. Its UE key is now bound to the MAC it joins with: K_j = HMAC(master, 'J' mac) (Keys.h joiner_key), provisioned out of band as a SIM's (CELL_UE_KEY). A DU reports the UEs it admitted through the RACH with their joiner MACs (F1 v10 Report.joined; static UEs and handover arrivals hold the cell's synthetic MAC and are not listed); on a joiner's first handover the CU takes its home from that report (ran.ho_cmd home J:<mac>) and home_of carries it on. A UE that joins claiming another's MAC only gets contexts it cannot unwrap. Rig (testrig scripts/m6_joinerkeys.sh): the joiner hands over 2/2 (context for its MAC, target in slot 99.9 %); holding another MAC's key it refuses every command and stays 2/2. Static per-cell keys and the re-join after a DU restart pass on F1 v10. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR Summary by QodoEnable per-cell-key handover for RACH-joined UEs
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
Code Review by Qodo
1.
|
…over command; one F1 datagram bound The CU inferred homes (an in-memory map lost on a CU restart and stale on an id reused for another UE; MAC equality, which cannot tell provenance). Now every DU records a UE's home WHEN it admits it — static (cell, UE), a RACH joiner's authenticated MAC, or the home its handover command carries (F1 v10 HoCommand::home, Keys.h HomeId) — and reports the homes of exactly the UEs a report names, under the same lock (Report.homes, replacing Report.joined). The CU keeps none of it: each context is wrapped for the home in the SOURCE DU's current report; no home, no context (never a guessed static key). UeRegistry is back as on master. F1 receive buffers on both sides are now kMaxDatagram (8192): a report at every list bound is 3673 bytes, which the 2048-byte buffers could already not hold before this PR; the cell_m6 ctest encodes one. Rig: joiner handover 2/2, another MAC's key refused 2/2, and a crossing there and back with the CU killed and restarted between the handovers: the restarted CU wraps the return for the joiner. Static per-cell keys, the re-join, and a shared-key handover pass. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Qodo review addressed in 1b3e387 by a design change: homes are now the DUs' record, not inferred by the CU.
Also on the rig: joiner handover 2/2, another MAC's key refused 2/2; static per-cell keys (handover, forge, isolate, coordination), the re-join under per-cell keys, and a shared-key handover pass. ctest 95/95. |
|
CI green; all five Qodo findings resolved. The remaining item is the high-level alternative (a join-time identity event to the CU), for which Qodo recommends the report-based approach used here (periodic DU state recovers identities without a separate event lifecycle). Merging with skip-qodo-gate. |
… own key (#542) M6 (#260, EPIC #254): per-UE area credentials. A UE beacon's area tag is now keyed by the UE's own key, which narrows the area-key exposure item from #540. ## Why Under per-cell keys (#540) the area key tagged two things: the neighbour list, and every UE beacon's area tag, which neighbour DUs check before counting a UE as heard or forwarding it for DAPS. Every UE holds the area key, so any UE could make another UE count as heard at a neighbour DU, or get forged uplinks forwarded for DAPS. ## What - **`Keys.h`:** - `ue_area_key(ue_mac)`: `A_u = hex(HMAC(K_u, 'V'))`, derived from the UE key the UE already holds (no new provisioning); - `wrap_area_key(du_mac, cell, ue, nonce, key)`: `key ⊕ HMAC(K_du, 'W' cell ue nonce)`, with a fresh nonce per wrap and its own inverse. - **F1 v11 `AreaKeys` (type 8):** CU → DU, the other cells' UEs' area keys, each wrapped under the receiving DU's cell key, since F1 is authenticated but not encrypted. It replaces the DU's table; `seq` gives freshness. It is bounded at 192 entries (`kMaxAreaKeys`), which fits `kMaxDatagram`. - **CU:** each plan round, for every fresh DU, derives `A_u` for every UE of the other fresh DUs from the homes they report (#541), wraps and sends. - **DU:** unwraps with its cell key into a (cell, UE) → key table. Under per-cell keys a heard UE beacon counts, as heard and for DAPS forwarding, only if its area tag verifies with *that UE's* key. One the DU has no key for isn't counted. - **UE:** under per-cell keys it tags its beacon's area tag with `A_u`. - **The area key keeps** only the neighbour list (what a UE checks of another cell). That residual is documented: measurement provenance only, no longer heard or DAPS presence. - **Selftests (cell_m6):** per-UE keys differ; the wrap is its own inverse, a fresh nonce gives a fresh pad, another DU's key fails; AreaKeys round-trips, is refused when forged, and a full table fits the datagram (one over the bound is refused). - **Docs:** `docs/cell-m6.md`. ## Rig (testrig `scripts/m6_ueareakeys.sh`, per-cell keys, co-channel) | case | runs | result | |---|---|---| | each DU counts the other cell's UE as heard (plan edges 1/1, 2/1) from per-UE tags | 2 | both edges; `ran_coord_analyze` pass | | UE1 holds the area key but a wrong UE key (wrong `A_u`) | 2 | DU2 never counts it as heard (no 1/1; 2/1 stays); DU1 still serves it, in slot 99.7–99.8 % | **Regression under per-cell keys:** - DAPS, now forwarded only by the UE's own area key: 0 vs 15 missing; - source failure mid-crossing: 0 vs 10 missing in the window; - joiner handover, re-join and a shared-key handover pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
M6 (#260, EPIC #254): UE keys for RACH-joined UEs, so a joiner can hand over under per-cell keys. This closes the open item from #540.
Why
Under per-cell keys (#540), the CU wraps each handover's context for the UE's key, which is derived from its home (cell, UE) identity. A joiner has no such identity, since its id comes from the join. So the CU couldn't wrap a context for it, and a joiner never handed over.
What
Keys.hjoiner_key(master, mac):K_j = hex(HMAC(master, 'J' mac(6))). A joiner's UE key is bound to the MAC it joins with (CELL_JOINER_MAC, or derived from host and radio) and provisioned out of band like a SIM's (CELL_UE_KEY).Report.joined: the UEs a DU admitted through the RACH, with their joiner MACs (UeRegistry::owners()). Static UEs and handover arrivals hold the cell's synthetic MAC and aren't listed. The DU takes the snapshot undermubefore the report takesf1_mu, so the two locks never nest.K_j(ran.ho_cmdhomeJ:<mac>).home_ofcarries it to later handovers.joiner_keyis deterministic and distinct from other joiners and from static keys;Report.joinedround-trips and is bounded;UeRegistry::owners.docs/cell-m6.mdcovers the joiner key and the rig result, and the open item is removed.Rig (testrig
scripts/m6_joinerkeys.sh, per-cell keys)UE1 joins cell 1 through the RACH with a fixed joiner MAC and that MAC's
K_j, and is handed over by the A3 at a −2 dB offset.J:02cefeaabb01); switch, target in slot 99.9 %,ran.ho_doneRegression on F1 v10: the static per-cell key set (handover, forge, isolate, coordination) and the re-join after a DU restart under per-cell keys both pass.
🤖 Generated with Claude Code