<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Parag Mali - tag: post-quantum-crypto</title><description>Posts tagged post-quantum-crypto.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sat, 25 Jul 2026 08:57:55 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/post-quantum-crypto/rss.xml" rel="self" type="application/rss+xml"/><item><title>Nobody Broke the Discrete Log: A Field Guide to Diffie-Hellman and Forward Secrecy</title><link>https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/</link><guid isPermaLink="true">https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/</guid><description>The discrete log held for fifty years; Logjam, small-subgroup, and invalid-curve attacks broke the deployment instead. A field guide to FFDHE, ECDH, and X25519.</description><pubDate>Mon, 13 Jul 2026 02:35:50 GMT</pubDate><content:encoded>
**Diffie-Hellman&apos;s hardness -- the discrete logarithm -- has held for fifty years and almost never breaks in the field.** What breaks is one of four conditions the caller must supply, plus one discipline beneath them all.&lt;p&gt;Forward secrecy needs a genuinely &lt;strong&gt;fresh&lt;/strong&gt; ephemeral: reuse it and the property silently evaporates, which is why the paper that named the problem is titled &lt;em&gt;Imperfect Forward Secrecy&lt;/em&gt;. The group must be &lt;strong&gt;strong and unshared&lt;/strong&gt;: finite-field DH has a sub-exponential attack whose expensive step depends only on the prime, so millions of servers sharing one 1024-bit prime let a nation-state precompute once and read everyone, while a 256-bit curve escapes this -- which is why X25519 keys are 32 bytes. The peer&apos;s public value must be &lt;strong&gt;validated&lt;/strong&gt;: an unvalidated point turns your reused key into a key-extraction oracle, and X25519 makes that check structural through twist security, so clamping is &lt;em&gt;not&lt;/em&gt; validation. The exchange must be &lt;strong&gt;authenticated&lt;/strong&gt;: raw DH stops a passive eavesdropper and not an active man-in-the-middle. Beneath all four, the raw shared secret is a group element, not a key: run it through a KDF.&lt;/p&gt;
&lt;p&gt;The fifty-year evolution -- finite-field DH in 1976, then elliptic-curve DH, then X25519, then the TLS 1.3 ephemeral mandate, then today&apos;s &lt;code&gt;X25519MLKEM768&lt;/code&gt; hybrid -- is one sustained move to bake each condition into the primitive, the named group, and the protocol, so the simple thing to do is the safe thing to do. Every one of these still falls to Shor, which is why ECDH is now being wrapped, not replaced. The math held; the deployment handed the secret away.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;1. They Precomputed the Prime Once, and Read Everyone&lt;/h2&gt;
&lt;p&gt;In 2015 a team of researchers pointed out something that should have been impossible: millions of HTTPS, SSH, and VPN servers were sharing the same handful of 1024-bit Diffie-Hellman primes. That sharing did not merely weaken those servers -- it changed the &lt;em&gt;economics&lt;/em&gt; of breaking them. The most expensive step of the best known algorithm for computing discrete logarithms depends only on the prime, not on any particular connection, so an adversary who could afford the precomputation for one common prime could then decrypt, passively and forever, every connection that used it [@weakdh].&lt;/p&gt;
&lt;p&gt;Breaking the single most common 1024-bit group would have exposed an estimated 18% of the top million HTTPS sites, and a second common prime an estimated 66% of VPNs [@weakdh]. Nobody had to break the math. They precomputed the prime once and read everyone.&lt;/p&gt;
&lt;p&gt;Here is the part that should unsettle you. The people running those servers were not careless. They believed they were doing the one thing everyone agrees you must do: generate a brand-new key for every connection. The researchers put it plainly -- practitioners &quot;believed this was safe as long as new key exchange messages were generated for every connection&quot; -- and they were wrong, because a &lt;em&gt;shared&lt;/em&gt; prime and a &lt;em&gt;fresh&lt;/em&gt; per-connection secret are two different conditions, and only one of them was being met [@weakdh].&lt;/p&gt;
&lt;p&gt;The attack, named Logjam, actually had a second half: an active attacker could force a browser and server to negotiate a deliberately breakable 512-bit &quot;export-grade&quot; group, so that even a modest adversary could solve the discrete log in real time [@weakdh]. Two coupled failures -- a downgrade and a precomputation -- and neither one touched the underlying hardness assumption. That active downgrade even had an exact RSA twin the same year, FREAK, which forced a browser down to breakable 512-bit &quot;export&quot; RSA instead of Diffie-Hellman [@freak]. The paper&apos;s title says the whole thing in two words: &lt;em&gt;Imperfect Forward Secrecy&lt;/em&gt;.&lt;/p&gt;

The math held; the deployment handed the secret away.
&lt;p&gt;This article is about that gap, and about closing it. The security a system actually gets from Diffie-Hellman is never simply &quot;the discrete logarithm is hard.&quot; It is a stack of four conditions the caller must independently satisfy -- a genuinely &lt;strong&gt;fresh&lt;/strong&gt; ephemeral secret, a &lt;strong&gt;strong and unshared&lt;/strong&gt; group, a &lt;strong&gt;validated&lt;/strong&gt; peer value, and an &lt;strong&gt;authenticated&lt;/strong&gt; exchange -- plus one discipline underneath them: the raw output is a group element, not a key, so it must pass through a key derivation function.&lt;/p&gt;
&lt;p&gt;Every world-scale Diffie-Hellman break you half-remember -- Logjam, small-subgroup key recovery, invalid-curve attacks on TLS, Raccoon, the anonymous man-in-the-middle -- is exactly one of those conditions quietly violated. The same unbroken math spawned a whole catalog of disasters, and the fifty-year history of the primitive is one sustained effort to pull those conditions back inside the primitive, the named group, and the protocol, so that a caller cannot forget them.&lt;/p&gt;
&lt;p&gt;To see why a shared prime is a catastrophe -- and why &quot;we generate a fresh key every connection&quot; was true yet still left everyone exposed -- you first have to be precise about what a Diffie-Hellman exchange promises, and the four jobs it quietly delegates to you.&lt;/p&gt;
&lt;h2&gt;2. What Diffie-Hellman Actually Is, and the Four Things You Must Supply&lt;/h2&gt;
&lt;p&gt;Two strangers who have never met, standing on either side of a wire the whole world can read, end up sharing a secret that no eavesdropper can compute. That is one of the deepest ideas in computer science, and it hides four decisions the scheme refuses to make for you.&lt;/p&gt;
&lt;h3&gt;The mechanism, with just enough math&lt;/h3&gt;
&lt;p&gt;Fix a public group with a generator $g$. Alice picks a secret number $a$ and sends $g^a$; Bob picks a secret $b$ and sends $g^b$. Each raises what they received to their own secret, and because exponents commute, they land on the same value:&lt;/p&gt;
&lt;p&gt;$$g^{ab} = (g^a)^b = (g^b)^a.$$&lt;/p&gt;
&lt;p&gt;An eavesdropper sees $g$, $g^a$, and $g^b$, but never $a$ or $b$. A student can implement the whole thing in three lines of modular exponentiation.&lt;/p&gt;

A protocol in which two parties exchange the public values $g^a$ and $g^b$ over an open channel and each computes the shared secret $g^{ab}$, which an eavesdropper who sees only $g$, $g^a$, and $g^b$ cannot compute.
&lt;p&gt;{&lt;code&gt;// Toy Diffie-Hellman over a tiny prime field. Illustrative sizes, NOT secure. const p = 2147483647n;   // a small 31-bit prime; a real DH prime is &amp;gt;= 3072 bits const g = 7n;            // a public generator element function powmod(base, exp, m){   let r = 1n; base %= m;   while (exp &amp;gt; 0n){ if (exp &amp;amp; 1n) r = (r * base) % m; base = (base * base) % m; exp &amp;gt;&amp;gt;= 1n; }   return r; } const a = 1234567n;             // Alice&apos;s fresh ephemeral secret (never sent) const b = 7654321n;             // Bob&apos;s fresh ephemeral secret   (never sent) const A = powmod(g, a, p);      // Alice sends this over the open wire const B = powmod(g, b, p);      // Bob sends this over the open wire const secretAlice = powmod(B, a, p);  // Alice computes (g^b)^a const secretBob   = powmod(A, b, p);  // Bob   computes (g^a)^b console.log(&apos;Alice g^ab =&apos;, secretAlice.toString()); console.log(&apos;Bob   g^ab =&apos;, secretBob.toString()); console.log(&apos;do they agree? -&amp;gt;&apos;, secretAlice === secretBob); // The raw shared value is a group element, NOT a key. Real code never uses it // directly: it feeds g^ab into a KDF (HKDF-SHA256) to extract a uniform key. const toyKey = (secretAlice * 2654435761n) % (2n ** 32n); // stand-in for HKDF only console.log(&apos;derived key (toy stand-in) =&apos;, toyKey.toString());&lt;/code&gt;}&lt;/p&gt;
&lt;h3&gt;Why it is hard to break&lt;/h3&gt;
&lt;p&gt;The security rests on the &lt;strong&gt;discrete logarithm problem&lt;/strong&gt;: given $g$ and $g^a$, recover the exponent $a$. In a well-chosen group this is believed intractable.&lt;/p&gt;

Given a generator $g$ and an element $g^a$ in a large group, the problem of recovering the exponent $a$. It is believed hard in carefully chosen groups, and it is the foundation Diffie-Hellman rests on.
&lt;p&gt;But hardness of the discrete log is not quite the property we need. Recovering $a$ is the discrete-log problem; &lt;em&gt;computing the shared secret&lt;/em&gt; $g^{ab}$ from $g^a$ and $g^b$ without recovering either exponent is the &lt;strong&gt;Computational Diffie-Hellman (CDH)&lt;/strong&gt; assumption, a slightly weaker thing to assume hard. Sharper still: &quot;the shared secret is indistinguishable from a random group element&quot; is the &lt;strong&gt;Decisional Diffie-Hellman (DDH)&lt;/strong&gt; assumption, and it is &lt;em&gt;strictly stronger&lt;/em&gt; than CDH [@cryptobook]. The distance between &quot;hard to compute&quot; and &quot;looks random&quot; is exactly why the raw output of the exchange is not yet a key.&lt;/p&gt;

CDH is the assumption that computing $g^{ab}$ from $g^a$ and $g^b$ is hard. DDH is the stronger assumption that $g^{ab}$ cannot even be distinguished from a random group element. DDH can fail in groups where CDH still holds, which is why the raw shared value is not treated as a uniform key.
&lt;p&gt;There are real groups -- pairing-friendly elliptic curves -- where DDH is &lt;em&gt;easy&lt;/em&gt; even though CDH is plausibly hard, so $g^{ab}$ is computable-hard yet distinguishable from random.This is why &quot;looks random&quot; is the &lt;em&gt;decisional&lt;/em&gt; assumption, not the computational one. The one-line consequence, paid off in the forward-secrecy and theory sections, is that you must extract a key from the shared value with a KDF rather than use it raw. Keep that fact in your pocket; it becomes a theorem later.&lt;/p&gt;
&lt;h3&gt;The thesis, stated as the primitive&apos;s interface&lt;/h3&gt;
&lt;p&gt;Now look at what the exchange quietly assumes but never enforces. Raw finite-field Diffie-Hellman outsources all of the following to the caller, at once:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Freshness.&lt;/strong&gt; The secret $a$ must be genuinely fresh per session, then destroyed. This is what buys forward secrecy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A strong, unshared group.&lt;/strong&gt; The group must be large enough and not shared in a way that lets an attacker amortize work across everyone who uses it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A validated peer value.&lt;/strong&gt; The incoming $g^b$ must actually be a well-formed element of the right group, not a hostile input chosen to leak your secret.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication.&lt;/strong&gt; Something must bind $g^a$ and $g^b$ to identities, or an active attacker sits in the middle.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And beneath all four, a fifth discipline: the raw $g^{ab}$ is a structured group element, not a key, so it must go through a &lt;a href=&quot;https://paragmali.com/blog/one-secret-is-not-one-key-the-discipline-of-key-derivation-w/&quot; rel=&quot;noopener&quot;&gt;key derivation function&lt;/a&gt; before anyone calls it a key [@rfc5869]. The original 1976 construction supplies &lt;em&gt;none&lt;/em&gt; of these; it hands you a shared secret and trusts you to remember the rest [@dh1976].&lt;/p&gt;

flowchart TD
    A[&quot;Alice: fresh secret a, send g^a&quot;] --&amp;gt; S[&quot;Shared value g^(ab)&quot;]
    B[&quot;Bob: fresh secret b, send g^b&quot;] --&amp;gt; S
    S --&amp;gt; K[&quot;KDF turns g^(ab) into a real key&quot;]
    F1[&quot;1. Freshness: is a truly ephemeral and destroyed&quot;] -.-&amp;gt; A
    F2[&quot;2. Strong and unshared group&quot;] -.-&amp;gt; S
    F3[&quot;3. Validate the peer value g^b&quot;] -.-&amp;gt; B
    F4[&quot;4. Authenticate the exchange&quot;] -.-&amp;gt; S
&lt;p&gt;This table is the entire article compressed into a grid: each row a condition, each column showing who historically lifted that burden off the caller. It fills in from left (the caller does everything by hand) to right (the primitive, the named group, or the protocol does it).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;What the caller must supply&lt;/th&gt;
&lt;th&gt;Where it eventually gets pulled inside&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1. Freshness&lt;/td&gt;
&lt;td&gt;A per-session ephemeral secret, destroyed after use&lt;/td&gt;
&lt;td&gt;The protocol: TLS 1.3 mandates ephemeral (EC)DHE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Strong, unshared group&lt;/td&gt;
&lt;td&gt;A large prime or curve nobody can precompute against&lt;/td&gt;
&lt;td&gt;The named group: RFC 7919 FFDHE safe primes; strong curves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Validated peer value&lt;/td&gt;
&lt;td&gt;An on-curve, correct-subgroup public value&lt;/td&gt;
&lt;td&gt;The primitive: X25519 twist security; or a hand-written check for classic ECDH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Authentication&lt;/td&gt;
&lt;td&gt;A signature or MAC binding the transcript to identities&lt;/td&gt;
&lt;td&gt;The protocol: STS/SIGMA, then TLS 1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. KDF discipline&lt;/td&gt;
&lt;td&gt;Extract a uniform key from the raw shared value&lt;/td&gt;
&lt;td&gt;Every modern design: HKDF over the shared secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The security a system gets from Diffie-Hellman is never &quot;the discrete log is hard.&quot; It is four conditions the caller must supply -- fresh, strong-and-unshared, validated, authenticated -- and then a KDF beneath them. Every named break in this article is exactly one of those quietly missing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The raw $g^{ab}$ (or, on a curve, the shared x-coordinate) is a group element, not a key. Run it through a KDF such as HKDF before you ever call it a key. This is the discipline beneath all four conditions, and later it turns out to be a theorem, not a style preference.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Three of those four conditions were not obvious in 1976. Each was discovered the hard way, by a named break. To understand why a fifty-year-old idea still hands them to you, go back to the moment key agreement was only an idea -- an idea that, it turns out, had already been discovered in secret.&lt;/p&gt;
&lt;h2&gt;3. Where This Came From: 1976, and the Secret That Came First&lt;/h2&gt;
&lt;p&gt;In 1976 two Stanford researchers opened a paper with a sentence that has aged remarkably well: &quot;We stand today on the brink of a revolution in cryptography&quot; [@dh1976]. Whitfield Diffie and Martin Hellman were right about the revolution. They were also, unknown to them, second.&lt;/p&gt;
&lt;h3&gt;The problem before the answer&lt;/h3&gt;
&lt;p&gt;Before public key agreement, two parties who wanted to talk securely had to already share a secret key, delivered out of band -- by a trusted courier, a locked briefcase, a prior meeting. That does not scale. A network of $n$ parties who each want to talk privately needs on the order of $n^2$ pairwise keys, every one of them distributed and stored in advance. The cost of moving keys, not the cost of encryption, is what throttled commercial cryptography for decades.&lt;/p&gt;
&lt;h3&gt;The public origin&lt;/h3&gt;
&lt;p&gt;Diffie and Hellman&apos;s &quot;New Directions in Cryptography&quot; broke the deadlock. Their key exchange let two parties agree on a shared secret over a channel an adversary could fully observe: fix a public generator $g$ over a finite field, keep the exponents private, exchange $g^a$ and $g^b$, and let both sides compute $g^{ab}$, with security resting on the discrete logarithm modulo a large prime [@dh1976]. The same paper credits Ralph Merkle&apos;s independent &quot;puzzles&quot; idea as a partial solution to the same problem, which is the seed of the argument -- pressed for years by Hellman -- that the scheme should be called Diffie-Hellman-Merkle [@dh1976], [@merkle]. Merkle&apos;s own framing was that his construction &quot;forces any enemy to expend an amount of work which increases as the square of the work required of the two communicants&quot; [@cacm-merkle].&lt;/p&gt;
&lt;p&gt;Merkle conceived his puzzles around 1974 and saw a 1975 write-up rejected; the work was not published until 1978. Yet Diffie and Hellman&apos;s 1976 paper already cites it as &quot;submitted to the CACM,&quot; which fixed the idea in the literature before its own publication -- part of why Hellman argued for the three-name credit [@merkle].&lt;/p&gt;
&lt;h3&gt;The classified precursor&lt;/h3&gt;
&lt;p&gt;Here is the part most textbooks skip. Inside the United Kingdom&apos;s signals-intelligence agency, GCHQ, and its information-security arm CESG, the same ideas had already arrived, and stayed locked in a safe. James Ellis conceived what he called &quot;non-secret encryption&quot; at the end of the 1960s -- the recipient, he realized, only needs to be in a &lt;em&gt;different position&lt;/em&gt; from the interceptor, not a secret one [@ellis]. Clifford Cocks worked out the trapdoor that we would now recognize as RSA in 1973, and Malcolm Williamson derived the discrete-exponentiation key exchange -- Diffie-Hellman in substance -- in 1974 [@wikipedia-cocks]. All of it remained classified until December 1997 [@wikipedia-cocks].&lt;/p&gt;
&lt;p&gt;The counterfactual sharpens the whole thesis of this article. The &lt;em&gt;idea&lt;/em&gt; of public key agreement existed inside a government agency and changed nothing about how the world built systems, because it was never published, never standardized, never turned into a named group or a protocol anyone could deploy. An idea that no caller can reach protects no one.&lt;/p&gt;
&lt;p&gt;The GCHQ dates are genuinely disputed. Ellis&apos;s own account traces the origin only to &quot;the 60&apos;s&quot;; the widely repeated &quot;1970&quot; is the date of his internal report. The primary CESG and gchq.gov.uk pages that once hosted this history are gone, so the record here rests on the archived Ellis document plus corroborating biography, not on any live government URL [@ellis], [@wikipedia-cocks].&lt;/p&gt;

timeline
    title From a secret idea to a mandated protocol
    1969 : Ellis conceives non-secret encryption inside GCHQ
    1974 : Williamson derives DH-style key exchange, kept classified
    1976 : Diffie and Hellman publish New Directions, finite-field DH
    1985 : Miller proposes elliptic-curve cryptography (Koblitz independently, published 1987)
    1992 : Station-to-Station signs the DH values for authentication
    2006 : Bernstein publishes Curve25519 and the X25519 function
    2015 : Logjam shows shared 1024-bit primes are a mass liability
    2018 : TLS 1.3 mandates ephemeral key exchange, removes static DH
    2024 : X25519MLKEM768 hybrid ships by default in OpenSSH 9.9
&lt;p&gt;Diffie and Hellman had shown that two strangers could agree on a secret against an eavesdropper who only &lt;em&gt;listens&lt;/em&gt;. They said almost nothing about an adversary who &lt;em&gt;acts&lt;/em&gt;. That omission is not a flaw in the paper; it is the second of the four conditions, and closing it took another two decades.&lt;/p&gt;
&lt;h2&gt;4. The Anonymous-DH Gap: Why Raw DH Is Only Half a Protocol&lt;/h2&gt;
&lt;p&gt;Raw ephemeral Diffie-Hellman defeats a passive eavesdropper &lt;em&gt;completely&lt;/em&gt;, and an &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;active attacker&lt;/a&gt; &lt;em&gt;not at all&lt;/em&gt;. The whole difference is one adversary who can inject packets, and it is the entire reason &quot;just do a DH exchange&quot; is never the whole answer.&lt;/p&gt;
&lt;h3&gt;The man-in-the-middle, mechanically&lt;/h3&gt;
&lt;p&gt;Suppose an attacker sits on the wire and can not only read but rewrite. When Alice sends $g^a$ toward Bob, the attacker keeps it and sends its own $g^m$ onward. When Bob replies with $g^b$, the attacker keeps that too and sends $g^m$ back to Alice. Now the attacker shares one secret, $g^{am}$, with Alice, and a &lt;em&gt;different&lt;/em&gt; secret, $g^{bm}$, with Bob. It decrypts whatever Alice sends, reads or modifies it, re-encrypts under the Bob-side secret, and forwards. Neither party sees anything wrong: both completed a textbook-perfect exchange. The primitive simply never promised anything about &lt;em&gt;who&lt;/em&gt; is on the other end.&lt;/p&gt;

sequenceDiagram
    participant A as Alice
    participant M as Attacker
    participant B as Bob
    A-&amp;gt;&amp;gt;M: sends g^a intended for Bob
    M-&amp;gt;&amp;gt;B: substitutes g^m
    B-&amp;gt;&amp;gt;M: sends g^b intended for Alice
    M-&amp;gt;&amp;gt;A: substitutes g^m
    Note over A,M: Alice agrees on secret g^am with the attacker
    Note over M,B: Bob agrees on a different secret g^bm
    Note over M: Attacker decrypts, reads, re-encrypts both directions
&lt;p&gt;This is a design gap, not an implementation bug. It falls to an &lt;em&gt;active&lt;/em&gt; adversary specifically; a passive listener still learns nothing.&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;// Toy anonymous-DH MITM. Illustrative sizes, NOT secure. const p = 2147483647n, g = 7n; function powmod(base, exp, m){   let r = 1n; base %= m;   while (exp &amp;gt; 0n){ if (exp &amp;amp; 1n) r = (r*base) % m; base = (base*base) % m; exp &amp;gt;&amp;gt;= 1n; }   return r; } const a = 111111n, b = 999999n;   // honest ephemeral secrets const A = powmod(g, a, p);        // Alice&apos;s message (intercepted) const B = powmod(g, b, p);        // Bob&apos;s message   (intercepted) const m = 424242n;                // the attacker&apos;s own secret const M = powmod(g, m, p);        // the attacker forwards THIS to both sides const aliceSide = powmod(M, a, p);   // Alice computes (g^m)^a const attackerA = powmod(A, m, p);   // attacker computes (g^a)^m  -&amp;gt; equal const bobSide   = powmod(M, b, p);   // Bob computes (g^m)^b const attackerB = powmod(B, m, p);   // attacker computes (g^b)^m  -&amp;gt; equal console.log(&apos;Alice-attacker secret agrees?&apos;, aliceSide === attackerA); console.log(&apos;Bob-attacker secret agrees?  &apos;, bobSide   === attackerB); console.log(&apos;Do Alice and Bob share a key?&apos;, aliceSide === bobSide); // false // The attacker now holds both session secrets and relays, reading everything.&lt;/code&gt;}&lt;/p&gt;

A key-agreement protocol that also binds the exchanged public values to identities, so that an active attacker cannot substitute its own values undetected. It is what raw Diffie-Hellman is missing.
&lt;h3&gt;The precision that carries the rest of the article&lt;/h3&gt;
&lt;p&gt;Authentication defeats the &lt;em&gt;active&lt;/em&gt; adversary, and only the active adversary. It is orthogonal to the hardness of the group and to input validation. The sections ahead each violate a &lt;em&gt;different&lt;/em&gt; condition, and it is tempting but wrong to think a signature would have saved them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Authentication closes the man-in-the-middle, and only that. It does not make a weak prime strong, an unvalidated point safe, or a reused ephemeral fresh. Those are three other conditions, each with its own catalog of breaks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The fix, historically&lt;/h3&gt;
&lt;p&gt;Naming the goal came first. Christoph Guenther&apos;s 1989 identity-based key-exchange work is where the term &lt;em&gt;perfect forward secrecy&lt;/em&gt; is generally said to first appear, attached to the goal that a compromised long-term key should not expose past sessions [@gunther1989]. Diffie, van Oorschot, and Wiener turned it into a concrete protocol in 1992 with &lt;strong&gt;Station-to-Station&lt;/strong&gt;, which authenticates the exchange by having each side &lt;em&gt;sign&lt;/em&gt; the Diffie-Hellman values, and which gave the standard, most-cited formal treatment of the property [@sts1992]. A decade later Hugo Krawczyk distilled the pattern into &lt;strong&gt;SIGMA&lt;/strong&gt;, the &quot;SIGn-and-MAc&quot; template that underlies IKE and, in spirit, the TLS 1.3 handshake; the SIGMA protocols &quot;provide perfect forward secrecy&quot; via a Diffie-Hellman exchange authenticated with &lt;a href=&quot;https://paragmali.com/blog/the-math-held-the-interface-leaked-a-field-guide-to-digital-/&quot; rel=&quot;noopener&quot;&gt;digital signatures&lt;/a&gt; [@sigma2003]. The authenticated-DH family is treated as standard material in the reference texts [@cryptobook], [@hac].&lt;/p&gt;
&lt;p&gt;The coinage is disputed and worth stating carefully: the &lt;em&gt;term&lt;/em&gt; &quot;perfect forward secrecy&quot; traces to Guenther (1989/1990), while Diffie, van Oorschot, and Wiener (1992) gave the standard, most-cited formal treatment via Station-to-Station. Present both and pick neither silently [@gunther1989], [@sts1992].&lt;/p&gt;
&lt;p&gt;Signing the DH values closes the man-in-the-middle. It does nothing for the &lt;em&gt;group&lt;/em&gt; the exchange runs in -- and the original group, the multiplicative integers modulo a prime, turned out to hand the caller two more conditions at once, plus a nation-state-scale way to violate them.&lt;/p&gt;
&lt;h2&gt;5. Generation 1: Finite-Field DH and the Tyranny of the Shared Prime&lt;/h2&gt;
&lt;p&gt;Here is the mechanism underneath Logjam, the small-subgroup CVEs, and those 3072-bit key sizes you have wondered about -- and here is why the original finite-field group hands the caller more traps than any generation since.&lt;/p&gt;
&lt;h3&gt;Finite-field DH, mechanically&lt;/h3&gt;
&lt;p&gt;Pick a large prime $p$, ideally a &lt;strong&gt;safe prime&lt;/strong&gt; of the form $p = 2q + 1$ with $q$ also prime, and a generator $g$ of the large prime-order subgroup. Alice and Bob exchange $g^a \bmod p$ and $g^b \bmod p$ and compute $g^{ab} \bmod p$. The safe-prime shape is not cosmetic: it forces the cofactor down to 2, leaving only the trivial order-two subgroup (the values $1$ and $p-1$). So a small-subgroup attack can pin a key to at most one bit, which is exactly why an implementation must still reject $0$, $1$, and $p-1$ -- a point that becomes concrete below.&lt;/p&gt;

A prime of the form $p = 2q + 1$ where $q$ is also prime. The multiplicative group modulo a safe prime has one large prime-order subgroup of order $q$ and only the trivial order-two subgroup (the values $1$ and $p-1$), a cofactor of 2. That confines any small-subgroup attack to at most one bit, which is why implementations still reject $0$, $1$, and $p-1$.
&lt;h3&gt;The cost that shaped everything&lt;/h3&gt;
&lt;p&gt;Finite-field discrete logs have a &lt;em&gt;sub-exponential&lt;/em&gt; attack. Index calculus, and its modern form the &lt;strong&gt;number field sieve (NFS)&lt;/strong&gt;, runs in heuristic complexity&lt;/p&gt;
&lt;p&gt;$$L_p[1/3,\ (64/9)^{1/3}] = \exp!\left((1.923 + o(1)),(\ln p)^{1/3}(\ln\ln p)^{2/3}\right),$$&lt;/p&gt;
&lt;p&gt;far faster than brute force. The practical consequence: to reach roughly 128-bit security you need a &lt;strong&gt;3072-bit&lt;/strong&gt; prime, an order of magnitude larger than the 256-bit curve that buys the same margin [@sp80057]. That size penalty is the whole reason finite-field DH lost.&lt;/p&gt;

The best known sub-exponential algorithm for factoring and for finite-field discrete logarithms. Its most expensive stage depends only on the modulus (the prime), not on any individual target, which is why a shared prime is a shared weakness.
&lt;h3&gt;Logjam, in full&lt;/h3&gt;
&lt;p&gt;That &quot;depends only on the prime&quot; clause is the entire disaster. The NFS&apos;s expensive first step &quot;is dependent only on this prime,&quot; so if millions of hosts share a handful of 1024-bit primes -- and in 2015 they did -- an adversary precomputes once and then decrypts every connection on that prime cheaply, passively, and indefinitely [@weakdh]. The Logjam researchers estimated that breaking the single most common 1024-bit prime would expose about 18% of the top million HTTPS sites, and a second common prime about 66% of VPNs and 26% of SSH servers [@weakdh]. And there was an &lt;em&gt;active&lt;/em&gt; half: an attacker could downgrade a TLS handshake to 512-bit &lt;code&gt;DHE_EXPORT&lt;/code&gt; parameters, which were breakable in near-real time, with 8.4% of the top million initially vulnerable [@weakdh].&lt;/p&gt;
&lt;p&gt;The paper priced that precomputation directly, and the numbers are the point. Breaking a 512-bit export prime was &lt;em&gt;demonstrated&lt;/em&gt;, not merely estimated: about 89,000 core-hours of work -- &quot;slightly over one week&quot; of wall-clock time spread across a couple of thousand cores -- after which each individual discrete log took a median of 70 seconds [@weakdh]. A 768-bit prime was an academic reach at roughly 36,500 core-years. And a 1024-bit prime was a nation-state one at roughly 45 million core-years of one-time precomputation, a figure the authors judged &quot;not necessarily out of reach for a nation state&quot; [@weakdh]. Every number there is a &lt;em&gt;fixed, per-prime&lt;/em&gt; cost that reading one more connection barely adds to.&lt;/p&gt;

flowchart TD
    P[&quot;One shared 1024-bit prime&quot;] --&amp;gt; NFS[&quot;NFS first step: precompute per prime, about 45M core-years at 1024-bit, done once&quot;]
    NFS --&amp;gt; DB[&quot;Reusable precomputation for that prime&quot;]
    DB --&amp;gt; D[&quot;Per-connection descent: about 70 s at 512-bit&quot;]
    D --&amp;gt; R[&quot;Passively decrypt every session on that prime&quot;]
    P -.-&amp;gt; X[&quot;Active downgrade to 512-bit export group&quot;]
    X --&amp;gt; R
&lt;p&gt;Notice what never happens in that diagram: nobody solves a fresh discrete logarithm. The costly work is done once, against the &lt;em&gt;prime&lt;/em&gt;, and amortized across everyone who shares it.&lt;/p&gt;

Practitioners &quot;believed this was safe as long as new key exchange messages were generated for every connection&quot; -- and were wrong.
&lt;p&gt;They were wrong because a fresh per-connection secret satisfies the &lt;em&gt;freshness&lt;/em&gt; condition while doing nothing about the &lt;em&gt;unshared-group&lt;/em&gt; condition. Both had to hold; only one did [@weakdh].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The attacker never computed a fresh discrete logarithm. They precomputed the shared prime once and read everyone on it. The math held; the deployment handed the secret away.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The RSA twin, in one breath&lt;/h3&gt;
&lt;p&gt;Logjam did not travel alone. The same year, and the same relic of 1990s U.S. export-crypto policy, produced its RSA counterpart: &lt;strong&gt;FREAK&lt;/strong&gt;, for &quot;Factoring RSA Export Keys,&quot; publicly disclosed on 3 March 2015 by Karthikeyan Bhargavan and the miTLS team, with the client-side flaw tracked as CVE-2015-0204 [@freak]. A man-in-the-middle downgrades a client to 512-bit export RSA, factors that small modulus cheaply on rented hardware, and reads the session. At disclosure 36.7% of HTTPS servers with browser-trusted certificates still accepted export RSA -- falling to 6.5% as patches landed -- along with 8.5% of the Alexa Top-1M [@freak].&lt;/p&gt;
&lt;p&gt;FREAK downgrades &lt;em&gt;RSA key transport&lt;/em&gt; and is fundamentally an &lt;strong&gt;implementation&lt;/strong&gt; bug, a client-side state-machine slip that accepts a key it never asked for; Logjam downgrades &lt;em&gt;Diffie-Hellman parameters&lt;/em&gt; and is, in the weakdh.org authors&apos; own words, &quot;due to a flaw in the TLS protocol rather than an implementation vulnerability&quot; [@weakdh]. RSA key transport is &lt;a href=&quot;https://paragmali.com/blog/rsa-is-a-trapdoor-not-a-cryptosystem-oaep-pss-and-the-25-yea/&quot; rel=&quot;noopener&quot;&gt;the RSA installment&lt;/a&gt;&apos;s territory, so FREAK stays a mention -- but it is proof that the export-downgrade wound had two matching edges, one for each key-exchange family.&lt;/p&gt;
&lt;p&gt;Two precisions to keep straight. First, Logjam is &lt;em&gt;two&lt;/em&gt; coupled failures, not one: the 512-bit export &lt;em&gt;downgrade&lt;/em&gt; and the shared-prime &lt;em&gt;precomputation&lt;/em&gt;. Second, FREAK is its RSA sibling, not the same attack -- both weaponize leftover 512-bit export keys, but FREAK is an implementation bug in RSA key transport while Logjam is a protocol flaw in Diffie-Hellman parameter negotiation [@weakdh], [@freak].&lt;/p&gt;
&lt;h3&gt;Small-subgroup confinement&lt;/h3&gt;
&lt;p&gt;The safe-prime rule has teeth. If $p$ is &lt;em&gt;not&lt;/em&gt; a safe prime, the group has small subgroups, and an attacker can weaponize them. The attacker sends a carefully chosen element of small order $r$ as its &quot;public key.&quot; The victim raises it to its private exponent $x$, producing an element that depends only on $x \bmod r$, and the attacker -- watching whether the handshake succeeds, or reading the resulting ciphertext -- recovers $x \bmod r$ by trying the $r$ possibilities. Repeat across several small factors and combine with the Chinese Remainder Theorem, and the private exponent falls out.&lt;/p&gt;
&lt;p&gt;This is the &lt;strong&gt;small-subgroup confinement attack&lt;/strong&gt; of Lim and Lee (CRYPTO &apos;97), and it works only when the victim &lt;em&gt;reuses&lt;/em&gt; the private exponent long enough to be probed [@limlee1997], [@twist]. It went live as &lt;strong&gt;CVE-2016-0701&lt;/strong&gt;, where OpenSSL failed to run the subgroup check on X9.42-style (RFC 5114) non-safe-prime parameters while reusing a DH key, letting an attacker &quot;discover a private DH exponent&quot; [@openssl-cve], [@nvd-0701].&lt;/p&gt;

An attack that sends a small-order group element as a public key so that the victim&apos;s response depends only on the private exponent reduced modulo a small number. Repeating across several small factors and applying the Chinese Remainder Theorem recovers a reused private exponent.
&lt;p&gt;{&lt;code&gt;// A NON-safe prime: p is prime but p-1 = 2^3 * 13 * 19 * 53. Insecure sizes. const p = 104729n; const g = 12n;              // a generator of the full multiplicative group function powmod(b, e, m){ let r = 1n; b %= m; while (e &amp;gt; 0n){ if (e &amp;amp; 1n) r = (r*b)%m; b = (b*b)%m; e &amp;gt;&amp;gt;= 1n; } return r; } const x = 8675n;           // Bob&apos;s REUSED private exponent -- the fatal choice const smalls = [13n, 19n, 53n];   // small prime factors of p-1 const residues = []; for (const r of smalls){   const h = powmod(g, (p - 1n) / r, p);   // an element of small order r   const target = powmod(h, x, p);         // the victim returns h^x = h^(x mod r)   let e = 0n;   for (let k = 0n; k &amp;lt; r; k++){ if (powmod(h, k, p) === target){ e = k; break; } }  // tiny search   residues.push([r, e]);   console.log(&apos;recovered x mod &apos; + r + &apos; = &apos; + e); } // Combine the residues with the Chinese Remainder Theorem: let Mtot = 1n; for (const r of smalls) Mtot *= r; let X = 0n; for (const [r, e] of residues){   const Mi = Mtot / r; let inv = 1n;   for (let k = 1n; k &amp;lt; r; k++){ if ((Mi * k) % r === 1n){ inv = k; break; } }   X = (X + e * Mi * inv) % Mtot; } console.log(&apos;reconstructed private exponent x = &apos; + X + &apos;  (true value &apos; + x + &apos;)&apos;);&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;Keep this attack distinct from the one in the next section. This is small-subgroup confinement: &lt;em&gt;one&lt;/em&gt; group, a &lt;em&gt;low-order element&lt;/em&gt;, Lim-Lee 1997, on finite fields. The invalid-curve attack of Section 6 uses &lt;em&gt;many&lt;/em&gt; curves and an &lt;em&gt;off-curve point&lt;/em&gt;, Biehl-Meyer-Mueller 2000, on elliptic curves. They are routinely conflated and they are not the same [@twist].&lt;/p&gt;
&lt;h3&gt;The fix: take &quot;which prime&quot; away from the caller&lt;/h3&gt;
&lt;p&gt;The response was to stop letting callers choose primes. RFC 7919 defines a fixed menu of named FFDHE groups (&lt;code&gt;ffdhe2048&lt;/code&gt; through &lt;code&gt;ffdhe8192&lt;/code&gt;), every one a safe prime, so a custom, shared, weak, or backdoored modulus is simply not expressible on the wire [@rfc7919]. That is condition #2 pulled inside a &lt;em&gt;named group&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Why take the choice away so completely? Because a caller-chosen modulus can fail in a way no inspection reveals. In early 2016 the &lt;strong&gt;socat&lt;/strong&gt; networking tool was found to have shipped a hard-coded 1024-bit Diffie-Hellman &lt;code&gt;p&lt;/code&gt; parameter that &lt;em&gt;was not prime&lt;/em&gt; at all, making a key exchange weaker than one over a genuine prime [@socat-secadv7]. The advisory (MSVR-1499) is worth quoting for its candor: because &quot;there is no indication of how these parameters were chosen, the existence of a trapdoor ... cannot be ruled out&quot; [@socat-secadv7].&lt;/p&gt;
&lt;p&gt;The repair generated a fresh 2048-bit prime with OpenSSL&apos;s &lt;code&gt;dhparam&lt;/code&gt;, and the flaw was reported by Santiago Zanella-Beguelin -- a Logjam co-author -- together with Microsoft Vulnerability Research [@socat-secadv7]. A bring-your-own modulus can be weak &lt;em&gt;or&lt;/em&gt; backdoored, and you cannot tell which by looking at it. That single degree of freedom is exactly what named safe-prime groups take away.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Do not generate or ship a custom finite-field DH prime, and never one below 2048 bits. A custom modulus can be weak or even backdoored and you cannot tell by looking -- in 2016 socat shipped a 1024-bit &quot;prime&quot; that was not prime at all. Use a named safe-prime group (&lt;code&gt;ffdhe3072&lt;/code&gt; or larger), or, better for most new work, move to a curve.&lt;/p&gt;
&lt;/blockquote&gt;

The number field sieve has an expensive precomputation stage whose cost &quot;is dependent only on this prime,&quot; followed by a cheap per-target descent [@weakdh]. So the marginal cost of reading one more connection on a broken prime is tiny; the fixed cost is paid once. When a handful of 1024-bit primes cover a large fraction of the internet, that fixed cost buys mass surveillance: the single most common prime makes roughly 18% of top HTTPS sites readable, and a second prime roughly 66% of VPNs [@weakdh].&lt;p&gt;The Logjam paper put a concrete price on the 1024-bit precomputation, and it lands squarely in nation-state territory. Roughly 45 million core-years of one-time work is &quot;a huge computational effort,&quot; the authors wrote, but &quot;not necessarily out of reach for a nation state&quot;: about $8 million of custom ASIC hardware would buy enough capacity to finish the sieving stage in a year, and the linear-algebra hardware was estimated &quot;plausibly on the order of hundreds of millions of dollars&quot; -- on general-purpose machines, the Titan supercomputer&apos;s cores would need 117 years for that stage alone [@weakdh].&lt;/p&gt;
&lt;p&gt;Set that against the roughly $10.5 billion the paper cites for the NSA&apos;s 2012 consolidated cryptologic-program budget, and the arithmetic is uncomfortable [@weakdh]. As a public, &lt;em&gt;measured&lt;/em&gt; front-door datapoint for the same algorithm one size down, the RSA-250 factoring record fell to the number field sieve in February 2020 at a cost of about 2,700 core-years, so the 1024-bit discrete-log estimate extrapolates a demonstrated technique rather than a hypothetical one [@rsa250].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Two conditions leaked at once here: the group was neither strong-per-bit nor unshared, and a reused key turned the exchange into an oracle. The obvious escape is a group where the discrete log is &lt;em&gt;harder per bit&lt;/em&gt; and there is no cheap shared-prime precomputation. That group is an elliptic curve -- and it introduced a sharper oracle of its own.&lt;/p&gt;
&lt;h2&gt;6. Generation 2: Elliptic-Curve DH, and the New Oracle&lt;/h2&gt;
&lt;p&gt;Move the &lt;em&gt;same&lt;/em&gt; Diffie-Hellman protocol onto an elliptic curve and the number field sieve vanishes. A 256-bit key now buys what a 3072-bit prime bought. But the smaller, structure-free group sharpened one of the four conditions into a live, key-recovering oracle.&lt;/p&gt;
&lt;h3&gt;ECDH, mechanically&lt;/h3&gt;
&lt;p&gt;The curve internals belong to &lt;a href=&quot;https://paragmali.com/blog/the-curve-was-hard-the-gap-was-soft-a-field-guide-to-using-e/&quot; rel=&quot;noopener&quot;&gt;the elliptic-curves installment of this series&lt;/a&gt;; here we only need the shape. Replace &quot;exponentiate $g$&quot; with &quot;scalar-multiply the base point $G$&quot;: a private key is a scalar $d$, the public key is the point $Q = dG$, and the shared secret is the point $d_A d_B G$, from which both sides take the x-coordinate [@sp80056a]. The payoff is in the attack cost. On a strong curve the best known attack is the &lt;em&gt;generic&lt;/em&gt; Pollard rho algorithm, running in about $\sqrt{n}$ steps with no sub-exponential index calculus available, so a 256-bit curve delivers roughly 128-bit security -- concretely, rho against P-256 costs about $2^{127.8}$ operations [@rho]. Victor Miller and Neal Koblitz independently proposed elliptic-curve cryptography in the mid-1980s for exactly this reason [@miller1985], [@koblitz1987].&lt;/p&gt;
&lt;h3&gt;The new failure class&lt;/h3&gt;
&lt;p&gt;The smaller group came with a sharper edge. The peer&apos;s point is attacker-controlled, and the standard scalar-multiplication formulas &quot;do not involve the constant coefficient $b$&quot; of the curve equation [@twist]. So if an attacker submits a point that satisfies everything &lt;em&gt;except&lt;/em&gt; lying on the intended curve, the victim&apos;s arithmetic processes it without complaint -- effectively doing the computation on a &lt;em&gt;different, weaker&lt;/em&gt; curve whose group has small subgroups. Scalar-multiplying such a point by a &lt;em&gt;reused&lt;/em&gt; private key turns the operation into a key-extraction oracle, one small residue at a time, combined with the Chinese Remainder Theorem across many curves. This is the &lt;strong&gt;invalid-curve attack&lt;/strong&gt; of Biehl, Meyer, and Mueller (CRYPTO 2000) [@bmm2000], [@twist]. Jager, Schwenk, and Somorovsky made it practical against real TLS servers doing static or reused ECDH at ESORICS 2015, recovering the server&apos;s private key [@jss2015].&lt;/p&gt;

An attack that submits a point lying on a different, weaker curve than the intended one. Because scalar-multiplication formulas ignore the curve&apos;s constant term, the victim computes on the wrong curve and, if it reuses its private key, leaks that key one small residue at a time.

flowchart TD
    A[&quot;Attacker sends a point not on the intended curve&quot;] --&amp;gt; B[&quot;Scalar multiply ignores the curve constant b&quot;]
    B --&amp;gt; C[&quot;Computation lands in a weak subgroup of another curve&quot;]
    C --&amp;gt; D[&quot;Reused key leaks d modulo a small order&quot;]
    D --&amp;gt; E[&quot;Repeat across many curves, then CRT to the full key d&quot;]
&lt;p&gt;This is the place readers most often blur two attacks, so once more, at the point of maximum confusion: Lim-Lee small-subgroup confinement (Section 5) is finite-field, one group, a low-order element; Biehl-Meyer-Mueller invalid-curve (here) is elliptic-curve, many curves, an off-curve point. And the practical TLS demonstration is Jager-Schwenk-Somorovsky&apos;s ESORICS 2015 paper, which is distinct from the same authors&apos; separate USENIX 2015 work [@twist], [@jss2015].&lt;/p&gt;
&lt;h3&gt;The lesson, made mandatory&lt;/h3&gt;
&lt;p&gt;For classic cofactor ECDH the caller &lt;em&gt;must&lt;/em&gt; validate every incoming point: it has to lie on the intended curve &lt;em&gt;and&lt;/em&gt; in the correct prime-order subgroup. NIST SP 800-56A codifies this as full or partial public-key validation [@sp80056a]. It is an explicit, easy-to-forget check the smaller group pushed back onto the implementer -- exactly the caller-facing condition this article tracks.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Reject any peer point that is not on the curve and not in the prime-order subgroup, every time. Miss the check once, on a server that reuses its key, and you have built a private-key-recovery oracle.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A related but separate ECC failure is worth one pointer so you do not file it under the wrong heading: Curveball (CVE-2020-0601) was a certificate-&lt;em&gt;validation&lt;/em&gt; bug in Windows CryptoAPI, not an ECDH key-agreement break [@nvd-0601]. Same family of &quot;validate the parameter you were handed,&quot; different layer.&lt;/p&gt;
&lt;p&gt;So ECDH shrank the key but still demanded a hand-written on-curve check and constant-time code that an implementer could botch. The next move makes both of those &lt;em&gt;unnecessary by construction&lt;/em&gt; -- a curve where every 32-byte string is a valid input and the arithmetic is constant-time by design.&lt;/p&gt;
&lt;h2&gt;7. Generation 3: X25519, or &quot;Make the Simple Implementation the Safe One&quot;&lt;/h2&gt;
&lt;p&gt;Everything classic ECDH asked the caller to remember -- validate the point, write the arithmetic in constant time -- one curve was designed to make impossible to get wrong. That is the whole point of X25519.&lt;/p&gt;
&lt;h3&gt;What X25519 is&lt;/h3&gt;
&lt;p&gt;X25519 is the Diffie-Hellman function over Curve25519, a Montgomery curve over the prime field $p = 2^{255} - 19$, computing &lt;strong&gt;x-coordinate-only&lt;/strong&gt; DH with a uniform &lt;strong&gt;Montgomery ladder&lt;/strong&gt;. Daniel Bernstein introduced it in 2006, and it was standardized as RFC 7748 in 2016 [@ecdh], [@rfc7748]. Two design decisions are the entire construction, and each one pulls a caller-facing condition inside the primitive.&lt;/p&gt;

A scalar-multiplication algorithm that processes the secret scalar in a fixed sequence of steps, performing the same field operations at every bit regardless of the secret&apos;s value. It computes only the x-coordinate of the result and has no secret-dependent branches.
&lt;h3&gt;Constant-time by construction&lt;/h3&gt;
&lt;p&gt;The ladder is a fixed sequence of roughly 255 iterations, each doing the same field operations no matter what the secret bits are: no secret-dependent branch, no secret-indexed table lookup [@rfc7748]. The timing side channel that a hand-written P-256 ladder can leak simply has no place to live. The primitive absorbed the constant-time requirement so the implementer cannot forget it.&lt;/p&gt;
&lt;h3&gt;Twist-security&lt;/h3&gt;
&lt;p&gt;The second decision is subtler and more beautiful. Curve25519 and its quadratic twist &lt;em&gt;both&lt;/em&gt; have near-prime order, so an attacker who submits an off-curve x-coordinate does not land in a weak subgroup -- it lands in &lt;em&gt;another&lt;/em&gt; strong group where there is nothing to confine a key into. The invalid-curve attack of Section 6 finds no purchase, and it needs no explicit on-curve check to stop it. That is why &quot;the Curve25519 function was carefully designed to allow all 32-byte strings as Diffie-Hellman public keys,&quot; and why Bernstein&apos;s guidance on validating public keys is, bluntly, &quot;Don&apos;t&quot; [@ecdh], [@twist].&lt;/p&gt;

A property of a curve whose quadratic twist also has near-prime order. An attacker who supplies an off-curve x-coordinate is forced into another strong group instead of a weak subgroup, so invalid-curve attacks fail without any explicit point validation.

flowchart TD
    In[&quot;Any 32-byte peer value, trusted or hostile&quot;] --&amp;gt; L[&quot;Montgomery ladder: same steps for every secret bit&quot;]
    L --&amp;gt; Tw[&quot;An off-curve input lands on the twist, another strong group&quot;]
    Tw --&amp;gt; Out[&quot;No weak subgroup to confine a key: validation is structural&quot;]
    L --&amp;gt; Ct[&quot;No secret-dependent branch or lookup: the timing channel is closed&quot;]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; X25519 is not merely a faster curve. It takes two conditions that classic ECDH left on the implementer -- validate the input, run in constant time -- and makes them structural properties of the primitive. The safety is by construction, not by discipline. That is the shape of the entire history in miniature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The two rules that trip people up&lt;/h3&gt;
&lt;p&gt;Two details cause most X25519 confusion.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;clamping&lt;/strong&gt;. Before use, the secret scalar is masked with &lt;code&gt;k[0] &amp;amp;= 248; k[31] &amp;amp;= 127; k[31] |= 64&lt;/code&gt; [@rfc7748]. Clamping constrains &lt;em&gt;your own&lt;/em&gt; scalar so your side avoids cofactor and small-order pitfalls. It is emphatically &lt;strong&gt;not&lt;/strong&gt; input validation, and it does nothing to a malicious peer value. X25519 is safe against hostile inputs because of &lt;em&gt;twist security&lt;/em&gt;, not because of clamping.&lt;/p&gt;

The bit-masking applied to an X25519 secret scalar before use, which fixes certain high and low bits. It shapes your own scalar to sidestep cofactor issues, and it performs no check whatsoever on the peer&apos;s public value.
&lt;p&gt;Second, the &lt;strong&gt;cofactor-8 all-zero check&lt;/strong&gt;. Curve25519 has cofactor 8, so a low-order peer input can drive the shared output to the all-zero value. RFC 7748 says implementations &quot;MAY check ... whether the resulting shared secret is the all-zero value and abort if so&quot; -- a MAY, tied to the nuance of contributory behavior, not a MUST [@rfc7748]. Most protocols do not need it because they mix identities and transcripts into the KDF anyway.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Clamping constrains your own scalar; it says nothing about the peer&apos;s value. If you find yourself reasoning that clamping makes an incoming point safe, stop -- the safety comes from twist security. Treat clamping and validation as different jobs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A precision that catches even careful readers: X25519 and its larger sibling X448 are the &lt;em&gt;Montgomery, x-coordinate-only&lt;/em&gt; variant of elliptic-curve DH, not the SP 800-56A cofactor ECDH of Section 6, even though &quot;ECDH&quot; is the umbrella term for both. X448 targets a more conservative security level near 224 bits and is defined in the same RFC 7748 [@rfc7748].&lt;/p&gt;
&lt;p&gt;Even here the KDF discipline is stated in the primitive&apos;s own voice: &quot;Both of you can then hash this shared secret and use the result as a key,&quot; never the raw x-coordinate [@ecdh].&lt;/p&gt;
&lt;p&gt;X25519 makes the &lt;em&gt;primitive&lt;/em&gt; misuse-resistant: validated by construction, constant-time by design. It says nothing about the other two conditions -- freshness and authentication -- which remain the protocol&apos;s to enforce. And the first of those, freshness, is the exact hinge on which the property everyone wants from Diffie-Hellman either holds or silently evaporates.&lt;/p&gt;
&lt;h2&gt;8. The Breakthrough: Ephemeral Exchange and (Imperfect) Forward Secrecy&lt;/h2&gt;
&lt;p&gt;This is the property that makes people say &quot;use DHE, not static,&quot; and the one most often stated as if it were automatic. It is not. It is a conjunction, and the failure catalog is its list of unmet conjuncts.&lt;/p&gt;
&lt;h3&gt;Forward secrecy, stated conditionally&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;If&lt;/strong&gt; the Diffie-Hellman secret is freshly generated per session, never reused, destroyed after use, and run over a strong unshared group, &lt;strong&gt;then&lt;/strong&gt; later compromise of the long-term authentication key cannot recover past session keys -- because the ephemeral secret those keys were derived from no longer exists. The long-term key&apos;s only job is to &lt;em&gt;authenticate&lt;/em&gt; the exchange; the session&apos;s confidentiality dies with the ephemeral. Every word of that conditional is load-bearing. Drop &quot;never reused&quot; and you get a static key in disguise; drop &quot;strong unshared group&quot; and Logjam reads you anyway.&lt;/p&gt;

The guarantee that compromising a long-term key does not expose past session keys. It holds only when the per-session ephemeral secret was freshly generated, never reused, destroyed after use, and run over a strong, unshared group. It is a conjunction of those conditions, not an automatic consequence of using DH.

An ephemeral DH key is generated fresh for a single session and discarded immediately after. A static key is long-lived and reused across sessions, providing no forward secrecy. A cached or reused &quot;ephemeral&quot; is, for security purposes, a static key.

flowchart TD
    E[&quot;Fresh ephemeral secret, used then destroyed&quot;] --&amp;gt; H[&quot;Handshake, DH values signed by the long-term key&quot;]
    LT[&quot;Long-term key authenticates only&quot;] --&amp;gt; H
    H --&amp;gt; K[&quot;Session key derived from the ephemeral&quot;]
    K --&amp;gt; D[&quot;Ephemeral destroyed when the session ends&quot;]
    C[&quot;Attacker later steals the long-term key&quot;] --&amp;gt; P[&quot;Past sessions stay secret: the ephemeral is gone&quot;]
    ST[&quot;Static DH: one reused key does everything&quot;] --&amp;gt; O[&quot;Its compromise opens every past session at once&quot;]
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Fresh per session?&lt;/th&gt;
&lt;th&gt;Forward secrecy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Static DH&lt;/td&gt;
&lt;td&gt;No -- one long-lived key&lt;/td&gt;
&lt;td&gt;None; a single key compromise opens all past sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reused or cached &quot;ephemeral&quot;&lt;/td&gt;
&lt;td&gt;No -- shared across sessions&lt;/td&gt;
&lt;td&gt;Collapses for every session that shares it; also enables Raccoon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;True ephemeral&lt;/td&gt;
&lt;td&gt;Yes -- destroyed after use&lt;/td&gt;
&lt;td&gt;Yes, conditional on a strong unshared group&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;The honest other half: how the property fails&lt;/h3&gt;
&lt;p&gt;The conditional fails in three documented ways, and each is a row in the catalog.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ephemeral-key reuse or caching.&lt;/strong&gt; Sold as a performance optimization, reusing an ephemeral turns it into a static key and collapses forward secrecy for every session that shares it. Worse, reuse is the precondition for &lt;strong&gt;Raccoon&lt;/strong&gt; (CVE-2020-1968): in TLS 1.2 and earlier, the premaster secret had its leading zero bytes stripped before the key derivation step, so the exact timing of the KDF leaked the most significant bits of the shared secret, recoverable as an instance of the Hidden Number Problem -- but &lt;em&gt;only&lt;/em&gt; when the server reused its DH key long enough to gather timing samples [@raccoon].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Shared-prime precomputation.&lt;/strong&gt; This is Logjam again, from the other direction: even a genuinely fresh per-connection ephemeral does not save you if the &lt;em&gt;prime&lt;/em&gt; is shared and precomputed [@weakdh]. It is the reason the paper is titled &lt;em&gt;Imperfect Forward Secrecy&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static DH.&lt;/strong&gt; No ephemeral at all, so no forward secrecy at all -- the baseline failure that TLS 1.3 later abolished [@rfc8446].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Caching or reusing a DH &quot;ephemeral&quot; collapses forward secrecy for every session that shares it, and reuse is exactly the condition Raccoon needs to mount its timing oracle. If it is not fresh and destroyed, it is not ephemeral.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Raccoon authors are blunt about the root condition: reusing a DH key &quot;was already considered bad practice before the attack, as it does not provide forward secrecy&quot; [@raccoon]. The timing oracle was a second penalty on top of the forward-secrecy loss.&lt;/p&gt;
&lt;h3&gt;The discipline beneath it all&lt;/h3&gt;
&lt;p&gt;Why must the raw shared value always be hashed? Because a Diffie-Hellman value &quot;is NOT a uniformly random or pseudorandom string&quot; -- it is a structured group element with detectable bias -- so it must be run through an extract-and-expand KDF before it becomes key material [@rfc5869]. This is the formal echo of the CDH-versus-DDH gap from Section 2, now stated as a standards requirement.&lt;/p&gt;

A Diffie-Hellman value &quot;is NOT a uniformly random or pseudorandom string.&quot; -- RFC 5869, Section 3.3

It is easy to over-claim what forward secrecy buys. It bounds the reach of a *future* compromise: an attacker who steals your long-term key tomorrow cannot decrypt the sessions you completed and destroyed today.&lt;p&gt;It does nothing about the present. An attacker who owns your endpoint &lt;em&gt;during&lt;/em&gt; a live session, or who grabs the ephemeral before you delete it, reads that session in full. Nor does it provide &lt;em&gt;post-compromise&lt;/em&gt; security -- the guarantee that sessions &lt;em&gt;after&lt;/em&gt; a compromise heal once the attacker loses access. That healing is a different property, achieved by ratcheting constructions such as the one in the Signal protocol, and it is out of scope for a single DH exchange. Forward secrecy is a statement about deleted secrets, and only about the past.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Freshness, a strong group, validated inputs, authentication, and a KDF -- five things, and the caller was trusted to remember all of them. The last move in this story stops trusting the caller: it bakes the conditions into named objects and a protocol that refuses to run without them.&lt;/p&gt;
&lt;h2&gt;9. State of the Art (2024-2026): X25519 Everywhere, and the Quantum Clock&lt;/h2&gt;
&lt;p&gt;Here is what a practitioner actually deploys today -- and the single overlay that reframes all of it: every discrete-log DH in this article has an expiration date, and a well-funded adversary can start the clock by simply recording your traffic now.&lt;/p&gt;
&lt;h3&gt;The deployed reality&lt;/h3&gt;
&lt;p&gt;X25519 is the dominant key agreement across TLS 1.3, OpenSSH, WireGuard, and Signal, and the word &quot;dominant&quot; now rests on measured deployment rather than defaults alone. Cloudflare, which terminates a large fraction of the world&apos;s HTTPS and can therefore see it, reports that X25519 &quot;now secures the vast majority of all Internet connections&quot; [@cloudflare-pq]; OpenSSH has shipped Curve25519 key exchange as &quot;the default when both the client and server support it&quot; since version 6.5, back in 2014 [@openssh65], [@ecdh]; and WireGuard and Signal are built on the same primitive [@wireguard], [@pqxdh]. Finite-field DH survives mainly as deprecated-but-present legacy in older TLS and IPsec [@weakdh], [@rfc8446].&lt;/p&gt;
&lt;p&gt;TLS 1.3 turned this whole article&apos;s argument into a requirement: RFC 8446 removed static RSA and static Diffie-Hellman key transport, requires ephemeral (EC)DHE, and restricts negotiation to named groups. The specification states the result directly: &quot;Static RSA and Diffie-Hellman cipher suites have been removed; all public-key based key exchange mechanisms now provide forward secrecy&quot; [@rfc8446]. That is conditions #1, #2, and #4 -- freshness, a vetted group, and authentication -- pulled inside the &lt;em&gt;protocol&lt;/em&gt; as non-optional. The fifty-year move is complete: the four conditions now live in the primitive, the named group, and the protocol, not in the caller&apos;s memory.&lt;/p&gt;
&lt;h3&gt;The quantum overlay&lt;/h3&gt;
&lt;p&gt;There is one problem no key size solves. All discrete-log Diffie-Hellman, finite-field and elliptic-curve alike, falls in polynomial time to Shor&apos;s algorithm on a sufficiently large quantum computer, and &quot;harvest-now, decrypt-later&quot; means an adversary can record your ECDH traffic today and decrypt it once such a machine exists [@rfc9794]. So the field is &lt;em&gt;wrapping&lt;/em&gt; ECDH rather than replacing it.&lt;/p&gt;

An attack strategy in which an adversary records encrypted traffic today and stores it, intending to decrypt it later once a cryptographically relevant quantum computer exists. It makes today&apos;s ECDH confidentiality a future liability.
&lt;p&gt;The wrapper is &lt;strong&gt;X25519MLKEM768&lt;/strong&gt;: it runs an X25519 exchange and an &lt;a href=&quot;https://paragmali.com/blog/the-thirty-year-migration-ships-in-a-pip-install-how-post-qu/&quot; rel=&quot;noopener&quot;&gt;ML-KEM-768&lt;/a&gt; key encapsulation in parallel and feeds &lt;em&gt;both&lt;/em&gt; shared secrets into the KDF, so the session stays secure as long as &lt;em&gt;either&lt;/em&gt; problem remains hard [@ecdhe-mlkem], [@fips203]. Concretely, the client key share is 1216 bytes (1184 for ML-KEM plus 32 for X25519), the server share is 1120 bytes, the negotiated combined secret is 64 bytes, and the TLS codepoint is &lt;code&gt;0x11EC&lt;/code&gt; [@ecdhe-mlkem].&lt;/p&gt;

A public-key primitive in which a sender encapsulates a fresh random key to a recipient&apos;s public key, producing a ciphertext the recipient decapsulates. Unlike Diffie-Hellman, a KEM is inherently interactive and has no non-interactive agreement between two static public keys.
&lt;p&gt;The transition was fast. &lt;strong&gt;OpenSSH 9.9&lt;/strong&gt;, released on 2024-09-19, enabled &lt;code&gt;mlkem768x25519-sha256&lt;/code&gt; by default [@openssh99]. &lt;strong&gt;OpenSSL 3.5&lt;/strong&gt;, a long-term-support release from April 2025, ships a DEFAULT TLS group list that &quot;selects X25519MLKEM768 as one of the predicted keyshares&quot; [@openssl35-release], [@openssl-groups]. Chrome and Cloudflare deployed post-quantum key agreement across their edges [@cloudflare-pq].&lt;/p&gt;

The classical primitives do not vanish. FFDHE and P-256 persist wherever FIPS validation or vendor interop compels them, and FIPS-flavored hybrids such as `SecP256r1MLKEM768` exist for exactly that constraint [@sp80056a]. There is also a plumbing wrinkle: the roughly 1.2 KB hybrid ClientHello may no longer fit in a single TCP segment, which OpenSSL&apos;s documentation calls out along with guidance on ordering group tuples to avoid a silent downgrade [@openssl-groups].
&lt;h3&gt;Two precisions that keep the story honest&lt;/h3&gt;
&lt;p&gt;First, a KEM is &lt;strong&gt;not&lt;/strong&gt; a drop-in Diffie-Hellman. It has no non-interactive static-static agreement, so &lt;code&gt;X25519MLKEM768&lt;/code&gt; is a &lt;em&gt;hybrid KEM handshake&lt;/em&gt;, not &quot;post-quantum Diffie-Hellman.&quot; Second, in messaging, Signal&apos;s &lt;strong&gt;PQXDH&lt;/strong&gt; (2023) is the analog: it adds an ML-KEM encapsulation to the older X3DH design but &quot;still relies on the hardness of the discrete log problem for mutual authentication in this revision,&quot; so X25519 is retained, not retired [@pqxdh], [@x3dh]. Signal described it as &quot;an upgrade to the X3DH specification which we are calling PQXDH&quot; [@pqxdh-blog].&lt;/p&gt;
&lt;p&gt;State the adoption figure carefully. Cloudflare measured post-quantum key agreement at nearly 2% of TLS 1.3 connections in early 2024, with double-digit adoption &lt;em&gt;projected&lt;/em&gt; by year-end rather than measured -- so &quot;roughly 2% in early 2024, rising&quot; is the defensible phrasing without a newer source [@cloudflare-pq].&lt;/p&gt;
&lt;p&gt;X25519 is the default, TLS 1.3 made the discipline mandatory, and the hybrid buys time against Shor. So which primitive, with which parameters, in which case? That is a decision matrix -- and it is not a single winner.&lt;/p&gt;
&lt;h2&gt;10. Competing Approaches: The Decision Matrix and the AKE Families&lt;/h2&gt;
&lt;p&gt;There is no single best key-agreement primitive. The state of the art is a small set of deployed constructions, each dominant in a niche, wrapped by a small set of authentication families. The honest answer is &quot;it depends.&quot;&lt;/p&gt;
&lt;h3&gt;The primitive decision matrix&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Best classical attack&lt;/th&gt;
&lt;th&gt;Classical security&lt;/th&gt;
&lt;th&gt;Post-quantum&lt;/th&gt;
&lt;th&gt;Public value size&lt;/th&gt;
&lt;th&gt;Peer-input validation&lt;/th&gt;
&lt;th&gt;Best suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;FFDHE-3072&lt;/td&gt;
&lt;td&gt;Sub-exponential NFS&lt;/td&gt;
&lt;td&gt;about 128-bit&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;about 384 bytes&lt;/td&gt;
&lt;td&gt;Subgroup check on a safe prime&lt;/td&gt;
&lt;td&gt;Legacy FIPS or interop only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECDH P-256&lt;/td&gt;
&lt;td&gt;Generic rho, about $2^{127.8}$&lt;/td&gt;
&lt;td&gt;about 128-bit&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;65 bytes&lt;/td&gt;
&lt;td&gt;Hand-written on-curve and subgroup MUST&lt;/td&gt;
&lt;td&gt;FIPS, HSM, existing deployments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X25519&lt;/td&gt;
&lt;td&gt;Generic rho, about $2^{125.8}$&lt;/td&gt;
&lt;td&gt;about 128-bit&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;32 bytes&lt;/td&gt;
&lt;td&gt;Structural, via twist security&lt;/td&gt;
&lt;td&gt;New classical designs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X25519MLKEM768&lt;/td&gt;
&lt;td&gt;Generic rho plus lattice attacks on ML-KEM&lt;/td&gt;
&lt;td&gt;about 128-bit and post-quantum&lt;/td&gt;
&lt;td&gt;Yes, if either half holds&lt;/td&gt;
&lt;td&gt;about 1.2 KB (client share)&lt;/td&gt;
&lt;td&gt;X25519 structural plus KEM ciphertext check&lt;/td&gt;
&lt;td&gt;The post-quantum default going forward&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two rows carry the weight. &quot;Best classical attack&quot; separates FFDHE&apos;s sub-exponential tax from the curves&apos; generic-only attack, which is why a curve needs a quarter of the bits [@rho], [@sp80057]. &quot;Peer-input validation&quot; separates a hand-written MUST for P-256 and a subgroup check for FFDHE from the structurally-unnecessary case of X25519 [@sp80056a], [@rfc7748]. The head-to-head rules fall out cleanly: &lt;strong&gt;X25519 for new designs; ECDHE with P-256 or FFDHE at 3072 bits and up only where FIPS or interop compels it; and ephemeral, always, for forward secrecy.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;The authentication families&lt;/h3&gt;
&lt;p&gt;A primitive is only half a deployment; something must authenticate the exchange. Four families cover essentially all real usage.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family&lt;/th&gt;
&lt;th&gt;How DH is authenticated&lt;/th&gt;
&lt;th&gt;Deployed in&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Signed-DH&lt;/td&gt;
&lt;td&gt;Each side signs the transcript or the DH values&lt;/td&gt;
&lt;td&gt;TLS 1.3, IKEv2, STS and SIGMA&lt;/td&gt;
&lt;td&gt;Simple and well understood; needs a PKI or pinned keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implicitly-authenticated DH&lt;/td&gt;
&lt;td&gt;Long-term and ephemeral keys combine inside the shared-secret formula&lt;/td&gt;
&lt;td&gt;MQV and HMQV, parts of IKE&lt;/td&gt;
&lt;td&gt;Compact, no signatures; subtle to get exactly right&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DH-pattern frameworks&lt;/td&gt;
&lt;td&gt;Compose several static and ephemeral DH operations&lt;/td&gt;
&lt;td&gt;Noise, WireGuard&apos;s Noise_IK&lt;/td&gt;
&lt;td&gt;Mutual auth plus identity hiding plus KCI resistance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Asynchronous AKE&lt;/td&gt;
&lt;td&gt;Prekeys let a recipient be offline&lt;/td&gt;
&lt;td&gt;Signal&apos;s X3DH and PQXDH&lt;/td&gt;
&lt;td&gt;Deniable and offline-friendly; more moving parts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Signed-DH dominates the open web through TLS 1.3, whose handshake is SIGMA in spirit [@rfc8446], [@sigma2003]. WireGuard composes several X25519 operations in the Noise_IK pattern to get mutual authentication, forward secrecy, key-compromise-impersonation resistance, and identity hiding at once [@wireguard]. Signal&apos;s X3DH and its post-quantum successor PQXDH deliver asynchronous, deniable key agreement to an offline recipient [@x3dh], [@pqxdh].&lt;/p&gt;
&lt;h3&gt;The composition hazard&lt;/h3&gt;
&lt;p&gt;One more lesson hides in those families, and it is easy to miss: sub-protocols each proven secure &lt;em&gt;in isolation&lt;/em&gt; can still compose into an insecure whole. The &lt;strong&gt;Triple Handshake attack&lt;/strong&gt; (Bhargavan, Delignat-Lavaud, Fournet, Pironti, and Strub, IEEE Symposium on Security and Privacy 2014) chained a TLS key exchange with session resumption and renegotiation so that two distinct connections could be driven to share the same master secret, which broke client authentication and defeated the countermeasures of the day [@triple-handshake].&lt;/p&gt;
&lt;p&gt;Paired with &lt;strong&gt;key-compromise impersonation&lt;/strong&gt; -- where stealing your long-term key lets an attacker impersonate &lt;em&gt;others&lt;/em&gt; to you -- it is the standing reminder that a secure handshake is not simply the sum of secure steps. It is also the concrete reason TLS 1.3 binds the entire transcript: the &lt;code&gt;Finished&lt;/code&gt; MAC covers the whole handshake, so two runs that differ anywhere cannot be coerced into agreeing [@rfc8446].&lt;/p&gt;
&lt;p&gt;The Triple Handshake attack (2014) and key-compromise impersonation are adjacent AKE-composition pitfalls, not breaks of the signed-DH template itself. They are why TLS 1.3 authenticates the full handshake transcript rather than individual messages [@triple-handshake].&lt;/p&gt;
&lt;h3&gt;Signposts, not sections&lt;/h3&gt;
&lt;p&gt;Several approaches deserve a pointer rather than a treatment. &lt;strong&gt;MQV and HMQV&lt;/strong&gt; are the implicitly-authenticated DH family, with narrow deployment in some IKE and Suite B profiles [@sp80056a]. &lt;strong&gt;DH-based PAKEs and OPRFs&lt;/strong&gt; -- CPace, OPAQUE, SPAKE2 -- use Diffie-Hellman for password-authenticated and oblivious key agreement and are the subject of &lt;a href=&quot;https://paragmali.com/blog/the-doorway-into-the-group-a-field-guide-to-hashing-a-string/&quot; rel=&quot;noopener&quot;&gt;the hash-to-curve and PAKE installment of this series&lt;/a&gt;. &lt;strong&gt;CSIDH&lt;/strong&gt; is the isogeny-based candidate for a genuinely post-quantum Diffie-Hellman analog, and its cautionary neighbor &lt;strong&gt;SIDH/SIKE&lt;/strong&gt; was broken outright in 2022 [@sike-break]. &lt;strong&gt;ElGamal&lt;/strong&gt; is DH&apos;s public-key encryption cousin, not a key-agreement scheme. And &lt;strong&gt;X448&lt;/strong&gt; is the conservative higher-security sibling of X25519. Each is one sentence here by design.&lt;/p&gt;
&lt;p&gt;Every row of those tables assumes the underlying hardness holds. That assumption has a precise shape on each track -- and one hard ceiling above both.&lt;/p&gt;
&lt;h2&gt;11. Theoretical Limits: Where the Hardness Lives, and Its Ceiling&lt;/h2&gt;
&lt;p&gt;Here is what a first course skips: the two Diffie-Hellman tracks do not have the &lt;em&gt;same kind&lt;/em&gt; of hardness, the raw shared secret is provably not a good key, and every scheme in this article dies to the same quantum algorithm.&lt;/p&gt;
&lt;h3&gt;Finite-field DLP is sub-exponential&lt;/h3&gt;
&lt;p&gt;The general number field sieve solves finite-field discrete logs in $L_p[1/3,\ (64/9)^{1/3}]$, and its first step depends only on the prime -- the engine behind Logjam. Because the attack is sub-exponential rather than exponential, key sizes must grow quickly to stay ahead: 3072 bits for 128-bit security, 7680 for 192-bit [@weakdh], [@sp80057]. The Logjam paper&apos;s own field estimates trace that curve concretely: a 512-bit prime falls in near-real time after a one-week, 89,000-core-hour precomputation (a median of 70 seconds per subsequent log); a 768-bit prime is an academic reach at about 36,500 core-years; and a shared 1024-bit prime is a standing nation-state liability at about 45 million core-years of one-time precomputation [@weakdh]. As an independent &lt;em&gt;measured&lt;/em&gt; anchor one size down, the RSA-250 factoring record fell to the number field sieve in February 2020 at about 2,700 core-years [@rsa250].&lt;/p&gt;
&lt;h3&gt;Elliptic-curve DLP is generic square-root, and no better&lt;/h3&gt;
&lt;p&gt;For a strong curve the best known attack is generic Pollard rho at about $0.886\sqrt{\ell}$ operations, and -- this is the deep part -- that &lt;em&gt;matches&lt;/em&gt; Shoup&apos;s generic lower bound of $\Omega(\sqrt{\ell})$ [@shoup1997]. For a well-chosen curve the gap between the best attack and the proven floor is &lt;em&gt;closed&lt;/em&gt;: Curve25519 sits at about $2^{125.8}$ and P-256 at about $2^{127.8}$ [@rho]. The only escapes are specific bad families -- pairing (MOV and Frey-Ruck) reductions and anomalous curves -- which is why rigidity and vetted parameters matter, and which the elliptic-curves installment of this series treats in full.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target security&lt;/th&gt;
&lt;th&gt;Finite-field DH prime&lt;/th&gt;
&lt;th&gt;Elliptic curve (rho cost)&lt;/th&gt;
&lt;th&gt;Best classical attack&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;3072-bit&lt;/td&gt;
&lt;td&gt;Curve25519 about $2^{125.8}$, P-256 about $2^{127.8}$&lt;/td&gt;
&lt;td&gt;Sub-exponential NFS vs generic rho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192-bit&lt;/td&gt;
&lt;td&gt;7680-bit&lt;/td&gt;
&lt;td&gt;P-384 about $2^{191.8}$&lt;/td&gt;
&lt;td&gt;Sub-exponential NFS vs generic rho&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Rho also amortizes across targets: breaking $T$ keys at once costs only about $\sqrt{T}$ times a single break, the elliptic-curve echo of Logjam&apos;s shared-prime amortization, and another reason to prefer a generous subgroup order [@rho].&lt;/p&gt;
&lt;h3&gt;The CDH-DDH gap is why the KDF is a theorem&lt;/h3&gt;
&lt;p&gt;The assumptions form a ladder, but mind the direction: an algorithm that solves the left solves the right, so DLP is at least as hard as CDH, which is at least as hard as DDH (read instead as &lt;em&gt;hardness assumptions&lt;/em&gt;, the implication runs the other way, with DDH-hardness implying CDH-hardness implying DLP-hardness). That last gap is real and not merely conservative. On pairing-friendly curves, DDH is &lt;em&gt;easy&lt;/em&gt; while CDH is plausibly still hard, so the raw $g^{ab}$ can be distinguished from uniform even when it cannot be computed by an attacker [@cryptobook]. That is precisely why RFC 5869 forbids skipping the extract step and insists the shared value be run through a KDF before use [@rfc5869]. Condition #5 is not a style preference; it is a consequence of a proven gap between two assumptions.&lt;/p&gt;
&lt;h3&gt;Forward secrecy&apos;s precise limits&lt;/h3&gt;
&lt;p&gt;Stated formally, forward secrecy protects the past, not the present or future, and only under the conjunction from Section 8: the ephemeral must have been ephemeral and the group strong. Its unmet conjuncts are, exactly, the failure catalog.&lt;/p&gt;
&lt;h3&gt;The ceiling: Shor&lt;/h3&gt;
&lt;p&gt;Above both tracks sits one algorithm that does not care which group you chose. Shor&apos;s algorithm solves discrete logarithms -- finite-field and elliptic-curve -- in polynomial time on a sufficiently large quantum computer, and no key size escapes it [@rfc9794]. This is the one break in the entire article that &lt;em&gt;is&lt;/em&gt; a break of the math rather than a leaked condition.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every other failure in this article was a leaked condition -- a shared prime, an unvalidated point, a reused ephemeral, a missing signature. Shor is the only one that breaks the math itself. That is why the answer to Shor is a new problem, ML-KEM wrapped around X25519, and not simply a bigger curve.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the theory is this settled -- a closed generic gap on curves, a sub-exponential tax on fields, a proven quantum cliff above both -- then what is left is genuinely open. And what is open is not the math. It is the interface, again.&lt;/p&gt;
&lt;h2&gt;12. Open Problems: What Remains Genuinely Unsettled&lt;/h2&gt;
&lt;p&gt;The discrete log is not the frontier. The frontier is what &quot;post-quantum Diffie-Hellman&quot; even means, whether constant-time can ever be a settled property, and how much broken cryptography is still in the field.&lt;/p&gt;
&lt;h3&gt;A practical post-quantum NIKE&lt;/h3&gt;
&lt;p&gt;Diffie-Hellman is a &lt;strong&gt;non-interactive key exchange&lt;/strong&gt;: two parties who have each merely &lt;em&gt;published&lt;/em&gt; a static public key can agree on a shared secret with no further interaction, and that single property is what makes asynchronous and static-static designs -- X3DH, Noise&apos;s static-static patterns -- possible. A KEM cannot do this. It is inherently interactive: one side encapsulates to the other&apos;s key and returns a ciphertext, so there is no agreement between two published static keys [@ecdhe-mlkem].&lt;/p&gt;

A scheme in which two parties agree on a shared secret purely from each other&apos;s published static public keys, with no live message exchange. Diffie-Hellman is a NIKE; a KEM is not, which is why KEMs cannot directly replace DH in static-static and asynchronous designs.
&lt;p&gt;The only serious candidate for a genuinely post-quantum NIKE is the isogeny scheme &lt;strong&gt;CSIDH&lt;/strong&gt; (Castryck, Lange, Martindale, Panny, and Renes, ASIACRYPT 2018), a commutative supersingular-isogeny group action that offers Diffie-Hellman&apos;s exact static-static, non-interactive interface. Its appeal is size: the paper advertises &quot;public keys of only 64 bytes at a conjectured AES-128 security level, matching NIST&apos;s post-quantum security category I&quot; [@csidh]. But that quantum-security claim is contested. Bonnetain and Schrottenloher analyzed the underlying quantum abelian-hidden-shift attack and concluded that &quot;the parameters proposed by the authors of CSIDH do not meet their expected quantum security&quot; (EUROCRYPT 2020), a finding Peikert reached independently the same year [@bs2020], [@peikert2020].&lt;/p&gt;
&lt;p&gt;The consensus repair is to grow the parameters: the SQALE authors set out to &quot;optimize large CSIDH parameters ... while still achieving the NIST security levels 1, 2, and 3,&quot; which pushes the prime to 1024 bits and beyond, with correspondingly slower group actions [@sqale]. And CSIDH&apos;s close relative &lt;strong&gt;SIDH/SIKE&lt;/strong&gt; was broken outright in 2022, when Castryck and Decru recovered keys against SIKEp434 &quot;in about ten minutes on a single core&quot; [@sike-break]. That is a reminder that young isogeny assumptions can fall catastrophically, and it is why today&apos;s hybrids &lt;em&gt;sidestep&lt;/em&gt; the missing PQ-NIKE rather than solve it [@pqxdh].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; &quot;Post-quantum Diffie-Hellman&quot; is not a solved problem. A KEM is not a drop-in for DH&apos;s non-interactive interface, and the closest true analog -- 64-byte CSIDH -- buys much less quantum security than its key size advertises, so restoring the intended level pushes it to far larger, far slower parameters. The hybrid buys time; it does not resurrect the NIKE.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Deniability versus post-quantum authentication&lt;/h3&gt;
&lt;p&gt;Diffie-Hellman gives messaging protocols a property signatures cannot: &lt;strong&gt;deniability&lt;/strong&gt;. Because a session secret in X3DH is derived from DH values rather than a signature, neither party can later prove to a third party &lt;em&gt;who&lt;/em&gt; they talked to -- the transcript could have been produced by either side alone [@x3dh]. That is a deliberate feature, and it is in tension with the post-quantum migration. Signal&apos;s PQXDH ships post-quantum &lt;em&gt;confidentiality&lt;/em&gt; first, but in its own words it &quot;provides post-quantum forward secrecy ... but still relies on the hardness of the discrete log problem for mutual authentication in this revision&quot; [@pqxdh]. The reason it keeps a DH operation for the authentication step is precisely the open problem: the obvious post-quantum fix -- a post-quantum signature -- is inherently &lt;em&gt;non-deniable&lt;/em&gt;, and a deniable post-quantum authentication scheme is not yet a settled, deployed design.&lt;/p&gt;
&lt;p&gt;This is why PQXDH retains a discrete-log DH operation for mutual authentication even after adding ML-KEM for confidentiality: post-quantum confidentiality has shipped ahead of post-quantum authentication, and the deniability that DH provides has no drop-in post-quantum replacement yet [@pqxdh].&lt;/p&gt;
&lt;h3&gt;Constant-time as a moving target&lt;/h3&gt;
&lt;p&gt;X25519&apos;s ladder removes the timing channel at the &lt;em&gt;primitive&lt;/em&gt; level, but Raccoon showed the same class of leak can live one layer up, in the &lt;em&gt;protocol&lt;/em&gt;, when a premaster secret has its leading zeros stripped before hashing -- and it recurs specifically when a DH key is reused [@raccoon]. Formally-verified constant-time implementations are the structural answer, and they are not yet universal [@rfc7748].&lt;/p&gt;
&lt;h3&gt;The legacy long tail&lt;/h3&gt;
&lt;p&gt;This is a deployment problem, not a math problem. 1024-bit FFDHE, shared and custom primes, static DH, and DHE on TLS 1.2 all persist in the field years after the fixes shipped, and Logjam&apos;s precomputation economics make those &lt;em&gt;shared&lt;/em&gt; 1024-bit primes a standing mass-decryption risk [@weakdh]. RFC 7919&apos;s named groups, the TLS 1.3 ephemeral mandate, and browsers dropping DHE have shrunk the surface, but the migration is incomplete [@rfc7919], [@rfc8446].&lt;/p&gt;
&lt;h3&gt;Hybrid-combiner and downgrade robustness&lt;/h3&gt;
&lt;p&gt;The entire value of &lt;code&gt;X25519MLKEM768&lt;/code&gt; is &quot;secure if &lt;em&gt;either&lt;/em&gt; half holds,&quot; and that guarantee is only as good as the combiner and the negotiation around it: a flawed combiner, or an attacker who forces a downgrade to the weaker half, voids it. RFC 9794 standardizes the post-quantum/traditional vocabulary and OpenSSL&apos;s group-tuple syntax guards the ordering, but end-to-end downgrade-resistance proofs for the deployed handshakes are still being worked out [@ecdhe-mlkem], [@rfc9794], [@openssl-groups].&lt;/p&gt;
&lt;p&gt;Flagged as reasoning, not documented fact: by analogy to every generation before it, the &lt;em&gt;next&lt;/em&gt; wave of post-quantum breaks will most likely again be interface leaks -- bad randomness, unvalidated ciphertexts, non-constant-time decapsulation -- rather than breaks of the lattice problem itself. No source documents a failure that has not yet happened; this is an inference from the pattern of the previous five generations.&lt;/p&gt;
&lt;p&gt;If the open problems are mostly about the interface, then so is the guidance. Here is fifty years of failure compressed into rules you can apply in a design review on Monday.&lt;/p&gt;
&lt;h2&gt;13. The Practical Guide, and the One Sentence Underneath&lt;/h2&gt;
&lt;p&gt;Every rule below is one row of the failure catalog, inverted. If you remember nothing else, remember the diagnostic question, and the four conditions it audits.&lt;/p&gt;
&lt;h3&gt;Decision rules: use X, with parameters Y, in case Z&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New key agreement, general purpose:&lt;/strong&gt; X25519, ephemeral [@rfc7748], [@ecdh].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Post-quantum-hardened (the recommended default going forward):&lt;/strong&gt; X25519MLKEM768 [@ecdhe-mlkem], [@openssl35-release], [@openssh99].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FIPS, regulated, or HSM-interop:&lt;/strong&gt; ECDHE with P-256 or P-384, ephemeral, with full public-key validation; or the FIPS hybrid &lt;code&gt;SecP256r1MLKEM768&lt;/code&gt; [@sp80056a], [@ecdhe-mlkem].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legacy finite-field interop only:&lt;/strong&gt; FFDHE &lt;code&gt;ffdhe3072&lt;/code&gt; or larger, ephemeral, with the subgroup check -- never a custom prime and never one below 2048 bits [@rfc7919], [@weakdh].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Always, regardless of primitive:&lt;/strong&gt; ephemeral and destroyed after use; validate the peer value appropriately (clamping is not validation); run the shared secret through HKDF; authenticate the exchange; never static DH; never reuse the ephemeral [@rfc5869], [@rfc8446].&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Key parameters&lt;/th&gt;
&lt;th&gt;Because (named break avoided)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;New design, general&lt;/td&gt;
&lt;td&gt;X25519, ephemeral&lt;/td&gt;
&lt;td&gt;32-byte keys, HKDF the secret&lt;/td&gt;
&lt;td&gt;Validation and timing are structural&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PQ-hardened default&lt;/td&gt;
&lt;td&gt;X25519MLKEM768&lt;/td&gt;
&lt;td&gt;Hybrid, both secrets into the KDF&lt;/td&gt;
&lt;td&gt;Shor and harvest-now-decrypt-later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIPS / HSM / interop&lt;/td&gt;
&lt;td&gt;ECDHE P-256 or P-384, ephemeral&lt;/td&gt;
&lt;td&gt;Full public-key validation&lt;/td&gt;
&lt;td&gt;Invalid-curve attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy FF interop only&lt;/td&gt;
&lt;td&gt;FFDHE &lt;code&gt;ffdhe3072&lt;/code&gt;+, ephemeral&lt;/td&gt;
&lt;td&gt;Subgroup check, never below 2048-bit&lt;/td&gt;
&lt;td&gt;Logjam shared-prime precomputation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;td&gt;Ephemeral, validate, KDF, authenticate&lt;/td&gt;
&lt;td&gt;Destroy the ephemeral after use&lt;/td&gt;
&lt;td&gt;Reuse, man-in-the-middle, CDH-DDH gap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; X25519 for greenfield. X25519MLKEM768 when you need post-quantum hardening. ECDHE-P256 or FFDHE-3072-plus only where FIPS or interop forces your hand. And always: ephemeral, validated, KDF&apos;d, and authenticated.&lt;/p&gt;
&lt;/blockquote&gt;

For TLS, run `openssl s_client -connect example.com:443 -tls1_3` and read the &quot;Negotiated TLS1.3 group&quot; line in the output; it names the group actually chosen, such as `X25519` or `X25519MLKEM768`. To offer only the hybrid and confirm the server supports it, add `-groups X25519MLKEM768` (OpenSSL 3.5 or later). For SSH, `ssh -Q kex` lists the key-exchange methods your client supports, and connecting with `ssh -vv` prints the method actually negotiated, for example `mlkem768x25519-sha256`.

flowchart TD
    A{&quot;New greenfield design?&quot;} -- no --&amp;gt; F{&quot;FIPS or HSM interop required?&quot;}
    A -- yes --&amp;gt; Q{&quot;Post-quantum sensitive?&quot;}
    Q -- yes --&amp;gt; H[&quot;X25519MLKEM768, ephemeral&quot;]
    Q -- no --&amp;gt; X[&quot;X25519, ephemeral&quot;]
    F -- yes --&amp;gt; P[&quot;ECDHE P-256 or P-384, ephemeral, full validation&quot;]
    F -- no --&amp;gt; L[&quot;FFDHE ffdhe3072 or larger, ephemeral, subgroup check&quot;]
&lt;h3&gt;The misuse catalog&lt;/h3&gt;
&lt;p&gt;Each common mistake maps to exactly one named break. Static DH gives no forward secrecy. A reused or cached &quot;ephemeral&quot; collapses forward secrecy and enables Raccoon. A custom, shared, or sub-2048-bit prime invites Logjam, and a non-prime &quot;prime&quot; you cannot audit invites the socat failure. Skipping the on-curve and subgroup check for P-256 ECDH builds an invalid-curve oracle. Treating clamping as validation misreads why X25519 is safe. Using the raw x-coordinate or $g^{ab}$ as a key ignores the CDH-DDH gap. And anonymous DH with no authentication is a standing man-in-the-middle.&lt;/p&gt;
&lt;h3&gt;The failure catalog&lt;/h3&gt;
&lt;p&gt;This is the thesis as evidence: every world-scale Diffie-Hellman break, in one grid, each a violated condition with the math intact underneath. The rows were derived across earlier sections -- Logjam [@weakdh] and its RSA twin FREAK [@freak], small-subgroup confinement [@limlee1997] and CVE-2016-0701 [@openssl-cve], the non-prime socat modulus [@socat-secadv7], the invalid-curve class [@bmm2000] and its practical TLS demonstration [@jss2015], Raccoon [@raccoon], and the KDF requirement [@rfc5869].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Break&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Condition violated&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Foreclosed by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Anonymous-DH MITM&lt;/td&gt;
&lt;td&gt;design gap&lt;/td&gt;
&lt;td&gt;#4 Authentication&lt;/td&gt;
&lt;td&gt;DH values bound to no identity&lt;/td&gt;
&lt;td&gt;STS/SIGMA; TLS 1.3 signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logjam shared-prime&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;#2 Strong, unshared group&lt;/td&gt;
&lt;td&gt;NFS first step precomputed per shared prime, about 45M core-years at 1024-bit&lt;/td&gt;
&lt;td&gt;RFC 7919 named safe primes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logjam 512-bit export&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;#2 Strong group (active)&lt;/td&gt;
&lt;td&gt;Downgrade to breakable export parameters&lt;/td&gt;
&lt;td&gt;TLS 1.3 removes export and static DH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FREAK (Logjam&apos;s RSA twin)&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;#2 Strong group (active)&lt;/td&gt;
&lt;td&gt;Downgrade to 512-bit export RSA key transport, then factor it&lt;/td&gt;
&lt;td&gt;TLS 1.3 removes export and static RSA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small-subgroup (Lim-Lee); CVE-2016-0701&lt;/td&gt;
&lt;td&gt;1997; 2016&lt;/td&gt;
&lt;td&gt;#3 Validation and #1 reuse&lt;/td&gt;
&lt;td&gt;Low-order element vs a reused exponent, no subgroup check&lt;/td&gt;
&lt;td&gt;Safe primes plus the subgroup check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-prime modulus (socat)&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;#2 Strong, unshared group&lt;/td&gt;
&lt;td&gt;A shipped custom 1024-bit &quot;prime&quot; that was composite, possibly backdoored&lt;/td&gt;
&lt;td&gt;RFC 7919 named safe primes; never bring your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid-curve (BMM); JSS TLS-ECDH&lt;/td&gt;
&lt;td&gt;2000; 2015&lt;/td&gt;
&lt;td&gt;#3 Validation&lt;/td&gt;
&lt;td&gt;Off-curve point, formulas ignore b, reused key&lt;/td&gt;
&lt;td&gt;Mandatory on-curve and subgroup check; X25519 twist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raccoon&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;#1 Freshness (reuse)&lt;/td&gt;
&lt;td&gt;Leading-zero stripping plus a reused DH key gives an MSB timing oracle&lt;/td&gt;
&lt;td&gt;No key reuse; TLS 1.3 key schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ephemeral reuse or caching&lt;/td&gt;
&lt;td&gt;ongoing&lt;/td&gt;
&lt;td&gt;#1 Freshness&lt;/td&gt;
&lt;td&gt;A cached &quot;ephemeral&quot; is a static key; forward secrecy collapses&lt;/td&gt;
&lt;td&gt;Fresh-per-session and destroyed; TLS 1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static DH&lt;/td&gt;
&lt;td&gt;ongoing&lt;/td&gt;
&lt;td&gt;#1 Freshness&lt;/td&gt;
&lt;td&gt;No ephemeral, so no forward secrecy&lt;/td&gt;
&lt;td&gt;TLS 1.3 ephemeral mandate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw x-coordinate as key&lt;/td&gt;
&lt;td&gt;ongoing&lt;/td&gt;
&lt;td&gt;#5 KDF discipline&lt;/td&gt;
&lt;td&gt;CDH-DDH gap leaves the shared value biased&lt;/td&gt;
&lt;td&gt;HKDF extract, per RFC 5869&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;


No, and this is the field&apos;s most-corrected misconception. Forward secrecy is a *conjunction*: the ephemeral must be freshly generated per session, never reused, destroyed after use, and run over a strong unshared group. Reuse a performance-optimized ephemeral and you have a static key; run over a shared 1024-bit prime and a precomputation reads you anyway, which is why the foundational paper is titled *Imperfect Forward Secrecy*. &quot;Ephemeral, therefore forward-secret&quot; is false without those conditions.


No. Clamping constrains *your own* secret scalar so your side of X25519 avoids cofactor and small-order pitfalls; it does nothing to check or reject a malicious peer value. X25519 is safe against off-curve inputs because of *twist security* -- the curve and its twist are both hard -- not because of clamping. That distinction trips up a lot of otherwise-correct code.


No. A Diffie-Hellman value is a structured group element, not a uniformly random string, and there are groups where it is even distinguishable from random -- the CDH-versus-DDH gap. Always run it through a KDF such as HKDF. Skipping the extract step is exactly what RFC 5869 warns against.


No. Fresh per-connection keys do not help against Logjam&apos;s central result: the number-field-sieve&apos;s expensive step depends only on the *prime*, so a shared or common 1024-bit prime can be precomputed once and then used to passively decrypt every connection on it. Use ffdhe3072 or larger, or move to a curve. Below 2048 bits is indefensible.


X25519 for anything greenfield: 32-byte keys, a single constant-time Montgomery ladder, and twist security that makes the two hardest ECDH mistakes -- timing leaks and missing point validation -- structurally unreachable. Use P-256 where FIPS, an HSM, or an existing deployment compels it, and then with full public-key validation and a constant-time library.


No. ML-KEM is a key encapsulation mechanism, which is inherently interactive and has no non-interactive static-static agreement, so it cannot express the asynchronous and static-static designs DH enables. That is why the post-quantum state of the art is a *hybrid* -- X25519MLKEM768, with X25519&apos;s secret combined with ML-KEM&apos;s -- rather than a like-for-like swap. The closest true post-quantum analog of Diffie-Hellman, the isogeny scheme CSIDH, does keep the non-interactive interface with 64-byte keys, but its quantum security is contested and reaching the intended level forces much larger, slower parameters.


No. Authentication defeats the *active* man-in-the-middle and nothing else -- it is orthogonal to the group&apos;s hardness and to input validation. A signed handshake over a shared 1024-bit prime is still Logjam-exposed, and a signed exchange that skips the on-curve check is still an invalid-curve oracle. Authentication is one of four conditions, not a substitute for the others.

&lt;h3&gt;The one sentence underneath&lt;/h3&gt;
&lt;p&gt;Return to Logjam. Across fifty years, almost no deployed break ever solved a discrete logarithm. Attackers precomputed a shared prime, confined a reused key to a small subgroup, fed an off-curve point to a validation-free server, read the clock while a server stripped leading zeros, shipped a modulus that was not even prime, or simply sat in the middle of an unauthenticated exchange. &quot;Using Diffie-Hellman safely&quot; is not one choice but a stack: a strong unshared group, a validated input, a genuinely fresh-and-destroyed ephemeral, an authenticated exchange, and a KDF beneath them. The fifty-year story is the field pulling each of those off the caller&apos;s shoulders and into the primitive, the named group, and the protocol, so that the simple thing to do became the safe thing to do. Remove any layer and the matching failure class reopens.&lt;/p&gt;

The math held; the deployment handed the secret away.
&lt;p&gt;There is one exception, and it is the horizon. Shor&apos;s algorithm is the single break in this whole story that touches the math itself, which is why today&apos;s ECDH is being &lt;em&gt;wrapped&lt;/em&gt;, not replaced, and why the destination is a hybrid rather than a bigger prime. So audit the exchange the way the whole article has: when a public value you did not generate arrives, and you combine it with a secret you must have freshly generated and will immediately destroy -- is the group strong and unshared, is that incoming value validated, is the exchange authenticated, and does the raw result go through a KDF before anyone calls it a key? Using Diffie-Hellman safely is the discipline of never handing an attacker a condition your protocol forgot to own.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;diffie-hellman-and-forward-secrecy&quot; keyTerms={[
  { term: &quot;Diffie-Hellman key agreement&quot;, definition: &quot;Two parties exchange g to the a and g to the b over an open channel and both compute g to the ab, a shared secret an eavesdropper who sees only the public values cannot compute.&quot; },
  { term: &quot;Discrete logarithm problem&quot;, definition: &quot;Given a generator g and the value g to the a, recover the exponent a. Believed hard in well-chosen groups, and the foundation Diffie-Hellman rests on.&quot; },
  { term: &quot;CDH vs DDH&quot;, definition: &quot;Computational Diffie-Hellman is computing the shared secret from the two public values; Decisional Diffie-Hellman is distinguishing it from random. DDH is strictly stronger, which is why the raw output is not yet a key.&quot; },
  { term: &quot;Forward secrecy&quot;, definition: &quot;Compromising a long-term key does not expose past sessions, but only if the per-session ephemeral was fresh, never reused, destroyed after use, and over a strong unshared group. It is a conjunction, not an automatic property.&quot; },
  { term: &quot;Safe prime&quot;, definition: &quot;A prime p equal to 2q plus 1 with q also prime. Its only small subgroup is the trivial order-two one (the values 1 and p minus 1, a cofactor of 2), so a small-subgroup attack can confine a key to at most one bit; implementations still reject 0, 1, and p minus 1.&quot; },
  { term: &quot;Number field sieve&quot;, definition: &quot;The best known sub-exponential attack on finite-field discrete logs. Its expensive first step depends only on the prime, so a shared prime is a shared weakness and the engine behind Logjam.&quot; },
  { term: &quot;Invalid-curve attack&quot;, definition: &quot;Sending a point on a different, weaker curve; because scalar-multiplication formulas ignore the curve constant, the victim computes on the wrong curve and leaks a reused private key.&quot; },
  { term: &quot;Twist security&quot;, definition: &quot;A curve whose quadratic twist also has near-prime order, so an off-curve input lands in another hard group. It makes X25519 safe against invalid inputs with no explicit validation.&quot; },
  { term: &quot;Clamping&quot;, definition: &quot;Bit-masking an X25519 secret scalar. It constrains your own scalar and is not validation of the peer value.&quot; },
  { term: &quot;Harvest-now decrypt-later&quot;, definition: &quot;Recording encrypted traffic today to decrypt once a quantum computer exists, which makes present-day ECDH confidentiality a future liability and motivates the post-quantum hybrids.&quot; }
]} questions={[
  { q: &quot;Why was Logjam possible even when servers generated a fresh key per connection?&quot;, a: &quot;Freshness and an unshared group are two different conditions. The number-field-sieve first step depends only on the prime, so a shared prime can be precomputed once and used to read every connection on it, no matter how fresh the per-connection secret is.&quot; },
  { q: &quot;What are the four conditions plus the discipline beneath them?&quot;, a: &quot;A fresh ephemeral, a strong and unshared group, a validated peer input, and an authenticated exchange, with a KDF beneath all four. Every named break is exactly one of these violated.&quot; },
  { q: &quot;Why is X25519 preferred over classic P-256 ECDH for new work?&quot;, a: &quot;Twist security makes input validation structural and the Montgomery ladder is constant-time by construction, so the two hardest ECDH mistakes, missing validation and timing leaks, are unreachable rather than merely avoided.&quot; },
  { q: &quot;Why must the raw shared secret be run through a KDF?&quot;, a: &quot;A Diffie-Hellman value is a structured group element, not a uniform string, and the CDH-DDH gap means it can even be distinguishable from random. RFC 5869 requires extracting a key from it rather than using it raw.&quot; },
  { q: &quot;Why is the post-quantum answer a hybrid rather than a bigger curve?&quot;, a: &quot;Shor&apos;s algorithm breaks all discrete-log DH in polynomial time regardless of key size, so the fix is to add a different hard problem, ML-KEM, wrapped around X25519, so the session is safe if either problem holds.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>diffie-hellman</category><category>forward-secrecy</category><category>x25519</category><category>ecdh</category><category>ffdhe</category><category>key-agreement</category><category>post-quantum-crypto</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Curve Was Hard; The Gap Was Soft: A Field Guide to Using Elliptic Curves Safely</title><link>https://paragmali.com/blog/the-curve-was-hard-the-gap-was-soft-a-field-guide-to-using-e/</link><guid isPermaLink="true">https://paragmali.com/blog/the-curve-was-hard-the-gap-was-soft-a-field-guide-to-using-e/</guid><description>Textbook ECC is hard; deployed ECC broke anyway. A field guide to P-256, Curve25519, Ristretto255, invalid-curve attacks, cofactors, and constant-time.</description><pubDate>Sat, 11 Jul 2026 16:08:14 GMT</pubDate><content:encoded>
**The elliptic-curve discrete-log problem was never the weak point.** On a well-chosen curve, ECDLP is the hardest discrete log we know how to build -- which is why a 256-bit curve buys what a 3072-bit RSA modulus does -- and yet across three decades of deployment, almost no real-world break ever solved one. They did not have to. Every named disaster lives in the gap between a hard curve and safe code: the server that fed an attacker&apos;s off-curve point into scalar multiplication and leaked its private key (invalid-curve attacks); the low-order point that clamping never filtered (the Monero cofactor bug); the certificate whose *base point* was never checked (Curveball, CVE-2020-0601); the microsecond of branch timing in a non-constant-time ladder (Brumley-Tuveri). The evolution of curves -- NIST short-Weierstrass (P-256, secp256k1), then Curve25519/X25519, then Ed25519, then the Ristretto255 prime-order abstraction -- is one sustained move: take the safety burden off the implementer and bake it into the curve, the group, and the encoding, so the simple implementation becomes the secure one. Using elliptic curves safely is a stack: the right curve for the job (X25519 for key agreement, Ed25519 for new signatures, P-256 only where FIPS compels it, secp256k1 only for blockchain), validate every untrusted point, multiply in constant time, and prefer constructions where the dangerous case cannot be reached. Remove any layer and the gap reopens.
&lt;h2&gt;1. Two Lines of Code&lt;/h2&gt;
&lt;p&gt;In January 2020, Microsoft patched a vulnerability the U.S. National Security Agency had quietly reported to it -- an unusually public gesture from an agency that normally hoards such findings. The bug was almost embarrassing in its simplicity. Windows would check a TLS certificate&apos;s elliptic-curve signature against a trusted root, and it would never check the curve&apos;s base point. If you supplied a certificate that carried its own curve parameters -- with a generator you had chosen yourself -- Windows would trust the forged chain as though a real &lt;a href=&quot;https://paragmali.com/blog/a-perfect-signature-for-a-certificate-that-should-never-have/&quot; rel=&quot;noopener&quot;&gt;certificate authority&lt;/a&gt; had signed it [@cve-2020-0601]. Nobody factored a modulus. Nobody solved a discrete logarithm. The math of P-256 stood untouched. The attacker handed the code one parameter it forgot to validate, and the industry named the result Curveball.&lt;/p&gt;
&lt;p&gt;Now go one layer down and back five years. In 2015, researchers recovered a TLS server&apos;s ECDH &lt;em&gt;private key&lt;/em&gt; -- the actual secret -- by feeding it points that were not on the intended curve at all. The server&apos;s own scalar-multiplication routine did the leaking, one small residue at a time, until the whole key fell out [@jager-2015]. Two breaks, at two different layers of the same stack, with exactly one shape. In neither case did anyone attack the problem the textbooks call hard.&lt;/p&gt;
&lt;p&gt;That is the claim this article proves. &lt;strong&gt;ECDLP-hardness is not the same thing as ECC-security, and every deployed elliptic-curve break lives in the gap between them.&lt;/strong&gt; The discrete logarithm on a good curve is the strongest one-way function in wide use. The security of a system built on it is a different property entirely -- one that depends on what your code does with a point before it has decided the point is legitimate, and whether it does the arithmetic in a time an attacker cannot measure.&lt;/p&gt;

The private curve stayed hard; the code around it confessed.
&lt;p&gt;So here is the diagnostic question the rest of this piece hands you, the one to ask of any elliptic-curve code you own: &lt;em&gt;when a point you did not generate arrives, what does your code do with it before it has checked that the point is on the intended curve and in the right prime-order subgroup -- and does it do that arithmetic in constant time?&lt;/em&gt; Hold that question. The names you already fear -- invalid-curve attacks, Curveball, twist attacks, small-subgroup leaks, the cofactor, timing side channels -- are all one sentence underneath it. One mechanism explains the entire catalog, and the thirty-year evolution of curves is a single sustained campaign to put that mechanism out of reach.&lt;/p&gt;
&lt;p&gt;To see how an unchecked point hands over a private key -- and why &quot;just use Curve25519&quot; is a real answer but not the whole one -- you first have to see what an elliptic curve actually &lt;em&gt;is&lt;/em&gt;, and what makes its discrete log hard.&lt;/p&gt;
&lt;h2&gt;2. What an Elliptic Curve Actually Is, and Where It Came From&lt;/h2&gt;
&lt;p&gt;In 1976, Whitfield Diffie and Martin Hellman handed the world a template rather than a cryptosystem [@diffie-hellman-1976]. Pick a mathematical group in which one operation is easy to compute and its inverse is hard. Publish the result of applying that operation $k$ times to a fixed element -- call it $[k]G$. Keep $k$ secret. Anyone can combine public values to agree on a shared secret; no eavesdropper can run the operation backward. The template was sound. The group they had, the integers modulo a large prime, was not thrifty: because sub-exponential algorithms (index calculus) attack discrete logs in that group, safety demanded enormous keys. The obvious question hung in the air for nine years. What if you kept the template and changed the group?&lt;/p&gt;
&lt;h3&gt;Two inventors, no shared citation&lt;/h3&gt;
&lt;p&gt;The answer arrived twice, independently, from two people who did not cite each other. Victor Miller proposed it at CRYPTO &apos;85 [@miller-1985]; Neal Koblitz published the same idea in &lt;em&gt;Mathematics of Computation&lt;/em&gt; in 1987 [@koblitz-1987]. Both suggested using the group of points on an elliptic curve over a finite field. The decisive property: for a well-chosen curve, the best algorithm anyone knows is &lt;em&gt;generic&lt;/em&gt; -- it works in any group and costs about $\sqrt{n}$ operations for a group of size $n$ -- rather than the sub-exponential index calculus that makes ordinary discrete logs cheap. No shortcut specific to the curve exists. That single absence is the whole reason elliptic-curve cryptography exists.Miller (1985) and Koblitz (1987) proposed elliptic-curve cryptography independently and neither cited the other. It is one of cryptography&apos;s cleaner cases of simultaneous invention -- the idea was ready to be found.&lt;/p&gt;
&lt;h3&gt;The primitive, kept tight&lt;/h3&gt;
&lt;p&gt;Here is the object itself, because this &lt;em&gt;is&lt;/em&gt; the primitive and everything later depends on reading it correctly. Take the equation&lt;/p&gt;
&lt;p&gt;$$y^2 = x^3 + ax + b$$&lt;/p&gt;
&lt;p&gt;over a finite field $\mathbb{F}_p$ (the integers modulo a prime $p$). The set of solutions $(x, y)$, together with one extra &quot;point at infinity&quot; that acts as a zero, forms a finite abelian group. The group operation is a geometric rule called chord-and-tangent addition: to add two points, draw the line through them, find the third place it meets the curve, and reflect over the $x$-axis. To double a point, use the tangent line instead. The rule looks whimsical and turns out to be associative -- the proof runs through divisors and Riemann-Roch, which we cheerfully defer to Silverman.&lt;/p&gt;

Repeated point addition: adding a base point G to itself k times, written [k]G. Computed in about log k doublings and additions by the double-and-add method, it is the easy, forward direction of the elliptic-curve trapdoor -- fast to compute, believed hard to reverse.
&lt;p&gt;Scalar multiplication is that forward direction: $[k]G = G + G + \dots + G$, $k$ times. Because you can double repeatedly, computing $[k]G$ for a 256-bit $k$ takes a few hundred operations, not $2^{256}$ of them. Reversing it -- recovering $k$ from $G$ and $Q = [k]G$ -- is the hard problem the whole field rests on.&lt;/p&gt;

Given a base point G on an elliptic curve and a second point Q = [k]G, recover the scalar k. This is the one-way assumption elliptic-curve cryptography depends on: easy to compute [k]G in the forward direction, believed infeasible to invert for a well-chosen curve.
&lt;p&gt;Feel the asymmetry directly. The demonstration below computes $[13]G$ on a tiny curve in a handful of steps, then tries to walk it back. At these toy sizes the reversal is trivial; at 256 bits the same reversal is roughly $2^{128}$ operations, which is why the forward arrow is a trapdoor and the backward arrow is a wall.&lt;/p&gt;
&lt;p&gt;{`
// Toy curve y^2 = x^3 + 2x + 2 over F_17. Illustrative sizes, NOT secure.
const p = 17n, a = 2n;
function mod(x){ return ((x % p) + p) % p; }
function inv(x){ // modular inverse via Fermat: x^(p-2) mod p
  let r = 1n, b = mod(x), e = p - 2n;
  while (e &amp;gt; 0n){ if (e &amp;amp; 1n) r = mod(r&lt;em&gt;b); b = mod(b&lt;/em&gt;b); e &amp;gt;&amp;gt;= 1n; }
  return r;
}
function add(P, Q){
  if (P === null) return Q;
  if (Q === null) return P;
  const [x1,y1] = P, [x2,y2] = Q;
  if (x1 === x2 &amp;amp;&amp;amp; mod(y1 + y2) === 0n) return null; // P + (-P) = O
  let s;
  if (x1 === x2 &amp;amp;&amp;amp; y1 === y2) s = mod((3n&lt;em&gt;x1&lt;/em&gt;x1 + a) * inv(2n&lt;em&gt;y1)); // tangent (double)
  else s = mod((y2 - y1) * inv(x2 - x1));                          // chord (add)
  const x3 = mod(s&lt;/em&gt;s - x1 - x2);
  const y3 = mod(s*(x1 - x3) - y1);
  return [x3, y3];
}
function mul(k, G){ // double-and-add: the FORWARD (easy) direction
  let R = null, base = G;
  while (k &amp;gt; 0n){ if (k &amp;amp; 1n) R = add(R, base); base = add(base, base); k &amp;gt;&amp;gt;= 1n; }
  return R;
}
const G = [5n, 1n];         // generator; this subgroup has prime order 19
const secret = 13n;         // the &quot;private key&quot;
const Q = mul(secret, G);   // the &quot;public key&quot; [13]G, computed in ~5 steps
console.log(&apos;public point [13]G =&apos;, Q[0].toString() + &apos;,&apos; + Q[1].toString());&lt;/p&gt;
&lt;p&gt;// Now INVERT it: recover the secret from G and Q. The only general method on a
// real curve is search -- trivial here, about 2^128 work at 256 bits.
let found = null, R = null;
for (let k = 1n; k &amp;lt;= 19n; k++){
  R = add(R, G);
  if (R &amp;amp;&amp;amp; R[0] === Q[0] &amp;amp;&amp;amp; R[1] === Q[1]){ found = k; break; }
}
console.log(&apos;recovered secret by brute force =&apos;, found.toString());
`}&lt;/p&gt;
&lt;h3&gt;Why the keys shrink&lt;/h3&gt;
&lt;p&gt;That missing shortcut is not an academic footnote; it is the entire economic case for the primitive. For discrete logs modulo a prime, and for RSA, index-calculus methods run in sub-exponential time, so defenders must inflate the modulus to stay ahead -- 3072 bits to reach roughly 128-bit security. On a well-chosen prime-field curve, no such method is known, so the attacker is stuck paying the generic $\sqrt{n}$ price. A 256-bit curve therefore delivers about 128 bits of security [@miller-1985], [@safecurves-rho]. Same protection, an order of magnitude smaller keys, faster operations. Hold onto one detail for later: this economic win, the thing everyone remembers about elliptic curves, is the one property that has &lt;em&gt;never&lt;/em&gt; been what broke.The absence of a sub-exponential attack on prime-field curves is the whole argument. It is why 256-bit ECC stands in for a 3072-bit RSA key -- and it is a statement about what we do not know how to do, not a theorem.&lt;/p&gt;
&lt;h3&gt;One clean curve, and the seeds of trouble&lt;/h3&gt;
&lt;p&gt;Standardization made the primitive interoperable. ECDSA was standardized as a digital-signature algorithm (ANSI X9.62) and folded into NIST&apos;s FIPS 186 series [@fips-186-5]; the P-curve we still call P-256 dates to FIPS 186-2 in 2000. The Standards for Efficient Cryptography Group published the matching &lt;code&gt;secp&lt;/code&gt; parameter sets -- including secp256r1, which &lt;em&gt;is&lt;/em&gt; P-256, and its efficiency-minded sibling secp256k1 [@sec2-v2]. The result was a single, clean, interoperable curve with prime order and a genuine strength most of its successors would come to envy. Which raises the puzzle that drives the next two sections: if P-256 is that good, why do the very people who designed its replacement consider &quot;just implement P-256&quot; to be dangerous advice?&lt;/p&gt;
&lt;h2&gt;3. The NIST Curves, and What They Cost&lt;/h2&gt;
&lt;p&gt;P-256 did one big thing exactly right, and three smaller things in a way that made the &lt;em&gt;simple&lt;/em&gt; implementation &lt;em&gt;insecure&lt;/em&gt;. Both halves are true at once, and holding them together is the beginning of expertise here.&lt;/p&gt;
&lt;h3&gt;What went right&lt;/h3&gt;
&lt;p&gt;P-256 is the curve $y^2 = x^3 - 3x + b$ over the special prime $p = 2^{256} - 2^{224} + 2^{192} + 2^{96} - 1$, and its single most underrated property is boring: its group has &lt;strong&gt;prime order&lt;/strong&gt;, cofactor 1 [@sec2-v2]. There is no smaller subgroup for an attacker to trap you in, no cofactor to clear, no torsion points hiding in the corners. Add to that an enormous interoperable deployment -- P-256 is the elliptic curve behind most of the web&apos;s ECC certificates [@ssl-pulse] -- and you have a curve whose &lt;em&gt;group-level&lt;/em&gt; security is excellent. Remember this when Curve25519 arrives two sections from now carrying a cofactor of 8.&lt;/p&gt;

The ratio of a curve&apos;s full group order to the prime order of the subgroup used for cryptography. P-256 has cofactor 1 (its whole group has prime order), while edwards25519 has cofactor 8, meaning small-order &quot;torsion&quot; points exist alongside the useful prime-order subgroup and must be handled with care.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Cofactor 1 means P-256 has no small-order subgroups at all. That single fact rules out an entire family of attacks at the group level. Keep it in mind when the next generation trades it away for other advantages -- prime order is exactly what Curve25519 gives up and what Ristretto255 later works hard to rebuild.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Cost one: nobody can explain the seed&lt;/h3&gt;
&lt;p&gt;The coefficient $b$ and the base point were produced from a seed run through SHA-1 -- &quot;verifiably random,&quot; in the standard&apos;s language, meaning anyone can rerun the hash and confirm the parameters were not obviously cooked. The catch is that the &lt;em&gt;seed itself&lt;/em&gt; is an unexplained string of bytes. Nobody outside its authors can say why that seed and not another. SafeCurves calls this failure &lt;strong&gt;rigidity&lt;/strong&gt;: a curve is rigid only when its parameters follow from stated principles with no unexplained freedom, and the NIST P-curves are not [@safecurves-index]. This is a trust concern, not a break -- but trust is a load-bearing beam, as the next beat shows.The P-256 parameters are &quot;verifiably random&quot; from a seed, yet the seed is never justified. That gap -- reproducible but unexplained -- is exactly what rigidity objects to. You can check the arithmetic and still not know why these constants.&lt;/p&gt;
&lt;h3&gt;Cost two: the addition law has holes&lt;/h3&gt;
&lt;p&gt;The classic short-Weierstrass addition formulas are not total. They have exceptional cases -- adding a point to its own negation, doubling a point whose $y$-coordinate is zero, anything involving the point at infinity -- where the generic slope formula divides by zero and returns nonsense. A working implementation must branch around those cases. And a branch whose direction depends on secret data is either wrong on the rare input that hits it, or observable through timing, or both. This incomplete addition law is the seed of the timing break we meet in the next section.&lt;/p&gt;
&lt;h3&gt;Cost three: the formula never looks at b&lt;/h3&gt;
&lt;p&gt;Here is the sharp one. Look back at the addition code in Section 2 and notice what is missing: the coefficient $b$ never appears. The short-Weierstrass scalar-multiplication formulas depend on $a$, on the coordinates, on the prime -- but not on $b$. So if you hand the routine a point that satisfies some &lt;em&gt;other&lt;/em&gt; curve $y^2 = x^3 + ax + c$ with a different constant term, the arithmetic proceeds without complaint, silently computing on that other, possibly far weaker, curve [@safecurves-twist]. The curve has no twist security to catch the mistake. This omission is the seed of the invalid-curve break, and we detonate it in Section 4.&lt;/p&gt;
&lt;h3&gt;The Dual EC shadow&lt;/h3&gt;
&lt;p&gt;There is one more reason the community distrusts NIST-supplied constants, and precision matters because it is widely misremembered. In 2007, Dan Shumow and Niels Ferguson showed that the Dual EC DRBG random-number generator &lt;em&gt;could&lt;/em&gt; conceal a back door: whoever chose its two special points might hold a secret relationship between them that predicts the generator&apos;s output [@shumow-ferguson-2007]. Edward Snowden&apos;s 2013 disclosures and, in 2015, a backdoor found inside Juniper&apos;s ScreenOS gave that theoretical worry a concrete afterlife.&lt;/p&gt;
&lt;p&gt;But be exact about what this does and does not implicate. &lt;strong&gt;No weakness has ever been demonstrated in the P-256 curve itself.&lt;/strong&gt; Dual EC poisoned trust in &lt;em&gt;NIST-supplied constants as a category&lt;/em&gt; -- which is precisely why so many engineers now prefer rigid curves -- but it is a trust-and-rigidity argument, not a proven backdoor in the P-curves.Dual EC DRBG is a random-number-generator story, not a P-curve story. It shows NIST-chosen constants &lt;em&gt;could&lt;/em&gt; be poisoned; it does not show that P-256&apos;s were. Conflating the two overstates the case and is a common error. And notice the shape already forming: prime-order, FIPS-mandated, mathematically strong -- yet the &lt;em&gt;simple&lt;/em&gt; implementation is the insecure one.&lt;/p&gt;
&lt;h3&gt;The efficiency sibling&lt;/h3&gt;
&lt;p&gt;One more curve belongs on the table. secp256k1 is the SECG&apos;s efficiency variant: the curve $y^2 = x^3 + 7$ over $p = 2^{256} - 2^{32} - 977$, with $j$-invariant 0 and an endomorphism (the GLV method) that speeds scalar multiplication [@sec2-v2]. It is the curve Bitcoin chose, and later the base for Taproot&apos;s BIP-340 Schnorr signatures [@bip-340]. Keep its domain straight throughout this article: secp256k1 owns the blockchain world and is essentially absent from web PKI and TLS -- a blockchain-interoperability answer, never a general-purpose competitor to the curves that carry your HTTPS traffic.&lt;/p&gt;
&lt;p&gt;Two of those three costs -- the unused $b$ and the branchy ladder -- are not abstractions. Each became a remote private-key recovery against real servers. To see how, you need the one mechanism that sits underneath the entire failure catalog.&lt;/p&gt;
&lt;h2&gt;4. The Failure Catalog: One Atom, Many Surfaces&lt;/h2&gt;
&lt;p&gt;Here is the mechanism the whole catalog reuses. Call it the atom: &lt;em&gt;a point you did not generate, fed into scalar multiplication before you validate it -- or multiplied in variable time -- is a key-recovery oracle.&lt;/em&gt; The discrete logarithm is never solved. The implementation simply computes on the attacker&apos;s terms and leaks the answer, one small piece at a time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The atom of every deployed elliptic-curve break: a point you did not generate, used before it is validated -- or a secret scalar multiplied in a time an attacker can measure -- turns your own scalar-multiplication routine into an oracle that hands back the private key. ECDLP stays intact; the code around it does the leaking.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Deriving the atom: invalid-curve attacks&lt;/h3&gt;
&lt;p&gt;Recall Cost three from the last section: the short-Weierstrass formulas never read $b$. Ingrid Biehl, Bernd Meyer, and Volker Muller turned that omission into an attack at CRYPTO 2000 [@biehl-meyer-muller-2000]. Send the victim a point that satisfies a &lt;em&gt;different&lt;/em&gt; curve $y^2 = x^3 + ax + c$ -- one you chose specifically because its group has small-order subgroups. The victim&apos;s routine, blind to the constant term, computes your scalar multiple on that weak curve. Its response now lives in a small subgroup, so it reveals the secret scalar modulo a small number. SafeCurves states the mechanism about as plainly as it can be stated:&lt;/p&gt;

The standard formulas for scalar multiplication on short Weierstrass curves do not involve the constant coefficient b, so they automatically also work for y^2 = x^3 + ax + c ... Bob will successfully compute n(x,y) without realizing that anything is amiss.

Feeding an implementation a point that is not on the intended curve into scalar-multiplication formulas that ignore the coefficient b. The operation runs on a different, attacker-chosen curve whose group has small subgroups, so the response leaks the private scalar modulo small numbers -- with the intended curve&apos;s discrete-log hardness never touched.

flowchart TD
    A[&quot;Attacker sends a point not on the intended curve&quot;] --&amp;gt; B[&quot;Scalar-multiply formula ignores coefficient b&quot;]
    B --&amp;gt; C[&quot;Arithmetic runs on a different, weaker curve&quot;]
    C --&amp;gt; D[&quot;That curve has small-order subgroups&quot;]
    D --&amp;gt; E[&quot;Response reveals the secret modulo a small number&quot;]
    E --&amp;gt; F[&quot;Repeat with several coprime small orders&quot;]
    F --&amp;gt; G[&quot;Chinese Remainder Theorem reassembles the whole key&quot;]
&lt;p&gt;The demonstration below runs the leak on a toy curve whose order factors as $105 = 3 \times 5 \times 7$. The attacker sends three low-order points, harvests three residues, and the Chinese Remainder Theorem stitches them into the secret. Nothing here is a discrete-log computation; it is bookkeeping.&lt;/p&gt;
&lt;p&gt;{`
// Toy curve y^2 = x^3 + x + 1 over F_101. Group order 105 = 3&lt;em&gt;5&lt;/em&gt;7.
// Illustrative ONLY -- a real curve has PRIME order and ~2^256 points.
const p = 101n, a = 1n;
const mod = x =&amp;gt; ((x % p) + p) % p;
function inv(x){ let r=1n,b=mod(x),e=p-2n; while(e&amp;gt;0n){ if(e&amp;amp;1n) r=mod(r&lt;em&gt;b); b=mod(b&lt;/em&gt;b); e&amp;gt;&amp;gt;=1n;} return r; }
function add(P,Q){
  if(P===null) return Q; if(Q===null) return P;
  const [x1,y1]=P,[x2,y2]=Q;
  if(x1===x2 &amp;amp;&amp;amp; mod(y1+y2)===0n) return null;      // P + (-P) = O
  const s = (x1===x2 &amp;amp;&amp;amp; y1===y2)
    ? mod((3n&lt;em&gt;x1&lt;/em&gt;x1+a)&lt;em&gt;inv(2n&lt;/em&gt;y1))                 // tangent (double)
    : mod((y2-y1)&lt;em&gt;inv(x2-x1));                     // chord (add)
  const x3 = mod(s&lt;/em&gt;s-x1-x2);
  return [x3, mod(s*(x1-x3)-y1)];
}
function mul(k,P){ let R=null,b=P; while(k&amp;gt;0n){ if(k&amp;amp;1n) R=add(R,b); b=add(b,b); k&amp;gt;&amp;gt;=1n;} return R; }&lt;/p&gt;
&lt;p&gt;const k = 47n;                    // the victim&apos;s PRIVATE scalar
// Attacker-crafted low-order points (orders 3, 5, 7). None is the real base point.
const traps = [ {P:[28n,8n], t:3n}, {P:[86n,67n], t:5n}, {P:[72n,5n], t:7n} ];&lt;/p&gt;
&lt;p&gt;const residues = [], moduli = [];
for (const {P,t} of traps){
  const Q = mul(k, P);            // victim naively returns [k]P = [k mod t]P
  let R = null;
  for (let i=0n;i&amp;lt;t;i++){         // attacker matches against the tiny orbit
    const same = (R===null&amp;amp;&amp;amp;Q===null) || (R&amp;amp;&amp;amp;Q&amp;amp;&amp;amp;R[0]===Q[0]&amp;amp;&amp;amp;R[1]===Q[1]);
    if (same){ residues.push(i); moduli.push(t); break; }
    R = add(R,P);
  }
}
console.log(&apos;leaked:&apos;, residues.map((r,i)=&amp;gt;&apos;k mod &apos;+moduli[i]+&apos; = &apos;+r).join(&apos;,  &apos;));&lt;/p&gt;
&lt;p&gt;// Chinese Remainder Theorem stitches the residues into k mod 105.
let M=1n; for(const m of moduli) M*=m;
let x=0n;
for(let i=0;i&amp;lt;residues.length;i++){
  const Mi=M/moduli[i];
  let mi=1n; for(let j=1n;j&amp;lt;moduli[i];j++){ if((Mi*j)%moduli[i]===1n){ mi=j; break; } }
  x += residues[i]&lt;em&gt;Mi&lt;/em&gt;mi;
}
console.log(&apos;CRT-reassembled secret =&apos;, (((x%M)+M)%M).toString(), &apos; (true k = &apos;+k+&apos;)&apos;);
`}&lt;/p&gt;
&lt;h3&gt;Surface one: small-subgroup attacks&lt;/h3&gt;
&lt;p&gt;You do not even need to leave the intended curve to run the atom. Chae Hoon Lim and Pil Joong Lee showed this at CRYPTO &apos;97 [@lim-lee-1997]. If the curve&apos;s group is not of prime order -- if it has small-order subgroups -- then an attacker submits an element of small order $t$, and the victim&apos;s response is forced into that tiny subgroup, revealing the secret modulo $t$. Collect a few coprime $t$ values, apply the Chinese Remainder Theorem, and the full scalar falls out. This is the conceptual root of the rule &quot;validate the subgroup, not just the curve,&quot; and it is exactly why prime order mattered so much back in Section 3. It also plants a flag we return to: any curve with a cofactor greater than 1 has these small subgroups by construction.&lt;/p&gt;

Submitting a group element of small order so that the victim&apos;s scalar multiplication lands in a tiny subgroup, making the response reveal the secret scalar modulo that small order. Repeated across several coprime small orders, the Chinese Remainder Theorem reassembles the full private key. It is the reason implementations must check that inputs lie in the correct prime-order subgroup.
&lt;h3&gt;Surface two: the atom made remote&lt;/h3&gt;
&lt;p&gt;Theory became a remote server compromise in 2015, when Tibor Jager, Jorg Schwenk, and Juraj Somorovsky recovered real TLS servers&apos; ECDH private keys [@jager-2015]. The conditions were mundane: a server that reused a single ECDH key pair across handshakes and did not validate the incoming peer point. Each handshake fed the server another attacker-chosen invalid point; each response leaked another small residue; a few thousand handshakes later, the CRT delivered the private key. No discrete logarithm was ever computed. This is the atom of Section 4, wearing a TLS badge.The practical TLS result is Jager, Schwenk, and Somorovsky&apos;s ESORICS 2015 paper, &quot;Practical Invalid Curve Attacks on TLS-ECDH&quot; -- distinct from the same trio&apos;s separate USENIX Security 2015 work. The theoretical root is Biehl-Meyer-Muller (2000). Getting the citation right matters, because the two 2015 papers are often conflated.&lt;/p&gt;
&lt;h3&gt;Surface three: the cofactor, in production&lt;/h3&gt;
&lt;p&gt;Now the promised return to cofactors. The curve edwards25519 (the signing sibling of Curve25519) has cofactor 8, which means low-order &quot;torsion&quot; points exist. Monero&apos;s ring-signature scheme used &lt;em&gt;key images&lt;/em&gt; to prevent double-spending: each spent output should map to exactly one key image. The bug was that the code never required a key image to lie in the prime-order subgroup, so an attacker could add a low-order torsion point to a legitimate key image, producing a different but still valid-looking image for the same output -- and spend it twice [@loup-vaillant-cofactor]. The fix was to demand that every key image $I$ satisfy $[\ell]I = \mathcal{O}$, where $\ell$ is the prime subgroup order: multiply by the group order and you must land on the identity, proving no torsion component is hiding inside.&lt;/p&gt;
&lt;p&gt;The precise lesson here is the one most often gotten wrong, so it gets its own box.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Curve25519 secret scalars are &quot;clamped&quot; -- specific bits are forced -- and engineers routinely mistake this for input validation. It is not. Clamping constrains &lt;em&gt;your own&lt;/em&gt; secret scalar so that it is a multiple of the cofactor and sits in a fixed range. It does nothing to a malicious low-order &lt;em&gt;input&lt;/em&gt; point someone else hands you. The Monero cofactor bug is exactly this confusion. If a protocol needs a prime-order group, validate or clear the cofactor on inputs -- or use a construction that removes the choice entirely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Surface four: Curveball, one layer up&lt;/h3&gt;
&lt;p&gt;The atom does not care which layer it runs on. In Curveball (CVE-2020-0601), the mishandled point was not a peer&apos;s ephemeral key but the curve&apos;s own &lt;em&gt;base point&lt;/em&gt;. Windows CryptoAPI accepted certificates carrying explicit curve parameters and trusted the generator inside them without checking it against the named curve&apos;s real generator. Choose your own generator with a known relationship to a trusted CA&apos;s public key, and you forge a signing chain [@cve-2020-0601]. Same disease, higher in the stack: a parameter that should have been validated -- here, the generator itself -- was taken on faith.&lt;/p&gt;
&lt;h3&gt;Surface five: the clock, with no malicious point at all&lt;/h3&gt;
&lt;p&gt;The final surface uses no bad input whatsoever. In 2011, Billy Bob Brumley and Nicola Tuveri recovered an OpenSSL ECDSA private key from a &lt;em&gt;remote&lt;/em&gt; server purely by timing its scalar multiplications [@brumley-tuveri-2011]. OpenSSL&apos;s ladder for binary-field curves ran in data-dependent time; the timing leaked the bit-length of each per-signature nonce, and a lattice computation turned a few hundred such leaks into the key. The underlying flaw earned CVE-2011-1945, whose description is blunt: the implementation &quot;does not properly implement curves over binary fields, which makes it easier for ... attackers to determine private keys via a timing attack and a lattice calculation&quot; [@cve-2011-1945]. This is the atom&apos;s other face: arithmetic on a secret, performed in observable time, is an oracle even when every input is honest.&lt;/p&gt;
&lt;h3&gt;The one family this article does not re-derive&lt;/h3&gt;

There is a whole lineage of ECDSA breaks -- fail0verflow&apos;s PlayStation 3 key recovery, biased-nonce lattice attacks, LadderLeak, Minerva, TPM-Fail -- that shares the atom&apos;s DNA but belongs to a sibling story about randomness and leakage rather than point validation [@minerva-2020], [@ladderleak-2020]. The result is quick to state: a repeated or biased per-signature nonce leaks the ECDSA private key through a lattice, deterministic nonces (RFC 6979) fix the generation half, and constant-time arithmetic fixes the leakage half [@rfc6979]. The lattice machinery is owned by this series&apos; dedicated nonce-reuse post, so this article signposts it and moves on.
&lt;h3&gt;The pattern, stated once&lt;/h3&gt;
&lt;p&gt;Read the six surfaces together and one sentence covers all of them: &lt;em&gt;the implementation touched a value it should have rejected, or acted in a time it should have hidden.&lt;/em&gt; The leaked bit is never &quot;the discrete logarithm.&quot; It is &quot;was this point on the curve,&quot; or &quot;was this element in the right subgroup,&quot; or &quot;how long did that multiply take,&quot; or &quot;do I trust this generator.&quot; The table below is the evidence for the entire thesis. Every row is the same atom on a different surface.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Channel&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;The rule it teaches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Small-subgroup, Lim-Lee [@lim-lee-1997]&lt;/td&gt;
&lt;td&gt;1997&lt;/td&gt;
&lt;td&gt;Subgroup&lt;/td&gt;
&lt;td&gt;Response residue&lt;/td&gt;
&lt;td&gt;Non-prime-order group element accepted&lt;/td&gt;
&lt;td&gt;Validate subgroup membership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invalid-curve, Biehl-Meyer-Muller [@biehl-meyer-muller-2000]&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;Off-curve point&lt;/td&gt;
&lt;td&gt;Response residue&lt;/td&gt;
&lt;td&gt;Formula ignores the coefficient b&lt;/td&gt;
&lt;td&gt;Check the point is on the intended curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remote timing, Brumley-Tuveri [@brumley-tuveri-2011], [@cve-2011-1945]&lt;/td&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;Scalar-mult timing&lt;/td&gt;
&lt;td&gt;Wall-clock time&lt;/td&gt;
&lt;td&gt;Non-constant-time ladder&lt;/td&gt;
&lt;td&gt;Multiply in constant time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Practical invalid-curve TLS, Jager et al. [@jager-2015]&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;Off-curve point, reused key&lt;/td&gt;
&lt;td&gt;Response residue&lt;/td&gt;
&lt;td&gt;No peer-point validation&lt;/td&gt;
&lt;td&gt;Validate points, do not reuse ECDH keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cofactor key image, Monero [@loup-vaillant-cofactor]&lt;/td&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;Cofactor torsion&lt;/td&gt;
&lt;td&gt;Forged key image&lt;/td&gt;
&lt;td&gt;Key image not in prime-order subgroup&lt;/td&gt;
&lt;td&gt;Enforce prime order, clamping is not validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curveball [@cve-2020-0601]&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;PKI base point&lt;/td&gt;
&lt;td&gt;Forged certificate chain&lt;/td&gt;
&lt;td&gt;Explicit generator trusted&lt;/td&gt;
&lt;td&gt;Use named curves, never trust explicit parameters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECDSA-nonce family [@minerva-2020], [@ladderleak-2020]&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;Nonce leakage&lt;/td&gt;
&lt;td&gt;Timing then lattice&lt;/td&gt;
&lt;td&gt;Biased or leaked nonce&lt;/td&gt;
&lt;td&gt;Deterministic nonces plus constant time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Six surfaces, one disease. And notice what the cure was &lt;em&gt;not&lt;/em&gt;: it was not a warning label stapled to P-256. It was a decade-long engineering program to build curves where the dangerous case cannot be reached at all -- and that program, not the catalog, is the most instructive part of the story.&lt;/p&gt;
&lt;h2&gt;5. The Evolution: Moving Safety Off the Implementer&lt;/h2&gt;
&lt;p&gt;Read the last section again as a to-do list for curve designers. Each generation that follows crosses off one entry -- and here is the pattern that makes the story an argument rather than a parade of curves: they do it not by warning implementers to be careful, but by making the mistake unreachable. It is an attacker&apos;s kill-chain run in reverse. Where an intruder normally advances stage by stage, here the &lt;em&gt;defenders&lt;/em&gt; advance, retiring one failure class per curve generation until the dangerous case no longer exists to reach.&lt;/p&gt;

flowchart LR
    G1[&quot;Gen 1 NIST short-Weierstrass&quot;] --&amp;gt; F1[&quot;Leaves timing and invalid-curve to the implementer&quot;]
    F1 --&amp;gt; G2[&quot;Gen 2 Curve25519 ladder&quot;]
    G2 --&amp;gt; R2[&quot;Retires branch timing and invalid-curve for DH&quot;]
    R2 --&amp;gt; G3[&quot;Gen 3 Ed25519 Edwards law&quot;]
    G3 --&amp;gt; R3[&quot;Retires incomplete addition and the nonce disaster&quot;]
    R3 --&amp;gt; G4[&quot;Gen 4 Ristretto255 encoding&quot;]
    G4 --&amp;gt; R4[&quot;Retires the cofactor class at the type level&quot;]
&lt;p&gt;The chronology underneath that progression spans half a century, from a template to a post-quantum hybrid.&lt;/p&gt;

flowchart LR
    A[&quot;1976 Diffie-Hellman template&quot;] --&amp;gt; B[&quot;1985-87 Miller and Koblitz propose ECC&quot;]
    B --&amp;gt; C[&quot;2000 NIST P-curves standardized&quot;]
    C --&amp;gt; D[&quot;2006 Curve25519 constant-time ladder&quot;]
    D --&amp;gt; E[&quot;2011 Ed25519 complete Edwards law&quot;]
    E --&amp;gt; F[&quot;2015-2023 Ristretto255 prime-order abstraction&quot;]
    F --&amp;gt; G[&quot;2024-2026 X25519 plus ML-KEM hybrid&quot;]
&lt;h3&gt;Generation one, and its own rescue&lt;/h3&gt;
&lt;p&gt;The NIST and SECG short-Weierstrass curves gave the world prime order, which we have already granted is a real strength. But their exceptional-case addition law and their branchy, easily-non-constant-time ladders left the two most dangerous jobs -- constant-time arithmetic and on-curve validation -- squarely with the implementer. Section 4 is the proof of what happens when an implementer drops them.&lt;/p&gt;
&lt;p&gt;The honest rebuttal belongs right here, though, because it is the reason &quot;P-256 is broken&quot; is the wrong sentence. In 2016, Joost Renes, Craig Costello, and Lejla Batina published efficient &lt;em&gt;complete&lt;/em&gt; addition formulas that work for every prime-order short-Weierstrass curve, with no exceptional cases to branch around [@rcb-2016]. Given those formulas and a vetted constant-time field implementation, P-256 is perfectly safe -- &lt;em&gt;harder to implement safely&lt;/em&gt;, not broken. Its danger is that the naive implementation is the insecure one, and for two decades the naive implementation is what shipped.&lt;/p&gt;
&lt;h3&gt;Generation two: the ladder that is constant-time by construction&lt;/h3&gt;
&lt;p&gt;In 2006, Daniel J. Bernstein introduced Curve25519, the Montgomery-form curve $y^2 = x^3 + 486662x^2 + x$ over the prime $2^{255} - 19$ [@curve25519-2006]. The form and its ladder trace back to Peter Montgomery&apos;s 1987 work on factorization, revived here for an entirely new purpose [@montgomery-1987]. Two design choices retire whole rows of the failure catalog for free.&lt;/p&gt;
&lt;p&gt;The first is the &lt;strong&gt;Montgomery ladder&lt;/strong&gt;: a scalar-multiplication method whose sequence of field operations is identical regardless of the secret bits. There is no secret-dependent branch to time, so the timing surface -- Brumley-Tuveri&apos;s surface -- closes by construction rather than by careful coding. Key agreement uses only the $x$-coordinate, so a peer sends 32 bytes and the $y$-coordinate never enters the arithmetic.&lt;/p&gt;

A scalar-multiplication algorithm that performs the same sequence of field operations for every bit of the secret -- one conditional swap plus a combined add-and-double per bit -- so its running time and memory-access pattern are independent of the scalar. It is constant-time by construction rather than by defensive programming.
&lt;p&gt;The second is &lt;strong&gt;twist security&lt;/strong&gt;. When someone hands you an $x$-coordinate, it corresponds either to a point on Curve25519 or to a point on its quadratic twist. Bernstein chose the parameters so that &lt;em&gt;both&lt;/em&gt; the curve and its twist have near-prime order [@curve25519-2006], [@safecurves-twist]. A hostile off-curve input therefore still lands you in a group where the discrete log is hard, and the invalid-curve attack of Section 4 is defused -- without any explicit on-curve check in the fast path.&lt;/p&gt;

A property of a curve whose quadratic twist also has near-prime order. Because a hostile input maps to a point on either the curve or its twist, and both groups are cryptographically hard, an off-curve input cannot be steered into a weak subgroup -- so invalid-curve attacks are defeated without an explicit on-curve validation step.
&lt;p&gt;The new cost is the seed of the next two generations: Curve25519 does not have prime order. Its group has cofactor 8, so low-order points exist.Bernstein states the orders exactly: the curve over its base field has $8 \times \ell$ points, where $\ell$ is the prime $2^{252} + 27742317\ldots$, and its twist has $4 \times \ell&apos;$ points for a different prime $\ell&apos;$ [@curve25519-2006]. That factor of 8 is the cofactor the next two generations spend their effort taming. To keep secret scalars inside the safe range, X25519 &lt;strong&gt;clamps&lt;/strong&gt; them. And clamping is the single most misunderstood step in the whole subject.&lt;/p&gt;

Forcing specific bits of an X25519 secret scalar: clearing the low three bits so the scalar is a multiple of the cofactor 8, clearing the high bit, and setting the second-highest bit so every scalar has the same length. Clamping constrains your own secret to sidestep small-subgroup and timing issues on your side. It is emphatically not validation of an input point someone else sends you.
&lt;p&gt;Clamping clears the low three bits (making your scalar a multiple of 8, so multiplying a low-order input contributes nothing on &lt;em&gt;your&lt;/em&gt; side), clears the top bit, and sets the second-highest bit so the ladder runs a fixed number of steps [@rfc7748].The X25519 clamp: &lt;code&gt;scalar[0] &amp;amp;= 248&lt;/code&gt; clears the low three bits, &lt;code&gt;scalar[31] &amp;amp;= 127&lt;/code&gt; clears bit 255, and &lt;code&gt;scalar[31] |= 64&lt;/code&gt; sets bit 254 [@rfc7748]. It protects you, the holder of the secret. It does nothing about a malicious low-order point arriving from outside -- which is why the Monero bug happened on a curve whose users clamped diligently.&lt;/p&gt;
&lt;h3&gt;Generation three: a complete addition law, and deterministic nonces&lt;/h3&gt;
&lt;p&gt;Curve25519 fixed key agreement, but signatures still lived on the branchy Weierstrass world or on Curve25519&apos;s cofactor. Ed25519 (2011) moved signing onto &lt;strong&gt;edwards25519&lt;/strong&gt;, the twisted-Edwards curve birationally equivalent to Curve25519 [@ed25519-2011]. The payoff is Harold Edwards&apos; 2007 normal form: a single addition formula with &lt;em&gt;no exceptional cases&lt;/em&gt; at all [@edwards-2007]. Doubling, adding, the identity -- one formula handles them, so the code has no secret-dependent branch and is naturally constant-time at the group-law level, retiring the incomplete-addition surface. Ed25519 also pairs the curve with &lt;strong&gt;deterministic nonces&lt;/strong&gt;, derived by hashing the message and a secret, which kill the catastrophic ECDSA per-signature-randomness disaster by construction (the nonce can no longer be repeated or biased by a &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;weak RNG&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The cofactor did not vanish, though. edwards25519 still has cofactor 8, which is why signature malleability, &lt;a href=&quot;https://paragmali.com/blog/the-signature-was-valid-the-message-was-forged-a-field-guide/&quot; rel=&quot;noopener&quot;&gt;non-canonical encodings&lt;/a&gt;, and disagreements about whether verification should be &quot;cofactored&quot; are real concerns -- and precisely why RFC 8032 specifies canonical-encoding checks, bounds on the signature scalar, and cofactor handling [@rfc8032]. The nonce disaster itself is this series&apos; nonce-reuse post&apos;s territory; here it is enough that determinism closes the generation half of it.&lt;/p&gt;
&lt;h3&gt;Generation four: quotient the cofactor out of existence&lt;/h3&gt;
&lt;p&gt;The fourth generation asks a sharper question. Instead of patching every protocol to handle cofactor 8, what if the cofactor simply did not exist at the level you program against? Mike Hamburg&apos;s Decaf (2015) showed how: build a genuine prime-order group &lt;em&gt;in the encoding&lt;/em&gt; on top of a cofactor curve, so that the torsion points are quotiented away and never appear as elements [@decaf-2015]. Ristretto255 is the cofactor-8 instantiation for edwards25519, standardized in RFC 9496 in 2023 [@rfc9496]. The consequence is the cleanest safety property in the whole article: &lt;strong&gt;decoding is validation.&lt;/strong&gt; A sequence of bytes either decodes to a unique, legitimate prime-order element or it is rejected. There is no low-order point to submit, because low-order points are not representable.&lt;/p&gt;

flowchart TD
    A[&quot;Wire bytes for a ristretto255 element&quot;] --&amp;gt; B{&quot;Valid canonical encoding?&quot;}
    B --&amp;gt;|No| C[&quot;Reject: not a group element&quot;]
    B --&amp;gt;|Yes| D[&quot;Decode yields a unique prime-order point&quot;]
    D --&amp;gt; E[&quot;No cofactor, no torsion, no small subgroup&quot;]
    E --&amp;gt; F[&quot;Element is safe to use directly&quot;]

A group of prime order with no cofactor, constructed over the cofactor-8 curve edwards25519 by encoding equivalence classes of points rather than raw points. Because only valid prime-order elements have encodings, decoding a byte string is itself the validation step, and the entire cofactor and small-subgroup failure class disappears at the type level.
&lt;p&gt;Curve448 and its prime-order sibling decaf448 play the conservative role: a larger, roughly 224-bit-security option standardized in the same family but far more thinly deployed than the 25519 curves [@ed448-goldilocks-2015], [@rfc9496]. Treat it as the extra-margin choice, not a rival to X25519&apos;s ubiquity.&lt;/p&gt;
&lt;h3&gt;The through-line&lt;/h3&gt;
&lt;p&gt;These four generations are not a strict succession where each kills the last. They coexist: P-256 remains FIPS-mandated, X25519 and Ed25519 split the key-agreement and signature jobs between them, and ristretto255 sits underneath new protocols that need a clean prime-order group. What each generation truly superseded was not its predecessor curve but the &lt;em&gt;burden on the person writing the code&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Each generation took a rule off the implementer and baked it into the curve, the group, or the encoding. Constant-time became the ladder&apos;s default. On-curve validation became twist security. Exception-free addition became the Edwards law. Cofactor handling became the ristretto255 encoding. The simple implementation kept becoming the secure one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Curve&lt;/th&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Field prime&lt;/th&gt;
&lt;th&gt;Cofactor&lt;/th&gt;
&lt;th&gt;Constant-time&lt;/th&gt;
&lt;th&gt;Twist secure&lt;/th&gt;
&lt;th&gt;Coordinates&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;P-256 (secp256r1)&lt;/td&gt;
&lt;td&gt;Short Weierstrass&lt;/td&gt;
&lt;td&gt;2^256 - 2^224 + 2^192 + 2^96 - 1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;With care (complete formulas)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Full (x, y)&lt;/td&gt;
&lt;td&gt;FIPS certificates, ECDSA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;secp256k1&lt;/td&gt;
&lt;td&gt;Short Weierstrass&lt;/td&gt;
&lt;td&gt;2^256 - 2^32 - 977&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;With care&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Full (x, y)&lt;/td&gt;
&lt;td&gt;Blockchain (Bitcoin, BIP-340)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curve25519 / X25519&lt;/td&gt;
&lt;td&gt;Montgomery&lt;/td&gt;
&lt;td&gt;2^255 - 19&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;By construction&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;x-only (32 bytes)&lt;/td&gt;
&lt;td&gt;Key agreement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;edwards25519 / Ed25519&lt;/td&gt;
&lt;td&gt;Twisted Edwards&lt;/td&gt;
&lt;td&gt;2^255 - 19&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;By construction&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;Compressed point&lt;/td&gt;
&lt;td&gt;Signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ristretto255&lt;/td&gt;
&lt;td&gt;Prime-order over edwards25519&lt;/td&gt;
&lt;td&gt;2^255 - 19&lt;/td&gt;
&lt;td&gt;1 (effective)&lt;/td&gt;
&lt;td&gt;By construction&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;32-byte encoding&lt;/td&gt;
&lt;td&gt;New prime-order protocols&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Four generations, each erasing one line of the failure catalog. Someone still had to name the law they were all obeying -- and to say out loud that a hard curve and a safe cryptosystem were never the same thing.&lt;/p&gt;
&lt;h2&gt;6. ECDLP-Hardness Is Not ECC-Security&lt;/h2&gt;
&lt;p&gt;In 2013, Daniel J. Bernstein and Tanja Lange put the entire argument of this article onto one website, with a claim engineered to provoke: if you implement the standard curves, chances are you are doing it wrong [@safecurves-index]. The website was SafeCurves, and it is the moment the field said the quiet part out loud.&lt;/p&gt;

There is a gap between ECDLP difficulty and ECC security ... if you implement the standard curves, chances are you&apos;re doing it wrong.
&lt;p&gt;SafeCurves names four concrete places that gap opens: implementations that return wrong results on rare curve points, that leak secrets when fed off-curve inputs, that branch in secret-dependent time, and that leak through cache-timing [@safecurves-index]. Every one of those is a surface from Section 4. And the criteria SafeCurves proposes -- rigidity, twist security, complete addition, ladder support, a handled cofactor -- are not a beauty contest. They are a single design program stated as a checklist: &lt;em&gt;choose the curve so that the easy implementation is the safe one.&lt;/em&gt; That is the whole of Section 5 compressed into one sentence.&lt;/p&gt;
&lt;p&gt;Which lets us state the thesis in its most general form.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Security is a property of the curve, the group, and the implementation together -- not of ECDLP alone. A hard discrete logarithm is necessary and nowhere near sufficient. Every generation in the previous section is this one sentence, applied to a different part of the stack.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The honest tension, handled carefully&lt;/h3&gt;
&lt;p&gt;It would be easy to read SafeCurves as a verdict that the NIST curves are unsafe. That reading is wrong, and getting it right is part of being an expert here rather than a partisan.&lt;/p&gt;

SafeCurves is a strong, deliberately opinionated argument from around 2013 to 2015, not a neutral consensus standard. NIST P-256 and P-384 do fail several of its criteria -- rigidity, twist security, complete addition in the naive formulas -- and yet they remain FIPS-mandated and, implemented with care, secure. The Renes-Costello-Batina complete formulas close the completeness gap in software [@rcb-2016], and vetted constant-time libraries close the rest. The defensible position is precise: P-256 is *harder to implement safely* than Curve25519, not *broken*. &quot;Prefer 25519 for new designs&quot; and &quot;P-256 is unsafe&quot; are different claims, and only the first is true [@safecurves-index].
&lt;p&gt;The scorecard below is the honest version. Read a &quot;No&quot; or a &quot;With care&quot; as &quot;the naive implementation needs help here,&quot; never as &quot;this curve is broken.&quot; Prime order is where the NIST curves quietly win, and it is exactly what the 25519 curves trade away.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Curve&lt;/th&gt;
&lt;th&gt;Rigidity&lt;/th&gt;
&lt;th&gt;Twist security&lt;/th&gt;
&lt;th&gt;Complete addition&lt;/th&gt;
&lt;th&gt;Ladder-friendly&lt;/th&gt;
&lt;th&gt;Prime order&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;P-256&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;With care&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;secp256k1&lt;/td&gt;
&lt;td&gt;Partly&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;With care&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Curve25519 / X25519&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (cofactor 8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;No (cofactor 8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ristretto255&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The footnote that keeps this table honest: every &quot;No&quot; for the NIST curves is a statement about the &lt;em&gt;default&lt;/em&gt; implementation, not the mathematics. secp256k1 is marked &quot;Partly&quot; rigid because its parameters follow a stated efficiency rationale rather than an unexplained seed. Give P-256 complete formulas and a constant-time field, and its row stops mattering. That is why the state of the art is not a single crowned curve but a matter of knowing which curve already does which job for you -- and in 2026, that picture is unusually settled.&lt;/p&gt;
&lt;h2&gt;7. State of the Art (2024-2026): Which Curve Does Which Job&lt;/h2&gt;
&lt;p&gt;There is no single best curve to crown. The state of the art is a small, stable set, each member owning a job -- plus one transition quietly rewriting every handshake on the internet.&lt;/p&gt;
&lt;h3&gt;X25519 owns key agreement&lt;/h3&gt;
&lt;p&gt;For ephemeral key agreement, X25519 is the default answer. It is constant-time by construction, twist-secure, and moves 32 bytes per side; it is a standardized TLS 1.3 named group [@rfc8446], the Diffie-Hellman step in WireGuard&apos;s Noise handshake [@wireguard-protocol], and the key exchange in Signal, all built on the same Curve25519 arithmetic [@rfc7748]. For plain Diffie-Hellman you need no explicit point-validation code, because twist security already handles hostile inputs -- the operational rules reduce to rejecting the all-zero output and running the shared secret through a KDF rather than using it raw.&lt;/p&gt;
&lt;h3&gt;P-256 ECDSA owns FIPS certificates&lt;/h3&gt;
&lt;p&gt;P-256 is co-dominant, and for a specific reason: it is FIPS-mandatory for certificates [@fips-186-5]. Among the curves used in web certificates it is overwhelmingly the one [@ssl-pulse], now implemented with complete formulas or vetted constant-time assembly plus point validation on untrusted inputs [@sp-800-56a-r3]. Where a compliance regime names the curve, P-256 done carefully is exactly right.&lt;/p&gt;
&lt;h3&gt;Ed25519 is now FIPS-approved too&lt;/h3&gt;
&lt;p&gt;The important recent shift for signatures is regulatory. FIPS 186-5, published February 3, 2023, approves EdDSA alongside ECDSA [@fips-186-5], so Ed25519 is now both the technically cleaner option and a compliant one. It is standard in OpenSSH, in Git commit signing, and in package-signing tools such as minisign and OpenBSD&apos;s signify. Ed25519 needs about 87,500 CPU cycles to sign and about 273,000 to verify -- near 134,000 per signature when many are checked in one batch [@ed25519-2011] -- with the eBACS suite serving as the live cross-CPU benchmark authority [@bench-cr-yp-to]. The state-of-the-art guidance: Ed25519 for new designs, ECDSA where compliance compels it [@rfc8032].&lt;/p&gt;
&lt;h3&gt;secp256k1 owns blockchain, and only blockchain&lt;/h3&gt;
&lt;p&gt;secp256k1 carries an enormous deployment -- Bitcoin, Ethereum, and their descendants -- through ECDSA and, since Taproot, BIP-340 Schnorr signatures [@bip-340]. It is nonetheless essentially absent from web PKI and TLS. On any decision about a certificate or a general key exchange, secp256k1 is not a candidate; it is the answer only when the job is blockchain interoperability.&lt;/p&gt;
&lt;h3&gt;Ristretto255 is the prime-order substrate for new protocols&lt;/h3&gt;
&lt;p&gt;When a protocol needs a clean prime-order group -- password-authenticated key exchange, verifiable random functions, oblivious pseudorandom functions, threshold signatures, zero-knowledge systems -- ristretto255 (RFC 9496) is the current substrate [@rfc9496]. It is the recommended ciphersuite of the FROST threshold-Schnorr standard, which is defined generically over any prime-order group and also specifies Ed25519, Ed448, P-256, and secp256k1 suites [@rfc9591]; and it ships in production inside Signal&apos;s libsignal, whose zkgroup and zkcredential modules build on ristretto255 points [@libsignal].&lt;/p&gt;
&lt;h3&gt;The post-quantum overlay: ECC wrapped, not replaced&lt;/h3&gt;
&lt;p&gt;The headline of 2024 to 2026 is not a new curve; it is a wrapper. The hybrid &lt;strong&gt;X25519 plus ML-KEM-768&lt;/strong&gt; combines the classical curve with the &lt;a href=&quot;https://paragmali.com/blog/the-thirty-year-migration-ships-in-a-pip-install-how-post-qu/&quot; rel=&quot;noopener&quot;&gt;NIST-standardized lattice KEM&lt;/a&gt;, and it is now shipping by default in Chrome and across Cloudflare&apos;s network [@cloudflare-pq-2024], and available in OpenSSH 9.9, released September 2024 [@openssh-9.9]. The motive is &quot;harvest now, decrypt later&quot;: an adversary can record today&apos;s X25519 traffic and wait for a quantum computer, so the hybrid forces them to break &lt;em&gt;both&lt;/em&gt; the curve and the lattice.Post-quantum-encrypted traffic on Cloudflare&apos;s network nearly doubled across 2025, from about 29% of human-generated web requests at the start of the year to roughly 52% by early December [@cloudflare-radar-2025], with a live tracker following the trend [@cloudflare-radar-pq-tracker].&lt;/p&gt;
&lt;p&gt;The enabling techniques are themselves state of the art. The Montgomery ladder, the Renes-Costello-Batina complete formulas [@rcb-2016], and machine-checked, correct-by-construction field arithmetic (fiat-crypto, whose code for Curve25519 and P-256 ships inside BoringSSL) are what make these curves safe in practice rather than merely on paper [@fiat-crypto].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; It is not a single best curve. It is &quot;the right curve for the job&quot; -- X25519 for key agreement, Ed25519 for new signatures, P-256 where FIPS compels it, secp256k1 only for blockchain, ristretto255 for new prime-order protocols -- plus the hybrid transition that wraps the classical curve in a post-quantum KEM.&lt;/p&gt;
&lt;/blockquote&gt;

FIPS 186-5 approves EdDSA, so Ed25519 is now a compliant signature choice [@fips-186-5]; ECDSA over P-256 remains mandated for certificates; and SP 800-56A Rev. 3 requires point and subgroup validation for untrusted inputs in key establishment [@sp-800-56a-r3]. Compliance and good engineering point the same way more often than they used to.
&lt;p&gt;&quot;Which curve owns which job&quot; is a summary, not a decision procedure. When the jobs overlap -- when a compliance regime, a blockchain, and a threshold protocol pull in three directions at once -- you need the head-to-head.&lt;/p&gt;
&lt;h2&gt;8. The Decision Matrix&lt;/h2&gt;
&lt;p&gt;The deployed curves do not all compete for the same job, so the useful comparison is per task -- and it hinges on one axis the benchmarks never advertise: &lt;em&gt;how much of the failure catalog you must defend against by hand.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Key agreement: X25519 unless something forces your hand&lt;/h3&gt;
&lt;p&gt;Default to X25519. Its twist security and constant-time ladder mean the dangerous inputs are already handled, so your implementation burden is low: reject the all-zero shared secret and run the result through a KDF. Choose P-256 ECDH only where a compliance regime mandates it -- and then take on the full burden the curve leaves you: validate the peer point is on the curve &lt;em&gt;and&lt;/em&gt; in the prime-order subgroup, use complete formulas or vetted constant-time code, and never feed the raw x-coordinate to anything but a KDF [@sp-800-56a-r3], [@rcb-2016]. Reach for X448 only when a policy demands a larger margin.&lt;/p&gt;
&lt;p&gt;| Curve | Form | Security | Cofactor | Constant-time | Twist secure | Element | Safety burden | Compliance |
| --- | --- | --- | --- | --- | --- | --- | --- |
| X25519 | Montgomery | ~128-bit | 8 | By construction | Yes | 32 B | Low: reject zero, KDF the output | Not required for certificates |
| P-256 ECDH | Short Weierstrass | ~128-bit | 1 | With care | No | 33 B compressed | High: validate point and subgroup, constant-time | FIPS-mandated |
| X448 | Montgomery | ~224-bit | 4 | By construction | Yes | 56 B | Low | Extra-margin option |&lt;/p&gt;
&lt;p&gt;The one rule that survives every row: never use the raw shared x-coordinate as a key. Run it through a KDF, as this series&apos; key-derivation installment argues.&lt;/p&gt;
&lt;h3&gt;Signatures: Ed25519 for new work, ECDSA where mandated&lt;/h3&gt;
&lt;p&gt;For new designs, Ed25519 wins on the axis that matters -- it removes the per-signature randomness that has destroyed more ECDSA keys than any cryptanalysis, and its complete addition law removes the exceptional cases [@rfc8032]. Use ECDSA over P-256 only where a standard names it, and then defend it deliberately: RFC 6979 deterministic nonces, constant-time scalar multiplication, and point validation [@rfc6979]. BIP-340 Schnorr is the right choice for new Bitcoin work and nowhere else [@bip-340].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Nonce&lt;/th&gt;
&lt;th&gt;Nonce-failure blast radius&lt;/th&gt;
&lt;th&gt;Addition law&lt;/th&gt;
&lt;th&gt;Malleability rule&lt;/th&gt;
&lt;th&gt;Sizes&lt;/th&gt;
&lt;th&gt;Batch verify&lt;/th&gt;
&lt;th&gt;Compliance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;td&gt;Deterministic (hash-based)&lt;/td&gt;
&lt;td&gt;None by construction&lt;/td&gt;
&lt;td&gt;Complete Edwards&lt;/td&gt;
&lt;td&gt;Enforce canonical S and cofactor checks&lt;/td&gt;
&lt;td&gt;64 B sig, 32 B key&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;FIPS 186-5 approved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECDSA P-256&lt;/td&gt;
&lt;td&gt;Random, or RFC 6979 deterministic&lt;/td&gt;
&lt;td&gt;Catastrophic: a reused or biased nonce leaks the key&lt;/td&gt;
&lt;td&gt;Incomplete Weierstrass&lt;/td&gt;
&lt;td&gt;Low-S recommended&lt;/td&gt;
&lt;td&gt;~64-72 B sig, 33 B key&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;FIPS-mandated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BIP-340 Schnorr&lt;/td&gt;
&lt;td&gt;Deterministic with auxiliary randomness&lt;/td&gt;
&lt;td&gt;Reduced versus ECDSA&lt;/td&gt;
&lt;td&gt;secp256k1&lt;/td&gt;
&lt;td&gt;Fixed by encoding&lt;/td&gt;
&lt;td&gt;64 B sig, 32 B x-only key&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Bitcoin only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Group abstraction: ristretto255 over hand-rolled cofactor clearing&lt;/h3&gt;
&lt;p&gt;If you are building a new prime-order protocol -- a PAKE, a VRF, an OPRF, a threshold scheme, a zero-knowledge system -- build it on ristretto255 rather than hand-rolling cofactor clearing over raw edwards25519 [@rfc9496]. Decode-is-validation removes the last implementation choice that could go wrong, which is the entire point of Generation four.&lt;/p&gt;
&lt;h3&gt;The cross-primitive view&lt;/h3&gt;
&lt;p&gt;Two comparisons sit outside this table but belong in your head. Against &lt;a href=&quot;https://paragmali.com/blog/rsa-is-a-trapdoor-not-a-cryptosystem-oaep-pss-and-the-25-yea/&quot; rel=&quot;noopener&quot;&gt;RSA&lt;/a&gt;, elliptic curves do the same public-key jobs with smaller keys and faster operations, and they share the reframe this series keeps returning to: the math was never the thing that broke. Against post-quantum schemes, ECC is not yet replaced -- the classical curve is the defense-in-depth half of a hybrid, present precisely because a lattice KEM is young and a recorded handshake is forever. Neither comparison changes a single row above; both change how long these rows stay valid.&lt;/p&gt;
&lt;p&gt;Every row here assumes the curve&apos;s discrete log is genuinely hard. That assumption has a precise boundary -- and a hard expiration date.&lt;/p&gt;
&lt;h2&gt;9. Where the Hardness Lives, and Its Ceiling&lt;/h2&gt;
&lt;p&gt;Here is the uncomfortable pair of facts a first course tends to skip. For a well-chosen curve, the security level is &lt;em&gt;exactly&lt;/em&gt; the generic bound -- there is no hidden margin above it, but nothing better is known either. And that entire edifice falls to a quantum computer in polynomial time.&lt;/p&gt;
&lt;h3&gt;The generic bound, matched from both sides&lt;/h3&gt;
&lt;p&gt;The best classical attacks on a well-chosen curve are &lt;em&gt;generic&lt;/em&gt; -- they work in any group and know nothing special about elliptic curves. Pollard rho finds a discrete log in about $\sqrt{n}$ group operations for a group of order $n$, using parallel collision search with distinguished points and almost no memory [@safecurves-rho]. Pohlig-Hellman reduces the problem to the largest prime factor of the group order, which is the deep reason prime order matters: a smooth order is a soft target. Against those upper bounds sits a matching lower bound -- Shoup (1997) and Nechaev (1994) proved that &lt;em&gt;any&lt;/em&gt; generic algorithm needs $\Omega(\sqrt{n})$ operations. Upper and lower bounds meet.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For a well-chosen curve, the security level is the generic square-root bound itself: about $\sqrt{n}$ work, matched by a proven lower bound. That is solid -- but it means there is no safety margin hiding above the number, and, as we are about to see, a quantum cliff waiting below it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That closed gap is genuinely unusual among cryptographic assumptions, and it is the whole reason a 256-bit curve is trusted at the 128-bit level.Most hardness assumptions leave a gap between the best known attack and the best provable lower bound. For generic ECDLP on a well-chosen curve, the two coincide at $\sqrt{n}$. You are trusting a bound that is tight, not merely unbroken. The reason no &lt;em&gt;better&lt;/em&gt; attack exists is the absence we opened with: no index-calculus method is known that beats $\sqrt{n}$ on prime-field curves. Semaev&apos;s summation polynomials and the programs built on them have not produced one. The economic case of Section 2 is really this limit, seen from the other side.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symmetric security&lt;/th&gt;
&lt;th&gt;Elliptic-curve size&lt;/th&gt;
&lt;th&gt;RSA / finite-field DH modulus&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;112-bit&lt;/td&gt;
&lt;td&gt;224-bit&lt;/td&gt;
&lt;td&gt;2048-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;256-bit&lt;/td&gt;
&lt;td&gt;3072-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;192-bit&lt;/td&gt;
&lt;td&gt;384-bit&lt;/td&gt;
&lt;td&gt;7680-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256-bit&lt;/td&gt;
&lt;td&gt;512-bit&lt;/td&gt;
&lt;td&gt;15360-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The table is the standard security-level mapping, and its middle row is the sentence everyone quotes: 256-bit ECC stands in for 3072-bit RSA at roughly 128-bit strength, because the curve attacker pays $\sqrt{n}$ while the RSA attacker enjoys sub-exponential index calculus [@safecurves-rho], [@miller-1985].&lt;/p&gt;
&lt;h3&gt;What &quot;well-chosen&quot; rules out&lt;/h3&gt;
&lt;p&gt;The generic bound holds only for curves that avoid a short list of special structures, and each item on that list is a curve that &lt;em&gt;looks&lt;/em&gt; fine but collapses. The MOV reduction (Menezes, Okamoto, and Vanstone, 1993) maps a curve with low embedding degree -- supersingular curves are the extreme case -- into a finite field where index calculus applies, collapsing its ECDLP [@mov-1993]. Anomalous, or trace-one, curves, where the number of points equals the field prime, admit an additive transfer that solves the discrete log in &lt;em&gt;polynomial&lt;/em&gt; time (Smart; independently Satoh-Araki and Semaev) [@smart-1999]. And the small-subgroup and twist conditions of Section 4 round out the list. &quot;Well-chosen&quot; is exactly &quot;avoids these.&quot;&lt;/p&gt;
&lt;h3&gt;The one bound that is not the square root&lt;/h3&gt;
&lt;p&gt;Every limit so far is classical. The exception is the one that sets the deadline. Shor&apos;s algorithm (1997) solves both factoring and discrete logarithms -- including ECDLP -- in polynomial time on a fault-tolerant quantum computer [@shor-1997]. Resource estimates make it concrete: at equal classical security ECC is the easier quantum target -- breaking a 256-bit curve needs roughly 2,330 logical qubits and about $1.3 \times 10^{11}$ Toffoli gates, versus roughly 6,100 logical qubits for RSA-3072, so ECC&apos;s smaller keys are the smaller quantum target and fall first [@roetteler-2017]. No choice of curve escapes this; it is a property of the group structure Shor exploits, not of any parameter.&lt;/p&gt;

This is why today&apos;s ECDH traffic is already on a clock, before any quantum computer exists. An adversary records your X25519 handshakes now and decrypts them the day a machine can run Shor at scale. The recorded session is not refreshed; its forward secrecy is only as good as the assumption that the curve stays hard forever -- which Shor says it will not [@shor-1997]. That single fact, not any classical advance, is what put the hybrid transition of Section 7 on every roadmap.
&lt;p&gt;If the mathematics is this settled -- a closed classical bound and a known quantum ceiling -- then everything still open lives in the code, the trust, and the migration. That is where the real frontier is.&lt;/p&gt;
&lt;h2&gt;10. What Remains Genuinely Unsettled&lt;/h2&gt;
&lt;p&gt;The core science is settled, and the failure catalog and its defenses are not in dispute. What is open sits at the edges -- and two of these are live in your dependency tree right now.&lt;/p&gt;
&lt;h3&gt;Constant-time is a perpetually moving target&lt;/h3&gt;
&lt;p&gt;Montgomery ladders and complete formulas close the obvious channels, and still the leaks keep reappearing at finer grain. Minerva recovered secp256r1 private keys from about 2,100 signatures by measuring how the nonce&apos;s bit-length shifted the signing time [@minerva-2020]. TPM-Fail pulled 256-bit ECDSA keys out of an Intel firmware TPM and an STMicroelectronics TPM despite their certifications [@tpm-fail-2020]. LadderLeak broke ECDSA with less than one bit of nonce leakage [@ladderleak-2020]. There is no general, machine-checkable guarantee of constant-time behavior that survives arbitrary compilers and microarchitectures. The lattice step that turns these leaks into keys belongs to this series&apos; &lt;a href=&quot;https://paragmali.com/blog/one-number-used-twice-how-a-repeated-nonce-hands-over-your-p/&quot; rel=&quot;noopener&quot;&gt;nonce-reuse post&lt;/a&gt;; the open problem here is the leakage itself.&lt;/p&gt;
&lt;h3&gt;The NIST-curve trust question is sociological, not solved&lt;/h3&gt;
&lt;p&gt;No weakness has ever been demonstrated in P-256 or P-384, and they remain FIPS-mandated [@safecurves-index]. Yet Dual EC DRBG proved that NIST-supplied constants &lt;em&gt;could&lt;/em&gt; be poisoned, which is why a large part of the community prefers rigid curves for new work [@shumow-ferguson-2007]. This is not the kind of question a proof can close -- you cannot prove the absence of a motive. The practical resolution is a posture, not a theorem: prefer 25519 for new designs, use P-256 with care where compliance compels it.&lt;/p&gt;
&lt;h3&gt;The cofactor long tail&lt;/h3&gt;
&lt;p&gt;RFC 9496 eliminates the cofactor failure class -- for the protocols that adopt it [@rfc9496]. The long tail of deployed EdDSA and X25519 protocols that predate or simply ignore ristretto255 remains a live source of subgroup bugs, with Monero the canonical example [@loup-vaillant-cofactor]. This is a migration problem, not a research problem, which somehow makes it more stubborn rather than less.&lt;/p&gt;
&lt;h3&gt;The post-quantum migration, past the easy part&lt;/h3&gt;
&lt;p&gt;Hybrid key agreement is deploying fast [@cloudflare-radar-2025], but the interesting questions start after the first hybrid ships: how long ECC&apos;s small keys keep it useful inside hybrids, whether and when to drop the classical half, which combiner and negotiation are right, and the genuinely harder migration on the signature side, where the post-quantum replacements are large and awkward.&lt;/p&gt;
&lt;h3&gt;Formal verification at scale&lt;/h3&gt;
&lt;p&gt;fiat-crypto ships proven-correct field arithmetic for Curve25519 and P-256 inside BoringSSL, which is a real milestone [@fiat-crypto]. But end-to-end verification -- field, then group law, then protocol, then constant-time behavior on the actual target instruction set -- is not yet routine. The field-arithmetic layer is solved; the stack above it is not.&lt;/p&gt;
&lt;p&gt;There is also a structural non-problem worth naming, so you can stop worrying about it: no one has found a faster-than-square-root attack on prime-field ECDLP, and the summation-polynomial work that raised hopes lives in binary and composite fields -- one more reason NIST now steers new work to prime-field curves: SP 800-186 deprecates the binary and Koblitz curves, retaining them only for legacy interoperability, and FIPS 186-5 dropped them from approval for new signatures [@sp-800-186], [@fips-186-5].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Of the five open fronts, two are the ones likely to be in &lt;em&gt;your&lt;/em&gt; stack today: a non-constant-time scalar multiplication somewhere in a dependency, and a protocol built on raw edwards25519 that never adopted ristretto255. The other three are the industry&apos;s problem. These two are yours.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Enough theory and frontier. Here is the entire article compressed into rules you can apply on Monday -- each one a named break from the catalog, turned inside out.&lt;/p&gt;
&lt;h2&gt;11. The Practical Guide and the Misuse Catalog&lt;/h2&gt;
&lt;p&gt;Every rule below is one row of the failure catalog, inverted. If you remember nothing else from this article, remember the decision tree.&lt;/p&gt;

flowchart TD
    A{&quot;What is the job?&quot;} --&amp;gt; B[&quot;Key agreement&quot;]
    A --&amp;gt; C[&quot;Signature&quot;]
    A --&amp;gt; D[&quot;Prime-order protocol&quot;]
    A --&amp;gt; E[&quot;Long-lived confidentiality&quot;]
    B --&amp;gt; B1{&quot;FIPS-mandated curve?&quot;}
    B1 --&amp;gt;|No| B2[&quot;X25519, reject zero output, KDF the result&quot;]
    B1 --&amp;gt;|Yes| B3[&quot;P-256 ECDH, validate point and subgroup, constant-time, KDF&quot;]
    C --&amp;gt; C1{&quot;New design?&quot;}
    C1 --&amp;gt;|Yes| C2[&quot;Ed25519, enforce canonical S, strict library&quot;]
    C1 --&amp;gt;|Mandated| C3[&quot;ECDSA P-256, RFC 6979 nonce, constant-time, validate points&quot;]
    D --&amp;gt; D1[&quot;Build on ristretto255 or decaf448&quot;]
    E --&amp;gt; E1[&quot;Wrap X25519 in the ML-KEM-768 hybrid&quot;]
&lt;h3&gt;The decision rules, in words&lt;/h3&gt;
&lt;p&gt;For &lt;strong&gt;key agreement&lt;/strong&gt;, reach for X25519 by default: constant-time by construction, twist security handling hostile inputs, and one operational rule -- run the shared secret through a KDF and reject the all-zero output, never using the raw x-coordinate as a key [@rfc7748]. Use P-256 ECDH only where FIPS compels it, and then validate the peer point on the curve &lt;em&gt;and&lt;/em&gt; in the prime-order subgroup, use complete formulas or a vetted constant-time library, and KDF the result [@sp-800-56a-r3], [@rcb-2016].&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;signatures&lt;/strong&gt;, choose Ed25519 for new designs -- deterministic nonces, a complete addition law, and a strict library that enforces the canonical-S check [@rfc8032]. Use ECDSA over P-256 only where mandated, with RFC 6979 deterministic nonces, constant-time scalar multiplication, and point validation [@rfc6979].&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;point handling&lt;/strong&gt;, always validate untrusted points -- on the curve and in the right subgroup -- remember that clamping is not validation, and never accept explicit curve parameters from a certificate or message; use named curves only, which is the whole lesson of Curveball [@cve-2020-0601]. For &lt;strong&gt;new prime-order protocols&lt;/strong&gt;, build on ristretto255 or decaf448 instead of hand-rolling cofactor clearing [@rfc9496]. For &lt;strong&gt;long-lived confidentiality&lt;/strong&gt;, deploy the X25519 plus ML-KEM-768 hybrid now rather than waiting for a pure post-quantum replacement.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Key parameters&lt;/th&gt;
&lt;th&gt;Because (the named break)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Key agreement (default)&lt;/td&gt;
&lt;td&gt;X25519&lt;/td&gt;
&lt;td&gt;Reject zero output, KDF the secret&lt;/td&gt;
&lt;td&gt;Raw-x misuse, twist security handles inputs [@rfc7748]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key agreement (FIPS)&lt;/td&gt;
&lt;td&gt;P-256 ECDH&lt;/td&gt;
&lt;td&gt;Validate point and subgroup, constant-time&lt;/td&gt;
&lt;td&gt;Invalid-curve TLS recovery [@jager-2015], timing [@brumley-tuveri-2011]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature (new)&lt;/td&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;td&gt;Canonical S, strict library&lt;/td&gt;
&lt;td&gt;Nonce disaster and malleability [@rfc8032]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature (mandated)&lt;/td&gt;
&lt;td&gt;ECDSA P-256&lt;/td&gt;
&lt;td&gt;RFC 6979 nonce, constant-time, validate&lt;/td&gt;
&lt;td&gt;Nonce reuse or bias [@rfc6979]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prime-order protocol&lt;/td&gt;
&lt;td&gt;ristretto255 / decaf448&lt;/td&gt;
&lt;td&gt;Decode is validation&lt;/td&gt;
&lt;td&gt;Cofactor double-spend [@loup-vaillant-cofactor]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certificates&lt;/td&gt;
&lt;td&gt;Named curves only&lt;/td&gt;
&lt;td&gt;Reject explicit parameters&lt;/td&gt;
&lt;td&gt;Curveball [@cve-2020-0601]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-lived secrecy&lt;/td&gt;
&lt;td&gt;X25519 + ML-KEM-768&lt;/td&gt;
&lt;td&gt;Hybrid combiner now&lt;/td&gt;
&lt;td&gt;Harvest-now-decrypt-later [@shor-1997]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Why &quot;clamping is not validation&quot; is a rule, not a slogan&lt;/h3&gt;
&lt;p&gt;The single most common confusion deserves a demonstration rather than an assertion. On a curve with a cofactor, an attacker can add a low-order point to a legitimate one. Clamping your secret scalar hides that tampering inside a Diffie-Hellman product -- so clamping alone cannot even detect it -- while an explicit prime-order subgroup check catches it immediately.&lt;/p&gt;
&lt;p&gt;{`
// Toy curve y^2 = x^3 + x over F_53. Group order 68 = 4 * 17 (cofactor 4, prime l = 17).
const p = 53n, a = 1n, ELL = 17n;
const mod = x =&amp;gt; ((x % p) + p) % p;
function inv(x){ let r=1n,b=mod(x),e=p-2n; while(e&amp;gt;0n){ if(e&amp;amp;1n) r=mod(r&lt;em&gt;b); b=mod(b&lt;/em&gt;b); e&amp;gt;&amp;gt;=1n;} return r; }
const eq = (P,Q) =&amp;gt; (P===null&amp;amp;&amp;amp;Q===null) || (P&amp;amp;&amp;amp;Q&amp;amp;&amp;amp;P[0]===Q[0]&amp;amp;&amp;amp;P[1]===Q[1]);
function add(P,Q){
  if(P===null) return Q; if(Q===null) return P;
  const [x1,y1]=P,[x2,y2]=Q;
  if(x1===x2 &amp;amp;&amp;amp; mod(y1+y2)===0n) return null;
  const s=(x1===x2&amp;amp;&amp;amp;y1===y2)? mod((3n&lt;em&gt;x1&lt;/em&gt;x1+a)&lt;em&gt;inv(2n&lt;/em&gt;y1)) : mod((y2-y1)&lt;em&gt;inv(x2-x1));
  const x3=mod(s&lt;/em&gt;s-x1-x2); return [x3, mod(s*(x1-x3)-y1)];
}
function mul(k,P){ let R=null,b=P; while(k&amp;gt;0n){ if(k&amp;amp;1n) R=add(R,b); b=add(b,b); k&amp;gt;&amp;gt;=1n;} return R; }&lt;/p&gt;
&lt;p&gt;const P = [6n,13n];                 // legitimate prime-order point (order 17)
const Low = [0n,0n];                // low-order point (order 2, divides the cofactor 4)
const tampered = add(P, Low);       // attacker adds a low-order component
console.log(&apos;P and P+Low are distinct encodings?&apos;, !eq(P, tampered));&lt;/p&gt;
&lt;p&gt;// Clamping forces the secret to a multiple of the cofactor, so it HIDES the tamper:
const kClamped = 4n * 5n;           // 20, a multiple of the cofactor 4
console.log(&apos;clamped [k]P equals clamped &lt;a href=&quot;P+Low&quot; rel=&quot;noopener&quot;&gt;k&lt;/a&gt;?&apos;, eq(mul(kClamped,P), mul(kClamped,tampered)));&lt;/p&gt;
&lt;p&gt;// The real defence is an explicit prime-order subgroup check [l]X == identity:
console.log(&apos;subgroup check -- [l]P is identity?&apos;, mul(ELL,P) === null);
console.log(&apos;subgroup check -- &lt;a href=&quot;P+Low&quot; rel=&quot;noopener&quot;&gt;l&lt;/a&gt; is identity?&apos;, mul(ELL,tampered) === null);
`}&lt;/p&gt;
&lt;p&gt;The clamped multiplication reports the tampered point as identical, and only the $[\ell]X = \mathcal{O}$ subgroup check flags it. That is the Monero bug in nine lines: the code that needed to reject the tampered element was not the clamp, it was the subgroup test.&lt;/p&gt;

On most systems you can see the real curves in seconds. `ssh -Q key` and `ssh -Q kex` list your SSH key and key-exchange algorithms -- look for `ssh-ed25519` and a hybrid like `mlkem768x25519-sha256`. For TLS, `openssl s_client -connect example.com:443 -tls1_3` reports the negotiated group, often `X25519` or the hybrid `X25519MLKEM768`. Versions differ, but these tell you which curve and which post-quantum wrapper your traffic actually uses today, rather than which one you assume it does.
&lt;h3&gt;The misuse catalog&lt;/h3&gt;
&lt;p&gt;Each antipattern below is exactly one violated rule, and each maps to a named break you have now met:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Using the raw ECDH x-coordinate as a key.&lt;/strong&gt; Run it through a KDF instead; the shared secret is an input to key derivation, not a key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treating clamping as validation.&lt;/strong&gt; Clamping constrains your secret; it does not filter a hostile input point (Monero [@loup-vaillant-cofactor]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skipping point validation on Weierstrass curves.&lt;/strong&gt; The formula ignores $b$, so an off-curve point is processed on a weaker curve (invalid-curve, Jager et al. [@jager-2015]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accepting explicit curve parameters.&lt;/strong&gt; Trusting a generator you did not choose forges chains (Curveball [@cve-2020-0601]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-constant-time or unvalidated P-256.&lt;/strong&gt; Data-dependent timing is an oracle even with honest inputs (Brumley-Tuveri [@brumley-tuveri-2011]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ignoring the cofactor or non-canonical encodings in edwards25519 protocols.&lt;/strong&gt; Low-order points and duplicate encodings break uniqueness (Monero [@loup-vaillant-cofactor]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rolling your own curve arithmetic.&lt;/strong&gt; Use fiat-crypto-backed libraries; hand-written field code is where constant-time dies [@fiat-crypto].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reusing or biasing ECDSA nonces.&lt;/strong&gt; A repeated or leaky nonce hands over the key through a lattice (RFC 6979 fixes the generation half [@rfc6979]).&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If your time is limited, hunt these first: the raw ECDH x-coordinate used directly as a key; a P-256 ECDH path that neither validates the peer point nor runs in constant time; and any code path that accepts explicit curve parameters instead of a named curve. Those three account for most real elliptic-curve findings in production code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Read the whole failure catalog again as this checklist, and one sentence remains true of every row.&lt;/p&gt;
&lt;h2&gt;12. The Curve Was Hard; The Gap Was Soft&lt;/h2&gt;
&lt;p&gt;Return to where we started. The NSA phoned Microsoft, and nobody solved a discrete logarithm [@cve-2020-0601]. That is not an anomaly; it is the whole pattern in one image. Across three decades of deployed elliptic-curve cryptography, almost no real break ever inverted the function the textbooks call hard. Attackers fed the implementation a point it should have rejected -- off the curve, or of small order, or a certificate&apos;s own untrusted generator (invalid-curve, small-subgroup, Curveball) [@jager-2015]. They added a low-order component that clamping never filtered (Monero) [@loup-vaillant-cofactor]. Or they simply read the clock while the code multiplied a secret (Brumley-Tuveri, and the Minerva-TPM-Fail-LadderLeak lineage) [@brumley-tuveri-2011].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The curve was hard; the gap was soft. ECDLP-hardness bought the small keys and the speed, and it held every single time. The breaks all lived in the code, the trust, and the encoding around the curve -- the space between a hard problem and a safe system.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So &quot;using elliptic curves safely&quot; is not one decision but a stack: the right curve for the job, point and subgroup validation on every untrusted input, constant-time scalar multiplication, and -- the deepest lesson the evolution kept teaching -- a preference for constructions where the dangerous case cannot be reached at all, as ristretto255 makes literal [@rfc9496]. Remove any single layer and the gap reopens. And the horizon is honest about its own limit: the destination is not a better curve, because no curve survives Shor [@shor-1997]. It is wrapping the curve in a post-quantum hybrid and buying time.&lt;/p&gt;

The private curve stayed hard; the code around it confessed.
&lt;p&gt;Which makes the diagnostic question we opened with finally answerable. When a point you did not generate arrives, what does your code reveal about it -- through its result, its timing, or the parameters it trusts? Using elliptic curves safely is the discipline of making that answer &lt;em&gt;nothing an attacker can measure.&lt;/em&gt;&lt;/p&gt;


No weakness has ever been shown in P-256 or P-384. The real concern is *rigidity*: their generator and coefficient come from an unexplained seed, and the Dual EC DRBG affair proved that NIST-supplied constants *could* be poisoned in some other primitive. That history is why many engineers prefer rigid curves like Curve25519 for new designs. But &quot;prefer 25519&quot; is a different claim from &quot;P-256 is broken,&quot; and only the first one is supported.


No -- it is *harder to implement safely*, not broken. Its naive addition formulas have exceptional cases and it lacks twist security, so a careless implementation can leak. Complete addition formulas (Renes-Costello-Batina) and vetted constant-time libraries close those gaps. Implemented carefully, P-256 is secure and remains FIPS-mandated for certificates.


For plain Diffie-Hellman, no on-curve check is required: Curve25519&apos;s twist security means a hostile input still lands in a hard group, so you just reject the all-zero output and run the result through a KDF. But low-order points still matter for protocols that need a genuine prime-order group. Validate or clear the cofactor there, or better, build on ristretto255 and let decoding do the validation.


No. Clamping constrains *your own* secret scalar so a low-order component contributes nothing to your Diffie-Hellman product. It does nothing to validate a malicious input point, and it does not make a tampered point canonical. The Monero cofactor bug is exactly this confusion -- the fix was an explicit prime-order subgroup check, not clamping.


Only if you are doing blockchain interoperability. secp256k1 is the Bitcoin and Ethereum curve; it is essentially absent from web PKI and TLS. For general key agreement use X25519, and for certificates use P-256. Reaching for secp256k1 outside the blockchain context is almost always a sign something is being reinvented.


Use Ed25519 for new work: deterministic nonces remove the per-signature randomness that has destroyed more ECDSA keys than any cryptanalysis, the addition law is complete and constant-time, and EdDSA has been FIPS-approved since 186-5. Use ECDSA over P-256 only where a standard mandates it, and then with RFC 6979 deterministic nonces, constant-time scalar multiplication, and point validation.


No. Shor&apos;s algorithm breaks the elliptic-curve discrete log in polynomial time on a fault-tolerant quantum computer, and at equal classical security ECC is the easier quantum target -- a 256-bit curve needs about 2,330 logical qubits versus roughly 6,100 for RSA-3072, so it falls first. Because recorded traffic can be decrypted later, deploy the X25519 plus ML-KEM-768 hybrid now rather than waiting for a pure post-quantum replacement.

&lt;p&gt;&amp;lt;StudyGuide slug=&quot;elliptic-curves-real-world-p256-curve25519&quot; keyTerms={[
  { term: &quot;ECDLP&quot;, definition: &quot;The elliptic-curve discrete-log problem: given a base point G and Q equal to k times G, recover the scalar k. Easy to compute forward, believed infeasible to invert on a well-chosen curve. It is the one-way assumption all of elliptic-curve cryptography rests on.&quot; },
  { term: &quot;Scalar multiplication&quot;, definition: &quot;Repeated point addition, adding G to itself k times, written as k times G. Computed in about log k steps by double-and-add. It is the fast forward direction of the trapdoor.&quot; },
  { term: &quot;Cofactor&quot;, definition: &quot;The ratio of a curve&apos;s full group order to the prime order of the subgroup used for cryptography. P-256 has cofactor 1 (prime order); edwards25519 has cofactor 8, so low-order torsion points exist and must be handled.&quot; },
  { term: &quot;Invalid-curve attack&quot;, definition: &quot;Feeding an implementation a point not on the intended curve into formulas that ignore the coefficient b, so the arithmetic runs on a weaker curve and leaks the secret modulo small numbers, with the intended curve untouched.&quot; },
  { term: &quot;Small-subgroup attack&quot;, definition: &quot;Submitting a low-order element so the response reveals the secret modulo that small order. Repeated over coprime orders, the Chinese Remainder Theorem reassembles the full private key.&quot; },
  { term: &quot;Twist security&quot;, definition: &quot;A property of a curve whose quadratic twist also has near-prime order, so a hostile off-curve input still lands in a hard group. Invalid-curve attacks are defeated without an explicit on-curve check.&quot; },
  { term: &quot;Montgomery ladder&quot;, definition: &quot;A scalar-multiplication method whose sequence of field operations is identical for every secret bit, so it is constant-time by construction rather than by defensive coding.&quot; },
  { term: &quot;Clamping&quot;, definition: &quot;Forcing specific bits of an X25519 secret scalar so it is a multiple of the cofactor and a fixed length. It constrains your own secret and is emphatically not validation of an input point.&quot; },
  { term: &quot;Prime-order group (ristretto255)&quot;, definition: &quot;A prime-order group built in the encoding over cofactor-8 edwards25519, so decoding a byte string is itself the validation step and the cofactor failure class disappears at the type level (RFC 9496).&quot; }
]} questions={[
  { q: &quot;Why is ECDLP-hardness not the same as ECC-security?&quot;, a: &quot;A hard discrete log is necessary but not sufficient. Security is a property of the curve, the group, and the implementation together. Every deployed break exploited the implementation or the encoding, not the discrete log.&quot; },
  { q: &quot;What single mechanism explains the whole failure catalog?&quot;, a: &quot;The atom: a point you did not generate, used before it is validated, or a secret multiplied in observable time, turns your own scalar-multiplication routine into an oracle that leaks the key one small piece at a time.&quot; },
  { q: &quot;Why is clamping not point validation?&quot;, a: &quot;Clamping constrains your own secret scalar so a low-order input contributes nothing to your Diffie-Hellman product, but it neither validates nor canonicalizes an input point. A protocol that relies on point uniqueness needs an explicit prime-order subgroup check.&quot; },
  { q: &quot;Which curve should you use for which job?&quot;, a: &quot;X25519 for key agreement, Ed25519 for new signatures, P-256 only where FIPS compels it, secp256k1 only for blockchain, and ristretto255 for new prime-order protocols. Long-lived confidentiality gets the X25519 plus ML-KEM-768 hybrid.&quot; },
  { q: &quot;Why does even correctly implemented ECC have an expiration date?&quot;, a: &quot;For a well-chosen curve the classical security level is exactly the generic square-root bound, with no margin above it, and Shor&apos;s algorithm breaks the discrete log in polynomial time on a quantum computer. The answer is to wrap ECC in a post-quantum hybrid.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>elliptic-curve-cryptography</category><category>curve25519</category><category>p-256</category><category>ristretto255</category><category>invalid-curve-attack</category><category>constant-time</category><category>post-quantum-crypto</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>One Secret Is Not One Key: The Discipline of Key Derivation with HKDF</title><link>https://paragmali.com/blog/one-secret-is-not-one-key-the-discipline-of-key-derivation-w/</link><guid isPermaLink="true">https://paragmali.com/blog/one-secret-is-not-one-key-the-discipline-of-key-derivation-w/</guid><description>A raw DH or ML-KEM shared secret is key material, not a key. How HKDF extract-then-expand, key separation, and the TLS 1.3 key schedule get it right.</description><pubDate>Sat, 11 Jul 2026 16:02:53 GMT</pubDate><content:encoded>
A raw shared secret is not a key -- it is key *material*. A Diffie-Hellman output is non-uniform and an ML-KEM output is single-use, and neither is safe to key a cipher with directly, so protocols run the secret through a key derivation function that does two separable jobs: **Extract** concentrates its entropy into one uniform pseudorandom key (it cannot *amplify* entropy -- that is where password hashing lives), and **Expand** stretches that key into many independent subkeys, each bound to a distinct context label [@rfc5869]. HKDF (RFC 5869) is the deployed instantiation, HMAC playing both roles [@rfc5869]; the TLS 1.3 key schedule is the flagship hierarchy built from it [@rfc8446], and post-quantum migration changed only what gets fed in, not the primitive [@draft-hybrid]. Get three rules right -- do not skip Extract, separate every key by context, bind the transcript -- and the named breaks such as Triple Handshake [@doi-sp2014] and Selfie [@eprint-2019-347] never happen to you.
&lt;h2&gt;1. One Secret Is Not One Key&lt;/h2&gt;
&lt;p&gt;You have just finished a Diffie-Hellman exchange -- or, in 2026, an ML-KEM encapsulation -- and you are holding a shared secret that no eavesdropper can compute [@fips203]. The obvious next move is to use it as your AES-256 key. That move is wrong, and it is wrong twice over. Those two reasons are the entire reason key derivation exists.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason one: the secret may not be uniform.&lt;/strong&gt; A Diffie-Hellman output is a structured group element -- an integer of a particular form living in a particular subgroup, not a uniformly random string of bytes. A block cipher and a MAC both expect their key to be indistinguishable from random bits, and a raw group element simply is not that. RFC 5869 says it plainly: a Diffie-Hellman value &quot;is NOT a uniformly random or pseudorandom string,&quot; so the extraction step &quot;SHOULD NOT be skipped&quot; [@rfc5869]. TLS 1.3 carries the same warning into its own design, never keying anything directly from the raw (EC)DHE value [@rfc8446].&lt;/p&gt;
&lt;p&gt;An ML-KEM shared secret is the other case. FIPS 203 derives it from an internal hash (G, SHA3-512), so the secret is already a uniform 256-bit key rather than a structured value, and it is not entropy extraction that makes it safe [@fips203]. What ships in practice, though, is a &lt;em&gt;hybrid&lt;/em&gt;: the input keying material is the concatenation of the ML-KEM secret and an X25519 secret, and because that concatenation still carries the non-uniform Diffie-Hellman half, the whole block goes through HKDF-Extract as one input [@draft-hybrid]. The rule you follow does not change -- run the shared secret through Extract -- but for ML-KEM the reason is the company it keeps, not its own distribution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reason two: one protocol needs many keys.&lt;/strong&gt; A single connection almost never wants one key. TLS wants a client write key and a server write key, an initialization vector for each direction, an exporter secret, a resumption secret, and fresh keys after every update [@rfc8446]. If you carve all of those out of one secret naively -- or worse, reuse the same key for encryption and authentication -- a weakness in one use leaks into the others. The keys must be &lt;em&gt;independent&lt;/em&gt;: learning one must tell an attacker nothing about the rest.&lt;/p&gt;

A KDF is a function that turns input keying material (IKM) -- a shared secret, a master key, or other high-entropy input -- into one or more output keys that are indistinguishable from random and independent of one another. It is the disciplined replacement for &quot;just hash the secret.&quot;
&lt;p&gt;Put the two reasons together and you get the sentence this entire article defends.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; One secret is not one key. A raw shared secret is &lt;em&gt;material&lt;/em&gt;: possibly non-uniform, and dangerous to use for more than one purpose. Key derivation is the discipline of turning that one secret into a hierarchy of keys that are each uniform and mutually independent.&lt;/p&gt;
&lt;/blockquote&gt;

flowchart LR
    S[&quot;One shared secret (DH or ML-KEM)&quot;] --&amp;gt; KDF[&quot;Key derivation&quot;]
    KDF --&amp;gt; K1[&quot;Client write key&quot;]
    KDF --&amp;gt; K2[&quot;Server write key&quot;]
    KDF --&amp;gt; K3[&quot;Client IV&quot;]
    KDF --&amp;gt; K4[&quot;Server IV&quot;]
    KDF --&amp;gt; K5[&quot;Exporter secret&quot;]
    KDF --&amp;gt; K6[&quot;Resumption secret&quot;]
&lt;p&gt;Everything that follows falls out of that picture as three rules. &lt;strong&gt;Rule one:&lt;/strong&gt; do not skip Extract on a non-uniform secret. &lt;strong&gt;Rule two:&lt;/strong&gt; do not reuse a derived key across contexts -- give each its own separation. &lt;strong&gt;Rule three:&lt;/strong&gt; do not bind too little context, or two different situations will collapse to the same key.&lt;/p&gt;
&lt;p&gt;Almost every famous break in this space -- the Triple Handshake attack [@doi-sp2014], the Selfie reflection [@eprint-2019-347], and the plain mistake of using a raw secret as a key [@rfc5869] -- is the violation of exactly one of these three rules. We will name each one in the catalog at the end.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Never key AES, ChaCha20, or HMAC directly from a Diffie-Hellman or KEM output. It is raw material -- non-uniform, single-use, or both -- not a finished key. Treat it as input to a KDF, and derive every working key with its own context.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One scope boundary before we start. This article is about the &lt;em&gt;high-entropy&lt;/em&gt; branch: secrets with real randomness in them, such as key-exchange outputs and master keys. Low-entropy inputs -- passwords and PINs -- live on a different branch (Part 12 of this series, on &lt;a href=&quot;https://paragmali.com/blog/the-one-job-of-a-password-hash-why-fast-is-the-enemy-and-mem/&quot; rel=&quot;noopener&quot;&gt;password hashing&lt;/a&gt;) for a reason we will make precise in the theory: Extract can concentrate the entropy already present, but it cannot invent entropy that was never there [@rfc5869].&lt;/p&gt;
&lt;p&gt;If the fix is a named discipline, where did it come from -- and why did the field take roughly fourteen years to get it right?&lt;/p&gt;
&lt;h2&gt;2. Key Derivation Before It Had a Name&lt;/h2&gt;
&lt;p&gt;Long before anyone wrote &quot;KDF&quot; on a whiteboard, every protocol still faced the same task: one secret in, many keys out. And for about fourteen years, they all reached for the same tool -- hash the secret together with a counter and a label -- without ever quite agreeing on what that tool was supposed to guarantee.&lt;/p&gt;
&lt;p&gt;Start with SSL 3.0 around 1996, from Netscape&apos;s Alan Freier, Philip Karlton, and Paul Kocher. Its key material came from a bespoke construction that nested MD5 and SHA-1 hashes over the master secret and the two hello nonces [@rfc6101]. There was no separation of concerns, no name for the two jobs it was quietly doing at once; it produced bytes, and the bytes looked random enough. TLS 1.0 openly descended from it: the RFC states it is &quot;based on the SSL 3.0 Protocol Specification as published by Netscape&quot; [@rfc2246].&lt;/p&gt;

timeline
    title Key derivation from 1996 to 2026
    1996 : SSL 3.0 bespoke MD5 and SHA-1 key block
    1999 : TLS 1.0 PRF, P_MD5 XOR P_SHA-1, feedback mode
    2009 : NIST SP 800-108 expand-only PRF modes
    2010 : HKDF extract-then-expand, RFC 5869 and CRYPTO 2010
    2018 : TLS 1.3 staged key schedule
    2020 : NIST SP 800-56C two-step converges on HKDF
    2024 : ML-KEM standardized, PQ hybrids ship
    2026 : NIST revises SP 800-56C for KEM secrets and KMAC
&lt;p&gt;In January 1999, Tim Dierks and Christopher Allen gave the pattern its first careful specification: the TLS 1.0 pseudorandom function. It split the secret into two halves and computed &lt;code&gt;P_MD5(S1, label + seed)&lt;/code&gt; XOR-ed with &lt;code&gt;P_SHA-1(S2, label + seed)&lt;/code&gt;, where each &lt;code&gt;P_hash&lt;/code&gt; is an HMAC-driven feedback-mode expander (&lt;code&gt;A(i) = HMAC(secret, A(i-1))&lt;/code&gt;) [@rfc2246]. The design hedged its bets across two hash functions so the result stayed secure if either one held.&lt;/p&gt;
&lt;p&gt;It was the first mass-deployed protocol KDF -- and it was expand-only in spirit: it stretched a secret into blocks with a label and a feedback chain, with no explicit extraction step and no clean argument for what happens when the input is a structured Diffie-Hellman value rather than an already-random key.&lt;/p&gt;

A PRF is a keyed function whose outputs are indistinguishable from those of a truly random function, to anyone who does not hold the key. HMAC is the workhorse PRF of this whole story; Part 11 of this series covers it in depth. A KDF&apos;s expansion step is essentially a PRF chained in feedback mode, driven by a counter.
&lt;p&gt;The same shape appeared in the public-key world. The ANSI X9.63 and IEEE 1363 working groups specified KDF1- and KDF2-style functions that produced keying material by hashing an (elliptic-curve) Diffie-Hellman shared secret together with a counter and some shared information [@sec1-v2]. RFC 5869 later named IEEE 1363a-2004 -- alongside NIST SP 800-56A and SP 800-108 -- among the schemes that &quot;do not explicitly differentiate between the &apos;extract&apos; and &apos;expand&apos; stages, often resulting in design shortcomings&quot; [@rfc5869].The ANS X9.63 standard itself sits behind a paywall, but SEC 1 v2.0 (section 3.6.1) documents the identical construction openly as ANSI-X9.63-KDF: &lt;code&gt;Ki = Hash(Z || Counter || SharedInfo)&lt;/code&gt; over the shared secret &lt;code&gt;Z&lt;/code&gt; [@sec1-v2]. Treat ANSI X9.63 as reported prior art of that &quot;hash the secret with a counter and shared info&quot; shape, not a scheme RFC 5869 names by title (the RFC names IEEE 1363a-2004, not X9.63).&lt;/p&gt;
&lt;p&gt;Then NIST codified its own line: a concatenation KDF in SP 800-56A [@sp800-56a], and, in 2009, Lily Chen&apos;s SP 800-108 with its counter, feedback, and double-pipeline modes for deriving keys from an already-uniform key [@sp800-108-r1].&lt;/p&gt;
&lt;p&gt;Look across all of them -- SSL 3.0, the TLS PRF, X9.63, IEEE 1363, SP 800-56A, SP 800-108 -- and one shape repeats: &lt;em&gt;hash the secret with a counter and a label&lt;/em&gt;. So does one blind spot: no explicit Extract step, and no honest argument for a non-uniform input, because every design implicitly treated the hash as if it were a perfectly random function.&lt;/p&gt;
&lt;p&gt;The two-hash hedge did not survive. TLS 1.2 replaced &lt;code&gt;P_MD5&lt;/code&gt; XOR &lt;code&gt;P_SHA-1&lt;/code&gt; with a single SHA-256 PRF, declaring that &quot;this PRF with the SHA-256 hash function is used for all cipher suites&quot; [@rfc5246]. The hedge added complexity without giving the modularity a real Extract/Expand split would later provide.&lt;/p&gt;

It is tempting to read a history of cryptography as a sequence of disasters, each fixed by the next design. This one is not that. The pre-HKDF KDFs were not catastrophically broken; they were *unproven and muddled*. Two lineages -- the IETF and academic line that became HKDF, and the NIST SP 800-56 and 800-108 line -- were separately groping toward the same structure. The interesting question is not &quot;when did they break&quot; but &quot;why could nobody prove they were sound,&quot; and the answer is what finally split the field&apos;s one confused step into two honest ones.
&lt;p&gt;These constructions shipped, and they protected real traffic for years without a headline disaster. So what, mechanically, was missing? Why could nobody prove they were sound?&lt;/p&gt;
&lt;h2&gt;3. Why &quot;Just Hash the Secret&quot; Cannot Be Proven Safe&lt;/h2&gt;
&lt;p&gt;Zoom in on the mechanics of the antipattern everyone kept reinventing: take the secret, hash it with a counter and a label, use the bytes. Two silent failures hide inside it, and one of them is the kind of bug you can never catch in a test -- only in a proof.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failure one: a non-uniform input with no proof.&lt;/strong&gt; Suppose your secret is a Diffie-Hellman value $g^{xy}$. It is unpredictable to an attacker, yes, but it is not uniform: it is an element of a specific group, with algebraic structure an adversary knows about. When you feed it straight into a hash and declare the output &quot;random,&quot; you are making an assumption you cannot back up. There is no standard-model argument that &lt;code&gt;H(g^{xy})&lt;/code&gt; is indistinguishable from a random string. The only way the old designs &quot;work&quot; is if you pretend the hash is a perfect random oracle. Krawczyk put the indictment precisely: in practice most KDFs, including widely standardized ones, &quot;follow ad-hoc approaches that treat cryptographic hash functions as perfectly random functions&quot; [@eprint-2010-264].&lt;/p&gt;

Min-entropy measures a source&apos;s worst-case unpredictability. If its most likely value occurs with probability $p$, its min-entropy is $-\log_2 p$. This -- not the friendlier Shannon entropy -- is the quantity that governs extraction, because security must hold against an attacker who guesses the single likeliest value first.
&lt;p&gt;&lt;strong&gt;Failure two: no context, so silent reuse.&lt;/strong&gt; The homemade construction has nowhere to say &lt;em&gt;what this key is for&lt;/em&gt;. With no distinct label bound into the derivation, the same secret hashed the same way yields the same bytes, and nothing stops a program from using those bytes for two different purposes. That is not a dramatic bug you can see; it is a latent precondition, the quiet setup behind attacks we will name later.&lt;/p&gt;

A randomness extractor is a function that takes a source with enough min-entropy but a non-uniform distribution and produces output statistically close to uniform. It is the formal object the &quot;Extract&quot; half of a KDF is supposed to be -- and precisely what &quot;hash it and hope&quot; fails to provide any guarantee of being.
&lt;p&gt;There is also a concrete, testable hazard in the naive &lt;code&gt;H(secret || info)&lt;/code&gt; form, separate from the proof problem.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; With a Merkle-Damgard hash (MD5, SHA-1, SHA-256), knowing &lt;code&gt;H(secret || info)&lt;/code&gt; and the length of the secret lets an attacker compute &lt;code&gt;H(secret || info || padding || suffix)&lt;/code&gt; for a suffix of their choosing -- without ever learning the secret. That is exactly why HMAC keys the hash instead of using raw &lt;code&gt;H&lt;/code&gt; [@bck96-hmac], and why homemade concatenation KDFs are dangerous. Sponge and tree hashes (SHA-3, BLAKE3) are not vulnerable to this. Part 10 of this series covers the mechanism in full.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So why not fix it with a better hash, or more rounds? Because the real problem is not the hash function -- it is that a single step is being asked to do two different jobs with two different security requirements, and being proven for neither. Concentrating dispersed entropy into a uniform key is one job. Stamping out many independent context-separated keys is another.&lt;/p&gt;
&lt;p&gt;The first of those two jobs even carries a measurable cost: concentrating entropy by purely &lt;em&gt;statistical&lt;/em&gt; means is lossy -- the theory section later makes that tax precise -- which is exactly why the eventual fix used a &lt;em&gt;computational&lt;/em&gt; extractor, keyed and analyzed under a computational assumption rather than a purely information-theoretic one [@eprint-2010-264].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Concentrating entropy and separating keys are two different jobs, and the old designs did both at once in a single step that was proven for neither -- surviving only by pretending the hash was a perfect random oracle. Name the two jobs, prove each, and the confusion dissolves.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If hashing-with-a-counter cannot be proven sound for a non-uniform secret, the field did not need a different hash. It needed a different &lt;em&gt;mental model&lt;/em&gt;. In 2010, one paper supplied it.&lt;/p&gt;
&lt;h2&gt;4. Krawczyk&apos;s Split: Two Jobs, Two Proofs&lt;/h2&gt;
&lt;p&gt;The insight that reorganized the field is almost embarrassing to state once you see it: &lt;em&gt;the two jobs everyone had been doing at once are actually two different jobs, and they need two different proofs.&lt;/em&gt; In 2010 Hugo Krawczyk named them, separated them, and proved each -- and in doing so converted fourteen years of ad-hoc practice into a deployable primitive.&lt;/p&gt;
&lt;p&gt;The first job is &lt;strong&gt;Extract&lt;/strong&gt;. Treat it as a keyed &lt;em&gt;computational randomness extractor&lt;/em&gt;: it takes the possibly non-uniform input keying material -- a Diffie-Hellman element, or a hybrid secret whose classical half is non-uniform -- and concentrates whatever entropy is dispersed inside it into one uniform pseudorandom key. Its defining limitation is the honest one from the previous section: it concentrates entropy, it cannot &lt;em&gt;create&lt;/em&gt; it.&lt;/p&gt;
&lt;p&gt;The second job is &lt;strong&gt;Expand&lt;/strong&gt;. Treat it as a &lt;em&gt;variable-length PRF&lt;/em&gt;: given that one uniform key, it stamps out arbitrarily many output keys, each independent of the others because each is bound to a distinct context string. The independence is not a hope; it is the PRF property, applied to distinct inputs.&lt;/p&gt;

The two-stage structure at the heart of HKDF. Extract concentrates a non-uniform secret&apos;s entropy into a single uniform pseudorandom key; Expand then derives many independent keys from that one key, each bound to a distinct context label. Splitting the derivation in two is what lets each half be defined, and proven, on its own terms.

An extractor whose output need only be *computationally* indistinguishable from uniform, rather than statistically close to it. Keyed by the salt and analyzed under HMAC&apos;s pseudorandomness, it avoids the Leftover Hash Lemma&apos;s entropy tax -- at the price of resting on a computational assumption instead of an information-theoretic one.
&lt;p&gt;The engineering move that made this practical was refusing to invent two new gadgets. Both stages are instantiated with the same primitive: &lt;a href=&quot;https://paragmali.com/blog/the-tag-verified-the-cipher-held-the-forgery-went-through-a-/&quot; rel=&quot;noopener&quot;&gt;HMAC&lt;/a&gt;. One construction, two roles. Extract is &lt;code&gt;PRK = HMAC(salt, IKM)&lt;/code&gt;; Expand is HMAC chained over its previous output (feedback mode, counter-stepped). Because HMAC was already everywhere, HKDF cost implementers essentially nothing new to adopt (Part 11 of this series treats HMAC as its own subject).&lt;/p&gt;

flowchart TD
    IKM[&quot;IKM -- the raw shared secret&quot;] --&amp;gt; EX[&quot;HKDF-Extract, a computational extractor&quot;]
    SALT[&quot;salt (optional, extract-time)&quot;] --&amp;gt; EX
    EX --&amp;gt; PRK[&quot;PRK -- one uniform pseudorandom key&quot;]
    PRK --&amp;gt; EXP[&quot;HKDF-Expand, a variable-length PRF&quot;]
    INFO[&quot;info -- a distinct context per key&quot;] --&amp;gt; EXP
    EXP --&amp;gt; OKM[&quot;OKM -- many independent subkeys&quot;]
&lt;p&gt;Two artifacts landed in 2010, weeks apart, and it is worth keeping them distinct. The &lt;em&gt;interface&lt;/em&gt; is RFC 5869, &quot;HMAC-based Extract-and-Expand Key Derivation Function,&quot; by Krawczyk and Pasi Eronen, published that May, whose stated goal was &quot;to discourage the proliferation of multiple KDF mechanisms&quot; [@rfc5869]. The &lt;em&gt;theory&lt;/em&gt; is the CRYPTO 2010 paper, which supplied what the field had never had.&lt;/p&gt;

&quot;...the first general and rigorous definition of KDFs and their security which we base on the notion of computational extractors.&quot; -- Hugo Krawczyk, &quot;Cryptographic Extraction and Key Derivation: The HKDF Scheme,&quot; CRYPTO 2010 [@eprint-2010-264]
&lt;p&gt;That is the whole reframing in one line. A KDF is not a fancy hash; it is an extractor followed by a PRF, and each has a definition and a proof. The paper analyzes the HMAC construction &quot;based on the extraction and pseudorandom properties of HMAC,&quot; and is careful about &quot;minimizing these assumptions as much as possible for each usage scenario&quot; [@eprint-2010-264].&lt;/p&gt;
&lt;p&gt;Here is the corroboration that it was a real discovery and not one team&apos;s preference: NIST arrived at the same structure independently. SP 800-56C Rev. 2, by Elaine Barker, Lily Chen, and Richard Davis (2020), standardizes a two-step &quot;extraction-then-expansion&quot; key-derivation method -- the identical shape -- for key establishment [@sp800-56c-r2], while SP 800-108 remains the expand-only sibling for inputs that are already uniform [@sp800-108-r1].&lt;/p&gt;
&lt;p&gt;Two lineages, one design. When the IETF and academic line and the NIST line converge on the same two-step structure from different starting points, that is the signature of having found something true rather than merely convenient.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A KDF is &lt;em&gt;two&lt;/em&gt; functions with &lt;em&gt;two&lt;/em&gt; security notions: an extractor that concentrates entropy into one uniform key (and provably cannot create entropy), and a PRF that stamps out arbitrarily many keys, each independent because each names a distinct context. Once you separate them -- and build both from the same HMAC -- everything the old designs groped toward becomes provable. Nothing was &quot;broken&quot;; the field finally named the two jobs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A design and a proof are worth nothing until you can actually &lt;em&gt;call&lt;/em&gt; the thing. What does the construction look like in code, and what, exactly, are &lt;code&gt;salt&lt;/code&gt; and &lt;code&gt;info&lt;/code&gt; for?&lt;/p&gt;
&lt;h2&gt;5. Extract, Then Expand: The Deployable Primitive&lt;/h2&gt;
&lt;p&gt;Here is the whole thing, worked. Two function calls you could implement this afternoon, and two knobs that do all the work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Extract&lt;/strong&gt; is one line. The salt becomes the HMAC key, and the secret becomes the message:&lt;/p&gt;
&lt;p&gt;$$\mathrm{PRK} = \mathrm{HMAC\text{-}Hash}(\text{salt},\ \mathrm{IKM})$$&lt;/p&gt;
&lt;p&gt;If you have no salt, RFC 5869 says use a string of &lt;code&gt;HashLen&lt;/code&gt; zero bytes [@rfc5869]. The output is one uniform pseudorandom key.&lt;/p&gt;

The uniform, pseudorandom key that Extract produces from the input keying material. It is `HashLen` bytes long (32 for SHA-256) and is the sole keying input to every subsequent Expand call. Everything downstream depends only on the PRK, never on the raw secret again.
&lt;p&gt;&lt;strong&gt;Expand&lt;/strong&gt; is an iterated (feedback-mode) PRF: it chains HMAC over its own previous output block, the context string, and a single incrementing counter byte, then takes the first &lt;code&gt;L&lt;/code&gt; bytes:&lt;/p&gt;
&lt;p&gt;$$T(i) = \mathrm{HMAC\text{-}Hash}(\mathrm{PRK},\ T(i-1)\ \Vert\ \text{info}\ \Vert\ \mathrm{byte}(i)), \quad T(0)=\varepsilon$$&lt;/p&gt;

flowchart TD
    PRK[&quot;PRK&quot;] --&amp;gt; T1[&quot;T1 = HMAC of PRK over info and byte 0x01&quot;]
    T1 --&amp;gt; T2[&quot;T2 = HMAC of PRK over T1, info, byte 0x02&quot;]
    T2 --&amp;gt; T3[&quot;T3 = HMAC of PRK over T2, info, byte 0x03&quot;]
    T1 --&amp;gt; OKM[&quot;OKM = first L bytes of T1 then T2 then T3&quot;]
    T2 --&amp;gt; OKM
    T3 --&amp;gt; OKM
&lt;p&gt;That single incrementing byte is why the output has a hard ceiling: $L \le 255 \cdot \mathrm{HashLen}$ [@rfc5869]. Ask for more and the counter would overflow a byte.The counter starts at 1, not 0, so the largest block index is 255 -- hence the factor of 255 rather than 256.&lt;/p&gt;

The pseudorandom bytes Expand produces, which the caller slices into the actual working keys. A single derivation is capped at $255 \cdot \mathrm{HashLen}$ bytes; longer needs cannot be met by asking for a bigger `L`.
&lt;p&gt;That cap is 8,160 bytes with SHA-256 and 16,320 bytes with SHA-512 -- comfortably more than any single key, but a real limit if you try to run HKDF as a general keystream generator. It is not one. Derive keys with it; use a stream cipher for streams.&lt;/p&gt;
&lt;p&gt;None of this needs to stay abstract. The code below is HKDF built from nothing but HMAC-SHA-256, run on RFC 5869&apos;s own first test vector, so you can check the output against the standard.&lt;/p&gt;
&lt;p&gt;{`
// HKDF (RFC 5869) built from HMAC-SHA-256, run on the RFC&apos;s own Test Vector 1.
const toHex = (buf) =&amp;gt;
  Array.from(new Uint8Array(buf)).map((b) =&amp;gt; b.toString(16).padStart(2, &apos;0&apos;)).join(&apos;&apos;);
const hexToBytes = (h) =&amp;gt; new Uint8Array(h.match(/../g).map((x) =&amp;gt; parseInt(x, 16)));&lt;/p&gt;
&lt;p&gt;async function hmac(keyBytes, msgBytes) {
  const key = await crypto.subtle.importKey(
    &apos;raw&apos;, keyBytes, { name: &apos;HMAC&apos;, hash: &apos;SHA-256&apos; }, false, [&apos;sign&apos;]);
  return new Uint8Array(await crypto.subtle.sign(&apos;HMAC&apos;, key, msgBytes));
}&lt;/p&gt;
&lt;p&gt;// Extract: concentrate the (non-uniform) IKM into one uniform PRK.
async function hkdfExtract(salt, ikm) { return hmac(salt, ikm); }&lt;/p&gt;
&lt;p&gt;// Expand: T(i) = HMAC(PRK, T(i-1) || info || byte(i)); OKM = first L bytes.
async function hkdfExpand(prk, info, L) {
  const N = Math.ceil(L / 32);              // SHA-256 output is 32 bytes
  let t = new Uint8Array(0);
  let okm = new Uint8Array(0);
  for (let i = 1; i &amp;lt;= N; i++) {            // L must stay &amp;lt;= 255 * 32 = 8160 bytes
    const block = new Uint8Array(t.length + info.length + 1);
    block.set(t, 0);
    block.set(info, t.length);
    block.set([i], t.length + info.length);
    t = await hmac(prk, block);
    const merged = new Uint8Array(okm.length + t.length);
    merged.set(okm, 0); merged.set(t, okm.length);
    okm = merged;
  }
  return okm.slice(0, L);
}&lt;/p&gt;
&lt;p&gt;(async () =&amp;gt; {
  const ikm  = hexToBytes(&apos;0b&apos;.repeat(22));
  const salt = hexToBytes(&apos;000102030405060708090a0b0c&apos;);
  const info = hexToBytes(&apos;f0f1f2f3f4f5f6f7f8f9&apos;);
  const prk = await hkdfExtract(salt, ikm);
  const okm = await hkdfExpand(prk, info, 42);
  console.log(&apos;PRK =&apos;, toHex(prk));   // expect 077709362c2e32df0ddc3f0dc47bba63...
  console.log(&apos;OKM =&apos;, toHex(okm));   // expect 3cb25f25faacd57a90434f64d0362f2a...
})();
`}&lt;/p&gt;

`PRK = 077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5` and `OKM = 3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865` -- byte for byte the values published in RFC 5869 Appendix A [@rfc5869]. If your implementation prints these two lines, your Extract and Expand are correct.
&lt;p&gt;Now the two knobs. &lt;strong&gt;Salt&lt;/strong&gt; is an extract-time input; RFC 5869 says a salt &quot;adds significantly to the strength of HKDF,&quot; and although it is optional it is recommended when you have one [@rfc5869]. &lt;strong&gt;Info&lt;/strong&gt; is an expand-time input, and it is the load-bearing one: distinct &lt;code&gt;info&lt;/code&gt; strings give you independent keys for free, because RFC 5869 designed &lt;code&gt;info&lt;/code&gt; precisely so that it &quot;may prevent the derivation of the same keying material for different contexts&quot; [@rfc5869].&lt;/p&gt;

A non-secret value used as the HMAC key during Extract. It is optional but recommended; RFC 5869 states it &quot;adds significantly to the strength of HKDF.&quot; Unlike a nonce, a salt is fixed and reusable -- the same salt across many sessions is fine.

Binding a distinct context label into each derivation so that keys for different purposes are cryptographically independent, even though they descend from one secret. In HKDF this is exactly the job of the `info` string.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;info&lt;/code&gt; is not decoration. It is the entire mechanism of key separation. Two Expand calls with the same PRK and the same &lt;code&gt;info&lt;/code&gt; return identical bytes; two calls with different &lt;code&gt;info&lt;/code&gt; return independent keys. Every key-separation bug in this article is, at bottom, a missing or duplicated &lt;code&gt;info&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Run the separation yourself. From one PRK, two different &lt;code&gt;info&lt;/code&gt; strings yield unrelated keys, and -- the part worth internalizing -- the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;info&lt;/code&gt; yields the &lt;em&gt;same&lt;/em&gt; key, which is the misuse made visible.&lt;/p&gt;
&lt;p&gt;{`
// Domain separation: one PRK, different info -&amp;gt; independent keys; same info -&amp;gt; identical.
const toHex = (buf) =&amp;gt;
  Array.from(new Uint8Array(buf)).map((b) =&amp;gt; b.toString(16).padStart(2, &apos;0&apos;)).join(&apos;&apos;);
const hexToBytes = (h) =&amp;gt; new Uint8Array(h.match(/../g).map((x) =&amp;gt; parseInt(x, 16)));
const bytes = (s) =&amp;gt; new TextEncoder().encode(s);&lt;/p&gt;
&lt;p&gt;async function hmac(k, m) {
  const key = await crypto.subtle.importKey(
    &apos;raw&apos;, k, { name: &apos;HMAC&apos;, hash: &apos;SHA-256&apos; }, false, [&apos;sign&apos;]);
  return new Uint8Array(await crypto.subtle.sign(&apos;HMAC&apos;, key, m));
}
async function hkdfExpand(prk, info, L) {
  const N = Math.ceil(L / 32);
  let t = new Uint8Array(0), okm = new Uint8Array(0);
  for (let i = 1; i &amp;lt;= N; i++) {
    const block = new Uint8Array(t.length + info.length + 1);
    block.set(t, 0); block.set(info, t.length); block.set([i], t.length + info.length);
    t = await hmac(prk, block);
    const merged = new Uint8Array(okm.length + t.length);
    merged.set(okm, 0); merged.set(t, okm.length); okm = merged;
  }
  return okm.slice(0, L);
}&lt;/p&gt;
&lt;p&gt;(async () =&amp;gt; {
  // Pretend we already ran Extract and hold this PRK.
  const prk = hexToBytes(&apos;077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5&apos;);
  const encKey  = await hkdfExpand(prk, bytes(&apos;app enc key&apos;), 32);
  const macKey  = await hkdfExpand(prk, bytes(&apos;app mac key&apos;), 32);
  const encKey2 = await hkdfExpand(prk, bytes(&apos;app enc key&apos;), 32);   // same info as encKey
  console.log(&apos;enc  =&apos;, toHex(encKey));
  console.log(&apos;mac  =&apos;, toHex(macKey));
  console.log(&apos;distinct info -&amp;gt; independent keys? &apos;, toHex(encKey) !== toHex(macKey));
  console.log(&apos;same info     -&amp;gt; identical keys?   &apos;, toHex(encKey) === toHex(encKey2));
})();
`}&lt;/p&gt;
&lt;p&gt;People constantly confuse &lt;code&gt;salt&lt;/code&gt; with a nonce. They are different animals, and so is &lt;code&gt;info&lt;/code&gt;. Keeping the three straight prevents a whole class of bugs.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;When it acts&lt;/th&gt;
&lt;th&gt;Reuse rule&lt;/th&gt;
&lt;th&gt;Cost of getting it wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;salt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extractor strengthener (the HMAC key in Extract)&lt;/td&gt;
&lt;td&gt;Extract time&lt;/td&gt;
&lt;td&gt;Fixed, reusable across sessions&lt;/td&gt;
&lt;td&gt;Weaker extraction, but not catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Domain / key-separation label&lt;/td&gt;
&lt;td&gt;Expand time&lt;/td&gt;
&lt;td&gt;Distinct per derived key&lt;/td&gt;
&lt;td&gt;Two keys collide -&amp;gt; reuse across contexts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nonce&lt;/td&gt;
&lt;td&gt;Per-message uniqueness for the AEAD&lt;/td&gt;
&lt;td&gt;Encryption time&lt;/td&gt;
&lt;td&gt;Never repeat under one key&lt;/td&gt;
&lt;td&gt;Catastrophic cipher failure -- and not a KDF input at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Part 2 of this series covers &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;salt and nonce generation&lt;/a&gt;; the one thing to carry here is that a salt is safe to hardcode per protocol, while a nonce never is.&lt;/p&gt;
&lt;p&gt;When &lt;em&gt;may&lt;/em&gt; you skip Extract? Only when the input is already a uniform key -- an existing symmetric master key, or a PRK you extracted earlier. RFC 5869 has a section literally titled &quot;To Skip or not to Skip,&quot; and the rule is exactly the entropy question: uniform input, Extract is wasted work; non-uniform input such as a Diffie-Hellman value, Extract &quot;SHOULD NOT be skipped&quot; [@rfc5869].&lt;/p&gt;
&lt;p&gt;One more property, easy to miss now and load-bearing later: because Extract is &lt;code&gt;HMAC(salt, IKM)&lt;/code&gt;, the salt and the secret both flow through HMAC, and HMAC can be argued secure even when the fresh secret rides in on the message (IKM) side while the salt keys HMAC -- the swap, or dual-PRF, direction. That &quot;either input can carry the security&quot; behavior -- the dual-PRF intuition -- is what lets a post-quantum secret be bolted onto a classical one without a redesign. Hold that thought.&lt;/p&gt;
&lt;p&gt;One Extract-and-Expand pair is a primitive. A real protocol needs a whole &lt;em&gt;hierarchy&lt;/em&gt; -- many keys, chained and context-bound, across an entire handshake. What does that look like at production scale?&lt;/p&gt;
&lt;h2&gt;6. How HKDF Is Wired Today: The TLS 1.3 Key Schedule&lt;/h2&gt;
&lt;p&gt;The primitive froze in 2010 and has not been superseded. Everything called &quot;state of the art&quot; since is about &lt;em&gt;wiring&lt;/em&gt; -- how you chain one Extract-and-Expand pair into an entire hierarchy without a separation bug. The flagship wiring is a diagram every TLS implementer has stared at: the TLS 1.3 key schedule, defined in RFC 8446 section 7.1 by Eric Rescorla in 2018, building on the OPTLS design of Krawczyk and Wee [@rfc8446] [@eprint-2015-978].&lt;/p&gt;
&lt;p&gt;TLS 1.3 first defines two thin wrappers over HKDF. &lt;code&gt;HKDF-Expand-Label&lt;/code&gt; is just &lt;code&gt;HKDF-Expand&lt;/code&gt; with the label prefixed by the string &lt;code&gt;tls13 &lt;/code&gt;, and &lt;code&gt;Derive-Secret&lt;/code&gt; is &lt;code&gt;HKDF-Expand-Label&lt;/code&gt; with the transcript hash of the handshake messages as its context [@rfc8446]. Then it chains a sequence of &lt;code&gt;HKDF-Extract&lt;/code&gt; calls, each using the previous secret as the salt and a new secret as the IKM:&lt;/p&gt;

flowchart TD
    Z0[&quot;0&quot;] --&amp;gt; E1[&quot;HKDF-Extract&quot;]
    PSK[&quot;PSK&quot;] --&amp;gt; E1
    E1 --&amp;gt; ES[&quot;Early Secret&quot;]
    ES --&amp;gt; B1[&quot;Derive-Secret: c e traffic (binds ClientHello)&quot;]
    ES --&amp;gt; D1[&quot;Derive-Secret: derived&quot;]
    D1 --&amp;gt; E2[&quot;HKDF-Extract&quot;]
    DHE[&quot;(EC)DHE shared secret&quot;] --&amp;gt; E2
    E2 --&amp;gt; HS[&quot;Handshake Secret&quot;]
    HS --&amp;gt; B2[&quot;Derive-Secret: s hs traffic (binds CH to SH)&quot;]
    HS --&amp;gt; D2[&quot;Derive-Secret: derived&quot;]
    D2 --&amp;gt; E3[&quot;HKDF-Extract&quot;]
    Z1[&quot;0&quot;] --&amp;gt; E3
    E3 --&amp;gt; MS[&quot;Master Secret&quot;]
    MS --&amp;gt; B3[&quot;Derive-Secret: s ap traffic (binds CH to server Finished)&quot;]
    MS --&amp;gt; B4[&quot;Derive-Secret: res master (binds CH to client Finished)&quot;]
&lt;p&gt;Read what that structure buys. The chain &lt;strong&gt;Extracts&lt;/strong&gt; on the non-uniform (EC)DHE value, so rule one is satisfied. &lt;code&gt;Derive-Secret&lt;/code&gt;&apos;s label separates every key -- &lt;code&gt;c hs traffic&lt;/code&gt; is not &lt;code&gt;s hs traffic&lt;/code&gt;, so client and server never share a key -- satisfying rule two. And every secret binds a &lt;strong&gt;transcript hash&lt;/strong&gt;, so two handshakes with different Hello randoms can never derive the same traffic keys, satisfying rule three [@rfc8446].&lt;/p&gt;
&lt;p&gt;The schedule folds in a pre-shared key, then the (EC)DHE secret, then a final zero. Along the way it stamps out early-data secrets, handshake traffic secrets, application traffic secrets, an exporter secret, a resumption secret, and the PSK binder keys (&lt;code&gt;ext binder&lt;/code&gt; and &lt;code&gt;res binder&lt;/code&gt;) that &quot;prevent the substitution of one type of PSK for the other&quot; [@rfc8446].&lt;/p&gt;
&lt;p&gt;Key updates ratchet forward with an &lt;code&gt;HKDF-Expand-Label&lt;/code&gt; using the label &lt;code&gt;traffic upd&lt;/code&gt; [@rfc8446]. The final working keys come from &lt;code&gt;HKDF-Expand-Label(Secret, &quot;key&quot;, ...)&lt;/code&gt; and &lt;code&gt;HKDF-Expand-Label(Secret, &quot;iv&quot;, ...)&lt;/code&gt;, and those feed the AEAD cipher -- &lt;a href=&quot;https://paragmali.com/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/&quot; rel=&quot;noopener&quot;&gt;AES-GCM or ChaCha20-Poly1305&lt;/a&gt;, the subject of Part 7 [@rfc8446].&lt;/p&gt;
&lt;p&gt;The RFC states the article&apos;s whole thesis in its own words, describing the shape of its own diagram.&lt;/p&gt;

&quot;...the secrets shown down the left side of the diagram are just raw entropy without context, whereas the secrets down the right side include Handshake Context and therefore can be used to derive working keys without additional context.&quot; -- RFC 8446, section 7.1 [@rfc8446]
&lt;p&gt;Left side, raw entropy; right side, entropy plus bound context. That is Extract and Expand, drawn as a protocol.&lt;/p&gt;
&lt;p&gt;The same staged-Extract, labeled-Expand pattern reappears everywhere HKDF is deployed: HPKE (RFC 9180), MLS, the Noise framework behind WireGuard [@noise], and Signal, whose X3DH keys HKDF with a zero-filled salt and an application-identifier context [@signal-x3dh] and whose Double Ratchet builds forward-secure KDF chains from it [@signal-doubleratchet].&lt;/p&gt;
&lt;h3&gt;The post-quantum front changed the input, not the primitive&lt;/h3&gt;
&lt;p&gt;The genuinely new development of 2024 to 2026 is not a KDF. It is &lt;em&gt;what gets fed into HKDF-Extract&lt;/em&gt;. To resist &quot;harvest now, decrypt later,&quot; protocols now run a classical and a post-quantum key exchange side by side and combine the two shared secrets.&lt;/p&gt;
&lt;p&gt;The recommended hybrid group [@iana-tls-groups], &lt;code&gt;X25519MLKEM768&lt;/code&gt;, concatenates the ML-KEM-768 secret from FIPS 203 [@fips203] and then the classical X25519 secret, and hands the pair to the &lt;em&gt;existing&lt;/em&gt; TLS 1.3 HKDF-Extract as its (EC)DHE input [@draft-hybrid]. Order matters for interop: the bytes are &lt;code&gt;ML-KEM || X25519&lt;/code&gt;, and the group &lt;em&gt;name&lt;/em&gt; deliberately lists the two components in the reverse order for historical reasons [@draft-ecdhe-mlkem].&lt;/p&gt;

flowchart LR
    M[&quot;ML-KEM-768 shared secret&quot;] --&amp;gt; CAT[&quot;Concatenate: ML-KEM then X25519&quot;]
    X[&quot;X25519 shared secret&quot;] --&amp;gt; CAT
    CAT --&amp;gt; IKM[&quot;Hybrid IKM, a longer secret&quot;]
    IKM --&amp;gt; EX[&quot;The same TLS 1.3 HKDF-Extract&quot;]
    EX --&amp;gt; HS[&quot;Handshake Secret, schedule unchanged&quot;]
&lt;p&gt;No new KDF. No new schedule. The hybrid secret is just a longer IKM, and the security goal is &quot;secure if either component is secure&quot; -- the dual-PRF payoff foreshadowed in the previous section [@draft-hybrid]. This is the entire reason extract-then-expand aged so well: a brand-new &lt;em&gt;kind&lt;/em&gt; of secret slotted in without touching the primitive.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; HKDF has not been replaced, improved, or deprecated since 2010. Every advance labeled &quot;state of the art&quot; -- the TLS 1.3 schedule, HPKE&apos;s labeled derivation, the post-quantum hybrids -- is a new way to &lt;em&gt;wire&lt;/em&gt; the same two functions. When a primitive absorbs a shift as large as post-quantum migration with zero changes to itself, that is the sign it was designed at the right level of abstraction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;IANA assigned &lt;code&gt;X25519MLKEM768&lt;/code&gt; the codepoint 4588, that is 0x11EC, marked Recommended, and retired the earlier &lt;code&gt;X25519Kyber768Draft00&lt;/code&gt; at 0x6399 as obsolete [@iana-tls-groups].&lt;/p&gt;
&lt;p&gt;How much of this is actually running? Time-stamp it, because it moves fast. Cloudflare reported that in early 2024 &quot;nearly two percent of all TLS 1.3 connections&quot; were post-quantum-secured, and expected &quot;double-digit adoption by the end of 2024&quot; [@cloudflare-pq]; Google documented Chrome&apos;s own migration to ML-KEM on the web [@google-blog]. Treat the two-percent figure as a dated snapshot, not a steady state.&lt;/p&gt;
&lt;p&gt;The standards body is now catching up to the deployment. In January 2026 NIST announced it will revise SP 800-56C Rev. 2 to let the shared secret incorporate a secret from an approved KEM, to allow flexible formatting of hybrid shared secrets, and to approve KMAC &quot;for the randomness extraction step (in addition to the expansion step)&quot; [@nist-2026-news].&lt;/p&gt;

FIPS-validated systems cannot ship a construction the standard does not yet permit, so the standard is currently *behind* deployed TLS. Browsers concatenated a KEM secret into HKDF-Extract in 2024; NIST announced the matching revision to SP 800-56C in 2026 [@nist-2026-news]. That gap is not dysfunction -- it is the normal order of operations. Engineering ships the wiring, the standard blesses it once the analysis settles, and validation follows the standard.
&lt;p&gt;TLS 1.3 is one superb design point. But HKDF is not the only KDF, and it is not always the right one. Given your input and your constraints, which do you actually reach for?&lt;/p&gt;
&lt;h2&gt;7. Choosing a KDF: The Design Space and a Decision Matrix&lt;/h2&gt;
&lt;p&gt;HKDF is not a monopoly. It is one answer to two orthogonal questions: &lt;em&gt;does your input need extraction?&lt;/em&gt; and &lt;em&gt;what is your compliance and library situation?&lt;/em&gt; Answer those two and the choice mostly makes itself. The methods below do not really race one another; they occupy different cells of an input-by-environment grid.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;extract-then-expand family&lt;/strong&gt; handles non-uniform, high-entropy secrets: HKDF [@rfc5869] and the NIST SP 800-56C two-step method [@sp800-56c-r2]. The &lt;strong&gt;expand-only family&lt;/strong&gt; assumes the input is already uniform and skips extraction: SP 800-108&apos;s counter, feedback, and double-pipeline modes over HMAC or KMAC [@sp800-108-r1], and libsodium&apos;s &lt;code&gt;crypto_kdf&lt;/code&gt; over BLAKE2b [@libsodium]. A &lt;strong&gt;single-primitive&lt;/strong&gt; school argues you never need a separate Extract step if the primitive earns separation structurally: KMAC and cSHAKE from a Keccak sponge [@sp800-185], and BLAKE3&apos;s &lt;code&gt;derive_key&lt;/code&gt;, which takes a mandatory hardcoded &lt;code&gt;context&lt;/code&gt; string [@blake3]. And SP 800-56C&apos;s one-step method is the &quot;one call&quot; middle ground [@sp800-56c-r2].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Core primitive&lt;/th&gt;
&lt;th&gt;Explicit Extract?&lt;/th&gt;
&lt;th&gt;Non-uniform input?&lt;/th&gt;
&lt;th&gt;Separation input&lt;/th&gt;
&lt;th&gt;FIPS-approved&lt;/th&gt;
&lt;th&gt;Best suited for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;HKDF (RFC 5869)&lt;/td&gt;
&lt;td&gt;HMAC&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HMAC approved, cited in NIST guidance&lt;/td&gt;
&lt;td&gt;DH/KEM secrets, protocol schedules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP 800-56C two-step&lt;/td&gt;
&lt;td&gt;HMAC or KMAC&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FixedInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;FIPS key establishment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP 800-56C one-step&lt;/td&gt;
&lt;td&gt;Hash / HMAC / KMAC&lt;/td&gt;
&lt;td&gt;Folded into one call&lt;/td&gt;
&lt;td&gt;Yes, with care&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FixedInfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;FIPS, one-call ECDH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SP 800-108 (expand-only)&lt;/td&gt;
&lt;td&gt;HMAC or KMAC&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No, extract first&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Label&lt;/code&gt; / &lt;code&gt;Context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Subkeys from a uniform key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KMAC-as-KDF (SP 800-185)&lt;/td&gt;
&lt;td&gt;Keccak sponge&lt;/td&gt;
&lt;td&gt;Implicit (sponge)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;customization string&lt;/td&gt;
&lt;td&gt;Yes (SHA-3)&lt;/td&gt;
&lt;td&gt;SHA-3 stacks, built-in separation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BLAKE3 &lt;code&gt;derive_key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLAKE3 tree hash&lt;/td&gt;
&lt;td&gt;Implicit (tree)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;High-throughput, non-FIPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libsodium &lt;code&gt;crypto_kdf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;BLAKE2b&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No, extract first&lt;/td&gt;
&lt;td&gt;8-byte &lt;code&gt;context&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Ergonomic subkeys from a master key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The load-bearing row is &quot;non-uniform input.&quot; If your secret is a raw Diffie-Hellman value, or a hybrid secret whose classical half is non-uniform, you need a real extractor -- HKDF, the 56C two-step, or a sponge or tree that doubles as one -- not a bare expander such as SP 800-108 or &lt;code&gt;crypto_kdf&lt;/code&gt;, which assume you already hold a uniform key. A stand-alone ML-KEM secret is already uniform, so extraction there is a harmless safe default rather than a necessity. Everything else in the table is a secondary tiebreak.&lt;/p&gt;
&lt;p&gt;libsodium&apos;s &lt;code&gt;crypto_kdf&lt;/code&gt; can derive up to $2^{64}$ subkeys from a single master key and context, each between 128 and 512 bits, using an 8-byte string context plus a 64-bit &lt;code&gt;subkey_id&lt;/code&gt; for separation [@libsodium]. It is delightful for subkeys -- and exactly wrong for a raw DH secret, because it never extracts.&lt;/p&gt;
&lt;p&gt;So the decision rules compress to three lines. A &lt;strong&gt;uniform key&lt;/strong&gt; and you need subkeys, reach for &lt;strong&gt;expand-only&lt;/strong&gt; (HKDF-Expand, SP 800-108, KMAC, or &lt;code&gt;crypto_kdf&lt;/code&gt;). A &lt;strong&gt;non-uniform high-entropy secret&lt;/strong&gt; such as a Diffie-Hellman output, or a hybrid whose classical half is non-uniform, reach for &lt;strong&gt;extract-then-expand&lt;/strong&gt; (HKDF or the 56C two-step). A &lt;strong&gt;low-entropy password&lt;/strong&gt;, reach for a &lt;strong&gt;password hash&lt;/strong&gt; (Argon2id), never a KDF from this table. Then the tiebreaks: need FIPS, use SP 800-56C or 800-108; on a SHA-3 stack, use KMAC; outside FIPS and chasing raw throughput on many long keys, use BLAKE3.&lt;/p&gt;
&lt;p&gt;That last branch -- passwords -- deserves care, because it is where the worst mistakes happen and where terminology misleads people.&lt;/p&gt;

Password hashing is genuinely a kind of key derivation -- PBKDF2 literally stands for Password-Based Key Derivation Function. The distinction is not &quot;KDF versus not-KDF&quot;; it is *input entropy*. HKDF and Argon2id are adjacent branches of one family, split by whether the input has real entropy. The error is therefore *directional*: never run HKDF on a password, because Extract &quot;cannot amplify entropy&quot; [@rfc5869], and never run a deliberately slow, memory-hard password hash on a high-entropy DH or KEM secret. Part 12 of this series covers the low-entropy branch; here it only marks the boundary.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You do not have to take my word on the boundary. BLAKE3&apos;s own documentation says it &quot;is not a password hashing algorithm&quot; and points you at Argon2 [@blake3]; libsodium keeps &lt;code&gt;crypto_kdf&lt;/code&gt; and its password-hashing API strictly separate. Part 10 of this series covers BLAKE3 as a hash in its own right.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Honesty about performance: these constructions do not meaningfully compete on speed. Qualitatively, the KDF&apos;s cost is dominated by the surrounding public-key operation -- computing an X25519 or ML-KEM shared secret is orders of magnitude more work than the handful of HMAC compressions HKDF adds on top, as public cycle-count benchmarks for these primitives make plain [@ebacs]. Selection is driven by input assumptions, compliance, and library availability, not throughput.&lt;/p&gt;
&lt;p&gt;Every method here, however clever, obeys the same wall: you can &lt;em&gt;concentrate&lt;/em&gt; entropy but you can never &lt;em&gt;create&lt;/em&gt; it. That conservation law -- and what it forbids -- is the theory underneath all of it.&lt;/p&gt;
&lt;h2&gt;8. You Cannot Create Entropy&lt;/h2&gt;
&lt;p&gt;The single most important result in this field is not an algorithm. It is a conservation law.&lt;/p&gt;

&quot;The extract step in HKDF can concentrate existing entropy but cannot amplify entropy.&quot; -- RFC 5869, section 4 [@rfc5869]
&lt;p&gt;Read literally, that sentence is the border of the entire subject. If the source &lt;code&gt;IKM&lt;/code&gt; carries $m$ bits of min-entropy, then no KDF -- however clever, however many rounds, however long its output -- yields more than about $m$ bits of key security [@rfc5869]. A source with 20 bits of entropy cannot become a 128-bit key by any extractor that has ever been or will ever be designed.&lt;/p&gt;
&lt;p&gt;This is exactly the wall between high-entropy KDFs and password hashing, and it explains why they are different branches. A password hash does &lt;em&gt;not&lt;/em&gt; break this bound either. It cannot add entropy. What it adds is &lt;em&gt;cost per guess&lt;/em&gt;: a salt to defeat precomputation, and deliberate slowness or memory-hardness to make each of the attacker&apos;s guesses expensive. The entropy of a weak password is still low after hashing; the attacker just pays more per attempt. Confusing &quot;make guessing expensive&quot; with &quot;add entropy&quot; is the conceptual error behind running HKDF on a password.&lt;/p&gt;
&lt;p&gt;The conservation law also explains the shape of the 2010 fix. Purely &lt;em&gt;statistical&lt;/em&gt; extraction is lossy: the classical Leftover Hash Lemma says that to land within statistical distance $\varepsilon$ of uniform you pay roughly $2\log_2(1/\varepsilon)$ bits of the source as overhead [@crypto-2004-dghkr]. A &lt;em&gt;computational&lt;/em&gt; extractor -- HMAC keyed by the salt -- sidesteps that tax, delivering full-length pseudorandom keys under a computational assumption instead of an information-theoretic one [@eprint-2010-264]. You trade a little theoretical purity for not throwing away entropy you cannot spare.&lt;/p&gt;
&lt;p&gt;So much for the lower bound. What is the &lt;em&gt;upper&lt;/em&gt; bound -- what can the best construction actually promise? At the primitive level, HKDF is a secure KDF if and only if HMAC is simultaneously a good computational extractor (for Extract) and a good variable-length PRF (for Expand) [@eprint-2010-264]. A real gap in that story was closed only recently: the 2023 dual-PRF analysis proved HMAC is a PRF for keys of &lt;em&gt;arbitrary length&lt;/em&gt;, in the &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;standard model&lt;/a&gt;, with good multi-user bounds [@eprint-2023-861].&lt;/p&gt;
&lt;p&gt;And at the schedule level -- the level that actually matters for a deployed protocol -- Fischlin and Gunther proved, in a multi-stage key-exchange model, that the TLS 1.3 handshake modes &quot;establish session keys with their desired security properties under standard cryptographic assumptions&quot; [@eprint-2020-1044]. That is the current ceiling on what we can prove about a whole deployed key schedule, not just one KDF call.&lt;/p&gt;

A function that stays a secure PRF whether it is keyed through its normal key input *or* through its message input. HMAC is relied on as a dual-PRF wherever a fresh secret arrives in the message slot -- as in the post-quantum hybrids of the previous section. The 2023 analysis shows this holds only for key sets satisfying a &quot;feasibility&quot; condition, which the authors argue real applications meet [@eprint-2023-861].
&lt;p&gt;The honesty this section owes: the theory is settled at the primitive level but has live gaps at the edges. Some HKDF arguments are cleanest with a salt and standard-model assumptions, yet salt-less HKDF -- the common case, since TLS uses all-zero salts at several stages -- leans on stronger, more idealized assumptions [@rfc5869]. The community shorthand for the danger of over-modeling is blunt.&lt;/p&gt;

It is tempting to reason about HKDF as if it were a perfect random function -- the very habit that made the old designs unprovable. HKDF is *not* a random oracle, and treating it as one can prove things that are not true of the real construction. Its guarantees are the extractor and PRF properties of HMAC, no more. The dual-PRF result sharpens the same caution: the swap-key guarantee is *conditional*, holding only for feasible key sets, so each new post-quantum wiring must re-check that its secrets land in a feasible set [@eprint-2023-861]. Part 1 of this series covers the PRF and random-oracle assumptions these arguments rest on.
&lt;p&gt;What would the &lt;em&gt;ideal&lt;/em&gt; KDF be? A random oracle keyed by the secret: a black box that, for each distinct pair of secret and context, returns fresh independent uniform bits, with security equal to the secret&apos;s min-entropy and zero leakage across contexts. True random oracles do not exist, so that ideal is not reachable in the standard model. But extract-then-expand, with a distinct &lt;code&gt;info&lt;/code&gt; per key, is the closest deployable approximation of it -- Extract gives &quot;uniform from non-uniform&quot; up to the entropy bound, and Expand-with-context gives &quot;independent per query&quot; up to PRF security.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; No KDF yields more key security than the min-entropy already in its input. That one conservation law is the entire border between HKDF and password hashing, and no amount of stretching moves it. Independence is not free either: if two derivations share a secret and bind the same or empty context, their outputs are &lt;em&gt;identical&lt;/em&gt; -- which is not a theoretical nicety but exactly how the Triple Handshake and Selfie breaks happened.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The primitive-level theory is essentially finished. So where is the real frontier? Not in the KDF box -- in everything wired around it.&lt;/p&gt;
&lt;h2&gt;9. Open Problems at the Wiring Layer&lt;/h2&gt;
&lt;p&gt;Here is the tell that tells you where the frontier is: &lt;em&gt;every named break in this space was a schedule-level bug, not a primitive-level one.&lt;/em&gt; The KDF itself is proven. The genuinely open, hard problems all live in how we wire it and how we prove the wiring.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How safe is plain concatenation as a post-quantum combiner?&lt;/strong&gt; Deployed hybrids concatenate the classical and post-quantum shared secrets and feed the result to the existing HKDF-Extract [@draft-hybrid]. Is that generic construction &lt;em&gt;always&lt;/em&gt; as good as a purpose-built combiner? Generic KEM-combiner theory gives black-box results -- the combined KEM is secure &quot;as long as at least one of the ingredient KEMs is&quot; [@eprint-2018-024]. The dedicated route, X-Wing, earns a tighter proof and better efficiency, but only because it fixes X25519, ML-KEM-768, and SHA3-256 concretely; its guarantees &quot;may not apply in the general case&quot; [@eprint-2024-039]. What is still missing is a clean, general characterization of when concatenation is safe -- covering KEM binding, ciphertext collisions, and how those interact with the rest of the schedule -- versus when a dedicated combiner is required. This is not academic: it guards a growing double-digit share of internet traffic during the migration.&lt;/p&gt;
&lt;p&gt;X-Wing&apos;s efficiency and tight proof are specific to its exact primitive choices, so they should not be read as a general statement about concatenation. Its author list is deliberately not asserted here: the source page renders none, and prior search results were inconsistent [@eprint-2024-039].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The pending NIST revision is announced, not published.&lt;/strong&gt; The January 2026 planning note says NIST will revise SP 800-56C to admit a KEM secret, allow flexible hybrid formatting, and approve KMAC as an extraction step [@nist-2026-news]. Until the document actually lands, the exact approved formatting for hybrid secrets and the KMAC-as-extractor parameters are unsettled.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Treat the 2026 SP 800-56C revision as a planning note, not a specification. NIST has stated its intent to bless KEM secrets and KMAC extraction, but the parameters are not final [@nist-2026-news]. Design against the current standard and the IETF drafts, and be ready to adjust the formatting details when the revised document is published.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nobody has machine-checked an &lt;em&gt;entire&lt;/em&gt; modern key schedule.&lt;/strong&gt; We can prove a great deal about one HKDF call; proving the whole hierarchy of a real protocol -- every stage, every label, resumption, 0-RTT, key updates, external PSKs -- is far harder, and symbolic and computational proofs cover different threat models. Symbolic analysis with Tamarin modeled all of the TLS 1.3 handshake modes and still surfaced an unexpected authentication behavior [@acm-tamarin]; computational multi-stage proofs fix a specific model and version [@eprint-2020-1044]. TLS 1.3 is among the most-analyzed protocols in history, and yet each proof pins one model and one version. The open goal is whole-schedule, machine-checked proofs that track the derivation graph -- labels, transcript binding, key separation -- for the newer hierarchies too: MLS, Signal&apos;s Double Ratchet, and the post-quantum hybrids.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Could a KDF API make misuse unrepresentable?&lt;/strong&gt; Every real break here is a &lt;em&gt;usage&lt;/em&gt; error the type system permitted: a skipped Extract, a missing or duplicated &lt;code&gt;info&lt;/code&gt;, one Expand output split across two keys. Labeled wrappers push toward &quot;you cannot derive a key without naming its purpose&quot; -- TLS&apos;s &lt;code&gt;HKDF-Expand-Label&lt;/code&gt;, HPKE&apos;s &lt;code&gt;LabeledExtract&lt;/code&gt; and &lt;code&gt;LabeledExpand&lt;/code&gt; with a suite identifier [@rfc9180], BLAKE3&apos;s mandatory &lt;code&gt;context&lt;/code&gt; [@blake3]. But these are conventions, not enforcement: they &lt;em&gt;encourage&lt;/em&gt; separation without &lt;em&gt;preventing&lt;/em&gt; reuse. The open design problem is an interface -- in the type or effect system -- where forgetting the context, reusing an output, or truncating instead of re-deriving simply does not compile.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is HMAC really a dual-PRF everywhere we now lean on it?&lt;/strong&gt; The dual-PRF guarantee holds only for feasible key sets [@eprint-2023-861]. TLS 1.3, KEMTLS, MLS, and Noise all assume the swap direction, and the new post-quantum wirings feed novel, structured lattice secrets into the message slot. The open problem is a per-deployment or automated check that each new hybrid schedule&apos;s secrets land in a feasible set -- so the assumption is &lt;em&gt;verified&lt;/em&gt; rather than &lt;em&gt;assumed&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;These are open at the research edge. But you have a protocol to ship this week. What does the whole discipline boil down to in practice?&lt;/p&gt;
&lt;h2&gt;10. A Practical Guide: What to Actually Type&lt;/h2&gt;
&lt;p&gt;Strip the theory to rules you can apply without reading a single proof. If you keep one diagram from this article, make it this one.&lt;/p&gt;

flowchart TD
    START[&quot;Your input keying material&quot;] --&amp;gt; Q1{&quot;Low-entropy password or PIN?&quot;}
    Q1 --&amp;gt;|Yes| PW[&quot;Password hash: Argon2id (Part 12), not HKDF&quot;]
    Q1 --&amp;gt;|No| Q2{&quot;Already a uniform key?&quot;}
    Q2 --&amp;gt;|Yes| EXPAND[&quot;Expand-only: HKDF-Expand, SP 800-108, KMAC, or crypto_kdf. Distinct label per subkey&quot;]
    Q2 --&amp;gt;|No, a raw Diffie-Hellman value| EXTRACT[&quot;Extract-then-expand: HKDF or SP 800-56C two-step. Do not skip Extract, salt if you have one&quot;]
&lt;p&gt;That tree is the whole decision. A &lt;strong&gt;low-entropy password&lt;/strong&gt; goes to Argon2id, never to HKDF, because Extract cannot manufacture the entropy a password lacks [@rfc5869]. A &lt;strong&gt;non-uniform high-entropy secret&lt;/strong&gt; -- a Diffie-Hellman output, or a hybrid whose classical half is non-uniform -- goes to extract-then-expand, and you must &lt;em&gt;not&lt;/em&gt; skip Extract [@rfc5869]. An &lt;strong&gt;already-uniform key&lt;/strong&gt; goes to expand-only, with a distinct label for every subkey.&lt;/p&gt;
&lt;p&gt;Once you are in the HKDF branch, a short checklist keeps you out of trouble:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Give every derived key its own &lt;code&gt;info&lt;/code&gt; or label.&lt;/strong&gt; This is rule two, made operational. Two keys, two distinct context strings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use a real salt when you have one&lt;/strong&gt;, otherwise a fixed protocol-labeled salt. The salt strengthens extraction; the one input you must never omit is the context [@rfc5869].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respect the output ceiling.&lt;/strong&gt; A single derivation caps at $255 \cdot \mathrm{HashLen}$ bytes [@rfc5869]. Need more keys? Do more derivations with distinct labels -- never truncate one long output into two keys, and never split one Expand output across two purposes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adopt a labeling convention.&lt;/strong&gt; TLS&apos;s &lt;code&gt;Derive-Secret&lt;/code&gt; and HPKE&apos;s &lt;code&gt;LabeledExtract&lt;/code&gt; and &lt;code&gt;LabeledExpand&lt;/code&gt; are reusable templates even outside those protocols [@rfc8446] [@rfc9180]. They bake domain separation into every call.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A practical recipe for the &lt;code&gt;info&lt;/code&gt; string is to give it structure: a protocol identifier, a version, the purpose, and, where authentication depends on it, the role or identity. &lt;code&gt;&quot;myapp v1 client write&quot;&lt;/code&gt; is a better context than &lt;code&gt;&quot;key1&quot;&lt;/code&gt;, because it cannot accidentally collide with another purpose and it says what it is.&lt;/p&gt;

Including a hash of the messages exchanged so far as context in a derivation, so that a derived key is valid only for one exact conversation. TLS 1.3&apos;s `Derive-Secret` binds a transcript hash at every stage -- which is precisely what makes the Triple Handshake attack impossible against it.
&lt;p&gt;You can make the correct behavior the &lt;em&gt;only&lt;/em&gt; behavior. The wrapper below refuses to derive a key without a purpose label -- a small, self-imposed version of the misuse-resistant API from the open problems. Run it: the unlabeled call is blocked, and the two labeled calls produce independent keys.&lt;/p&gt;
&lt;p&gt;{`
// A Derive-Secret-style wrapper: no purpose label, no key.
const toHex = (buf) =&amp;gt;
  Array.from(new Uint8Array(buf)).map((b) =&amp;gt; b.toString(16).padStart(2, &apos;0&apos;)).join(&apos;&apos;);
const hexToBytes = (h) =&amp;gt; new Uint8Array(h.match(/../g).map((x) =&amp;gt; parseInt(x, 16)));
const bytes = (s) =&amp;gt; new TextEncoder().encode(s);&lt;/p&gt;
&lt;p&gt;async function hmac(k, m) {
  const key = await crypto.subtle.importKey(
    &apos;raw&apos;, k, { name: &apos;HMAC&apos;, hash: &apos;SHA-256&apos; }, false, [&apos;sign&apos;]);
  return new Uint8Array(await crypto.subtle.sign(&apos;HMAC&apos;, key, m));
}
async function hkdfExpand(prk, info, L) {
  const N = Math.ceil(L / 32);
  let t = new Uint8Array(0), okm = new Uint8Array(0);
  for (let i = 1; i &amp;lt;= N; i++) {
    const block = new Uint8Array(t.length + info.length + 1);
    block.set(t, 0); block.set(info, t.length); block.set([i], t.length + info.length);
    t = await hmac(prk, block);
    const merged = new Uint8Array(okm.length + t.length);
    merged.set(okm, 0); merged.set(t, okm.length); okm = merged;
  }
  return okm.slice(0, L);
}&lt;/p&gt;
&lt;p&gt;// Every derived key MUST name its purpose. The info encodes protocol, version, purpose.
async function deriveKey(prk, purpose, lengthBytes) {
  if (!purpose || purpose.length === 0) {
    throw new Error(&apos;refusing to derive a key with no purpose label&apos;);
  }
  return hkdfExpand(prk, bytes(&apos;myapp v1 &apos; + purpose), lengthBytes);
}&lt;/p&gt;
&lt;p&gt;(async () =&amp;gt; {
  const prk = hexToBytes(&apos;077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5&apos;);
  try {
    await deriveKey(prk, &apos;&apos;, 32);            // forgot the purpose -&amp;gt; blocked
  } catch (e) {
    console.log(&apos;blocked:&apos;, e.message);
  }
  const clientWrite = await deriveKey(prk, &apos;client write&apos;, 32);
  const serverWrite = await deriveKey(prk, &apos;server write&apos;, 32);
  console.log(&apos;client write =&apos;, toHex(clientWrite));
  console.log(&apos;server write =&apos;, toHex(serverWrite));
})();
`}&lt;/p&gt;
&lt;p&gt;You rarely need to implement any of this by hand. HKDF ships in OpenSSL (via &lt;code&gt;EVP_KDF&lt;/code&gt;) [@openssl-hkdf] and BoringSSL (via the standalone &lt;code&gt;HKDF&lt;/code&gt;, &lt;code&gt;HKDF_extract&lt;/code&gt;, and &lt;code&gt;HKDF_expand&lt;/code&gt; functions in &lt;code&gt;hkdf.h&lt;/code&gt;) [@boringssl-hkdf], in Python&apos;s &lt;code&gt;cryptography&lt;/code&gt; [@pyca-hkdf], in RustCrypto&apos;s &lt;code&gt;hkdf&lt;/code&gt; [@rustcrypto-hkdf], and in Go&apos;s standard library.Go promoted HKDF into the standard library as &lt;code&gt;crypto/hkdf&lt;/code&gt; in Go 1.24; before that it lived in &lt;code&gt;golang.org/x/crypto/hkdf&lt;/code&gt; [@go124-notes]. The move is a small signal of how settled the primitive is -- stable enough to be standard-library furniture. For the expand-only and FIPS paths, reach for the SP 800-108 and 56C key-based derivation functions in your platform&apos;s crypto library.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The single habit that prevents most KDF bugs: give every derived key an explicit, unique purpose string, and never reuse or split one. If your code derives a key without naming what it is for, that missing name &lt;em&gt;is&lt;/em&gt; the bug. And keep the salt-versus-nonce line straight -- a salt is fixed and reusable, a nonce is per-message and must never repeat (Part 2).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every rule above exists because someone shipped its opposite. Here is the catalog of what goes wrong -- and the three-rule lens that explains all of it.&lt;/p&gt;
&lt;h2&gt;11. The Antipattern Catalog, Root-Caused&lt;/h2&gt;
&lt;p&gt;Almost every notable, well-documented break in this space is the violation of one of the three rules from the very first section. Once you see the pattern, you cannot unsee it.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Break&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Rule broken&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Triple Handshake&lt;/td&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;Master secret not bound to the session transcript&lt;/td&gt;
&lt;td&gt;Rule 3: too little context&lt;/td&gt;
&lt;td&gt;Extended Master Secret&lt;/td&gt;
&lt;td&gt;[@doi-sp2014] [@rfc7627]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Selfie&lt;/td&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;Derived keys not bound to who is speaking&lt;/td&gt;
&lt;td&gt;Rule 3: identity not bound&lt;/td&gt;
&lt;td&gt;Bind role and identity&lt;/td&gt;
&lt;td&gt;[@eprint-2019-347]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw secret as key&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;Skipped Extract on a non-uniform secret&lt;/td&gt;
&lt;td&gt;Rule 1: do not skip Extract&lt;/td&gt;
&lt;td&gt;Run Extract first&lt;/td&gt;
&lt;td&gt;[@rfc5869]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homemade `H(secret&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;info)`&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;No proof, plus length extension&lt;/td&gt;
&lt;td&gt;Rule 1, and key the hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One key for encrypt and MAC&lt;/td&gt;
&lt;td&gt;any&lt;/td&gt;
&lt;td&gt;One key serving two purposes&lt;/td&gt;
&lt;td&gt;Rule 2: separate by context&lt;/td&gt;
&lt;td&gt;Two distinct &lt;code&gt;info&lt;/code&gt; strings&lt;/td&gt;
&lt;td&gt;[@rfc5869]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Take the two famous ones in turn. In the &lt;strong&gt;Triple Handshake&lt;/strong&gt; attack, an active attacker set up two TLS sessions so that both derived the &lt;em&gt;same&lt;/em&gt; master secret, because that secret was not bound to the sessions&apos; certificates and parameters -- breaking the authentication that resumption and renegotiation depend on [@doi-sp2014]. Root cause: too little context in the derived secret. The fix, the Extended Master Secret, bound the master secret to a hash of the full handshake, and TLS 1.3 later generalized that into per-stage transcript binding [@rfc7627].&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;Selfie&lt;/strong&gt;, a party using an external pre-shared key could be tricked into completing a handshake &lt;em&gt;with itself&lt;/em&gt; -- a reflection -- because the derived keys were not bound to &lt;em&gt;who&lt;/em&gt; was speaking [@eprint-2019-347]. Root cause: identity and role not bound. It is the same rule-three failure as Triple Handshake, wearing the costume of identity rather than transcript.&lt;/p&gt;
&lt;p&gt;And the third break needs no research paper, because it is the antipattern from the opening: use the raw Diffie-Hellman or ML-KEM output directly as a key. For a Diffie-Hellman value that is skipping Extract, which RFC 5869 names and forbids in one clause; a uniform ML-KEM secret is still single-use, so using it directly collapses every purpose onto one key [@rfc5869].&lt;/p&gt;
&lt;p&gt;The rest of the misuse list is the same handful of errors: a homemade &lt;code&gt;H(secret || info)&lt;/code&gt; that has no proof and leaks by &lt;a href=&quot;https://paragmali.com/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp/&quot; rel=&quot;noopener&quot;&gt;length extension&lt;/a&gt;; reusing one derived key for both encryption and its MAC, or splitting one Expand output across two purposes; a missing or duplicate &lt;code&gt;info&lt;/code&gt; that silently collides two keys; running HKDF on a password; confusing a fixed salt with a per-message nonce; and truncating a longer output rather than re-deriving with the right length and label [@rfc5869].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you remember a single prohibition from this article, make it this: never key a cipher or a MAC directly from a raw Diffie-Hellman or ML-KEM secret. A Diffie-Hellman value is non-uniform, so skipping its Extract step is rule one -- the root of the whole misuse catalog, and RFC 5869 spells it out, the extract step &quot;SHOULD NOT be skipped&quot; [@rfc5869]. An ML-KEM secret is already uniform but single-use, so it still must be expanded into a distinct key per context before it goes near a cipher.&lt;/p&gt;
&lt;/blockquote&gt;


No. A Diffie-Hellman output is non-uniform; an ML-KEM output is already uniform but single-use; neither is a finished key. Run HKDF-Extract to get a uniform PRK, then Expand a distinct key for each use with its own `info` string. In the deployed X25519MLKEM768 hybrid the concatenated secret still needs Extract, because its classical X25519 half is non-uniform [@rfc5869].


No. A homemade `H(secret || context)` has no security proof for a non-uniform input, and with a Merkle-Damgard hash such as SHA-256 it invites length-extension attacks. Use HMAC-based HKDF instead; Part 10 covers the hashing details [@rfc5869].


They are one family split by input entropy, and the error is directional. Use HKDF for high-entropy secrets such as DH and KEM outputs; use Argon2id for passwords. Never run HKDF on a password -- Extract cannot amplify entropy -- and never run a slow, memory-hard password hash on a high-entropy DH or KEM secret [@rfc5869].


The `salt` and `info` are both optional in RFC 5869 but recommended. A salt strengthens extraction, and a fixed protocol-labeled salt is acceptable when you have no fresh one. The context input, `info`, is the one you must never omit -- it is what separates your keys [@rfc5869].


No. Derive two keys with two distinct `info` strings. Never split one Expand output across two purposes, and never truncate a longer output into two keys -- re-derive with distinct labels instead [@rfc5869].


No. An ML-KEM shared secret is just another high-entropy input. Deployed hybrids concatenate the classical and post-quantum secrets and feed the pair into the same, unchanged HKDF-Extract [@draft-hybrid]. The primitive did not change; only what you feed it did [@fips203].

&lt;p&gt;So return to the moment we started in: the handshake is done, the shared secret is in your hand, and the tempting one-liner is to use it as a key. You now know why not, twice over, and you know the discipline that replaces the temptation. Extract concentrates the entropy that is there -- never inventing what is not. Expand, with a distinct context for every key, stamps out independence. Bind the transcript and the identities, and two situations can never collapse into one key.&lt;/p&gt;
&lt;p&gt;The TLS 1.3 schedule is not a special trick; it is simply what obeying all three rules at once looks like, drawn as a protocol -- and post-quantum migration proved the design right by changing the input without touching the primitive.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; One secret is not one key. Extract concentrates entropy and can never amplify it; Expand plus a distinct context stamps out independent keys; and binding the transcript keeps two conversations from ever sharing a key. Get those three right and the named breaks never reach you. Reach for the raw secret, and you have already made the one mistake the whole discipline exists to prevent.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Part 14 turns from deriving keys to spending them: the authenticated encryption those traffic keys feed. But the habit you carry out of this one is smaller and sharper than any schedule -- never again reach for the raw secret.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;key-derivation-hkdf-key-hierarchies&quot; keyTerms={[
  { term: &quot;Key material&quot;, definition: &quot;A raw shared secret, possibly non-uniform and single-use, that must be run through a KDF before it becomes a usable key.&quot; },
  { term: &quot;Extract&quot;, definition: &quot;The KDF stage that concentrates a non-uniform secret&apos;s entropy into one uniform pseudorandom key. It cannot amplify entropy.&quot; },
  { term: &quot;Expand&quot;, definition: &quot;The KDF stage that derives many independent keys from the PRK, each bound to a distinct info context.&quot; },
  { term: &quot;PRK&quot;, definition: &quot;Pseudorandom Key: the uniform HashLen-byte output of Extract, and the sole keying input to Expand.&quot; },
  { term: &quot;Domain separation&quot;, definition: &quot;Binding a distinct context label into each derivation so that keys for different purposes are independent.&quot; },
  { term: &quot;Transcript binding&quot;, definition: &quot;Including a hash of the handshake messages as context so a key is valid for only one exact conversation.&quot; },
  { term: &quot;Dual-PRF&quot;, definition: &quot;A PRF that stays secure when keyed through its message input, relied on when a fresh secret is fed into HMAC&apos;s message slot.&quot; }
]} questions={[
  { q: &quot;Why is a raw Diffie-Hellman output not safe to use directly as an AES key?&quot;, a: &quot;It is non-uniform, a structured group element rather than random bytes, and single-purpose. It must be extracted into a uniform PRK, then expanded into a distinct key per use.&quot; },
  { q: &quot;What is the one thing Extract can never do?&quot;, a: &quot;Create entropy. It concentrates the min-entropy already present but cannot amplify it, which is exactly the boundary with password hashing.&quot; },
  { q: &quot;What are the three rules whose violation explains almost every break?&quot;, a: &quot;Do not skip Extract on a non-uniform secret; do not reuse a derived key across contexts; do not bind too little context.&quot; },
  { q: &quot;Which rule did the Triple Handshake attack break, and how was it fixed?&quot;, a: &quot;Rule three, too little context: the master secret was not bound to the transcript. The Extended Master Secret bound it to a session hash, and TLS 1.3 made per-stage transcript binding structural.&quot; },
  { q: &quot;How did post-quantum migration change key derivation?&quot;, a: &quot;Only the input changed. The classical and ML-KEM shared secrets are concatenated and fed into the same, unchanged HKDF-Extract.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>key-derivation</category><category>hkdf</category><category>tls-1-3</category><category>key-separation</category><category>extract-then-expand</category><category>post-quantum-crypto</category><category>cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>RSA Is a Trapdoor, Not a Cryptosystem: OAEP, PSS, and the 25-Year Padding-Oracle Lineage</title><link>https://paragmali.com/blog/rsa-is-a-trapdoor-not-a-cryptosystem-oaep-pss-and-the-25-yea/</link><guid isPermaLink="true">https://paragmali.com/blog/rsa-is-a-trapdoor-not-a-cryptosystem-oaep-pss-and-the-25-yea/</guid><description>Textbook RSA is a trapdoor, not a cryptosystem. A field guide to OAEP, PSS, PKCS#1 v1.5, and the Bleichenbacher-ROBOT-Marvin padding-oracle lineage, done right.</description><pubDate>Sat, 11 Jul 2026 09:07:03 GMT</pubDate><content:encoded>
**RSA is a trapdoor permutation, not a cryptosystem** -- and almost every real-world RSA break came from treating the bare trapdoor as if it were already secure. Textbook RSA is deterministic and malleable. PKCS#1 v1.5 *encryption* turns &quot;is this padding valid?&quot; into a decryption oracle that leaked through ever-quieter channels for 25 years, from Bleichenbacher (1998) [@bleichenbacher98] through DROWN [@drown16] and ROBOT [@robot18] to Marvin&apos;s pure-timing attack (2023) [@marvin23]. &quot;Done right&quot; is a stack that must all hold at once: the right scheme (**OAEP** for encryption, **PSS** for new signatures), the right parameters (2048-bit floor, `e = 65537`, CSPRNG primes), and a **constant-time, fault-checked implementation** -- because security is a property of the scheme *and* its implementation, not of the math. And keep one split absolute: v1.5 *signatures* are unbroken and still dominant (verify strictly), while v1.5 *encryption* must be retired.
&lt;h2&gt;1. The Modulus Was Never Factored&lt;/h2&gt;
&lt;p&gt;In 2018, three researchers signed a message with the private key behind &lt;code&gt;facebook.com&lt;/code&gt;&apos;s TLS certificate [@robot18]. They never factored Facebook&apos;s 2,048-bit modulus. Nobody has ever publicly factored a 2,048-bit modulus -- the public record stands at 829 bits, and that took roughly 2,700 core-years [@rsa250].&lt;/p&gt;
&lt;p&gt;Instead, they asked one of Facebook&apos;s front-end servers the same yes-or-no question a few hundred thousand times -- &lt;em&gt;is this padding valid?&lt;/em&gt; -- and let the pattern of answers spell out the secret [@robot18]. The attack they used was already nineteen years old [@robotsite]. Five years later, a Red Hat engineer reproduced the same break against software that was supposed to be immune, using nothing but a stopwatch, and called it Marvin [@marvin23].&lt;/p&gt;
&lt;p&gt;Notice what did not happen. No prime was recovered. No number was factored. The RSA problem -- invert &lt;code&gt;c = m^e mod N&lt;/code&gt; without the private key -- stood exactly as hard the day after as the day before. What broke was the server&apos;s &lt;em&gt;reaction&lt;/em&gt;: the way it answered a question about a ciphertext it did not create.&lt;/p&gt;
&lt;p&gt;That is the whole subject in one sentence. Textbook RSA -- the bare &lt;code&gt;m^e mod N&lt;/code&gt; you meet in a first course -- is a &lt;strong&gt;trapdoor permutation, not a cryptosystem&lt;/strong&gt;. It is a beautiful one-way function with a secret shortcut, and nothing more.&lt;/p&gt;
&lt;p&gt;Everything that turns it into secure encryption or a secure signature lives &lt;em&gt;around&lt;/em&gt; the permutation, in the padding, the parameters, and the code that runs the operation. Almost every famous RSA disaster of the last three decades is a variation on one theme: someone used the bare trapdoor as if it were already a cryptosystem, or let the machinery meant to secure it confess -- through an error message, a network timeout, a microsecond of timing, or an injected fault -- whether a check had passed.&lt;/p&gt;

The math was never broken. The padding check told the attacker whether it passed.
&lt;p&gt;So here is the diagnostic question this article keeps asking, the one that unlocks the entire failure catalog: &lt;strong&gt;when a ciphertext or signature it did not create arrives, what does the receiver reveal about it -- through its answer, its timing, or its faults -- before and after it has checked the padding?&lt;/strong&gt; Four names you may know as headlines -- Bleichenbacher, DROWN, ROBOT, Marvin -- are four answers to that question, each leaking the same single bit through a quieter channel than the last. To see why one yes-or-no question about padding is enough to reconstruct a session key, you first have to see what RSA actually &lt;em&gt;is&lt;/em&gt;, and what it is not.&lt;/p&gt;
&lt;h2&gt;2. A Trapdoor for Diffie-Hellman&apos;s Challenge&lt;/h2&gt;
&lt;p&gt;In 1976, Whitfield Diffie and Martin Hellman wrote down the shape of a future that did not yet have an engine. Their paper &lt;em&gt;New Directions in Cryptography&lt;/em&gt; described public-key encryption and, remarkably, the idea of a digital signature -- a value only you can produce but anyone can check [@dh76]. What they could not supply was a concrete &lt;em&gt;trapdoor&lt;/em&gt;: a function easy to compute in one direction, hard to invert, yet effortless to invert if you hold a secret. Their paper is a challenge with a hole in it, and the hole is shaped exactly like RSA.&lt;/p&gt;
&lt;p&gt;A year later, Ron Rivest, Adi Shamir, and Leonard Adleman filled it. Pick two large primes $p$ and $q$ and set $N = pq$. Choose a public exponent $e$, and compute a private exponent $d$ with $ed \equiv 1 \pmod{\lambda(N)}$. Publish $(N, e)$; keep $d$, $p$, and $q$ secret. To encrypt a message represented as a number $m &amp;lt; N$, raise it to the public exponent; to decrypt, raise the result to the private one [@rsa78]:&lt;/p&gt;
&lt;p&gt;$$c = m^e \bmod N, \qquad m = c^d \bmod N.$$&lt;/p&gt;
&lt;p&gt;Why does the second operation undo the first? Because of Euler&apos;s theorem (1763): for any $m$ coprime to $N$, $m^{\phi(N)} \equiv 1 \pmod N$, with the totient $\phi(N) = (p-1)(q-1)$. Its Carmichael-function refinement (Carmichael, 1910) gives the same identity for the smaller $\lambda(N) = \mathrm{lcm}(p-1, q-1)$. Since $ed \equiv 1 \pmod{\lambda(N)}$, we have $ed = 1 + k\lambda(N)$ for some integer $k$, so $c^d = m^{ed} = m \cdot (m^{\lambda(N)})^k \equiv m \pmod N$. The exponents cancel, and the plaintext falls out -- but only for someone who could compute $d$, and computing $d$ requires $\lambda(N)$, which requires the factors of $N$. That is the trapdoor: the factorization of $N$ is the secret that inverts the permutation.The original 1978 paper used $\phi(N) = (p-1)(q-1)$, Euler&apos;s totient. Modern standards use the smaller Carmichael function $\lambda(N) = \mathrm{lcm}(p-1, q-1)$, which yields a smaller valid $d$ and the same correctness. Either works; $\lambda(N)$ is now the convention in FIPS 186-5.&lt;/p&gt;

A function that is easy to evaluate in the forward direction, computationally hard to invert without a secret, and easy to invert with it. RSA&apos;s forward map is x maps to x raised to the e, modulo N; the trapdoor that inverts it is the factorization of N. A trapdoor permutation is a primitive, not a complete encryption scheme -- it says nothing about hiding partial information or resisting tampering.
&lt;p&gt;The elegance that made RSA famous is that the &lt;em&gt;same&lt;/em&gt; operation both encrypts and signs. Swap the roles of the exponents: to sign a message, raise it to the private exponent, $s = m^d \bmod N$, an act only the key holder can perform; to verify, raise the signature to the public exponent and check that $s^e \equiv m \pmod N$, which anyone can do [@rsa78]. One modular exponentiation, read in two directions, is both a lock only you can open and a seal only you can stamp. Boneh&apos;s survey calls this duality the source of both RSA&apos;s reach and its long catalogue of misuse [@boneh99].&lt;/p&gt;
&lt;p&gt;Two engineering choices from this era matter later, because each returns as an attack surface. The first is the public exponent. Almost every deployed RSA key uses $e = 65537$.65537 is the Fermat prime $F_4 = 2^{16} + 1$. Written in binary it is &lt;code&gt;1&lt;/code&gt; followed by fifteen &lt;code&gt;0&lt;/code&gt;s and a final &lt;code&gt;1&lt;/code&gt; -- a Hamming weight of just 2 -- so public-key operations cost only sixteen squarings and one multiplication. Small enough to be fast, large enough to dodge the low-exponent traps of $e = 3$. The second is how the private operation is computed. Rather than one exponentiation modulo $N$, implementations work modulo $p$ and modulo $q$ separately and recombine, using the Chinese Remainder Theorem for roughly a fourfold speedup.&lt;/p&gt;

A technique for computing the private operation c raised to the d, modulo N, by working separately modulo p and modulo q on half-size numbers and then recombining the two halves. It cuts the cost of decryption and signing by about four times. Because it splits the secret operation across the two prime factors, it also opens a physical-fault attack surface that the single-modulus form does not have.
&lt;p&gt;Hold those two choices in mind: the small public exponent and the CRT shortcut will each come back to bite an implementation that treats them casually. But the deepest seed was planted in the 1978 paper itself, and it was invisible at the time.&lt;/p&gt;
&lt;p&gt;Rivest, Shamir, and Adleman demonstrated the trapdoor on &lt;em&gt;raw&lt;/em&gt; message numbers. That bare application is deterministic, algebraically malleable, and carries structure an attacker can exploit -- three properties that, in 1978, looked like harmless features of a clean mathematical object. One operation, elegant enough to both lock and sign. So why is calling that operation directly -- what cryptographers now dismiss as &quot;textbook RSA&quot; -- treated as a bug in every serious codebase?&lt;/p&gt;
&lt;h2&gt;3. Why Textbook RSA Is Not a Cryptosystem&lt;/h2&gt;
&lt;p&gt;Encryption has a minimum bar -- lower than most people think -- and textbook RSA fails to clear it. The bar is called &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;IND-CPA&lt;/a&gt;: an adversary who picks two plaintexts and receives the encryption of one cannot tell which, better than a coin flip. (Part 1 of this series builds that definition carefully; here we only need its consequences.) Bare RSA loses this game three separate ways, each a direct consequence of the naked permutation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It is deterministic.&lt;/strong&gt; The same plaintext always encrypts to the same ciphertext, because $m^e \bmod N$ is a function with no randomness in it. That is fatal whenever the message space is small or guessable. Suppose a server encrypts a one-word trading instruction, either &lt;code&gt;BUY&lt;/code&gt; or &lt;code&gt;SELL&lt;/code&gt;, under a known public key. An eavesdropper does not need to decrypt anything: they encrypt both candidate words, compare against the captured ciphertext, and read the plaintext with zero queries. Determinism can never be semantic security -- this is structural, not a missing optimization you can bolt on later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It is malleable.&lt;/strong&gt; RSA is multiplicatively homomorphic: multiply a ciphertext by $k^e$ and the plaintext is silently multiplied by $k$.&lt;/p&gt;

A cipher is malleable when an attacker can transform a ciphertext into another valid ciphertext whose plaintext is a predictable function of the original -- without decrypting anything. RSA satisfies the multiplicative relation: the encryption of m times the encryption-form of k decrypts to m times k, modulo N. Useful for some protocols, catastrophic for confidentiality, and the exact algebraic lever Bleichenbacher later pulls.
&lt;p&gt;That relation is not a curiosity; it is the crowbar behind the entire padding-oracle lineage. The demonstration below uses toy primes so the arithmetic is legible in a browser, but the algebra is identical at 2,048 bits.&lt;/p&gt;
&lt;p&gt;{`
// Toy RSA: p=61, q=53, N=3233, e=17, d=2753. NOT secure sizes -- for illustration.
function modpow(base, exp, mod) {
  base = base % mod; let r = 1n;
  while (exp &amp;gt; 0n) {
    if (exp &amp;amp; 1n) r = (r * base) % mod;
    base = (base * base) % mod; exp &amp;gt;&amp;gt;= 1n;
  }
  return r;
}
const N = 3233n, e = 17n, d = 2753n;
const m = 42n;&lt;/p&gt;
&lt;p&gt;// 1) Deterministic: encrypting the same message twice gives the same ciphertext.
const c1 = modpow(m, e, N), c2 = modpow(m, e, N);
console.log(&apos;encrypt 42 twice -&amp;gt;&apos;, c1.toString(), c2.toString(), &apos;| identical?&apos;, c1 === c2);&lt;/p&gt;
&lt;p&gt;// 2) Malleable: multiply the ciphertext by k^e and the plaintext scales by k.
const k = 2n;
const cForged = (c1 * modpow(k, e, N)) % N;      // attacker never decrypts
const mForged = modpow(cForged, d, N);           // what the victim would recover
console.log(&apos;tampered ct decrypts to&apos;, mForged.toString(), &apos;= (42*2) mod N =&apos;, ((m * k) % N).toString());
`}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It leaks structure for small exponents and short messages.&lt;/strong&gt; If a message is short enough that $m^e &amp;lt; N$, no modular reduction ever happens, so recovering $m$ is a plain integer $e$-th root -- no factoring required.&lt;/p&gt;
&lt;p&gt;Johan Hastad showed in 1988 that if the same message is broadcast to $e$ recipients under $e = 3$, the plaintext falls out by the Chinese Remainder Theorem [@hastad88]. Don Coppersmith sharpened this in 1996 into a lattice method that recovers a message whenever the unknown part is smaller than $N^{1/e}$, breaking &quot;stereotyped&quot; messages with a small hidden field [@coppersmith97]. Every one of these is an attack on &lt;em&gt;textbook&lt;/em&gt; RSA, not on the RSA problem itself -- the factoring assumption stays intact while the plaintext walks out the front door [@boneh99].&lt;/p&gt;
&lt;p&gt;So a trapdoor permutation is not a cryptosystem. To become one, it needs a transformation applied to the message &lt;em&gt;before&lt;/em&gt; the permutation -- padding -- and that padding has to do three separable jobs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A trapdoor becomes a cryptosystem only when the padding does three separable jobs: (1) add &lt;strong&gt;randomness&lt;/strong&gt;, so equal plaintexts produce different ciphertexts; (2) add &lt;strong&gt;checkable redundancy&lt;/strong&gt;, so the receiver can detect a tampered or malformed ciphertext; and (3) -- the job the field kept forgetting -- be &lt;strong&gt;implemented so that no observable behaviour reveals whether that check passed.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first two jobs are about the &lt;em&gt;scheme&lt;/em&gt;. The third is about the &lt;em&gt;implementation&lt;/em&gt;, and the entire history of RSA breaks is the field learning, over and over, that the third job is not optional. Hold that third clause; it is the trap the next section springs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If your code invokes a modular-exponentiation &quot;textbook RSA&quot; function directly on a message -- no OAEP, no PSS, no padding layer -- it is broken before it ships. Every serious library hides the bare permutation precisely so that no application accidentally uses it as encryption or a signature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The first widely deployed answer to those three jobs was PKCS#1 version 1.5, specified by RSA Laboratories in the early 1990s and republished as RFC 2313 in 1998 [@rfc2313]. For encryption, it wraps the message as &lt;code&gt;0x00 || 0x02 || PS || 0x00 || M&lt;/code&gt;, where &lt;code&gt;PS&lt;/code&gt; is at least eight nonzero random bytes -- that random padding supplies job one [@rfc8017]. For signatures it uses a different, fixed frame, &lt;code&gt;0x00 || 0x01 || 0xFF...0xFF || 0x00 || DigestInfo&lt;/code&gt;, whose rigid structure supplies job two [@rfc8017].&lt;/p&gt;
&lt;p&gt;On paper it fixed textbook RSA&apos;s two headline problems: the random bytes killed determinism, and the fixed structure gave the receiver something to check. For five years, it looked as if PKCS#1 v1.5 had turned the trapdoor into a cryptosystem. The structure was supposed to &lt;em&gt;add&lt;/em&gt; security. In 1998, Daniel Bleichenbacher showed that the structure was the vulnerability.&lt;/p&gt;
&lt;h2&gt;4. The Atom and Its Echoes: The Encryption Padding-Oracle Lineage&lt;/h2&gt;
&lt;p&gt;Before the detailed walk, here is the whole story on one timeline -- two intertwined tracks, the schemes on one side and the breaks on the other, with each break forcing the next response.&lt;/p&gt;

timeline
    title RSA schemes and their breaks, 1976 to 2026
    1977 : RSA trapdoor answers the Diffie-Hellman challenge
    1990 : Wiener breaks small private exponents
    1993 : PKCS#1 v1.5 padding deployed
    1997 : Bellcore CRT fault factors N from one signature
    1998 : Bleichenbacher padding oracle : OAEP standardized in response
    2001 : Manger reopens the oracle inside OAEP decoders
    2003 : Remote timing attacks proven practical : PSS standardized
    2016 : DROWN revives the oracle through SSLv2
    2017 : ROCA factors structured Infineon keys
    2018 : ROBOT signs with the facebook.com key : TLS 1.3 deletes RSA key exchange
    2023 : Marvin recovers keys by timing alone
    2024 : NIST IR 8547 sets the quantum retirement clock
    2025 : CFRG draft moves to deprecate v1.5 encryption
&lt;p&gt;Bleichenbacher&apos;s insight was not about RSA the mathematics at all. It was about a server&apos;s manners. A TLS server in the 1990s received an RSA-encrypted pre-master secret, decrypted it, and checked whether the result had valid PKCS#1 v1.5 padding -- did it start with the bytes &lt;code&gt;0x00 0x02&lt;/code&gt;, with a nonzero pad and a delimiter in the right place? If not, it returned an error. That error, however polite, answered a question the attacker was not supposed to be able to ask: &lt;em&gt;was this the encryption of a well-formed message?&lt;/em&gt; And because RSA is malleable, the attacker could turn that one bit of feedback into a full decryption.&lt;/p&gt;
&lt;p&gt;Here is the mechanism. The attacker holds a target ciphertext $c$ that encrypts an unknown $m$ -- say, a captured TLS session key. They pick a value $s$, compute $c \cdot s^e \bmod N$, and send it to the server. By the homomorphic property, the server is really decrypting $m \cdot s \bmod N$. If the server reports valid padding, the attacker learns that $m \cdot s \bmod N$ begins with &lt;code&gt;0x00 0x02&lt;/code&gt; -- which means it lies in the interval $[2B, 3B)$, where $B = 2^{8(k-2)}$ for a $k$-byte modulus.&lt;/p&gt;
&lt;p&gt;Each conforming $s$ is a linear inequality on the secret $m$. Collect enough of them, adaptively choosing each new $s$ to bisect the surviving range, and the interval of possible $m$ collapses to a single value. Bleichenbacher&apos;s 1998 paper showed this takes on the order of $2^{20}$ -- about a million -- queries for a 1,024-bit key, and the private key is never touched [@bleichenbacher98].&lt;/p&gt;

sequenceDiagram
    participant A as Attacker
    participant S as Decrypting server
    Note over A: Holds target ciphertext c, wants secret m
    A-&amp;gt;&amp;gt;S: Submit c times s to the e, for chosen s
    S-&amp;gt;&amp;gt;S: Decrypt and check PKCS#1 v1.5 padding
    alt Decrypted value starts with 00 02
        S--&amp;gt;&amp;gt;A: Conforming, no error or fast reply
        Note over A,S: Attacker learns m times s mod N is in 2B to 3B
    else Padding invalid
        S--&amp;gt;&amp;gt;A: Non-conforming, error, alert, or slow reply
    end
    Note over A: Narrow the possible range of m, choose next s
    A-&amp;gt;&amp;gt;S: Repeat, roughly a million queries in total
    Note over A: Range collapses to one value, plaintext m recovered
&lt;p&gt;A generation of engineers filed this under &quot;performance&quot; or &quot;obscure edge case.&quot; It is neither. It is a correctness and security failure of the &lt;em&gt;construction&lt;/em&gt;: the receiver&apos;s validity check, exposed through any observable side effect, is a decryption oracle for chosen ciphertexts. That is the &lt;a href=&quot;https://paragmali.com/blog/they-read-your-plaintext-without-breaking-your-cipher-a-fiel/&quot; rel=&quot;noopener&quot;&gt;symmetric-side padding-oracle attack&lt;/a&gt; of Part 6, transplanted to public-key land -- the same disease, a different organ.&lt;/p&gt;

Any observable behaviour -- an error message, a network reset, an injected fault, or a difference in response time -- that reveals whether a decrypted ciphertext had valid padding. Because the attacker chooses the ciphertexts, a padding-validity signal becomes a decryption oracle: enough yes-or-no answers reconstruct the plaintext without ever recovering the private key.
&lt;p&gt;The break violates the strongest standard security goal for encryption, IND-CCA2, in which the adversary may submit chosen ciphertexts to a decryption oracle and still must not learn anything about a challenge plaintext.&lt;/p&gt;

The security goal a padding oracle destroys: an adversary who can submit arbitrary ciphertexts for decryption still cannot distinguish which of two chosen plaintexts a challenge ciphertext encrypts. PKCS#1 v1.5 encryption fails this because its validity check leaks. Part 1 of this series develops the full definition.
&lt;p&gt;The scheme-level fix arrived almost immediately. In direct response to Bleichenbacher, RSA Laboratories standardized a new encryption padding, RSAES-OAEP, in PKCS#1 v2.0 (RFC 2437) later in 1998 [@rfc2437]. OAEP&apos;s design goal, spelled out in Section 6, is that any tampered ciphertext decodes to unstructured noise, so there is no &quot;conformant&quot; interval left to test -- the oracle has nothing to grade. It fixed the scheme. What it did not, and could not, fix by itself was the decoder.&lt;/p&gt;
&lt;p&gt;James Manger proved that in 2001. OAEP&apos;s decoding has two distinct early failure modes: the recovered integer can be too large to fit the byte block, or it can fit but fail the padding check. A decoder that distinguishes those two cases through different errors or different timing hands the attacker a &lt;em&gt;new&lt;/em&gt; oracle -- and Manger&apos;s variant is dramatically cheaper than Bleichenbacher&apos;s, needing on the order of $\log_2 N$ queries, roughly 2,048 for a 2,048-bit key, instead of $2^{20}$ [@manger01]. OAEP, the provably secure scheme, re-opened the identical class of attack through an imperfect implementation. The lesson the field wrote down, and then kept having to re-learn, was not &quot;use OAEP.&quot; It was &quot;use OAEP &lt;em&gt;and&lt;/em&gt; decode it in constant time.&quot;&lt;/p&gt;
&lt;p&gt;If Bleichenbacher&apos;s attack is the atom, the next quarter-century is three quieter echoes of it -- the same leaked bit, reached through channels that get progressively harder to see.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DROWN (2016): a cross-protocol channel.&lt;/strong&gt; By 2016, TLS servers had long since patched the obvious padding-error message. But many still supported SSLv2, an obsolete protocol from the 1990s, often on a different service such as a mail server -- and often with the &lt;em&gt;same&lt;/em&gt; RSA key. DROWN showed that an attacker could use the weak, deliberately export-crippled SSLv2 endpoint as the padding oracle, then use its answers to decrypt a modern TLS session that shared the key.The DROWN team measured that roughly 33 percent of all HTTPS servers were vulnerable at disclosure in March 2016, because key reuse across a TLS service and a forgotten SSLv2 service was rampant [@drownsite]. The math of the oracle was 1998&apos;s; only the delivery was new [@drown16].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ROBOT (2018): the same oracle, nineteen years on.&lt;/strong&gt; Hanno Bock, Juraj Somorovsky, and Craig Young revisited the original attack and found it alive across the modern internet. The padding-error message was gone, but the oracle now leaked through subtler tells: a TCP reset here, a connection timeout there, a duplicated alert message somewhere else -- any behaviour that differed between conforming and non-conforming padding. It affected almost a third of the top 100 domains, including Facebook and PayPal, and traced to products from F5, Citrix, Radware, Palo Alto Networks, IBM, and Cisco [@robot18].To make the point unmissable, the ROBOT authors used the recovered oracle to sign a message with the private key behind facebook.com&apos;s certificate -- a decryption oracle repurposed to forge a signature, without ever touching the factorization [@robotsite].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Marvin (2023): pure timing, no error at all.&lt;/strong&gt; The quietest channel yet removes the error entirely. Hubert Kario&apos;s Marvin attack observes only the &lt;em&gt;time&lt;/em&gt; the decryption operation takes, since a conforming and a non-conforming padding often run through subtly different code paths. No alert, no reset, no message -- just a stopwatch. Marvin re-found exploitable leaks in implementations that had been declared immune after ROBOT, and it reaches well beyond TLS into S/MIME, JSON Web Tokens, and hardware tokens such as HSMs and smartcards [@marvin23]. The peer-reviewed write-up appeared at ESORICS 2023 under the fitting title &lt;em&gt;Everlasting ROBOT&lt;/em&gt; [@eprint2023]. Its recommendation was blunt: stop using PKCS#1 v1.5 encryption.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every member of this lineage leaks the same single bit -- was the padding valid? -- through a different channel. You cannot patch your way to safety one channel at a time, because the next channel is always quieter than the last. The only durable fixes are structural: remove the mode (as TLS 1.3 did) or forbid it (as FIPS did). If a standard forces RSA encryption, use OAEP with constant-time decoding, never v1.5.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Read the four together and the pattern is impossible to miss. The channel gets quieter each time -- an error string in 1998, a cross-protocol zombie in 2016, a TCP quirk in 2018, a bare microsecond in 2023 -- but the leaked bit never changes. This is why &quot;add more padding&quot; was never the answer and constant-time, uniform-failure decoding was. The catalog below is the evidence for this article&apos;s whole argument: every row is some party using the bare trapdoor as if it were a scheme, or letting a check confess.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Front&lt;/th&gt;
&lt;th&gt;Leaked channel&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;The rule it teaches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Textbook RSA [@rsa78]&lt;/td&gt;
&lt;td&gt;1978&lt;/td&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;None needed&lt;/td&gt;
&lt;td&gt;Deterministic, malleable bare permutation&lt;/td&gt;
&lt;td&gt;Never encrypt with the raw primitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hastad / Coppersmith [@hastad88, @coppersmith97]&lt;/td&gt;
&lt;td&gt;1988 / 96&lt;/td&gt;
&lt;td&gt;Params&lt;/td&gt;
&lt;td&gt;Algebraic structure&lt;/td&gt;
&lt;td&gt;Small e, short or related messages&lt;/td&gt;
&lt;td&gt;Pad first, use e equals 65537&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bleichenbacher [@bleichenbacher98]&lt;/td&gt;
&lt;td&gt;1998&lt;/td&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;Padding-error message&lt;/td&gt;
&lt;td&gt;v1.5 validity check is a decryption oracle&lt;/td&gt;
&lt;td&gt;Uniform, unobservable failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manger [@manger01]&lt;/td&gt;
&lt;td&gt;2001&lt;/td&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;Error-type or timing split&lt;/td&gt;
&lt;td&gt;Non-constant-time OAEP decode&lt;/td&gt;
&lt;td&gt;OAEP AND constant-time decoding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DROWN [@drown16]&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;SSLv2 cross-protocol&lt;/td&gt;
&lt;td&gt;Same RSA key on a weak zombie protocol&lt;/td&gt;
&lt;td&gt;Never reuse keys, kill SSLv2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROBOT [@robot18]&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;Encryption&lt;/td&gt;
&lt;td&gt;TCP reset, timeout, alert&lt;/td&gt;
&lt;td&gt;v1.5 oracle still live behind subtle tells&lt;/td&gt;
&lt;td&gt;Remove v1.5 key exchange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marvin [@marvin23]&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;Decryption timing only&lt;/td&gt;
&lt;td&gt;Non-constant-time v1.5 depadding&lt;/td&gt;
&lt;td&gt;Retire v1.5 encryption entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;e equals 3 forgery [@cve2006]&lt;/td&gt;
&lt;td&gt;2006&lt;/td&gt;
&lt;td&gt;Signature&lt;/td&gt;
&lt;td&gt;None, forged offline&lt;/td&gt;
&lt;td&gt;Lax verifier ignores trailing bytes&lt;/td&gt;
&lt;td&gt;Verify strictly, parse it all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BERserk [@cve2014]&lt;/td&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;Signature&lt;/td&gt;
&lt;td&gt;None, forged offline&lt;/td&gt;
&lt;td&gt;NSS mis-parses ASN.1 lengths&lt;/td&gt;
&lt;td&gt;Verify strictly, re-encode and compare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bellcore fault [@bdl97]&lt;/td&gt;
&lt;td&gt;1997&lt;/td&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;One faulty signature&lt;/td&gt;
&lt;td&gt;CRT fault reveals a prime by gcd&lt;/td&gt;
&lt;td&gt;Verify signature before release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kocher, Brumley-Boneh [@kocher96, @brumleyboneh03]&lt;/td&gt;
&lt;td&gt;1996 / 2003&lt;/td&gt;
&lt;td&gt;Implementation&lt;/td&gt;
&lt;td&gt;Exponentiation timing&lt;/td&gt;
&lt;td&gt;Secret-dependent modexp time&lt;/td&gt;
&lt;td&gt;Base blinding, constant time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wiener [@wiener90]&lt;/td&gt;
&lt;td&gt;1990&lt;/td&gt;
&lt;td&gt;Params&lt;/td&gt;
&lt;td&gt;Public key structure&lt;/td&gt;
&lt;td&gt;Private exponent d too small&lt;/td&gt;
&lt;td&gt;Never shrink d for speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mining Ps and Qs [@miningpq12]&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;Params&lt;/td&gt;
&lt;td&gt;Shared prime factors&lt;/td&gt;
&lt;td&gt;Low boot-time entropy at keygen&lt;/td&gt;
&lt;td&gt;Seed the CSPRNG properly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROCA [@roca17]&lt;/td&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;Params&lt;/td&gt;
&lt;td&gt;Public modulus structure&lt;/td&gt;
&lt;td&gt;Infineon&apos;s structured primes&lt;/td&gt;
&lt;td&gt;Generate primes uniformly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Fourteen rows, one disease. But the encryption oracle is only one of four ways the bare trapdoor betrays its owner. The other three -- the lazy verifier, the glitched chip, and the badly chosen numbers -- are just as instructive, and one of them factors your modulus from a single fault.&lt;/p&gt;
&lt;h2&gt;5. The Rest of the Catalog: Signatures, Faults, and Bad Primes&lt;/h2&gt;
&lt;p&gt;If the encryption oracle is the trapdoor leaking through the decryptor&apos;s &lt;em&gt;answer&lt;/em&gt;, the remaining breaks are the trapdoor leaking through the verifier&apos;s &lt;em&gt;laziness&lt;/em&gt;, the hardware&apos;s &lt;em&gt;faults&lt;/em&gt;, and the &lt;em&gt;numbers&lt;/em&gt; you fed it. Three fronts, and the encryption-versus-signature split stays absolute across all of them.&lt;/p&gt;
&lt;h3&gt;Front A: the signature track, and the precision that governs it&lt;/h3&gt;
&lt;p&gt;Start with a fact that surprises people who lump all of v1.5 together: RFC 8017 records &lt;strong&gt;no known attack against the RSASSA-PKCS1-v1_5 &lt;em&gt;signature scheme itself&lt;/em&gt;&lt;/strong&gt; [@rfc8017]. Unlike v1.5 encryption, the signature padding has never been broken as a construction. It rests on a heuristic argument rather than a tight proof, which is why cryptographers reach for PSS in new designs, but it is legacy-not-broken. Where it &lt;em&gt;is&lt;/em&gt; fragile is at the verifier -- and that fragility has bitten twice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The $e = 3$ forgery (2006, CVE-2006-4339).&lt;/strong&gt; Daniel Bleichenbacher -- the same name, a different result eight years later -- described a forgery that needs no private key at all. A v1.5 signature block ends with the ASN.1 &lt;code&gt;DigestInfo&lt;/code&gt; structure, and a lazy verifier checks that the high-order bytes look right without confirming that the &lt;code&gt;DigestInfo&lt;/code&gt; sits flush against the end with no trailing data.&lt;/p&gt;
&lt;p&gt;With a small public exponent such as $e = 3$, an attacker can construct a number whose cube reproduces the correct prefix and hash in the leading bytes, then pack arbitrary garbage into the trailing bytes the verifier never inspects. Because cubing is cheap and the low bytes are free, the forged &quot;signature&quot; is just a carefully chosen cube root [@cve2006]. OpenSSL&apos;s advisory of 5 September 2006 traced it to exactly that omission: &quot;not checking for excess data&quot; after the exponentiation [@openssl2006].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BERserk (2014, CVE-2014-1568).&lt;/strong&gt; Eight years on, the same antipattern returned in a different guise. Mozilla&apos;s NSS library mis-parsed ASN.1 length fields during signature verification, letting an attacker smuggle forged content past the check and produce signatures that NSS -- and therefore Firefox and Chrome builds of the era -- accepted as valid, including for TLS certificates [@cve2014]. Once again, no scheme was broken; a verifier was.&lt;/p&gt;
&lt;p&gt;The recurring lesson is one line: &lt;strong&gt;verify strictly.&lt;/strong&gt; Parse the entire structure, reject any trailing data, re-encode the expected block and compare it byte-for-byte, and never take an $e = 3$ shortcut. The provable alternative, PSS, removes the temptation to hand-roll a lenient check by making the encoding randomized and its verification total; its mechanism is in the next section [@pss96]. These verifier bugs live where v1.5 signatures live -- inside the &lt;a href=&quot;https://paragmali.com/blog/a-perfect-signature-for-a-certificate-that-should-never-have/&quot; rel=&quot;noopener&quot;&gt;X.509 and PKI machinery&lt;/a&gt; that Part 4 of this series covers.&lt;/p&gt;

&quot;Still-deployed PKCS#1 v1.5&quot; hides two opposite truths, and conflating them is the single most common error in RSA writing. v1.5 *encryption* (RSAES-PKCS1-v1_5) is fundamentally dangerous padding: its validity check is a decryption oracle, and it should be retired. v1.5 *signatures* (RSASSA-PKCS1-v1_5) are dominant across Web PKI, FIPS-approved in FIPS 186-5, and unbroken as a scheme -- keep them where mandated and verify strictly [@rfc8017], [@fips1865]. One phrase must never carry both meanings. When you read &quot;RSA v1.5 is broken,&quot; ask which one, because the honest answer is &quot;the encryption, not the signatures.&quot;
&lt;h3&gt;Front B: the physical layer&lt;/h3&gt;
&lt;p&gt;Here the secret &lt;em&gt;is&lt;/em&gt; the factorization, so anything the hardware leaks about the private operation leaks the factors directly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The CRT fault (Boneh, DeMillo, and Lipton, 1997).&lt;/strong&gt; Recall that fast RSA signing computes the result modulo $p$ and modulo $q$ separately, then recombines. Suppose a single bit flips during the modulo-$p$ half -- from a voltage glitch, a clock fault, cosmic radiation, or a deliberate injection. The faulty signature $s&apos;$ is now correct modulo $q$ but wrong modulo $p$. Then $s&apos;^e - m$ is divisible by $q$ but not by $p$, so a single $\gcd(s&apos;^e - m, N)$ hands you $q$ -- and the modulus is factored from &lt;em&gt;one&lt;/em&gt; faulty signature [@bdl97]. The demonstration below does exactly that with toy primes.&lt;/p&gt;
&lt;p&gt;{`
// Bellcore fault: a fault in one CRT half leaks a prime of N. Toy primes for clarity.
function modpow(base, exp, mod) {
  base = ((base % mod) + mod) % mod; let r = 1n;
  while (exp &amp;gt; 0n) { if (exp &amp;amp; 1n) r = (r * base) % mod; base = (base * base) % mod; exp &amp;gt;&amp;gt;= 1n; }
  return r;
}
function gcd(a, b) { while (b) { const t = a % b; a = b; b = t; } return a; }
const p = 61n, q = 53n, N = p * q, e = 17n, d = 2753n;
const m = 123n % N;                       // message representative to sign
const dp = d % (p - 1n), dq = d % (q - 1n);
const qInv = modpow(q, p - 2n, p);&lt;/p&gt;
&lt;p&gt;// Correct CRT signature (Garner recombination):
const sp = modpow(m, dp, p), sq = modpow(m, dq, q);
const s = (sq + q * (((qInv * (sp - sq)) % p + p) % p)) % N;
console.log(&apos;correct signature verifies?&apos;, modpow(s, e, N) === m);&lt;/p&gt;
&lt;p&gt;// A fault corrupts ONLY the mod-p half:
const spBad = (sp + 1n) % p;
const sBad = (sq + q * (((qInv * (spBad - sq)) % p + p) % p)) % N;
console.log(&apos;faulty  signature verifies?&apos;, modpow(sBad, e, N) === m);&lt;/p&gt;
&lt;p&gt;// One gcd factors the modulus:
const diff = ((modpow(sBad, e, N) - m) % N + N) % N;
console.log(&apos;gcd(sBad^e - m, N) =&apos;, gcd(diff, N).toString(), &apos;-&amp;gt; a secret prime of N (61 or 53)&apos;);
`}&lt;/p&gt;
&lt;p&gt;The fix is a single line of discipline: &lt;strong&gt;verify before release.&lt;/strong&gt; Recompute $s^e \bmod N$ and confirm it equals $m$ before the signature ever leaves the chip. A faulty signature never escapes, and the gcd trick has nothing to work with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Timing (Kocher, 1996; Brumley and Boneh, 2003).&lt;/strong&gt; Paul Kocher observed in 1996 that the time to compute $c^d \bmod N$ depends on the secret bits of $d$, because square-and-multiply does extra work on &lt;code&gt;1&lt;/code&gt; bits [@kocher96]. Folklore held that timing attacks needed local access. David Brumley and Dan Boneh demolished that in 2003 by recovering an OpenSSL server&apos;s private key &lt;em&gt;over a network&lt;/em&gt;, measuring only response times [@brumleyboneh03].&lt;/p&gt;
&lt;p&gt;The fix is &lt;strong&gt;base blinding&lt;/strong&gt;: multiply the input by $r^e$ for a fresh random $r$ before exponentiating, then divide the result by $r$ afterward, so the timing depends on a value the attacker cannot predict. OpenSSL turned blinding on by default in 2003. Marvin, from the previous section, is this front&apos;s 2023 revival -- the timing channel never truly closed.&lt;/p&gt;
&lt;h3&gt;Front C: the parameters&lt;/h3&gt;
&lt;p&gt;The trapdoor is only as strong as the numbers behind it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wiener (1990).&lt;/strong&gt; If you shrink the private exponent for speed so that $d &amp;lt; \tfrac{1}{3} N^{1/4}$, a continued-fraction expansion of $e/N$ recovers $d$ outright [@wiener90]. You cannot buy decryption speed by making the secret small.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coppersmith (1996 to 1997).&lt;/strong&gt; His lattice method sets the barrier $|x| &amp;lt; N^{1/e}$ that both &lt;em&gt;enables&lt;/em&gt; low-exponent attacks on short or stereotyped messages and &lt;em&gt;bounds&lt;/em&gt; them -- the same result that returns, weaponized, as ROCA [@coppersmith97].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mining Your Ps and Qs (2012).&lt;/strong&gt; Heninger and colleagues scanned the internet&apos;s public keys and computed pairwise gcds. Devices that generated keys at first boot, before they had gathered entropy, sometimes shared a prime -- and a shared prime means a free gcd factors both moduli. They computed private keys for about 0.50 percent of TLS hosts and 0.03 percent of SSH hosts this way [@miningpq12].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ROCA (2017, CVE-2017-15361).&lt;/strong&gt; Infineon&apos;s key-generation library built primes with a special structure to speed things up. That structure let a Coppersmith attack factor the public modulus for a practical cost, and the affected chips were everywhere: TPMs, YubiKey 4 tokens, national electronic ID cards, and BitLocker deployments [@roca17], [@cve2017]. Coppersmith&apos;s 1996 barrier, patient for twenty years, collected its debt.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The root cause of the last two is not RSA math at all -- it is the &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;CSPRNG that generates the primes&lt;/a&gt;, the subject of Part 2 of this series. Feed RSA weak randomness and no scheme, no padding, and no proof can save it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Turn the diagnostic question on each front and it answers itself. The verifier confesses through a check it skipped. The chip confesses through a fault it did not catch. The clock confesses through a branch it did not equalize. The keygen confesses through primes it did not draw uniformly. In every case the attacker reads the private key off the receiver&apos;s &lt;em&gt;behaviour&lt;/em&gt;, not off the mathematics.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Keep the empirical yardstick in view: the largest RSA modulus ever publicly factored is RSA-250, at 829 bits.RSA-250 was factored on 28 February 2020 using the Number Field Sieve, at a cost of roughly 2,700 core-years of computation [@rsa250]. That is the real, measured cost of breaking RSA by its front door -- which is exactly why nobody attacks that door when a padding check will open the side gate for a million cheap queries. Four fronts, one disease. The cure is not a cleverer patch on any single front. It is a change in what we mean when we say a scheme is secure.&lt;/p&gt;
&lt;h2&gt;6. Security Is a Property of the Scheme AND the Implementation&lt;/h2&gt;
&lt;p&gt;There are two famous ideas behind &quot;RSA done right,&quot; and a third, deeper one that the first two kept teaching the hard way.&lt;/p&gt;
&lt;h3&gt;Idea 1: OAEP, the all-or-nothing randomized transform&lt;/h3&gt;
&lt;p&gt;Optimal Asymmetric Encryption Padding, designed by Mihir Bellare and Phillip Rogaway in 1994, encodes the message before the RSA permutation so that the encoded block has no gradable structure for an attacker to probe [@oaep94]. Its construction is a two-round Feistel network with a hash-based mask-generation function, MGF1, as the round function -- the &lt;a href=&quot;https://paragmali.com/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp/&quot; rel=&quot;noopener&quot;&gt;hashing machinery&lt;/a&gt; that Part 10 of this series develops. Writing $\Vert$ for concatenation and $\oplus$ for XOR, and starting from a random $\mathrm{seed}$:&lt;/p&gt;
&lt;p&gt;$$\mathrm{DB} = \mathrm{lHash} \Vert \mathrm{PS} \Vert \texttt{0x01} \Vert M$$
$$\mathrm{maskedDB} = \mathrm{DB} \oplus \mathrm{MGF1}(\mathrm{seed}), \qquad \mathrm{maskedSeed} = \mathrm{seed} \oplus \mathrm{MGF1}(\mathrm{maskedDB})$$
$$\mathrm{EM} = \texttt{0x00} \Vert \mathrm{maskedSeed} \Vert \mathrm{maskedDB}$$&lt;/p&gt;
&lt;p&gt;Then the RSA permutation is applied to $\mathrm{EM}$ [@rfc8017]. The two Feistel rounds make every bit of the encoded block depend on every bit of the message &lt;em&gt;and&lt;/em&gt; the random seed. That is the property that kills the padding oracle: flip a single bit of an OAEP ciphertext and the decoded block is randomized wholesale, so a tampered ciphertext decodes to noise with overwhelming probability. There is no conformant interval to bisect, no structured remnant to grade.&lt;/p&gt;
&lt;p&gt;With the randomness supplying non-determinism and the all-or-nothing mixing supplying tamper-detection, OAEP is not only semantically secure but also non-malleable and secure against chosen-ciphertext attack -- IND-CCA2 in the random-oracle model [@oaep94abs].&lt;/p&gt;

Optimal Asymmetric Encryption Padding: a randomized, all-or-nothing transform applied to a message before the RSA permutation. It mixes the message with a random seed through two mask-generation passes -- a two-round Feistel network -- so that every bit of the encoded block depends on every bit of the input. Flipping any ciphertext bit randomizes the whole decoded block, which destroys malleability and yields chosen-ciphertext security in the random-oracle model.

OAEP&apos;s history includes a moment cryptography should be proud of. In 2001, Victor Shoup showed that the celebrated 1994 security proof did not go through for an arbitrary trapdoor permutation -- it quietly assumed more than the definition guarantees [@shoup01]. The scheme was not broken; the *argument* was. Later that year, Fujisaki, Okamoto, Pointcheval, and Stern repaired it, proving RSA-OAEP is chosen-ciphertext secure in the random-oracle model under the RSA assumption, by leaning on a property called partial-domain one-wayness -- though the resulting reduction is non-tight [@fops04]. A public &quot;our proof, not our scheme, was wrong,&quot; followed by a public fix, is exactly how a mature field is supposed to work.
&lt;h3&gt;Idea 2: PSS, the salted encoding with a tight proof&lt;/h3&gt;
&lt;p&gt;For signatures, the Probabilistic Signature Scheme, also from Bellare and Rogaway, plays the analogous role [@pss96]. To sign, it hashes the message to $\mathrm{mHash}$, draws a random $\mathrm{salt}$, and forms $M&apos; = \texttt{padding} \Vert \mathrm{mHash} \Vert \mathrm{salt}$; sets $H = \mathrm{Hash}(M&apos;)$; builds $\mathrm{DB} = \mathrm{PS} \Vert \texttt{0x01} \Vert \mathrm{salt}$; masks it as $\mathrm{maskedDB} = \mathrm{DB} \oplus \mathrm{MGF1}(H)$; and assembles $\mathrm{EM} = \mathrm{maskedDB} \Vert H \Vert \texttt{0xbc}$ before applying the RSA private operation [@rfc8017].&lt;/p&gt;
&lt;p&gt;The randomization and MGF mixing buy something v1.5 signatures never had: a &lt;em&gt;tight&lt;/em&gt; exact-security reduction. A forger against PSS implies an algorithm that inverts RSA at almost the same cost -- so breaking the signatures is essentially as hard as the RSA problem itself, not merely conjectured to be [@pss96]. That is strictly stronger than v1.5&apos;s heuristic argument. PSS entered the standard in PKCS#1 v2.1 (RFC 3447) in 2003 and remains in the current v2.2 [@rfc3447]. Yet the split holds: PSS is the provable &lt;em&gt;upgrade&lt;/em&gt;, but v1.5 signatures are not superseded in deployment -- they remain the Web PKI default.Beyond these two deployed schemes, the literature holds academic-only refinements -- SAEP/SAEP+, OAEP+, three-round OAEP, tight RSA-KEM variants, and message-recovery PSS-R -- which tighten proofs or trim assumptions but never displaced OAEP and PSS in practice. A pointer, not a section.&lt;/p&gt;

Probabilistic Signature Scheme: a randomized, salted RSA signature encoding. A fresh random salt and MGF1 mixing make every signature of the same message different, and the scheme comes with a tight security reduction -- a forger would yield an almost-equally-efficient algorithm for inverting RSA, the strongest guarantee any RSA scheme offers.
&lt;h3&gt;Idea 3: the one the first two kept teaching&lt;/h3&gt;
&lt;p&gt;Here is where the whole article turns. OAEP is provably secure, yet Manger re-opened the identical oracle inside a non-constant-time &lt;em&gt;decoder&lt;/em&gt;. PSS is provably secure, yet a fault or a timing leak in the &lt;em&gt;signer&lt;/em&gt; factors the key. The scheme was never the whole story.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Security is a property of the scheme AND its implementation, not of the math. A perfect scheme run by a decoder that leaks whether padding verified is exactly as broken as no scheme at all. The implementation disciplines below are part of the construction, not optional hygiene layered on top.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Three disciplines close the three channels the catalog exposed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Constant-time depadding with implicit rejection.&lt;/strong&gt; On any decryption failure, do not branch or return a distinguishable error; substitute a deterministic pseudo-random value and continue, so whether the padding verified stays unobservable. OpenSSL 3.2 ships this by default and aligned its behaviour with NSS so neither library can be an oracle for the other [@openssl32], [@gorsa].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CRT verify-before-release.&lt;/strong&gt; Recompute and check the signature before it leaves the device, defeating Bellcore faults [@bdl97].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base blinding.&lt;/strong&gt; Randomize the input before exponentiation so timing reveals nothing about the secret [@kocher96], [@brumleyboneh03].&lt;/li&gt;
&lt;/ul&gt;

A decryption strategy in which a padding failure does not produce a distinguishable outcome. Instead of branching or returning a specific error, the code substitutes a deterministic pseudo-random value derived from the private key and ciphertext and proceeds. Success and failure become indistinguishable through content, error type, or timing, so there is no oracle left to query.

flowchart TD
    M[Message or ciphertext arrives] --&amp;gt; SCH{&quot;Scheme layer&quot;}
    SCH --&amp;gt;|Encrypt| OAEP[RSAES-OAEP, randomized all-or-nothing]
    SCH --&amp;gt;|Sign| PSS[RSASSA-PSS, salted with tight proof]
    OAEP --&amp;gt; IMPL{&quot;Implementation layer&quot;}
    PSS --&amp;gt; IMPL
    IMPL --&amp;gt; CT[Constant-time depad, implicit rejection]
    IMPL --&amp;gt; VBR[CRT verify-before-release]
    IMPL --&amp;gt; BL[Base blinding]
    CT --&amp;gt; PAR{&quot;Parameter layer&quot;}
    VBR --&amp;gt; PAR
    BL --&amp;gt; PAR
    PAR --&amp;gt; P1[Modulus at least 2048 bits, e equals 65537, CSPRNG primes]
    P1 --&amp;gt; GATE{&quot;Does any behaviour reveal the padding verdict?&quot;}
    GATE --&amp;gt;|No| SAFE[RSA done right]
    GATE --&amp;gt;|Yes| BROKEN[Oracle reopens, the catalog repeats]
&lt;p&gt;The libraries now say this in plain language. Go&apos;s standard library deprecated its v1.5 decryption function outright, with a warning that doubles as the thesis of this article:&lt;/p&gt;

&quot;PKCS #1 v1.5 encryption is dangerous and should not be used ... whether this function returns an error or not discloses secret information.&quot; -- Go crypto/rsa package documentation [@gorsa]
&lt;p&gt;That is the whole discipline in one sentence: done right closes the padding-oracle class Part 6 traces across all of cryptography, and it does so at the implementation layer, not the math. So what does the whole stack look like in 2024 to 2026 -- and which parts are quietly being switched off?&lt;/p&gt;
&lt;h2&gt;7. State of the Art (2024 to 2026): Done Right Is a Stack, Not a Method&lt;/h2&gt;
&lt;p&gt;Most primitives have a single &quot;best&quot; option you can name. RSA does not. &quot;Done right&quot; in 2026 is a &lt;em&gt;conjunction&lt;/em&gt; of independent choices that must all hold at once. Remove any one and the 25-year catalog reopens at that layer. Here is the stack, layer by layer.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;What it adds&lt;/th&gt;
&lt;th&gt;Security status&lt;/th&gt;
&lt;th&gt;Still requires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;v1.5 encryption&lt;/td&gt;
&lt;td&gt;Encrypt&lt;/td&gt;
&lt;td&gt;Randomness only&lt;/td&gt;
&lt;td&gt;Broken as a target -- padding oracle&lt;/td&gt;
&lt;td&gt;Retire it; no safe deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSAES-OAEP&lt;/td&gt;
&lt;td&gt;Encrypt&lt;/td&gt;
&lt;td&gt;Randomness plus all-or-nothing mixing&lt;/td&gt;
&lt;td&gt;IND-CCA2 in the random-oracle model&lt;/td&gt;
&lt;td&gt;Constant-time, implicit-rejection decode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;v1.5 signature&lt;/td&gt;
&lt;td&gt;Sign&lt;/td&gt;
&lt;td&gt;Fixed checkable structure&lt;/td&gt;
&lt;td&gt;Unbroken scheme, heuristic argument&lt;/td&gt;
&lt;td&gt;Strict verification, no e equals 3 shortcut&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSASSA-PSS&lt;/td&gt;
&lt;td&gt;Sign&lt;/td&gt;
&lt;td&gt;Salt plus MGF, tight proof&lt;/td&gt;
&lt;td&gt;Provably as hard as the RSA problem&lt;/td&gt;
&lt;td&gt;CRT verify-before-release, blinding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Encryption scheme.&lt;/strong&gt; When a standard genuinely mandates RSA encryption of a small payload, the answer is RSAES-OAEP with SHA-256 and MGF1-SHA-256, decoded in constant time [@rfc8017]. But note the capacity ceiling: a 2,048-bit OAEP-SHA-256 ciphertext carries at most about 190 bytes of plaintext [@sp80056b]. &quot;Encrypt a file with RSA-OAEP&quot; is therefore not just risky but physically wrong -- RSA encryption is for keys, never bulk data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Signatures.&lt;/strong&gt; New designs use RSASSA-PSS, which TLS 1.3 makes mandatory for handshake signatures [@pss96], [@rfc8446]. Yet RSASSA-PKCS1-v1_5 signatures remain dominant and FIPS-approved across X.509 and TLS certificates [@fips1865]. This is not a contradiction: keep v1.5 signatures where they are mandated, and verify them strictly [@rfc8017].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation hardening.&lt;/strong&gt; This is the layer that actually stops the attacks. OpenSSL 3.2 enables constant-time depadding with implicit rejection by default [@openssl32], Go has deprecated its v1.5 decryption entry point [@gorsa], and the CFRG&apos;s 2025 implementation-guidance draft folds all of this into formal advice amending RFC 8017 [@cfrgdraft].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parameters.&lt;/strong&gt; A 2,048-bit modulus is the floor for 112-bit security; use 3,072 or 4,096 bits for long-term secrets; keep $e = 65537$; and draw primes from a properly seeded CSPRNG [@sp80057], [@fips1865].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deployment and policy.&lt;/strong&gt; The most durable fixes were structural, not cryptographic. TLS 1.3 &lt;em&gt;deleted&lt;/em&gt; RSA key exchange entirely, which killed the Bleichenbacher-on-TLS class at the protocol level rather than patching each oracle [@rfc8446]. FIPS policy disallows RSA v1.5 key transport after 31 December 2023 [@sp800131a], [@sp80056b]. And NIST IR 8547 puts a clock on RSA itself, deprecating 112-bit strength after 2030 and disallowing it after 2035 [@ir8547].&lt;/p&gt;

If you operate under FIPS 140-3, the encryption side of this is not advisory. NIST SP 800-131A Rev. 2 and SP 800-56B Rev. 2 together disallow RSAES-PKCS1-v1_5 key transport after 31 December 2023; approved RSA key establishment must use OAEP [@sp800131a], [@sp80056b]. The signature side is the opposite: RSASSA-PKCS1-v1_5 signatures remain FIPS-approved under FIPS 186-5 [@fips1865]. Same version number, opposite compliance verdicts -- which is exactly why the encryption-signature split is not pedantry.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Right scheme (OAEP to encrypt, PSS for new signatures), right implementation (constant-time depad with implicit rejection, CRT verify-before-release, base blinding), right parameters (2,048-bit floor, e equals 65537, CSPRNG primes), inside the right protocol (TLS 1.3, no RSA key exchange). Every deployed disaster in the catalog removed exactly one of these. None of it is new in 2026; the interesting movement is not toward a better RSA but away from RSA entirely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;RSA done right is &lt;em&gt;stable&lt;/em&gt;. The genuinely current story is mostly about what to switch off and what is migrating away -- because for both of RSA&apos;s jobs, something smaller and faster is already taking over.&lt;/p&gt;
&lt;h2&gt;8. What RSA Is Losing To: ECDH, KEM-DEM, and the Post-Quantum Elephant&lt;/h2&gt;
&lt;p&gt;RSA is not being fixed into the future; it is being replaced out of it -- and the reasons have nothing to do with padding.&lt;/p&gt;
&lt;p&gt;The cleanest of those reasons is a design pattern that removes the attacker&apos;s target entirely. Never RSA-encrypt data. Instead, encapsulate a fresh random &lt;em&gt;symmetric&lt;/em&gt; key with the public key, and let an authenticated cipher carry the bulk. The public-key operation then transports only a uniform random key -- there is no attacker-chosen, structured plaintext for any oracle to grade. The entire Bleichenbacher-to-Marvin family loses its object, because the thing being decrypted is indistinguishable from random by construction [@rfc8446].&lt;/p&gt;

A two-part construction for hybrid encryption. A Key Encapsulation Mechanism (KEM) uses the public key to transport a freshly generated random symmetric key; a Data Encapsulation Mechanism (DEM) then encrypts the actual data with that key under an authenticated cipher. Because the public-key step only ever carries a uniform random key -- never a chosen, structured message -- there is no padding structure for an oracle to probe, so the padding-oracle class simply does not apply.

sequenceDiagram
    participant S as Sender
    participant R as Recipient
    Note over R: Publishes a public key
    S-&amp;gt;&amp;gt;S: Encapsulate, generate a random key K for the recipient
    S-&amp;gt;&amp;gt;S: Stretch K with a KDF, AEAD-encrypt the data
    S-&amp;gt;&amp;gt;R: Send the encapsulation and the AEAD ciphertext
    R-&amp;gt;&amp;gt;R: Decapsulate to recover K, run the same KDF
    R-&amp;gt;&amp;gt;R: AEAD-decrypt and verify the data
    Note over R,S: The public-key part carried only a random K, nothing to grade
&lt;p&gt;This is why elliptic-curve key agreement (ECDH, with Ed25519 and ECDSA for signatures) displaced RSA key transport: it does the same jobs with far smaller keys, faster operations, and forward secrecy -- a property RSA key transport never had, and the direct reason TLS 1.3 could &lt;em&gt;delete&lt;/em&gt; RSA key exchange rather than merely patch it [@rfc8446]. The &lt;a href=&quot;https://paragmali.com/blog/the-tag-verified-the-cipher-held-the-forgery-went-through-a-/&quot; rel=&quot;noopener&quot;&gt;KEM-DEM composition&lt;/a&gt; is developed in Part 11 of this series, the KDF step in Part 13, and the &lt;a href=&quot;https://paragmali.com/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/&quot; rel=&quot;noopener&quot;&gt;AEAD that carries the payload&lt;/a&gt; in Part 7.&lt;/p&gt;
&lt;p&gt;Then there is the elephant. Peter Shor&apos;s algorithm factors integers in polynomial time on a large fault-tolerant quantum computer, which makes &lt;em&gt;all&lt;/em&gt; factoring-based cryptography -- every RSA key length, done right or not -- eventually breakable. NIST has already named the destination: ML-KEM (FIPS 203) for key encapsulation, and ML-DSA and SLH-DSA (FIPS 204 and 205) for signatures [@fips203], [@fips204], [@fips205].&lt;/p&gt;
&lt;p&gt;The migration is lopsided. Key encapsulation is moving fast -- hybrid X25519 plus ML-KEM-768 already carries a double-digit percentage of Cloudflare&apos;s TLS 1.3 traffic -- while post-quantum &lt;em&gt;signatures&lt;/em&gt; lag badly, because they run 10 to 200 times larger than an RSA signature and no public post-quantum certificate infrastructure existed before roughly 2026 [@cloudflare24], [@ir8547]. The lattice and hash-based internals belong to a later part of this series; here they matter only as RSA&apos;s replacement, not its repair.&lt;/p&gt;

Quantum risk is not only a future problem. An adversary can record RSA-encrypted traffic today and decrypt it once a capable quantum computer exists. For any secret that must stay confidential past roughly 2030, the exposure is *present tense* -- which is why NIST IR 8547 frames the transition as urgent rather than eventual, and why hybrid key exchange is being deployed now rather than when the machine arrives [@ir8547], [@cloudflare24].
&lt;p&gt;Laid side by side, the trade-offs explain why key transport migrated first and signatures are dragging. For moving a key:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key transport&lt;/th&gt;
&lt;th&gt;Forward secrecy&lt;/th&gt;
&lt;th&gt;Padding-oracle exposure&lt;/th&gt;
&lt;th&gt;Relative cost&lt;/th&gt;
&lt;th&gt;Quantum status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;RSAES-OAEP [@rfc8017]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes, if decode is not constant-time&lt;/td&gt;
&lt;td&gt;Large ciphertext, slow keygen&lt;/td&gt;
&lt;td&gt;Broken by Shor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECDH (X25519) [@rfc8446]&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None, no RSA decryption&lt;/td&gt;
&lt;td&gt;Small and fast&lt;/td&gt;
&lt;td&gt;Broken by Shor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML-KEM-768 [@fips203]&lt;/td&gt;
&lt;td&gt;Yes (ephemeral)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Kilobyte-scale, fast&lt;/td&gt;
&lt;td&gt;Resistant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid X25519 + ML-KEM-768 [@cloudflare24]&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Sum of both, still practical&lt;/td&gt;
&lt;td&gt;Resistant if either holds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;And for signing:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signature&lt;/th&gt;
&lt;th&gt;Provable security&lt;/th&gt;
&lt;th&gt;Approx. signature size&lt;/th&gt;
&lt;th&gt;Main failure mode&lt;/th&gt;
&lt;th&gt;Quantum status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;RSASSA-PSS [@pss96]&lt;/td&gt;
&lt;td&gt;Yes, tight in ROM&lt;/td&gt;
&lt;td&gt;256 bytes at 2048-bit&lt;/td&gt;
&lt;td&gt;Fault or timing in the signer&lt;/td&gt;
&lt;td&gt;Broken by Shor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSASSA-PKCS1-v1_5 [@rfc8017]&lt;/td&gt;
&lt;td&gt;No, heuristic only&lt;/td&gt;
&lt;td&gt;256 bytes at 2048-bit&lt;/td&gt;
&lt;td&gt;Lax-verifier forgery&lt;/td&gt;
&lt;td&gt;Broken by Shor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECDSA / Ed25519 [@rfc8446]&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;td&gt;Nonce reuse (ECDSA)&lt;/td&gt;
&lt;td&gt;Broken by Shor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML-DSA-65 [@fips204]&lt;/td&gt;
&lt;td&gt;Yes, lattice&lt;/td&gt;
&lt;td&gt;About 3.3 kilobytes&lt;/td&gt;
&lt;td&gt;Implementation bugs&lt;/td&gt;
&lt;td&gt;Resistant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLH-DSA [@fips205]&lt;/td&gt;
&lt;td&gt;Yes, hash-based&lt;/td&gt;
&lt;td&gt;Many kilobytes&lt;/td&gt;
&lt;td&gt;Large and slow&lt;/td&gt;
&lt;td&gt;Resistant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The signature column is the migration&apos;s hard problem, which is why post-quantum certificates lag key exchange by years [@ir8547]. If RSA is on the clock, it is worth asking where its security actually came from in the first place -- and why nobody has ever proved it was there.&lt;/p&gt;
&lt;h2&gt;9. Theoretical Limits: Where the Security Comes From, and Its Ceiling&lt;/h2&gt;
&lt;p&gt;Here is the uncomfortable truth a first course skips: there is no proof that RSA is secure.&lt;/p&gt;
&lt;p&gt;Start with the assumption itself. The RSA problem is to compute $e$-th roots modulo $N$ without the factorization. We know this is &lt;em&gt;no harder&lt;/em&gt; than factoring -- if you can factor $N$, you can compute $d$ and invert everything, so RSA $\le$ factoring. What nobody has shown is the reverse. It is an open question whether breaking RSA is &lt;em&gt;equivalent&lt;/em&gt; to factoring, and Boneh and Venkatesan gave evidence that a broad class of algebraic reductions from factoring to low-exponent RSA is unlikely to exist -- suggesting the two problems may not be equivalent at all [@bv98].&lt;/p&gt;
&lt;p&gt;Worse, factoring itself is not known to be hard in any proven sense: it sits in NP intersect co-NP, which is evidence it is probably &lt;em&gt;not&lt;/em&gt; NP-complete, and no one has proved a super-polynomial lower bound for it. RSA&apos;s security is heuristic and empirical -- it has survived decades of attack, and that is the entire argument [@boneh99].&lt;/p&gt;
&lt;p&gt;The empirical ceiling is concrete. The best classical algorithm, the General Number Field Sieve, runs in sub-exponential time, and the public record is RSA-250 at 829 bits, factored in February 2020 for roughly 2,700 core-years [@rsa250]. RSA-250 was one of the RSA Factoring Challenge moduli that RSA Laboratories first published in 1991 as public benchmarks for exactly this kind of progress [@rsanumbers]. That record sets the practical floors:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;RSA modulus&lt;/th&gt;
&lt;th&gt;Symmetric-equivalent strength&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1024-bit&lt;/td&gt;
&lt;td&gt;About 80 bits&lt;/td&gt;
&lt;td&gt;Broken within reach, disallowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2048-bit&lt;/td&gt;
&lt;td&gt;112 bits&lt;/td&gt;
&lt;td&gt;Current minimum, deprecated after 2030&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3072-bit&lt;/td&gt;
&lt;td&gt;128 bits&lt;/td&gt;
&lt;td&gt;Long-term use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4096-bit&lt;/td&gt;
&lt;td&gt;About 140 bits (interpolated)&lt;/td&gt;
&lt;td&gt;High assurance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7680-bit&lt;/td&gt;
&lt;td&gt;192 bits&lt;/td&gt;
&lt;td&gt;Next tabulated SP 800-57 step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15360-bit&lt;/td&gt;
&lt;td&gt;256 bits&lt;/td&gt;
&lt;td&gt;Shows how badly RSA scales&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Those equivalences come from NIST SP 800-57, except the 4,096-bit figure -- a common interpolation, since the standard steps directly from 3,072 bits (128) to 7,680 bits (192). The last row is the quiet punchline: matching a 256-bit symmetric key needs a 15,360-bit RSA modulus, because RSA strength grows only sub-exponentially in key length while the cost of using it grows with the cube [@sp80057]. RSA scales badly, and that alone pushes new systems toward elliptic curves.&lt;/p&gt;
&lt;p&gt;Then the cliff. On a large fault-tolerant quantum computer, Shor&apos;s algorithm factors in polynomial time -- not faster, but &lt;em&gt;categorically&lt;/em&gt; faster, collapsing the whole assumption. The only question is engineering, and the estimates are falling fast. In 2019, Gidney and Ekera estimated 20 million noisy qubits and 8 hours to factor a 2,048-bit modulus [@gidney19]; by 2025, Gidney had cut the qubit estimate to under a million [@gidney25].A 20-fold reduction in the resource estimate in six years, with no fundamental barrier in sight, is precisely the trend that drove NIST to put firm dates -- 2030 and 2035 -- on RSA&apos;s retirement in IR 8547. The deadline is set by the slope, not by any single machine.&lt;/p&gt;
&lt;p&gt;Three impossibility results frame the whole subject, each already seen in this article. First, determinism can never be IND-CPA -- that is structural, not fixable, which is why padding must randomize. Second, there is no known standard-model proof of IND-CCA2 for RSA-OAEP under the plain RSA assumption; the guarantee is random-oracle-model only, and even there non-tight [@shoup01], [@fops04]. Third, Coppersmith&apos;s barrier $|x| &amp;lt; N^{1/e}$ both enables low-exponent attacks and bounds them, a hard mathematical edge that no parameter choice moves [@coppersmith97].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; RSA&apos;s safety lives in an unproven gap -- breaking it is no harder than factoring, but maybe strictly easier, and factoring is not even proven hard -- and that gap sits on the near side of a quantum cliff with a falling date on it. &quot;Done right&quot; buys you security against every known classical attack. It does not buy you a proof, and it cannot buy you time past Shor.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If the ground under RSA is this uncertain, what exactly is still unsettled -- and which of those open problems can bite you today?&lt;/p&gt;
&lt;h2&gt;10. Open Problems: What Remains Genuinely Unsettled&lt;/h2&gt;
&lt;p&gt;Some of these are for theorists. Others are live in your dependency tree right now.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Is the RSA problem equivalent to factoring?&lt;/strong&gt; We have only one direction, RSA $\le$ factoring; the reverse is open, with evidence it may fail [@bv98]. It matters because RSA&apos;s whole security story rests on a hardness we have never actually pinned down.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A deployed, standard-model IND-CCA2 RSA scheme.&lt;/strong&gt; Constructions that avoid the random-oracle model exist on paper, but none is a shipping default; practice sidesteps the gap entirely by using KEM-DEM instead of RSA encryption [@fops04].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constant-time RSA is a perpetually moving target.&lt;/strong&gt; Marvin re-found timing leaks in implementations previously believed immune, and only a handful -- such as BearSSL and BoringSSL -- passed its tests; leaks hide in general-purpose bignum code and even in error-logging paths, which is why the CFRG draft concludes the only safe path is to deprecate v1.5 encryption outright rather than keep hardening it [@marvin23], [@cfrgdraft].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The long tail of v1.5 encryption.&lt;/strong&gt; HSMs, PKCS#11 tokens, S/MIME, and JWE still use it in places that cannot simply be switched off -- exactly where Marvin keeps finding live oracles [@marvin23], [@gorsa].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generation-time entropy and structured primes.&lt;/strong&gt; Mining Your Ps and Qs and ROCA are operational failures with no clean universal fix: you cannot prove every device in the world seeded its CSPRNG correctly [@miningpq12], [@roca17].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;a href=&quot;https://paragmali.com/blog/the-thirty-year-migration-ships-in-a-pip-install-how-post-qu/&quot; rel=&quot;noopener&quot;&gt;post-quantum migration timeline&lt;/a&gt;.&lt;/strong&gt; Key exchange is migrating; signatures are years behind, and harvest-now-decrypt-later keeps the pressure on [@cloudflare24], [@ir8547].&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Most of this list is someone else&apos;s research agenda. Two entries are not: the v1.5 &lt;em&gt;encryption&lt;/em&gt; long tail and weak key-generation entropy are the ones most likely to be sitting in your own stack right now, in an HSM integration or an embedded device you inherited. Audit those two first.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Enough theory. Here is the whole argument compressed into rules you can apply on Monday.&lt;/p&gt;
&lt;h2&gt;11. The Practical Guide and the Misuse Catalog&lt;/h2&gt;
&lt;p&gt;Every rule below is one named break from the catalog, turned inside out. If you remember nothing else, remember the decision tree.&lt;/p&gt;

flowchart TD
    START{&quot;What do you need?&quot;} --&amp;gt;|Confidentiality| ENC{&quot;Can you avoid RSA encryption?&quot;}
    START --&amp;gt;|Authenticity| SIG{&quot;New design or legacy mandate?&quot;}
    ENC --&amp;gt;|Yes| KEM[Use ECDH or hybrid X25519 plus ML-KEM-768]
    ENC --&amp;gt;|No, a standard forces RSA| OAEP[RSAES-OAEP, SHA-256, constant-time decode, at least 2048-bit]
    SIG --&amp;gt;|New design| PSS[RSASSA-PSS with SHA-256]
    SIG --&amp;gt;|Legacy mandate| V15[v1.5 signature, verify strictly, no e equals 3 shortcut]
    KEM --&amp;gt; KEYS[Keys: e equals 65537, CSPRNG primes, verify-before-release, blinding]
    OAEP --&amp;gt; KEYS
    PSS --&amp;gt; KEYS
    V15 --&amp;gt; KEYS
    KEYS --&amp;gt; PQ{&quot;Secret must survive past 2030?&quot;}
    PQ --&amp;gt;|Yes| HY[Deploy hybrid post-quantum now]
    PQ --&amp;gt;|No| DONE[Ship it]
&lt;p&gt;Spelled out as decision rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Encryption and key transport.&lt;/strong&gt; Prefer ECDH or a KEM, migrating to hybrid X25519 plus ML-KEM-768. If a standard forces RSA encryption, use RSAES-OAEP with SHA-256 and MGF1-SHA-256, a modulus of at least 2,048 bits, and a constant-time decoder with implicit rejection -- never v1.5 encryption [@rfc8017], [@sp80056b]. Never RSA-encrypt a payload; encapsulate a symmetric key and let an AEAD carry the data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signatures.&lt;/strong&gt; Use RSASSA-PSS for new designs [@pss96], [@fips1865]. Use v1.5 signatures only where mandated, and then verify strictly: full parse, re-encode and compare, reject trailing data, no $e = 3$ shortcut [@rfc8017].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keys and parameters.&lt;/strong&gt; A 2,048-bit floor, 3,072 or 4,096 bits for long-term secrets; $e = 65537$; independent CSPRNG-drawn primes per key; CRT with verify-before-release; base blinding [@sp80057], [@fips1865].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migration.&lt;/strong&gt; Deploy hybrid post-quantum key agreement now, and inventory every RSA usage against the IR 8547 2030 and 2035 clock [@ir8547].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The same rules as a lookup table:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Key parameters&lt;/th&gt;
&lt;th&gt;Because (which break)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Key transport, preferred&lt;/td&gt;
&lt;td&gt;ECDH or hybrid X25519 + ML-KEM-768&lt;/td&gt;
&lt;td&gt;Ephemeral keys&lt;/td&gt;
&lt;td&gt;Forward secrecy, no oracle, quantum hedge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key transport, if RSA forced&lt;/td&gt;
&lt;td&gt;RSAES-OAEP, constant-time decode&lt;/td&gt;
&lt;td&gt;SHA-256, MGF1-SHA-256, at least 2048-bit&lt;/td&gt;
&lt;td&gt;Bleichenbacher, Manger, Marvin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature, new design&lt;/td&gt;
&lt;td&gt;RSASSA-PSS&lt;/td&gt;
&lt;td&gt;SHA-256, random salt&lt;/td&gt;
&lt;td&gt;v1.5 hand-wave, e equals 3 forgery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature, legacy mandate&lt;/td&gt;
&lt;td&gt;v1.5, verified strictly&lt;/td&gt;
&lt;td&gt;Full parse, reject trailing bytes&lt;/td&gt;
&lt;td&gt;e equals 3 forgery, BERserk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key generation&lt;/td&gt;
&lt;td&gt;e equals 65537, CSPRNG primes&lt;/td&gt;
&lt;td&gt;2048 floor, 3072+ long-term&lt;/td&gt;
&lt;td&gt;Wiener, ROCA, Mining Ps and Qs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private operation&lt;/td&gt;
&lt;td&gt;CRT verify-before-release, base blinding&lt;/td&gt;
&lt;td&gt;Recompute before output&lt;/td&gt;
&lt;td&gt;Bellcore fault, Kocher timing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-term secrets&lt;/td&gt;
&lt;td&gt;Hybrid post-quantum now&lt;/td&gt;
&lt;td&gt;Inventory to IR 8547 clock&lt;/td&gt;
&lt;td&gt;Shor, harvest-now-decrypt-later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Now the misuse catalog. Each antipattern maps to exactly one rule it violates -- the findings that recur in real code review:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;v1.5 encryption &quot;kept for compatibility.&quot;&lt;/strong&gt; The single most dangerous line in an RSA integration. Violates: retire v1.5 encryption.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-constant-time OAEP or v1.5 decode.&lt;/strong&gt; A decoder that branches or times differently on padding failure. Violates: constant-time, implicit-rejection decoding (Manger, Marvin).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distinct decryption error messages, or branch timing.&lt;/strong&gt; Any observable difference between &quot;bad padding&quot; and &quot;other error.&quot; Violates: uniform, unobservable failure (Bleichenbacher).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing CRT verify-before-release.&lt;/strong&gt; Signing without recomputing the result first. Violates: verify before release (Bellcore fault).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unblinded exponentiation.&lt;/strong&gt; A modexp whose time depends on the secret. Violates: base blinding (Kocher, Brumley-Boneh).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An $e = 3$ shortcut in a verifier.&lt;/strong&gt; Checking the prefix without rejecting trailing data. Violates: verify strictly (e-equals-3 forgery, BERserk).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Textbook RSA copied from a tutorial.&lt;/strong&gt; The raw permutation on a message. Violates: never call the raw primitive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RSA-encrypting a large payload.&lt;/strong&gt; Treating RSA as a bulk cipher. Violates: encapsulate a key, never encrypt data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weak, shared, or reused-modulus keys.&lt;/strong&gt; Primes drawn from a cold CSPRNG or a structured library. Violates: seed the CSPRNG, draw primes uniformly (Mining Ps and Qs, ROCA).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OAEP hash or label mismatch, or MGF1 silently defaulting to SHA-1.&lt;/strong&gt; A quiet interoperability and downgrade trap. Violates: pin the hash and MGF explicitly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treating a &quot;small&quot; side channel as unexploitable.&lt;/strong&gt; Marvin demolished that folklore -- a few microseconds recovered keys [@marvin23]. Violates: assume every observable leaks.&lt;/li&gt;
&lt;/ul&gt;

Grep for the dangerous entry points and confirm every RSA decryption path uses OAEP with a constant-time decoder:&lt;ul&gt;
&lt;li&gt;Go: search for &lt;code&gt;DecryptPKCS1v15&lt;/code&gt; and &lt;code&gt;EncryptPKCS1v15&lt;/code&gt;; move to &lt;code&gt;DecryptOAEP&lt;/code&gt; and &lt;code&gt;EncryptOAEP&lt;/code&gt;. The v1.5 decryptor is deprecated for the reason quoted earlier [@gorsa].&lt;/li&gt;
&lt;li&gt;Java: flag &lt;code&gt;Cipher.getInstance(&quot;RSA/ECB/PKCS1Padding&quot;)&lt;/code&gt;; require &lt;code&gt;RSA/ECB/OAEPWithSHA-256AndMGF1Padding&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;OpenSSL CLI: &lt;code&gt;openssl pkeyutl -encrypt -pubin -inkey pub.pem -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If a dependency genuinely still needs v1.5 decryption, confirm it runs a constant-time, implicit-rejection decoder -- OpenSSL 3.2 does so by default [@openssl32].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In practice, three violations dominate real audits: RSA v1.5 &lt;em&gt;encryption&lt;/em&gt; still enabled &quot;for a legacy client,&quot; a decryption path that is not verifiably constant-time, and RSA being used to encrypt bulk data instead of a symmetric key. Fix those three and you have closed most of the catalog [@marvin23], [@gorsa], [@openssl32].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Read the whole failure catalog again as this checklist, and one pattern remains.&lt;/p&gt;
&lt;h2&gt;12. Trapdoor, Not Cryptosystem&lt;/h2&gt;
&lt;p&gt;Return to where we began. The researchers who signed with facebook.com&apos;s key never factored its modulus, and neither did anyone in the twenty-five years of breaks between Bleichenbacher and Marvin. The factoring problem stood untouched the whole time. What fell, every single time, was something else: the decryptor&apos;s error handling, the signer&apos;s fault behaviour, the implementation&apos;s clock, or the generator&apos;s entropy. The attacker&apos;s real move is never to solve the hard math -- it is to turn the receiver&apos;s own reaction into the private-key operation.&lt;/p&gt;

The attacker does not factor the modulus. They turn the decryptor&apos;s reaction into the private-key operation -- and &quot;done right&quot; is the discipline of leaving that reaction with nothing to say.
&lt;p&gt;That is why &quot;RSA done right&quot; is a stack and not a setting. The right scheme, OAEP to encrypt and PSS for new signatures, does two of the three jobs. The right parameters -- a 2,048-bit floor, $e = 65537$, primes from a real CSPRNG -- keep the trapdoor strong. And the constant-time, fault-checked, blinded implementation does the third job, the one the field kept forgetting: it leaves no observable behaviour that answers the attacker&apos;s one question.&lt;/p&gt;
&lt;p&gt;Every deployed disaster in this article is a stack with exactly one layer missing. Textbook RSA is broken not because the math is weak but because it is &lt;em&gt;only&lt;/em&gt; the trapdoor, with none of the layers that make a trapdoor into a cryptosystem.&lt;/p&gt;
&lt;p&gt;The forward horizon makes the discipline sharper, not softer. RSA done right is stable against every known classical attack, and it still has an expiration date, because the one gap RSA can never close is the quantum one. The destination is not a better RSA. It is KEM-DEM composition and post-quantum algorithms -- constructions where the public-key operation carries only a uniform random key, and where Shor has no polynomial-time shortcut to wait for.&lt;/p&gt;
&lt;p&gt;So ask the diagnostic question one final time, now that it is answerable. When a ciphertext or signature it did not create arrives, what does the receiver reveal about it -- through its answer, its timing, or its faults? Done right, the answer is nothing at all.&lt;/p&gt;


No, but the word &quot;RSA&quot; hides three different answers. Textbook RSA is broken. PKCS#1 v1.5 *encryption* is dangerous and should be retired [@marvin23]. Done-right RSA -- OAEP or PSS, constant-time, at least 2,048 bits, public exponent 65537 -- is fine against every known classical attack, right up until a large quantum computer exists, which is why you should be planning migration in parallel [@ir8547].


No. Encapsulate a fresh random symmetric key with the public key and let an authenticated cipher encrypt the file. RSA-OAEP at 2,048 bits carries only about 190 bytes of plaintext anyway, so it was never meant for bulk data -- it moves keys, not files [@sp80056b].


No. OAEP fixes the *scheme*, not the decoder. James Manger showed that a non-constant-time OAEP decoder re-opens the very same padding oracle, and more cheaply than the original Bleichenbacher attack [@manger01]. The rule is &quot;OAEP AND constant-time decoding,&quot; never OAEP alone.


Not as a scheme -- there is no known attack against the RSASSA-PKCS1-v1_5 signature construction itself [@rfc8017]. What breaks is lax *verifiers*: the 2006 e-equals-3 forgery and the 2014 BERserk bug both forged signatures past sloppy verification, not by breaking the scheme [@cve2006], [@cve2014]. Verify strictly -- parse the entire structure and reject any trailing data.


Only with correct padding and a strict verifier, and it is not worth the risk. A small exponent has repeatedly enabled Hastad&apos;s broadcast attack, Coppersmith&apos;s low-exponent attacks, and signature forgeries [@hastad88], [@coppersmith97]. Use 65537: it is fast and dodges every low-exponent trap.


Yes for today -- a 2,048-bit modulus gives about 112 bits of security [@sp80057]. Use 3,072 bits or more for anything that must stay secret long-term, and start post-quantum planning: NIST deprecates 112-bit RSA after 2030 and disallows it after 2035 [@ir8547].


For most new work, yes. ECDH and Ed25519 give smaller, faster keys with the forward secrecy RSA key transport never had [@rfc8446], and hybrid X25519 plus ML-KEM-768 is already carrying real TLS traffic for the quantum transition [@cloudflare24]. RSA&apos;s destination is retirement, not repair.

&lt;p&gt;&amp;lt;StudyGuide slug=&quot;rsa-done-right-oaep-pss-bleichenbacher&quot; keyTerms={[
  { term: &quot;Trapdoor permutation&quot;, definition: &quot;A one-way function with a secret shortcut. RSA maps x to x-to-the-e modulo N, invertible only by whoever knows the factorization of N. It is a primitive, not a complete cryptosystem.&quot; },
  { term: &quot;Malleability&quot;, definition: &quot;RSA&apos;s multiplicative homomorphism: multiplying a ciphertext by a chosen factor predictably scales the plaintext, letting an attacker transform messages without decrypting. It is the lever behind the padding-oracle attacks.&quot; },
  { term: &quot;Padding oracle&quot;, definition: &quot;Any observable behaviour (an error, a timing difference, or a fault) that reveals whether a decrypted ciphertext had valid padding, turning a validity check into a decryption oracle.&quot; },
  { term: &quot;RSAES-OAEP&quot;, definition: &quot;A randomized, all-or-nothing encryption padding applied before the RSA permutation, giving chosen-ciphertext security in the random-oracle model, provided the decoder runs in constant time.&quot; },
  { term: &quot;RSASSA-PSS&quot;, definition: &quot;A randomized, salted RSA signature encoding with a tight security reduction to the RSA problem, the provable choice for new signature designs.&quot; },
  { term: &quot;Implicit rejection&quot;, definition: &quot;Returning a deterministic pseudo-random value on a padding failure instead of a distinguishable error, so success and failure are unobservable through content, error type, or timing.&quot; },
  { term: &quot;CRT in RSA&quot;, definition: &quot;Computing the private operation modulo p and modulo q separately for a roughly fourfold speedup, at the cost of a fault-attack surface unless the result is verified before release.&quot; },
  { term: &quot;KEM-DEM&quot;, definition: &quot;Transporting a random symmetric key with the public key, then encrypting the data with that key under an authenticated cipher, so no chosen structured plaintext exists for an oracle to grade.&quot; },
  { term: &quot;IND-CCA2&quot;, definition: &quot;The strongest standard security goal for encryption: even with access to a decryption oracle, an attacker cannot tell which plaintext a ciphertext hides. Padding oracles violate it.&quot; }
]} questions={[
  { q: &quot;Why is textbook RSA not a cryptosystem?&quot;, a: &quot;It is deterministic, malleable, and structurally leaky for small exponents. It needs padding that adds randomness, adds checkable redundancy, and leaks nothing about whether the check passed.&quot; },
  { q: &quot;What single bit did every attack from Bleichenbacher to Marvin leak?&quot;, a: &quot;Whether the padding was valid. Only the channel changed, from an error message to a cross-protocol zombie to a TCP quirk to pure timing.&quot; },
  { q: &quot;Why is OAEP necessary but not sufficient?&quot;, a: &quot;OAEP secures the scheme, but Manger showed that a non-constant-time decoder re-opens the same oracle. Security is a property of the scheme and its implementation together.&quot; },
  { q: &quot;What is the one split you must never blur?&quot;, a: &quot;v1.5 encryption is dangerous and should be retired, while v1.5 signatures are unbroken as a scheme and still dominant. Verify the signatures strictly.&quot; },
  { q: &quot;Why does done-right RSA still have an expiration date?&quot;, a: &quot;There is no proof RSA is secure, and Shor&apos;s algorithm factors in polynomial time on a quantum computer, so the destination is KEM-DEM plus post-quantum cryptography.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>rsa</category><category>oaep</category><category>rsa-pss</category><category>padding-oracle</category><category>bleichenbacher</category><category>post-quantum-crypto</category><category>cryptography</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>