How the wireless session keys are derived
A wired switch running 802.1X authorizes a port and forwards frames in the clear. It never derives an encryption key. A wireless access point cannot do that: after the RADIUS server says Accept, the AP runs a 4-way handshake with the client to derive the over-the-air keys, and it needs a Pairwise Master Key to start. The AP never sees that key directly. The RADIUS server sends it in the Access-Accept as two salt-encrypted vendor attributes, MS-MPPE-Send-Key and MS-MPPE-Recv-Key, and those are what this page is about. Get them wrong and the client authenticates but never associates, which looks like a silent handshake timeout.
The key is a projection of the TLS session
Section titled “The key is a projection of the TLS session”The master session key (MSK) is derived from the PEAP tunnel’s own TLS secrets, so the server and the client compute the same value independently without ever sending it. The derivation is a pseudo-random function over the TLS master secret:
MSK = TLS-1.2-PRF(master_secret, "client EAP encryption", client_random + server_random)[:64]MS-MPPE-Recv-Key = MSK[0:32]MS-MPPE-Send-Key = MSK[32:64]That is the same construction EAP-TLS uses (RFC 5216), borrowed by PEAP.
The trap: classic PRF, not the RFC 5705 exporter
Section titled “The trap: classic PRF, not the RFC 5705 exporter”There are two things called “client EAP encryption,” and picking the wrong one produces a key that is wrong in a way nothing tells you about until the AP rejects the association.
- The classic derivation runs the TLS 1.2 PRF directly over the master secret with the label and the two Hello randoms as the seed. TLS 1.2 EAP methods use this.
- The RFC 5705 exporter is a different computation with the same label. It is
what TLS 1.3 EAP (RFC 9190) uses, and what a TLS library’s
export_keying_materialgives you.
Because PEAP pins the tunnel to TLS 1.2, the classic PRF is the correct one. Calling
the exporter would compile, run, and hand back 64 confident bytes that no access
point will accept. l2trace does the classic PRF, and the PRF itself is validated
against OpenSSL’s TLS1-PRF byte for byte so it cannot drift.
The PRF hash follows the cipher
Section titled “The PRF hash follows the cipher”The TLS 1.2 PRF is not a single function. RFC 5246 section 5 makes SHA-256 the
default hash, but it lets a cipher suite name its own, and the SHA384 AEAD suites
(such as ECDHE-ECDSA-AES256-GCM-SHA384) do exactly that: their PRF runs on
SHA-384. So the hash the derivation uses has to follow whatever cipher the handshake
actually negotiated, not a fixed choice.
l2trace learned this on the air. A hardcoded SHA-256 PRF produced a wrong MSK the
moment a client and AP negotiated a SHA384 suite. Authentication itself completed
fine, then the AP’s 4-way handshake failed with a PMKID mismatch, which surfaces as
an opaque association timeout with nothing pointing at the key. The fix maps the
negotiated cipher name to its PRF hash (prf_hash_for_cipher in
src/l2trace/radius/mppe.py) so the derivation tracks the cipher.
The unit tests did not catch this, and the reason is worth keeping. The
two-independent-endpoints MSK check passed even while the hash was wrong, because
both endpoints shared the same wrong SHA-256 derivation and so agreed with each
other. Agreeing with yourself is not the same as being right. Only an external
reference caught it: OpenSSL’s TLS1-PRF known-answer output, and then the real
wpa_supplicant on the air. The lesson is to anchor anything negotiable to an
external oracle and to exercise every option, both cipher families here, rather than
trusting a self-consistent round trip. The cipher-dependent hash is now validated
end to end against a real UniFi 6 Lite access point, with the client associating
under CCMP.
Getting the master secret out of Python
Section titled “Getting the master secret out of Python”The wrinkle is that Python’s ssl module does not expose the TLS master secret
through any in-memory API. The only hook is SSLContext.keylog_filename, which
writes the NSS key-log line CLIENT_RANDOM <client_random> <master_secret> to a
file as the handshake completes.
l2trace treats that file as the sensitive thing it is. Each PEAP conversation points
its context at a private 0600 temp file, reads the one line back the instant the
TLS handshake completes, derives the MSK, and unlinks the file immediately. The
master secret’s lifetime on disk is bounded to the handshake window, and an
abandoned handshake still has its key-log reclaimed by the conversation store. The
server_random is not in the key-log, so it is read straight off the wire from the
ServerHello the server itself emitted.
Why two independent endpoints matter
Section titled “Why two independent endpoints matter”The strongest correctness check is not a single hard-coded vector, it is that two
independent TLS endpoints derive the same key. The server reads server_random
from what it sent; an in-process test supplicant reads it from what it received;
both pull the master secret and client_random from their own key-logs. They arrive
at the identical 64-octet MSK. That is an interoperability proof: a real access
point computing the peer key the way the spec says will match the server’s key. A
self-round-trip could pass with a compensating bug on both sides; two independent
derivations cannot.
The RADIUS attribute layer
Section titled “The RADIUS attribute layer”The two keys ride in the Access-Accept as Microsoft vendor attributes (vendor 311), salt-encrypted per RFC 2548 section 2.4.2: a two-octet salt, then the key masked by an MD5 chain keyed on the shared secret and the request Authenticator. l2trace pins the reply’s request Authenticator to the Access-Request’s, so the encryption keys on the value the AP will decrypt with, rather than falling back to the reply’s own Authenticator and handing the AP a garbage key.
The monitor-mode gate
Section titled “The monitor-mode gate”On a wired port, monitor mode is safe because an Accept with no VLAN changes nothing. On wireless there is no such thing: an Access-Accept is association. So the keys are emitted only on a genuine authentication success. A monitor-mode PEAP failure (a bad password) still returns Access-Accept, so a wired port is never cut, but it carries no MS-MPPE keys. The AP’s 4-way handshake then fails and the client does not get on the air, while the discrepancy is still logged. That is what keeps monitor mode from silently admitting a bad credential onto a wireless network.
See also
Section titled “See also”- Stand up wireless 802.1X, the task-oriented setup
- The security model, where wireless access control sits in the threat model
- 802.1X with Nautobot, and what happens when Nautobot is down