Skip to content

sta: a device-free 802.11 station core - frames, BSS table, WPA2-PSK supplicant, CCMP - #454

Merged
josephnef merged 1 commit into
OpenIPC:masterfrom
snokvist:pr/sta-core
Sep 28, 2026
Merged

josephnef merged 1 commit into
OpenIPC:masterfrom
snokvist:pr/sta-core

Conversation

@snokvist

@snokvist snokvist commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

sta: a device-free 802.11 station core - frames, BSS table, WPA2-PSK supplicant, CCMP

Size at a glance

+12,937 / −8 lines across 28 files. Under a third is library code:

Part Lines Files
src/sta/ headers (the code under review) 3,812 7 headers
Headless selftests 6,975 5 test files
Test-side helpers 166 openssl_crypto_ops.h, ccmp_software.h
Checked-in known-answer vectors (generated) 627 ccmp_kernel_vectors.h, eapol_kernel_vectors.h, ccmp_vectors.h
Vector capture, extract and generate scripts 1,028 6 scripts
Docs, CMake, CI 329 docs/station-core.md, src/sta/CLAUDE.md, root CLAUDE.md, CMakeLists.txt, CI workflow

Suggested reading order

Each header only depends on the ones before it:

  1. Dot11.h: frames, the IE walker, parse_rsn, the duplicate cache; no dependencies.
  2. BssTable.h: scan results; uses Dot11.
  3. CryptoOps.h, then Ccmp.h: the crypto interface, then CCMP framing and
    the replay window.
  4. Eapol.h: EAPOL-Key format and the key derivations.
  5. Supplicant.h: the handshake decisions; uses Eapol.
  6. StationSm.h: the association state machine that drives it all.

src/sta/CLAUDE.md maps which header holds which rule and which test covers it.

What changed

New header-only module src/sta/, which is pure: no IRadio, no libusb, no
clock, no threads, no environment. Time is passed in as an argument. Frames
come in through on_rx() and go out through pop_tx(). Crypto is a
CryptoOps vtable that the caller fills, so libdevourer gains no dependency.

Header Role
Dot11.h Management and data frame builders and parsers, a bounds-checked IE walker, RSN element parsing (full suite sets), the duplicate cache, the 12-bit sequence counter, a minimal TIM
BssTable.h Scan results collapsed per BSSID; select() offers only a BSS this station can finish a handshake with
CryptoOps.h AES-128-CCM, HMAC-SHA1, PBKDF2-HMAC-SHA1, RFC 3394 unwrap, as an interface
Ccmp.h CCMP AAD, nonce, header and PN framing; CcmpReplay (a per-TID 64-wide sliding window, plus seed(rsc))
Eapol.h EAPOL-Key format (descriptor v2 only), PRF, PTK, constant-time MIC verify, GTK KDE, pmk_from_psk, secure_wipe
Supplicant.h The station half of the 4-way and group-key handshakes, with a counter for every refusal
StationSm.h Authenticate → associate → 4-way → connected (Open or WPA2-PSK), timeouts driven by now_ms, the decrypted-MSDU path for group rekeys

Tests: six ctest cells. dot11_frames and bss_table always run.
ccmp_framing, supplicant and station_sm need OpenSSL, since
tests/openssl_crypto_ops.h is the test CryptoOps. ccmp_vectors_generated
needs Python 3 and is skipped without python-cryptography.

Without OpenSSL the three crypto cells are not registered at all, and
configure prints a WARNING. The new option DEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON
turns that into a configure error, and CI sets it on every job that runs
ctest. In the last CI run every job I checked found OpenSSL: Ubuntu gcc/clang,
macOS gcc/clang, MSVC, mingw, sanitizers, and a sampled build-configs cell
(they all share the Ubuntu runner image).

  • Ubuntu and macOS now install OpenSSL explicitly: libssl-dev, and
    openssl@3 with OPENSSL_ROOT_DIR.
  • mingw installs mingw-w64-x86_64-openssl.
  • MSVC relies on the OpenSSL its runner image already ships. Building one
    through vcpkg would add minutes per run, so it is left as is, but the
    option makes a missing one fail loudly.

The standalone targets request cxx_std_20 for MSVC.

The AP-side multi-station table (per-station PTK, TX PN and replay state) is
deliberately left out. It will come with a later AP-side PR, together with
its consumer.

Why

An earlier WPA2/CCMP station client, #335 by another contributor, mixed the
protocol and crypto code into a large device PR. Its review found the
EAPOL-Key MIC never verified and KRACK-class handshake defects, and the PR was
later withdrawn. This PR takes the protocol and crypto half on its own, with
no device code, so it can be reviewed and tested without hardware, with a
named test behind every security rule. Backends and harnesses that use
it can follow as separate PRs.

What is measured

