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

This commit is contained in:
2026-08-06 19:44:35 +02:00
parent a01e63c717
commit bbe1df62fd
40 changed files with 5878 additions and 272 deletions
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Independent WPA2 authenticator driving the kernel supplicant through a
4-way handshake. All crypto here comes from hashlib / the `cryptography`
package, so it is a genuine oracle for the C++ implementation."""
import hashlib, hmac, os, subprocess, sys, struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.keywrap import aes_key_wrap, aes_key_unwrap
HARNESS = sys.argv[1]
def prf(key, label, data, nbytes):
r = b''
i = 0
while len(r) < nbytes:
r += hmac.new(key, label.encode() + b'\x00' + data + bytes([i]), hashlib.sha1).digest()
i += 1
return r[:nbytes]
def kdf_sha256(key, label, data, nbytes):
r = b''
i = 1
bits = nbytes * 8
while len(r) < nbytes:
r += hmac.new(key, struct.pack('<H', i) + label.encode() + data + struct.pack('<H', bits),
hashlib.sha256).digest()
i += 1
return r[:nbytes]
def derive_ptk(pmk, aa, spa, anonce, snonce, nbytes, sha256=False):
data = min(aa, spa) + max(aa, spa) + min(anonce, snonce) + max(anonce, snonce)
f = kdf_sha256 if sha256 else prf
return f(pmk, "Pairwise key expansion", data, nbytes)
def mic(kck, frame, ver):
if ver == 3:
from cryptography.hazmat.primitives.cmac import CMAC
c = CMAC(algorithms.AES(kck)); c.update(frame); return c.finalize()[:16]
return hmac.new(kck, frame, hashlib.sha1).digest()[:16]
HDR = 99
def build(ver, desc, key_info, replay, nonce, rsc, key_data, kck=None, mic_ver=2):
body = bytearray(HDR + len(key_data))
body[0] = ver
body[1] = 3 # EAPOL-Key
struct.pack_into('>H', body, 2, HDR + len(key_data) - 4)
body[4] = desc
struct.pack_into('>H', body, 5, key_info)
struct.pack_into('>H', body, 7, 16) # key length (CCMP)
body[9:17] = replay
body[17:49] = nonce
body[65:73] = rsc
struct.pack_into('>H', body, 97, len(key_data))
body[HDR:] = key_data
if key_info & 0x0100: # MIC bit
body[81:97] = mic(kck, bytes(body), mic_ver)
return bytes(body)
def parse(frame):
return {
'key_info': struct.unpack_from('>H', frame, 5)[0],
'replay': frame[9:17],
'nonce': frame[17:49],
'mic': frame[81:97],
'kdlen': struct.unpack_from('>H', frame, 97)[0],
'kd': frame[HDR:],
}
def check_mic(frame, kck, ver):
f = bytearray(frame); f[81:97] = b'\x00' * 16
return mic(kck, bytes(f), ver) == frame[81:97]
def run_case(name, ssid, passphrase, akm, pcipher, gcipher, desc_ver, corrupt_pass=False):
print(f"\n=== {name} ===")
aa = bytes.fromhex('001122334455') # AP
spa = bytes.fromhex('aabbccddeeff') # station
p = subprocess.Popen([HARNESS], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
def send(line):
p.stdin.write(line + '\n'); p.stdin.flush()
def readline():
return p.stdout.readline().strip()
sta_pass = passphrase + ('X' if corrupt_pass else '')
send(f"START {spa.hex()} {aa.hex()} {ssid} {sta_pass} {akm} {pcipher} {gcipher}")
ok = readline()
rsnie = readline()
assert ok.startswith('START-OK 1'), ok
sta_rsn = bytes.fromhex(rsnie.split()[1])
print(f" supplicant RSN IE: {sta_rsn.hex()}")
pmk = hashlib.pbkdf2_hmac('sha1', passphrase.encode(), ssid.encode(), 4096, 32)
anonce = os.urandom(32)
replay = (1).to_bytes(8, 'big')
# --- message 1 ---
m1 = build(2, 2, desc_ver | 0x0008 | 0x0080, replay, anonce, b'\x00'*8, b'')
send("RX " + m1.hex())
tx = readline()
assert tx.startswith('TX '), f"expected msg2, got {tx}"
m2 = bytes.fromhex(tx.split()[1])
status = readline()
print(f" msg2 {len(m2)} bytes, {status}")
i2 = parse(m2)
snonce = i2['nonce']
ptk = derive_ptk(pmk, aa, spa, anonce, snonce, 48, sha256=(akm == 6))
kck, kek, tk = ptk[:16], ptk[16:32], ptk[32:48]
m2_ok = check_mic(m2, kck, desc_ver)
print(f" msg2 MIC verifies against independently derived PTK: {m2_ok}")
if corrupt_pass:
assert not m2_ok, "a wrong passphrase must not produce a valid MIC"
print(" (expected: wrong passphrase -> MIC mismatch)")
p.stdin.close(); return
assert m2_ok
assert i2['replay'] == replay, "msg2 must echo the replay counter"
assert i2['kd'] == sta_rsn, "msg2 key data must be the supplicant's RSN IE"
assert i2['key_info'] & 0x0008, "msg2 must set the pairwise bit"
assert i2['key_info'] & 0x0100, "msg2 must set the MIC bit"
# --- message 3, carrying an AES-wrapped GTK KDE ---
gtk = os.urandom(16)
gtk_kde = bytes([0xdd, 6 + len(gtk), 0x00, 0x0f, 0xac, 0x01, 0x02, 0x00]) + gtk
kd = gtk_kde
if len(kd) % 8:
kd += b'\xdd' + b'\x00' * (7 - len(kd) % 8)
wrapped = aes_key_wrap(kek, kd)
replay3 = (2).to_bytes(8, 'big')
rsc = bytes.fromhex('0102030405060000')
m3 = build(2, 2, desc_ver | 0x0008 | 0x0040 | 0x0080 | 0x0100 | 0x0200 | 0x1000,
replay3, anonce, rsc, wrapped, kck, desc_ver)
send("RX " + m3.hex())
got = {}
while True:
l = readline()
if l.startswith('RX-OK'):
print(f" {l}")
break
k, _, v = l.partition(' ')
got[k] = v
if k == 'TX':
m4 = bytes.fromhex(v)
assert 'TX' in got, "supplicant must answer message 3 with message 4"
m4_ok = check_mic(m4, kck, desc_ver)
i4 = parse(m4)
print(f" msg4 MIC verifies: {m4_ok}")
assert m4_ok
assert i4['replay'] == replay3, "msg4 must echo message 3's replay counter"
assert i4['key_info'] & 0x0200, "msg4 must set the secure bit"
assert i4['kdlen'] == 0, "msg4 must carry no key data"
assert got['PTK'] == tk.hex(), f"installed TK {got['PTK']} != expected {tk.hex()}"
print(f" installed TK matches the AP's derivation: True")
assert got['GTK'] == gtk.hex(), f"installed GTK {got['GTK']} != {gtk.hex()}"
print(f" installed GTK matches: True (key index {got['GTK-IDX']})")
assert got['GTK-RSC'].startswith('010203040506')
assert 'STATE 3' in l, f"supplicant should be Complete, got {l}"
print(" PASS")
p.stdin.close()
run_case("WPA2-PSK / CCMP / key descriptor v2", "MontaukTest", "supersecret123", 2, 4, 4, 2)
run_case("WPA2-PSK-SHA256 / CCMP / key descriptor v3", "MontaukTest", "supersecret123", 6, 4, 4, 3)
run_case("wrong passphrase is rejected", "MontaukTest", "supersecret123", 2, 4, 4, 2, corrupt_pass=True)
print("\nAll supplicant cases passed.")
+640
View File
@@ -0,0 +1,640 @@
#!/usr/bin/env python3
"""Drives the real MLME through a complete join and validates every frame it
puts on the air, plus the 802.11 <-> Ethernet translation in both directions.
The access point side is written independently here (hashlib / cryptography for
the handshake, hand-decoded 802.11 for the frames), so this is a check of what
the driver actually emits rather than a restatement of it.
Copyright (c) 2026 Daniel Hammer
"""
import hashlib, hmac, os, struct, subprocess, sys
from cryptography.hazmat.primitives.keywrap import aes_key_wrap
HARNESS = sys.argv[1]
AP = bytes.fromhex('001122334455')
STA = bytes.fromhex('aabbccddeeff')
SSID = "MontaukTest"
PASS = "supersecret123"
CHANNEL = 6
fails = []
def state_of(result):
f = result.split()
return int(f[f.index('STATE') + 1]) if 'STATE' in f else -1
def link_of(result):
f = result.split()
return int(f[f.index('LINK') + 1]) if 'LINK' in f else -1
def check(cond, what):
print(f" {'PASS' if cond else 'FAIL'} {what}")
if not cond:
fails.append(what)
# --------------------------------------------------------------- harness I/O
class Driver:
def __init__(self):
self.p = subprocess.Popen([HARNESS], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, text=True, bufsize=1)
def cmd(self, line):
"""Send a command; collect emitted events until the terminator."""
self.p.stdin.write(line + '\n'); self.p.stdin.flush()
ev = {'TX': [], 'KEY': [], 'KEYDEL': [], 'ETH': [], 'CMD': [], 'TXQ': []}
while True:
l = self.p.stdout.readline()
if not l:
raise RuntimeError("harness died")
l = l.strip()
k = l.split()[0] if l else ''
if k == 'TX':
f = l.split()
ev['TX'].append({'enc': f[1] == 'enc=1', 'rate': f[2] == 'rate=1',
'hdr': bytes.fromhex(f[3]),
'body': bytes.fromhex(f[4]) if len(f) > 4 else b''})
elif k == 'KEY':
f = l.split()
ev['KEY'].append({'pairwise': f[1] == 'pairwise=1',
'idx': int(f[2].split('=')[1]),
'cipher': int(f[3].split('=')[1]),
'key': bytes.fromhex(f[4]),
'rsc': bytes.fromhex(f[5]) if len(f) > 5 else b''})
elif k == 'ETH':
ev['ETH'].append(bytes.fromhex(l.split()[1]))
elif k == 'CMD':
f = l.split()
ev['CMD'].append((int(f[1]),
bytes.fromhex(f[2]) if len(f) > 2 else b''))
elif k == 'KEY-REMOVE':
ev['KEYDEL'].append(l)
elif k in ('TXQ-UP', 'TXQ-DOWN'):
ev['TXQ'].append(l)
else:
ev['result'] = l
return ev
# --------------------------------------------------------------- 802.11 bits
def mgmt(subtype, dst, src, bssid, body, seq=0):
return bytes([0x00 | subtype, 0x00]) + b'\x00\x00' + dst + src + bssid \
+ struct.pack('<H', seq << 4) + body
def data_from_ds(da, bssid, sa, ethertype, payload, protected=False):
fc1 = 0x02 | (0x40 if protected else 0)
hdr = bytes([0x08, fc1]) + b'\x00\x00' + da + bssid + sa + b'\x00\x00'
# The firmware decrypts in place and strips the MIC, but leaves the
# 8-byte CCMP header, so model that when the protected bit is set.
iv = b'\x11\x22\x00\x20\x00\x00\x00\x00' if protected else b''
snap = bytes([0xaa, 0xaa, 0x03, 0, 0, 0]) + struct.pack('>H', ethertype)
return hdr + iv + snap + payload
def parse_ies(b):
ies, off = {}, 0
while off + 2 <= len(b):
i, l = b[off], b[off + 1]
if off + 2 + l > len(b): break
ies[i] = b[off + 2:off + 2 + l]
off += 2 + l
return ies
def suite(t): return bytes([0x00, 0x0f, 0xac, t])
AP_RSN = (struct.pack('<H', 1) + suite(4)
+ struct.pack('<H', 1) + suite(4)
+ struct.pack('<H', 1) + suite(2)
+ struct.pack('<H', 0))
# ------------------------------------------------------------------- EAPOL
def prf(key, label, data, n):
r, i = b'', 0
while len(r) < n:
r += hmac.new(key, label.encode() + b'\x00' + data + bytes([i]), hashlib.sha1).digest(); i += 1
return r[:n]
def derive_ptk(pmk, an, sn):
return prf(pmk, "Pairwise key expansion",
min(AP, STA) + max(AP, STA) + min(an, sn) + max(an, sn), 48)
def emic(kck, f): return hmac.new(kck, f, hashlib.sha1).digest()[:16]
HDR = 99
def eapol(ki, replay, nonce, rsc, kd, kck=None):
b = bytearray(HDR + len(kd)); b[0] = 2; b[1] = 3
struct.pack_into('>H', b, 2, HDR + len(kd) - 4); b[4] = 2
struct.pack_into('>H', b, 5, ki); struct.pack_into('>H', b, 7, 16)
b[9:17] = replay; b[17:49] = nonce; b[65:73] = rsc
struct.pack_into('>H', b, 97, len(kd)); b[HDR:] = kd
if ki & 0x0100: b[81:97] = emic(kck, bytes(b))
return bytes(b)
def check_emic(f, kck):
x = bytearray(f); x[81:97] = b'\x00' * 16
return emic(kck, bytes(x)) == f[81:97]
def gtk_kde(gtk, idx):
kd = bytes([0xdd, 6 + len(gtk), 0x00, 0x0f, 0xac, 0x01, idx, 0x00]) + gtk
if len(kd) % 8: kd += b'\xdd' + b'\x00' * (7 - len(kd) % 8)
return kd
# =============================================================================
d = Driver()
d.cmd("MAC " + STA.hex())
print("=== authentication ===")
ev = d.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 {SSID} {PASS} {AP_RSN.hex()}")
check(ev['result'].startswith('CONNECT-OK 1'), "connect starts")
check(len(ev['TXQ']) == 1 and ev['TXQ'][0].startswith('TXQ-UP'),
"a transmit queue is opened before any frame is sent")
check(len(ev['TX']) == 1, "exactly one frame is sent (the authentication request)")
auth = ev['TX'][0]
h, b = auth['hdr'], auth['body']
check(h[0] == 0xb0, "authentication frame: type management, subtype auth")
check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP,
"authentication frame: addressed to the AP, from us, BSSID correct")
check(len(h) == 24, "authentication frame: 24-byte header")
check(struct.unpack('<H', b[0:2])[0] == 0, "authentication: open system algorithm")
check(struct.unpack('<H', b[2:4])[0] == 1, "authentication: transaction sequence 1")
check(struct.unpack('<H', b[4:6])[0] == 0, "authentication: status 0")
check(not auth['enc'] and auth['rate'],
"authentication frame sent in the clear at a fixed rate")
print("\n=== association ===")
d.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
ev = d.cmd("SERVICE")
check(len(ev['TX']) == 1, "authentication success triggers one association request")
ar = ev['TX'][0]
check(ar['hdr'][0] == 0x00, "association request: subtype assoc-req")
check(ar['hdr'][4:10] == AP and ar['hdr'][10:16] == STA,
"association request: addressing correct")
caps = struct.unpack('<H', ar['body'][0:2])[0]
check(caps & 0x0001, "association request: ESS capability set")
check(caps & 0x0010, "association request: privacy bit set for an encrypted network")
li = struct.unpack('<H', ar['body'][2:4])[0]
check(li == 10, "association request: listen interval present")
ies = parse_ies(ar['body'][4:])
check(ies.get(0) == SSID.encode(), "association request: SSID element matches")
check(1 in ies and len(ies[1]) >= 4, "association request: supported rates present")
check(any(r & 0x80 for r in ies[1]), "association request: at least one basic rate")
check(48 in ies, "association request: RSN element present")
sta_rsn = ies[48]
check(sta_rsn[0:2] == struct.pack('<H', 1), "RSN element: version 1")
check(sta_rsn[2:6] == suite(4), "RSN element: CCMP group cipher")
check(sta_rsn[8:12] == suite(4), "RSN element: CCMP pairwise cipher")
check(sta_rsn[14:18] == suite(2), "RSN element: PSK AKM")
aid = 0x0007
d.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0431, 0, aid | 0xc000)).hex())
ev = d.cmd("SERVICE")
check(state_of(ev['result']) == 6, "association moves the link into the handshake")
check(len(ev['CMD']) >= 2, "post-association context updates are sent")
print("\n=== 4-way handshake carried over 802.11 data frames ===")
pmk = hashlib.pbkdf2_hmac('sha1', PASS.encode(), SSID.encode(), 4096, 32)
anonce = os.urandom(32)
m1 = eapol(0x0002 | 0x0008 | 0x0080, (1).to_bytes(8, 'big'), anonce, b'\x00' * 8, b'')
d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m1).hex())
ev = d.cmd("SERVICE")
check(len(ev['TX']) == 1, "EAPOL message 1 produces exactly one reply")
m2f = ev['TX'][0]
h = m2f['hdr']
check(h[0] == 0x08 and (h[1] & 0x01), "EAPOL reply: data frame with to-DS set")
check(not (h[1] & 0x40), "EAPOL reply: not marked protected (no key yet)")
check(not m2f['enc'], "EAPOL reply: firmware told not to encrypt")
check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP,
"EAPOL reply: addr1 the AP, addr2 us, addr3 the AP")
check(m2f['body'][0:6] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]),
"EAPOL reply: RFC 1042 LLC/SNAP shim")
check(m2f['body'][6:8] == struct.pack('>H', 0x888e),
"EAPOL reply: EtherType 0x888e")
m2 = m2f['body'][8:]
snonce = m2[17:49]
ptk = derive_ptk(pmk, anonce, snonce)
kck, kek, tk = ptk[:16], ptk[16:32], ptk[32:48]
check(check_emic(m2, kck), "EAPOL message 2 MIC verifies under the AP's own PTK")
check(m2[HDR:] == bytes([48, len(sta_rsn)]) + sta_rsn,
"EAPOL message 2 carries the same RSN element as the association request")
gtk = os.urandom(16)
rsc = bytes.fromhex('0a0b0c0d0e0f0000')
m3 = eapol(0x0002 | 0x0008 | 0x0040 | 0x0080 | 0x0100 | 0x0200 | 0x1000,
(2).to_bytes(8, 'big'), anonce, rsc, aes_key_wrap(kek, gtk_kde(gtk, 1)), kck)
d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m3).hex())
ev = d.cmd("SERVICE")
check(len(ev['TX']) == 1, "EAPOL message 3 produces message 4")
m4 = ev['TX'][0]['body'][8:]
check(check_emic(m4, kck), "EAPOL message 4 MIC verifies")
keys = {('pairwise' if k['pairwise'] else 'group'): k for k in ev['KEY']}
check('pairwise' in keys and keys['pairwise']['key'] == tk,
"pairwise key installed matches the AP's temporal key")
check(keys.get('pairwise', {}).get('cipher') == 4, "pairwise key installed as CCMP")
check('group' in keys and keys['group']['key'] == gtk, "group key installed matches")
check(keys.get('group', {}).get('rsc') == rsc[:6],
"group key installed with the AP's receive sequence counter")
check(state_of(ev['result']) == 7 and link_of(ev['result']) == 1,
"link reports up once keyed")
print("\n=== data path ===")
ip = bytes.fromhex('4500002800010000401100000a0000010a000002') + b'payload-here'
peer = bytes.fromhex('665544332211')
eth_out = peer + STA + struct.pack('>H', 0x0800) + ip
ev = d.cmd("TXETH " + eth_out.hex())
check(ev['result'] == 'TXETH-OK 1', "an Ethernet frame is accepted for transmit")
f = ev['TX'][0]
h = f['hdr']
check(h[0] == 0x08 and (h[1] & 0x01), "outbound data: to-DS data frame")
check(h[1] & 0x40, "outbound data: protected bit set now that keys are installed")
check(f['enc'], "outbound data: firmware asked to encrypt")
check(not f['rate'], "outbound data: rate control left to the firmware")
check(h[4:10] == AP, "outbound data: addr1 is the AP")
check(h[10:16] == STA, "outbound data: addr2 is us")
check(h[16:22] == peer, "outbound data: addr3 is the final destination")
check(f['body'][0:8] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]) + struct.pack('>H', 0x0800),
"outbound data: LLC/SNAP carries the EtherType")
check(f['body'][8:] == ip, "outbound data: IP payload preserved byte for byte")
seq1 = struct.unpack('<H', h[22:24])[0]
ev2 = d.cmd("TXETH " + eth_out.hex())
seq2 = struct.unpack('<H', ev2['TX'][0]['hdr'][22:24])[0]
check(seq2 != seq1, "outbound data: the sequence number advances between frames")
reply = bytes.fromhex('450000300002000040110000' '0a000002' '0a000001') + b'inbound-payload'
ev = d.cmd("RXDATA " + data_from_ds(STA, AP, peer, 0x0800, reply, protected=True).hex())
check(len(ev['ETH']) == 1, "an encrypted inbound data frame yields one Ethernet frame")
e = ev['ETH'][0]
check(e[0:6] == STA, "inbound data: Ethernet destination is addr1")
check(e[6:12] == peer, "inbound data: Ethernet source is addr3")
check(e[12:14] == struct.pack('>H', 0x0800), "inbound data: EtherType recovered")
check(e[14:] == reply, "inbound data: payload recovered past the CCMP header")
bcast = bytes.fromhex('ffffffffffff')
ev = d.cmd("RXDATA " + data_from_ds(bcast, AP, peer, 0x0806, b'arp-request-body').hex())
check(len(ev['ETH']) == 1 and ev['ETH'][0][0:6] == bcast,
"inbound broadcast (ARP) is delivered")
ev = d.cmd("RXDATA " + data_from_ds(STA, bytes.fromhex('aa0000000001'),
peer, 0x0800, b'from-a-stranger').hex())
check(len(ev['ETH']) == 0, "a data frame from a different BSSID is ignored")
ev = d.cmd("RXDATA " + (bytes([0x48, 0x02]) + b'\x00\x00' + STA + AP + peer
+ b'\x00\x00').hex())
check(len(ev['ETH']) == 0, "a null-data keepalive produces no Ethernet frame")
print("\n=== teardown ===")
ev = d.cmd("ABORT")
check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']), "abort closes the transmit queue")
check(len(ev['KEYDEL']) == 2, "abort removes both hardware keys before the station")
check(state_of(ev['result']) == 0, "abort returns to idle")
ev = d.cmd("TXETH " + eth_out.hex())
check(ev['result'] == 'TXETH-OK 0', "transmit is refused once the link is down")
# =============================================================================
# Other paths through the state machine, each on a fresh harness.
# =============================================================================
print("\n=== open network ===")
d2 = Driver()
d2.cmd("MAC " + STA.hex())
ev = d2.cmd(f"CONNECT {AP.hex()} 11 0 OpenNet - -")
check(ev['result'].startswith('CONNECT-OK 1'), "open network: connect starts")
caps_seen = []
d2.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
ev = d2.cmd("SERVICE")
ar = ev['TX'][0]
caps = struct.unpack('<H', ar['body'][0:2])[0]
check(not (caps & 0x0010), "open network: privacy bit clear")
ies = parse_ies(ar['body'][4:])
check(48 not in ies, "open network: no RSN element in the association request")
d2.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 3 | 0xc000)).hex())
ev = d2.cmd("SERVICE")
check(state_of(ev['result']) == 7 and link_of(ev['result']) == 1,
"open network: link comes up straight after association, no handshake")
check(len(ev['KEY']) == 0, "open network: no keys installed")
ev = d2.cmd("TXETH " + (peer + STA + struct.pack('>H', 0x0800) + ip).hex())
f = ev['TX'][0]
check(not (f['hdr'][1] & 0x40) and not f['enc'],
"open network: data frames are sent unprotected")
print("\n=== the access point never answers ===")
d3 = Driver()
d3.cmd("MAC " + STA.hex())
d3.cmd(f"CONNECT {AP.hex()} 6 0 Nowhere - -")
retries = 0
for _ in range(6):
d3.cmd("TICK 500")
ev = d3.cmd("SERVICE")
retries += len(ev['TX'])
if state_of(ev['result']) == 8:
break
check(retries >= 3, f"authentication is retransmitted ({retries} retries) before giving up")
check(state_of(ev['result']) == 8, "the attempt eventually fails rather than hanging")
check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']),
"giving up tears the transmit queue back down")
print("\n=== the access point rejects us ===")
d4 = Driver()
d4.cmd("MAC " + STA.hex())
d4.cmd(f"CONNECT {AP.hex()} 6 0 Rejects - -")
ev = d4.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 1)).hex())
check(state_of(ev['result']) == 8, "an authentication rejection fails the attempt")
d5 = Driver()
d5.cmd("MAC " + STA.hex())
d5.cmd(f"CONNECT {AP.hex()} 6 0 Rejects - -")
d5.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
d5.cmd("SERVICE")
ev = d5.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 17, 0)).hex())
check(state_of(ev['result']) == 8, "an association rejection fails the attempt")
print("\n=== the access point drops us ===")
d6 = Driver()
d6.cmd("MAC " + STA.hex())
d6.cmd(f"CONNECT {AP.hex()} 6 0 OpenNet - -")
d6.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
d6.cmd("SERVICE")
d6.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 3 | 0xc000)).hex())
ev = d6.cmd("SERVICE")
check(link_of(ev['result']) == 1, "link up before the deauthentication")
d6.cmd("RXMGMT " + mgmt(0xc0, STA, AP, AP, struct.pack('<H', 3)).hex())
ev = d6.cmd("SERVICE")
check(state_of(ev['result']) == 0 and link_of(ev['result']) == 0,
"a deauthentication from the AP brings the link down")
ev = d6.cmd("TXETH " + (peer + STA + struct.pack('>H', 0x0800) + ip).hex())
check(ev['result'] == 'TXETH-OK 0', "transmit is refused after being dropped")
# =============================================================================
# PHY context command layout.
#
# Regression test for a lockup: firmware 89 advertises ULTRA_HB_CHANNELS, which
# selects an 8-byte channel-info sub-structure (32-bit channel first). Sending
# the older 4-byte form shifted every field after it, asserted the firmware, and
# the driver then spun forever waiting for a reply that would never come.
# =============================================================================
print("\n=== PHY context command layout ===")
PHY_CONTEXT_CMD = 0x08
ULTRA_HB = 48
def phy_cmd_for(uhb):
dd = Driver()
dd.cmd("MAC " + STA.hex())
dd.cmd(f"CAPA {ULTRA_HB} {1 if uhb else 0}")
ev = dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
dd.p.stdin.close()
for cid, payload in ev['CMD']:
if cid == PHY_CONTEXT_CMD:
return payload
return None
p_uhb = phy_cmd_for(True)
check(p_uhb is not None and len(p_uhb) == 32,
f"ultra-high-band firmware gets a 32-byte PHY context ({len(p_uhb) if p_uhb else 0})")
if p_uhb and len(p_uhb) == 32:
chan = struct.unpack_from('<I', p_uhb, 8)[0]
band, width, ctrl = p_uhb[12], p_uhb[13], p_uhb[14]
lmac = struct.unpack_from('<I', p_uhb, 16)[0]
check(chan == CHANNEL, "UHB layout: channel is the 32-bit field at offset 8")
check(band == 1, "UHB layout: band follows the channel (2.4 GHz = 1)")
check(width == 0 and ctrl == 0, "UHB layout: 20 MHz, control position below")
check(lmac == 0, "UHB layout: lmac_id lands at offset 16, not shifted")
p_v1 = phy_cmd_for(False)
check(p_v1 is not None and len(p_v1) == 28,
f"older firmware still gets the 28-byte form ({len(p_v1) if p_v1 else 0})")
if p_v1 and len(p_v1) == 28:
check(p_v1[8] == 1 and p_v1[9] == CHANNEL,
"legacy layout: band then channel, both single bytes")
check(struct.unpack_from('<I', p_v1, 12)[0] == 0,
"legacy layout: lmac_id at offset 12")
# =============================================================================
# MLD command sizes and layout.
#
# This firmware implements the MLD API, so the connect path uses MAC_CONFIG /
# LINK_CONFIG / STA_CONFIG rather than the legacy MAC_CONTEXT / BINDING /
# ADD_STA. Sizes and field offsets below are exactly what Linux puts on the
# wire against this same firmware (tests/wifi/decode_iwl_trace.py).
# =============================================================================
print("\n=== MLD command sizes ===")
MAC_CONFIG = (3 << 8) | 0x08
LINK_CONFIG = (3 << 8) | 0x09
STA_CONFIG = (3 << 8) | 0x0a
STA_REMOVE = (3 << 8) | 0x0c
PHY_CONTEXT = 0x08
dd = Driver()
dd.cmd("MAC " + STA.hex())
dd.cmd(f"CAPA {ULTRA_HB} 1")
ev = dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
sizes, first = {}, {}
for cid, payload in ev['CMD']:
sizes.setdefault(cid, len(payload))
first.setdefault(cid, payload)
check(sizes.get(MAC_CONFIG) == 52, f"MAC_CONFIG_CMD is 52 bytes (got {sizes.get(MAC_CONFIG)})")
check(sizes.get(LINK_CONFIG) == 208, f"LINK_CONFIG_CMD is 208 bytes (got {sizes.get(LINK_CONFIG)})")
check(sizes.get(STA_CONFIG) == 96, f"STA_CONFIG_CMD is 96 bytes (got {sizes.get(STA_CONFIG)})")
check(sizes.get(PHY_CONTEXT) == 32, f"PHY_CONTEXT_CMD is 32 bytes (got {sizes.get(PHY_CONTEXT)})")
check(0x28 not in sizes and 0x2b not in sizes and 0x18 not in sizes,
"no legacy MAC_CONTEXT / BINDING / ADD_STA is sent")
m = first.get(MAC_CONFIG)
if m:
check(struct.unpack_from('<I', m, 8)[0] == 5, "MAC_CONFIG: mac_type is BSS_STA")
check(m[12:18] == STA, "MAC_CONFIG: local_mld_addr is our MAC")
check(struct.unpack_from('<I', m, 20)[0] == 0x0c,
"MAC_CONFIG: accepts group + beacon frames before association")
check(m[36] == 0, "MAC_CONFIG: is_assoc clear on the initial add")
l = first.get(LINK_CONFIG)
if l:
check(struct.unpack_from('<I', l, 0)[0] == 1, "LINK_CONFIG: first one is an ADD")
check(struct.unpack_from('<I', l, 12)[0] == 0xffffffff,
"LINK_CONFIG: phy_id is INVALID until the link is bound")
check(l[16:22] == STA, "LINK_CONFIG: local_link_addr is our MAC")
check(struct.unpack_from('<I', l, 136)[0] == 0,
"LINK_CONFIG: no beacon timing before association")
# The PHY binding and the activation must be two separate MODIFYs, in that
# order, after the PHY context exists. Folding them into one command asserts
# fw 89 (UMAC error 0x2010330F, seen on the AX211); Linux sends the binding
# first with mask 0 (__iwl_mvm_mld_assign_vif_chanctx).
links = [(i, p) for i, (c, p) in enumerate(ev['CMD']) if c == LINK_CONFIG]
phy_add = next((i for i, (c, p) in enumerate(ev['CMD'])
if c == PHY_CONTEXT and struct.unpack_from('<I', p, 4)[0] == 1), None)
check(len(links) >= 3, f"link add, PHY binding and activation are three commands "
f"(got {len(links)})")
if len(links) >= 3 and phy_add is not None:
bind_i, bind = links[1]
act_i, act = links[2]
check(phy_add < bind_i, "the PHY context exists before the link binds to it")
check(struct.unpack_from('<I', bind, 0)[0] == 2
and struct.unpack_from('<I', bind, 12)[0] == 0
and struct.unpack_from('<I', bind, 24)[0] == 0
and struct.unpack_from('<I', bind, 28)[0] == 0,
"PHY binding is its own MODIFY: phy_id set, mask 0, still inactive")
check(struct.unpack_from('<I', act, 0)[0] == 2
and struct.unpack_from('<I', act, 24)[0] == 0x03
and struct.unpack_from('<I', act, 28)[0] == 1,
"activation is a later MODIFY with exactly ACTIVE|RATES_INFO")
# Field values the Linux trace carries at this stage (fw 89 asserted with
# UMAC error 0x2010303E when they differed).
check(struct.unpack_from('<I', act, 136)[0] == 100
and struct.unpack_from('<I', act, 140)[0] == 0,
"activation carries the beacon interval but no DTIM interval yet")
check(struct.unpack_from('<I', act, 56)[0] == 0x2,
"pre-assoc qos_flags is TGN without UPDATE_EDCA")
check(struct.unpack_from('<H', act, 82)[0] == 0
and struct.unpack_from('<H', act, 62)[0] == 1023,
"pre-assoc EDCA is the default contention set, not WMM")
check(struct.unpack_from('<H', act, 146)[0] == 0,
"no RTS threshold before association")
st = first.get(STA_CONFIG)
if st:
check(struct.unpack_from('<I', st, 0)[0] == 0, "STA_CONFIG: station id 0")
check(st[8:14] == AP and st[16:22] == AP,
"STA_CONFIG: both peer addresses are the BSSID")
check(struct.unpack_from('<I', st, 28)[0] == 0,
"STA_CONFIG: association id 0 before association")
dd.p.stdin.close()
# =============================================================================
# Beacon timing arrives with association, not before.
# =============================================================================
print("\n=== link beacon timing ===")
dd = Driver()
dd.cmd("MAC " + STA.hex())
dd.cmd(f"CAPA {ULTRA_HB} 1")
dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -")
dd.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
dd.cmd("SERVICE")
dd.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
ev = dd.cmd("SERVICE")
link_i = next((i for i, (c, p) in enumerate(ev['CMD']) if c == LINK_CONFIG), None)
mac_i = next((i for i, (c, p) in enumerate(ev['CMD']) if c == MAC_CONFIG), None)
link = ev['CMD'][link_i][1] if link_i is not None else None
mac = ev['CMD'][mac_i][1] if mac_i is not None else None
check(link is not None and mac is not None,
"association updates the link and the MAC")
check(link_i is not None and mac_i is not None and link_i < mac_i,
"one-shot link timing is programmed before the MAC is marked associated")
if link:
mask = struct.unpack_from('<I', link, 24)[0]
check(struct.unpack_from('<I', link, 0)[0] == 2, "post-assoc link is a MODIFY")
check(mask & 0x10, "post-assoc link sets the BEACON_TIMING modify bit")
check(mask == 0x1e,
f"legacy post-assoc link changes exactly rates/protection/QoS/timing (got {mask:#x})")
check(not (mask & 0x01),
"post-assoc link does not re-assert ACTIVE on an active link")
check(struct.unpack_from('<I', link, 136)[0] == 100,
"post-assoc link carries the beacon interval")
check(struct.unpack_from('<I', link, 140)[0] == 200,
"post-assoc link carries bi * dtim period")
check(struct.unpack_from('<I', link, 56)[0] == 0x3,
"post-assoc qos_flags is TGN plus UPDATE_EDCA")
check(struct.unpack_from('<H', link, 82)[0] == 3008,
"post-assoc EDCA switches to the WMM set")
if mac:
check(mac[36] == 1, "post-assoc MAC has is_assoc set")
check(struct.unpack_from('<H', mac, 40)[0] == 7, "post-assoc MAC carries the AID")
check(struct.unpack_from('<I', mac, 20)[0] == 0x04,
"post-assoc MAC stops asking for beacons")
check(not any(c == STA_CONFIG for c, p in ev['CMD']),
"client AID is not incorrectly written to STA_CONFIG's GO-only assoc_id")
dd.p.stdin.close()
# =============================================================================
# Deadlines are not allowed to expire the moment they are set.
#
# The service loop sampled the clock once at the top of the pass, then spent
# real milliseconds inside the post-association context commands, each of which
# stamps a *newer* timestamp. Comparing the stale `now` against those stamps
# underflowed the unsigned subtraction, so a join that had just succeeded
# reported "timed out while joining the network" straight after "Associated",
# tore the contexts down, and the access point's first EAPOL frame arrived to
# find the station already gone.
#
# The harness advances its clock on every host command (CMD_ROUND_TRIP_MS), so
# a pass that sends commands and then checks a deadline reproduces this.
# =============================================================================
print("\n=== deadlines survive the time spent sending commands ===")
d7 = Driver()
d7.cmd("MAC " + STA.hex())
d7.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 {SSID} {PASS} {AP_RSN.hex()}")
d7.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('<HHH', 0, 2, 0)).hex())
ev = d7.cmd("SERVICE")
check(state_of(ev['result']) == 4,
"sending the association request does not immediately expire its own retry timer")
check(len(ev['TX']) == 1,
"the association request is sent once per pass, not retransmitted on the spot")
d7.cmd("RXMGMT " + mgmt(0x10, STA, AP, AP,
struct.pack('<HHH', 0x0421, 0, 7 | 0xc000)).hex())
ev = d7.cmd("SERVICE")
check(state_of(ev['result']) == 6,
"association reaches the handshake instead of timing out in the same pass")
check(not any(x.startswith('TXQ-DOWN') for x in ev['TXQ']),
"a successful association does not tear the transmit queue back down")
check(not any(c == STA_REMOVE for c, p in ev['CMD']),
"a successful association does not remove the station it just added")
# The EAPOL exchange arrives a moment later, exactly as it does on the air.
m1_late = eapol(0x0002 | 0x0008 | 0x0080, (1).to_bytes(8, 'big'),
bytes(range(32)), b'\x00' * 8, b'')
ev = d7.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m1_late).hex())
ev = d7.cmd("SERVICE")
check(len(ev['TX']) == 1,
"an EAPOL message 1 that arrives after the context updates is still answered")
check(state_of(ev['result']) == 6,
"the station is still in the handshake when message 1 is answered")
# =============================================================================
# Teardown order: the MAC gives up its association first.
#
# While MAC_CONFIG.is_assoc is set, the firmware's MAC context owns the link
# carrying the BSS. Deactivating that link underneath it asserts the UMAC
# (0x2000320F on firmware 89) -- the failure the log above ends in.
# =============================================================================
print("\n=== teardown clears the association before the link ===")
ev = d7.cmd("ABORT")
seq = [(c, p) for c, p in ev['CMD']]
def index_of(pred):
return next((i for i, (c, p) in enumerate(seq) if pred(c, p)), None)
deassoc = index_of(lambda c, p: c == MAC_CONFIG
and struct.unpack_from('<I', p, 4)[0] == 2 and p[36] == 0)
sta_rm = index_of(lambda c, p: c == STA_REMOVE)
deact = index_of(lambda c, p: c == LINK_CONFIG
and struct.unpack_from('<I', p, 0)[0] == 2
and struct.unpack_from('<I', p, 24)[0] & 0x01
and struct.unpack_from('<I', p, 28)[0] == 0)
link_rm = index_of(lambda c, p: c == LINK_CONFIG
and struct.unpack_from('<I', p, 0)[0] == 3)
mac_rm = index_of(lambda c, p: c == MAC_CONFIG
and struct.unpack_from('<I', p, 4)[0] == 3)
check(deassoc is not None,
"teardown sends a MAC_CONFIG MODIFY clearing is_assoc")
check(deact is not None,
"teardown deactivates the link before removing it")
check(deassoc is not None and deact is not None and deassoc < deact,
"the association is cleared before the link is deactivated")
check(deassoc is not None and sta_rm is not None and deassoc < sta_rm,
"the association is cleared before the station is removed")
check(sta_rm is not None and deact is not None and sta_rm < deact,
"the station is removed before the link it sits on is deactivated")
check(deact is not None and link_rm is not None and deact < link_rm,
"the link is deactivated before it is removed")
check(link_rm is not None and mac_rm is not None and link_rm < mac_rm,
"the link is removed before the MAC that owns it")
d7.p.stdin.close()
print()
if fails:
for f in fails: print("FAILURE:", f)
sys.exit(1)
print("All MLME and data-path cases passed.")
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""Second-round supplicant tests: GTK rekey, message-3 retransmission, and RSN
negotiation against realistic access-point information elements."""
import hashlib, hmac, os, subprocess, sys, struct
from cryptography.hazmat.primitives.keywrap import aes_key_wrap
HARNESS = sys.argv[1]
fails = []
def prf(key, label, data, n):
r=b''; i=0
while len(r)<n:
r+=hmac.new(key, label.encode()+b'\x00'+data+bytes([i]), hashlib.sha1).digest(); i+=1
return r[:n]
def derive_ptk(pmk, aa, spa, an, sn):
return prf(pmk, "Pairwise key expansion", min(aa,spa)+max(aa,spa)+min(an,sn)+max(an,sn), 48)
def mic(kck, f): return hmac.new(kck, f, hashlib.sha1).digest()[:16]
HDR=99
def build(ki, replay, nonce, rsc, kd, kck=None):
b=bytearray(HDR+len(kd)); b[0]=2; b[1]=3
struct.pack_into('>H',b,2,HDR+len(kd)-4); b[4]=2
struct.pack_into('>H',b,5,ki); struct.pack_into('>H',b,7,16)
b[9:17]=replay; b[17:49]=nonce; b[65:73]=rsc
struct.pack_into('>H',b,97,len(kd)); b[HDR:]=kd
if ki & 0x0100: b[81:97]=mic(kck, bytes(b))
return bytes(b)
def check_mic(f,kck):
x=bytearray(f); x[81:97]=b'\x00'*16
return mic(kck,bytes(x))==f[81:97]
def gtk_kde(gtk, idx):
kd = bytes([0xdd, 6+len(gtk), 0x00,0x0f,0xac, 0x01, idx, 0x00]) + gtk
if len(kd)%8: kd += b'\xdd' + b'\x00'*(7-len(kd)%8)
return kd
def start(p, ssid, passphrase, spa, aa):
p.stdin.write(f"START {spa.hex()} {aa.hex()} {ssid} {passphrase} 2 4 4\n"); p.stdin.flush()
p.stdout.readline(); p.stdout.readline()
def rd(p): return p.stdout.readline().strip()
# ---------------------------------------------------------------- handshake +
print("=== GTK rekey and message-3 retransmission ===")
aa=bytes.fromhex('001122334455'); spa=bytes.fromhex('aabbccddeeff')
ssid="MontaukTest"; pw="supersecret123"
p=subprocess.Popen([HARNESS],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True,bufsize=1)
start(p, ssid, pw, spa, aa)
pmk=hashlib.pbkdf2_hmac('sha1',pw.encode(),ssid.encode(),4096,32)
an=os.urandom(32)
p.stdin.write("RX "+build(0x0002|0x0008|0x0080,(1).to_bytes(8,'big'),an,b'\x00'*8,b'').hex()+"\n"); p.stdin.flush()
m2=bytes.fromhex(rd(p).split()[1]); rd(p)
sn=m2[17:49]
ptk=derive_ptk(pmk,aa,spa,an,sn); kck,kek,tk=ptk[:16],ptk[16:32],ptk[32:48]
gtk1=os.urandom(16)
m3=build(0x0002|0x0008|0x0040|0x0080|0x0100|0x0200|0x1000,(2).to_bytes(8,'big'),an,
b'\x00'*8, aes_key_wrap(kek, gtk_kde(gtk1,1)), kck)
p.stdin.write("RX "+m3.hex()+"\n"); p.stdin.flush()
got={}
while True:
l=rd(p)
if l.startswith('RX-OK'): break
k,_,v=l.partition(' '); got[k]=v
assert got['GTK']==gtk1.hex() and got['PTK']==tk.hex()
print(f" initial handshake complete, state {l}")
# message 3 retransmitted (AP missed message 4)
m3b=build(0x0002|0x0008|0x0040|0x0080|0x0100|0x0200|0x1000,(3).to_bytes(8,'big'),an,
b'\x00'*8, aes_key_wrap(kek, gtk_kde(gtk1,1)), kck)
p.stdin.write("RX "+m3b.hex()+"\n"); p.stdin.flush()
got2={}
while True:
l=rd(p)
if l.startswith('RX-OK'): break
k,_,v=l.partition(' '); got2[k]=v
if 'TX' not in got2:
fails.append("no message 4 in response to a retransmitted message 3")
else:
m4=bytes.fromhex(got2['TX'])
ok = check_mic(m4,kck) and m4[9:17]==(3).to_bytes(8,'big')
print(f" answered retransmitted message 3 with a fresh message 4: {ok}")
if not ok: fails.append("message 4 replay answer malformed")
# group key rekey (2-way, no pairwise bit)
gtk2=os.urandom(16)
gk=build(0x0002|0x0080|0x0100|0x0200|0x1000,(4).to_bytes(8,'big'),b'\x00'*32,
bytes.fromhex('0a0b0c000000 0000'.replace(' ','')), aes_key_wrap(kek, gtk_kde(gtk2,2)), kck)
p.stdin.write("RX "+gk.hex()+"\n"); p.stdin.flush()
got3={}
while True:
l=rd(p)
if l.startswith('RX-OK'): break
k,_,v=l.partition(' '); got3[k]=v
if got3.get('GTK')!=gtk2.hex():
fails.append(f"group rekey installed {got3.get('GTK')} instead of {gtk2.hex()}")
else:
print(f" group rekey installed the new GTK (index {got3['GTK-IDX']})")
if 'TX' not in got3:
fails.append("no acknowledgement to the group key message")
else:
m=bytes.fromhex(got3['TX'])
ki=struct.unpack_from('>H',m,5)[0]
ok = bool(check_mic(m, kck) and not (ki & 0x0008) and (ki & 0x0200))
print(f" group key acknowledged correctly (no pairwise bit, secure set): {ok}")
if not ok: fails.append("group key acknowledgement malformed")
p.stdin.close()
# ---------------------------------------------------------------- RSN parsing
print("\n=== RSN negotiation against real-world information elements ===")
def suite(t): return bytes([0x00,0x0f,0xac,t])
def ie(group, pairwise, akms, caps=0):
b = struct.pack('<H',1) + suite(group)
b += struct.pack('<H',len(pairwise)) + b''.join(suite(x) for x in pairwise)
b += struct.pack('<H',len(akms)) + b''.join(suite(x) for x in akms)
b += struct.pack('<H',caps)
return b
cases = [
("WPA2-PSK, CCMP only", ie(4,[4],[2]), True, 2, 4),
("WPA2 mixed TKIP+CCMP (TKIP group)",ie(2,[4,2],[2]), False,0, 0),
("WPA2 mixed pairwise, CCMP group", ie(4,[4,2],[2]), True, 2, 4),
("WPA2/WPA3 transition (PSK+SAE)", ie(4,[4],[2,8],0x0080), True, 2, 4),
("WPA3-only SAE, MFP required", ie(4,[4],[8],0x00c0), False,0, 0),
("TKIP-only", ie(2,[2],[2]), False,0, 0),
("enterprise 802.1X", ie(4,[4],[1]), False,0, 0),
("PSK-SHA256 only", ie(4,[4],[6]), True, 6, 4),
("GCMP-256", ie(9,[9],[2]), True, 2, 9),
]
p=subprocess.Popen([HARNESS],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True,bufsize=1)
for name, body, expect_ok, expect_akm, expect_pc in cases:
p.stdin.write("PARSE "+body.hex()+"\n"); p.stdin.flush()
r=rd(p).split()
ok = r[1]=='1'; akm=int(r[3]); pc=int(r[5])
good = (ok==expect_ok) and (not expect_ok or (akm==expect_akm and pc==expect_pc))
print(f" {'PASS' if good else 'FAIL'} {name:36} -> accepted={ok} akm={akm} pairwise={pc}")
if not good: fails.append(f"RSN parse: {name}")
p.stdin.close()
print()
if fails:
for f in fails: print("FAILURE:", f)
sys.exit(1)
print("All second-round supplicant cases passed.")
+111
View File
@@ -0,0 +1,111 @@
#include <cstdio>
#include <cstring>
#include "Libraries/Crypto.hpp"
using namespace Kt::Crypto;
static int fails = 0;
static void hexdump(const char* label, const uint8_t* p, size_t n) {
printf("%s: ", label);
for (size_t i = 0; i < n; i++) printf("%02x", p[i]);
printf("\n");
}
static void check(const char* name, const uint8_t* got, const char* wantHex) {
size_t n = strlen(wantHex) / 2;
uint8_t want[128];
for (size_t i = 0; i < n; i++) { unsigned v; sscanf(wantHex + 2*i, "%2x", &v); want[i] = (uint8_t)v; }
if (memcmp(got, want, n) == 0) { printf("PASS %s\n", name); }
else { printf("FAIL %s\n", name); hexdump(" got ", got, n); printf(" want: %s\n", wantHex); fails++; }
}
int main() {
uint8_t out[64];
Sha1("abc", 3, out);
check("sha1(abc)", out, "a9993e364706816aba3e25717850c26c9cd0d89d");
Sha1("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, out);
check("sha1(448bit)", out, "84983e441c3bd26ebaae4aa1f95129e5e54670f1");
Sha256("abc", 3, out);
check("sha256(abc)", out, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad");
Sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56, out);
check("sha256(448bit)", out, "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1");
{ uint8_t k[20]; memset(k, 0x0b, 20);
HmacSha1(k, 20, "Hi There", 8, out);
check("hmac-sha1 rfc2202#1", out, "b617318655057264e28bc0b6fb378c8ef146be00"); }
{ HmacSha1((const uint8_t*)"Jefe", 4, "what do ya want for nothing?", 28, out);
check("hmac-sha1 rfc2202#2", out, "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"); }
{ uint8_t k[20]; memset(k, 0x0b, 20);
HmacSha256(k, 20, "Hi There", 8, out);
check("hmac-sha256 rfc4231#1", out, "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); }
// FIPS-197 AES-128 / AES-256
{ AesCtx c; uint8_t key[16], pt[16], ct[16], back[16];
for (int i = 0; i < 16; i++) key[i] = (uint8_t)i;
for (int i = 0; i < 16; i++) pt[i] = (uint8_t)(i * 0x11);
AesInit(c, key, 16); AesEncryptBlock(c, pt, ct);
check("aes128 fips197", ct, "69c4e0d86a7b0430d8cdb78070b4c55a");
AesDecryptBlock(c, ct, back);
check("aes128 decrypt", back, "00112233445566778899aabbccddeeff"); }
{ AesCtx c; uint8_t key[32], pt[16], ct[16], back[16];
for (int i = 0; i < 32; i++) key[i] = (uint8_t)i;
for (int i = 0; i < 16; i++) pt[i] = (uint8_t)(i * 0x11);
AesInit(c, key, 32); AesEncryptBlock(c, pt, ct);
check("aes256 fips197", ct, "8ea2b7ca516745bfeafc49904b496089");
AesDecryptBlock(c, ct, back);
check("aes256 decrypt", back, "00112233445566778899aabbccddeeff"); }
// RFC 3394 section 4.1 (128-bit KEK, 128-bit key) and 4.6 (256/256)
{ uint8_t kek[16], kd[16], wrapped[24], unwrapped[16];
for (int i = 0; i < 16; i++) kek[i] = (uint8_t)i;
for (int i = 0; i < 16; i++) kd[i] = (uint8_t)(i * 0x11);
AesKeyWrap(kek, 16, kd, 16, wrapped);
check("keywrap rfc3394 4.1", wrapped, "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5");
bool ok = AesKeyUnwrap(kek, 16, wrapped, 24, unwrapped);
printf("%s keyunwrap integrity\n", ok ? "PASS" : "FAIL"); if (!ok) fails++;
check("keyunwrap rfc3394 4.1", unwrapped, "00112233445566778899aabbccddeeff"); }
{ // 256-bit KEK, 256-bit key data (RFC3394 4.6)
uint8_t kek[32], kd[32], wrapped[40], unwrapped[32];
for (int i = 0; i < 32; i++) kek[i] = (uint8_t)i;
const char* kdhex = "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F";
for (int i = 0; i < 32; i++) { unsigned v; sscanf(kdhex + 2*i, "%2x", &v); kd[i] = (uint8_t)v; }
AesKeyWrap(kek, 32, kd, 32, wrapped);
check("keywrap rfc3394 4.6", wrapped, "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21");
bool ok = AesKeyUnwrap(kek, 32, wrapped, 40, unwrapped);
printf("%s keyunwrap256 integrity\n", ok ? "PASS" : "FAIL"); if (!ok) fails++; }
// RFC 4493 AES-CMAC
{ uint8_t key[16] = {0x2b,0x7e,0x15,0x16,0x28,0xae,0xd2,0xa6,0xab,0xf7,0x15,0x88,0x09,0xcf,0x4f,0x3c};
const uint8_t* parts[1]; size_t lens[1];
uint8_t empty[1] = {0};
parts[0] = empty; lens[0] = 0;
AesCmac(key, 16, parts, lens, 1, out);
check("cmac rfc4493 len0", out, "bb1d6929e95937287fa37d129b756746");
uint8_t msg[64] = {0x6b,0xc1,0xbe,0xe2,0x2e,0x40,0x9f,0x96,0xe9,0x3d,0x7e,0x11,0x73,0x93,0x17,0x2a,
0xae,0x2d,0x8a,0x57,0x1e,0x03,0xac,0x9c,0x9e,0xb7,0x6f,0xac,0x45,0xaf,0x8e,0x51,
0x30,0xc8,0x1c,0x46,0xa3,0x5c,0xe4,0x11,0xe5,0xfb,0xc1,0x19,0x1a,0x0a,0x52,0xef,
0xf6,0x9f,0x24,0x45,0xdf,0x4f,0x9b,0x17,0xad,0x2b,0x41,0x7b,0xe6,0x6c,0x37,0x10};
parts[0] = msg; lens[0] = 16;
AesCmac(key, 16, parts, lens, 1, out);
check("cmac rfc4493 len16", out, "070a16b46b4d4144f79bdd9dd04a287c");
parts[0] = msg; lens[0] = 40;
AesCmac(key, 16, parts, lens, 1, out);
check("cmac rfc4493 len40", out, "dfa66747de9ae63030ca32611497c827");
parts[0] = msg; lens[0] = 64;
AesCmac(key, 16, parts, lens, 1, out);
check("cmac rfc4493 len64", out, "51f0bebf7e3b9d92fc49741779363cfe");
// split across parts to exercise the streaming path
const uint8_t* sp[3] = {msg, msg+10, msg+33}; size_t sl[3] = {10, 23, 31};
AesCmac(key, 16, sp, sl, 3, out);
check("cmac split len64", out, "51f0bebf7e3b9d92fc49741779363cfe"); }
// WPA PSK vectors (IEEE 802.11i Annex H.4)
{ uint8_t psk[32];
Pbkdf2Sha1("password", 8, (const uint8_t*)"IEEE", 4, 4096, psk, 32);
check("pbkdf2 wpa 'password'/IEEE", psk, "f42c6fc52df0ebef9ebb4b90b38a5f902e83fe1b135a70e23aed762e9710a12e");
Pbkdf2Sha1("ThisIsAPassword", 15, (const uint8_t*)"ThisIsASSID", 11, 4096, psk, 32);
check("pbkdf2 wpa 'ThisIsAPassword'", psk, "0dc0d6eb90555ed6419756b9a15ec3e3209b63df707dd508d14581f8982721af"); }
printf(fails ? "\n%d FAILURES\n" : "\nall vectors pass\n", fails);
return fails != 0;
}
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Decode an iwlwifi host-command trace captured from Linux on the same adapter.
Linux drives this exact firmware successfully, so its host commands are ground
truth for what the firmware expects -- far more reliable than reading struct
definitions out of a kernel tree and hoping the version matches.
Capture on a Linux box with the adapter, as root:
trace-cmd record -e iwlwifi_dev_hcmd -o /tmp/iwl.dat \
sh -c 'nmcli radio wifi off; sleep 2; nmcli radio wifi on; sleep 20'
Then run this against /tmp/iwl.dat. dump_iwl_cmds.py prints full payload hex
for each command instead of a decoded summary.
Copyright (c) 2026 Daniel Hammer
"""
import re, subprocess
out = subprocess.run(['trace-cmd','report','-R','-i','/tmp/iwl.dat'],
capture_output=True, text=True).stdout
ACT={0:'STUB',1:'ADD',2:'MODIFY',3:'REMOVE'}
def u32(p,o): return p[o]|(p[o+1]<<8)|(p[o+2]<<16)|(p[o+3]<<24)
rows=[]
for line in out.splitlines():
m=re.search(r'hcmd=ARRAY\[(.*?)\]',line); t=re.search(r'\s(\d+\.\d+):',line)
if not m: continue
b=[int(x,16) for x in m.group(1).split(', ')]
if len(b)<8: continue
grp,op=b[1],b[0]; ln=b[4]|(b[5]<<8); p=b[8:8+ln]; ts=float(t.group(1)) if t else 0
if (grp,op)==(3,0x08) and ln>=52:
rows.append((ts,f"MAC_CONFIG action={ACT.get(u32(p,4)):6} id={u32(p,0)} is_assoc={p[36]} aid={p[40]|p[41]<<8} filter=0x{u32(p,20):x}"))
elif (grp,op)==(3,0x09) and ln>=208:
rows.append((ts,f"LINK_CONFIG action={ACT.get(u32(p,0)):6} link={u32(p,4)} mac={u32(p,8)} phy={u32(p,12)} mask=0x{u32(p,24):02x} active={p[28]} bi={u32(p,136)}"))
elif (grp,op)==(3,0x0a) and ln>=96:
rows.append((ts,f"STA_CONFIG sta={u32(p,0)} link={u32(p,4)} type={u32(p,24)} aid={u32(p,28)}"))
elif (grp,op)==(1,0x08) and ln>=32:
rows.append((ts,f"PHY_CONTEXT action={ACT.get(u32(p,4)):6} id={u32(p,0)} chan={u32(p,8)} band={p[12]} width={p[13]}"))
elif (grp,op)==(3,0x05):
rows.append((ts,f"SESSION_PROT action={ACT.get(u32(p,4)):6} conf_id={u32(p,8)} dur={u32(p,12)}"))
elif (grp,op)==(5,0x17):
rows.append((ts,f"SCD_QUEUE_CFG op={u32(p,0)}"))
elif (grp,op)==(3,0x0c):
rows.append((ts,"STA_REMOVE"))
rows.sort(); base=rows[0][0]
for ts,s in rows:
if ts-base > 2.3: print(f"{ts-base:7.3f} {s}")
+27
View File
@@ -0,0 +1,27 @@
import re, sys, subprocess
out = subprocess.run(['trace-cmd','report','-R','-i','/tmp/iwl.dat'],
capture_output=True, text=True).stdout
NAMES = {(3,0x08):'MAC_CONFIG', (3,0x09):'LINK_CONFIG', (3,0x0a):'STA_CONFIG',
(3,0x0c):'STA_REMOVE', (1,0x08):'PHY_CONTEXT', (3,0x05):'SESSION_PROT'}
seen = {}
for line in out.splitlines():
m = re.search(r'hcmd=ARRAY\[(.*?)\]', line)
if not m: continue
b = [int(x,16) for x in m.group(1).split(', ')]
if len(b) < 8: continue
op, grp = b[0], b[1]
ln = b[4] | (b[5] << 8)
key = (grp, op)
if key not in NAMES: continue
payload = b[8:8+ln]
action = payload[0] | (payload[1]<<8) | (payload[2]<<16) | (payload[3]<<24) if len(payload)>=4 else -1
seen.setdefault(key, []).append((ln, payload))
for key, lst in seen.items():
print(f"\n===== {NAMES[key]} (group 0x{key[0]:02x} cmd 0x{key[1]:02x}) x{len(lst)} =====")
lens = {l for l,_ in lst}
print(f"payload length(s): {sorted(lens)}")
# show the most 'interesting' one (most non-zero bytes)
ln, p = max(lst, key=lambda t: sum(1 for x in t[1] if x))
for off in range(0, len(p), 16):
chunk = p[off:off+16]
print(f" +{off:3d}: " + " ".join(f"{x:02x}" for x in chunk))
+217
View File
@@ -0,0 +1,217 @@
/*
* mlme_harness.cpp
* Host harness for the 802.11 MLME and data path.
*
* Compiles the real IwxConnect.cpp (and the supplicant behind it) against a
* stubbed transport, so everything the driver puts on the air and everything
* it makes of what comes back can be checked without the adapter. The
* firmware/radio interaction is what remains untestable here; the frame
* construction, parsing, encapsulation and state machine are not.
*
* Speaks hex over stdio; ap_mlme.py is the peer.
*
* Copyright (c) 2026 Daniel Hammer
*/
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <string>
#include <iostream>
#include "Drivers/Net/Wifi/Iwx.hpp"
#include "Drivers/Net/Wifi/Ieee80211.hpp"
#include "Drivers/Net/Wifi/Wpa.hpp"
namespace Timekeeping {
uint64_t g_ms = 1000;
uint64_t GetMilliseconds() { return g_ms; }
}
// A host command is a round trip to the firmware: it takes real time, and the
// clock moves on while the service loop is inside one. Modelling that is what
// catches elapsed-time arithmetic that samples the clock once and then compares
// it against timestamps taken later in the same pass (an unsigned underflow
// that reads as an instant timeout). A frozen clock hides that class of bug
// entirely, which is why this is not simply left at a constant.
static constexpr uint64_t CMD_ROUND_TRIP_MS = 1;
using namespace Drivers::Net::Wifi;
static void puthex(const char* tag, const uint8_t* p, uint32_t n) {
printf("%s ", tag);
for (uint32_t i = 0; i < n; i++) printf("%02x", p[i]);
printf("\n");
}
// =============================================================================
// Stubbed transport
// =============================================================================
namespace Drivers::Net::Wifi {
IwxState g_iwx;
// Host commands: record the opcode and the exact bytes, always succeed.
// The payload matters -- a struct that does not match the version the
// firmware advertises asserts it on real hardware.
static void DumpCmd(uint32_t id, const void* data, uint32_t len) {
printf("CMD %u ", id);
const uint8_t* p = (const uint8_t*)data;
for (uint32_t i = 0; i < len; i++) printf("%02x", p[i]);
printf("\n");
Timekeeping::g_ms += CMD_ROUND_TRIP_MS;
}
bool IwxSendCmdPdu(uint32_t id, const void* data, uint32_t len) {
DumpCmd(id, data, len);
return true;
}
bool IwxSendCmdStatus(uint32_t id, const void* data, uint32_t len,
uint32_t* statusOut) {
DumpCmd(id, data, len);
// ADD_STA reports success in the low byte; everything else uses 0.
if (statusOut) *statusOut = (id == IWX_ADD_STA || id == IWX_ADD_STA_KEY)
? IWX_ADD_STA_SUCCESS : 0;
return true;
}
bool IwxSendCmd(IwxHostCmd& cmd) { (void)cmd; return true; }
int IwxLookupCmdVer(uint8_t group, uint8_t cmd) {
// Match what AX211 firmware 89 advertises for the versions the MLME
// branches on.
if (group == IWX_DATA_PATH_GROUP && cmd == IWX_RLC_CONFIG_CMD) return 2;
if (group == IWX_DATA_PATH_GROUP && cmd == IWX_SCD_QUEUE_CONFIG_CMD) return 3;
return -1;
}
int IwxLookupNotifVer(uint8_t, uint8_t) { return 7; }
bool IwxAbortScan() { return true; }
bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) {
(void)staId; (void)tid;
ring.Qid = qid;
ring.Active = true;
ring.StageSlots = IWX_TX_STAGE_SLOTS;
printf("TXQ-UP %d\n", qid);
return true;
}
void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) {
(void)staId; (void)tid;
ring.Active = false;
printf("TXQ-DOWN\n");
}
// Capture what the driver wants to put on the air.
bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen,
const uint8_t* payload, uint32_t payloadLen,
bool encrypt, bool fixedRate) {
if (!ring.Active) { printf("TX-DROP queue-down\n"); return false; }
printf("TX enc=%d rate=%d ", encrypt ? 1 : 0, fixedRate ? 1 : 0);
for (uint32_t i = 0; i < hdrLen; i++) printf("%02x", hdr[i]);
printf(" ");
for (uint32_t i = 0; i < payloadLen; i++) printf("%02x", payload[i]);
printf("\n");
return true;
}
bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx,
bool pairwise, uint8_t cipher, const uint8_t* rsc) {
printf("KEY pairwise=%d idx=%u cipher=%u ", pairwise ? 1 : 0, keyIdx, cipher);
for (uint32_t i = 0; i < keyLen; i++) printf("%02x", key[i]);
printf(" ");
if (rsc) for (int i = 0; i < 6; i++) printf("%02x", rsc[i]);
printf("\n");
return true;
}
bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher,
uint32_t keyLen) {
printf("KEY-REMOVE pairwise=%d idx=%u cipher=%u len=%u\n",
pairwise ? 1 : 0, keyIdx, cipher, keyLen);
return true;
}
// Sink normally provided by Wifi.cpp.
void WifiRxEthernet(const uint8_t* frame, uint32_t len) {
puthex("ETH", frame, len);
}
}
// =============================================================================
// Driver
// =============================================================================
static int unhex(const std::string& s, uint8_t* out) {
int n = 0;
for (size_t i = 0; i + 1 < s.size(); i += 2) {
unsigned v; sscanf(s.c_str() + i, "%2x", &v); out[n++] = (uint8_t)v;
}
return n;
}
int main() {
// A firmware state good enough for the MLME: alive, one antenna, a MAC.
g_iwx.State = IwxFwState::Running;
g_iwx.Fw.PhyConfig = (1u << IWX_FW_PHY_CFG_TX_CHAIN_POS)
| (1u << IWX_FW_PHY_CFG_RX_CHAIN_POS);
g_iwx.Nvm.ValidTxAnt = 1;
g_iwx.Nvm.ValidRxAnt = 1;
std::string line;
uint8_t buf[4096];
while (std::getline(std::cin, line)) {
if (line.rfind("MAC ", 0) == 0) {
unhex(line.substr(4), g_iwx.Nvm.HwAddr);
printf("DONE\n");
} else if (line.rfind("CAPA ", 0) == 0) {
unsigned bit, on;
sscanf(line.c_str(), "CAPA %u %u", &bit, &on);
if (on) g_iwx.Fw.Capa[bit / 8] |= (uint8_t)(1 << (bit % 8));
else g_iwx.Fw.Capa[bit / 8] &= (uint8_t)~(1 << (bit % 8));
printf("DONE\n");
} else if (line.rfind("CONNECT ", 0) == 0) {
// CONNECT <bssid> <channel> <is5> <ssid> <pass|-> <rsnie|->
char bssid[64], ssid[64], pass[128], rsn[256];
unsigned chan, is5;
sscanf(line.c_str(), "CONNECT %63s %u %u %63s %127s %255s",
bssid, &chan, &is5, ssid, pass, rsn);
uint8_t bs[6]; unhex(bssid, bs);
uint8_t ie[128]; int ieLen = 0;
if (strcmp(rsn, "-") != 0) ieLen = unhex(rsn, ie);
bool ok = IwxConnectStart(bs, (uint8_t)chan, is5 != 0, ssid,
strcmp(pass, "-") == 0 ? nullptr : pass,
ieLen ? ie : nullptr, (uint32_t)ieLen,
100, 2);
printf("CONNECT-OK %d STATE %d\n", ok ? 1 : 0, IwxConnectState());
} else if (line.rfind("RXMGMT ", 0) == 0) {
int n = unhex(line.substr(7), buf);
IwxConnectRxMgmt(buf, (uint32_t)n);
printf("DONE STATE %d\n", IwxConnectState());
} else if (line.rfind("RXDATA ", 0) == 0) {
int n = unhex(line.substr(7), buf);
IwxConnectRxData(buf, (uint32_t)n);
printf("DONE STATE %d\n", IwxConnectState());
} else if (line.rfind("TXETH ", 0) == 0) {
int n = unhex(line.substr(6), buf);
bool ok = IwxConnectSendEthernet(buf, (uint32_t)n);
printf("TXETH-OK %d\n", ok ? 1 : 0);
} else if (line.rfind("SERVICE", 0) == 0) {
IwxConnectService();
printf("DONE STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);
} else if (line.rfind("TICK ", 0) == 0) {
unsigned ms; sscanf(line.c_str(), "TICK %u", &ms);
Timekeeping::g_ms += ms;
printf("DONE\n");
} else if (line.rfind("ABORT", 0) == 0) {
IwxConnectAbort();
printf("DONE STATE %d\n", IwxConnectState());
} else if (line.rfind("STATE", 0) == 0) {
printf("STATE %d LINK %d\n", IwxConnectState(), IwxLinkUp() ? 1 : 0);
} else if (line.rfind("QUIT", 0) == 0) {
break;
}
fflush(stdout);
}
return 0;
}
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
#
# Host-side tests for the Wi-Fi crypto and the WPA supplicant.
#
# These compile the real kernel sources (Libraries/Crypto.cpp and
# Drivers/Net/Wifi/Wpa.cpp) for the host against the small shim in shim/, so
# what is exercised is the code that ships, not a copy of it.
#
# Requires: g++ with C++20, python3 with the `cryptography` package.
#
# Copyright (c) 2026 Daniel Hammer
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/../.." && pwd)"
src="$root/kernel/src"
out="$(mktemp -d)"
trap 'rm -rf "$out"' EXIT
CXX="${CXX:-g++}"
CXXFLAGS="-O1 -std=c++20 -Wall -I$here/shim -I$src"
echo "== crypto primitives against published vectors =="
$CXX $CXXFLAGS -o "$out/crypto_vectors" \
"$here/crypto_vectors.cpp" "$src/Libraries/Crypto.cpp"
"$out/crypto_vectors"
echo
echo "== supplicant against an independent authenticator =="
$CXX $CXXFLAGS -o "$out/supplicant" \
"$here/supplicant_harness.cpp" \
"$src/Drivers/Net/Wifi/Wpa.cpp" "$src/Libraries/Crypto.cpp"
python3 "$here/ap_handshake.py" "$out/supplicant"
python3 "$here/ap_rekey_and_rsn.py" "$out/supplicant"
echo
echo "== MLME and data path against a simulated access point =="
$CXX $CXXFLAGS -o "$out/mlme" \
"$here/mlme_harness.cpp" \
"$src/Drivers/Net/Wifi/IwxConnect.cpp" \
"$src/Drivers/Net/Wifi/Wpa.cpp" "$src/Libraries/Crypto.cpp"
python3 "$here/ap_mlme.py" "$out/mlme"
echo
echo "All Wi-Fi host tests passed."
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <atomic>
#include <cstdint>
namespace kcp {
class Spinlock {
std::atomic_flag f{ATOMIC_FLAG_INIT};
public:
void Acquire() { while (f.test_and_set(std::memory_order_acquire)) {} }
void Release() { f.clear(std::memory_order_release); }
};
// The kernel's Mutex is the non-interrupt-disabling variant; on the host
// there is nothing to disable, so the two are the same here.
class Mutex {
std::atomic_flag f{ATOMIC_FLAG_INIT};
public:
void Acquire() { while (f.test_and_set(std::memory_order_acquire)) {} }
void Release() { f.clear(std::memory_order_release); }
};
}
+2
View File
@@ -0,0 +1,2 @@
#pragma once
namespace base { struct Manip {}; inline Manip hex, dec; }
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include <cstring>
+4
View File
@@ -0,0 +1,4 @@
#pragma once
#include <cstdint>
// Only the type name is needed: the MLME never touches PCI.
namespace Pci { struct PciDevice { uint8_t Bus, Device, Function; }; }
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <cstdio>
#include <cstdint>
namespace Kt {
enum KernelLogLevel { INFO, OK, WARNING, ERROR };
class KernelLogStream {
public:
KernelLogStream(KernelLogLevel l, const char* c) { fprintf(stderr, " [%s] ", c); (void)l; }
~KernelLogStream() { fprintf(stderr, "\n"); }
KernelLogStream& operator<<(const char* s) { fprintf(stderr, "%s", s); return *this; }
KernelLogStream& operator<<(uint64_t v) { fprintf(stderr, "%llu", (unsigned long long)v); return *this; }
KernelLogStream& operator<<(int v) { fprintf(stderr, "%d", v); return *this; }
};
}
@@ -0,0 +1,3 @@
#pragma once
#include <cstdint>
namespace Timekeeping { uint64_t GetMilliseconds(); }
+94
View File
@@ -0,0 +1,94 @@
// Host harness: drives the real kernel supplicant through a 4-way handshake.
// Frames come in / go out as hex on stdio so an independent Python AP can be
// the oracle.
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <string>
#include <iostream>
#include "Drivers/Net/Wifi/Wpa.hpp"
#include "Drivers/Net/Wifi/Ieee80211.hpp"
namespace Timekeeping { static uint64_t g_ms = 0; uint64_t GetMilliseconds() { return g_ms; } }
using namespace Drivers::Net::Wifi;
static void puthex(const char* tag, const uint8_t* p, size_t n) {
printf("%s ", tag);
for (size_t i = 0; i < n; i++) printf("%02x", p[i]);
printf("\n");
}
// --- hooks the supplicant calls back into ---
namespace Drivers::Net::Wifi {
bool WpaTxEapol(const uint8_t* body, uint32_t len) {
puthex("TX", body, len);
return true;
}
bool WpaInstallPtk(const uint8_t* tk, uint32_t tkLen, uint8_t cipher) {
printf("PTK-CIPHER %u\n", cipher);
puthex("PTK", tk, tkLen);
return true;
}
bool WpaInstallGtk(const uint8_t* gtk, uint32_t gtkLen, uint8_t keyIdx,
uint8_t cipher, const uint8_t* rsc) {
printf("GTK-IDX %u\nGTK-CIPHER %u\n", keyIdx, cipher);
puthex("GTK", gtk, gtkLen);
puthex("GTK-RSC", rsc, 8);
return true;
}
}
static int unhex(const std::string& s, uint8_t* out) {
int n = 0;
for (size_t i = 0; i + 1 < s.size(); i += 2) {
unsigned v; sscanf(s.c_str() + i, "%2x", &v); out[n++] = (uint8_t)v;
}
return n;
}
int main() {
std::string line;
uint8_t buf[2048];
while (std::getline(std::cin, line)) {
if (line.rfind("START ", 0) == 0) {
// START <ownmac> <bssid> <ssid> <passphrase> <akm> <pcipher> <gcipher>
char own[64], bss[64], ssid[64], pass[128];
unsigned akm, pc, gc;
sscanf(line.c_str(), "START %63s %63s %63s %127s %u %u %u",
own, bss, ssid, pass, &akm, &pc, &gc);
WpaConfig cfg = {};
unhex(own, cfg.OwnMac);
unhex(bss, cfg.Bssid);
cfg.SsidLen = (uint8_t)strlen(ssid);
memcpy(cfg.Ssid, ssid, cfg.SsidLen);
cfg.PassLen = (uint8_t)strlen(pass);
memcpy(cfg.Passphrase, pass, cfg.PassLen);
cfg.Akm = (uint8_t)akm;
cfg.PairwiseCipher = (uint8_t)pc;
cfg.GroupCipher = (uint8_t)gc;
printf("START-OK %d\n", WpaStart(cfg) ? 1 : 0);
uint8_t ie[64];
uint32_t ieLen = WpaBuildRsnIe(ie, sizeof(ie));
puthex("RSNIE", ie, ieLen);
} else if (line.rfind("RX ", 0) == 0) {
int n = unhex(line.substr(3), buf);
bool consumed = WpaOnEapol(buf, (uint32_t)n);
printf("RX-OK %d STATE %d\n", consumed ? 1 : 0, (int)WpaGetState());
} else if (line.rfind("PARSE ", 0) == 0) {
int n = unhex(line.substr(6), buf);
WpaConfig cfg = {};
bool ok = WpaParseApRsn(buf, (uint32_t)n, cfg);
printf("PARSE-OK %d AKM %u PCIPHER %u GCIPHER %u\n", ok ? 1 : 0,
(unsigned)cfg.Akm, (unsigned)cfg.PairwiseCipher,
(unsigned)cfg.GroupCipher);
} else if (line.rfind("STATE", 0) == 0) {
printf("STATE %d\n", (int)WpaGetState());
} else if (line.rfind("QUIT", 0) == 0) {
break;
}
fflush(stdout);
}
return 0;
}