diff --git a/docs/wifi.md b/docs/wifi.md new file mode 100644 index 0000000..601cffc --- /dev/null +++ b/docs/wifi.md @@ -0,0 +1,234 @@ +to-do: rewrite & convert to html for docs pages + +# Wi-Fi + +MontaukOS drives Intel AX210-family adapters (the reference part is the AX211). +The driver scans, joins open and WPA2/WPA3-PSK networks, and presents itself to +the network stack as an ordinary Ethernet interface, so `dhcp`, `ping`, `nslookup` +and anything speaking sockets work over Wi-Fi exactly as they do over a cable. + +``` +wifi scan list nearby networks +wifi connect join one +dhcp pick up an address +wifi status what you are connected to +``` + +## Layout + +``` +kernel/src/Drivers/Net/Wifi/ + IwxTrans.cpp PCIe transport: MMIO, MSI-X, DMA rings, firmware boot, + host commands, RX processing, frame TX, key installation + IwxFw.cpp .ucode / .pnvm TLV parsing + IwxMvm.cpp post-ALIVE init, NVM, UMAC scan, RX dispatch + IwxConnect.cpp MLME: contexts, authenticate, associate, 802.11 <-> Ethernet + Wpa.cpp WPA2/WPA3-PSK supplicant (EAPOL 4-way + group rekey) + Ieee80211.hpp frame, element and RSN constants + Wifi.cpp subsystem facade: probe, scan table, syscalls, netif hooks +kernel/src/Libraries/Crypto.cpp SHA-1/SHA-256, HMAC, PBKDF2, AES, CMAC +kernel/src/Net/NetIf.cpp interface registry the Ethernet layer uses +``` + +## Joining a network + +`SYS_WIFI_CONNECT` blocks until the link is up or the attempt fails, and +returns a `WIFI_ERR_*` code the `wifi` tool turns into a specific message +(wrong passphrase, unsupported security, AP out of range, and so on). + +The sequence: + +1. **Look up the BSS.** The SSID is matched against the scan table, strongest + signal first. If it is not there, one scan is run automatically and the + lookup retried, so `wifi connect` works without scanning first. +2. **Negotiate ciphers.** The AP's RSN element (kept verbatim in the scan + table) picks the pairwise cipher and AKM. CCMP is preferred over GCMP, + plain PSK over PSK-SHA256. +3. **Derive the PMK.** PBKDF2-HMAC-SHA1 over the passphrase with the SSID as + salt, 4096 iterations. A 64-character hex string is taken as a raw PSK + instead. +4. **Bring up firmware contexts.** PHY, MAC, binding and station, then one TX + queue on the management TID. +5. **Authenticate and associate.** Open-system authentication, then an + association request carrying the SSID, supported rates and, for encrypted + networks, the RSN element the supplicant built. Both are retransmitted up + to four times at 400 ms. +6. **Run the 4-way handshake.** EAPOL-Key messages 1-4, then the pairwise key + and the group key go into the firmware with `ADD_STA_KEY`. +7. **Report the link up.** Only now does `NetIf` see `wlan0` as usable. + +## The data path + +Once associated the driver translates between 802.11 and Ethernet II: + +- **TX** - an Ethernet frame becomes a to-DS 802.11 data header plus an + RFC 1042 LLC/SNAP shim carrying the EtherType. The protected bit is set + once keys are installed and the firmware does the CCMP encryption. +- **RX** - the firmware decrypts and strips the MIC but leaves the 8-byte + CCMP header, which is skipped; the LLC/SNAP shim is replaced by an Ethernet + header built from addresses 1 and 3. EAPOL frames are diverted to the + supplicant instead. + +One TX queue carries management frames, EAPOL and non-QoS data. The firmware +maps non-QoS data onto the management TID anyway, and the driver never +negotiates block-ack sessions that would need per-TID queues. + +### Threading + +The RX path runs under `IwxProcessEvents()`'s reentrancy guard and **must not +send a host command** - the command's completion is pumped by the very +function it would be re-entering. Anything needing a command (post-association +context updates, key installation, and therefore the whole EAPOL handshake) is +queued and applied from `IwxConnectService()`, which every idling core calls +and which is itself serialized. Transmitting is safe from either context: it +only writes a descriptor and rings the doorbell. + +### Never wait on the clock under a spinlock + +`kcp::Spinlock::Acquire()` does `cli`, and `Timekeeping::GetMilliseconds()` is +driven by the APIC timer interrupt. A wall-clock timeout inside a spinlock +therefore cannot expire: the counter never advances, the loop never ends, and +the machine locks solid with interrupts off - no mouse, no keyboard, no +scheduler. This is the same trap `ApicTimer.cpp` documents for the idle path. + +`IwxSendCmd` holds a lock across a wait for the firmware's reply, so that lock +is a `kcp::Mutex` (which keeps interrupts enabled), and the wait carries a spin +cap as well as the clock check so it terminates even if the clock is somehow +stuck. It also bails immediately once the firmware is known to have asserted, +because nothing after that will ever be answered. + +A firmware command that goes unanswered now dumps the firmware's own error +table on the first timeout - that names the command that asserted - and gives +up on the adapter after three, rather than stalling for seconds per command. + +### Sample the clock after the work, not before it + +`IwxConnectService()` sends host commands, and every one of them is a round +trip to the adapter that takes real milliseconds. Each step also stamps the +timestamp its deadline is measured from - `EnterState()` sets +`g_stateEnteredMs`, transmitting sets `g_lastTxMs`. A `now` read at the top of +the pass is therefore *older* than the stamps it is about to be compared +against, and because these are unsigned counters, `now - g_stateEnteredMs` +wraps to about 2^64 instead of going negative. Every deadline in the pass then +reads as long expired. + +The symptom was a join that failed the instant it succeeded: + +``` +WiFi: [OK] Associated, AID 3 +WiFi: [WARNING] Connection failed: timed out while joining the network +WiFi: [INFO] EAPOL frame received (99 bytes) +``` + +The two post-association context commands moved the clock forward, the timeout +check compared a stale `now` against the `g_stateEnteredMs` they had just set, +and the contexts came down before the access point's first EAPOL frame could +arrive - which is why message 1 shows up *after* the failure. So the clock is +read only once the command-sending work in the pass is done, and the +comparisons go through `Elapsed()`, which refuses to underflow. + +### Teardown unwinds contexts in the order they depend on each other + +Firmware 89 asserts when a context is taken away while another still points at +it, and the assert names the command rather than the reason. Three of these +have been hit so far: + +| UMAC error | Cause | +|---|---| +| 0x2010330F | PHY binding and link activation folded into one LINK_CONFIG | +| 0x2010330E | link removed while its station still existed | +| 0x2000320F | link deactivated while the MAC was still marked associated | + +The last one is the teardown side of the same rule. While +`MAC_CONFIG.is_assoc` is set, the firmware's MAC context owns the link carrying +the BSS, so `TearDown()` sends a `MAC_CONFIG` MODIFY clearing `is_assoc` first, +and only then removes the station, deactivates the link, removes the link, +removes the MAC and drops the PHY context - the order +`iwl_mvm_mld_vif_cfg_changed_station` and the paths below it use on the way +down. `tests/wifi/ap_mlme.py` pins that order. + +## The interface registry + +`Net::NetIf` replaced the Ethernet layer's direct calls into the E1000 +drivers. Drivers register a name, a kind, and three function pointers; the +stack sends through `NetIf::Active()`, which prefers a wired interface with a +link and otherwise takes the first interface reporting one. A Wi-Fi-only +machine therefore has no link until it joins a network, and a machine with a +cable plugged in keeps using it. + +`SYS_NETSTATUS` reports whichever interface is active, so `ifconfig` shows the +wireless counters once Wi-Fi is carrying traffic. + +## What is supported + +| | | +|---|---| +| Open networks | yes | +| WPA2-PSK, CCMP or GCMP | yes | +| WPA2-PSK-SHA256 | yes | +| WPA3 transition mode (PSK advertised alongside SAE) | yes, joins via PSK | +| Group key rekeying | yes | +| WPA3-only (SAE) | no | +| Management frame protection required (MFPR) | no | +| WEP, original WPA / TKIP | no | +| 802.1X enterprise (EAP) | no | +| Block-ack aggregation, HT/VHT/HE rates | no - legacy rates only | + +SAE needs finite-field or elliptic-curve arithmetic that does not belong in +this kernel, and MFP needs BIP. Both are rejected up front with a specific +log line rather than failing partway through a handshake. Mixed WPA/WPA2 +networks that still broadcast under TKIP are refused for the same reason: the +pairwise key would install but every broadcast frame would be dropped, which +looks like a working connection that cannot get a DHCP lease. + +## Crypto + +`kernel/src/Libraries/Crypto.cpp` exists because the supplicant runs in the +kernel and BearSSL is a userspace library. It provides SHA-1, SHA-256, HMAC +over both, PBKDF2-HMAC-SHA1, AES-128/256, RFC 3394 key wrap/unwrap and +AES-CMAC. It is not a general-purpose crypto library and should not be used +as one. + +## Testing + +`./tests/wifi/run.sh` compiles the shipping sources for the host against a +small shim and drives them from Python. It is the real `Crypto.cpp`, +`Wpa.cpp` and `IwxConnect.cpp`, not a copy, with only the transport stubbed. + +- **Crypto primitives** against the published vectors - FIPS-197 for AES, + RFC 2202/4231 for HMAC, RFC 3394 for key wrap, RFC 4493 for CMAC, and the + IEEE 802.11i Annex H.4 WPA passphrase vectors for PBKDF2. +- **The supplicant** against an independent authenticator using `hashlib` and + `cryptography`: messages 2 and 4 carry MICs that verify under a PTK the AP + derived itself, the installed TK and GTK match the AP's, a wrong passphrase + produces a MIC the AP rejects, group rekeys and message-3 retransmissions + are answered, and RSN negotiation picks the right suites across eight + real-world information elements. +- **The MLME and data path** against a simulated AP that decodes every frame + the driver emits: the authentication request, the association request and + its elements (SSID, rates, capabilities, RSN), the handshake carried inside + real 802.11 data frames, key installation arguments, and the encapsulation + both ways - to-DS addressing, the protected bit, LLC/SNAP, sequence numbers, + broadcast delivery, and the filtering of foreign-BSSID and null-data frames. + Also the branches: open networks, retransmission and give-up when the AP is + silent, authentication and association rejections, and an AP-initiated + deauthentication bringing the link down. +- **Firmware context ordering** - the MLD command sizes and field offsets + against the decoded Linux trace, and the order the teardown unwinds the + contexts in, which is what the asserts above are about. + +The harness clock advances on every host command +(`CMD_ROUND_TRIP_MS` in `mlme_harness.cpp`) rather than standing still. That +detail matters: a frozen clock makes every elapsed-time comparison in the +service loop trivially true or trivially false, and hid the underflow described +under "Sample the clock after the work, not before it" - the host tests passed +while the adapter could not join a network at all. Anything that reads +`Timekeeping::GetMilliseconds()` should be tested with time actually moving. + +What is left needs the adapter, because it is the firmware's opinion rather +than the driver's logic: whether the firmware accepts the TX command and TFD +layout and actually radiates the frames, whether `ADD_STA_KEY` installs the +keys the driver asks for, whether the RX MPDU descriptor is read correctly off +real receptions, and whether association succeeds against a real AP's timing +and rate expectations. None of that can be exercised in QEMU, which has no +AX210-family device to emulate. diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 190b865..47eb71e 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 53 +#define MONTAUK_BUILD_NUMBER 69 diff --git a/kernel/src/Api/Device.hpp b/kernel/src/Api/Device.hpp index 530300b..fb28000 100644 --- a/kernel/src/Api/Device.hpp +++ b/kernel/src/Api/Device.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -127,6 +128,11 @@ namespace montauk::abi { if (Drivers::Net::E1000E::IsInitialized()) { add(5, "Intel E1000E", "Gigabit Ethernet (82574L)"); } + if (Drivers::Net::Wifi::IsPresent()) { + add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp() + ? "802.11 wireless (connected)" + : "802.11 wireless"); + } // Display (category 6) if (Drivers::Graphics::IntelGPU::IsInitialized()) { diff --git a/kernel/src/Api/Net.hpp b/kernel/src/Api/Net.hpp index 94b79a3..b13a4dc 100644 --- a/kernel/src/Api/Net.hpp +++ b/kernel/src/Api/Net.hpp @@ -15,8 +15,10 @@ #include #include #include +#include #include #include +#include #include "Syscall.hpp" @@ -96,12 +98,9 @@ namespace montauk::abi { out->subnetMask = Net::GetSubnetMask(); out->gateway = Net::GetGateway(); - const uint8_t* mac = nullptr; - if (Drivers::Net::E1000::IsInitialized()) { - mac = Drivers::Net::E1000::GetMacAddress(); - } else if (Drivers::Net::E1000E::IsInitialized()) { - mac = Drivers::Net::E1000E::GetMacAddress(); - } + // Whichever interface is carrying traffic; on a Wi-Fi link this is the + // adapter's MAC, not an idle Ethernet port's. + const uint8_t* mac = ::Net::NetIf::ActiveMac(); if (mac) { for (int i = 0; i < 6; i++) out->macAddress[i] = mac[i]; } else { @@ -122,6 +121,9 @@ namespace montauk::abi { dst[i] = '\0'; } + // Reports whichever interface is currently carrying traffic, so a machine + // that joined a Wi-Fi network sees the wireless counters here rather than + // an idle Ethernet port. static void Sys_NetStatus(NetStatus* out) { if (out == nullptr) return; @@ -133,15 +135,24 @@ namespace montauk::abi { out->txPackets = 0; CopyNetStatusDriver(out->driver, "No adapter"); - if (Drivers::Net::E1000::IsInitialized()) { - out->initialized = 1; - out->linkUp = Drivers::Net::E1000::IsLinkUp() ? 1 : 0; + const auto* iface = ::Net::NetIf::Active(); + if (iface == nullptr) return; + + out->initialized = 1; + out->linkUp = iface->IsLinkUp() ? 1 : 0; + + if (iface->Type == ::Net::NetIf::Kind::Wireless) { + WifiInfo wi; + if (Drivers::Net::Wifi::GetInfo(&wi) == 0) { + out->rxPackets = wi.rxPackets; + out->txPackets = wi.txPackets; + } + CopyNetStatusDriver(out->driver, "Intel Wi-Fi (wlan0)"); + } else if (Drivers::Net::E1000::IsInitialized()) { out->rxPackets = Drivers::Net::E1000::GetRxPacketCount(); out->txPackets = Drivers::Net::E1000::GetTxPacketCount(); CopyNetStatusDriver(out->driver, "Intel 82540EM"); } else if (Drivers::Net::E1000E::IsInitialized()) { - out->initialized = 1; - out->linkUp = Drivers::Net::E1000E::IsLinkUp() ? 1 : 0; out->polling = Drivers::Net::E1000E::RequiresPolling() ? 1 : 0; out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount(); out->txPackets = Drivers::Net::E1000E::GetTxPacketCount(); diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index c35329c..f9055ad 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -671,6 +671,26 @@ namespace montauk::abi { uint16_t beaconInterval; // TU }; + // Association progress reported in WifiInfo.connState. + static constexpr uint32_t WIFI_CONN_IDLE = 0; + static constexpr uint32_t WIFI_CONN_CONTEXTS_UP = 1; + static constexpr uint32_t WIFI_CONN_AUTHENTICATING = 2; + static constexpr uint32_t WIFI_CONN_AUTHENTICATED = 3; + static constexpr uint32_t WIFI_CONN_ASSOCIATING = 4; + static constexpr uint32_t WIFI_CONN_ASSOCIATED = 5; + static constexpr uint32_t WIFI_CONN_HANDSHAKING = 6; + static constexpr uint32_t WIFI_CONN_CONNECTED = 7; + static constexpr uint32_t WIFI_CONN_FAILED = 8; + + // Negative results from SYS_WIFI_CONNECT. + static constexpr int WIFI_ERR_NO_ADAPTER = -1; // no adapter, or not ready + static constexpr int WIFI_ERR_NOT_FOUND = -2; // SSID absent from the scan + static constexpr int WIFI_ERR_NEED_KEY = -3; // encrypted, no passphrase + static constexpr int WIFI_ERR_UNSUPPORTED = -4; // WPA3-SAE, WEP, enterprise + static constexpr int WIFI_ERR_AUTH = -5; // key exchange rejected + static constexpr int WIFI_ERR_TIMEOUT = -6; // AP never answered + static constexpr int WIFI_ERR_FAILED = -7; // anything else + // Adapter status (returned by SYS_WIFI_INFO). struct WifiInfo { uint8_t mac[6]; @@ -682,7 +702,12 @@ namespace montauk::abi { char fwVersion[32]; uint64_t rxPackets; uint32_t fwErrors; - uint32_t connState; // 0 idle, >0 connection setup in progress + uint32_t connState; // WIFI_CONN_* + uint64_t txPackets; + char ssid[36]; // network joined, empty when disconnected + uint8_t bssid[6]; + uint8_t connected; // 1 once the link can carry IP traffic + uint8_t channel; }; struct ThermalInfo { diff --git a/kernel/src/Drivers/Net/Wifi/Ieee80211.hpp b/kernel/src/Drivers/Net/Wifi/Ieee80211.hpp new file mode 100644 index 0000000..d9a4f19 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/Ieee80211.hpp @@ -0,0 +1,149 @@ +/* + * Ieee80211.hpp + * 802.11 frame and RSN constants shared by the MLME and the supplicant. + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Drivers::Net::Wifi { + + // ========================================================================= + // Frame control + // ========================================================================= + + constexpr uint16_t IEEE80211_FC0_TYPE_MASK = 0x0c; + constexpr uint16_t IEEE80211_FC0_TYPE_MGT = 0x00; + constexpr uint16_t IEEE80211_FC0_TYPE_CTL = 0x04; + constexpr uint16_t IEEE80211_FC0_TYPE_DATA = 0x08; + constexpr uint16_t IEEE80211_FC0_SUBTYPE_MASK = 0xf0; + + // Management subtypes (already shifted into the frame-control byte) + constexpr uint8_t IEEE80211_SUBTYPE_ASSOC_REQ = 0x00; + constexpr uint8_t IEEE80211_SUBTYPE_ASSOC_RESP = 0x10; + constexpr uint8_t IEEE80211_SUBTYPE_REASSOC_REQ = 0x20; + constexpr uint8_t IEEE80211_SUBTYPE_PROBE_REQ = 0x40; + constexpr uint8_t IEEE80211_SUBTYPE_PROBE_RESP = 0x50; + constexpr uint8_t IEEE80211_SUBTYPE_BEACON = 0x80; + constexpr uint8_t IEEE80211_SUBTYPE_DISASSOC = 0xa0; + constexpr uint8_t IEEE80211_SUBTYPE_AUTH = 0xb0; + constexpr uint8_t IEEE80211_SUBTYPE_DEAUTH = 0xc0; + constexpr uint8_t IEEE80211_SUBTYPE_ACTION = 0xd0; + + // Data subtypes + constexpr uint8_t IEEE80211_SUBTYPE_DATA = 0x00; + constexpr uint8_t IEEE80211_SUBTYPE_QOS_DATA = 0x80; + constexpr uint8_t IEEE80211_SUBTYPE_NULL = 0x40; + + // Frame control byte 1 + constexpr uint8_t IEEE80211_FC1_TO_DS = 0x01; + constexpr uint8_t IEEE80211_FC1_FROM_DS = 0x02; + constexpr uint8_t IEEE80211_FC1_MORE_FRAG = 0x04; + constexpr uint8_t IEEE80211_FC1_RETRY = 0x08; + constexpr uint8_t IEEE80211_FC1_PWR_MGT = 0x10; + constexpr uint8_t IEEE80211_FC1_MORE_DATA = 0x20; + constexpr uint8_t IEEE80211_FC1_PROTECTED = 0x40; + + constexpr uint32_t IEEE80211_HDR_LEN = 24; + constexpr uint32_t IEEE80211_QOS_HDR_LEN = 26; + + // Capability bits in beacons / association requests + constexpr uint16_t IEEE80211_CAPINFO_ESS = 0x0001; + constexpr uint16_t IEEE80211_CAPINFO_PRIVACY = 0x0010; + constexpr uint16_t IEEE80211_CAPINFO_SHORT_PREAMBLE = 0x0020; + constexpr uint16_t IEEE80211_CAPINFO_SHORT_SLOT = 0x0400; + + // Authentication algorithms + constexpr uint16_t IEEE80211_AUTH_ALG_OPEN = 0; + constexpr uint16_t IEEE80211_AUTH_ALG_SAE = 3; + + // Reason / status codes used here + constexpr uint16_t IEEE80211_STATUS_SUCCESS = 0; + constexpr uint16_t IEEE80211_REASON_UNSPECIFIED = 1; + constexpr uint16_t IEEE80211_REASON_DEAUTH_LEAVING = 3; + constexpr uint16_t IEEE80211_REASON_MIC_FAILURE = 14; + constexpr uint16_t IEEE80211_REASON_4WAY_TIMEOUT = 15; + + // ========================================================================= + // Information elements + // ========================================================================= + + constexpr uint8_t IEEE80211_ELEMID_SSID = 0; + constexpr uint8_t IEEE80211_ELEMID_RATES = 1; + constexpr uint8_t IEEE80211_ELEMID_DSPARMS = 3; + constexpr uint8_t IEEE80211_ELEMID_XRATES = 50; + constexpr uint8_t IEEE80211_ELEMID_RSN = 48; + constexpr uint8_t IEEE80211_ELEMID_HTCAPS = 45; + constexpr uint8_t IEEE80211_ELEMID_VENDOR = 221; + + // RSN cipher / AKM suite selectors (00-0F-AC:) + constexpr uint32_t RSN_OUI = 0x000fac; + + constexpr uint8_t RSN_CIPHER_NONE = 0; + constexpr uint8_t RSN_CIPHER_WEP40 = 1; + constexpr uint8_t RSN_CIPHER_TKIP = 2; + constexpr uint8_t RSN_CIPHER_CCMP = 4; + constexpr uint8_t RSN_CIPHER_WEP104 = 5; + constexpr uint8_t RSN_CIPHER_BIP_CMAC = 6; + constexpr uint8_t RSN_CIPHER_GCMP = 8; + constexpr uint8_t RSN_CIPHER_GCMP_256 = 9; + constexpr uint8_t RSN_CIPHER_CCMP_256 = 10; + + constexpr uint8_t RSN_AKM_8021X = 1; + constexpr uint8_t RSN_AKM_PSK = 2; + constexpr uint8_t RSN_AKM_FT_PSK = 4; + constexpr uint8_t RSN_AKM_PSK_SHA256 = 6; + constexpr uint8_t RSN_AKM_SAE = 8; + constexpr uint8_t RSN_AKM_FT_SAE = 9; + + // RSN capabilities + constexpr uint16_t RSN_CAP_MFPR = 0x0040; // management protection required + constexpr uint16_t RSN_CAP_MFPC = 0x0080; // management protection capable + + // ========================================================================= + // LLC/SNAP: the 8-byte shim between an 802.11 data payload and an + // Ethernet II EtherType. + // ========================================================================= + + struct LlcSnapHeader { + uint8_t dsap; // 0xaa + uint8_t ssap; // 0xaa + uint8_t control; // 0x03 + uint8_t oui[3]; // 00-00-00 (RFC 1042) + uint16_t etherType; // big endian + } __attribute__((packed)); + + constexpr uint32_t LLC_SNAP_LEN = 8; + constexpr uint8_t LLC_SNAP_LSAP = 0xaa; + + constexpr uint16_t ETHERTYPE_IPV4 = 0x0800; + constexpr uint16_t ETHERTYPE_ARP = 0x0806; + constexpr uint16_t ETHERTYPE_EAPOL = 0x888e; + + // ========================================================================= + // Byte helpers (802.11 fields are little endian, EAPOL is big endian) + // ========================================================================= + + inline uint16_t Get16Le(const uint8_t* p) { + return (uint16_t)(p[0] | ((uint16_t)p[1] << 8)); + } + inline void Put16Le(uint8_t* p, uint16_t v) { + p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); + } + inline uint16_t Get16Be(const uint8_t* p) { + return (uint16_t)(((uint16_t)p[0] << 8) | p[1]); + } + inline void Put16Be(uint8_t* p, uint16_t v) { + p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v; + } + + inline bool AddrEqual(const uint8_t* a, const uint8_t* b) { + for (int i = 0; i < 6; i++) if (a[i] != b[i]) return false; + return true; + } + inline bool AddrIsBroadcast(const uint8_t* a) { + for (int i = 0; i < 6; i++) if (a[i] != 0xff) return false; + return true; + } +} diff --git a/kernel/src/Drivers/Net/Wifi/Iwx.hpp b/kernel/src/Drivers/Net/Wifi/Iwx.hpp index 84acb96..6f679b4 100644 --- a/kernel/src/Drivers/Net/Wifi/Iwx.hpp +++ b/kernel/src/Drivers/Net/Wifi/Iwx.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "IwxReg.hpp" namespace Drivers::Net::Wifi { @@ -85,6 +86,13 @@ namespace Drivers::Net::Wifi { // Rings // ========================================================================= + // Frames waiting in a TX queue are staged in single pages, one per + // in-flight frame. The descriptor ring itself has IWX_TX_RING_COUNT + // entries (the size the firmware was told about), but outstanding frames + // are capped at the number of staging pages so a slot can never be reused + // while the hardware is still reading it. + constexpr uint32_t IWX_TX_STAGE_SLOTS = 64; + struct IwxTxRing { int Qid = 0; IwxDma Desc; // IwxTfhTfd[IWX_TX_RING_COUNT] @@ -94,6 +102,13 @@ namespace Drivers::Net::Wifi { uint32_t Cur = 0; // ring slot (0..count-1) uint32_t CurHw = 0; // hardware index (0..65535) uint32_t Queued = 0; + + // TX staging (data/management queues only; the command queue stages + // into its per-slot Cmd area instead). + uint8_t* Stage[IWX_TX_STAGE_SLOTS] = {}; + uint64_t StagePhys[IWX_TX_STAGE_SLOTS] = {}; + uint32_t StageSlots = 0; + bool Active = false; // configured with the firmware }; struct IwxRxRing { @@ -181,7 +196,11 @@ namespace Drivers::Net::Wifi { IwxRxRing RxQ; IwxTxRing CmdQ; // queue 0: host commands - IwxTxRing MgmtQ; // queue 1: management frames (connect path) + // One queue carries management frames, EAPOL and non-QoS data: the + // firmware maps non-QoS data onto the management TID anyway, and the + // driver never negotiates block-ack sessions that would need per-TID + // queues. + IwxTxRing MgmtQ; // ALIVE / init-complete tracking (set from notification processing) volatile bool AliveIntr = false; @@ -194,18 +213,29 @@ namespace Drivers::Net::Wifi { uint32_t UmacErrorTable = 0; uint32_t LmacErrorTable = 0; uint32_t LastCmdId = 0; + uint32_t LastCmdLen = 0; + uint8_t LastCmdPayload[192] = {}; // dumped on a firmware assert bool LtrEnabled = false; // PCIe LTR capability advertised // Synchronous-command bookkeeping (commands are fully serialized) - kcp::Spinlock CmdLock; // serializes SendCmd callers + // A Mutex, deliberately not a Spinlock: kcp::Spinlock disables + // interrupts, and this lock is held across a wall-clock wait for the + // firmware's response. With interrupts off the millisecond counter + // (driven by the APIC timer interrupt) never advances, so the timeout + // could never expire and one unanswered command would hang the machine + // with interrupts disabled. Every caller is process or idle context; + // the hard IRQ only latches WorkPending. + kcp::Mutex CmdLock; // serializes SendCmd callers volatile bool CmdDone = false; volatile bool CmdWantResp = false; uint8_t CmdRespBuf[4096]; volatile uint32_t CmdRespLen = 0; uint32_t CmdIdx = 0; // ring slot of in-flight command - // Reentrancy guard for ProcessEvents - volatile bool InProcessEvents = false; + // Reentrancy guard for ProcessEvents. Interrupts stay enabled while + // commands wait, so two cores can genuinely race here; a plain bool + // would let both through. + std::atomic_flag InProcessEvents = ATOMIC_FLAG_INIT; IwxNvmData Nvm; IwxChannel Channels[IWX_MAX_CHANNELS_TRACKED]; @@ -215,8 +245,19 @@ namespace Drivers::Net::Wifi { volatile bool ScanActive = false; volatile bool ScanCompleted = false; + // Band of the BSS currently being joined; picks the legacy rate used + // for management frames. + bool Is5GHz = false; + + // Serializes frame TX (the netif and the MLME both queue frames). + // Process/idle context only, so it keeps interrupts enabled too. + kcp::Mutex TxLock; + // Statistics/diagnostics uint64_t RxPackets = 0; + uint64_t TxPackets = 0; + uint64_t TxFailures = 0; + uint64_t RxDataPackets = 0; uint64_t FwErrors = 0; }; @@ -271,8 +312,28 @@ namespace Drivers::Net::Wifi { bool IwxCheckRfKill(); - // TX queue management (used by connect path) - bool IwxEnableTxq(int staId, int qid, int tid); + // TX queue management (used by the connect path). On the new data-path + // API the firmware picks the queue number and reports it back, so `qid` is + // only a hint; the assigned value lands in ring.Qid. + bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid); + void IwxDisableTxq(IwxTxRing& ring, int staId, int tid); + + // Transmit one 802.11 frame. The header and payload are copied into a + // staging page, wrapped in a TX_CMD and handed to the queue. + // encrypt - let the firmware apply the installed key (data frames + // after the handshake); cleared for auth/assoc/EAPOL + // fixedRate - send at the lowest basic rate instead of letting the + // firmware's rate control pick (required for management + // frames, which have no rate table yet) + bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen, + const uint8_t* payload, uint32_t payloadLen, + bool encrypt, bool fixedRate); + + // Install or remove a hardware key for the connected station. + bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx, + bool pairwise, uint8_t cipher, const uint8_t* rsc); + bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher, + uint32_t keyLen); // ========================================================================= // Firmware file parsing (IwxFw.cpp) @@ -300,15 +361,51 @@ namespace Drivers::Net::Wifi { void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel, int8_t rssiDbm); + // TX completion, called from RX processing for a TX_CMD response. + void IwxTxComplete(int qid, int idx, uint32_t status); + // ========================================================================= - // Connect groundwork (IwxConnect.cpp) - UNTESTED scaffolding + // Connect / MLME (IwxConnect.cpp) // ========================================================================= + // Association states reported through WifiInfo.connState. + enum class IwxConnStateId : int { + Idle = 0, + ContextsUp, + Authenticating, + Authenticated, + Associating, + Associated, // 802.11 link up; open networks stop here + Handshaking, // WPA 4-way in progress + Connected, // keys installed (or open); data can flow + Failed, + }; + + // `rsnIe` is the AP's RSN element body (may be null for open networks). bool IwxConnectStart(const uint8_t* bssid, uint8_t channel, bool is5GHz, - const char* ssid); + const char* ssid, const char* password, + const uint8_t* rsnIe, uint32_t rsnIeLen, + uint16_t beaconInterval, uint8_t dtimPeriod); void IwxConnectAbort(); + // Why the last IwxConnectStart() refused: false means the radio or the + // firmware failed, true means the network's security is unsupported. The + // two need very different advice, so they must not be conflated. + bool IwxConnectRefusedForSecurity(); void IwxConnectRxMgmt(const uint8_t* frame, uint32_t len); + // Inbound 802.11 data frame from the RX path. + void IwxConnectRxData(const uint8_t* frame, uint32_t len); // Apply state changes the RX path queued (it cannot send commands itself). void IwxConnectService(); int IwxConnectState(); + + // Link status for the network stack. + bool IwxLinkUp(); // associated and keyed + const uint8_t* IwxConnectBssid(); + const char* IwxConnectSsid(); + + // Send an Ethernet frame over the air (called by the netif). + bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len); + + // Sink for decapsulated Ethernet frames, implemented by Wifi.cpp. + void WifiRxEthernet(const uint8_t* frame, uint32_t len); } diff --git a/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp index e104c1d..14dacaf 100644 --- a/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp +++ b/kernel/src/Drivers/Net/Wifi/IwxConnect.cpp @@ -1,38 +1,38 @@ /* * IwxConnect.cpp - * Association groundwork: PHY/MAC context setup, station add, and the - * open-system authentication + association exchange. + * 802.11 MLME and the data path: firmware context setup, open-system + * authentication, association, and the 802.11 <-> Ethernet translation that + * lets the network stack treat the adapter like any other NIC. * - * STATUS: this path is scaffolding. It builds the firmware contexts the - * same way iwlwifi does and drives the 802.11 state machine far enough to - * authenticate and associate with an open network, but it has never been - * exercised on hardware, and the WPA2 key exchange is deliberately not - * implemented (see WifiConnectSecurity notes in Wifi.cpp). Scanning is the - * supported operation today. + * Encrypted networks hand off to the supplicant in Wpa.cpp once association + * completes; the link is only reported up after the pairwise key is in the + * firmware. + * + * Threading: the RX path runs under IwxProcessEvents()'s reentrancy guard + * and must never send a host command, because the command's completion is + * pumped by the very function it would be re-entering. Anything that needs + * a command (context updates, key installation, and therefore the whole + * EAPOL handshake) is queued here and applied from IwxConnectService(), + * which the idle loop calls. Frame transmission is safe from either + * context: it only writes a descriptor and rings the doorbell. * * Copyright (c) 2026 Daniel Hammer */ #include "Iwx.hpp" +#include "Ieee80211.hpp" +#include "Wpa.hpp" #include #include #include +#include #include using namespace Kt; namespace Drivers::Net::Wifi { - // Connection state machine. - enum class ConnState : int { - Idle = 0, - ContextsUp, // PHY/MAC/binding/STA programmed, ready to authenticate - Authenticating, - Authenticated, - Associating, - Associated, - Failed, - }; + using ConnState = IwxConnStateId; static ConnState g_state = ConnState::Idle; static uint8_t g_bssid[6] = {}; @@ -41,6 +41,16 @@ namespace Drivers::Net::Wifi { static char g_ssid[33] = {}; static uint8_t g_ssidLen = 0; static uint16_t g_aid = 0; + static uint16_t g_beaconInterval = 100; // TU, from the beacon + static uint8_t g_dtimPeriod = 1; // beacons per DTIM + static uint16_t g_seqNum = 0; + + static uint16_t NextSeq() { + return __atomic_fetch_add(&g_seqNum, 1, __ATOMIC_RELAXED); + } + + static bool g_secured = false; + static WpaConfig g_wpaCfg = {}; static constexpr uint32_t MAC_ID = 0; static constexpr uint32_t MAC_COLOR = 0; @@ -49,15 +59,42 @@ namespace Drivers::Net::Wifi { static bool g_phyActive = false; static bool g_macActive = false; + // Whether the MAC context is currently marked associated. The teardown + // has to undo that before it touches the link the association hangs on. + static bool g_macAssoc = false; static bool g_bindingActive = false; + static bool g_linkActive = false; static bool g_staActive = false; - static bool g_mgmtQueueUp = false; + static bool g_keysInstalled = false; + // Distinguishes "this network's security is unsupported" from "the radio + // would not come up", which need different advice to the user. + static bool g_refusedForSecurity = false; - // Work discovered while parsing an inbound frame, applied later from the - // idle loop: RX processing must not send commands (it would re-enter the - // event pump that its own completion depends on). + // Work discovered while parsing an inbound frame, applied from the idle + // loop. See the threading note at the top of the file. static volatile bool g_postAssocPending = false; static volatile bool g_teardownPending = false; + static volatile bool g_sendAssocPending = false; + + // Timers for retransmission and give-up. + static uint64_t g_stateEnteredMs = 0; + static uint64_t g_lastTxMs = 0; + static int g_tries = 0; + + static constexpr uint64_t MGMT_RETRY_MS = 400; + static constexpr int MGMT_MAX_TRIES = 4; + static constexpr uint64_t CONNECT_TIMEOUT_MS = 12000; + + // Inbound EAPOL frames waiting for the service loop. + struct PendingEapol { + uint8_t Data[512]; + uint32_t Len; + }; + static constexpr int EAPOL_QUEUE_DEPTH = 4; + static PendingEapol g_eapolQ[EAPOL_QUEUE_DEPTH]; + static volatile uint32_t g_eapolHead = 0; // next slot to fill + static volatile uint32_t g_eapolTail = 0; // next slot to drain + static kcp::Spinlock g_eapolLock; static uint8_t FwValidRxAntConn() { uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_RX_CHAIN) @@ -66,30 +103,65 @@ namespace Drivers::Net::Wifi { return ant; } + static void EnterState(ConnState s) { + g_state = s; + g_stateEnteredMs = Timekeeping::GetMilliseconds(); + g_tries = 0; + } + + // "At least `ms` have passed since `since`", written so that it cannot + // underflow. These are unsigned millisecond counters and the timestamps + // are stamped as work completes, so a `since` from the future relative to + // `now` is a real possibility rather than a theoretical one; the plain + // subtraction wraps to an enormous number and the deadline fires at once. + static bool Elapsed(uint64_t now, uint64_t since, uint64_t ms) { + return now > since && now - since > ms; + } + // ========================================================================= // Firmware contexts // ========================================================================= static bool PhyCtxtCmd(uint32_t action) { - IwxPhyContextCmd cmd = {}; - cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); - cmd.action = action; - cmd.lmac_id = (!g_is5GHz - || !IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_CDB_SUPPORT)) - ? IWX_LMAC_24G_INDEX : IWX_LMAC_5G_INDEX; - cmd.ci.band = g_is5GHz ? IWX_PHY_BAND_5 : IWX_PHY_BAND_24; - cmd.ci.channel = g_channel; - cmd.ci.width = IWX_PHY_VHT_CHANNEL_MODE20; - cmd.ci.ctrl_pos = IWX_PHY_VHT_CTRL_POS_1_BELOW; + uint32_t lmacId = (!g_is5GHz + || !IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_CDB_SUPPORT)) + ? IWX_LMAC_24G_INDEX : IWX_LMAC_5G_INDEX; + uint8_t band = g_is5GHz ? IWX_PHY_BAND_5 : IWX_PHY_BAND_24; // From RLC_CONFIG v2 on, the chain configuration moved out of this // command into its own RLC command. + uint32_t rxchain = 0; if (IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_RLC_CONFIG_CMD) != 2) { - cmd.rxchain_info = (uint32_t)FwValidRxAntConn() << IWX_PHY_RX_CHAIN_VALID_POS; - cmd.rxchain_info |= 1u << IWX_PHY_RX_CHAIN_CNT_POS; - cmd.rxchain_info |= 1u << IWX_PHY_RX_CHAIN_MIMO_CNT_POS; + rxchain = (uint32_t)FwValidRxAntConn() << IWX_PHY_RX_CHAIN_VALID_POS; + rxchain |= 1u << IWX_PHY_RX_CHAIN_CNT_POS; + rxchain |= 1u << IWX_PHY_RX_CHAIN_MIMO_CNT_POS; } + // The channel-info layout depends on whether the firmware understands + // ultra-high-band channels; getting it wrong asserts the firmware and + // it stops answering commands from then on. + if (IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_ULTRA_HB_CHANNELS)) { + IwxPhyContextCmd cmd = {}; + cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); + cmd.action = action; + cmd.ci.channel = g_channel; + cmd.ci.band = band; + cmd.ci.width = IWX_PHY_VHT_CHANNEL_MODE20; + cmd.ci.ctrl_pos = IWX_PHY_VHT_CTRL_POS_1_BELOW; + cmd.lmac_id = lmacId; + cmd.rxchain_info = rxchain; + return IwxSendCmdPdu(IWX_PHY_CONTEXT_CMD, &cmd, sizeof(cmd)); + } + + IwxPhyContextCmdV1Chan cmd = {}; + cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); + cmd.action = action; + cmd.ci.band = band; + cmd.ci.channel = g_channel; + cmd.ci.width = IWX_PHY_VHT_CHANNEL_MODE20; + cmd.ci.ctrl_pos = IWX_PHY_VHT_CTRL_POS_1_BELOW; + cmd.lmac_id = lmacId; + cmd.rxchain_info = rxchain; return IwxSendCmdPdu(IWX_PHY_CONTEXT_CMD, &cmd, sizeof(cmd)); } @@ -106,79 +178,166 @@ namespace Drivers::Net::Wifi { &cmd, sizeof(cmd)); } - static bool MacCtxtCmd(uint32_t action, bool assoc) { - IwxMacCtxCmd cmd = {}; + // ========================================================================= + // MLD contexts + // + // This firmware implements the MLD API: a MAC, a link that binds that MAC + // to a PHY, and a station on that link. The legacy MAC_CONTEXT/BINDING/ + // ADD_STA commands are absent and asserting on them is what a wrong guess + // looks like. Field values below follow a host-command trace captured from + // Linux driving this same adapter; see tests/wifi/decode_iwl_trace.py. + // ========================================================================= + + static constexpr uint32_t LINK_ID = 0; + + static bool MacConfigCmd(uint32_t action, bool assoc) { + IwxMacConfigCmd cmd = {}; cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); cmd.action = action; cmd.mac_type = IWX_FW_MAC_TYPE_BSS_STA; - cmd.tsf_id = IWX_TSF_ID_A; - memcpy(cmd.node_addr, g_iwx.Nvm.HwAddr, 6); - memcpy(cmd.bssid_addr, g_bssid, 6); + memcpy(cmd.local_mld_addr, g_iwx.Nvm.HwAddr, 6); - // Basic rate masks: CCK 1/2/5.5/11 on 2.4 GHz, OFDM 6/12/24 everywhere. - // The firmware indexes these bitmaps against its own rate tables. - cmd.cck_rates = g_is5GHz ? 0 : 0x0f; + // Beacons are only wanted until the link is up; afterwards the + // firmware tracks them itself. + cmd.filter_flags = IWX_MAC_CFG_FILTER_ACCEPT_GRP; + if (!assoc) cmd.filter_flags |= IWX_MAC_CFG_FILTER_ACCEPT_BEACON; + + // We associate as a legacy station: no HE, no EHT. + cmd.he_support = 0; + cmd.eht_support = 0; + cmd.nic_not_ack_enabled = 0; + + cmd.client.is_assoc = assoc ? 1 : 0; + cmd.client.assoc_id = assoc ? g_aid : 0; + + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_MAC_CONF_GROUP, IWX_MAC_CONFIG_CMD), + &cmd, sizeof(cmd)); + } + + // EDCA parameters in the firmware's access-category order, with the FIFO + // numbering the trace shows (1..4, not 0..3). Before association Linux + // sends mac80211's conservative defaults (every AC contends like best + // effort, no TXOP); the WMM values only appear once the AP's parameters + // are known. + static void FillLinkEdca(IwxLinkConfigCmd& cmd, bool assoc) { + struct AcParams { uint16_t cwMin, cwMax, txop; uint8_t aifsn, fifo; }; + static constexpr AcParams kPre[IWX_AC_NUM] = { + { 15, 1023, 0, 7, 2 }, // background + { 15, 1023, 0, 3, 4 }, // best effort + { 15, 1023, 0, 2, 8 }, // video + { 15, 1023, 0, 2, 16 }, // voice + }; + static constexpr AcParams kWmm[IWX_AC_NUM] = { + { 15, 1023, 0, 7, 2 }, // background + { 15, 1023, 0, 3, 4 }, // best effort + { 7, 15, 3008, 2, 8 }, // video + { 3, 7, 1504, 2, 16 }, // voice + }; + const AcParams* kAc = assoc ? kWmm : kPre; + for (uint32_t i = 0; i < IWX_AC_NUM; i++) { + cmd.ac[i].cw_min = kAc[i].cwMin; + cmd.ac[i].cw_max = kAc[i].cwMax; + cmd.ac[i].aifsn = kAc[i].aifsn; + cmd.ac[i].fifos_mask = kAc[i].fifo; + cmd.ac[i].edca_txop = kAc[i].txop; + } + } + + // The three LINK_CONFIG shapes Linux sends, kept separate on purpose. In + // particular the firmware learns the link's PHY binding from a MODIFY of + // its own (mask 0, still inactive) before the MODIFY that activates the + // link; folding the binding and ACTIVE into one command asserts fw 89 + // (UMAC error 0x2010330F on the AX211). iwlwifi's "send it first with + // phy context ID" in __iwl_mvm_mld_assign_vif_chanctx is about this. + + static bool LinkAddCmd() { + IwxLinkConfigCmd cmd = {}; + cmd.action = IWX_FW_CTXT_ACTION_ADD; + cmd.link_id = LINK_ID; + cmd.mac_id = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); + cmd.phy_id = IWX_FW_CTXT_INVALID; + memcpy(cmd.local_link_addr, g_iwx.Nvm.HwAddr, 6); + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_MAC_CONF_GROUP, IWX_LINK_CONFIG_CMD), + &cmd, sizeof(cmd)); + } + + // The full link state, applied selectively by `modifyMask` -- mirrors + // iwl_mvm_link_changed, which fills every field on every modify and lets + // the mask say which ones the firmware should act on. Every value below + // is byte-for-byte what the Linux trace carries at the same stage; in + // particular `bi` is filled in from the very first modify (the firmware + // needs it to activate the link), while the DTIM interval, WMM EDCA, + // short slot and the RTS threshold only appear once associated. + static bool LinkModifyCmd(uint32_t modifyMask, bool active, bool assoc) { + IwxLinkConfigCmd cmd = {}; + cmd.action = IWX_FW_CTXT_ACTION_MODIFY; + cmd.link_id = LINK_ID; + cmd.mac_id = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); + cmd.phy_id = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); + memcpy(cmd.local_link_addr, g_iwx.Nvm.HwAddr, 6); + cmd.modify_mask = modifyMask; + cmd.active = active ? 1 : 0; + + // The trace sends the CCK basic rates even on a 5 GHz channel. + cmd.cck_rates = 0x0f; cmd.ofdm_rates = 0x15; cmd.cck_short_preamble = 0; - cmd.short_slot = 0; - cmd.filter_flags = IWX_MAC_FILTER_ACCEPT_GRP | IWX_MAC_FILTER_IN_BEACON; - cmd.qos_flags = IWX_MAC_QOS_FLG_UPDATE_EDCA; + cmd.short_slot = (assoc && g_is5GHz) ? 1 : 0; + cmd.protection_flags = 0; + cmd.qos_flags = IWX_MAC_QOS_FLG_TGN + | (assoc ? IWX_MAC_QOS_FLG_UPDATE_EDCA : 0); + FillLinkEdca(cmd, assoc); - // Default EDCA parameters, one entry per access category. - for (uint32_t i = 0; i < IWX_AC_NUM; i++) { - cmd.ac[i].cw_min = 15; - cmd.ac[i].cw_max = 1023; - cmd.ac[i].aifsn = 2; - cmd.ac[i].fifos_mask = (uint8_t)(1 << i); - cmd.ac[i].edca_txop = 0; + cmd.bi = g_beaconInterval; + if (assoc) { + cmd.dtim_interval = (uint32_t)g_beaconInterval * g_dtimPeriod; + cmd.frame_time_rts_th = 0x3ff; } - cmd.sta.is_assoc = assoc ? 1 : 0; - cmd.sta.bi = 100; - cmd.sta.dtim_interval = 100 * 3; - cmd.sta.listen_interval = 10; - cmd.sta.assoc_id = g_aid; - - return IwxSendCmdPdu(IWX_MAC_CONTEXT_CMD, &cmd, sizeof(cmd)); + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_MAC_CONF_GROUP, IWX_LINK_CONFIG_CMD), + &cmd, sizeof(cmd)); } - static bool BindingCmd(uint32_t action) { - IwxBindingCmd cmd = {}; - cmd.id_and_color = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); - cmd.action = action; - cmd.phy = IWX_FW_CMD_ID_AND_COLOR(PHY_ID, PHY_COLOR); - cmd.macs[0] = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); - for (uint32_t i = 1; i < IWX_MAX_MACS_IN_BINDING; i++) - cmd.macs[i] = IWX_FW_CTXT_INVALID; - cmd.lmac_id = (!g_is5GHz - || !IwxBitSet(g_iwx.Fw.Capa, IWX_UCODE_TLV_CAPA_CDB_SUPPORT)) - ? IWX_LMAC_24G_INDEX : IWX_LMAC_5G_INDEX; - - uint32_t status = 0; - if (!IwxSendCmdStatus(IWX_BINDING_CONTEXT_CMD, &cmd, sizeof(cmd), &status)) - return false; - return status == 0; + static bool LinkRemoveCmd() { + IwxLinkConfigCmd cmd = {}; + cmd.action = IWX_FW_CTXT_ACTION_REMOVE; + cmd.link_id = LINK_ID; + cmd.phy_id = IWX_FW_CTXT_INVALID; + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_MAC_CONF_GROUP, IWX_LINK_CONFIG_CMD), + &cmd, sizeof(cmd)); } - static bool AddStaCmd(bool update) { - IwxAddStaCmd cmd = {}; - cmd.add_modify = update ? 1 : 0; - cmd.mac_id_n_color = IWX_FW_CMD_ID_AND_COLOR(MAC_ID, MAC_COLOR); + static bool StaConfigCmd() { + IwxStaConfigCmd cmd = {}; cmd.sta_id = IWX_STATION_ID; - cmd.station_type = IWX_STA_TYPE_LINK; - memcpy(cmd.addr, g_bssid, 6); - cmd.tid_disable_tx = 0xffff; // aggregation disabled for now + cmd.link_id = LINK_ID; + // Not an MLD peer, so both addresses are simply the BSSID. + memcpy(cmd.peer_mld_address, g_bssid, 6); + memcpy(cmd.peer_link_address, g_bssid, 6); + cmd.station_type = 0; // normal link station + // assoc_id is only meaningful when we are the GO/AP configuring one + // of our clients. For a BSS client our own AID belongs in + // MAC_CONFIG.client.assoc_id; Linux leaves this field zero throughout + // the connection on the same firmware. + cmd.assoc_id = 0; + cmd.mfp = 0; + cmd.mimo = 0; + cmd.tx_ampdu_spacing = 0; + cmd.tx_ampdu_max_size = 0; - uint32_t status = 0; - if (!IwxSendCmdStatus(IWX_ADD_STA, &cmd, sizeof(cmd), &status)) - return false; - return (status & IWX_ADD_STA_STATUS_MASK) == IWX_ADD_STA_SUCCESS; + // STA_CONFIG is acknowledged with an EMPTY response -- Linux sends it + // as a plain pdu. Expecting a status word here misread acceptance as + // failure, and the teardown that followed removed a link that still + // had its station, asserting the firmware (UMAC error 0x2010330E). + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_MAC_CONF_GROUP, IWX_STA_CONFIG_CMD), + &cmd, sizeof(cmd)); } - static bool RemoveStaCmd() { - struct { uint8_t sta_id; uint8_t reserved[3]; } __attribute__((packed)) cmd = {}; + static bool StaRemoveCmd() { + IwxStaRemoveCmd cmd = {}; cmd.sta_id = IWX_STATION_ID; - return IwxSendCmdPdu(IWX_REMOVE_STA, &cmd, sizeof(cmd)); + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_MAC_CONF_GROUP, IWX_STA_REMOVE_CMD), + &cmd, sizeof(cmd)); } // Keep the firmware on our channel for the duration of the exchange. @@ -195,26 +354,318 @@ namespace Drivers::Net::Wifi { &cmd, sizeof(cmd)); } + // ========================================================================= + // Management frame construction + // ========================================================================= + + // All the management frames this driver sends are addressed to the AP, so + // the three address fields are always the same. + static uint32_t BuildMgmtHeader(uint8_t* hdr, uint8_t subtype) { + hdr[0] = (uint8_t)(IEEE80211_FC0_TYPE_MGT | subtype); + hdr[1] = 0; + Put16Le(hdr + 2, 0); // duration, set by firmware + memcpy(hdr + 4, g_bssid, 6); // addr1: receiver + memcpy(hdr + 10, g_iwx.Nvm.HwAddr, 6); // addr2: transmitter + memcpy(hdr + 16, g_bssid, 6); // addr3: BSSID + Put16Le(hdr + 22, (uint16_t)(NextSeq() << 4)); + return IEEE80211_HDR_LEN; + } + + static bool SendAuth() { + uint8_t hdr[IEEE80211_HDR_LEN]; + BuildMgmtHeader(hdr, IEEE80211_SUBTYPE_AUTH); + + uint8_t body[6]; + Put16Le(body + 0, IEEE80211_AUTH_ALG_OPEN); + Put16Le(body + 2, 1); // transaction sequence 1 + Put16Le(body + 4, IEEE80211_STATUS_SUCCESS); + + g_lastTxMs = Timekeeping::GetMilliseconds(); + return IwxTxFrame(g_iwx.MgmtQ, hdr, sizeof(hdr), body, sizeof(body), + false, true); + } + + // Supported-rate sets, in the IE encoding (bit 7 marks a basic rate). + static const uint8_t kRates24[] = { + 0x82, 0x84, 0x8b, 0x96, 0x0c, 0x12, 0x18, 0x24 + }; + static const uint8_t kXRates24[] = { 0x30, 0x48, 0x60, 0x6c }; + static const uint8_t kRates5[] = { + 0x8c, 0x12, 0x98, 0x24, 0xb0, 0x48, 0x60, 0x6c + }; + + static bool SendAssocReq() { + uint8_t hdr[IEEE80211_HDR_LEN]; + BuildMgmtHeader(hdr, IEEE80211_SUBTYPE_ASSOC_REQ); + + uint8_t body[192]; + uint32_t n = 0; + + uint16_t caps = IEEE80211_CAPINFO_ESS; + if (g_secured) caps |= IEEE80211_CAPINFO_PRIVACY; + if (!g_is5GHz) caps |= IEEE80211_CAPINFO_SHORT_PREAMBLE; + caps |= IEEE80211_CAPINFO_SHORT_SLOT; + Put16Le(body + n, caps); n += 2; + Put16Le(body + n, 10); n += 2; // listen interval + + body[n++] = IEEE80211_ELEMID_SSID; + body[n++] = g_ssidLen; + memcpy(body + n, g_ssid, g_ssidLen); + n += g_ssidLen; + + const uint8_t* rates = g_is5GHz ? kRates5 : kRates24; + uint32_t rateLen = g_is5GHz ? sizeof(kRates5) : sizeof(kRates24); + body[n++] = IEEE80211_ELEMID_RATES; + body[n++] = (uint8_t)rateLen; + memcpy(body + n, rates, rateLen); + n += rateLen; + + if (!g_is5GHz) { + body[n++] = IEEE80211_ELEMID_XRATES; + body[n++] = (uint8_t)sizeof(kXRates24); + memcpy(body + n, kXRates24, sizeof(kXRates24)); + n += sizeof(kXRates24); + } + + if (g_secured) { + // Must match the IE the supplicant puts in EAPOL message 2 byte for + // byte; the AP compares them to detect downgrade attacks. + uint32_t rsnLen = WpaBuildRsnIe(body + n, (uint32_t)sizeof(body) - n); + if (!rsnLen) return false; + n += rsnLen; + } + + g_lastTxMs = Timekeeping::GetMilliseconds(); + return IwxTxFrame(g_iwx.MgmtQ, hdr, sizeof(hdr), body, n, false, true); + } + + static bool SendDeauth(uint16_t reason) { + uint8_t hdr[IEEE80211_HDR_LEN]; + BuildMgmtHeader(hdr, IEEE80211_SUBTYPE_DEAUTH); + uint8_t body[2]; + Put16Le(body, reason); + return IwxTxFrame(g_iwx.MgmtQ, hdr, sizeof(hdr), body, sizeof(body), + false, true); + } + + // ========================================================================= + // Data path + // ========================================================================= + + bool IwxLinkUp() { + return g_state == ConnState::Connected; + } + + const uint8_t* IwxConnectBssid() { return g_bssid; } + const char* IwxConnectSsid() { return g_ssid; } + + // Build the 802.11 data header for a frame going to the AP (to-DS). + static uint32_t BuildDataHeader(uint8_t* hdr, const uint8_t* destMac, + bool protectedFrame) { + hdr[0] = (uint8_t)(IEEE80211_FC0_TYPE_DATA | IEEE80211_SUBTYPE_DATA); + hdr[1] = IEEE80211_FC1_TO_DS; + if (protectedFrame) hdr[1] |= IEEE80211_FC1_PROTECTED; + Put16Le(hdr + 2, 0); + memcpy(hdr + 4, g_bssid, 6); // addr1: the AP + memcpy(hdr + 10, g_iwx.Nvm.HwAddr, 6); // addr2: us + memcpy(hdr + 16, destMac, 6); // addr3: final destination + Put16Le(hdr + 22, (uint16_t)(NextSeq() << 4)); + return IEEE80211_HDR_LEN; + } + + // Matches Net::Ethernet::MAX_PAYLOAD_SIZE; the 802.11 frame this becomes + // still fits comfortably in one staging page. + static constexpr uint32_t MAX_ETH_PAYLOAD = 1504; + static constexpr uint32_t MAX_ETH_FRAME = 14 + MAX_ETH_PAYLOAD; + + // Transmit an 802.2 payload to `destMac` with the given EtherType. + static bool SendDataFrame(const uint8_t* destMac, uint16_t etherType, + const uint8_t* payload, uint32_t payloadLen, + bool encrypt) { + if (payloadLen > MAX_ETH_PAYLOAD) return false; + + uint8_t hdr[IEEE80211_HDR_LEN]; + uint32_t hdrLen = BuildDataHeader(hdr, destMac, encrypt); + + // LLC/SNAP prefix, then the original payload. + uint8_t buf[LLC_SNAP_LEN + MAX_ETH_PAYLOAD]; + auto* llc = (LlcSnapHeader*)buf; + llc->dsap = LLC_SNAP_LSAP; + llc->ssap = LLC_SNAP_LSAP; + llc->control = 0x03; + llc->oui[0] = llc->oui[1] = llc->oui[2] = 0x00; + Put16Be((uint8_t*)&llc->etherType, etherType); + memcpy(buf + LLC_SNAP_LEN, payload, payloadLen); + + return IwxTxFrame(g_iwx.MgmtQ, hdr, hdrLen, buf, + LLC_SNAP_LEN + payloadLen, encrypt, false); + } + + bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len) { + if (!IwxLinkUp()) return false; + if (len < 14 || len > MAX_ETH_FRAME) return false; + + const uint8_t* dst = frame; + uint16_t etherType = Get16Be(frame + 12); + // Encrypt once the pairwise key is in place; open networks never do. + return SendDataFrame(dst, etherType, frame + 14, len - 14, g_keysInstalled); + } + + // The supplicant transmits EAPOL frames unencrypted: they are what + // establishes the key in the first place. + bool WpaTxEapol(const uint8_t* body, uint32_t len) { + return SendDataFrame(g_bssid, ETHERTYPE_EAPOL, body, len, false); + } + + // The keys that made it into the firmware, so the teardown can remove + // them again (Linux removes both before the station goes). + struct InstalledKey { bool Set; uint8_t Idx; uint8_t Cipher; uint8_t Len; }; + static InstalledKey g_ptkKey = {}; + static InstalledKey g_gtkKey = {}; + + bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) { + if (!IwxSetKey(tk, tkLen, 0, true, cipher, nullptr)) return false; + g_ptkKey = { true, 0, cipher, (uint8_t)tkLen }; + return true; + } + + bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx, + uint8_t cipher, const uint8_t* rsc) { + if (gtkLen != 16 && gtkLen != 32) return false; + if (!IwxSetKey(gtk, gtkLen, keyIdx, false, cipher, rsc)) return false; + g_gtkKey = { true, keyIdx, cipher, (uint8_t)gtkLen }; + return true; + } + + // Hand an inbound EAPOL frame to the service loop: the handshake installs + // keys, which needs host commands the RX path cannot send. + static void QueueEapol(const uint8_t* data, uint32_t len) { + if (len > sizeof(g_eapolQ[0].Data)) return; + + g_eapolLock.Acquire(); + uint32_t next = (g_eapolHead + 1) % EAPOL_QUEUE_DEPTH; + if (next == g_eapolTail) { // full; drop the oldest + g_eapolTail = (g_eapolTail + 1) % EAPOL_QUEUE_DEPTH; + } + memcpy(g_eapolQ[g_eapolHead].Data, data, len); + g_eapolQ[g_eapolHead].Len = len; + g_eapolHead = next; + g_eapolLock.Release(); + } + + void IwxConnectRxData(const uint8_t* frame, uint32_t len) { + if (g_state < ConnState::Associated) return; + if (len < IEEE80211_HDR_LEN) return; + + uint8_t subtype = (uint8_t)(frame[0] & IEEE80211_FC0_SUBTYPE_MASK); + // Null-data frames are keepalives and carry nothing. + if (subtype & IEEE80211_SUBTYPE_NULL) return; + + uint32_t hdrLen = IEEE80211_HDR_LEN; + if (subtype & IEEE80211_SUBTYPE_QOS_DATA) hdrLen = IEEE80211_QOS_HDR_LEN; + + // Only traffic relayed by our AP is interesting. + if (!(frame[1] & IEEE80211_FC1_FROM_DS)) return; + if (!AddrEqual(frame + 10, g_bssid)) return; + + const uint8_t* da = frame + 4; + const uint8_t* sa = frame + 16; + + uint32_t off = hdrLen; + if (frame[1] & IEEE80211_FC1_PROTECTED) off += IWX_CCMP_HDR_LEN; + + if (off + LLC_SNAP_LEN > len) return; + + auto* llc = (const LlcSnapHeader*)(frame + off); + if (llc->dsap != LLC_SNAP_LSAP || llc->ssap != LLC_SNAP_LSAP) return; + uint16_t etherType = Get16Be((const uint8_t*)&llc->etherType); + + const uint8_t* payload = frame + off + LLC_SNAP_LEN; + uint32_t payloadLen = len - off - LLC_SNAP_LEN; + + g_iwx.RxDataPackets++; + + if (etherType == ETHERTYPE_EAPOL) { + KernelLogStream(INFO, "WiFi") << "EAPOL frame received (" + << (uint64_t)payloadLen << " bytes)"; + QueueEapol(payload, payloadLen); + return; + } + + if (!IwxLinkUp()) return; + + // Re-encapsulate as Ethernet II for the network stack. + uint8_t eth[MAX_ETH_FRAME]; + if (payloadLen > MAX_ETH_PAYLOAD) return; + memcpy(eth, da, 6); + memcpy(eth + 6, sa, 6); + Put16Be(eth + 12, etherType); + memcpy(eth + 14, payload, payloadLen); + + WifiRxEthernet(eth, 14 + payloadLen); + } + // ========================================================================= // Teardown // ========================================================================= static void TearDown() { - if (g_staActive) { RemoveStaCmd(); g_staActive = false; } + WpaReset(); + g_keysInstalled = false; + + IwxDisableTxq(g_iwx.MgmtQ, IWX_STATION_ID, IWX_MGMT_TID); + + // Keys come out before the station they are bound to. + if (g_ptkKey.Set) { + IwxRemoveKey(g_ptkKey.Idx, true, g_ptkKey.Cipher, g_ptkKey.Len); + g_ptkKey = {}; + } + if (g_gtkKey.Set) { + IwxRemoveKey(g_gtkKey.Idx, false, g_gtkKey.Cipher, g_gtkKey.Len); + g_gtkKey = {}; + } + // The MAC gives up its association before anything it points at is + // taken away. While is_assoc is set the firmware's MAC context holds + // the link as the one carrying the BSS, and deactivating a link out + // from under it asserts the UMAC (0x2000320F on firmware 89) -- the + // same ordering rule that governs the two asserts noted above, seen + // from the teardown side. iwl_mvm_mld_vif_cfg_changed_station sends + // this MODIFY on its way down for exactly this reason, before the + // station is removed and before the channel context is unassigned. + if (g_macAssoc) { + MacConfigCmd(IWX_FW_CTXT_ACTION_MODIFY, false); + g_macAssoc = false; + } + if (g_staActive) { StaRemoveCmd(); g_staActive = false; } + if (g_linkActive) { + // An active link must be deactivated before it can be removed + // (iwl_mvm_disable_link does the same two steps). + LinkModifyCmd(IWX_LINK_MODIFY_ACTIVE, false, false); + g_linkActive = false; + } if (g_bindingActive) { - BindingCmd(IWX_FW_CTXT_ACTION_REMOVE); + LinkRemoveCmd(); g_bindingActive = false; } if (g_macActive) { - MacCtxtCmd(IWX_FW_CTXT_ACTION_REMOVE, false); + MacConfigCmd(IWX_FW_CTXT_ACTION_REMOVE, false); g_macActive = false; } if (g_phyActive) { PhyCtxtCmd(IWX_FW_CTXT_ACTION_REMOVE); g_phyActive = false; } - g_mgmtQueueUp = false; g_aid = 0; + g_eapolHead = 0; + g_eapolTail = 0; + memset(&g_wpaCfg, 0, sizeof(g_wpaCfg)); + g_secured = false; + } + + static void Fail(const char* why) { + KernelLogStream(WARNING, "WiFi") << "Connection failed: " << why; + TearDown(); + g_state = ConnState::Failed; } // ========================================================================= @@ -222,14 +673,19 @@ namespace Drivers::Net::Wifi { // ========================================================================= bool IwxConnectStart(const uint8_t* bssid, uint8_t channel, bool is5GHz, - const char* ssid) { + const char* ssid, const char* password, + const uint8_t* rsnIe, uint32_t rsnIeLen, + uint16_t beaconInterval, uint8_t dtimPeriod) { if (g_iwx.State != IwxFwState::Running) return false; if (g_state != ConnState::Idle) IwxConnectAbort(); memcpy(g_bssid, bssid, 6); g_channel = channel; g_is5GHz = is5GHz; + g_iwx.Is5GHz = is5GHz; g_aid = 0; + g_beaconInterval = beaconInterval ? beaconInterval : 100; + g_dtimPeriod = dtimPeriod ? dtimPeriod : 1; g_ssidLen = 0; if (ssid) { while (g_ssidLen < 32 && ssid[g_ssidLen]) { @@ -239,10 +695,62 @@ namespace Drivers::Net::Wifi { } g_ssid[g_ssidLen] = '\0'; + // Work out what the encryption negotiation looks like before touching + // the hardware, so an unsupported network fails cheaply and clearly. + g_refusedForSecurity = false; + g_secured = rsnIe && rsnIeLen > 0; + if (g_secured) { + memset(&g_wpaCfg, 0, sizeof(g_wpaCfg)); + if (!WpaParseApRsn(rsnIe, rsnIeLen, g_wpaCfg)) { + g_refusedForSecurity = true; + return false; + } + + uint32_t pl = 0; + if (password) while (pl < sizeof(g_wpaCfg.Passphrase) - 1 && password[pl]) pl++; + if (pl == 0) { + KernelLogStream(WARNING, "WiFi") + << "This network is encrypted and needs a passphrase"; + g_refusedForSecurity = true; + return false; + } + memcpy(g_wpaCfg.Passphrase, password, pl); + g_wpaCfg.Passphrase[pl] = '\0'; + g_wpaCfg.PassLen = (uint8_t)pl; + memcpy(g_wpaCfg.OwnMac, g_iwx.Nvm.HwAddr, 6); + memcpy(g_wpaCfg.Bssid, g_bssid, 6); + memcpy(g_wpaCfg.Ssid, g_ssid, g_ssidLen); + g_wpaCfg.SsidLen = g_ssidLen; + + // The supplicant has to know the ciphers now: the RSN IE it builds + // goes into the association request below. + if (!WpaStart(g_wpaCfg)) { + g_refusedForSecurity = true; + return false; + } + } + if (g_iwx.ScanActive) IwxAbortScan(); + // Order follows the captured Linux sequence: the MAC exists first, then + // a link is created unbound, then the PHY, then the link is pointed at + // the PHY and activated, and only then does the station appear on it. + if (!MacConfigCmd(IWX_FW_CTXT_ACTION_ADD, false)) { + KernelLogStream(ERROR, "WiFi") << "Could not add MAC context"; + return false; + } + g_macActive = true; + + if (!LinkAddCmd()) { + KernelLogStream(ERROR, "WiFi") << "Could not add link"; + TearDown(); + return false; + } + g_bindingActive = true; + if (!PhyCtxtCmd(IWX_FW_CTXT_ACTION_ADD)) { KernelLogStream(ERROR, "WiFi") << "Could not add PHY context"; + TearDown(); return false; } g_phyActive = true; @@ -253,59 +761,62 @@ namespace Drivers::Net::Wifi { return false; } - if (!MacCtxtCmd(IWX_FW_CTXT_ACTION_ADD, false)) { - KernelLogStream(ERROR, "WiFi") << "Could not add MAC context"; + // Point the link at the PHY in a command of its own, then activate it + // with the rate set (see the note above LinkAddCmd for why these must + // not be one command). + if (!LinkModifyCmd(0, false, false)) { + KernelLogStream(ERROR, "WiFi") << "Could not bind the link to the PHY"; TearDown(); return false; } - g_macActive = true; - - if (!BindingCmd(IWX_FW_CTXT_ACTION_ADD)) { - KernelLogStream(ERROR, "WiFi") << "Could not add binding"; + if (!LinkModifyCmd(IWX_LINK_MODIFY_ACTIVE | IWX_LINK_MODIFY_RATES_INFO, + true, false)) { + KernelLogStream(ERROR, "WiFi") << "Could not activate the link"; TearDown(); return false; } - g_bindingActive = true; + g_linkActive = true; - if (!AddStaCmd(false)) { + if (!StaConfigCmd()) { KernelLogStream(ERROR, "WiFi") << "Could not add station"; TearDown(); return false; } g_staActive = true; - // Non-QoS management frames go out on the MGMT TID/queue. - if (!IwxEnableTxq(IWX_STATION_ID, IWX_DQA_MGMT_QUEUE, IWX_MGMT_TID)) { - KernelLogStream(WARNING, "WiFi") - << "Management TX queue unavailable; cannot transmit auth frames"; + // One queue on the management TID carries management frames, EAPOL and + // non-QoS data for the whole session. + if (!IwxEnableTxq(g_iwx.MgmtQ, IWX_STATION_ID, IWX_DQA_MGMT_QUEUE, + IWX_MGMT_TID)) { + KernelLogStream(ERROR, "WiFi") << "Could not open a transmit queue"; TearDown(); return false; } - g_mgmtQueueUp = true; // Beacon interval 100 TU * 9 is what iwlwifi reserves for the whole // authenticate + associate exchange. ScheduleSessionProtection(900); - g_state = ConnState::ContextsUp; + EnterState(ConnState::ContextsUp); KernelLogStream(INFO, "WiFi") - << "Firmware contexts up for BSSID " << base::hex - << (uint64_t)g_bssid[0] << ":" << (uint64_t)g_bssid[1] << ":" - << (uint64_t)g_bssid[2] << ":" << (uint64_t)g_bssid[3] << ":" - << (uint64_t)g_bssid[4] << ":" << (uint64_t)g_bssid[5] << base::dec - << " on channel " << (uint64_t)g_channel; + << "Joining \"" << g_ssid << "\" on channel " << (uint64_t)g_channel; - // Transmitting the authentication frame itself requires the TX data - // path (TFD assembly, rate selection, TX status handling), which this - // driver does not implement yet. Everything above is the firmware-side - // state the exchange needs; the 802.11 handshake is the remaining work. - KernelLogStream(WARNING, "WiFi") - << "Authentication frame TX is not implemented; stopping after context setup"; + if (!SendAuth()) { + Fail("could not transmit the authentication frame"); + return false; + } + EnterState(ConnState::Authenticating); + g_tries = 1; return true; } void IwxConnectAbort() { if (g_state == ConnState::Idle) return; + // Tell the AP we are leaving, but only while the station context (and + // therefore the transmit queue) still exists. + if (g_staActive && g_state >= ConnState::Authenticated + && g_state != ConnState::Failed) + SendDeauth(IEEE80211_REASON_DEAUTH_LEAVING); TearDown(); g_state = ConnState::Idle; } @@ -313,25 +824,21 @@ namespace Drivers::Net::Wifi { // Inbound management frames for the connection state machine. Beacons and // probe responses are consumed by the scan path instead. void IwxConnectRxMgmt(const uint8_t* frame, uint32_t len) { - if (g_state == ConnState::Idle || len < 24) return; + if (g_state == ConnState::Idle || len < IEEE80211_HDR_LEN) return; - uint8_t subtype = (uint8_t)(frame[0] & 0xf0); - constexpr uint8_t SUBTYPE_AUTH = 0xb0; - constexpr uint8_t SUBTYPE_ASSOC_RESP = 0x10; - constexpr uint8_t SUBTYPE_DEAUTH = 0xc0; - constexpr uint8_t SUBTYPE_DISASSOC = 0xa0; + uint8_t subtype = (uint8_t)(frame[0] & IEEE80211_FC0_SUBTYPE_MASK); // Only frames from the BSS we are joining are interesting here. - for (int i = 0; i < 6; i++) - if (frame[10 + i] != g_bssid[i]) return; + if (!AddrEqual(frame + 10, g_bssid)) return; switch (subtype) { - case SUBTYPE_AUTH: { - if (len < 30) return; - uint16_t status = (uint16_t)(frame[28] | (frame[29] << 8)); - if (status == 0) { - g_state = ConnState::Authenticated; + case IEEE80211_SUBTYPE_AUTH: { + if (len < IEEE80211_HDR_LEN + 6) return; + uint16_t status = Get16Le(frame + IEEE80211_HDR_LEN + 4); + if (status == IEEE80211_STATUS_SUCCESS) { KernelLogStream(OK, "WiFi") << "Authenticated"; + EnterState(ConnState::Authenticated); + g_sendAssocPending = true; } else { g_state = ConnState::Failed; KernelLogStream(WARNING, "WiFi") @@ -339,16 +846,14 @@ namespace Drivers::Net::Wifi { } break; } - case SUBTYPE_ASSOC_RESP: { - if (len < 30) return; - uint16_t status = (uint16_t)(frame[26] | (frame[27] << 8)); - if (status == 0) { - g_aid = (uint16_t)((frame[28] | (frame[29] << 8)) & 0x3fff); - g_state = ConnState::Associated; - // This runs inside RX processing, which is guarded against - // reentry; sending the context updates from here would - // deadlock their own completion wait. Defer to the idle - // loop (IwxConnectService). + case IEEE80211_SUBTYPE_ASSOC_RESP: { + if (len < IEEE80211_HDR_LEN + 6) return; + uint16_t status = Get16Le(frame + IEEE80211_HDR_LEN + 2); + if (status == IEEE80211_STATUS_SUCCESS) { + g_aid = (uint16_t)(Get16Le(frame + IEEE80211_HDR_LEN + 4) & 0x3fff); + EnterState(ConnState::Associated); + // Sending the context updates from here would deadlock + // their own completion wait; defer to IwxConnectService. g_postAssocPending = true; KernelLogStream(OK, "WiFi") << "Associated, AID " << (uint64_t)g_aid; @@ -359,9 +864,9 @@ namespace Drivers::Net::Wifi { } break; } - case SUBTYPE_DEAUTH: - case SUBTYPE_DISASSOC: - KernelLogStream(INFO, "WiFi") << "Link torn down by AP"; + case IEEE80211_SUBTYPE_DEAUTH: + case IEEE80211_SUBTYPE_DISASSOC: + KernelLogStream(INFO, "WiFi") << "Link torn down by the access point"; g_teardownPending = true; // see IwxConnectService break; default: @@ -371,20 +876,141 @@ namespace Drivers::Net::Wifi { // Apply work queued by the RX path. Called from the idle loop, where // sending firmware commands (and pumping their completions) is safe. - void IwxConnectService() { + static void ServiceLocked() { if (g_teardownPending) { g_teardownPending = false; TearDown(); g_state = ConnState::Idle; return; } + + if (g_state == ConnState::Idle || g_state == ConnState::Failed) return; + + if (g_sendAssocPending) { + g_sendAssocPending = false; + if (SendAssocReq()) { + EnterState(ConnState::Associating); + g_tries = 1; + } else { + Fail("could not transmit the association request"); + return; + } + } + if (g_postAssocPending) { g_postAssocPending = false; - MacCtxtCmd(IWX_FW_CTXT_ACTION_MODIFY, true); - AddStaCmd(true); + // Association id and beacon timing are only known now. Firmware + // 89 requires the one-shot BEACON_TIMING update while the MAC is + // still in its pre-association state. Marking the MAC associated + // first and then sending this LINK_CONFIG asserts the UMAC + // (0x2010330F). This order is also what iwlwifi emits in the + // successful host-command trace in tests/wifi/decode_iwl_trace.py. + // + // The link is already active, so ACTIVE must stay out of the + // modify mask. We operate as a legacy station and therefore do + // not set the HE/BSS-color/EHT bits seen in Linux's HE-capable + // association. + if (!LinkModifyCmd(IWX_LINK_MODIFY_RATES_INFO + | IWX_LINK_MODIFY_PROTECT_FLAGS + | IWX_LINK_MODIFY_QOS_PARAMS + | IWX_LINK_MODIFY_BEACON_TIMING, + true, true)) { + Fail("could not apply post-association link settings"); + return; + } + if (!MacConfigCmd(IWX_FW_CTXT_ACTION_MODIFY, true)) { + Fail("could not mark the MAC associated"); + return; + } + g_macAssoc = true; + + if (!g_secured) { + EnterState(ConnState::Connected); + KernelLogStream(OK, "WiFi") + << "Connected to \"" << g_ssid << "\" (open network)"; + } else { + EnterState(ConnState::Handshaking); + } + } + + // Drain queued EAPOL frames; the handshake installs keys from here. + static uint8_t eapolScratch[sizeof(g_eapolQ[0].Data)]; + while (g_eapolTail != g_eapolHead) { + g_eapolLock.Acquire(); + const PendingEapol& e = g_eapolQ[g_eapolTail]; + uint32_t elen = e.Len; + memcpy(eapolScratch, e.Data, elen); + g_eapolTail = (g_eapolTail + 1) % EAPOL_QUEUE_DEPTH; + g_eapolLock.Release(); + + WpaOnEapol(eapolScratch, elen); + } + + // Only now is it safe to read the clock. Everything above spends real + // time inside firmware commands -- the post-association context updates + // and the key installations are round trips to the adapter -- and each + // step stamps g_stateEnteredMs or g_lastTxMs with the time it finished. + // A `now` sampled at the top of this function would therefore be OLDER + // than the timestamps it is compared against below, every unsigned + // difference would wrap to ~2^64, and every deadline would read as + // expired the instant it was set. That is what made an association + // that had just succeeded fail with "timed out while joining the + // network" before the access point's first EAPOL frame could arrive. + uint64_t now = Timekeeping::GetMilliseconds(); + + if (g_state == ConnState::Handshaking) { + WpaService(now); + if (WpaIsComplete()) { + g_keysInstalled = true; + EnterState(ConnState::Connected); + KernelLogStream(OK, "WiFi") + << "Connected to \"" << g_ssid << "\""; + } else if (WpaGetState() == WpaState::Failed) { + SendDeauth(IEEE80211_REASON_4WAY_TIMEOUT); + Fail("the key exchange did not complete"); + return; + } + } + + // Retransmit management frames the AP has not answered. + if (g_state == ConnState::Authenticating + && Elapsed(now, g_lastTxMs, MGMT_RETRY_MS)) { + if (g_tries >= MGMT_MAX_TRIES) { + Fail("no response to the authentication request"); + return; + } + g_tries++; + SendAuth(); + } else if (g_state == ConnState::Associating + && Elapsed(now, g_lastTxMs, MGMT_RETRY_MS)) { + if (g_tries >= MGMT_MAX_TRIES) { + Fail("no response to the association request"); + return; + } + g_tries++; + SendAssocReq(); + } + + if (g_state != ConnState::Connected + && Elapsed(now, g_stateEnteredMs, CONNECT_TIMEOUT_MS)) { + Fail("timed out while joining the network"); } } + // Every idling core runs the idle loop, and a blocking connect pumps this + // too, so more than one caller can arrive at once. Commands themselves are + // serialized by CmdLock, but the state machine and the EAPOL queue are not: + // let exactly one caller through and have the others come back later. + static volatile bool g_inService = false; + + void IwxConnectService() { + if (__atomic_test_and_set(&g_inService, __ATOMIC_ACQUIRE)) return; + ServiceLocked(); + __atomic_clear(&g_inService, __ATOMIC_RELEASE); + } + + bool IwxConnectRefusedForSecurity() { return g_refusedForSecurity; } + int IwxConnectState() { return (int)g_state; } diff --git a/kernel/src/Drivers/Net/Wifi/IwxMvm.cpp b/kernel/src/Drivers/Net/Wifi/IwxMvm.cpp index 2ed3ac9..330a9af 100644 --- a/kernel/src/Drivers/Net/Wifi/IwxMvm.cpp +++ b/kernel/src/Drivers/Net/Wifi/IwxMvm.cpp @@ -7,6 +7,7 @@ */ #include "Iwx.hpp" +#include "Ieee80211.hpp" #include #include #include @@ -629,12 +630,23 @@ namespace Drivers::Net::Wifi { uint32_t offset = (uint32_t)(frame - (const uint8_t*)pkt); if (offset + len > bufLen) return; - // The firmware pads the header to a 4-byte boundary when the flag is - // set; the payload then starts two bytes later. + // The firmware pads a MAC header that is not a multiple of four bytes + // (in practice: every QoS data header) out to alignment by inserting + // two bytes between the header -- and the crypto IV, if any -- and + // the payload; mpdu_len counts them. Cut them out so the rest of the + // driver sees a contiguous 802.11 frame. Getting this wrong loses + // every QoS data frame while management frames keep working. if (desc->mac_flags2 & IWX_RX_MPDU_MFLG2_PAD) { - if (len < 2) return; - frame += 2; - if (offset + 2 + len > bufLen) return; + uint32_t hdrLen = 24; + if ((frame[0] & IEEE80211_FC0_TYPE_MASK) == IEEE80211_FC0_TYPE_DATA + && (frame[0] & IEEE80211_SUBTYPE_QOS_DATA)) + hdrLen += 2; + if (frame[1] & IEEE80211_FC1_PROTECTED) + hdrLen += IWX_CCMP_HDR_LEN; + if (len < hdrLen + 2) return; + memmove((uint8_t*)frame + hdrLen, frame + hdrLen + 2, + len - hdrLen - 2); + len -= 2; } int energyA = desc->v3.energy_a ? -(int)desc->v3.energy_a : -256; @@ -642,6 +654,26 @@ namespace Drivers::Net::Wifi { int rssi = energyA > energyB ? energyA : energyB; if (rssi < -128) rssi = -128; + uint8_t type = (uint8_t)(frame[0] & IEEE80211_FC0_TYPE_MASK); + + if (type == IEEE80211_FC0_TYPE_DATA) { + // Encrypted frames are decrypted by the firmware, which strips the + // MIC but leaves the CCMP/GCMP header in place; drop anything that + // failed to decrypt rather than handing up ciphertext. + if (frame[1] & IEEE80211_FC1_PROTECTED) { + uint32_t sec = desc->status & IWX_RX_MPDU_STATUS_SEC_MASK; + bool ok = (desc->status & IWX_RX_MPDU_STATUS_DECRYPTED) + && (desc->status & IWX_RX_MPDU_STATUS_MIC_OK) + && (sec == IWX_RX_MPDU_STATUS_SEC_CCM + || sec == IWX_RX_MPDU_STATUS_SEC_GCM); + if (!ok) return; + } + IwxConnectRxData(frame, len); + return; + } + + if (type != IEEE80211_FC0_TYPE_MGT) return; + WifiRxMgmtFrame(frame, len, desc->v3.channel, (int8_t)rssi); } diff --git a/kernel/src/Drivers/Net/Wifi/IwxReg.hpp b/kernel/src/Drivers/Net/Wifi/IwxReg.hpp index c522179..731a260 100644 --- a/kernel/src/Drivers/Net/Wifi/IwxReg.hpp +++ b/kernel/src/Drivers/Net/Wifi/IwxReg.hpp @@ -567,7 +567,10 @@ inline uint32_t IwxRxPacketPayloadLen(const IwxRxPacket* pkt) { return IwxRxPacketLen(pkt) - sizeof(IwxCmdHeader); } -// DQA queue assignment +// DQA queue assignment. The management queue number is only a hint: on the v3 +// data-path API the firmware assigns the queue and reports the number back. +// TID 15 carries management frames, EAPOL and non-QoS data -- the firmware maps +// non-QoS data onto this TID regardless. constexpr int IWX_DQA_CMD_QUEUE = 0; constexpr int IWX_DQA_MGMT_QUEUE = 1; constexpr int IWX_MGMT_TID = 15; @@ -949,6 +952,20 @@ constexpr uint8_t IWX_SCAN_ADWELL_N_APS_SOCIAL_CHS = 2; constexpr uint32_t IWX_RX_MPDU_RES_STATUS_CRC_OK = 1u << 0; constexpr uint32_t IWX_RX_MPDU_RES_STATUS_OVERRUN_OK = 1u << 1; +constexpr uint32_t IWX_RX_MPDU_STATUS_KEY_VALID = 1u << 3; +constexpr uint32_t IWX_RX_MPDU_STATUS_ICV_OK = 1u << 5; +constexpr uint32_t IWX_RX_MPDU_STATUS_MIC_OK = 1u << 6; +constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_MASK = 0x7u << 8; +constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_NONE = 0x0u << 8; +constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_WEP = 0x1u << 8; +constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_CCM = 0x2u << 8; +constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_TKIP = 0x3u << 8; +constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_EXT_ENC = 0x4u << 8; +constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_GCM = 0x5u << 8; +constexpr uint32_t IWX_RX_MPDU_STATUS_DECRYPTED = 1u << 11; + +// The hardware strips the CCMP/GCMP MIC but leaves the 8-byte IV in place. +constexpr uint32_t IWX_CCMP_HDR_LEN = 8; constexpr uint8_t IWX_RX_MPDU_MFLG2_PAD = 0x20; constexpr uint8_t IWX_RX_MPDU_MFLG2_AMSDU = 0x40; @@ -1000,21 +1017,46 @@ constexpr uint32_t IWX_FW_CTXT_ACTION_REMOVE = 3; constexpr uint32_t IWX_LMAC_24G_INDEX = 0; constexpr uint32_t IWX_LMAC_5G_INDEX = 1; -// PHY context -struct IwxFwChannelInfoV1 { +// PHY context. +// +// The channel-info sub-structure has two shapes and the firmware picks which +// one it expects via IWX_UCODE_TLV_CAPA_ULTRA_HB_CHANNELS. AX211 firmware 89 +// sets that bit, so it wants the v2 form: a 32-bit channel number first, then +// the band. Sending the 4-byte v1 form to a firmware expecting 8 bytes +// shifts lmac_id/rxchain_info and asserts the firmware, which then stops +// answering host commands entirely. +struct IwxFwChannelInfoV1 { // CHANNEL_CONFIG_API_S_VER_1 uint8_t band; uint8_t channel; uint8_t width; uint8_t ctrl_pos; } __attribute__((packed)); +struct IwxFwChannelInfo { // CHANNEL_CONFIG_API_S_VER_2 + uint32_t channel; + uint8_t band; + uint8_t width; + uint8_t ctrl_pos; + uint8_t reserved; +} __attribute__((packed)); + constexpr uint8_t IWX_PHY_VHT_CHANNEL_MODE20 = 0x0; constexpr uint8_t IWX_PHY_VHT_CTRL_POS_1_BELOW = 0x0; constexpr uint32_t IWX_PHY_RX_CHAIN_VALID_POS = 1; constexpr uint32_t IWX_PHY_RX_CHAIN_CNT_POS = 10; constexpr uint32_t IWX_PHY_RX_CHAIN_MIMO_CNT_POS = 12; -struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB) +struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_S_VER_3/4, UHB + uint32_t id_and_color; + uint32_t action; + IwxFwChannelInfo ci; + uint32_t lmac_id; + uint32_t rxchain_info; // reserved from VER_4 on + uint32_t dsp_cfg_flags; + uint32_t reserved; +} __attribute__((packed)); + +struct IwxPhyContextCmdV1Chan { // same command, pre-UHB channel info uint32_t id_and_color; uint32_t action; IwxFwChannelInfoV1 ci; @@ -1024,11 +1066,15 @@ struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB) uint32_t reserved; } __attribute__((packed)); +static_assert(sizeof(IwxPhyContextCmd) == 32, "PHY context (UHB) must be 32 bytes"); +static_assert(sizeof(IwxPhyContextCmdV1Chan) == 28, "PHY context (legacy) must be 28 bytes"); + // MAC context constexpr uint32_t IWX_FW_MAC_TYPE_BSS_STA = 5; constexpr uint32_t IWX_TSF_ID_A = 0; constexpr uint32_t IWX_AC_NUM = 4; constexpr uint32_t IWX_MAC_QOS_FLG_UPDATE_EDCA = 1u << 0; +constexpr uint32_t IWX_MAC_QOS_FLG_TGN = 1u << 1; constexpr uint32_t IWX_MAC_FILTER_IN_CONTROL_AND_MGMT = 1u << 1; constexpr uint32_t IWX_MAC_FILTER_ACCEPT_GRP = 1u << 2; constexpr uint32_t IWX_MAC_FILTER_IN_BEACON = 1u << 6; @@ -1055,6 +1101,16 @@ struct IwxMacDataSta { uint32_t assoc_beacon_arrive_time; } __attribute__((packed)); +// The per-mac-type tail of MAC_CONTEXT_CMD is a union in the firmware API, so +// the command length is that of its LARGEST member -- p2p_sta, which is +// iwl_mac_data_sta plus a ctwin word. Sending only the sta member makes the +// command four bytes short and asserts the firmware (observed on AX211 fw 89: +// UMAC error 0x201002FF on command 0x128). +struct IwxMacDataP2pSta { + IwxMacDataSta sta; + uint32_t ctwin; +} __attribute__((packed)); + struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta) uint32_t id_and_color; uint32_t action; @@ -1072,9 +1128,15 @@ struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta) uint32_t filter_flags; uint32_t qos_flags; IwxAcQos ac[IWX_AC_NUM + 1]; - IwxMacDataSta sta; + union { + IwxMacDataSta sta; + IwxMacDataP2pSta p2p_sta; // the largest member: sizes the command + } u; } __attribute__((packed)); +static_assert(sizeof(IwxMacCtxCmd) == 148, + "MAC context command must stay 148 bytes (union sized by p2p_sta)"); + // Binding context constexpr uint32_t IWX_MAX_MACS_IN_BINDING = 3; struct IwxBindingCmd { @@ -1178,8 +1240,9 @@ struct IwxRlcConfigCmd { uint8_t reserved[3]; } __attribute__((packed)); -// Session protection -constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 1; +// Session protection. ASSOC is the first value of +// enum iwl_session_prot_conf_id, i.e. zero -- 1 is GO_CLIENT_ASSOC. +constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 0; struct IwxSessionProtCmd { uint32_t id_and_color; uint32_t action; @@ -1196,6 +1259,313 @@ struct IwxSessionProtNotif { uint32_t conf_id; } __attribute__((packed)); +// ============================================================================= +// MLD API (MAC_CONF group) +// +// Firmware that advertises IWX_UCODE_TLV_CAPA_MLD_API_SUPPORT -- which AX211 +// firmware 89 does -- implements these instead of the legacy MAC_CONTEXT_CMD / +// BINDING_CONTEXT_CMD / ADD_STA. The legacy commands are simply absent, and +// sending one asserts the firmware. +// +// Every layout and size below was confirmed against a host-command trace taken +// from Linux driving this same adapter and firmware (see +// tests/wifi/decode_iwl_trace.py), not inferred from a kernel header. +// ============================================================================= + +constexpr uint8_t IWX_MAC_CONFIG_CMD = 0x08; // MAC_CONF group +constexpr uint8_t IWX_LINK_CONFIG_CMD = 0x09; +constexpr uint8_t IWX_STA_CONFIG_CMD = 0x0a; +constexpr uint8_t IWX_AUX_STA_CMD = 0x0b; +constexpr uint8_t IWX_STA_REMOVE_CMD = 0x0c; + +// iwl_mac_config_filter_flags +constexpr uint32_t IWX_MAC_CFG_FILTER_PROMISC = 1u << 0; +constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_CTRL_MGMT = 1u << 1; +constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_GRP = 1u << 2; +constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_BEACON = 1u << 3; +constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_BCAST_PROBE_RESP = 1u << 4; +constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_PROBE_REQ = 1u << 5; + +struct IwxMacClientData { // MAC_CONTEXT_CONFIG_CLIENT_DATA_API_S_VER_2 + uint8_t is_assoc; + uint8_t esr_transition_timeout; + uint16_t medium_sync_delay; + uint16_t assoc_id; + uint16_t reserved1; + uint16_t data_policy; + uint16_t reserved2; + uint32_t ctwin; +} __attribute__((packed)); + +struct IwxMacConfigCmd { // MAC_CONTEXT_CONFIG_CMD_API_S_VER_2 + uint32_t id_and_color; + uint32_t action; + uint32_t mac_type; + uint8_t local_mld_addr[6]; + uint16_t reserved_for_local_mld_addr; + uint32_t filter_flags; + uint16_t he_support; + uint16_t he_ap_support; + uint32_t eht_support; + uint32_t nic_not_ack_enabled; + IwxMacClientData client; // union with p2p_dev; client is the largest +} __attribute__((packed)); + +static_assert(sizeof(IwxMacConfigCmd) == 52, "MAC_CONFIG_CMD must be 52 bytes"); + +// iwl_link_ctx_modify_flags +constexpr uint32_t IWX_LINK_MODIFY_ACTIVE = 1u << 0; +constexpr uint32_t IWX_LINK_MODIFY_RATES_INFO = 1u << 1; +constexpr uint32_t IWX_LINK_MODIFY_PROTECT_FLAGS = 1u << 2; +constexpr uint32_t IWX_LINK_MODIFY_QOS_PARAMS = 1u << 3; +constexpr uint32_t IWX_LINK_MODIFY_BEACON_TIMING = 1u << 4; +constexpr uint32_t IWX_LINK_MODIFY_HE_PARAMS = 1u << 5; +constexpr uint32_t IWX_LINK_MODIFY_ALL = 0xff; + +struct IwxHeBackoffConf { // AC_QOS_MU_EDCA_API_S + uint16_t cwmin; + uint16_t cwmax; + uint16_t aifsn; + uint16_t mu_time; +} __attribute__((packed)); + +struct IwxLinkConfigCmd { // LINK_CONTEXT_CONFIG_CMD_API_S_VER_1/2/3 + uint32_t action; + uint32_t link_id; + uint32_t mac_id; + uint32_t phy_id; // IWX_FW_CTXT_INVALID until bound + uint8_t local_link_addr[6]; + uint16_t reserved_for_local_link_addr; + uint32_t modify_mask; + uint32_t active; + uint32_t listen_lmac; + uint32_t cck_rates; + uint32_t ofdm_rates; + uint32_t cck_short_preamble; + uint32_t short_slot; + uint32_t protection_flags; + uint32_t qos_flags; + IwxAcQos ac[IWX_AC_NUM + 1]; + uint8_t htc_trig_based_pkt_ext; + uint8_t rand_alloc_ecwmin; + uint8_t rand_alloc_ecwmax; + uint8_t ndp_fdbk_buff_th_exp; + IwxHeBackoffConf trig_based_txf[IWX_AC_NUM]; + uint32_t bi; + uint32_t dtim_interval; + uint16_t puncture_mask; // removed in _VER_3 + uint16_t frame_time_rts_th; + uint32_t flags; + uint32_t flags_mask; + uint8_t ref_bssid_addr[6]; + uint16_t reserved_for_ref_bssid_addr; + uint8_t bssid_index; + uint8_t bss_color; + uint8_t spec_link_id; + uint8_t reserved2; + uint8_t ibss_bssid_addr[6]; + uint16_t reserved_for_ibss_bssid_addr; + uint32_t reserved3[8]; +} __attribute__((packed)); + +static_assert(sizeof(IwxLinkConfigCmd) == 208, "LINK_CONFIG_CMD must be 208 bytes"); + +// 2 spatial streams x 5 bandwidth indices x 2 thresholds +struct IwxHePktExtV2 { + uint8_t pkt_ext_qam_th[2][5][2]; +} __attribute__((packed)); + +struct IwxStaConfigCmd { // STA_CMD_API_S_VER_1 + uint32_t sta_id; + uint32_t link_id; + uint8_t peer_mld_address[6]; + uint16_t reserved_for_peer_mld_address; + uint8_t peer_link_address[6]; + uint16_t reserved_for_peer_link_address; + uint32_t station_type; + uint32_t assoc_id; + uint32_t beamform_flags; + uint32_t mfp; + uint32_t mimo; + uint32_t mimo_protection; + uint32_t ack_enabled; + uint32_t trig_rnd_alloc; + uint32_t tx_ampdu_spacing; + uint32_t tx_ampdu_max_size; + uint32_t sp_length; + uint32_t uapsd_acs; + IwxHePktExtV2 pkt_ext; + uint32_t htc_flags; +} __attribute__((packed)); + +static_assert(sizeof(IwxStaConfigCmd) == 96, "STA_CONFIG_CMD must be 96 bytes"); + +struct IwxStaRemoveCmd { + uint32_t sta_id; +} __attribute__((packed)); + +// ============================================================================= +// TX path (AX210 / "new TX API") +// ============================================================================= + +// TX_CMD on a data queue carries the *short* 4-byte command header and stays in +// group 0; only host commands on the command queue are re-tagged into +// LONG_GROUP. Layout of a queued frame: +// +// [IwxCmdHeader 4][IwxTxCmdGen3 28][802.11 header][pad to 4][payload] +// +// TB0 covers the first 20 bytes, TB1 the rest of the header block (dword +// aligned) and TB2 the payload, matching iwl_txq_gen2_build_tx(). + +struct IwxDramSecInfo { + uint32_t pn_low; + uint16_t pn_high; + uint16_t aux_info; +} __attribute__((packed)); // DRAM_SEC_INFO_API_S_VER_1 + +struct IwxTxCmdGen3 { + uint16_t len; // total 802.11 frame length, plaintext + uint16_t flags; // IWX_TX_FLAGS_* + uint32_t offload_assist; + IwxDramSecInfo dram_info; + uint32_t rate_n_flags; + uint8_t reserved[8]; // named "ttl" in TX_CMD_API_S_VER_8 + // 802.11 header follows +} __attribute__((packed)); // TX_CMD_API_S_VER_8 / _10 + +static_assert(sizeof(IwxTxCmdGen3) == 28, "TX command header must stay 28 bytes"); + +// iwl_tx_cmd_flags (TX_FLAGS_BITS_API_S_VER_3) +constexpr uint16_t IWX_TX_FLAGS_CMD_RATE = 1 << 0; // use rate_n_flags +constexpr uint16_t IWX_TX_FLAGS_ENCRYPT_DIS = 1 << 1; // send in the clear +constexpr uint16_t IWX_TX_FLAGS_HIGH_PRI = 1 << 2; +constexpr uint16_t IWX_TX_FLAGS_RTS = 1 << 3; +constexpr uint16_t IWX_TX_FLAGS_CTS = 1 << 4; + +// iwl_tx_offload_assist_flags_pos +constexpr uint32_t IWX_TX_CMD_OFFLD_MH_SIZE_POS = 8; // header length in words +constexpr uint32_t IWX_TX_CMD_OFFLD_MH_MASK = 0x1f; +constexpr uint32_t IWX_TX_CMD_OFFLD_PAD = 1u << 13; +constexpr uint32_t IWX_TX_CMD_OFFLD_AMSDU = 1u << 14; + +// rate_n_flags. The firmware advertises which encoding it wants: TX_CMD +// notification version 7 and later use the "version 2" layout, where bits 10-8 +// select the modulation and bits 3-0 index the legacy rate table. Older +// firmware uses the version 1 layout, which carries the PLCP value directly and +// flags CCK with bit 9. Antenna selection sits at bits 15-14 in both. +constexpr uint32_t IWX_RATE_MCS_ANT_POS = 14; +constexpr uint32_t IWX_RATE_MCS_ANT_A = 1u << IWX_RATE_MCS_ANT_POS; +constexpr uint32_t IWX_RATE_MCS_ANT_B = 2u << IWX_RATE_MCS_ANT_POS; + +// version 2 +constexpr uint32_t IWX_RATE_MCS_MOD_TYPE_POS = 8; +constexpr uint32_t IWX_RATE_MCS_MOD_CCK = 0u << IWX_RATE_MCS_MOD_TYPE_POS; +constexpr uint32_t IWX_RATE_MCS_MOD_LEGACY_OFDM = 1u << IWX_RATE_MCS_MOD_TYPE_POS; +constexpr uint32_t IWX_RATE_LEGACY_RATE_MSK = 0x7; +constexpr uint32_t IWX_RATE_MCS_CHAN_WIDTH_20 = 0u << 11; + +// version 1 (PLCP encoded directly in bits 7-0) +constexpr uint32_t IWX_RATE_MCS_CCK_MSK_V1 = 1u << 9; +constexpr uint8_t IWX_RATE_1M_PLCP = 10; +constexpr uint8_t IWX_RATE_6M_PLCP = 13; + +// TX response (TX_CMD notification). Only the leading fields are used: the +// driver just needs the slot back and a success/failure verdict. +constexpr uint32_t IWX_TX_STATUS_MSK = 0x000000ff; +constexpr uint32_t IWX_TX_STATUS_SUCCESS = 0x01; +constexpr uint32_t IWX_TX_STATUS_DIRECT_DONE = 0x02; + +struct IwxTxResp { + uint8_t frame_count; + uint8_t bt_kill_count; + uint8_t failure_rts; + uint8_t failure_frame; + uint32_t initial_rate; + uint16_t wireless_media_time; + uint8_t pa_status; + uint8_t pa_integ_res_a[3]; + uint8_t pa_integ_res_b[3]; + uint8_t pa_integ_res_c[3]; + uint16_t measurement_req_id; + uint8_t reduced_tpc; + uint8_t reserved; + uint32_t tfd_info; + uint16_t seq_ctl; + uint16_t byte_cnt; + uint8_t tlc_info; + uint8_t ra_tid; + uint16_t frame_ctrl; + // followed by per-frame status entries; entry 0 is what matters here + uint16_t status; + uint16_t sequence; +} __attribute__((packed)); + +// ============================================================================= +// ADD_STA_KEY (hardware key installation) +// ============================================================================= + +// iwl_sta_key_flag +constexpr uint16_t IWX_STA_KEY_FLG_NO_ENC = 0 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_WEP = 1 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_CCM = 2 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_TKIP = 3 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_EXT = 4 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_GCMP = 5 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_CMAC = 6 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_EN_MSK = 7 << 0; +constexpr uint16_t IWX_STA_KEY_FLG_WEP_KEY_MAP = 1 << 3; +constexpr uint16_t IWX_STA_KEY_FLG_KEYID_POS = 8; +constexpr uint16_t IWX_STA_KEY_FLG_KEYID_MSK = 3 << IWX_STA_KEY_FLG_KEYID_POS; +constexpr uint16_t IWX_STA_KEY_NOT_VALID = 1 << 11; +constexpr uint16_t IWX_STA_KEY_FLG_KEY_32BYTES = 1 << 12; +constexpr uint16_t IWX_STA_KEY_MULTICAST = 1 << 14; +constexpr uint16_t IWX_STA_KEY_MFP = 1 << 15; + +struct IwxAddStaKeyCommon { + uint8_t sta_id; + uint8_t key_offset; + uint16_t key_flags; + uint8_t key[32]; + uint8_t rx_secur_seq_cnt[16]; +} __attribute__((packed)); + +struct IwxAddStaKeyCmd { + IwxAddStaKeyCommon common; + uint64_t rx_mic_key; + uint64_t tx_mic_key; + uint64_t transmit_seq_cnt; +} __attribute__((packed)); + +static_assert(sizeof(IwxAddStaKeyCmd) == 76, "ADD_STA_KEY layout changed"); + +// SEC_KEY_CMD (DATA_PATH group): key installation for the MLD firmware. The +// legacy ADD_STA_KEY above is absent from fw 89 like the rest of the legacy +// station API; the Linux trace installs both the PTK and the GTK with this. +constexpr uint8_t IWX_SEC_KEY_CMD = 0x18; + +// iwl_sec_key_flags: cipher in the low 3 bits, modifiers above. +constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_CCMP = 0x02; +constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_TKIP = 0x03; +constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_GCMP = 0x05; +constexpr uint32_t IWX_SEC_KEY_FLAG_NO_TX = 0x08; +constexpr uint32_t IWX_SEC_KEY_FLAG_KEY_SIZE = 0x10; // 256-bit key +constexpr uint32_t IWX_SEC_KEY_FLAG_MFP = 0x20; +constexpr uint32_t IWX_SEC_KEY_FLAG_MCAST_KEY = 0x40; + +struct IwxSecKeyCmd { // SEC_KEY_CMD_API_S_VER_1 (the add form) + uint32_t action; // IWX_FW_CTXT_ACTION_* + uint32_t sta_mask; + uint32_t key_id; + uint32_t key_flags; + uint8_t key[32]; + uint8_t tkip_mic_rx_key[8]; + uint8_t tkip_mic_tx_key[8]; + uint64_t rx_seq; + uint64_t tx_seq; +} __attribute__((packed)); + +static_assert(sizeof(IwxSecKeyCmd) == 80, "SEC_KEY_CMD must be 80 bytes"); + // ============================================================================= // Firmware error log (read from device SRAM after an assert) // ============================================================================= diff --git a/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp b/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp index 10048b2..0d17965 100644 --- a/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp +++ b/kernel/src/Drivers/Net/Wifi/IwxTrans.cpp @@ -17,6 +17,7 @@ */ #include "Iwx.hpp" +#include "Ieee80211.hpp" #include #include #include @@ -459,11 +460,15 @@ namespace Drivers::Net::Wifi { asm volatile("" ::: "memory"); } - static bool IwxAllocTxRing(IwxTxRing& ring, int qid) { + // `stageSlots` reserves one page per concurrently queued frame; pass 0 for + // queues that only ever carry host commands. + static bool IwxAllocTxRing(IwxTxRing& ring, int qid, uint32_t stageSlots = 0) { ring.Qid = qid; ring.Cur = 0; ring.CurHw = 0; ring.Queued = 0; + ring.StageSlots = 0; + ring.Active = false; if (!IwxDmaAlloc(ring.Desc, sizeof(IwxTfhTfd) * IWX_TX_RING_COUNT)) return false; @@ -476,6 +481,15 @@ namespace Drivers::Net::Wifi { // are staged in this page instead of the per-slot command area. if (!IwxDmaAlloc(ring.Bounce, 4096)) return false; + + if (stageSlots > IWX_TX_STAGE_SLOTS) stageSlots = IWX_TX_STAGE_SLOTS; + for (uint32_t i = 0; i < stageSlots; i++) { + void* p = Memory::g_pfa->AllocateZeroed(); + if (!p) return false; + ring.Stage[i] = (uint8_t*)p; + ring.StagePhys[i] = Memory::SubHHDM(p); + ring.StageSlots = i + 1; + } return true; } @@ -484,6 +498,14 @@ namespace Drivers::Net::Wifi { IwxDmaFree(ring.BcTbl); IwxDmaFree(ring.Cmd); IwxDmaFree(ring.Bounce); + for (uint32_t i = 0; i < ring.StageSlots; i++) { + if (ring.Stage[i]) { + Memory::g_pfa->Free(ring.Stage[i]); + ring.Stage[i] = nullptr; + } + } + ring.StageSlots = 0; + ring.Active = false; } static void IwxResetTxRing(IwxTxRing& ring) { @@ -846,6 +868,9 @@ namespace Drivers::Net::Wifi { return -1; } + void IwxDumpFwError(); + static uint32_t g_cmdTimeouts = 0; // consecutive unanswered commands + bool IwxSendCmd(IwxHostCmd& hcmd) { if (g_iwx.State == IwxFwState::Error) return false; @@ -909,6 +934,12 @@ namespace Drivers::Net::Wifi { g_iwx.CmdDone = false; g_iwx.LastCmdId = code; + // Keep the payload so a firmware assert can show exactly what it + // choked on -- struct mismatches are invisible without the bytes. + g_iwx.LastCmdLen = hcmd.Len; + uint32_t keep = hcmd.Len < sizeof(g_iwx.LastCmdPayload) + ? hcmd.Len : (uint32_t)sizeof(g_iwx.LastCmdPayload); + if (hcmd.Data && keep) memcpy(g_iwx.LastCmdPayload, hcmd.Data, keep); g_iwx.CmdWantResp = hcmd.WantResp; g_iwx.CmdRespLen = 0; g_iwx.CmdIdx = idx; @@ -921,18 +952,47 @@ namespace Drivers::Net::Wifi { // Wait for the firmware's response/ack. Commands are serialized by // CmdLock, so exactly one can be in flight and the completion is // unambiguous. + // Two independent bounds. The wall clock is the intended one, but it + // is driven by the timer interrupt, so anything that leaves this loop + // running with interrupts disabled would spin forever and take the + // whole machine down with it -- the spin cap makes that impossible. + // Bail immediately if the firmware has asserted, because it will never + // answer this or any later command. + constexpr uint32_t MAX_SPINS = 20000; // ~2 s at 100 us bool ok = false; + bool died = false; uint64_t start = Timekeeping::GetMilliseconds(); - while (Timekeeping::GetMilliseconds() - start < 1000) { + for (uint32_t spins = 0; spins < MAX_SPINS; spins++) { IwxProcessEvents(); if (g_iwx.CmdDone) { ok = true; break; } + if (g_iwx.State == IwxFwState::Error) { died = true; break; } + if (Timekeeping::GetMilliseconds() - start >= 1000) break; IwxDelayUs(100); } if (!ok) { KernelLogStream(WARNING, "WiFi") << "Command 0x" << base::hex - << (uint64_t)code << base::dec << " timed out"; + << (uint64_t)code << base::dec + << (died ? " abandoned: firmware has stopped responding" + : " timed out"); if (ring.Queued > 0) ring.Queued--; + if (!died) { + // Dump on the first silence: the firmware's error table names + // the command that asserted, and it is overwritten as later + // commands go unanswered. + if (++g_cmdTimeouts == 1) IwxDumpFwError(); + // Repeated silence means it is wedged and every later command + // would burn the same timeout, so stop trying. A single late + // response is not worth disabling the adapter over. + if (g_cmdTimeouts >= 3) { + KernelLogStream(ERROR, "WiFi") + << "Firmware stopped responding to host commands"; + g_iwx.FwErrors++; + g_iwx.State = IwxFwState::Error; + } + } + } else { + g_cmdTimeouts = 0; } g_iwx.CmdWantResp = false; @@ -969,10 +1029,10 @@ namespace Drivers::Net::Wifi { // TX queue configuration (used by the connect path) // ========================================================================= - bool IwxEnableTxq(int staId, int qid, int tid) { - IwxTxRing& ring = g_iwx.MgmtQ; + bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) { IwxResetTxRing(ring); ring.Qid = qid; + ring.Active = false; int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD); @@ -1016,14 +1076,280 @@ namespace Drivers::Net::Wifi { return false; auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf; auto* resp = (IwxTxQueueCfgRsp*)pkt->data; + + // On the v3 data-path API the firmware owns queue assignment: `qid` is + // only a hint and the response names the queue we actually got. if (resp->queue_number != qid) { - KernelLogStream(WARNING, "WiFi") << "Firmware assigned queue " - << (uint64_t)resp->queue_number << ", expected " << (uint64_t)qid; + KernelLogStream(INFO, "WiFi") << "Firmware assigned TX queue " + << (uint64_t)resp->queue_number << " (asked for " + << (uint64_t)qid << ")"; + } + ring.Qid = resp->queue_number; + ring.Active = true; + return true; + } + + void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) { + if (!ring.Active) return; + ring.Active = false; + + int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD); + if (cmdVer == 3) { + IwxScdQueueCfgCmd cmd = {}; + cmd.operation = IWX_SCD_QUEUE_REMOVE; + cmd.u.remove.sta_mask = 1u << staId; + cmd.u.remove.tid = (uint32_t)tid; + IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD), + &cmd, sizeof(cmd)); + } else { + IwxTxQueueCfgCmd cmd = {}; + cmd.sta_id = (uint8_t)staId; + cmd.tid = (uint8_t)tid; + cmd.flags = 0; // clear ENABLE_QUEUE + cmd.cb_size = IWX_TFD_QUEUE_CB_SIZE(IWX_TX_RING_COUNT); + cmd.byte_cnt_addr = ring.BcTbl.Phys; + cmd.tfdq_addr = ring.Desc.Phys; + IwxSendCmdPdu(IWX_SCD_QUEUE_CFG, &cmd, sizeof(cmd)); + } + IwxResetTxRing(ring); + } + + // ========================================================================= + // Frame transmission + // ========================================================================= + + // Lowest usable transmit antenna, as a rate_n_flags antenna field. + static uint32_t IwxTxAntBits() { + uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_TX_CHAIN) + >> IWX_FW_PHY_CFG_TX_CHAIN_POS); + if (g_iwx.Nvm.ValidTxAnt) ant &= g_iwx.Nvm.ValidTxAnt; + if (!ant) ant = 1; + uint8_t lowest = (uint8_t)(ant & (uint8_t)(~ant + 1)); // isolate low bit + return (uint32_t)lowest << IWX_RATE_MCS_ANT_POS; + } + + // The lowest basic rate for the current band, in whichever rate_n_flags + // encoding the firmware advertises: 1 Mbps CCK on 2.4 GHz, 6 Mbps OFDM on + // 5 GHz. Management frames go out at this rate because rate control has + // no table for the station until it is associated. + static uint32_t IwxLowestRate() { + uint32_t ant = IwxTxAntBits(); + + // Firmware exposing TX_CMD notification version 7 or later (equally, + // command version 9+) uses the "version 2" rate layout. + int notifVer = IwxLookupNotifVer(IWX_LONG_GROUP, IWX_TX_CMD); + int cmdVer = IwxLookupCmdVer(IWX_LONG_GROUP, IWX_TX_CMD); + bool v2 = notifVer > 6 || cmdVer >= 9; + + if (v2) { + uint32_t mod = g_iwx.Is5GHz ? IWX_RATE_MCS_MOD_LEGACY_OFDM + : IWX_RATE_MCS_MOD_CCK; + return ant | mod | IWX_RATE_MCS_CHAN_WIDTH_20 | 0u; // index 0 + } + + uint32_t plcp = g_iwx.Is5GHz ? IWX_RATE_6M_PLCP : IWX_RATE_1M_PLCP; + uint32_t cck = g_iwx.Is5GHz ? 0 : IWX_RATE_MCS_CCK_MSK_V1; + return ant | cck | plcp; + } + + // Recover a queue whose completions stopped arriving. Without this a + // single lost TX response would permanently consume a slot and, after + // StageSlots of them, wedge the queue. + static uint64_t g_txStallMs = 0; + + static bool IwxTxQueueHasRoom(IwxTxRing& ring) { + if (ring.Queued < ring.StageSlots) { + g_txStallMs = 0; + return true; + } + uint64_t now = Timekeeping::GetMilliseconds(); + if (g_txStallMs == 0) { + g_txStallMs = now; return false; } + if (now - g_txStallMs < 2000) return false; + + KernelLogStream(WARNING, "WiFi") + << "TX queue " << (uint64_t)ring.Qid + << " stopped completing; resetting its outstanding count"; + ring.Queued = 0; + g_txStallMs = 0; return true; } + bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen, + const uint8_t* payload, uint32_t payloadLen, + bool encrypt, bool fixedRate) { + if (g_iwx.State != IwxFwState::Running) return false; + if (!ring.Active || ring.StageSlots == 0) return false; + if (!hdr || hdrLen < IEEE80211_HDR_LEN || hdrLen > 32) return false; + + // The 802.11 header is padded to a dword boundary before the payload; + // TX_CMD_OFFLD_PAD tells the firmware to skip those bytes. + uint32_t padded = (hdrLen + 3) & ~3u; + uint32_t head = (uint32_t)(sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3)) + padded; + if (head + payloadLen > 4096) return false; + + g_iwx.TxLock.Acquire(); + + if (!IwxTxQueueHasRoom(ring)) { + g_iwx.TxLock.Release(); + return false; + } + + uint32_t idx = ring.Cur; + uint32_t slot = idx % ring.StageSlots; + uint8_t* buf = ring.Stage[slot]; + uint64_t phys = ring.StagePhys[slot]; + + memset(buf, 0, head); + + auto* ch = (IwxCmdHeader*)buf; + ch->code = IWX_TX_CMD; + ch->flags = 0; // TX_CMD stays in the legacy group + ch->idx = (uint8_t)idx; + ch->qid = (uint8_t)ring.Qid; + + auto* tx = (IwxTxCmdGen3*)(buf + sizeof(IwxCmdHeader)); + tx->len = (uint16_t)(hdrLen + payloadLen); + + uint16_t flags = 0; + if (!encrypt) flags |= IWX_TX_FLAGS_ENCRYPT_DIS; + if (fixedRate) { + flags |= IWX_TX_FLAGS_CMD_RATE; + tx->rate_n_flags = IwxLowestRate(); + } + tx->flags = flags; + + uint32_t offload = ((hdrLen / 2) & IWX_TX_CMD_OFFLD_MH_MASK) + << IWX_TX_CMD_OFFLD_MH_SIZE_POS; + if (hdrLen % 4) offload |= IWX_TX_CMD_OFFLD_PAD; + tx->offload_assist = offload; + + uint8_t* body = buf + sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3); + memcpy(body, hdr, hdrLen); + if (payloadLen) memcpy(body + padded, payload, payloadLen); + + auto* desc = &((IwxTfhTfd*)ring.Desc.Virt)[idx]; + memset(desc, 0, sizeof(*desc)); + desc->tbs[0].tb_len = (uint16_t)IWX_FIRST_TB_SIZE; + desc->tbs[0].addr = phys; + desc->tbs[1].tb_len = (uint16_t)(head - IWX_FIRST_TB_SIZE); + desc->tbs[1].addr = phys + IWX_FIRST_TB_SIZE; + uint16_t numTbs = 2; + if (payloadLen) { + desc->tbs[2].tb_len = (uint16_t)payloadLen; + desc->tbs[2].addr = phys + head; + numTbs = 3; + } + desc->num_tbs = numTbs; + + // Byte-count table: AX210 wants the frame length in bytes plus the + // number of extra 64-byte chunks the firmware must fetch for the TFD. + uint32_t filled = (uint32_t)(sizeof(uint16_t) + numTbs * sizeof(IwxTfhTb)); + uint32_t chunks = ((filled + 63) / 64) - 1; + auto* bc = (IwxGen3BcTblEntry*)ring.BcTbl.Virt; + bc[idx].tfd_offset = (uint16_t)((hdrLen + payloadLen) | (chunks << 14)); + + asm volatile("" ::: "memory"); + + ring.Queued++; + ring.Cur = (ring.Cur + 1) % IWX_TX_RING_COUNT; + ring.CurHw = (ring.CurHw + 1) % IWX_TFD_QUEUE_SIZE_MAX_GEN3; + IwxWrite32(IWX_HBUS_TARG_WRPTR, ((uint32_t)ring.Qid << 16) | ring.CurHw); + + g_iwx.TxLock.Release(); + return true; + } + + void IwxTxComplete(int qid, int idx, uint32_t status) { + (void)idx; + IwxTxRing* ring = nullptr; + if (g_iwx.MgmtQ.Active && qid == g_iwx.MgmtQ.Qid) ring = &g_iwx.MgmtQ; + if (!ring) return; + + g_iwx.TxLock.Acquire(); + if (ring->Queued > 0) ring->Queued--; + g_iwx.TxLock.Release(); + + if (status == IWX_TX_STATUS_SUCCESS || status == IWX_TX_STATUS_DIRECT_DONE) + g_iwx.TxPackets++; + else + g_iwx.TxFailures++; + } + + // ========================================================================= + // Hardware key installation + // ========================================================================= + + // Keys go in and out through SEC_KEY_CMD: the MLD firmware does not + // implement the legacy ADD_STA_KEY, like the rest of the legacy station + // API. Values mirror the Linux trace: PTK as {sta_mask 1, key_id 0, + // flags CIPHER}, GTK as {sta_mask 1, key_id N, flags CIPHER|MCAST}. + + static uint32_t SecKeyFlags(uint8_t cipher, uint32_t keyLen, bool pairwise) { + uint32_t flags; + switch (cipher) { + case RSN_CIPHER_CCMP: + case RSN_CIPHER_CCMP_256: + flags = IWX_SEC_KEY_FLAG_CIPHER_CCMP; + break; + case RSN_CIPHER_GCMP: + case RSN_CIPHER_GCMP_256: + flags = IWX_SEC_KEY_FLAG_CIPHER_GCMP; + break; + default: + return 0; + } + if (keyLen == 32) flags |= IWX_SEC_KEY_FLAG_KEY_SIZE; + if (!pairwise) flags |= IWX_SEC_KEY_FLAG_MCAST_KEY; + return flags; + } + + bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx, + bool pairwise, uint8_t cipher, const uint8_t* rsc) { + if (!key || (keyLen != 16 && keyLen != 32)) return false; + + uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise); + if (!flags) { + KernelLogStream(WARNING, "WiFi") + << "Cannot install a key for cipher " << (uint64_t)cipher; + return false; + } + + IwxSecKeyCmd cmd = {}; + cmd.action = IWX_FW_CTXT_ACTION_ADD; + cmd.sta_mask = 1u << IWX_STATION_ID; + cmd.key_id = keyIdx; + cmd.key_flags = flags; + memcpy(cmd.key, key, keyLen); + + // The EAPOL RSC carries the AP's packet number for the group key, + // lowest byte first; it becomes the initial receive counter. + if (rsc) { + uint64_t pn = 0; + for (int i = 5; i >= 0; i--) pn = (pn << 8) | rsc[i]; + cmd.rx_seq = pn; + } + + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD), + &cmd, sizeof(cmd)); + } + + bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher, + uint32_t keyLen) { + uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise); + if (!flags) return false; + + IwxSecKeyCmd cmd = {}; + cmd.action = IWX_FW_CTXT_ACTION_REMOVE; + cmd.sta_mask = 1u << IWX_STATION_ID; + cmd.key_id = keyIdx; + cmd.key_flags = flags; + return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD), + &cmd, sizeof(cmd)); + } + // ========================================================================= // RX / notification processing // ========================================================================= @@ -1083,7 +1409,24 @@ namespace Drivers::Net::Wifi { // between them they pin down which host command the firmware rejected. void IwxDumpFwError() { KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert; last command sent: 0x" - << base::hex << (uint64_t)g_iwx.LastCmdId << base::dec; + << base::hex << (uint64_t)g_iwx.LastCmdId << base::dec + << " (" << (uint64_t)g_iwx.LastCmdLen << " byte payload)"; + + // Dump the payload: a struct that does not match the firmware's + // expected layout is otherwise impossible to spot from the log. + { + uint32_t n = g_iwx.LastCmdLen; + if (n > sizeof(g_iwx.LastCmdPayload)) n = sizeof(g_iwx.LastCmdPayload); + for (uint32_t off = 0; off < n; off += 32) { + auto line = KernelLogStream(INFO, "WiFi-FW"); + line << " +" << (uint64_t)off << ": " << base::hex; + for (uint32_t i = off; i < n && i < off + 32; i++) { + if (g_iwx.LastCmdPayload[i] < 0x10) line << "0"; + line << (uint64_t)g_iwx.LastCmdPayload[i]; + } + line << base::dec; + } + } uint32_t base_ = g_iwx.UmacErrorTable; if (base_ < 0x400000) { @@ -1141,6 +1484,16 @@ namespace Drivers::Net::Wifi { case IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_PNVM_INIT_COMPLETE): g_iwx.InitComplete |= 0x2; break; + case IWX_TX_CMD: { + // TX completion for a frame we queued on a data/mgmt queue. + uint32_t status = 0; + if (IwxRxPacketPayloadLen(pkt) >= sizeof(IwxTxResp)) { + auto* r = (const IwxTxResp*)pkt->data; + status = r->status & IWX_TX_STATUS_MSK; + } + IwxTxComplete(qid & ~0x80, pkt->hdr.idx, status); + break; + } case IWX_REPLY_ERROR: { if (IwxRxPacketPayloadLen(pkt) >= 8) { uint32_t errType = *(const uint32_t*)pkt->data; @@ -1208,8 +1561,9 @@ namespace Drivers::Net::Wifi { void IwxProcessEvents() { if (!g_iwx.Mmio) return; - if (g_iwx.InProcessEvents) return; // never nest - g_iwx.InProcessEvents = true; + // Never nest. With interrupts enabled during command waits this is a + // genuine multi-core race, so it has to be an atomic test-and-set. + if (g_iwx.InProcessEvents.test_and_set(std::memory_order_acquire)) return; if (g_msix) { uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD); @@ -1276,7 +1630,7 @@ namespace Drivers::Net::Wifi { IwxNotifIntr(); g_iwx.WorkPending = false; - g_iwx.InProcessEvents = false; + g_iwx.InProcessEvents.clear(std::memory_order_release); } // ========================================================================= @@ -1435,7 +1789,8 @@ namespace Drivers::Net::Wifi { || !IwxDmaAlloc(g_iwx.PrphInfo, 4096) || !IwxAllocRxRing() || !IwxAllocTxRing(g_iwx.CmdQ, IWX_DQA_CMD_QUEUE) - || !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE)) { + || !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE, + IWX_TX_STAGE_SLOTS)) { KernelLogStream(ERROR, "WiFi") << "Could not allocate device DMA memory"; IwxFreeRxRing(); IwxFreeTxRing(g_iwx.CmdQ); diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.cpp b/kernel/src/Drivers/Net/Wifi/Wifi.cpp index d0df900..e5f2e24 100644 --- a/kernel/src/Drivers/Net/Wifi/Wifi.cpp +++ b/kernel/src/Drivers/Net/Wifi/Wifi.cpp @@ -13,6 +13,7 @@ #include "Wifi.hpp" #include "Iwx.hpp" +#include "Wpa.hpp" #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include using namespace Kt; @@ -36,6 +38,10 @@ namespace Drivers::Net::Wifi { static constexpr int MAX_SCAN_RESULTS = 64; + // Long enough for a personal-mode RSN element: version, group cipher, one + // or two pairwise ciphers, a handful of AKMs and the capability field. + static constexpr int MAX_RSN_IE = 64; + struct ScanEntry { uint8_t Bssid[6]; char Ssid[33]; @@ -45,6 +51,9 @@ namespace Drivers::Net::Wifi { uint8_t Band; // 0 = 2.4 GHz, 1 = 5 GHz uint8_t Security; // WifiSecurity value uint16_t BeaconInterval; + uint8_t DtimPeriod; + uint8_t RsnIe[MAX_RSN_IE]; // element body, without id/len + uint8_t RsnIeLen; bool Used; }; @@ -71,6 +80,7 @@ namespace Drivers::Net::Wifi { // Element IDs used here. static constexpr uint8_t ELEMID_SSID = 0; static constexpr uint8_t ELEMID_DSPARMS = 3; + static constexpr uint8_t ELEMID_TIM = 5; static constexpr uint8_t ELEMID_RSN = 48; static constexpr uint8_t ELEMID_VENDOR = 221; @@ -108,6 +118,9 @@ namespace Drivers::Net::Wifi { uint8_t Channel = 0; uint8_t Security = WIFI_SEC_OPEN; uint16_t BeaconInterval = 0; + uint8_t DtimPeriod = 0; + const uint8_t* Rsn = nullptr; // RSN element body + uint8_t RsnLen = 0; }; static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) { @@ -139,9 +152,18 @@ namespace Drivers::Net::Wifi { case ELEMID_DSPARMS: if (ielen >= 1) out->Channel = body[0]; break; + case ELEMID_TIM: + // DTIM count, then DTIM period. The firmware needs the + // period to schedule wake-ups once associated. + if (ielen >= 2) out->DtimPeriod = body[1]; + break; case ELEMID_RSN: haveRsn = true; out->Security = ClassifyRsn(body, ielen); + // Kept verbatim: the connect path negotiates ciphers and + // AKMs straight out of it. + out->Rsn = body; + out->RsnLen = ielen; break; case ELEMID_VENDOR: // WPA1: Microsoft OUI 00:50:F2, type 1. @@ -220,6 +242,12 @@ namespace Drivers::Net::Wifi { slot->Band = ch > 14 ? 1 : 0; slot->Security = pb.Security; slot->BeaconInterval = pb.BeaconInterval; + if (pb.DtimPeriod) slot->DtimPeriod = pb.DtimPeriod; + slot->RsnIeLen = 0; + if (pb.Rsn && pb.RsnLen && pb.RsnLen <= MAX_RSN_IE) { + memcpy(slot->RsnIe, pb.Rsn, pb.RsnLen); + slot->RsnIeLen = pb.RsnLen; + } g_resultLock.Release(); } @@ -371,8 +399,18 @@ namespace Drivers::Net::Wifi { | (g_iwx.Nvm.Sku52GHz ? 2 : 0)); out->channels = (uint16_t)g_iwx.ChannelCount; out->rxPackets = g_iwx.RxPackets; + out->txPackets = g_iwx.TxPackets; out->fwErrors = (uint32_t)g_iwx.FwErrors; out->connState = (uint32_t)IwxConnectState(); + out->connected = IwxLinkUp() ? 1 : 0; + + if (IwxConnectState() != (int)IwxConnStateId::Idle) { + const char* ssid = IwxConnectSsid(); + int k = 0; + for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k]; + out->ssid[k] = '\0'; + memcpy(out->bssid, IwxConnectBssid(), 6); + } int i = 0; for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i]; @@ -380,55 +418,136 @@ namespace Drivers::Net::Wifi { return 0; } + // Wait for the association + handshake to settle. The whole exchange is + // driven from IwxConnectService(), so this pumps both while it waits + // instead of relying on the idle loop getting scheduled. + static int WaitForConnection(uint32_t timeoutMs) { + uint64_t start = Timekeeping::GetMilliseconds(); + + while (Timekeeping::GetMilliseconds() - start < timeoutMs) { + IwxProcessEvents(); + IwxConnectService(); + + auto state = (IwxConnStateId)IwxConnectState(); + if (state == IwxConnStateId::Connected) return 0; + if (state == IwxConnStateId::Failed) { + // A handshake that got as far as exchanging EAPOL frames and + // then failed is almost always a wrong passphrase. + int rc = WpaGetState() == WpaState::Failed + ? WIFI_ERR_AUTH : WIFI_ERR_FAILED; + // A rejection seen on the RX path only marks the state; the + // firmware contexts are still up and have to come back down. + IwxConnectAbort(); + return rc; + } + if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED; + if (g_iwx.State == IwxFwState::Error) { + IwxConnectAbort(); + return WIFI_ERR_FAILED; + } + + // Yield rather than burn the core: this can wait seconds, and the + // idle loop on other cores drives the same state machine. + Sched::Schedule(); + } + + IwxConnectAbort(); + return WIFI_ERR_TIMEOUT; + } + int Connect(const char* ssid, const char* password) { - if (!g_initialized || !ssid) return -1; + if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER; + if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER; // Locate the network in the most recent scan results: the firmware - // contexts need its BSSID and channel. + // contexts need its BSSID and channel, and the connect path needs its + // RSN element to negotiate ciphers. uint8_t bssid[6]; uint8_t channel = 0; bool is5 = false; uint8_t security = WIFI_SEC_OPEN; + uint8_t rsnIe[MAX_RSN_IE]; + uint8_t rsnIeLen = 0; + uint16_t beaconInterval = 0; + uint8_t dtimPeriod = 0; bool found = false; - g_resultLock.Acquire(); - for (int i = 0; i < MAX_SCAN_RESULTS; i++) { - if (!g_results[i].Used) continue; - const ScanEntry& e = g_results[i]; - bool match = true; - for (int k = 0; k < 32; k++) { - char a = e.Ssid[k], b = ssid[k]; - if (a != b) { match = false; break; } - if (a == '\0') break; + // Strongest first, so a network seen on several bands or repeaters is + // joined through the best AP rather than whichever answered first. + int8_t bestRssi = -128; + + for (int attempt = 0; attempt < 2 && !found; attempt++) { + if (attempt == 1) { + // Nothing matched: the caller may never have scanned, or the + // results may predate this network appearing. + KernelLogStream(INFO, "WiFi") + << "\"" << ssid << "\" is not in the scan results; scanning again"; + WifiNetwork tmp[1]; + Scan(tmp, 1, 4000); } - if (!match) continue; - memcpy(bssid, e.Bssid, 6); - channel = e.Channel; - is5 = e.Band == 1; - security = e.Security; - found = true; - break; - } - g_resultLock.Release(); - if (!found) { + g_resultLock.Acquire(); + for (int i = 0; i < MAX_SCAN_RESULTS; i++) { + if (!g_results[i].Used) continue; + const ScanEntry& e = g_results[i]; + bool match = true; + for (int k = 0; k < 32; k++) { + char a = e.Ssid[k], b = ssid[k]; + if (a != b) { match = false; break; } + if (a == '\0') break; + } + if (!match) continue; + if (found && e.Rssi <= bestRssi) continue; + memcpy(bssid, e.Bssid, 6); + channel = e.Channel; + is5 = e.Band == 1; + security = e.Security; + rsnIeLen = e.RsnIeLen; + if (rsnIeLen) memcpy(rsnIe, e.RsnIe, rsnIeLen); + beaconInterval = e.BeaconInterval; + dtimPeriod = e.DtimPeriod; + bestRssi = e.Rssi; + found = true; + } + g_resultLock.Release(); + } + + if (!found) return WIFI_ERR_NOT_FOUND; + + bool havePassword = password && password[0]; + + // WEP and WPA1 use RC4/TKIP, which the supplicant deliberately does + // not implement; say so rather than failing mid-handshake. + if (security == WIFI_SEC_WEP || security == WIFI_SEC_WPA) { KernelLogStream(WARNING, "WiFi") - << "Network not in scan results; run a scan first"; - return -1; + << "WEP and the original WPA use ciphers this driver does not implement"; + return WIFI_ERR_UNSUPPORTED; } - // Encryption is checked before the passphrase: the WPA2/WPA3 key - // exchange (PMK derivation, EAPOL 4-way, HW key install) is not - // implemented at all, so a passphrase would not help and reporting - // "needs a passphrase" would be misleading. if (security != WIFI_SEC_OPEN) { - KernelLogStream(WARNING, "WiFi") - << "Encrypted networks are not supported yet (open only)"; - return -2; + if (!rsnIeLen) { + KernelLogStream(WARNING, "WiFi") + << "The beacon for this network carried no RSN element"; + return WIFI_ERR_UNSUPPORTED; + } + if (!havePassword) return WIFI_ERR_NEED_KEY; } - (void)password; - return IwxConnectStart(bssid, channel, is5, ssid) ? 0 : -1; + if (!IwxConnectStart(bssid, channel, is5, ssid, + havePassword ? password : nullptr, + security == WIFI_SEC_OPEN ? nullptr : rsnIe, + security == WIFI_SEC_OPEN ? 0 : rsnIeLen, + beaconInterval, dtimPeriod)) { + // IwxConnectStart already logged the specific reason. Only report + // a security problem when that is genuinely what happened; a radio + // or firmware failure needs completely different advice. + bool security_ = IwxConnectRefusedForSecurity(); + auto state = (IwxConnStateId)IwxConnectState(); + if (state != IwxConnStateId::Idle) IwxConnectAbort(); + return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED; + } + + return WaitForConnection(15000); } int Disconnect() { @@ -436,4 +555,26 @@ namespace Drivers::Net::Wifi { IwxConnectAbort(); return 0; } + + // ========================================================================= + // Network interface + // ========================================================================= + + static RxCallback g_rxCallback = nullptr; + + const uint8_t* GetMacAddress() { return g_iwx.Nvm.HwAddr; } + + bool IsLinkUp() { return g_initialized && IwxLinkUp(); } + + bool SendPacket(const uint8_t* data, uint16_t length) { + if (!IsLinkUp()) return false; + return IwxConnectSendEthernet(data, length); + } + + void SetRxCallback(RxCallback callback) { g_rxCallback = callback; } + + // Called by the connect path for every decapsulated Ethernet frame. + void WifiRxEthernet(const uint8_t* frame, uint32_t len) { + if (g_rxCallback && len <= 0xffff) g_rxCallback(frame, (uint16_t)len); + } } diff --git a/kernel/src/Drivers/Net/Wifi/Wifi.hpp b/kernel/src/Drivers/Net/Wifi/Wifi.hpp index c176fe3..4d70a02 100644 --- a/kernel/src/Drivers/Net/Wifi/Wifi.hpp +++ b/kernel/src/Drivers/Net/Wifi/Wifi.hpp @@ -32,9 +32,24 @@ namespace Drivers::Net::Wifi { // Fill in adapter/firmware status. int GetInfo(montauk::abi::WifiInfo* out); - // Association groundwork. Returns 0 when the firmware contexts came up, - // negative on failure. See IwxConnect.cpp: the 802.11 handshake itself is - // not implemented yet, so this cannot establish a usable link. + // Join a network. Blocks (pumping firmware events) until the link is up + // or the attempt fails. Returns 0 on success, or a negative WIFI_ERR_* + // value describing why it could not connect. int Connect(const char* ssid, const char* password); int Disconnect(); + + // ------------------------------------------------------------------------- + // Network interface, registered with Net::NetIf once associated + // ------------------------------------------------------------------------- + + const uint8_t* GetMacAddress(); + + // Send an Ethernet frame. Fails while the link is down. + bool SendPacket(const uint8_t* data, uint16_t length); + + // True once a network has been joined and, for encrypted networks, keyed. + bool IsLinkUp(); + + using RxCallback = void(*)(const uint8_t* data, uint16_t length); + void SetRxCallback(RxCallback callback); } diff --git a/kernel/src/Drivers/Net/Wifi/Wpa.cpp b/kernel/src/Drivers/Net/Wifi/Wpa.cpp new file mode 100644 index 0000000..7581bcb --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/Wpa.cpp @@ -0,0 +1,830 @@ +/* + * Wpa.cpp + * WPA2/WPA3-PSK supplicant: PMK derivation, the EAPOL-Key 4-way handshake + * and the group-key handshake used for periodic GTK rekeying. + * + * Supported: RSN (WPA2) with CCMP or GCMP, AKM PSK (00-0F-AC:2) and + * PSK-SHA256 (:6). Key descriptor versions 2 (HMAC-SHA1-128 MIC, AES key + * wrap) and 3 (AES-128-CMAC MIC) are handled. + * + * Not supported: SAE (WPA3 needs finite-field / elliptic-curve crypto that + * is well beyond what belongs in this kernel), TKIP and WEP (descriptor + * version 1 needs HMAC-MD5 and RC4, and the ciphers are broken anyway), and + * 802.1X/EAP enterprise authentication. Those are rejected up front with a + * clear reason rather than failing halfway through the handshake. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Wpa.hpp" +#include "Ieee80211.hpp" +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Net::Wifi { + + // ========================================================================= + // EAPOL-Key frame layout (IEEE 802.1X-2004 + IEEE 802.11 key descriptor) + // ========================================================================= + + constexpr uint8_t EAPOL_TYPE_KEY = 3; + constexpr uint8_t EAPOL_KEY_DESC_RSN = 2; + constexpr uint8_t EAPOL_KEY_DESC_WPA = 254; + + constexpr uint16_t KEY_INFO_VERSION_MASK = 0x0007; + constexpr uint16_t KEY_INFO_KEY_TYPE = 0x0008; // set = pairwise + constexpr uint16_t KEY_INFO_INSTALL = 0x0040; + constexpr uint16_t KEY_INFO_ACK = 0x0080; + constexpr uint16_t KEY_INFO_MIC = 0x0100; + constexpr uint16_t KEY_INFO_SECURE = 0x0200; + constexpr uint16_t KEY_INFO_ERROR = 0x0400; + constexpr uint16_t KEY_INFO_REQUEST = 0x0800; + constexpr uint16_t KEY_INFO_ENCRYPTED = 0x1000; + + // Key descriptor versions: 1 is HMAC-MD5 + RC4 (unsupported), 2 is + // HMAC-SHA1-128 + AES key wrap, 3 is AES-128-CMAC + AES key wrap. + constexpr uint8_t KEY_DESC_VER_RC4 = 1; + constexpr uint8_t KEY_DESC_VER_AES = 2; + constexpr uint8_t KEY_DESC_VER_AES_CMAC = 3; + + struct EapolKey { + uint8_t version; + uint8_t type; + uint8_t length[2]; // big endian, bytes after this field + uint8_t descType; + uint8_t keyInfo[2]; // big endian + uint8_t keyLength[2]; // big endian + uint8_t replay[8]; + uint8_t nonce[32]; + uint8_t iv[16]; + uint8_t rsc[8]; + uint8_t keyId[8]; + uint8_t mic[16]; + uint8_t keyDataLen[2]; // big endian + // key data follows + } __attribute__((packed)); + + static_assert(sizeof(EapolKey) == 99, "EAPOL-Key header must stay 99 bytes"); + + constexpr uint32_t EAPOL_MIC_LEN = 16; + constexpr uint32_t MAX_KEY_DATA = 512; + constexpr uint32_t MAX_EAPOL_TX = sizeof(EapolKey) + 128; + + // ========================================================================= + // Supplicant state + // ========================================================================= + + static WpaConfig g_cfg = {}; + static WpaState g_state = WpaState::Idle; + + static uint8_t g_pmk[32]; + static uint8_t g_ptk[64]; // KCK | KEK | TK + static uint32_t g_kckLen = 16; + static uint32_t g_kekLen = 16; + static uint32_t g_tkLen = 16; + + static uint8_t g_anonce[32]; + static uint8_t g_snonce[32]; + static uint8_t g_replay[8]; + static bool g_haveReplay = false; + static uint8_t g_keyDescVer = KEY_DESC_VER_AES; + static uint8_t g_descType = EAPOL_KEY_DESC_RSN; + static uint8_t g_eapolVersion = 2; + + static uint8_t g_rsnIe[32]; + static uint32_t g_rsnIeLen = 0; + + // Retransmission of the last message we sent. The AP retries msg 1 and 3 + // on its own, but a lost msg 2 or 4 otherwise stalls the exchange until the + // AP gives up and deauthenticates. + static uint8_t g_lastTx[MAX_EAPOL_TX]; + static uint32_t g_lastTxLen = 0; + static uint64_t g_lastTxMs = 0; + static int g_retries = 0; + static uint64_t g_startMs = 0; + + static constexpr uint64_t RETRY_INTERVAL_MS = 500; + static constexpr int MAX_RETRIES = 4; + static constexpr uint64_t HANDSHAKE_TIMEOUT_MS = 5000; + + // ========================================================================= + // Nonce generation + // ========================================================================= + + // A SNonce only has to be unique per (PMK, ANonce) pair, but making it + // unpredictable costs nothing: fold a run of TSC samples into SHA-256 along + // with our MAC and a monotonic counter. RDRAND is deliberately avoided -- + // see Api/Random.hpp for why it is unreliable on this hardware. + static void GenNonce(uint8_t out[32]) { + static uint64_t counter = 0; + + struct { + uint64_t tsc[8]; + uint64_t counter; + uint8_t mac[6]; + uint8_t pad[2]; + } pool; + + for (int i = 0; i < 8; i++) { + uint64_t tsc; + asm volatile("rdtsc; shl $32, %%rdx; or %%rdx, %%rax" + : "=a"(tsc) :: "rdx"); + pool.tsc[i] = tsc; + // Space the samples out so the low bits differ between them. + for (int k = 0; k < 64; k++) asm volatile("pause" ::: "memory"); + } + pool.counter = ++counter; + memcpy(pool.mac, g_cfg.OwnMac, 6); + pool.pad[0] = pool.pad[1] = 0; + + Crypto::Sha256(&pool, sizeof(pool), out); + } + + // ========================================================================= + // Key derivation + // ========================================================================= + + // IEEE 802.11 PRF-N built on HMAC-SHA1 (used by AKM PSK). + static void Prf(const uint8_t* key, uint32_t keyLen, const char* label, + const uint8_t* data, uint32_t dataLen, + uint8_t* out, uint32_t outLen) { + uint32_t labelLen = 0; + while (label[labelLen]) labelLen++; + + uint8_t counter = 0; + uint32_t pos = 0; + const uint8_t zero = 0; + + while (pos < outLen) { + const uint8_t* parts[4] = { + (const uint8_t*)label, &zero, data, &counter + }; + size_t lens[4] = { labelLen, 1, dataLen, 1 }; + + uint8_t digest[Crypto::SHA1_DIGEST_SIZE]; + Crypto::HmacSha1(key, keyLen, parts, lens, 4, digest); + + uint32_t take = outLen - pos; + if (take > Crypto::SHA1_DIGEST_SIZE) take = Crypto::SHA1_DIGEST_SIZE; + memcpy(out + pos, digest, take); + pos += take; + counter++; + Crypto::SecureZero(digest, sizeof(digest)); + } + } + + // IEEE 802.11 KDF built on HMAC-SHA256 (used by AKM PSK-SHA256 and SAE). + static void KdfSha256(const uint8_t* key, uint32_t keyLen, const char* label, + const uint8_t* data, uint32_t dataLen, + uint8_t* out, uint32_t outLen) { + uint32_t labelLen = 0; + while (label[labelLen]) labelLen++; + + uint16_t bits = (uint16_t)(outLen * 8); + uint8_t lenLe[2] = { (uint8_t)bits, (uint8_t)(bits >> 8) }; + + uint16_t iter = 1; + uint32_t pos = 0; + while (pos < outLen) { + uint8_t iterLe[2] = { (uint8_t)iter, (uint8_t)(iter >> 8) }; + const uint8_t* parts[4] = { + iterLe, (const uint8_t*)label, data, lenLe + }; + size_t lens[4] = { 2, labelLen, dataLen, 2 }; + + uint8_t digest[Crypto::SHA256_DIGEST_SIZE]; + Crypto::HmacSha256(key, keyLen, parts, lens, 4, digest); + + uint32_t take = outLen - pos; + if (take > Crypto::SHA256_DIGEST_SIZE) take = Crypto::SHA256_DIGEST_SIZE; + memcpy(out + pos, digest, take); + pos += take; + iter++; + Crypto::SecureZero(digest, sizeof(digest)); + } + } + + static bool UsesSha256Kdf(uint8_t akm) { + return akm == RSN_AKM_PSK_SHA256 || akm == RSN_AKM_SAE + || akm == RSN_AKM_FT_SAE; + } + + // PTK = KDF(PMK, "Pairwise key expansion", + // min(AA,SPA) || max(AA,SPA) || min(ANonce,SNonce) || max(...)) + static void DerivePtk() { + const uint8_t* aa = g_cfg.Bssid; + const uint8_t* spa = g_cfg.OwnMac; + + uint8_t data[76]; + int cmp = memcmp(aa, spa, 6); + const uint8_t* lo = (cmp < 0) ? aa : spa; + const uint8_t* hi = (cmp < 0) ? spa : aa; + memcpy(data, lo, 6); + memcpy(data + 6, hi, 6); + + cmp = memcmp(g_anonce, g_snonce, 32); + const uint8_t* nlo = (cmp < 0) ? g_anonce : g_snonce; + const uint8_t* nhi = (cmp < 0) ? g_snonce : g_anonce; + memcpy(data + 12, nlo, 32); + memcpy(data + 44, nhi, 32); + + uint32_t ptkLen = g_kckLen + g_kekLen + g_tkLen; + if (UsesSha256Kdf(g_cfg.Akm)) + KdfSha256(g_pmk, 32, "Pairwise key expansion", data, sizeof(data), + g_ptk, ptkLen); + else + Prf(g_pmk, 32, "Pairwise key expansion", data, sizeof(data), + g_ptk, ptkLen); + + Crypto::SecureZero(data, sizeof(data)); + } + + static const uint8_t* Kck() { return g_ptk; } + static const uint8_t* Kek() { return g_ptk + g_kckLen; } + static const uint8_t* Tk() { return g_ptk + g_kckLen + g_kekLen; } + + // ========================================================================= + // MIC + // ========================================================================= + + // The MIC covers the whole 802.1X frame with the MIC field zeroed. + static void ComputeMic(const uint8_t* frame, uint32_t len, uint8_t out[16]) { + if (g_keyDescVer == KEY_DESC_VER_AES_CMAC) { + const uint8_t* parts[1] = { frame }; + size_t lens[1] = { len }; + Crypto::AesCmac(Kck(), g_kckLen, parts, lens, 1, out); + } else { + uint8_t digest[Crypto::SHA1_DIGEST_SIZE]; + Crypto::HmacSha1(Kck(), g_kckLen, frame, len, digest); + memcpy(out, digest, 16); + Crypto::SecureZero(digest, sizeof(digest)); + } + } + + static bool VerifyMic(const uint8_t* frame, uint32_t len) { + // Copy so the received frame can keep its MIC for logging. + static uint8_t scratch[sizeof(EapolKey) + MAX_KEY_DATA]; + if (len > sizeof(scratch)) return false; + memcpy(scratch, frame, len); + + auto* k = (EapolKey*)scratch; + uint8_t received[16]; + memcpy(received, k->mic, 16); + memset(k->mic, 0, 16); + + uint8_t computed[16]; + ComputeMic(scratch, len, computed); + return Crypto::SecureEqual(received, computed, 16); + } + + // ========================================================================= + // RSN information element + // ========================================================================= + + static void PutSuite(uint8_t* p, uint8_t type) { + p[0] = 0x00; p[1] = 0x0f; p[2] = 0xac; p[3] = type; + } + + static bool IsRsnSuite(const uint8_t* p) { + return p[0] == 0x00 && p[1] == 0x0f && p[2] == 0xac; + } + + uint32_t WpaBuildRsnIe(uint8_t* out, uint32_t cap) { + constexpr uint32_t BODY = 20; + if (cap < BODY + 2) return 0; + + uint8_t* p = out; + *p++ = IEEE80211_ELEMID_RSN; + *p++ = (uint8_t)BODY; + Put16Le(p, 1); p += 2; // RSN version + PutSuite(p, g_cfg.GroupCipher); p += 4; + Put16Le(p, 1); p += 2; // one pairwise cipher + PutSuite(p, g_cfg.PairwiseCipher); p += 4; + Put16Le(p, 1); p += 2; // one AKM + PutSuite(p, g_cfg.Akm); p += 4; + // RSN capabilities: no PMF (BIP/IGTK is not implemented), no preauth, + // one replay counter per key. + Put16Le(p, 0); p += 2; + + return (uint32_t)(p - out); + } + + bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg) { + if (len < 8) return false; + uint32_t off = 0; + + uint16_t version = Get16Le(ie); + off += 2; + if (version != 1) return false; + + if (off + 4 > len) return false; + uint8_t groupCipher = RSN_CIPHER_CCMP; + if (IsRsnSuite(ie + off)) groupCipher = ie[off + 3]; + off += 4; + + if (off + 2 > len) return false; + uint16_t pairwiseCount = Get16Le(ie + off); + off += 2; + + // Prefer CCMP; fall back to GCMP. TKIP-only networks are rejected. + int bestPairwise = -1; + for (uint16_t i = 0; i < pairwiseCount && off + 4 <= len; i++, off += 4) { + if (!IsRsnSuite(ie + off)) continue; + uint8_t c = ie[off + 3]; + if (c == RSN_CIPHER_CCMP) bestPairwise = c; + else if (c == RSN_CIPHER_GCMP && bestPairwise < 0) bestPairwise = c; + else if (c == RSN_CIPHER_GCMP_256 && bestPairwise < 0) bestPairwise = c; + else if (c == RSN_CIPHER_CCMP_256 && bestPairwise < 0) bestPairwise = c; + } + if (bestPairwise < 0) { + KernelLogStream(WARNING, "WiFi") + << "AP offers no supported pairwise cipher (CCMP/GCMP required)"; + return false; + } + + if (off + 2 > len) return false; + uint16_t akmCount = Get16Le(ie + off); + off += 2; + + // Prefer plain PSK; PSK-SHA256 works too but needs the SHA-256 KDF. + int bestAkm = -1; + bool sawSae = false; + for (uint16_t i = 0; i < akmCount && off + 4 <= len; i++, off += 4) { + if (!IsRsnSuite(ie + off)) continue; + uint8_t a = ie[off + 3]; + if (a == RSN_AKM_SAE || a == RSN_AKM_FT_SAE) sawSae = true; + if (a == RSN_AKM_PSK) bestAkm = a; + else if (a == RSN_AKM_PSK_SHA256 && bestAkm < 0) bestAkm = a; + } + if (bestAkm < 0) { + if (sawSae) + KernelLogStream(WARNING, "WiFi") + << "Network is WPA3-only (SAE); SAE authentication is not implemented"; + else + KernelLogStream(WARNING, "WiFi") + << "AP offers no pre-shared-key AKM (enterprise 802.1X is not supported)"; + return false; + } + + uint16_t rsnCaps = 0; + if (off + 2 <= len) rsnCaps = Get16Le(ie + off); + + if (rsnCaps & RSN_CAP_MFPR) { + KernelLogStream(WARNING, "WiFi") + << "AP requires management frame protection, which needs BIP; not supported"; + return false; + } + + // A mixed WPA/WPA2 network can pair with CCMP but still broadcast under + // TKIP. The firmware key slot and the RX decryption check both only + // handle CCMP/GCMP, so the group key would silently fail to install and + // every broadcast frame -- ARP requests, broadcast DHCP replies -- + // would be dropped. Refuse up front instead of half connecting. + if (groupCipher != RSN_CIPHER_CCMP && groupCipher != RSN_CIPHER_GCMP + && groupCipher != RSN_CIPHER_CCMP_256 + && groupCipher != RSN_CIPHER_GCMP_256) { + KernelLogStream(WARNING, "WiFi") + << "AP broadcasts with an unsupported group cipher (" + << (uint64_t)groupCipher + << "); this is usually a mixed WPA/WPA2 network still using TKIP"; + return false; + } + + cfg.GroupCipher = groupCipher; + cfg.PairwiseCipher = (uint8_t)bestPairwise; + cfg.Akm = (uint8_t)bestAkm; + cfg.Mfp = false; + return true; + } + + // ========================================================================= + // Start / reset + // ========================================================================= + + static bool HexNibble(char c, uint8_t* out) { + if (c >= '0' && c <= '9') { *out = (uint8_t)(c - '0'); return true; } + if (c >= 'a' && c <= 'f') { *out = (uint8_t)(c - 'a' + 10); return true; } + if (c >= 'A' && c <= 'F') { *out = (uint8_t)(c - 'A' + 10); return true; } + return false; + } + + // A 64-character hex string is the raw 256-bit PSK; anything else is a + // passphrase and goes through PBKDF2 with the SSID as salt. + static bool DerivePmk() { + if (g_cfg.PassLen == 64) { + bool allHex = true; + uint8_t tmp[32]; + for (int i = 0; i < 32; i++) { + uint8_t hi, lo; + if (!HexNibble(g_cfg.Passphrase[i * 2], &hi) + || !HexNibble(g_cfg.Passphrase[i * 2 + 1], &lo)) { + allHex = false; + break; + } + tmp[i] = (uint8_t)((hi << 4) | lo); + } + if (allHex) { + memcpy(g_pmk, tmp, 32); + Crypto::SecureZero(tmp, sizeof(tmp)); + return true; + } + } + + if (g_cfg.PassLen < 8) { + KernelLogStream(WARNING, "WiFi") + << "WPA passphrases must be at least 8 characters"; + return false; + } + + // 4096 iterations of HMAC-SHA1 over a 32-byte output: this is a couple + // of hundred milliseconds of pure CPU, once per connect. + Crypto::Pbkdf2Sha1(g_cfg.Passphrase, g_cfg.PassLen, + g_cfg.Ssid, g_cfg.SsidLen, 4096, g_pmk, 32); + return true; + } + + void WpaReset() { + g_state = WpaState::Idle; + g_haveReplay = false; + g_lastTxLen = 0; + g_retries = 0; + Crypto::SecureZero(g_pmk, sizeof(g_pmk)); + Crypto::SecureZero(g_ptk, sizeof(g_ptk)); + Crypto::SecureZero(g_snonce, sizeof(g_snonce)); + Crypto::SecureZero(&g_cfg.Passphrase, sizeof(g_cfg.Passphrase)); + } + + bool WpaStart(const WpaConfig& cfg) { + WpaReset(); + g_cfg = cfg; + + switch (g_cfg.PairwiseCipher) { + case RSN_CIPHER_CCMP: + case RSN_CIPHER_GCMP: + g_tkLen = 16; + break; + case RSN_CIPHER_CCMP_256: + case RSN_CIPHER_GCMP_256: + g_tkLen = 32; + break; + default: + KernelLogStream(WARNING, "WiFi") + << "Unsupported pairwise cipher " << (uint64_t)g_cfg.PairwiseCipher; + return false; + } + g_kckLen = 16; + g_kekLen = 16; + + if (!DerivePmk()) return false; + + g_rsnIeLen = WpaBuildRsnIe(g_rsnIe, sizeof(g_rsnIe)); + if (!g_rsnIeLen) return false; + + GenNonce(g_snonce); + g_state = WpaState::WaitMsg1; + g_startMs = 0; + + KernelLogStream(INFO, "WiFi") << "Starting WPA handshake (AKM " + << (uint64_t)g_cfg.Akm << ", pairwise cipher " + << (uint64_t)g_cfg.PairwiseCipher << ")"; + return true; + } + + WpaState WpaGetState() { return g_state; } + bool WpaIsComplete() { return g_state == WpaState::Complete; } + + // ========================================================================= + // Outbound messages + // ========================================================================= + + static bool SendKeyFrame(uint16_t keyInfo, const uint8_t* keyData, + uint32_t keyDataLen, const uint8_t* nonce) { + uint32_t total = sizeof(EapolKey) + keyDataLen; + if (total > sizeof(g_lastTx)) return false; + + memset(g_lastTx, 0, total); + auto* k = (EapolKey*)g_lastTx; + + k->version = g_eapolVersion; + k->type = EAPOL_TYPE_KEY; + Put16Be(k->length, (uint16_t)(total - 4)); + k->descType = g_descType; + Put16Be(k->keyInfo, keyInfo); + Put16Be(k->keyLength, 0); // RSN: always zero from the STA + memcpy(k->replay, g_replay, 8); + if (nonce) memcpy(k->nonce, nonce, 32); + Put16Be(k->keyDataLen, (uint16_t)keyDataLen); + if (keyDataLen) memcpy(g_lastTx + sizeof(EapolKey), keyData, keyDataLen); + + if (keyInfo & KEY_INFO_MIC) ComputeMic(g_lastTx, total, k->mic); + + g_lastTxLen = total; + g_lastTxMs = Timekeeping::GetMilliseconds(); + g_retries = 0; + return WpaTxEapol(g_lastTx, total); + } + + static bool SendMsg2() { + uint16_t keyInfo = (uint16_t)(g_keyDescVer | KEY_INFO_KEY_TYPE + | KEY_INFO_MIC); + return SendKeyFrame(keyInfo, g_rsnIe, g_rsnIeLen, g_snonce); + } + + static bool SendMsg4() { + uint16_t keyInfo = (uint16_t)(g_keyDescVer | KEY_INFO_KEY_TYPE + | KEY_INFO_MIC | KEY_INFO_SECURE); + return SendKeyFrame(keyInfo, nullptr, 0, nullptr); + } + + // Group key handshake reply: same shape as msg 4 without the pairwise bit. + static bool SendGroupAck() { + uint16_t keyInfo = (uint16_t)(g_keyDescVer | KEY_INFO_MIC + | KEY_INFO_SECURE); + return SendKeyFrame(keyInfo, nullptr, 0, nullptr); + } + + // ========================================================================= + // Key data (KDE) handling + // ========================================================================= + + // Decrypt the key data field of msg 3 or a group-key message. Returns the + // plaintext length, or 0 on failure. + static uint32_t DecryptKeyData(const uint8_t* in, uint32_t inLen, + uint8_t* out, uint32_t outCap) { + if (g_keyDescVer == KEY_DESC_VER_RC4) { + KernelLogStream(WARNING, "WiFi") + << "AP used RC4 key wrapping (WPA1/TKIP), which is not supported"; + return 0; + } + if (inLen < 24 || (inLen % 8) != 0 || inLen - 8 > outCap) return 0; + if (!Crypto::AesKeyUnwrap(Kek(), g_kekLen, in, inLen, out)) { + KernelLogStream(WARNING, "WiFi") + << "EAPOL key data failed its integrity check (wrong passphrase?)"; + return 0; + } + return inLen - 8; + } + + // Walk the KDE list looking for a GTK (00-0F-AC data type 1). + static bool FindGtk(const uint8_t* data, uint32_t len, + const uint8_t** gtk, uint32_t* gtkLen, uint8_t* keyIdx) { + uint32_t off = 0; + while (off + 2 <= len) { + uint8_t id = data[off]; + uint8_t elen = data[off + 1]; + if (elen == 0 || off + 2 + elen > len) break; + const uint8_t* body = data + off + 2; + + if (id == IEEE80211_ELEMID_VENDOR && elen >= 6 && IsRsnSuite(body) + && body[3] == 1) { + // GTK KDE: [OUI 3][type 1][keyid+tx 1][reserved 1][GTK ...] + *keyIdx = (uint8_t)(body[4] & 0x03); + *gtk = body + 6; + *gtkLen = (uint32_t)(elen - 6); + return true; + } + off += 2 + elen; + } + return false; + } + + // ========================================================================= + // Inbound handling + // ========================================================================= + + static void Fail(const char* why) { + KernelLogStream(WARNING, "WiFi") << "WPA handshake failed: " << why; + g_state = WpaState::Failed; + Crypto::SecureZero(g_ptk, sizeof(g_ptk)); + } + + static void HandleMsg1(const uint8_t* frame, uint32_t len) { + (void)len; + auto* k = (const EapolKey*)frame; + + memcpy(g_anonce, k->nonce, 32); + memcpy(g_replay, k->replay, 8); + g_haveReplay = true; + + DerivePtk(); + + if (!SendMsg2()) { + Fail("could not transmit message 2"); + return; + } + g_state = WpaState::WaitMsg3; + KernelLogStream(INFO, "WiFi") << "Handshake message 1 received; sent message 2"; + } + + static void HandleMsg3(const uint8_t* frame, uint32_t len) { + auto* k = (const EapolKey*)frame; + KernelLogStream(INFO, "WiFi") << "Handshake message 3 received"; + + // The ANonce must not have changed; if it has, the AP restarted the + // exchange and our PTK is stale. + if (memcmp(k->nonce, g_anonce, 32) != 0) { + Fail("the AP changed its nonce mid-handshake"); + return; + } + + memcpy(g_replay, k->replay, 8); + + uint16_t keyInfo = Get16Be(k->keyInfo); + uint32_t keyDataLen = Get16Be(k->keyDataLen); + if (sizeof(EapolKey) + keyDataLen > len) { + Fail("truncated key data"); + return; + } + const uint8_t* keyData = frame + sizeof(EapolKey); + + uint8_t plain[MAX_KEY_DATA]; + uint32_t plainLen = 0; + if (keyInfo & KEY_INFO_ENCRYPTED) { + plainLen = DecryptKeyData(keyData, keyDataLen, plain, sizeof(plain)); + if (!plainLen) { + Fail("could not decrypt the group key"); + return; + } + } else if (keyDataLen <= sizeof(plain)) { + memcpy(plain, keyData, keyDataLen); + plainLen = keyDataLen; + } + + // Message 4 goes out before the keys are installed: the AP is still + // sending in the clear until it sees it. + if (!SendMsg4()) { + Fail("could not transmit message 4"); + return; + } + + if (!WpaInstallPtk(Tk(), g_tkLen, g_cfg.PairwiseCipher)) { + Fail("the firmware rejected the pairwise key"); + return; + } + + const uint8_t* gtk = nullptr; + uint32_t gtkLen = 0; + uint8_t keyIdx = 0; + if (FindGtk(plain, plainLen, >k, >kLen, &keyIdx)) { + if (!WpaInstallGtk(gtk, gtkLen, keyIdx, g_cfg.GroupCipher, k->rsc)) + KernelLogStream(WARNING, "WiFi") + << "Group key install failed; broadcast traffic will not be received"; + } else { + KernelLogStream(WARNING, "WiFi") + << "No group key in message 3; broadcast traffic will not be received"; + } + + g_state = WpaState::Complete; + g_lastTxLen = 0; + KernelLogStream(OK, "WiFi") << "WPA handshake complete; link is encrypted"; + } + + // Periodic GTK rekey initiated by the AP (a 2-way exchange). + static void HandleGroupKey(const uint8_t* frame, uint32_t len) { + auto* k = (const EapolKey*)frame; + memcpy(g_replay, k->replay, 8); + + uint16_t keyInfo = Get16Be(k->keyInfo); + uint32_t keyDataLen = Get16Be(k->keyDataLen); + if (sizeof(EapolKey) + keyDataLen > len) return; + const uint8_t* keyData = frame + sizeof(EapolKey); + + uint8_t plain[MAX_KEY_DATA]; + uint32_t plainLen = 0; + if (keyInfo & KEY_INFO_ENCRYPTED) { + plainLen = DecryptKeyData(keyData, keyDataLen, plain, sizeof(plain)); + if (!plainLen) return; + } else if (keyDataLen <= sizeof(plain)) { + memcpy(plain, keyData, keyDataLen); + plainLen = keyDataLen; + } + + const uint8_t* gtk = nullptr; + uint32_t gtkLen = 0; + uint8_t keyIdx = 0; + if (FindGtk(plain, plainLen, >k, >kLen, &keyIdx)) + WpaInstallGtk(gtk, gtkLen, keyIdx, g_cfg.GroupCipher, k->rsc); + + SendGroupAck(); + KernelLogStream(INFO, "WiFi") << "Group key rekeyed"; + } + + bool WpaOnEapol(const uint8_t* data, uint32_t len) { + if (g_state == WpaState::Idle || g_state == WpaState::Failed) return false; + if (len < sizeof(EapolKey)) return false; + + auto* k = (const EapolKey*)data; + if (k->type != EAPOL_TYPE_KEY) return false; + if (k->descType != EAPOL_KEY_DESC_RSN && k->descType != EAPOL_KEY_DESC_WPA) + return false; + + // Trust the frame's own length field over the (possibly padded) buffer + // the driver handed us. + uint32_t declared = (uint32_t)Get16Be(k->length) + 4; + if (declared < sizeof(EapolKey) || declared > len) return false; + len = declared; + + g_eapolVersion = k->version > 3 ? 2 : k->version; + g_descType = k->descType; + + uint16_t keyInfo = Get16Be(k->keyInfo); + uint8_t ver = (uint8_t)(keyInfo & KEY_INFO_VERSION_MASK); + + if (keyInfo & KEY_INFO_REQUEST) return false; // STA->AP direction + + if (ver == KEY_DESC_VER_RC4) { + Fail("the AP asked for RC4/TKIP key wrapping, which is not supported"); + return true; + } + if (ver != KEY_DESC_VER_AES && ver != KEY_DESC_VER_AES_CMAC) { + Fail("unknown EAPOL key descriptor version"); + return true; + } + + bool pairwise = (keyInfo & KEY_INFO_KEY_TYPE) != 0; + bool hasMic = (keyInfo & KEY_INFO_MIC) != 0; + bool hasAck = (keyInfo & KEY_INFO_ACK) != 0; + + if (keyInfo & KEY_INFO_ERROR) { + Fail("the AP reported a MIC failure"); + return true; + } + + // Message 1 is the only frame that arrives before a PTK exists, so it + // is also the only one whose descriptor version we can adopt. + if (pairwise && hasAck && !hasMic) { + if (g_state != WpaState::WaitMsg1 && g_state != WpaState::WaitMsg3) { + // The AP restarted the handshake; take it from the top. + GenNonce(g_snonce); + } + g_keyDescVer = ver; + HandleMsg1(data, len); + return true; + } + + // Everything else is MIC-protected, so the PTK has to exist first. + if (!hasMic) return false; + if (g_state == WpaState::WaitMsg1) return false; + + if (!VerifyMic(data, len)) { + KernelLogStream(WARNING, "WiFi") + << "Discarding EAPOL frame with a bad MIC"; + return true; + } + + if (pairwise && hasAck) { + if (g_state == WpaState::Complete) { + // Our message 4 did not reach the AP and it retried message 3. + // The keys are already in place, so just answer again. + memcpy(g_replay, k->replay, 8); + SendMsg4(); + } else { + HandleMsg3(data, len); + } + } else if (!pairwise && hasAck) { + HandleGroupKey(data, len); + } + return true; + } + + // ========================================================================= + // Retransmission / timeout + // ========================================================================= + + // Unsigned deadline test that survives a `since` newer than `nowMs`. The + // caller samples the clock once per service pass while g_lastTxMs is + // stamped the moment a frame goes out, so the two can cross; a plain + // subtraction then wraps and expires every timer at once. + static bool Elapsed(uint64_t nowMs, uint64_t since, uint64_t ms) { + return nowMs > since && nowMs - since > ms; + } + + void WpaService(uint64_t nowMs) { + if (g_state != WpaState::WaitMsg1 && g_state != WpaState::WaitMsg3) return; + + if (g_startMs == 0) g_startMs = nowMs; + + if (Elapsed(nowMs, g_startMs, HANDSHAKE_TIMEOUT_MS)) { + Fail("the AP stopped responding"); + return; + } + + // Only our own messages are worth retrying; while waiting for msg 1 + // there is nothing to resend, the AP drives that. + if (g_state == WpaState::WaitMsg3 && g_lastTxLen + && Elapsed(nowMs, g_lastTxMs, RETRY_INTERVAL_MS)) { + if (g_retries >= MAX_RETRIES) { + Fail("no response to message 2"); + return; + } + g_retries++; + g_lastTxMs = nowMs; + WpaTxEapol(g_lastTx, g_lastTxLen); + } + } +} diff --git a/kernel/src/Drivers/Net/Wifi/Wpa.hpp b/kernel/src/Drivers/Net/Wifi/Wpa.hpp new file mode 100644 index 0000000..48aefd2 --- /dev/null +++ b/kernel/src/Drivers/Net/Wifi/Wpa.hpp @@ -0,0 +1,82 @@ +/* + * Wpa.hpp + * WPA2/WPA3-PSK supplicant: PMK derivation and the EAPOL-Key 4-way + * handshake that unlocks the link after association. + * + * The supplicant lives in the kernel because the handshake sits between + * association and the first IP packet: nothing above the driver can send or + * receive until the pairwise key is installed in the firmware. It drives + * the exchange but does not touch the hardware itself -- transmitting and + * key installation are provided by the MLME through the three hooks at the + * bottom of this header. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Drivers::Net::Wifi { + + enum class WpaState : uint8_t { + Idle = 0, + WaitMsg1, // associated, waiting for the AP to start the exchange + WaitMsg3, // msg 2 sent, waiting for the GTK + Complete, // keys installed, link is usable + Failed, + }; + + struct WpaConfig { + uint8_t OwnMac[6]; + uint8_t Bssid[6]; + uint8_t Ssid[32]; + uint8_t SsidLen; + char Passphrase[64]; + uint8_t PassLen; + uint8_t Akm; // RSN_AKM_* + uint8_t PairwiseCipher; // RSN_CIPHER_* + uint8_t GroupCipher; + bool Mfp; // management frame protection negotiated + }; + + // Derive the PMK and arm the handshake. Returns false when the + // configuration names a cipher or AKM this supplicant cannot do. + bool WpaStart(const WpaConfig& cfg); + void WpaReset(); + + // Feed the 802.1X payload of an inbound EAPOL frame (everything after the + // LLC/SNAP header). Returns true when the frame was consumed. + bool WpaOnEapol(const uint8_t* data, uint32_t len); + + // Re-send the last outbound message if the AP has gone quiet, and fail the + // handshake once it has been silent for too long. Called from the idle + // loop; `nowMs` is a monotonic millisecond clock. + void WpaService(uint64_t nowMs); + + WpaState WpaGetState(); + bool WpaIsComplete(); + + // Build the RSN information element advertising what WpaStart() was + // configured with. Returns the number of bytes written, 0 on error. + uint32_t WpaBuildRsnIe(uint8_t* out, uint32_t cap); + + // Pick the pairwise/group cipher and AKM out of an AP's RSN IE. `ie` + // points at the element body (after id/len). Returns false when nothing + // in the IE is supported. + bool WpaParseApRsn(const uint8_t* ie, uint32_t len, WpaConfig& cfg); + + // ------------------------------------------------------------------------- + // Hooks implemented by the MLME (IwxConnect.cpp) + // ------------------------------------------------------------------------- + + // Transmit an EAPOL frame body (802.1X header included) to the AP. + bool WpaTxEapol(const uint8_t* body, uint32_t len); + + // Install the pairwise temporal key. `cipher` is an RSN_CIPHER_* value. + bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher); + + // Install a group temporal key at `keyIdx`. `rsc` is the EAPOL key RSC + // field: 8 bytes, of which the low 6 are the AP's packet number. + bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx, + uint8_t cipher, const uint8_t* rsc); +} diff --git a/kernel/src/Libraries/Crypto.cpp b/kernel/src/Libraries/Crypto.cpp new file mode 100644 index 0000000..e2933cb --- /dev/null +++ b/kernel/src/Libraries/Crypto.cpp @@ -0,0 +1,700 @@ +/* + * Crypto.cpp + * SHA-1, SHA-256, HMAC, PBKDF2, AES and AES-CMAC for the Wi-Fi supplicant. + * See Crypto.hpp for why these live in the kernel. + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Crypto.hpp" +#include + +namespace Kt::Crypto { + + // ========================================================================= + // Small helpers + // ========================================================================= + + static inline uint32_t Rol32(uint32_t v, int n) { + return (v << n) | (v >> (32 - n)); + } + static inline uint32_t Ror32(uint32_t v, int n) { + return (v >> n) | (v << (32 - n)); + } + static inline uint32_t LoadBe32(const uint8_t* p) { + return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) + | ((uint32_t)p[2] << 8) | (uint32_t)p[3]; + } + static inline void StoreBe32(uint8_t* p, uint32_t v) { + p[0] = (uint8_t)(v >> 24); p[1] = (uint8_t)(v >> 16); + p[2] = (uint8_t)(v >> 8); p[3] = (uint8_t)v; + } + static inline void StoreBe64(uint8_t* p, uint64_t v) { + StoreBe32(p, (uint32_t)(v >> 32)); + StoreBe32(p + 4, (uint32_t)v); + } + + void SecureZero(void* p, size_t len) { + volatile uint8_t* q = (volatile uint8_t*)p; + while (len--) *q++ = 0; + } + + bool SecureEqual(const void* a, const void* b, size_t len) { + const uint8_t* x = (const uint8_t*)a; + const uint8_t* y = (const uint8_t*)b; + uint8_t diff = 0; + for (size_t i = 0; i < len; i++) diff |= (uint8_t)(x[i] ^ y[i]); + return diff == 0; + } + + // ========================================================================= + // SHA-1 + // ========================================================================= + + static void Sha1Block(uint32_t* st, const uint8_t* block) { + uint32_t w[80]; + for (int i = 0; i < 16; i++) w[i] = LoadBe32(block + i * 4); + for (int i = 16; i < 80; i++) + w[i] = Rol32(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + + uint32_t a = st[0], b = st[1], c = st[2], d = st[3], e = st[4]; + + for (int i = 0; i < 80; i++) { + uint32_t f, k; + if (i < 20) { f = (b & c) | (~b & d); k = 0x5A827999; } + else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; } + else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; } + else { f = b ^ c ^ d; k = 0xCA62C1D6; } + + uint32_t t = Rol32(a, 5) + f + e + k + w[i]; + e = d; d = c; c = Rol32(b, 30); b = a; a = t; + } + + st[0] += a; st[1] += b; st[2] += c; st[3] += d; st[4] += e; + } + + void Sha1Init(Sha1Ctx& ctx) { + ctx.State[0] = 0x67452301; ctx.State[1] = 0xEFCDAB89; + ctx.State[2] = 0x98BADCFE; ctx.State[3] = 0x10325476; + ctx.State[4] = 0xC3D2E1F0; + ctx.Count = 0; + ctx.Partial = 0; + } + + void Sha1Update(Sha1Ctx& ctx, const void* data, size_t len) { + const uint8_t* p = (const uint8_t*)data; + ctx.Count += len; + + if (ctx.Partial) { + uint32_t need = SHA1_BLOCK_SIZE - ctx.Partial; + uint32_t take = (len < need) ? (uint32_t)len : need; + memcpy(ctx.Buffer + ctx.Partial, p, take); + ctx.Partial += take; + p += take; + len -= take; + if (ctx.Partial < SHA1_BLOCK_SIZE) return; + Sha1Block(ctx.State, ctx.Buffer); + ctx.Partial = 0; + } + + while (len >= SHA1_BLOCK_SIZE) { + Sha1Block(ctx.State, p); + p += SHA1_BLOCK_SIZE; + len -= SHA1_BLOCK_SIZE; + } + if (len) { + memcpy(ctx.Buffer, p, len); + ctx.Partial = (uint32_t)len; + } + } + + void Sha1Final(Sha1Ctx& ctx, uint8_t out[SHA1_DIGEST_SIZE]) { + uint64_t bits = ctx.Count * 8; + + ctx.Buffer[ctx.Partial++] = 0x80; + if (ctx.Partial > SHA1_BLOCK_SIZE - 8) { + memset(ctx.Buffer + ctx.Partial, 0, SHA1_BLOCK_SIZE - ctx.Partial); + Sha1Block(ctx.State, ctx.Buffer); + ctx.Partial = 0; + } + memset(ctx.Buffer + ctx.Partial, 0, SHA1_BLOCK_SIZE - 8 - ctx.Partial); + StoreBe64(ctx.Buffer + SHA1_BLOCK_SIZE - 8, bits); + Sha1Block(ctx.State, ctx.Buffer); + + for (int i = 0; i < 5; i++) StoreBe32(out + i * 4, ctx.State[i]); + SecureZero(ctx.Buffer, sizeof(ctx.Buffer)); + } + + void Sha1(const void* data, size_t len, uint8_t out[SHA1_DIGEST_SIZE]) { + Sha1Ctx ctx; + Sha1Init(ctx); + Sha1Update(ctx, data, len); + Sha1Final(ctx, out); + } + + // ========================================================================= + // SHA-256 + // ========================================================================= + + static const uint32_t kSha256K[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, + }; + + static void Sha256Block(uint32_t* st, const uint8_t* block) { + uint32_t w[64]; + for (int i = 0; i < 16; i++) w[i] = LoadBe32(block + i * 4); + for (int i = 16; i < 64; i++) { + uint32_t s0 = Ror32(w[i - 15], 7) ^ Ror32(w[i - 15], 18) ^ (w[i - 15] >> 3); + uint32_t s1 = Ror32(w[i - 2], 17) ^ Ror32(w[i - 2], 19) ^ (w[i - 2] >> 10); + w[i] = w[i - 16] + s0 + w[i - 7] + s1; + } + + uint32_t a = st[0], b = st[1], c = st[2], d = st[3]; + uint32_t e = st[4], f = st[5], g = st[6], h = st[7]; + + for (int i = 0; i < 64; i++) { + uint32_t S1 = Ror32(e, 6) ^ Ror32(e, 11) ^ Ror32(e, 25); + uint32_t ch = (e & f) ^ (~e & g); + uint32_t t1 = h + S1 + ch + kSha256K[i] + w[i]; + uint32_t S0 = Ror32(a, 2) ^ Ror32(a, 13) ^ Ror32(a, 22); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t t2 = S0 + maj; + + h = g; g = f; f = e; e = d + t1; + d = c; c = b; b = a; a = t1 + t2; + } + + st[0] += a; st[1] += b; st[2] += c; st[3] += d; + st[4] += e; st[5] += f; st[6] += g; st[7] += h; + } + + void Sha256Init(Sha256Ctx& ctx) { + ctx.State[0] = 0x6a09e667; ctx.State[1] = 0xbb67ae85; + ctx.State[2] = 0x3c6ef372; ctx.State[3] = 0xa54ff53a; + ctx.State[4] = 0x510e527f; ctx.State[5] = 0x9b05688c; + ctx.State[6] = 0x1f83d9ab; ctx.State[7] = 0x5be0cd19; + ctx.Count = 0; + ctx.Partial = 0; + } + + void Sha256Update(Sha256Ctx& ctx, const void* data, size_t len) { + const uint8_t* p = (const uint8_t*)data; + ctx.Count += len; + + if (ctx.Partial) { + uint32_t need = SHA256_BLOCK_SIZE - ctx.Partial; + uint32_t take = (len < need) ? (uint32_t)len : need; + memcpy(ctx.Buffer + ctx.Partial, p, take); + ctx.Partial += take; + p += take; + len -= take; + if (ctx.Partial < SHA256_BLOCK_SIZE) return; + Sha256Block(ctx.State, ctx.Buffer); + ctx.Partial = 0; + } + + while (len >= SHA256_BLOCK_SIZE) { + Sha256Block(ctx.State, p); + p += SHA256_BLOCK_SIZE; + len -= SHA256_BLOCK_SIZE; + } + if (len) { + memcpy(ctx.Buffer, p, len); + ctx.Partial = (uint32_t)len; + } + } + + void Sha256Final(Sha256Ctx& ctx, uint8_t out[SHA256_DIGEST_SIZE]) { + uint64_t bits = ctx.Count * 8; + + ctx.Buffer[ctx.Partial++] = 0x80; + if (ctx.Partial > SHA256_BLOCK_SIZE - 8) { + memset(ctx.Buffer + ctx.Partial, 0, SHA256_BLOCK_SIZE - ctx.Partial); + Sha256Block(ctx.State, ctx.Buffer); + ctx.Partial = 0; + } + memset(ctx.Buffer + ctx.Partial, 0, SHA256_BLOCK_SIZE - 8 - ctx.Partial); + StoreBe64(ctx.Buffer + SHA256_BLOCK_SIZE - 8, bits); + Sha256Block(ctx.State, ctx.Buffer); + + for (int i = 0; i < 8; i++) StoreBe32(out + i * 4, ctx.State[i]); + SecureZero(ctx.Buffer, sizeof(ctx.Buffer)); + } + + void Sha256(const void* data, size_t len, uint8_t out[SHA256_DIGEST_SIZE]) { + Sha256Ctx ctx; + Sha256Init(ctx); + Sha256Update(ctx, data, len); + Sha256Final(ctx, out); + } + + // ========================================================================= + // HMAC + // ========================================================================= + + void HmacSha1(const uint8_t* key, size_t keyLen, + const uint8_t* const* parts, const size_t* lens, int count, + uint8_t out[SHA1_DIGEST_SIZE]) { + uint8_t k[SHA1_BLOCK_SIZE] = {}; + if (keyLen > SHA1_BLOCK_SIZE) { + Sha1(key, keyLen, k); + } else { + memcpy(k, key, keyLen); + } + + uint8_t pad[SHA1_BLOCK_SIZE]; + Sha1Ctx ctx; + + for (int i = 0; i < (int)SHA1_BLOCK_SIZE; i++) pad[i] = (uint8_t)(k[i] ^ 0x36); + Sha1Init(ctx); + Sha1Update(ctx, pad, SHA1_BLOCK_SIZE); + for (int i = 0; i < count; i++) Sha1Update(ctx, parts[i], lens[i]); + uint8_t inner[SHA1_DIGEST_SIZE]; + Sha1Final(ctx, inner); + + for (int i = 0; i < (int)SHA1_BLOCK_SIZE; i++) pad[i] = (uint8_t)(k[i] ^ 0x5c); + Sha1Init(ctx); + Sha1Update(ctx, pad, SHA1_BLOCK_SIZE); + Sha1Update(ctx, inner, SHA1_DIGEST_SIZE); + Sha1Final(ctx, out); + + SecureZero(k, sizeof(k)); + SecureZero(pad, sizeof(pad)); + SecureZero(inner, sizeof(inner)); + } + + void HmacSha1(const uint8_t* key, size_t keyLen, + const void* data, size_t len, uint8_t out[SHA1_DIGEST_SIZE]) { + const uint8_t* p = (const uint8_t*)data; + HmacSha1(key, keyLen, &p, &len, 1, out); + } + + void HmacSha256(const uint8_t* key, size_t keyLen, + const uint8_t* const* parts, const size_t* lens, int count, + uint8_t out[SHA256_DIGEST_SIZE]) { + uint8_t k[SHA256_BLOCK_SIZE] = {}; + if (keyLen > SHA256_BLOCK_SIZE) { + Sha256(key, keyLen, k); + } else { + memcpy(k, key, keyLen); + } + + uint8_t pad[SHA256_BLOCK_SIZE]; + Sha256Ctx ctx; + + for (int i = 0; i < (int)SHA256_BLOCK_SIZE; i++) pad[i] = (uint8_t)(k[i] ^ 0x36); + Sha256Init(ctx); + Sha256Update(ctx, pad, SHA256_BLOCK_SIZE); + for (int i = 0; i < count; i++) Sha256Update(ctx, parts[i], lens[i]); + uint8_t inner[SHA256_DIGEST_SIZE]; + Sha256Final(ctx, inner); + + for (int i = 0; i < (int)SHA256_BLOCK_SIZE; i++) pad[i] = (uint8_t)(k[i] ^ 0x5c); + Sha256Init(ctx); + Sha256Update(ctx, pad, SHA256_BLOCK_SIZE); + Sha256Update(ctx, inner, SHA256_DIGEST_SIZE); + Sha256Final(ctx, out); + + SecureZero(k, sizeof(k)); + SecureZero(pad, sizeof(pad)); + SecureZero(inner, sizeof(inner)); + } + + void HmacSha256(const uint8_t* key, size_t keyLen, + const void* data, size_t len, uint8_t out[SHA256_DIGEST_SIZE]) { + const uint8_t* p = (const uint8_t*)data; + HmacSha256(key, keyLen, &p, &len, 1, out); + } + + // ========================================================================= + // PBKDF2-HMAC-SHA1 + // ========================================================================= + + void Pbkdf2Sha1(const char* password, size_t passLen, + const uint8_t* salt, size_t saltLen, + uint32_t iterations, uint8_t* out, size_t outLen) { + const uint8_t* pw = (const uint8_t*)password; + uint32_t block = 1; + + while (outLen > 0) { + uint8_t counter[4] = { + (uint8_t)(block >> 24), (uint8_t)(block >> 16), + (uint8_t)(block >> 8), (uint8_t)block + }; + const uint8_t* parts[2] = { salt, counter }; + size_t lens[2] = { saltLen, 4 }; + + uint8_t u[SHA1_DIGEST_SIZE]; + uint8_t acc[SHA1_DIGEST_SIZE]; + HmacSha1(pw, passLen, parts, lens, 2, u); + memcpy(acc, u, SHA1_DIGEST_SIZE); + + for (uint32_t i = 1; i < iterations; i++) { + HmacSha1(pw, passLen, u, SHA1_DIGEST_SIZE, u); + for (int j = 0; j < (int)SHA1_DIGEST_SIZE; j++) acc[j] ^= u[j]; + } + + size_t take = outLen < SHA1_DIGEST_SIZE ? outLen : SHA1_DIGEST_SIZE; + memcpy(out, acc, take); + out += take; + outLen -= take; + block++; + + SecureZero(u, sizeof(u)); + SecureZero(acc, sizeof(acc)); + } + } + + // ========================================================================= + // AES + // ========================================================================= + + static const uint8_t kSbox[256] = { + 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, + 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, + 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, + 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, + 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, + 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, + 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, + 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, + 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, + 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, + 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, + 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, + 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, + 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, + 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, + 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16, + }; + + static const uint8_t kRsbox[256] = { + 0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb, + 0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb, + 0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e, + 0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25, + 0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92, + 0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84, + 0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06, + 0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b, + 0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73, + 0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e, + 0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b, + 0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4, + 0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f, + 0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef, + 0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61, + 0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d, + }; + + // Round constants; indexed by (i / Nk), which starts at 1. + static const uint8_t kRcon[11] = { + 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 + }; + + static inline uint8_t Xtime(uint8_t x) { + return (uint8_t)((x << 1) ^ (((x >> 7) & 1) * 0x1b)); + } + + // Galois-field multiply, used only by the inverse mix-columns step. + static uint8_t GfMul(uint8_t x, uint8_t y) { + uint8_t r = 0; + for (int i = 0; i < 8; i++) { + if (y & 1) r ^= x; + uint8_t hi = (uint8_t)(x & 0x80); + x <<= 1; + if (hi) x ^= 0x1b; + y >>= 1; + } + return r; + } + + bool AesInit(AesCtx& ctx, const uint8_t* key, size_t keyLen) { + int nk, nr; + if (keyLen == 16) { nk = 4; nr = 10; } + else if (keyLen == 32) { nk = 8; nr = 14; } + else return false; + + ctx.Rounds = nr; + uint8_t* rk = ctx.RoundKey; + memcpy(rk, key, keyLen); + + for (int i = nk; i < 4 * (nr + 1); i++) { + uint8_t t[4]; + int k = (i - 1) * 4; + t[0] = rk[k + 0]; t[1] = rk[k + 1]; t[2] = rk[k + 2]; t[3] = rk[k + 3]; + + if (i % nk == 0) { + uint8_t tmp = t[0]; + t[0] = kSbox[t[1]]; t[1] = kSbox[t[2]]; + t[2] = kSbox[t[3]]; t[3] = kSbox[tmp]; + t[0] ^= kRcon[i / nk]; + } else if (nk > 6 && i % nk == 4) { + t[0] = kSbox[t[0]]; t[1] = kSbox[t[1]]; + t[2] = kSbox[t[2]]; t[3] = kSbox[t[3]]; + } + + int j = i * 4; + k = (i - nk) * 4; + rk[j + 0] = (uint8_t)(rk[k + 0] ^ t[0]); + rk[j + 1] = (uint8_t)(rk[k + 1] ^ t[1]); + rk[j + 2] = (uint8_t)(rk[k + 2] ^ t[2]); + rk[j + 3] = (uint8_t)(rk[k + 3] ^ t[3]); + } + return true; + } + + // The state is column-major: s[4 * col + row], matching the AES input map. + static inline void AddRoundKey(uint8_t* s, const uint8_t* rk, int round) { + const uint8_t* k = rk + round * 16; + for (int i = 0; i < 16; i++) s[i] ^= k[i]; + } + + static void SubShift(uint8_t* s) { + for (int i = 0; i < 16; i++) s[i] = kSbox[s[i]]; + + uint8_t t; + // Row 1 left by one. + t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t; + // Row 2 left by two. + t = s[2]; s[2] = s[10]; s[10] = t; + t = s[6]; s[6] = s[14]; s[14] = t; + // Row 3 left by three (equivalently right by one). + t = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = s[3]; s[3] = t; + } + + static void InvShiftSub(uint8_t* s) { + uint8_t t; + // Row 1 right by one. + t = s[13]; s[13] = s[9]; s[9] = s[5]; s[5] = s[1]; s[1] = t; + // Row 2 right by two. + t = s[2]; s[2] = s[10]; s[10] = t; + t = s[6]; s[6] = s[14]; s[14] = t; + // Row 3 right by three. + t = s[3]; s[3] = s[7]; s[7] = s[11]; s[11] = s[15]; s[15] = t; + + for (int i = 0; i < 16; i++) s[i] = kRsbox[s[i]]; + } + + static void MixColumns(uint8_t* s) { + for (int c = 0; c < 4; c++) { + uint8_t* p = s + c * 4; + uint8_t a0 = p[0]; + uint8_t all = (uint8_t)(p[0] ^ p[1] ^ p[2] ^ p[3]); + p[0] ^= (uint8_t)(Xtime((uint8_t)(p[0] ^ p[1])) ^ all); + p[1] ^= (uint8_t)(Xtime((uint8_t)(p[1] ^ p[2])) ^ all); + p[2] ^= (uint8_t)(Xtime((uint8_t)(p[2] ^ p[3])) ^ all); + p[3] ^= (uint8_t)(Xtime((uint8_t)(p[3] ^ a0)) ^ all); + } + } + + static void InvMixColumns(uint8_t* s) { + for (int c = 0; c < 4; c++) { + uint8_t* p = s + c * 4; + uint8_t a = p[0], b = p[1], d = p[2], e = p[3]; + p[0] = (uint8_t)(GfMul(a, 0x0e) ^ GfMul(b, 0x0b) ^ GfMul(d, 0x0d) ^ GfMul(e, 0x09)); + p[1] = (uint8_t)(GfMul(a, 0x09) ^ GfMul(b, 0x0e) ^ GfMul(d, 0x0b) ^ GfMul(e, 0x0d)); + p[2] = (uint8_t)(GfMul(a, 0x0d) ^ GfMul(b, 0x09) ^ GfMul(d, 0x0e) ^ GfMul(e, 0x0b)); + p[3] = (uint8_t)(GfMul(a, 0x0b) ^ GfMul(b, 0x0d) ^ GfMul(d, 0x09) ^ GfMul(e, 0x0e)); + } + } + + void AesEncryptBlock(const AesCtx& ctx, const uint8_t in[16], uint8_t out[16]) { + uint8_t s[16]; + memcpy(s, in, 16); + + AddRoundKey(s, ctx.RoundKey, 0); + for (int round = 1; round < ctx.Rounds; round++) { + SubShift(s); + MixColumns(s); + AddRoundKey(s, ctx.RoundKey, round); + } + SubShift(s); + AddRoundKey(s, ctx.RoundKey, ctx.Rounds); + + memcpy(out, s, 16); + } + + void AesDecryptBlock(const AesCtx& ctx, const uint8_t in[16], uint8_t out[16]) { + uint8_t s[16]; + memcpy(s, in, 16); + + AddRoundKey(s, ctx.RoundKey, ctx.Rounds); + for (int round = ctx.Rounds - 1; round > 0; round--) { + InvShiftSub(s); + AddRoundKey(s, ctx.RoundKey, round); + InvMixColumns(s); + } + InvShiftSub(s); + AddRoundKey(s, ctx.RoundKey, 0); + + memcpy(out, s, 16); + } + + // ========================================================================= + // RFC 3394 AES key wrap + // ========================================================================= + + static const uint8_t kKeyWrapIv[8] = { + 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6 + }; + + bool AesKeyUnwrap(const uint8_t* kek, size_t kekLen, + const uint8_t* in, size_t inLen, uint8_t* out) { + if (inLen < 24 || (inLen % 8) != 0) return false; + size_t n = inLen / 8 - 1; + + AesCtx ctx; + if (!AesInit(ctx, kek, kekLen)) return false; + + uint8_t a[8]; + memcpy(a, in, 8); + memcpy(out, in + 8, n * 8); + + uint8_t block[16]; + for (int j = 5; j >= 0; j--) { + for (size_t i = n; i >= 1; i--) { + uint64_t t = (uint64_t)n * (uint64_t)j + i; + memcpy(block, a, 8); + // A ^= t, big-endian over the full 8-byte word. + for (int b = 0; b < 8; b++) + block[7 - b] ^= (uint8_t)(t >> (8 * b)); + memcpy(block + 8, out + (i - 1) * 8, 8); + + AesDecryptBlock(ctx, block, block); + memcpy(a, block, 8); + memcpy(out + (i - 1) * 8, block + 8, 8); + } + } + + SecureZero(block, sizeof(block)); + SecureZero(&ctx, sizeof(ctx)); + return SecureEqual(a, kKeyWrapIv, 8); + } + + bool AesKeyWrap(const uint8_t* kek, size_t kekLen, + const uint8_t* in, size_t inLen, uint8_t* out) { + if (inLen < 16 || (inLen % 8) != 0) return false; + size_t n = inLen / 8; + + AesCtx ctx; + if (!AesInit(ctx, kek, kekLen)) return false; + + uint8_t a[8]; + memcpy(a, kKeyWrapIv, 8); + memcpy(out + 8, in, inLen); + + uint8_t block[16]; + for (int j = 0; j < 6; j++) { + for (size_t i = 1; i <= n; i++) { + memcpy(block, a, 8); + memcpy(block + 8, out + i * 8, 8); + AesEncryptBlock(ctx, block, block); + + uint64_t t = (uint64_t)n * (uint64_t)j + i; + memcpy(a, block, 8); + for (int b = 0; b < 8; b++) + a[7 - b] ^= (uint8_t)(t >> (8 * b)); + memcpy(out + i * 8, block + 8, 8); + } + } + memcpy(out, a, 8); + + SecureZero(block, sizeof(block)); + SecureZero(&ctx, sizeof(ctx)); + return true; + } + + // ========================================================================= + // AES-CMAC (RFC 4493) + // ========================================================================= + + static void CmacShiftLeft(const uint8_t in[16], uint8_t out[16]) { + uint8_t carry = 0; + for (int i = 15; i >= 0; i--) { + uint8_t v = in[i]; + out[i] = (uint8_t)((v << 1) | carry); + carry = (uint8_t)((v >> 7) & 1); + } + } + + void AesCmac(const uint8_t* key, size_t keyLen, + const uint8_t* const* parts, const size_t* lens, int count, + uint8_t out[16]) { + AesCtx ctx; + if (!AesInit(ctx, key, keyLen)) { + memset(out, 0, 16); + return; + } + + // Subkey generation. + uint8_t zero[16] = {}; + uint8_t l[16], k1[16], k2[16]; + AesEncryptBlock(ctx, zero, l); + CmacShiftLeft(l, k1); + if (l[0] & 0x80) k1[15] ^= 0x87; + CmacShiftLeft(k1, k2); + if (k1[0] & 0x80) k2[15] ^= 0x87; + + size_t total = 0; + for (int i = 0; i < count; i++) total += lens[i]; + + uint8_t x[16] = {}; + uint8_t block[16]; + uint32_t fill = 0; + + // Stream the parts through, holding back the final block so it can be + // padded and XORed with the right subkey. + size_t consumed = 0; + for (int i = 0; i < count; i++) { + const uint8_t* p = parts[i]; + size_t n = lens[i]; + while (n) { + uint32_t take = 16 - fill; + if (take > n) take = (uint32_t)n; + memcpy(block + fill, p, take); + fill += take; + p += take; + n -= take; + consumed += take; + + if (fill == 16 && consumed < total) { + for (int b = 0; b < 16; b++) x[b] ^= block[b]; + AesEncryptBlock(ctx, x, x); + fill = 0; + } + } + } + + if (fill == 16 && total != 0) { + for (int b = 0; b < 16; b++) block[b] ^= k1[b]; + } else { + block[fill] = 0x80; + for (uint32_t b = fill + 1; b < 16; b++) block[b] = 0; + for (int b = 0; b < 16; b++) block[b] ^= k2[b]; + } + + for (int b = 0; b < 16; b++) x[b] ^= block[b]; + AesEncryptBlock(ctx, x, out); + + SecureZero(l, sizeof(l)); + SecureZero(k1, sizeof(k1)); + SecureZero(k2, sizeof(k2)); + SecureZero(block, sizeof(block)); + SecureZero(&ctx, sizeof(ctx)); + } +} diff --git a/kernel/src/Libraries/Crypto.hpp b/kernel/src/Libraries/Crypto.hpp new file mode 100644 index 0000000..5dccd8b --- /dev/null +++ b/kernel/src/Libraries/Crypto.hpp @@ -0,0 +1,132 @@ +/* + * Crypto.hpp + * Minimal kernel-side crypto primitives. + * + * These exist for the Wi-Fi WPA2/WPA3 supplicant, which has to run inside the + * kernel (the 4-way handshake is bound to the driver's TX/RX path and has to + * complete before any IP traffic can flow). BearSSL lives in userspace and + * is not linkable here, so the handful of primitives the handshake needs are + * implemented directly: + * + * SHA-1 / SHA-256 digest + HMAC EAPOL MIC, PRF, PBKDF2 + * PBKDF2-HMAC-SHA1 WPA passphrase -> 256-bit PSK + * AES-128/256 key unwrap, CMAC + * AES key unwrap (RFC 3394) encrypted EAPOL key data + * AES-CMAC key-descriptor version 3 MIC + * + * Nothing here is constant-time hardened beyond avoiding secret-dependent + * branches in the comparison helper; it is not a general-purpose crypto + * library and should not be used as one. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include +#include + +namespace Kt::Crypto { + + // ========================================================================= + // SHA-1 + // ========================================================================= + + constexpr uint32_t SHA1_DIGEST_SIZE = 20; + constexpr uint32_t SHA1_BLOCK_SIZE = 64; + + struct Sha1Ctx { + uint32_t State[5]; + uint64_t Count; // total bytes fed + uint8_t Buffer[SHA1_BLOCK_SIZE]; + uint32_t Partial; + }; + + void Sha1Init(Sha1Ctx& ctx); + void Sha1Update(Sha1Ctx& ctx, const void* data, size_t len); + void Sha1Final(Sha1Ctx& ctx, uint8_t out[SHA1_DIGEST_SIZE]); + void Sha1(const void* data, size_t len, uint8_t out[SHA1_DIGEST_SIZE]); + + // ========================================================================= + // SHA-256 + // ========================================================================= + + constexpr uint32_t SHA256_DIGEST_SIZE = 32; + constexpr uint32_t SHA256_BLOCK_SIZE = 64; + + struct Sha256Ctx { + uint32_t State[8]; + uint64_t Count; + uint8_t Buffer[SHA256_BLOCK_SIZE]; + uint32_t Partial; + }; + + void Sha256Init(Sha256Ctx& ctx); + void Sha256Update(Sha256Ctx& ctx, const void* data, size_t len); + void Sha256Final(Sha256Ctx& ctx, uint8_t out[SHA256_DIGEST_SIZE]); + void Sha256(const void* data, size_t len, uint8_t out[SHA256_DIGEST_SIZE]); + + // ========================================================================= + // HMAC + // ========================================================================= + + // Multi-part variants: `parts`/`lens` describe `count` chunks that are + // hashed as one message. The 802.11 PRF feeds four or five chunks per + // iteration, so this avoids staging a concatenation buffer every time. + void HmacSha1(const uint8_t* key, size_t keyLen, + const uint8_t* const* parts, const size_t* lens, int count, + uint8_t out[SHA1_DIGEST_SIZE]); + void HmacSha1(const uint8_t* key, size_t keyLen, + const void* data, size_t len, uint8_t out[SHA1_DIGEST_SIZE]); + + void HmacSha256(const uint8_t* key, size_t keyLen, + const uint8_t* const* parts, const size_t* lens, int count, + uint8_t out[SHA256_DIGEST_SIZE]); + void HmacSha256(const uint8_t* key, size_t keyLen, + const void* data, size_t len, uint8_t out[SHA256_DIGEST_SIZE]); + + // ========================================================================= + // PBKDF2-HMAC-SHA1 (WPA passphrase -> PSK, RFC 2898) + // ========================================================================= + + void Pbkdf2Sha1(const char* password, size_t passLen, + const uint8_t* salt, size_t saltLen, + uint32_t iterations, uint8_t* out, size_t outLen); + + // ========================================================================= + // AES (128 and 256 bit keys, single block) + // ========================================================================= + + struct AesCtx { + uint8_t RoundKey[240]; // 15 round keys, the AES-256 maximum + int Rounds; + }; + + // keyLen must be 16 or 32 bytes. Returns false otherwise. + bool AesInit(AesCtx& ctx, const uint8_t* key, size_t keyLen); + void AesEncryptBlock(const AesCtx& ctx, const uint8_t in[16], uint8_t out[16]); + void AesDecryptBlock(const AesCtx& ctx, const uint8_t in[16], uint8_t out[16]); + + // RFC 3394 AES key wrap / unwrap. `outLen` is `inLen - 8` for unwrap and + // `inLen + 8` for wrap; both operate on multiples of 8 bytes. Unwrap + // returns false when the integrity check value does not match. + bool AesKeyUnwrap(const uint8_t* kek, size_t kekLen, + const uint8_t* in, size_t inLen, uint8_t* out); + bool AesKeyWrap(const uint8_t* kek, size_t kekLen, + const uint8_t* in, size_t inLen, uint8_t* out); + + // AES-CMAC (RFC 4493), truncated by the caller as needed. + void AesCmac(const uint8_t* key, size_t keyLen, + const uint8_t* const* parts, const size_t* lens, int count, + uint8_t out[16]); + + // ========================================================================= + // Helpers + // ========================================================================= + + // Length-fixed comparison that does not short-circuit on the first + // difference: used for MIC checks so a mismatch position is not observable. + bool SecureEqual(const void* a, const void* b, size_t len); + + // Wipe key material. Marked so the compiler cannot elide the stores. + void SecureZero(void* p, size_t len); +} diff --git a/kernel/src/Net/Arp.cpp b/kernel/src/Net/Arp.cpp index ed34fe2..cce221a 100644 --- a/kernel/src/Net/Arp.cpp +++ b/kernel/src/Net/Arp.cpp @@ -15,16 +15,18 @@ #include #include #include +#include #include using namespace Kt; namespace Net::Arp { + // Must be the MAC of the interface the frame actually leaves by: an ARP + // reply carrying the wired card's address while the frame goes out over + // Wi-Fi would be answered to a station that is not there. static const uint8_t* GetActiveNicMac() { - if (Drivers::Net::E1000::IsInitialized()) - return Drivers::Net::E1000::GetMacAddress(); - return Drivers::Net::E1000E::GetMacAddress(); + return NetIf::ActiveMac(); } // ARP cache entry diff --git a/kernel/src/Net/Ethernet.cpp b/kernel/src/Net/Ethernet.cpp index 7590f79..f543092 100644 --- a/kernel/src/Net/Ethernet.cpp +++ b/kernel/src/Net/Ethernet.cpp @@ -8,8 +8,7 @@ #include #include #include -#include -#include +#include #include #include #include @@ -18,22 +17,24 @@ using namespace Kt; namespace Net::Ethernet { + // Which device the frames actually go out of is the registry's decision; + // this layer only cares that something is there. static const uint8_t* GetActiveNicMac() { - if (Drivers::Net::E1000::IsInitialized()) - return Drivers::Net::E1000::GetMacAddress(); - return Drivers::Net::E1000E::GetMacAddress(); + return NetIf::ActiveMac(); } static bool ActiveNicSend(const uint8_t* data, uint16_t length) { - if (Drivers::Net::E1000::IsInitialized()) - return Drivers::Net::E1000::SendPacket(data, length); - return Drivers::Net::E1000E::SendPacket(data, length); + return NetIf::ActiveSend(data, length); } void Initialize() { KernelLogStream(OK, "Net") << "Ethernet layer initialized"; } + const uint8_t* GetMacAddress() { + return NetIf::ActiveMac(); + } + bool Send(const uint8_t* destMac, uint16_t etherType, const uint8_t* payload, uint16_t payloadLen) { if (payload == nullptr || payloadLen == 0 || payloadLen > MAX_PAYLOAD_SIZE) { return false; diff --git a/kernel/src/Net/Ethernet.hpp b/kernel/src/Net/Ethernet.hpp index 5a80404..09a18a8 100644 --- a/kernel/src/Net/Ethernet.hpp +++ b/kernel/src/Net/Ethernet.hpp @@ -30,7 +30,10 @@ namespace Net::Ethernet { // Send an Ethernet frame with the given EtherType and payload bool Send(const uint8_t* destMac, uint16_t etherType, const uint8_t* payload, uint16_t payloadLen); - // Called by E1000 RX handler to dispatch received frames + // Called by a driver's RX handler to dispatch received frames void OnFrameReceived(const uint8_t* data, uint16_t length); + // MAC address of the interface currently carrying traffic + const uint8_t* GetMacAddress(); + } diff --git a/kernel/src/Net/Net.cpp b/kernel/src/Net/Net.cpp index 43a88d7..5a51db3 100644 --- a/kernel/src/Net/Net.cpp +++ b/kernel/src/Net/Net.cpp @@ -1,11 +1,12 @@ /* * Net.cpp * Network stack initialization - * Copyright (c) 2025 Daniel Hammer + * Copyright (c) 2025-2026 Daniel Hammer */ #include "Net.hpp" #include +#include #include #include #include @@ -15,6 +16,7 @@ #include #include #include +#include #include #include @@ -22,9 +24,57 @@ using namespace Kt; namespace Net { + // A driver that never initialized has no link regardless of what its + // carrier register says. + static bool E1000LinkUp() { + return Drivers::Net::E1000::IsInitialized() + && Drivers::Net::E1000::IsLinkUp(); + } + static bool E1000ELinkUp() { + return Drivers::Net::E1000E::IsInitialized() + && Drivers::Net::E1000E::IsLinkUp(); + } + + static void RegisterInterfaces() { + if (Drivers::Net::E1000::IsInitialized()) { + NetIf::Register({ + "eth0", NetIf::Kind::Ethernet, + Drivers::Net::E1000::GetMacAddress, + Drivers::Net::E1000::SendPacket, + E1000LinkUp, + }); + Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived); + } + + if (Drivers::Net::E1000E::IsInitialized()) { + NetIf::Register({ + "eth1", NetIf::Kind::Ethernet, + Drivers::Net::E1000E::GetMacAddress, + Drivers::Net::E1000E::SendPacket, + E1000ELinkUp, + }); + Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived); + } + + // Wi-Fi registers whenever the adapter exists; its link only comes up + // once a network has been joined and keyed. + if (Drivers::Net::Wifi::IsPresent()) { + NetIf::Register({ + "wlan0", NetIf::Kind::Wireless, + Drivers::Net::Wifi::GetMacAddress, + Drivers::Net::Wifi::SendPacket, + Drivers::Net::Wifi::IsLinkUp, + }); + Drivers::Net::Wifi::SetRxCallback(Ethernet::OnFrameReceived); + } + } + void Initialize() { - if (!Drivers::Net::E1000::IsInitialized() && !Drivers::Net::E1000E::IsInitialized()) { - KernelLogStream(WARNING, "Net") << "No NIC initialized, skipping network stack"; + RegisterInterfaces(); + + if (NetIf::Count() == 0) { + KernelLogStream(WARNING, "Net") + << "No network interface found, skipping network stack"; return; } @@ -37,17 +87,12 @@ namespace Net { Tcp::Initialize(); Socket::Initialize(); - // Hook the active NIC's RX to our Ethernet dispatcher - if (Drivers::Net::E1000::IsInitialized()) { - Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived); - } else { - Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived); - } + // Announce ourselves, but only if something is actually carrying + // traffic: a Wi-Fi-only machine has no link until it joins a network. + if (NetIf::AnyLinkUp()) Arp::SendRequest(GetIpAddress()); - // Send a gratuitous ARP to announce ourselves on the network - Arp::SendRequest(GetIpAddress()); - - KernelLogStream(OK, "Net") << "Network stack initialized"; + KernelLogStream(OK, "Net") << "Network stack initialized with " + << (uint64_t)NetIf::Count() << " interface(s)"; } } diff --git a/kernel/src/Net/NetIf.cpp b/kernel/src/Net/NetIf.cpp new file mode 100644 index 0000000..53062b8 --- /dev/null +++ b/kernel/src/Net/NetIf.cpp @@ -0,0 +1,59 @@ +/* + * NetIf.cpp + * Network interface registry. + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#include "NetIf.hpp" + +namespace Net::NetIf { + + static Interface g_ifaces[MAX_INTERFACES]; + static int g_count = 0; + + static const uint8_t kZeroMac[6] = {}; + + bool Register(const Interface& iface) { + if (g_count >= MAX_INTERFACES) return false; + if (!iface.GetMac || !iface.Send || !iface.IsLinkUp) return false; + g_ifaces[g_count++] = iface; + return true; + } + + int Count() { return g_count; } + + const Interface* At(int index) { + if (index < 0 || index >= g_count) return nullptr; + return &g_ifaces[index]; + } + + const Interface* Active() { + if (g_count == 0) return nullptr; + + for (int i = 0; i < g_count; i++) + if (g_ifaces[i].Type == Kind::Ethernet && g_ifaces[i].IsLinkUp()) + return &g_ifaces[i]; + + for (int i = 0; i < g_count; i++) + if (g_ifaces[i].IsLinkUp()) + return &g_ifaces[i]; + + return &g_ifaces[0]; + } + + const uint8_t* ActiveMac() { + const Interface* i = Active(); + return i ? i->GetMac() : kZeroMac; + } + + bool ActiveSend(const uint8_t* data, uint16_t length) { + const Interface* i = Active(); + return i ? i->Send(data, length) : false; + } + + bool AnyLinkUp() { + for (int i = 0; i < g_count; i++) + if (g_ifaces[i].IsLinkUp()) return true; + return false; + } +} diff --git a/kernel/src/Net/NetIf.hpp b/kernel/src/Net/NetIf.hpp new file mode 100644 index 0000000..4e14dd7 --- /dev/null +++ b/kernel/src/Net/NetIf.hpp @@ -0,0 +1,50 @@ +/* + * NetIf.hpp + * Network interface registry. + * + * The Ethernet layer used to reach straight into the E1000/E1000E drivers. + * Wi-Fi is a third link-layer device that carries the same Ethernet frames + * once it is associated, so drivers now register a small vtable here and the + * stack talks to whichever interface currently has a link. + * + * Copyright (c) 2025-2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Net::NetIf { + + enum class Kind : uint8_t { + Ethernet = 0, + Wireless = 1, + }; + + struct Interface { + const char* Name; // "eth0", "wlan0" + Kind Type; + const uint8_t* (*GetMac)(); + bool (*Send)(const uint8_t* data, uint16_t length); + bool (*IsLinkUp)(); + }; + + constexpr int MAX_INTERFACES = 4; + + // Register a link-layer device. Order of registration decides ties. + bool Register(const Interface& iface); + + int Count(); + const Interface* At(int index); + + // The interface traffic is currently going out of: the first registered + // interface reporting a link, with wired preferred over wireless so a + // plugged-in cable keeps winning. Falls back to the first registered + // interface when nothing reports a link, so ifconfig still has a MAC to + // show. Returns null when nothing is registered at all. + const Interface* Active(); + + // Convenience wrappers used by the Ethernet layer. + const uint8_t* ActiveMac(); + bool ActiveSend(const uint8_t* data, uint16_t length); + bool AnyLinkUp(); +} diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index 69dd86c..187d49d 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -596,6 +596,26 @@ namespace montauk::abi { uint16_t beaconInterval; // TU }; + // Association progress reported in WifiInfo.connState. + static constexpr uint32_t WIFI_CONN_IDLE = 0; + static constexpr uint32_t WIFI_CONN_CONTEXTS_UP = 1; + static constexpr uint32_t WIFI_CONN_AUTHENTICATING = 2; + static constexpr uint32_t WIFI_CONN_AUTHENTICATED = 3; + static constexpr uint32_t WIFI_CONN_ASSOCIATING = 4; + static constexpr uint32_t WIFI_CONN_ASSOCIATED = 5; + static constexpr uint32_t WIFI_CONN_HANDSHAKING = 6; + static constexpr uint32_t WIFI_CONN_CONNECTED = 7; + static constexpr uint32_t WIFI_CONN_FAILED = 8; + + // Negative results from SYS_WIFI_CONNECT. + static constexpr int WIFI_ERR_NO_ADAPTER = -1; // no adapter, or not ready + static constexpr int WIFI_ERR_NOT_FOUND = -2; // SSID absent from the scan + static constexpr int WIFI_ERR_NEED_KEY = -3; // encrypted, no passphrase + static constexpr int WIFI_ERR_UNSUPPORTED = -4; // WPA3-SAE, WEP, enterprise + static constexpr int WIFI_ERR_AUTH = -5; // key exchange rejected + static constexpr int WIFI_ERR_TIMEOUT = -6; // AP never answered + static constexpr int WIFI_ERR_FAILED = -7; // anything else + // Adapter status (returned by SYS_WIFI_INFO). struct WifiInfo { uint8_t mac[6]; @@ -607,7 +627,12 @@ namespace montauk::abi { char fwVersion[32]; uint64_t rxPackets; uint32_t fwErrors; - uint32_t connState; // 0 idle, >0 connection setup in progress + uint32_t connState; // WIFI_CONN_* + uint64_t txPackets; + char ssid[36]; // network joined, empty when disconnected + uint8_t bssid[6]; + uint8_t connected; // 1 once the link can carry IP traffic + uint8_t channel; }; struct ThermalInfo { diff --git a/programs/src/wifi/main.cpp b/programs/src/wifi/main.cpp index 4559cb5..aad1b49 100644 --- a/programs/src/wifi/main.cpp +++ b/programs/src/wifi/main.cpp @@ -7,8 +7,10 @@ * wifi scan [seconds] scan for a specific duration (default 5, max 20) * wifi info adapter, firmware and radio status * wifi debug info plus raw counters, repeated scan detail - * wifi connect bring up the firmware contexts for an open network - * wifi disconnect tear those contexts back down + * wifi connect [passphrase] + * join a network (WPA2/WPA3-PSK or open) + * wifi status the network currently joined + * wifi disconnect leave it * * Copyright (c) 2026 Daniel Hammer */ @@ -85,6 +87,21 @@ static const char* state_name(uint8_t state) { } } +static const char* conn_state_name(uint32_t s) { + switch (s) { + case abi::WIFI_CONN_IDLE: return "not connected"; + case abi::WIFI_CONN_CONTEXTS_UP: return "preparing radio"; + case abi::WIFI_CONN_AUTHENTICATING: return "authenticating"; + case abi::WIFI_CONN_AUTHENTICATED: return "authenticated"; + case abi::WIFI_CONN_ASSOCIATING: return "associating"; + case abi::WIFI_CONN_ASSOCIATED: return "associated"; + case abi::WIFI_CONN_HANDSHAKING: return "exchanging keys"; + case abi::WIFI_CONN_CONNECTED: return "connected"; + case abi::WIFI_CONN_FAILED: return "failed"; + default: return "unknown"; + } +} + // A rough signal-quality bar from the RSSI: -50 dBm and up is excellent, // -90 dBm and below is unusable. static void put_signal_bar(int8_t rssi) { @@ -159,15 +176,50 @@ static void print_info(const abi::WifiInfo& info, bool verbose) { print(" channels : "); put_u64(info.channels); print(" usable after regulatory filtering\n"); print(" scanning : "); print(info.scanning ? "yes" : "no"); print("\n"); + print(" network : "); + if (info.ssid[0]) { + print(info.ssid); + print(" ("); print(conn_state_name(info.connState)); print(")"); + } else { + print(conn_state_name(info.connState)); + } + print("\n"); + if (info.ssid[0]) { + print(" bssid : "); put_mac(info.bssid); print("\n"); + } if (verbose) { print(" rx packets : "); put_u64(info.rxPackets); print("\n"); + print(" tx packets : "); put_u64(info.txPackets); print("\n"); print(" fw errors : "); put_u64(info.fwErrors); print("\n"); print(" conn state : "); put_u64(info.connState); - print(" (0 = idle)\n"); + print(" ("); print(conn_state_name(info.connState)); print(")\n"); } } +static int cmd_status() { + abi::WifiInfo info; + if (wifi_info(&info) != 0 && !info.present) { + print("wifi: no supported Wi-Fi adapter found.\n"); + return 1; + } + if (!info.connected) { + print("Not connected."); + if (info.connState != abi::WIFI_CONN_IDLE) { + print(" Currently "); print(conn_state_name(info.connState)); print("."); + } + print("\n"); + return 1; + } + print("Connected to \""); print(info.ssid); print("\"\n"); + print(" bssid : "); put_mac(info.bssid); print("\n"); + print(" interface : wlan0 ("); put_mac(info.mac); print(")\n"); + print(" rx / tx : "); put_u64(info.rxPackets); + print(" / "); put_u64(info.txPackets); print(" packets\n"); + print("\nRun \"dhcp\" to pick up an address if you have not already.\n"); + return 0; +} + static int cmd_scan(uint32_t seconds, bool verbose) { abi::WifiInfo info; if (!require_adapter(info)) return 1; @@ -317,20 +369,44 @@ static int cmd_connect(const char* ssid, const char* password) { print("Connecting to \""); print(ssid); print("\"...\n"); int rc = wifi_connect(ssid, password); + if (rc == 0) { - print("Firmware contexts are up.\n"); - print("NOTE: the 802.11 authentication exchange is not implemented yet,\n"); - print(" so this does not establish a usable link.\n"); + print("Connected.\n"); + print("Run \"dhcp\" to get an address, then the network is usable\n"); + print("just like a wired connection.\n"); return 0; } - if (rc == -2) { - print("wifi: \""); print(ssid); print("\" is encrypted (WPA2/WPA3).\n"); - print(" Only open networks can be joined so far - the key exchange\n"); - print(" is not implemented yet. Scanning is unaffected.\n"); - return 1; + + switch (rc) { + case abi::WIFI_ERR_NOT_FOUND: + print("wifi: \""); print(ssid); print("\" was not found in the last scan.\n"); + print(" Run \"wifi scan\" first, and check the SSID spelling.\n"); + break; + case abi::WIFI_ERR_NEED_KEY: + print("wifi: \""); print(ssid); print("\" is encrypted and needs a passphrase.\n"); + print(" Usage: wifi connect "); print(ssid); print(" \n"); + break; + case abi::WIFI_ERR_UNSUPPORTED: + print("wifi: \""); print(ssid); print("\" uses security this driver cannot do.\n"); + print(" Supported: open, and WPA2/WPA3-PSK with CCMP or GCMP.\n"); + print(" Not supported: WEP, the original WPA (TKIP), WPA3 SAE-only\n"); + print(" networks, and 802.1X enterprise. Check klog for the detail.\n"); + break; + case abi::WIFI_ERR_AUTH: + print("wifi: the key exchange was rejected - the passphrase is\n"); + print(" probably wrong. Check it and try again.\n"); + break; + case abi::WIFI_ERR_TIMEOUT: + print("wifi: the access point did not respond in time.\n"); + print(" It may be out of range; \"wifi scan\" shows the signal.\n"); + break; + case abi::WIFI_ERR_NO_ADAPTER: + print("wifi: the adapter is not ready.\n"); + break; + default: + print("wifi: could not connect. See klog for the kernel-side detail.\n"); + break; } - print("wifi: \""); print(ssid); print("\" was not found in the last scan.\n"); - print(" Run \"wifi scan\" first, and check the SSID spelling.\n"); return 1; } @@ -339,8 +415,9 @@ static void usage() { print(" scan [seconds] scan and list nearby networks (default 5)\n"); print(" info adapter, firmware and radio status\n"); print(" debug diagnostics plus per-BSS scan detail\n"); - print(" connect bring up firmware contexts (open networks)\n"); - print(" disconnect tear those contexts down\n\n"); + print(" connect [key] join a network (open or WPA2/WPA3-PSK)\n"); + print(" status show the network currently joined\n"); + print(" disconnect leave the current network\n\n"); print("With no command, runs a 5 second scan.\n"); } @@ -363,14 +440,20 @@ extern "C" void _start() { exit(cmd_info(false)); } else if (streq(cmd, "debug")) { exit(cmd_debug()); + } else if (streq(cmd, "status")) { + exit(cmd_status()); } else if (streq(cmd, "connect")) { char ssid[40], pass[80]; if (!next_token(&rest, ssid, sizeof(ssid))) { print("wifi: connect needs an SSID\n"); exit(1); } - pass[0] = '\0'; - next_token(&rest, pass, sizeof(pass)); + // Everything after the SSID is the passphrase, spaces and all. + const char* p = skip_spaces(rest); + int n = 0; + while (p[n] && n < (int)sizeof(pass) - 1) { pass[n] = p[n]; n++; } + while (n > 0 && pass[n - 1] == ' ') n--; + pass[n] = '\0'; exit(cmd_connect(ssid, pass)); } else if (streq(cmd, "disconnect")) { wifi_disconnect(); diff --git a/tests/wifi/ap_handshake.py b/tests/wifi/ap_handshake.py new file mode 100644 index 0000000..500eb33 --- /dev/null +++ b/tests/wifi/ap_handshake.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Independent WPA2 authenticator driving the kernel supplicant through a +4-way handshake. All crypto here comes from hashlib / the `cryptography` +package, so it is a genuine oracle for the C++ implementation.""" +import hashlib, hmac, os, subprocess, sys, struct +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.keywrap import aes_key_wrap, aes_key_unwrap + +HARNESS = sys.argv[1] + +def prf(key, label, data, nbytes): + r = b'' + i = 0 + while len(r) < nbytes: + r += hmac.new(key, label.encode() + b'\x00' + data + bytes([i]), hashlib.sha1).digest() + i += 1 + return r[:nbytes] + +def kdf_sha256(key, label, data, nbytes): + r = b'' + i = 1 + bits = nbytes * 8 + while len(r) < nbytes: + r += hmac.new(key, struct.pack('H', body, 2, HDR + len(key_data) - 4) + body[4] = desc + struct.pack_into('>H', body, 5, key_info) + struct.pack_into('>H', body, 7, 16) # key length (CCMP) + body[9:17] = replay + body[17:49] = nonce + body[65:73] = rsc + struct.pack_into('>H', body, 97, len(key_data)) + body[HDR:] = key_data + if key_info & 0x0100: # MIC bit + body[81:97] = mic(kck, bytes(body), mic_ver) + return bytes(body) + +def parse(frame): + return { + 'key_info': struct.unpack_from('>H', frame, 5)[0], + 'replay': frame[9:17], + 'nonce': frame[17:49], + 'mic': frame[81:97], + 'kdlen': struct.unpack_from('>H', frame, 97)[0], + 'kd': frame[HDR:], + } + +def check_mic(frame, kck, ver): + f = bytearray(frame); f[81:97] = b'\x00' * 16 + return mic(kck, bytes(f), ver) == frame[81:97] + +def run_case(name, ssid, passphrase, akm, pcipher, gcipher, desc_ver, corrupt_pass=False): + print(f"\n=== {name} ===") + aa = bytes.fromhex('001122334455') # AP + spa = bytes.fromhex('aabbccddeeff') # station + p = subprocess.Popen([HARNESS], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1) + + def send(line): + p.stdin.write(line + '\n'); p.stdin.flush() + def readline(): + return p.stdout.readline().strip() + + sta_pass = passphrase + ('X' if corrupt_pass else '') + send(f"START {spa.hex()} {aa.hex()} {ssid} {sta_pass} {akm} {pcipher} {gcipher}") + ok = readline() + rsnie = readline() + assert ok.startswith('START-OK 1'), ok + sta_rsn = bytes.fromhex(rsnie.split()[1]) + print(f" supplicant RSN IE: {sta_rsn.hex()}") + + pmk = hashlib.pbkdf2_hmac('sha1', passphrase.encode(), ssid.encode(), 4096, 32) + anonce = os.urandom(32) + replay = (1).to_bytes(8, 'big') + + # --- message 1 --- + m1 = build(2, 2, desc_ver | 0x0008 | 0x0080, replay, anonce, b'\x00'*8, b'') + send("RX " + m1.hex()) + tx = readline() + assert tx.startswith('TX '), f"expected msg2, got {tx}" + m2 = bytes.fromhex(tx.split()[1]) + status = readline() + print(f" msg2 {len(m2)} bytes, {status}") + + i2 = parse(m2) + snonce = i2['nonce'] + ptk = derive_ptk(pmk, aa, spa, anonce, snonce, 48, sha256=(akm == 6)) + kck, kek, tk = ptk[:16], ptk[16:32], ptk[32:48] + + m2_ok = check_mic(m2, kck, desc_ver) + print(f" msg2 MIC verifies against independently derived PTK: {m2_ok}") + if corrupt_pass: + assert not m2_ok, "a wrong passphrase must not produce a valid MIC" + print(" (expected: wrong passphrase -> MIC mismatch)") + p.stdin.close(); return + assert m2_ok + assert i2['replay'] == replay, "msg2 must echo the replay counter" + assert i2['kd'] == sta_rsn, "msg2 key data must be the supplicant's RSN IE" + assert i2['key_info'] & 0x0008, "msg2 must set the pairwise bit" + assert i2['key_info'] & 0x0100, "msg2 must set the MIC bit" + + # --- message 3, carrying an AES-wrapped GTK KDE --- + gtk = os.urandom(16) + gtk_kde = bytes([0xdd, 6 + len(gtk), 0x00, 0x0f, 0xac, 0x01, 0x02, 0x00]) + gtk + kd = gtk_kde + if len(kd) % 8: + kd += b'\xdd' + b'\x00' * (7 - len(kd) % 8) + wrapped = aes_key_wrap(kek, kd) + replay3 = (2).to_bytes(8, 'big') + rsc = bytes.fromhex('0102030405060000') + m3 = build(2, 2, desc_ver | 0x0008 | 0x0040 | 0x0080 | 0x0100 | 0x0200 | 0x1000, + replay3, anonce, rsc, wrapped, kck, desc_ver) + send("RX " + m3.hex()) + + got = {} + while True: + l = readline() + if l.startswith('RX-OK'): + print(f" {l}") + break + k, _, v = l.partition(' ') + got[k] = v + if k == 'TX': + m4 = bytes.fromhex(v) + + assert 'TX' in got, "supplicant must answer message 3 with message 4" + m4_ok = check_mic(m4, kck, desc_ver) + i4 = parse(m4) + print(f" msg4 MIC verifies: {m4_ok}") + assert m4_ok + assert i4['replay'] == replay3, "msg4 must echo message 3's replay counter" + assert i4['key_info'] & 0x0200, "msg4 must set the secure bit" + assert i4['kdlen'] == 0, "msg4 must carry no key data" + + assert got['PTK'] == tk.hex(), f"installed TK {got['PTK']} != expected {tk.hex()}" + print(f" installed TK matches the AP's derivation: True") + assert got['GTK'] == gtk.hex(), f"installed GTK {got['GTK']} != {gtk.hex()}" + print(f" installed GTK matches: True (key index {got['GTK-IDX']})") + assert got['GTK-RSC'].startswith('010203040506') + assert 'STATE 3' in l, f"supplicant should be Complete, got {l}" + print(" PASS") + p.stdin.close() + +run_case("WPA2-PSK / CCMP / key descriptor v2", "MontaukTest", "supersecret123", 2, 4, 4, 2) +run_case("WPA2-PSK-SHA256 / CCMP / key descriptor v3", "MontaukTest", "supersecret123", 6, 4, 4, 3) +run_case("wrong passphrase is rejected", "MontaukTest", "supersecret123", 2, 4, 4, 2, corrupt_pass=True) +print("\nAll supplicant cases passed.") diff --git a/tests/wifi/ap_mlme.py b/tests/wifi/ap_mlme.py new file mode 100644 index 0000000..9c11df0 --- /dev/null +++ b/tests/wifi/ap_mlme.py @@ -0,0 +1,640 @@ +#!/usr/bin/env python3 +"""Drives the real MLME through a complete join and validates every frame it +puts on the air, plus the 802.11 <-> Ethernet translation in both directions. + +The access point side is written independently here (hashlib / cryptography for +the handshake, hand-decoded 802.11 for the frames), so this is a check of what +the driver actually emits rather than a restatement of it. + +Copyright (c) 2026 Daniel Hammer +""" +import hashlib, hmac, os, struct, subprocess, sys +from cryptography.hazmat.primitives.keywrap import aes_key_wrap + +HARNESS = sys.argv[1] +AP = bytes.fromhex('001122334455') +STA = bytes.fromhex('aabbccddeeff') +SSID = "MontaukTest" +PASS = "supersecret123" +CHANNEL = 6 + +fails = [] +def state_of(result): + f = result.split() + return int(f[f.index('STATE') + 1]) if 'STATE' in f else -1 + +def link_of(result): + f = result.split() + return int(f[f.index('LINK') + 1]) if 'LINK' in f else -1 + +def check(cond, what): + print(f" {'PASS' if cond else 'FAIL'} {what}") + if not cond: + fails.append(what) + +# --------------------------------------------------------------- harness I/O +class Driver: + def __init__(self): + self.p = subprocess.Popen([HARNESS], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, text=True, bufsize=1) + def cmd(self, line): + """Send a command; collect emitted events until the terminator.""" + self.p.stdin.write(line + '\n'); self.p.stdin.flush() + ev = {'TX': [], 'KEY': [], 'KEYDEL': [], 'ETH': [], 'CMD': [], 'TXQ': []} + while True: + l = self.p.stdout.readline() + if not l: + raise RuntimeError("harness died") + l = l.strip() + k = l.split()[0] if l else '' + if k == 'TX': + f = l.split() + ev['TX'].append({'enc': f[1] == 'enc=1', 'rate': f[2] == 'rate=1', + 'hdr': bytes.fromhex(f[3]), + 'body': bytes.fromhex(f[4]) if len(f) > 4 else b''}) + elif k == 'KEY': + f = l.split() + ev['KEY'].append({'pairwise': f[1] == 'pairwise=1', + 'idx': int(f[2].split('=')[1]), + 'cipher': int(f[3].split('=')[1]), + 'key': bytes.fromhex(f[4]), + 'rsc': bytes.fromhex(f[5]) if len(f) > 5 else b''}) + elif k == 'ETH': + ev['ETH'].append(bytes.fromhex(l.split()[1])) + elif k == 'CMD': + f = l.split() + ev['CMD'].append((int(f[1]), + bytes.fromhex(f[2]) if len(f) > 2 else b'')) + elif k == 'KEY-REMOVE': + ev['KEYDEL'].append(l) + elif k in ('TXQ-UP', 'TXQ-DOWN'): + ev['TXQ'].append(l) + else: + ev['result'] = l + return ev + +# --------------------------------------------------------------- 802.11 bits +def mgmt(subtype, dst, src, bssid, body, seq=0): + return bytes([0x00 | subtype, 0x00]) + b'\x00\x00' + dst + src + bssid \ + + struct.pack('H', ethertype) + return hdr + iv + snap + payload + +def parse_ies(b): + ies, off = {}, 0 + while off + 2 <= len(b): + i, l = b[off], b[off + 1] + if off + 2 + l > len(b): break + ies[i] = b[off + 2:off + 2 + l] + off += 2 + l + return ies + +def suite(t): return bytes([0x00, 0x0f, 0xac, t]) +AP_RSN = (struct.pack('H', b, 2, HDR + len(kd) - 4); b[4] = 2 + struct.pack_into('>H', b, 5, ki); struct.pack_into('>H', b, 7, 16) + b[9:17] = replay; b[17:49] = nonce; b[65:73] = rsc + struct.pack_into('>H', b, 97, len(kd)); b[HDR:] = kd + if ki & 0x0100: b[81:97] = emic(kck, bytes(b)) + return bytes(b) +def check_emic(f, kck): + x = bytearray(f); x[81:97] = b'\x00' * 16 + return emic(kck, bytes(x)) == f[81:97] +def gtk_kde(gtk, idx): + kd = bytes([0xdd, 6 + len(gtk), 0x00, 0x0f, 0xac, 0x01, idx, 0x00]) + gtk + if len(kd) % 8: kd += b'\xdd' + b'\x00' * (7 - len(kd) % 8) + return kd + +# ============================================================================= +d = Driver() +d.cmd("MAC " + STA.hex()) + +print("=== authentication ===") +ev = d.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 {SSID} {PASS} {AP_RSN.hex()}") +check(ev['result'].startswith('CONNECT-OK 1'), "connect starts") +check(len(ev['TXQ']) == 1 and ev['TXQ'][0].startswith('TXQ-UP'), + "a transmit queue is opened before any frame is sent") +check(len(ev['TX']) == 1, "exactly one frame is sent (the authentication request)") +auth = ev['TX'][0] +h, b = auth['hdr'], auth['body'] +check(h[0] == 0xb0, "authentication frame: type management, subtype auth") +check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP, + "authentication frame: addressed to the AP, from us, BSSID correct") +check(len(h) == 24, "authentication frame: 24-byte header") +check(struct.unpack('= 4, "association request: supported rates present") +check(any(r & 0x80 for r in ies[1]), "association request: at least one basic rate") +check(48 in ies, "association request: RSN element present") +sta_rsn = ies[48] +check(sta_rsn[0:2] == struct.pack('= 2, "post-association context updates are sent") + +print("\n=== 4-way handshake carried over 802.11 data frames ===") +pmk = hashlib.pbkdf2_hmac('sha1', PASS.encode(), SSID.encode(), 4096, 32) +anonce = os.urandom(32) +m1 = eapol(0x0002 | 0x0008 | 0x0080, (1).to_bytes(8, 'big'), anonce, b'\x00' * 8, b'') +d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m1).hex()) +ev = d.cmd("SERVICE") +check(len(ev['TX']) == 1, "EAPOL message 1 produces exactly one reply") +m2f = ev['TX'][0] +h = m2f['hdr'] +check(h[0] == 0x08 and (h[1] & 0x01), "EAPOL reply: data frame with to-DS set") +check(not (h[1] & 0x40), "EAPOL reply: not marked protected (no key yet)") +check(not m2f['enc'], "EAPOL reply: firmware told not to encrypt") +check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP, + "EAPOL reply: addr1 the AP, addr2 us, addr3 the AP") +check(m2f['body'][0:6] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]), + "EAPOL reply: RFC 1042 LLC/SNAP shim") +check(m2f['body'][6:8] == struct.pack('>H', 0x888e), + "EAPOL reply: EtherType 0x888e") +m2 = m2f['body'][8:] +snonce = m2[17:49] +ptk = derive_ptk(pmk, anonce, snonce) +kck, kek, tk = ptk[:16], ptk[16:32], ptk[32:48] +check(check_emic(m2, kck), "EAPOL message 2 MIC verifies under the AP's own PTK") +check(m2[HDR:] == bytes([48, len(sta_rsn)]) + sta_rsn, + "EAPOL message 2 carries the same RSN element as the association request") + +gtk = os.urandom(16) +rsc = bytes.fromhex('0a0b0c0d0e0f0000') +m3 = eapol(0x0002 | 0x0008 | 0x0040 | 0x0080 | 0x0100 | 0x0200 | 0x1000, + (2).to_bytes(8, 'big'), anonce, rsc, aes_key_wrap(kek, gtk_kde(gtk, 1)), kck) +d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m3).hex()) +ev = d.cmd("SERVICE") +check(len(ev['TX']) == 1, "EAPOL message 3 produces message 4") +m4 = ev['TX'][0]['body'][8:] +check(check_emic(m4, kck), "EAPOL message 4 MIC verifies") +keys = {('pairwise' if k['pairwise'] else 'group'): k for k in ev['KEY']} +check('pairwise' in keys and keys['pairwise']['key'] == tk, + "pairwise key installed matches the AP's temporal key") +check(keys.get('pairwise', {}).get('cipher') == 4, "pairwise key installed as CCMP") +check('group' in keys and keys['group']['key'] == gtk, "group key installed matches") +check(keys.get('group', {}).get('rsc') == rsc[:6], + "group key installed with the AP's receive sequence counter") +check(state_of(ev['result']) == 7 and link_of(ev['result']) == 1, + "link reports up once keyed") + +print("\n=== data path ===") +ip = bytes.fromhex('4500002800010000401100000a0000010a000002') + b'payload-here' +peer = bytes.fromhex('665544332211') +eth_out = peer + STA + struct.pack('>H', 0x0800) + ip +ev = d.cmd("TXETH " + eth_out.hex()) +check(ev['result'] == 'TXETH-OK 1', "an Ethernet frame is accepted for transmit") +f = ev['TX'][0] +h = f['hdr'] +check(h[0] == 0x08 and (h[1] & 0x01), "outbound data: to-DS data frame") +check(h[1] & 0x40, "outbound data: protected bit set now that keys are installed") +check(f['enc'], "outbound data: firmware asked to encrypt") +check(not f['rate'], "outbound data: rate control left to the firmware") +check(h[4:10] == AP, "outbound data: addr1 is the AP") +check(h[10:16] == STA, "outbound data: addr2 is us") +check(h[16:22] == peer, "outbound data: addr3 is the final destination") +check(f['body'][0:8] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]) + struct.pack('>H', 0x0800), + "outbound data: LLC/SNAP carries the EtherType") +check(f['body'][8:] == ip, "outbound data: IP payload preserved byte for byte") +seq1 = struct.unpack('H', 0x0800), "inbound data: EtherType recovered") +check(e[14:] == reply, "inbound data: payload recovered past the CCMP header") + +bcast = bytes.fromhex('ffffffffffff') +ev = d.cmd("RXDATA " + data_from_ds(bcast, AP, peer, 0x0806, b'arp-request-body').hex()) +check(len(ev['ETH']) == 1 and ev['ETH'][0][0:6] == bcast, + "inbound broadcast (ARP) is delivered") + +ev = d.cmd("RXDATA " + data_from_ds(STA, bytes.fromhex('aa0000000001'), + peer, 0x0800, b'from-a-stranger').hex()) +check(len(ev['ETH']) == 0, "a data frame from a different BSSID is ignored") + +ev = d.cmd("RXDATA " + (bytes([0x48, 0x02]) + b'\x00\x00' + STA + AP + peer + + b'\x00\x00').hex()) +check(len(ev['ETH']) == 0, "a null-data keepalive produces no Ethernet frame") + +print("\n=== teardown ===") +ev = d.cmd("ABORT") +check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']), "abort closes the transmit queue") +check(len(ev['KEYDEL']) == 2, "abort removes both hardware keys before the station") +check(state_of(ev['result']) == 0, "abort returns to idle") +ev = d.cmd("TXETH " + eth_out.hex()) +check(ev['result'] == 'TXETH-OK 0', "transmit is refused once the link is down") + +# ============================================================================= +# Other paths through the state machine, each on a fresh harness. +# ============================================================================= + +print("\n=== open network ===") +d2 = Driver() +d2.cmd("MAC " + STA.hex()) +ev = d2.cmd(f"CONNECT {AP.hex()} 11 0 OpenNet - -") +check(ev['result'].startswith('CONNECT-OK 1'), "open network: connect starts") +caps_seen = [] +d2.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('H', 0x0800) + ip).hex()) +f = ev['TX'][0] +check(not (f['hdr'][1] & 0x40) and not f['enc'], + "open network: data frames are sent unprotected") + +print("\n=== the access point never answers ===") +d3 = Driver() +d3.cmd("MAC " + STA.hex()) +d3.cmd(f"CONNECT {AP.hex()} 6 0 Nowhere - -") +retries = 0 +for _ in range(6): + d3.cmd("TICK 500") + ev = d3.cmd("SERVICE") + retries += len(ev['TX']) + if state_of(ev['result']) == 8: + break +check(retries >= 3, f"authentication is retransmitted ({retries} retries) before giving up") +check(state_of(ev['result']) == 8, "the attempt eventually fails rather than hanging") +check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']), + "giving up tears the transmit queue back down") + +print("\n=== the access point rejects us ===") +d4 = Driver() +d4.cmd("MAC " + STA.hex()) +d4.cmd(f"CONNECT {AP.hex()} 6 0 Rejects - -") +ev = d4.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('H', 0x0800) + ip).hex()) +check(ev['result'] == 'TXETH-OK 0', "transmit is refused after being dropped") + +# ============================================================================= +# PHY context command layout. +# +# Regression test for a lockup: firmware 89 advertises ULTRA_HB_CHANNELS, which +# selects an 8-byte channel-info sub-structure (32-bit channel first). Sending +# the older 4-byte form shifted every field after it, asserted the firmware, and +# the driver then spun forever waiting for a reply that would never come. +# ============================================================================= + +print("\n=== PHY context command layout ===") +PHY_CONTEXT_CMD = 0x08 +ULTRA_HB = 48 + +def phy_cmd_for(uhb): + dd = Driver() + dd.cmd("MAC " + STA.hex()) + dd.cmd(f"CAPA {ULTRA_HB} {1 if uhb else 0}") + ev = dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -") + dd.p.stdin.close() + for cid, payload in ev['CMD']: + if cid == PHY_CONTEXT_CMD: + return payload + return None + +p_uhb = phy_cmd_for(True) +check(p_uhb is not None and len(p_uhb) == 32, + f"ultra-high-band firmware gets a 32-byte PHY context ({len(p_uhb) if p_uhb else 0})") +if p_uhb and len(p_uhb) == 32: + chan = struct.unpack_from('= 3, f"link add, PHY binding and activation are three commands " + f"(got {len(links)})") +if len(links) >= 3 and phy_add is not None: + bind_i, bind = links[1] + act_i, act = links[2] + check(phy_add < bind_i, "the PHY context exists before the link binds to it") + check(struct.unpack_from('H',b,2,HDR+len(kd)-4); b[4]=2 + struct.pack_into('>H',b,5,ki); struct.pack_into('>H',b,7,16) + b[9:17]=replay; b[17:49]=nonce; b[65:73]=rsc + struct.pack_into('>H',b,97,len(kd)); b[HDR:]=kd + if ki & 0x0100: b[81:97]=mic(kck, bytes(b)) + return bytes(b) +def check_mic(f,kck): + x=bytearray(f); x[81:97]=b'\x00'*16 + return mic(kck,bytes(x))==f[81:97] +def gtk_kde(gtk, idx): + kd = bytes([0xdd, 6+len(gtk), 0x00,0x0f,0xac, 0x01, idx, 0x00]) + gtk + if len(kd)%8: kd += b'\xdd' + b'\x00'*(7-len(kd)%8) + return kd + +def start(p, ssid, passphrase, spa, aa): + p.stdin.write(f"START {spa.hex()} {aa.hex()} {ssid} {passphrase} 2 4 4\n"); p.stdin.flush() + p.stdout.readline(); p.stdout.readline() + +def rd(p): return p.stdout.readline().strip() + +# ---------------------------------------------------------------- handshake + +print("=== GTK rekey and message-3 retransmission ===") +aa=bytes.fromhex('001122334455'); spa=bytes.fromhex('aabbccddeeff') +ssid="MontaukTest"; pw="supersecret123" +p=subprocess.Popen([HARNESS],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True,bufsize=1) +start(p, ssid, pw, spa, aa) +pmk=hashlib.pbkdf2_hmac('sha1',pw.encode(),ssid.encode(),4096,32) +an=os.urandom(32) +p.stdin.write("RX "+build(0x0002|0x0008|0x0080,(1).to_bytes(8,'big'),an,b'\x00'*8,b'').hex()+"\n"); p.stdin.flush() +m2=bytes.fromhex(rd(p).split()[1]); rd(p) +sn=m2[17:49] +ptk=derive_ptk(pmk,aa,spa,an,sn); kck,kek,tk=ptk[:16],ptk[16:32],ptk[32:48] +gtk1=os.urandom(16) +m3=build(0x0002|0x0008|0x0040|0x0080|0x0100|0x0200|0x1000,(2).to_bytes(8,'big'),an, + b'\x00'*8, aes_key_wrap(kek, gtk_kde(gtk1,1)), kck) +p.stdin.write("RX "+m3.hex()+"\n"); p.stdin.flush() +got={} +while True: + l=rd(p) + if l.startswith('RX-OK'): break + k,_,v=l.partition(' '); got[k]=v +assert got['GTK']==gtk1.hex() and got['PTK']==tk.hex() +print(f" initial handshake complete, state {l}") + +# message 3 retransmitted (AP missed message 4) +m3b=build(0x0002|0x0008|0x0040|0x0080|0x0100|0x0200|0x1000,(3).to_bytes(8,'big'),an, + b'\x00'*8, aes_key_wrap(kek, gtk_kde(gtk1,1)), kck) +p.stdin.write("RX "+m3b.hex()+"\n"); p.stdin.flush() +got2={} +while True: + l=rd(p) + if l.startswith('RX-OK'): break + k,_,v=l.partition(' '); got2[k]=v +if 'TX' not in got2: + fails.append("no message 4 in response to a retransmitted message 3") +else: + m4=bytes.fromhex(got2['TX']) + ok = check_mic(m4,kck) and m4[9:17]==(3).to_bytes(8,'big') + print(f" answered retransmitted message 3 with a fresh message 4: {ok}") + if not ok: fails.append("message 4 replay answer malformed") + +# group key rekey (2-way, no pairwise bit) +gtk2=os.urandom(16) +gk=build(0x0002|0x0080|0x0100|0x0200|0x1000,(4).to_bytes(8,'big'),b'\x00'*32, + bytes.fromhex('0a0b0c000000 0000'.replace(' ','')), aes_key_wrap(kek, gtk_kde(gtk2,2)), kck) +p.stdin.write("RX "+gk.hex()+"\n"); p.stdin.flush() +got3={} +while True: + l=rd(p) + if l.startswith('RX-OK'): break + k,_,v=l.partition(' '); got3[k]=v +if got3.get('GTK')!=gtk2.hex(): + fails.append(f"group rekey installed {got3.get('GTK')} instead of {gtk2.hex()}") +else: + print(f" group rekey installed the new GTK (index {got3['GTK-IDX']})") +if 'TX' not in got3: + fails.append("no acknowledgement to the group key message") +else: + m=bytes.fromhex(got3['TX']) + ki=struct.unpack_from('>H',m,5)[0] + ok = bool(check_mic(m, kck) and not (ki & 0x0008) and (ki & 0x0200)) + print(f" group key acknowledged correctly (no pairwise bit, secure set): {ok}") + if not ok: fails.append("group key acknowledgement malformed") +p.stdin.close() + +# ---------------------------------------------------------------- RSN parsing +print("\n=== RSN negotiation against real-world information elements ===") +def suite(t): return bytes([0x00,0x0f,0xac,t]) +def ie(group, pairwise, akms, caps=0): + b = struct.pack(' accepted={ok} akm={akm} pairwise={pc}") + if not good: fails.append(f"RSN parse: {name}") +p.stdin.close() + +print() +if fails: + for f in fails: print("FAILURE:", f) + sys.exit(1) +print("All second-round supplicant cases passed.") diff --git a/tests/wifi/crypto_vectors.cpp b/tests/wifi/crypto_vectors.cpp new file mode 100644 index 0000000..5a38511 --- /dev/null +++ b/tests/wifi/crypto_vectors.cpp @@ -0,0 +1,111 @@ +#include +#include +#include "Libraries/Crypto.hpp" +using namespace Kt::Crypto; + +static int fails = 0; +static void hexdump(const char* label, const uint8_t* p, size_t n) { + printf("%s: ", label); + for (size_t i = 0; i < n; i++) printf("%02x", p[i]); + printf("\n"); +} +static void check(const char* name, const uint8_t* got, const char* wantHex) { + size_t n = strlen(wantHex) / 2; + uint8_t want[128]; + for (size_t i = 0; i < n; i++) { unsigned v; sscanf(wantHex + 2*i, "%2x", &v); want[i] = (uint8_t)v; } + if (memcmp(got, want, n) == 0) { printf("PASS %s\n", name); } + else { printf("FAIL %s\n", name); hexdump(" got ", got, n); printf(" want: %s\n", wantHex); fails++; } +} + +int main() { + uint8_t out[64]; + + Sha1("abc", 3, out); + check("sha1(abc)", out, "a9993e364706816aba3e25717850c26c9cd0d89d"); + Sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, out); + check("sha1(448bit)", out, "84983e441c3bd26ebaae4aa1f95129e5e54670f1"); + + Sha256("abc", 3, out); + check("sha256(abc)", out, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + Sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, out); + check("sha256(448bit)", out, "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"); + + { uint8_t k[20]; memset(k, 0x0b, 20); + HmacSha1(k, 20, "Hi There", 8, out); + check("hmac-sha1 rfc2202#1", out, "b617318655057264e28bc0b6fb378c8ef146be00"); } + { HmacSha1((const uint8_t*)"Jefe", 4, "what do ya want for nothing?", 28, out); + check("hmac-sha1 rfc2202#2", out, "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"); } + { uint8_t k[20]; memset(k, 0x0b, 20); + HmacSha256(k, 20, "Hi There", 8, out); + check("hmac-sha256 rfc4231#1", out, "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); } + + // FIPS-197 AES-128 / AES-256 + { AesCtx c; uint8_t key[16], pt[16], ct[16], back[16]; + for (int i = 0; i < 16; i++) key[i] = (uint8_t)i; + for (int i = 0; i < 16; i++) pt[i] = (uint8_t)(i * 0x11); + AesInit(c, key, 16); AesEncryptBlock(c, pt, ct); + check("aes128 fips197", ct, "69c4e0d86a7b0430d8cdb78070b4c55a"); + AesDecryptBlock(c, ct, back); + check("aes128 decrypt", back, "00112233445566778899aabbccddeeff"); } + { AesCtx c; uint8_t key[32], pt[16], ct[16], back[16]; + for (int i = 0; i < 32; i++) key[i] = (uint8_t)i; + for (int i = 0; i < 16; i++) pt[i] = (uint8_t)(i * 0x11); + AesInit(c, key, 32); AesEncryptBlock(c, pt, ct); + check("aes256 fips197", ct, "8ea2b7ca516745bfeafc49904b496089"); + AesDecryptBlock(c, ct, back); + check("aes256 decrypt", back, "00112233445566778899aabbccddeeff"); } + + // RFC 3394 section 4.1 (128-bit KEK, 128-bit key) and 4.6 (256/256) + { uint8_t kek[16], kd[16], wrapped[24], unwrapped[16]; + for (int i = 0; i < 16; i++) kek[i] = (uint8_t)i; + for (int i = 0; i < 16; i++) kd[i] = (uint8_t)(i * 0x11); + AesKeyWrap(kek, 16, kd, 16, wrapped); + check("keywrap rfc3394 4.1", wrapped, "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5"); + bool ok = AesKeyUnwrap(kek, 16, wrapped, 24, unwrapped); + printf("%s keyunwrap integrity\n", ok ? "PASS" : "FAIL"); if (!ok) fails++; + check("keyunwrap rfc3394 4.1", unwrapped, "00112233445566778899aabbccddeeff"); } + { // 256-bit KEK, 256-bit key data (RFC3394 4.6) + uint8_t kek[32], kd[32], wrapped[40], unwrapped[32]; + for (int i = 0; i < 32; i++) kek[i] = (uint8_t)i; + const char* kdhex = "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F"; + for (int i = 0; i < 32; i++) { unsigned v; sscanf(kdhex + 2*i, "%2x", &v); kd[i] = (uint8_t)v; } + AesKeyWrap(kek, 32, kd, 32, wrapped); + check("keywrap rfc3394 4.6", wrapped, "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21"); + bool ok = AesKeyUnwrap(kek, 32, wrapped, 40, unwrapped); + printf("%s keyunwrap256 integrity\n", ok ? "PASS" : "FAIL"); if (!ok) fails++; } + + // RFC 4493 AES-CMAC + { uint8_t key[16] = {0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c}; + const uint8_t* parts[1]; size_t lens[1]; + uint8_t empty[1] = {0}; + parts[0] = empty; lens[0] = 0; + AesCmac(key, 16, parts, lens, 1, out); + check("cmac rfc4493 len0", out, "bb1d6929e95937287fa37d129b756746"); + uint8_t msg[64] = {0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a, + 0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c,0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51, + 0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11,0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xef, + 0xf6,0x9f,0x24,0x45,0xdf,0x4f,0x9b,0x17,0xad,0x2b,0x41,0x7b,0xe6,0x6c,0x37,0x10}; + parts[0] = msg; lens[0] = 16; + AesCmac(key, 16, parts, lens, 1, out); + check("cmac rfc4493 len16", out, "070a16b46b4d4144f79bdd9dd04a287c"); + parts[0] = msg; lens[0] = 40; + AesCmac(key, 16, parts, lens, 1, out); + check("cmac rfc4493 len40", out, "dfa66747de9ae63030ca32611497c827"); + parts[0] = msg; lens[0] = 64; + AesCmac(key, 16, parts, lens, 1, out); + check("cmac rfc4493 len64", out, "51f0bebf7e3b9d92fc49741779363cfe"); + // split across parts to exercise the streaming path + const uint8_t* sp[3] = {msg, msg+10, msg+33}; size_t sl[3] = {10, 23, 31}; + AesCmac(key, 16, sp, sl, 3, out); + check("cmac split len64", out, "51f0bebf7e3b9d92fc49741779363cfe"); } + + // WPA PSK vectors (IEEE 802.11i Annex H.4) + { uint8_t psk[32]; + Pbkdf2Sha1("password", 8, (const uint8_t*)"IEEE", 4, 4096, psk, 32); + check("pbkdf2 wpa 'password'/IEEE", psk, "f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e"); + Pbkdf2Sha1("ThisIsAPassword", 15, (const uint8_t*)"ThisIsASSID", 11, 4096, psk, 32); + check("pbkdf2 wpa 'ThisIsAPassword'", psk, "0dc0d6eb90555ed6419756b9a15ec3e3209b63df707dd508d14581f8982721af"); } + + printf(fails ? "\n%d FAILURES\n" : "\nall vectors pass\n", fails); + return fails != 0; +} diff --git a/tests/wifi/decode_iwl_trace.py b/tests/wifi/decode_iwl_trace.py new file mode 100644 index 0000000..c7e3140 --- /dev/null +++ b/tests/wifi/decode_iwl_trace.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +"""Decode an iwlwifi host-command trace captured from Linux on the same adapter. + +Linux drives this exact firmware successfully, so its host commands are ground +truth for what the firmware expects -- far more reliable than reading struct +definitions out of a kernel tree and hoping the version matches. + +Capture on a Linux box with the adapter, as root: + + trace-cmd record -e iwlwifi_dev_hcmd -o /tmp/iwl.dat \ + sh -c 'nmcli radio wifi off; sleep 2; nmcli radio wifi on; sleep 20' + +Then run this against /tmp/iwl.dat. dump_iwl_cmds.py prints full payload hex +for each command instead of a decoded summary. + +Copyright (c) 2026 Daniel Hammer +""" +import re, subprocess +out = subprocess.run(['trace-cmd','report','-R','-i','/tmp/iwl.dat'], + capture_output=True, text=True).stdout +ACT={0:'STUB',1:'ADD',2:'MODIFY',3:'REMOVE'} +def u32(p,o): return p[o]|(p[o+1]<<8)|(p[o+2]<<16)|(p[o+3]<<24) +rows=[] +for line in out.splitlines(): + m=re.search(r'hcmd=ARRAY\[(.*?)\]',line); t=re.search(r'\s(\d+\.\d+):',line) + if not m: continue + b=[int(x,16) for x in m.group(1).split(', ')] + if len(b)<8: continue + grp,op=b[1],b[0]; ln=b[4]|(b[5]<<8); p=b[8:8+ln]; ts=float(t.group(1)) if t else 0 + if (grp,op)==(3,0x08) and ln>=52: + rows.append((ts,f"MAC_CONFIG action={ACT.get(u32(p,4)):6} id={u32(p,0)} is_assoc={p[36]} aid={p[40]|p[41]<<8} filter=0x{u32(p,20):x}")) + elif (grp,op)==(3,0x09) and ln>=208: + rows.append((ts,f"LINK_CONFIG action={ACT.get(u32(p,0)):6} link={u32(p,4)} mac={u32(p,8)} phy={u32(p,12)} mask=0x{u32(p,24):02x} active={p[28]} bi={u32(p,136)}")) + elif (grp,op)==(3,0x0a) and ln>=96: + rows.append((ts,f"STA_CONFIG sta={u32(p,0)} link={u32(p,4)} type={u32(p,24)} aid={u32(p,28)}")) + elif (grp,op)==(1,0x08) and ln>=32: + rows.append((ts,f"PHY_CONTEXT action={ACT.get(u32(p,4)):6} id={u32(p,0)} chan={u32(p,8)} band={p[12]} width={p[13]}")) + elif (grp,op)==(3,0x05): + rows.append((ts,f"SESSION_PROT action={ACT.get(u32(p,4)):6} conf_id={u32(p,8)} dur={u32(p,12)}")) + elif (grp,op)==(5,0x17): + rows.append((ts,f"SCD_QUEUE_CFG op={u32(p,0)}")) + elif (grp,op)==(3,0x0c): + rows.append((ts,"STA_REMOVE")) +rows.sort(); base=rows[0][0] +for ts,s in rows: + if ts-base > 2.3: print(f"{ts-base:7.3f} {s}") diff --git a/tests/wifi/dump_iwl_cmds.py b/tests/wifi/dump_iwl_cmds.py new file mode 100644 index 0000000..97ce5eb --- /dev/null +++ b/tests/wifi/dump_iwl_cmds.py @@ -0,0 +1,27 @@ +import re, sys, subprocess +out = subprocess.run(['trace-cmd','report','-R','-i','/tmp/iwl.dat'], + capture_output=True, text=True).stdout +NAMES = {(3,0x08):'MAC_CONFIG', (3,0x09):'LINK_CONFIG', (3,0x0a):'STA_CONFIG', + (3,0x0c):'STA_REMOVE', (1,0x08):'PHY_CONTEXT', (3,0x05):'SESSION_PROT'} +seen = {} +for line in out.splitlines(): + m = re.search(r'hcmd=ARRAY\[(.*?)\]', line) + if not m: continue + b = [int(x,16) for x in m.group(1).split(', ')] + if len(b) < 8: continue + op, grp = b[0], b[1] + ln = b[4] | (b[5] << 8) + key = (grp, op) + if key not in NAMES: continue + payload = b[8:8+ln] + action = payload[0] | (payload[1]<<8) | (payload[2]<<16) | (payload[3]<<24) if len(payload)>=4 else -1 + seen.setdefault(key, []).append((ln, payload)) +for key, lst in seen.items(): + print(f"\n===== {NAMES[key]} (group 0x{key[0]:02x} cmd 0x{key[1]:02x}) x{len(lst)} =====") + lens = {l for l,_ in lst} + print(f"payload length(s): {sorted(lens)}") + # show the most 'interesting' one (most non-zero bytes) + ln, p = max(lst, key=lambda t: sum(1 for x in t[1] if x)) + for off in range(0, len(p), 16): + chunk = p[off:off+16] + print(f" +{off:3d}: " + " ".join(f"{x:02x}" for x in chunk)) diff --git a/tests/wifi/mlme_harness.cpp b/tests/wifi/mlme_harness.cpp new file mode 100644 index 0000000..68cd23d --- /dev/null +++ b/tests/wifi/mlme_harness.cpp @@ -0,0 +1,217 @@ +/* + * mlme_harness.cpp + * Host harness for the 802.11 MLME and data path. + * + * Compiles the real IwxConnect.cpp (and the supplicant behind it) against a + * stubbed transport, so everything the driver puts on the air and everything + * it makes of what comes back can be checked without the adapter. The + * firmware/radio interaction is what remains untestable here; the frame + * construction, parsing, encapsulation and state machine are not. + * + * Speaks hex over stdio; ap_mlme.py is the peer. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include +#include +#include +#include +#include + +#include "Drivers/Net/Wifi/Iwx.hpp" +#include "Drivers/Net/Wifi/Ieee80211.hpp" +#include "Drivers/Net/Wifi/Wpa.hpp" + +namespace Timekeeping { + uint64_t g_ms = 1000; + uint64_t GetMilliseconds() { return g_ms; } +} + +// A host command is a round trip to the firmware: it takes real time, and the +// clock moves on while the service loop is inside one. Modelling that is what +// catches elapsed-time arithmetic that samples the clock once and then compares +// it against timestamps taken later in the same pass (an unsigned underflow +// that reads as an instant timeout). A frozen clock hides that class of bug +// entirely, which is why this is not simply left at a constant. +static constexpr uint64_t CMD_ROUND_TRIP_MS = 1; + +using namespace Drivers::Net::Wifi; + +static void puthex(const char* tag, const uint8_t* p, uint32_t n) { + printf("%s ", tag); + for (uint32_t i = 0; i < n; i++) printf("%02x", p[i]); + printf("\n"); +} + +// ============================================================================= +// Stubbed transport +// ============================================================================= + +namespace Drivers::Net::Wifi { + + IwxState g_iwx; + + // Host commands: record the opcode and the exact bytes, always succeed. + // The payload matters -- a struct that does not match the version the + // firmware advertises asserts it on real hardware. + static void DumpCmd(uint32_t id, const void* data, uint32_t len) { + printf("CMD %u ", id); + const uint8_t* p = (const uint8_t*)data; + for (uint32_t i = 0; i < len; i++) printf("%02x", p[i]); + printf("\n"); + Timekeeping::g_ms += CMD_ROUND_TRIP_MS; + } + bool IwxSendCmdPdu(uint32_t id, const void* data, uint32_t len) { + DumpCmd(id, data, len); + return true; + } + bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len, + uint32_t* statusOut) { + DumpCmd(id, data, len); + // ADD_STA reports success in the low byte; everything else uses 0. + if (statusOut) *statusOut = (id == IWX_ADD_STA || id == IWX_ADD_STA_KEY) + ? IWX_ADD_STA_SUCCESS : 0; + return true; + } + bool IwxSendCmd(IwxHostCmd& cmd) { (void)cmd; return true; } + + int IwxLookupCmdVer(uint8_t group, uint8_t cmd) { + // Match what AX211 firmware 89 advertises for the versions the MLME + // branches on. + if (group == IWX_DATA_PATH_GROUP && cmd == IWX_RLC_CONFIG_CMD) return 2; + if (group == IWX_DATA_PATH_GROUP && cmd == IWX_SCD_QUEUE_CONFIG_CMD) return 3; + return -1; + } + int IwxLookupNotifVer(uint8_t, uint8_t) { return 7; } + + bool IwxAbortScan() { return true; } + + bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) { + (void)staId; (void)tid; + ring.Qid = qid; + ring.Active = true; + ring.StageSlots = IWX_TX_STAGE_SLOTS; + printf("TXQ-UP %d\n", qid); + return true; + } + void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) { + (void)staId; (void)tid; + ring.Active = false; + printf("TXQ-DOWN\n"); + } + + // Capture what the driver wants to put on the air. + bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen, + const uint8_t* payload, uint32_t payloadLen, + bool encrypt, bool fixedRate) { + if (!ring.Active) { printf("TX-DROP queue-down\n"); return false; } + printf("TX enc=%d rate=%d ", encrypt ? 1 : 0, fixedRate ? 1 : 0); + for (uint32_t i = 0; i < hdrLen; i++) printf("%02x", hdr[i]); + printf(" "); + for (uint32_t i = 0; i < payloadLen; i++) printf("%02x", payload[i]); + printf("\n"); + return true; + } + + bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx, + bool pairwise, uint8_t cipher, const uint8_t* rsc) { + printf("KEY pairwise=%d idx=%u cipher=%u ", pairwise ? 1 : 0, keyIdx, cipher); + for (uint32_t i = 0; i < keyLen; i++) printf("%02x", key[i]); + printf(" "); + if (rsc) for (int i = 0; i < 6; i++) printf("%02x", rsc[i]); + printf("\n"); + return true; + } + + bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher, + uint32_t keyLen) { + printf("KEY-REMOVE pairwise=%d idx=%u cipher=%u len=%u\n", + pairwise ? 1 : 0, keyIdx, cipher, keyLen); + return true; + } + + // Sink normally provided by Wifi.cpp. + void WifiRxEthernet(const uint8_t* frame, uint32_t len) { + puthex("ETH", frame, len); + } +} + +// ============================================================================= +// Driver +// ============================================================================= + +static int unhex(const std::string& s, uint8_t* out) { + int n = 0; + for (size_t i = 0; i + 1 < s.size(); i += 2) { + unsigned v; sscanf(s.c_str() + i, "%2x", &v); out[n++] = (uint8_t)v; + } + return n; +} + +int main() { + // A firmware state good enough for the MLME: alive, one antenna, a MAC. + g_iwx.State = IwxFwState::Running; + g_iwx.Fw.PhyConfig = (1u << IWX_FW_PHY_CFG_TX_CHAIN_POS) + | (1u << IWX_FW_PHY_CFG_RX_CHAIN_POS); + g_iwx.Nvm.ValidTxAnt = 1; + g_iwx.Nvm.ValidRxAnt = 1; + + std::string line; + uint8_t buf[4096]; + + while (std::getline(std::cin, line)) { + if (line.rfind("MAC ", 0) == 0) { + unhex(line.substr(4), g_iwx.Nvm.HwAddr); + printf("DONE\n"); + } else if (line.rfind("CAPA ", 0) == 0) { + unsigned bit, on; + sscanf(line.c_str(), "CAPA %u %u", &bit, &on); + if (on) g_iwx.Fw.Capa[bit / 8] |= (uint8_t)(1 << (bit % 8)); + else g_iwx.Fw.Capa[bit / 8] &= (uint8_t)~(1 << (bit % 8)); + printf("DONE\n"); + } else if (line.rfind("CONNECT ", 0) == 0) { + // CONNECT + char bssid[64], ssid[64], pass[128], rsn[256]; + unsigned chan, is5; + sscanf(line.c_str(), "CONNECT %63s %u %u %63s %127s %255s", + bssid, &chan, &is5, ssid, pass, rsn); + uint8_t bs[6]; unhex(bssid, bs); + uint8_t ie[128]; int ieLen = 0; + if (strcmp(rsn, "-") != 0) ieLen = unhex(rsn, ie); + bool ok = IwxConnectStart(bs, (uint8_t)chan, is5 != 0, ssid, + strcmp(pass, "-") == 0 ? nullptr : pass, + ieLen ? ie : nullptr, (uint32_t)ieLen, + 100, 2); + printf("CONNECT-OK %d STATE %d\n", ok ? 1 : 0, IwxConnectState()); + } else if (line.rfind("RXMGMT ", 0) == 0) { + int n = unhex(line.substr(7), buf); + IwxConnectRxMgmt(buf, (uint32_t)n); + printf("DONE STATE %d\n", IwxConnectState()); + } else if (line.rfind("RXDATA ", 0) == 0) { + int n = unhex(line.substr(7), buf); + IwxConnectRxData(buf, (uint32_t)n); + printf("DONE STATE %d\n", IwxConnectState()); + } else if (line.rfind("TXETH ", 0) == 0) { + int n = unhex(line.substr(6), buf); + bool ok = IwxConnectSendEthernet(buf, (uint32_t)n); + printf("TXETH-OK %d\n", ok ? 1 : 0); + } else if (line.rfind("SERVICE", 0) == 0) { + IwxConnectService(); + printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0); + } else if (line.rfind("TICK ", 0) == 0) { + unsigned ms; sscanf(line.c_str(), "TICK %u", &ms); + Timekeeping::g_ms += ms; + printf("DONE\n"); + } else if (line.rfind("ABORT", 0) == 0) { + IwxConnectAbort(); + printf("DONE STATE %d\n", IwxConnectState()); + } else if (line.rfind("STATE", 0) == 0) { + printf("STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0); + } else if (line.rfind("QUIT", 0) == 0) { + break; + } + fflush(stdout); + } + return 0; +} diff --git a/tests/wifi/run.sh b/tests/wifi/run.sh new file mode 100755 index 0000000..8fb77fa --- /dev/null +++ b/tests/wifi/run.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# +# Host-side tests for the Wi-Fi crypto and the WPA supplicant. +# +# These compile the real kernel sources (Libraries/Crypto.cpp and +# Drivers/Net/Wifi/Wpa.cpp) for the host against the small shim in shim/, so +# what is exercised is the code that ships, not a copy of it. +# +# Requires: g++ with C++20, python3 with the `cryptography` package. +# +# Copyright (c) 2026 Daniel Hammer + +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$here/../.." && pwd)" +src="$root/kernel/src" +out="$(mktemp -d)" +trap 'rm -rf "$out"' EXIT + +CXX="${CXX:-g++}" +CXXFLAGS="-O1 -std=c++20 -Wall -I$here/shim -I$src" + +echo "== crypto primitives against published vectors ==" +$CXX $CXXFLAGS -o "$out/crypto_vectors" \ + "$here/crypto_vectors.cpp" "$src/Libraries/Crypto.cpp" +"$out/crypto_vectors" + +echo +echo "== supplicant against an independent authenticator ==" +$CXX $CXXFLAGS -o "$out/supplicant" \ + "$here/supplicant_harness.cpp" \ + "$src/Drivers/Net/Wifi/Wpa.cpp" "$src/Libraries/Crypto.cpp" + +python3 "$here/ap_handshake.py" "$out/supplicant" +python3 "$here/ap_rekey_and_rsn.py" "$out/supplicant" + +echo +echo "== MLME and data path against a simulated access point ==" +$CXX $CXXFLAGS -o "$out/mlme" \ + "$here/mlme_harness.cpp" \ + "$src/Drivers/Net/Wifi/IwxConnect.cpp" \ + "$src/Drivers/Net/Wifi/Wpa.cpp" "$src/Libraries/Crypto.cpp" + +python3 "$here/ap_mlme.py" "$out/mlme" + +echo +echo "All Wi-Fi host tests passed." diff --git a/tests/wifi/shim/CppLib/Spinlock.hpp b/tests/wifi/shim/CppLib/Spinlock.hpp new file mode 100644 index 0000000..df60a37 --- /dev/null +++ b/tests/wifi/shim/CppLib/Spinlock.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include +namespace kcp { + class Spinlock { + std::atomic_flag f{ATOMIC_FLAG_INIT}; + public: + void Acquire() { while (f.test_and_set(std::memory_order_acquire)) {} } + void Release() { f.clear(std::memory_order_release); } + }; + // The kernel's Mutex is the non-interrupt-disabling variant; on the host + // there is nothing to disable, so the two are the same here. + class Mutex { + std::atomic_flag f{ATOMIC_FLAG_INIT}; + public: + void Acquire() { while (f.test_and_set(std::memory_order_acquire)) {} } + void Release() { f.clear(std::memory_order_release); } + }; +} diff --git a/tests/wifi/shim/CppLib/Stream.hpp b/tests/wifi/shim/CppLib/Stream.hpp new file mode 100644 index 0000000..4842ce8 --- /dev/null +++ b/tests/wifi/shim/CppLib/Stream.hpp @@ -0,0 +1,2 @@ +#pragma once +namespace base { struct Manip {}; inline Manip hex, dec; } diff --git a/tests/wifi/shim/Libraries/Memory.hpp b/tests/wifi/shim/Libraries/Memory.hpp new file mode 100644 index 0000000..80b2a09 --- /dev/null +++ b/tests/wifi/shim/Libraries/Memory.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/tests/wifi/shim/Pci/Pci.hpp b/tests/wifi/shim/Pci/Pci.hpp new file mode 100644 index 0000000..ce9324f --- /dev/null +++ b/tests/wifi/shim/Pci/Pci.hpp @@ -0,0 +1,4 @@ +#pragma once +#include +// Only the type name is needed: the MLME never touches PCI. +namespace Pci { struct PciDevice { uint8_t Bus, Device, Function; }; } diff --git a/tests/wifi/shim/Terminal/Terminal.hpp b/tests/wifi/shim/Terminal/Terminal.hpp new file mode 100644 index 0000000..a510697 --- /dev/null +++ b/tests/wifi/shim/Terminal/Terminal.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include +namespace Kt { + enum KernelLogLevel { INFO, OK, WARNING, ERROR }; + class KernelLogStream { + public: + KernelLogStream(KernelLogLevel l, const char* c) { fprintf(stderr, " [%s] ", c); (void)l; } + ~KernelLogStream() { fprintf(stderr, "\n"); } + KernelLogStream& operator<<(const char* s) { fprintf(stderr, "%s", s); return *this; } + KernelLogStream& operator<<(uint64_t v) { fprintf(stderr, "%llu", (unsigned long long)v); return *this; } + KernelLogStream& operator<<(int v) { fprintf(stderr, "%d", v); return *this; } + }; +} diff --git a/tests/wifi/shim/Timekeeping/ApicTimer.hpp b/tests/wifi/shim/Timekeeping/ApicTimer.hpp new file mode 100644 index 0000000..2f8c66e --- /dev/null +++ b/tests/wifi/shim/Timekeeping/ApicTimer.hpp @@ -0,0 +1,3 @@ +#pragma once +#include +namespace Timekeeping { uint64_t GetMilliseconds(); } diff --git a/tests/wifi/supplicant_harness.cpp b/tests/wifi/supplicant_harness.cpp new file mode 100644 index 0000000..9a427fd --- /dev/null +++ b/tests/wifi/supplicant_harness.cpp @@ -0,0 +1,94 @@ +// Host harness: drives the real kernel supplicant through a 4-way handshake. +// Frames come in / go out as hex on stdio so an independent Python AP can be +// the oracle. +#include +#include +#include +#include +#include +#include "Drivers/Net/Wifi/Wpa.hpp" +#include "Drivers/Net/Wifi/Ieee80211.hpp" + +namespace Timekeeping { static uint64_t g_ms = 0; uint64_t GetMilliseconds() { return g_ms; } } + +using namespace Drivers::Net::Wifi; + +static void puthex(const char* tag, const uint8_t* p, size_t n) { + printf("%s ", tag); + for (size_t i = 0; i < n; i++) printf("%02x", p[i]); + printf("\n"); +} + +// --- hooks the supplicant calls back into --- +namespace Drivers::Net::Wifi { + bool WpaTxEapol(const uint8_t* body, uint32_t len) { + puthex("TX", body, len); + return true; + } + bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) { + printf("PTK-CIPHER %u\n", cipher); + puthex("PTK", tk, tkLen); + return true; + } + bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx, + uint8_t cipher, const uint8_t* rsc) { + printf("GTK-IDX %u\nGTK-CIPHER %u\n", keyIdx, cipher); + puthex("GTK", gtk, gtkLen); + puthex("GTK-RSC", rsc, 8); + return true; + } +} + +static int unhex(const std::string& s, uint8_t* out) { + int n = 0; + for (size_t i = 0; i + 1 < s.size(); i += 2) { + unsigned v; sscanf(s.c_str() + i, "%2x", &v); out[n++] = (uint8_t)v; + } + return n; +} + +int main() { + std::string line; + uint8_t buf[2048]; + + while (std::getline(std::cin, line)) { + if (line.rfind("START ", 0) == 0) { + // START + char own[64], bss[64], ssid[64], pass[128]; + unsigned akm, pc, gc; + sscanf(line.c_str(), "START %63s %63s %63s %127s %u %u %u", + own, bss, ssid, pass, &akm, &pc, &gc); + WpaConfig cfg = {}; + unhex(own, cfg.OwnMac); + unhex(bss, cfg.Bssid); + cfg.SsidLen = (uint8_t)strlen(ssid); + memcpy(cfg.Ssid, ssid, cfg.SsidLen); + cfg.PassLen = (uint8_t)strlen(pass); + memcpy(cfg.Passphrase, pass, cfg.PassLen); + cfg.Akm = (uint8_t)akm; + cfg.PairwiseCipher = (uint8_t)pc; + cfg.GroupCipher = (uint8_t)gc; + printf("START-OK %d\n", WpaStart(cfg) ? 1 : 0); + uint8_t ie[64]; + uint32_t ieLen = WpaBuildRsnIe(ie, sizeof(ie)); + puthex("RSNIE", ie, ieLen); + } else if (line.rfind("RX ", 0) == 0) { + int n = unhex(line.substr(3), buf); + bool consumed = WpaOnEapol(buf, (uint32_t)n); + printf("RX-OK %d STATE %d\n", consumed ? 1 : 0, (int)WpaGetState()); + } else if (line.rfind("PARSE ", 0) == 0) { + int n = unhex(line.substr(6), buf); + WpaConfig cfg = {}; + bool ok = WpaParseApRsn(buf, (uint32_t)n, cfg); + printf("PARSE-OK %d AKM %u PCIPHER %u GCIPHER %u\n", ok ? 1 : 0, + (unsigned)cfg.Akm, (unsigned)cfg.PairwiseCipher, + (unsigned)cfg.GroupCipher); + } else if (line.rfind("STATE", 0) == 0) { + printf("STATE %d\n", (int)WpaGetState()); + } else if (line.rfind("QUIT", 0) == 0) { + break; + } + fflush(stdout); + } + return 0; +}