Skip to content

crypto::prf

The IEEE 802.11 PRF, over HMAC-SHA1.

Where the pairwise transient key comes from. Once the PMK is known — from wpa_psk — the 4-way handshake mixes it with both MAC addresses and both nonces through this PRF to produce the PTK, from which the key-encryption key, the key-confirmation key and the temporal (data) key are sliced. sha1_prf in the adapter’s crypto table.

Byte layout, taken from the reference not from memory

Section titled “Byte layout, taken from the reference not from memory”

The exact input to each HMAC is label ‖ NUL ‖ data ‖ counter, where the label’s length includes its NUL terminator and the counter is a single byte starting at zero and incrementing per 20-byte output block. That is read directly from esp-idf’s sha1-prf.c (components/wpa_supplicant), which is the implementation the blobs on the other side of this were built to interoperate with — so matching it byte for byte is the point, and a guess about the spec’s wording is exactly what would not do.

fn ptk<const X: usize>(pmk: &[u8], aa: &[u8; 6], spa: &[u8; 6], anonce: &[u8; 32], snonce: &[u8; 32]) -> [u8; X]

Derive the PTK for a CCMP association.

The handshake’s fixed label is "Pairwise key expansion", and the data is the two MAC addresses then the two nonces, each pair in ascending byte order — the ordering is what lets both ends derive the same key without agreeing who is “first”. X is the PTK length in bytes: 48 for CCMP (16 KCK ‖ 16 KEK ‖ 16 TK), 64 for TKIP.

fn sha1_prf(key: &[u8], label: &[u8], data: &[u8], out: &mut [u8])

IEEE 802.11 PRF: fill out from key, a text label, and data.

Each 20-byte block of out is HMAC-SHA1(key, label ‖ NUL ‖ data ‖ i) for i = 0, 1, 2, …; the final block is truncated to fit. This is the general form; [ptk] wires in the handshake’s specific label and data.