From 9342fb46e16066100312f4198519c396702fc235 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:27:52 +0300 Subject: [PATCH] M6: grant-map authentication (GM IE v2, the cell key's tag) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grant map IE was the one scheduling IE without a tag: a forged map could move a UE's slots and, since #536, reset its grant state through the restart path. GM v2 carries the first 8 bytes of mac('G' || body..crc) with the cell key (CELL_PSK), as the NL/MR/handover IEs do; encode/decode/find take the Mac (mandatory: every caller decides). With a key a forged or untagged map is refused before any offer; without one (an unauthenticated development cell, a passive witness) only the structure is checked; v1 is refused. Mac moves from Rach.h to GrantMap.h. The UE counts serving maps taken and refused (cell.ue.done gm_ok/gm_refused, cell.ue.gm_refused). docs: the M5/M6 rig results before this ran without CELL_PSK (zero tags, unchecked; F1 was keyed) — stated, and the suite re-run keyed. Rig (testrig, every cell role on one CELL_PSK): m6_gmauth.sh 2/2 — UE1 586-587 maps taken, 0 refused, in slot 99.8 %; UE2 under another key 0 taken, 586-587 refused, never in slot. The M6 suite keyed: handover 2+1, inter-frequency 2, DAPS 2+2 (0/0 missing vs 11/16), measurement gaps 2, phase planning 2, clock fit + restart 1+1 — all pass, no refusal at any keyed UE. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/cell-m6.md | 61 ++++++++++++++++++++++------ examples/cell/cell.h | 6 +-- examples/cell/main.cpp | 37 +++++++++++++---- src/cell/GrantMap.h | 79 ++++++++++++++++++++++++++++-------- src/cell/Rach.h | 3 +- tests/cell_node_selftest.cpp | 7 ++-- tests/cell_selftest.cpp | 60 ++++++++++++++++++++------- 7 files changed, 192 insertions(+), 61 deletions(-) diff --git a/docs/cell-m6.md b/docs/cell-m6.md index e4a82f78..6fb2a9cc 100644 --- a/docs/cell-m6.md +++ b/docs/cell-m6.md @@ -411,13 +411,12 @@ heard in the new grid. instance, as on a handover switch. That is the DL HARQ sequence space, the MR generation (the DU checks it against its own new count), the neighbour list and gap parking, and any handover IE the old instance aired. -- **The exposure:** grant maps carry no tag (they never did), so the epoch - rule orders maps; it does not authenticate them. A forged map could - already redirect a UE's slots. The restart path widens that: within the - re-attach window after a clock step, a forged lower-epoch map due near the - beacon's count also resets the UE's per-DU state (HARQ, MR generation, - neighbours, pending handover IEs). Grant-map authentication is open - (below). +- **Authentication:** grant maps carry the cell key's tag (v2, + `src/cell/GrantMap.h`), so with `CELL_PSK` a forged map is refused before + either rule sees it. It can neither move a UE's slots nor, through the + restart path, reset its per-DU state. What remains is a replayed genuine + map. A pre-restart one is due far from the new count and refused; one + from the new instance is that DU's own. **Rig result** (testrig `scripts/m6_fitstep.sh`, restart case, 3 runs, all pass). @@ -447,6 +446,40 @@ pass). position showed as DL out of slot (0.05 in slot); the frames themselves were right. It now uses the grant in force. +## Grant-map authentication, and the suite keyed + +The grant map IE (v2, `src/cell/GrantMap.h`) carries the cell key's tag +after its CRC, as the NL, MR and handover IEs do. +- **With `CELL_PSK`:** a UE takes only authentic maps. A forged map neither + moves its slots nor, through the restart path, resets its grant state. +- **Without a key:** an unauthenticated development cell, or a passive + witness, checks the structure only. +- **v1 maps** (untagged) are refused. +- **Counters:** the UE counts serving maps taken and refused (`gm_ok`, + `gm_refused` in `cell.ue.done`; the first refusal logs + `cell.ue.gm_refused`). + +**On air** (testrig `scripts/m6_gmauth.sh`, 2 runs, pass). Every role holds +the cell key except UE2, which holds another. To UE2 each genuine map is a +map under the wrong key, as a forged one is to a keyed UE. + +| | maps taken | maps refused | heard in slot by its DU | +|---|---|---|---| +| UE1 (cell key) | 586–587 | 0 | 99.8 % | +| UE2 (another key) | 0 | 586–587 | never | + +**The M6 suite re-run keyed** (all roles on one `CELL_PSK`; all pass). No +keyed UE refused a map, apart from UE2 above. + +| test | runs | result | +|---|---|---| +| same-channel handover + fallback | 2 + 1 | interruption 1 sf; fallback 10 sf | +| inter-frequency handover | 2 | interruption 2–3 sf, retune 23.6 ms | +| DAPS (source killed mid-crossing) | 2 + 2 | missing uplinks 0 / 0, against 11 / 16 without | +| measurement-gap handover | 2 | deaf 54.1 ms, A3 fired at defaults | +| phase planning | 2 | DU2 at 51200, both UEs 57 gaps heard | +| clock-fit jump + DU restart | 1 + 1 | restart followed and re-attached; transient ignored | + ## DAPS: the duplicated uplink (same channel) **Common sequence.** A UE uplinks once per superframe. Every DU that hears @@ -547,10 +580,6 @@ superframe when there is nothing to serve. running ones, which never move. With three or more cells on different channels, the 26 ms retune leaves no layout where every pair can measure both ways. Re-phasing live cells, or longer superframes, is open. -- **Grant-map authentication:** the grant map IE has no tag, unlike the - NL, MR and handover IEs. A tag keyed per cell (as `CELL_PSK`), checked - before any `offer`, would close the forged-map exposure, including the - restart path. It is a wire change for the DU, UE and witness. - **A DU restart under RACH-joined UEs:** re-attachment (above) is for the rig's static UEs, whose id the restarted DU grants again. A UE that joined through the RACH holds an id the new DU instance does not know; re-joining @@ -560,6 +589,12 @@ superframe when there is nothing to serve. two cells. No A→B path-loss ramp has been run, so none of these numbers validates a crossing. An emulated crossing (the source DU ramping its TX power, which its beacons follow) is the way to get one. -- **Security context:** the cells share one key on the rig (`CELL_PSK`). - Pre-staging a per-cell context (802.11r-style) is open. +- **Security context:** the cells share one key (`CELL_PSK`). Pre-staging a + per-cell context (802.11r-style) is open. +- **Keyed rig runs only from the grant-map authentication on:** the + handover, DAPS, inter-frequency, measurement-gap, phase-planning and + clock-fit rig results above were run without `CELL_PSK`, an + unauthenticated development cell. Their tags were zeros and were not + checked; F1 was keyed throughout. The testrig scripts now give every cell + role one key. The suite re-run keyed is in the next section. - **EVM:** not in the measurement reports. diff --git a/examples/cell/cell.h b/examples/cell/cell.h index ef509098..569cd972 100644 --- a/examples/cell/cell.h +++ b/examples/cell/cell.h @@ -98,16 +98,16 @@ inline std::vector beacon_head(const uint8_t sa[6], int interval_tu) { } } // namespace detail -// The DU beacon: header + the grant map IE (+ the RACH responses IE when +// The DU beacon: header + the grant map IE (tagged with `mac`, the cell key) (+ the RACH responses IE when // `rach_rsp` — an encode_rach_responses body — has any entry). Empty if // either cannot encode. -inline std::vector build_du_beacon(const GrantMap &m, +inline std::vector build_du_beacon(const GrantMap &m, const devourer::cell::Mac &mac, const std::vector &rach_rsp = {}, const std::vector &extra_ies = {}) { uint8_t sa[6]; cell_mac(sa, m.cell_id, 0); auto f = detail::beacon_head(sa, m.period_tu); - const auto ie = devourer::cell::encode_grant_ie(m); + const auto ie = devourer::cell::encode_grant_ie(m, mac); /* tagged with the cell key */ if (ie.empty()) return {}; f.insert(f.end(), ie.begin(), ie.end()); diff --git a/examples/cell/main.cpp b/examples/cell/main.cpp index fb087db7..389df1e6 100644 --- a/examples/cell/main.cpp +++ b/examples/cell/main.cpp @@ -451,7 +451,7 @@ bool du_budget_ok(const Cfg &c, const std::vector &ues) { rar[1] = 'B'; rar[2] = static_cast(devourer::cell::kRachMaxPerBeacon); } - const int64_t bcn_need = 22 + ofdm_airtime_us(build_du_beacon(probe, rar).size() + 4, 6); + const int64_t bcn_need = 22 + ofdm_airtime_us(build_du_beacon(probe, cell_auth(c), rar).size() + 4, 6); SlotBudget dl; dl.mpdu_len = build_dl_frame(c.cell, 1, 0, 0, pad).size() + 4; dl.acked = true; @@ -773,7 +773,7 @@ int run_du(IRadio *dev, const Cfg &c) { } emit(l); } - auto bcn = build_du_beacon(map); + auto bcn = build_du_beacon(map, auth); if (bcn.empty() || !dev->StartBeacon(bcn.data(), bcn.size(), c.tu)) { fprintf(stderr, "StartBeacon failed\n"); return 1; @@ -2017,7 +2017,7 @@ int run_du(IRadio *dev, const Cfg &c) { : ho_eff ? *ho_eff : static_cast(sf + 3); } - const auto nb = build_du_beacon(next, rar_now, ho_ies); + const auto nb = build_du_beacon(next, auth, rar_now, ho_ies); const bool ok = !nb.empty() && dev->UpdateBeaconPayload(nb.data(), nb.size()); beacon_fail_run = ok ? 0 : beacon_fail_run + 1; if (ok) { @@ -2213,6 +2213,9 @@ int run_ue(IRadio *dev, const Cfg &c) { double dl_snr_sum = 0; uint64_t dl_snr_n = 0; uint64_t dl_new = 0, dl_dup = 0; + /* Serving beacons whose grant map is intact but not authentic (another + * key, or none): refused (mu). */ + uint64_t gm_ok = 0, gm_refused = 0; const int64_t P = static_cast(c.tu) * devourer::cell::kTuUs; /* The capacity grid (same env as the DU): positions never move. */ const std::optional grid = grid_for(c); @@ -2337,7 +2340,7 @@ int run_ue(IRadio *dev, const Cfg &c) { * switch (they do not feed the clock: that is the source's until * the switch, then shifted, not re-acquired). */ if (rxr_target && std::memcmp(p.Data.data() + 10, tgt_du_mac, 6) == 0) - if (auto m = devourer::cell::find_grant_map(p.Data.data(), p.Data.size())) + if (auto m = devourer::cell::find_grant_map(p.Data.data(), p.Data.size(), auth)) rxr_target->offer(*m); return; } @@ -2364,7 +2367,18 @@ int run_ue(IRadio *dev, const Cfg &c) { reattach_armed_steps = sync.Steps(); reattach_until_ms = ue_ms() + reattach_sf * P / 1000; } - if (auto m = devourer::cell::find_grant_map(p.Data.data(), p.Data.size())) { + auto m = devourer::cell::find_grant_map(p.Data.data(), p.Data.size(), auth); + if (m) { + ++gm_ok; + } else if (auth && devourer::cell::find_grant_map(p.Data.data(), p.Data.size(), {})) { + if (gm_refused++ == 0) { + char l[120]; + snprintf(l, sizeof l, "{\"ev\":\"cell.ue.gm_refused\",\"ue\":%d,\"cell\":%u}", + me_a.load(), cell_a.load()); + emit(l); + } + } + if (m) { const uint32_t old_epoch = rxr.newest_epoch(); if (!rxr.offer(*m) && ue_ms() < reattach_until_ms && rxr.offer_after_restart(*m, static_cast(*eg) / static_cast(P))) { @@ -3043,18 +3057,23 @@ int run_ue(IRadio *dev, const Cfg &c) { dev->StopRxLoop(); rx.join(); dev->ClearAckResponder(); - char l[200]; + char l[300]; snprintf(l, sizeof l, "{\"ev\":\"cell.ue.done\",\"ue\":%u,\"steps\":%ld,\"step_fails\":%ld," - "\"listen_from_sf\":%lld,\"listen_to_sf\":%lld,\"dl_new\":%llu,\"dl_dup\":%llu}", + "\"listen_from_sf\":%lld,\"listen_to_sf\":%lld,\"dl_new\":%llu,\"dl_dup\":%llu," + "\"keyed\":%d,\"gm_ok\":%llu,\"gm_refused\":%llu}", me_a.load(), steps, step_fails, (long long)listen_from, (long long)listen_to, - (unsigned long long)dl_new, (unsigned long long)dl_dup); + (unsigned long long)dl_new, (unsigned long long)dl_dup, auth ? 1 : 0, + (unsigned long long)gm_ok, (unsigned long long)gm_refused); emit(l); return 0; } // --------------------------------------------------------------------------- int run_witness(IRadio *dev, const Cfg &c) { + /* With CELL_PSK, a beacon's grant map counts only if authentic; without + * one the witness is a passive recorder (structure only). */ + const CellMac wauth = cell_auth(c); /* CELL_WITNESS_CELLS [CELL_ID]: the cells to record (M5: two co-channel * cells on one receiver clock); each line names its cell. */ std::vector cells; @@ -3091,7 +3110,7 @@ int run_witness(IRadio *dev, const Cfg &c) { if (st > 0) { if (auto ub = parse_ue_beacon(d, p.Data.size(), cell)) ep = ub->epoch; - } else if (auto m = devourer::cell::find_grant_map(d, p.Data.size())) { + } else if (auto m = devourer::cell::find_grant_map(d, p.Data.size(), wauth)) { ep = m->epoch; } snprintf(l, sizeof l, diff --git a/src/cell/GrantMap.h b/src/cell/GrantMap.h index 7b544bd5..62895e5e 100644 --- a/src/cell/GrantMap.h +++ b/src/cell/GrantMap.h @@ -10,18 +10,25 @@ * (decode does not; that is UE state, see GrantMapReceiver). * * Wire format (little-endian), inside IE 221 (vendor) with OUI kGrantOui: - * 'G' 'M' ver(1)=1 flags(1) cell_id(2) epoch(4) effective_sf(8) + * 'G' 'M' ver(1)=2 flags(1) cell_id(2) epoch(4) effective_sf(8) * period_tu(2) n(1) n x {ue(2) dl_slot(1) ul_slot(1) rate_mbps(1) - * tx_pwr_qdb(1, signed) ta_us(2, signed)} crc16(2) + * tx_pwr_qdb(1, signed) ta_us(2, signed)} crc16(2) tag(8) * The CRC (CCITT-FALSE, over 'G' through the last grant) detects a torn or - * corrupted body from one frame. It is not authentication: integrity against - * an active attacker (EPIC #254's envelope item) is out of M1's scope. + * corrupted body from one frame. The tag (v2, M6 #260) is the cell key's: + * the first 8 bytes of mac('G' || 'G' 'M' .. crc), as the scheduling IEs' + * (NL, MR, handover): a receiver with the key refuses a map that is not the + * DU's, so a forged map neither moves a UE's slots nor, after a DU restart, + * resets its grant state. Without a key (an unauthenticated development + * cell, a passive witness) the tag is zeros and only the structure is + * checked. Replay of a genuine map stays the epoch's job (GrantMapReceiver). * Pure header; cell_selftest covers encode/decode round trips and rejects. */ #ifndef DEVOURER_CELL_GRANTMAP_H #define DEVOURER_CELL_GRANTMAP_H #include #include +#include +#include #include #include @@ -30,8 +37,14 @@ namespace devourer::cell { constexpr uint8_t kGrantOui[3] = {0x57, 0x42, 0x75}; /* devourer test OUI */ -constexpr uint8_t kGrantVersion = 1; -constexpr size_t kGrantMax = kMaxUesPerCell; /* 3+21+24*8+2 = 218 <= 255 IE bytes */ +/* v2: the cell key's tag after the CRC (M6, #260). */ +constexpr uint8_t kGrantVersion = 2; +constexpr size_t kGrantTagLen = 8; +constexpr size_t kGrantMax = kMaxUesPerCell; /* 3+21+24*8+2+8 = 226 <= 255 IE bytes */ + +/* HMAC-SHA1 keyed by the cell PSK: data -> 20-byte tag. False on failure. + * Empty = an unauthenticated cell: the tag is zeros and is not checked. */ +using Mac = std::function; /* GrantMap::flags. kGrantAutonomous (M4, #258): the DU has suspended its * COORDINATED schedule (it lost sync with the fabric, see src/cell/SyncHealth.h). * Its UEs fall back to carrier sense and a random uplink-beacon backoff, as @@ -88,10 +101,26 @@ inline uint64_t get(const uint8_t *p, int n) { } constexpr size_t kHdrLen = 2 + 1 + 1 + 2 + 4 + 8 + 2 + 1; /* 'G'..n */ constexpr size_t kGrantLen = 2 + 1 + 1 + 1 + 1 + 2; +/* The first kGrantTagLen bytes of mac('G' || body), body = everything up to + * and including the CRC; zeros without a key. */ +inline bool grant_tag(const Mac &mac, const uint8_t *body, size_t n, uint8_t out[kGrantTagLen]) { + if (!mac) { + std::memset(out, 0, kGrantTagLen); + return true; + } + std::vector m = {'G'}; + m.insert(m.end(), body, body + n); + uint8_t full[20]; + if (!mac(m.data(), m.size(), full)) + return false; + std::memcpy(out, full, kGrantTagLen); + return true; +} } /* namespace detail */ -/* The full IE (221, len, OUI, body). Empty when there are > kGrantMax grants. */ -inline std::vector encode_grant_ie(const GrantMap &m) { +/* The full IE (221, len, OUI, body, crc, tag). Empty when there are > + * kGrantMax grants or the Mac fails. */ +inline std::vector encode_grant_ie(const GrantMap &m, const Mac &mac) { if (m.grants.size() > kGrantMax) return {}; std::vector body; @@ -113,24 +142,29 @@ inline std::vector encode_grant_ie(const GrantMap &m) { detail::put(body, static_cast(g.ta_us), 2); } detail::put(body, detail::crc16_ccitt(body.data(), body.size()), 2); + uint8_t tag[kGrantTagLen]; + if (!detail::grant_tag(mac, body.data(), body.size(), tag)) + return {}; + body.insert(body.end(), tag, tag + kGrantTagLen); std::vector ie = {221, static_cast(3 + body.size()), kGrantOui[0], kGrantOui[1], kGrantOui[2]}; ie.insert(ie.end(), body.begin(), body.end()); return ie; } -/* Decode one IE (starting at its 221 tag). nullopt on anything but an intact - * version-1 grant map: wrong tag/OUI/magic/version, a length that disagrees - * with n, or a CRC mismatch. */ -inline std::optional decode_grant_ie(const uint8_t *ie, size_t len) { +/* Decode one IE (starting at its 221 tag). nullopt on anything but an intact, + * authentic grant map: wrong tag/OUI/magic/version, a length that disagrees + * with n, a CRC mismatch, or (with a key) a tag mismatch — a forged map is + * inert. */ +inline std::optional decode_grant_ie(const uint8_t *ie, size_t len, const Mac &mac) { if (ie == nullptr || len < 2 || ie[0] != 221 || static_cast(ie[1]) + 2 > len) return std::nullopt; const size_t ie_len = ie[1]; - if (ie_len < 3 + detail::kHdrLen + 2 || ie[2] != kGrantOui[0] || + if (ie_len < 3 + detail::kHdrLen + 2 + kGrantTagLen || ie[2] != kGrantOui[0] || ie[3] != kGrantOui[1] || ie[4] != kGrantOui[2]) return std::nullopt; const uint8_t *b = ie + 5; - const size_t blen = ie_len - 3; + const size_t blen = ie_len - 3 - kGrantTagLen; /* up to and including the CRC */ if (b[0] != 'G' || b[1] != 'M' || b[2] != kGrantVersion) return std::nullopt; const size_t n = b[detail::kHdrLen - 1]; @@ -138,6 +172,16 @@ inline std::optional decode_grant_ie(const uint8_t *ie, size_t len) { return std::nullopt; if (detail::get(b + blen - 2, 2) != detail::crc16_ccitt(b, blen - 2)) return std::nullopt; + if (mac) { + uint8_t want[kGrantTagLen]; + if (!detail::grant_tag(mac, b, blen, want)) + return std::nullopt; + uint8_t d = 0; + for (size_t i = 0; i < kGrantTagLen; ++i) + d |= static_cast(want[i] ^ b[blen + i]); + if (d != 0) + return std::nullopt; + } GrantMap m; m.flags = b[3]; m.cell_id = static_cast(detail::get(b + 4, 2)); @@ -159,8 +203,9 @@ inline std::optional decode_grant_ie(const uint8_t *ie, size_t len) { } /* Find and decode the grant IE in a beacon MPDU (24-byte header, 12 fixed - * bytes, then IEs). nullopt when the beacon carries none or it is damaged. */ -inline std::optional find_grant_map(const uint8_t *mpdu, size_t len) { + * bytes, then IEs). nullopt when the beacon carries none, it is damaged, or + * (with a key) it is not authentic. */ +inline std::optional find_grant_map(const uint8_t *mpdu, size_t len, const Mac &mac) { if (mpdu == nullptr || len < 36 || mpdu[0] != 0x80) return std::nullopt; size_t off = 36; @@ -171,7 +216,7 @@ inline std::optional find_grant_map(const uint8_t *mpdu, size_t len) { if (mpdu[off] == 221 && l >= 5 && mpdu[off + 2] == kGrantOui[0] && mpdu[off + 3] == kGrantOui[1] && mpdu[off + 4] == kGrantOui[2] && mpdu[off + 5] == 'G' && mpdu[off + 6] == 'M') - return decode_grant_ie(mpdu + off, len - off); + return decode_grant_ie(mpdu + off, len - off, mac); off += 2 + l; } return std::nullopt; diff --git a/src/cell/Rach.h b/src/cell/Rach.h index 2b207b5d..0131106a 100644 --- a/src/cell/Rach.h +++ b/src/cell/Rach.h @@ -52,8 +52,7 @@ namespace devourer::cell { -/* HMAC-SHA1 keyed by the cell PSK: data -> 20-byte tag. False on failure. */ -using Mac = std::function; +/* Mac: cell/GrantMap.h (HMAC-SHA1 keyed by the cell PSK). */ constexpr size_t kRachTagLen = 8; /* msgA envelope version: an unknown version is refused before anything else diff --git a/tests/cell_node_selftest.cpp b/tests/cell_node_selftest.cpp index 7aee746d..94b2b7f9 100644 --- a/tests/cell_node_selftest.cpp +++ b/tests/cell_node_selftest.cpp @@ -38,14 +38,15 @@ int main() { g.epoch = 9; g.period_tu = 100; g.grants = {{1, 1, 4, 6, 0, -3}, {2, 2, 5, 6, 0, 7}}; - const auto b = build_du_beacon(g); + const devourer::cell::Mac none; + const auto b = build_du_beacon(g, none); CHECK(!b.empty() && b[0] == 0x80 && cell_station(b.data() + 10, 5) == 0); CHECK(b[32] == 100 && b[33] == 0); /* beacon interval */ - auto d = devourer::cell::find_grant_map(b.data(), b.size()); + auto d = devourer::cell::find_grant_map(b.data(), b.size(), none); CHECK(d.has_value() && *d == g); GrantMap big = g; big.grants.resize(devourer::cell::kGrantMax + 1); - CHECK(build_du_beacon(big).empty()); + CHECK(build_du_beacon(big, none).empty()); } /* UE beacon round trip and rejects. */ diff --git a/tests/cell_selftest.cpp b/tests/cell_selftest.cpp index 98f5397c..8d4b1bf3 100644 --- a/tests/cell_selftest.cpp +++ b/tests/cell_selftest.cpp @@ -137,9 +137,23 @@ int main() { m.effective_sf = 0x0102030405060708ull; m.period_tu = 100; m.grants = {{1, 1, 4, 6, -8, -12}, {2, 2, 5, 24, 4, 300}}; - const auto ie = encode_grant_ie(m); - CHECK(ie.size() == 2 + 3 + 21 + 2 * 8 + 2); - auto d = decode_grant_ie(ie.data(), ie.size()); + /* A keyed stand-in for HMAC-SHA1 (NOT cryptographic). */ + auto key = [](uint64_t k) -> Mac { + return [k](const uint8_t *dd, size_t n, uint8_t out[20]) { + uint64_t h = 1469598103934665603ull ^ k; + for (size_t i = 0; i < n; ++i) + h = (h ^ dd[i]) * 1099511628211ull; + for (int i = 0; i < 20; ++i) { + h = (h ^ static_cast(i)) * 1099511628211ull; + out[i] = static_cast(h >> 32); + } + return true; + }; + }; + const Mac k = key(1), other = key(2), none; + const auto ie = encode_grant_ie(m, k); + CHECK(ie.size() == 2 + 3 + 21 + 2 * 8 + 2 + kGrantTagLen); + auto d = decode_grant_ie(ie.data(), ie.size(), k); CHECK(d.has_value() && *d == m); if (d) { CHECK(d->find(2) && d->find(2)->ta_us == 300); @@ -152,30 +166,48 @@ int main() { const std::vector ssid = {0, 3, 'G', 'N', 'T'}; bcn.insert(bcn.end(), ssid.begin(), ssid.end()); bcn.insert(bcn.end(), ie.begin(), ie.end()); - auto f = find_grant_map(bcn.data(), bcn.size()); + auto f = find_grant_map(bcn.data(), bcn.size(), k); CHECK(f.has_value() && *f == m); - CHECK(!find_grant_map(bcn.data(), 36 + ssid.size()).has_value()); /* no IE */ + CHECK(!find_grant_map(bcn.data(), 36 + ssid.size(), k).has_value()); /* no IE */ + /* Authentication (v2): a forged map is inert; a keyless receiver checks + * the structure only; a keyed one refuses an untagged (keyless) map. */ + CHECK(!decode_grant_ie(ie.data(), ie.size(), other).has_value()); /* another key */ + CHECK(!find_grant_map(bcn.data(), bcn.size(), other).has_value()); auto bad = ie; + bad.back() ^= 0x01; /* tag bit flip */ + CHECK(!decode_grant_ie(bad.data(), bad.size(), k).has_value()); + CHECK(decode_grant_ie(ie.data(), ie.size(), none).has_value()); /* passive: structure */ + const auto open_ie = encode_grant_ie(m, none); + CHECK(open_ie.size() == ie.size() && decode_grant_ie(open_ie.data(), open_ie.size(), none)); + CHECK(!decode_grant_ie(open_ie.data(), open_ie.size(), k).has_value()); /* zero tag */ + GrantMap moved = m; /* a forger's edit, CRC recomputed: the tag still fails */ + moved.grants[0].ul_slot = 9; + auto forged = encode_grant_ie(moved, other); + CHECK(!forged.empty() && !decode_grant_ie(forged.data(), forged.size(), k).has_value()); + + bad = ie; bad[10] ^= 0x01; /* body bit flip -> CRC */ - CHECK(!decode_grant_ie(bad.data(), bad.size()).has_value()); + CHECK(!decode_grant_ie(bad.data(), bad.size(), k).has_value()); bad = ie; - bad[7] = 2; /* version */ - CHECK(!decode_grant_ie(bad.data(), bad.size()).has_value()); + bad[7] = 1; /* version 1 (untagged): refused */ + CHECK(!decode_grant_ie(bad.data(), bad.size(), k).has_value()); bad = ie; bad[2] = 0x00; /* OUI */ - CHECK(!decode_grant_ie(bad.data(), bad.size()).has_value()); + CHECK(!decode_grant_ie(bad.data(), bad.size(), k).has_value()); bad = ie; bad[1] = static_cast(bad[1] - 1); /* length disagrees with n */ - CHECK(!decode_grant_ie(bad.data(), bad.size()).has_value()); - CHECK(!decode_grant_ie(ie.data(), ie.size() - 1).has_value()); /* truncated */ + CHECK(!decode_grant_ie(bad.data(), bad.size(), k).has_value()); + CHECK(!decode_grant_ie(ie.data(), ie.size() - 1, k).has_value()); /* truncated */ GrantMap big; big.grants.resize(kGrantMax + 1); - CHECK(encode_grant_ie(big).empty()); + CHECK(encode_grant_ie(big, k).empty()); big.grants.resize(kGrantMax); - const auto full = encode_grant_ie(big); - CHECK(!full.empty() && full[1] <= 255 && decode_grant_ie(full.data(), full.size())); + const auto full = encode_grant_ie(big, k); + CHECK(!full.empty() && full[1] <= 255 && decode_grant_ie(full.data(), full.size(), k)); + const Mac failing = [](const uint8_t *, size_t, uint8_t *) { return false; }; + CHECK(encode_grant_ie(m, failing).empty()); /* a failing Mac: nothing aired */ } /* Receiver: newer epochs only; a map takes effect at its superframe. */