blog article
Constant-time isn't enough: why WPA3-SAE had to abandon Hunting-and-Pecking for Hash-to-Element
WPA3-SAE turns your password into an elliptic-curve point, and the old Hunting-and-Pecking method leaked it through timing. Padding the loop didn't fix it — and neither does SSWU alone. A deep look at constant-time crypto as a property of the silicon: why the leak channel changes across Cortex-M, Cortex-A and x86, why DIT/DOITM and your compiler matter, and how you'd actually measure it.
TL;DR: WPA3-SAE turns your password into an elliptic-curve point, and the original Hunting-and-Pecking search leaked it through variable loop timing. Padding the loop to a fixed 40 iterations didn’t close the leak — the branch predictor, the cache, and variable-time field math still expose which iteration hit. The fix is Hash-to-Element (SSWU): a branchless, deterministic password-to-point map with no secret-dependent control flow. But SSWU only settles the algorithm — constant-time is equally a property of the silicon. The live side-channel changes across a Cortex-M33 (nRF5340 / nRF54LM20B), a Cortex-A53 AP, and an x86 core; a compiler can quietly turn your
CMOVback into a branch; and the DIT/DOITM hardware timing modes don’t exist on the constrained parts. So on real hardware you measure it (dudect), you don’t assume it. We’re taking this upstream, too: a design proposal to add a constant-time, H2E-only WPA3-SAE to the PSA Crypto API in TF-PSA-Crypto (PR #837).
Every WPA3-Personal handshake starts with a small, awkward problem: it has to turn a human password into a point on an elliptic curve. That conversion — password string in, curve point out — is where WPA3’s first big security lesson lived. The obvious way to do it, “Hunting-and-Pecking,” leaked the password over the air. The obvious fix for that leak — “just pad the loop so it always runs the same number of times” — is the interesting part, because it felt correct, it shipped, and it still leaked.
This is a post about why software-level constant-time is a losing game on modern CPUs, and why the fix wasn’t a better loop but a different kind of math: the Hash-to-Element (H2E) map built on SSWU. Dragonblood and the hash-to-curve debate have been covered thoroughly elsewhere (Vanhoef & Ronen’s Dragonblood, WizardFi’s hash-to-curve writeup) — so we’ll keep the history to a primer and spend the depth where it’s more useful: the microarchitecture that makes “constant-time HnP” an oxymoron.
The setup: a password has to become a curve point
WPA3-Personal replaces WPA2’s PSK four-way handshake with SAE (Simultaneous Authentication of Equals), the Dragonfly PAKE. Before the two sides can run their Diffie-Hellman exchange, they both need to derive the same secret starting point from the shared password. On the elliptic-curve groups everyone actually uses (P-256 is Group 19), that starting point is a curve point — historically called the Password Element, PWE, and in the H2E era PT.
Here’s the catch that drives the whole story. A short-Weierstrass curve is the set of points satisfying
y² = x³ + ax + b (mod p)
For a randomly chosen x in the field, the right-hand side is a quadratic
residue — an actual square with a valid y — only about half the time.
The other half of x values simply aren’t on the curve; there is no y. So
you can’t just hash the password to an integer and call it an x-coordinate.
Roughly 50% of your guesses land nowhere. You need a strategy for the misses.
The two strategies WPA3 has used are the entire subject of this post.
The primer: Hunting-and-Pecking, and why it leaked
The original SAE method was Hunting-and-Pecking (HnP): literally guess, test, and retry until a guess lands on the curve.
def hunt_and_peck(password, addr1, addr2, p, a, b):
counter = 1
while True:
# derive a candidate x-coordinate from pw + a counter
seed = KDF(password, addr1, addr2, counter)
x = seed mod p
rhs = (x**3 + a*x + b) mod p
if is_quadratic_residue(rhs, p): # ~50% chance
y = modular_sqrt(rhs, p)
return (x, y) # found PWE — stop
counter += 1 # miss: try the next counter
The loop stops the moment it finds a residue. That single return is the
whole vulnerability. Because each iteration succeeds with probability ~½, the
number of iterations is a random variable that depends on the password. One
passphrase resolves in 3 iterations; another takes 8; another takes 12.

And the counter is seeded together with the two MAC addresses, which are public and observable. So an attacker who can measure how long the derivation takes — over the air, or by watching cache behaviour on a shared host — learns the iteration count for a given identity, and iteration count correlates with the password. Feed that into an offline dictionary attack and you can prune candidate passwords that would have produced a different timing signature. That is the core of Dragonblood (Vanhoef & Ronen, 2019): the timing and cache side-channels of SAE’s password-to-element step.
The obvious objection — “Wi-Fi jitter is enormous; you can’t time a few field multiplications over the air” — is correct, and it’s worth being precise about why the attack works anyway. The remote signal isn’t a handful of cycles; it’s the number of iterations, and each extra iteration is a whole extra KDF/hash invocation — tens of thousands of cycles, a discrete step, not a smear. The attacker also chooses the measurement: because PWE is derived per MAC-address pair, spoofing many client MACs forces the victim to run many independent derivations with different iteration counts, and averaging over those samples lifts the signal out of the noise. Dragonblood’s cleanest remote timing results were actually against the MODP/FFC groups (where the iteration count leaks most directly); for the ECC groups the sharper attack was the local cache channel, which needs co-residency but gives per-iteration resolution. Two different attacker models, two different signals — keep them separate, because the fixes differ too.
The intuitive fix that failed: “just always loop 40 times”
Any software engineer’s first instinct on seeing a secret-dependent loop is correct in spirit: remove the data dependence from the loop count. Don’t stop when you find PWE — keep going for a fixed number of iterations (the countermeasure settled on a minimum of k = 40), stash the first hit in a variable, and return it at the end. On paper the wall-clock time is now constant regardless of password.
def hunt_and_peck_fixed(password, addr1, addr2, p, a, b, k=40):
pwe = None
found = False
for counter in range(1, k + 1):
seed = KDF(password, addr1, addr2, counter)
x = seed mod p
rhs = (x**3 + a*x + b) mod p
if not found and is_quadratic_residue(rhs, p):
pwe = (x, modular_sqrt(rhs, p)) # first hit only
found = True
# keep looping either way — no early return
return pwe
This is the fix that shipped as a Dragonblood countermeasure, and it is where the “constant-time in software” mindset runs headfirst into the reality of the chip it runs on. Padding the loop hides how many iterations ran. It does not hide which iteration was the real one — and modern CPUs leak that in at least three independent ways.

a) Branch predictors and pipeline flushes
if not found and is_quadratic_residue(...) is a branch, and its behaviour
changes exactly once per handshake — on the iteration that flips found
from false to true. A modern out-of-order core has a branch predictor that
learns the steady-state pattern (the residue test failing, or found already
true) and speculatively runs down it. On the one iteration where reality
diverges from the learned pattern, the CPU mispredicts, discards the
speculated instructions, and refills the pipeline. That flush costs a
measurable burst of cycles — a timing discontinuity pinned to the exact hit
iteration. The loop count is constant; the shape of the timing curve inside
it is not.
b) Cache-line leaks (Flush+Reload)
The found iteration does something the dummy iterations don’t: it writes the
real (x, y) into the pwe buffer and touches the modular_sqrt code path.
Even if you “fix” that by always writing somewhere — a decoy buffer on misses —
the found and not-found paths touch different cache lines. An attacker
process co-resident on the same physical core (think a hostile app on the same
phone, or a neighbour VM on shared silicon) runs Flush+Reload: flush the
target cache line, let the victim run one iteration, then time how long it
takes to reload that line. A fast reload means the victim just touched it. That
tells the attacker which iteration was the hit — no over-the-air timing
needed, just shared hardware. This is precisely the class of leak Dragonblood
demonstrated against real implementations.
c) Variable-time math inside every iteration
Even if you somehow neutralised the control flow and memory access, the arithmetic itself isn’t constant-time. The quadratic-residue test is a Legendre symbol, computed as a modular exponentiation:
legendre(v) = v^((p−1)/2) mod p
A textbook square-and-multiply skips the multiply on zero exponent bits, so its
cycle count depends on the operand. Modular inversion (extended Euclid /
binary GCD) and modular_sqrt are likewise operand-dependent. And the operand
here — the candidate value v — is derived from the password. SAE tried to
paper over this with blinding (multiplying by a random quadratic residue or
non-residue before the test), which helps, but the composite operation still
had exploitable timing variation across iterations. You are trying to make a
pile of individually leaky primitives collectively constant-time by
arranging them carefully. On an out-of-order, speculating, caching CPU, that
arrangement does not hold.
The through-line: a data-dependent branch is not something you can safely keep and then hide. The moment your control flow, your memory-access pattern, or your operand values depend on a secret, you are relying on the microarchitecture to keep that dependence invisible — and it won’t.
The channel depends on the chip: same code, three threat models
Here’s the part most write-ups skip, and it’s the part that actually matters if you ship. WPA3-SAE runs on wildly different silicon — a Cortex-M33 in an IoT Wi-Fi client, a Cortex-A53 in an access point, an x86 core in a laptop — and the three channels above are not equally present on all of them. The same source compiles to three different leak surfaces.

