feat: wi-fi - join WPA2/WPA3-PSK networks and carry traffic like ethernet
This commit is contained in:
+234
@@ -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.
|
||||
@@ -12,4 +12,4 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#define MONTAUK_BUILD_NUMBER 53
|
||||
#define MONTAUK_BUILD_NUMBER 69
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <Drivers/USB/Bluetooth/Bluetooth.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
#include <Drivers/Graphics/IntelGPU.hpp>
|
||||
#include <Drivers/Storage/Ahci.hpp>
|
||||
#include <Drivers/Audio/IntelHda.hpp>
|
||||
@@ -127,6 +128,11 @@ namespace montauk::abi {
|
||||
if (Drivers::Net::E1000E::IsInitialized()) {
|
||||
add(5, "Intel E1000E", "Gigabit Ethernet (82574L)");
|
||||
}
|
||||
if (Drivers::Net::Wifi::IsPresent()) {
|
||||
add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp()
|
||||
? "802.11 wireless (connected)"
|
||||
: "802.11 wireless");
|
||||
}
|
||||
|
||||
// Display (category 6)
|
||||
if (Drivers::Graphics::IntelGPU::IsInitialized()) {
|
||||
|
||||
+21
-10
@@ -15,8 +15,10 @@
|
||||
#include <Net/Ipv4.hpp>
|
||||
#include <Net/Socket.hpp>
|
||||
#include <Net/NetConfig.hpp>
|
||||
#include <Net/NetIf.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
|
||||
#include "Syscall.hpp"
|
||||
|
||||
@@ -96,12 +98,9 @@ namespace montauk::abi {
|
||||
out->subnetMask = Net::GetSubnetMask();
|
||||
out->gateway = Net::GetGateway();
|
||||
|
||||
const uint8_t* mac = nullptr;
|
||||
if (Drivers::Net::E1000::IsInitialized()) {
|
||||
mac = Drivers::Net::E1000::GetMacAddress();
|
||||
} else if (Drivers::Net::E1000E::IsInitialized()) {
|
||||
mac = Drivers::Net::E1000E::GetMacAddress();
|
||||
}
|
||||
// Whichever interface is carrying traffic; on a Wi-Fi link this is the
|
||||
// adapter's MAC, not an idle Ethernet port's.
|
||||
const uint8_t* mac = ::Net::NetIf::ActiveMac();
|
||||
if (mac) {
|
||||
for (int i = 0; i < 6; i++) out->macAddress[i] = mac[i];
|
||||
} else {
|
||||
@@ -122,6 +121,9 @@ namespace montauk::abi {
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
// Reports whichever interface is currently carrying traffic, so a machine
|
||||
// that joined a Wi-Fi network sees the wireless counters here rather than
|
||||
// an idle Ethernet port.
|
||||
static void Sys_NetStatus(NetStatus* out) {
|
||||
if (out == nullptr) return;
|
||||
|
||||
@@ -133,15 +135,24 @@ namespace montauk::abi {
|
||||
out->txPackets = 0;
|
||||
CopyNetStatusDriver(out->driver, "No adapter");
|
||||
|
||||
if (Drivers::Net::E1000::IsInitialized()) {
|
||||
const auto* iface = ::Net::NetIf::Active();
|
||||
if (iface == nullptr) return;
|
||||
|
||||
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->txPackets = Drivers::Net::E1000::GetTxPacketCount();
|
||||
CopyNetStatusDriver(out->driver, "Intel 82540EM");
|
||||
} else if (Drivers::Net::E1000E::IsInitialized()) {
|
||||
out->initialized = 1;
|
||||
out->linkUp = Drivers::Net::E1000E::IsLinkUp() ? 1 : 0;
|
||||
out->polling = Drivers::Net::E1000E::RequiresPolling() ? 1 : 0;
|
||||
out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount();
|
||||
out->txPackets = Drivers::Net::E1000E::GetTxPacketCount();
|
||||
|
||||
@@ -671,6 +671,26 @@ namespace montauk::abi {
|
||||
uint16_t beaconInterval; // TU
|
||||
};
|
||||
|
||||
// Association progress reported in WifiInfo.connState.
|
||||
static constexpr uint32_t WIFI_CONN_IDLE = 0;
|
||||
static constexpr uint32_t WIFI_CONN_CONTEXTS_UP = 1;
|
||||
static constexpr uint32_t WIFI_CONN_AUTHENTICATING = 2;
|
||||
static constexpr uint32_t WIFI_CONN_AUTHENTICATED = 3;
|
||||
static constexpr uint32_t WIFI_CONN_ASSOCIATING = 4;
|
||||
static constexpr uint32_t WIFI_CONN_ASSOCIATED = 5;
|
||||
static constexpr uint32_t WIFI_CONN_HANDSHAKING = 6;
|
||||
static constexpr uint32_t WIFI_CONN_CONNECTED = 7;
|
||||
static constexpr uint32_t WIFI_CONN_FAILED = 8;
|
||||
|
||||
// Negative results from SYS_WIFI_CONNECT.
|
||||
static constexpr int WIFI_ERR_NO_ADAPTER = -1; // no adapter, or not ready
|
||||
static constexpr int WIFI_ERR_NOT_FOUND = -2; // SSID absent from the scan
|
||||
static constexpr int WIFI_ERR_NEED_KEY = -3; // encrypted, no passphrase
|
||||
static constexpr int WIFI_ERR_UNSUPPORTED = -4; // WPA3-SAE, WEP, enterprise
|
||||
static constexpr int WIFI_ERR_AUTH = -5; // key exchange rejected
|
||||
static constexpr int WIFI_ERR_TIMEOUT = -6; // AP never answered
|
||||
static constexpr int WIFI_ERR_FAILED = -7; // anything else
|
||||
|
||||
// Adapter status (returned by SYS_WIFI_INFO).
|
||||
struct WifiInfo {
|
||||
uint8_t mac[6];
|
||||
@@ -682,7 +702,12 @@ namespace montauk::abi {
|
||||
char fwVersion[32];
|
||||
uint64_t rxPackets;
|
||||
uint32_t fwErrors;
|
||||
uint32_t connState; // 0 idle, >0 connection setup in progress
|
||||
uint32_t connState; // WIFI_CONN_*
|
||||
uint64_t txPackets;
|
||||
char ssid[36]; // network joined, empty when disconnected
|
||||
uint8_t bssid[6];
|
||||
uint8_t connected; // 1 once the link can carry IP traffic
|
||||
uint8_t channel;
|
||||
};
|
||||
|
||||
struct ThermalInfo {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <cstdint>
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <atomic>
|
||||
#include "IwxReg.hpp"
|
||||
|
||||
namespace Drivers::Net::Wifi {
|
||||
@@ -85,6 +86,13 @@ namespace Drivers::Net::Wifi {
|
||||
// Rings
|
||||
// =========================================================================
|
||||
|
||||
// Frames waiting in a TX queue are staged in single pages, one per
|
||||
// in-flight frame. The descriptor ring itself has IWX_TX_RING_COUNT
|
||||
// entries (the size the firmware was told about), but outstanding frames
|
||||
// are capped at the number of staging pages so a slot can never be reused
|
||||
// while the hardware is still reading it.
|
||||
constexpr uint32_t IWX_TX_STAGE_SLOTS = 64;
|
||||
|
||||
struct IwxTxRing {
|
||||
int Qid = 0;
|
||||
IwxDma Desc; // IwxTfhTfd[IWX_TX_RING_COUNT]
|
||||
@@ -94,6 +102,13 @@ namespace Drivers::Net::Wifi {
|
||||
uint32_t Cur = 0; // ring slot (0..count-1)
|
||||
uint32_t CurHw = 0; // hardware index (0..65535)
|
||||
uint32_t Queued = 0;
|
||||
|
||||
// TX staging (data/management queues only; the command queue stages
|
||||
// into its per-slot Cmd area instead).
|
||||
uint8_t* Stage[IWX_TX_STAGE_SLOTS] = {};
|
||||
uint64_t StagePhys[IWX_TX_STAGE_SLOTS] = {};
|
||||
uint32_t StageSlots = 0;
|
||||
bool Active = false; // configured with the firmware
|
||||
};
|
||||
|
||||
struct IwxRxRing {
|
||||
@@ -181,7 +196,11 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
IwxRxRing RxQ;
|
||||
IwxTxRing CmdQ; // queue 0: host commands
|
||||
IwxTxRing MgmtQ; // queue 1: management frames (connect path)
|
||||
// One queue carries management frames, EAPOL and non-QoS data: the
|
||||
// firmware maps non-QoS data onto the management TID anyway, and the
|
||||
// driver never negotiates block-ack sessions that would need per-TID
|
||||
// queues.
|
||||
IwxTxRing MgmtQ;
|
||||
|
||||
// ALIVE / init-complete tracking (set from notification processing)
|
||||
volatile bool AliveIntr = false;
|
||||
@@ -194,18 +213,29 @@ namespace Drivers::Net::Wifi {
|
||||
uint32_t UmacErrorTable = 0;
|
||||
uint32_t LmacErrorTable = 0;
|
||||
uint32_t LastCmdId = 0;
|
||||
uint32_t LastCmdLen = 0;
|
||||
uint8_t LastCmdPayload[192] = {}; // dumped on a firmware assert
|
||||
bool LtrEnabled = false; // PCIe LTR capability advertised
|
||||
|
||||
// Synchronous-command bookkeeping (commands are fully serialized)
|
||||
kcp::Spinlock CmdLock; // serializes SendCmd callers
|
||||
// A Mutex, deliberately not a Spinlock: kcp::Spinlock disables
|
||||
// interrupts, and this lock is held across a wall-clock wait for the
|
||||
// firmware's response. With interrupts off the millisecond counter
|
||||
// (driven by the APIC timer interrupt) never advances, so the timeout
|
||||
// could never expire and one unanswered command would hang the machine
|
||||
// with interrupts disabled. Every caller is process or idle context;
|
||||
// the hard IRQ only latches WorkPending.
|
||||
kcp::Mutex CmdLock; // serializes SendCmd callers
|
||||
volatile bool CmdDone = false;
|
||||
volatile bool CmdWantResp = false;
|
||||
uint8_t CmdRespBuf[4096];
|
||||
volatile uint32_t CmdRespLen = 0;
|
||||
uint32_t CmdIdx = 0; // ring slot of in-flight command
|
||||
|
||||
// Reentrancy guard for ProcessEvents
|
||||
volatile bool InProcessEvents = false;
|
||||
// Reentrancy guard for ProcessEvents. Interrupts stay enabled while
|
||||
// commands wait, so two cores can genuinely race here; a plain bool
|
||||
// would let both through.
|
||||
std::atomic_flag InProcessEvents = ATOMIC_FLAG_INIT;
|
||||
|
||||
IwxNvmData Nvm;
|
||||
IwxChannel Channels[IWX_MAX_CHANNELS_TRACKED];
|
||||
@@ -215,8 +245,19 @@ namespace Drivers::Net::Wifi {
|
||||
volatile bool ScanActive = false;
|
||||
volatile bool ScanCompleted = false;
|
||||
|
||||
// Band of the BSS currently being joined; picks the legacy rate used
|
||||
// for management frames.
|
||||
bool Is5GHz = false;
|
||||
|
||||
// Serializes frame TX (the netif and the MLME both queue frames).
|
||||
// Process/idle context only, so it keeps interrupts enabled too.
|
||||
kcp::Mutex TxLock;
|
||||
|
||||
// Statistics/diagnostics
|
||||
uint64_t RxPackets = 0;
|
||||
uint64_t TxPackets = 0;
|
||||
uint64_t TxFailures = 0;
|
||||
uint64_t RxDataPackets = 0;
|
||||
uint64_t FwErrors = 0;
|
||||
};
|
||||
|
||||
@@ -271,8 +312,28 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
bool IwxCheckRfKill();
|
||||
|
||||
// TX queue management (used by connect path)
|
||||
bool IwxEnableTxq(int staId, int qid, int tid);
|
||||
// TX queue management (used by the connect path). On the new data-path
|
||||
// API the firmware picks the queue number and reports it back, so `qid` is
|
||||
// only a hint; the assigned value lands in ring.Qid.
|
||||
bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid);
|
||||
void IwxDisableTxq(IwxTxRing& ring, int staId, int tid);
|
||||
|
||||
// Transmit one 802.11 frame. The header and payload are copied into a
|
||||
// staging page, wrapped in a TX_CMD and handed to the queue.
|
||||
// encrypt - let the firmware apply the installed key (data frames
|
||||
// after the handshake); cleared for auth/assoc/EAPOL
|
||||
// fixedRate - send at the lowest basic rate instead of letting the
|
||||
// firmware's rate control pick (required for management
|
||||
// frames, which have no rate table yet)
|
||||
bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen,
|
||||
const uint8_t* payload, uint32_t payloadLen,
|
||||
bool encrypt, bool fixedRate);
|
||||
|
||||
// Install or remove a hardware key for the connected station.
|
||||
bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx,
|
||||
bool pairwise, uint8_t cipher, const uint8_t* rsc);
|
||||
bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher,
|
||||
uint32_t keyLen);
|
||||
|
||||
// =========================================================================
|
||||
// Firmware file parsing (IwxFw.cpp)
|
||||
@@ -300,15 +361,51 @@ namespace Drivers::Net::Wifi {
|
||||
void WifiRxMgmtFrame(const uint8_t* frame, uint32_t len, uint8_t channel,
|
||||
int8_t rssiDbm);
|
||||
|
||||
// TX completion, called from RX processing for a TX_CMD response.
|
||||
void IwxTxComplete(int qid, int idx, uint32_t status);
|
||||
|
||||
// =========================================================================
|
||||
// Connect groundwork (IwxConnect.cpp) - UNTESTED scaffolding
|
||||
// Connect / MLME (IwxConnect.cpp)
|
||||
// =========================================================================
|
||||
|
||||
// Association states reported through WifiInfo.connState.
|
||||
enum class IwxConnStateId : int {
|
||||
Idle = 0,
|
||||
ContextsUp,
|
||||
Authenticating,
|
||||
Authenticated,
|
||||
Associating,
|
||||
Associated, // 802.11 link up; open networks stop here
|
||||
Handshaking, // WPA 4-way in progress
|
||||
Connected, // keys installed (or open); data can flow
|
||||
Failed,
|
||||
};
|
||||
|
||||
// `rsnIe` is the AP's RSN element body (may be null for open networks).
|
||||
bool IwxConnectStart(const uint8_t* bssid, uint8_t channel, bool is5GHz,
|
||||
const char* ssid);
|
||||
const char* ssid, const char* password,
|
||||
const uint8_t* rsnIe, uint32_t rsnIeLen,
|
||||
uint16_t beaconInterval, uint8_t dtimPeriod);
|
||||
void IwxConnectAbort();
|
||||
// Why the last IwxConnectStart() refused: false means the radio or the
|
||||
// firmware failed, true means the network's security is unsupported. The
|
||||
// two need very different advice, so they must not be conflated.
|
||||
bool IwxConnectRefusedForSecurity();
|
||||
void IwxConnectRxMgmt(const uint8_t* frame, uint32_t len);
|
||||
// Inbound 802.11 data frame from the RX path.
|
||||
void IwxConnectRxData(const uint8_t* frame, uint32_t len);
|
||||
// Apply state changes the RX path queued (it cannot send commands itself).
|
||||
void IwxConnectService();
|
||||
int IwxConnectState();
|
||||
|
||||
// Link status for the network stack.
|
||||
bool IwxLinkUp(); // associated and keyed
|
||||
const uint8_t* IwxConnectBssid();
|
||||
const char* IwxConnectSsid();
|
||||
|
||||
// Send an Ethernet frame over the air (called by the netif).
|
||||
bool IwxConnectSendEthernet(const uint8_t* frame, uint32_t len);
|
||||
|
||||
// Sink for decapsulated Ethernet frames, implemented by Wifi.cpp.
|
||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
#include "Iwx.hpp"
|
||||
#include "Ieee80211.hpp"
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
@@ -629,12 +630,23 @@ namespace Drivers::Net::Wifi {
|
||||
uint32_t offset = (uint32_t)(frame - (const uint8_t*)pkt);
|
||||
if (offset + len > bufLen) return;
|
||||
|
||||
// The firmware pads the header to a 4-byte boundary when the flag is
|
||||
// set; the payload then starts two bytes later.
|
||||
// The firmware pads a MAC header that is not a multiple of four bytes
|
||||
// (in practice: every QoS data header) out to alignment by inserting
|
||||
// two bytes between the header -- and the crypto IV, if any -- and
|
||||
// the payload; mpdu_len counts them. Cut them out so the rest of the
|
||||
// driver sees a contiguous 802.11 frame. Getting this wrong loses
|
||||
// every QoS data frame while management frames keep working.
|
||||
if (desc->mac_flags2 & IWX_RX_MPDU_MFLG2_PAD) {
|
||||
if (len < 2) return;
|
||||
frame += 2;
|
||||
if (offset + 2 + len > bufLen) return;
|
||||
uint32_t hdrLen = 24;
|
||||
if ((frame[0] & IEEE80211_FC0_TYPE_MASK) == IEEE80211_FC0_TYPE_DATA
|
||||
&& (frame[0] & IEEE80211_SUBTYPE_QOS_DATA))
|
||||
hdrLen += 2;
|
||||
if (frame[1] & IEEE80211_FC1_PROTECTED)
|
||||
hdrLen += IWX_CCMP_HDR_LEN;
|
||||
if (len < hdrLen + 2) return;
|
||||
memmove((uint8_t*)frame + hdrLen, frame + hdrLen + 2,
|
||||
len - hdrLen - 2);
|
||||
len -= 2;
|
||||
}
|
||||
|
||||
int energyA = desc->v3.energy_a ? -(int)desc->v3.energy_a : -256;
|
||||
@@ -642,6 +654,26 @@ namespace Drivers::Net::Wifi {
|
||||
int rssi = energyA > energyB ? energyA : energyB;
|
||||
if (rssi < -128) rssi = -128;
|
||||
|
||||
uint8_t type = (uint8_t)(frame[0] & IEEE80211_FC0_TYPE_MASK);
|
||||
|
||||
if (type == IEEE80211_FC0_TYPE_DATA) {
|
||||
// Encrypted frames are decrypted by the firmware, which strips the
|
||||
// MIC but leaves the CCMP/GCMP header in place; drop anything that
|
||||
// failed to decrypt rather than handing up ciphertext.
|
||||
if (frame[1] & IEEE80211_FC1_PROTECTED) {
|
||||
uint32_t sec = desc->status & IWX_RX_MPDU_STATUS_SEC_MASK;
|
||||
bool ok = (desc->status & IWX_RX_MPDU_STATUS_DECRYPTED)
|
||||
&& (desc->status & IWX_RX_MPDU_STATUS_MIC_OK)
|
||||
&& (sec == IWX_RX_MPDU_STATUS_SEC_CCM
|
||||
|| sec == IWX_RX_MPDU_STATUS_SEC_GCM);
|
||||
if (!ok) return;
|
||||
}
|
||||
IwxConnectRxData(frame, len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != IEEE80211_FC0_TYPE_MGT) return;
|
||||
|
||||
WifiRxMgmtFrame(frame, len, desc->v3.channel, (int8_t)rssi);
|
||||
}
|
||||
|
||||
|
||||
@@ -567,7 +567,10 @@ inline uint32_t IwxRxPacketPayloadLen(const IwxRxPacket* pkt) {
|
||||
return IwxRxPacketLen(pkt) - sizeof(IwxCmdHeader);
|
||||
}
|
||||
|
||||
// DQA queue assignment
|
||||
// DQA queue assignment. The management queue number is only a hint: on the v3
|
||||
// data-path API the firmware assigns the queue and reports the number back.
|
||||
// TID 15 carries management frames, EAPOL and non-QoS data -- the firmware maps
|
||||
// non-QoS data onto this TID regardless.
|
||||
constexpr int IWX_DQA_CMD_QUEUE = 0;
|
||||
constexpr int IWX_DQA_MGMT_QUEUE = 1;
|
||||
constexpr int IWX_MGMT_TID = 15;
|
||||
@@ -949,6 +952,20 @@ constexpr uint8_t IWX_SCAN_ADWELL_N_APS_SOCIAL_CHS = 2;
|
||||
|
||||
constexpr uint32_t IWX_RX_MPDU_RES_STATUS_CRC_OK = 1u << 0;
|
||||
constexpr uint32_t IWX_RX_MPDU_RES_STATUS_OVERRUN_OK = 1u << 1;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_KEY_VALID = 1u << 3;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_ICV_OK = 1u << 5;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_MIC_OK = 1u << 6;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_MASK = 0x7u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_NONE = 0x0u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_WEP = 0x1u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_CCM = 0x2u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_TKIP = 0x3u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_EXT_ENC = 0x4u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_SEC_GCM = 0x5u << 8;
|
||||
constexpr uint32_t IWX_RX_MPDU_STATUS_DECRYPTED = 1u << 11;
|
||||
|
||||
// The hardware strips the CCMP/GCMP MIC but leaves the 8-byte IV in place.
|
||||
constexpr uint32_t IWX_CCMP_HDR_LEN = 8;
|
||||
constexpr uint8_t IWX_RX_MPDU_MFLG2_PAD = 0x20;
|
||||
constexpr uint8_t IWX_RX_MPDU_MFLG2_AMSDU = 0x40;
|
||||
|
||||
@@ -1000,21 +1017,46 @@ constexpr uint32_t IWX_FW_CTXT_ACTION_REMOVE = 3;
|
||||
constexpr uint32_t IWX_LMAC_24G_INDEX = 0;
|
||||
constexpr uint32_t IWX_LMAC_5G_INDEX = 1;
|
||||
|
||||
// PHY context
|
||||
struct IwxFwChannelInfoV1 {
|
||||
// PHY context.
|
||||
//
|
||||
// The channel-info sub-structure has two shapes and the firmware picks which
|
||||
// one it expects via IWX_UCODE_TLV_CAPA_ULTRA_HB_CHANNELS. AX211 firmware 89
|
||||
// sets that bit, so it wants the v2 form: a 32-bit channel number first, then
|
||||
// the band. Sending the 4-byte v1 form to a firmware expecting 8 bytes
|
||||
// shifts lmac_id/rxchain_info and asserts the firmware, which then stops
|
||||
// answering host commands entirely.
|
||||
struct IwxFwChannelInfoV1 { // CHANNEL_CONFIG_API_S_VER_1
|
||||
uint8_t band;
|
||||
uint8_t channel;
|
||||
uint8_t width;
|
||||
uint8_t ctrl_pos;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxFwChannelInfo { // CHANNEL_CONFIG_API_S_VER_2
|
||||
uint32_t channel;
|
||||
uint8_t band;
|
||||
uint8_t width;
|
||||
uint8_t ctrl_pos;
|
||||
uint8_t reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
constexpr uint8_t IWX_PHY_VHT_CHANNEL_MODE20 = 0x0;
|
||||
constexpr uint8_t IWX_PHY_VHT_CTRL_POS_1_BELOW = 0x0;
|
||||
constexpr uint32_t IWX_PHY_RX_CHAIN_VALID_POS = 1;
|
||||
constexpr uint32_t IWX_PHY_RX_CHAIN_CNT_POS = 10;
|
||||
constexpr uint32_t IWX_PHY_RX_CHAIN_MIMO_CNT_POS = 12;
|
||||
|
||||
struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB)
|
||||
struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_S_VER_3/4, UHB
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
IwxFwChannelInfo ci;
|
||||
uint32_t lmac_id;
|
||||
uint32_t rxchain_info; // reserved from VER_4 on
|
||||
uint32_t dsp_cfg_flags;
|
||||
uint32_t reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxPhyContextCmdV1Chan { // same command, pre-UHB channel info
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
IwxFwChannelInfoV1 ci;
|
||||
@@ -1024,11 +1066,15 @@ struct IwxPhyContextCmd { // PHY_CONTEXT_CMD_API_VER_3/4 (non-UHB)
|
||||
uint32_t reserved;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxPhyContextCmd) == 32, "PHY context (UHB) must be 32 bytes");
|
||||
static_assert(sizeof(IwxPhyContextCmdV1Chan) == 28, "PHY context (legacy) must be 28 bytes");
|
||||
|
||||
// MAC context
|
||||
constexpr uint32_t IWX_FW_MAC_TYPE_BSS_STA = 5;
|
||||
constexpr uint32_t IWX_TSF_ID_A = 0;
|
||||
constexpr uint32_t IWX_AC_NUM = 4;
|
||||
constexpr uint32_t IWX_MAC_QOS_FLG_UPDATE_EDCA = 1u << 0;
|
||||
constexpr uint32_t IWX_MAC_QOS_FLG_TGN = 1u << 1;
|
||||
constexpr uint32_t IWX_MAC_FILTER_IN_CONTROL_AND_MGMT = 1u << 1;
|
||||
constexpr uint32_t IWX_MAC_FILTER_ACCEPT_GRP = 1u << 2;
|
||||
constexpr uint32_t IWX_MAC_FILTER_IN_BEACON = 1u << 6;
|
||||
@@ -1055,6 +1101,16 @@ struct IwxMacDataSta {
|
||||
uint32_t assoc_beacon_arrive_time;
|
||||
} __attribute__((packed));
|
||||
|
||||
// The per-mac-type tail of MAC_CONTEXT_CMD is a union in the firmware API, so
|
||||
// the command length is that of its LARGEST member -- p2p_sta, which is
|
||||
// iwl_mac_data_sta plus a ctwin word. Sending only the sta member makes the
|
||||
// command four bytes short and asserts the firmware (observed on AX211 fw 89:
|
||||
// UMAC error 0x201002FF on command 0x128).
|
||||
struct IwxMacDataP2pSta {
|
||||
IwxMacDataSta sta;
|
||||
uint32_t ctwin;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta)
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
@@ -1072,9 +1128,15 @@ struct IwxMacCtxCmd { // IWX_MAC_CONTEXT_CMD_API_S_VER_1 (sta)
|
||||
uint32_t filter_flags;
|
||||
uint32_t qos_flags;
|
||||
IwxAcQos ac[IWX_AC_NUM + 1];
|
||||
union {
|
||||
IwxMacDataSta sta;
|
||||
IwxMacDataP2pSta p2p_sta; // the largest member: sizes the command
|
||||
} u;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxMacCtxCmd) == 148,
|
||||
"MAC context command must stay 148 bytes (union sized by p2p_sta)");
|
||||
|
||||
// Binding context
|
||||
constexpr uint32_t IWX_MAX_MACS_IN_BINDING = 3;
|
||||
struct IwxBindingCmd {
|
||||
@@ -1178,8 +1240,9 @@ struct IwxRlcConfigCmd {
|
||||
uint8_t reserved[3];
|
||||
} __attribute__((packed));
|
||||
|
||||
// Session protection
|
||||
constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 1;
|
||||
// Session protection. ASSOC is the first value of
|
||||
// enum iwl_session_prot_conf_id, i.e. zero -- 1 is GO_CLIENT_ASSOC.
|
||||
constexpr uint32_t IWX_SESSION_PROTECT_CONF_ASSOC = 0;
|
||||
struct IwxSessionProtCmd {
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
@@ -1196,6 +1259,313 @@ struct IwxSessionProtNotif {
|
||||
uint32_t conf_id;
|
||||
} __attribute__((packed));
|
||||
|
||||
// =============================================================================
|
||||
// MLD API (MAC_CONF group)
|
||||
//
|
||||
// Firmware that advertises IWX_UCODE_TLV_CAPA_MLD_API_SUPPORT -- which AX211
|
||||
// firmware 89 does -- implements these instead of the legacy MAC_CONTEXT_CMD /
|
||||
// BINDING_CONTEXT_CMD / ADD_STA. The legacy commands are simply absent, and
|
||||
// sending one asserts the firmware.
|
||||
//
|
||||
// Every layout and size below was confirmed against a host-command trace taken
|
||||
// from Linux driving this same adapter and firmware (see
|
||||
// tests/wifi/decode_iwl_trace.py), not inferred from a kernel header.
|
||||
// =============================================================================
|
||||
|
||||
constexpr uint8_t IWX_MAC_CONFIG_CMD = 0x08; // MAC_CONF group
|
||||
constexpr uint8_t IWX_LINK_CONFIG_CMD = 0x09;
|
||||
constexpr uint8_t IWX_STA_CONFIG_CMD = 0x0a;
|
||||
constexpr uint8_t IWX_AUX_STA_CMD = 0x0b;
|
||||
constexpr uint8_t IWX_STA_REMOVE_CMD = 0x0c;
|
||||
|
||||
// iwl_mac_config_filter_flags
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_PROMISC = 1u << 0;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_CTRL_MGMT = 1u << 1;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_GRP = 1u << 2;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_BEACON = 1u << 3;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_BCAST_PROBE_RESP = 1u << 4;
|
||||
constexpr uint32_t IWX_MAC_CFG_FILTER_ACCEPT_PROBE_REQ = 1u << 5;
|
||||
|
||||
struct IwxMacClientData { // MAC_CONTEXT_CONFIG_CLIENT_DATA_API_S_VER_2
|
||||
uint8_t is_assoc;
|
||||
uint8_t esr_transition_timeout;
|
||||
uint16_t medium_sync_delay;
|
||||
uint16_t assoc_id;
|
||||
uint16_t reserved1;
|
||||
uint16_t data_policy;
|
||||
uint16_t reserved2;
|
||||
uint32_t ctwin;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxMacConfigCmd { // MAC_CONTEXT_CONFIG_CMD_API_S_VER_2
|
||||
uint32_t id_and_color;
|
||||
uint32_t action;
|
||||
uint32_t mac_type;
|
||||
uint8_t local_mld_addr[6];
|
||||
uint16_t reserved_for_local_mld_addr;
|
||||
uint32_t filter_flags;
|
||||
uint16_t he_support;
|
||||
uint16_t he_ap_support;
|
||||
uint32_t eht_support;
|
||||
uint32_t nic_not_ack_enabled;
|
||||
IwxMacClientData client; // union with p2p_dev; client is the largest
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxMacConfigCmd) == 52, "MAC_CONFIG_CMD must be 52 bytes");
|
||||
|
||||
// iwl_link_ctx_modify_flags
|
||||
constexpr uint32_t IWX_LINK_MODIFY_ACTIVE = 1u << 0;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_RATES_INFO = 1u << 1;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_PROTECT_FLAGS = 1u << 2;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_QOS_PARAMS = 1u << 3;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_BEACON_TIMING = 1u << 4;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_HE_PARAMS = 1u << 5;
|
||||
constexpr uint32_t IWX_LINK_MODIFY_ALL = 0xff;
|
||||
|
||||
struct IwxHeBackoffConf { // AC_QOS_MU_EDCA_API_S
|
||||
uint16_t cwmin;
|
||||
uint16_t cwmax;
|
||||
uint16_t aifsn;
|
||||
uint16_t mu_time;
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxLinkConfigCmd { // LINK_CONTEXT_CONFIG_CMD_API_S_VER_1/2/3
|
||||
uint32_t action;
|
||||
uint32_t link_id;
|
||||
uint32_t mac_id;
|
||||
uint32_t phy_id; // IWX_FW_CTXT_INVALID until bound
|
||||
uint8_t local_link_addr[6];
|
||||
uint16_t reserved_for_local_link_addr;
|
||||
uint32_t modify_mask;
|
||||
uint32_t active;
|
||||
uint32_t listen_lmac;
|
||||
uint32_t cck_rates;
|
||||
uint32_t ofdm_rates;
|
||||
uint32_t cck_short_preamble;
|
||||
uint32_t short_slot;
|
||||
uint32_t protection_flags;
|
||||
uint32_t qos_flags;
|
||||
IwxAcQos ac[IWX_AC_NUM + 1];
|
||||
uint8_t htc_trig_based_pkt_ext;
|
||||
uint8_t rand_alloc_ecwmin;
|
||||
uint8_t rand_alloc_ecwmax;
|
||||
uint8_t ndp_fdbk_buff_th_exp;
|
||||
IwxHeBackoffConf trig_based_txf[IWX_AC_NUM];
|
||||
uint32_t bi;
|
||||
uint32_t dtim_interval;
|
||||
uint16_t puncture_mask; // removed in _VER_3
|
||||
uint16_t frame_time_rts_th;
|
||||
uint32_t flags;
|
||||
uint32_t flags_mask;
|
||||
uint8_t ref_bssid_addr[6];
|
||||
uint16_t reserved_for_ref_bssid_addr;
|
||||
uint8_t bssid_index;
|
||||
uint8_t bss_color;
|
||||
uint8_t spec_link_id;
|
||||
uint8_t reserved2;
|
||||
uint8_t ibss_bssid_addr[6];
|
||||
uint16_t reserved_for_ibss_bssid_addr;
|
||||
uint32_t reserved3[8];
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxLinkConfigCmd) == 208, "LINK_CONFIG_CMD must be 208 bytes");
|
||||
|
||||
// 2 spatial streams x 5 bandwidth indices x 2 thresholds
|
||||
struct IwxHePktExtV2 {
|
||||
uint8_t pkt_ext_qam_th[2][5][2];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxStaConfigCmd { // STA_CMD_API_S_VER_1
|
||||
uint32_t sta_id;
|
||||
uint32_t link_id;
|
||||
uint8_t peer_mld_address[6];
|
||||
uint16_t reserved_for_peer_mld_address;
|
||||
uint8_t peer_link_address[6];
|
||||
uint16_t reserved_for_peer_link_address;
|
||||
uint32_t station_type;
|
||||
uint32_t assoc_id;
|
||||
uint32_t beamform_flags;
|
||||
uint32_t mfp;
|
||||
uint32_t mimo;
|
||||
uint32_t mimo_protection;
|
||||
uint32_t ack_enabled;
|
||||
uint32_t trig_rnd_alloc;
|
||||
uint32_t tx_ampdu_spacing;
|
||||
uint32_t tx_ampdu_max_size;
|
||||
uint32_t sp_length;
|
||||
uint32_t uapsd_acs;
|
||||
IwxHePktExtV2 pkt_ext;
|
||||
uint32_t htc_flags;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxStaConfigCmd) == 96, "STA_CONFIG_CMD must be 96 bytes");
|
||||
|
||||
struct IwxStaRemoveCmd {
|
||||
uint32_t sta_id;
|
||||
} __attribute__((packed));
|
||||
|
||||
// =============================================================================
|
||||
// TX path (AX210 / "new TX API")
|
||||
// =============================================================================
|
||||
|
||||
// TX_CMD on a data queue carries the *short* 4-byte command header and stays in
|
||||
// group 0; only host commands on the command queue are re-tagged into
|
||||
// LONG_GROUP. Layout of a queued frame:
|
||||
//
|
||||
// [IwxCmdHeader 4][IwxTxCmdGen3 28][802.11 header][pad to 4][payload]
|
||||
//
|
||||
// TB0 covers the first 20 bytes, TB1 the rest of the header block (dword
|
||||
// aligned) and TB2 the payload, matching iwl_txq_gen2_build_tx().
|
||||
|
||||
struct IwxDramSecInfo {
|
||||
uint32_t pn_low;
|
||||
uint16_t pn_high;
|
||||
uint16_t aux_info;
|
||||
} __attribute__((packed)); // DRAM_SEC_INFO_API_S_VER_1
|
||||
|
||||
struct IwxTxCmdGen3 {
|
||||
uint16_t len; // total 802.11 frame length, plaintext
|
||||
uint16_t flags; // IWX_TX_FLAGS_*
|
||||
uint32_t offload_assist;
|
||||
IwxDramSecInfo dram_info;
|
||||
uint32_t rate_n_flags;
|
||||
uint8_t reserved[8]; // named "ttl" in TX_CMD_API_S_VER_8
|
||||
// 802.11 header follows
|
||||
} __attribute__((packed)); // TX_CMD_API_S_VER_8 / _10
|
||||
|
||||
static_assert(sizeof(IwxTxCmdGen3) == 28, "TX command header must stay 28 bytes");
|
||||
|
||||
// iwl_tx_cmd_flags (TX_FLAGS_BITS_API_S_VER_3)
|
||||
constexpr uint16_t IWX_TX_FLAGS_CMD_RATE = 1 << 0; // use rate_n_flags
|
||||
constexpr uint16_t IWX_TX_FLAGS_ENCRYPT_DIS = 1 << 1; // send in the clear
|
||||
constexpr uint16_t IWX_TX_FLAGS_HIGH_PRI = 1 << 2;
|
||||
constexpr uint16_t IWX_TX_FLAGS_RTS = 1 << 3;
|
||||
constexpr uint16_t IWX_TX_FLAGS_CTS = 1 << 4;
|
||||
|
||||
// iwl_tx_offload_assist_flags_pos
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_MH_SIZE_POS = 8; // header length in words
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_MH_MASK = 0x1f;
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_PAD = 1u << 13;
|
||||
constexpr uint32_t IWX_TX_CMD_OFFLD_AMSDU = 1u << 14;
|
||||
|
||||
// rate_n_flags. The firmware advertises which encoding it wants: TX_CMD
|
||||
// notification version 7 and later use the "version 2" layout, where bits 10-8
|
||||
// select the modulation and bits 3-0 index the legacy rate table. Older
|
||||
// firmware uses the version 1 layout, which carries the PLCP value directly and
|
||||
// flags CCK with bit 9. Antenna selection sits at bits 15-14 in both.
|
||||
constexpr uint32_t IWX_RATE_MCS_ANT_POS = 14;
|
||||
constexpr uint32_t IWX_RATE_MCS_ANT_A = 1u << IWX_RATE_MCS_ANT_POS;
|
||||
constexpr uint32_t IWX_RATE_MCS_ANT_B = 2u << IWX_RATE_MCS_ANT_POS;
|
||||
|
||||
// version 2
|
||||
constexpr uint32_t IWX_RATE_MCS_MOD_TYPE_POS = 8;
|
||||
constexpr uint32_t IWX_RATE_MCS_MOD_CCK = 0u << IWX_RATE_MCS_MOD_TYPE_POS;
|
||||
constexpr uint32_t IWX_RATE_MCS_MOD_LEGACY_OFDM = 1u << IWX_RATE_MCS_MOD_TYPE_POS;
|
||||
constexpr uint32_t IWX_RATE_LEGACY_RATE_MSK = 0x7;
|
||||
constexpr uint32_t IWX_RATE_MCS_CHAN_WIDTH_20 = 0u << 11;
|
||||
|
||||
// version 1 (PLCP encoded directly in bits 7-0)
|
||||
constexpr uint32_t IWX_RATE_MCS_CCK_MSK_V1 = 1u << 9;
|
||||
constexpr uint8_t IWX_RATE_1M_PLCP = 10;
|
||||
constexpr uint8_t IWX_RATE_6M_PLCP = 13;
|
||||
|
||||
// TX response (TX_CMD notification). Only the leading fields are used: the
|
||||
// driver just needs the slot back and a success/failure verdict.
|
||||
constexpr uint32_t IWX_TX_STATUS_MSK = 0x000000ff;
|
||||
constexpr uint32_t IWX_TX_STATUS_SUCCESS = 0x01;
|
||||
constexpr uint32_t IWX_TX_STATUS_DIRECT_DONE = 0x02;
|
||||
|
||||
struct IwxTxResp {
|
||||
uint8_t frame_count;
|
||||
uint8_t bt_kill_count;
|
||||
uint8_t failure_rts;
|
||||
uint8_t failure_frame;
|
||||
uint32_t initial_rate;
|
||||
uint16_t wireless_media_time;
|
||||
uint8_t pa_status;
|
||||
uint8_t pa_integ_res_a[3];
|
||||
uint8_t pa_integ_res_b[3];
|
||||
uint8_t pa_integ_res_c[3];
|
||||
uint16_t measurement_req_id;
|
||||
uint8_t reduced_tpc;
|
||||
uint8_t reserved;
|
||||
uint32_t tfd_info;
|
||||
uint16_t seq_ctl;
|
||||
uint16_t byte_cnt;
|
||||
uint8_t tlc_info;
|
||||
uint8_t ra_tid;
|
||||
uint16_t frame_ctrl;
|
||||
// followed by per-frame status entries; entry 0 is what matters here
|
||||
uint16_t status;
|
||||
uint16_t sequence;
|
||||
} __attribute__((packed));
|
||||
|
||||
// =============================================================================
|
||||
// ADD_STA_KEY (hardware key installation)
|
||||
// =============================================================================
|
||||
|
||||
// iwl_sta_key_flag
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_NO_ENC = 0 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_WEP = 1 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_CCM = 2 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_TKIP = 3 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_EXT = 4 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_GCMP = 5 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_CMAC = 6 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_EN_MSK = 7 << 0;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_WEP_KEY_MAP = 1 << 3;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_KEYID_POS = 8;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_KEYID_MSK = 3 << IWX_STA_KEY_FLG_KEYID_POS;
|
||||
constexpr uint16_t IWX_STA_KEY_NOT_VALID = 1 << 11;
|
||||
constexpr uint16_t IWX_STA_KEY_FLG_KEY_32BYTES = 1 << 12;
|
||||
constexpr uint16_t IWX_STA_KEY_MULTICAST = 1 << 14;
|
||||
constexpr uint16_t IWX_STA_KEY_MFP = 1 << 15;
|
||||
|
||||
struct IwxAddStaKeyCommon {
|
||||
uint8_t sta_id;
|
||||
uint8_t key_offset;
|
||||
uint16_t key_flags;
|
||||
uint8_t key[32];
|
||||
uint8_t rx_secur_seq_cnt[16];
|
||||
} __attribute__((packed));
|
||||
|
||||
struct IwxAddStaKeyCmd {
|
||||
IwxAddStaKeyCommon common;
|
||||
uint64_t rx_mic_key;
|
||||
uint64_t tx_mic_key;
|
||||
uint64_t transmit_seq_cnt;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxAddStaKeyCmd) == 76, "ADD_STA_KEY layout changed");
|
||||
|
||||
// SEC_KEY_CMD (DATA_PATH group): key installation for the MLD firmware. The
|
||||
// legacy ADD_STA_KEY above is absent from fw 89 like the rest of the legacy
|
||||
// station API; the Linux trace installs both the PTK and the GTK with this.
|
||||
constexpr uint8_t IWX_SEC_KEY_CMD = 0x18;
|
||||
|
||||
// iwl_sec_key_flags: cipher in the low 3 bits, modifiers above.
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_CCMP = 0x02;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_TKIP = 0x03;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_CIPHER_GCMP = 0x05;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_NO_TX = 0x08;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_KEY_SIZE = 0x10; // 256-bit key
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_MFP = 0x20;
|
||||
constexpr uint32_t IWX_SEC_KEY_FLAG_MCAST_KEY = 0x40;
|
||||
|
||||
struct IwxSecKeyCmd { // SEC_KEY_CMD_API_S_VER_1 (the add form)
|
||||
uint32_t action; // IWX_FW_CTXT_ACTION_*
|
||||
uint32_t sta_mask;
|
||||
uint32_t key_id;
|
||||
uint32_t key_flags;
|
||||
uint8_t key[32];
|
||||
uint8_t tkip_mic_rx_key[8];
|
||||
uint8_t tkip_mic_tx_key[8];
|
||||
uint64_t rx_seq;
|
||||
uint64_t tx_seq;
|
||||
} __attribute__((packed));
|
||||
|
||||
static_assert(sizeof(IwxSecKeyCmd) == 80, "SEC_KEY_CMD must be 80 bytes");
|
||||
|
||||
// =============================================================================
|
||||
// Firmware error log (read from device SRAM after an assert)
|
||||
// =============================================================================
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
|
||||
#include "Iwx.hpp"
|
||||
#include "Ieee80211.hpp"
|
||||
#include <Pci/Pci.hpp>
|
||||
#include <Memory/HHDM.hpp>
|
||||
#include <Memory/Paging.hpp>
|
||||
@@ -459,11 +460,15 @@ namespace Drivers::Net::Wifi {
|
||||
asm volatile("" ::: "memory");
|
||||
}
|
||||
|
||||
static bool IwxAllocTxRing(IwxTxRing& ring, int qid) {
|
||||
// `stageSlots` reserves one page per concurrently queued frame; pass 0 for
|
||||
// queues that only ever carry host commands.
|
||||
static bool IwxAllocTxRing(IwxTxRing& ring, int qid, uint32_t stageSlots = 0) {
|
||||
ring.Qid = qid;
|
||||
ring.Cur = 0;
|
||||
ring.CurHw = 0;
|
||||
ring.Queued = 0;
|
||||
ring.StageSlots = 0;
|
||||
ring.Active = false;
|
||||
|
||||
if (!IwxDmaAlloc(ring.Desc, sizeof(IwxTfhTfd) * IWX_TX_RING_COUNT))
|
||||
return false;
|
||||
@@ -476,6 +481,15 @@ namespace Drivers::Net::Wifi {
|
||||
// are staged in this page instead of the per-slot command area.
|
||||
if (!IwxDmaAlloc(ring.Bounce, 4096))
|
||||
return false;
|
||||
|
||||
if (stageSlots > IWX_TX_STAGE_SLOTS) stageSlots = IWX_TX_STAGE_SLOTS;
|
||||
for (uint32_t i = 0; i < stageSlots; i++) {
|
||||
void* p = Memory::g_pfa->AllocateZeroed();
|
||||
if (!p) return false;
|
||||
ring.Stage[i] = (uint8_t*)p;
|
||||
ring.StagePhys[i] = Memory::SubHHDM(p);
|
||||
ring.StageSlots = i + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -484,6 +498,14 @@ namespace Drivers::Net::Wifi {
|
||||
IwxDmaFree(ring.BcTbl);
|
||||
IwxDmaFree(ring.Cmd);
|
||||
IwxDmaFree(ring.Bounce);
|
||||
for (uint32_t i = 0; i < ring.StageSlots; i++) {
|
||||
if (ring.Stage[i]) {
|
||||
Memory::g_pfa->Free(ring.Stage[i]);
|
||||
ring.Stage[i] = nullptr;
|
||||
}
|
||||
}
|
||||
ring.StageSlots = 0;
|
||||
ring.Active = false;
|
||||
}
|
||||
|
||||
static void IwxResetTxRing(IwxTxRing& ring) {
|
||||
@@ -846,6 +868,9 @@ namespace Drivers::Net::Wifi {
|
||||
return -1;
|
||||
}
|
||||
|
||||
void IwxDumpFwError();
|
||||
static uint32_t g_cmdTimeouts = 0; // consecutive unanswered commands
|
||||
|
||||
bool IwxSendCmd(IwxHostCmd& hcmd) {
|
||||
if (g_iwx.State == IwxFwState::Error) return false;
|
||||
|
||||
@@ -909,6 +934,12 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
g_iwx.CmdDone = false;
|
||||
g_iwx.LastCmdId = code;
|
||||
// Keep the payload so a firmware assert can show exactly what it
|
||||
// choked on -- struct mismatches are invisible without the bytes.
|
||||
g_iwx.LastCmdLen = hcmd.Len;
|
||||
uint32_t keep = hcmd.Len < sizeof(g_iwx.LastCmdPayload)
|
||||
? hcmd.Len : (uint32_t)sizeof(g_iwx.LastCmdPayload);
|
||||
if (hcmd.Data && keep) memcpy(g_iwx.LastCmdPayload, hcmd.Data, keep);
|
||||
g_iwx.CmdWantResp = hcmd.WantResp;
|
||||
g_iwx.CmdRespLen = 0;
|
||||
g_iwx.CmdIdx = idx;
|
||||
@@ -921,18 +952,47 @@ namespace Drivers::Net::Wifi {
|
||||
// Wait for the firmware's response/ack. Commands are serialized by
|
||||
// CmdLock, so exactly one can be in flight and the completion is
|
||||
// unambiguous.
|
||||
// Two independent bounds. The wall clock is the intended one, but it
|
||||
// is driven by the timer interrupt, so anything that leaves this loop
|
||||
// running with interrupts disabled would spin forever and take the
|
||||
// whole machine down with it -- the spin cap makes that impossible.
|
||||
// Bail immediately if the firmware has asserted, because it will never
|
||||
// answer this or any later command.
|
||||
constexpr uint32_t MAX_SPINS = 20000; // ~2 s at 100 us
|
||||
bool ok = false;
|
||||
bool died = false;
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
while (Timekeeping::GetMilliseconds() - start < 1000) {
|
||||
for (uint32_t spins = 0; spins < MAX_SPINS; spins++) {
|
||||
IwxProcessEvents();
|
||||
if (g_iwx.CmdDone) { ok = true; break; }
|
||||
if (g_iwx.State == IwxFwState::Error) { died = true; break; }
|
||||
if (Timekeeping::GetMilliseconds() - start >= 1000) break;
|
||||
IwxDelayUs(100);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Command 0x" << base::hex
|
||||
<< (uint64_t)code << base::dec << " timed out";
|
||||
<< (uint64_t)code << base::dec
|
||||
<< (died ? " abandoned: firmware has stopped responding"
|
||||
: " timed out");
|
||||
if (ring.Queued > 0) ring.Queued--;
|
||||
if (!died) {
|
||||
// Dump on the first silence: the firmware's error table names
|
||||
// the command that asserted, and it is overwritten as later
|
||||
// commands go unanswered.
|
||||
if (++g_cmdTimeouts == 1) IwxDumpFwError();
|
||||
// Repeated silence means it is wedged and every later command
|
||||
// would burn the same timeout, so stop trying. A single late
|
||||
// response is not worth disabling the adapter over.
|
||||
if (g_cmdTimeouts >= 3) {
|
||||
KernelLogStream(ERROR, "WiFi")
|
||||
<< "Firmware stopped responding to host commands";
|
||||
g_iwx.FwErrors++;
|
||||
g_iwx.State = IwxFwState::Error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
g_cmdTimeouts = 0;
|
||||
}
|
||||
|
||||
g_iwx.CmdWantResp = false;
|
||||
@@ -969,10 +1029,10 @@ namespace Drivers::Net::Wifi {
|
||||
// TX queue configuration (used by the connect path)
|
||||
// =========================================================================
|
||||
|
||||
bool IwxEnableTxq(int staId, int qid, int tid) {
|
||||
IwxTxRing& ring = g_iwx.MgmtQ;
|
||||
bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) {
|
||||
IwxResetTxRing(ring);
|
||||
ring.Qid = qid;
|
||||
ring.Active = false;
|
||||
|
||||
int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD);
|
||||
|
||||
@@ -1016,14 +1076,280 @@ namespace Drivers::Net::Wifi {
|
||||
return false;
|
||||
auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf;
|
||||
auto* resp = (IwxTxQueueCfgRsp*)pkt->data;
|
||||
|
||||
// On the v3 data-path API the firmware owns queue assignment: `qid` is
|
||||
// only a hint and the response names the queue we actually got.
|
||||
if (resp->queue_number != qid) {
|
||||
KernelLogStream(WARNING, "WiFi") << "Firmware assigned queue "
|
||||
<< (uint64_t)resp->queue_number << ", expected " << (uint64_t)qid;
|
||||
KernelLogStream(INFO, "WiFi") << "Firmware assigned TX queue "
|
||||
<< (uint64_t)resp->queue_number << " (asked for "
|
||||
<< (uint64_t)qid << ")";
|
||||
}
|
||||
ring.Qid = resp->queue_number;
|
||||
ring.Active = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) {
|
||||
if (!ring.Active) return;
|
||||
ring.Active = false;
|
||||
|
||||
int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD);
|
||||
if (cmdVer == 3) {
|
||||
IwxScdQueueCfgCmd cmd = {};
|
||||
cmd.operation = IWX_SCD_QUEUE_REMOVE;
|
||||
cmd.u.remove.sta_mask = 1u << staId;
|
||||
cmd.u.remove.tid = (uint32_t)tid;
|
||||
IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD),
|
||||
&cmd, sizeof(cmd));
|
||||
} else {
|
||||
IwxTxQueueCfgCmd cmd = {};
|
||||
cmd.sta_id = (uint8_t)staId;
|
||||
cmd.tid = (uint8_t)tid;
|
||||
cmd.flags = 0; // clear ENABLE_QUEUE
|
||||
cmd.cb_size = IWX_TFD_QUEUE_CB_SIZE(IWX_TX_RING_COUNT);
|
||||
cmd.byte_cnt_addr = ring.BcTbl.Phys;
|
||||
cmd.tfdq_addr = ring.Desc.Phys;
|
||||
IwxSendCmdPdu(IWX_SCD_QUEUE_CFG, &cmd, sizeof(cmd));
|
||||
}
|
||||
IwxResetTxRing(ring);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Frame transmission
|
||||
// =========================================================================
|
||||
|
||||
// Lowest usable transmit antenna, as a rate_n_flags antenna field.
|
||||
static uint32_t IwxTxAntBits() {
|
||||
uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_TX_CHAIN)
|
||||
>> IWX_FW_PHY_CFG_TX_CHAIN_POS);
|
||||
if (g_iwx.Nvm.ValidTxAnt) ant &= g_iwx.Nvm.ValidTxAnt;
|
||||
if (!ant) ant = 1;
|
||||
uint8_t lowest = (uint8_t)(ant & (uint8_t)(~ant + 1)); // isolate low bit
|
||||
return (uint32_t)lowest << IWX_RATE_MCS_ANT_POS;
|
||||
}
|
||||
|
||||
// The lowest basic rate for the current band, in whichever rate_n_flags
|
||||
// encoding the firmware advertises: 1 Mbps CCK on 2.4 GHz, 6 Mbps OFDM on
|
||||
// 5 GHz. Management frames go out at this rate because rate control has
|
||||
// no table for the station until it is associated.
|
||||
static uint32_t IwxLowestRate() {
|
||||
uint32_t ant = IwxTxAntBits();
|
||||
|
||||
// Firmware exposing TX_CMD notification version 7 or later (equally,
|
||||
// command version 9+) uses the "version 2" rate layout.
|
||||
int notifVer = IwxLookupNotifVer(IWX_LONG_GROUP, IWX_TX_CMD);
|
||||
int cmdVer = IwxLookupCmdVer(IWX_LONG_GROUP, IWX_TX_CMD);
|
||||
bool v2 = notifVer > 6 || cmdVer >= 9;
|
||||
|
||||
if (v2) {
|
||||
uint32_t mod = g_iwx.Is5GHz ? IWX_RATE_MCS_MOD_LEGACY_OFDM
|
||||
: IWX_RATE_MCS_MOD_CCK;
|
||||
return ant | mod | IWX_RATE_MCS_CHAN_WIDTH_20 | 0u; // index 0
|
||||
}
|
||||
|
||||
uint32_t plcp = g_iwx.Is5GHz ? IWX_RATE_6M_PLCP : IWX_RATE_1M_PLCP;
|
||||
uint32_t cck = g_iwx.Is5GHz ? 0 : IWX_RATE_MCS_CCK_MSK_V1;
|
||||
return ant | cck | plcp;
|
||||
}
|
||||
|
||||
// Recover a queue whose completions stopped arriving. Without this a
|
||||
// single lost TX response would permanently consume a slot and, after
|
||||
// StageSlots of them, wedge the queue.
|
||||
static uint64_t g_txStallMs = 0;
|
||||
|
||||
static bool IwxTxQueueHasRoom(IwxTxRing& ring) {
|
||||
if (ring.Queued < ring.StageSlots) {
|
||||
g_txStallMs = 0;
|
||||
return true;
|
||||
}
|
||||
uint64_t now = Timekeeping::GetMilliseconds();
|
||||
if (g_txStallMs == 0) {
|
||||
g_txStallMs = now;
|
||||
return false;
|
||||
}
|
||||
if (now - g_txStallMs < 2000) return false;
|
||||
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "TX queue " << (uint64_t)ring.Qid
|
||||
<< " stopped completing; resetting its outstanding count";
|
||||
ring.Queued = 0;
|
||||
g_txStallMs = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen,
|
||||
const uint8_t* payload, uint32_t payloadLen,
|
||||
bool encrypt, bool fixedRate) {
|
||||
if (g_iwx.State != IwxFwState::Running) return false;
|
||||
if (!ring.Active || ring.StageSlots == 0) return false;
|
||||
if (!hdr || hdrLen < IEEE80211_HDR_LEN || hdrLen > 32) return false;
|
||||
|
||||
// The 802.11 header is padded to a dword boundary before the payload;
|
||||
// TX_CMD_OFFLD_PAD tells the firmware to skip those bytes.
|
||||
uint32_t padded = (hdrLen + 3) & ~3u;
|
||||
uint32_t head = (uint32_t)(sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3)) + padded;
|
||||
if (head + payloadLen > 4096) return false;
|
||||
|
||||
g_iwx.TxLock.Acquire();
|
||||
|
||||
if (!IwxTxQueueHasRoom(ring)) {
|
||||
g_iwx.TxLock.Release();
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t idx = ring.Cur;
|
||||
uint32_t slot = idx % ring.StageSlots;
|
||||
uint8_t* buf = ring.Stage[slot];
|
||||
uint64_t phys = ring.StagePhys[slot];
|
||||
|
||||
memset(buf, 0, head);
|
||||
|
||||
auto* ch = (IwxCmdHeader*)buf;
|
||||
ch->code = IWX_TX_CMD;
|
||||
ch->flags = 0; // TX_CMD stays in the legacy group
|
||||
ch->idx = (uint8_t)idx;
|
||||
ch->qid = (uint8_t)ring.Qid;
|
||||
|
||||
auto* tx = (IwxTxCmdGen3*)(buf + sizeof(IwxCmdHeader));
|
||||
tx->len = (uint16_t)(hdrLen + payloadLen);
|
||||
|
||||
uint16_t flags = 0;
|
||||
if (!encrypt) flags |= IWX_TX_FLAGS_ENCRYPT_DIS;
|
||||
if (fixedRate) {
|
||||
flags |= IWX_TX_FLAGS_CMD_RATE;
|
||||
tx->rate_n_flags = IwxLowestRate();
|
||||
}
|
||||
tx->flags = flags;
|
||||
|
||||
uint32_t offload = ((hdrLen / 2) & IWX_TX_CMD_OFFLD_MH_MASK)
|
||||
<< IWX_TX_CMD_OFFLD_MH_SIZE_POS;
|
||||
if (hdrLen % 4) offload |= IWX_TX_CMD_OFFLD_PAD;
|
||||
tx->offload_assist = offload;
|
||||
|
||||
uint8_t* body = buf + sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3);
|
||||
memcpy(body, hdr, hdrLen);
|
||||
if (payloadLen) memcpy(body + padded, payload, payloadLen);
|
||||
|
||||
auto* desc = &((IwxTfhTfd*)ring.Desc.Virt)[idx];
|
||||
memset(desc, 0, sizeof(*desc));
|
||||
desc->tbs[0].tb_len = (uint16_t)IWX_FIRST_TB_SIZE;
|
||||
desc->tbs[0].addr = phys;
|
||||
desc->tbs[1].tb_len = (uint16_t)(head - IWX_FIRST_TB_SIZE);
|
||||
desc->tbs[1].addr = phys + IWX_FIRST_TB_SIZE;
|
||||
uint16_t numTbs = 2;
|
||||
if (payloadLen) {
|
||||
desc->tbs[2].tb_len = (uint16_t)payloadLen;
|
||||
desc->tbs[2].addr = phys + head;
|
||||
numTbs = 3;
|
||||
}
|
||||
desc->num_tbs = numTbs;
|
||||
|
||||
// Byte-count table: AX210 wants the frame length in bytes plus the
|
||||
// number of extra 64-byte chunks the firmware must fetch for the TFD.
|
||||
uint32_t filled = (uint32_t)(sizeof(uint16_t) + numTbs * sizeof(IwxTfhTb));
|
||||
uint32_t chunks = ((filled + 63) / 64) - 1;
|
||||
auto* bc = (IwxGen3BcTblEntry*)ring.BcTbl.Virt;
|
||||
bc[idx].tfd_offset = (uint16_t)((hdrLen + payloadLen) | (chunks << 14));
|
||||
|
||||
asm volatile("" ::: "memory");
|
||||
|
||||
ring.Queued++;
|
||||
ring.Cur = (ring.Cur + 1) % IWX_TX_RING_COUNT;
|
||||
ring.CurHw = (ring.CurHw + 1) % IWX_TFD_QUEUE_SIZE_MAX_GEN3;
|
||||
IwxWrite32(IWX_HBUS_TARG_WRPTR, ((uint32_t)ring.Qid << 16) | ring.CurHw);
|
||||
|
||||
g_iwx.TxLock.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
void IwxTxComplete(int qid, int idx, uint32_t status) {
|
||||
(void)idx;
|
||||
IwxTxRing* ring = nullptr;
|
||||
if (g_iwx.MgmtQ.Active && qid == g_iwx.MgmtQ.Qid) ring = &g_iwx.MgmtQ;
|
||||
if (!ring) return;
|
||||
|
||||
g_iwx.TxLock.Acquire();
|
||||
if (ring->Queued > 0) ring->Queued--;
|
||||
g_iwx.TxLock.Release();
|
||||
|
||||
if (status == IWX_TX_STATUS_SUCCESS || status == IWX_TX_STATUS_DIRECT_DONE)
|
||||
g_iwx.TxPackets++;
|
||||
else
|
||||
g_iwx.TxFailures++;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Hardware key installation
|
||||
// =========================================================================
|
||||
|
||||
// Keys go in and out through SEC_KEY_CMD: the MLD firmware does not
|
||||
// implement the legacy ADD_STA_KEY, like the rest of the legacy station
|
||||
// API. Values mirror the Linux trace: PTK as {sta_mask 1, key_id 0,
|
||||
// flags CIPHER}, GTK as {sta_mask 1, key_id N, flags CIPHER|MCAST}.
|
||||
|
||||
static uint32_t SecKeyFlags(uint8_t cipher, uint32_t keyLen, bool pairwise) {
|
||||
uint32_t flags;
|
||||
switch (cipher) {
|
||||
case RSN_CIPHER_CCMP:
|
||||
case RSN_CIPHER_CCMP_256:
|
||||
flags = IWX_SEC_KEY_FLAG_CIPHER_CCMP;
|
||||
break;
|
||||
case RSN_CIPHER_GCMP:
|
||||
case RSN_CIPHER_GCMP_256:
|
||||
flags = IWX_SEC_KEY_FLAG_CIPHER_GCMP;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
if (keyLen == 32) flags |= IWX_SEC_KEY_FLAG_KEY_SIZE;
|
||||
if (!pairwise) flags |= IWX_SEC_KEY_FLAG_MCAST_KEY;
|
||||
return flags;
|
||||
}
|
||||
|
||||
bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx,
|
||||
bool pairwise, uint8_t cipher, const uint8_t* rsc) {
|
||||
if (!key || (keyLen != 16 && keyLen != 32)) return false;
|
||||
|
||||
uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise);
|
||||
if (!flags) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Cannot install a key for cipher " << (uint64_t)cipher;
|
||||
return false;
|
||||
}
|
||||
|
||||
IwxSecKeyCmd cmd = {};
|
||||
cmd.action = IWX_FW_CTXT_ACTION_ADD;
|
||||
cmd.sta_mask = 1u << IWX_STATION_ID;
|
||||
cmd.key_id = keyIdx;
|
||||
cmd.key_flags = flags;
|
||||
memcpy(cmd.key, key, keyLen);
|
||||
|
||||
// The EAPOL RSC carries the AP's packet number for the group key,
|
||||
// lowest byte first; it becomes the initial receive counter.
|
||||
if (rsc) {
|
||||
uint64_t pn = 0;
|
||||
for (int i = 5; i >= 0; i--) pn = (pn << 8) | rsc[i];
|
||||
cmd.rx_seq = pn;
|
||||
}
|
||||
|
||||
return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD),
|
||||
&cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher,
|
||||
uint32_t keyLen) {
|
||||
uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise);
|
||||
if (!flags) return false;
|
||||
|
||||
IwxSecKeyCmd cmd = {};
|
||||
cmd.action = IWX_FW_CTXT_ACTION_REMOVE;
|
||||
cmd.sta_mask = 1u << IWX_STATION_ID;
|
||||
cmd.key_id = keyIdx;
|
||||
cmd.key_flags = flags;
|
||||
return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD),
|
||||
&cmd, sizeof(cmd));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// RX / notification processing
|
||||
// =========================================================================
|
||||
@@ -1083,7 +1409,24 @@ namespace Drivers::Net::Wifi {
|
||||
// between them they pin down which host command the firmware rejected.
|
||||
void IwxDumpFwError() {
|
||||
KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert; last command sent: 0x"
|
||||
<< base::hex << (uint64_t)g_iwx.LastCmdId << base::dec;
|
||||
<< base::hex << (uint64_t)g_iwx.LastCmdId << base::dec
|
||||
<< " (" << (uint64_t)g_iwx.LastCmdLen << " byte payload)";
|
||||
|
||||
// Dump the payload: a struct that does not match the firmware's
|
||||
// expected layout is otherwise impossible to spot from the log.
|
||||
{
|
||||
uint32_t n = g_iwx.LastCmdLen;
|
||||
if (n > sizeof(g_iwx.LastCmdPayload)) n = sizeof(g_iwx.LastCmdPayload);
|
||||
for (uint32_t off = 0; off < n; off += 32) {
|
||||
auto line = KernelLogStream(INFO, "WiFi-FW");
|
||||
line << " +" << (uint64_t)off << ": " << base::hex;
|
||||
for (uint32_t i = off; i < n && i < off + 32; i++) {
|
||||
if (g_iwx.LastCmdPayload[i] < 0x10) line << "0";
|
||||
line << (uint64_t)g_iwx.LastCmdPayload[i];
|
||||
}
|
||||
line << base::dec;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t base_ = g_iwx.UmacErrorTable;
|
||||
if (base_ < 0x400000) {
|
||||
@@ -1141,6 +1484,16 @@ namespace Drivers::Net::Wifi {
|
||||
case IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_PNVM_INIT_COMPLETE):
|
||||
g_iwx.InitComplete |= 0x2;
|
||||
break;
|
||||
case IWX_TX_CMD: {
|
||||
// TX completion for a frame we queued on a data/mgmt queue.
|
||||
uint32_t status = 0;
|
||||
if (IwxRxPacketPayloadLen(pkt) >= sizeof(IwxTxResp)) {
|
||||
auto* r = (const IwxTxResp*)pkt->data;
|
||||
status = r->status & IWX_TX_STATUS_MSK;
|
||||
}
|
||||
IwxTxComplete(qid & ~0x80, pkt->hdr.idx, status);
|
||||
break;
|
||||
}
|
||||
case IWX_REPLY_ERROR: {
|
||||
if (IwxRxPacketPayloadLen(pkt) >= 8) {
|
||||
uint32_t errType = *(const uint32_t*)pkt->data;
|
||||
@@ -1208,8 +1561,9 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
void IwxProcessEvents() {
|
||||
if (!g_iwx.Mmio) return;
|
||||
if (g_iwx.InProcessEvents) return; // never nest
|
||||
g_iwx.InProcessEvents = true;
|
||||
// Never nest. With interrupts enabled during command waits this is a
|
||||
// genuine multi-core race, so it has to be an atomic test-and-set.
|
||||
if (g_iwx.InProcessEvents.test_and_set(std::memory_order_acquire)) return;
|
||||
|
||||
if (g_msix) {
|
||||
uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD);
|
||||
@@ -1276,7 +1630,7 @@ namespace Drivers::Net::Wifi {
|
||||
IwxNotifIntr();
|
||||
|
||||
g_iwx.WorkPending = false;
|
||||
g_iwx.InProcessEvents = false;
|
||||
g_iwx.InProcessEvents.clear(std::memory_order_release);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -1435,7 +1789,8 @@ namespace Drivers::Net::Wifi {
|
||||
|| !IwxDmaAlloc(g_iwx.PrphInfo, 4096)
|
||||
|| !IwxAllocRxRing()
|
||||
|| !IwxAllocTxRing(g_iwx.CmdQ, IWX_DQA_CMD_QUEUE)
|
||||
|| !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE)) {
|
||||
|| !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE,
|
||||
IWX_TX_STAGE_SLOTS)) {
|
||||
KernelLogStream(ERROR, "WiFi") << "Could not allocate device DMA memory";
|
||||
IwxFreeRxRing();
|
||||
IwxFreeTxRing(g_iwx.CmdQ);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Wifi.hpp"
|
||||
#include "Iwx.hpp"
|
||||
#include "Wpa.hpp"
|
||||
#include <Fs/Vfs.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
@@ -20,6 +21,7 @@
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Hal/SmpBoot.hpp>
|
||||
#include <Sched/Scheduler.hpp>
|
||||
#include <atomic>
|
||||
|
||||
using namespace Kt;
|
||||
@@ -36,6 +38,10 @@ namespace Drivers::Net::Wifi {
|
||||
|
||||
static constexpr int MAX_SCAN_RESULTS = 64;
|
||||
|
||||
// Long enough for a personal-mode RSN element: version, group cipher, one
|
||||
// or two pairwise ciphers, a handful of AKMs and the capability field.
|
||||
static constexpr int MAX_RSN_IE = 64;
|
||||
|
||||
struct ScanEntry {
|
||||
uint8_t Bssid[6];
|
||||
char Ssid[33];
|
||||
@@ -45,6 +51,9 @@ namespace Drivers::Net::Wifi {
|
||||
uint8_t Band; // 0 = 2.4 GHz, 1 = 5 GHz
|
||||
uint8_t Security; // WifiSecurity value
|
||||
uint16_t BeaconInterval;
|
||||
uint8_t DtimPeriod;
|
||||
uint8_t RsnIe[MAX_RSN_IE]; // element body, without id/len
|
||||
uint8_t RsnIeLen;
|
||||
bool Used;
|
||||
};
|
||||
|
||||
@@ -71,6 +80,7 @@ namespace Drivers::Net::Wifi {
|
||||
// Element IDs used here.
|
||||
static constexpr uint8_t ELEMID_SSID = 0;
|
||||
static constexpr uint8_t ELEMID_DSPARMS = 3;
|
||||
static constexpr uint8_t ELEMID_TIM = 5;
|
||||
static constexpr uint8_t ELEMID_RSN = 48;
|
||||
static constexpr uint8_t ELEMID_VENDOR = 221;
|
||||
|
||||
@@ -108,6 +118,9 @@ namespace Drivers::Net::Wifi {
|
||||
uint8_t Channel = 0;
|
||||
uint8_t Security = WIFI_SEC_OPEN;
|
||||
uint16_t BeaconInterval = 0;
|
||||
uint8_t DtimPeriod = 0;
|
||||
const uint8_t* Rsn = nullptr; // RSN element body
|
||||
uint8_t RsnLen = 0;
|
||||
};
|
||||
|
||||
static bool ParseBeacon(const uint8_t* frame, uint32_t len, ParsedBeacon* out) {
|
||||
@@ -139,9 +152,18 @@ namespace Drivers::Net::Wifi {
|
||||
case ELEMID_DSPARMS:
|
||||
if (ielen >= 1) out->Channel = body[0];
|
||||
break;
|
||||
case ELEMID_TIM:
|
||||
// DTIM count, then DTIM period. The firmware needs the
|
||||
// period to schedule wake-ups once associated.
|
||||
if (ielen >= 2) out->DtimPeriod = body[1];
|
||||
break;
|
||||
case ELEMID_RSN:
|
||||
haveRsn = true;
|
||||
out->Security = ClassifyRsn(body, ielen);
|
||||
// Kept verbatim: the connect path negotiates ciphers and
|
||||
// AKMs straight out of it.
|
||||
out->Rsn = body;
|
||||
out->RsnLen = ielen;
|
||||
break;
|
||||
case ELEMID_VENDOR:
|
||||
// WPA1: Microsoft OUI 00:50:F2, type 1.
|
||||
@@ -220,6 +242,12 @@ namespace Drivers::Net::Wifi {
|
||||
slot->Band = ch > 14 ? 1 : 0;
|
||||
slot->Security = pb.Security;
|
||||
slot->BeaconInterval = pb.BeaconInterval;
|
||||
if (pb.DtimPeriod) slot->DtimPeriod = pb.DtimPeriod;
|
||||
slot->RsnIeLen = 0;
|
||||
if (pb.Rsn && pb.RsnLen && pb.RsnLen <= MAX_RSN_IE) {
|
||||
memcpy(slot->RsnIe, pb.Rsn, pb.RsnLen);
|
||||
slot->RsnIeLen = pb.RsnLen;
|
||||
}
|
||||
|
||||
g_resultLock.Release();
|
||||
}
|
||||
@@ -371,8 +399,18 @@ namespace Drivers::Net::Wifi {
|
||||
| (g_iwx.Nvm.Sku52GHz ? 2 : 0));
|
||||
out->channels = (uint16_t)g_iwx.ChannelCount;
|
||||
out->rxPackets = g_iwx.RxPackets;
|
||||
out->txPackets = g_iwx.TxPackets;
|
||||
out->fwErrors = (uint32_t)g_iwx.FwErrors;
|
||||
out->connState = (uint32_t)IwxConnectState();
|
||||
out->connected = IwxLinkUp() ? 1 : 0;
|
||||
|
||||
if (IwxConnectState() != (int)IwxConnStateId::Idle) {
|
||||
const char* ssid = IwxConnectSsid();
|
||||
int k = 0;
|
||||
for (; k < 32 && ssid[k]; k++) out->ssid[k] = ssid[k];
|
||||
out->ssid[k] = '\0';
|
||||
memcpy(out->bssid, IwxConnectBssid(), 6);
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
for (; i < 31 && g_iwx.Fw.Version[i]; i++) out->fwVersion[i] = g_iwx.Fw.Version[i];
|
||||
@@ -380,17 +418,74 @@ namespace Drivers::Net::Wifi {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Wait for the association + handshake to settle. The whole exchange is
|
||||
// driven from IwxConnectService(), so this pumps both while it waits
|
||||
// instead of relying on the idle loop getting scheduled.
|
||||
static int WaitForConnection(uint32_t timeoutMs) {
|
||||
uint64_t start = Timekeeping::GetMilliseconds();
|
||||
|
||||
while (Timekeeping::GetMilliseconds() - start < timeoutMs) {
|
||||
IwxProcessEvents();
|
||||
IwxConnectService();
|
||||
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
if (state == IwxConnStateId::Connected) return 0;
|
||||
if (state == IwxConnStateId::Failed) {
|
||||
// A handshake that got as far as exchanging EAPOL frames and
|
||||
// then failed is almost always a wrong passphrase.
|
||||
int rc = WpaGetState() == WpaState::Failed
|
||||
? WIFI_ERR_AUTH : WIFI_ERR_FAILED;
|
||||
// A rejection seen on the RX path only marks the state; the
|
||||
// firmware contexts are still up and have to come back down.
|
||||
IwxConnectAbort();
|
||||
return rc;
|
||||
}
|
||||
if (state == IwxConnStateId::Idle) return WIFI_ERR_FAILED;
|
||||
if (g_iwx.State == IwxFwState::Error) {
|
||||
IwxConnectAbort();
|
||||
return WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
// Yield rather than burn the core: this can wait seconds, and the
|
||||
// idle loop on other cores drives the same state machine.
|
||||
Sched::Schedule();
|
||||
}
|
||||
|
||||
IwxConnectAbort();
|
||||
return WIFI_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
int Connect(const char* ssid, const char* password) {
|
||||
if (!g_initialized || !ssid) return -1;
|
||||
if (!g_initialized || !ssid) return WIFI_ERR_NO_ADAPTER;
|
||||
if (g_iwx.State != IwxFwState::Running) return WIFI_ERR_NO_ADAPTER;
|
||||
|
||||
// Locate the network in the most recent scan results: the firmware
|
||||
// contexts need its BSSID and channel.
|
||||
// contexts need its BSSID and channel, and the connect path needs its
|
||||
// RSN element to negotiate ciphers.
|
||||
uint8_t bssid[6];
|
||||
uint8_t channel = 0;
|
||||
bool is5 = false;
|
||||
uint8_t security = WIFI_SEC_OPEN;
|
||||
uint8_t rsnIe[MAX_RSN_IE];
|
||||
uint8_t rsnIeLen = 0;
|
||||
uint16_t beaconInterval = 0;
|
||||
uint8_t dtimPeriod = 0;
|
||||
bool found = false;
|
||||
|
||||
// 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();
|
||||
for (int i = 0; i < MAX_SCAN_RESULTS; i++) {
|
||||
if (!g_results[i].Used) continue;
|
||||
@@ -402,33 +497,57 @@ namespace Drivers::Net::Wifi {
|
||||
if (a == '\0') break;
|
||||
}
|
||||
if (!match) continue;
|
||||
if (found && e.Rssi <= bestRssi) continue;
|
||||
memcpy(bssid, e.Bssid, 6);
|
||||
channel = e.Channel;
|
||||
is5 = e.Band == 1;
|
||||
security = e.Security;
|
||||
rsnIeLen = e.RsnIeLen;
|
||||
if (rsnIeLen) memcpy(rsnIe, e.RsnIe, rsnIeLen);
|
||||
beaconInterval = e.BeaconInterval;
|
||||
dtimPeriod = e.DtimPeriod;
|
||||
bestRssi = e.Rssi;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
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
|
||||
// exchange (PMK derivation, EAPOL 4-way, HW key install) is not
|
||||
// implemented at all, so a passphrase would not help and reporting
|
||||
// "needs a passphrase" would be misleading.
|
||||
if (!found) return WIFI_ERR_NOT_FOUND;
|
||||
|
||||
bool havePassword = password && password[0];
|
||||
|
||||
// WEP and WPA1 use RC4/TKIP, which the supplicant deliberately does
|
||||
// not implement; say so rather than failing mid-handshake.
|
||||
if (security == WIFI_SEC_WEP || security == WIFI_SEC_WPA) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "WEP and the original WPA use ciphers this driver does not implement";
|
||||
return WIFI_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
if (security != WIFI_SEC_OPEN) {
|
||||
if (!rsnIeLen) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Encrypted networks are not supported yet (open only)";
|
||||
return -2;
|
||||
<< "The beacon for this network carried no RSN element";
|
||||
return WIFI_ERR_UNSUPPORTED;
|
||||
}
|
||||
if (!havePassword) return WIFI_ERR_NEED_KEY;
|
||||
}
|
||||
(void)password;
|
||||
|
||||
return IwxConnectStart(bssid, channel, is5, ssid) ? 0 : -1;
|
||||
if (!IwxConnectStart(bssid, channel, is5, ssid,
|
||||
havePassword ? password : nullptr,
|
||||
security == WIFI_SEC_OPEN ? nullptr : rsnIe,
|
||||
security == WIFI_SEC_OPEN ? 0 : rsnIeLen,
|
||||
beaconInterval, dtimPeriod)) {
|
||||
// IwxConnectStart already logged the specific reason. Only report
|
||||
// a security problem when that is genuinely what happened; a radio
|
||||
// or firmware failure needs completely different advice.
|
||||
bool security_ = IwxConnectRefusedForSecurity();
|
||||
auto state = (IwxConnStateId)IwxConnectState();
|
||||
if (state != IwxConnStateId::Idle) IwxConnectAbort();
|
||||
return security_ ? WIFI_ERR_UNSUPPORTED : WIFI_ERR_FAILED;
|
||||
}
|
||||
|
||||
return WaitForConnection(15000);
|
||||
}
|
||||
|
||||
int Disconnect() {
|
||||
@@ -436,4 +555,26 @@ namespace Drivers::Net::Wifi {
|
||||
IwxConnectAbort();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Network interface
|
||||
// =========================================================================
|
||||
|
||||
static RxCallback g_rxCallback = nullptr;
|
||||
|
||||
const uint8_t* GetMacAddress() { return g_iwx.Nvm.HwAddr; }
|
||||
|
||||
bool IsLinkUp() { return g_initialized && IwxLinkUp(); }
|
||||
|
||||
bool SendPacket(const uint8_t* data, uint16_t length) {
|
||||
if (!IsLinkUp()) return false;
|
||||
return IwxConnectSendEthernet(data, length);
|
||||
}
|
||||
|
||||
void SetRxCallback(RxCallback callback) { g_rxCallback = callback; }
|
||||
|
||||
// Called by the connect path for every decapsulated Ethernet frame.
|
||||
void WifiRxEthernet(const uint8_t* frame, uint32_t len) {
|
||||
if (g_rxCallback && len <= 0xffff) g_rxCallback(frame, (uint16_t)len);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,24 @@ namespace Drivers::Net::Wifi {
|
||||
// Fill in adapter/firmware status.
|
||||
int GetInfo(montauk::abi::WifiInfo* out);
|
||||
|
||||
// Association groundwork. Returns 0 when the firmware contexts came up,
|
||||
// negative on failure. See IwxConnect.cpp: the 802.11 handshake itself is
|
||||
// not implemented yet, so this cannot establish a usable link.
|
||||
// Join a network. Blocks (pumping firmware events) until the link is up
|
||||
// or the attempt fails. Returns 0 on success, or a negative WIFI_ERR_*
|
||||
// value describing why it could not connect.
|
||||
int Connect(const char* ssid, const char* password);
|
||||
int Disconnect();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Network interface, registered with Net::NetIf once associated
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const uint8_t* GetMacAddress();
|
||||
|
||||
// Send an Ethernet frame. Fails while the link is down.
|
||||
bool SendPacket(const uint8_t* data, uint16_t length);
|
||||
|
||||
// True once a network has been joined and, for encrypted networks, keyed.
|
||||
bool IsLinkUp();
|
||||
|
||||
using RxCallback = void(*)(const uint8_t* data, uint16_t length);
|
||||
void SetRxCallback(RxCallback callback);
|
||||
}
|
||||
|
||||
@@ -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, >k, >kLen, &keyIdx)) {
|
||||
if (!WpaInstallGtk(gtk, gtkLen, keyIdx, g_cfg.GroupCipher, k->rsc))
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Group key install failed; broadcast traffic will not be received";
|
||||
} else {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "No group key in message 3; broadcast traffic will not be received";
|
||||
}
|
||||
|
||||
g_state = WpaState::Complete;
|
||||
g_lastTxLen = 0;
|
||||
KernelLogStream(OK, "WiFi") << "WPA handshake complete; link is encrypted";
|
||||
}
|
||||
|
||||
// Periodic GTK rekey initiated by the AP (a 2-way exchange).
|
||||
static void HandleGroupKey(const uint8_t* frame, uint32_t len) {
|
||||
auto* k = (const EapolKey*)frame;
|
||||
memcpy(g_replay, k->replay, 8);
|
||||
|
||||
uint16_t keyInfo = Get16Be(k->keyInfo);
|
||||
uint32_t keyDataLen = Get16Be(k->keyDataLen);
|
||||
if (sizeof(EapolKey) + keyDataLen > len) return;
|
||||
const uint8_t* keyData = frame + sizeof(EapolKey);
|
||||
|
||||
uint8_t plain[MAX_KEY_DATA];
|
||||
uint32_t plainLen = 0;
|
||||
if (keyInfo & KEY_INFO_ENCRYPTED) {
|
||||
plainLen = DecryptKeyData(keyData, keyDataLen, plain, sizeof(plain));
|
||||
if (!plainLen) return;
|
||||
} else if (keyDataLen <= sizeof(plain)) {
|
||||
memcpy(plain, keyData, keyDataLen);
|
||||
plainLen = keyDataLen;
|
||||
}
|
||||
|
||||
const uint8_t* gtk = nullptr;
|
||||
uint32_t gtkLen = 0;
|
||||
uint8_t keyIdx = 0;
|
||||
if (FindGtk(plain, plainLen, >k, >kLen, &keyIdx))
|
||||
WpaInstallGtk(gtk, gtkLen, keyIdx, g_cfg.GroupCipher, k->rsc);
|
||||
|
||||
SendGroupAck();
|
||||
KernelLogStream(INFO, "WiFi") << "Group key rekeyed";
|
||||
}
|
||||
|
||||
bool WpaOnEapol(const uint8_t* data, uint32_t len) {
|
||||
if (g_state == WpaState::Idle || g_state == WpaState::Failed) return false;
|
||||
if (len < sizeof(EapolKey)) return false;
|
||||
|
||||
auto* k = (const EapolKey*)data;
|
||||
if (k->type != EAPOL_TYPE_KEY) return false;
|
||||
if (k->descType != EAPOL_KEY_DESC_RSN && k->descType != EAPOL_KEY_DESC_WPA)
|
||||
return false;
|
||||
|
||||
// Trust the frame's own length field over the (possibly padded) buffer
|
||||
// the driver handed us.
|
||||
uint32_t declared = (uint32_t)Get16Be(k->length) + 4;
|
||||
if (declared < sizeof(EapolKey) || declared > len) return false;
|
||||
len = declared;
|
||||
|
||||
g_eapolVersion = k->version > 3 ? 2 : k->version;
|
||||
g_descType = k->descType;
|
||||
|
||||
uint16_t keyInfo = Get16Be(k->keyInfo);
|
||||
uint8_t ver = (uint8_t)(keyInfo & KEY_INFO_VERSION_MASK);
|
||||
|
||||
if (keyInfo & KEY_INFO_REQUEST) return false; // STA->AP direction
|
||||
|
||||
if (ver == KEY_DESC_VER_RC4) {
|
||||
Fail("the AP asked for RC4/TKIP key wrapping, which is not supported");
|
||||
return true;
|
||||
}
|
||||
if (ver != KEY_DESC_VER_AES && ver != KEY_DESC_VER_AES_CMAC) {
|
||||
Fail("unknown EAPOL key descriptor version");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool pairwise = (keyInfo & KEY_INFO_KEY_TYPE) != 0;
|
||||
bool hasMic = (keyInfo & KEY_INFO_MIC) != 0;
|
||||
bool hasAck = (keyInfo & KEY_INFO_ACK) != 0;
|
||||
|
||||
if (keyInfo & KEY_INFO_ERROR) {
|
||||
Fail("the AP reported a MIC failure");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Message 1 is the only frame that arrives before a PTK exists, so it
|
||||
// is also the only one whose descriptor version we can adopt.
|
||||
if (pairwise && hasAck && !hasMic) {
|
||||
if (g_state != WpaState::WaitMsg1 && g_state != WpaState::WaitMsg3) {
|
||||
// The AP restarted the handshake; take it from the top.
|
||||
GenNonce(g_snonce);
|
||||
}
|
||||
g_keyDescVer = ver;
|
||||
HandleMsg1(data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Everything else is MIC-protected, so the PTK has to exist first.
|
||||
if (!hasMic) return false;
|
||||
if (g_state == WpaState::WaitMsg1) return false;
|
||||
|
||||
if (!VerifyMic(data, len)) {
|
||||
KernelLogStream(WARNING, "WiFi")
|
||||
<< "Discarding EAPOL frame with a bad MIC";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pairwise && hasAck) {
|
||||
if (g_state == WpaState::Complete) {
|
||||
// Our message 4 did not reach the AP and it retried message 3.
|
||||
// The keys are already in place, so just answer again.
|
||||
memcpy(g_replay, k->replay, 8);
|
||||
SendMsg4();
|
||||
} else {
|
||||
HandleMsg3(data, len);
|
||||
}
|
||||
} else if (!pairwise && hasAck) {
|
||||
HandleGroupKey(data, len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Retransmission / timeout
|
||||
// =========================================================================
|
||||
|
||||
// Unsigned deadline test that survives a `since` newer than `nowMs`. The
|
||||
// caller samples the clock once per service pass while g_lastTxMs is
|
||||
// stamped the moment a frame goes out, so the two can cross; a plain
|
||||
// subtraction then wraps and expires every timer at once.
|
||||
static bool Elapsed(uint64_t nowMs, uint64_t since, uint64_t ms) {
|
||||
return nowMs > since && nowMs - since > ms;
|
||||
}
|
||||
|
||||
void WpaService(uint64_t nowMs) {
|
||||
if (g_state != WpaState::WaitMsg1 && g_state != WpaState::WaitMsg3) return;
|
||||
|
||||
if (g_startMs == 0) g_startMs = nowMs;
|
||||
|
||||
if (Elapsed(nowMs, g_startMs, HANDSHAKE_TIMEOUT_MS)) {
|
||||
Fail("the AP stopped responding");
|
||||
return;
|
||||
}
|
||||
|
||||
// Only our own messages are worth retrying; while waiting for msg 1
|
||||
// there is nothing to resend, the AP drives that.
|
||||
if (g_state == WpaState::WaitMsg3 && g_lastTxLen
|
||||
&& Elapsed(nowMs, g_lastTxMs, RETRY_INTERVAL_MS)) {
|
||||
if (g_retries >= MAX_RETRIES) {
|
||||
Fail("no response to message 2");
|
||||
return;
|
||||
}
|
||||
g_retries++;
|
||||
g_lastTxMs = nowMs;
|
||||
WpaTxEapol(g_lastTx, g_lastTxLen);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -15,16 +15,18 @@
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
#include <Timekeeping/ApicTimer.hpp>
|
||||
#include <Net/NetIf.hpp>
|
||||
#include <CppLib/Spinlock.hpp>
|
||||
|
||||
using namespace Kt;
|
||||
|
||||
namespace Net::Arp {
|
||||
|
||||
// Must be the MAC of the interface the frame actually leaves by: an ARP
|
||||
// reply carrying the wired card's address while the frame goes out over
|
||||
// Wi-Fi would be answered to a station that is not there.
|
||||
static const uint8_t* GetActiveNicMac() {
|
||||
if (Drivers::Net::E1000::IsInitialized())
|
||||
return Drivers::Net::E1000::GetMacAddress();
|
||||
return Drivers::Net::E1000E::GetMacAddress();
|
||||
return NetIf::ActiveMac();
|
||||
}
|
||||
|
||||
// ARP cache entry
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#include <Net/ByteOrder.hpp>
|
||||
#include <Net/Arp.hpp>
|
||||
#include <Net/Ipv4.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Net/NetIf.hpp>
|
||||
#include <Libraries/Memory.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
@@ -18,22 +17,24 @@ using namespace Kt;
|
||||
|
||||
namespace Net::Ethernet {
|
||||
|
||||
// Which device the frames actually go out of is the registry's decision;
|
||||
// this layer only cares that something is there.
|
||||
static const uint8_t* GetActiveNicMac() {
|
||||
if (Drivers::Net::E1000::IsInitialized())
|
||||
return Drivers::Net::E1000::GetMacAddress();
|
||||
return Drivers::Net::E1000E::GetMacAddress();
|
||||
return NetIf::ActiveMac();
|
||||
}
|
||||
|
||||
static bool ActiveNicSend(const uint8_t* data, uint16_t length) {
|
||||
if (Drivers::Net::E1000::IsInitialized())
|
||||
return Drivers::Net::E1000::SendPacket(data, length);
|
||||
return Drivers::Net::E1000E::SendPacket(data, length);
|
||||
return NetIf::ActiveSend(data, length);
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
KernelLogStream(OK, "Net") << "Ethernet layer initialized";
|
||||
}
|
||||
|
||||
const uint8_t* GetMacAddress() {
|
||||
return NetIf::ActiveMac();
|
||||
}
|
||||
|
||||
bool Send(const uint8_t* destMac, uint16_t etherType, const uint8_t* payload, uint16_t payloadLen) {
|
||||
if (payload == nullptr || payloadLen == 0 || payloadLen > MAX_PAYLOAD_SIZE) {
|
||||
return false;
|
||||
|
||||
@@ -30,7 +30,10 @@ namespace Net::Ethernet {
|
||||
// Send an Ethernet frame with the given EtherType and payload
|
||||
bool Send(const uint8_t* destMac, uint16_t etherType, const uint8_t* payload, uint16_t payloadLen);
|
||||
|
||||
// Called by E1000 RX handler to dispatch received frames
|
||||
// Called by a driver's RX handler to dispatch received frames
|
||||
void OnFrameReceived(const uint8_t* data, uint16_t length);
|
||||
|
||||
// MAC address of the interface currently carrying traffic
|
||||
const uint8_t* GetMacAddress();
|
||||
|
||||
}
|
||||
|
||||
+58
-13
@@ -1,11 +1,12 @@
|
||||
/*
|
||||
* Net.cpp
|
||||
* Network stack initialization
|
||||
* Copyright (c) 2025 Daniel Hammer
|
||||
* Copyright (c) 2025-2026 Daniel Hammer
|
||||
*/
|
||||
|
||||
#include "Net.hpp"
|
||||
#include <Net/Ethernet.hpp>
|
||||
#include <Net/NetIf.hpp>
|
||||
#include <Net/Arp.hpp>
|
||||
#include <Net/Ipv4.hpp>
|
||||
#include <Net/Icmp.hpp>
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <Net/NetConfig.hpp>
|
||||
#include <Drivers/Net/E1000.hpp>
|
||||
#include <Drivers/Net/E1000E.hpp>
|
||||
#include <Drivers/Net/Wifi/Wifi.hpp>
|
||||
#include <Terminal/Terminal.hpp>
|
||||
#include <CppLib/Stream.hpp>
|
||||
|
||||
@@ -22,9 +24,57 @@ using namespace Kt;
|
||||
|
||||
namespace Net {
|
||||
|
||||
// A driver that never initialized has no link regardless of what its
|
||||
// carrier register says.
|
||||
static bool E1000LinkUp() {
|
||||
return Drivers::Net::E1000::IsInitialized()
|
||||
&& Drivers::Net::E1000::IsLinkUp();
|
||||
}
|
||||
static bool E1000ELinkUp() {
|
||||
return Drivers::Net::E1000E::IsInitialized()
|
||||
&& Drivers::Net::E1000E::IsLinkUp();
|
||||
}
|
||||
|
||||
static void RegisterInterfaces() {
|
||||
if (Drivers::Net::E1000::IsInitialized()) {
|
||||
NetIf::Register({
|
||||
"eth0", NetIf::Kind::Ethernet,
|
||||
Drivers::Net::E1000::GetMacAddress,
|
||||
Drivers::Net::E1000::SendPacket,
|
||||
E1000LinkUp,
|
||||
});
|
||||
Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived);
|
||||
}
|
||||
|
||||
if (Drivers::Net::E1000E::IsInitialized()) {
|
||||
NetIf::Register({
|
||||
"eth1", NetIf::Kind::Ethernet,
|
||||
Drivers::Net::E1000E::GetMacAddress,
|
||||
Drivers::Net::E1000E::SendPacket,
|
||||
E1000ELinkUp,
|
||||
});
|
||||
Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived);
|
||||
}
|
||||
|
||||
// Wi-Fi registers whenever the adapter exists; its link only comes up
|
||||
// once a network has been joined and keyed.
|
||||
if (Drivers::Net::Wifi::IsPresent()) {
|
||||
NetIf::Register({
|
||||
"wlan0", NetIf::Kind::Wireless,
|
||||
Drivers::Net::Wifi::GetMacAddress,
|
||||
Drivers::Net::Wifi::SendPacket,
|
||||
Drivers::Net::Wifi::IsLinkUp,
|
||||
});
|
||||
Drivers::Net::Wifi::SetRxCallback(Ethernet::OnFrameReceived);
|
||||
}
|
||||
}
|
||||
|
||||
void Initialize() {
|
||||
if (!Drivers::Net::E1000::IsInitialized() && !Drivers::Net::E1000E::IsInitialized()) {
|
||||
KernelLogStream(WARNING, "Net") << "No NIC initialized, skipping network stack";
|
||||
RegisterInterfaces();
|
||||
|
||||
if (NetIf::Count() == 0) {
|
||||
KernelLogStream(WARNING, "Net")
|
||||
<< "No network interface found, skipping network stack";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,17 +87,12 @@ namespace Net {
|
||||
Tcp::Initialize();
|
||||
Socket::Initialize();
|
||||
|
||||
// Hook the active NIC's RX to our Ethernet dispatcher
|
||||
if (Drivers::Net::E1000::IsInitialized()) {
|
||||
Drivers::Net::E1000::SetRxCallback(Ethernet::OnFrameReceived);
|
||||
} else {
|
||||
Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived);
|
||||
}
|
||||
// Announce ourselves, but only if something is actually carrying
|
||||
// traffic: a Wi-Fi-only machine has no link until it joins a network.
|
||||
if (NetIf::AnyLinkUp()) Arp::SendRequest(GetIpAddress());
|
||||
|
||||
// Send a gratuitous ARP to announce ourselves on the network
|
||||
Arp::SendRequest(GetIpAddress());
|
||||
|
||||
KernelLogStream(OK, "Net") << "Network stack initialized";
|
||||
KernelLogStream(OK, "Net") << "Network stack initialized with "
|
||||
<< (uint64_t)NetIf::Count() << " interface(s)";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -596,6 +596,26 @@ namespace montauk::abi {
|
||||
uint16_t beaconInterval; // TU
|
||||
};
|
||||
|
||||
// Association progress reported in WifiInfo.connState.
|
||||
static constexpr uint32_t WIFI_CONN_IDLE = 0;
|
||||
static constexpr uint32_t WIFI_CONN_CONTEXTS_UP = 1;
|
||||
static constexpr uint32_t WIFI_CONN_AUTHENTICATING = 2;
|
||||
static constexpr uint32_t WIFI_CONN_AUTHENTICATED = 3;
|
||||
static constexpr uint32_t WIFI_CONN_ASSOCIATING = 4;
|
||||
static constexpr uint32_t WIFI_CONN_ASSOCIATED = 5;
|
||||
static constexpr uint32_t WIFI_CONN_HANDSHAKING = 6;
|
||||
static constexpr uint32_t WIFI_CONN_CONNECTED = 7;
|
||||
static constexpr uint32_t WIFI_CONN_FAILED = 8;
|
||||
|
||||
// Negative results from SYS_WIFI_CONNECT.
|
||||
static constexpr int WIFI_ERR_NO_ADAPTER = -1; // no adapter, or not ready
|
||||
static constexpr int WIFI_ERR_NOT_FOUND = -2; // SSID absent from the scan
|
||||
static constexpr int WIFI_ERR_NEED_KEY = -3; // encrypted, no passphrase
|
||||
static constexpr int WIFI_ERR_UNSUPPORTED = -4; // WPA3-SAE, WEP, enterprise
|
||||
static constexpr int WIFI_ERR_AUTH = -5; // key exchange rejected
|
||||
static constexpr int WIFI_ERR_TIMEOUT = -6; // AP never answered
|
||||
static constexpr int WIFI_ERR_FAILED = -7; // anything else
|
||||
|
||||
// Adapter status (returned by SYS_WIFI_INFO).
|
||||
struct WifiInfo {
|
||||
uint8_t mac[6];
|
||||
@@ -607,7 +627,12 @@ namespace montauk::abi {
|
||||
char fwVersion[32];
|
||||
uint64_t rxPackets;
|
||||
uint32_t fwErrors;
|
||||
uint32_t connState; // 0 idle, >0 connection setup in progress
|
||||
uint32_t connState; // WIFI_CONN_*
|
||||
uint64_t txPackets;
|
||||
char ssid[36]; // network joined, empty when disconnected
|
||||
uint8_t bssid[6];
|
||||
uint8_t connected; // 1 once the link can carry IP traffic
|
||||
uint8_t channel;
|
||||
};
|
||||
|
||||
struct ThermalInfo {
|
||||
|
||||
+99
-16
@@ -7,8 +7,10 @@
|
||||
* wifi scan [seconds] scan for a specific duration (default 5, max 20)
|
||||
* wifi info adapter, firmware and radio status
|
||||
* wifi debug info plus raw counters, repeated scan detail
|
||||
* wifi connect <ssid> bring up the firmware contexts for an open network
|
||||
* wifi disconnect tear those contexts back down
|
||||
* wifi connect <ssid> [passphrase]
|
||||
* join a network (WPA2/WPA3-PSK or open)
|
||||
* wifi status the network currently joined
|
||||
* wifi disconnect leave it
|
||||
*
|
||||
* Copyright (c) 2026 Daniel Hammer
|
||||
*/
|
||||
@@ -85,6 +87,21 @@ static const char* state_name(uint8_t state) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char* conn_state_name(uint32_t s) {
|
||||
switch (s) {
|
||||
case abi::WIFI_CONN_IDLE: return "not connected";
|
||||
case abi::WIFI_CONN_CONTEXTS_UP: return "preparing radio";
|
||||
case abi::WIFI_CONN_AUTHENTICATING: return "authenticating";
|
||||
case abi::WIFI_CONN_AUTHENTICATED: return "authenticated";
|
||||
case abi::WIFI_CONN_ASSOCIATING: return "associating";
|
||||
case abi::WIFI_CONN_ASSOCIATED: return "associated";
|
||||
case abi::WIFI_CONN_HANDSHAKING: return "exchanging keys";
|
||||
case abi::WIFI_CONN_CONNECTED: return "connected";
|
||||
case abi::WIFI_CONN_FAILED: return "failed";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// A rough signal-quality bar from the RSSI: -50 dBm and up is excellent,
|
||||
// -90 dBm and below is unusable.
|
||||
static void put_signal_bar(int8_t rssi) {
|
||||
@@ -159,15 +176,50 @@ static void print_info(const abi::WifiInfo& info, bool verbose) {
|
||||
print(" channels : "); put_u64(info.channels);
|
||||
print(" usable after regulatory filtering\n");
|
||||
print(" scanning : "); print(info.scanning ? "yes" : "no"); print("\n");
|
||||
print(" network : ");
|
||||
if (info.ssid[0]) {
|
||||
print(info.ssid);
|
||||
print(" ("); print(conn_state_name(info.connState)); print(")");
|
||||
} else {
|
||||
print(conn_state_name(info.connState));
|
||||
}
|
||||
print("\n");
|
||||
if (info.ssid[0]) {
|
||||
print(" bssid : "); put_mac(info.bssid); print("\n");
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
print(" rx packets : "); put_u64(info.rxPackets); print("\n");
|
||||
print(" tx packets : "); put_u64(info.txPackets); print("\n");
|
||||
print(" fw errors : "); put_u64(info.fwErrors); print("\n");
|
||||
print(" conn state : "); put_u64(info.connState);
|
||||
print(" (0 = idle)\n");
|
||||
print(" ("); print(conn_state_name(info.connState)); print(")\n");
|
||||
}
|
||||
}
|
||||
|
||||
static int cmd_status() {
|
||||
abi::WifiInfo info;
|
||||
if (wifi_info(&info) != 0 && !info.present) {
|
||||
print("wifi: no supported Wi-Fi adapter found.\n");
|
||||
return 1;
|
||||
}
|
||||
if (!info.connected) {
|
||||
print("Not connected.");
|
||||
if (info.connState != abi::WIFI_CONN_IDLE) {
|
||||
print(" Currently "); print(conn_state_name(info.connState)); print(".");
|
||||
}
|
||||
print("\n");
|
||||
return 1;
|
||||
}
|
||||
print("Connected to \""); print(info.ssid); print("\"\n");
|
||||
print(" bssid : "); put_mac(info.bssid); print("\n");
|
||||
print(" interface : wlan0 ("); put_mac(info.mac); print(")\n");
|
||||
print(" rx / tx : "); put_u64(info.rxPackets);
|
||||
print(" / "); put_u64(info.txPackets); print(" packets\n");
|
||||
print("\nRun \"dhcp\" to pick up an address if you have not already.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_scan(uint32_t seconds, bool verbose) {
|
||||
abi::WifiInfo info;
|
||||
if (!require_adapter(info)) return 1;
|
||||
@@ -317,20 +369,44 @@ static int cmd_connect(const char* ssid, const char* password) {
|
||||
|
||||
print("Connecting to \""); print(ssid); print("\"...\n");
|
||||
int rc = wifi_connect(ssid, password);
|
||||
|
||||
if (rc == 0) {
|
||||
print("Firmware contexts are up.\n");
|
||||
print("NOTE: the 802.11 authentication exchange is not implemented yet,\n");
|
||||
print(" so this does not establish a usable link.\n");
|
||||
print("Connected.\n");
|
||||
print("Run \"dhcp\" to get an address, then the network is usable\n");
|
||||
print("just like a wired connection.\n");
|
||||
return 0;
|
||||
}
|
||||
if (rc == -2) {
|
||||
print("wifi: \""); print(ssid); print("\" is encrypted (WPA2/WPA3).\n");
|
||||
print(" Only open networks can be joined so far - the key exchange\n");
|
||||
print(" is not implemented yet. Scanning is unaffected.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
switch (rc) {
|
||||
case abi::WIFI_ERR_NOT_FOUND:
|
||||
print("wifi: \""); print(ssid); print("\" was not found in the last scan.\n");
|
||||
print(" Run \"wifi scan\" first, and check the SSID spelling.\n");
|
||||
break;
|
||||
case abi::WIFI_ERR_NEED_KEY:
|
||||
print("wifi: \""); print(ssid); print("\" is encrypted and needs a passphrase.\n");
|
||||
print(" Usage: wifi connect "); print(ssid); print(" <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;
|
||||
}
|
||||
|
||||
@@ -339,8 +415,9 @@ static void usage() {
|
||||
print(" scan [seconds] scan and list nearby networks (default 5)\n");
|
||||
print(" info adapter, firmware and radio status\n");
|
||||
print(" debug diagnostics plus per-BSS scan detail\n");
|
||||
print(" connect <ssid> bring up firmware contexts (open networks)\n");
|
||||
print(" disconnect tear those contexts down\n\n");
|
||||
print(" connect <ssid> [key] join a network (open or WPA2/WPA3-PSK)\n");
|
||||
print(" status show the network currently joined\n");
|
||||
print(" disconnect leave the current network\n\n");
|
||||
print("With no command, runs a 5 second scan.\n");
|
||||
}
|
||||
|
||||
@@ -363,14 +440,20 @@ extern "C" void _start() {
|
||||
exit(cmd_info(false));
|
||||
} else if (streq(cmd, "debug")) {
|
||||
exit(cmd_debug());
|
||||
} else if (streq(cmd, "status")) {
|
||||
exit(cmd_status());
|
||||
} else if (streq(cmd, "connect")) {
|
||||
char ssid[40], pass[80];
|
||||
if (!next_token(&rest, ssid, sizeof(ssid))) {
|
||||
print("wifi: connect needs an SSID\n");
|
||||
exit(1);
|
||||
}
|
||||
pass[0] = '\0';
|
||||
next_token(&rest, pass, sizeof(pass));
|
||||
// Everything after the SSID is the passphrase, spaces and all.
|
||||
const char* p = skip_spaces(rest);
|
||||
int n = 0;
|
||||
while (p[n] && n < (int)sizeof(pass) - 1) { pass[n] = p[n]; n++; }
|
||||
while (n > 0 && pass[n - 1] == ' ') n--;
|
||||
pass[n] = '\0';
|
||||
exit(cmd_connect(ssid, pass));
|
||||
} else if (streq(cmd, "disconnect")) {
|
||||
wifi_disconnect();
|
||||
|
||||
@@ -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.")
|
||||
@@ -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.")
|
||||
@@ -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.")
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}")
|
||||
@@ -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))
|
||||
@@ -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;
|
||||
}
|
||||
Executable
+48
@@ -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."
|
||||
@@ -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); }
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
namespace base { struct Manip {}; inline Manip hex, dec; }
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include <cstring>
|
||||
@@ -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; }; }
|
||||
@@ -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(); }
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user