Everything here is a headless assertion. Nothing was measured on air in this PR.

  • Security properties, each pinned by a named cell:
    • No key is installed before its MIC verifies, and message 1 never moves
      the replay counter.
    • An EAPOL-Key counter must be strictly greater than the last
      authenticated one to install anything. An equal counter on the same
      message is the AP's retransmission: it is answered with the cached reply
      and installs nothing. Anything else at or below the counter is refused as
      a replay.
    • On the data plane an equal or zero PN is refused.
    • No key reinstallation (the CVE-2017-13077/13078/13080 class).
    • GTK RSC seeding of the group window.
    • The RSNE downgrade check (802.11-2016 12.7.6.4), over the full suite
      sets.
    • ccmp_decrypt refuses a NULL output, which OpenSSL's CCM would otherwise
      treat as AAD and skip the tag check.
    • ccmp_decrypt requires the Protected bit and Ext IV, and ccmp_encrypt
      refuses a PN past 48 bits rather than reusing a nonce.
    • Message 3 must set Secure and carry Key Length 16. Key data must parse to
      its end; its padding is 0xdd then zeros. Only a 16-byte (CCMP-128) GTK
      at key id 1-3 installs. The GTK KDE and message 3's RSN element are
      found by one shared key-data walker, so the two cannot disagree about
      where an element starts.
    • A same-counter message 1 with a different ANonce is not answered from
      the cache, so a forged message 1 that arrives first cannot poison the
      genuine one. A message 1 whose derivation fails in CryptoOps leaves the
      candidate of the handshake in flight untouched.
    • Once a PTK is installed, an EAPOL-Key in the clear is dropped, with one
      exception: the AP's retransmission of the installed handshake's message 3
      (same ANonce, Install and Secure set). hostapd installs its PTK only
      after it receives message 4, so when message 4 is lost the retransmitted
      message 3 arrives unencrypted, and refusing it would cost the link. A
      group message 1 or a message 3 with a new ANonce in the clear never
      drives a rekey.
    • join() on a live association deauthenticates from the old AP before
      authenticating to the new one, and refuses a BSS whose SSID is not the
      configured one (Failure::SsidMismatch).
    • The beacon-loss window is capped (beacon interval clamped at 1000 TU,
      so at most 10,240 ms), and a beacon whose SSID is longer than 32 octets
      is malformed.
    • A truncated authentication or association response counts as
      malformed. A QoS Null (or any no-data subtype) counts as ignored and
      still refreshes AP liveness. pop_tx(nullptr) is refused and the frame
      stays queued.
    • A failure, a peer deauth or leave() drops the queued frames, the
      association's keys and the AID. leave() sends a deauthentication only
      if the AP accepted an authentication.
    • The station reports Connected only once message 4 has actually been
      queued. A dropped message 4 leaves it in FourWay until a retransmitted
      message 3 is answered, or the handshake timeout ends it.
    • A BSS's channel is its DS element, else the channel the caller received
      it on, each accepted only if it is a real channel (1–14 or 32–253).
      With neither, the BSS is never offered for join, so a 5 GHz beacon
      without a DS element is never sent a 2.4 GHz association request.
    • Only an infrastructure BSS (ESS set, IBSS clear) is offered or joined,
      and WPA2 needs the Privacy bit as well as the RSN element.
    • Only an Association Response answers the Association Request.
    • AP liveness ("beacon loss") counts any frame from the AP: a beacon with
      its fixed body, a data frame from the BSSID addressed to this station,
      or a decrypted MSDU. A link carrying downlink traffic is not declared
      lost for want of beacons, and a header-only beacon does not count.
    • Only EAPOL-Key reaches the supplicant. EAP, EAPOL-Start and Logoff are
      handed back to the caller on the decrypted path, and ignored in the
      clear.
    • A reconfigure wipes the previous PMK before deriving the new one, so a
      refused passphrase cannot leave it resident.
    • The 64-hex PSK spelling.
    • The group rekey path through StationSm.
  • Code-reviewed, not pinned by a cell: the EAPOL MIC compare in
    eapol_mic_ok is constant-time (an OR-reduction over all 16 bytes, no early
    exit). A headless cell cannot observe timing, so the only check is reading
    the loop.
  • Independent known answers: CCMP frames that the Linux kernel encrypted
    at all eight TIDs, and a real hostapd/wpa_supplicant 4-way (with the PTK
    and GTK those tools logged). Both were captured off mac80211_hwsim and are
    checked in, so ctest needs no rig. The IEEE 802.11i Annex H.4.2 PSK
    vectors are also included.
  • The adversarial counterpart, stated honestly:
    • tests/ccmp_vectors.h (python-cryptography) is a same-author
      transcription
      of the framing. It pins only the cipher plumbing; a zero
      CCM nonce Flags octet passes it. Its new --check proves only
      that the file matches its generator, not that the generator is right.
    • The supplicant's and the state machine's fixture authenticators share an
      author with the code they test. That includes the new group-rekey cell;
      there is no captured hostapd group rekey.
    • The kernel and hostapd captures exist to close that gap. They are an
      interop reference, not the IEEE Annex J vector. If mac80211 and this code
      misread the same clause in the same way, no cell would notice.
    • The captures themselves are not in the tree, so those two headers cannot
      be regenerated from it; a new capture negotiates new keys.
    • A forged message 1 (spoofed BSSID, arbitrary ANonce) replaces the
      single in-flight candidate PTK. The genuine message 3 is then refused at
      the ANonce comparison until the AP restarts the handshake. This is the same exposure
      wpa_supplicant has with its TPTK. During the initial 4-way it costs an
      attempt. During a PTK rekey on an established association it can cost
      the link, if the AP gives up and deauthenticates. The installed PTK is
      never touched. Documented at Supplicant::on_msg1.
    • The RSNE comparison treats suites outside 00-0F-AC by presence only, so
      two different vendor suites compare equal. That is stricter than
      wpa_supplicant, which drops them, but it is not exact.

What it deliberately does not do

  • Drive a device. It has no scanning, tuning, TX or RX, and makes no radio
    calls. A caller feeds frames in.
  • Hardware crypto offload. CCMP is software through CryptoOps, and its CPU
    cost is not measured here.
  • PMF / 802.11w. A BSS that requires MFP is not selected, and deauth/disassoc
    frames are accepted unauthenticated.
  • TKIP, AES-CMAC (descriptor v3), SAE/WPA3, 802.1X/EAP.
  • AP-side per-station state (see above). This tree's AP harnesses carry their
    own inline builders; Dot11.h's golden-byte cells pin the bytes they air.
    msdu_to_eth/eth_to_msdu have no in-tree caller yet, and neither does
    DupDetector: StationSm runs no duplicate cache, so the data-plane
    caller that src/sta/CLAUDE.md and the DupDetector contract describe
    must keep one per transmitter itself.
  • Renew the SNonce on an in-association rekey. This is by design: the caller
    supplies it per association, and the library has no RNG. The PTK still
    changes because the ANonce does.
  • Handle a replay window wider than 64 PNs. That is enough for HT/VHT
    BlockAck. An HE/EHT peer negotiating 256/1024 would need it widened first.

Review rounds

Every finding was checked against the code before any change. Each code fix
comes with a named selftest cell, and each was mutation-checked: the fix was
reverted and its cell confirmed to fail. The per-finding detail is in the
review threads on this PR, not repeated here.

Round Findings Fixed Declined
Pre-upstream (two independent reviews) 13 13 3 related suggestions, see below
qodo pass 1 11 11 part of one, see below
qodo pass 2 3 3 0
qodo pass 3 5 4 1, see below
qodo pass 4 7 7 0
Pre-ready pass 12 11 1 unreachable: the TX queue cannot fill during send_auth/send_assoc, now a static_assert
qodo ready pass 6 4 2, see below

The pre-ready cleartext-EAPOL rule keeps one exception (the retransmitted installed message 3), because hostapd sends it in the clear after a lost message 4.