- Cortex-M class (the IoT client — the Cortex-M33 in an nRF5340 or an
nRF54LM20B). A 3-stage in-order pipeline, no dynamic branch predictor, and
an instruction cache at most — no data cache over the crypto working
set. Channels (a) and (b) largely evaporate: there’s no data-cache line for
Flush+Reload to probe, and on a single-core MCU there’s no co-resident
attacker context to probe it from in the first place. That isn’t a win —
it just relocates the threat. The dominant channels become power and EM
analysis (DPA), where a probe on a $30 module is a far more realistic
adversary than a neighbour VM, and variable-latency arithmetic: the M33’s
integer divide is data-dependent (a
UDIV/SDIVtakes a variable number of cycles), and older cores early-terminate multiply. An engineer who reads a generic “beware the branch predictor” post and ships an nRF5340 supplicant is hardening a channel that isn’t there while ignoring the two that are. - Cortex-A class (the AP). Now you have a branch predictor and a shared L2 (often a shared last-level cache across the SoC), so (a) and (b) come back fully. And an A53 is ARMv8.0 — it has no DIT bit (more on that below), so you cannot ask the hardware for data-independent timing; you’re on your own in software.
- x86 (the laptop/desktop). Aggressive out-of-order execution, deep speculation, and a cross-core inclusive-ish LLC make cache attacks the marquee threat, exploitable from another core or another VM.
The lesson isn’t “MCUs are safe.” It’s that “constant-time” is meaningless without naming the microarchitecture you mean it on. A mitigation that closes the cache channel on an A53 does nothing about the EM channel on the M33 running the other end of the same handshake.
The real fix: Hash-to-Element with the SSWU map
The way out isn’t a cleverer loop. It’s to delete the search entirely and replace “try random x-coordinates until one is on the curve” with a deterministic function that maps any field element straight onto a valid curve point — no misses, no retries, nothing to terminate early. That function is the Simplified Shallue–van de Woestijne–Ulas (SSWU) map, and wrapping it into SAE gives Hash-to-Element (H2E), standardised in the WPA3 specification and drawing on the IETF hash-to-curve work (RFC 9380). H2E is mandatory in the WPA3 spec’s later revisions and is what Wi-Fi 6E/7 devices are certified against; HnP is deprecated.
One precision worth stating, because it’s usually blurred: SSWU is the mechanism for the ECC groups (P-256/384/521 — groups 19–21). The FFC/MODP groups (22–24) don’t use SSWU at all; their H2E is a different, simpler direct construction that hashes to an exponent and raises a generator, with no curve map involved. When people say “H2E uses SSWU” they mean the elliptic-curve groups everyone actually deploys — which is the case we’ll follow here.
It runs in two straight-line stages.
Step 1 — a clean, unbiased field element
First, turn the password into a field element u ∈ 𝔽_p with no modulo bias.
The naïve hash(pw) mod p is biased: a 256-bit hash reduced mod the P-256
prime slightly favours the low end of the range, and biased inputs leak
structure. RFC 9380’s hash_to_field avoids this by expanding the password to
more bytes than the field needs — L = ⌈(⌈log₂ p⌉ + k) / 8⌉ bytes, e.g. 48
bytes for P-256 with a 128-bit security margin — via an HKDF-style
expand_message_xmd, and then reducing mod p. Expanding to p’s bit-length
plus 128 bits drives the reduction bias below 2⁻¹²⁸. Uniform element in,
every time.
Step 2 — the SSWU map: rational formulas, then a conditional move
Now map u to a point with algebra, not search. SSWU computes two candidate
x-coordinates from fixed rational formulas in u, evaluates the curve equation
at each, and picks the one that’s a square — and crucially, it is constructed
so that at least one of the two is always a valid square. There is no
“none of them worked, loop again” case. Sketching the flow for a
short-Weierstrass curve:
u ──► tv1 = 1 / (Z²·u⁴ + Z·u²) # constant-time inversion (0 handled)
x1 = (−b/a)·(1 + tv1)
x2 = Z·u²·x1 # the "other" candidate
gx1 = x1³ + a·x1 + b
gx2 = x2³ + a·x2 + b
─────────────────────────────────────────────
e = is_square(gx1) # branchless test
x = CMOV(x2, x1, e) # select the valid x (MOVE, not branch)
gx = CMOV(gx2, gx1, e) # select its g(x) — the guaranteed square
y = sqrt(gx) # ONE sqrt, always on the valid square
y = CMOV(−y, y, sign(u) == sign(y)) # fix sign deterministically
─────────────────────────────────────────────
◄──── PT = (x, y) # always valid, fixed cost
Note the ordering, because it matters for a real implementation: select the
winning g(x) with a CMOV first, then take the square root once. This is how
RFC 9380 writes it, and it sidesteps a subtle footgun — you never call sqrt
on the candidate that isn’t a square. That matters if your field’s root
routine is a library modular_sqrt() that rejects (or returns garbage for)
non-residues: feeding it gx1 when gx1 is a non-square is undefined
behaviour you’d have to special-case.
Even the underlying primitive is total, though. When sqrt is the constant-time
fixed exponentiation — for P-256, p ≡ 3 (mod 4) means the root is a single
a^((p+1)/4) ladder — it’s defined on every field element. Hand it a
non-square and it doesn’t error; it just returns a value whose square is −gx
rather than gx, i.e. a non-valid root that the CMOV mask would discard anyway.
So the RFC ordering isn’t strictly required for correctness with an
a^((p+1)/4) sqrt — but it’s the honest way to write the sketch, keeps you
safe against a stricter sqrt, and costs one exponentiation instead of two.
Every selection is a CMOV — a constant-time conditional move that computes
both sides and picks one with an arithmetic mask, executing identically
regardless of which candidate wins. is_square(v) is itself the Legendre-style
v^((p−1)/2) exponentiation done at fixed cost, and the a^((p+1)/4) root runs
the same for every input. No branch depends on a secret; no memory write is
conditional on a secret. At the algorithm level, the derivation now takes the
same shape for every password — which is exactly the guarantee HnP could never
give.

