#!/usr/bin/env python3 """Drives the real MLME through a complete join and validates every frame it puts on the air, plus the 802.11 <-> Ethernet translation in both directions. The access point side is written independently here (hashlib / cryptography for the handshake, hand-decoded 802.11 for the frames), so this is a check of what the driver actually emits rather than a restatement of it. Copyright (c) 2026 Daniel Hammer """ import hashlib, hmac, os, struct, subprocess, sys from cryptography.hazmat.primitives.keywrap import aes_key_wrap HARNESS = sys.argv[1] AP = bytes.fromhex('001122334455') STA = bytes.fromhex('aabbccddeeff') SSID = "MontaukTest" PASS = "supersecret123" CHANNEL = 6 fails = [] def state_of(result): f = result.split() return int(f[f.index('STATE') + 1]) if 'STATE' in f else -1 def link_of(result): f = result.split() return int(f[f.index('LINK') + 1]) if 'LINK' in f else -1 def check(cond, what): print(f" {'PASS' if cond else 'FAIL'} {what}") if not cond: fails.append(what) # --------------------------------------------------------------- harness I/O class Driver: def __init__(self): self.p = subprocess.Popen([HARNESS], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1) def cmd(self, line): """Send a command; collect emitted events until the terminator.""" self.p.stdin.write(line + '\n'); self.p.stdin.flush() ev = {'TX': [], 'KEY': [], 'KEYDEL': [], 'ETH': [], 'CMD': [], 'TXQ': []} while True: l = self.p.stdout.readline() if not l: raise RuntimeError("harness died") l = l.strip() k = l.split()[0] if l else '' if k == 'TX': f = l.split() ev['TX'].append({'enc': f[1] == 'enc=1', 'rate': f[2] == 'rate=1', 'hdr': bytes.fromhex(f[3]), 'body': bytes.fromhex(f[4]) if len(f) > 4 else b''}) elif k == 'KEY': f = l.split() ev['KEY'].append({'pairwise': f[1] == 'pairwise=1', 'idx': int(f[2].split('=')[1]), 'cipher': int(f[3].split('=')[1]), 'key': bytes.fromhex(f[4]), 'rsc': bytes.fromhex(f[5]) if len(f) > 5 else b''}) elif k == 'ETH': ev['ETH'].append(bytes.fromhex(l.split()[1])) elif k == 'CMD': f = l.split() ev['CMD'].append((int(f[1]), bytes.fromhex(f[2]) if len(f) > 2 else b'')) elif k == 'KEY-REMOVE': ev['KEYDEL'].append(l) elif k in ('TXQ-UP', 'TXQ-DOWN'): ev['TXQ'].append(l) else: ev['result'] = l return ev # --------------------------------------------------------------- 802.11 bits def mgmt(subtype, dst, src, bssid, body, seq=0): return bytes([0x00 | subtype, 0x00]) + b'\x00\x00' + dst + src + bssid \ + struct.pack('H', ethertype) return hdr + iv + snap + payload def parse_ies(b): ies, off = {}, 0 while off + 2 <= len(b): i, l = b[off], b[off + 1] if off + 2 + l > len(b): break ies[i] = b[off + 2:off + 2 + l] off += 2 + l return ies def suite(t): return bytes([0x00, 0x0f, 0xac, t]) AP_RSN = (struct.pack('H', b, 2, HDR + len(kd) - 4); b[4] = 2 struct.pack_into('>H', b, 5, ki); struct.pack_into('>H', b, 7, 16) b[9:17] = replay; b[17:49] = nonce; b[65:73] = rsc struct.pack_into('>H', b, 97, len(kd)); b[HDR:] = kd if ki & 0x0100: b[81:97] = emic(kck, bytes(b)) return bytes(b) def check_emic(f, kck): x = bytearray(f); x[81:97] = b'\x00' * 16 return emic(kck, bytes(x)) == f[81:97] def gtk_kde(gtk, idx): kd = bytes([0xdd, 6 + len(gtk), 0x00, 0x0f, 0xac, 0x01, idx, 0x00]) + gtk if len(kd) % 8: kd += b'\xdd' + b'\x00' * (7 - len(kd) % 8) return kd # ============================================================================= d = Driver() d.cmd("MAC " + STA.hex()) print("=== authentication ===") ev = d.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 {SSID} {PASS} {AP_RSN.hex()}") check(ev['result'].startswith('CONNECT-OK 1'), "connect starts") check(len(ev['TXQ']) == 1 and ev['TXQ'][0].startswith('TXQ-UP'), "a transmit queue is opened before any frame is sent") check(len(ev['TX']) == 1, "exactly one frame is sent (the authentication request)") auth = ev['TX'][0] h, b = auth['hdr'], auth['body'] check(h[0] == 0xb0, "authentication frame: type management, subtype auth") check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP, "authentication frame: addressed to the AP, from us, BSSID correct") check(len(h) == 24, "authentication frame: 24-byte header") check(struct.unpack('= 4, "association request: supported rates present") check(any(r & 0x80 for r in ies[1]), "association request: at least one basic rate") check(48 in ies, "association request: RSN element present") sta_rsn = ies[48] check(sta_rsn[0:2] == struct.pack('= 2, "post-association context updates are sent") print("\n=== 4-way handshake carried over 802.11 data frames ===") pmk = hashlib.pbkdf2_hmac('sha1', PASS.encode(), SSID.encode(), 4096, 32) anonce = os.urandom(32) m1 = eapol(0x0002 | 0x0008 | 0x0080, (1).to_bytes(8, 'big'), anonce, b'\x00' * 8, b'') d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m1).hex()) ev = d.cmd("SERVICE") check(len(ev['TX']) == 1, "EAPOL message 1 produces exactly one reply") m2f = ev['TX'][0] h = m2f['hdr'] check(h[0] == 0x08 and (h[1] & 0x01), "EAPOL reply: data frame with to-DS set") check(not (h[1] & 0x40), "EAPOL reply: not marked protected (no key yet)") check(not m2f['enc'], "EAPOL reply: firmware told not to encrypt") check(h[4:10] == AP and h[10:16] == STA and h[16:22] == AP, "EAPOL reply: addr1 the AP, addr2 us, addr3 the AP") check(m2f['body'][0:6] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]), "EAPOL reply: RFC 1042 LLC/SNAP shim") check(m2f['body'][6:8] == struct.pack('>H', 0x888e), "EAPOL reply: EtherType 0x888e") m2 = m2f['body'][8:] snonce = m2[17:49] ptk = derive_ptk(pmk, anonce, snonce) kck, kek, tk = ptk[:16], ptk[16:32], ptk[32:48] check(check_emic(m2, kck), "EAPOL message 2 MIC verifies under the AP's own PTK") check(m2[HDR:] == bytes([48, len(sta_rsn)]) + sta_rsn, "EAPOL message 2 carries the same RSN element as the association request") gtk = os.urandom(16) rsc = bytes.fromhex('0a0b0c0d0e0f0000') m3 = eapol(0x0002 | 0x0008 | 0x0040 | 0x0080 | 0x0100 | 0x0200 | 0x1000, (2).to_bytes(8, 'big'), anonce, rsc, aes_key_wrap(kek, gtk_kde(gtk, 1)), kck) d.cmd("RXDATA " + data_from_ds(STA, AP, AP, 0x888e, m3).hex()) ev = d.cmd("SERVICE") check(len(ev['TX']) == 1, "EAPOL message 3 produces message 4") m4 = ev['TX'][0]['body'][8:] check(check_emic(m4, kck), "EAPOL message 4 MIC verifies") keys = {('pairwise' if k['pairwise'] else 'group'): k for k in ev['KEY']} check('pairwise' in keys and keys['pairwise']['key'] == tk, "pairwise key installed matches the AP's temporal key") check(keys.get('pairwise', {}).get('cipher') == 4, "pairwise key installed as CCMP") check('group' in keys and keys['group']['key'] == gtk, "group key installed matches") check(keys.get('group', {}).get('rsc') == rsc[:6], "group key installed with the AP's receive sequence counter") check(state_of(ev['result']) == 7 and link_of(ev['result']) == 1, "link reports up once keyed") print("\n=== data path ===") ip = bytes.fromhex('4500002800010000401100000a0000010a000002') + b'payload-here' peer = bytes.fromhex('665544332211') eth_out = peer + STA + struct.pack('>H', 0x0800) + ip ev = d.cmd("TXETH " + eth_out.hex()) check(ev['result'] == 'TXETH-OK 1', "an Ethernet frame is accepted for transmit") f = ev['TX'][0] h = f['hdr'] check(h[0] == 0x08 and (h[1] & 0x01), "outbound data: to-DS data frame") check(h[1] & 0x40, "outbound data: protected bit set now that keys are installed") check(f['enc'], "outbound data: firmware asked to encrypt") check(not f['rate'], "outbound data: rate control left to the firmware") check(h[4:10] == AP, "outbound data: addr1 is the AP") check(h[10:16] == STA, "outbound data: addr2 is us") check(h[16:22] == peer, "outbound data: addr3 is the final destination") check(f['body'][0:8] == bytes([0xaa, 0xaa, 0x03, 0, 0, 0]) + struct.pack('>H', 0x0800), "outbound data: LLC/SNAP carries the EtherType") check(f['body'][8:] == ip, "outbound data: IP payload preserved byte for byte") seq1 = struct.unpack('H', 0x0800), "inbound data: EtherType recovered") check(e[14:] == reply, "inbound data: payload recovered past the CCMP header") bcast = bytes.fromhex('ffffffffffff') ev = d.cmd("RXDATA " + data_from_ds(bcast, AP, peer, 0x0806, b'arp-request-body').hex()) check(len(ev['ETH']) == 1 and ev['ETH'][0][0:6] == bcast, "inbound broadcast (ARP) is delivered") ev = d.cmd("RXDATA " + data_from_ds(STA, bytes.fromhex('aa0000000001'), peer, 0x0800, b'from-a-stranger').hex()) check(len(ev['ETH']) == 0, "a data frame from a different BSSID is ignored") ev = d.cmd("RXDATA " + (bytes([0x48, 0x02]) + b'\x00\x00' + STA + AP + peer + b'\x00\x00').hex()) check(len(ev['ETH']) == 0, "a null-data keepalive produces no Ethernet frame") print("\n=== teardown ===") ev = d.cmd("ABORT") check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']), "abort closes the transmit queue") check(len(ev['KEYDEL']) == 2, "abort removes both hardware keys before the station") check(state_of(ev['result']) == 0, "abort returns to idle") ev = d.cmd("TXETH " + eth_out.hex()) check(ev['result'] == 'TXETH-OK 0', "transmit is refused once the link is down") # ============================================================================= # Other paths through the state machine, each on a fresh harness. # ============================================================================= print("\n=== open network ===") d2 = Driver() d2.cmd("MAC " + STA.hex()) ev = d2.cmd(f"CONNECT {AP.hex()} 11 0 OpenNet - -") check(ev['result'].startswith('CONNECT-OK 1'), "open network: connect starts") caps_seen = [] d2.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('H', 0x0800) + ip).hex()) f = ev['TX'][0] check(not (f['hdr'][1] & 0x40) and not f['enc'], "open network: data frames are sent unprotected") print("\n=== the access point never answers ===") d3 = Driver() d3.cmd("MAC " + STA.hex()) d3.cmd(f"CONNECT {AP.hex()} 6 0 Nowhere - -") retries = 0 for _ in range(6): d3.cmd("TICK 500") ev = d3.cmd("SERVICE") retries += len(ev['TX']) if state_of(ev['result']) == 8: break check(retries >= 3, f"authentication is retransmitted ({retries} retries) before giving up") check(state_of(ev['result']) == 8, "the attempt eventually fails rather than hanging") check(any(x.startswith('TXQ-DOWN') for x in ev['TXQ']), "giving up tears the transmit queue back down") print("\n=== the access point rejects us ===") d4 = Driver() d4.cmd("MAC " + STA.hex()) d4.cmd(f"CONNECT {AP.hex()} 6 0 Rejects - -") ev = d4.cmd("RXMGMT " + mgmt(0xb0, STA, AP, AP, struct.pack('H', 0x0800) + ip).hex()) check(ev['result'] == 'TXETH-OK 0', "transmit is refused after being dropped") # ============================================================================= # PHY context command layout. # # Regression test for a lockup: firmware 89 advertises ULTRA_HB_CHANNELS, which # selects an 8-byte channel-info sub-structure (32-bit channel first). Sending # the older 4-byte form shifted every field after it, asserted the firmware, and # the driver then spun forever waiting for a reply that would never come. # ============================================================================= print("\n=== PHY context command layout ===") PHY_CONTEXT_CMD = 0x08 ULTRA_HB = 48 def phy_cmd_for(uhb): dd = Driver() dd.cmd("MAC " + STA.hex()) dd.cmd(f"CAPA {ULTRA_HB} {1 if uhb else 0}") ev = dd.cmd(f"CONNECT {AP.hex()} {CHANNEL} 0 OpenNet - -") dd.p.stdin.close() for cid, payload in ev['CMD']: if cid == PHY_CONTEXT_CMD: return payload return None p_uhb = phy_cmd_for(True) check(p_uhb is not None and len(p_uhb) == 32, f"ultra-high-band firmware gets a 32-byte PHY context ({len(p_uhb) if p_uhb else 0})") if p_uhb and len(p_uhb) == 32: chan = struct.unpack_from('= 3, f"link add, PHY binding and activation are three commands " f"(got {len(links)})") if len(links) >= 3 and phy_add is not None: bind_i, bind = links[1] act_i, act = links[2] check(phy_add < bind_i, "the PHY context exists before the link binds to it") check(struct.unpack_from('