feat: wi-fi - join WPA2/WPA3-PSK networks and carry traffic like ethernet

This commit is contained in:
2026-08-06 19:44:35 +02:00
parent a01e63c717
commit bbe1df62fd
40 changed files with 5878 additions and 272 deletions
+234
View File
@@ -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 <ssid> <passphrase> 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.
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 53 #define MONTAUK_BUILD_NUMBER 69
+6
View File
@@ -13,6 +13,7 @@
#include <Drivers/USB/Bluetooth/Bluetooth.hpp> #include <Drivers/USB/Bluetooth/Bluetooth.hpp>
#include <Drivers/Net/E1000.hpp> #include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp> #include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp>
#include <Drivers/Graphics/IntelGPU.hpp> #include <Drivers/Graphics/IntelGPU.hpp>
#include <Drivers/Storage/Ahci.hpp> #include <Drivers/Storage/Ahci.hpp>
#include <Drivers/Audio/IntelHda.hpp> #include <Drivers/Audio/IntelHda.hpp>
@@ -127,6 +128,11 @@ namespace montauk::abi {
if (Drivers::Net::E1000E::IsInitialized()) { if (Drivers::Net::E1000E::IsInitialized()) {
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)"); 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) // Display (category 6)
if (Drivers::Graphics::IntelGPU::IsInitialized()) { if (Drivers::Graphics::IntelGPU::IsInitialized()) {
+21 -10
View File
@@ -15,8 +15,10 @@
#include <Net/Ipv4.hpp> #include <Net/Ipv4.hpp>
#include <Net/Socket.hpp> #include <Net/Socket.hpp>
#include <Net/NetConfig.hpp> #include <Net/NetConfig.hpp>
#include <Net/NetIf.hpp>
#include <Drivers/Net/E1000.hpp> #include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp> #include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp>
#include "Syscall.hpp" #include "Syscall.hpp"
@@ -96,12 +98,9 @@ namespace montauk::abi {
out->subnetMask = Net::GetSubnetMask(); out->subnetMask = Net::GetSubnetMask();
out->gateway = Net::GetGateway(); out->gateway = Net::GetGateway();
const uint8_t* mac = nullptr; // Whichever interface is carrying traffic; on a Wi-Fi link this is the
if (Drivers::Net::E1000::IsInitialized()) { // adapter's MAC, not an idle Ethernet port's.
mac = Drivers::Net::E1000::GetMacAddress(); const uint8_t* mac = ::Net::NetIf::ActiveMac();
} else if (Drivers::Net::E1000E::IsInitialized()) {
mac = Drivers::Net::E1000E::GetMacAddress();
}
if (mac) { if (mac) {
for (int i = 0; i < 6; i++) out->macAddress[i] = mac[i]; for (int i = 0; i < 6; i++) out->macAddress[i] = mac[i];
} else { } else {
@@ -122,6 +121,9 @@ namespace montauk::abi {
dst[i] = '\0'; 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) { static void Sys_NetStatus(NetStatus* out) {
if (out == nullptr) return; if (out == nullptr) return;
@@ -133,15 +135,24 @@ namespace montauk::abi {
out->txPackets = 0; out->txPackets = 0;
CopyNetStatusDriver(out->driver, "No adapter"); CopyNetStatusDriver(out->driver, "No adapter");
if (Drivers::Net::E1000::IsInitialized()) { const auto* iface = ::Net::NetIf::Active();
if (iface == nullptr) return;
out->initialized = 1; out->initialized = 1;
out->linkUp = Drivers::Net::E1000::IsLinkUp() ? 1 : 0; 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->rxPackets = Drivers::Net::E1000::GetRxPacketCount();
out->txPackets = Drivers::Net::E1000::GetTxPacketCount(); out->txPackets = Drivers::Net::E1000::GetTxPacketCount();
CopyNetStatusDriver(out->driver, "Intel 82540EM"); CopyNetStatusDriver(out->driver, "Intel 82540EM");
} else if (Drivers::Net::E1000E::IsInitialized()) { } 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->polling = Drivers::Net::E1000E::RequiresPolling() ? 1 : 0;
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount(); out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount(); out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
+26 -1
View File
@@ -671,6 +671,26 @@ namespace montauk::abi {
uint16_t beaconInterval; // TU 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). // Adapter status (returned by SYS_WIFI_INFO).
struct WifiInfo { struct WifiInfo {
uint8_t mac[6]; uint8_t mac[6];
@@ -682,7 +702,12 @@ namespace montauk::abi {
char fwVersion[32]; char fwVersion[32];
uint64_t rxPackets; uint64_t rxPackets;
uint32_t fwErrors; 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 { struct ThermalInfo {
+149
View File
@@ -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 <cstdint>
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:<type>)
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;
}
}
+105 -8
View File
@@ -17,6 +17,7 @@
#include <cstdint> #include <cstdint>
#include <Pci/Pci.hpp> #include <Pci/Pci.hpp>
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
#include <atomic>
#include "IwxReg.hpp" #include "IwxReg.hpp"
namespace Drivers::Net::Wifi { namespace Drivers::Net::Wifi {
@@ -85,6 +86,13 @@ namespace Drivers::Net::Wifi {
// Rings // 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 { struct IwxTxRing {
int Qid = 0; int Qid = 0;
IwxDma Desc; // IwxTfhTfd[IWX_TX_RING_COUNT] 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 Cur = 0; // ring slot (0..count-1)
uint32_t CurHw = 0; // hardware index (0..65535) uint32_t CurHw = 0; // hardware index (0..65535)
uint32_t Queued = 0; 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 { struct IwxRxRing {
@@ -181,7 +196,11 @@ namespace Drivers::Net::Wifi {
IwxRxRing RxQ; IwxRxRing RxQ;
IwxTxRing CmdQ; // queue 0: host commands 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) // ALIVE / init-complete tracking (set from notification processing)
volatile bool AliveIntr = false; volatile bool AliveIntr = false;
@@ -194,18 +213,29 @@ namespace Drivers::Net::Wifi {
uint32_t UmacErrorTable = 0; uint32_t UmacErrorTable = 0;
uint32_t LmacErrorTable = 0; uint32_t LmacErrorTable = 0;
uint32_t LastCmdId = 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 bool LtrEnabled = false; // PCIe LTR capability advertised
// Synchronous-command bookkeeping (commands are fully serialized) // 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 CmdDone = false;
volatile bool CmdWantResp = false; volatile bool CmdWantResp = false;
uint8_t CmdRespBuf[4096]; uint8_t CmdRespBuf[4096];
volatile uint32_t CmdRespLen = 0; volatile uint32_t CmdRespLen = 0;
uint32_t CmdIdx = 0; // ring slot of in-flight command uint32_t CmdIdx = 0; // ring slot of in-flight command
// Reentrancy guard for ProcessEvents // Reentrancy guard for ProcessEvents. Interrupts stay enabled while
volatile bool InProcessEvents = false; // 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; IwxNvmData Nvm;
IwxChannel Channels[IWX_MAX_CHANNELS_TRACKED]; IwxChannel Channels[IWX_MAX_CHANNELS_TRACKED];
@@ -215,8 +245,19 @@ namespace Drivers::Net::Wifi {
volatile bool ScanActive = false; volatile bool ScanActive = false;
volatile bool ScanCompleted = 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 // Statistics/diagnostics
uint64_t RxPackets = 0; uint64_t RxPackets = 0;
uint64_t TxPackets = 0;
uint64_t TxFailures = 0;
uint64_t RxDataPackets = 0;
uint64_t FwErrors = 0; uint64_t FwErrors = 0;
}; };
@@ -271,8 +312,28 @@ namespace Drivers::Net::Wifi {
bool IwxCheckRfKill(); bool IwxCheckRfKill();
// TX queue management (used by connect path) // TX queue management (used by the connect path). On the new data-path
bool IwxEnableTxq(int staId, int qid, int tid); // 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) // 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, void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
int8_t rssiDbm); 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, 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(); 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); 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). // Apply state changes the RX path queued (it cannot send commands itself).
void IwxConnectService(); void IwxConnectService();
int IwxConnectState(); 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);
} }
File diff suppressed because it is too large Load Diff
+37 -5
View File
@@ -7,6 +7,7 @@
*/ */
#include "Iwx.hpp" #include "Iwx.hpp"
#include "Ieee80211.hpp"
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -629,12 +630,23 @@ namespace Drivers::Net::Wifi {
uint32_t offset = (uint32_t)(frame - (const uint8_t*)pkt); uint32_t offset = (uint32_t)(frame - (const uint8_t*)pkt);
if (offset + len > bufLen) return; if (offset + len > bufLen) return;
// The firmware pads the header to a 4-byte boundary when the flag is // The firmware pads a MAC header that is not a multiple of four bytes
// set; the payload then starts two bytes later. // (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 (desc->mac_flags2 & IWX_RX_MPDU_MFLG2_PAD) {
if (len < 2) return; uint32_t hdrLen = 24;
frame += 2; if ((frame[0] & IEEE80211_FC0_TYPE_MASK) == IEEE80211_FC0_TYPE_DATA
if (offset + 2 + len > bufLen) return; && (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; 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; int rssi = energyA > energyB ? energyA : energyB;
if (rssi < -128) rssi = -128; 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); WifiRxMgmtFrame(frame, len, desc->v3.channel, (int8_t)rssi);
} }
+376 -6
View File
@@ -567,7 +567,10 @@ inline uint32_t IwxRxPacketPayloadLen(const IwxRxPacket* pkt) {
return IwxRxPacketLen(pkt) - sizeof(IwxCmdHeader); 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_CMD_QUEUE = 0;
constexpr int IWX_DQA_MGMT_QUEUE = 1; constexpr int IWX_DQA_MGMT_QUEUE = 1;
constexpr int IWX_MGMT_TID = 15; 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_CRC_OK = 1u << 0;
constexpr uint32_t IWX_RX_MPDU_RES_STATUS_OVERRUN_OK = 1u << 1; 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_PAD = 0x20;
constexpr uint8_t IWX_RX_MPDU_MFLG2_AMSDU = 0x40; 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_24G_INDEX = 0;
constexpr uint32_t IWX_LMAC_5G_INDEX = 1; constexpr uint32_t IWX_LMAC_5G_INDEX = 1;
// PHY context // PHY context.
struct IwxFwChannelInfoV1 { //
// 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 band;
uint8_t channel; uint8_t channel;
uint8_t width; uint8_t width;
uint8_t ctrl_pos; uint8_t ctrl_pos;
} __attribute__((packed)); } __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_CHANNEL_MODE20 = 0x0;
constexpr uint8_t IWX_PHY_VHT_CTRL_POS_1_BELOW = 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_VALID_POS = 1;
constexpr uint32_t IWX_PHY_RX_CHAIN_CNT_POS = 10; constexpr uint32_t IWX_PHY_RX_CHAIN_CNT_POS = 10;
constexpr uint32_t IWX_PHY_RX_CHAIN_MIMO_CNT_POS = 12; 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 id_and_color;
uint32_t action; uint32_t action;
IwxFwChannelInfoV1 ci; IwxFwChannelInfoV1 ci;
@@ -1024,11 +1066,15 @@ struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB)
uint32_t reserved; uint32_t reserved;
} __attribute__((packed)); } __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 // MAC context
constexpr uint32_t IWX_FW_MAC_TYPE_BSS_STA = 5; constexpr uint32_t IWX_FW_MAC_TYPE_BSS_STA = 5;
constexpr uint32_t IWX_TSF_ID_A = 0; constexpr uint32_t IWX_TSF_ID_A = 0;
constexpr uint32_t IWX_AC_NUM = 4; 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_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_IN_CONTROL_AND_MGMT = 1u << 1;
constexpr uint32_t IWX_MAC_FILTER_ACCEPT_GRP = 1u << 2; constexpr uint32_t IWX_MAC_FILTER_ACCEPT_GRP = 1u << 2;
constexpr uint32_t IWX_MAC_FILTER_IN_BEACON = 1u << 6; constexpr uint32_t IWX_MAC_FILTER_IN_BEACON = 1u << 6;
@@ -1055,6 +1101,16 @@ struct IwxMacDataSta {
uint32_t assoc_beacon_arrive_time; uint32_t assoc_beacon_arrive_time;
} __attribute__((packed)); } __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) struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta)
uint32_t id_and_color; uint32_t id_and_color;
uint32_t action; uint32_t action;
@@ -1072,9 +1128,15 @@ struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta)
uint32_t filter_flags; uint32_t filter_flags;
uint32_t qos_flags; uint32_t qos_flags;
IwxAcQos ac[IWX_AC_NUM + 1]; IwxAcQos ac[IWX_AC_NUM + 1];
union {
IwxMacDataSta sta; IwxMacDataSta sta;
IwxMacDataP2pSta p2p_sta; // the largest member: sizes the command
} u;
} __attribute__((packed)); } __attribute__((packed));
static_assert(sizeof(IwxMacCtxCmd) == 148,
"MAC context command must stay 148 bytes (union sized by p2p_sta)");
// Binding context // Binding context
constexpr uint32_t IWX_MAX_MACS_IN_BINDING = 3; constexpr uint32_t IWX_MAX_MACS_IN_BINDING = 3;
struct IwxBindingCmd { struct IwxBindingCmd {
@@ -1178,8 +1240,9 @@ struct IwxRlcConfigCmd {
uint8_t reserved[3]; uint8_t reserved[3];
} __attribute__((packed)); } __attribute__((packed));
// Session protection // Session protection. ASSOC is the first value of
constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 1; // 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 { struct IwxSessionProtCmd {
uint32_t id_and_color; uint32_t id_and_color;
uint32_t action; uint32_t action;
@@ -1196,6 +1259,313 @@ struct IwxSessionProtNotif {
uint32_t conf_id; uint32_t conf_id;
} __attribute__((packed)); } __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) // Firmware error log (read from device SRAM after an assert)
// ============================================================================= // =============================================================================
+367 -12
View File
@@ -17,6 +17,7 @@
*/ */
#include "Iwx.hpp" #include "Iwx.hpp"
#include "Ieee80211.hpp"
#include <Pci/Pci.hpp> #include <Pci/Pci.hpp>
#include <Memory/HHDM.hpp> #include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp> #include <Memory/Paging.hpp>
@@ -459,11 +460,15 @@ namespace Drivers::Net::Wifi {
asm volatile("" ::: "memory"); 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.Qid = qid;
ring.Cur = 0; ring.Cur = 0;
ring.CurHw = 0; ring.CurHw = 0;
ring.Queued = 0; ring.Queued = 0;
ring.StageSlots = 0;
ring.Active = false;
if (!IwxDmaAlloc(ring.Desc, sizeof(IwxTfhTfd) * IWX_TX_RING_COUNT)) if (!IwxDmaAlloc(ring.Desc, sizeof(IwxTfhTfd) * IWX_TX_RING_COUNT))
return false; return false;
@@ -476,6 +481,15 @@ namespace Drivers::Net::Wifi {
// are staged in this page instead of the per-slot command area. // are staged in this page instead of the per-slot command area.
if (!IwxDmaAlloc(ring.Bounce, 4096)) if (!IwxDmaAlloc(ring.Bounce, 4096))
return false; 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; return true;
} }
@@ -484,6 +498,14 @@ namespace Drivers::Net::Wifi {
IwxDmaFree(ring.BcTbl); IwxDmaFree(ring.BcTbl);
IwxDmaFree(ring.Cmd); IwxDmaFree(ring.Cmd);
IwxDmaFree(ring.Bounce); 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) { static void IwxResetTxRing(IwxTxRing& ring) {
@@ -846,6 +868,9 @@ namespace Drivers::Net::Wifi {
return -1; return -1;
} }
void IwxDumpFwError();
static uint32_t g_cmdTimeouts = 0; // consecutive unanswered commands
bool IwxSendCmd(IwxHostCmd& hcmd) { bool IwxSendCmd(IwxHostCmd& hcmd) {
if (g_iwx.State == IwxFwState::Error) return false; if (g_iwx.State == IwxFwState::Error) return false;
@@ -909,6 +934,12 @@ namespace Drivers::Net::Wifi {
g_iwx.CmdDone = false; g_iwx.CmdDone = false;
g_iwx.LastCmdId = code; 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.CmdWantResp = hcmd.WantResp;
g_iwx.CmdRespLen = 0; g_iwx.CmdRespLen = 0;
g_iwx.CmdIdx = idx; g_iwx.CmdIdx = idx;
@@ -921,18 +952,47 @@ namespace Drivers::Net::Wifi {
// Wait for the firmware's response/ack. Commands are serialized by // Wait for the firmware's response/ack. Commands are serialized by
// CmdLock, so exactly one can be in flight and the completion is // CmdLock, so exactly one can be in flight and the completion is
// unambiguous. // 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 ok = false;
bool died = false;
uint64_t start = Timekeeping::GetMilliseconds(); uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < 1000) { for (uint32_t spins = 0; spins < MAX_SPINS; spins++) {
IwxProcessEvents(); IwxProcessEvents();
if (g_iwx.CmdDone) { ok = true; break; } if (g_iwx.CmdDone) { ok = true; break; }
if (g_iwx.State == IwxFwState::Error) { died = true; break; }
if (Timekeeping::GetMilliseconds() - start >= 1000) break;
IwxDelayUs(100); IwxDelayUs(100);
} }
if (!ok) { if (!ok) {
KernelLogStream(WARNING, "WiFi") << "Command 0x" << base::hex 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 (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; g_iwx.CmdWantResp = false;
@@ -969,10 +1029,10 @@ namespace Drivers::Net::Wifi {
// TX queue configuration (used by the connect path) // TX queue configuration (used by the connect path)
// ========================================================================= // =========================================================================
bool IwxEnableTxq(int staId, int qid, int tid) { bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) {
IwxTxRing& ring = g_iwx.MgmtQ;
IwxResetTxRing(ring); IwxResetTxRing(ring);
ring.Qid = qid; ring.Qid = qid;
ring.Active = false;
int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD); int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD);
@@ -1016,14 +1076,280 @@ namespace Drivers::Net::Wifi {
return false; return false;
auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf; auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf;
auto* resp = (IwxTxQueueCfgRsp*)pkt->data; 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) { if (resp->queue_number != qid) {
KernelLogStream(WARNING, "WiFi") << "Firmware assigned queue " KernelLogStream(INFO, "WiFi") << "Firmware assigned TX queue "
<< (uint64_t)resp->queue_number << ", expected " << (uint64_t)qid; << (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; 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; 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 // RX / notification processing
// ========================================================================= // =========================================================================
@@ -1083,7 +1409,24 @@ namespace Drivers::Net::Wifi {
// between them they pin down which host command the firmware rejected. // between them they pin down which host command the firmware rejected.
void IwxDumpFwError() { void IwxDumpFwError() {
KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert; last command sent: 0x" 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; uint32_t base_ = g_iwx.UmacErrorTable;
if (base_ < 0x400000) { if (base_ < 0x400000) {
@@ -1141,6 +1484,16 @@ namespace Drivers::Net::Wifi {
case IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_PNVM_INIT_COMPLETE): case IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_PNVM_INIT_COMPLETE):
g_iwx.InitComplete |= 0x2; g_iwx.InitComplete |= 0x2;
break; 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: { case IWX_REPLY_ERROR: {
if (IwxRxPacketPayloadLen(pkt) >= 8) { if (IwxRxPacketPayloadLen(pkt) >= 8) {
uint32_t errType = *(const uint32_t*)pkt->data; uint32_t errType = *(const uint32_t*)pkt->data;
@@ -1208,8 +1561,9 @@ namespace Drivers::Net::Wifi {
void IwxProcessEvents() { void IwxProcessEvents() {
if (!g_iwx.Mmio) return; if (!g_iwx.Mmio) return;
if (g_iwx.InProcessEvents) return; // never nest // Never nest. With interrupts enabled during command waits this is a
g_iwx.InProcessEvents = true; // 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) { if (g_msix) {
uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD); uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD);
@@ -1276,7 +1630,7 @@ namespace Drivers::Net::Wifi {
IwxNotifIntr(); IwxNotifIntr();
g_iwx.WorkPending = false; 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) || !IwxDmaAlloc(g_iwx.PrphInfo, 4096)
|| !IwxAllocRxRing() || !IwxAllocRxRing()
|| !IwxAllocTxRing(g_iwx.CmdQ, IWX_DQA_CMD_QUEUE) || !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"; KernelLogStream(ERROR, "WiFi") << "Could not allocate device DMA memory";
IwxFreeRxRing(); IwxFreeRxRing();
IwxFreeTxRing(g_iwx.CmdQ); IwxFreeTxRing(g_iwx.CmdQ);
+157 -16
View File
@@ -13,6 +13,7 @@
#include "Wifi.hpp" #include "Wifi.hpp"
#include "Iwx.hpp" #include "Iwx.hpp"
#include "Wpa.hpp"
#include <Fs/Vfs.hpp> #include <Fs/Vfs.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
@@ -20,6 +21,7 @@
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/ApicTimer.hpp>
#include <Hal/SmpBoot.hpp> #include <Hal/SmpBoot.hpp>
#include <Sched/Scheduler.hpp>
#include <atomic> #include <atomic>
using namespace Kt; using namespace Kt;
@@ -36,6 +38,10 @@ namespace Drivers::Net::Wifi {
static constexpr int MAX_SCAN_RESULTS = 64; 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 { struct ScanEntry {
uint8_t Bssid[6]; uint8_t Bssid[6];
char Ssid[33]; char Ssid[33];
@@ -45,6 +51,9 @@ namespace Drivers::Net::Wifi {
uint8_t Band; // 0 = 2.4 GHz, 1 = 5 GHz uint8_t Band; // 0 = 2.4 GHz, 1 = 5 GHz
uint8_t Security; // WifiSecurity value uint8_t Security; // WifiSecurity value
uint16_t BeaconInterval; uint16_t BeaconInterval;
uint8_t DtimPeriod;
uint8_t RsnIe[MAX_RSN_IE]; // element body, without id/len
uint8_t RsnIeLen;
bool Used; bool Used;
}; };
@@ -71,6 +80,7 @@ namespace Drivers::Net::Wifi {
// Element IDs used here. // Element IDs used here.
static constexpr uint8_t ELEMID_SSID = 0; static constexpr uint8_t ELEMID_SSID = 0;
static constexpr uint8_t ELEMID_DSPARMS = 3; 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_RSN = 48;
static constexpr uint8_t ELEMID_VENDOR = 221; static constexpr uint8_t ELEMID_VENDOR = 221;
@@ -108,6 +118,9 @@ namespace Drivers::Net::Wifi {
uint8_t Channel = 0; uint8_t Channel = 0;
uint8_t Security = WIFI_SEC_OPEN; uint8_t Security = WIFI_SEC_OPEN;
uint16_t BeaconInterval = 0; 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) { static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
@@ -139,9 +152,18 @@ namespace Drivers::Net::Wifi {
case ELEMID_DSPARMS: case ELEMID_DSPARMS:
if (ielen >= 1) out->Channel = body[0]; if (ielen >= 1) out->Channel = body[0];
break; 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: case ELEMID_RSN:
haveRsn = true; haveRsn = true;
out->Security = ClassifyRsn(body, ielen); 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; break;
case ELEMID_VENDOR: case ELEMID_VENDOR:
// WPA1: Microsoft OUI 00:50:F2, type 1. // WPA1: Microsoft OUI 00:50:F2, type 1.
@@ -220,6 +242,12 @@ namespace Drivers::Net::Wifi {
slot->Band = ch > 14 ? 1 : 0; slot->Band = ch > 14 ? 1 : 0;
slot->Security = pb.Security; slot->Security = pb.Security;
slot->BeaconInterval = pb.BeaconInterval; 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(); g_resultLock.Release();
} }
@@ -371,8 +399,18 @@ namespace Drivers::Net::Wifi {
| (g_iwx.Nvm.Sku52GHz ? 2 : 0)); | (g_iwx.Nvm.Sku52GHz ? 2 : 0));
out->channels = (uint16_t)g_iwx.ChannelCount; out->channels = (uint16_t)g_iwx.ChannelCount;
out->rxPackets = g_iwx.RxPackets; out->rxPackets = g_iwx.RxPackets;
out->txPackets = g_iwx.TxPackets;
out->fwErrors = (uint32_t)g_iwx.FwErrors; out->fwErrors = (uint32_t)g_iwx.FwErrors;
out->connState = (uint32_t)IwxConnectState(); 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; int i = 0;
for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i]; for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i];
@@ -380,17 +418,74 @@ namespace Drivers::Net::Wifi {
return 0; 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) { 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 // 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 bssid[6];
uint8_t channel = 0; uint8_t channel = 0;
bool is5 = false; bool is5 = false;
uint8_t security = WIFI_SEC_OPEN; 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; bool found = false;
// 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);
}
g_resultLock.Acquire(); g_resultLock.Acquire();
for (int i = 0; i < MAX_SCAN_RESULTS; i++) { for (int i = 0; i < MAX_SCAN_RESULTS; i++) {
if (!g_results[i].Used) continue; if (!g_results[i].Used) continue;
@@ -402,33 +497,57 @@ namespace Drivers::Net::Wifi {
if (a == '\0') break; if (a == '\0') break;
} }
if (!match) continue; if (!match) continue;
if (found && e.Rssi <= bestRssi) continue;
memcpy(bssid, e.Bssid, 6); memcpy(bssid, e.Bssid, 6);
channel = e.Channel; channel = e.Channel;
is5 = e.Band == 1; is5 = e.Band == 1;
security = e.Security; security = e.Security;
rsnIeLen = e.RsnIeLen;
if (rsnIeLen) memcpy(rsnIe, e.RsnIe, rsnIeLen);
beaconInterval = e.BeaconInterval;
dtimPeriod = e.DtimPeriod;
bestRssi = e.Rssi;
found = true; found = true;
break;
} }
g_resultLock.Release(); g_resultLock.Release();
if (!found) {
KernelLogStream(WARNING, "WiFi")
<< "Network not in scan results; run a scan first";
return -1;
} }
// Encryption is checked before the passphrase: the WPA2/WPA3 key if (!found) return WIFI_ERR_NOT_FOUND;
// exchange (PMK derivation, EAPOL 4-way, HW key install) is not
// implemented at all, so a passphrase would not help and reporting bool havePassword = password && password[0];
// "needs a passphrase" would be misleading.
// 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")
<< "WEP and the original WPA use ciphers this driver does not implement";
return WIFI_ERR_UNSUPPORTED;
}
if (security != WIFI_SEC_OPEN) { if (security != WIFI_SEC_OPEN) {
if (!rsnIeLen) {
KernelLogStream(WARNING, "WiFi") KernelLogStream(WARNING, "WiFi")
<< "Encrypted networks are not supported yet (open only)"; << "The beacon for this network carried no RSN element";
return -2; 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() { int Disconnect() {
@@ -436,4 +555,26 @@ namespace Drivers::Net::Wifi {
IwxConnectAbort(); IwxConnectAbort();
return 0; 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);
}
} }
+18 -3
View File
@@ -32,9 +32,24 @@ namespace Drivers::Net::Wifi {
// Fill in adapter/firmware status. // Fill in adapter/firmware status.
int GetInfo(montauk::abi::WifiInfo* out); int GetInfo(montauk::abi::WifiInfo* out);
// Association groundwork. Returns 0 when the firmware contexts came up, // Join a network. Blocks (pumping firmware events) until the link is up
// negative on failure. See IwxConnect.cpp: the 802.11 handshake itself is // or the attempt fails. Returns 0 on success, or a negative WIFI_ERR_*
// not implemented yet, so this cannot establish a usable link. // value describing why it could not connect.
int Connect(const char* ssid, const char* password); int Connect(const char* ssid, const char* password);
int Disconnect(); 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);
} }
+830
View File
@@ -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 <Libraries/Crypto.hpp>
#include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp>
#include <Timekeeping/ApicTimer.hpp>
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, &gtk, &gtkLen, &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, &gtk, &gtkLen, &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);
}
}
}
+82
View File
@@ -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 <cstdint>
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);
}
+700
View File
@@ -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 <Libraries/Memory.hpp>
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));
}
}
+132
View File
@@ -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 <cstdint>
#include <cstddef>
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);
}
+5 -3
View File
@@ -15,16 +15,18 @@
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
#include <Timekeeping/ApicTimer.hpp> #include <Timekeeping/ApicTimer.hpp>
#include <Net/NetIf.hpp>
#include <CppLib/Spinlock.hpp> #include <CppLib/Spinlock.hpp>
using namespace Kt; using namespace Kt;
namespace Net::Arp { 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() { static const uint8_t* GetActiveNicMac() {
if (Drivers::Net::E1000::IsInitialized()) return NetIf::ActiveMac();
return Drivers::Net::E1000::GetMacAddress();
return Drivers::Net::E1000E::GetMacAddress();
} }
// ARP cache entry // ARP cache entry
+9 -8
View File
@@ -8,8 +8,7 @@
#include <Net/ByteOrder.hpp> #include <Net/ByteOrder.hpp>
#include <Net/Arp.hpp> #include <Net/Arp.hpp>
#include <Net/Ipv4.hpp> #include <Net/Ipv4.hpp>
#include <Drivers/Net/E1000.hpp> #include <Net/NetIf.hpp>
#include <Drivers/Net/E1000E.hpp>
#include <Libraries/Memory.hpp> #include <Libraries/Memory.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -18,22 +17,24 @@ using namespace Kt;
namespace Net::Ethernet { 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() { static const uint8_t* GetActiveNicMac() {
if (Drivers::Net::E1000::IsInitialized()) return NetIf::ActiveMac();
return Drivers::Net::E1000::GetMacAddress();
return Drivers::Net::E1000E::GetMacAddress();
} }
static bool ActiveNicSend(const uint8_t* data, uint16_t length) { static bool ActiveNicSend(const uint8_t* data, uint16_t length) {
if (Drivers::Net::E1000::IsInitialized()) return NetIf::ActiveSend(data, length);
return Drivers::Net::E1000::SendPacket(data, length);
return Drivers::Net::E1000E::SendPacket(data, length);
} }
void Initialize() { void Initialize() {
KernelLogStream(OK, "Net") << "Ethernet layer initialized"; 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) { 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) { if (payload == nullptr || payloadLen == 0 || payloadLen > MAX_PAYLOAD_SIZE) {
return false; return false;
+4 -1
View File
@@ -30,7 +30,10 @@ namespace Net::Ethernet {
// Send an Ethernet frame with the given EtherType and payload // 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); 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); void OnFrameReceived(const uint8_t* data, uint16_t length);
// MAC address of the interface currently carrying traffic
const uint8_t* GetMacAddress();
} }
+58 -13
View File
@@ -1,11 +1,12 @@
/* /*
* Net.cpp * Net.cpp
* Network stack initialization * Network stack initialization
* Copyright (c) 2025 Daniel Hammer * Copyright (c) 2025-2026 Daniel Hammer
*/ */
#include "Net.hpp" #include "Net.hpp"
#include <Net/Ethernet.hpp> #include <Net/Ethernet.hpp>
#include <Net/NetIf.hpp>
#include <Net/Arp.hpp> #include <Net/Arp.hpp>
#include <Net/Ipv4.hpp> #include <Net/Ipv4.hpp>
#include <Net/Icmp.hpp> #include <Net/Icmp.hpp>
@@ -15,6 +16,7 @@
#include <Net/NetConfig.hpp> #include <Net/NetConfig.hpp>
#include <Drivers/Net/E1000.hpp> #include <Drivers/Net/E1000.hpp>
#include <Drivers/Net/E1000E.hpp> #include <Drivers/Net/E1000E.hpp>
#include <Drivers/Net/Wifi/Wifi.hpp>
#include <Terminal/Terminal.hpp> #include <Terminal/Terminal.hpp>
#include <CppLib/Stream.hpp> #include <CppLib/Stream.hpp>
@@ -22,9 +24,57 @@ using namespace Kt;
namespace Net { 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() { void Initialize() {
if (!Drivers::Net::E1000::IsInitialized() && !Drivers::Net::E1000E::IsInitialized()) { RegisterInterfaces();
KernelLogStream(WARNING, "Net") << "No NIC initialized, skipping network stack";
if (NetIf::Count() == 0) {
KernelLogStream(WARNING, "Net")
<< "No network interface found, skipping network stack";
return; return;
} }
@@ -37,17 +87,12 @@ namespace Net {
Tcp::Initialize(); Tcp::Initialize();
Socket::Initialize(); Socket::Initialize();
// Hook the active NIC's RX to our Ethernet dispatcher // Announce ourselves, but only if something is actually carrying
if (Drivers::Net::E1000::IsInitialized()) { // traffic: a Wi-Fi-only machine has no link until it joins a network.
Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived); if (NetIf::AnyLinkUp()) Arp::SendRequest(GetIpAddress());
} else {
Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived);
}
// Send a gratuitous ARP to announce ourselves on the network KernelLogStream(OK, "Net") << "Network stack initialized with "
Arp::SendRequest(GetIpAddress()); << (uint64_t)NetIf::Count() << " interface(s)";
KernelLogStream(OK, "Net") << "Network stack initialized";
} }
} }
+59
View File
@@ -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;
}
}
+50
View File
@@ -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 <cstdint>
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();
}
+26 -1
View File
@@ -596,6 +596,26 @@ namespace montauk::abi {
uint16_t beaconInterval; // TU 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). // Adapter status (returned by SYS_WIFI_INFO).
struct WifiInfo { struct WifiInfo {
uint8_t mac[6]; uint8_t mac[6];
@@ -607,7 +627,12 @@ namespace montauk::abi {
char fwVersion[32]; char fwVersion[32];
uint64_t rxPackets; uint64_t rxPackets;
uint32_t fwErrors; 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 { struct ThermalInfo {
+99 -16
View File
@@ -7,8 +7,10 @@
* wifi scan [seconds] scan for a specific duration (default 5, max 20) * wifi scan [seconds] scan for a specific duration (default 5, max 20)
* wifi info adapter, firmware and radio status * wifi info adapter, firmware and radio status
* wifi debug info plus raw counters, repeated scan detail * wifi debug info plus raw counters, repeated scan detail
* wifi connect <ssid> bring up the firmware contexts for an open network * wifi connect <ssid> [passphrase]
* wifi disconnect tear those contexts back down * join a network (WPA2/WPA3-PSK or open)
* wifi status the network currently joined
* wifi disconnect leave it
* *
* Copyright (c) 2026 Daniel Hammer * 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, // A rough signal-quality bar from the RSSI: -50 dBm and up is excellent,
// -90 dBm and below is unusable. // -90 dBm and below is unusable.
static void put_signal_bar(int8_t rssi) { 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(" channels : "); put_u64(info.channels);
print(" usable after regulatory filtering\n"); print(" usable after regulatory filtering\n");
print(" scanning : "); print(info.scanning ? "yes" : "no"); print("\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) { if (verbose) {
print(" rx packets : "); put_u64(info.rxPackets); print("\n"); 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(" fw errors : "); put_u64(info.fwErrors); print("\n");
print(" conn state : "); put_u64(info.connState); 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) { static int cmd_scan(uint32_t seconds, bool verbose) {
abi::WifiInfo info; abi::WifiInfo info;
if (!require_adapter(info)) return 1; 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"); print("Connecting to \""); print(ssid); print("\"...\n");
int rc = wifi_connect(ssid, password); int rc = wifi_connect(ssid, password);
if (rc == 0) { if (rc == 0) {
print("Firmware contexts are up.\n"); print("Connected.\n");
print("NOTE: the 802.11 authentication exchange is not implemented yet,\n"); print("Run \"dhcp\" to get an address, then the network is usable\n");
print(" so this does not establish a usable link.\n"); print("just like a wired connection.\n");
return 0; return 0;
} }
if (rc == -2) {
print("wifi: \""); print(ssid); print("\" is encrypted (WPA2/WPA3).\n"); switch (rc) {
print(" Only open networks can be joined so far - the key exchange\n"); case abi::WIFI_ERR_NOT_FOUND:
print(" is not implemented yet. Scanning is unaffected.\n");
return 1;
}
print("wifi: \""); print(ssid); print("\" was not found in the last scan.\n"); print("wifi: \""); print(ssid); print("\" was not found in the last scan.\n");
print(" Run \"wifi scan\" first, and check the SSID spelling.\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(" <passphrase>\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;
}
return 1; return 1;
} }
@@ -339,8 +415,9 @@ static void usage() {
print(" scan [seconds] scan and list nearby networks (default 5)\n"); print(" scan [seconds] scan and list nearby networks (default 5)\n");
print(" info adapter, firmware and radio status\n"); print(" info adapter, firmware and radio status\n");
print(" debug diagnostics plus per-BSS scan detail\n"); print(" debug diagnostics plus per-BSS scan detail\n");
print(" connect <ssid> bring up firmware contexts (open networks)\n"); print(" connect <ssid> [key] join a network (open or WPA2/WPA3-PSK)\n");
print(" disconnect tear those contexts down\n\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"); print("With no command, runs a 5 second scan.\n");
} }
@@ -363,14 +440,20 @@ extern "C" void _start() {
exit(cmd_info(false)); exit(cmd_info(false));
} else if (streq(cmd, "debug")) { } else if (streq(cmd, "debug")) {
exit(cmd_debug()); exit(cmd_debug());
} else if (streq(cmd, "status")) {
exit(cmd_status());
} else if (streq(cmd, "connect")) { } else if (streq(cmd, "connect")) {
char ssid[40], pass[80]; char ssid[40], pass[80];
if (!next_token(&rest, ssid, sizeof(ssid))) { if (!next_token(&rest, ssid, sizeof(ssid))) {
print("wifi: connect needs an SSID\n"); print("wifi: connect needs an SSID\n");
exit(1); exit(1);
} }
pass[0] = '\0'; // Everything after the SSID is the passphrase, spaces and all.
next_token(&rest, pass, sizeof(pass)); 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)); exit(cmd_connect(ssid, pass));
} else if (streq(cmd, "disconnect")) { } else if (streq(cmd, "disconnect")) {
wifi_disconnect(); wifi_disconnect();
+166
View File
@@ -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', i) + label.encode() + data + struct.pack('<H', bits),
hashlib.sha256).digest()
i += 1
return r[:nbytes]
def derive_ptk(pmk, aa, spa, anonce, snonce, nbytes, sha256=False):
data = min(aa, spa) + max(aa, spa) + min(anonce, snonce) + max(anonce, snonce)
f = kdf_sha256 if sha256 else prf
return f(pmk, "Pairwise key expansion", data, nbytes)
def mic(kck, frame, ver):
if ver == 3:
from cryptography.hazmat.primitives.cmac import CMAC
c = CMAC(algorithms.AES(kck)); c.update(frame); return c.finalize()[:16]
return hmac.new(kck, frame, hashlib.sha1).digest()[:16]
HDR = 99
def build(ver, desc, key_info, replay, nonce, rsc, key_data, kck=None, mic_ver=2):
body = bytearray(HDR + len(key_data))
body[0] = ver
body[1] = 3 # EAPOL-Key
struct.pack_into('>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.")
+640
View File
@@ -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', seq << 4) + body
def data_from_ds(da, bssid, sa, ethertype, payload, protected=False):
fc1 = 0x02 | (0x40 if protected else 0)
hdr = bytes([0x08, fc1]) + b'\x00\x00' + da + bssid + sa + b'\x00\x00'
# The firmware decrypts in place and strips the MIC, but leaves the
# 8-byte CCMP header, so model that when the protected bit is set.
iv = b'\x11\x22\x00\x20\x00\x00\x00\x00' if protected else b''
snap = bytes([0xaa, 0xaa, 0x03, 0, 0, 0]) + 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', 1) + suite(4)
+ struct.pack('<H', 1) + suite(4)
+ struct.pack('<H', 1) + suite(2)
+ struct.pack('<H', 0))
# ------------------------------------------------------------------- EAPOL
def prf(key, label, data, n):
r, i = b'', 0
while len(r) < n:
r += hmac.new(key, label.encode() + b'\x00' + data + bytes([i]), hashlib.sha1).digest(); i += 1
return r[:n]
def derive_ptk(pmk, an, sn):
return prf(pmk, "Pairwise key expansion",
min(AP, STA) + max(AP, STA) + min(an, sn) + max(an, sn), 48)
def emic(kck, f): return hmac.new(kck, f, hashlib.sha1).digest()[:16]
HDR = 99
def eapol(ki, replay, nonce, rsc, kd, kck=None):
b = bytearray(HDR + len(kd)); b[0] = 2; b[1] = 3
struct.pack_into('>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('<H', b[0:2])[0] == 0, "authentication: open system algorithm")
check(struct.unpack('<H', b[2:4])[0] == 1, "authentication: transaction sequence 1")
check(struct.unpack('<H', b[4:6])[0] == 0, "authentication: status 0")
check(not auth['enc'] and auth['rate'],
"authentication frame sent in the clear at a fixed rate")
print("\n=== association ===")
d.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
ev = d.cmd("SERVICE")
check(len(ev['TX']) == 1, "authentication success triggers one association request")
ar = ev['TX'][0]
check(ar['hdr'][0] == 0x00, "association request: subtype assoc-req")
check(ar['hdr'][4:10] == AP and ar['hdr'][10:16] == STA,
"association request: addressing correct")
caps = struct.unpack('<H', ar['body'][0:2])[0]
check(caps & 0x0001, "association request: ESS capability set")
check(caps & 0x0010, "association request: privacy bit set for an encrypted network")
li = struct.unpack('<H', ar['body'][2:4])[0]
check(li == 10, "association request: listen interval present")
ies = parse_ies(ar['body'][4:])
check(ies.get(0) == SSID.encode(), "association request: SSID element matches")
check(1 in ies and len(ies[1]) >= 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('<H', 1), "RSN element: version 1")
check(sta_rsn[2:6] == suite(4), "RSN element: CCMP group cipher")
check(sta_rsn[8:12] == suite(4), "RSN element: CCMP pairwise cipher")
check(sta_rsn[14:18] == suite(2), "RSN element: PSK AKM")
aid = 0x0007
d.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0431, 0, aid | 0xc000)).hex())
ev = d.cmd("SERVICE")
check(state_of(ev['result']) == 6, "association moves the link into the handshake")
check(len(ev['CMD']) >= 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', h[22:24])[0]
ev2 = d.cmd("TXETH " + eth_out.hex())
seq2 = struct.unpack('<H', ev2['TX'][0]['hdr'][22:24])[0]
check(seq2 != seq1, "outbound data: the sequence number advances between frames")
reply = bytes.fromhex('450000300002000040110000' '0a000002' '0a000001') + b'inbound-payload'
ev = d.cmd("RXDATA " + data_from_ds(STA, AP, peer, 0x0800, reply, protected=True).hex())
check(len(ev['ETH']) == 1, "an encrypted inbound data frame yields one Ethernet frame")
e = ev['ETH'][0]
check(e[0:6] == STA, "inbound data: Ethernet destination is addr1")
check(e[6:12] == peer, "inbound data: Ethernet source is addr3")
check(e[12:14] == struct.pack('>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('<HHH', 0, 2, 0)).hex())
ev = d2.cmd("SERVICE")
ar = ev['TX'][0]
caps = struct.unpack('<H', ar['body'][0:2])[0]
check(not (caps & 0x0010), "open network: privacy bit clear")
ies = parse_ies(ar['body'][4:])
check(48 not in ies, "open network: no RSN element in the association request")
d2.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 3 | 0xc000)).hex())
ev = d2.cmd("SERVICE")
check(state_of(ev['result']) == 7 and link_of(ev['result']) == 1,
"open network: link comes up straight after association, no handshake")
check(len(ev['KEY']) == 0, "open network: no keys installed")
ev = d2.cmd("TXETH " + (peer + STA + 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('<HHH', 0, 2, 1)).hex())
check(state_of(ev['result']) == 8, "an authentication rejection fails the attempt")
d5 = Driver()
d5.cmd("MAC " + STA.hex())
d5.cmd(f"CONNECT {AP.hex()} 6 0 Rejects - -")
d5.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
d5.cmd("SERVICE")
ev = d5.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 17, 0)).hex())
check(state_of(ev['result']) == 8, "an association rejection fails the attempt")
print("\n=== the access point drops us ===")
d6 = Driver()
d6.cmd("MAC " + STA.hex())
d6.cmd(f"CONNECT {AP.hex()} 6 0 OpenNet - -")
d6.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
d6.cmd("SERVICE")
d6.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 3 | 0xc000)).hex())
ev = d6.cmd("SERVICE")
check(link_of(ev['result']) == 1, "link up before the deauthentication")
d6.cmd("RXMGMT " + mgmt(0xc0, STA, AP, AP, struct.pack('<H', 3)).hex())
ev = d6.cmd("SERVICE")
check(state_of(ev['result']) == 0 and link_of(ev['result']) == 0,
"a deauthentication from the AP brings the link down")
ev = d6.cmd("TXETH " + (peer + STA + 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('<I', p_uhb, 8)[0]
band, width, ctrl = p_uhb[12], p_uhb[13], p_uhb[14]
lmac = struct.unpack_from('<I', p_uhb, 16)[0]
check(chan == CHANNEL, "UHB layout: channel is the 32-bit field at offset 8")
check(band == 1, "UHB layout: band follows the channel (2.4 GHz = 1)")
check(width == 0 and ctrl == 0, "UHB layout: 20 MHz, control position below")
check(lmac == 0, "UHB layout: lmac_id lands at offset 16, not shifted")
p_v1 = phy_cmd_for(False)
check(p_v1 is not None and len(p_v1) == 28,
f"older firmware still gets the 28-byte form ({len(p_v1) if p_v1 else 0})")
if p_v1 and len(p_v1) == 28:
check(p_v1[8] == 1 and p_v1[9] == CHANNEL,
"legacy layout: band then channel, both single bytes")
check(struct.unpack_from('<I', p_v1, 12)[0] == 0,
"legacy layout: lmac_id at offset 12")
# =============================================================================
# MLD command sizes and layout.
#
# This firmware implements the MLD API, so the connect path uses MAC_CONFIG /
# LINK_CONFIG / STA_CONFIG rather than the legacy MAC_CONTEXT / BINDING /
# ADD_STA. Sizes and field offsets below are exactly what Linux puts on the
# wire against this same firmware (tests/wifi/decode_iwl_trace.py).
# =============================================================================
print("\n=== MLD command sizes ===")
MAC_CONFIG = (3 << 8) | 0x08
LINK_CONFIG = (3 << 8) | 0x09
STA_CONFIG = (3 << 8) | 0x0a
STA_REMOVE = (3 << 8) | 0x0c
PHY_CONTEXT = 0x08
dd = Driver()
dd.cmd("MAC " + STA.hex())
dd.cmd(f"CAPA {ULTRA_HB} 1")
ev = dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
sizes, first = {}, {}
for cid, payload in ev['CMD']:
sizes.setdefault(cid, len(payload))
first.setdefault(cid, payload)
check(sizes.get(MAC_CONFIG) == 52, f"MAC_CONFIG_CMD is 52 bytes (got {sizes.get(MAC_CONFIG)})")
check(sizes.get(LINK_CONFIG) == 208, f"LINK_CONFIG_CMD is 208 bytes (got {sizes.get(LINK_CONFIG)})")
check(sizes.get(STA_CONFIG) == 96, f"STA_CONFIG_CMD is 96 bytes (got {sizes.get(STA_CONFIG)})")
check(sizes.get(PHY_CONTEXT) == 32, f"PHY_CONTEXT_CMD is 32 bytes (got {sizes.get(PHY_CONTEXT)})")
check(0x28 not in sizes and 0x2b not in sizes and 0x18 not in sizes,
"no legacy MAC_CONTEXT / BINDING / ADD_STA is sent")
m = first.get(MAC_CONFIG)
if m:
check(struct.unpack_from('<I', m, 8)[0] == 5, "MAC_CONFIG: mac_type is BSS_STA")
check(m[12:18] == STA, "MAC_CONFIG: local_mld_addr is our MAC")
check(struct.unpack_from('<I', m, 20)[0] == 0x0c,
"MAC_CONFIG: accepts group + beacon frames before association")
check(m[36] == 0, "MAC_CONFIG: is_assoc clear on the initial add")
l = first.get(LINK_CONFIG)
if l:
check(struct.unpack_from('<I', l, 0)[0] == 1, "LINK_CONFIG: first one is an ADD")
check(struct.unpack_from('<I', l, 12)[0] == 0xffffffff,
"LINK_CONFIG: phy_id is INVALID until the link is bound")
check(l[16:22] == STA, "LINK_CONFIG: local_link_addr is our MAC")
check(struct.unpack_from('<I', l, 136)[0] == 0,
"LINK_CONFIG: no beacon timing before association")
# The PHY binding and the activation must be two separate MODIFYs, in that
# order, after the PHY context exists. Folding them into one command asserts
# fw 89 (UMAC error 0x2010330F, seen on the AX211); Linux sends the binding
# first with mask 0 (__iwl_mvm_mld_assign_vif_chanctx).
links = [(i, p) for i, (c, p) in enumerate(ev['CMD']) if c == LINK_CONFIG]
phy_add = next((i for i, (c, p) in enumerate(ev['CMD'])
if c == PHY_CONTEXT and struct.unpack_from('<I', p, 4)[0] == 1), None)
check(len(links) >= 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('<I', bind, 0)[0] == 2
and struct.unpack_from('<I', bind, 12)[0] == 0
and struct.unpack_from('<I', bind, 24)[0] == 0
and struct.unpack_from('<I', bind, 28)[0] == 0,
"PHY binding is its own MODIFY: phy_id set, mask 0, still inactive")
check(struct.unpack_from('<I', act, 0)[0] == 2
and struct.unpack_from('<I', act, 24)[0] == 0x03
and struct.unpack_from('<I', act, 28)[0] == 1,
"activation is a later MODIFY with exactly ACTIVE|RATES_INFO")
# Field values the Linux trace carries at this stage (fw 89 asserted with
# UMAC error 0x2010303E when they differed).
check(struct.unpack_from('<I', act, 136)[0] == 100
and struct.unpack_from('<I', act, 140)[0] == 0,
"activation carries the beacon interval but no DTIM interval yet")
check(struct.unpack_from('<I', act, 56)[0] == 0x2,
"pre-assoc qos_flags is TGN without UPDATE_EDCA")
check(struct.unpack_from('<H', act, 82)[0] == 0
and struct.unpack_from('<H', act, 62)[0] == 1023,
"pre-assoc EDCA is the default contention set, not WMM")
check(struct.unpack_from('<H', act, 146)[0] == 0,
"no RTS threshold before association")
st = first.get(STA_CONFIG)
if st:
check(struct.unpack_from('<I', st, 0)[0] == 0, "STA_CONFIG: station id 0")
check(st[8:14] == AP and st[16:22] == AP,
"STA_CONFIG: both peer addresses are the BSSID")
check(struct.unpack_from('<I', st, 28)[0] == 0,
"STA_CONFIG: association id 0 before association")
dd.p.stdin.close()
# =============================================================================
# Beacon timing arrives with association, not before.
# =============================================================================
print("\n=== link beacon timing ===")
dd = Driver()
dd.cmd("MAC " + STA.hex())
dd.cmd(f"CAPA {ULTRA_HB} 1")
dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
dd.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
dd.cmd("SERVICE")
dd.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
ev = dd.cmd("SERVICE")
link_i = next((i for i, (c, p) in enumerate(ev['CMD']) if c == LINK_CONFIG), None)
mac_i = next((i for i, (c, p) in enumerate(ev['CMD']) if c == MAC_CONFIG), None)
link = ev['CMD'][link_i][1] if link_i is not None else None
mac = ev['CMD'][mac_i][1] if mac_i is not None else None
check(link is not None and mac is not None,
"association updates the link and the MAC")
check(link_i is not None and mac_i is not None and link_i < mac_i,
"one-shot link timing is programmed before the MAC is marked associated")
if link:
mask = struct.unpack_from('<I', link, 24)[0]
check(struct.unpack_from('<I', link, 0)[0] == 2, "post-assoc link is a MODIFY")
check(mask & 0x10, "post-assoc link sets the BEACON_TIMING modify bit")
check(mask == 0x1e,
f"legacy post-assoc link changes exactly rates/protection/QoS/timing (got {mask:#x})")
check(not (mask & 0x01),
"post-assoc link does not re-assert ACTIVE on an active link")
check(struct.unpack_from('<I', link, 136)[0] == 100,
"post-assoc link carries the beacon interval")
check(struct.unpack_from('<I', link, 140)[0] == 200,
"post-assoc link carries bi * dtim period")
check(struct.unpack_from('<I', link, 56)[0] == 0x3,
"post-assoc qos_flags is TGN plus UPDATE_EDCA")
check(struct.unpack_from('<H', link, 82)[0] == 3008,
"post-assoc EDCA switches to the WMM set")
if mac:
check(mac[36] == 1, "post-assoc MAC has is_assoc set")
check(struct.unpack_from('<H', mac, 40)[0] == 7, "post-assoc MAC carries the AID")
check(struct.unpack_from('<I', mac, 20)[0] == 0x04,
"post-assoc MAC stops asking for beacons")
check(not any(c == STA_CONFIG for c, p in ev['CMD']),
"client AID is not incorrectly written to STA_CONFIG's GO-only assoc_id")
dd.p.stdin.close()
# =============================================================================
# Deadlines are not allowed to expire the moment they are set.
#
# The service loop sampled the clock once at the top of the pass, then spent
# real milliseconds inside the post-association context commands, each of which
# stamps a *newer* timestamp. Comparing the stale `now` against those stamps
# underflowed the unsigned subtraction, so a join that had just succeeded
# reported "timed out while joining the network" straight after "Associated",
# tore the contexts down, and the access point's first EAPOL frame arrived to
# find the station already gone.
#
# The harness advances its clock on every host command (CMD_ROUND_TRIP_MS), so
# a pass that sends commands and then checks a deadline reproduces this.
# =============================================================================
print("\n=== deadlines survive the time spent sending commands ===")
d7 = Driver()
d7.cmd("MAC " + STA.hex())
d7.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 {SSID} {PASS} {AP_RSN.hex()}")
d7.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
ev = d7.cmd("SERVICE")
check(state_of(ev['result']) == 4,
"sending the association request does not immediately expire its own retry timer")
check(len(ev['TX']) == 1,
"the association request is sent once per pass, not retransmitted on the spot")
d7.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
ev = d7.cmd("SERVICE")
check(state_of(ev['result']) == 6,
"association reaches the handshake instead of timing out in the same pass")
check(not any(x.startswith('TXQ-DOWN') for x in ev['TXQ']),
"a successful association does not tear the transmit queue back down")
check(not any(c == STA_REMOVE for c, p in ev['CMD']),
"a successful association does not remove the station it just added")
# The EAPOL exchange arrives a moment later, exactly as it does on the air.
m1_late = eapol(0x0002 | 0x0008 | 0x0080, (1).to_bytes(8, 'big'),
bytes(range(32)), b'\x00' * 8, b'')
ev = d7.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m1_late).hex())
ev = d7.cmd("SERVICE")
check(len(ev['TX']) == 1,
"an EAPOL message 1 that arrives after the context updates is still answered")
check(state_of(ev['result']) == 6,
"the station is still in the handshake when message 1 is answered")
# =============================================================================
# Teardown order: the MAC gives up its association first.
#
# While MAC_CONFIG.is_assoc is set, the firmware's MAC context owns the link
# carrying the BSS. Deactivating that link underneath it asserts the UMAC
# (0x2000320F on firmware 89) -- the failure the log above ends in.
# =============================================================================
print("\n=== teardown clears the association before the link ===")
ev = d7.cmd("ABORT")
seq = [(c, p) for c, p in ev['CMD']]
def index_of(pred):
return next((i for i, (c, p) in enumerate(seq) if pred(c, p)), None)
deassoc = index_of(lambda c, p: c == MAC_CONFIG
and struct.unpack_from('<I', p, 4)[0] == 2 and p[36] == 0)
sta_rm = index_of(lambda c, p: c == STA_REMOVE)
deact = index_of(lambda c, p: c == LINK_CONFIG
and struct.unpack_from('<I', p, 0)[0] == 2
and struct.unpack_from('<I', p, 24)[0] & 0x01
and struct.unpack_from('<I', p, 28)[0] == 0)
link_rm = index_of(lambda c, p: c == LINK_CONFIG
and struct.unpack_from('<I', p, 0)[0] == 3)
mac_rm = index_of(lambda c, p: c == MAC_CONFIG
and struct.unpack_from('<I', p, 4)[0] == 3)
check(deassoc is not None,
"teardown sends a MAC_CONFIG MODIFY clearing is_assoc")
check(deact is not None,
"teardown deactivates the link before removing it")
check(deassoc is not None and deact is not None and deassoc < deact,
"the association is cleared before the link is deactivated")
check(deassoc is not None and sta_rm is not None and deassoc < sta_rm,
"the association is cleared before the station is removed")
check(sta_rm is not None and deact is not None and sta_rm < deact,
"the station is removed before the link it sits on is deactivated")
check(deact is not None and link_rm is not None and deact < link_rm,
"the link is deactivated before it is removed")
check(link_rm is not None and mac_rm is not None and link_rm < mac_rm,
"the link is removed before the MAC that owns it")
d7.p.stdin.close()
print()
if fails:
for f in fails: print("FAILURE:", f)
sys.exit(1)
print("All MLME and data-path cases passed.")
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""Second-round supplicant tests: GTK rekey, message-3 retransmission, and RSN
negotiation against realistic access-point information elements."""
import hashlib, hmac, os, subprocess, sys, struct
from cryptography.hazmat.primitives.keywrap import aes_key_wrap
HARNESS = sys.argv[1]
fails = []
def prf(key, label, data, n):
r=b''; i=0
while len(r)<n:
r+=hmac.new(key, label.encode()+b'\x00'+data+bytes([i]), hashlib.sha1).digest(); i+=1
return r[:n]
def derive_ptk(pmk, aa, spa, an, sn):
return prf(pmk, "Pairwise key expansion", min(aa,spa)+max(aa,spa)+min(an,sn)+max(an,sn), 48)
def mic(kck, f): return hmac.new(kck, f, hashlib.sha1).digest()[:16]
HDR=99
def build(ki, replay, nonce, rsc, kd, kck=None):
b=bytearray(HDR+len(kd)); b[0]=2; b[1]=3
struct.pack_into('>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('<H',1) + suite(group)
b += struct.pack('<H',len(pairwise)) + b''.join(suite(x) for x in pairwise)
b += struct.pack('<H',len(akms)) + b''.join(suite(x) for x in akms)
b += struct.pack('<H',caps)
return b
cases = [
("WPA2-PSK, CCMP only", ie(4,[4],[2]), True, 2, 4),
("WPA2 mixed TKIP+CCMP (TKIP group)",ie(2,[4,2],[2]), False,0, 0),
("WPA2 mixed pairwise, CCMP group", ie(4,[4,2],[2]), True, 2, 4),
("WPA2/WPA3 transition (PSK+SAE)", ie(4,[4],[2,8],0x0080), True, 2, 4),
("WPA3-only SAE, MFP required", ie(4,[4],[8],0x00c0), False,0, 0),
("TKIP-only", ie(2,[2],[2]), False,0, 0),
("enterprise 802.1X", ie(4,[4],[1]), False,0, 0),
("PSK-SHA256 only", ie(4,[4],[6]), True, 6, 4),
("GCMP-256", ie(9,[9],[2]), True, 2, 9),
]
p=subprocess.Popen([HARNESS],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True,bufsize=1)
for name, body, expect_ok, expect_akm, expect_pc in cases:
p.stdin.write("PARSE "+body.hex()+"\n"); p.stdin.flush()
r=rd(p).split()
ok = r[1]=='1'; akm=int(r[3]); pc=int(r[5])
good = (ok==expect_ok) and (not expect_ok or (akm==expect_akm and pc==expect_pc))
print(f" {'PASS' if good else 'FAIL'} {name:36} -> 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.")
+111
View File
@@ -0,0 +1,111 @@
#include <cstdio>
#include <cstring>
#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;
}
+46
View File
@@ -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}")
+27
View File
@@ -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))
+217
View File
@@ -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 <cstdio>
#include <cstring>
#include <cstdint>
#include <string>
#include <iostream>
#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 <bssid> <channel> <is5> <ssid> <pass|-> <rsnie|->
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;
}
+48
View File
@@ -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."
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <atomic>
#include <cstdint>
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); }
};
}
+2
View File
@@ -0,0 +1,2 @@
#pragma once
namespace base { struct Manip {}; inline Manip hex, dec; }
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include <cstring>
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include <cstdint>
// Only the type name is needed: the MLME never touches PCI.
namespace Pci { struct PciDevice { uint8_t Bus, Device, Function; }; }
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <cstdio>
#include <cstdint>
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; }
};
}
@@ -0,0 +1,3 @@
#pragma once
#include <cstdint>
namespace Timekeeping { uint64_t GetMilliseconds(); }
+94
View File
@@ -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 <cstdio>
#include <cstring>
#include <cstdint>
#include <string>
#include <iostream>
#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 <ownmac> <bssid> <ssid> <passphrase> <akm> <pcipher> <gcipher>
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;
}