That’s the whole philosophical shift. HnP asks “is this random guess on the curve?” and its runtime is an emergent property of luck and the password. SSWU asks nothing — it computes a point. Constant-time stops being a fragile property you engineer on top of leaky primitives and becomes a structural guarantee of the algorithm: there is no secret-dependent control flow to leak, because there is no control flow at all.
SSWU is necessary, but not sufficient — the silicon still gets a vote
Here’s the honest caveat that turns “constant-time by construction” from a slogan into engineering. SSWU removes the algorithmic leak — the secret-dependent branch. It does not, by itself, remove the operand-timing leak or guarantee the compiler kept your CMOVs. Straight-line source is necessary; it is not sufficient. Three ways the hardware and toolchain can put the leak back:
1. Your CMOV is a lie until you check the assembly. x = e ? a : b — and
even hand-rolled bit-masking — is routinely lowered back into a branch by
optimising compilers when they decide it’s cheaper. This is the single most
common constant-time bug in the field (“constant-time source, variable-time
binary”). The straight-line SSWU sketch above is only constant-time if the
emitted machine code actually contains a CSEL/CMOV, not a conditional jump.
The fixes are unglamorous: constant-time selection via arithmetic masks behind
an optimisation barrier, vetted ct_* primitives (BearSSL-style), or inline
asm — and then you read the disassembly to confirm it survived -O2,
because the next compiler bump can silently undo it.
2. The arithmetic underneath may not be operand-independent. SSWU is a pile
of field multiplications, an inversion, and two exponentiations. If any of those
primitives has data-dependent latency, the whole map leaks despite having no
branches. Classic offenders: early-terminating multipliers (some Arm cores
short-circuit on small operands) and variable-latency integer division
(Cortex-M SDIV/UDIV run 2–12 cycles depending on the values). The
constant-time discipline here is concrete — for example, do modular inversion by
Fermat’s little theorem (a^(p−2)), a fixed exponentiation ladder, rather
than binary extended-GCD, which branches on operand bits; or use a Bernstein–Yang
“safegcd” constant-time inversion.
3. Modern CPUs offer a hardware switch for exactly this — one you must
explicitly enable on the parts that have it, and that’s simply absent on the
parts that don’t. Armv8.4 added DIT (Data-Independent Timing), a
PSTATE.DIT bit (defaulting to 0) that forces a defined set of instructions to
run in constant time regardless of operand values. Intel’s equivalent is
DOITM (Data Operand Independent Timing Mode), toggled via an MSR. Note both
are A-profile / application-CPU features — M-profile Cortex-M has no such bit at
all. Two things a systems engineer needs to internalise about them:
- They cover operand-value timing, not memory-address timing. DIT does nothing about the cache channel — secret-dependent addresses (table lookups, the Flush+Reload surface) are still your problem to eliminate in software. Operand-timing and address-timing are two independent axes; DIT/DOITM only buy you the first.
- They exist only on recent silicon. The Cortex-A53 in a typical AP is ARMv8.0 — no DIT. The Cortex-M33 in the client — no DIT. On exactly the constrained parts where you most want the guarantee, you don’t get it, and you’re back to auditing the arithmetic by hand.
And one more embedded wrinkle that generic posts never reach: on many Wi-Fi MCUs the EC math is offloaded to a PKA / crypto accelerator (Arm CryptoCell, a vendor PKA block). Whether the PWE derivation is constant-time then depends on that accelerator’s microarchitecture — which is usually undocumented, and which DIT, your compiler flags, and your source-level audit cannot reach. Sometimes the most defensible answer is “we measured it” (next section) rather than “the spec says so.”
What your crypto library actually gives you: Mbed TLS in the Zephyr stack
All of this is easier to reason about against a concrete library, and in the ZephyrRTOS Wi-Fi stack that library is Mbed TLS (now split with TF-PSA-Crypto in the 4.x line). It’s worth knowing precisely which of the guarantees above you get for free and which you still own.
What it does give you. Mbed TLS ships a dedicated constant-time module
(mbedtls_ct_*) — constant-time comparison and conditional-select primitives,
hardened after the Lucky13 padding-oracle era. Crucially for the compiler
footgun above, that module wraps its masked selects in assembly optimisation
barriers on supported architectures (x86, Arm/Thumb, AArch64) precisely so
-O2 can’t lower the mask back into a branch — the library read the
disassembly so you don’t have to, on those targets. Underneath,
mbedtls_mpi_exp_mod is a constant-time modular exponentiation (Montgomery
arithmetic plus a constant-time window-table lookup) — exactly the primitive a
Legendre is_square and a Fermat a^(p−2) inversion are built from — and
mbedtls_ecp_mul does constant-time scalar multiplication with a fixed comb
pattern and constant-time point selection, with optional projective-coordinate
randomisation when you pass it an RNG, as an SPA/DPA countermeasure. That last
knob is the one you specifically want lit on the Cortex-M33 column, where power
and EM are the live threat.
What it doesn’t. Two honest gaps. First, Mbed TLS has no hash-to-curve / SSWU API today — SAE’s PWE/PT derivation lives in hostap / wpa_supplicant, which calls Mbed TLS only for the underlying bignum and ECP arithmetic. So the constant-timeness of H2E is a joint property: hostap’s SAE code has to use the constant-time primitives correctly and Mbed TLS has to provide them — the library alone never hands you a constant-time password-to-point. (We traced how SAE reaches past the clean PSA surface into raw ECP in the Mbed TLS 4.x Wi-Fi crypto post.) Second, Mbed TLS is explicit that its side-channel resistance is best-effort at the C level, not a guarantee on every compiler and target — it does not flip the DIT/DOITM hardware bit for you, and some operations (classic modular inversion, much of parsing) were never constant-time to begin with. Which lands you right back where the last section did: a good library gets you most of the way, and then you still measure.
Where this is heading — and where we’re pushing it. That first gap is
starting to close at the API level, and it’s work we’re driving. ARM’s PSA
Crypto API 1.4 added a first-class WPA3-SAE PAKE (the WPA3_SAE_H2E key
derivation, with FIXED and GDH variants), and we’ve authored the
Phase 0 design proposal
to implement it in TF-PSA-Crypto’s built-in driver
(feature request #709).
What’s worth calling out is that the design is this blog post at the altitude
of a spec. We made a deliberate call to implement hash-to-element only and
not implement the Looping (HnP) method at all — it returns
PSA_ERROR_NOT_SUPPORTED — precisely because HnP has no constant-time PWE
(Dragonblood, CVE-2019-9494) and H2E supersedes it. There’s no point carrying a
method into a fresh constant-time primitive whose entire reason for being
deprecated is that it can’t be made constant-time. Its new primitives
are the RFC 9380 hash_to_field + Simplified SWU we walked through, plus a
constant-time modular square root (w^((p+1)/4) mod p, squareness checked by
squaring the root back rather than a second exponentiation — no early exit). The
constant-time strategy is written out per secret-dependent step (branch-free
SSWU with mbedtls_mpi_core_cond_assign as the CMOV, CT exp_mod, blinded
mbedtls_ecp_mul), and it’s verified in CI the way the previous section argued
you must — secrets poisoned with constant-flow markers, run under MemSan and
Valgrind. Pulling PWE derivation out of every Wi-Fi supplicant and into one
constant-time PSA primitive is how you stop re-litigating this side-channel in
each stack that speaks WPA3.
Side-by-side
| HnP | HnP + 40 dummy loops | H2E (SSWU) | |
|---|---|---|---|
| Approach | Trial-and-error search | Padded trial-and-error | Deterministic map |
| Execution path | Data-dependent loop | Fixed loop count, branchy body | Straight-line, no loop |
| Secret-dependent branch | Yes (early return) | Yes (the found flip) | None (CMOV only) |
| Conditional memory write | Yes | Yes (real vs decoy buffer) | None |
| Timing / cache leak | Over-the-air and cache | Cache + branch-predictor + math | None by construction |
| Modulo bias | Present | Present | Removed via hash_to_field |
| WPA3 status | Original, deprecated | Interim countermeasure | Mandatory (H2E-required certs) |
How you’d actually prove it, not just claim it
Everything above is an argument; a security-conscious team wants a measurement. Constant-timeness is testable, and the tooling is the same regardless of whether it’s WPA3 PWE, an ECDSA nonce, or a Kyber decapsulation:
dudect(Reparaz et al.) runs the routine on two input classes and applies a Welch’s t-test to the timing distributions — if they’re distinguishable, it leaks. It runs on the target device, which is what you want for an M33 or an AP SoC, because it catches the arithmetic and the accelerator, not just the source.ctgrind(Langley’s Valgrind patch) marks secret buffers as uninitialised and lets Memcheck flag any branch or memory index that depends on them — catching the “compiler turned my CMOV into a jump” bug at dev time, before it ever ships.- Microwalk / ct-verif / Binsec-Rel go further, at binary or formal level, for when you need assurance rather than a spot check.
The point for a reviewer: “we ran dudect on the PWE path on the actual part
and the t-statistic stayed under threshold across 10⁶ samples” is a claim you
can stand behind. “The spec is constant-time” is not.
Takeaways for systems developers
-
“Constant-time” is meaningless until you name the microarchitecture. The branch-predictor and cache channels that dominate on an application processor barely exist on an in-order MCU — where power/EM and variable-latency arithmetic take over instead. A mitigation aimed at one core can leave the other end of the same handshake wide open. Threat-model per chip, not per algorithm.
-
Straight-line source is necessary, not sufficient. SSWU removes the algorithmic branch, but the compiler can re-introduce one, the multiplier can be data-dependent, and DIT/DOITM — the hardware guarantee — is absent on most constrained parts and covers only operand timing, never the cache. Read the disassembly, pin the arithmetic, and where it matters, measure (
dudect,ctgrind) rather than assert. -
This pattern generalises well beyond Wi-Fi. Any “loop until a candidate passes a test” over secret data — RSA prime generation, rejection-sampling a nonce, ECDSA
kselection, lattice sampling in PQC — carries the same risk. Prefer deterministic maps, constant-time selects (CMOV/cswap) you’ve verified survive the optimiser, constant-time inversion (Fermat / safegcd), and unbiasedhash_to_fieldwherever the input touches a key or a password.
At Dotstar Systems we work across the wireless firmware and protocol stack — from IEEE draft specs down to the wpa_supplicant commit and the ZephyrRTOS Wi-Fi stack, including the crypto plumbing where SAE’s PWE/PT derivation actually lives — and we take that work upstream: the TF-PSA-Crypto / PSA Crypto API design above is one of several places we contribute the fix rather than just the analysis. If you’re auditing a supplicant for side-channels, migrating a constrained client to H2E, or reasoning about constant-time crypto on real silicon, we’d welcome the conversation.
References
Attacks and analysis
- M. Vanhoef and E. Ronen, “Dragonblood: Analyzing the Dragonfly Handshake of WPA3 and EAP-pwd,” IEEE Symposium on Security and Privacy (S&P), 2020. https://wpa3.mathyvanhoef.com/
- Y. Yarom and K. Falkner, “FLUSH+RELOAD: A High Resolution, Low Noise, L3 Cache Side-Channel Attack,” USENIX Security, 2014. https://www.usenix.org/conference/usenixsecurity14/technical-sessions/presentation/yarom
Specifications and standards
- A. Faz-Hernández, S. Scott, N. Sullivan, R. S. Wahby, and C. A. Wood, RFC 9380: Hashing to Elliptic Curves, IETF, August 2023. https://www.rfc-editor.org/rfc/rfc9380.html
- D. Harkins, RFC 7664: Dragonfly Key Exchange, IETF, November 2015. https://www.rfc-editor.org/rfc/rfc7664.html
- IEEE Std 802.11-2020, Clause 12.4 (Authentication using a password — SAE), including the Hash-to-Element derivation. https://standards.ieee.org/ieee/802.11/7028/
- Wi-Fi Alliance, WPA3 Specification (v3.x), which mandates Hash-to-Element for SAE. https://www.wi-fi.org/discover-wi-fi/security
- Arm, PSA Certified Crypto API 1.4 — WPA3-SAE PAKE support (§10.13.12–13). https://arm-software.github.io/psa-api/crypto/1.4/
- Dotstar Systems, “WPA3-SAE (PSA Crypto API 1.4) design proposal,” TF-PSA-Crypto PR #837 and feature request #709. https://github.com/Mbed-TLS/TF-PSA-Crypto/pull/837
Constant-time engineering
- E. Käsper and P. Schwabe, “Faster and Timing-Attack Resistant AES-GCM,” CHES 2009 — on why data-dependent branches and table lookups leak.
- R. S. Wahby and D. Boneh, “Fast and simple constant-time hashing to the BLS12-381 elliptic curve,” CHES 2019 — the practical SSWU treatment. https://eprint.iacr.org/2019/403
- D. Kaufmann, H. Pelletier, S. Vaudenay, and A. Villani, “When Constant-Time Source Yields Variable-Time Binary: Exploiting Curve25519-donna Built with MSVC 2015,” CANS 2016 — the compiler-defeats-constant-time problem.
- L. Simon, D. Chisnall, and R. Anderson, “What You Get is What You C: Controlling Side Effects in Mainstream C Compilers,” IEEE EuroS&P 2018.
- O. Reparaz, J. Balasch, and I. Verbauwhede, “Dude, is my code constant time?”
DATE 2017 — the
dudectmethodology. https://github.com/oreparaz/dudect - T. Pornin, “Why Constant-Time Crypto?” (BearSSL constant-time coding rules). https://www.bearssl.org/constanttime.html
- Mbed TLS, constant-time module (
mbedtls_ct_*) and project security / threat-model guidance on best-effort side-channel resistance. https://github.com/Mbed-TLS/mbedtls/blob/development/SECURITY.md
CPU / SoC timing architecture
- Arm, “Armv8.4-A: Data-Independent Timing (DIT),” Arm Architecture Reference
Manual (PSTATE.DIT) and developer guidance on
FEAT_DIT. https://developer.arm.com/documentation/ddi0595/latest/AArch64-Registers/DIT—Data-Independent-Timing - Intel, “Data Operand Independent Timing Instruction Set Architecture (DOIT)” and DOITM guidance. https://www.intel.com/content/www/us/en/developer/articles/technical/software-security-guidance/best-practices/data-operand-independent-timing-isa-guidance.html
- D. J. Bernstein and B.-Y. Yang, “Fast constant-time gcd computation and modular inversion,” CHES 2019 (safegcd). https://eprint.iacr.org/2019/266
PS: This article was drafted with AI assistance (Claude + Gemini) from raw technical notes and spec analysis. The opinions, framing, and any errors are the author’s; the AI helped shape the narrative and the diagrams.