Declined, with reasons:

  • Requiring parse_rsn to consume the whole element (qodo pass 3).
    wpa_supplicant ignores trailing bytes after the last field it knows, and
    reads an optional field only when all of it is present. That leaves room
    for fields a later amendment appends. The parser already has that shape,
    and it rejects a PMKID count that overruns the element, as wpa_supplicant
    does. test_parse_rsn_sets pins both.
  • A fresh SNonce for a second message 1 (qodo pass 1). The rest of that
    finding was fixed: the msg1 cache now matches the ANonce. The library takes
    its SNonce from the caller per association and has no RNG, and
    wpa_supplicant also keeps one SNonce within a handshake.
  • qodo ready pass:
    • Requiring parse_rsn to consume the whole element (i == len): the
      same trailing-field rule as the qodo pass 3 decline.
    • parse_beacon keeping a BSS whose last IE is truncated: the elements
      before the truncation are used, as Linux's ieee802_11_parse_elems
      does. An AP whose RSN element is the truncated one still sets Privacy,
      so it is offered neither as open nor as WPA2.
  • Pre-upstream:
    • parse_rsn still accepts a truncated RSNE, because 802.11 allows
      trailing fields to be omitted.
    • No null guards were added to internal crypto helpers whose contracts are
      documented.
    • "No in-tree consumer" was already stated.

Maintainer review

Answers to the review of this draft, point by point.

  1. Convention: current-state comments, no issue references, no dated
    history.
    All src/sta/ headers, the five station selftests, the vector
    files and the generator/extractor/capture scripts were swept.
    • Every PR number, date and "used to / earlier version / until / first
      draft / review found" passage is rewritten as the rule plus its reason,
      in the present tense.
    • Where the history was the only justification, the cell that pins the
      rule is named instead.
    • The generated kernel-vector headers and the extractor templates that
      produce them were changed identically, and tests/ccmp_vectors.h still
      reproduces byte for byte.
    • docs/station-core.md keeps its provenance section; it has no
      review-round narration.
  2. Liveness is beacon-only. Any frame from the AP now refreshes the
    AP-liveness clock: a beacon with its fixed body, a from-DS data frame from
    the BSSID addressed to this station (protected or not, with its full
    header present), or any MSDU handed to on_decrypted_msdu. A header-only
    beacon still does not count.
    • The public names Failure::BeaconLost, beacon_loss_ms() and
      beacons_rx are kept, to avoid API churn. They are documented as "no
      frame from the AP within the window". The private member is renamed
      last_heard_ms_.
    • Cell test_data_keeps_the_link_alive: three loss windows of downlink
      data (protected frames through on_rx, then decrypted MSDUs) with no
      beacons stay Connected. With nothing at all, the link reaches BeaconLost.
      Each of the two refresh points was mutation-checked on its own.
  3. Message 3 Key Length. It must be 16 (CCMP-128), or the message is
    refused as Malformed before its MIC is checked. Cell
    test_msg3_key_length_must_be_16.
  4. GTK at KeyID 0. Refused in message 3 and in the group key handshake.
    Cell test_gtk_key_id_zero_is_refused.
  5. Key-data padding. find_gtk_kde no longer steps over a lone 0x00, so
    it walks key data exactly as find_rsn_element does. The 0xdd pad is read
    as an empty element, pairs of zeros after it as empty ID-0 elements, and a
    final odd byte ends the walk. The captured hostapd four-way still passes.
    Cell test_gtk_kde_padding: 1..7 bytes of padding after a GTK KDE are
    accepted, and a lone 0x00 ahead of it is not skipped.
  6. The forged-message-1 comment. Corrected. The genuine message 3 is
    refused at the ANonce comparison (Malformed) before any MIC is computed;
    it does not fail its MIC.
  7. Deauth from leave() after a timeout. A deauthentication goes out
    only if the AP accepted an authentication. There is none after an
    AuthTimeout, and none after the AP's own deauth.
    • After an AssocTimeout one is sent, deliberately: the AP holds our
      authentication, and a deauth is what clears it.
    • Cell test_leave_after_a_timeout covers AuthTimeout (nothing sent),
      AssocTimeout (one deauth) and a peer deauth (nothing sent).

Verification

cmake -S . -B build -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON
cmake --build build -j4
cd build && ctest -j4    # 76/76 pass (2 pre-existing mt7612u cells skip), 6 new
cmake --build build --target selftests   # the mingw path collects the C++ cells

Also checked:

Configuration Result
RTL8733B-only subset (every other per-chip option OFF, --target selftests) 71/71 pass
OpenSSL forced absent (-DCMAKE_DISABLE_FIND_PACKAGE_OpenSSL=ON) configure WARNs and 68/68 pass, with the three crypto cells unregistered and nothing "Not Run"
The same with -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON configure fails with the FATAL_ERROR
The five station cells under DEVOURER_SANITIZE=address+undefined 5/5 pass
python3 tests/ccmp_gen_vectors.py --check tests/ccmp_vectors.h reproduced byte for byte

The vector capture scripts (tests/ccmp_capture_vectors.sh,
tests/eapol_capture_vectors.sh) need root and mac80211_hwsim. They are for
regenerating the checked-in headers only and were not run for this PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3

@snokvist

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Station overview repeats cleartext rules ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
docs/station-core.md repeats the cleartext EAPOL-Key acceptance and refusal rules already
documented beside StationSm::cleartext_eapol_allowed. If that gate changes, the overview can
retain an obsolete account of which handshake messages reach the supplicant.
Code

docs/station-core.md[R47-50]

+- **Which EAPOL-Key may still arrive in the clear once a PTK is installed**:
+  only the AP's retransmitted message 3 of the installed handshake (its
+  answer to a lost message 4); a group message 1 or a fresh four-way in the
+  clear is dropped. `StationSm::cleartext_eapol_allowed`. Cell: `station_sm`.
Evidence
Rule 1 requires guidance to point to header documentation rather than repeat it. The overview lists
the same accepted retransmission and refused cleartext messages that the header comment specifies at
the gate.

CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced: CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced: CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced: CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced: CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced: CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced: CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced: CLAUDE.md: Keep Repository Guidance Narrow and Evidence-Balanced
docs/station-core.md[47-50]
src/sta/StationSm.h[590-608]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The station overview duplicates the cleartext EAPOL-Key rules documented at the gate, creating a second account that can become stale.
## Fix Focus Areas
- docs/station-core.md[47-50]
## Recommended Fix
Replace the acceptance and refusal details with a short topic label and a reference to `StationSm::cleartext_eapol_allowed` and its test cell.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Station guidance repeats channel rules ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
src/sta/CLAUDE.md restates the channel fallback, valid ranges, and infrastructure eligibility
already documented beside BssTable::observe() and channel_valid(). A later change to channel
selection would require matching edits to this guidance or leave maintainers with conflicting
descriptions of which networks can be joined.
Code

