770 lines
31 KiB
C++
770 lines
31 KiB
C++
/*
|
|
* brcmf_harness.cpp
|
|
* Host harness for the Broadcom FullMAC driver's protocol and connection
|
|
* layers (BrcmfProto.cpp, BrcmfConnect.cpp, with the real supplicant).
|
|
*
|
|
* The SDIO bus and the backplane are replaced by a simulated CYW4345x
|
|
* firmware: it decodes every SDPCM frame the driver writes to function 2
|
|
* (CDC ioctls, BDC data), answers ioctls the way the firmware does, and
|
|
* feeds back events and data frames through the same framing. The access
|
|
* point side of the WPA2 handshake is written here independently of
|
|
* Wpa.cpp, on the crypto primitives crypto_vectors checks against the
|
|
* published vectors.
|
|
*
|
|
* What stays untested is everything that needs the chip: the SDIO host,
|
|
* the backplane, firmware download (BrcmfSdio.cpp, BrcmfChip.cpp), and
|
|
* the firmware's own opinion of the structures it is sent.
|
|
*
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
*/
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <deque>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Drivers/Net/Wifi/Brcmf.hpp"
|
|
#include "Drivers/Net/Wifi/BrcmfSdio.hpp"
|
|
#include "Drivers/Net/Wifi/Ieee80211.hpp"
|
|
#include "Drivers/Net/Wifi/Wpa.hpp"
|
|
#include "Drivers/Platform/RpiFirmware.hpp"
|
|
#include "Libraries/Crypto.hpp"
|
|
|
|
using namespace Drivers::Net::Wifi;
|
|
using Bytes = std::vector<uint8_t>;
|
|
|
|
namespace Timekeeping {
|
|
uint64_t g_ms = 1000;
|
|
uint64_t GetMilliseconds() { return g_ms; }
|
|
void BusyWaitMs(uint64_t ms) { g_ms += ms; }
|
|
void BusyWaitUs(uint64_t us) { g_ms += (us + 999) / 1000; }
|
|
}
|
|
|
|
static int g_fails = 0;
|
|
static void check(bool cond, const char* what) {
|
|
printf(" %s %s\n", cond ? "PASS" : "FAIL", what);
|
|
if (!cond) g_fails++;
|
|
}
|
|
|
|
static uint16_t be16(const uint8_t* p) { return (uint16_t)(p[0] << 8 | p[1]); }
|
|
static uint16_t le16(const uint8_t* p) { return (uint16_t)(p[0] | p[1] << 8); }
|
|
static uint32_t le32(const uint8_t* p) { uint32_t v; memcpy(&v, p, 4); return v; }
|
|
static void putle16(Bytes& b, uint16_t v) { b.push_back((uint8_t)v); b.push_back((uint8_t)(v >> 8)); }
|
|
static void putle32(Bytes& b, uint32_t v) { for (int i = 0; i < 4; i++) b.push_back((uint8_t)(v >> (8 * i))); }
|
|
static void putbe16(Bytes& b, uint16_t v) { b.push_back((uint8_t)(v >> 8)); b.push_back((uint8_t)v); }
|
|
static void putbe32(Bytes& b, uint32_t v) { for (int i = 3; i >= 0; i--) b.push_back((uint8_t)(v >> (8 * i))); }
|
|
static void append(Bytes& b, const uint8_t* p, size_t n) { b.insert(b.end(), p, p + n); }
|
|
|
|
static const uint8_t PLACEHOLDER_MAC[6] = { 0xb8, 0x27, 0xeb, 0x74, 0xf2, 0x6c };
|
|
static const uint8_t BOARD_MAC[6] = { 0xdc, 0xa6, 0x32, 0x01, 0x02, 0xff };
|
|
static const uint8_t WLAN_MAC[6] = { 0xdc, 0xa6, 0x32, 0x01, 0x03, 0x00 }; // board + 1, with carry
|
|
static const uint8_t AP_OPEN[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x01 };
|
|
static const uint8_t AP_WPA[6] = { 0x00, 0x11, 0x22, 0x33, 0x44, 0x02 };
|
|
|
|
// =============================================================================
|
|
// Simulated firmware
|
|
// =============================================================================
|
|
|
|
struct Ioctl {
|
|
uint32_t Cmd;
|
|
bool Set;
|
|
std::string Name; // iovar name, for GET_VAR/SET_VAR
|
|
Bytes Data; // what follows the name (or the whole payload)
|
|
uint64_t Order;
|
|
};
|
|
|
|
struct Fw {
|
|
std::deque<Bytes> ToHost; // complete SDPCM frames
|
|
Bytes Cur;
|
|
size_t CurOff = 0;
|
|
uint8_t HostSeq = 0; // next sequence number expected
|
|
uint8_t Window = 8;
|
|
bool SeqError = false;
|
|
bool Mute = false; // stop answering ioctls
|
|
std::vector<Ioctl> Ioctls;
|
|
std::vector<std::pair<Bytes, uint64_t>> DataTx; // Ethernet frames, order
|
|
uint64_t Order = 0;
|
|
bool AutoJoin = true;
|
|
void (*OnEapol)(const Bytes& body) = nullptr;
|
|
};
|
|
static Fw fw;
|
|
|
|
static void QueueFrame(uint8_t channel, const Bytes& payload) {
|
|
Bytes f(12, 0);
|
|
uint16_t len = (uint16_t)(12 + payload.size());
|
|
f[0] = (uint8_t)len; f[1] = (uint8_t)(len >> 8);
|
|
f[2] = (uint8_t)~len; f[3] = (uint8_t)(~len >> 8);
|
|
f[4] = 0;
|
|
f[5] = channel;
|
|
f[7] = 12;
|
|
f[9] = (uint8_t)(fw.HostSeq + fw.Window); // credit
|
|
append(f, payload.data(), payload.size());
|
|
fw.ToHost.push_back(f);
|
|
}
|
|
|
|
static Bytes Bdc(const Bytes& eth) {
|
|
Bytes b = { 0x20, 0, 0, 0 };
|
|
append(b, eth.data(), eth.size());
|
|
return b;
|
|
}
|
|
|
|
static void QueueEvent(uint32_t type, uint32_t status, uint32_t reason, uint16_t flags,
|
|
const uint8_t* addr, const Bytes& data = {}) {
|
|
Bytes e;
|
|
append(e, WLAN_MAC, 6);
|
|
append(e, AP_WPA, 6);
|
|
putbe16(e, 0x886C);
|
|
putbe16(e, 0x8001);
|
|
putbe16(e, (uint16_t)(48 + data.size()));
|
|
e.push_back(1);
|
|
e.push_back(0x00); e.push_back(0x10); e.push_back(0x18);
|
|
putbe16(e, 1);
|
|
putbe16(e, 2); // version
|
|
putbe16(e, flags);
|
|
putbe32(e, type);
|
|
putbe32(e, status);
|
|
putbe32(e, reason);
|
|
putbe32(e, 0); // auth type
|
|
putbe32(e, (uint32_t)data.size());
|
|
Bytes a(6, 0);
|
|
if (addr) memcpy(a.data(), addr, 6);
|
|
append(e, a.data(), 6);
|
|
Bytes ifname(16, 0);
|
|
memcpy(ifname.data(), "wlan0", 5);
|
|
append(e, ifname.data(), 16);
|
|
e.push_back(0); // ifidx
|
|
e.push_back(0); // bsscfgidx
|
|
append(e, data.data(), data.size());
|
|
QueueFrame(1, Bdc(e));
|
|
}
|
|
|
|
static void QueueEthernet(const uint8_t* src, uint16_t type, const Bytes& body) {
|
|
Bytes e;
|
|
append(e, WLAN_MAC, 6);
|
|
append(e, src, 6);
|
|
putbe16(e, type);
|
|
append(e, body.data(), body.size());
|
|
QueueFrame(2, Bdc(e));
|
|
}
|
|
|
|
// One brcmf_bss_info_le, laid out as Linux's struct.
|
|
static Bytes BssInfo(const uint8_t* bssid, const char* ssid, uint16_t chanspec, uint8_t ctl,
|
|
int16_t rssi, uint16_t caps, const Bytes& ies) {
|
|
Bytes b(128, 0);
|
|
memcpy(&b[0], "\x6d\x00\x00\x00", 4); // version 109
|
|
uint32_t len = (uint32_t)(128 + ies.size());
|
|
memcpy(&b[4], &len, 4);
|
|
memcpy(&b[8], bssid, 6);
|
|
uint16_t bcn = 100;
|
|
memcpy(&b[14], &bcn, 2);
|
|
memcpy(&b[16], &caps, 2);
|
|
b[18] = (uint8_t)strlen(ssid);
|
|
memcpy(&b[19], ssid, strlen(ssid));
|
|
memcpy(&b[72], &chanspec, 2);
|
|
b[76] = 3;
|
|
memcpy(&b[78], &rssi, 2);
|
|
b[88] = ctl;
|
|
uint16_t ieOff = 128;
|
|
memcpy(&b[116], &ieOff, 2);
|
|
uint32_t ieLen = (uint32_t)ies.size();
|
|
memcpy(&b[120], &ieLen, 4);
|
|
append(b, ies.data(), ies.size());
|
|
return b;
|
|
}
|
|
|
|
static const Bytes RSN_CCMP = {
|
|
0x01, 0x00, 0x00, 0x0f, 0xac, 0x04, 0x01, 0x00, 0x00, 0x0f, 0xac, 0x04,
|
|
0x01, 0x00, 0x00, 0x0f, 0xac, 0x02, 0x00, 0x00,
|
|
};
|
|
static const Bytes RSN_GCMP = {
|
|
0x01, 0x00, 0x00, 0x0f, 0xac, 0x08, 0x01, 0x00, 0x00, 0x0f, 0xac, 0x08,
|
|
0x01, 0x00, 0x00, 0x0f, 0xac, 0x02, 0x00, 0x00,
|
|
};
|
|
|
|
static Bytes Ies(const char* ssid, uint8_t channel, const Bytes* rsn) {
|
|
Bytes ie;
|
|
ie.push_back(0); ie.push_back((uint8_t)strlen(ssid));
|
|
append(ie, (const uint8_t*)ssid, strlen(ssid));
|
|
ie.push_back(1); ie.push_back(4); ie.push_back(0x82); ie.push_back(0x84); ie.push_back(0x0b); ie.push_back(0x16);
|
|
ie.push_back(3); ie.push_back(1); ie.push_back(channel);
|
|
if (rsn) {
|
|
ie.push_back(48); ie.push_back((uint8_t)rsn->size());
|
|
append(ie, rsn->data(), rsn->size());
|
|
}
|
|
return ie;
|
|
}
|
|
|
|
static void QueueEscan(uint16_t syncId) {
|
|
Bytes bss1 = BssInfo(AP_OPEN, "OpenNet", 0x1006, 6, -48, 0x0401, Ies("OpenNet", 6, nullptr));
|
|
Bytes bss2 = BssInfo(AP_WPA, "SecureNet", 0xd024, 36, -61, 0x0411, Ies("SecureNet", 36, &RSN_CCMP));
|
|
for (const Bytes* bss : { &bss1, &bss2 }) {
|
|
Bytes r;
|
|
putle32(r, (uint32_t)(12 + bss->size()));
|
|
putle32(r, 1);
|
|
putle16(r, syncId);
|
|
putle16(r, 1);
|
|
append(r, bss->data(), bss->size());
|
|
QueueEvent(69, 8, 0, 0, nullptr, r); // PARTIAL
|
|
}
|
|
QueueEvent(69, 0, 0, 0, nullptr); // done
|
|
}
|
|
|
|
static void FwControl(const uint8_t* p, size_t len) {
|
|
uint32_t cmd = le32(p), dlen = le32(p + 4), flags = le32(p + 8);
|
|
bool set = flags & 2;
|
|
const uint8_t* data = p + 16;
|
|
size_t avail = len - 16;
|
|
if (dlen > avail) dlen = (uint32_t)avail;
|
|
|
|
Ioctl io{cmd, set, "", {}, fw.Order++};
|
|
if (cmd == 262 || cmd == 263) {
|
|
size_t n = strnlen((const char*)data, dlen);
|
|
io.Name.assign((const char*)data, n);
|
|
if (n + 1 < dlen) io.Data.assign(data + n + 1, data + dlen);
|
|
} else {
|
|
io.Data.assign(data, data + dlen);
|
|
}
|
|
fw.Ioctls.push_back(io);
|
|
if (fw.Mute) return;
|
|
|
|
int32_t status = 0;
|
|
Bytes resp(dlen, 0);
|
|
if (cmd == 262) {
|
|
if (io.Name == "cur_etheraddr") memcpy(resp.data(), PLACEHOLDER_MAC, 6);
|
|
else if (io.Name == "ver") {
|
|
const char* v = "wl0: Aug 29 2023 01:51:02 version 7.45.265 (28bca26 CY) FWID 01-b677b91b\n";
|
|
memcpy(resp.data(), v, strlen(v) < dlen ? strlen(v) : dlen);
|
|
} else if (io.Name == "clmload_status") {
|
|
} else status = -23; // BCME_UNSUPPORTED
|
|
} else if (cmd == 263) {
|
|
if (io.Name == "escan") {
|
|
QueueEscan(le16(io.Data.data() + 6));
|
|
} else if (io.Name == "join" && fw.AutoJoin) {
|
|
QueueEvent(0, 0, 0, 0, io.Data.data() + 56);
|
|
QueueEvent(16, 0, 0, 1, io.Data.data() + 56);
|
|
} else if (io.Name == "bus:rxglom" || io.Name == "mfp" || io.Name == "sup_wpa") {
|
|
status = -23;
|
|
}
|
|
} else if (cmd == 217) { // valid channels
|
|
uint32_t list[3] = { 2, 1, 36 };
|
|
memcpy(resp.data(), list, sizeof(list) < dlen ? sizeof(list) : dlen);
|
|
}
|
|
|
|
Bytes r;
|
|
putle32(r, cmd);
|
|
putle32(r, dlen);
|
|
putle32(r, (flags & 0xFFFF0002u) | (status < 0 ? 1 : 0));
|
|
putle32(r, (uint32_t)status);
|
|
append(r, resp.data(), resp.size());
|
|
QueueFrame(0, r);
|
|
}
|
|
|
|
static void FwReceive(const uint8_t* f, size_t busLen) {
|
|
uint16_t len = le16(f), check = le16(f + 2);
|
|
if ((uint16_t)(len ^ check) != 0xFFFF || len > busLen) { fw.SeqError = true; return; }
|
|
if (f[4] != fw.HostSeq) fw.SeqError = true;
|
|
fw.HostSeq = (uint8_t)(f[4] + 1);
|
|
uint8_t ch = f[5] & 0x0F, off = f[7];
|
|
const uint8_t* p = f + off;
|
|
size_t plen = len - off;
|
|
if (ch == 0) {
|
|
FwControl(p, plen);
|
|
} else if (ch == 2) {
|
|
if (plen < 4 || (p[0] >> 4) != 2 || p[3] != 0) { fw.SeqError = true; return; }
|
|
Bytes eth(p + 4, p + plen);
|
|
fw.DataTx.push_back({eth, fw.Order++});
|
|
if (eth.size() >= 14 && be16(ð[12]) == 0x888E && fw.OnEapol)
|
|
fw.OnEapol(Bytes(eth.begin() + 14, eth.end()));
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// What the driver links against instead of the hardware
|
|
// =============================================================================
|
|
|
|
namespace Drivers::Net::Wifi {
|
|
|
|
BrcmfDevice g_brcmf;
|
|
|
|
bool BrcmfBpRead32(uint32_t addr, uint32_t& value) {
|
|
value = 0;
|
|
if (addr == g_brcmf.SdioCoreBase + 0x20 && (!fw.ToHost.empty() || !fw.Cur.empty()))
|
|
value = 0x40; // I_HMB_FRAME_IND
|
|
return true;
|
|
}
|
|
bool BrcmfBpWrite32(uint32_t, uint32_t) { return true; }
|
|
bool BrcmfBpReadMem(uint32_t, uint8_t*, uint32_t) { return true; }
|
|
bool BrcmfBpWriteMem(uint32_t, const uint8_t*, uint32_t) { return true; }
|
|
bool BrcmfBpSelectWindow(uint32_t) { return true; }
|
|
bool BrcmfF1Read8(uint32_t, uint8_t& v) { v = 0; return true; }
|
|
bool BrcmfF1Write8(uint32_t, uint8_t) { return true; }
|
|
|
|
bool BrcmfChipBringUp() {
|
|
g_brcmf.FwName = "43456";
|
|
g_brcmf.SdioCoreBase = 0x18002000;
|
|
g_brcmf.HostIntMask = 0x200000F0;
|
|
return true;
|
|
}
|
|
void BrcmfChipPowerOff() {}
|
|
void BrcmfDumpConsole() {}
|
|
|
|
uint8_t* BrcmfLoadFile(const char* ext, uint32_t& size, bool) {
|
|
if (strcmp(ext, "clm_blob") != 0) return nullptr;
|
|
size = 3000;
|
|
uint8_t* p = (uint8_t*)malloc(size);
|
|
for (uint32_t i = 0; i < size; i++) p[i] = (uint8_t)i;
|
|
return p;
|
|
}
|
|
|
|
struct Seen { Bytes Bssid; Bytes Ies; uint16_t Beacon, Caps; uint8_t Channel; int8_t Rssi; };
|
|
static std::vector<Seen> g_seen;
|
|
static std::vector<Bytes> g_delivered;
|
|
|
|
void WifiRxBssInfo(const uint8_t* bssid, uint16_t beaconInterval, uint16_t capability,
|
|
const uint8_t* ies, uint32_t iesLen, uint8_t channel, int8_t rssiDbm) {
|
|
g_seen.push_back({Bytes(bssid, bssid + 6), Bytes(ies, ies + iesLen), beaconInterval,
|
|
capability, channel, rssiDbm});
|
|
}
|
|
void WifiRxEthernet(const uint8_t* frame, uint32_t len) {
|
|
g_delivered.push_back(Bytes(frame, frame + len));
|
|
}
|
|
|
|
// Routed by Wifi.cpp in the kernel.
|
|
bool WpaTxEapol(const uint8_t* body, uint32_t len) { return BrcmfWpaTxEapol(body, len); }
|
|
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
|
|
return BrcmfWpaInstallPtk(tk, tkLen, cipher);
|
|
}
|
|
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
|
|
uint8_t cipher, const uint8_t* rsc) {
|
|
return BrcmfWpaInstallGtk(gtk, gtkLen, keyIdx, cipher, rsc);
|
|
}
|
|
}
|
|
|
|
namespace Drivers::Net::Wifi::Sdio {
|
|
bool HostInit(uint64_t, uint32_t) { return true; }
|
|
bool CardInit() { return true; }
|
|
bool Read8(uint32_t, uint32_t, uint8_t& v) { v = 0; return true; }
|
|
bool Write8(uint32_t, uint32_t, uint8_t) { return true; }
|
|
bool EnableFunction(uint32_t, bool, uint64_t) { return true; }
|
|
bool EnableCardInterrupts(uint8_t) { return true; }
|
|
void Abort(uint32_t) {}
|
|
bool CardInterruptPending() { return !fw.ToHost.empty(); }
|
|
bool IsInitialized() { return true; }
|
|
|
|
bool ReadBlock(uint32_t fn, uint32_t, bool, uint8_t* buffer, uint32_t len) {
|
|
if (fn != FN2 || (len & 3)) return false;
|
|
Timekeeping::g_ms++;
|
|
if (fw.Cur.empty()) {
|
|
if (fw.ToHost.empty()) { memset(buffer, 0, len); return true; }
|
|
fw.Cur = fw.ToHost.front();
|
|
fw.ToHost.pop_front();
|
|
fw.CurOff = 0;
|
|
}
|
|
for (uint32_t i = 0; i < len; i++, fw.CurOff++)
|
|
buffer[i] = fw.CurOff < fw.Cur.size() ? fw.Cur[fw.CurOff] : 0;
|
|
if (fw.CurOff >= ((fw.Cur.size() + 3) & ~(size_t)3)) fw.Cur.clear();
|
|
return true;
|
|
}
|
|
|
|
bool WriteBlock(uint32_t fn, uint32_t, bool, const uint8_t* buffer, uint32_t len) {
|
|
if (fn != FN2 || (len & 3)) return false;
|
|
if (len > FN2_BLOCK && len % FN2_BLOCK) fw.SeqError = true; // padded to whole blocks
|
|
Timekeeping::g_ms++;
|
|
FwReceive(buffer, len);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
namespace Drivers::Platform::RpiFirmware {
|
|
bool GetBoardMacAddress(uint8_t mac[6]) { memcpy(mac, BOARD_MAC, 6); return true; }
|
|
bool SetGpioOutput(uint32_t, bool) { return true; }
|
|
}
|
|
|
|
// =============================================================================
|
|
// An independent WPA2-PSK authenticator
|
|
// =============================================================================
|
|
|
|
namespace Ap {
|
|
using namespace Kt::Crypto;
|
|
static const char* SSID = "SecureNet";
|
|
static const char* PASS = "supersecret123";
|
|
static uint8_t pmk[32], ptk[48], anonce[32], snonce[32], gtk[16];
|
|
static uint64_t replay = 0;
|
|
static Bytes lastMsg2, lastMsg4, staRsn;
|
|
static bool msg2MicOk = false, msg4MicOk = false;
|
|
|
|
static void Prf(const uint8_t* key, const char* label, const Bytes& data, uint8_t* out, size_t n) {
|
|
uint8_t counter = 0;
|
|
size_t done = 0;
|
|
while (done < n) {
|
|
Bytes m((const uint8_t*)label, (const uint8_t*)label + strlen(label) + 1);
|
|
append(m, data.data(), data.size());
|
|
m.push_back(counter++);
|
|
uint8_t h[20];
|
|
HmacSha1(key, 32, m.data(), m.size(), h);
|
|
size_t take = n - done < 20 ? n - done : 20;
|
|
memcpy(out + done, h, take);
|
|
done += take;
|
|
}
|
|
}
|
|
|
|
static Bytes KeyFrame(uint16_t info, const Bytes& keyData, bool mic) {
|
|
Bytes k(99, 0);
|
|
k[0] = 2; k[1] = 3;
|
|
uint16_t bodyLen = (uint16_t)(95 + keyData.size());
|
|
k[2] = (uint8_t)(bodyLen >> 8); k[3] = (uint8_t)bodyLen;
|
|
k[4] = 2;
|
|
k[5] = (uint8_t)(info >> 8); k[6] = (uint8_t)info;
|
|
k[8] = 16;
|
|
for (int i = 0; i < 8; i++) k[9 + i] = (uint8_t)(replay >> (56 - 8 * i));
|
|
memcpy(&k[17], anonce, 32);
|
|
k[97] = (uint8_t)(keyData.size() >> 8); k[98] = (uint8_t)keyData.size();
|
|
append(k, keyData.data(), keyData.size());
|
|
if (mic) {
|
|
uint8_t h[20];
|
|
HmacSha1(ptk, 16, k.data(), k.size(), h);
|
|
memcpy(&k[81], h, 16);
|
|
}
|
|
return k;
|
|
}
|
|
|
|
static bool MicOk(Bytes frame) {
|
|
if (frame.size() < 99) return false;
|
|
uint8_t mic[16];
|
|
memcpy(mic, &frame[81], 16);
|
|
memset(&frame[81], 0, 16);
|
|
uint8_t h[20];
|
|
HmacSha1(ptk, 16, frame.data(), frame.size(), h);
|
|
return memcmp(h, mic, 16) == 0;
|
|
}
|
|
|
|
static void Start(const char* pass) {
|
|
Pbkdf2Sha1(pass, strlen(pass), (const uint8_t*)SSID, strlen(SSID), 4096, pmk, 32);
|
|
for (int i = 0; i < 32; i++) anonce[i] = (uint8_t)(0xA0 + i);
|
|
for (int i = 0; i < 16; i++) gtk[i] = (uint8_t)(0x30 + i);
|
|
replay = 1;
|
|
lastMsg2.clear(); lastMsg4.clear(); staRsn.clear();
|
|
msg2MicOk = msg4MicOk = false;
|
|
QueueEthernet(AP_WPA, 0x888E, KeyFrame(0x008A, {}, false)); // msg 1
|
|
}
|
|
|
|
static void OnEapol(const Bytes& f) {
|
|
if (f.size() < 99 || f[1] != 3) return;
|
|
uint16_t info = be16(&f[5]);
|
|
if (!(info & 0x0100)) return;
|
|
if (lastMsg2.empty()) {
|
|
lastMsg2 = f;
|
|
memcpy(snonce, &f[17], 32);
|
|
Bytes data;
|
|
const uint8_t* a = memcmp(AP_WPA, WLAN_MAC, 6) < 0 ? AP_WPA : WLAN_MAC;
|
|
const uint8_t* b = a == AP_WPA ? WLAN_MAC : AP_WPA;
|
|
append(data, a, 6); append(data, b, 6);
|
|
const uint8_t* n1 = memcmp(anonce, snonce, 32) < 0 ? anonce : snonce;
|
|
const uint8_t* n2 = n1 == anonce ? snonce : anonce;
|
|
append(data, n1, 32); append(data, n2, 32);
|
|
Prf(pmk, "Pairwise key expansion", data, ptk, 48);
|
|
msg2MicOk = MicOk(f);
|
|
uint16_t kdl = be16(&f[97]);
|
|
staRsn.assign(f.begin() + 99, f.begin() + 99 + kdl);
|
|
if (!msg2MicOk) {
|
|
// Wrong passphrase: the AP gives up and deauthenticates.
|
|
QueueEvent(6, 0, 15, 0, AP_WPA);
|
|
return;
|
|
}
|
|
// Message 3: our RSN IE and the GTK KDE, key-wrapped under the KEK.
|
|
Bytes plain = { 0x30, (uint8_t)RSN_CCMP.size() };
|
|
append(plain, RSN_CCMP.data(), RSN_CCMP.size());
|
|
Bytes kde = { 0xdd, 22, 0x00, 0x0f, 0xac, 0x01, 0x01, 0x00 };
|
|
append(kde, gtk, 16);
|
|
append(plain, kde.data(), kde.size());
|
|
plain.push_back(0xdd);
|
|
while (plain.size() % 8) plain.push_back(0);
|
|
Bytes wrapped(plain.size() + 8);
|
|
AesKeyWrap(ptk + 16, 16, plain.data(), plain.size(), wrapped.data());
|
|
replay = 2;
|
|
QueueEthernet(AP_WPA, 0x888E, KeyFrame(0x13CA, wrapped, true));
|
|
} else if (lastMsg4.empty()) {
|
|
lastMsg4 = f;
|
|
msg4MicOk = MicOk(f);
|
|
}
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Cases
|
|
// =============================================================================
|
|
|
|
static void Pump(int rounds = 20) {
|
|
for (int i = 0; i < rounds; i++) {
|
|
BrcmfService();
|
|
Timekeeping::g_ms += 5;
|
|
}
|
|
}
|
|
|
|
static const Ioctl* LastSet(const char* name) {
|
|
for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it)
|
|
if (it->Set && it->Name == name) return &*it;
|
|
return nullptr;
|
|
}
|
|
|
|
static uint32_t SetInt(const char* name) {
|
|
const Ioctl* io = LastSet(name);
|
|
return io && io->Data.size() >= 4 ? le32(io->Data.data()) : 0xDEADBEEF;
|
|
}
|
|
|
|
static const Ioctl* LastCmd(uint32_t cmd) {
|
|
for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it)
|
|
if (it->Cmd == cmd) return &*it;
|
|
return nullptr;
|
|
}
|
|
|
|
static constexpr int CONN_IDLE = 0, CONN_ASSOCIATING = 4, CONN_HANDSHAKING = 6,
|
|
CONN_CONNECTED = 7, CONN_FAILED = 8;
|
|
|
|
static void CaseInit() {
|
|
printf("== bring-up and preinit ==\n");
|
|
check(BrcmfProbe(0xFE300000), "the Arasan controller is claimed");
|
|
check(BrcmfCompleteInit(), "preinit completes against the firmware");
|
|
check(!fw.SeqError, "every frame carries the next sequence number and a valid header");
|
|
check(BrcmfGetState() == BrcmfState::Running, "the radio is running");
|
|
check(memcmp(g_brcmf.Mac, WLAN_MAC, 6) == 0, "the NVRAM placeholder MAC becomes the board address + 1");
|
|
const Ioctl* mac = LastSet("cur_etheraddr");
|
|
check(mac && mac->Data.size() == 6 && memcmp(mac->Data.data(), WLAN_MAC, 6) == 0,
|
|
"and is programmed into the firmware");
|
|
check(strcmp(g_brcmf.FwVersion, "7.45.265") == 0, "the firmware version is read from \"ver\"");
|
|
|
|
std::vector<const Ioctl*> clm;
|
|
for (auto& io : fw.Ioctls) if (io.Set && io.Name == "clmload") clm.push_back(&io);
|
|
check(clm.size() == 3, "a 3000-byte CLM blob goes down in three chunks");
|
|
if (clm.size() == 3) {
|
|
check(le16(clm[0]->Data.data()) == 0x1002 && le16(clm[1]->Data.data()) == 0x1000 &&
|
|
le16(clm[2]->Data.data()) == 0x1004, "flagged begin / middle / end, handler version 1");
|
|
check(le16(clm[0]->Data.data() + 2) == 2, "of type CLM");
|
|
check(le32(clm[0]->Data.data() + 4) == 1400 && le32(clm[2]->Data.data() + 4) == 200,
|
|
"with 1400-byte chunks and the remainder last");
|
|
check(clm[1]->Data[12] == (uint8_t)1400 && clm[2]->Data[12 + 199] == (uint8_t)2999,
|
|
"carrying the blob's bytes in order");
|
|
}
|
|
|
|
const Ioctl* ev = LastSet("event_msgs");
|
|
bool evOk = ev && ev->Data.size() == 16;
|
|
if (evOk) {
|
|
for (uint32_t e : { 0u, 5u, 6u, 12u, 16u, 69u })
|
|
evOk = evOk && (ev->Data[e / 8] & (1u << (e % 8)));
|
|
}
|
|
check(evOk, "events for join, link, deauth/disassoc and escan are enabled");
|
|
check(SetInt("roam_off") == 1, "firmware roaming is off");
|
|
const Ioctl* up = LastCmd(2);
|
|
check(up && up->Set, "WLC_UP is sent");
|
|
const Ioctl* pm = LastCmd(86);
|
|
check(pm && le32(pm->Data.data()) == 0, "power save is off");
|
|
check(g_brcmf.ChannelCount == 2 && g_brcmf.Band24 && g_brcmf.Band5,
|
|
"the valid channel list gives the channel count and bands");
|
|
|
|
uint32_t v = 0;
|
|
check(!BrcmfIovarGetInt("no_such_iovar", v, true), "an iovar the firmware rejects reports failure");
|
|
check(BrcmfGetState() == BrcmfState::Running, "without counting against the firmware's health");
|
|
}
|
|
|
|
static void CaseScan() {
|
|
printf("== escan ==\n");
|
|
check(BrcmfStartScan(), "a scan starts");
|
|
const Ioctl* es = LastSet("escan");
|
|
check(es && es->Data.size() == 72, "escan carries the 72-byte v1 parameter block");
|
|
if (es && es->Data.size() == 72) {
|
|
const uint8_t* p = es->Data.data();
|
|
check(le32(p) == 1 && le16(p + 4) == 1, "version 1, action start");
|
|
bool bcast = true;
|
|
for (int i = 0; i < 6; i++) bcast = bcast && p[44 + i] == 0xFF;
|
|
check(bcast && p[50] == 2 && p[51] == 0, "broadcast BSSID, any BSS type, active");
|
|
check(le32(p + 52) == 0xFFFFFFFF && le32(p + 68) == 0, "default dwells, every channel");
|
|
}
|
|
Pump();
|
|
check(!BrcmfScanActive(), "the completion event ends the scan");
|
|
check(g_seen.size() == 2, "both BSSs reach the scan table");
|
|
if (g_seen.size() == 2) {
|
|
check(memcmp(g_seen[0].Bssid.data(), AP_OPEN, 6) == 0 && g_seen[0].Channel == 6 &&
|
|
g_seen[0].Rssi == -48 && g_seen[0].Beacon == 100, "BSSID, channel, RSSI, beacon interval");
|
|
check(g_seen[1].Channel == 36 && (g_seen[1].Caps & 0x10), "the 5 GHz network, privacy set");
|
|
check(g_seen[1].Ies.size() == Ies("SecureNet", 36, &RSN_CCMP).size() && g_seen[1].Ies[0] == 0,
|
|
"with its information elements intact");
|
|
}
|
|
}
|
|
|
|
static void CaseOpenJoin() {
|
|
printf("== open network ==\n");
|
|
check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "the join is accepted");
|
|
check(SetInt("wpa_auth") == 0 && SetInt("wsec") == 0 && SetInt("auth") == 0,
|
|
"no WPA, no encryption, open system authentication");
|
|
const Ioctl* j = LastSet("join");
|
|
check(j && j->Data.size() == 72, "join carries brcmf_ext_join_params_le");
|
|
if (j && j->Data.size() == 72) {
|
|
const uint8_t* p = j->Data.data();
|
|
check(le32(p) == 7 && memcmp(p + 4, "OpenNet", 7) == 0, "the SSID");
|
|
check(memcmp(p + 56, AP_OPEN, 6) == 0 && le32(p + 64) == 1 && le16(p + 68) == 0x1006,
|
|
"the BSSID and the chanspec escan reported");
|
|
}
|
|
check(BrcmfConnectState() == CONN_ASSOCIATING, "associating until the firmware says otherwise");
|
|
Pump();
|
|
check(BrcmfConnectState() == CONN_CONNECTED && BrcmfLinkUp(), "E_SET_SSID + E_LINK bring the link up");
|
|
|
|
Bytes eth;
|
|
append(eth, AP_OPEN, 6); append(eth, WLAN_MAC, 6); putbe16(eth, 0x0800);
|
|
for (int i = 0; i < 100; i++) eth.push_back((uint8_t)i);
|
|
size_t before = fw.DataTx.size();
|
|
check(BrcmfSendEthernet(eth.data(), (uint32_t)eth.size()), "an Ethernet frame is accepted");
|
|
Pump(2);
|
|
check(fw.DataTx.size() == before + 1 && fw.DataTx.back().first == eth,
|
|
"and reaches the firmware unchanged behind a BDC header");
|
|
|
|
Bytes big = eth;
|
|
big.resize(1514, 0x5a);
|
|
check(BrcmfSendEthernet(big.data(), (uint32_t)big.size()), "a full-size frame is accepted");
|
|
Pump(2);
|
|
check(fw.DataTx.back().first == big && !fw.SeqError, "and goes out padded to whole blocks");
|
|
|
|
g_delivered.clear();
|
|
QueueEthernet(AP_OPEN, 0x0800, Bytes(60, 0x11));
|
|
Pump(2);
|
|
check(g_delivered.size() == 1 && g_delivered[0].size() == 74 && be16(&g_delivered[0][12]) == 0x0800,
|
|
"a received data frame is delivered to the stack");
|
|
|
|
QueueEvent(6, 0, 3, 0, AP_OPEN); // DEAUTH_IND
|
|
Pump();
|
|
check(BrcmfConnectState() == CONN_IDLE && !BrcmfLinkUp(), "a deauthentication takes the link down");
|
|
check(!BrcmfSendEthernet(eth.data(), (uint32_t)eth.size()), "and nothing is sent while it is down");
|
|
}
|
|
|
|
static const Ioctl* FindKey(uint32_t index) {
|
|
for (auto it = fw.Ioctls.rbegin(); it != fw.Ioctls.rend(); ++it)
|
|
if (it->Set && it->Name == "wsec_key" && it->Data.size() == 164 && le32(it->Data.data()) == index)
|
|
return &*it;
|
|
return nullptr;
|
|
}
|
|
|
|
static void CaseWpaJoin() {
|
|
printf("== WPA2-PSK ==\n");
|
|
fw.OnEapol = Ap::OnEapol;
|
|
size_t ioctlsBefore = fw.Ioctls.size();
|
|
check(BrcmfConnectStart(AP_WPA, 36, "SecureNet", Ap::PASS, RSN_CCMP.data(), (uint8_t)RSN_CCMP.size()),
|
|
"the join is accepted");
|
|
check(SetInt("wpa_auth") == 0x80 && SetInt("wsec") == 4 && SetInt("auth") == 0,
|
|
"WPA2-PSK, AES, open system authentication");
|
|
const Ioctl* ie = LastSet("wpaie");
|
|
Bytes want = { 0x30, (uint8_t)RSN_CCMP.size() };
|
|
append(want, RSN_CCMP.data(), RSN_CCMP.size());
|
|
check(ie && ie->Data == want, "the supplicant's RSN element goes into the association request");
|
|
const Ioctl* j = LastSet("join");
|
|
check(j && le16(j->Data.data() + 68) == 0xd024, "the 5 GHz chanspec from the scan");
|
|
|
|
Pump();
|
|
check(BrcmfConnectState() == CONN_HANDSHAKING && !BrcmfLinkUp(),
|
|
"association waits for the handshake before the link is usable");
|
|
|
|
Ap::Start(Ap::PASS);
|
|
Pump(40);
|
|
check(!Ap::lastMsg2.empty() && Ap::msg2MicOk, "message 2 carries a MIC the AP verifies");
|
|
check(Ap::staRsn == want, "and the same RSN element");
|
|
check(!Ap::lastMsg4.empty() && Ap::msg4MicOk, "message 4 follows message 3, MIC verified");
|
|
check(BrcmfConnectState() == CONN_CONNECTED && BrcmfLinkUp(), "the link is up once keyed");
|
|
|
|
const Ioctl* ptk = FindKey(0);
|
|
const Ioctl* gtk = FindKey(1);
|
|
check(ptk && memcmp(ptk->Data.data() + 8, Ap::ptk + 32, 16) == 0 && le32(ptk->Data.data() + 4) == 16,
|
|
"the pairwise key is the TK the AP derived");
|
|
if (ptk) {
|
|
const uint8_t* k = ptk->Data.data();
|
|
check(le32(k + 112) == 4 && le32(k + 116) == 0 && memcmp(k + 156, AP_WPA, 6) == 0,
|
|
"AES-CCM, bound to the AP's address");
|
|
}
|
|
check(gtk && memcmp(gtk->Data.data() + 8, Ap::gtk, 16) == 0, "the group key is the AP's GTK");
|
|
if (gtk) {
|
|
const uint8_t* k = gtk->Data.data();
|
|
bool noEa = true;
|
|
for (int i = 0; i < 6; i++) noEa = noEa && k[156 + i] == 0;
|
|
check(le32(k + 116) == 2 && noEa && le32(k + 132) == 1, "primary, broadcast, receive IV set");
|
|
}
|
|
uint64_t msg4Order = 0;
|
|
for (auto& d : fw.DataTx)
|
|
if (d.first.size() > 14 && be16(&d.first[12]) == 0x888E) msg4Order = d.second;
|
|
check(ptk && msg4Order != 0 && msg4Order < ptk->Order,
|
|
"message 4 reaches the radio before the pairwise key is installed");
|
|
(void)ioctlsBefore;
|
|
|
|
QueueEvent(16, 0, 0, 0, AP_WPA); // link down
|
|
Pump();
|
|
check(BrcmfConnectState() == CONN_IDLE, "losing the link returns to idle");
|
|
}
|
|
|
|
static void CaseWrongPassphrase() {
|
|
printf("== wrong passphrase ==\n");
|
|
check(BrcmfConnectStart(AP_WPA, 36, "SecureNet", "not-the-password", RSN_CCMP.data(),
|
|
(uint8_t)RSN_CCMP.size()), "the join is accepted");
|
|
Pump();
|
|
Ap::Start(Ap::PASS);
|
|
Pump(40);
|
|
check(!Ap::lastMsg2.empty() && !Ap::msg2MicOk, "the AP rejects message 2's MIC");
|
|
check(BrcmfConnectState() == CONN_FAILED, "the deauthentication fails the join");
|
|
check(BrcmfJoinFailedOnAuth(), "and it is reported as an authentication failure");
|
|
BrcmfConnectAbort();
|
|
const Ioctl* d = LastCmd(52);
|
|
check(d && d->Set && memcmp(d->Data.data() + 4, AP_WPA, 6) == 0, "abort disassociates from the AP");
|
|
check(BrcmfConnectState() == CONN_IDLE, "and leaves the connection idle");
|
|
}
|
|
|
|
static void CaseRefusals() {
|
|
printf("== refusals ==\n");
|
|
check(!BrcmfConnectStart(AP_WPA, 36, "SecureNet", Ap::PASS, RSN_GCMP.data(), (uint8_t)RSN_GCMP.size()),
|
|
"a GCMP network is refused before anything is sent");
|
|
check(BrcmfConnectRefusedForSecurity(), "as a security refusal");
|
|
check(!BrcmfConnectStart(AP_WPA, 36, "SecureNet", nullptr, RSN_CCMP.data(), (uint8_t)RSN_CCMP.size()),
|
|
"an encrypted network without a passphrase is refused");
|
|
}
|
|
|
|
static void CaseJoinTimeout() {
|
|
printf("== join timeout ==\n");
|
|
fw.AutoJoin = false;
|
|
check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "the join is accepted");
|
|
Timekeeping::g_ms += 13000;
|
|
Pump(2);
|
|
check(BrcmfConnectState() == CONN_FAILED, "no answer from the firmware fails the join in bounded time");
|
|
BrcmfConnectAbort();
|
|
|
|
check(BrcmfConnectStart(AP_OPEN, 6, "OpenNet", nullptr, nullptr, 0), "another join is accepted");
|
|
QueueEvent(0, 3, 0, 0, AP_OPEN); // SET_SSID: no networks
|
|
Pump();
|
|
check(BrcmfConnectState() == CONN_FAILED, "an E_SET_SSID failure fails it at once");
|
|
BrcmfConnectAbort();
|
|
fw.AutoJoin = true;
|
|
}
|
|
|
|
static void CaseDeadFirmware() {
|
|
printf("== firmware stops answering ==\n");
|
|
fw.Mute = true;
|
|
uint32_t v = 0;
|
|
for (int i = 0; i < 3; i++) BrcmfIovarGetInt("ver", v, true);
|
|
check(BrcmfGetState() == BrcmfState::Error, "three unanswered commands mark the radio failed");
|
|
check(!BrcmfStartScan(), "and nothing more is attempted");
|
|
fw.Mute = false;
|
|
}
|
|
|
|
int main() {
|
|
CaseInit();
|
|
CaseScan();
|
|
CaseOpenJoin();
|
|
CaseWpaJoin();
|
|
CaseWrongPassphrase();
|
|
CaseRefusals();
|
|
CaseJoinTimeout();
|
|
CaseDeadFirmware();
|
|
if (g_fails) {
|
|
printf("\n%d Broadcom case(s) FAILED\n", g_fails);
|
|
return 1;
|
|
}
|
|
printf("\nAll Broadcom protocol and connection cases passed.\n");
|
|
return 0;
|
|
}
|