src/sta/CLAUDE.md[R50-53]

+- A BSS's channel is its DS element, else the channel the caller received
+  it on (`observe`'s `rx_channel`), each only if `channel_valid()` (1..14,
+  32..253); with neither it is never offered and `join()` refuses it. Only
+  an infrastructure BSS (ESS set, IBSS clear) is offered or joined —
Evidence
Rule 1 requires references rather than copies of header contracts. The new guidance states the
channel rules that the cited header comments already explain.

CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims
src/sta/CLAUDE.md[50-55]
src/sta/BssTable.h[92-108]
src/sta/Dot11.h[538-551]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The subtree guidance copies channel-selection contracts already documented in the headers.
## Fix Focus Areas
- src/sta/CLAUDE.md[50-55]
- src/sta/BssTable.h[92-108]
- src/sta/Dot11.h[538-551]
## Recommended Fix
Replace the detailed channel and infrastructure conditions with pointers to the documented declarations and their tests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Station overview repeats cipher refusals ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
docs/station-core.md enumerates specific ccmp_decrypt() refusals, including null output and a
missing extended initialization vector, instead of pointing readers to the function’s existing
contract. If those receive-side checks change, the separate overview list can give maintainers an
obsolete account of what the cipher path accepts.
Code

docs/station-core.md[R36-39]

+- **Hostile-input refusals**: a NULL output in `ccmp_decrypt`, a CCMP header
+  without Ext IV, a PN past 48 bits, a constant-time EAPOL MIC compare,
+  length fields that overflow or disagree, key data that does not parse to
+  its end, a GTK that is not 16 bytes, a message 3 without Secure, a deauth
Evidence
Rule 1 calls for referring to existing header documentation instead of duplicating it. The new
overview lists cipher refusal conditions already documented at the cited declaration.

CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims: CLAUDE.md: Keep Repository Guidance Narrow and Avoid Unsupported Measurement Claims
docs/station-core.md[36-41]
src/sta/Ccmp.h[281-324]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The station overview copies cipher refusal conditions documented at the function declaration.
## Fix Focus Areas
- docs/station-core.md[36-41]
- src/sta/Ccmp.h[281-324]
## Recommended Fix
Replace the detailed refusal list with references to the relevant header contracts and named tests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (1)
4. A dropped handshake reply marks the link ready ✓ Resolved 🐞 Bug ≡ Correctness
Description
StationSm::eapol_reply() changes state_ to Connected as soon as the supplicant accepts message
3, before on_eapol() wraps and inserts message 4 into the bounded transmit queue. When the queue
is full, queue() drops message 4 and returns without reverting that transition, while the AP has
not received the handshake completion.
Code

src/sta/StationSm.h[R596-599]

+    if (sup_.state() == Supplicant::State::Done && sup_.ptk_valid() &&
+        state_ == State::FourWay) {
+      state_ = State::Connected;
+      last_beacon_ms_ = now_ms;
Evidence
The state transition occurs before queue insertion, and queue insertion has an explicit drop path.
The supplicant has already installed keys and entered Done by the time the outer state machine
reaches this code, so a dropped message 4 does not undo the promotion.

src/sta/StationSm.h[490-494]
src/sta/StationSm.h[596-604]
src/sta/StationSm.h[612-619]
src/sta/Supplicant.h[492-506]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`StationSm` promotes a WPA2 association to `Connected` before its message-4 reply has been accepted by the bounded TX queue. A full queue drops that reply, leaving the object keyed and connected even though the authenticator cannot complete the four-way handshake.
## Fix Focus Areas
- src/sta/StationSm.h[596-619]
## Recommended Fix
Make the `FourWay` to `Connected` transition conditional on successfully retaining the message-4 reply for transmission. Move the promotion after a successful `queue()` in the cleartext EAPOL path, or otherwise fail and wipe the association when queue insertion fails; preserve the existing connected-state handling for group-key replies.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn on the rule miner and Qodo learns your standards from review history

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/station-core.md Outdated
Comment thread src/sta/Ccmp.h
Comment thread src/sta/Supplicant.h
Comment thread src/sta/StationSm.h Outdated
Comment thread src/sta/Ccmp.h
Comment thread src/sta/Eapol.h Outdated
Comment thread src/sta/Eapol.h Outdated
Comment thread src/sta/Supplicant.h
Comment thread src/sta/StationSm.h
Comment thread src/sta/StationSm.h
@snokvist

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread src/sta/Dot11.h Outdated
Comment thread src/sta/StationSm.h Outdated
Comment thread src/sta/StationSm.h
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2c31af5

@snokvist

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread src/sta/BssTable.h Outdated
Comment thread src/sta/StationSm.h
Comment thread src/sta/StationSm.h Outdated
Comment thread src/sta/Dot11.h
Comment thread src/sta/BssTable.h
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 67068cd

@snokvist

Copy link
Copy Markdown
Collaborator Author

/review

Comment thread src/sta/CLAUDE.md Outdated
Comment thread docs/station-core.md Outdated
Comment thread src/sta/Ccmp.h
Comment thread src/sta/StationSm.h
Comment thread src/sta/StationSm.h
Comment thread src/sta/BssTable.h Outdated
Comment thread src/sta/Eapol.h
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 33baa1a

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 534b644 (draft). Substance: approve; convention: one pass needed before leaving draft.

What I ran

  • -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON: builds warning-free, ctest 76/76; tests/ccmp_gen_vectors.py --check reproduces byte for byte.
  • ASan+UBSan build of the five sta selftests: rc 0, zero reports.
  • A random-input harness (ASan+UBSan, -fno-sanitize-recover) over find_ie, parse_rsn, parse_beacon/auth/assoc_resp/reason, parse_eapol_key, eapol_mic_ok, find_gtk_kde, ccmp_aad/nonce/decrypt/encrypt, CcmpReplay, BssTable::observe+select, Supplicant::on_eapol with a permissive stub CryptoOps so the install paths run, and StationSm::on_rx/on_decrypted_msdu/tick/join/leave: 150 s, 654k iterations, no library finding.

Protocol review against 802.11-2016: MIC before any install (constant-time), replay counter moves only after MIC on msg3/group1, strictly-greater-to-install with equal-same-kind answered from cache, msg1 cache keyed on (counter, ANonce), PTK reinstalled only on differing bytes and GTK install a no-op for identical (id, bytes) without re-reading RSC (the CVE-2017-13077/78/80 classes), msg3 Secure+Install+Ack+MIC + encrypted key data + exactly one 16-byte GTK KDE, RSNE downgrade compare over the full suite sets, PTK 16/16/16, PRF-384 label/counter, PBKDF2 4096. CCMP AAD masking (subtype for data, Retry/PwrMgt/MoreData, Order for QoS, Protected set, fragment kept, TID-only QC, A4 included), nonce flags TID | mgmt<<4, 48-bit BE PN, Ext IV + Protected required, NULL-out refusal with the empty-body scratch byte, PN > 2^48-1 refused, replay window PN 0 / equal refused and shift >= 64 guarded. All consistent with the standard and mac80211. MSVC: no VLAs, no attributes, no POSIX headers. CI installs OpenSSL on every ctest job and the require flag is wired on all five.

Should-fix before leaving draft

  1. Convention: PR #335 appears 8 times in src/sta/ headers (Dot11.h, Ccmp.h, CryptoOps.h, Eapol.h x2, Supplicant.h x2) and there are dated narratives ("missing until 2026-09-21", "hardcoded ... until 2026-09-20", Ccmp.h:103/146/292, Eapol.h:377, StationSm.h:414) plus a dozen "an earlier version / used to" passages (BssTable.h:99/196, Ccmp.h:46/79/397, ...). Code comments are current-state only with no issue refs; state the rule and why, and let the named test cells carry provenance. Docs may keep the #335 history.
  2. StationSm.h:484 liveness is beacon-only: only beacons/probe responses (lines 249, 306) refresh last_beacon_ms_. A Connected station receiving downlink data but missing 10 beacons (the promiscuous-RX-under-video-load case where beacons are exactly what gets dropped) is declared BeaconLost while traffic flows. Any frame from the BSSID addressed to us, on_decrypted_msdu included, should count as liveness, with a cell: data flowing, no beacons, no BeaconLost.

Nits
3. Supplicant.h:393: msg3 Key Length is never compared to 16 (CCMP TK).
4. Supplicant.h:602: a GTK KDE with KeyID 0 installs; a data plane indexing by header key-id cannot tell it from the PTK slot. hostapd never sends it; refuse it.
5. Eapol.h:420: find_gtk_kde steps one byte over a lone 0x00 as padding, but 802.11 padding is 0xdd then zeros and EID 0 is SSID; find_rsn_element in the same file does not skip 0x00. Align the two (not attacker-reachable: input is KEK-unwrapped and MIC-verified).
6. Supplicant.h:350 comment says the genuine msg3 "fails its MIC against that candidate"; it fails the ANonce compare at line 407 (Malformed).
7. StationSm.h:259: leave() from Failed queues a deauth even after AuthTimeout/AssocTimeout where no association exists.

Nothing on air was measured, as the body says; that is right for this PR's scope.

@snokvist

Copy link
Copy Markdown
Collaborator Author

Thanks for the review and the fuzz run. All seven points are addressed in e8ddc34; the "Maintainer review" section of the description has the detail for each.

  1. The convention pass covers every src/sta header, the five station selftests, the vector headers and the generator, extractor and capture scripts. A grep for PR references, dates, "used to", "earlier version" and review narration is now empty. The docs keep the Add a WPA2/CCMP station-mode client (APFPV) #335 provenance.
  2. Any frame from the AP now counts as liveness: a beacon with its fixed body, a from-DS data frame from the BSSID addressed to us, or any MSDU through on_decrypted_msdu. Cell test_data_keeps_the_link_alive keeps the link up through three loss windows of downlink data with no beacons, and the control with nothing received still reaches BeaconLost.
    3.-7. Message 3 Key Length must be 16. A GTK at KeyID 0 is refused in both paths. find_gtk_kde walks key data exactly as find_rsn_element does, with a padding cell for 1..7 bytes. The on_msg1 comment is corrected. leave() deauthenticates only when the AP accepted an authentication, so nothing goes out after an AuthTimeout or a peer deauth. After an AssocTimeout one deauth does go out, because the AP holds our authentication; say if you'd prefer association-only.

REQUIRE ON: ctest 76/76, the RTL8733B-only selftests 71/71, the station cells under ASan+UBSan, and ccmp_gen_vectors.py --check reproducing byte for byte. Every code fix was mutation-checked. The PR stays a draft until the owner marks it ready.

@snokvist
snokvist marked this pull request as ready for review September 27, 2026 17:26
@snokvist
snokvist requested a review from josephnef September 27, 2026 17:26
@snokvist

Copy link
Copy Markdown
Collaborator Author

@josephnef marking this ready. Your draft review is addressed in e8ddc34, and on top of that 442c9f5 is a pre-ready hardening pass (the "Pre-ready pass" row in the Review rounds table).

It adds three behaviour rules:

  • Once the PTK is installed, EAPOL-Key frames in the clear are dropped. The one exception is the AP's retransmission of the installed message 3, which hostapd sends in the clear after a lost message 4.
  • join() on a live association deauthenticates from the old BSS first, and refuses a BSS whose SSID doesn't match the configured one.
  • The beacon-loss window is capped at 1000 TU.

The rest is consistency: counters for malformed auth/assoc responses and ignored QoS Nulls, one shared key-data walker, and the on_msg1 derive-failure path. The history sweep also covers CMakeLists.txt now. REQUIRE ON: ctest 76/76, CI 23/23.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add a device-free 802.11 station core with WPA2-PSK and CCMP

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add headless 802.11 frame handling, BSS selection, and open or WPA2-PSK association.
• Separate handshake and CCMP security decisions from radio I/O through caller-provided crypto and
 time.
• Add interoperability vectors, adversarial selftests, and CI checks for crypto-test coverage.
Diagram

graph TD
  Caller["Radio caller"] --> Frames["Dot11 frames"] --> Bss["BSS table"] --> Station["Station state"] --> Supplicant["WPA2 supplicant"] --> Eapol["EAPOL keys"] --> Crypto["Crypto ops"]
  Caller --> Ccmp["CCMP framing"]
  Crypto --> Ccmp
  Station --> Frames
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Delegate to an established supplicant
  • ➕ Reduces locally maintained security-critical handshake logic.
  • ➕ Offers broader interoperability coverage.
  • ➖ Adds a runtime dependency and integration layer.
  • ➖ Complicates the device-free core.
2. Integrate directly with a radio backend
  • ➕ Could validate live associations in the same change.
  • ➖ Entangles protocol decisions with hardware behavior.
  • ➖ Makes headless security tests harder.

Recommendation: Keep the device-free core and pluggable CryptoOps: this isolates security decisions for hardware-independent testing without a mandatory crypto dependency. Review the caller-facing nonce, key-generation, and replay-window contracts particularly closely; live-radio validation remains for a later integration.

Files changed (28) +12808 / -8

Enhancement (7) +3780 / -0
BssTable.hSelect joinable BSSes from scan observations +312/-0

Select joinable BSSes from scan observations

• Adds bounded per-BSSID observation, expiration, eviction, and open or WPA2 selection. Filters candidates by channel, infrastructure status, and security support.

src/sta/BssTable.h

Ccmp.hAdd CCMP framing and replay windows +476/-0

Add CCMP framing and replay windows

• Builds CCMP authenticated data, nonces, headers, and protected frames through CryptoOps. Adds per-TID sliding replay windows with group-key RSC seeding.

src/sta/Ccmp.h

CryptoOps.hDefine a pluggable cryptography interface +72/-0

Define a pluggable cryptography interface

• Declares AES-CCM, HMAC-SHA1, PBKDF2, and AES key-unwrap operations without imposing a library dependency.

src/sta/CryptoOps.h

Dot11.hAdd role-neutral 802.11 frame utilities +899/-0

Add role-neutral 802.11 frame utilities

• Introduces frame builders and parsers, bounded element walking, RSN parsing, sequence assignment, duplicate detection, and MSDU conversion helpers.

src/sta/Dot11.h

Eapol.hAdd WPA2 EAPOL-Key formats and derivations +481/-0

Add WPA2 EAPOL-Key formats and derivations

• Parses and builds descriptor-version-2 messages and provides PTK derivation, MIC verification, GTK key-data parsing, PSK derivation, and secure wiping.

src/sta/Eapol.h

StationSm.hDrive association and link supervision +821/-0

Drive association and link supervision

• Adds open and WPA2 station flows with caller-supplied time, bounded transmit queue, retries, and failure reporting. Connects EAPOL-Key messages to the supplicant and supports protected group rekeys through a decrypted-MSDU entry point.

src/sta/StationSm.h

Supplicant.hImplement station-side WPA2 key handshakes +719/-0

Implement station-side WPA2 key handshakes

• Adds four-way and group-key decisions, MIC and replay gates, RSN downgrade checks, retransmission replies, and refusal counters. Key generations advance only when installed key material changes.

src/sta/Supplicant.h

Tests (16) +8697 / -0
bss_table_selftest.cppTest BSS observation and selection +667/-0

Test BSS observation and selection

• Exercises deduplication, channel fallback, security and infrastructure filters, eviction, recency, and malformed observations without a radio or OpenSSL.

tests/bss_table_selftest.cpp

ccmp_capture_vectors.shCapture kernel-produced CCMP frames +170/-0

Capture kernel-produced CCMP frames

• Sets up temporary virtual radios to capture protected traffic for checked-in vectors. Checks for an existing virtual-radio rig before proceeding.

tests/ccmp_capture_vectors.sh

ccmp_extract_vectors.pyExtract CCMP frames from captures +198/-0

Extract CCMP frames from captures

• Selects protected QoS frames by direction and TID from radiotap captures and emits C vectors without recomputing their cryptography.

tests/ccmp_extract_vectors.py

ccmp_gen_vectors.pyGenerate reproducible AES-CCM vectors +278/-0

Generate reproducible AES-CCM vectors

• Uses python-cryptography to generate fixed CCMP cases and provides a check mode comparing output with the committed header.

tests/ccmp_gen_vectors.py

ccmp_kernel_vectors.hStore kernel CCMP interoperability vectors +266/-0

Store kernel CCMP interoperability vectors

• Checks in protected MPDUs and key material captured from mac80211_hwsim, covering both directions and all eight QoS TIDs.

tests/ccmp_kernel_vectors.h

ccmp_selftest.cppVerify CCMP framing and replay behavior +910/-0

Verify CCMP framing and replay behavior

• Checks generated and kernel-produced vectors alongside header and nonce rules, invalid inputs, MIC failures, PN bounds, and replay windows.

tests/ccmp_selftest.cpp

ccmp_software.hProvide an OpenSSL CCM test primitive +62/-0

Provide an OpenSSL CCM test primitive

• Wraps OpenSSL AES-128-CCM for selftests, including safe handling of zero-length input and output pointers.

tests/ccmp_software.h

ccmp_tid_send.pyGenerate traffic for every QoS TID +23/-0

Generate traffic for every QoS TID

• Sends UDP datagrams with explicit socket priorities so captures contain all eight 802.11 user priorities.

tests/ccmp_tid_send.py

ccmp_vectors.hStore generated CCMP cipher vectors +232/-0

Store generated CCMP cipher vectors

• Checks in deterministic frame and ciphertext examples from the Python generator for byte-exact cipher-plumbing tests.

tests/ccmp_vectors.h

dot11_selftest.cppTest 802.11 frame formats and parsers +1002/-0

Test 802.11 frame formats and parsers

• Adds headless assertions for management-frame bytes, bounded parsing, RSN handling, sequence numbers, duplicate detection, and data-frame helpers.

tests/dot11_selftest.cpp

eapol_capture_vectors.shCapture a WPA2 four-way handshake +157/-0

Capture a WPA2 four-way handshake

• Runs hostapd and wpa_supplicant on virtual radios and records their EAPOL exchange and logged keys for extraction.

tests/eapol_capture_vectors.sh

eapol_extract_vectors.pyExtract EAPOL interoperability vectors +196/-0

Extract EAPOL interoperability vectors

• Cuts four EAPOL-Key messages from a radiotap capture and emits a header containing the exchange and externally logged PTK and GTK.

tests/eapol_extract_vectors.py

eapol_kernel_vectors.hStore captured WPA2 handshake answers +129/-0

Store captured WPA2 handshake answers

• Checks in hostapd and wpa_supplicant EAPOL-Key frames and logged key material to test derivation, MICs, and GTK parsing against independent implementations.

tests/eapol_kernel_vectors.h

openssl_crypto_ops.hImplement CryptoOps for selftests +104/-0

Implement CryptoOps for selftests

• Provides OpenSSL-backed implementations of the required cryptographic operations and a test-only AES key-wrap helper.

tests/openssl_crypto_ops.h

station_sm_selftest.cppExercise station lifecycle without hardware +2260/-0

Exercise station lifecycle without hardware

• Uses a fixture AP and controlled timestamps to test joins, retries, failures, liveness, transmit queues, cleartext EAPOL policy, and protected group rekeys.

tests/station_sm_selftest.cpp

supplicant_selftest.cppTest WPA2 handshake security decisions +2043/-0

Test WPA2 handshake security decisions

• Exercises MIC and replay refusals, retransmissions, key-installation rules, PSK answers, and a captured hostapd/wpa_supplicant four-way exchange.

tests/supplicant_selftest.cpp

Documentation (3) +179 / -1
CLAUDE.mdLink station subtree guidance +2/-1

Link station subtree guidance

• Adds src/sta/ to the root documentation map.

CLAUDE.md

station-core.mdDocument station scope and verification +76/-0

Document station scope and verification

• Explains the device-free API, test coverage, vector provenance, and unsupported features.

docs/station-core.md

CLAUDE.mdMap station contracts to headers and tests +101/-0

Map station contracts to headers and tests

• Records header dependencies, security-rule locations, test cells, vector provenance, and caller-owned data-plane responsibilities.

src/sta/CLAUDE.md

Other (2) +152 / -7
cmake-multi-platform.ymlRequire station crypto coverage in CI +29/-7

Require station crypto coverage in CI

• Installs OpenSSL explicitly on Linux, macOS, and mingw. Makes applicable CI configurations fail if the crypto selftests cannot be registered.

.github/workflows/cmake-multi-platform.yml

CMakeLists.txtRegister standalone station selftests +123/-0

Register standalone station selftests

• Adds five C++ selftests and a generated-vector check. Gates three crypto tests on OpenSSL and offers a configuration error when their coverage is required but unavailable.

CMakeLists.txt

Comment thread docs/station-core.md Outdated
Comment thread src/sta/Dot11.h
Comment thread src/sta/Dot11.h
Comment thread tests/ccmp_capture_vectors.sh Outdated
Comment thread src/sta/Supplicant.h Outdated
Comment thread src/sta/Ccmp.h
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 442c9f5

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at eb720f2 (the squash of e8ddc34 + 442c9f5 + the qodo ready-pass fixes). All seven points from my draft review are addressed, and I read the pre-ready hardening pass and the ready-pass fixes in full.

What I checked at this head:

  • Liveness: on_rx filters on addr2 == BSSID, so a from-DS data frame the AP relays for any SA passes the filter and refreshes last_heard_ms_; decrypted MSDUs refresh it too. test_data_keeps_the_link_alive and test_qos_null_is_ignored_and_alive pin both.
  • Cleartext EAPOL-Key after keying: group messages never from the clear; before the PTK only pairwise; after it only a retransmission of the installed message 3 (is_installed_msg3 matches ANonce + Install + Secure). That is the right exception, since hostapd installs its PTK only on message 4.
  • join() on a live association queues the deauth to the old BSSID before bssid_ is overwritten, wipes the supplicant, and refuses an SSID that is not the configured one. The 2*kMaxTries+1 <= kMaxTxQueue static_assert makes the "management requests are never dropped" claim structural.
  • leave() after a timeout: no deauth after AuthTimeout or a peer deauth, one after AssocTimeout. The AssocTimeout choice is right as is: the AP holds our authentication and a deauth is what clears it.
  • Message 3 Key Length must be 16, GTK key id 0 refused on both routes, one walk_key_data for the GTK KDE and the RSN element with the 0xDD padding walked as elements (1..7 bytes pinned, lone 0x00 not skipped).
  • on_msg1 commits the candidate only on success; eapol_mic_ok is tri-state and a provider failure counts as crypto_errors, not mic_failures.
  • Ccmp length arithmetic refuses a wrapping sum (encrypt) and subtracts before indexing (decrypt); the SIZE_MAX cells cover both edges.
  • Overlong SSID refuses the beacon; beacon interval capped at 1000 TU; pop_tx(nullptr) refused.
  • Convention sweep: no PR references, dates or "used to" narration left in src/sta, the five selftests, the vector headers or the scripts; the capture scripts now kill by recorded PID only.

Verified locally: -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON, ctest 76/76 (the two mt76-submodule cells skip); tests/ccmp_gen_vectors.py --check reproduces tests/ccmp_vectors.h byte for byte; the five station cells pass under DEVOURER_SANITIZE=address+undefined. Headless only, as the PR states; nothing here touches a radio.

The two declines (whole-element parse_rsn, keeping a beacon whose trailing IE is truncated) match wpa_supplicant and mac80211 respectively and are pinned by cells, so they stand.

Approving.

josephnef
josephnef previously approved these changes Sep 28, 2026

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at eb720f2 (the squash of e8ddc34 + 442c9f5 + the qodo ready-pass fixes). All seven points from my draft review are addressed, and I read the pre-ready hardening pass and the ready-pass fixes in full.

What I checked at this head:

  • Liveness: on_rx filters on addr2 == BSSID, so a from-DS data frame the AP relays for any SA passes the filter and refreshes last_heard_ms_; decrypted MSDUs refresh it too. test_data_keeps_the_link_alive and test_qos_null_is_ignored_and_alive pin both.
  • Cleartext EAPOL-Key after keying: group messages never from the clear; before the PTK only pairwise; after it only a retransmission of the installed message 3 (is_installed_msg3 matches ANonce + Install + Secure). That is the right exception, since hostapd installs its PTK only on message 4.
  • join() on a live association queues the deauth to the old BSSID before bssid_ is overwritten, wipes the supplicant, and refuses an SSID that is not the configured one. The 2*kMaxTries+1 <= kMaxTxQueue static_assert makes the "management requests are never dropped" claim structural.
  • leave() after a timeout: no deauth after AuthTimeout or a peer deauth, one after AssocTimeout. The AssocTimeout choice is right as is: the AP holds our authentication and a deauth is what clears it.
  • Message 3 Key Length must be 16, GTK key id 0 refused on both routes, one walk_key_data for the GTK KDE and the RSN element with the 0xDD padding walked as elements (1..7 bytes pinned, lone 0x00 not skipped).
  • on_msg1 commits the candidate only on success; eapol_mic_ok is tri-state and a provider failure counts as crypto_errors, not mic_failures.
  • Ccmp length arithmetic refuses a wrapping sum (encrypt) and subtracts before indexing (decrypt); the SIZE_MAX cells cover both edges.
  • Overlong SSID refuses the beacon; beacon interval capped at 1000 TU; pop_tx(nullptr) refused.
  • Convention sweep: no PR references, dates or "used to" narration left in src/sta, the five selftests, the vector headers or the scripts; the capture scripts now kill by recorded PID only.

Verified locally: -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON, ctest 76/76 (the two mt76-submodule cells skip); tests/ccmp_gen_vectors.py --check reproduces tests/ccmp_vectors.h byte for byte; the five station cells pass under DEVOURER_SANITIZE=address+undefined. Headless only, as the PR states; nothing here touches a radio.

The two declines (whole-element parse_rsn, keeping a beacon whose trailing IE is truncated) match wpa_supplicant and mac80211 respectively and are pinned by cells, so they stand.

Approving.

@josephnef
josephnef dismissed their stale review September 28, 2026 09:08

Duplicate of the approval posted 30 s earlier (pagination hid it from my check).

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Sep 28, 2026
…supplicant, CCMP

Header-only and pure under src/sta/: no IRadio, no libusb, no clock, no
threads. Time is an argument, frames go in through on_rx() and out through
pop_tx(), and crypto is a CryptoOps vtable the caller fills, so libdevourer
gains no dependency and every line is testable under plain ctest.

- Dot11.h: management/data frame builders and parsers, a bounds-checked IE
  walker, RSN element parsing (full suite sets), the duplicate cache, the
  12-bit sequence counter, a minimal TIM.
- BssTable.h: scan results and select(); offers only an infrastructure BSS
  (ESS set, IBSS clear) on a valid channel (1..14 or 32..253: the DS element,
  else the caller's RX channel), and for WPA2 only with the Privacy bit set;
  refuses an MFP-required BSS; a wrap-safe recency tie-break.
- CryptoOps.h: AES-128-CCM, HMAC-SHA1, PBKDF2-HMAC-SHA1, RFC 3394 unwrap.
- Ccmp.h: CCMP AAD/nonce/header/PN framing, and CcmpReplay - a per-TID
  64-wide sliding replay window with seed(rsc) for group keys.
- Eapol.h: EAPOL-Key format (descriptor v2 only), PRF, PTK derivation,
  constant-time MIC verify, GTK KDE, one shared key-data walker,
  pmk_from_psk (8..63 passphrase or exactly 64 hex), secure_wipe.
- Supplicant.h: the station half of the 4-way and group-key handshakes.
- StationSm.h: authenticate -> associate -> 4-way -> connected, Open or
  WPA2-PSK, with injectable time and a decrypted-MSDU path for group rekeys.

Security and robustness properties pinned by headless cells:
- keys: nothing is installed before its MIC verifies; message 1 never moves
  the replay counter; only a strictly greater EAPOL-Key counter installs
  anything, and an equal one is answered with the cached reply; no key
  reinstallation (CVE-2017-13077/13078/13080 class); GTK RSC seeding of the
  group replay window.
- messages: the msg1 cache matches counter AND ANonce, so a forged msg1
  that arrives first cannot poison the genuine one; a msg1 whose
  derivation fails in CryptoOps leaves the in-flight candidate untouched;
  message 3 must set Secure and carry Key Length 16; only a 16-byte
  (CCMP-128) GTK at key id 1-3 installs, from key data that parses to its
  end (0xdd+zeros padding); the GTK KDE and message 3's RSNE are found by
  the same walker; the RSNE downgrade check of 802.11-2016 12.7.6.4 over
  the full suite sets; only EAPOL-Key reaches the supplicant.
- cleartext EAPOL once a PTK is installed: dropped, except the AP's
  retransmission of the installed handshake's message 3 (same ANonce,
  Install and Secure), which hostapd sends unencrypted after a lost
  message 4; a group message 1 or a new-ANonce message 3 in the clear
  never drives a rekey.
- CCMP: ccmp_decrypt refuses a NULL output (which OpenSSL CCM would treat
  as "AAD, no tag check"), a frame without the Protected bit and a header
  without Ext IV; ccmp_encrypt refuses a PN past 48 bits; lengths whose
  sum would wrap size_t are refused before any buffer is touched
  (ccmp_encrypted_len returns 0 for them).
- EAPOL format: length fields that overflow or disagree; build_eapol_key
  refuses inconsistent arguments; the MIC check is tri-state, so a failed
  HMAC in our own CryptoOps is a CryptoError, never a MicFailed; the
  64-hex PSK spelling.
- state machine: a failure, peer deauth or leave() drops the queue, the
  association's keys and the AID, and leave() deauthenticates only if the
  AP accepted an authentication; join() on a live association
  deauthenticates from the old AP first and refuses a BSS whose SSID is not
  the configured one; Connected only once message 4 has actually been
  queued; a dropped reply does not move the handshake deadline; AP
  liveness counts any frame from the AP (a beacon with its fixed body, data
  addressed to us, a decrypted MSDU, a QoS Null, which is otherwise
  ignored); the beacon-loss window is capped (interval clamped at 1000 TU);
  a beacon with an SSID over 32 octets and a truncated auth or association
  response are malformed; only an Association Response answers the
  Association Request; pop_tx(nullptr) is refused; a reconfigure wipes the
  previous PMK before deriving; the group rekey through the state machine.

Code-reviewed rather than cell-pinned: the EAPOL MIC compare is
constant-time (no headless cell can observe timing).

Known answers: CCMP frames the Linux kernel encrypted and a real
hostapd/wpa_supplicant 4-way, both captured off mac80211_hwsim and checked
in (the capture scripts need root + hwsim and are for regeneration only;
the captures are not in the tree); IEEE 802.11i Annex H.4.2 PSK vectors;
and python-cryptography CCM vectors, a same-author transcription of the
framing that pins only the cipher plumbing, with a --check mode.

Not covered: any device, hardware crypto offload, PMF/802.11w, TKIP/CMAC/
SAE/EAP, AP-side per-station state, a replay window wider than 64 (HE/EHT
BlockAck), and SNonce renewal on an in-association rekey (by design; the
caller supplies it). A forged message 1 can still cost a handshake, as it
can with wpa_supplicant; documented at Supplicant::on_msg1. DupDetector
and the MSDU<->Ethernet helpers have no in-tree caller: StationSm runs no
duplicate cache, so the data-plane caller keeps one.

Comments in src/sta/ and the station tests state the current rule and its
reason, with the named test cells as provenance: no issue references, no
dates.

Build: six new ctest cells (dot11_frames, bss_table always; ccmp_framing,
supplicant, station_sm when OpenSSL is found; ccmp_vectors_generated when
Python 3 is). DEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON turns the OpenSSL-missing
configure WARNING into an error, and CI sets it on every ctest job (with
OpenSSL installed explicitly on Ubuntu, macOS and mingw). The standalone
targets request cxx_std_20 for MSVC. docs/station-core.md is the overview;
src/sta/CLAUDE.md maps the subtree for maintainers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
@josephnef
josephnef merged commit 956e723 into OpenIPC:master Sep 28, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants