<?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: applied-cryptography</title><description>Posts tagged applied-cryptography.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sun, 19 Jul 2026 05:08:40 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/applied-cryptography/rss.xml" rel="self" type="application/rss+xml"/><item><title>How Elliptic Curves and Diffie-Hellman Break in Real Life: The Discrete Log Never Fell</title><link>https://paragmali.com/blog/how-elliptic-curves-and-diffie-hellman-break-in-real-life-th/</link><guid isPermaLink="true">https://paragmali.com/blog/how-elliptic-curves-and-diffie-hellman-break-in-real-life-th/</guid><description>No one has solved the discrete log on a strong curve or a 2048-bit group -- yet PS3, Android wallets, TPMs, CurveBall, and Logjam all fell. Here is exactly how.</description><pubDate>Sat, 18 Jul 2026 00:00:00 GMT</pubDate><content:encoded>
On a strong curve or a 2048-bit group, no one has ever solved the discrete logarithm in the field -- yet PlayStation 3 consoles, Android Bitcoin wallets, tamper-resistant TPMs, Windows certificate validation, and live TLS sessions all fell. Every one of those breaks attacked a layer the discrete-log math depends on but does not control: the per-signature nonce (PS3, Android, Minerva, TPM-Fail, LadderLeak), the curve and certificate parameters (CurveBall), or the negotiated strength (Logjam). A *reused* nonce surrenders the key from just two signatures; a *biased* nonce needs a statistical attack over many. The only discrete log ever computed here was Logjam&apos;s, against a downgrade-forced, export-crippled, shared 512-bit prime -- never a strong deployed curve. The fix is never a bigger curve. It is hedged, constant-time signing, validated parameters, and retiring export and shared groups.
&lt;h2&gt;1. Seven Breaks, One Unbroken Problem&lt;/h2&gt;
&lt;p&gt;On a well-chosen elliptic curve, the best attack anyone has ever fielded against the discrete logarithm is a square-root-time random walk that has barely moved since the 1970s [@hac-ch11][@teske-2001]. No deployed P-256, Curve25519, or secp256k1 key, and no standard 2048-bit Diffie-Hellman group, has ever had its discrete log solved in public. By every measurement we can make, the mathematics is holding.&lt;/p&gt;
&lt;p&gt;And yet. PlayStation 3 consoles were jailbroken [@fail0verflow-ps3-2010]. Android Bitcoin wallets were emptied [@bitcoin-android-2013]. Tamper-resistant TPMs certified to government standards surrendered their signing keys [@tpmfail-2020]. Windows accepted forged certificates that chained to a counterfeit authority [@curveball-cve-2020-0601]. Live TLS sessions were downgraded and decrypted in transit [@logjam-weakdh-2015]. Every one of those systems ran the discrete-log math exactly as specified. The math was never where they broke.&lt;/p&gt;
&lt;p&gt;That is the paradox this article resolves, and the resolution fits in one sentence: none of these attacks solved a discrete logarithm. Each attacked a layer the math &lt;em&gt;depends on but does not control&lt;/em&gt;. There are exactly three such layers, and they organize everything that follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The per-signature nonce&lt;/strong&gt; -- the throwaway secret ECDSA needs for every signature. PS3, Android, Minerva, TPM-Fail, and LadderLeak all landed here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The curve and certificate parameters&lt;/strong&gt; -- which curve, and whose generator, you are actually talking to. CurveBall landed here.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The negotiated strength&lt;/strong&gt; -- how strong a key exchange two parties agree to use before they exchange anything. Logjam landed here.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The discrete-logarithm assumption protects the private key against being &lt;em&gt;directly computed&lt;/em&gt; -- and nothing else. &quot;Nobody solved the discrete log&quot; and &quot;somebody recovered the private key&quot; are not contradictory statements, because the discrete-log &lt;em&gt;problem&lt;/em&gt; (genuinely hard) and a discrete-log &lt;em&gt;deployment&lt;/em&gt; (a nonce, some parameters, a negotiation) are different things. Every real break in this article lives in the gap between them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A note on scope. This piece treats the hardness of the elliptic-curve discrete logarithm as a black box: assume it holds, and watch everything around it fail. The structural mathematics of curve choice -- why one curve is weak and another is strong, how the number field sieve and Shor&apos;s algorithm actually work -- belongs to a companion article, &quot;How the Discrete Log Would Break: Elliptic Curves, Finite Fields, and Why Curve Choice Is the Weak Part.&quot; Here, we assume the strongbox is uncrackable and ask how the key kept getting out anyway.&lt;/p&gt;
&lt;p&gt;To see why a hard problem protects so little, you first have to see exactly what it promises -- and what it quietly leaves to the deployment.&lt;/p&gt;
&lt;h2&gt;2. What the Math Actually Promises&lt;/h2&gt;
&lt;p&gt;In 1976, Whitfield Diffie and Martin Hellman reduced the ancient problem of exchanging a secret over an open wire to a single piece of hard arithmetic: the discrete logarithm [@diffie-hellman-1976]. Two parties each pick a private exponent, exchange public powers of a shared generator, and combine them into a key that an eavesdropper cannot reconstruct without solving for one of those exponents.&lt;/p&gt;
&lt;p&gt;Nine years later, Victor Miller and, independently, Neal Koblitz moved the same hard problem onto elliptic curves [@miller-1985][@koblitz-1987]. Koblitz argued the curve version was &quot;likely to be harder than the classical discrete logarithm problem&quot; [@koblitz-1987] -- which is why a 256-bit curve buys security that a finite field needs thousands of bits to match. A clean, triumphant origin. So where did the promise leak?&lt;/p&gt;

Given a generator `G` and a public point `Q = dG` on an elliptic curve (or a base `g` and `g^d` in a finite field), recovering the secret integer `d` is the discrete logarithm problem. The security of ECDSA, ECDH, and Diffie-Hellman rests on the assumption that this recovery is computationally infeasible for well-chosen parameters. It is the one thing the math promises -- and, as we will see, the only thing.
&lt;p&gt;Signatures are where the story starts, because signatures introduce a second secret. The Elliptic Curve Digital Signature Algorithm (ECDSA) signs a message hash &lt;code&gt;z&lt;/code&gt; under a long-term private key &lt;code&gt;d&lt;/code&gt; by generating a fresh random value &lt;code&gt;k&lt;/code&gt;, computing a point &lt;code&gt;kG&lt;/code&gt;, taking its x-coordinate as &lt;code&gt;r&lt;/code&gt;, and producing:&lt;/p&gt;
&lt;p&gt;$$s = k^{-1},(z + r,d) \bmod n$$&lt;/p&gt;
&lt;p&gt;where &lt;code&gt;n&lt;/code&gt; is the order of the group. The verifier only ever sees &lt;code&gt;(r, s)&lt;/code&gt;. The private key &lt;code&gt;d&lt;/code&gt; is buried inside, protected by the discrete-log assumption -- nobody can read &lt;code&gt;d&lt;/code&gt; off the signature. But look at what else is in that equation.&lt;/p&gt;

The `k` in ECDSA is a *nonce*: a number used once. It must be secret, statistically uniform, and unique to each signature. It is not the private key, and it is thrown away the instant the signature is produced. It is also the single most load-bearing prerequisite in the whole scheme -- because `k` and `d` appear together in one linear equation, anything an attacker learns about `k` turns directly into knowledge of `d`.
&lt;p&gt;The discrete-log assumption guards &lt;code&gt;d&lt;/code&gt; against being &lt;em&gt;computed&lt;/em&gt; from the public key. It says nothing whatsoever about three things the assumption quietly depends on but does not itself enforce:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The nonce &lt;code&gt;k&lt;/code&gt; must be secret, uniform, and unique.&lt;/strong&gt; The math assumes a perfect &lt;code&gt;k&lt;/code&gt; and never checks that you supplied one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The parameters must be the standard, named ones.&lt;/strong&gt; The math assumes you are on the curve you think you are, with the generator you think you have.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The negotiated strength must not be crippled or shared.&lt;/strong&gt; The math assumes the group you agreed to use is actually strong.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of those is the deployment&apos;s responsibility, not the primitive&apos;s. And each one became a pillar of failure.&lt;/p&gt;

flowchart TD
    DLP[&quot;(EC)DLP is hard: d cannot be computed from the public key dG&quot;]
    DLP --&amp;gt; N[&quot;Depends on the per-signature nonce k&quot;]
    DLP --&amp;gt; P[&quot;Depends on the curve and certificate parameters&quot;]
    DLP --&amp;gt; G[&quot;Depends on the negotiated group strength&quot;]
    N --&amp;gt; P1[&quot;Pillar 1 nonce: PS3, Android, Minerva, TPM-Fail, LadderLeak&quot;]
    P --&amp;gt; P2[&quot;Pillar 2 validation: CurveBall&quot;]
    G --&amp;gt; P3[&quot;Pillar 3 downgrade: Logjam&quot;]
&lt;p&gt;The curve migration itself is not our subject.Miller (1985) and Koblitz (1987) are mentioned here only to fix the primitive&apos;s birth. Whether a &lt;em&gt;particular&lt;/em&gt; curve is structurally weak -- anomalous curves, small embedding degrees, the number field sieve, Shor&apos;s algorithm -- is the structural mathematics deferred to the companion article. This article assumes the curve is strong and attacks everything around it. The math guards the key against being computed. It says nothing about the key being &lt;em&gt;handed over&lt;/em&gt;. Three contracts govern that handover, and each one broke.&lt;/p&gt;
&lt;h2&gt;3. Three Contracts the Deployment Signs and Never Reads&lt;/h2&gt;
&lt;p&gt;Every cryptographic deployment signs a contract it never reads. There are three clauses, one for each layer the math leans on, and the fine print on all three is where real products sank.&lt;/p&gt;
&lt;h3&gt;The nonce contract: k must be fresh&lt;/h3&gt;
&lt;p&gt;Take the ECDSA signing equation and ask the cruelest possible question: what if &lt;code&gt;k&lt;/code&gt; is not unique? Suppose two different messages, with hashes &lt;code&gt;z1&lt;/code&gt; and &lt;code&gt;z2&lt;/code&gt;, are signed under the &lt;em&gt;same&lt;/em&gt; nonce &lt;code&gt;k&lt;/code&gt;. Because &lt;code&gt;r&lt;/code&gt; is the x-coordinate of &lt;code&gt;kG&lt;/code&gt;, the same &lt;code&gt;k&lt;/code&gt; produces the same &lt;code&gt;r&lt;/code&gt; in both signatures. Now write them down side by side:&lt;/p&gt;
&lt;p&gt;$$s_1 = k^{-1}(z_1 + r,d), \qquad s_2 = k^{-1}(z_2 + r,d) \pmod n$$&lt;/p&gt;
&lt;p&gt;Subtract. The &lt;code&gt;rd&lt;/code&gt; term is identical in both, so it cancels, and &lt;code&gt;k&lt;/code&gt; falls straight out:&lt;/p&gt;
&lt;p&gt;$$s_1 - s_2 = k^{-1}(z_1 - z_2), \qquad k = \frac{z_1 - z_2}{s_1 - s_2} \bmod n$$&lt;/p&gt;
&lt;p&gt;Once you know &lt;code&gt;k&lt;/code&gt;, the first equation has exactly one unknown left, and you solve for the private key by hand:&lt;/p&gt;
&lt;p&gt;$$d = \frac{s_1,k - z_1}{r} \bmod n$$&lt;/p&gt;
&lt;p&gt;No lattice. No side channel. No discrete log. &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;Two signatures that share a nonce&lt;/a&gt; are two linear equations in two unknowns, and a middle-schooler with modular arithmetic can finish the job.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The discrete-logarithm assumption protects &lt;code&gt;d&lt;/code&gt; against &lt;em&gt;direct computation from the public key&lt;/em&gt; -- and nothing else. It does not protect &lt;code&gt;d&lt;/code&gt; against being algebraically extracted when the nonce misbehaves. The moment two signatures reuse &lt;code&gt;k&lt;/code&gt;, the strongbox is irrelevant, because the deployment left a second door wide open.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The contract, then, is strict: &lt;code&gt;k&lt;/code&gt; must be secret, statistically uniform, and never reused or partially leaked. Break any word of that clause and the algebra above (or its statistical cousin, later) takes your key.&lt;/p&gt;
&lt;h3&gt;The parameter contract: talk to the curve you think you are&lt;/h3&gt;
&lt;p&gt;The second clause is about identity. A &lt;em&gt;named&lt;/em&gt; curve -- &quot;P-256,&quot; &quot;secp256k1&quot; -- is an agreement to use one specific set of domain parameters, and in particular one specific generator, the base point &lt;code&gt;G&lt;/code&gt; that every public key is measured against.&lt;/p&gt;

Every named elliptic curve fixes a single public point `G` from which all key material is derived: a private key `d` corresponds to the public point `dG`. The generator is not a secret -- it is a published constant. But it is load-bearing: &quot;the public key is `dG`&quot; is only meaningful if both sides agree on which `G`.

A certificate can specify its curve two ways. A *named curve* is an object identifier pointing at standardized domain parameters (fixed `a`, `b`, `p`, `G`, `n`). *Explicit parameters* spell out all of those numbers inline, which lets the certificate carry a *custom* generator. Accepting explicit parameters without checking them against a known named curve means trusting an attacker to tell you what `G` is.
&lt;p&gt;The contract: never trust attacker-supplied explicit parameters. A validator that checks a public &lt;em&gt;point&lt;/em&gt; but not the &lt;em&gt;generator&lt;/em&gt; it was measured against has, in effect, let the attacker choose the ruler and then declared the measurement correct. That is precisely the door CurveBall walked through [@curveball-cve-2020-0601].&lt;/p&gt;
&lt;h3&gt;The negotiation contract: agree to strength, not to a museum piece&lt;/h3&gt;
&lt;p&gt;The third clause governs what two parties agree to &lt;em&gt;before&lt;/em&gt; they exchange anything. In &lt;a href=&quot;https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/&quot; rel=&quot;noopener&quot;&gt;ephemeral Diffie-Hellman&lt;/a&gt;, each side picks a fresh secret per connection, which is supposed to guarantee that compromising one session cannot retroactively expose others.&lt;/p&gt;

In ephemeral DH, the private exponents are generated fresh for each connection and discarded afterward, so a key recovered later cannot decrypt past sessions. This property is called forward secrecy. It is the reason &quot;just use a fresh ephemeral key&quot; feels like a complete defense -- and, as Logjam showed, the reason that feeling is dangerously incomplete.

In the 1990s, US export regulations capped the strength of cryptography shipped abroad, producing deliberately weakened cipher suites such as 512-bit `DHE_EXPORT` Diffie-Hellman [@logjam-weakdh-2015]. The policy was abandoned, but the code was not: two decades later, servers and libraries still spoke these crippled suites on request.
&lt;p&gt;The contract: the negotiated group must be strong, fresh, and unshared. The fine print that sank it: many servers ship the &lt;em&gt;same&lt;/em&gt; standardized 512-bit prime, so breaking one group breaks all of them at once [@logjam-weakdh-2015]. A fresh ephemeral exponent does not save you when the prime itself is weak and shared.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Contract&lt;/th&gt;
&lt;th&gt;What it requires&lt;/th&gt;
&lt;th&gt;What violates it&lt;/th&gt;
&lt;th&gt;Field break&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Nonce&lt;/td&gt;
&lt;td&gt;&lt;code&gt;k&lt;/code&gt; secret, uniform, unique per signature&lt;/td&gt;
&lt;td&gt;a constant, repeated, or biased &lt;code&gt;k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;PS3, Android, Minerva, TPM-Fail, LadderLeak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameter&lt;/td&gt;
&lt;td&gt;named curve, standard generator &lt;code&gt;G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;trusting attacker-supplied explicit parameters&lt;/td&gt;
&lt;td&gt;CurveBall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Negotiation&lt;/td&gt;
&lt;td&gt;strong, fresh, unshared group&lt;/td&gt;
&lt;td&gt;downgrade to a crippled, shared group&lt;/td&gt;
&lt;td&gt;Logjam&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Three contracts, three clauses of fine print. Over fifteen years, every clause was breached in the field -- and the breaches fall into exactly three piles.&lt;/p&gt;
&lt;h2&gt;4. The Failure Catalog: Seven Breaks, Three Layers, One Shape&lt;/h2&gt;
&lt;p&gt;None of the seven incidents below solved a discrete logarithm. Every one of them recovered a private key or forged a trusted one. Watch where each attack actually landed -- because the landing spot, not the marquee name, is the whole lesson.&lt;/p&gt;

timeline
    title EC and DH in the field, the assumption versus the breaks
    1976 : Diffie-Hellman, the assumption is born
    1985-1987 : Miller and Koblitz, the curve migration
    2010 : PS3 constant nonce, reuse
    2013 : Android SecureRandom, reuse
    2015 : Logjam, downgrade plus shared-prime NFS
    2019-2020 : Minerva and TPM-Fail, lattice bias
    2020 : LadderLeak, Bleichenbacher-Fourier sub-bit : CurveBall, unvalidated generator
&lt;p&gt;Here is the entire corpus on one page. The rightmost column is the thesis in a single word, repeated seven times.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Effort to break&lt;/th&gt;
&lt;th&gt;What fell&lt;/th&gt;
&lt;th&gt;Discrete log solved?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;PS3&lt;/td&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;Nonce (reuse)&lt;/td&gt;
&lt;td&gt;a constant &lt;code&gt;k&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 signatures&lt;/td&gt;
&lt;td&gt;ECDSA firmware-signing key&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android wallets&lt;/td&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;Nonce (reuse)&lt;/td&gt;
&lt;td&gt;repeated &lt;code&gt;k&lt;/code&gt; from a broken RNG&lt;/td&gt;
&lt;td&gt;2 reused signatures&lt;/td&gt;
&lt;td&gt;Bitcoin wallet keys&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logjam&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;Downgrade&lt;/td&gt;
&lt;td&gt;forced 512-bit shared prime&lt;/td&gt;
&lt;td&gt;1 NFS precompute per prime&lt;/td&gt;
&lt;td&gt;TLS session secrecy&lt;/td&gt;
&lt;td&gt;Only a crippled, shared 512-bit export prime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPM-Fail&lt;/td&gt;
&lt;td&gt;2019-2020&lt;/td&gt;
&lt;td&gt;Nonce (bias)&lt;/td&gt;
&lt;td&gt;timing leak of nonce bit-length&lt;/td&gt;
&lt;td&gt;many signatures + lattice&lt;/td&gt;
&lt;td&gt;256-bit TPM keys&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minerva&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;Nonce (bias)&lt;/td&gt;
&lt;td&gt;timing leak of nonce bit-length&lt;/td&gt;
&lt;td&gt;about 2100 signatures + lattice&lt;/td&gt;
&lt;td&gt;ECDSA key&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LadderLeak&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;Nonce (bias)&lt;/td&gt;
&lt;td&gt;sub-bit ladder leak&lt;/td&gt;
&lt;td&gt;many signatures + Bleichenbacher/Fourier&lt;/td&gt;
&lt;td&gt;ECDSA key&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CurveBall&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;Validation&lt;/td&gt;
&lt;td&gt;an unchecked generator&lt;/td&gt;
&lt;td&gt;1 forged certificate&lt;/td&gt;
&lt;td&gt;certificate trust itself&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Six &quot;No&quot;s and one heavily qualified &quot;Yes.&quot; Let us walk the three pillars in turn.&lt;/p&gt;
&lt;h3&gt;Pillar 1a: the nonce, reused (certain, from two signatures)&lt;/h3&gt;
&lt;p&gt;In December 2010, at the 27th Chaos Communication Congress, the group fail0verflow presented &quot;Console Hacking 2010&quot; and revealed that Sony had signed PlayStation 3 firmware using ECDSA with a &lt;em&gt;constant&lt;/em&gt; nonce [@fail0verflow-ps3-2010]. Not a weak random &lt;code&gt;k&lt;/code&gt;. The same &lt;code&gt;k&lt;/code&gt;, every time. Every signature Sony ever produced shared a nonce with every other, which means any two of them collapse to the algebra from Section 3.Keep two PS3 events distinct. fail0verflow&apos;s 27C3 talk exposed the constant-nonce &lt;em&gt;mechanism&lt;/em&gt;. George Hotz released a separate PS3 root key weeks later, in January 2011 [@hotz-ps3-2011]. The teaching case -- a hardcoded constant nonce -- is fail0verflow&apos;s. The console&apos;s chain of trust, the thing Sony spent years engineering, came apart because &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;a random number generator&lt;/a&gt; was told to always return the same number.&lt;/p&gt;

flowchart LR
    A[&quot;Sig 1: s1 = k^-1 (z1 + r d)&quot;] --&amp;gt; C[&quot;Same k means same r&quot;]
    B[&quot;Sig 2: s2 = k^-1 (z2 + r d)&quot;] --&amp;gt; C
    C --&amp;gt; D[&quot;Subtract, k cancels: k = (z1 - z2) / (s1 - s2) mod n&quot;]
    D --&amp;gt; E[&quot;Solve for d = (s1 k - z1) / r mod n&quot;]
    E --&amp;gt; F[&quot;Private key recovered, no discrete log solved&quot;]
&lt;p&gt;This is not a metaphor. Here is the recovery, executable. Two signatures share a nonce, and a dozen lines of BigInt arithmetic reproduce the private key exactly:&lt;/p&gt;
&lt;p&gt;{`
// secp256k1 group order
const n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;&lt;/p&gt;
&lt;p&gt;// Modular inverse via the extended Euclidean algorithm
function inv(a, m) {
  let [oldr, r] = [mod(a, m), m];
  let [olds, s] = [1n, 0n];
  while (r !== 0n) {
    const q = oldr / r;
    [oldr, r] = [r, oldr - q * r];
    [olds, s] = [s, olds - q * s];
  }
  return mod(olds, m);
}&lt;/p&gt;
&lt;p&gt;// A victim signs two different messages with the SAME nonce k
const d  = 0x00c0ffee1234567890abcdef00c0ffee1234567890abcdef00c0ffee12345678n; // secret
const k  = 0x0000deadbeef0000feed0000deadbeef0000feed0000deadbeef0000feed0001n; // reused nonce
const r  = mod(0x1122334455667788112233445566778811223344556677881122334455667788n, n);
const z1 = 0x1111111111111111111111111111111111111111111111111111111111111111n;
const z2 = 0x2222222222222222222222222222222222222222222222222222222222222222n;
const s1 = mod(inv(k, n) * mod(z1 + mod(r * d, n), n), n);
const s2 = mod(inv(k, n) * mod(z2 + mod(r * d, n), n), n);&lt;/p&gt;
&lt;p&gt;// The attacker sees only (r, s1, z1) and (r, s2, z2). Recover k, then d.
const kRec = mod((z1 - z2) * inv(mod(s1 - s2, n), n), n);
const dRec = mod((mod(s1 * kRec, n) - z1) * inv(r, n), n);
console.log(&apos;recovered nonce matches:  &apos; + (kRec === k));
console.log(&apos;recovered private key:    &apos; + (dRec === d));
console.log(&apos;d = 0x&apos; + dRec.toString(16));
`}&lt;/p&gt;
&lt;p&gt;Three years later, the same algebra emptied wallets. In August 2013, an advisory on bitcoin.org warned that &quot;a component of Android responsible for generating secure random numbers contains critical weaknesses&quot; that rendered &quot;all Android wallets&quot; vulnerable to theft [@bitcoin-android-2013]. The root cause, per Google&apos;s own postmortem, was that the platform&apos;s &lt;code&gt;SecureRandom&lt;/code&gt; &quot;may not receive cryptographically strong values&quot; because the underlying PRNG was improperly initialized [@android-securerandom-2013].&lt;/p&gt;
&lt;p&gt;The consequence was nonce &lt;em&gt;repetition&lt;/em&gt; across transactions. And because every Bitcoin signature is broadcast to a public ledger, anyone could scan the chain for two signatures sharing an &lt;code&gt;r&lt;/code&gt; and drain the wallet behind them.The bitcoin.org advisory names the flaw and the affected wallets but states no total loss figure [@bitcoin-android-2013]. Any &quot;coins stolen&quot; number you have seen is reported, not established -- treat it accordingly.&lt;/p&gt;
&lt;p&gt;The detector that would have caught both PS3 and Android is embarrassingly short. Group public signatures by &lt;code&gt;r&lt;/code&gt;; any collision is an exposed key:&lt;/p&gt;
&lt;p&gt;{`
const n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;
function inv(a, m) {
  let [oldr, r] = [mod(a, m), m];
  let [olds, s] = [1n, 0n];
  while (r !== 0n) { const q = oldr / r; [oldr, r] = [r, oldr - q * r]; [olds, s] = [s, olds - q * s]; }
  return mod(olds, m);
}
// Build a public feed in which one signer reused a nonce, then forget the secret
const dSecret = 777n, kReused = 424242n, rReused = 987654321n;
const mk = (z) =&amp;gt; ({ r: rReused, s: mod(inv(kReused, n) * mod(z + mod(rReused * dSecret, n), n), n), z });
const feed = [ { r: 111n, s: 22n, z: 5n }, mk(1001n), { r: 222n, s: 44n, z: 9n }, mk(2002n), { r: 333n, s: 66n, z: 12n } ];&lt;/p&gt;
&lt;p&gt;const seen = new Map();
for (const g of feed) {
  const key = g.r.toString();
  if (seen.has(key)) {
    const a = seen.get(key), b = g;
    const k = mod((a.z - b.z) * inv(mod(a.s - b.s, n), n), n);
    const d = mod((mod(a.s * k, n) - a.z) * inv(a.r, n), n);
    console.log(&apos;NONCE REUSE at r = &apos; + key + &apos;  -&amp;gt;  private key d = &apos; + d.toString());
  } else {
    seen.set(key, g);
  }
}
`}&lt;/p&gt;
&lt;p&gt;Two incidents, two different root causes -- a hardcoded constant and a starved RNG -- and one identical, certain outcome. This is the crudest form of the whole thesis: the deployment lost the key while the discrete log stood untouched.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A &lt;em&gt;repeated&lt;/em&gt; nonce is certain key recovery from just two signatures, by elementary algebra. A &lt;em&gt;biased&lt;/em&gt; nonce -- one that merely leans, statistically, toward certain values -- leaks only a partial statistic and requires a statistical attack over many signatures. &quot;One biased signature leaks your key&quot; is false. The distinction is the difference between the next two subsections, and blurring it is the single most common error in this area.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Reused nonce&lt;/th&gt;
&lt;th&gt;Biased nonce&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;What leaks&lt;/td&gt;
&lt;td&gt;the whole nonce (it repeats)&lt;/td&gt;
&lt;td&gt;a partial statistic about the nonce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signatures needed&lt;/td&gt;
&lt;td&gt;exactly 2&lt;/td&gt;
&lt;td&gt;many (Minerva about 2100; PuTTY about 60 [@putty-cve-2024-31497])&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attack&lt;/td&gt;
&lt;td&gt;elementary algebra&lt;/td&gt;
&lt;td&gt;Hidden Number Problem (lattice or Fourier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Certainty&lt;/td&gt;
&lt;td&gt;certain&lt;/td&gt;
&lt;td&gt;statistical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Examples&lt;/td&gt;
&lt;td&gt;PS3, Android&lt;/td&gt;
&lt;td&gt;Minerva, TPM-Fail, LadderLeak, PuTTY&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Pillar 1b: the nonce, biased (statistical, over many signatures)&lt;/h3&gt;
&lt;p&gt;Reuse is loud. The subtler failure is a nonce that never repeats but is not quite uniform -- one whose top bit is a little too often zero, say. It turns out that even this surrenders the key, and the theory predicting it predates the exploits by two decades.&lt;/p&gt;

Given many samples that each leak a few of the most-significant bits of `k`, together with the public signature data, recover the fixed secret `d`. Boneh and Venkatesan defined this problem in 1996 and proved it tractable [@boneh-venkatesan-1996]. Every biased-nonce attack in this article -- Minerva, TPM-Fail, LadderLeak, PuTTY -- is an instance of it.
&lt;p&gt;Nguyen and Shparlinski turned the abstract HNP into a concrete lattice attack on (EC)DSA with partially known nonces [@nguyen-shparlinski-2003], and Brumley and Tuveri showed in 2011 that the necessary bit-leak could be harvested &lt;em&gt;remotely&lt;/em&gt;, over a network, from an OpenSSL implementation whose scalar multiplication was not constant-time [@brumley-tuveri-2011]. Then the field split into two solver regimes, and telling them apart is essential.&lt;/p&gt;
&lt;p&gt;When the &lt;a href=&quot;https://paragmali.com/blog/correct-constant-time-and-still-owned-a-field-guide-to-side-/&quot; rel=&quot;noopener&quot;&gt;timing channel&lt;/a&gt; leaks a &lt;em&gt;few bits&lt;/em&gt; of the nonce -- typically its exact bit-length -- the HNP is solved by &lt;strong&gt;lattice reduction&lt;/strong&gt;. Minerva (CHES 2020) demonstrated this against smartcards and libraries whose scalar multiplication ran in time correlated with the nonce&apos;s length, reporting roughly &quot;2100 signatures to recover the private key on the standard secp256r1 curve&quot; [@minerva-2020].&lt;/p&gt;
&lt;p&gt;TPM-Fail (USENIX Security 2020) carried the same idea into hardware roots of trust. A timing leak in Intel firmware TPMs (CVE-2019-11090 [@cve-2019-11090]) and STMicroelectronics ST33 chips (CVE-2019-16863 [@cve-2019-16863]) recovered a 256-bit ECDSA key from an Intel fTPM in 4 to 20 minutes, and a VPN authentication key remotely in about five hours [@tpmfail-2020].Two honesty notes. Minerva&apos;s project page retracted its original EdDSA claim; the confirmed result is ECDSA-only [@minerva-2020]. And the TPM-Fail targets were not toys -- they held Common Criteria EAL4+ and FIPS 140-2 certification, and the leak survived that scrutiny [@tpmfail-2020].&lt;/p&gt;

flowchart TD
    L[&quot;A timing channel leaks partial information about the nonce k&quot;] --&amp;gt; Q{&quot;How much leaks per signature?&quot;}
    Q --&amp;gt;|&quot;at least 2 bits&quot;| LAT[&quot;Lattice reduction of the HNP: Minerva, TPM-Fail&quot;]
    Q --&amp;gt;|&quot;under 1 bit&quot;| FFT[&quot;Bleichenbacher and Fourier HNP: LadderLeak&quot;]
    LAT --&amp;gt; R[&quot;Private key d recovered over MANY signatures&quot;]
    FFT --&amp;gt; R
&lt;p&gt;The second regime is where people get it wrong. When the leak drops &lt;em&gt;below a single bit&lt;/em&gt; -- LadderLeak revealed the most-significant bit of the nonce &quot;but with probability less than one&quot; -- lattices become intractable, and the attack instead revives a Fourier-analysis method the paper credits to Daniel Bleichenbacher [@ladderleak-2020].&lt;/p&gt;
&lt;p&gt;The LadderLeak authors note that &quot;most of the attacks so far have relied on at least two bits of nonce bias,&quot; precisely the lattice regime. Their contribution was to break below it, over curves sect163r1 and NIST P-192, using the Fourier solver rather than a lattice [@ladderleak-2020]. Calling LadderLeak a lattice attack is the field&apos;s classic mistake.&lt;/p&gt;

&quot;Breaking ECDSA With Less Than One Bit Of Nonce Leakage&quot; -- the title of the LadderLeak paper, and the most compact statement of how little a nonce must betray before the key is gone.
&lt;p&gt;Notice what happened across this subsection. Every one of these attacks beat a signer that had already removed the reuse bug. The leak had moved from how the nonce was &lt;em&gt;generated&lt;/em&gt; to how it was &lt;em&gt;used&lt;/em&gt; -- a variable-time scalar multiplication -- and the cure had to move with it. Hold that thought; it is the pivot of the entire article. But reuse or bias, lattice or Fourier, two signatures or two thousand, the outcome is the same and the discrete logarithm is never touched: the deployment hands over &lt;code&gt;d&lt;/code&gt;, and the strongbox stays locked.&lt;/p&gt;
&lt;h3&gt;Pillar 2: the parameters, unvalidated (CurveBall)&lt;/h3&gt;
&lt;p&gt;The nonce pillar is about a secret the deployment mishandles. The second pillar is about an identity the deployment fails to check. On 14 January 2020, Microsoft patched CVE-2020-0601, reported by the NSA: &quot;a spoofing vulnerability exists in the way Windows CryptoAPI (Crypt32.dll) validates Elliptic Curve Cryptography (ECC) certificates&quot; [@curveball-cve-2020-0601]. It became known as CurveBall, and it is the cleanest example in the whole corpus of a break that has nothing to do with the mathematics of ECC.&lt;/p&gt;
&lt;p&gt;Recall the parameter contract. A public key is the point &lt;code&gt;dG&lt;/code&gt; -- meaningful only if everyone agrees on &lt;code&gt;G&lt;/code&gt;. CryptoAPI, when handed a certificate that specified its curve with &lt;em&gt;explicit&lt;/em&gt; parameters, checked that the certificate&apos;s public &lt;em&gt;point&lt;/em&gt; matched a trusted root&apos;s public key. What it did not check was the &lt;em&gt;generator&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So an attacker could craft a curve with a custom generator &lt;code&gt;G&apos;&lt;/code&gt;, pick any private key &lt;code&gt;d&apos;&lt;/code&gt; they liked, and compute &lt;code&gt;G&apos;&lt;/code&gt; such that &lt;code&gt;d&apos; * G&apos;&lt;/code&gt; equals the exact public point of a &lt;a href=&quot;https://paragmali.com/blog/a-perfect-signature-for-a-certificate-that-should-never-have/&quot; rel=&quot;noopener&quot;&gt;trusted root certificate authority&lt;/a&gt;. The forged certificate then presents the right public point on the wrong curve. Windows, comparing only the point, accepts it -- while a library that pins named curves rejects it instantly, because &lt;code&gt;G&apos;&lt;/code&gt; is not the standard base point.&lt;/p&gt;

flowchart TD
    A[&quot;Attacker picks explicit curve parameters with a custom generator G-prime&quot;] --&amp;gt; B[&quot;Chooses private key d-prime so that d-prime times G-prime equals a trusted root public point Q&quot;]
    B --&amp;gt; C[&quot;Forged certificate presents point Q on the attacker curve&quot;]
    C --&amp;gt; D{&quot;Windows CryptoAPI validation&quot;}
    D --&amp;gt;|&quot;Public point matches a trusted root? YES&quot;| E[&quot;Accepted, but the generator was never checked&quot;]
    D -.-&amp;gt;|&quot;A named-curve-pinning library also checks G&quot;| F[&quot;Rejected, G-prime is not the standard base point&quot;]
    E --&amp;gt; G[&quot;Forged code-signing or TLS certificate chains to a trusted root&quot;]
&lt;p&gt;The ECDLP was never in play. The attacker chose their own private key; they never had to recover anyone&apos;s. The failure is a single missing comparison -- generator against named curve -- buried in a certificate validation stack that a billion machines trusted. CurveBall is a validation bug, not a curve break, and the fix is exactly what the missing check would have been: reject explicit parameters, or fully re-validate them against the claimed named curve.&lt;/p&gt;
&lt;h3&gt;Pillar 3: the negotiation, downgraded (Logjam)&lt;/h3&gt;
&lt;p&gt;The third pillar is the one honest exception, and handled carelessly it looks like a refutation of the whole thesis. In 2015, the Logjam team (CVE-2015-4000) showed that a man-in-the-middle could rewrite a TLS handshake -- &quot;rewriting a ClientHello with DHE replaced by DHE_EXPORT&quot; -- to force the connection onto a 512-bit export-grade Diffie-Hellman group [@cve-2015-4000]. And here, unlike everywhere else, the attacker really does compute a discrete logarithm.&lt;/p&gt;
&lt;p&gt;The trick is that the expensive part of the number field sieve has to be done only once per prime, and servers share primes.&lt;/p&gt;

The fastest known classical algorithm for the finite-field discrete logarithm (and for integer factoring). Its decisive feature here: its costly first stage depends only on the *prime* being attacked, not on any particular exchanged key. Precompute once per prime, and every session under that prime becomes cheap to break. The internals belong to the companion article; what matters here is that one structural fact.
&lt;p&gt;The Logjam researchers found that &quot;8.4% of the Top 1 Million domains were initially vulnerable,&quot; and that because so many servers reused a handful of standardized 512-bit primes, &quot;the first step in the number field sieve ... is dependent only on this prime&quot; [@logjam-weakdh-2015]. One precomputation, amortized across every server that shipped that prime.&lt;/p&gt;

&quot;The first step in the number field sieve ... is dependent only on this prime.&quot; One precomputation breaks every server that shares the group.

sequenceDiagram
    participant C as Client
    participant M as MITM
    participant S as Server
    Note over M: One-time NFS precomputation per shared 512-bit prime
    C-&amp;gt;&amp;gt;M: ClientHello offering DHE
    M-&amp;gt;&amp;gt;S: ClientHello rewritten to DHE_EXPORT
    S-&amp;gt;&amp;gt;M: 512-bit export DH parameters, a shared standardized prime
    M-&amp;gt;&amp;gt;C: forwards the weak parameters
    Note over M: Precomputation done, so this connection solves fast
    M--&amp;gt;&amp;gt;C: session decrypted in near real time

The 512-bit `DHE_EXPORT` groups existed because 1990s US export regulations forbade shipping strong cryptography abroad, mandating deliberately breakable key exchange for foreign markets [@logjam-weakdh-2015]. The policy was abandoned by the early 2000s, but the cipher suites were never removed from server software. Logjam, in 2015, exploited a legal compromise that had been dead for over a decade and a codebase that never got the memo. The math did not decay -- the deployment simply kept a museum piece plugged in.
&lt;p&gt;Read carefully, Logjam is not a counterexample; it is the thesis in its most literal form. A discrete log &lt;em&gt;was&lt;/em&gt; computed, but only against a 512-bit prime that a downgrade forced the connection onto and that policy had left crippled and shared. The deployment set the weak, shared group on the table; the number field sieve merely finished the job. No strong, standard-sized group ever fell. The fix is the same shape as the break: remove export ciphers, negotiate vetted named groups (RFC 7919 [@rfc7919]), and adopt TLS 1.3, which removes static Diffie-Hellman entirely [@rfc8446].&lt;/p&gt;
&lt;p&gt;Three pillars, seven breaks, one shape: every attack hit a layer the discrete-log math depends on but does not control. Name the shape, and you have the whole discipline.&lt;/p&gt;
&lt;h2&gt;5. The Realization: Fix the Contracts, Not the Curve&lt;/h2&gt;
&lt;p&gt;There was no single eureka here. The insight accumulated across fifteen years of incidents and then snapped into one sentence: every field break attacked a layer the discrete-log math &lt;em&gt;depends on but does not control&lt;/em&gt; -- the nonce, then the parameters, then the negotiated strength -- and every fix changed &lt;em&gt;how the primitive is generated, used, validated, or negotiated&lt;/em&gt;, never the curve and never the log.&lt;/p&gt;
&lt;p&gt;The sharpest crystallization hides inside the word &quot;bias.&quot; For years it was treated as one problem with one fix: remove the random number generator, derive the nonce deterministically (RFC 6979 [@rfc6979], later blessed by FIPS 186-5 [@fips-186-5]), and the biased-nonce class is solved. But Minerva, TPM-Fail, and LadderLeak all recovered keys from signers that had &lt;em&gt;already&lt;/em&gt; done that. The reason is that &quot;bias&quot; was concealing two distinct root causes with two distinct cures.&lt;/p&gt;

flowchart TD
    B[&quot;One word: nonce bias&quot;] --&amp;gt; G[&quot;Generation defect: the RNG returns a constant, repeated, or predictable k&quot;]
    B --&amp;gt; U[&quot;Usage defect: variable-time scalar multiplication leaks the bit-length of k&quot;]
    G --&amp;gt; GC[&quot;Cure: deterministic and hedged nonces, RFC 6979 and FIPS 186-5&quot;]
    U --&amp;gt; UC[&quot;Cure: constant-time, exception-free scalar multiplication&quot;]
    GC --&amp;gt; BOTH[&quot;You need BOTH, they are orthogonal&quot;]
    UC --&amp;gt; BOTH
&lt;p&gt;A &lt;em&gt;generation&lt;/em&gt; defect is a bad nonce coming out of the sampler -- a constant (PS3), a repeat (Android), a predictable value. Determinism and hedging cure it. A &lt;em&gt;usage&lt;/em&gt; defect is a perfectly good nonce leaking through the &lt;em&gt;timing&lt;/em&gt; of the scalar multiplication that consumes it -- the bit-length falling out through a side channel (Minerva, TPM-Fail) or a sub-bit correlation (LadderLeak). Only constant-time code cures that. The two cures are orthogonal, and you need both.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Generalize the two-cures fork and you get the entire discipline of this field: four contracts, not one curve. &lt;em&gt;Generate&lt;/em&gt; the nonce so it cannot be predicted or repeated; &lt;em&gt;use&lt;/em&gt; it in constant time so it cannot leak; &lt;em&gt;validate&lt;/em&gt; your parameters so no attacker chooses your generator; &lt;em&gt;negotiate&lt;/em&gt; only strong, unshared groups so no downgrade can strand you. Every documented break is a breach of one of those four. No break in the corpus is a break of the discrete logarithm.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This also dissolves the apparent contradiction the article opened with. &quot;Nobody solved the discrete log&quot; and &quot;somebody recovered the key&quot; only sound incompatible if you conflate the discrete-log &lt;em&gt;problem&lt;/em&gt; with a discrete-log &lt;em&gt;deployment&lt;/em&gt;. The problem is a statement about computational hardness; the deployment is a nonce, a set of parameters, and a negotiation, wrapped around that hardness. The problem stayed hard. The wrapping tore.&lt;/p&gt;

If you remember one operational thing from this article, make it this: deterministic ECDSA and constant-time scalar multiplication are not two names for the same safeguard. A team can adopt RFC 6979, check the box marked &quot;nonce,&quot; and still ship a signer that a co-located attacker times to death. Ask both questions separately -- how is the nonce generated, and how is it used -- because they have separate answers.
&lt;p&gt;If the discipline is &quot;honor the four contracts, not the curve,&quot; what does a deployment that honors all four actually look like in 2026?&lt;/p&gt;
&lt;h2&gt;6. What Correct EC/DH Deployment Looks Like in 2026&lt;/h2&gt;
&lt;p&gt;The state of the art in this field is not a better discrete-log solver -- no such thing exists. It is a &lt;em&gt;deployment discipline&lt;/em&gt; at the three layers the math never controlled. Here it is, pillar by pillar.&lt;/p&gt;
&lt;h3&gt;Nonce: two orthogonal cures, and a primitive that folds in both&lt;/h3&gt;

Derive the nonce `k` deterministically from the private key and the message hash via an HMAC-based construction, so the signer &quot;do[es] not need access to a source of high-quality randomness&quot; [@rfc6979]. Identical key-and-message pairs always yield the same `k`; distinct messages yield unrelated nonces. This closes the *generation* defect -- the PS3 and Android reuse classes cannot occur.
&lt;p&gt;RFC 6979 also quietly records the deeper lesson. It insists on comparing each candidate nonce against the group order rather than reducing it modulo the order, warning that a &quot;simple modular reduction would induce biases that would be detrimental to signature security&quot; [@rfc6979]. That is a standards document, in 2013, acknowledging that &lt;em&gt;bias&lt;/em&gt;, not just reuse, is fatal. You can watch the effect yourself:&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;// A raw sampler emits values in [0, 256). We need a nonce in [0, q). const q = 200n, B = 256n, trials = 400000; const raw = () =&amp;gt; BigInt(Math.floor(Number(B) * Math.random())); let biasedHigh = 0, fairHigh = 0; const half = q / 2n; for (let i = 0; i &amp;lt; trials; i++) {   const reduced = raw() % q;              // reduce mod q -&amp;gt; BIASED   if (reduced &amp;gt;= half) biasedHigh++;   let fair; do { fair = raw(); } while (fair &amp;gt;= q);  // compare to q, retry -&amp;gt; FAIR   if (fair &amp;gt;= half) fairHigh++; } console.log(&apos;reduce mod q -&amp;gt; P(nonce in top half): &apos; + (biasedHigh / trials).toFixed(4)); console.log(&apos;compare to q -&amp;gt; P(nonce in top half): &apos; + (fairHigh / trials).toFixed(4)); console.log(&apos;That gap from 0.5 is a free bit for a Hidden Number Problem attacker.&apos;);&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;Determinism, though, fixes only generation. On hardware it also &lt;em&gt;adds&lt;/em&gt; a risk.&lt;/p&gt;

Compute the nonce from the private key, the message hash, *and* a few bytes of fresh randomness. A dead RNG falls back to the safe deterministic core; a differential fault, which needs two identical signings to differ, is defeated by the fresh input. FIPS 186-5 recommends this for hardware because bare determinism is &quot;of particular concern for ... deterministic signature schemes, as well as embedded or IoT devices and smartcards&quot; [@fips-186-5].

Compute `kG` with a data-independent trace of instructions, branches, and memory accesses, so the runtime never depends on the bit-length or value of `k`. This is the *usage* cure -- the only defense against Minerva, TPM-Fail, and LadderLeak, whose leaks all live in the multiplication, not the sampler. Its precursor is Brumley and Tuveri&apos;s 2011 remote attack on a non-constant-time OpenSSL ladder [@brumley-tuveri-2011].
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Hedged signing and constant-time multiplication are the two cures of the fork, and they do not substitute for each other. A hardware signer needs &lt;em&gt;both&lt;/em&gt;: hedge the generation, and make the usage constant-time. Checking one box leaves the other break wide open.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;New designs can sidestep the whole fork by choosing a misuse-resistant primitive. Ed25519 (RFC 8032) derives its nonce as a hash of the secret and message -- deterministic by construction, no per-signature RNG -- and signs with a single &lt;em&gt;complete&lt;/em&gt;, constant-time addition law that has no exceptional cases to mishandle [@rfc8032]. It folds both cures into the primitive and removes the dangerous choices from the caller&apos;s hands.Misuse-resistant is not misuse-proof. PuTTY derived P-521 ECDSA nonces such that &quot;the first 9 bits of each ECDSA nonce are zero,&quot; allowing &quot;full secret key recovery in roughly 60 signatures&quot; (CVE-2024-31497) [@putty-cve-2024-31497]. Determinism implemented carelessly re-introduced exactly the bias it was meant to prevent.&lt;/p&gt;
&lt;h3&gt;Validation: reject what you cannot vouch for&lt;/h3&gt;
&lt;p&gt;The CurveBall cure is direct: bind a certificate&apos;s curve to the standard parameters of its named curve, and reject explicit parameters or fully re-validate them. The January 2020 patch made &lt;code&gt;crypt32.dll&lt;/code&gt; check the generator, not merely the point [@curveball-cve-2020-0601].&lt;/p&gt;
&lt;p&gt;For key &lt;em&gt;exchange&lt;/em&gt; with static keys, the discipline is fuller. Before using a received point, confirm it is not the identity, lies on the curve, and sits in the correct prime-order subgroup -- the full public-key validation of NIST SP 800-56A, which blocks &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;invalid-curve and small-subgroup attacks&lt;/a&gt; that would otherwise bleed a static key out one query at a time [@sp-800-56a-r3].&lt;/p&gt;
&lt;p&gt;The most durable answer is to make invalid inputs unrepresentable. X25519 works on a Montgomery curve chosen so that &lt;em&gt;every&lt;/em&gt; 32-byte string is an acceptable public key; the RFC even instructs implementations to &quot;accept non-canonical values,&quot; and to run the conditional swap &quot;in constant time&quot; [@rfc7748]. There is no explicit-parameter path to abuse and no on-curve check to forget.This is the &quot;make invalid states unrepresentable&quot; principle applied to cryptography: the safest validation code is the code you deleted because the type system already guarantees the property. But note the boundary carefully.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; X25519&apos;s scalar &quot;clamping&quot; fixes the cofactor and twist problems for ephemeral key exchange. It does &lt;em&gt;not&lt;/em&gt; replace named-curve pinning in the X.509 world, and some protocols still need a contributory-behavior check on the shared secret. Twist-secure curves solve the problems they were designed for -- not every parameter problem.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Negotiation: remove the weak option entirely&lt;/h3&gt;
&lt;p&gt;The downgrade pillar&apos;s fix is structural. RFC 7919 replaced ad-hoc, shared Diffie-Hellman primes with a menu of vetted named groups negotiated like curves [@rfc7919]. TLS 1.3 went further and deleted the weak options outright, so there is nothing to downgrade &lt;em&gt;to&lt;/em&gt;.&lt;/p&gt;

&quot;Static RSA and Diffie-Hellman cipher suites have been removed; all public-key based key exchange mechanisms now provide forward secrecy.&quot; -- RFC 8446, the TLS 1.3 specification [@rfc8446].
&lt;p&gt;Because TLS 1.3 permits only strong ephemeral groups and binds the negotiation into the handshake transcript, the Logjam rewrite is not merely discouraged -- it is structurally impossible [@rfc8446]. The end state of the negotiation pillar is a protocol in which the crippled, shared 512-bit group Logjam relied on simply cannot be selected.&lt;/p&gt;
&lt;p&gt;Each of these is a choice, and where there is a choice, engineers argue. Which discipline actually wins in which threat model?&lt;/p&gt;
&lt;h2&gt;7. Competing Approaches: Which Defense Answers Which Adversary&lt;/h2&gt;
&lt;p&gt;These defenses are not rungs on one ladder. They are answers to different adversaries, and the honest way to compare them is one pillar at a time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A constant-time multiplier and a named-curve pin do not compete -- they defend different layers, and a real system needs one from each pillar. The comparisons below rank options &lt;em&gt;within&lt;/em&gt; a pillar. Picking the best nonce discipline does nothing for your parameter validation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Nonce: which signing discipline stops which adversary&lt;/h3&gt;
&lt;p&gt;The load-bearing insight is that no single column stops every row. Generation cures (deterministic [@rfc6979], hedged [@fips-186-5]) and the usage cure (constant-time) are orthogonal, which is why hedged-plus-constant-time became the practitioner default and why Ed25519 [@rfc8032] folds most of both into the primitive.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adversary / failure&lt;/th&gt;
&lt;th&gt;Randomized (bare RNG)&lt;/th&gt;
&lt;th&gt;Deterministic (RFC 6979)&lt;/th&gt;
&lt;th&gt;Hedged (FIPS 186-5)&lt;/th&gt;
&lt;th&gt;Constant-time mult&lt;/th&gt;
&lt;th&gt;Ed25519&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;RNG returns constant or repeat (PS3, Android)&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;stops&lt;/td&gt;
&lt;td&gt;stops&lt;/td&gt;
&lt;td&gt;orthogonal&lt;/td&gt;
&lt;td&gt;stops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Differential fault on a deterministic signer&lt;/td&gt;
&lt;td&gt;randomness helps&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;stops&lt;/td&gt;
&lt;td&gt;orthogonal&lt;/td&gt;
&lt;td&gt;inherits the caveat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timing leak of nonce bit-length (Minerva, TPM-Fail)&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;stops&lt;/td&gt;
&lt;td&gt;stops (uniform ladder)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sub-bit ladder leak (LadderLeak)&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;fails&lt;/td&gt;
&lt;td&gt;stops&lt;/td&gt;
&lt;td&gt;stops (uniform ladder)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read the rows, not the columns: reuse fails only the first row and is certain from two signatures; the timing leaks fail three of the five columns and are cured on a different axis entirely. That is the two-cures fork, rendered as a matrix.&lt;/p&gt;
&lt;h3&gt;Validation: three philosophies of not trusting your input&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Named-curve pinning&lt;/th&gt;
&lt;th&gt;Full validation (SP 800-56A)&lt;/th&gt;
&lt;th&gt;All-inputs-valid (X25519)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Philosophy&lt;/td&gt;
&lt;td&gt;reject non-standard parameters&lt;/td&gt;
&lt;td&gt;validate every received input&lt;/td&gt;
&lt;td&gt;make invalid inputs unrepresentable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stops CurveBall (unvalidated generator)&lt;/td&gt;
&lt;td&gt;yes, its exact fix&lt;/td&gt;
&lt;td&gt;yes, re-derives the generator&lt;/td&gt;
&lt;td&gt;yes, no explicit-parameter path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stops invalid-curve / small-subgroup&lt;/td&gt;
&lt;td&gt;partial&lt;/td&gt;
&lt;td&gt;yes, on-curve plus subgroup&lt;/td&gt;
&lt;td&gt;yes, twist-secure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-operation cost&lt;/td&gt;
&lt;td&gt;O(1) table compare&lt;/td&gt;
&lt;td&gt;O(1) up to one scalar mult&lt;/td&gt;
&lt;td&gt;one constant-time ladder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;X.509, code-signing, PKI&lt;/td&gt;
&lt;td&gt;static-key ECDH, HSMs&lt;/td&gt;
&lt;td&gt;ephemeral ECDH&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Named-curve pinning is the precise CurveBall fix [@curveball-cve-2020-0601]; SP 800-56A validation is the belt-and-suspenders choice for static keys [@sp-800-56a-r3]; X25519 deletes the problem by construction [@rfc7748]. They converge on stopping CurveBall and diverge on everything else.&lt;/p&gt;
&lt;h3&gt;Downgrade: hardening, deletion, or hedging against tomorrow&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Config hardening + FFDHE&lt;/th&gt;
&lt;th&gt;TLS 1.3&lt;/th&gt;
&lt;th&gt;Hybrid post-quantum&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Removes the 512-bit export rung&lt;/td&gt;
&lt;td&gt;yes, within FFDHE&lt;/td&gt;
&lt;td&gt;yes, structurally&lt;/td&gt;
&lt;td&gt;yes, inherits 1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removes shared or unknown primes&lt;/td&gt;
&lt;td&gt;yes, named menu&lt;/td&gt;
&lt;td&gt;yes, fixed groups&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Downgrade protection in the handshake&lt;/td&gt;
&lt;td&gt;no, still in-band&lt;/td&gt;
&lt;td&gt;yes, transcript-bound&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Defends against a future Shor adversary&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes, if ML-KEM holds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Residual weakness&lt;/td&gt;
&lt;td&gt;the legacy long tail&lt;/td&gt;
&lt;td&gt;non-TLS 1024-bit primes&lt;/td&gt;
&lt;td&gt;a KEM is not a NIKE, young PQC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;RFC 7919 named groups [@rfc7919] are the transitional hardening for TLS 1.2; TLS 1.3 is the structural end state [@rfc8446]; hybrid X25519 with &lt;a href=&quot;https://paragmali.com/blog/one-event-three-assumptions-five-answers-a-field-guide-to-th/&quot; rel=&quot;noopener&quot;&gt;ML-KEM&lt;/a&gt; (RFC 9954 [@rfc9954], ML-KEM standardized as FIPS 203 [@fips-203]) is the forward hedge against a quantum adversary. Only the last column defends against a break that would be genuine mathematics rather than a deployment slip.&lt;/p&gt;
&lt;p&gt;Every column in every table changed the deployment, not the curve. So is the discrete log itself simply unbreakable? Almost -- and the &quot;almost&quot; is the most honest paragraph in this article.&lt;/p&gt;
&lt;h2&gt;8. Theoretical Limits: What Is Provably True on Both Sides&lt;/h2&gt;
&lt;p&gt;This is the honest concession the thesis has been earning the right to make. There are two provable sides here, and the argument needs both.&lt;/p&gt;
&lt;h3&gt;The hardness side: no room for a strong curve to fall&lt;/h3&gt;
&lt;p&gt;On a well-chosen group, the discrete logarithm is genuinely hard, and we can say so with unusual precision. The best classical attack is Pollard&apos;s rho, a square-root-time random walk that needs negligible memory [@teske-2001]. In the strict generic model its expected cost is about $\sqrt{\pi n/2}$, roughly 1.25 times $\sqrt{n}$ group operations; on an elliptic curve, the negation map (where $P$ and $-P$ share an x-coordinate) trims that to about 0.886 times $\sqrt{n}$. Either way the exponent is the same, matching baby-step giant-step&apos;s $O(\sqrt{n})$ time [@hac-ch11].&lt;/p&gt;
&lt;p&gt;Against that upper bound, Shoup proved a generic &lt;em&gt;lower&lt;/em&gt; bound of $\Omega(\sqrt{p})$: no algorithm restricted to the group operation can do better [@shoup-1997]. The two meet at $\Theta(\sqrt{n})$. The generic gap is essentially closed -- which is exactly why a 256-bit curve delivers around 128-bit security, and why no strong deployed curve&apos;s discrete log has fallen.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Quantity&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Best classical DL attack&lt;/td&gt;
&lt;td&gt;Pollard&apos;s rho, about 1.25 times sqrt(n) generic; 0.886 times sqrt(n) on curves&lt;/td&gt;
&lt;td&gt;negligible space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baby-step giant-step&lt;/td&gt;
&lt;td&gt;O(sqrt(n)) time and O(sqrt(n)) space&lt;/td&gt;
&lt;td&gt;the space-heavy twin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic lower bound&lt;/td&gt;
&lt;td&gt;Omega(sqrt(p)), provably no faster generic algorithm&lt;/td&gt;
&lt;td&gt;Shoup 1997&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Finite-field DH (non-generic)&lt;/td&gt;
&lt;td&gt;sub-exponential via the number field sieve&lt;/td&gt;
&lt;td&gt;why DH needs about 3072-bit primes (2048-bit minimum)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong deployed curve or standard group ever solved&lt;/td&gt;
&lt;td&gt;none, only Logjam&apos;s crippled shared 512-bit prime&lt;/td&gt;
&lt;td&gt;the thesis in one row&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The last two rows carry the concession. Finite-field Diffie-Hellman is &lt;em&gt;not&lt;/em&gt; generic: the number field sieve solves it in sub-exponential time, which is why finite-field DH needs primes of about 3072 bits to match the security a 256-bit curve gives (2048 bits is the absolute minimum), and why Logjam&apos;s downgrade to 512 bits was finishable at all [@logjam-weakdh-2015]. The number-field-sieve mathematics belongs to the companion article.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Logjam is the only break in this corpus that reached the mathematics, and the reason is structural, not accidental. A finite field carries extra arithmetic structure that a generic group lacks, which lets the number field sieve run in sub-exponential time, faster than the square-root barrier that protects a strong elliptic curve. That structural gap is the sole opening the discrete log ever offered, and even it was not enough alone: the computation became feasible only after the prime was cut to 512 export-grade bits. Raise the group back to a standard 2048-bit prime, or move to a curve where no such shortcut exists, and the opening closes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The leakage side: no room to hide a little bias&lt;/h3&gt;
&lt;p&gt;The other provable side indicts the deployment just as sharply. The Hidden Number Problem makes partial leakage fatal by construction: Boneh and Venkatesan proved that recovering a secret from many small most-significant-bit leaks is tractable [@boneh-venkatesan-1996], and Nguyen and Shparlinski turned that into a working lattice attack on (EC)DSA with partial nonces [@nguyen-shparlinski-2003].&lt;/p&gt;
&lt;p&gt;There is no &quot;a little bias is tolerable&quot; regime: any &lt;em&gt;consistent&lt;/em&gt; partial leak of &lt;code&gt;k&lt;/code&gt;, integrated over enough signatures, provably recovers &lt;code&gt;d&lt;/code&gt;. Two or more leaked bits fall to a lattice; a sub-bit leak falls to Bleichenbacher&apos;s Fourier method [@ladderleak-2020]. &quot;&lt;code&gt;k&lt;/code&gt; is secret, uniform, and unique&quot; is not a best practice -- it is a hard boundary.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The math is safe at the sizes we deploy (the generic gap is closed), and leakage is fatal at any size (the HNP admits no safe margin). Both proofs point the same way: the primitive is not the weak part. The nonce, the parameters, and the negotiation are.&lt;/p&gt;
&lt;/blockquote&gt;

Two clouds sit on the horizon. First, the shared-prime structure Logjam exploited at 512 bits revives at 1024. The Logjam team estimated that precomputing the single most common 1024-bit prime would expose roughly 18% of the top HTTPS sites; a second common prime would reach about 66% of IPsec VPNs and around 26% of SSH servers [@logjam-weakdh-2015] -- the same trick, resized, and a resourced adversary&apos;s project. Second, a large fault-tolerant quantum computer running Shor&apos;s algorithm would break the discrete logarithm outright, curve and finite field alike. That is the one future break that *is* the mathematics, and it is the subject of the companion article, not this one. The deployment answer, already shipping, is hybrid key exchange.
&lt;p&gt;So where, concretely, does EC and DH still break tomorrow?&lt;/p&gt;
&lt;h2&gt;9. Where EC and DH Still Break in the Field&lt;/h2&gt;
&lt;p&gt;The incidents are settled. The &lt;em&gt;classes&lt;/em&gt; are not. Every open problem below is a documented pattern waiting for its next instance, and each one maps to a pillar you have already met.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The next SecureRandom (nonce generation, Pillar 1).&lt;/strong&gt; A signer needs a unique, secret nonce at the exact moment its entropy is weakest: first boot, a headless IoT device with no entropy events, or a freshly cloned virtual machine carrying a duplicated seed. This is the mechanism behind Android 2013 [@android-securerandom-2013], and it recurs whenever hardware is provisioned faster than it can gather randomness.Cloned-VM state duplication is an especially nasty modern form: two hosts that never coordinated can emit the &lt;em&gt;same&lt;/em&gt; &quot;random&quot; nonce because they booted from one snapshot. The signing path can be hedged [@fips-186-5], but key generation and ephemeral key exchange still need a live, well-seeded source.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The next Minerva or LadderLeak (nonce usage, Pillar 1).&lt;/strong&gt; Constant-time scalar multiplication must be re-established against every new microarchitecture -- caches, execution ports, prefetchers, simultaneous multithreading -- and on every fragile incomplete-addition curve. The bit-length leak of Minerva [@minerva-2020] and the sub-bit leak of LadderLeak [@ladderleak-2020] both post-date deterministic nonces precisely because the leak had moved to usage. Formally verified primitives close the &lt;em&gt;known&lt;/em&gt; channels on the &lt;em&gt;chosen&lt;/em&gt; curve; FIPS-mandated P-curves and secp256k1 keep hand-written code in production.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The long tail of shared primes (downgrade, Pillar 3).&lt;/strong&gt; TLS 1.3 closes the web protocol layer, but export-grade and shared standardized primes outlived their policy elsewhere. IKEv1 VPNs and older SSH still ship shared 1024-bit Diffie-Hellman groups, and a single precomputation per shared prime amortizes across every server using it [@logjam-weakdh-2015]. The consensus recommendation is blunt: retire 1024-bit DH now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The next CurveBall (validation, Pillar 2).&lt;/strong&gt; CurveBall was one unchecked generator in one library [@curveball-cve-2020-0601]; the class is &quot;somewhere in a large validation stack, attacker-supplied parameters are trusted.&quot; Every new parser, certificate profile, or post-quantum key format reopens the class, and a single such bug in a widely embedded validator forges trust across everything that depends on it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-quantum migration ahead of Shor (cross-pillar).&lt;/strong&gt; Hybrid key exchange -- running X25519 alongside ML-KEM so the session stays secure if either component holds -- is standardized (RFC 9954 [@rfc9954]; concrete groups such as X25519MLKEM768 in the ongoing ECDHE-MLKEM work [@draft-ietf-tls-ecdhe-mlkem]; ML-KEM standardized as FIPS 203 [@fips-203]) and is already deployed at scale: it protected a majority of the human web traffic Cloudflare measured by late 2025 [@cloudflare-pq-radar]. But the migration carries a trap that has nothing to do with the discrete log.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; ML-KEM is a key-&lt;em&gt;encapsulation&lt;/em&gt; mechanism, not a non-interactive key agreement. It does not reproduce X25519&apos;s static Diffie-Hellman semantics, where two long-term public keys combine without any live exchange. Systems that quietly assume &quot;post-quantum DH&quot; is a drop-in replacement for classical DH will find the shape of the primitive has changed underneath them. Migrate deliberately, not by search-and-replace.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;None of these needs new mathematics to break you. Each needs exactly one deployment mistake. Here is how not to make it.&lt;/p&gt;
&lt;h2&gt;10. What to Do on Monday&lt;/h2&gt;
&lt;p&gt;Every item below maps to a specific incident in this article. Do these, and you do not ship the next one.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Never put a bare runtime RNG in the signing path.&lt;/strong&gt; Use deterministic (RFC 6979 [@rfc6979]) or, on hardware, hedged (FIPS 186-5 [@fips-186-5]) nonces. &lt;em&gt;Prevents:&lt;/em&gt; PS3, Android.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Demand constant-time, exception-free scalar multiplication.&lt;/strong&gt; This is a separate control from the nonce discipline, not a duplicate of it. &lt;em&gt;Prevents:&lt;/em&gt; Minerva, TPM-Fail, LadderLeak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer misuse-resistant primitives.&lt;/strong&gt; Ed25519 for new signing [@rfc8032], X25519 for ephemeral key exchange [@rfc7748]; for static-key ECDH, run full public-key validation per SP 800-56A [@sp-800-56a-r3]. &lt;em&gt;Prevents:&lt;/em&gt; whole classes of implementer error.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reject explicit curve parameters; require named curves.&lt;/strong&gt; Pin the curve, and the generator with it. &lt;em&gt;Prevents:&lt;/em&gt; CurveBall [@curveball-cve-2020-0601].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disable export ciphers and use vetted groups.&lt;/strong&gt; RFC 7919 named groups [@rfc7919] for residual TLS 1.2 DHE, or move to TLS 1.3 [@rfc8446]. &lt;em&gt;Prevents:&lt;/em&gt; Logjam.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never assume a fresh ephemeral rescues a shared prime.&lt;/strong&gt; The number-field-sieve precomputation is per-prime, not per-connection [@logjam-weakdh-2015]. &lt;em&gt;Prevents:&lt;/em&gt; the subtle half of Logjam.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan the post-quantum migration as hybrid, not pure.&lt;/strong&gt; Run a classical curve alongside ML-KEM (RFC 9954 [@rfc9954]); remember a KEM is not a NIKE. &lt;em&gt;Prevents:&lt;/em&gt; harvest-now-decrypt-later exposure.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; New signing design, free to pick the curve: Ed25519. Must use ECDSA on a NIST or secp256k1 curve: hedged RFC 6979 &lt;em&gt;and&lt;/em&gt; a vetted constant-time multiply. Hardware or embedded signer: hedged, never bare-deterministic. Ephemeral key exchange: X25519. Static-key ECDH: full SP 800-56A validation. Certificate or PKI validation: pin named curves. Any TLS: version 1.3, and if you are stuck on 1.2 DHE, RFC 7919 groups with export ciphers off. Long-lived confidentiality: hybrid X25519 with ML-KEM.&lt;/p&gt;
&lt;/blockquote&gt;

1. **Nonce:** For every signature, can you show the nonce is both unpredictable (hedged or deterministic) *and* consumed in constant time? If you can only answer one half, you are exposed to the other.
2. **Parameters:** Does your certificate and key-exchange path reject attacker-supplied explicit parameters and validate every received point against the named curve?
3. **Negotiation:** Can a man-in-the-middle force any weaker group or version than you intend? If you cannot prove &quot;no,&quot; assume &quot;yes.&quot;
&lt;p&gt;Notice that not one item on this list is &quot;use a bigger curve.&quot; For every break except Logjam&apos;s downgrade, key size is irrelevant -- a 384-bit curve reuses a nonce exactly as fatally as a 256-bit one. The controls are about generation, usage, validation, and negotiation, because those are the four contracts, and the contracts are where reality broke.&lt;/p&gt;
&lt;p&gt;Two questions decide whether you got this right, and they happen to be the two most-repeated errors in the field.&lt;/p&gt;
&lt;h2&gt;11. FAQ: Misconceptions, Precisely Corrected&lt;/h2&gt;

Once, and only against a deliberately crippled target. Logjam computed discrete logarithms against a 512-bit export-grade Diffie-Hellman prime -- one that a man-in-the-middle downgrade could select and that many servers had in common [@logjam-weakdh-2015][@cve-2015-4000]. No strong deployed curve -- P-256, Curve25519, secp256k1 -- and no standard-sized group of 2048 bits or more has ever had its discrete log solved in public. The ceiling of the entire corpus is a 512-bit prime that policy left broken.

No, and this is the most common error. A *repeated* nonce recovers the key with certainty from exactly two signatures, by elementary algebra. A merely *biased* nonce emits only a faint statistical signal per signature, so recovery takes many of them plus a dedicated lattice or Fourier attack -- about 2100 for Minerva on secp256r1 [@minerva-2020], roughly 60 for PuTTY&apos;s nine-zero-bit bias [@putty-cve-2024-31497]. One biased signature leaks almost nothing.

No. LadderLeak&apos;s leak is under a single bit per signature, which lattice methods cannot exploit; it revives Bleichenbacher&apos;s Fourier-analysis solver instead [@ladderleak-2020]. The lattice regime is Minerva and TPM-Fail, whose leaks are two bits or more [@minerva-2020][@tpmfail-2020]. Calling all three &quot;lattice attacks&quot; collapses a distinction the attacks themselves are built on.

No. Padding oracles are an RSA and CBC-mode phenomenon; there is no elliptic-curve or Diffie-Hellman padding-oracle exemplar, and you should not go looking to manufacture one. The EC and DH attack surface is exactly the three pillars here -- the nonce, parameter validation, and downgrade. RSA&apos;s padding oracles, and the fact that RSA signing has no per-signature secret nonce to reuse, are covered in the companion piece &quot;How RSA Breaks in Real Life.&quot;

Only against half the problem. RFC 6979 removes the random number generator from the signing path and eliminates the reuse class [@rfc6979], but it does nothing about a timing leak during nonce *usage* -- which is exactly how Minerva, TPM-Fail, and LadderLeak recovered keys from deterministic signers. The complete answer is hedged signing plus constant-time scalar multiplication [@fips-186-5], the two orthogonal cures.

Almost never. The nonce, validation, and downgrade defects are all indifferent to key size: a forged CurveBall certificate is accepted no matter how large the curve is, because the flaw is a skipped generator check rather than a hard curve [@curveball-cve-2020-0601], and a reused or leaked nonce surrenders the private key whether that key is 256 or 384 bits. The one place size mattered was the export-group downgrade, and there the fix was to stop negotiating the 512-bit group at all [@logjam-weakdh-2015], not to enlarge it.

The primitive, no. The discrete logarithm on a strong group remains as hard as it was in the 1970s. What broke, repeatedly, was the deployment around it. Fix the nonce path, validate your parameters against named curves, and retire export and shared groups, and you have closed every break in this article. CurveBall was a validation bug, not a curve break [@curveball-cve-2020-0601] -- and that distinction is the whole point.
&lt;p&gt;Half a century after Diffie and Hellman reduced key exchange to one hard problem, the scoreboard is unambiguous: seven documented field breaks across three deployment layers, and a discrete-log ceiling that only ever reached a deliberately crippled, shared 512-bit export group -- never a strong deployed curve, never a standard-sized group. The mathematics kept its promise. The deployments did not.&lt;/p&gt;

The discrete log never fell. The nonce did, the parameters did, and the negotiation did -- and every one of those is the deployment&apos;s job, not the curve&apos;s.
&lt;p&gt;For the other half of the story -- how a curve itself could be structurally weak, how the number field sieve and Shor&apos;s algorithm actually break the math -- see the structural companion, &quot;How the Discrete Log Would Break.&quot; For the primitive that has no per-signature nonce to reuse and its own &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;padding-oracle history&lt;/a&gt;, see the series sibling, &quot;How RSA Breaks in Real Life.&quot; This article was about the gap between a hard problem and a shipped product. That gap is where the keys keep getting out.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;how-ecc-and-dh-break-in-real-life&quot; keyTerms={[
  { term: &quot;Per-signature nonce (k)&quot;, definition: &quot;The once-used secret in ECDSA that must be secret, uniform, and unique per signature; the load-bearing prerequisite behind PS3, Android, Minerva, TPM-Fail, and LadderLeak.&quot; },
  { term: &quot;Nonce reuse vs. bias&quot;, definition: &quot;A repeated nonce recovers the key with certainty from two signatures; a biased nonce needs a statistical Hidden Number Problem attack over many signatures.&quot; },
  { term: &quot;Hidden Number Problem&quot;, definition: &quot;Recovering a fixed secret from many samples that each leak a few most-significant bits; the framework behind every biased-nonce attack.&quot; },
  { term: &quot;Two-cures fork&quot;, definition: &quot;Determinism and hedging fix nonce generation; constant-time scalar multiplication fixes nonce usage. The two are orthogonal and both are required.&quot; },
  { term: &quot;CurveBall (CVE-2020-0601)&quot;, definition: &quot;A Windows CryptoAPI bug that matched a certificate public point but never checked the generator against the named curve; a validation bug, not a curve break.&quot; },
  { term: &quot;Logjam (CVE-2015-4000)&quot;, definition: &quot;A TLS downgrade to a shared 512-bit export Diffie-Hellman prime, breakable because the number field sieve precomputes once per prime; the only discrete log in the corpus.&quot; },
  { term: &quot;Named curve vs. explicit parameters&quot;, definition: &quot;Referencing standardized domain parameters by identifier versus trusting attacker-supplied inline parameters, including a custom generator.&quot; }
]} questions={[
  { q: &quot;Why does a reused nonce recover the key from only two signatures?&quot;, a: &quot;Two signatures with the same nonce share r; subtracting the signing equations cancels the private-key term, yielding the nonce, and one equation then solves for the key.&quot; },
  { q: &quot;Why is LadderLeak not a lattice attack?&quot;, a: &quot;Its leak is under one bit per signature, which lattices cannot exploit, so it uses Bleichenbacher&apos;s Fourier method instead. Lattices are the Minerva and TPM-Fail regime.&quot; },
  { q: &quot;Is Logjam a counterexample to the thesis?&quot;, a: &quot;No. The only discrete log computed landed on a downgrade-forced, crippled, shared 512-bit prime; no strong or standard-sized group ever fell.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>elliptic-curve-cryptography</category><category>ecdsa</category><category>diffie-hellman</category><category>nonce-reuse</category><category>side-channel-attacks</category><category>tls-security</category><category>post-quantum</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Correct, Constant-Time, and Still Owned: A Field Guide to Side Channels, Faults, and Key Custody</title><link>https://paragmali.com/blog/correct-constant-time-and-still-owned-a-field-guide-to-side-/</link><guid isPermaLink="true">https://paragmali.com/blog/correct-constant-time-and-still-owned-a-field-guide-to-side-/</guid><description>Correct, constant-time crypto still leaks keys through timing, cache, speculation, and fault channels, and deployed systems break most often at key custody.</description><pubDate>Tue, 14 Jul 2026 01:57:15 GMT</pubDate><content:encoded>
**Correct algorithms still leak.** Timing, cache, power, electromagnetic, speculative-execution, and induced-fault channels hand attackers keys that the mathematical proof never modeled -- and constant-time coding closes only the channels you can name. But relative to those exotic breaks, and relative to breaking the ciphers themselves, deployed systems fail most often at the least glamorous layer: key and secret management. Put the root key in hardware, use it without exporting it, envelope-wrap and auto-rotate everything else, and keep no long-lived secret sitting in software.
&lt;h2&gt;1. The Algorithm Was Correct. The Key Left Anyway.&lt;/h2&gt;
&lt;p&gt;In early 2024, researchers pulled private keys out of Apple&apos;s newest laptops -- not from buggy code, but from OpenSSL&apos;s Diffie-Hellman, Go&apos;s RSA, and the freshly standardized post-quantum algorithms &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;CRYSTALS-Kyber and CRYSTALS-Dilithium&lt;/a&gt;, every one of them written to run in constant time [@gofetch-2024]. Twenty-eight years earlier, Paul Kocher had recovered a different private key by doing nothing more exotic than watching how long the math took [@kocher-1996]. Same lesson, two eras: the proof modeled the algorithm; the attacker measured the machine.&lt;/p&gt;
&lt;p&gt;This is the twenty-fifth and final part of a field guide that spent twenty-four installments proving, primitive by primitive, that the mathematics holds. Each of those proofs came with a silent condition attached. RSA is secure &lt;em&gt;if&lt;/em&gt; you blind the exponent. Elliptic-curve signatures are unforgeable &lt;em&gt;if&lt;/em&gt; the nonce is uniform and the scalar multiplication is data-independent. AES-GCM is authenticated &lt;em&gt;if&lt;/em&gt; you never reuse a nonce and you compare tags in constant time. Twenty-four times, the phrase &quot;if the caller does X&quot; carried the whole argument. This part is about what happens when that &quot;if&quot; meets a physical machine and a running organization.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The math held; the machine and the operations handed the secret away. A cryptographic proof reasons about an algorithm -- an input/output relation. An attack happens to an implementation -- a physical, timed, power-drawing, speculating, operated process. The distance between those two things is the entire subject of this article.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That distance has a name, the &lt;em&gt;abstraction gap&lt;/em&gt;, and it opens onto five surfaces where a correct algorithm sheds its secret. The first four are the exotic ones that make headlines: timing and cache channels, power and electromagnetic emanations, microarchitectural and transient-execution leaks, and induced faults. The fifth is the unglamorous one that ends most real incidents: the operational handling of keys and secrets themselves. The first four are where clever attackers earn conference papers. The fifth is where ordinary systems quietly die.&lt;/p&gt;
&lt;p&gt;So this is not an encyclopedia of channels. It is an argument, backed by a catalog: every famous break in the record violates exactly one of four invariants -- a secret touched &lt;em&gt;time&lt;/em&gt;, a secret touched an &lt;em&gt;address&lt;/em&gt;, a &lt;em&gt;fault&lt;/em&gt; skipped a check, or a &lt;em&gt;key&lt;/em&gt; outlived or escaped its custody. Seen through those four invariants, the sprawling zoo of attacks collapses into a map that tells you where to spend your defensive budget.&lt;/p&gt;
&lt;p&gt;To see why a correct algorithm leaks, though, you first have to see the gap the proofs never modeled. And it has a birthday.&lt;/p&gt;
&lt;h2&gt;2. Naming the Gap&lt;/h2&gt;
&lt;p&gt;Ask a cryptographer from 1990 what it means for a cryptosystem to be secure and you would get a clean answer: an adversary who sees only the inputs and outputs -- the ciphertexts, the signatures, the public keys -- cannot do better than guessing. The whole edifice of provable security, the IND-CPA and EUF-CMA definitions this series has used throughout, rests on that framing. The adversary is a mathematical object that submits queries and reads replies. It has no stopwatch, no oscilloscope, and no physical access to the box doing the computing.&lt;/p&gt;
&lt;p&gt;That framing quietly omitted an entire dimension. A real computer does not merely map inputs to outputs. It takes &lt;em&gt;time&lt;/em&gt;. It draws &lt;em&gt;power&lt;/em&gt;. It radiates. It caches. It speculates. It can be made to stumble. None of those behaviors appears in the security definition, which means none of them was ever proven absent. The proof was silent about them not because they were safe, but because the model could not see them.&lt;/p&gt;

Information leaked by a computation&apos;s physical or microarchitectural behavior -- its running time, power draw, electromagnetic emanation, or cache state -- rather than by its defined input/output relation. A side channel is real even when the algorithm is mathematically perfect, because it lives in the machine, not the math.
&lt;p&gt;There was already a Cold-War counterexample sitting in the open literature. In 1985, Wim van Eck published the first unclassified demonstration that a video display&apos;s electromagnetic emanations could be intercepted from a distance and its screen contents reconstructed with modest equipment [@van-eck-1985]. Militaries had worried about &quot;compromising emanations&quot; under the codename TEMPEST for decades; van Eck moved the problem into public view. The lesson was blunt: computation emits exploitable physical signals whether or not the theory chooses to look.Van Eck&apos;s result is not itself a cryptographic attack -- it reconstructs a screen, not a key -- but it is the moment compromising emanations entered the open scientific record, which is why it anchors the side-channel lineage [@van-eck-1985].&lt;/p&gt;

flowchart TD
    A[&quot;Algorithm: an input/output relation the proof reasons about&quot;] --&amp;gt;|realized as| B[&quot;Implementation: a physical, timed, operated process&quot;]
    A --&amp;gt; H[&quot;Output the proof models: ciphertext or signature&quot;]
    B --&amp;gt; C[&quot;Leak channel: time and cache addresses&quot;]
    B --&amp;gt; D[&quot;Leak channel: power and EM&quot;]
    B --&amp;gt; E[&quot;Leak channel: speculation and prefetch&quot;]
    B --&amp;gt; F[&quot;Leak channel: induced faults&quot;]
    B --&amp;gt; G[&quot;Leak channel: key custody and secret handling&quot;]
&lt;h3&gt;The eureka: time is an output&lt;/h3&gt;
&lt;p&gt;The gap got its name in 1996. Paul Kocher, then an independent cryptographer, made a single almost impudent observation: if the running time of a correct RSA or Diffie-Hellman exponentiation depends on the bits of the secret exponent, then merely &lt;em&gt;timing&lt;/em&gt; the operation leaks the key [@kocher-1996]. No implementation bug is required. The code can be a flawless transcription of the textbook algorithm. The leak is in the fact that textbook modular exponentiation does more work for a one bit than for a zero bit.&lt;/p&gt;

The distance between an algorithm -- an input/output relation a proof reasons about -- and an implementation, which is a physical, timed, power-drawing, speculating, operated process. Every side-channel and key-management break in this article is a way of falling through that gap.
&lt;p&gt;The canonical exponentiation loop makes the leak concrete. Square-and-multiply walks the secret exponent bit by bit: it squares at every step, and it performs an &lt;em&gt;extra&lt;/em&gt; multiplication only when the current bit is one. The total running time therefore tracks the number of one bits, and finer measurements over chosen inputs recover the bits individually.&lt;/p&gt;

sequenceDiagram
    participant E as Exponent bit
    participant C as CPU
    participant T as Wall-clock time
    E-&amp;gt;&amp;gt;C: current bit is 0
    C-&amp;gt;&amp;gt;C: square only
    C-&amp;gt;&amp;gt;T: short step
    E-&amp;gt;&amp;gt;C: current bit is 1
    C-&amp;gt;&amp;gt;C: square then multiply
    C-&amp;gt;&amp;gt;T: longer step
    Note over C,T: total time tracks the number of one bits in the secret exponent
&lt;p&gt;Kocher&apos;s insight was not a new algorithm. It was the naming of a missing dimension. And once you accept that wall-clock time is an output the proof forgot to model, the same argument applies to every other physical observable: power, electromagnetic field, cache occupancy, speculative footprint, the very correctness of the result. The defensive program for the next thirty years becomes a single sentence: find each channel and remove the secret&apos;s influence over it.Paul Kocher bookends this whole field. He named the timing channel in 1996, co-invented Differential Power Analysis in 1999, and two decades later was a lead author of Spectre in 2018 [@kocher-1996][@dpa-1999][@spectre-2019]. One researcher&apos;s career traces the arc from a stopwatch to speculative execution.&lt;/p&gt;
&lt;p&gt;That program organizes the rest of this article around those four invariants -- time, address, fault, custody. Hold them in mind. If time was an output the proof ignored, so was every other physical observable, and the attackers were only getting started.&lt;/p&gt;
&lt;h2&gt;3. The First Defenses, and Their First Breaks&lt;/h2&gt;
&lt;p&gt;Here is a fact that ought to feel counterintuitive: within one year of Kocher showing you could learn a key by measuring a &lt;em&gt;correct&lt;/em&gt; machine, two teams showed you did not even need the machine to be correct. You could make it compute &lt;em&gt;wrong&lt;/em&gt; on purpose and read the key out of the error.&lt;/p&gt;
&lt;p&gt;The first generation of defenses was a scramble of point countermeasures, one patch per channel. Against timing, Kocher himself proposed &lt;em&gt;blinding&lt;/em&gt;: before the secret operation, randomize the input so its running time carries no information about the true message. For RSA decryption you pick a random $r$, multiply the ciphertext by $r^e$, decrypt, and divide out $r$. The identity that makes it work is&lt;/p&gt;
&lt;p&gt;$$(m \cdot r^e)^d \equiv m^d \cdot r^{ed} \equiv m^d \cdot r \pmod{n},$$&lt;/p&gt;
&lt;p&gt;so multiplying the result by $r^{-1} \bmod n$ recovers $m^d$ while the timing now depends on the random blind, not the secret. Blinding was a proposal in 1996; it became a default only after David Brumley and Dan Boneh showed in 2003 that an OpenSSL RSA key could be recovered &lt;em&gt;over a network&lt;/em&gt; by timing decryptions, ending the comfortable assumption that timing attacks needed a local probe [@brumley-boneh-2003]. OpenSSL turned blinding on by default in response: a theoretical nicety became a shipped mitigation the moment someone proved the threat was remote.&lt;/p&gt;
&lt;h3&gt;Breaking the machine on purpose&lt;/h3&gt;
&lt;p&gt;While the timing people were blinding, the fault people arrived. In 1997, Dan Boneh, Richard DeMillo, and Richard Lipton at Bellcore showed that a single faulty &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 signature&lt;/a&gt; computed with the Chinese Remainder Theorem optimization lets an attacker factor the modulus with one greatest-common-divisor computation [@bellcore-1997]. The RSA-CRT speedup computes the signature modulo each prime separately and recombines the halves; corrupt just one half-computation -- a voltage glitch, a clock spike, a stray cosmic ray -- and the faulty signature $s&apos;$ comes out right modulo one prime but wrong modulo the other. Raising it to the public exponent should return the message $m$; instead $s&apos;^e - m$ shares exactly one of the two prime factors with $n$, so $\gcd(s&apos;^e - m \bmod n, n)$ hands you that factor. One bad signature, and the private key falls out.&lt;/p&gt;

An attack that induces a computational error -- through a voltage or clock glitch, an electromagnetic or laser pulse, or a software-triggered disturbance like Rowhammer -- so that the faulty output reveals the secret. The Bellcore result is the archetype: one incorrect RSA-CRT signature factors the modulus.
&lt;p&gt;The same year, Eli Biham and Adi Shamir extended the idea from public-key to symmetric ciphers with Differential Fault Analysis: inject faults during a DES computation, difference the correct and faulty ciphertexts, and the secret key emerges [@biham-shamir-1997]. The countermeasures wrote themselves in principle -- &lt;em&gt;verify before you output&lt;/em&gt; a CRT signature, add &lt;em&gt;redundancy&lt;/em&gt; and recompute, sense the glitch and refuse -- but each one added cost and each one had gaps.&lt;/p&gt;
&lt;p&gt;Then, in 1999, Kocher returned with Joshua Jaffe and Benjamin Jun and opened the power rail. Simple and Differential Power Analysis showed that a device&apos;s instantaneous power draw depends on the data it switches, and that correlating power traces across many operations extracts a key even when the per-trace signal is buried in noise [@dpa-1999]. A smartcard running AES or RSA was leaking its key through its power pins to anyone with an oscilloscope and patience.&lt;/p&gt;

A statistical attack that correlates a device&apos;s measured power consumption across many operations with a hypothesized intermediate value, extracting the key even under heavy noise. Its countermeasures are masking (split secrets into random shares) and hiding (flatten or randomize the power signal).
&lt;p&gt;The pattern is the argument. Each attack bred a defense -- blinding, verify-before-output, masking and hiding -- and each defense&apos;s gaps bred the next attack. This is whack-a-mole, and playing it one mole at a time was already visibly losing. Three structural cracks doomed the patch-by-patch approach: the channels were multiplying faster than the patches, each patch protected one primitive rather than the class, and the deepest crack was about to open -- the leak was preparing to jump from the &lt;em&gt;value&lt;/em&gt; of the secret to the &lt;em&gt;addresses&lt;/em&gt; the secret makes the processor touch.&lt;/p&gt;

There is a third way implementations betray correct algorithms that this article names but does not re-derive: the [random-number generator](/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/). The 2008 Debian OpenSSL disaster (a commented-out line collapsed key entropy to a few thousand possibilities) [@debian-openssl-2008], the 2010 PlayStation 3 ECDSA break (a reused signing nonce) [@ps3-ecdsa-2010], and 2017&apos;s ROCA (a structured-prime library flaw) [@roca-2017] are all implementation and operational failures, not broken math. They belong to the randomness and [nonce-reuse](/blog/one-number-used-twice-how-a-repeated-nonce-hands-over-your-p/) story told in Parts 2 and 8 of this series. Keep them in view as a fourth failure family; we return to their operational cousin -- key custody -- as this article&apos;s payoff.
&lt;h2&gt;4. The Channels Multiply&lt;/h2&gt;
&lt;p&gt;The next twenty years were a single sentence repeated in new dialects: &lt;em&gt;the secret leaked through a channel the proof never modeled&lt;/em&gt;. What changed each time was the channel. Here is the whole family tree, and the timeline that carries it.&lt;/p&gt;

timeline
    title Thirty years of the abstraction gap
    1985 : van Eck EM emanations
    1996 : Kocher timing attack
    1997 : Bellcore RSA-CRT fault : Biham-Shamir DFA
    1999 : Kocher-Jaffe-Jun DPA
    2003 : Brumley-Boneh remote timing
    2005 : Bernstein AES cache : Percival hyperthreading
    2013 : Lucky Thirteen breaks TLS
    2014 : Yarom-Falkner Flush and Reload : Rowhammer
    2018 : Meltdown and Spectre
    2020 : Minerva : TPM-Fail
    2022 : Hertzbleed via DVFS
    2024 : GoFetch : KyberSlash
&lt;h3&gt;From the value to the address&lt;/h3&gt;
&lt;p&gt;The deepest move in the whole story is the one that happened between 2005 and 2014: the leak stopped being about the &lt;em&gt;value&lt;/em&gt; of the secret and became about the &lt;em&gt;addresses&lt;/em&gt; the secret makes the processor touch. Daniel Bernstein&apos;s 2005 cache-timing attack on AES showed that software using secret-dependent table lookups -- the standard fast AES implementation, indexing precomputed T-tables by key-dependent bytes -- leaks the key through cache timing, because which table entries end up cached depends on which ones the victim accessed [@bernstein-2005].&lt;/p&gt;
&lt;p&gt;The same year, Colin Percival showed that Intel Hyper-Threading&apos;s shared L1 cache lets one thread spy on a co-resident thread&apos;s access pattern, recovering RSA key bits from a concurrent OpenSSL process [@percival-2005]. In 2006, Dag Arne Osvik, Adi Shamir, and Eran Tromer named and formalized the reusable primitives the field still uses [@osvik-shamir-tromer-2006]: Prime+Probe fills (primes) a cache set with the attacker&apos;s own lines, lets the victim run, then re-accesses (probes) those lines and times its own reload -- a slow reload reveals the victim evicted a line, so it needs no shared memory, while Evict+Time measures the victim directly.&lt;/p&gt;

A leak that recovers which memory addresses a victim touched by measuring shared-cache timing -- a hit is fast, a miss is slow. Flush+Reload is the sharpest variant: an attacker flushes a shared line, waits, then times the reload to learn whether the victim accessed it in the interval.
&lt;p&gt;The technique reached its high-resolution form in 2014, when Yuval Yarom and Katrina Falkner delivered Flush+Reload: flush a shared cache line with &lt;code&gt;clflush&lt;/code&gt;, wait, then time the reload to learn whether the victim touched it, all through the last-level cache shared across cores [@flush-reload-2014]. It recovered GnuPG RSA keys across cores and became the workhorse primitive behind the transient-execution attacks that followed. The countermeasure had crystallized into a principle, not a patch: never let a secret decide which address you touch.&lt;/p&gt;
&lt;h3&gt;From local to remote, and a flaw in the spec&lt;/h3&gt;
&lt;p&gt;Parallel to the address story ran the reach story. Brumley and Boneh had already made timing a network threat in 2003. Then, in 2013, Nadhem AlFardan and Kenneth Paterson published &lt;a href=&quot;https://paragmali.com/blog/they-read-your-plaintext-without-breaking-your-cipher-a-fiel/&quot; rel=&quot;noopener&quot;&gt;Lucky Thirteen&lt;/a&gt;, and it stung in a new way: the non-constant-time MAC-and-padding check in TLS and DTLS CBC-mode record processing is a timing oracle, and a man-in-the-middle can recover plaintext from it [@lucky13-2013]. The wound was not one library&apos;s bug. The TLS &lt;em&gt;specification&lt;/em&gt; mandated a MAC-then-pad-then-encrypt construction whose safe verification is genuinely hard to do in constant time. This was a spec-level flaw -- a reminder that &quot;write it in constant time&quot; is sometimes fighting the protocol design itself.&lt;/p&gt;
&lt;h3&gt;Below the instruction set&lt;/h3&gt;
&lt;p&gt;For a decade the discipline looked like it might be enough. Then, on 3 January 2018, the floor gave way. Meltdown and Spectre showed that &lt;em&gt;transient execution&lt;/em&gt; -- the out-of-order and speculative machinery every fast CPU uses -- runs secret-dependent code paths that the architectural program never takes, and leaks their traces through the cache [@meltdown-2018][@spectre-2019][@projectzero-2018]. Meltdown let an unprivileged process transiently read kernel memory. Spectre poisoned branch prediction so that even &lt;em&gt;correct, constant-time&lt;/em&gt; code speculatively executed a secret-dependent path and left it in the cache for a Flush+Reload probe to recover.&lt;/p&gt;

Instructions a CPU runs speculatively or out of order and then discards architecturally, so they never affect the program&apos;s official result -- but whose microarchitectural traces, chiefly cache state, survive and leak. Spectre and Meltdown proved that constant-time source code can be defeated by what the silicon does beneath it.
&lt;p&gt;This was the qualitative break. Constant-time is a property of the instruction stream the programmer wrote. Speculation executes an instruction stream the programmer did &lt;em&gt;not&lt;/em&gt; write, chosen by a branch predictor the attacker can train. The abstraction did not spring a leak; it was breached from underneath.Minerva (2020) is the miniature of the whole pattern: many ECDSA implementations leak the bit-length of the per-signature nonce during a non-constant-time scalar multiplication. Gather a couple thousand signatures, feed the tiny leaks to a lattice attack, and the long-term key falls out. On an Athena IDProtect smartcard the team needed about 2,100 signatures and recovered a P-256 key in minutes [@minerva-2020].&lt;/p&gt;
&lt;h3&gt;The machine keeps inventing channels&lt;/h3&gt;
&lt;p&gt;If Spectre broke constant-time from below through speculation, the next wave broke it from the side, through physics the instruction set never promised to hold still. In 2022, Yingchen Wang, Riccardo Paccagnella, and colleagues published Hertzbleed: modern x86 dynamic voltage and frequency scaling makes the CPU&apos;s clock frequency depend on the data being processed, so a program&apos;s wall-clock time depends on its data values -- turning a power side channel into a &lt;em&gt;remote timing&lt;/em&gt; channel with no power measurement at all [@hertzbleed-2022]. They extracted a full key from a constant-time implementation of the post-quantum candidate SIKE, remotely.SIKE died twice. Hertzbleed broke its constant-time &lt;em&gt;implementation&lt;/em&gt; in 2022 through frequency scaling [@hertzbleed-2022]. Weeks later, Wouter Castryck and Thomas Decru broke the &lt;em&gt;mathematics&lt;/em&gt; of SIDH outright with a classical key-recovery attack, and SIKE was withdrawn [@castryck-decru-2023]. The math break is out of this article&apos;s implementation scope, but the coincidence is a useful reminder that a primitive can fall from either direction.&lt;/p&gt;
&lt;p&gt;Then, in 2024, came the break that opens this article. Apple&apos;s M-series CPUs contain a Data Memory-dependent Prefetcher (DMP) that inspects values loaded from memory and, when one &quot;looks like&quot; a pointer, dereferences it speculatively to prefetch what it points to [@gofetch-2024]. That behavior is precisely the data-to-address coupling constant-time forbids. By crafting chosen inputs so that an intermediate value looks like a pointer only when a guess about a key bit is correct, the GoFetch team achieved end-to-end key extraction from constant-time OpenSSL Diffie-Hellman, Go RSA, and -- the part that should worry every protocol designer -- the post-quantum CRYSTALS-Kyber and CRYSTALS-Dilithium.&lt;/p&gt;

sequenceDiagram
    participant A as Attacker
    participant M as Victim memory
    participant DMP as Data-memory prefetcher
    participant Cache as Cache state
    A-&amp;gt;&amp;gt;M: craft input so a value looks pointer-like only when the key-bit guess is right
    M-&amp;gt;&amp;gt;DMP: value flows through a constant-time load
    DMP-&amp;gt;&amp;gt;DMP: value resembles a pointer, so dereference it
    DMP-&amp;gt;&amp;gt;Cache: prefetch pulls the pointed-to line into the cache
    A-&amp;gt;&amp;gt;Cache: time the reload to confirm the guess
    Note over A,Cache: data influenced an address, the exact coupling constant-time forbids
&lt;p&gt;The same year, KyberSlash showed the discipline is unfinished in a humbler way. Several Kyber implementations contained a single line that divides a &lt;em&gt;secret&lt;/em&gt; numerator by a &lt;em&gt;public&lt;/em&gt; denominator; because integer division takes a data-dependent number of cycles on many CPUs, that one statement leaks ML-KEM secret bits into timing, sometimes exploitably [@kyberslash-2024]. Not an exotic prefetcher -- just a division, in a freshly standardized algorithm, that no one flagged.&lt;/p&gt;
&lt;h3&gt;The fault lineage, in parallel&lt;/h3&gt;
&lt;p&gt;Running alongside the leakage story the whole time was the fault story, and it too grew more remote. Classical fault injection meant physically glitching a chip with voltage, clock, electromagnetic, or laser pulses. In 2014, Rowhammer changed the threat model: repeatedly activating one DRAM row flips bits in physically adjacent rows &lt;em&gt;without accessing them&lt;/em&gt;, a hardware fault triggered entirely in software [@rowhammer-2014]. Plundervolt then showed you could undervolt an Intel CPU from software to induce faults inside an SGX enclave, corrupting cryptographic computations from the operating system [@plundervolt-2019]. The fault, like the leak, had escaped the need for physical possession.&lt;/p&gt;
&lt;h3&gt;One lens for the whole catalog&lt;/h3&gt;
&lt;p&gt;Set every named break side by side and re-read each as exactly one violated invariant. The zoo becomes a table.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Break (year)&lt;/th&gt;
&lt;th&gt;Surface / channel&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Violated invariant&lt;/th&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Kocher timing (1996) [@kocher-1996]&lt;/td&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;Secret-dependent modular-exponentiation time&lt;/td&gt;
&lt;td&gt;secret touched time&lt;/td&gt;
&lt;td&gt;time is an unmodeled output; blind the operand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bellcore RSA-CRT (1997) [@bellcore-1997]&lt;/td&gt;
&lt;td&gt;Fault&lt;/td&gt;
&lt;td&gt;One faulty CRT signature, then a single GCD factors n&lt;/td&gt;
&lt;td&gt;fault skipped a check&lt;/td&gt;
&lt;td&gt;verify before output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DFA (1997) [@biham-shamir-1997]&lt;/td&gt;
&lt;td&gt;Fault&lt;/td&gt;
&lt;td&gt;Fault plus differencing recovers the DES key&lt;/td&gt;
&lt;td&gt;fault skipped a check&lt;/td&gt;
&lt;td&gt;redundancy and verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DPA (1999) [@dpa-1999]&lt;/td&gt;
&lt;td&gt;Power&lt;/td&gt;
&lt;td&gt;Data-dependent power draw over many traces&lt;/td&gt;
&lt;td&gt;secret touched power&lt;/td&gt;
&lt;td&gt;masking and hiding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Brumley-Boneh (2003) [@brumley-boneh-2003]&lt;/td&gt;
&lt;td&gt;Remote timing&lt;/td&gt;
&lt;td&gt;OpenSSL RSA decryption timing over a network&lt;/td&gt;
&lt;td&gt;secret touched time, remotely&lt;/td&gt;
&lt;td&gt;blinding on by default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bernstein AES cache (2005) [@bernstein-2005]&lt;/td&gt;
&lt;td&gt;Cache&lt;/td&gt;
&lt;td&gt;Secret-indexed T-table lookups&lt;/td&gt;
&lt;td&gt;secret touched an address&lt;/td&gt;
&lt;td&gt;constant-time, AES-NI, bitslicing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Percival (2005) [@percival-2005]&lt;/td&gt;
&lt;td&gt;Cache under SMT&lt;/td&gt;
&lt;td&gt;Shared L1 under Hyper-Threading&lt;/td&gt;
&lt;td&gt;secret touched an address&lt;/td&gt;
&lt;td&gt;co-residency threat model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lucky Thirteen (2013) [@lucky13-2013]&lt;/td&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;Non-constant-time MAC-and-pad check in TLS CBC&lt;/td&gt;
&lt;td&gt;secret touched time&lt;/td&gt;
&lt;td&gt;constant-time MAC verification, a spec fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flush+Reload (2014) [@flush-reload-2014]&lt;/td&gt;
&lt;td&gt;Cache&lt;/td&gt;
&lt;td&gt;Flush plus reload on shared last-level cache&lt;/td&gt;
&lt;td&gt;secret touched an address&lt;/td&gt;
&lt;td&gt;high-resolution shared-cache primitive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rowhammer (2014) [@rowhammer-2014]&lt;/td&gt;
&lt;td&gt;Fault, software-triggered&lt;/td&gt;
&lt;td&gt;DRAM disturbance flips adjacent-row bits&lt;/td&gt;
&lt;td&gt;fault skipped a check, no physical access&lt;/td&gt;
&lt;td&gt;software can induce hardware faults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meltdown and Spectre (2018) [@meltdown-2018][@spectre-2019]&lt;/td&gt;
&lt;td&gt;Transient execution&lt;/td&gt;
&lt;td&gt;Speculation runs secret-dependent paths, leaks via cache&lt;/td&gt;
&lt;td&gt;secret touched an address below the ISA&lt;/td&gt;
&lt;td&gt;constant-time broken beneath the ISA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minerva (2020) [@minerva-2020]&lt;/td&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;Non-constant-time ECDSA nonce bit-length plus lattice&lt;/td&gt;
&lt;td&gt;secret touched time&lt;/td&gt;
&lt;td&gt;constant-time scalar multiplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TPM-Fail (2020) [@tpm-fail-2020]&lt;/td&gt;
&lt;td&gt;Timing&lt;/td&gt;
&lt;td&gt;Secret-dependent ECDSA time in a certified TPM&lt;/td&gt;
&lt;td&gt;secret touched time&lt;/td&gt;
&lt;td&gt;certified is not side-channel-safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hertzbleed (2022) [@hertzbleed-2022]&lt;/td&gt;
&lt;td&gt;Power to remote timing via DVFS&lt;/td&gt;
&lt;td&gt;Frequency scaling makes wall-time data-dependent&lt;/td&gt;
&lt;td&gt;secret touched time, remotely&lt;/td&gt;
&lt;td&gt;power channels can go remote&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GoFetch (2024) [@gofetch-2024]&lt;/td&gt;
&lt;td&gt;Cache via DMP prefetcher&lt;/td&gt;
&lt;td&gt;Prefetcher dereferences pointer-like data&lt;/td&gt;
&lt;td&gt;secret touched an address below the ISA&lt;/td&gt;
&lt;td&gt;constant-time unfinished, reaches PQC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KyberSlash (2024) [@kyberslash-2024]&lt;/td&gt;
&lt;td&gt;Timing via division&lt;/td&gt;
&lt;td&gt;Secret numerator over public denominator, variable-time divide&lt;/td&gt;
&lt;td&gt;secret touched time&lt;/td&gt;
&lt;td&gt;constant-time incompletely applied to ML-KEM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storm-0558 (2023) [@csrb-storm0558]&lt;/td&gt;
&lt;td&gt;Operational key custody&lt;/td&gt;
&lt;td&gt;Unrotated, software-resident 2016 signing key&lt;/td&gt;
&lt;td&gt;a key outlived and escaped its custody&lt;/td&gt;
&lt;td&gt;custody, not math&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; Every row above reduces to one of four sentences. A secret touched &lt;em&gt;time&lt;/em&gt;. A secret touched an &lt;em&gt;address&lt;/em&gt;. A &lt;em&gt;fault&lt;/em&gt; skipped a check. A &lt;em&gt;key&lt;/em&gt; outlived or escaped its custody. This is not a mnemonic; it is the map. Each invariant names both the break and the defense that closes it, and the last one -- custody -- is where the whole article is heading.&lt;/p&gt;
&lt;/blockquote&gt;

You can only close the channels you model, and the machine keeps inventing new ones.
&lt;p&gt;Read the catalog through those four invariants and the pattern snaps into focus. Three of the four -- time, address, fault -- are the exotic surfaces, and they share one defense strategy. The fourth is different in kind, and it is where deployed systems actually break. That split is the breakthrough.&lt;/p&gt;
&lt;h2&gt;5. Two Breakthroughs That Arrive Together&lt;/h2&gt;
&lt;p&gt;After a decade of one-patch-per-channel, the field found the single rule that closes an entire family of channels at the source. And in almost the same breath, it learned the rule was necessary but not sufficient -- which forced a second, operational breakthrough to sit beside the first. The two arrive together because each is the other&apos;s admission of limits.&lt;/p&gt;
&lt;h3&gt;The software breakthrough: constant-time as a discipline&lt;/h3&gt;
&lt;p&gt;State it precisely, because the precision is the whole point. Constant-time code has &lt;em&gt;no secret-dependent branches, no secret-dependent memory indices, and no data-dependent variable-time instructions&lt;/em&gt;. That is it. Crucially -- and this is the single most misunderstood idea in the field -- constant-time does not mean &lt;em&gt;fixed-duration&lt;/em&gt; execution. It means &lt;em&gt;data-independent&lt;/em&gt; execution: the same instruction stream and the same sequence of memory addresses regardless of the secret&apos;s value. A routine can finish in wildly different absolute times on different inputs and still be constant-time, so long as the variation never depends on a secret.&lt;/p&gt;

Code whose instruction sequence and memory-address trace do not depend on secret values. It is NOT fixed-duration execution. The rule has three clauses: no branch on a secret, no memory index computed from a secret, and no variable-latency instruction (early-exit compare, data-dependent division) applied to a secret.
&lt;p&gt;In practice the discipline replaces three forbidden patterns. A secret-dependent &lt;code&gt;if&lt;/code&gt; becomes a branchless select computed with masks and arithmetic. A secret-indexed table lookup, &lt;code&gt;table[secret]&lt;/code&gt;, becomes a full scan of the table with a constant-time select, or is dissolved entirely by &lt;em&gt;bitslicing&lt;/em&gt; the algorithm into boolean operations that touch no data-dependent address [@bernstein-2005]. A variable-time operation on a secret -- an early-exit &lt;code&gt;memcmp&lt;/code&gt; on a MAC, a division with a secret numerator -- is banned outright. Whole primitives were redesigned to make the discipline natural: &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;Curve25519&lt;/a&gt;&apos;s Montgomery ladder runs the identical steps for every bit of the scalar, so there is no secret-dependent path left to leak.&lt;/p&gt;
&lt;p&gt;The comparison case is worth seeing in code, because it is the exact mistake behind Lucky Thirteen and a hundred MAC-verification bugs.&lt;/p&gt;
&lt;p&gt;{`
// Early-exit compare: returns as soon as two bytes differ.
// The iteration count reveals how long a prefix matched -- a timing leak.
function earlyExitEqual(a, b) {
  if (a.length !== b.length) return false;
  for (let i = 0; i &amp;lt; a.length; i++) {
    if (a[i] !== b[i]) return false;   // stops at the first difference
  }
  return true;
}&lt;/p&gt;
&lt;p&gt;// Constant-time compare: always scans every byte, accumulating differences.
// Running time depends only on length, never on the secret contents.
function constantTimeEqual(a, b) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i &amp;lt; a.length; i++) {
    diff |= a[i] ^ b[i];               // no early exit, no branch on the data
  }
  return diff === 0;
}&lt;/p&gt;
&lt;p&gt;const tag      = [1, 2, 3, 4, 5, 6, 7, 8];
const nearMiss = [1, 2, 3, 4, 5, 6, 7, 9]; // matches 7 of 8 bytes
const farMiss  = [9, 2, 3, 4, 5, 6, 7, 8]; // differs at the very first byte
// earlyExitEqual iterates 8 times for nearMiss but only once for farMiss:
// that difference is the leak an attacker times to forge a tag byte by byte.
console.log(&apos;constant-time, near miss:&apos;, constantTimeEqual(tag, nearMiss));
console.log(&apos;constant-time, far miss :&apos;, constantTimeEqual(tag, farMiss));
`}&lt;/p&gt;
&lt;p&gt;The other half of the breakthrough is that the property became &lt;em&gt;checkable&lt;/em&gt;, and even &lt;em&gt;provable&lt;/em&gt;. Oscar Reparaz and colleagues built dudect, which runs a routine on fixed-versus-random secret inputs and applies a Welch t-test: a stable t-statistic past the usual threshold flags a data-dependent timing leak, while a genuinely constant-time routine stays flat [@dudect-2017]. On the formal side, ct-verif encodes constant-timeness as a property over pairs of executions differing only in secrets and discharges it mechanically [@ct-verif-2016]. Constant-time stopped being a matter of the author&apos;s vigilance and became something a machine could test in CI or prove outright.&lt;/p&gt;
&lt;p&gt;For the physical surface -- power, electromagnetic, glitch -- the corresponding discipline is masking, and it too became principled rather than ad hoc.&lt;/p&gt;

Masking splits every secret intermediate into $d+1$ random shares so that any $d$ probed values are statistically independent of the secret; the computation runs on shares. Threshold implementations add non-completeness and uniformity so the construction stays secure even under hardware glitches, which transiently leak the unmasked value [@threshold-impl-2006].
&lt;p&gt;But masking closes only the physical channel, and constant-time closes only the modeled ISA-level channels. Spectre, Hertzbleed, GoFetch, and KyberSlash all live &lt;em&gt;below&lt;/em&gt; the abstraction the discipline reasons about. You can be perfectly constant-time in the instruction stream you wrote and still leak, because the silicon runs an instruction stream you did not write. That is the wall the software breakthrough hits -- and it is the reason for the second breakthrough.&lt;/p&gt;
&lt;h3&gt;The operational breakthrough: key custody as an architecture&lt;/h3&gt;
&lt;p&gt;If you can never prove the &lt;em&gt;whole machine&lt;/em&gt; leak-free, stop trying to eliminate leakage and instead bound what a leak can cost. That is the pivot. Concretely: put the root key inside a hardware boundary that generates and &lt;em&gt;uses&lt;/em&gt; it without ever exporting it, and reach your data through an envelope.&lt;/p&gt;

A tamper-resistant boundary -- a dedicated appliance, a cloud Managed HSM, a TPM, or an embedded secure element -- that generates and uses keys without releasing them in plaintext. Software sends in a request (sign this, unwrap that) and gets back a result, never the key bytes. FIPS 140-3 validates such modules, including some non-invasive side-channel requirements.

A pattern in which a cheap Data-Encryption Key (DEK) encrypts the bulk data, and a Key-Encryption Key (KEK) that never leaves the HSM or KMS wraps the DEK. The wrapped DEK is stored right beside the ciphertext; only the KMS can unwrap it. Standardized authenticated key wrapping (NIST SP 800-38F) supplies the wrap operation [@nist-sp800-38f].
&lt;p&gt;The economics are what make it deployable. A hardware module is slow and expensive, so you do not run all your data through it -- you run one small wrap and unwrap through it, and let a cheap symmetric DEK do the bulk work. A leaked DEK exposes only the data it wrapped; the KEK, and therefore every other DEK, stays safe inside the boundary. Rotation becomes automatic and its blast radius bounded. This is the move from &quot;make the code not leak&quot; to &quot;make a leak not matter,&quot; and it maps directly onto how NIST SP 800-57 tells you to think about key lifecycles and crypto-periods [@nist-sp800-57].&lt;/p&gt;

flowchart LR
    D[&quot;Plaintext data&quot;] --&amp;gt;|&quot;encrypt with&quot;| DEK[&quot;Data-Encryption Key (DEK)&quot;]
    DEK --&amp;gt;|&quot;wrap with KEK&quot;| WD[&quot;Wrapped DEK, stored beside ciphertext&quot;]
    KEK[&quot;Key-Encryption Key (KEK)&quot;] --&amp;gt; HSM[&quot;Inside HSM or cloud KMS, never exported&quot;]
    WD -.-&amp;gt;|&quot;ask KMS to unwrap&quot;| HSM
    HSM -.-&amp;gt;|&quot;plaintext DEK, policy permitting&quot;| App[&quot;Application decrypts the data&quot;]
&lt;h3&gt;The junction&lt;/h3&gt;
&lt;p&gt;The two breakthroughs are two chains of the same story, and they meet at one move. Chain A drives the code toward not leaking, generation by generation, until it hits the microarchitectural wall. Chain B accepts that wall and shrinks what any residual leak is worth. The thesis of this whole series is the junction of the two: the math held; Chain A closes the channels you can model, and Chain B bounds the cost of the ones you cannot.&lt;/p&gt;

flowchart TD
    A0[&quot;A0: correctness is enough&quot;] --&amp;gt; A1[&quot;A1: point patches, blinding, verify, masking&quot;]
    A1 --&amp;gt; A2[&quot;A2: constant-time discipline&quot;]
    A2 --&amp;gt; A3[&quot;A3: verified plus hardware-assisted constant-time&quot;]
    B0[&quot;B0: keys as data, hardcoded or in config&quot;] --&amp;gt; B1[&quot;B1: OS or software keystore&quot;]
    B1 --&amp;gt; B2[&quot;B2: TPM, HSM, secure element&quot;]
    B2 --&amp;gt; B3[&quot;B3: cloud KMS, envelope, automatic rotation&quot;]
    A3 --&amp;gt; T[&quot;Thesis: the math held. Close the modeled channels, bound the cost of the rest&quot;]
    B3 --&amp;gt; T
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Defense evolves from &quot;make the code not leak&quot; to &quot;make a leak not matter.&quot; The first is Chain A and it is necessary. The second is Chain B and it is what saves you when Chain A inevitably has a gap the machine invented last week.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Both breakthroughs are engineering realities you can deploy today: verified constant-time libraries on one side, cloud KMS with envelope encryption and automatic rotation on the other. So what does the 2024 to 2026 state of the art actually look like when you ship both at once -- and where is it still losing?&lt;/p&gt;
&lt;h2&gt;6. The State of the Art, 2024 to 2026&lt;/h2&gt;
&lt;p&gt;There is no single best implementation. There are two parallel frontiers, and the 2024-to-2026 practitioner ships both at once. Call them Chain A, leakage defense, and Chain B, key custody.&lt;/p&gt;
&lt;h3&gt;Chain A: the leakage-defense frontier&lt;/h3&gt;
&lt;p&gt;At the base sits the constant-time-by-construction library. libsodium, Google&apos;s BoringSSL, Thomas Pornin&apos;s BearSSL, and Rust&apos;s ring obey the three-clause rule everywhere a secret is touched. This is table stakes now, not a differentiator -- if a general-purpose crypto library is not constant-time by default in 2026, it is not a serious library.&lt;/p&gt;
&lt;p&gt;Above that sits the highest-assurance floor: &lt;em&gt;formally verified&lt;/em&gt; constant-time crypto, where a machine-checked proof replaces human review.&lt;/p&gt;

Cryptographic code whose secret-independence is established by a machine-checked mathematical proof, not by review or testing. HACL* and EverCrypt (written in a subset of F*) and the Jasmin and libjade toolchain (with EasyCrypt) are the two production lineages; the proof itself is the constant-time argument, discharged once and re-checked automatically [@hacl-star].
&lt;p&gt;This is not a laboratory curiosity. HACL* ships verified Curve25519 in Mozilla Firefox through NSS, and a formally verified Curve25519 rode into the Linux kernel alongside WireGuard; verified SHA-3 and HMAC from the same project ship inside CPython [@hacl-star]. Proof-carrying crypto is in the browser and the operating system you are reading this on.&lt;/p&gt;
&lt;p&gt;Verifying and testing the property is its own layered toolchain, and no single tool dominates. A 2023 systematic evaluation quantified the trade-offs across real libraries [@sidetool-eval-2023]; the shape of the field is a ladder from cheap-and-empirical to expensive-and-proven.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Technique&lt;/th&gt;
&lt;th&gt;Level analyzed&lt;/th&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;th&gt;Catches compiler-introduced leaks&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;th&gt;Microarchitecture&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;dudect [@dudect-2017]&lt;/td&gt;
&lt;td&gt;Welch t-test on timing&lt;/td&gt;
&lt;td&gt;Black-box, runtime&lt;/td&gt;
&lt;td&gt;Empirical, tested paths&lt;/td&gt;
&lt;td&gt;Only if exercised&lt;/td&gt;
&lt;td&gt;Very low, CI drop-in&lt;/td&gt;
&lt;td&gt;Not modeled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ctgrind / TIMECOP&lt;/td&gt;
&lt;td&gt;Dynamic taint over Valgrind&lt;/td&gt;
&lt;td&gt;Binary, dynamic&lt;/td&gt;
&lt;td&gt;Empirical, executed paths&lt;/td&gt;
&lt;td&gt;Yes, when observed&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Not modeled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ct-verif [@ct-verif-2016]&lt;/td&gt;
&lt;td&gt;Formal self-composition&lt;/td&gt;
&lt;td&gt;LLVM IR&lt;/td&gt;
&lt;td&gt;All paths at the IR&lt;/td&gt;
&lt;td&gt;No, runs before compilation&lt;/td&gt;
&lt;td&gt;Medium, needs annotations&lt;/td&gt;
&lt;td&gt;Not modeled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binsec/Rel [@binsec-rel]&lt;/td&gt;
&lt;td&gt;Relational symbolic execution&lt;/td&gt;
&lt;td&gt;Final binary&lt;/td&gt;
&lt;td&gt;All paths, bounded depth&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High, setup and path explosion&lt;/td&gt;
&lt;td&gt;Not modeled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HACL* / Jasmin [@hacl-star]&lt;/td&gt;
&lt;td&gt;Proof in F* or EasyCrypt&lt;/td&gt;
&lt;td&gt;Source down to C or assembly&lt;/td&gt;
&lt;td&gt;Whole-program theorem&lt;/td&gt;
&lt;td&gt;Verified assembly, yes&lt;/td&gt;
&lt;td&gt;Very high, proof engineering&lt;/td&gt;
&lt;td&gt;Not modeled&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read the last column: every tool shares the same blind spot. None models the microarchitecture, because none can -- a limit we return to in Section 8. The empirical tools like dudect are the ones you actually gate CI with, and the Welch t-test at their heart is simple enough to see in a few lines.&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;// Simulate timing samples for two input classes: a &quot;fixed&quot; secret and &quot;random&quot; secrets. // A variable-time operation reacts differently to the two classes; a constant-time one does not. function mean(xs) { return xs.reduce((s, x) =&amp;gt; s + x, 0) / xs.length; } function variance(xs, m) {   return xs.reduce((s, x) =&amp;gt; s + (x - m) * (x - m), 0) / (xs.length - 1); } // Welch t-statistic for two samples with unequal variance. function welchT(a, b) {   const ma = mean(a), mb = mean(b);   const va = variance(a, ma), vb = variance(b, mb);   return (ma - mb) / Math.sqrt(va / a.length + vb / b.length); } function samples(n, base, classShift) {   const out = [];   for (let i = 0; i &amp;lt; n; i++) out.push(base + Math.random() * 5 + classShift);   return out; } const N = 5000; // Variable-time op: the random class runs a few units longer on average, a real leak. const leakFixed = samples(N, 100, 0), leakRandom = samples(N, 100, 8); // Constant-time op: both classes share one distribution, no leak. const ctFixed = samples(N, 100, 0), ctRandom = samples(N, 100, 0); console.log(&apos;variable-time absolute t:&apos;, Math.abs(welchT(leakFixed, leakRandom)).toFixed(1), &apos;past 4.5 threshold, flagged&apos;); console.log(&apos;constant-time absolute t:&apos;, Math.abs(welchT(ctFixed, ctRandom)).toFixed(1), &apos;below 4.5, clean&apos;);&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;Beneath the software sits hardware backing. Intel&apos;s Data Operand Independent Timing Mode (DOITM) and Arm&apos;s Data Independent Timing (DIT) bit promise that an enumerated set of instructions runs with data-independent latency, re-establishing at the silicon level the guarantee the source could only assert [@intel-doitm][@arm-dit]. But their coverage is exactly their limit. The lists are vendor-specific and enumerated, and they pointedly exclude integer and floating-point division and square root -- which is why KyberSlash&apos;s secret division leaks -- and they do not model the prefetcher or frequency scaling, which is why GoFetch and Hertzbleed operate outside their guarantee entirely [@kyberslash-2024][@gofetch-2024].&lt;/p&gt;
&lt;p&gt;For the physical surface -- power, electromagnetic, glitch -- masking and threshold implementations plus on-die fault sensors remain the answer, and the live research frontier is masking the post-quantum algorithms at practical cost.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Reach for a verified or constant-time-first library -- libsodium, BoringSSL, BearSSL, ring, or the verified HACL* and libcrux -- and gate it in CI with dudect or TIMECOP [@hacl-star][@dudect-2017]. Home-rolled constant-time code is wrong until a tool says otherwise, and even expert-written code needs re-checking after every compiler upgrade, because optimizers reintroduce the branches you removed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Chain B: the key-custody frontier&lt;/h3&gt;
&lt;p&gt;The operational state of the art is cloud KMS plus envelope encryption plus automatic rotation. AWS KMS, Azure Key Vault and Managed HSM, and Google Cloud KMS all keep a small number of Key-Encryption Keys inside a hardware boundary and wrap an unbounded number of cheap Data-Encryption Keys under them, using the authenticated key-wrapping of NIST SP 800-38F and the lifecycle discipline of NIST SP 800-57 [@nist-sp800-38f][@nist-sp800-57].AWS puts a concrete number on the crypto-period: keys that AWS manages rotate on a required schedule of every year, approximately 365 days, and customer-managed keys support optional automatic rotation on a period you choose [@aws-kms]. Rotation is O(1) to request; the hard part is re-wrapping and inventorying what each key protects.&lt;/p&gt;
&lt;p&gt;For root keys -- certificate authorities, code-signing, payment keys -- the anchor is a FIPS 140-3 validated module.&lt;/p&gt;

The current U.S. standard for validating cryptographic modules, superseding FIPS 140-2. Unlike its predecessor&apos;s reputation, FIPS 140-3 explicitly addresses non-invasive security -- that is, side-channel resistance -- among its requirements, across four ascending security levels with increasing physical tamper response [@fips-140-3].
&lt;p&gt;The third pillar is the least technical and the most often fatal: secrets management and non-human-identity hygiene. Application secrets -- API keys, database credentials, tokens, signing keys -- must stay out of code, config files, container images, and CI logs. OWASP made cryptographic-usage and keying failures the number-two web-application risk in 2021, and the 2025 OWASP Non-Human Identities Top 10 put improper offboarding (NHI1) and secret leakage (NHI2) at the very top of the list, with long-lived secrets also named (NHI7) [@owasp-a02-2021][@owasp-nhi-2025]. The tooling exists -- secrets managers that issue short-lived attested credentials, CI secret-scanning, workload federation -- but it demands organizational discipline no library can supply.&lt;/p&gt;
&lt;h3&gt;The concrete arc: Storm-0558 to Managed HSM&lt;/h3&gt;
&lt;p&gt;The clearest picture of Chain B&apos;s state of the art is the failure that forced it and the remedy that answered it. In 2023, the threat actor Storm-0558 forged Microsoft consumer authentication tokens using a 2016 signing key that was software-resident, held outside any HSM, and had never been rotated; a token-validation flaw let that consumer key mint enterprise tokens, and the actor downloaded roughly 60,000 State Department emails (about ten accounts), part of an intrusion that reached around two dozen organizations [@csrb-storm0558][@msrc-storm0558]. The U.S. Cyber Safety Review Board judged the intrusion preventable. No cipher was broken. A key simply outlived and escaped its custody.&lt;/p&gt;
&lt;p&gt;Microsoft&apos;s remedy, under its Secure Future Initiative, is the Chain B playbook made concrete: the MSA and Entra ID signing keys moved into Azure Managed HSM with automatic rotation, and the signing service itself moved into confidential-computing VMs [@microsoft-sfi-2024]. Put the root key in hardware, use it without exporting it, rotate it automatically. That is the entire prescription, and it is exactly what was missing in 2016.&lt;/p&gt;
&lt;p&gt;The honest caveat closes the section. Hardware custody is not a magic floor. A 2025 taxonomy catalogs how cloud HSM and TPM custody actually fails in practice, and TPM-Fail already showed that certified modules leak keys through timing [@hsm-tpm-failures-2025][@tpm-fail-2020]. Hardware moves the trust boundary; it does not remove it.&lt;/p&gt;

A few commands turn the abstract advice into an audit. Confirm a KMS key actually rotates with `aws kms get-key-rotation-status --key-id KEY_ID`. List which principals can call decrypt on it by reading the key policy with `aws kms get-key-policy --key-id KEY_ID --policy-name default`. Scan a repository&apos;s history for committed secrets with `gitleaks detect --source .` or `trufflehog filesystem .`. Gate a C library&apos;s tag comparison in CI by building it under `valgrind` with the TIMECOP macros, or by wiring a dudect harness around the routine. None of these is exotic; all of them catch the mistakes that end real incidents.
&lt;p&gt;You now know the tools on both frontiers. The hard part is the two decisions nobody hands you: where do you put the defense, and where do you put the key?&lt;/p&gt;
&lt;h2&gt;7. Where Do You Put the Defense, and Where the Key?&lt;/h2&gt;
&lt;p&gt;Every choice in this field trades assurance against cost, portability, and threat model. Two decision axes organize the whole thing, and the good news is that within each axis the options mostly &lt;em&gt;compose&lt;/em&gt; rather than compete.&lt;/p&gt;
&lt;h3&gt;Axis one: defense placement&lt;/h3&gt;
&lt;p&gt;The four leakage defenses are not rivals fighting for one slot. They stack. Ship verified constant-time as the floor, enable hardware data-independent-timing as backing, and add masking only when the adversary can physically hold and instrument the device.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Assurance&lt;/th&gt;
&lt;th&gt;Runtime cost&lt;/th&gt;
&lt;th&gt;Portability&lt;/th&gt;
&lt;th&gt;Defends against&lt;/th&gt;
&lt;th&gt;Blind to&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;Software constant-time&lt;/td&gt;
&lt;td&gt;Review-level&lt;/td&gt;
&lt;td&gt;Near-constant factor&lt;/td&gt;
&lt;td&gt;High, any CPU&lt;/td&gt;
&lt;td&gt;Timing and cache&lt;/td&gt;
&lt;td&gt;Microarchitecture: Spectre, DVFS, DMP&lt;/td&gt;
&lt;td&gt;Every deployment, the floor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verified constant-time [@hacl-star]&lt;/td&gt;
&lt;td&gt;Machine-checked proof&lt;/td&gt;
&lt;td&gt;Same as software&lt;/td&gt;
&lt;td&gt;High, generated C&lt;/td&gt;
&lt;td&gt;Timing and cache, proven&lt;/td&gt;
&lt;td&gt;Below the verified boundary; microarchitecture&lt;/td&gt;
&lt;td&gt;Long-lived high-assurance primitives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware DIT / DOITM [@intel-doitm][@arm-dit]&lt;/td&gt;
&lt;td&gt;Vendor guarantee, listed instructions&lt;/td&gt;
&lt;td&gt;Near zero for covered instructions&lt;/td&gt;
&lt;td&gt;Low, vendor-specific&lt;/td&gt;
&lt;td&gt;Operand timing of listed instructions&lt;/td&gt;
&lt;td&gt;Prefetcher, frequency scaling, division&lt;/td&gt;
&lt;td&gt;Defense-in-depth beneath software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Masking / threshold [@isw-2003]&lt;/td&gt;
&lt;td&gt;Provable order-d in the probing model&lt;/td&gt;
&lt;td&gt;Quadratic in the order&lt;/td&gt;
&lt;td&gt;Hardware-oriented, costly in software&lt;/td&gt;
&lt;td&gt;Power, EM, glitch faults&lt;/td&gt;
&lt;td&gt;Higher-order analysis; cost&lt;/td&gt;
&lt;td&gt;Physical-possession adversary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The table&apos;s lesson is the composition, not any single winner. Each defense covers what the one above it cannot; masking, in particular, is the only thing that helps once an attacker owns the silicon, and you pay for it by the square of the order. None of them closes the microarchitectural channels alone -- which is the structural reason Chain B has to exist.&lt;/p&gt;
&lt;h3&gt;Axis two: the key-custody spectrum&lt;/h3&gt;
&lt;p&gt;Custody is a ladder, and the single most useful skill in this whole article is matching the rung to the adversary and the asset value.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rung&lt;/th&gt;
&lt;th&gt;Key location&lt;/th&gt;
&lt;th&gt;Exportable?&lt;/th&gt;
&lt;th&gt;Blast radius on host compromise&lt;/th&gt;
&lt;th&gt;Cost / latency&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;th&gt;Anchor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Hardcoded, config, env var, repo&lt;/td&gt;
&lt;td&gt;Yes, in plaintext&lt;/td&gt;
&lt;td&gt;Total, the key is the leak&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Never, an anti-pattern&lt;/td&gt;
&lt;td&gt;Storm-0558 [@csrb-storm0558]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;OS or software keystore&lt;/td&gt;
&lt;td&gt;Yes, to root or admin&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low-value single-host secrets&lt;/td&gt;
&lt;td&gt;OWASP A02 [@owasp-a02-2021]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;TPM or secure element&lt;/td&gt;
&lt;td&gt;No, use without export&lt;/td&gt;
&lt;td&gt;Low, but see TPM-Fail&lt;/td&gt;
&lt;td&gt;Low to medium&lt;/td&gt;
&lt;td&gt;Device-bound keys, measured boot&lt;/td&gt;
&lt;td&gt;TPM-Fail [@tpm-fail-2020]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Cloud KMS plus envelope&lt;/td&gt;
&lt;td&gt;KEK no, DEK wrapped&lt;/td&gt;
&lt;td&gt;Bounded to unwrapped DEKs&lt;/td&gt;
&lt;td&gt;One KMS round-trip per DEK&lt;/td&gt;
&lt;td&gt;Data at rest at scale&lt;/td&gt;
&lt;td&gt;AWS KMS [@aws-kms]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Dedicated or Managed HSM&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;Higher&lt;/td&gt;
&lt;td&gt;Root, CA, code-signing, payment keys&lt;/td&gt;
&lt;td&gt;FIPS 140-3 [@fips-140-3]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Threshold or MPC custody&lt;/td&gt;
&lt;td&gt;No single holder&lt;/td&gt;
&lt;td&gt;Requires several compromises&lt;/td&gt;
&lt;td&gt;Highest, protocol rounds&lt;/td&gt;
&lt;td&gt;No single point of trust, see Part 22&lt;/td&gt;
&lt;td&gt;NIST SP 800-57 [@nist-sp800-57]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Here is the capstone&apos;s punchline, sitting in one table. The exotic Chain-A attacks -- a timing leak against a certified TPM at rung 2, say -- are real but rare. The overwhelming majority of real-world breaks cluster at rungs 0 and 1: keys hardcoded, sprawled across config and CI logs, software-resident, and never rotated. Storm-0558 was a rung-0 failure with a nation-state on the other end. The state-of-the-art move for almost everyone is simply to climb to rung 3 or 4 with automatic rotation, and to accept the custody regress that waits at the top.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A key in source, in a config file, in an environment variable baked into an image, or in a repository is not &quot;low assurance.&quot; It is the leak, waiting for any code, log, or memory disclosure to complete it. There is no asset value low enough to justify rung 0. Move to a secrets manager and rotate.&lt;/p&gt;
&lt;/blockquote&gt;

It is tempting to read a Common Criteria or FIPS certificate as &quot;side-channel-proof.&quot; It is not. TPM-Fail recovered 256-bit ECDSA keys from an Intel firmware TPM and from a Common Criteria EAL4+ and FIPS 140-2 Level 2 certified STMicroelectronics chip, in minutes locally and about five hours across a network [@tpm-fail-2020]. Minerva broke FIPS-certified smartcards from a couple thousand signatures [@minerva-2020]. Certification validates a *modeled* threat surface against a defined protection profile; it bounds process, not physics. When the modeled surface omits the channel the attacker uses, the certificate is silent.

&quot;Certification has failed to protect the product against an attack that is considered by the protection profile.&quot; -- Moghimi et al., the TPM-Fail disclosure [@tpm-fail-2020]
&lt;p&gt;These tables tell you what to reach for. But can leakage ever be &lt;em&gt;eliminated&lt;/em&gt;, or are we only ever bounding its cost? The honest answer is uncomfortable, and it comes in two parts.&lt;/p&gt;
&lt;h2&gt;8. Can Leakage Be Eliminated?&lt;/h2&gt;
&lt;p&gt;The field owes you two very different answers. One is a clean, provable bound. The other is an honest impossibility that is not a theorem at all but a structural fact about how computers are specified.&lt;/p&gt;
&lt;h3&gt;The masking cost curve is a real bound, and it is not a wall&lt;/h3&gt;
&lt;p&gt;For the physical channel, the mathematics is unusually crisp. In the Ishai-Sahai-Wagner probing model, protecting a secret against an adversary who can observe up to $d$ internal wires requires at least $d+1$ random shares, and that bound is tight -- you cannot buy order-$d$ security with fewer [@isw-2003]. Securely masking a single multiplication in that model costs $O(d^2)$ in circuit size and randomness. So security grows linearly in the order while cost grows quadratically: doubling your resistance roughly quadruples the work.&lt;/p&gt;
&lt;p&gt;Under a physically realistic noisy-leakage model, the picture gets better in one specific way. Duc, Dziembowski, and Faust proved that masking&apos;s security amplifies &lt;em&gt;exponentially&lt;/em&gt; in the number of shares, provided each share carries enough noise, by reducing noisy leakage back to the clean probing model [@ddf-2014]. Put the two results together and you get the defining shape of the whole physical-defense story: leakage is a &lt;em&gt;cost curve, not a wall&lt;/em&gt;. You can push the attacker&apos;s required number of traces arbitrarily high by paying $O(d^2)$ and ensuring noise. You can never push it to zero.&lt;/p&gt;
&lt;h3&gt;Whole-machine constant-time is an impossibility, not a theorem&lt;/h3&gt;
&lt;p&gt;For the timing and cache channels, there is no comparable happy bound, and the reason is almost embarrassingly simple. &lt;em&gt;Constant-time cannot be guaranteed at the microarchitectural level because the instruction set architecture does not specify timing.&lt;/em&gt; A constant-time proof is a statement about ISA-visible behavior. The silicon underneath is free to introduce data-dependence the ISA never promised to forbid, and it repeatedly does.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The ISA does not specify timing, so a constant-time proof only constrains architecture-visible behavior. You can only close the channels you model, and the set of channels is open-ended and adversary-discovered. Speculation, frequency scaling, and data-memory-dependent prefetching each reopened it after the proof was done.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is why Spectre, Hertzbleed, GoFetch, and KyberSlash are not a series of fixable bugs but a series of demonstrations of the same structural gap. Each exploited a microarchitectural behavior -- speculation, DVFS, a prefetcher, variable-time division -- that no source-level or even ISA-level guarantee covered. There is no proof that a real machine leaks nothing through timing, and, because the ISA is silent on timing, none is possible with today&apos;s hardware contracts. The gap is not small and closing. It is structurally open until a complete, cross-vendor data-independent-timing contract exists, which DIT and DOITM only partially provide.&lt;/p&gt;
&lt;h3&gt;The custody regress&lt;/h3&gt;
&lt;p&gt;The operational half has its own impossibility, and it is the reason this article lands on key custody rather than on some final leak-proof coprocessor. Someone must ultimately hold a root secret. Hardware roots -- HSMs, TPMs, Managed HSMs -- &lt;em&gt;move&lt;/em&gt; the trust boundary inward and defend it far better, but they do not &lt;em&gt;remove&lt;/em&gt; it, and that smaller boundary has its own documented failure modes, from TPM-Fail&apos;s timing leak to the 2025 taxonomy of cloud HSM and TPM failures [@tpm-fail-2020][@hsm-tpm-failures-2025]. There is no keyless bottom. There is only a smaller, better-defended custodian, and certification of that custodian is necessary but never sufficient.&lt;/p&gt;

Hardware moves the trust boundary; it does not remove it.
&lt;p&gt;So perfection is off the table on both halves. What replaces it is not despair but a different target.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The honest goal is not zero leakage and not an unbreakable key. It is cost-optimality: make leakage so expensive to exploit, and each secret so short-lived and tightly bounded, that no leak is worth the attacker&apos;s effort. Masking raises the price of a power trace; short key lifetimes shrink the value of any single leaked bit; envelope encryption bounds the blast radius. You are not building a wall. You are pricing the attacker out.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The purely theoretical program of leakage-resilient cryptography pushes on the same questions from the model side, but relative to deployed masking and hardware custody it remains largely academic. If perfection is unreachable, the frontier turns into a list of concrete engineering fights -- and, refreshingly, they are winnable one channel at a time.&lt;/p&gt;
&lt;h2&gt;9. The 2026 Frontier&lt;/h2&gt;
&lt;p&gt;Name the open problems precisely and the field stops looking like whack-a-mole and starts looking like a research agenda. Each of these is calibrated engineering, not undetectable magic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A complete microarchitectural timing contract.&lt;/strong&gt; DIT and DOITM enumerate data-independent instructions, but they are vendor-specific and exclude division, square root, the prefetcher, and frequency scaling [@intel-doitm][@arm-dit]. Every constant-time proof is therefore conditional on an unmodeled microarchitecture, which is exactly the seam GoFetch and Hertzbleed pried open. The open problem is an ISA-level guarantee that extends a constant-time proof &lt;em&gt;below&lt;/em&gt; the ISA, across vendors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Speculative constant-time at scale.&lt;/strong&gt; Spectre-class transient execution breaks architecturally constant-time code from beneath [@spectre-2019]. Retpoline, LFENCE barriers, and Speculative Load Hardening are point defenses with real, often double-digit-percent overhead [@llvm-slh]. A scalable, low-cost compiler that guarantees data-independence even under speculation, for whole libraries, does not yet exist.The community conjecture is that without hardware speculation-taint tracking, a software-only speculative-constant-time pass cannot be both complete and cheap. You either pay in overhead or accept coverage gaps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Whole-library verification through the compiler.&lt;/strong&gt; Compiler lowering reintroduces leaks that IR-level proofs miss, which is the entire motivation for binary-level tools like Binsec/Rel [@binsec-rel]. Today we have per-primitive proofs and per-binary bounded checks; 2024 work such as CT-Prover targets the scalability wall, and benchmarks map the tool terrain [@ct-prover-2024][@sidetool-eval-2023]. A push-button, whole-library, binary-level verifier usable by non-experts remains out of reach -- and usability studies show even experts misconfigure the current tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constant-time and fault-resistant post-quantum code.&lt;/strong&gt; KyberSlash and GoFetch proved the discipline is unfinished for the freshly standardized lattice schemes [@kyberslash-2024][@gofetch-2024]. Shipping ML-KEM and ML-DSA implementations that are simultaneously constant-time, masked against physical attack, and fast across diverse CPUs -- and proving it -- is an active frontier, because the arithmetic-heavy PQC code has larger, fresher leakage surfaces than classical crypto.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Secretless architectures and minimal key lifetimes.&lt;/strong&gt; The dominant real-world failure surface is long-lived, sprawled, unrotated secrets [@owasp-nhi-2025]. Rotation, workload attestation, and short-lived derived credentials shrink the custody surface, but the custody regress forbids a truly secretless bottom. The open question is quantitative: how small can the irreducible custodian be made?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-quantum key management at fleet scale.&lt;/strong&gt; Larger keys, hybrid classical-and-PQ modes, and harvest-now-decrypt-later urgency make the key lifecycle harder precisely as it becomes critical. Zero-downtime migration across a whole estate, with a cryptographic bill of materials for inventory, anchored on the lifecycle discipline of NIST SP 800-57, is an open operational problem handed off to the crypto-agility part of this series [@nist-sp800-57].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every item above is a bounded engineering fight with measurable partial results, not a wish for undetectable magic. That framing matters: it tells you which problems to fund and which &quot;solutions&quot; to distrust. Anyone selling a complete, cheap, whole-machine leak-free implementation today is selling the impossibility from Section 8.&lt;/p&gt;
&lt;/blockquote&gt;

This capstone deliberately defers depth to its siblings. Randomness and RNG failures live in Part 2; padding oracles and the Lucky Thirteen mechanics in Part 6; nonce reuse and the Minerva and PS3 stories in Part 8; RSA-CRT and Bleichenbacher detail in Part 14; scalar-multiplication leakage of the nonce in Part 15; deterministic-versus-hedged signing and its fault risk in Part 17; threshold and MPC custody, the top rung of the spectrum, in Part 22; ML-KEM and ML-DSA internals in Part 23; and rotation-at-fleet-scale, crypto-agility, and the cryptographic bill of materials in Part 24. Read as a set, the twenty-five parts are one argument.
&lt;p&gt;The research agenda is clear. The deployment agenda is clearer still. Here is what to do on Monday.&lt;/p&gt;
&lt;h2&gt;10. What To Do on Monday&lt;/h2&gt;
&lt;p&gt;Everything above collapses into a handful of rules you can apply without a research budget.&lt;/p&gt;
&lt;h3&gt;The constant-time checklist&lt;/h3&gt;
&lt;p&gt;Anything that touches a secret gets three prohibitions: no branch on it, no memory index computed from it, no variable-time operation applied to it -- and &quot;variable-time&quot; specifically includes integer division and early-exit comparison. Test the result with dudect or TIMECOP in CI, prefer a verified library over your own code, and re-check after every toolchain upgrade, because compilers betray branchless code back into branches. That single decision rule fits in a diagram.&lt;/p&gt;

flowchart TD
    S[&quot;A value in your code&quot;] --&amp;gt; Q{&quot;Does it touch a secret?&quot;}
    Q --&amp;gt;|no| OK[&quot;Ordinary code is fine&quot;]
    Q --&amp;gt;|yes| R[&quot;Forbid all three&quot;]
    R --&amp;gt; B[&quot;No branch on it&quot;]
    R --&amp;gt; M[&quot;No memory index from it&quot;]
    R --&amp;gt; V[&quot;No variable-time op: no divide, no early-exit compare&quot;]
&lt;h3&gt;Threat-model routing for leakage defense&lt;/h3&gt;
&lt;p&gt;Match the defense to the adversary. Everyone starts from the same floor and adds as the threat model widens.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Adversary&lt;/th&gt;
&lt;th&gt;Add to the floor&lt;/th&gt;
&lt;th&gt;Because&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Remote network&lt;/td&gt;
&lt;td&gt;Verified or constant-time crypto, constant-time MAC checks, no padding oracles&lt;/td&gt;
&lt;td&gt;This is the floor for everyone; it closes Lucky Thirteen and the timing family&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Co-tenant, shared cache (cloud)&lt;/td&gt;
&lt;td&gt;Cache-hardening, per-tenant isolation, hardware CT where honored&lt;/td&gt;
&lt;td&gt;Prime+Probe and Flush+Reload cross tenant boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physical possession (smartcard, wallet, IoT)&lt;/td&gt;
&lt;td&gt;Masking or threshold implementations, fault sensors, a secure element&lt;/td&gt;
&lt;td&gt;The attacker owns and can instrument the silicon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assume speculation, DVFS, DMP exist&lt;/td&gt;
&lt;td&gt;Treat CT as necessary-not-sufficient; isolate secrets across process and VM boundaries; keep key lifetimes short&lt;/td&gt;
&lt;td&gt;These channels live below the ISA and cannot be closed in source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;The key-management playbook&lt;/h3&gt;
&lt;p&gt;The custody decision is a lookup table keyed on asset and threat. A low-value single-host secret belongs in the OS keystore. A device-bound key with measured boot belongs in a TPM or secure element. Lots of data at rest belongs behind cloud KMS with envelope encryption and automatic rotation [@aws-kms]. A root, certificate-authority, code-signing, or payment key belongs in a dedicated or Managed FIPS 140-3 HSM [@fips-140-3].&lt;/p&gt;
&lt;p&gt;A key that must survive with no single point of trust belongs in &lt;a href=&quot;https://paragmali.com/blog/nobody-broke-shamir-a-field-guide-to-secret-sharing-and-thre/&quot; rel=&quot;noopener&quot;&gt;threshold or MPC custody&lt;/a&gt;, the subject of Part 22. And any application secret belongs in a secrets manager issuing short-lived attested credentials, with CI secret-scanning behind it -- never hardcoded, always rotated, and stale non-human identities always offboarded [@owasp-nhi-2025][@nist-sp800-57].&lt;/p&gt;
&lt;p&gt;One caution about rotation, because it is oversold. Rotation bounds the blast radius of a leaked key -- it caps how much a single compromised key is worth and for how long -- but it is not a substitute for custody. Storm-0558&apos;s key was both never rotated &lt;em&gt;and&lt;/em&gt; software-resident; rotation alone would have shortened the exposure window, but keeping the key in an HSM is what stops the export in the first place. Rotate &lt;em&gt;and&lt;/em&gt; custody; the two do different jobs.&lt;/p&gt;
&lt;h3&gt;The common-misuse catalog&lt;/h3&gt;
&lt;p&gt;Most real leaks are not exotic. They are the same dozen mistakes, made again.One misuse deserves special note because the fix for a &lt;em&gt;different&lt;/em&gt; problem creates it: &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;deterministic ECDSA&lt;/a&gt; (RFC 6979) removes the nonce-RNG risk [@rfc6979], but a fault during signing then yields two signatures on the same nonce, which recovers the key [@poddebniak-2018]. Hedged signing -- mixing in fresh randomness -- restores fault resistance without reopening the RNG risk. Part 17 treats this trade in full.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse&lt;/th&gt;
&lt;th&gt;Why it leaks&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memcmp&lt;/code&gt; on a MAC or tag&lt;/td&gt;
&lt;td&gt;Early-exit timing oracle, the Lucky Thirteen class [@lucky13-2013]&lt;/td&gt;
&lt;td&gt;Constant-time compare (CRYPTO_memcmp, sodium_memcmp)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret-indexed table lookup&lt;/td&gt;
&lt;td&gt;Cache-timing leak of the accessed address [@bernstein-2005]&lt;/td&gt;
&lt;td&gt;AES-NI or bitslicing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret-dependent division or branch&lt;/td&gt;
&lt;td&gt;Variable-time; division is not a data-independent instruction [@kyberslash-2024]&lt;/td&gt;
&lt;td&gt;Replace with multiply, mask, or branchless select&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiler betrayal&lt;/td&gt;
&lt;td&gt;Optimizer restores a branch, or elides a memset that scrubs a secret&lt;/td&gt;
&lt;td&gt;Verify the binary, use secure-zero intrinsics, re-check after upgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skipping RSA-CRT verification&lt;/td&gt;
&lt;td&gt;Reopens the Bellcore fault; one bad signature factors n [@bellcore-1997]&lt;/td&gt;
&lt;td&gt;Verify the signature before releasing it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Home-rolled constant-time&lt;/td&gt;
&lt;td&gt;Assume it is not constant-time until a tool proves otherwise&lt;/td&gt;
&lt;td&gt;dudect or TIMECOP, plus a verified library&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded or sprawled secrets&lt;/td&gt;
&lt;td&gt;Rung 0; the key is the leak [@csrb-storm0558]&lt;/td&gt;
&lt;td&gt;Secrets manager, rotation, offboarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusing a KEK as a DEK&lt;/td&gt;
&lt;td&gt;Couples bulk data directly to the root key&lt;/td&gt;
&lt;td&gt;Keep DEK and KEK distinct; wrap, do not encrypt with the KEK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-constant-time post-quantum code&lt;/td&gt;
&lt;td&gt;Fresh, unhardened leakage surface [@gofetch-2024]&lt;/td&gt;
&lt;td&gt;Patched, verified PQC libraries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The envelope pattern at the bottom of the custody advice is worth seeing in code, because the blast-radius argument is the whole point.&lt;/p&gt;
&lt;p&gt;{`
// Illustrative only. Real systems use AES-GCM to encrypt and AES-KW to wrap,
// and the KEK lives inside an HSM or cloud KMS that performs wrap and unwrap on request.
function xorCrypt(bytes, key) { return bytes.map((b, i) =&amp;gt; b ^ key[i % key.length]); }
function randomKey(n) { return Array.from({ length: n }, () =&amp;gt; Math.floor(Math.random() * 256)); }&lt;/p&gt;
&lt;p&gt;// The KEK never leaves this boundary; software only sends wrap and unwrap requests.
const KEK = randomKey(16);
function wrap(dek)  { return xorCrypt(dek, KEK); }  // stands in for a KMS wrap call
function unwrap(wd) { return xorCrypt(wd, KEK); }   // stands in for a KMS unwrap call&lt;/p&gt;
&lt;p&gt;// Encrypt: fresh DEK per object, encrypt the data, wrap the DEK, store the wrapped DEK beside it.
const data = [72, 105, 33];
const dek = randomKey(16);
const ciphertext = xorCrypt(data, dek);
const wrappedDek = wrap(dek);   // this, not the plaintext DEK, is stored with the ciphertext&lt;/p&gt;
&lt;p&gt;// Decrypt: ask the boundary to unwrap the DEK, use it, discard it.
const plaintext = xorCrypt(ciphertext, unwrap(wrappedDek));
console.log(&apos;round-trips:&apos;, plaintext.join(&apos;,&apos;) === data.join(&apos;,&apos;));
// Blast radius: a leaked wrappedDek without the KEK reveals nothing; a leaked DEK exposes only its one object.
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Print the table above and run it against any code that touches a key. Every row has ended a real system. The first three are the constant-time family, the next four are custody, and the last two are the modern frontier. If your review cannot rule out all nine, you have not finished reviewing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Ship verified constant-time as the floor, back it with hardware timing modes and, for physical adversaries, masking -- then spend the rest of your budget where systems actually break, on key custody. Close the channels you can model; bound the cost of the ones you cannot.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;11. Seven Beliefs That Ship to Production and Get People Owned&lt;/h2&gt;


No, and this is the single most consequential misunderstanding in the field. Constant-time means *data-independent* execution: the same instruction stream and the same memory-address trace regardless of the secret&apos;s value. It does not mean fixed duration. A routine can run for different absolute times on different inputs and still be constant-time, as long as the variation never depends on a secret. Conversely, padding a function to a fixed wall-clock time does not make it constant-time if it still branches on a secret underneath.


No. Certification validates a modeled threat surface, not physics. TPM-Fail recovered 256-bit ECDSA keys from an Intel firmware TPM and a Common Criteria EAL4+ and FIPS-certified STMicroelectronics chip via a timing-and-lattice attack -- minutes locally, hours remotely [@tpm-fail-2020]. A certificate tells you the module passed a defined evaluation; it does not tell you the evaluation covered the channel your attacker will use.


Necessary, not sufficient. Constant-time is a property of the instruction stream you wrote; the silicon runs one you did not. Spectre executes secret-dependent paths speculatively, Hertzbleed turns data-dependent frequency into remote timing, GoFetch&apos;s prefetcher dereferences pointer-like secrets, and KyberSlash leaks through a variable-time division -- all beneath the constant-time abstraction [@spectre-2019][@hertzbleed-2022][@gofetch-2024][@kyberslash-2024]. Constant-time is the floor, not the ceiling.


Mostly true, and you should threat-model it honestly: if no adversary can hold or instrument your device, the classical power and EM channels are lower priority than the remote ones. But the boundary is not absolute. Hertzbleed turned a power side channel into a *remote* timing attack by exploiting dynamic frequency scaling, extracting a key from constant-time SIKE over a network with no physical probe at all [@hertzbleed-2022].


Rotation bounds the blast radius of a leak; it does not prevent the leak. It caps how long a compromised key stays valid and how much it protects, which is real and valuable. But it is not custody. Storm-0558&apos;s key was never rotated *and* software-resident; rotation would have shortened the window, while hardware custody would have stopped the export that started the whole incident [@csrb-storm0558]. Do both.


No. It is custody plus performance, not redundancy. A cheap Data-Encryption Key does the bulk work while the Key-Encryption Key that wraps it never leaves the HSM or KMS, using standardized key wrapping [@nist-sp800-38f]. The payoff is a bounded blast radius: a leaked DEK exposes only the one object it encrypted, and the root key stays in hardware. Double encryption gives you two layers of the same risk; envelope encryption gives you a small, cheap, disposable key over a small, protected, permanent one.


No -- they are broken implementations and mismanaged keys. Scope the claim carefully: relative to breaking the primitives and relative to the exotic side channels this article catalogs, operational key and secret management is the dominant failure surface, evidenced by cryptographic failures ranking as the number-two web risk and by Storm-0558&apos;s uncracked-but-stolen key [@owasp-a02-2021][@csrb-storm0558]. It is *not* a claim that key management outranks every software vulnerability class -- memory-safety bugs lead by raw count. The math almost never breaks. The handling of the math does.

&lt;h2&gt;The Proof Was Conditional&lt;/h2&gt;
&lt;p&gt;Twenty-four times, this series proved that the mathematics holds. Every one of those proofs carried a silent condition: supply a fresh nonce, validate the point, derive a uniform key, compare tags in constant time, keep the secret in its custody. This final part is the condition, standing alone and made visible. The math held. The machine and the operations are what handed the secret away.&lt;/p&gt;
&lt;p&gt;You now have the lens to read any future break the same way, without waiting for someone to explain it. When the next headline attack lands, ask which of four invariants it violated. Did a secret touch &lt;em&gt;time&lt;/em&gt; -- a branch, a division, a data-dependent frequency? Did a secret touch an &lt;em&gt;address&lt;/em&gt; -- a table index, a cache line, a prefetched pointer? Did a &lt;em&gt;fault&lt;/em&gt; skip a check -- a glitch, a hammered row, an undervolted enclave? Or did a &lt;em&gt;key&lt;/em&gt; outlive or escape its custody -- hardcoded, sprawled, unrotated, software-resident? Kocher&apos;s stopwatch in 1996 and Apple&apos;s prefetcher in 2024 are the same sentence in two dialects, and Storm-0558 is the fourth invariant in its purest form: no cipher broken, a key simply gone.&lt;/p&gt;

This article has claimed that key and secret management is where deployed systems most often break, and that claim needs its fences. It is scoped: *relative to breaking the primitives, and relative to the exotic side channels catalogued here*, operational custody is the dominant failure surface -- supported by cryptographic failures ranking as the number-two web-application risk and by nation-state incidents like Storm-0558 that cracked no math [@owasp-a02-2021][@csrb-storm0558]. It is *not* a measured majority over all software vulnerability classes; memory-safety bugs lead by raw count. The umbrella term is &quot;key and secret management,&quot; and the honest version of the thesis never says &quot;most of all breaches&quot; -- it says &quot;the dominant *operational* failure surface for cryptographic systems.&quot;
&lt;p&gt;So end the series where the evidence points. Spend real effort on the exotic channels, because verified constant-time and hardware timing modes are now deployable and the microarchitecture will keep testing them. But spend the larger share of your budget on the unglamorous layer, because that is where the incidents actually cluster: put the root key in hardware, use it without exporting it, envelope-wrap and auto-rotate everything else, and keep no long-lived secret sitting in software. The twenty-five parts of this guide are one argument, and this is its last line.&lt;/p&gt;

The math held. Make sure the machine, and the people running it, do not hand the secret away.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;secure-implementation-side-channels-key-custody&quot; keyTerms={[
  { term: &quot;Side channel&quot;, definition: &quot;Information leaked by a computation&apos;s physical or microarchitectural behavior, not its input/output relation.&quot; },
  { term: &quot;Abstraction gap&quot;, definition: &quot;The distance between an algorithm as an I/O relation and an implementation as a physical, timed, operated process.&quot; },
  { term: &quot;Constant-time&quot;, definition: &quot;Data-independent execution: instruction and address traces do not depend on secrets. Not fixed duration.&quot; },
  { term: &quot;Differential Power Analysis&quot;, definition: &quot;Correlating power draw across many traces to extract a key even under heavy noise.&quot; },
  { term: &quot;Fault attack&quot;, definition: &quot;Inducing a computational error so the faulty output reveals the secret, as in Bellcore RSA-CRT.&quot; },
  { term: &quot;Cache side channel&quot;, definition: &quot;Recovering which addresses a victim touched by timing shared-cache hits and misses, as in Flush and Reload.&quot; },
  { term: &quot;Transient execution&quot;, definition: &quot;Speculatively executed, later-discarded instructions whose cache traces still leak, as in Spectre.&quot; },
  { term: &quot;Masking&quot;, definition: &quot;Splitting a secret into d+1 random shares so any d observed values are independent of it.&quot; },
  { term: &quot;HSM&quot;, definition: &quot;A tamper-resistant boundary that uses keys without exporting them in plaintext.&quot; },
  { term: &quot;Envelope encryption&quot;, definition: &quot;A cheap DEK encrypts data; a KEK inside the HSM wraps the DEK; the wrapped DEK is stored beside the ciphertext.&quot; }
]} flashcards={[
  { front: &quot;The four invariants a break violates&quot;, back: &quot;A secret touched time; a secret touched an address; a fault skipped a check; a key outlived or escaped its custody.&quot; },
  { front: &quot;Why is constant-time necessary but not sufficient?&quot;, back: &quot;The ISA does not specify timing, so speculation, frequency scaling, prefetchers, and division reintroduce data-dependence below it.&quot; },
  { front: &quot;The masking cost curve&quot;, back: &quot;Order-d security needs at least d+1 shares; a masked multiply costs O(d^2). Leakage is a cost curve, not a wall.&quot; },
  { front: &quot;The capstone rule&quot;, back: &quot;Verified constant-time as the floor, hardware modes and masking as backing, then spend the rest on key custody.&quot; }
]} questions={[
  { q: &quot;Why does square-and-multiply leak the exponent through timing?&quot;, a: &quot;A one bit triggers an extra multiply, so total time tracks the number of one bits in the secret exponent.&quot; },
  { q: &quot;What made Storm-0558 a custody failure rather than a cryptographic one?&quot;, a: &quot;A 2016 software-resident, never-rotated signing key was stolen and used to forge tokens; no cipher was broken.&quot; },
  { q: &quot;How does GoFetch break constant-time code?&quot;, a: &quot;Apple&apos;s data-memory-dependent prefetcher dereferences pointer-like values, coupling data to addresses below the ISA.&quot; },
  { q: &quot;What does rotation fix, and what does it not fix?&quot;, a: &quot;It bounds a leaked key&apos;s blast radius and lifetime; it does not stop the export, which is custody&apos;s job.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>constant-time</category><category>side-channels</category><category>key-management</category><category>fault-attacks</category><category>cryptographic-engineering</category><category>hsm</category><category>spectre</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Discrete Log Held; The Proofs Leaked: A Field Guide to Commitments and Sigma Protocols</title><link>https://paragmali.com/blog/the-discrete-log-held-the-proofs-leaked-a-field-guide-to-com/</link><guid isPermaLink="true">https://paragmali.com/blog/the-discrete-log-held-the-proofs-leaked-a-field-guide-to-com/</guid><description>How the discrete log stayed hard for forty years while the proofs built on it kept breaking: Pedersen commitments, Schnorr, Sigma protocols, and Fiat-Shamir.</description><pubDate>Mon, 13 Jul 2026 20:58:24 GMT</pubDate><content:encoded>
A discrete-log zero-knowledge proof is one three-move conversation -- commit, challenge, respond -- and for forty years every real break landed on one of those moves, never on the discrete logarithm underneath. Move 1, the commitment: Pedersen hides perfectly and binds on the discrete log, but a trapdoor-holder can equivocate. Move 2, the Fiat-Shamir challenge: hash too little and you get weak Fiat-Shamir, the Frozen Heart bug that forged &quot;zero-knowledge&quot; proofs in Helios (2012) and in Bulletproofs and PlonK (2022) with the math untouched. Move 3, the response: reuse the nonce and the witness falls out by algebra; exploit its linearity across concurrent sessions and the ROS attack forges naive multisignatures in polynomial time. This is the field guide to those three moves -- Pedersen, Schnorr, Sigma protocols, Fiat-Shamir -- how they compose into Bulletproofs, MuSig2, FROST, Signal&apos;s credentials, and Privacy Pass, and every documented way each has been gotten wrong.
&lt;h2&gt;1. The Math Was Fine. The Proofs Were Forgeable.&lt;/h2&gt;
&lt;p&gt;In April 2022, Trail of Bits published a coordinated disclosure with an unsettling shape [@tob-part1]. Across three unrelated &quot;zero-knowledge&quot; proof systems -- Girault&apos;s classic proof of knowledge, Bulletproofs (the range proof securing confidential Monero transactions), and PlonK (a modern general-purpose SNARK) -- the proofs were &lt;em&gt;forgeable&lt;/em&gt;. An attacker could produce a convincing proof of a statement that was simply false [@tob-part1], [@tob-girault], [@tob-bp], [@tob-plonk].&lt;/p&gt;
&lt;p&gt;And in none of the three cases had anyone broken the discrete logarithm, or factoring, or any hard problem. The math was untouched. The bug was that a single hash had omitted part of the statement it was supposed to be about [@tob-part1].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In every Frozen Heart case, the discrete logarithm was never solved. The forgery lived entirely in what a hash function failed to bind. Soundness turned out to be a property of the protocol&apos;s plumbing, not of the hard problem beneath it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The Bulletproofs instance was the sharpest. Trail of Bits traced it to an insecure Fiat-Shamir generation &lt;em&gt;recommended in the original 2017 Bulletproofs paper itself&lt;/em&gt; -- the flaw was in the specification, not a careless implementer&apos;s shortcut [@tob-bp], [@bulletproofs]. It received a CVE: CVE-2022-29566 [@cve]. A vulnerability class was born, and Trail of Bits named it. The word &quot;Frozen&quot; is the acronym -- FoRging Of ZEro-kNowledge proofs; &quot;Heart&quot; nods to the Fiat-Shamir transform that sits at the heart of these proof systems [@tob-part1].&lt;/p&gt;
&lt;p&gt;Then the twist that makes it a pattern, not an incident: this exact bug had already been named and published a decade earlier. In 2012, David Bernhard, Olivier Pereira, and Bogdan Warinschi showed that the Helios internet-voting system used the same shortcut -- and that its &quot;zero-knowledge&quot; ballot proofs could be transplanted and forged the same way [@bpw]. Two breaks, ten years apart, identical root cause, and the second one shipped in production behind a CVE.&lt;/p&gt;
&lt;p&gt;Now widen the aperture. The discrete logarithm -- the hard problem under Diffie-Hellman, Schnorr, ElGamal, and the whole family -- has resisted forty years of attack; &lt;a href=&quot;https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/&quot; rel=&quot;noopener&quot;&gt;nobody broke the discrete log&lt;/a&gt;, as Part 18 argued at length. Yet the proof systems built on top of it break with some regularity. Why?&lt;/p&gt;
&lt;p&gt;Because a discrete-log zero-knowledge proof is not one thing. It is a three-move conversation -- &lt;strong&gt;commit, challenge, respond&lt;/strong&gt; -- and each move is a separate attack surface. The prover sends a commitment, the verifier sends a random challenge, the prover sends a response, and one equation decides the outcome.&lt;/p&gt;
&lt;p&gt;Frozen Heart and Helios are failures of the second move, the challenge. Reusing a signature nonce -- the catastrophe that &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;hands over a private key when one number repeats&lt;/a&gt; (Part 8) -- is a failure of the third move, the response. A trapdoor that lets a committer change its mind after the fact is a failure of the first move, the commitment. Three moves, three ways to leak, and not one of them is the discrete logarithm.&lt;/p&gt;
&lt;p&gt;That is the whole thesis of this article, stated once so you can hold it through everything that follows: &lt;strong&gt;the discrete log held; the three moves leaked.&lt;/strong&gt; The hard problem underneath these systems has never fallen. The conversation built on top of it has failed, repeatedly, in ways that are precise, named, and preventable.&lt;/p&gt;
&lt;p&gt;The reach of these three moves is easy to underestimate. The same commit-challenge-respond skeleton runs under Bitcoin&apos;s Taproot multisignatures, under Monero&apos;s confidential transaction amounts, under Signal&apos;s private group membership, and under the digital-identity credentials now standardizing at the IETF. Master the three moves and you have a lens that resolves an entire branch of applied cryptography -- and a checklist that separates a deployment that ships from one that ships a CVE.&lt;/p&gt;
&lt;p&gt;Those three moves were not invented together, and the cleanest one was invented first and deployed last -- frozen for nearly two decades by a patent [@schnorr-patent]. To see why the conversation has exactly three moves, and why each one leaks, we start where the vocabulary was born.&lt;/p&gt;
&lt;h2&gt;2. The Three-Move Conversation&lt;/h2&gt;
&lt;p&gt;Strip away the applications -- the multisignatures, the range proofs, the credentials -- and there is one shape underneath all of them. Learn that shape precisely, and every guarantee and every failure in this article becomes a statement about one of its three parts.&lt;/p&gt;
&lt;p&gt;The archetype is Schnorr&apos;s protocol for proving you know a discrete logarithm. You have a public key $y = g^x$ in a group of prime order $q$ generated by $g$, and you want to convince a verifier that you know the secret exponent $x$ without revealing it. The conversation has exactly three moves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Commit.&lt;/strong&gt; Pick a fresh random nonce $r$ and send the commitment $a = g^r$.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Challenge.&lt;/strong&gt; The verifier picks a random $e$ and sends it back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Respond.&lt;/strong&gt; Send $z = r + e\cdot x$.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The verifier accepts if and only if $g^z = a\cdot y^e$. Check the algebra: $g^{r + e x} = g^r\cdot (g^x)^e = a\cdot y^e$. It works whenever the prover really knows $x$.&lt;/p&gt;

A three-move public-coin proof -- commitment, then a random challenge, then a response -- in which the verifier checks a single equation. It must satisfy two properties: special soundness and special honest-verifier zero-knowledge. Schnorr&apos;s proof of knowledge of a discrete logarithm is the archetype [@damgard].

sequenceDiagram
    participant P as Prover (knows x)
    participant V as Verifier (knows y)
    Note over P: pick fresh random nonce r
    Note over P: commitment a holds g to the r
    P-&amp;gt;&amp;gt;V: send commitment a
    Note over V: pick random challenge e
    V-&amp;gt;&amp;gt;P: send challenge e
    Note over P: response z holds r plus e times x
    P-&amp;gt;&amp;gt;V: send response z
    Note over V: check g to the z equals a times y to the e
&lt;p&gt;Why is this a &lt;em&gt;proof&lt;/em&gt;, and why does it reveal nothing? Both answers are visible by inspection, which is exactly why Schnorr is the teaching example.&lt;/p&gt;
&lt;h3&gt;Two accepting transcripts hand you the secret&lt;/h3&gt;
&lt;p&gt;Suppose a prover produces two accepting conversations that share the same commitment $a$ but have different challenges $e_1 \ne e_2$, with responses $z_1$ and $z_2$. Then $g^{z_1} = a\cdot y^{e_1}$ and $g^{z_2} = a\cdot y^{e_2}$. Divide the two equations and the $a$ cancels: $g^{z_1 - z_2} = y^{e_1 - e_2}$. Since $y = g^x$, the exponents must match, and you can solve for the witness:&lt;/p&gt;
&lt;p&gt;$$x = \frac{z_1 - z_2}{e_1 - e_2} \pmod q.$$&lt;/p&gt;
&lt;p&gt;This is &lt;strong&gt;special soundness&lt;/strong&gt;, and the algorithm that recovers $x$ is called the &lt;em&gt;extractor&lt;/em&gt;. It is what makes Schnorr a proof of &lt;em&gt;knowledge&lt;/em&gt;: a prover who can answer two different challenges on the same commitment demonstrably possesses $x$, because we could run the extractor and read it out [@damgard].&lt;/p&gt;

The property that two accepting transcripts sharing a commitment but with different challenges yield the witness through an efficient extractor. It is what upgrades a Sigma protocol from &quot;the statement is true&quot; to &quot;the prover actually knows a witness&quot; [@damgard].
&lt;p&gt;Hold on to that extractor. In Section 6 we will watch an attacker run exactly this formula -- same $a$, two different $e$ values -- against a signer who reused a nonce, and read out the private key. The defender&apos;s soundness proof and the attacker&apos;s key-recovery are the same three lines of algebra.&lt;/p&gt;
&lt;h3&gt;One accepting transcript hands you nothing&lt;/h3&gt;
&lt;p&gt;Now run the trick the other way. Pick the response $z$ and the challenge $e$ &lt;em&gt;first&lt;/em&gt;, both at random, and then define the commitment as $a = g^z\cdot y^{-e}$. By construction $g^z = a\cdot y^e$, so this transcript passes verification perfectly -- and you built it without knowing $x$ at all. The distribution of $(a, e, z)$ produced this way is identical to a real run.&lt;/p&gt;

The property that any accepting transcript can be produced without the witness by choosing the response and challenge first and back-solving for the commitment. The algorithm that does this is the simulator, and its existence proves the exchange reveals nothing about the secret [@damgard].
&lt;p&gt;That algorithm is the &lt;em&gt;simulator&lt;/em&gt;, and its existence is the proof that the conversation leaks nothing about $x$: anything a verifier sees, it could have generated by itself. The whole edifice of zero knowledge -- the idea that &quot;reveals nothing&quot; can be made mathematically precise by demanding a simulator -- traces to Goldwasser, Micali, and Rackoff in 1985 [@gmr]. Part 1 covers those &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;security definitions&lt;/a&gt; in full; here we take them as the bedrock they are.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A Sigma protocol is one three-move conversation -- commit, challenge, respond -- whose two guarantees are both visible by inspection. Two accepting transcripts on the same commitment extract the secret (special soundness); a single transcript can be simulated without the secret (special honest-verifier zero knowledge). Two transcripts hand you the key; one transcript hands you nothing. That knife-edge is the entire game.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The distinction the extractor forces is worth naming. A Sigma protocol is a &lt;strong&gt;proof of knowledge&lt;/strong&gt; -- the prover provably &lt;em&gt;possesses&lt;/em&gt; a witness -- not merely a &lt;strong&gt;proof of membership&lt;/strong&gt;, which would only assert that the statement is true. The gap matters: &quot;this public key has a corresponding private key&quot; is trivially true for every well-formed key, but &quot;I know that private key&quot; is the thing a signature must establish.&lt;/p&gt;

A proof that the prover actually possesses a witness, formalized by the existence of an extractor that could recover the witness from a successful prover. It is strictly stronger than a proof of membership, which asserts only that the statement is true [@damgard].

The simulator is not just a proof device; it is a construction tool. To prove &quot;I know the secret behind public key A OR the one behind B&quot; without revealing which, the prover *honestly* runs the branch it can answer and *simulates* the branch it cannot -- choosing that branch&apos;s challenge and response first and back-solving for its commitment, exactly as SHVZK allows. A shared challenge, split between the branches, ties them together so a cheater cannot simulate both at once. That is the Cramer-Damgard-Schoenmakers OR-proof, and it is why the toolkit composes into disjunctions, ring signatures, and selective-disclosure credentials [@cds].
&lt;p&gt;Here is that whole story in code. The extractor is not a metaphor; it is a five-line function that turns two transcripts into a private key.&lt;/p&gt;
&lt;p&gt;{`
// A toy prime-order group: the order-q subgroup of the integers mod p.
// p = 2039 is prime, q = 1019 is prime, and g = 4 has order exactly q.
const p = 2039n, q = 1019n, g = 4n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;
function powmod(base, exp, m) {
  let r = 1n; base = mod(base, m);
  while (exp &amp;gt; 0n) { if (exp &amp;amp; 1n) r = mod(r * base, m); base = mod(base * base, m); exp &amp;gt;&amp;gt;= 1n; }
  return r;
}
const invModQ = (a) =&amp;gt; powmod(a, q - 2n, q);   // inverse mod prime q, via Fermat&lt;/p&gt;
&lt;p&gt;const x = 777n;                                 // the secret witness (never sent)
const y = powmod(g, x, p);                      // public key y = g^x&lt;/p&gt;
&lt;p&gt;// One honest run with nonce r and challenge e:
function transcript(r, e) {
  const a = powmod(g, r, p);                    // commit
  const z = mod(r + e * x, q);                  // respond
  return { a, e, z };
}
const verify = (t) =&amp;gt; powmod(g, t.z, p) === mod(t.a * powmod(y, t.e, p), p);&lt;/p&gt;
&lt;p&gt;const r = 314n;                                 // ONE nonce, reused across two challenges
const t1 = transcript(r, 11n);
const t2 = transcript(r, 90n);                  // same a, different e -- the fatal pair
console.log(&apos;t1 verifies = &apos; + verify(t1) + &apos;, t2 verifies = &apos; + verify(t2));&lt;/p&gt;
&lt;p&gt;// The extractor: x = (z1 - z2) / (e1 - e2) mod q
const xRecovered = mod((t1.z - t2.z) * invModQ(mod(t1.e - t2.e, q)), q);
console.log(&apos;recovered x = &apos; + xRecovered + &apos;, true x = &apos; + x + &apos;, match = &apos; + (xRecovered === x));
`}&lt;/p&gt;
&lt;p&gt;That is the knife-edge in code: two transcripts hand you the key, one hands you nothing -- so every guarantee and every failure ahead is a statement about one of the three moves. But this clean picture took eleven years and five separate ideas to assemble, and the cleanest idea of all sat frozen behind a patent.&lt;/p&gt;
&lt;h2&gt;3. Five Ideas and a Patent (1985-1996)&lt;/h2&gt;
&lt;p&gt;The three-move conversation was assembled one idea at a time, mostly by people who were not trying to build it. And the sharpest tension in the story is this: the cleanest, smallest scheme in the whole toolkit was invented in 1989 and could not be legally deployed at scale until its patent expired around 2008 [@schnorr-patent]. The best piece shipped last. Here is how five disconnected papers became a coherent toolkit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Goldwasser, Micali, and Rackoff (1985)&lt;/strong&gt; gave the field its vocabulary. Their paper on the knowledge complexity of interactive proof systems defined zero knowledge through the simulator argument -- a proof reveals nothing if a simulator can produce the same transcript without the secret [@gmr]. Without this definition, none of the three moves would be analyzable; &quot;reveals nothing&quot; would be a slogan rather than a theorem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fiat and Shamir (1986)&lt;/strong&gt; contributed two things in one paper. First, a practical zero-knowledge identification scheme based on factoring. Second, and far more consequential, the &lt;em&gt;transform&lt;/em&gt; that bears their name: replace the verifier&apos;s random challenge with the output of a hash function, and the interactive protocol collapses into a non-interactive one -- a signature [@fiat-shamir]. This is the birth of Move 2, and it arrived as a heuristic, a plausible trick without a proof. That gap between &quot;plausible&quot; and &quot;proven&quot; is where three decades of trouble would later live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schnorr (1989)&lt;/strong&gt; produced the efficient discrete-log archetype we dissected in Section 2: three moves, special soundness by inspection, and -- once you apply Fiat-Shamir -- the compact Schnorr signature.Schnorr&apos;s US Patent 4,995,082, granted in 1991, is widely reported to have expired around 2008. That is why patent-free DSA and later ECDSA colonized the internet through the 1990s, 2000s, and 2010s while Schnorr-proper waited, and why Bitcoin&apos;s BIP-340 could finally standardize native Schnorr signatures only in 2020 [@schnorr-patent], [@bip340]. Part 17 tells that &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;signature story&lt;/a&gt;. The irony is total: the scheme with the cleanest security argument was the one the market could not touch, so the industry standardized on messier alternatives and lived with them for twenty years.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pedersen (1991)&lt;/strong&gt; was working on verifiable secret sharing, not commitments, when he handed the field its Move 1 workhorse.The Pedersen commitment debuted as a &lt;em&gt;byproduct&lt;/em&gt; of a non-interactive, information-theoretically secure verifiable secret sharing scheme [@pedersen]. The toolkit&apos;s most-used commitment arrived as a side effect of a paper about something else -- a recurring pattern in this history. His commitment $C = g^m\cdot h^r$ is perfectly hiding, computationally binding, and additively homomorphic [@pedersen] -- three properties we will spend all of Section 4 unpacking, because the entire range-proof line stands on the third one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cramer, Damgard, and Schoenmakers (1994)&lt;/strong&gt; turned Schnorr from a single protocol into a toolkit. Their proofs-of-partial-knowledge construction composes Sigma protocols with AND and OR: prove you know &lt;em&gt;one&lt;/em&gt; of two secrets without revealing which, by honestly answering the branch you know and &lt;em&gt;simulating&lt;/em&gt; the branch you do not [@cds].The name &quot;Sigma protocol&quot; was coined later, in Cramer&apos;s 1996 doctoral thesis. The letter is said to trace the three-move zig-zag of the conversation -- commitment down, challenge across, response back -- though the mnemonic is folklore. The OR-proof trick reuses the very simulator that proves zero knowledge, now as a constructive building block [@cds]. This is the deep reuse the article keeps returning to -- the simulator that proves a single proof leaks nothing is the same object that fakes the clauses you cannot answer in a disjunction. Okamoto&apos;s 1992 identification scheme sharpens the point: it is the witness-indistinguishable sibling of Schnorr, with a two-generator, Pedersen-shaped first move $y = g^{x_1} h^{x_2}$.Because many witness pairs $(x_1, x_2)$ open the same $y = g^{x_1} h^{x_2}$, an Okamoto transcript never reveals &lt;em&gt;which&lt;/em&gt; pair the prover used -- the scheme is witness-indistinguishable by construction [@okamoto]. That is exactly the property a CDS OR-proof leans on to hide which clause is the real one, and it is why the Pedersen shape shows up again the moment you want to hide a witness among many.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pointcheval and Stern (1996)&lt;/strong&gt; finally proved that Fiat-Shamir signatures are unforgeable -- in the random-oracle model -- with the forking lemma: rewind a successful forger, feed it a different challenge on the same commitment, and its two answers extract the discrete log, contradicting hardness [@pointcheval-stern-96], [@pointcheval-stern]. The proof works, but it is &lt;em&gt;loose&lt;/em&gt;: it loses a quadratic factor, and it does not extend cleanly to many sessions running at once. That looseness is not a blemish to be polished away later -- it is the first visible crack of the concurrency failure that Section 6 tears wide open.&lt;/p&gt;

flowchart LR
    A[&quot;1982 Chaum blinding&quot;] --&amp;gt; B[&quot;1985 GMR zero knowledge&quot;]
    B --&amp;gt; C[&quot;1986 Fiat-Shamir transform&quot;]
    C --&amp;gt; D[&quot;1989 Schnorr identification&quot;]
    D --&amp;gt; E[&quot;1991 Pedersen commitment&quot;]
    E --&amp;gt; F[&quot;1994 CDS OR-proofs&quot;]
    F --&amp;gt; G[&quot;1996 forking lemma&quot;]
    G --&amp;gt; H[&quot;2018 Bulletproofs&quot;]
    H --&amp;gt; I[&quot;2021 ROS and MuSig2&quot;]
    I --&amp;gt; J[&quot;2024 FROST RFC 9591&quot;]
    J --&amp;gt; K[&quot;2026 BBS drafts&quot;]
&lt;p&gt;By 1996 the conversation was complete and had a name. The rest of the story is what happened when people started &lt;em&gt;building&lt;/em&gt; with it -- and the failures organize themselves, cleanly, one per move. Start with the move you make first: the commitment.&lt;/p&gt;
&lt;h2&gt;4. Move 1: What You Bind&lt;/h2&gt;
&lt;p&gt;Every proof begins by pinning down a value you are not yet ready to reveal. A commitment is the cryptographic envelope: you seal a value now and open it later, and the sealing must satisfy two opposing demands. It must be &lt;strong&gt;hiding&lt;/strong&gt; -- the sealed envelope leaks nothing about what is inside -- and &lt;strong&gt;binding&lt;/strong&gt; -- you cannot swap the contents after sealing. There are two standard ways to build one, and they sit at opposite corners of a trade-off you cannot escape.&lt;/p&gt;

A protocol to lock in a value now and reveal it later, such that the commitment leaks nothing about the value (hiding) and cannot be opened to a different value than the one committed (binding). No scheme can make both properties perfect at once [@damgard].
&lt;h3&gt;The Pedersen corner&lt;/h3&gt;
&lt;p&gt;Pedersen&apos;s commitment lives in a group of prime order $q$ with two generators $g$ and $h$, where nobody knows the discrete logarithm $\log_g h$. To commit to a message $m$, pick a random blinding scalar $r$ and publish&lt;/p&gt;
&lt;p&gt;$$C = g^m\cdot h^r.$$&lt;/p&gt;
&lt;p&gt;It has three properties, and each one matters downstream [@pedersen]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Perfectly hiding.&lt;/strong&gt; Because $r$ is uniform, $h^r$ is a uniform group element, so $C$ is uniformly distributed regardless of $m$. Even an adversary with unlimited computing power learns &lt;em&gt;nothing&lt;/em&gt; about $m$ from $C$. This is information-theoretic, not merely computational.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Computationally binding.&lt;/strong&gt; Opening $C$ to two different messages means finding $(m, r) \ne (m&apos;, r&apos;)$ with $g^m h^r = g^{m&apos;} h^{r&apos;}$, which rearranges to $\log_g h = (m - m&apos;)/(r&apos; - r)$. In other words, breaking binding &lt;em&gt;is&lt;/em&gt; computing a discrete logarithm. The binding reduction is not near the hard problem; it &lt;em&gt;is&lt;/em&gt; the hard problem [@pedersen].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Additively homomorphic.&lt;/strong&gt; Multiply two commitments and the exponents add: $C(m_1, r_1)\cdot C(m_2, r_2) = C(m_1 + m_2,\ r_1 + r_2)$. You can add committed values without opening them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That third property is the one the whole range-proof line stands on, so hold it in mind. But first, the precision that must never be smoothed over.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A Pedersen commitment is perfectly &lt;em&gt;hiding&lt;/em&gt; and only &lt;em&gt;computationally&lt;/em&gt; binding. Anyone who knows the trapdoor $\log_g h$ can &lt;strong&gt;equivocate&lt;/strong&gt;: open a single commitment to any message they like, by solving one linear equation for a matching blinder. The security of the whole scheme rests on nobody knowing that trapdoor -- which makes the generation of $h$ a security-critical step, not a formality.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Equivocation is the entire Move-1 failure class. If the party who set up the parameters secretly chose $h = g^t$ and kept $t$, then for any commitment $C = g^m h^r$ they can produce a blinder $r&apos;$ that opens it to a different message $m&apos;$ -- because $C = g^{m + t r}$, and they can solve $m&apos; + t r&apos; \equiv m + t r$ for $r&apos;$. The defense is to make the trapdoor un-knowable to everyone.The second generator $h$ must have unknown $\log_g h$, so it is derived by a nothing-up-my-sleeve procedure -- hash a fixed string to a curve point, so that no party could have planted a trapdoor. Part 16 covers &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;hashing a string into the group&lt;/a&gt;; the takeaway here is that &quot;pick $h$ honestly&quot; is a real engineering requirement, not a throwaway line.&lt;/p&gt;
&lt;h3&gt;The hash corner&lt;/h3&gt;
&lt;p&gt;The dual construction throws away the algebra. Commit to $m$ by publishing &lt;code&gt;H(m ‖ r)&lt;/code&gt; for a random $r$ and a collision-resistant hash &lt;code&gt;H&lt;/code&gt;. This is computationally hiding and computationally binding -- and its binding rests on collision resistance, not on the discrete logarithm, which makes it the post-quantum-friendly choice. What it gives up is homomorphism: there is no way to add two hash commitments and get a commitment to the sum. Hashes shred structure by design.&lt;/p&gt;
&lt;p&gt;Why can you not have everything -- perfect hiding &lt;em&gt;and&lt;/em&gt; perfect binding? Because it is impossible. If a commitment is perfectly hiding, then $C$ is statistically independent of $m$, which means some opening to every message must exist; a computationally unbounded opener could always equivocate, so binding cannot also be perfect [@damgard]. Pedersen sits at the perfectly-hiding end; hash commitments sit near the perfectly-binding end; nothing sits at both.&lt;/p&gt;

flowchart TD
    Q{&quot;What does the job need?&quot;}
    Q --&amp;gt;|&quot;add or range-prove values&quot;| PED[&quot;Pedersen commitment g to the m times h to the r&quot;]
    Q --&amp;gt;|&quot;post-quantum binding, no algebra&quot;| HASH[&quot;Hash commitment H of m with r&quot;]
    PED --&amp;gt; P1[&quot;perfectly hiding&quot;]
    PED --&amp;gt; P2[&quot;computationally binding on the discrete log&quot;]
    PED --&amp;gt; P3[&quot;additively homomorphic&quot;]
    PED --&amp;gt; P4[&quot;trapdoor holder can equivocate&quot;]
    HASH --&amp;gt; H1[&quot;computationally hiding and binding&quot;]
    HASH --&amp;gt; H2[&quot;not homomorphic&quot;]
    HASH --&amp;gt; H3[&quot;post-quantum from collision resistance&quot;]
&lt;p&gt;Here is the corner choice as a table you can keep next to a design review.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Pedersen &lt;code&gt;g^m · h^r&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Hash &lt;code&gt;H(m ‖ r)&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Hiding&lt;/td&gt;
&lt;td&gt;Perfect (information-theoretic)&lt;/td&gt;
&lt;td&gt;Computational&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Binding&lt;/td&gt;
&lt;td&gt;Computational (discrete log)&lt;/td&gt;
&lt;td&gt;Computational (collision resistance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homomorphic&lt;/td&gt;
&lt;td&gt;Yes, additively&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-quantum&lt;/td&gt;
&lt;td&gt;No -- Shor breaks binding&lt;/td&gt;
&lt;td&gt;Yes -- binding survives Shor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commitment size&lt;/td&gt;
&lt;td&gt;~32 bytes (one ristretto255 point)&lt;/td&gt;
&lt;td&gt;~32 bytes (one SHA-256 output)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equivocation risk&lt;/td&gt;
&lt;td&gt;Trapdoor holder of &lt;code&gt;log_g h&lt;/code&gt; opens to anything&lt;/td&gt;
&lt;td&gt;None -- needs a hash collision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;Confidential amounts, range proofs, sums&lt;/td&gt;
&lt;td&gt;PQ binding, timestamps, no algebra needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The homomorphism is not a curiosity; it is the concrete cash-in of Move 1. Committed values that add correctly are exactly what confidential transactions need -- inputs and outputs whose hidden amounts must balance -- and they are the value commitments that a range proof later proves lie in a valid range. Section 7 follows that thread from Pedersen to Bulletproofs. First, watch the homomorphism and the equivocation trapdoor in code.&lt;/p&gt;
&lt;p&gt;{`
const p = 2039n, q = 1019n, g = 4n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;
function powmod(base, exp, m) {
  let r = 1n; base = mod(base, m);
  while (exp &amp;gt; 0n) { if (exp &amp;amp; 1n) r = mod(r * base, m); base = mod(base * base, m); exp &amp;gt;&amp;gt;= 1n; }
  return r;
}
// In a real system NOBODY knows t; we plant it here only to demonstrate equivocation.
const t = 503n;                                  // the trapdoor: t = log_g h
const h = powmod(g, t, p);
const commit = (m, r) =&amp;gt; mod(powmod(g, m, p) * powmod(h, r, p), p);&lt;/p&gt;
&lt;p&gt;// 1) Additive homomorphism: C(m1,r1) * C(m2,r2) opens to (m1+m2, r1+r2)
const C1 = commit(100n, 7n), C2 = commit(250n, 9n);
const product = mod(C1 * C2, p);
const summed  = commit(mod(100n + 250n, q), mod(7n + 9n, q));
console.log(&apos;homomorphism holds = &apos; + (product === summed));&lt;/p&gt;
&lt;p&gt;// 2) Equivocation: the trapdoor holder opens ONE commitment to a different message
const m = 100n, r = 7n, C = commit(m, r);
const mFake = 999n;
const rFake = mod(r + (m - mFake) * powmod(t, q - 2n, q), q);   // solve m&apos;+t r&apos; = m+t r
console.log(&apos;same C reopened to &apos; + mFake + &apos; = &apos; + (commit(mFake, rFake) === C));
`}&lt;/p&gt;
&lt;p&gt;A Pedersen commitment binds a value the verifier cannot see. But an interactive proof about it needs a live verifier to send a random challenge -- and live verifiers do not scale to signatures or blockchains. The fix is to fake the verifier with a hash. That single fix is the most-broken move in the toolkit.&lt;/p&gt;
&lt;h2&gt;5. Move 2: What Goes Into the Hash&lt;/h2&gt;
&lt;p&gt;The transform that makes the whole toolkit shippable is one line of code -- and that one line has been written wrong, in production, for thirty years.&lt;/p&gt;
&lt;p&gt;The interactive Sigma protocol needs a live verifier to supply a random challenge $e$. That is fine for a login handshake and useless for a signature or a blockchain, where there is no verifier standing by. Fiat and Shamir&apos;s fix was to &lt;em&gt;derive&lt;/em&gt; the challenge from a hash of the transcript so far: compute $e = H(\dots)$ yourself and proceed [@fiat-shamir]. In the random-oracle model -- where the hash is treated as a truly random function -- the hash &lt;em&gt;is&lt;/em&gt; the verifier, and the interactive proof becomes a non-interactive one.&lt;/p&gt;

A method to make an interactive Sigma protocol non-interactive by replacing the verifier&apos;s random challenge with a hash of the transcript. The prover computes the challenge itself, turning a proof of knowledge into a signature or an on-chain proof [@fiat-shamir].

An idealization that treats a hash function as a truly random function answering consistently to repeated queries. It is the model in which Fiat-Shamir soundness is provable; Part 10 covers the [hash machinery](/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp/) it idealizes [@fiat-shamir].
&lt;p&gt;Everything now rides on what you put inside that hash. And this is where the failures cluster, because the answer is not obvious and the wrong answer still runs.&lt;/p&gt;
&lt;h3&gt;The decision rule&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Strong Fiat-Shamir&lt;/strong&gt; binds everything the proof is about:&lt;/p&gt;
&lt;p&gt;$$e = H(\texttt{domain_separator} ,|, \text{statement} ,|, \text{public params} ,|, a ,|, \text{transcript}).$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weak Fiat-Shamir&lt;/strong&gt; hashes only the commitment, $e = H(a)$, and omits the statement. That omission is the bug. When the challenge does not depend on the statement, a proof built for one statement can be transplanted onto another -- a statement it was never made for -- and it still verifies [@bpw]. The extreme case is a signature: if the message is not in the hash, a signature on &quot;pay 5&quot; is a valid signature on &quot;pay 5000,&quot; because verification never touches the message.&lt;/p&gt;

flowchart TD
    A[&quot;Sigma commitment a and statement x&quot;] --&amp;gt; B{&quot;What enters the challenge hash?&quot;}
    B --&amp;gt;|&quot;weak FS: only a&quot;| W[&quot;e is H of a&quot;]
    B --&amp;gt;|&quot;strong FS: statement, params, a&quot;| S[&quot;e is H of x with params and a&quot;]
    W --&amp;gt; W2[&quot;challenge ignores the statement&quot;]
    W2 --&amp;gt; W3[&quot;transplant the proof onto a false x prime&quot;]
    W3 --&amp;gt; W4[&quot;forgery verifies&quot;]
    S --&amp;gt; S2[&quot;challenge binds the exact statement&quot;]
    S2 --&amp;gt; S3[&quot;transplant recomputes a different e&quot;]
    S3 --&amp;gt; S4[&quot;forgery rejected&quot;]
&lt;p&gt;You can watch the transplant happen and then watch the fix stop it. In the model below, the &quot;statement&quot; is the signed message; weak Fiat-Shamir lets one signature verify against any message, and strong Fiat-Shamir binds it.&lt;/p&gt;
&lt;p&gt;{`
const p = 2039n, q = 1019n, g = 4n;
const mod = (a, m) =&amp;gt; ((a % m) + m) % m;
function powmod(base, exp, m) {
  let r = 1n; base = mod(base, m);
  while (exp &amp;gt; 0n) { if (exp &amp;amp; 1n) r = mod(r * base, m); base = mod(base * base, m); exp &amp;gt;&amp;gt;= 1n; }
  return r;
}
function H(str) { let h = 0n; for (const ch of str) h = mod(h * 131n + BigInt(ch.charCodeAt(0)), q); return mod(h, q); }
const x = 777n, y = powmod(g, x, p);
const r = 421n;   // nonce hardcoded only so the demo is deterministic&lt;/p&gt;
&lt;p&gt;// WEAK Fiat-Shamir: the challenge hashes only the commitment a (message omitted)
const signWeak = () =&amp;gt; { const a = powmod(g, r, p); const e = H(a.toString()); return { a, z: mod(r + e * x, q) }; };
const verifyWeak = (msg, s) =&amp;gt; { const e = H(s.a.toString()); return powmod(g, s.z, p) === mod(s.a * powmod(y, e, p), p); };
const sigW = signWeak();
console.log(&apos;weak  valid on original  = &apos; + verifyWeak(&apos;alice pays 5&apos;, sigW));
console.log(&apos;weak  valid on FORGED    = &apos; + verifyWeak(&apos;alice pays 5000&apos;, sigW));  // true -&amp;gt; universal forgery&lt;/p&gt;
&lt;p&gt;// STRONG Fiat-Shamir: the challenge binds the public key AND the message AND a
const signStrong = (msg) =&amp;gt; { const a = powmod(g, r, p); const e = H(y.toString() + &apos;|&apos; + msg + &apos;|&apos; + a.toString()); return { a, z: mod(r + e * x, q) }; };
const verifyStrong = (msg, s) =&amp;gt; { const e = H(y.toString() + &apos;|&apos; + msg + &apos;|&apos; + s.a.toString()); return powmod(g, s.z, p) === mod(s.a * powmod(y, e, p), p); };
const sigS = signStrong(&apos;alice pays 5&apos;);
console.log(&apos;strong valid on original = &apos; + verifyStrong(&apos;alice pays 5&apos;, sigS));
console.log(&apos;strong valid on FORGED   = &apos; + verifyStrong(&apos;alice pays 5000&apos;, sigS)); // false -&amp;gt; rejected
`}&lt;/p&gt;
&lt;h3&gt;The same bug, forged twice, a decade apart&lt;/h3&gt;
&lt;p&gt;This is not a hypothetical. It is the single most-repeated real-world break in the toolkit, and it has a body count.&lt;/p&gt;
&lt;p&gt;In 2012, Bernhard, Pereira, and Warinschi formalized the weak-versus-strong distinction and showed that the Helios internet-voting system used weak Fiat-Shamir. Its ballot proofs -- the &quot;zero-knowledge&quot; proofs that a vote is well-formed -- could be transplanted and forged [@bpw]. The paper&apos;s title is a warning label: &lt;em&gt;How Not to Prove Yourself&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Ten years later, in April 2022, Trail of Bits disclosed the same class of bug in three live systems at once: Girault&apos;s proof of knowledge, Bulletproofs, and PlonK [@tob-part1], [@tob-girault], [@tob-bp], [@tob-plonk]. The Bulletproofs instance was traced to an insecure Fiat-Shamir generation recommended in the original 2017 paper, and it carried CVE-2022-29566 [@tob-bp], [@cve], [@bulletproofs].Note the attribution carefully: &quot;Girault&quot; is the name of an &lt;em&gt;affected scheme&lt;/em&gt; (a proof of knowledge in an RSA group), not the researcher. The disclosure was by Jim Miller at Trail of Bits [@tob-part1], [@tob-girault].&lt;/p&gt;

&quot;We&apos;ve dubbed this class of vulnerabilities Frozen Heart. The word frozen is an acronym for FoRging Of ZEro kNowledge proofs ... a mistake in the original academic paper, in which the authors recommend an insecure Fiat-Shamir generation.&quot; -- Trail of Bits [@tob-part1]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Helios in 2012 and Frozen Heart in 2022 are the same failure: a challenge hash that omitted the statement. In neither case did anyone break the discrete logarithm. The security lived in one line of the hash, and that line was wrong -- in a shipped e-voting system and in a peer-reviewed range-proof paper -- ten years apart [@bpw], [@tob-part1].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Bind the whole statement and the challenge is safe. But Fiat-Shamir also turns the Schnorr &lt;em&gt;identification&lt;/em&gt; protocol into the Schnorr &lt;em&gt;signature&lt;/em&gt; -- and that hands the third move, the response, a fresh way to leak. This one does not even need a mistake in the hash. It needs a mistake in a single random number.&lt;/p&gt;
&lt;h2&gt;6. Move 3: How the Response Behaves&lt;/h2&gt;
&lt;p&gt;The response $z = r + e\cdot x$ is the only message in the whole conversation that touches the secret, and it protects that secret with exactly one thing: a fresh, secret, one-time nonce $r$. Everything the third move can suffer is a story about that nonce -- a number that is never even transmitted, yet is where the key most often escapes.&lt;/p&gt;
&lt;p&gt;Start with the bridge that Fiat-Shamir just built. Apply the transform to Schnorr&apos;s identification protocol and you get the Schnorr signature: the commitment $a$ and response $z$ are the signature, and the challenge is $e = H(y ,|, m ,|, a)$. Read that carefully, because it is one of the article&apos;s quiet revelations: &lt;strong&gt;a Schnorr signature is a non-interactive proof of knowledge of the private key.&lt;/strong&gt; Signing is proving you know $x$; the message rides along in the challenge hash [@bip340].&lt;/p&gt;
&lt;h3&gt;Reuse the nonce, lose the key&lt;/h3&gt;
&lt;p&gt;Now recall the extractor from Section 2. Two accepting transcripts that share the commitment $a$ but differ in the challenge $e$ yield the witness $x = (z_1 - z_2)/(e_1 - e_2)$. That was the &lt;em&gt;defender&apos;s&lt;/em&gt; proof that Schnorr is a proof of knowledge. Turn it around: if a signer produces two signatures with the same nonce $r$, they produce two transcripts with the same $a = g^r$ and different challenges. The attacker runs the same three lines of algebra the extractor runs, and the private key falls out.&lt;/p&gt;
&lt;p&gt;This is the identical failure as a reused ECDSA nonce -- the catastrophe covered in Parts 8 and 17 -- because both signature schemes hide the key behind a per-signature nonce, and both surrender it the moment the nonce repeats.There is a sharp distinction worth carrying. The reused-nonce break in &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;Part 8&lt;/a&gt; happens &lt;em&gt;within&lt;/em&gt; a single scheme when one nonce is used for two signatures. The ROS break below happens &lt;em&gt;across&lt;/em&gt; many concurrent sessions of a multi-party scheme, exploiting the linearity of the response rather than a literal repeat. Same move, two different exploits. A biased nonce -- not repeated, just slightly non-uniform -- leaks the key more slowly, through lattice attacks that aggregate many partial leaks. The discipline is absolute: nonces must be fresh, secret, and uniform, whether drawn from a &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;well-seeded CSPRNG or derived deterministically&lt;/a&gt; as Part 2 describes.&lt;/p&gt;
&lt;h3&gt;Why concurrency is the hard part&lt;/h3&gt;
&lt;p&gt;Pointcheval and Stern&apos;s forking lemma proves single-session Schnorr unforgeable in the random-oracle model: rewind a forger, give it a fresh challenge on the same commitment, and its two answers extract the discrete log, which is assumed hard [@pointcheval-stern]. The proof works, but it pays a price -- a quadratic loss in tightness -- and, more importantly, the rewinding trick does not compose when many sessions run at once. You cannot cleanly rewind one of a hundred interleaved conversations without disturbing the rest.&lt;/p&gt;

The rewinding technique of Pointcheval and Stern that turns a random-oracle forger into a discrete-log solver by running it twice with different challenges on the same commitment. It gives Schnorr and Fiat-Shamir their ROM security proof, but loosely -- with a quadratic security loss -- and only for the single-session setting [@pointcheval-stern].
&lt;p&gt;That gap between &quot;one session is provably secure&quot; and &quot;many concurrent sessions are secure&quot; stayed hidden for two decades. Then it was pried open.&lt;/p&gt;
&lt;h3&gt;The ROS attack&lt;/h3&gt;
&lt;p&gt;In 2021, Benhamouda, Lepoint, Loss, Orru, and Raykova published a polynomial-time solution to the ROS problem, and with it broke naive two-round Schnorr multisignatures and blind Schnorr signatures [@ros]. The mechanism is pure linear algebra. Open enough concurrent signing sessions -- more than about $\log_2 p$ of them -- collect the responses, and set up a linear system whose solution forges a signature on a message the signers never agreed to. No discrete logarithm is solved anywhere in the attack.&lt;/p&gt;

&quot;Random inhomogeneities in an Overdetermined, Solvable system of linear equations.&quot; It is solvable in polynomial time once the number of concurrent sessions exceeds roughly $\log_2 p$, which is exactly what breaks naive linear Schnorr multi-signatures and blind signatures. It is emphatically NOT &quot;Random Oracle Substitution,&quot; a common web mistranslation [@ros].
&lt;p&gt;The 2021 result had a predecessor. In 2019, Drijvers and co-authors had already shown, at IEEE S&amp;amp;P, that the same naive two-round multisignatures were insecure under concurrency -- their attack ran in sub-exponential time using Wagner&apos;s generalized-birthday (k-sum) algorithm [@drijvers], [@wagner]. The 2021 ROS work sharpened sub-exponential into polynomial. Together they closed the case: naive linear Schnorr multisignatures are not merely hard to prove secure under concurrency; they are concretely broken.&lt;/p&gt;

flowchart TD
    Z[&quot;response z holds r plus e times x&quot;] --&amp;gt; B{&quot;how is the nonce misused?&quot;}
    B --&amp;gt;|&quot;reuse or bias one nonce&quot;| R1[&quot;two transcripts share commitment a&quot;]
    R1 --&amp;gt; R2[&quot;extractor recovers x from two responses&quot;]
    B --&amp;gt;|&quot;open many concurrent sessions&quot;| C1[&quot;responses are linear across sessions&quot;]
    C1 --&amp;gt; C2[&quot;ROS solves the linear system&quot;]
    C2 --&amp;gt; C3[&quot;forge a signature, no discrete log solved&quot;]

No discrete log is ever solved. The response simply leaks across concurrent sessions, and linear algebra does the rest.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The simple &quot;everyone sends a nonce, everyone sends a response&quot; two-round multisignature is forgeable under concurrent sessions -- sub-exponentially since Drijvers (S&amp;amp;P 2019), and in polynomial time since ROS (2021). Use MuSig2 (n-of-n) or FROST (t-of-n), which are built specifically to defeat this attack [@drijvers], [@ros].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Move 1 can be equivocated, Move 2 can be transplanted, Move 3 can be solved by linear algebra -- and not one of these touched the discrete logarithm. That is the thesis, proven three times. The story from here is how each sub-lineage climbed &lt;em&gt;out&lt;/em&gt; of its break, while the three moves kept running underneath.&lt;/p&gt;
&lt;h2&gt;7. What Superseded What (and What Never Did)&lt;/h2&gt;
&lt;p&gt;Here is the classic mistake this article exists to correct: there is no single &quot;what superseded what&quot; ladder for this toolkit. The three moves are a &lt;em&gt;substrate&lt;/em&gt;. Supersession happens in the &lt;em&gt;composites&lt;/em&gt; built on top of them, and there are exactly &lt;strong&gt;three construction ladders&lt;/strong&gt; -- one per application: range proofs, multi-party Schnorr, and anonymous credentials. Every rung was forced by a break or a cost on a specific move, never by an advance against the discrete logarithm.&lt;/p&gt;
&lt;p&gt;The one change to the foundation itself -- weak Fiat-Shamir hardened to strong -- is not a fourth ladder but a fix applied in place, so we take it first.&lt;/p&gt;

flowchart TD
    subgraph FOUND[&quot;Foundations, never superseded, Move 2 hardened weak to strong in place&quot;]
        F1[&quot;Move 1 Pedersen&quot;]
        F2[&quot;Move 2 Fiat-Shamir, hardened weak to strong&quot;]
        F3[&quot;Move 3 Schnorr response&quot;]
    end
    FOUND --&amp;gt; RP[&quot;Range proofs&quot;]
    FOUND --&amp;gt; MP[&quot;Multi-party Schnorr&quot;]
    FOUND --&amp;gt; AC[&quot;Anonymous credentials&quot;]
    RP --&amp;gt; RP1[&quot;Borromean 2015&quot;]
    RP1 --&amp;gt;|&quot;superseded by&quot;| RP2[&quot;Bulletproofs 2018&quot;]
    RP2 --&amp;gt;|&quot;superseded by&quot;| RP3[&quot;Bulletproofs plus 2022&quot;]
    MP --&amp;gt; MP1[&quot;naive two-round&quot;]
    MP1 --&amp;gt;|&quot;superseded by&quot;| MP2[&quot;three-round&quot;]
    MP2 --&amp;gt;|&quot;superseded by&quot;| MP3[&quot;MuSig2 and FROST&quot;]
    AC --&amp;gt; AC1[&quot;Chaum blind signature&quot;]
    AC1 --&amp;gt;|&quot;superseded by&quot;| AC2[&quot;CL-Idemix and U-Prove&quot;]
    AC2 --&amp;gt;|&quot;shipped instead&quot;| AC3[&quot;KVAC, BBS, Privacy Pass&quot;]
&lt;h3&gt;Foundation hardening: weak Fiat-Shamir becomes strong&lt;/h3&gt;
&lt;p&gt;The one part of the foundation that changed was never superseded -- it was hardened in place. Weak Fiat-Shamir was the default misreading of the heuristic; Helios in 2012 and Frozen Heart in 2022 forged proofs under it [@bpw], [@tob-part1], [@cve]. Strong Fiat-Shamir -- bind the whole statement -- is what the random-oracle proof had silently assumed all along. The two generations differ only in what enters one hash, and the discrete logarithm is untouched in both. Move 2 was not replaced by a better construction; it was corrected to the instantiation it always needed.&lt;/p&gt;
&lt;h3&gt;Ladder A -- range proofs: cashing in the homomorphism&lt;/h3&gt;
&lt;p&gt;This ladder cashes in Move 1&apos;s homomorphism, and it is the one with dramatic size numbers. The textbook starting point is a bit-decomposition OR-proof: to show a committed value lies in $[0, 2^n)$, prove each of its $n$ bits is either 0 or 1 with an OR-proof. It works and it is $O(n)$ -- linear in the bit width, which is a lot of bytes.&lt;/p&gt;
&lt;p&gt;Monero&apos;s RingCT initially shipped Borromean ring range proofs (Maxwell and Poelstra, 2015), which packed the bit proofs into ring signatures at roughly 6 kB per output -- the dominant cost of a confidential transaction [@monero]. Then &lt;strong&gt;Bulletproofs&lt;/strong&gt; (Bunz, Bootle, Boneh, Poelstra, Wuille, and Maxwell, 2018) reduced the range statement to an inner-product relation and compressed it with a recursive argument that halves the vectors each round, yielding an $O(\log n)$ proof of about 600 to 700 bytes -- with &lt;strong&gt;no trusted setup&lt;/strong&gt; [@bulletproofs], [@monero].&lt;/p&gt;
&lt;p&gt;Monero&apos;s own documentation reports the deployment cut transaction size by at least 80 percent [@monero]. &lt;strong&gt;Bulletproofs+&lt;/strong&gt; (2022) shrank the constant further with a weighted inner-product argument [@bulletproofs-plus].&lt;/p&gt;
&lt;p&gt;The honest footnote on this ladder is the one Section 5 set up: Bulletproofs also &lt;em&gt;broke&lt;/em&gt;, via weak Fiat-Shamir, as CVE-2022-29566 [@tob-bp], [@cve]. That was a Move-2 failure inside a Move-1 construction, and it was fixed by strong Fiat-Shamir -- not by Bulletproofs+. The size ladder and the security fix are independent axes on the same object.&lt;/p&gt;
&lt;h3&gt;Ladder B -- multi-party Schnorr: climbing out of the ROS break&lt;/h3&gt;
&lt;p&gt;This ladder is Move 3 recovering from the concurrency break of Section 6. The naive two-round multisignature -- everyone sums nonces, everyone sums responses -- fell to Drijvers (2019) and then ROS (2021) [@drijvers], [@ros]. The first fix was to spend a round: three-round commit-then-reveal schemes make each signer commit to its nonce before revealing it, which restores concurrent security at the cost of an extra round trip [@musig2].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MuSig2&lt;/strong&gt; (Nick, Ruffing, and Seurin, 2021) got back to two rounds by a clever trick: each signer publishes &lt;em&gt;two&lt;/em&gt; nonces, and the effective nonce is a hash-weighted combination of them, so the combined nonce is a &lt;em&gt;nonlinear&lt;/em&gt; function of the inputs -- exactly what defeats the linear-algebra ROS forgery [@musig2]. &lt;strong&gt;FROST&lt;/strong&gt; (standardized as RFC 9591 in June 2024) then added genuine threshold signing [@rfc9591].&lt;/p&gt;
&lt;p&gt;The distinction that matters: MuSig2, standardized as BIP-327, is &lt;strong&gt;n-of-n&lt;/strong&gt; -- every signer must participate, and the output is a single 64-byte signature indistinguishable from a single-key one [@bip327], [@musig2]. FROST is the &lt;strong&gt;t-of-n threshold&lt;/strong&gt; scheme, where any $t$ of $n$ signers suffice [@rfc9591]. They are different tools for different policies, not two names for the same thing.&lt;/p&gt;
&lt;h3&gt;Ladder C -- anonymous credentials: the elegant generation that lost&lt;/h3&gt;
&lt;p&gt;This ladder is the strange one, because the most elegant generation is the one that did &lt;em&gt;not&lt;/em&gt; win. It begins with Chaum&apos;s blind signatures -- blind, let the issuer sign, unblind -- introduced in 1982 [@chaum-blind] and set out as a whole transaction system in his 1985 paper, the root of unlinkable credentials, though single-show [@chaum-cacm]. It climbs to the multi-attribute systems: Camenisch-Lysyanskaya (Idemix) and Brands (U-Prove), which let a holder prove statements about many attributes and were genuinely beautiful cryptography [@cl-2001], [@cl-2004], [@uprove]. And then, mostly, it did not ship.&lt;/p&gt;

Camenisch-Lysyanskaya / Idemix and Brands / U-Prove were the theoretically complete multi-attribute credentials of the 2000s -- standardized (U-Prove under ISO/IEC 18370-2 and released under Microsoft&apos;s Open Specification Promise) and mathematically lovely [@uprove], [@cl-2001], [@cl-2004]. Yet they were largely never deployed at population scale, losing on cost, complexity, and integration friction. The systems that shipped instead were pragmatic ones -- Signal&apos;s keyed-verification credentials, BBS, and Privacy Pass. This is the rare case in cryptography where the most elegant design is not the one that wins -- a deployment failure, not a cryptographic break. The exception proves the rule: Direct Anonymous Attestation, a Camenisch-Lysyanskaya-family credential presented through a Sigma proof of possession, shipped in billions of TPM security chips -- precisely because its target was fixed hardware with a captive issuer, exactly the friction the general-purpose systems could not overcome [@daa], [@tpm2-spec].
&lt;p&gt;The winners are three deployed corners, not one. &lt;strong&gt;Signal KVAC&lt;/strong&gt; (2020) is a keyed-verification credential where the issuer is also the verifier, built on an algebraic MAC and a Sigma-protocol presentation proof, deployed in Signal&apos;s private groups [@signal], [@kvac]. &lt;strong&gt;BBS&lt;/strong&gt; signatures are the publicly-verifiable corner, a multi-message credential now in IETF drafts [@bbs-draft].&lt;/p&gt;
&lt;p&gt;And &lt;strong&gt;Privacy Pass / Apple Private Access Tokens&lt;/strong&gt;, standardized at the IETF in June 2024, are the unlinkable single-use authorization-token corner -- the deployed realization of Chaum&apos;s blind-token idea, in which a client redeems an anonymously issued token at an origin that cannot link it back to issuance [@rfc9576], [@cloudflare-pat]. All three are Sigma proofs over the same three moves, filled in for three different trust models; Section 8 attaches the parameters to each.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every real-world break lives on one of the three moves. The discrete logarithm was never broken. The commitment equivocates, the challenge transplants, the response leaks -- and the composites climb their ladders while Pedersen, Fiat-Shamir, and Schnorr keep running underneath, never superseded.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the whole picture as one matrix.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Lineage / move&lt;/th&gt;
&lt;th&gt;Key idea&lt;/th&gt;
&lt;th&gt;Size or cost&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;Weak Fiat-Shamir&lt;/td&gt;
&lt;td&gt;1986&lt;/td&gt;
&lt;td&gt;Move 2&lt;/td&gt;
&lt;td&gt;hash only the commitment&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Superseded (insecure)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strong Fiat-Shamir&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;Move 2&lt;/td&gt;
&lt;td&gt;bind the whole statement&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bit-decomposition range proof&lt;/td&gt;
&lt;td&gt;classic&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;OR-prove each bit&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;Historical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Borromean range proof&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;ring signatures over bits&lt;/td&gt;
&lt;td&gt;~6 kB/output&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulletproofs&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;recursive inner-product&lt;/td&gt;
&lt;td&gt;~600-700 B, O(log n)&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulletproofs+&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;weighted inner-product&lt;/td&gt;
&lt;td&gt;shorter constant&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naive two-round multisig&lt;/td&gt;
&lt;td&gt;2010s&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;sum nonces and responses&lt;/td&gt;
&lt;td&gt;2 rounds, broken&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-round multisig&lt;/td&gt;
&lt;td&gt;2006&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;commit-then-reveal nonces&lt;/td&gt;
&lt;td&gt;3 rounds&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuSig2 (BIP-327)&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;two nonces, nonlinear combine&lt;/td&gt;
&lt;td&gt;2 rounds, n-of-n&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FROST (RFC 9591)&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;threshold nonce commitments&lt;/td&gt;
&lt;td&gt;2 rounds, t-of-n&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chaum blind signature&lt;/td&gt;
&lt;td&gt;1982&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;blind, sign, unblind&lt;/td&gt;
&lt;td&gt;single-show&lt;/td&gt;
&lt;td&gt;Historical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CL-Idemix / U-Prove&lt;/td&gt;
&lt;td&gt;2001&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;multi-attribute proofs&lt;/td&gt;
&lt;td&gt;multi-show (Idemix); single-show (U-Prove)&lt;/td&gt;
&lt;td&gt;Niche (undeployed)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal KVAC&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;algebraic MAC, issuer verifies&lt;/td&gt;
&lt;td&gt;deployed&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BBS&lt;/td&gt;
&lt;td&gt;2023&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;pairing, multi-message&lt;/td&gt;
&lt;td&gt;~80 B signature&lt;/td&gt;
&lt;td&gt;Standardizing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Pass / Apple PAT&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;Credentials&lt;/td&gt;
&lt;td&gt;unlinkable single-use token&lt;/td&gt;
&lt;td&gt;RFC 9576/9577/9578&lt;/td&gt;
&lt;td&gt;Deployed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Three ladders, three break-driven climbs, over a foundation that was itself hardened in place -- three ascents on one unchanging three-move base. Which rung is at the top of each ladder today -- and with which exact parameters -- is the difference between a design review that passes and one that ships a CVE.&lt;/p&gt;
&lt;h2&gt;8. State of the Art (2024-2026): What Actually Ships&lt;/h2&gt;
&lt;p&gt;The frontier is not one best algorithm. It is a layered stack: three foundational moves that were never superseded, and above them a handful of composites that ship at scale, each with a standard and a parameter set. If you have to build with this in 2026, here is the state of the art with the numbers attached.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The moves, still the state of the art as primitives.&lt;/strong&gt; A Pedersen commitment is a single 32-byte point on ristretto255. Strong Fiat-Shamir is the correct instantiation now baked into every standard below. And Schnorr-proper, standardized as &lt;strong&gt;BIP-340&lt;/strong&gt;, is a 64-byte signature [@bip340]; Bitcoin&apos;s Taproot upgrade activated it at block 709,632 in November 2021 [@bip341], [@taproot-wiki], and native Schnorr has been available for every spend since. These are not legacy pieces waiting to be replaced; they are the load-bearing base of everything else.&lt;/p&gt;

BIP-340 is Schnorr-proper over the secp256k1 curve [@bip340]. EdDSA and its instance Ed25519 are also Schnorr-*family* signatures -- same linear response structure -- but with different encoding, different nonce derivation, and a different curve. They are cousins, not the same standard, and conflating them causes real interoperability and security-review confusion. When someone says &quot;Schnorr signature,&quot; ask which one.
&lt;p&gt;&lt;strong&gt;MuSig2 / BIP-327&lt;/strong&gt; is the deployed answer for n-of-n multisignatures: a two-round protocol producing a single 64-byte Schnorr signature that is indistinguishable on-chain from a single-key spend, implemented in &lt;code&gt;libsecp256k1&lt;/code&gt; and &lt;code&gt;secp256k1-zkp&lt;/code&gt; [@bip327], [@musig2]. Every co-signer participates, and the verifier does one ordinary Schnorr check.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FROST / RFC 9591&lt;/strong&gt; (June 2024) is the most consequential &lt;em&gt;new&lt;/em&gt; standard in the window: a two-round t-of-n threshold Schnorr signature, specified across five ciphersuites -- Ed25519, ristretto255, Ed448, P-256, and secp256k1 [@rfc9591]. On its own, FROST assumes signers do not vanish mid-protocol; &lt;strong&gt;ROAST&lt;/strong&gt; (CCS 2022) wraps it to keep signing alive even when some signers are faulty or offline, giving fault tolerance for asynchronous deployments [@roast].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Nonce structure&lt;/th&gt;
&lt;th&gt;Rounds&lt;/th&gt;
&lt;th&gt;Concurrent-secure&lt;/th&gt;
&lt;th&gt;Fault-tolerant&lt;/th&gt;
&lt;th&gt;Signature&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;Naive two-round&lt;/td&gt;
&lt;td&gt;sum the nonces&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;No (Drijvers, ROS)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;Broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Three-round&lt;/td&gt;
&lt;td&gt;commit then reveal&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B&lt;/td&gt;
&lt;td&gt;Superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MuSig2 (BIP-327)&lt;/td&gt;
&lt;td&gt;two nonces, nonlinear&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B (n-of-n)&lt;/td&gt;
&lt;td&gt;Deployed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FROST (RFC 9591)&lt;/td&gt;
&lt;td&gt;threshold commitments&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;64 B (t-of-n)&lt;/td&gt;
&lt;td&gt;Standardized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FROST + ROAST&lt;/td&gt;
&lt;td&gt;ROAST wrapper&lt;/td&gt;
&lt;td&gt;2+&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;64 B (t-of-n)&lt;/td&gt;
&lt;td&gt;Active&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

A zero-knowledge proof that a committed value lies in a fixed interval such as $[0, 2^n)$ without revealing the value. Bulletproofs are the deployed transparent construction, proving a Pedersen-committed amount is in range in about 600 to 700 bytes [@bulletproofs].
&lt;p&gt;&lt;strong&gt;Bulletproofs and Bulletproofs+&lt;/strong&gt; are the state of the art for &lt;em&gt;transparent&lt;/em&gt; range proofs -- logarithmic size, no trusted setup -- and they secure confidential amounts in Monero, where Bulletproofs deployed in October 2018 and Bulletproofs+ followed in 2022 [@bulletproofs], [@bulletproofs-plus], [@monero]. They are the default whenever you need a range proof without a setup ceremony and can accept linear verification time. This is the immediate cash-in of Move 1&apos;s homomorphism at production scale.&lt;/p&gt;
&lt;p&gt;On the credential side, three systems ship, and they occupy different economic corners.&lt;/p&gt;

An anonymous credential in which the issuer and the verifier are the same party, so the credential can be built on a fast algebraic MAC instead of a public-key signature. The holder presents it with a Sigma-protocol proof of knowledge, revealing only the attributes it chooses [@kvac], [@signal].
&lt;p&gt;&lt;strong&gt;Signal KVAC&lt;/strong&gt; is the keyed-verification credential in production in Signal&apos;s private group system: the issuer is also the verifier, so it runs on an algebraic MAC with no pairings, and presentation is a Sigma-protocol proof of knowledge [@kvac], [@signal]. It is the standing counter-example to &quot;elegant but undeployed.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;BBS and Blind BBS&lt;/strong&gt;BBS takes its name from the 2004 Boneh-Boyen-Shacham short group signature scheme it descends from, by way of the later BBS+ variant [@bbs-origin]. are the publicly-verifiable answer: multi-message, selective-disclosure credentials over the BLS12-381 pairing curve, with roughly 80-byte signatures, now in &lt;code&gt;draft-irtf-cfrg-bbs-signatures-10&lt;/code&gt; (8 January 2026) and Blind BBS &lt;code&gt;draft-03&lt;/code&gt; [@bbs-draft], [@blind-bbs]. BBS is the standardizing choice for W3C Verifiable Credentials and identity wallets, where a third party must verify what an issuer signed. KVAC and BBS are parallel answers -- private-issuer economy versus public verifier -- not one replacing the other.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Privacy Pass&lt;/strong&gt;, deployed at population scale as &lt;strong&gt;Apple Private Access Tokens&lt;/strong&gt;, is the third corner: a different shape from the other two -- an unlinkable single-use authorization token rather than a multi-attribute credential. A client anonymously obtains a token backed by some attestation and later redeems it at an origin that cannot link the redemption to issuance.&lt;/p&gt;
&lt;p&gt;It was standardized at the IETF in June 2024 across three documents -- &lt;strong&gt;RFC 9576&lt;/strong&gt; for the architecture, &lt;strong&gt;RFC 9578&lt;/strong&gt; for issuance, and &lt;strong&gt;RFC 9577&lt;/strong&gt; for the &lt;code&gt;PrivateToken&lt;/code&gt; HTTP authentication scheme [@rfc9576], [@rfc9578], [@rfc9577]. Apple shipped it in iOS 16 and macOS Ventura in 2022 with hardware-backed device attestation, and Cloudflare uses it to drop the CAPTCHA for attested devices [@cloudflare-pat].Direct Anonymous Attestation (2004), the deployed exception detailed in Section 7, ships in two hardware generations -- TPM 1.2 RSA-DAA and TPM 2.0 ECDAA -- and adds per-verifier basename linkability, so a device proves it is genuine without becoming trackable [@daa], [@tpm2-spec].&lt;/p&gt;
&lt;p&gt;Underneath, it is still commit, challenge, respond over the discrete log. Keep it distinct from KVAC and BBS: it is single-&lt;em&gt;show&lt;/em&gt; authorization, not multi-attribute selective disclosure, and its privately-verifiable issuance is a verifiable oblivious PRF whose Chaum-Pedersen mechanism Part 20 owns and this section only signposts.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Verifiability&lt;/th&gt;
&lt;th&gt;Cryptographic base&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;U-Prove / Idemix&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;Discrete log, RSA, pairings&lt;/td&gt;
&lt;td&gt;multi-kB&lt;/td&gt;
&lt;td&gt;Standardized, largely undeployed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal KVAC&lt;/td&gt;
&lt;td&gt;Keyed (issuer verifies)&lt;/td&gt;
&lt;td&gt;Algebraic MAC, no pairings&lt;/td&gt;
&lt;td&gt;Compact&lt;/td&gt;
&lt;td&gt;Deployed in Signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BBS / Blind BBS&lt;/td&gt;
&lt;td&gt;Public&lt;/td&gt;
&lt;td&gt;Pairings on BLS12-381&lt;/td&gt;
&lt;td&gt;~80 B signature&lt;/td&gt;
&lt;td&gt;Standardizing (IETF)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Pass / Apple PAT&lt;/td&gt;
&lt;td&gt;Public or keyed, per token type&lt;/td&gt;
&lt;td&gt;VOPRF or blind RSA issuance&lt;/td&gt;
&lt;td&gt;Small single-use token&lt;/td&gt;
&lt;td&gt;Deployed (iOS 16, macOS Ventura)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;This is the same primitive Part 20 built on: an &lt;a href=&quot;https://paragmali.com/blog/the-server-helped-and-never-saw-a-field-guide-to-oblivious-p/&quot; rel=&quot;noopener&quot;&gt;oblivious PRF&apos;s verifiability&lt;/a&gt; rests on a Chaum-Pedersen Sigma proof -- and Privacy Pass&apos;s privately-verifiable issuance is exactly that VOPRF. The deployed &lt;a href=&quot;https://paragmali.com/blog/the-age-gate-that-doesnt-know-your-age-how-anonymous-credent/&quot; rel=&quot;noopener&quot;&gt;anonymous-credential systems&lt;/a&gt; are Sigma proofs of knowledge of a signature, and &lt;a href=&quot;https://paragmali.com/blog/direct-anonymous-attestation-the-zero-knowledge-proof-alread/&quot; rel=&quot;noopener&quot;&gt;Direct Anonymous Attestation&lt;/a&gt; is the Sigma-protocol credential already shipping in TPM silicon. Every composite in this section is the three moves with the parameters filled in.&lt;/p&gt;
&lt;p&gt;Every one of these is a defensible point on a Pareto frontier, not a single winner -- which is exactly why the toolkit is a toolkit. To choose between them, and between the whole family and a general-purpose proof system, you need the line where Sigma plus Fiat-Shamir stops being the right tool.&lt;/p&gt;
&lt;h2&gt;9. Competing Approaches and Where the Line Is&lt;/h2&gt;
&lt;p&gt;The toolkit&apos;s real competition is the general-purpose proof system -- the SNARK or STARK that can prove &lt;em&gt;any&lt;/em&gt; circuit. Knowing when &lt;em&gt;not&lt;/em&gt; to reach for one is half of using this toolkit well.&lt;/p&gt;
&lt;p&gt;A bespoke Sigma proof plus strong Fiat-Shamir is transparent (it needs only the random-oracle assumption, no setup ceremony), tiny for narrow statements (a constant number of group elements, growing only with the number of clauses), and cheap to prove. A pairing-based SNARK gives a constant-size proof and near-constant verification, but at the price of a trusted setup: Groth16 lands near 200 bytes and PLONK near 400 [@groth16], [@plonk].&lt;/p&gt;
&lt;p&gt;A STARK is transparent and plausibly post-quantum, but its proofs run to tens or hundreds of kilobytes. The Bulletproofs authors made the transparency contrast explicit: their range proofs need no trusted setup, unlike the succinct pairing SNARKs [@bulletproofs].&lt;/p&gt;
&lt;p&gt;The decision rule falls out of the shape of your statement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;fixed algebraic relation&lt;/strong&gt; -- &quot;I know a discrete log,&quot; &quot;this Pedersen value is in range,&quot; &quot;one of these $n$ public keys signed&quot; -- wants a hand-verified Sigma proof with strong Fiat-Shamir. It is smaller, faster, and setup-free.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;arbitrary circuit&lt;/strong&gt; -- &quot;this value is the output of running this program&quot; -- is where you reach for a SNARK or STARK, accepting the setup or the proof size in exchange for expressiveness you cannot get from a fixed relation.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Proof size (64-bit range)&lt;/th&gt;
&lt;th&gt;Trusted setup&lt;/th&gt;
&lt;th&gt;Prover&lt;/th&gt;
&lt;th&gt;Verify&lt;/th&gt;
&lt;th&gt;Post-quantum&lt;/th&gt;
&lt;th&gt;Aggregation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Bit-decomposition OR&lt;/td&gt;
&lt;td&gt;O(n), large&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Weak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Borromean&lt;/td&gt;
&lt;td&gt;~6 kB/output&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulletproofs&lt;/td&gt;
&lt;td&gt;~0.6-0.7 kB, O(log n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n), batchable&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;Bulletproofs+&lt;/td&gt;
&lt;td&gt;smaller constant, O(log n)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n), batchable&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;SNARK (Groth16/PLONK)&lt;/td&gt;
&lt;td&gt;~0.2 kB (Groth16) to ~0.4 kB (PLONK), O(1)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;O(circuit)&lt;/td&gt;
&lt;td&gt;~O(1)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Circuit-dependent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STARK&lt;/td&gt;
&lt;td&gt;tens to hundreds of kB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;O(n log n)&lt;/td&gt;
&lt;td&gt;polylog&lt;/td&gt;
&lt;td&gt;Plausibly&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two more axes decide the corner. &lt;strong&gt;Commitment choice&lt;/strong&gt; is the Section 4 recap: need to add committed values, pick Pedersen; need post-quantum binding or no algebra, pick a hash commitment. And &lt;strong&gt;interactivity&lt;/strong&gt; is a genuine fork: an interactive Sigma protocol is &lt;em&gt;deniable&lt;/em&gt; -- because anyone could have simulated the transcript, it proves nothing to a third party -- and needs no random-oracle assumption, while Fiat-Shamir buys you a signature or an on-chain proof at the cost of making the proof transferable to everyone forever. Deniability versus non-interactivity is a real tension we return to in the open problems.&lt;/p&gt;

The challenge move is the shared weak point of proof systems well beyond this toolkit. PlonK -- a general-purpose SNARK, not a Sigma protocol -- was hit by the *same* weak Fiat-Shamir bug as Bulletproofs in the 2022 Frozen Heart disclosure [@tob-plonk], [@tob-part1]. And in 2025, Khovratovich, Rothblum, and Soukhanov demonstrated a *practical* soundness break against a GKR-based commercial proof system, tricking it into certifying false statements regardless of which hash function instantiates Fiat-Shamir [@khovratovich], [@quanta]. This boundary keeps the article honest: weak-challenge failures are not unique to Sigma protocols -- the move that most often leaks is the same across the whole family of proof systems.
&lt;p&gt;The line is drawn by the shape of your statement and the trust you can assume. But even the right tool, used perfectly, runs into limits that no parameter choice can buy its way out of.&lt;/p&gt;
&lt;h2&gt;10. Theoretical Limits&lt;/h2&gt;
&lt;p&gt;The discrete log has held for forty years. But &quot;the hard problem is hard&quot; is not the same claim as &quot;your proof is secure,&quot; and the gap between them is where this section lives. Every limit below survives even if the discrete logarithm stays hard forever.&lt;/p&gt;
&lt;p&gt;Start with the good news, because it is genuinely good. For the &lt;em&gt;interactive&lt;/em&gt; protocol, special soundness gives a tight, unconditional extractor -- two transcripts and three lines of algebra recover the witness, no assumptions -- and SHVZK gives a &lt;em&gt;perfect&lt;/em&gt; simulator [@damgard]. Interactive Schnorr is about as close to an ideal proof of knowledge as anything in cryptography gets. The trouble starts when you make it non-interactive, or run many copies at once.&lt;/p&gt;
&lt;h3&gt;Soundness is only ROM-deep&lt;/h3&gt;
&lt;p&gt;Fiat-Shamir soundness is provable, but only in the random-oracle model, via the forking lemma [@pointcheval-stern]. In the standard model it is &lt;em&gt;uninstantiable&lt;/em&gt;: Goldwasser and Kalai (FOCS 2003) exhibited schemes whose Fiat-Shamir transform is insecure for &lt;em&gt;every&lt;/em&gt; concrete hash function you could plug in [@gk-focs]. That was a theoretical construction for two decades. Then in 2025, Khovratovich, Rothblum, and Soukhanov turned it practical, making a GKR-based commercial proof system accept false statements regardless of the hash instantiating the transform [@khovratovich], [@quanta].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The Khovratovich-Rothblum-Soukhanov result targets Fiat-Shamir applied to GKR-style succinct arguments, not the short algebraic Sigma statements that strong Fiat-Shamir fully binds [@khovratovich]. It does not forge a Schnorr signature. What it does is harden the general lesson: Fiat-Shamir soundness is a property of the random-oracle idealization, and the more your proof leans on the hash doing something a real hash may not do, the thinner the ice [@gk-focs].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Forking is loose, and looseness is the seam&lt;/h3&gt;
&lt;p&gt;The random-oracle reduction is not tight. Rewinding a forger to extract the discrete log loses roughly a quadratic factor in the security bound [@pointcheval-stern]. This is not sloppy proof engineering; it is inherent to the rewinding technique.The quadratic looseness and the concurrency failure are the same fact seen twice. Rewinding extracts from &lt;em&gt;one&lt;/em&gt; session by replaying it with a fresh challenge; it does not compose when many sessions interleave, because you cannot rewind one without disturbing the others. That is precisely the seam the ROS attack pries open [@pointcheval-stern], [@ros]. One line of work sidesteps rewinding entirely: the Fischlin transform (2005) uses an &lt;em&gt;online&lt;/em&gt;, straight-line extractor that reads the witness out of a single transcript by inspecting the prover&apos;s oracle queries, so its extraction composes concurrently and carries more cleanly into the quantum random-oracle model -- at the price of larger proofs, which is why it coexists with Fiat-Shamir rather than replacing it [@fischlin]. And because rewinding does not compose across concurrent sessions, the single-session forking proof simply does not extend to the multi-party concurrent setting.&lt;/p&gt;
&lt;p&gt;That gap has a sharp quantitative edge. Naive linear Schnorr multisignatures and blind signatures are secure only while the number of concurrent open sessions stays below roughly $\log_2 p$; above that threshold, the ROS forgery runs in polynomial time with no discrete log solved [@ros]. MuSig2&apos;s two-nonce nonlinear combination is the construction that evades the bound -- not a discrete-log advance, just a change that makes the response nonlinear so the linear system has no solution [@musig2].&lt;/p&gt;
&lt;h3&gt;Two settled negatives&lt;/h3&gt;
&lt;p&gt;Some limits are not open questions; they are proven impossibilities you must design around.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;No commitment is both perfectly hiding and perfectly binding.&lt;/strong&gt; The Move-1 trade-off of Section 4 is fundamental, not an artifact of Pedersen [@damgard]. You choose a corner; you do not get both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;None of this is post-quantum.&lt;/strong&gt; Every guarantee in the article rests on the hardness of the discrete logarithm or of pairing problems, and Shor&apos;s algorithm breaks all of them on a sufficiently large quantum computer. No parameter choice rescues it -- a bigger group buys nothing against Shor. The hard problem that held against classical attackers for forty years is exactly the one a quantum computer dissolves.&lt;/li&gt;
&lt;/ol&gt;

The discrete log is hard, and this multisignature is secure under concurrency, are simply different claims -- and the second one was false until MuSig2 made the combined nonce nonlinear.
&lt;p&gt;Those are the settled negatives. The open frontiers -- where the field is still moving -- are next, and the sharpest is the quantum cliff the whole toolkit stands at the edge of.&lt;/p&gt;
&lt;h2&gt;11. Open Problems&lt;/h2&gt;
&lt;p&gt;The toolkit is deployed and load-bearing, and it still has open edges sharp enough to end careers on. Here is what is genuinely unsolved, and which of it should keep a protocol designer awake.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standard-model Fiat-Shamir.&lt;/strong&gt; A universal, hash-instantiable, tight Fiat-Shamir is believed impossible in general. The 2003 uninstantiability result and the 2025 practical break together bound the ambition: the realistic goal is not a universal fix but per-protocol &lt;em&gt;correlation intractability&lt;/em&gt; -- proving a specific hash is good enough for a specific proof system [@gk-focs], [@khovratovich]. Straight-line alternatives such as the Fischlin transform trade larger proofs for cleaner, rewinding-free extraction, but they are not a universal drop-in replacement [@fischlin]. For the short algebraic statements in this article, strong Fiat-Shamir remains the right practice; for succinct arguments, the question is live.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-quantum Sigma protocols.&lt;/strong&gt; This is the sharpest frontier, because it is where the whole toolkit meets its deadline. Discrete-log zero knowledge dies to Shor -- but the three-move &lt;em&gt;template&lt;/em&gt; does not. It reappears over other hardness assumptions.The Sigma template outlives its discrete-log instantiation. Fiat-Shamir-with-aborts over lattices is the shape behind Dilithium-style signatures, and MPC-in-the-head -- instantiated as Picnic -- builds a zero-knowledge signature from a hash and a secret-sharing protocol, needing no structured hardness assumption at all [@picnic]. Commit, challenge, respond survives the move to a post-quantum world; only the algebra underneath changes. What does &lt;em&gt;not&lt;/em&gt; yet transfer cleanly is the efficient, verifiable, partially-oblivious machinery: post-quantum range proofs and post-quantum multi-show credentials exist but are large and immature compared to Bulletproofs and BBS. 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;thirty-year migration to post-quantum cryptography&lt;/a&gt; is the sibling part that tracks this deadline in full.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Round-optimal blind Schnorr after ROS.&lt;/strong&gt; Plain two-round blind Schnorr is inherently ROS-vulnerable [@ros]. Getting a secure blind signature in few rounds needs extra structure: clause or boosting variants that break the linearity, a proof in the algebraic group model (Fuchsbauer, Plouviez, and Seurin), or a different scheme entirely [@fps]. There is no known plain, standard-model, round-optimal blind Schnorr that survives concurrency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Threshold Schnorr, hardened.&lt;/strong&gt; RFC 9591 standardized two-round threshold Schnorr for custody in June 2024, and FROST is now entering production custody [@rfc9591], but stronger guarantees remain research targets: adaptive security (against an attacker who corrupts signers mid-protocol), proofs that do not lean on the algebraic group model, and distributed key generation that scales asynchronously to large signer sets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Anonymous-credential revocation at population scale&lt;/strong&gt; without breaking unlinkability is still mostly finessed rather than solved -- deployed systems lean on short-lived credentials that simply expire, because a true privacy-preserving revocation list that scales to millions of holders is hard to make both efficient and unlinkable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deniability versus non-interactivity.&lt;/strong&gt; A Fiat-Shamir&apos;d proof is transferable -- anyone who receives it is convinced, forever. An interactive Sigma proof is deniable -- because it could have been simulated, it convinces only the live verifier. Getting both properties at once is a per-application tension, not a solved problem.&lt;/p&gt;
&lt;p&gt;None of these block you from shipping today; they tell you what to watch. What you ship comes down to a small, unglamorous set of rules that separate a correct deployment from a forged one.&lt;/p&gt;
&lt;h2&gt;12. Use X With These Parameters In Case Y&lt;/h2&gt;
&lt;p&gt;Here are the rules a reviewer can apply without re-deriving anything, organized -- of course -- by the three moves, in the order you make them.&lt;/p&gt;
&lt;h3&gt;Move 1: pick the commitment by the job&lt;/h3&gt;
&lt;p&gt;Need to add or range-prove committed values -- confidential amounts, homomorphic tallies? Use a &lt;strong&gt;Pedersen commitment on ristretto255&lt;/strong&gt;, and derive the second generator $h$ by hashing a fixed string to a curve point so that nobody holds the trapdoor $\log_g h$. A known trapdoor is silent equivocation; the generator setup is security-critical, not boilerplate. Need post-quantum binding, or you are committing to something with no algebra -- a Merkle leaf, a coin flip? Use a &lt;strong&gt;hash commitment&lt;/strong&gt; &lt;code&gt;H(m ‖ r)&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Move 2: get the challenge right, every time&lt;/h3&gt;
&lt;p&gt;Use &lt;strong&gt;strong Fiat-Shamir&lt;/strong&gt;: hash the full statement, all public parameters, the prior transcript, and a domain separator. The operational habit that prevents the entire Frozen Heart class is one sentence: for every proof, write down &quot;what is this proof &lt;em&gt;about&lt;/em&gt;?&quot; and confirm each item on that list is inside the hash. The statement is about a public key? The public key goes in the hash. It is about a specific message or ciphertext? That goes in too. Weak Fiat-Shamir is what happens when that list is incomplete [@tob-part1].&lt;/p&gt;
&lt;h3&gt;Move 3: nonce discipline&lt;/h3&gt;
&lt;p&gt;Nonces must be fresh, secret, and uniform per session. Draw them from a well-seeded CSPRNG, or derive them deterministically with the tagged-hash discipline BIP-340 specifies so a broken RNG cannot repeat or bias them [@bip340]. In MuSig2 and FROST, honor the two-nonce and preprocessing rules exactly, and never reuse a nonce commitment across sessions -- the multi-party schemes defeat ROS only if the nonce rules are followed [@musig2], [@rfc9591].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Move 1 -- is your second generator $h$ from a nothing-up-my-sleeve hash, with no known trapdoor? Move 2 -- write down what the proof is about, and verify every item is inside the challenge hash, behind a domain separator. Move 3 -- is every nonce fresh, secret, and uniform, and are the MuSig2 or FROST nonce rules followed to the letter? Three questions, in order. Most Frozen-Heart and nonce-reuse incidents fail one of them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Then pick the composite&lt;/h3&gt;

flowchart TD
    START{&quot;What are you building?&quot;}
    START --&amp;gt;|&quot;commit to a value&quot;| COMMIT{&quot;need to add or range-prove it?&quot;}
    COMMIT --&amp;gt;|&quot;yes&quot;| C1[&quot;Pedersen on ristretto255&quot;]
    COMMIT --&amp;gt;|&quot;no, need PQ binding&quot;| C2[&quot;Hash commitment&quot;]
    START --&amp;gt;|&quot;prove a range&quot;| C3[&quot;Bulletproofs plus, aggregated, strong FS&quot;]
    START --&amp;gt;|&quot;multi-party signature&quot;| SIG{&quot;all signers or t of n?&quot;}
    SIG --&amp;gt;|&quot;all n&quot;| C4[&quot;MuSig2 BIP-327&quot;]
    SIG --&amp;gt;|&quot;t of n&quot;| C5[&quot;FROST RFC 9591 plus ROAST&quot;]
    START --&amp;gt;|&quot;anonymous credential&quot;| CRED{&quot;who verifies?&quot;}
    CRED --&amp;gt;|&quot;the issuer itself&quot;| C6[&quot;Signal KVAC&quot;]
    CRED --&amp;gt;|&quot;any third party&quot;| C7[&quot;BBS&quot;]
    START --&amp;gt;|&quot;arbitrary circuit&quot;| C8[&quot;SNARK or STARK&quot;]
&lt;p&gt;The composite rules, stated plainly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Range proof, small and transparent:&lt;/strong&gt; Bulletproofs+, aggregated, with strong Fiat-Shamir binding [@bulletproofs-plus].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;All parties co-sign one key (n-of-n):&lt;/strong&gt; MuSig2, standardized as BIP-327 [@bip327].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Any t of n signers suffice:&lt;/strong&gt; FROST, RFC 9591, run with a distributed key generation -- and add ROAST if a faulty signer must not be able to stall the protocol [@rfc9591], [@roast].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authenticate your own users anonymously (you issue and verify):&lt;/strong&gt; Signal KVAC [@kvac], [@signal].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A third party must verify selective disclosure:&lt;/strong&gt; BBS [@bbs-draft].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A fixed algebraic relation, tiny and setup-free:&lt;/strong&gt; a hand-rolled Sigma proof with strong Fiat-Shamir, using a vetted library -- never your own group arithmetic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An arbitrary circuit:&lt;/strong&gt; a SNARK or STARK, accepting the setup or proof-size cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anything that must stay secure through the quantum transition:&lt;/strong&gt; do not rely on these guarantees; plan a post-quantum migration.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the implementations, use the libraries that encode these defaults: &lt;code&gt;bitcoin-core/secp256k1&lt;/code&gt; and &lt;code&gt;secp256k1-zkp&lt;/code&gt; for BIP-340 and MuSig2 [@bip340], [@bip327]; FROST implementations validated against the RFC 9591 test vectors [@rfc9591]; &lt;code&gt;dalek-cryptography&lt;/code&gt; for Bulletproofs; Signal&apos;s &lt;code&gt;libsignal&lt;/code&gt; and zkgroup for KVAC [@signal]; BBS libraries tracking the CFRG drafts [@bbs-draft]; and Damgard&apos;s notes together with Trail of Bits&apos; ZKDocs when you need the correct Fiat-Shamir recipe written out [@damgard], [@tob-part1].&lt;/p&gt;
&lt;p&gt;Most of the ways this goes wrong are the same handful of mistakes phrased for different audiences -- and they are worth naming head-on, because reviewers find them in real code every week.&lt;/p&gt;
&lt;h2&gt;13. Common Misuse, and the Questions Engineers Ask&lt;/h2&gt;
&lt;p&gt;The antipattern catalog is short because the failures are few and repeat. Reviewers find these in real code, and every one maps to a move.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse&lt;/th&gt;
&lt;th&gt;Move&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Weak Fiat-Shamir in real code&lt;/td&gt;
&lt;td&gt;Move 2&lt;/td&gt;
&lt;td&gt;statement omitted from the challenge hash&lt;/td&gt;
&lt;td&gt;bind the full statement and parameters (strong FS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reused or biased nonce&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;two transcripts share a commitment; extractor recovers the key&lt;/td&gt;
&lt;td&gt;fresh, uniform, per-session nonces; hedged derivation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assuming Pedersen is perfectly binding&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;it is only computationally binding&lt;/td&gt;
&lt;td&gt;design for computational binding; protect the trapdoor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equivocation via a known &lt;code&gt;log_g h&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Move 1&lt;/td&gt;
&lt;td&gt;someone knows the trapdoor&lt;/td&gt;
&lt;td&gt;derive &lt;code&gt;h&lt;/code&gt; by nothing-up-my-sleeve hash-to-curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rolling your own OR-proof or CDS composition&lt;/td&gt;
&lt;td&gt;all three&lt;/td&gt;
&lt;td&gt;simulator and challenge-splitting are subtle&lt;/td&gt;
&lt;td&gt;use a vetted implementation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naive linear multisig under concurrency&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;linear responses solved by ROS&lt;/td&gt;
&lt;td&gt;MuSig2 (n-of-n) or FROST (t-of-n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treating credentials as &quot;just signatures&quot;&lt;/td&gt;
&lt;td&gt;Move 1-3&lt;/td&gt;
&lt;td&gt;they are Sigma proofs with selective disclosure&lt;/td&gt;
&lt;td&gt;model unlinkability and presentation proofs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;ROS means Random Oracle Substitution&quot;&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;mistranslated acronym&lt;/td&gt;
&lt;td&gt;it is an overdetermined, solvable linear system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflating BIP-340 with EdDSA&lt;/td&gt;
&lt;td&gt;Move 3&lt;/td&gt;
&lt;td&gt;different encoding, nonce, and curve&lt;/td&gt;
&lt;td&gt;pin the exact standard you mean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;


No. A Pedersen commitment is perfectly *hiding* and only *computationally* binding. A party who knows the trapdoor $\log_g h$ can open a single commitment to any message they like, by solving one linear equation for a matching blinder [@pedersen], [@damgard]. This is why the second generator must be produced so that nobody knows that trapdoor, and why &quot;Pedersen is perfectly binding&quot; is a dangerous thing to write in a spec.


The full statement, all public parameters, the prior transcript, and a domain separator -- not just the commitment [@bpw]. Omitting the statement is weak Fiat-Shamir, the bug behind both the Helios voting break in 2012 and the Frozen Heart disclosures in 2022 [@bpw], [@tob-part1]. The habit that prevents it: write down what the proof is about, and confirm each item is inside the hash.


Yes, and seeing that is worth the price of admission. A Schnorr signature is Fiat-Shamir applied to the Schnorr identification protocol, with the verifier&apos;s challenge replaced by a hash of the public key, message, and commitment [@fiat-shamir], [@bip340]. A signature *is* a non-interactive proof of knowledge of the private key; the message just rides along in the challenge.


Don&apos;t. The CDS construction for AND/OR composition relies on splitting the challenge and simulating the branches you cannot answer, and getting the challenge algebra or the simulator subtly wrong breaks soundness or zero knowledge [@cds]. Use a vetted implementation; this is exactly the code that looks simple and is not.


No. It is forgeable under concurrent sessions -- sub-exponentially since Drijvers at S&amp;amp;P 2019, and in polynomial time since the ROS result in 2021 [@drijvers], [@ros]. Use MuSig2 for n-of-n signing or FROST for t-of-n threshold signing; both are engineered specifically to defeat that attack [@musig2], [@rfc9591].


No. They are Sigma-protocol proofs of knowledge of a signature or an algebraic MAC, and they support selective disclosure and unlinkability -- a plain signature has neither [@bbs-draft], [@signal]. Treating a credential presentation as an ordinary signature verification misses the entire privacy machinery that is the point of the primitive. Privacy Pass and Apple&apos;s Private Access Tokens are the single-*show* authorization-token cousin -- unlinkable, but a one-time &quot;you are allowed&quot; token rather than a multi-attribute selective-disclosure credential [@rfc9576].


No, and the mistranslation is everywhere online. ROS is &quot;Random inhomogeneities in an Overdetermined, Solvable system of linear equations&quot; -- a linear-algebra object, not an oracle trick [@ros]. The attack forges by solving a system of equations over responses collected across concurrent sessions, with no discrete logarithm ever solved.

&lt;h3&gt;The discrete log held; the three moves leaked&lt;/h3&gt;
&lt;p&gt;Follow the whole failure catalog back and it lands in one place. In April 2022, three unrelated proof systems were forgeable at once, and the discrete logarithm was untouched in all three [@tob-part1]. A decade earlier, the same weak-Fiat-Shamir bug had forged ballot proofs in Helios [@bpw]. A reused nonce hands over a private key by the same algebra that proves Schnorr sound [@bip340]. The ROS attack forges naive multisignatures in polynomial time by solving a linear system, no hard problem broken [@ros]. A Pedersen commitment equivocates the instant someone knows a trapdoor [@pedersen].&lt;/p&gt;
&lt;p&gt;Four decades, and the discrete logarithm never fell. Every named break -- Frozen Heart, Helios, nonce reuse, ROS, Drijvers, equivocation -- landed on one of the three moves built on top of it.&lt;/p&gt;
&lt;p&gt;And the composites climbed while the foundations stayed put. Borromean gave way to Bulletproofs and Bulletproofs+ [@bulletproofs], [@bulletproofs-plus]; naive multisignatures gave way to MuSig2 and FROST [@musig2], [@rfc9591]; Chaum&apos;s blind signatures gave way, in deployment, to Signal KVAC, BBS, and Privacy Pass [@signal], [@bbs-draft], [@rfc9576]. Not one of those ladders replaced Pedersen, Schnorr, or Fiat-Shamir. The three moves keep running underneath every rung, exactly as they were written between 1985 and 1996 -- and where the foundation moved at all, it was Move 2 being hardened in place, weak Fiat-Shamir corrected to strong, never a piece torn out and replaced.&lt;/p&gt;
&lt;p&gt;This is the frame the series keeps returning to from a new angle each time. Part 18 argued that &lt;a href=&quot;https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/&quot; rel=&quot;noopener&quot;&gt;nobody broke the discrete log&lt;/a&gt;; Part 17 that the math held and the interface leaked. Here, the math held and the &lt;em&gt;conversation&lt;/em&gt; leaked -- because a discrete-log zero-knowledge proof was never one thing. It was always three moves, and each one is a separate promise that can be broken without touching the problem underneath.&lt;/p&gt;

The discrete log held. The three moves leaked.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;commitments-and-sigma-protocols-pedersen-schnorr-and-the-dis&quot; keyTerms={[
  { term: &quot;Sigma protocol&quot;, definition: &quot;A three-move public-coin proof -- commit, challenge, respond -- with special soundness and special honest-verifier zero knowledge, checked by one verification equation.&quot; },
  { term: &quot;Special soundness&quot;, definition: &quot;Two accepting transcripts sharing a commitment but with different challenges yield the witness via an extractor; what makes a Sigma protocol a proof of knowledge.&quot; },
  { term: &quot;SHVZK&quot;, definition: &quot;Special honest-verifier zero knowledge: any accepting transcript can be simulated without the witness by choosing the response and challenge first, proving the exchange reveals nothing.&quot; },
  { term: &quot;Pedersen commitment&quot;, definition: &quot;C = g^m times h^r: perfectly hiding, computationally binding on the discrete log, additively homomorphic -- and NOT perfectly binding, since a trapdoor holder can equivocate.&quot; },
  { term: &quot;Fiat-Shamir transform&quot;, definition: &quot;Replacing the interactive verifier&apos;s challenge with a hash of the transcript, making a Sigma protocol non-interactive. Strong FS binds the whole statement; weak FS omits it and is forgeable.&quot; },
  { term: &quot;Forking lemma&quot;, definition: &quot;The rewinding proof that turns a random-oracle forger into a discrete-log solver, giving Schnorr its ROM security -- loosely, with a quadratic loss, and only for single sessions.&quot; },
  { term: &quot;ROS problem&quot;, definition: &quot;Random inhomogeneities in an Overdetermined, Solvable system of linear equations; solvable in polynomial time above about log2(p) concurrent sessions, breaking naive linear Schnorr multi and blind signatures.&quot; },
  { term: &quot;MuSig2 vs FROST&quot;, definition: &quot;MuSig2 (BIP-327) is n-of-n two-round Schnorr multisig with a nonlinear two-nonce trick; FROST (RFC 9591) is t-of-n threshold Schnorr. Both defeat ROS.&quot; },
  { term: &quot;KVAC vs BBS&quot;, definition: &quot;Signal KVAC is a keyed-verification credential (issuer verifies, algebraic MAC, deployed); BBS is a publicly-verifiable multi-message credential on BLS12-381 (standardizing).&quot; },
  { term: &quot;Privacy Pass / PAT&quot;, definition: &quot;An unlinkable single-use authorization token (RFC 9576/9577/9578, June 2024), deployed as Apple Private Access Tokens; the single-show cousin of credentials, not multi-attribute selective disclosure.&quot; }
]} questions={[
  { q: &quot;A proof verifies but the statement is false, and nobody solved the discrete log. Which move failed, and how?&quot;, a: &quot;Move 2, the challenge. Weak Fiat-Shamir omitted the statement from the hash, so a valid proof was transplanted onto a false statement -- the Frozen Heart and Helios bug.&quot; },
  { q: &quot;Two Schnorr signatures share a nonce. What can an attacker compute, and why is it the same math as a soundness proof?&quot;, a: &quot;The private key, via x = (z1 - z2)/(e1 - e2). That is exactly the special-soundness extractor from two transcripts on the same commitment, run by the attacker instead of the security proof.&quot; },
  { q: &quot;Why is a naive two-round Schnorr multisignature insecure under concurrency when single-session Schnorr is proven secure?&quot;, a: &quot;The forking-lemma proof does not compose across concurrent sessions, and the linear response lets the ROS attack solve a system over many open sessions to forge -- with no discrete log solved. MuSig2&apos;s nonlinear two-nonce trick evades it.&quot; },
  { q: &quot;Is a Pedersen commitment perfectly binding, and what is the security-critical setup step?&quot;, a: &quot;No -- it is perfectly hiding and only computationally binding. The critical step is deriving the second generator h with an unknown discrete log, by hashing to the curve, so no party holds the equivocation trapdoor.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>commitments</category><category>sigma-protocols</category><category>pedersen-commitment</category><category>schnorr</category><category>fiat-shamir</category><category>zero-knowledge-proofs</category><category>bulletproofs</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Server Helped, and Never Saw: A Field Guide to Oblivious Pseudorandom Functions</title><link>https://paragmali.com/blog/the-server-helped-and-never-saw-a-field-guide-to-oblivious-p/</link><guid isPermaLink="true">https://paragmali.com/blog/the-server-helped-and-never-saw-a-field-guide-to-oblivious-p/</guid><description>How a server applies its secret key to your secret input without ever seeing it: OPRF, VOPRF, and POPRF (RFC 9497), the engine of OPAQUE and Privacy Pass.</description><pubDate>Mon, 13 Jul 2026 10:23:11 GMT</pubDate><content:encoded>
**An oblivious pseudorandom function (OPRF) lets a server apply its secret key to your secret input so that it learns neither the input nor the output.** That single move is the engine under OPAQUE logins, Privacy Pass tokens, and private password-breach checks [@rfc9497]. The trick is David Chaum&apos;s 1982 blinding transplanted from RSA into a Diffie-Hellman group [@chaum82]: hash your input to a curve point, multiply by a fresh random blind, let the server multiply by its key, then divide the blind back out. RFC 9497 (2023) pins this down as three modes -- OPRF, VOPRF, and POPRF -- that answer three questions: is it blind, which key produced the answer, and how many times can one key be asked before it bleeds out [@rfc9497]. This is the field guide to that engine: every generation that built it, every real break that shaped it, and every way it still gets misused in production code.
&lt;h2&gt;1. The Server Hardened Your Password Without Ever Seeing It&lt;/h2&gt;
&lt;p&gt;You want to make a stolen password database worthless. The standard defense is to key every stored password with a secret the attacker cannot exfiltrate -- a &quot;pepper&quot; that lives only in your server&apos;s memory -- so a dumped hash is uncrackable without also breaking the keyholder. This is the logic behind server-assisted password hardening, and it is exactly what OPAQUE was designed to formalize [@opaque-paper].&lt;/p&gt;
&lt;p&gt;But look closely at the obvious way to apply that key. To hash a password under the pepper, the server has to be handed the password. That is the one thing you were trying not to do. The service that hardens your secret must first be shown your secret. It feels like a law of nature: to key a value, you have to see the value.&lt;/p&gt;
&lt;p&gt;It is not a law of nature. Here is the whole resolution, and it fits in four lines.&lt;/p&gt;
&lt;p&gt;The client hashes its password to a point on an elliptic curve, $P = H(pw)$. It picks a fresh uniformly random scalar $r$ and sends not $P$ but $r \cdot P$, a uniformly random point. The server multiplies by its secret key: $k \cdot (r \cdot P)$. The client divides the blind back out to get $k \cdot P$, and hashes that: $F(k, pw) = H_2(pw,, k \cdot H(pw))$.&lt;/p&gt;
&lt;p&gt;Now state exactly what the server saw. It saw $r \cdot P$: a uniformly random point on the curve, and nothing else. It never saw the password. It never saw the output. It applied its key to a value it could not read, and handed back a value it could not read either.&lt;/p&gt;

A two-party protocol that computes $F(k, x)$ where the server holds the key $k$, the client holds the input $x$, and when the protocol finishes the server has learned neither $x$ nor the output, while the client has learned only $F(k, x)$ and nothing about $k$ [@rfc9497].

A keyed function $F(k, x)$ whose outputs are indistinguishable from those of a truly random function to anyone who does not hold $k$. It is the &quot;keyed lock&quot;: only the keyholder can compute it, and its outputs reveal nothing about the key [@rfc9497].

The fresh, uniformly random scalar $r$ that the client multiplies into $H(x)$ before sending it. Because $r$ is uniform and independent of the input, the transmitted point $r \cdot H(x)$ is itself uniform and carries no information about $x$ [@rfc9497].

sequenceDiagram
    participant C as Client (holds input x)
    participant S as Server (holds key k)
    Note over C: P holds HashToGroup(x)
    Note over C: pick fresh random blind r
    Note over C: blindedElement holds r times P
    C-&amp;gt;&amp;gt;S: send blindedElement, a uniform random point
    Note over S: evaluatedElement holds k times blindedElement
    S-&amp;gt;&amp;gt;C: return evaluatedElement
    Note over C: divide out r to recover k times P
    Note over C: output holds Hash of x with that point
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The oblivious PRF applies a secret key to a secret input so that neither party learns the other&apos;s secret. Everything else in this guide is a variation on that one move, or a story about what happens when one half of it is done wrong.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That two-line trick is not a party piece. The &lt;em&gt;same&lt;/em&gt; two lines are how Privacy Pass hands you a token that a server can validate but cannot link back to you [@dgstv18]; how Google&apos;s Password Checkup tells you a credential appears in a breach corpus without Google learning the credential [@thomas19]; and how two organizations compute the intersection of their customer lists without either revealing its list [@fipr05]. Replace &quot;the component that used to have to see the secret&quot; with a blind, verifiable, rate-limited oracle that does not, and you have described every one of these systems at once.&lt;/p&gt;
&lt;p&gt;Because the move is always the same, you can interrogate any deployment with the same three questions. &lt;strong&gt;Is it blind?&lt;/strong&gt; Does the server ever see the input, or link two evaluations of it? &lt;strong&gt;Which key?&lt;/strong&gt; Can the client prove the answer was produced by the one key the server committed to? &lt;strong&gt;How many asks?&lt;/strong&gt; How many blind evaluations can one key answer before an attacker can reconstruct it? Hold onto those three questions. They are the spine of this article, and by the end you will see that every named failure in the primitive&apos;s history is one of them answered wrong.&lt;/p&gt;
&lt;p&gt;The 2HashDH shape $F(k, x) = H_2(x,, H_1(x)^k)$ we just wrote by hand is the actual construction that ships today [@jkk14]. If it feels like Diffie-Hellman run one-sided, that is exactly right: Part 18 argued that nobody has broken &lt;a href=&quot;https://paragmali.com/blog/nobody-broke-the-discrete-log-a-field-guide-to-diffie-hellma/&quot; rel=&quot;noopener&quot;&gt;the discrete logarithm&lt;/a&gt;, and this is its constructive twin -- the same group structure, run so that a server &lt;em&gt;applies&lt;/em&gt; a secret rather than agreeing on one. If you are wondering why we &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;harden passwords&lt;/a&gt; at all, Part 12 is the prerequisite. Here we take the hardening as given and ask how a server can perform it blind.&lt;/p&gt;
&lt;p&gt;{`
// STRUCTURAL MODEL, NOT REAL CRYPTO. It shows only the SHAPE of blind/evaluate/unblind.
// Model an elliptic-curve group of prime order q: a &quot;point&quot; is an integer mod q,
// and &quot;scalar multiplication&quot; scalar*point is multiplication mod q. BigInt avoids overflow.
const q = 2147483647n;                       // a prime group order (toy)
const mod = (a) =&amp;gt; ((a % q) + q) % q;
const scalarMul = (s, point) =&amp;gt; mod(s * point);
function invScalar(a) {                       // inverse of a scalar mod q (q prime), via Fermat
  let r = 1n, b = mod(a), e = q - 2n;
  while (e &amp;gt; 0n) { if (e &amp;amp; 1n) r = mod(r * b); b = mod(b * b); e &amp;gt;&amp;gt;= 1n; }
  return r;
}&lt;/p&gt;
&lt;p&gt;const k  = 1337n;                             // SERVER secret key (client never learns it)
const Hx = 991n;                              // H(x): the hashed input, as a group element
const r  = 42424243n;                         // CLIENT fresh random blind&lt;/p&gt;
&lt;p&gt;const blinded   = scalarMul(r, Hx);           // client SENDS r&lt;em&gt;H(x): looks uniformly random
const evaluated = scalarMul(k, blinded);      // server returns k&lt;/em&gt;(r&lt;em&gt;H(x))
const unblinded = scalarMul(invScalar(r), evaluated); // client removes r: recovers k&lt;/em&gt;H(x)
const direct    = scalarMul(k, Hx);           // what F would be if the server had seen x&lt;/p&gt;
&lt;p&gt;console.log(&apos;server saw            = &apos; + blinded.toString() + &apos; (independent of H(x)=&apos; + Hx.toString() + &apos;)&apos;);
console.log(&apos;client recovered      = &apos; + unblinded.toString());
console.log(&apos;equals direct k*H(x)  = &apos; + (unblinded === direct));
`}&lt;/p&gt;
&lt;p&gt;That trick is forty years old, and for its first two decades it did not even have a name. To understand why it is &lt;em&gt;secure&lt;/em&gt;, and why there turned out to be &lt;em&gt;three&lt;/em&gt; versions of it, we have to start where the blinding started: in 1982, with digital cash.&lt;/p&gt;
&lt;h2&gt;2. Blinding, Before It Had a Name&lt;/h2&gt;
&lt;p&gt;The move is older than the problem it now solves. In 1982, David Chaum wanted untraceable digital cash: a way for a bank to sign a coin without learning which coin it signed, so the coin could later be spent without the bank linking it back to the withdrawal. His solution was blinding [@chaum82]. The client multiplies its message by a random factor, the bank signs the blinded message under its &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 key&lt;/a&gt;, and the client divides the factor out to recover a valid signature on the original. Blind, let the keyholder act, unblind. Structurally, that is the exact template an OPRF runs on.Chaum&apos;s 1982 construction used multiplicative blinding in an RSA group, and its output was a publicly verifiable &lt;em&gt;signature&lt;/em&gt;. The OPRF performs the same blind-act-unblind dance in a Diffie-Hellman group, and its output is a pseudorandom value only the querier can read. Part 14 covers the RSA lineage in full.&lt;/p&gt;
&lt;p&gt;So if the trick existed in 1982, why did the OPRF take until 2005 to be named and 2023 to be standardized? Because Chaum&apos;s version produced the wrong kind of output, and the right kind needed a second ingredient that did not exist yet.&lt;/p&gt;
&lt;p&gt;Watch the shape of what Chaum built. A blind &lt;em&gt;signature&lt;/em&gt; is public: anyone with the bank&apos;s public key can verify it. That is perfect for a coin you want a merchant to check, and exactly wrong for a pepper you want no one to be able to reproduce. To harden a password, you do not want a verifiable signature on it; you want a pseudorandom value that only the keyholder could have produced and only the querier can read.&lt;/p&gt;
&lt;p&gt;Chaum had the blinding. He did not have a &lt;em&gt;pseudorandom&lt;/em&gt; thing to blind. This is why &quot;Blind RSA,&quot; standardized decades later as RFC 9474, is not an OPRF but its publicly verifiable cousin -- a distinction that will matter enormously when we reach Privacy Pass [@rfc9474].&lt;/p&gt;
&lt;p&gt;The second ingredient arrived in 1997. Moni Naor and Omer Reingold built a pseudorandom function out of pure number theory: $F_k(x)$ computed as a single group exponentiation whose exponent is a subset-product of the key, $g^{\prod a_i^{x_i}}$, provably secure under the Decisional Diffie-Hellman assumption [@naorreingold97]. What mattered was not the security proof but the &lt;em&gt;shape&lt;/em&gt;. Because the function was built from group exponentiations, it had enough algebraic structure that a blinded input could pass through it and come out blinded -- the homomorphic property that makes oblivious evaluation possible at all. A random hash function scrambles its input beyond recovery; an algebraically structured PRF lets the blind survive the trip.&lt;/p&gt;
&lt;p&gt;Now the two ingredients were on the table. In 2005, Michael Freedman, Yuval Ishai, Benny Pinkas, and Omer Reingold fused them, gave first constructions, and coined the term: the &lt;strong&gt;Oblivious Pseudorandom Function&lt;/strong&gt; [@fipr05]. They were not thinking about passwords. They wanted private keyword search and private set intersection -- letting one party query another&apos;s database, or intersect two sets, without either side revealing its contents.&lt;/p&gt;
&lt;p&gt;The abstraction they named is the one we still use: transplant Chaum&apos;s blinding out of RSA, point it at a Diffie-Hellman-structured PRF, and you get a value that is pseudorandom rather than a signature, and blind rather than seen.RFC 9497 standardizes &lt;em&gt;multiplicative&lt;/em&gt; blinding, the variant described here: its &lt;code&gt;Blind&lt;/code&gt; step computes &lt;code&gt;blindedElement = blind * inputElement&lt;/code&gt;, and that multiplicative form is what ships.&lt;/p&gt;

flowchart LR
    A[&quot;1982 Chaum blinding in RSA&quot;] --&amp;gt; B[&quot;1986 Fiat-Shamir transform&quot;]
    B --&amp;gt; C[&quot;1992 Chaum-Pedersen DLEQ proof&quot;]
    C --&amp;gt; D[&quot;1997 Naor-Reingold algebraic PRF&quot;]
    D --&amp;gt; E[&quot;2005 FIPR names the OPRF&quot;]
    E --&amp;gt; F[&quot;2009 Jarecki-Liu efficient DH OPRF&quot;]
    F --&amp;gt; G[&quot;2014 2HashDH, the workhorse&quot;]
    G --&amp;gt; H[&quot;2018 Privacy Pass VOPRF&quot;]
    H --&amp;gt; I[&quot;2022 3HashSDHI POPRF&quot;]
    I --&amp;gt; J[&quot;2023 RFC 9497 standard&quot;]
    J --&amp;gt; K[&quot;2025 RFC 9807 OPAQUE&quot;]
&lt;p&gt;Two other pieces on that timeline were quietly waiting to become load-bearing. In 1986, Fiat and Shamir showed how to turn an interactive proof into a one-shot, non-interactive one by replacing the verifier&apos;s random challenge with a hash of the transcript [@fiatshamir86]. In 1992, Chaum and Pedersen gave a zero-knowledge proof that two discrete logarithms are equal [@chaumped92]. Neither was about OPRFs. Both become the machinery of &lt;em&gt;verifiability&lt;/em&gt; when, twenty-six years later, Privacy Pass needs a client to check which key the server used. Keep them in view; they return in Section 5.&lt;/p&gt;
&lt;p&gt;The problem was defined and it had a name. What it did not have was a construction cheap enough to ship. And the first one that looked practical turned out to have a slow poison built into it.&lt;/p&gt;
&lt;h2&gt;3. The First Construction That Got Weaker With Use&lt;/h2&gt;
&lt;p&gt;Naming a primitive is not the same as having a recipe worth shipping. The constructions that came with the 2005 definition were general and powerful and, for a deployed service, awkward. The oblivious-transfer and oblivious-polynomial-evaluation approaches were round-heavy. The Naor-Reingold-based ones inherited the PRF&apos;s per-bit structure: they consumed the input one bit at a time, so their cost scaled with the input length rather than staying flat.The linear-in-input-length cost of the early Naor-Reingold-style OPRFs is an inference from that PRF&apos;s per-bit construction rather than a benchmark quoted from the 2005 paper; treat it as a structural signpost, not a measured figure. For private set intersection in a research prototype, fine. For a server answering millions of blind queries a second, not yet.&lt;/p&gt;
&lt;p&gt;The efficient turn came in 2009. Stanislaw Jarecki and Xiaomin Liu built the first OPRF that was both fast and &lt;em&gt;committed&lt;/em&gt; -- verifiable -- in a prime-order group [@jareckiliu09]. Instead of the exponentiation-per-bit structure, it used a Dodis-Yampolskiy-style PRF of the form $g^{1/(k+x)}$: the server inverts $k + x$ in the exponent, so evaluation is a single group operation regardless of input length.&lt;/p&gt;
&lt;p&gt;And because the construction committed to the key, the client got assurance not just that &lt;em&gt;some&lt;/em&gt; key was applied but that it was the server&apos;s key. In the language of our three questions, this one answered Q1 and Q2 at once. It looked like the deployable OPRF.&lt;/p&gt;
&lt;p&gt;Then look at the assumption it rested on. Security of the $g^{1/(k+x)}$ family reduces to a &lt;em&gt;q-strong-Diffie-Hellman&lt;/em&gt; assumption: hardness holds as long as the attacker cannot exploit a growing collection of key-dependent samples. And three years earlier, in 2006, Jung Hee Cheon had shown precisely how to exploit them. Cheon&apos;s analysis proved that the strong-Diffie-Hellman family weakens as the attacker obtains more samples tied to the same secret -- security &lt;em&gt;degrades with the number of queries&lt;/em&gt; [@cheon06].&lt;/p&gt;
&lt;p&gt;Sit with what that means for an OPRF specifically. An OPRF&apos;s entire job is to be offered as a service: to answer blind query after blind query, indefinitely. But every answer this construction gave was another key-dependent sample, and Cheon&apos;s result said each sample chipped away at the secret. The primitive got &lt;em&gt;weaker the more it was used&lt;/em&gt;. A lock that loosens every time you turn the key is a strange thing to install on a vault you intend to open a billion times.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Most cryptographic primitives are static: a hash function is exactly as strong after a trillion evaluations as after one. The 2009 Jarecki-Liu OPRF was not. Its security was a budget that the act of answering queries spent down, because each blind evaluation handed the attacker another sample against the strong-Diffie-Hellman assumption it relied on [@cheon06]. For a primitive whose whole purpose is to be queried without limit, that is a design-level contradiction, not a tuning problem. This is our third question -- &lt;em&gt;how many asks?&lt;/em&gt; -- appearing for the first time, and appearing as a wound rather than a footnote.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is worth marking as special: it is the only true assumption-level break in the whole story. Everything else that supersedes something in the lineage does so because it is cheaper, or answers one more question. Only here does a construction fall because the mathematical ground under it gave way. The strong-Diffie-Hellman assumption was the wrong foundation, and no engineering on top could fix a foundation that eroded by design.&lt;/p&gt;
&lt;p&gt;The fix was not a cleverer attack defense. It was to stop building on an assumption that decays -- to hash the input to &lt;em&gt;one&lt;/em&gt; group element and re-base the whole construction on an assumption that does not weaken sample by sample. That single stroke produced the workhorse that every deployment now runs, and it is where the OPRF stops being a research object and becomes infrastructure.&lt;/p&gt;
&lt;h2&gt;4. Moving the Server From Trusted to Blind&lt;/h2&gt;
&lt;p&gt;Here is the whole next fifteen years compressed into one sentence: each generation moves the server one step from &quot;must be trusted, sees the secret&quot; toward &quot;blind, verifiable, query-limited,&quot; and -- the part that surprises people -- most of those steps are &lt;em&gt;additive&lt;/em&gt;. A later construction rarely throws out its predecessor. It bolts one more guarantee onto it. To read the lineage correctly you have to separate two kinds of arrow: &lt;strong&gt;superseded by&lt;/strong&gt;, a genuine replacement, and &lt;strong&gt;extended by&lt;/strong&gt;, an additive answer to one more question with the old construction still running underneath.&lt;/p&gt;
&lt;h3&gt;The workhorse: 2HashDH&lt;/h3&gt;
&lt;p&gt;In 2014, Stanislaw Jarecki, Aggelos Kiayias, and Hugo Krawczyk wrote down the construction we built by hand in Section 1: $F(k, x) = H_2(x,, H_1(x)^k)$ [@jkk14]. Hash the input into the group with $H_1$, exponentiate by the key, hash out with $H_2$.&lt;/p&gt;
&lt;p&gt;It is round-optimal -- one message each way, and you provably cannot do it in fewer. And its security rests on the &lt;strong&gt;One-More Gap Diffie-Hellman&lt;/strong&gt; assumption rather than the strong-Diffie-Hellman family, which is the whole point: One-More Gap security is not a function of a growing pile of key-dependent samples, so it sidesteps the Cheon erosion that doomed the 2009 construction.&quot;One-More&quot; assumptions bound how much an attacker can produce relative to how many genuine evaluations it was given -- it cannot get &quot;one more&quot; than it was handed. This framing is what breaks the dependence on a growing q-tuple that made the strong-Diffie-Hellman family decay under Cheon&apos;s attack. Because the input is hashed to a single group element, cost is flat in input length. Jarecki, Kiayias, Krawczyk, and Jiayu Xu put it on composable footing two years later [@jkkx16].&lt;/p&gt;

The construction $F(k, x) = H_2(x,\, H_1(x)^k)$: hash the input into a prime-order group, raise it to the secret key, and hash the result back out with the input bound in. It is the OPRF and VOPRF core standardized in RFC 9497 [@jkk14], [@rfc9497].
&lt;p&gt;This is the OPRF that ships. But notice what it does &lt;em&gt;not&lt;/em&gt; do. It answers Q1 -- the server never sees the input -- and nothing more. The client has no way to tell &lt;em&gt;which&lt;/em&gt; key the server used. A malicious server can quietly assign a different key to each client and, later, use the distinct responses to tell which client was which. In an anonymity system that is fatal: the primitive designed to hide you becomes the tag that identifies you. 2HashDH left Q2 open. The next generation did not replace it; it extended it.&lt;/p&gt;
&lt;h3&gt;Verifiability you can batch: the VOPRF&lt;/h3&gt;
&lt;p&gt;In 2018, Alex Davidson, Ian Goldberg, Nick Sullivan, George Tankersley, and Filippo Valsorda shipped Privacy Pass, and with it the deployed &lt;strong&gt;VOPRF&lt;/strong&gt; [@dgstv18]. The addition is precisely the 1992 machinery we set aside. The server publishes a commitment to its key -- a public key $pk_S = k \cdot G$ -- and alongside each response attaches a Chaum-Pedersen proof that the &lt;em&gt;same&lt;/em&gt; $k$ relates its public key and its answer, made non-interactive by Fiat-Shamir [@chaumped92], [@fiatshamir86]. The client verifies before it accepts. Answering Q2, at last.&lt;/p&gt;
&lt;p&gt;The engineering move that made it deployable was batching: many evaluations fold into a &lt;em&gt;single&lt;/em&gt; proof via a random linear combination, so a client redeeming a thousand tokens verifies one proof, not a thousand [@dgstv18]. That is what let Privacy Pass run at content-delivery-network scale. That scale is what surfaced the primitive&apos;s real-world failure modes -- key consistency, tagging, rotation, double-spend -- that no research prototype had stress-tested.&lt;/p&gt;

An OPRF augmented with a proof that the server applied the key committed in its published public key, so a client can reject an answer computed under any other key. In RFC 9497 the proof is a batchable Chaum-Pedersen DLEQ [@rfc9497], [@dgstv18].
&lt;p&gt;The VOPRF still left one gap. Its verifiability binds &lt;em&gt;one&lt;/em&gt; key to &lt;em&gt;one&lt;/em&gt; public key, which means one key cannot cleanly serve many domains, tenants, or rotation epochs -- to separate them you would need a different key per context, and a different public key per context is, again, a tag. What was missing was a way to fold a &lt;em&gt;public&lt;/em&gt; label into the evaluation without changing the key.&lt;/p&gt;
&lt;h3&gt;Partial obliviousness: heavy, then light&lt;/h3&gt;
&lt;p&gt;The first answer came early and expensive. In 2015, Adam Everspaugh, Rahul Chatterjee, Samuel Scott, Ari Juels, and Thomas Ristenpart built Pythia, the first &lt;strong&gt;partially oblivious&lt;/strong&gt; PRF: a public per-tenant tweak let one server key serve many domains and support bulk key rotation, while the password stayed oblivious [@pythia]. It worked, but it needed bilinear pairings -- special curves, heavier operations, a bigger dependency. Powerful, but not the thing you reach for by default.&lt;/p&gt;
&lt;p&gt;The light answer came in 2022. Nirvan Tyagi, Sofia Celi, Thomas Ristenpart, Nick Sullivan, Stefano Tessaro, and Christopher Wood built &lt;strong&gt;3HashSDHI&lt;/strong&gt;: the first POPRF that needs no pairings [@tcrstw22]. It folds the public input &lt;code&gt;info&lt;/code&gt; into the key by inversion. The server computes $t = k + H(\text{info})$ and returns $t^{-1} \cdot \text{blindedElement}$, with a DLEQ proof against the tweaked key. The private input stays oblivious; &lt;code&gt;info&lt;/code&gt; is public and known to both sides. And it runs, in the authors&apos; words, as fast as the standards-track 2HashDH protocol [@tcrstw22].&lt;/p&gt;

A VOPRF that also binds a *public* input `info` into the evaluation, computing $F(k, x, \text{info})$ while keeping $x$ oblivious. One published key can then serve many public contexts -- tenants, rotation epochs, rate-limit buckets. A POPRF with a fixed `info` is functionally a VOPRF [@rfc9497], [@tcrstw22].
&lt;p&gt;Here is the detail worth savoring. To fold in &lt;code&gt;info&lt;/code&gt;, 3HashSDHI uses key inversion -- $t^{-1}$ where $t = k + H(\text{info})$ -- which is the &lt;em&gt;same&lt;/em&gt; Dodis-Yampolskiy inversion structure Jarecki and Liu used back in 2009 [@tcrstw22], [@jareckiliu09]. The mechanism that got the field burned by Cheon&apos;s attack was never actually wrong. What was wrong was the &lt;em&gt;assumption it had been analyzed under&lt;/em&gt;. Tyagi and co-authors re-proved the same inversion under a One-More Gap assumption that reduces to plain discrete log in the algebraic group model, and the old idea walked back onto the field rehabilitated [@tcrstw22].&lt;/p&gt;
&lt;p&gt;The lesson is one every protocol designer should internalize: a construction can be sound while the security assumption used to justify it is not, and better analysis can resurrect a &quot;broken&quot; mechanism untouched.&lt;/p&gt;

flowchart TD
    G0[&quot;Gen 0: Chaum blinding plus Naor-Reingold PRF&quot;] --&amp;gt;|superseded| G1[&quot;Gen 1: FIPR names the OPRF, generic&quot;]
    G1 --&amp;gt;|superseded on cost| G2[&quot;Gen 2: Jarecki-Liu committed DH OPRF&quot;]
    G2 --&amp;gt;|superseded by Cheon erosion| G3[&quot;Gen 3: 2HashDH, answers Q1&quot;]
    G3 --&amp;gt;|extended, adds Q2| G4[&quot;Gen 4: VOPRF with batched DLEQ&quot;]
    G4 --&amp;gt;|extended, adds public info| G5[&quot;Gen 5: POPRF 3HashSDHI&quot;]
    G5 --&amp;gt; RFC[&quot;RFC 9497: three modes, one interface&quot;]
    G3 -.-&amp;gt;|parallel mechanism| K[&quot;KKRT16 OT-extension OPRF, for PSI&quot;]
&lt;h3&gt;The other lineage, in one breath&lt;/h3&gt;
&lt;p&gt;Not every OPRF is a Diffie-Hellman OPRF. In 2016, Vladimir Kolesnikov, Ranjit Kumaresan, Mike Rosulek, and Ni Trieu built a completely different one: a batched OPRF from oblivious-transfer extension, symmetric-key rather than public-key, and enormously fast in bulk [@kkrt16]. It is not verifiable, not publicly reusable, and only semi-honest secure, but it intersects two sets of a million ($2^{20}$) elements each in about 3.8 seconds [@kkrt16], and OT-extension OPRFs of this kind were for years the throughput workhorse of high-volume semi-honest PSI, until VOLE-based PSI overtook them in communication for very large sets [@rindalvolepsi]. The takeaway for a practitioner: &quot;OPRF&quot; names a &lt;em&gt;goal&lt;/em&gt;, and at least two very different mechanisms reach it. Do not assume every system labeled OPRF is the standardized Diffie-Hellman family.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Key idea&lt;/th&gt;
&lt;th&gt;Questions answered&lt;/th&gt;
&lt;th&gt;Fate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Chaum blind signature&lt;/td&gt;
&lt;td&gt;1982&lt;/td&gt;
&lt;td&gt;Blind, sign, unblind in RSA&lt;/td&gt;
&lt;td&gt;none (a signature, not a PRF)&lt;/td&gt;
&lt;td&gt;Ancestor; became Blind RSA [@chaum82]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIPR generic OPRF&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;td&gt;Obliviously evaluate an algebraic PRF; names &quot;OPRF&quot;&lt;/td&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;Superseded on cost [@fipr05]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jarecki-Liu&lt;/td&gt;
&lt;td&gt;2009&lt;/td&gt;
&lt;td&gt;$g^{1/(k+x)}$, committed&lt;/td&gt;
&lt;td&gt;Q1 + Q2&lt;/td&gt;
&lt;td&gt;Superseded: strong-DH erodes with use [@jareckiliu09]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2HashDH&lt;/td&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;$H_2(x, H_1(x)^k)$; One-More Gap CDH&lt;/td&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;Active core of RFC 9497 [@jkk14]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VOPRF&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;2HashDH + batched Chaum-Pedersen DLEQ&lt;/td&gt;
&lt;td&gt;Q1 + Q2&lt;/td&gt;
&lt;td&gt;Active; extends 2HashDH [@dgstv18]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pythia&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;Verifiable partially oblivious PRF, pairings&lt;/td&gt;
&lt;td&gt;Q1 + Q2 + info&lt;/td&gt;
&lt;td&gt;Superseded by 3HashSDHI [@pythia]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3HashSDHI&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;$(k + H(\text{info}))^{-1}$; no pairings&lt;/td&gt;
&lt;td&gt;Q1 + Q2 + info&lt;/td&gt;
&lt;td&gt;Active POPRF of RFC 9497 [@tcrstw22]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KKRT16&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;Batched OPRF from OT-extension (symmetric)&lt;/td&gt;
&lt;td&gt;Q1 (relaxed, semi-honest)&lt;/td&gt;
&lt;td&gt;Active, niche: high-volume PSI [@kkrt16]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Twenty years of constructions now answered all three questions. But each was a separate paper with its own parameters, its own group choices, and its own foot-guns. The last move was not a new construction. It was a standard that unified them and, unusually for a specification, wrote the one unavoidable cost down as a number.&lt;/p&gt;
&lt;h2&gt;5. RFC 9497: Three Modes, One Interface&lt;/h2&gt;
&lt;p&gt;In December 2023, the IRTF&apos;s Crypto Forum Research Group published RFC 9497, written by Alex Davidson, Armando Faz-Hernandez, Nick Sullivan, and Christopher Wood [@rfc9497]. It did what twenty years of individual papers had not: it turned the whole family into a single misuse-resistant interface, fixed five ciphersuites, mandated the safety checks, and -- the honest move no construction paper had made -- wrote the one attack it cannot design away down as a number.&lt;/p&gt;
&lt;p&gt;Start with the interface, because all three modes share it. Over a prime-order group with generator $G$, and using the &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;hash-to-group machinery of RFC 9380&lt;/a&gt;, the protocol is three calls [@rfc9497], [@rfc9380]:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Blind(x):
    blind           &amp;lt;- random nonzero scalar               # fresh, uniform, per call
    blindedElement  &amp;lt;- blind * HashToGroup(x)              # a uniformly random point
    if blindedElement == Identity: abort                   # element validation
    return blind, blindedElement

BlindEvaluate(k, blindedElement, [info]):
    OPRF / VOPRF:  evaluatedElement &amp;lt;- k * blindedElement
    POPRF:         t &amp;lt;- k + HashToScalar(info)
                   evaluatedElement &amp;lt;- t^(-1) * blindedElement
    # VOPRF and POPRF also emit a DLEQ proof (see below)

Finalize(x, blind, evaluatedElement, [info]):
    N &amp;lt;- blind^(-1) * evaluatedElement    # OPRF/VOPRF: = k * HashToGroup(x); POPRF: = HashToGroup(x)^(1/(k+H(info)))
    return Hash( len(x) || x || [len(info) || info ||] len(N) || N || &quot;Finalize&quot; )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server sees only &lt;code&gt;blindedElement&lt;/code&gt;, a uniformly random point independent of $x$; the client recovers exactly one PRF value and nothing about $k$ [@rfc9497]. That is the same 2HashDH we have been circling, now with every byte of the output hash pinned down.&lt;code&gt;HashToGroup&lt;/code&gt; is RFC 9380 hash-to-curve, the subject of Part 16. Its defining property is that the output point has an &lt;em&gt;unknown&lt;/em&gt; discrete logarithm: no one, including the client, knows a scalar relating it to the generator. That unknown-discrete-log property is exactly why blinding hides the input and why the server cannot invert the map.&lt;/p&gt;
&lt;h3&gt;The three modes are three answers&lt;/h3&gt;
&lt;p&gt;This is where the second shift in understanding lands. The three modes are not three products. They are three points on one axis, distinguished by exactly which of our questions they answer.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;base OPRF&lt;/strong&gt; answers Q1 and stops. &lt;code&gt;BlindEvaluate&lt;/code&gt; is just $k \cdot \text{blindedElement}$, with no proof. The client trusts that &lt;em&gt;some&lt;/em&gt; key was applied; it gets no assurance about which. Cheapest mode, smallest messages, right only when a wrong key can hurt no one but the querier [@rfc9497].&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;VOPRF&lt;/strong&gt; answers Q1 and Q2. The server commits to a public key $pk_S = k \cdot G$ and attaches a proof; the client verifies before finalizing [@rfc9497]. The &lt;strong&gt;POPRF&lt;/strong&gt; answers Q1 and Q2 and binds a public &lt;code&gt;info&lt;/code&gt;, via the key-inversion tweak. A POPRF with a fixed &lt;code&gt;info&lt;/code&gt; collapses back to a VOPRF [@rfc9497]. Base to verifiable to partially oblivious: blind, then blind-plus-which-key, then blind-plus-which-key-plus-public-tweak.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Three modes are three answers to three questions. OPRF answers &quot;is it blind?&quot; VOPRF adds &quot;which key?&quot; POPRF adds &quot;and bind this public label.&quot; You do not choose a mode by taste; you choose it by counting how many of the three questions your threat model forces you to answer.&lt;/p&gt;
&lt;/blockquote&gt;

flowchart LR
    Q1[&quot;Q1: is it blind?&quot;] --&amp;gt; OPRF[&quot;OPRF: blind only&quot;]
    OPRF --&amp;gt;|add DLEQ proof against one committed key| VOPRF[&quot;VOPRF: blind plus which key&quot;]
    VOPRF --&amp;gt;|fold in a public info tweak| POPRF[&quot;POPRF: blind, which key, public label&quot;]
&lt;h3&gt;Verifiability, in depth&lt;/h3&gt;
&lt;p&gt;The proof is the 1992 machinery finally doing its job. A Chaum-Pedersen DLEQ proof establishes that two discrete logarithms are equal: that the $k$ relating $pk_S$ to $G$ is the &lt;em&gt;same&lt;/em&gt; $k$ relating &lt;code&gt;evaluatedElement&lt;/code&gt; to &lt;code&gt;blindedElement&lt;/code&gt; [@rfc9497], [@chaumped92]. If the server used any other key, no valid proof exists. Fiat-Shamir turns the interactive proof into a single non-interactive object the client checks offline, so verifiability costs no extra round [@fiatshamir86].The DLEQ proof is a sigma protocol made non-interactive by Fiat-Shamir -- the same transform behind &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;Schnorr signatures&lt;/a&gt;, which Part 17 covers. The client recomputes the challenge as a hash of the transcript and checks one equation; there is no back-and-forth with the server.&lt;/p&gt;

A non-interactive zero-knowledge proof that two discrete logarithms are equal -- here, that the server&apos;s public key and its response were produced under the same secret key. It is what makes a VOPRF verifiable, and via a random linear combination it can prove a whole batch of responses with one object [@rfc9497], [@chaumped92].
&lt;p&gt;Batching is what makes verifiability affordable. Many evaluations fold into one proof through a random linear combination, so a client redeeming a batch verifies a single object, not one proof per element [@rfc9497], [@dgstv18]. That is one proof object per batch, not $n$ -- but verifying it still forms one linear combination across the batch, so the win is skipping $n$ separate proof checks, not making verification constant-time in the batch size. That is what let anonymous-token issuance scale.&lt;/p&gt;
&lt;p&gt;There is a subtlety that quietly carries the entire security of any anonymity system built on a VOPRF: verifiability is only as strong as &lt;em&gt;key consistency&lt;/em&gt;. Proving the server used the key in its public key is worthless if the server hands a &lt;em&gt;different&lt;/em&gt; public key to every client, because then the unique key is itself a deanonymizing tag. Verifiability protects you only when everyone is served under &lt;em&gt;one&lt;/em&gt; transparently published key [@rfc9497]. We will return to this in the practical guide; for now, note that &quot;add a proof&quot; and &quot;pin one key&quot; are two requirements, not one.&lt;/p&gt;

sequenceDiagram
    participant C as Client (input x)
    participant S as Server (key k, public pkS)
    Note over C: blindedElement holds blind times HashToGroup(x)
    C-&amp;gt;&amp;gt;S: send blindedElement
    Note over S: evaluatedElement holds k times blindedElement
    Note over S: GenerateProof binds pkS and the answer to one k
    S-&amp;gt;&amp;gt;C: return evaluatedElement and DLEQ proof
    Note over C: VerifyProof against pkS, abort on failure
    Note over C: unblind, then Finalize with domain separation
&lt;h3&gt;The machinery that makes it safe&lt;/h3&gt;
&lt;p&gt;A specification earns &quot;misuse-resistant&quot; in the details, and RFC 9497&apos;s details are where the accumulated scar tissue lives. Hashing and group operations are delegated, not reinvented: &lt;code&gt;HashToGroup&lt;/code&gt; and &lt;code&gt;HashToScalar&lt;/code&gt; come from RFC 9380, and the groups themselves from RFC 9496 -- &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;ristretto255 and decaf448&lt;/a&gt; -- or the NIST curves [@rfc9497], [@rfc9380], [@rfc9496].ristretto255 and decaf448 are prime-order abstractions built over Edwards curves whose raw order has a small cofactor. They remove the cofactor pitfalls that Part 15 covers -- the small-subgroup and equivalent-point traps -- which is exactly why RFC 9497 prefers them and why it mandates rejecting the identity element on deserialization.&lt;/p&gt;
&lt;p&gt;Three requirements are worth naming because dropping any one silently breaks the guarantee:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; RFC 9497 requires that every deserialized element be &lt;em&gt;validated&lt;/em&gt; -- on-curve, in the right range, and not the identity -- or an attacker can probe the key with malformed inputs [@rfc9497]. It requires that operations on secret data, &lt;em&gt;including&lt;/em&gt; &lt;code&gt;GenerateProof&lt;/code&gt; and &lt;code&gt;BlindEvaluate&lt;/code&gt;, run in &lt;em&gt;constant time&lt;/em&gt;, or the server leaks its key through timing [@rfc9497]. And it requires &lt;em&gt;domain separation&lt;/em&gt;: the literal &lt;code&gt;&quot;Finalize&quot;&lt;/code&gt; tag and the mode context string keep one application&apos;s evaluations from colliding with another&apos;s [@rfc9497]. None of these is optional hardening; each is load-bearing for a property the primitive claims.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &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;domain separation&lt;/a&gt; is the discipline Part 13 argued for, applied here: the output hash binds the input length, the input, the point, and a fixed label, so no two contexts can be coaxed into producing the same value. Inputs are bounded -- shorter than $2^{16}-1$ bytes, with a two-byte length prefix -- so longer inputs must be pre-hashed [@rfc9497].&lt;/p&gt;

The guarantee that the server cannot link two evaluations of the same input, nor learn anything about the input. Because the blind is fresh and uniform, this holds *even against an adversary with unbounded computation* -- it is information-theoretic, not merely computational [@rfc9497], [@dgstv18].
&lt;p&gt;{`
// STRUCTURAL MODEL. Shows why Finalize binds lengths + a &quot;Finalize&quot; tag, not just N.
function toyHash(str) {                       // a tiny non-cryptographic stand-in
  let h = 5381 &amp;gt;&amp;gt;&amp;gt; 0;
  for (const ch of str) h = (Math.imul(h, 33) ^ ch.charCodeAt(0)) &amp;gt;&amp;gt;&amp;gt; 0;
  return (h &amp;gt;&amp;gt;&amp;gt; 0).toString(16).padStart(8, &apos;0&apos;);
}
// length-prefixed encoding: len(x) || x, so boundaries are unambiguous
function lp(s) { return String(s.length).padStart(4, &apos;0&apos;) + s; }&lt;/p&gt;
&lt;p&gt;// RFC 9497 Finalize binds: len(x) || x || len(N) || N || &quot;Finalize&quot;
function finalize(x, N) { return toyHash(lp(x) + lp(N) + &apos;Finalize&apos;); }&lt;/p&gt;
&lt;p&gt;// Without length prefixes, (&quot;ab&quot;,&quot;c&quot;) and (&quot;a&quot;,&quot;bc&quot;) would collide. With them, they don&apos;t:
console.log(&apos;naive  ab|c   =&apos;, toyHash(&apos;ab&apos; + &apos;c&apos;));
console.log(&apos;naive  a|bc   =&apos;, toyHash(&apos;a&apos; + &apos;bc&apos;), &apos;(collides above)&apos;);
console.log(&apos;finalize ab,c =&apos;, finalize(&apos;ab&apos;, &apos;c&apos;));
console.log(&apos;finalize a,bc =&apos;, finalize(&apos;a&apos;, &apos;bc&apos;), &apos;(distinct)&apos;);
`}&lt;/p&gt;
&lt;p&gt;Finally, the honesty. RFC 9497 section 7.2.3 states outright that every &lt;code&gt;BlindEvaluate&lt;/code&gt; is a static-Diffie-Hellman oracle sample, and quantifies the erosion: the best-known attacks reduce the group&apos;s security by $\log_2(Q)/2$ bits, where $Q$ is the number of calls [@rfc9497], [@browngallant04], [@cheon06]. That is the domesticated ghost of the very failure that killed the 2009 construction -- no longer a surprise, but a budget line. We give it the full treatment it deserves in Section 8. First: who actually runs this, and what looks like it but is not.&lt;/p&gt;
&lt;h2&gt;6. Who Runs This Today&lt;/h2&gt;
&lt;p&gt;The OPRF is quietly load-bearing. You almost certainly used one this week without knowing it, because it lives one layer below the protocols you &lt;em&gt;do&lt;/em&gt; know. Here are the four deployed consumers, each at &quot;exactly where and why it calls the OPRF,&quot; and each paired with the look-alike an expert must not confuse it with.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OPAQUE, standardized as RFC 9807 in July 2025.&lt;/strong&gt; This is the flagship. OPAQUE is an augmented password-authenticated key exchange: the server authenticates you by password without ever seeing the password, even at registration, and with no cleartext salt an attacker could precompute against [@rfc9807], [@opaque-paper]. The OPRF is the tool that makes the password oblivious, and it is what defeats precomputation attacks -- there is nothing static to build a rainbow table against.&lt;/p&gt;
&lt;p&gt;RFC 9807 derives a &lt;em&gt;per-user&lt;/em&gt; OPRF key from a single seed, keyed by each &lt;code&gt;credential_identifier&lt;/code&gt;, so a server compromise does not let an attacker enumerate accounts, and it discusses a threshold-OPRF defense that forces an attacker online before any dictionary attack can even begin [@rfc9807]. The RFC names WhatsApp&apos;s end-to-end encrypted backups as a driving use case, and Meta&apos;s audited &lt;code&gt;opaque-ke&lt;/code&gt; is the reference implementation [@rfc9807], [@opaque-ke]. If Part 12 explained why offline dictionary attacks are the thing to fear, OPAQUE is the OPRF&apos;s answer to them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Privacy Pass, RFCs 9576, 9577, and 9578, June 2024.&lt;/strong&gt; The in-scope token type is &lt;code&gt;0x0001 = VOPRF(P-384, SHA-384)&lt;/code&gt;; RFC 9578 says plainly that its privately verifiable issuance is based on the OPRF [@rfc9578], [@rfc9576], [@rfc9577]. This is the VOPRF most engineers meet by name. But Privacy Pass is &lt;em&gt;not one thing&lt;/em&gt;, and this is the boundary the whole article has been protecting.&lt;/p&gt;

RFC 9578 defines two interchangeable token types. Token `0x0001` is a VOPRF, and it is an OPRF. Token `0x0002` is Blind RSA (2048-bit), standardized in RFC 9474, and it is a blind *signature* -- Chaum&apos;s 1982 construction, publicly verifiable, and emphatically *not* an OPRF [@rfc9578], [@rfc9474]. The most visible Privacy Pass deployment in the world, Apple&apos;s Private Access Tokens, uses the Blind-RSA token, not the VOPRF one. The two make a real, standardized trade-off: VOPRF tokens are issuer-verifiable only, small, and easy to rotate; Blind-RSA tokens are publicly verifiable with the RSA public key but larger and heavier to rotate [@rfc9578]. So never write &quot;Privacy Pass equals OPRF.&quot; One of its two token types is not an OPRF at all -- a point the earlier field-guide entry on anonymous credentials shipping develops further.
&lt;p&gt;&lt;strong&gt;Password breach checking.&lt;/strong&gt; Google&apos;s Password Checkup performs a blinded, OPRF-based lookup: it checks your credential against a corpus of breached ones without Google learning the credential [@thomas19], [@google-blog]. Microsoft&apos;s Password Monitor in Edge does something related but not identical, and the difference matters.&lt;/p&gt;

It is tempting to describe Microsoft Password Monitor as &quot;an OPRF breach check,&quot; but that undersells the machinery. The OPRF is one layer; the actual set-matching runs on *homomorphic encryption* -- Labeled Private Set Intersection built on the Microsoft SEAL library, evaluating the match under encryption [@msft-monitor]. The OPRF hides the query; the fully homomorphic layer performs the private lookup against the breach set. Do not attribute the set intersection to the OPRF alone. And note the well-known *non-OPRF* alternative for the same job: Have I Been Pwned&apos;s k-anonymity range API, where the client sends the first five hex digits of a SHA-1 hash and receives the whole bucket to search locally [@cloudflare-kanon]. That leaks a hash prefix by design; it is prefix-bucketed hashing, simpler and cache-friendly, but not oblivious. The earlier entry on [Edge&apos;s two password cryptographies](/blog/edge-two-password-cryptographies/) traces this contrast in detail.
&lt;p&gt;&lt;strong&gt;OPRF-based private set intersection.&lt;/strong&gt; OPRF-PSI is &lt;em&gt;one approach among several&lt;/em&gt; -- plain Diffie-Hellman PSI, oblivious-transfer and circuit-based PSI, and homomorphic-encryption PSI all compete for different workloads [@fipr05]. Even within OPRF-PSI, the two mechanisms from Section 4 occupy different corners: the public-key 2HashDH family when you want few messages and verifiability, and the symmetric KKRT16 OPRF when you want raw throughput over enormous sets [@kkrt16].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;What the OPRF does there&lt;/th&gt;
&lt;th&gt;The non-OPRF look-alike to not confuse it with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;OPAQUE (RFC 9807)&lt;/td&gt;
&lt;td&gt;Makes the password oblivious; defeats precomputation; per-user keys block enumeration [@rfc9807]&lt;/td&gt;
&lt;td&gt;A plain salted or peppered hash, where the server &lt;em&gt;must&lt;/em&gt; see the password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Pass token &lt;code&gt;0x0001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;VOPRF issuance of unlinkable, issuer-verifiable tokens [@rfc9578]&lt;/td&gt;
&lt;td&gt;Token &lt;code&gt;0x0002&lt;/code&gt; = Blind RSA, a publicly verifiable blind &lt;em&gt;signature&lt;/em&gt; (Apple PAT) [@rfc9474]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Password Checkup&lt;/td&gt;
&lt;td&gt;Blinded OPRF lookup against a breach corpus [@thomas19]&lt;/td&gt;
&lt;td&gt;HIBP k-anonymity range API: leaks a SHA-1 prefix, not oblivious [@cloudflare-kanon]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Password Monitor&lt;/td&gt;
&lt;td&gt;OPRF hides the query; FHE Labeled PSI does the match [@msft-monitor]&lt;/td&gt;
&lt;td&gt;Assuming the OPRF alone does the set intersection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPRF-based PSI&lt;/td&gt;
&lt;td&gt;Oblivious set membership via 2HashDH or KKRT16 [@kkrt16]&lt;/td&gt;
&lt;td&gt;DH-PSI, OT/circuit-PSI, and FHE-PSI, which are different approaches&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The production substrates are a short list: Cloudflare&apos;s CIRCL in Go, the &lt;code&gt;voprf&lt;/code&gt; crate and &lt;code&gt;opaque-ke&lt;/code&gt; in Rust, and curve25519-dalek providing the ristretto255 group backend [@circl], [@voprf-crate], [@opaque-ke], [@dalek]. Four deployments, one primitive, three look-alikes that are emphatically not it. When you actually have to choose, the decision comes down to a small matrix.&lt;/p&gt;
&lt;h2&gt;7. The Decision Matrix&lt;/h2&gt;
&lt;p&gt;Given a real problem, each decision has a crisp rule. Here they are, head to head.&lt;/p&gt;
&lt;h3&gt;Which mode?&lt;/h3&gt;
&lt;p&gt;The primary choice is the mode, and it falls straight out of the three questions.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;OPRF (base)&lt;/th&gt;
&lt;th&gt;VOPRF&lt;/th&gt;
&lt;th&gt;POPRF&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Questions answered&lt;/td&gt;
&lt;td&gt;Q1 (blind)&lt;/td&gt;
&lt;td&gt;Q1 + Q2 (which key)&lt;/td&gt;
&lt;td&gt;Q1 + Q2 + public tweak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra output&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;DLEQ proof (batchable)&lt;/td&gt;
&lt;td&gt;DLEQ proof (batchable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public input &lt;code&gt;info&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;yes, $F(k, x, \text{info})$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Construction&lt;/td&gt;
&lt;td&gt;2HashDH&lt;/td&gt;
&lt;td&gt;2HashDH + DLEQ&lt;/td&gt;
&lt;td&gt;3HashSDHI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assumption&lt;/td&gt;
&lt;td&gt;One-More Gap CDH&lt;/td&gt;
&lt;td&gt;One-More Gap CDH&lt;/td&gt;
&lt;td&gt;One-More Gap SDHI (to q-DL, AGM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client trust in key&lt;/td&gt;
&lt;td&gt;must trust&lt;/td&gt;
&lt;td&gt;verifies&lt;/td&gt;
&lt;td&gt;verifies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;trusted-key internal oracle&lt;/td&gt;
&lt;td&gt;anonymity systems, OPAQUE&lt;/td&gt;
&lt;td&gt;per-tenant, rotation, buckets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sources: RFC 9497 sections 3.3.1 through 3.3.3, plus DGSTV18 and TCRSTW22 [@rfc9497], [@dgstv18], [@tcrstw22]. &lt;strong&gt;The rule:&lt;/strong&gt; if you trust the server&apos;s key by construction and need only obliviousness, use the base OPRF. If any adversarial server could hand you a wrong or per-client key -- anonymity systems, OPAQUE -- use the VOPRF. If you need to bind a &lt;em&gt;public&lt;/em&gt; label such as a per-tenant salt, rotation epoch, or rate-limit bucket, use the POPRF, and never put a secret in &lt;code&gt;info&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Which ciphersuite?&lt;/h3&gt;
&lt;p&gt;Pick the ciphersuite by your query budget, because the group size is what buys you static-Diffie-Hellman headroom (Section 8).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ciphersuite&lt;/th&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Nominal security&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;ristretto255-SHA512&lt;/td&gt;
&lt;td&gt;ristretto255&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Default: prime-order, fast, no cofactor traps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decaf448-SHAKE256&lt;/td&gt;
&lt;td&gt;decaf448&lt;/td&gt;
&lt;td&gt;224-bit&lt;/td&gt;
&lt;td&gt;Internet-facing oracle needing static-DH headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P-256-SHA256&lt;/td&gt;
&lt;td&gt;NIST P-256&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;FIPS or interop constraints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P-384-SHA384&lt;/td&gt;
&lt;td&gt;NIST P-384&lt;/td&gt;
&lt;td&gt;192-bit&lt;/td&gt;
&lt;td&gt;Higher budget; Privacy Pass &lt;code&gt;0x0001&lt;/code&gt; uses this&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P-521-SHA512&lt;/td&gt;
&lt;td&gt;NIST P-521&lt;/td&gt;
&lt;td&gt;256-bit&lt;/td&gt;
&lt;td&gt;Maximum static-Diffie-Hellman headroom&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Source: RFC 9497 sections 4.1 through 4.5 [@rfc9497]. &lt;strong&gt;The rule:&lt;/strong&gt; default to ristretto255-SHA512. If you expose an &lt;em&gt;unmetered public&lt;/em&gt; oracle on a 128-bit group, the $\log_2(Q)/2$ erosion argues for decaf448, P-384, or P-521 -- RFC 9497 recommends exactly these for applications that cannot tolerate discrete-logarithm security below 128 bits [@rfc9497]. Choose P-256-SHA256 only when FIPS or interoperability forces it. And never roll your own group or hash-to-curve; use RFC 9380 and RFC 9496 [@rfc9380], [@rfc9496].&lt;/p&gt;
&lt;h3&gt;Which token cryptography?&lt;/h3&gt;
&lt;p&gt;If you are issuing anonymous tokens, the choice between the two Privacy Pass token types is a genuine trade-off, not a default.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;VOPRF token &lt;code&gt;0x0001&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Blind-RSA token &lt;code&gt;0x0002&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Primitive&lt;/td&gt;
&lt;td&gt;OPRF (VOPRF, P-384/SHA-384)&lt;/td&gt;
&lt;td&gt;Blind &lt;em&gt;signature&lt;/em&gt; (RSA-2048), not an OPRF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifiability&lt;/td&gt;
&lt;td&gt;Issuer-verifiable only (secret key)&lt;/td&gt;
&lt;td&gt;Publicly verifiable (RSA public key)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token size&lt;/td&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;td&gt;Larger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key rotation&lt;/td&gt;
&lt;td&gt;Easy&lt;/td&gt;
&lt;td&gt;Heavier (RSA keypair)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flagship deployment&lt;/td&gt;
&lt;td&gt;CDN anonymous rate-limiting&lt;/td&gt;
&lt;td&gt;Apple Private Access Tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spec&lt;/td&gt;
&lt;td&gt;RFC 9578, RFC 9497&lt;/td&gt;
&lt;td&gt;RFC 9578, RFC 9474&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Sources: RFC 9578 and RFC 9474 [@rfc9578], [@rfc9474]. &lt;strong&gt;The rule:&lt;/strong&gt; if only the issuer needs to verify tokens and you want small, easily rotated tokens, use the VOPRF token. If a &lt;em&gt;third party&lt;/em&gt; must verify a token without contacting the issuer, you need public verifiability, and that means a blind signature -- the Blind-RSA token -- which is not an OPRF at all.&lt;/p&gt;
&lt;h3&gt;The remaining two, briefly&lt;/h3&gt;
&lt;p&gt;For private set intersection, choose the public-key 2HashDH OPRF when you want few messages and malicious-secure verifiability, and the symmetric KKRT16 OPRF when you want to intersect enormous sets fast under a semi-honest model [@jkk14], [@kkrt16]. For breach checking, choose an OPRF when query privacy is a hard requirement, and k-anonymity when simplicity and cache-friendliness outweigh the leak of a hash prefix [@thomas19], [@cloudflare-kanon].&lt;/p&gt;
&lt;p&gt;Every row above assumes one thing worth making explicit, because it is the property the whole family exists to deliver. Against a plain salted or peppered server-side hash, the server &lt;em&gt;must see the secret&lt;/em&gt; to key it. That is the single compromise the OPRF removes, and each decision in this matrix is a variation on how much structure you are willing to add on top of &quot;the server never saw it.&quot; Which is worth asking: what, precisely, can this primitive prove -- and what can it provably not?&lt;/p&gt;
&lt;h2&gt;8. What the Assumptions Buy, and What They Cost&lt;/h2&gt;
&lt;p&gt;If the discrete logarithm has never been broken, what can still go wrong with an OPRF -- and what can never be fixed? The answer separates cleanly into what the primitive provably delivers, what it rests on, and the one price it can never stop paying.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it delivers.&lt;/strong&gt; RFC 9497 section 7.1 enumerates the guarantees [@rfc9497]. Pseudorandomness: for a random key, the output is indistinguishable from a random function. Nonmalleability: you cannot turn one evaluation into another. Verifiability, in the VOPRF and POPRF modes. Partial obliviousness, in the POPRF. And the one worth pausing on -- &lt;em&gt;unconditional input secrecy&lt;/em&gt;. Because the blind is fresh and uniform, the point the server receives is uniform and independent of the input, so input secrecy holds even against an adversary with unbounded computation [@rfc9497]. That is not &quot;secure until someone finds a faster algorithm.&quot; It is information-theoretic. The secret was never in the message, so there is nothing for computation to extract.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it rests on.&lt;/strong&gt; The OPRF and VOPRF rest on the well-studied One-More Gap Computational Diffie-Hellman assumption; the POPRF on One-More Gap Strong-Diffie-Hellman-Inversion [@rfc9497], [@jkk14], [@tcrstw22]. The POPRF&apos;s assumption comes with a load-bearing result: it is shown to be implied by the plain $q$-discrete-logarithm assumption in the algebraic group model [@tcrstw22]. All three are discrete-log-family assumptions believed hard in the groups actually used, so the deployed OPRF is anchored to the same discrete-log problem Part 18 argued has stood unbroken for half a century. That is about as good a foundation as applied cryptography offers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What it costs, forever.&lt;/strong&gt; Now the third question comes due. Every &lt;code&gt;BlindEvaluate&lt;/code&gt; is, by construction, a static-Diffie-Hellman oracle sample: the server computes $x \mapsto x^k$ for a fixed secret $k$, and Brown-Gallant and Cheon showed that collecting such samples recovers $k$ at a cost that drops with the count [@rfc9497], [@browngallant04], [@cheon06].&lt;/p&gt;

A service that, for a fixed secret key $k$, answers queries of the form $x \mapsto x^k$. Each answer is a sample that erodes the key: after $Q$ queries, the best-known attacks recover $k$ at a cost reduced by $\log_2(Q)/2$ bits (the Brown-Gallant and Cheon bound). Offering an OPRF *is* offering this oracle [@browngallant04], [@cheon06].
&lt;p&gt;The specification does not bury this. It states it:&lt;/p&gt;

&quot;Best-known attacks reduce the security of the prime-order group instantiation by log_2(Q) / 2 bits, where Q is the number of BlindEvaluate calls.&quot; -- RFC 9497, section 7.2.3
&lt;p&gt;Work the arithmetic and the design pressure becomes concrete. On a 128-bit group, effective key security is $128 - \log_2(Q)/2$ bits. Answer $2^{40}$ queries and you have shed 20 bits, down to 108. Answer $2^{60}$ and you are at 98. This is not an implementation flaw you can patch; it is a lower bound on the attacker&apos;s advantage that &lt;em&gt;no construction in this family removes&lt;/em&gt;, because it follows from the primitive doing the one thing it exists to do.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;BlindEvaluate calls $Q$&lt;/th&gt;
&lt;th&gt;Bits lost, $\log_2(Q)/2$&lt;/th&gt;
&lt;th&gt;Effective security on a 128-bit group&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;$2^{20}$&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;118 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$2^{40}$&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;108 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$2^{60}$&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;98 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$2^{80}$&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;88 bits&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; &quot;Keep this key secret forever&quot; and &quot;answer unlimited public blind queries on a 128-bit group&quot; cannot both hold. The static-Diffie-Hellman erosion is inherent to exposing the oracle, so an unmetered public OPRF on a 128-bit group provably cannot retain 128-bit key security [@rfc9497], [@browngallant04]. The only resolutions are operational: rate-limit &lt;code&gt;BlindEvaluate&lt;/code&gt;, rotate the key on a schedule, or move to a larger group (decaf448, P-384, P-521). You are choosing a budget, not eliminating a cost.&lt;/p&gt;
&lt;/blockquote&gt;

Look at what just happened across five sections. The strong-Diffie-Hellman decay that doomed the 2009 Jarecki-Liu construction was treated then as a fatal flaw -- the reason to abandon a whole generation [@jareckiliu09], [@cheon06]. The *same physics* reappears in RFC 9497, but now it is a written-down budget with a formula and a mitigation list [@rfc9497]. Nothing about the mathematics changed. What changed is the engineering posture: a phenomenon that was a silent, disqualifying weakness became an explicit, managed policy knob. This is often how a field matures -- not by defeating a limit, but by naming it precisely enough to live with it.
&lt;p&gt;Two more facts bound the theory. Round-optimality: two messages is provably the minimum, and 2HashDH achieves it, so there is no efficiency left to win there.The two-message floor is structural: the client must send at least one blinded value and receive at least one response to recover an output, so no protocol of this shape can use fewer round-trips. And two honest open edges. RFC 9497 admits that the exact multi-key, batched protocol the industry actually deploys has no security proof:&lt;/p&gt;

&quot;There is currently no security analysis available for the VOPRF protocol described in this document in a setting with multiple server keys or batching.&quot; -- RFC 9497, section 7.2.1
&lt;p&gt;The deployed reality -- many issuer keys, batched proofs -- outruns the single-key proof it is built on [@rfc9497]. And the whole family rests on discrete log, so a scalable quantum computer running Shor&apos;s algorithm breaks obliviousness and unforgeability alike. Two of these limits are not settled facts but open frontiers -- and the sharpest one opened with a construction that was broken within a year of being proposed.&lt;/p&gt;
&lt;h2&gt;9. Where It Is Still Moving&lt;/h2&gt;
&lt;p&gt;For the single-key, non-batched, classical setting, the theory is essentially closed: round-optimal, constant communication, unconditional input secrecy, a clean assumption reducing to discrete log. The live research is almost entirely about the edges the deployments already stand on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The post-quantum frontier, which opened with a cautionary tale.&lt;/strong&gt; Because every deployed OPRF falls to Shor, the sharpest open problem is a post-quantum replacement -- and the first serious attempt is exactly why the field is cautious.&lt;/p&gt;
&lt;p&gt;In 2020, Dan Boneh, Dmitry Kogan, and Katharine Woo built the first substantial post-quantum OPRF from supersingular isogenies [@bkw20]. Within a year, Andrea Basso, Peter Kutas, Simon-Philipp Merz, Christophe Petit, and Antonio Sanso cryptanalyzed it, giving a polynomial-time attack and a subexponential one that survived the obvious countermeasures -- together breaking the parameters the authors had proposed [@basso21].&lt;/p&gt;
&lt;p&gt;A construction proven secure under a named assumption was undone because the assumption itself did not hold. It is the cleanest possible reminder that a proof is only as good as what it reduces to.The isogeny paper actually offered &lt;em&gt;two&lt;/em&gt; constructions. Its second one revived the 1997 Naor-Reingold PRF, reinterpreted over commutative group actions (the CSIDH setting) [@bkw20], [@naorreingold97]. The oldest algebraic PRF in this story turned out to be the one with a plausible post-quantum future -- a twenty-three-year-old idea made newly relevant by a change of group.&lt;/p&gt;
&lt;p&gt;The other post-quantum directions are advancing but heavy. Martin Albrecht, Alex Davidson, Amit Deo, and Nigel Smart gave a round-optimal verifiable OPRF from ideal lattices in 2021 [@adds21]; Albrecht and Kamil Doruk Gur made it &quot;practical-ish and thresholdisable&quot; in 2024, cutting its lattice bandwidth roughly fourfold and adding a threshold variant [@albrechtgur24]. And a general framework arrived in 2025: Ward Beullens, Lucas Dodgson, Sebastian Faller, and Julia Hesse compiled quantum-safe OPRFs from secure multi-party evaluation of simple functions, reporting a working construction in about 0.57 seconds with under a megabyte of communication [@beullens25].&lt;/p&gt;
&lt;p&gt;That is a genuine milestone -- and it is still orders of magnitude heavier than the sub-millisecond classical evaluation it aims to replace. An efficient, &lt;em&gt;verifiable&lt;/em&gt;, &lt;em&gt;partially oblivious&lt;/em&gt; post-quantum OPRF, matching the classical profile on all three counts at once, does not yet exist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The proof the industry is running ahead of.&lt;/strong&gt; Section 8&apos;s second pull quote is also an open problem: prove the multi-key, batched VOPRF that Privacy Pass actually deploys [@rfc9497]. Batching is a well-used heuristic, but the formal guarantee still covers only the single-key case.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Removing the single point of failure.&lt;/strong&gt; A threshold or distributed OPRF splits the key across several servers so no one of them holds it, which is the structural answer to server compromise. RFC 9807 names exactly this as the defense that forces an attacker online before any dictionary attack can start [@rfc9807]. Round-optimal, misuse-resistant threshold OPRF specifications are still maturing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The limits that are policy, not cryptanalysis.&lt;/strong&gt; Two open problems are engineering rather than mathematics. The static-Diffie-Hellman budget is a permanent tax on any public oracle; the open question is operational -- what query cap and rotation cadence keep a given group above 128 bits -- not whether the bound can be improved, since Brown-Gallant and Cheon are believed essentially tight for generic groups [@browngallant04], [@cheon06].&lt;/p&gt;
&lt;p&gt;And verifiability quietly collapses if a malicious issuer hands each user a different public key, so making &quot;one key for everyone&quot; &lt;em&gt;auditable&lt;/em&gt; needs key-transparency infrastructure that does not yet ship as a turnkey component [@rfc9497]. Formally verified constant-time implementations sit in the same category: RFC 9497 mandates constant-time secret operations, but the guarantee is only as strong as the machine-checked code behind it [@rfc9497].&lt;/p&gt;
&lt;p&gt;None of these block you from shipping today. They tell you what to watch. What you ship today has a small, boring set of rules that separate a correct deployment from a broken one -- and unlike the open problems, those rules are entirely settled.&lt;/p&gt;
&lt;h2&gt;10. The Rules, Made Operational&lt;/h2&gt;
&lt;p&gt;Here is the whole deployment discipline in a form a reviewer can apply without rereading the RFC. It has three parts: pick two things, always do a handful, and never do another handful.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pick the mode by the questions.&lt;/strong&gt; Trust the key by construction: base OPRF. Must verify which key: VOPRF. Must bind a public label: POPRF, and never smuggle a secret into &lt;code&gt;info&lt;/code&gt;, which is public by definition [@rfc9497], [@tcrstw22]. &lt;strong&gt;Pick the ciphersuite by the query budget.&lt;/strong&gt; Default to ristretto255-SHA512; for an internet-facing unmetered oracle move to decaf448, P-384, or P-521; for FIPS or interop use P-256-SHA256. Never roll your own group or hash-to-curve -- use RFC 9380 and RFC 9496 [@rfc9497], [@rfc9380], [@rfc9496].&lt;/p&gt;

flowchart TD
    Start[&quot;Choose an OPRF deployment&quot;] --&amp;gt; A{&quot;Could the server be adversarial about which key it uses?&quot;}
    A --&amp;gt;|no, key trusted| B[&quot;Base OPRF&quot;]
    A --&amp;gt;|yes| C{&quot;Must you bind a public label such as tenant or epoch?&quot;}
    C --&amp;gt;|no| D[&quot;VOPRF&quot;]
    C --&amp;gt;|yes| E[&quot;POPRF&quot;]
    B --&amp;gt; F{&quot;Unmetered public oracle?&quot;}
    D --&amp;gt; F
    E --&amp;gt; F
    F --&amp;gt;|no| G[&quot;ristretto255-SHA512 default&quot;]
    F --&amp;gt;|yes| H[&quot;decaf448 or P-384 or P-521&quot;]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every call: draw a &lt;em&gt;fresh, uniform blind&lt;/em&gt;, and a fresh nonce for the DLEQ proof. Every received element: &lt;em&gt;validate&lt;/em&gt; it -- on the curve, in range, and not the identity -- before you touch it [@rfc9497]. Every secret operation, including &lt;code&gt;BlindEvaluate&lt;/code&gt; and &lt;code&gt;GenerateProof&lt;/code&gt;: run it in &lt;em&gt;constant time&lt;/em&gt; [@rfc9497]. Every output: &lt;em&gt;domain-separate&lt;/em&gt; with the &lt;code&gt;&quot;Finalize&quot;&lt;/code&gt; tag and mode context, and keep inputs within the length limit [@rfc9497]. For a VOPRF or POPRF: publish and pin &lt;em&gt;one&lt;/em&gt; public key, transparently, for everyone. For a POPRF: treat an &lt;code&gt;InverseError&lt;/code&gt; as a signal that the tweak collided and it is time to rotate [@tcrstw22]. And bound $Q$: rate-limit &lt;code&gt;BlindEvaluate&lt;/code&gt; or rotate keys so the static-Diffie-Hellman loss stays inside budget [@rfc9497], [@browngallant04].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &quot;never&quot; list is just the failure catalog from the last nine sections, re-read as review rules. Each entry maps to one of the three questions answered wrong.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Anti-pattern&lt;/th&gt;
&lt;th&gt;Why it breaks&lt;/th&gt;
&lt;th&gt;Question violated&lt;/th&gt;
&lt;th&gt;The fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Sending the un-blinded input&lt;/td&gt;
&lt;td&gt;The server sees the input; obliviousness is gone&lt;/td&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;Always apply the fresh blind before sending [@rfc9497]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A MAC or HKDF &quot;used as an OPRF&quot;&lt;/td&gt;
&lt;td&gt;If the server computes it, it &lt;em&gt;saw&lt;/em&gt; the input&lt;/td&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;Use a real two-party OPRF; the blind is the point [@rfc9497]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusing the blind across calls&lt;/td&gt;
&lt;td&gt;Two evaluations become linkable, and the input can leak&lt;/td&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;Fresh, uniform blind every call [@rfc9497]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skipping element validation&lt;/td&gt;
&lt;td&gt;Malformed points enable key-probing attacks&lt;/td&gt;
&lt;td&gt;Q1/Q3&lt;/td&gt;
&lt;td&gt;Reject off-curve, out-of-range, and identity elements [@rfc9497]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dropping or unsoundly batching the DLEQ&lt;/td&gt;
&lt;td&gt;A malicious server can use a per-client key and tag you&lt;/td&gt;
&lt;td&gt;Q2&lt;/td&gt;
&lt;td&gt;Verify the proof; combine batches soundly [@rfc9497], [@dgstv18]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A per-user public key&lt;/td&gt;
&lt;td&gt;The unique key is itself a deanonymizing tag&lt;/td&gt;
&lt;td&gt;Q2&lt;/td&gt;
&lt;td&gt;Pin one transparently published key for all [@rfc9497]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-constant-time secret ops&lt;/td&gt;
&lt;td&gt;The key leaks through timing&lt;/td&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;Constant-time group ops, proof, and evaluate [@rfc9497]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treating POPRF &lt;code&gt;info&lt;/code&gt; as secret&lt;/td&gt;
&lt;td&gt;&lt;code&gt;info&lt;/code&gt; is public by definition; obliviousness assumed of it fails&lt;/td&gt;
&lt;td&gt;Q1&lt;/td&gt;
&lt;td&gt;Put only public labels in &lt;code&gt;info&lt;/code&gt; [@tcrstw22]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;An unmetered oracle on a 128-bit group&lt;/td&gt;
&lt;td&gt;The key erodes by $\log_2(Q)/2$ bits, unbounded&lt;/td&gt;
&lt;td&gt;Q3&lt;/td&gt;
&lt;td&gt;Rate-limit, rotate, or use a bigger group [@rfc9497], [@browngallant04]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Using an OPRF where you need public verifiability&lt;/td&gt;
&lt;td&gt;A third party cannot verify a VOPRF token&lt;/td&gt;
&lt;td&gt;(wrong primitive)&lt;/td&gt;
&lt;td&gt;Use a blind signature (Blind RSA), not an OPRF [@rfc9474]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;{`
// STRUCTURAL MODEL. Shows why reusing the blind across calls destroys unlinkability.
// Same additive group model: a &quot;point&quot; is an integer mod q, scalar*point is mod q.
const q = 2147483647;
const mod = (a) =&amp;gt; ((a % q) + q) % q;
const scalarMul = (s, point) =&amp;gt; mod(s * point);&lt;/p&gt;
&lt;p&gt;const Hx = 991;                              // H(x): the SAME secret input, evaluated twice
const blindedWith = (r) =&amp;gt; scalarMul(r, Hx); // what goes on the wire: r*H(x)&lt;/p&gt;
&lt;p&gt;// BAD: reuse the same blind r for both calls
const rFixed = 55555;
const wire1_bad = blindedWith(rFixed);
const wire2_bad = blindedWith(rFixed);
console.log(&apos;reused blind -&amp;gt; wire equal? &apos;, wire1_bad === wire2_bad, &apos;(server links the two as one input)&apos;);&lt;/p&gt;
&lt;p&gt;// GOOD: a fresh uniform blind each call
const wire1_ok = blindedWith(37337);
const wire2_ok = blindedWith(88123);
console.log(&apos;fresh blind  -&amp;gt; wire equal? &apos;, wire1_ok === wire2_ok, &apos;(server cannot link them)&apos;);
`}&lt;/p&gt;
&lt;p&gt;Most of those &quot;nevers&quot; are the same handful of mistakes phrased for different audiences: do not let the server see the input, do not let it pick a per-client key, and do not let it answer unlimited queries. It is worth answering the questions engineers actually ask about them head on.&lt;/p&gt;

The `voprf` Rust crate and Cloudflare&apos;s CIRCL both implement all three modes against the RFC 9497 test vectors [@voprf-crate], [@circl]. A good first exercise: run the base OPRF, print the blinded element on two evaluations of the same input, and confirm the two wire values differ because the blind is fresh. Then switch to the verifiable mode, tamper with one byte of the server&apos;s response, and watch `VerifyProof` reject it. That single afternoon teaches more about Q1 and Q2 than any diagram.
&lt;h2&gt;11. Questions Engineers Actually Ask&lt;/h2&gt;


No, and the difference is the whole primitive. If the server computes `HMAC(k, x)` for you, it *saw* `x`. That is exactly the compromise the OPRF removes: it applies the key to a blinded value it cannot read, so it never sees the input or the output [@rfc9497]. An HMAC that the server evaluates on your plaintext input is not oblivious by any definition.


In any setting with an untrusted server, yes, decisively. The base OPRF answers only &quot;is it blind?&quot; Without the DLEQ proof, a malicious server can hand each client a *different* key and later use the distinct responses to tell clients apart -- deanonymizing the very users an anonymity system is meant to protect [@rfc9497], [@dgstv18]. The VOPRF&apos;s proof, plus one transparently pinned public key, is what closes that gap.


Because every `BlindEvaluate` is a static-Diffie-Hellman oracle sample, and collecting samples recovers the key at a cost that drops by $\log_2(Q)/2$ bits after $Q$ calls [@rfc9497], [@browngallant04], [@cheon06]. It is inherent to offering a Diffie-Hellman oracle, not an implementation defect. Rate-limit, rotate keys, or use a larger group to keep the loss inside budget.


Half of it. Privacy Pass token type `0x0001` is a VOPRF, which is an OPRF. Token type `0x0002` is Blind RSA -- a blind *signature*, not an OPRF -- and it is the type behind Apple&apos;s Private Access Tokens, the most visible deployment [@rfc9578], [@rfc9474]. So &quot;Privacy Pass equals OPRF&quot; is wrong; one of its two token types is a different primitive entirely.


No. Checkup performs a blinded, OPRF-based lookup, so Google does not learn the credential being checked [@thomas19]. Have I Been Pwned&apos;s range API uses k-anonymity: you send the first five hex digits of a SHA-1 hash and receive the whole bucket to search locally [@cloudflare-kanon]. That leaks a hash prefix by design. It is simpler and cache-friendly, but it is not oblivious.


No. Reusing the blind across calls makes two evaluations of the same input produce the same on-wire value, so the server can link them, and in some settings it can leak the input [@rfc9497]. The blind and the proof nonce must be freshly and uniformly drawn every single call. This is non-negotiable.


No. Every RFC 9497 ciphersuite rests on discrete-log hardness, which Shor&apos;s algorithm breaks, so a scalable quantum computer defeats both obliviousness and unforgeability [@rfc9497]. Post-quantum OPRFs are an open frontier: the first serious isogeny construction was broken within a year [@bkw20], [@basso21], and lattice and MPC candidates remain far heavier than classical evaluation [@albrechtgur24], [@beullens25]. And to preempt the related question: no, a POPRF&apos;s `info` is not secret -- it is public by definition [@tcrstw22].

&lt;p&gt;Read back across the whole story and the shape is unmistakable. Every named failure was one of three questions answered wrong. FIPR&apos;s early constructions answered &quot;is it blind?&quot; but too expensively to ship. Jarecki and Liu answered &quot;which key?&quot; too, and then the ground under &quot;how many asks?&quot; gave way beneath them. A base OPRF deployed where a VOPRF was needed answers &quot;which key?&quot; wrong and tags its users. A reused blind answers &quot;is it blind?&quot; wrong. An unmetered public oracle answers &quot;how many asks?&quot; wrong. The broken isogeny construction answered all three -- until its assumption did not hold. The diagnostic is not a teaching device bolted onto the history; it &lt;em&gt;is&lt;/em&gt; the history.&lt;/p&gt;
&lt;p&gt;And the resolution is a single move, assembled from primitives this series has already built. Take Chaum&apos;s 1982 blinding from the RSA world of Part 14, run it in the Diffie-Hellman group of Part 18 -- the discrete log that Part 18 argued nobody has broken, here run one-sided so a server &lt;em&gt;applies&lt;/em&gt; a secret rather than agreeing on one -- hash the input into the group with the hash-to-curve of Part 16, and pin the key with the equal-discrete-log proof descended from the sigma protocols of Part 17.&lt;/p&gt;
&lt;p&gt;Four earlier building blocks, one new primitive. Later parts of this guide return to its consumers as topics in their own right -- the augmented PAKE, anonymous credentials, private set intersection -- because each is, underneath, this same engine.&lt;/p&gt;

The server helped, and it still never saw.
&lt;p&gt;That is the sentence to keep. A pseudorandom function is a lock only the keyholder can open, which is exactly what made it dangerous to offer as a service. The oblivious PRF removed the danger without removing the help: it lets a server apply its secret key to your secret input, prove it used the right key, and do so within a query budget it is honest about -- all without ever seeing what it operated on. Blind, verifiable, rate-limited. The server helped, and it still never saw.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;oblivious-pseudorandom-functions-oprf-voprf-poprf&quot; keyTerms={[
  { term: &quot;OPRF&quot;, definition: &quot;A two-party protocol computing F(k, x) where the server holds k and the client holds x, and the server learns neither x nor the output while the client learns only F(k, x).&quot; },
  { term: &quot;VOPRF&quot;, definition: &quot;An OPRF plus a proof that the server used the key committed in its published public key, so the client can reject any other key.&quot; },
  { term: &quot;POPRF&quot;, definition: &quot;A VOPRF that also binds a public input info into the evaluation, F(k, x, info), keeping x oblivious while one key serves many public contexts.&quot; },
  { term: &quot;2HashDH&quot;, definition: &quot;The construction F(k, x) = H2(x, H1(x)^k): hash into the group, exponentiate by the key, hash out; the OPRF/VOPRF core of RFC 9497.&quot; },
  { term: &quot;DLEQ proof&quot;, definition: &quot;A Chaum-Pedersen non-interactive proof that two discrete logarithms are equal, binding the server&apos;s response to its committed key; batchable via a random linear combination.&quot; },
  { term: &quot;Blinding factor&quot;, definition: &quot;The fresh, uniform random scalar r that multiplies HashToGroup(x) so the transmitted point is uniform and independent of the input.&quot; },
  { term: &quot;Static Diffie-Hellman oracle&quot;, definition: &quot;A service answering x to x^k for fixed secret k; each query erodes k by the Brown-Gallant and Cheon bound, log2(Q)/2 bits after Q calls.&quot; },
  { term: &quot;Unlinkability&quot;, definition: &quot;Unconditional input secrecy: because the blind is uniform, the server cannot link or learn anything about the input, even with unbounded computation.&quot; }
]} questions={[
  { q: &quot;Which of the three questions does a base OPRF leave unanswered, and what attack does that enable?&quot;, a: &quot;It does not answer &apos;which key?&apos; A malicious server can use a per-client key and later tag which client made a request, breaking anonymity.&quot; },
  { q: &quot;Why does a POPRF&apos;s info have to be public?&quot;, a: &quot;The partial-obliviousness guarantee only protects the private input x; info is folded into the key as a public tweak and is known to both parties by construction.&quot; },
  { q: &quot;Why can an unmetered public OPRF on a 128-bit group not keep 128-bit key security?&quot;, a: &quot;Every BlindEvaluate is a static-Diffie-Hellman sample, so Q queries erode the key by log2(Q)/2 bits; keeping the key forever and answering unlimited queries are provably incompatible.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>oblivious-pseudorandom-function</category><category>oprf</category><category>voprf</category><category>cryptography</category><category>privacy-pass</category><category>opaque</category><category>rfc-9497</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Standard Was Boring; That Was the Point: A Field Guide to HPKE (RFC 9180)</title><link>https://paragmali.com/blog/the-standard-was-boring-that-was-the-point-a-field-guide-to-/</link><guid isPermaLink="true">https://paragmali.com/blog/the-standard-was-boring-that-was-the-point-a-field-guide-to-/</guid><description>The industry standardized encrypt-to-a-public-key four incompatible ways. HPKE (RFC 9180) is the boring, correct fix -- and its non-goals are the whole discipline.</description><pubDate>Mon, 13 Jul 2026 08:42:43 GMT</pubDate><content:encoded>
**&quot;Encrypt to a public key&quot; sounds like the most basic promise in cryptography, yet the industry standardized it four incompatible ways.** ANSI X9.63, IEEE 1363a, ISO/IEC 18033-2, and SEC 1 all shipped a version of ECIES; two conforming implementations could produce ciphertexts that would not decrypt for each other, none came with shared test vectors, and one standard openly documented that an attacker could forge a second valid ciphertext for the same plaintext and named it &quot;benign malleability&quot; [@sec1], [@maea10]. HPKE (RFC 9180, 2022) is the boring, correct fix: one KEM-DEM construction where a Key Encapsulation Mechanism *generates* a shared secret and its encapsulation, an HKDF key schedule with every public key bound in derives the working keys, and an AEAD seals the data [@rfc9180]. It is IND-CCA2-proven, machine-checked across all four modes (`base`, `psk`, `auth`, `auth_psk`), and shipped with per-ciphersuite test vectors [@lipp20], [@abhklr20]. Because the primitive is correct by construction, HPKE has no known catastrophic real-world break; the entire discipline is knowing its four modes and its non-goals. It does *not* provide forward secrecy against recipient compromise in any mode, `auth` mode is *not* a signature, and its AEADs are *not* key-committing -- and every real HPKE incident to date is one of those non-goals assumed away or a check an implementation dropped [@rfc9180], [@ghsa]. It is the sealing primitive behind Encrypted Client Hello (RFC 9849), MLS, Oblivious HTTP, and Oblivious DNS; the post-quantum path is to swap the KEM (X-Wing / ML-KEM), not to redesign the scheme [@rfc9849], [@xwing]. The math held; the risk moved to the interface.
&lt;h2&gt;1. An Elementary Task the Industry Got Wrong Four Times&lt;/h2&gt;
&lt;p&gt;&quot;Encrypt this to their public key&quot; is the first promise public-key cryptography ever made. It is the one-line pitch of RSA, the thing every engineer assumes was solved and made boring decades ago, a settled corner of the field you reach for without thinking. Yet for roughly twenty-five years there was no single correct way to do it.&lt;/p&gt;
&lt;p&gt;At least four &quot;standard&quot; versions of the Elliptic Curve Integrated Encryption Scheme (ECIES) shipped: ANSI X9.63 in 2001, IEEE 1363a in 2004, ISO/IEC 18033-2 in 2006, and SECG&apos;s SEC 1 version 2 in 2009 [@maea10], [@rfc9180]. They disagreed on the key-derivation function, the message authentication code, whether elliptic-curve points were compressed or uncompressed, and which optional fields went into the hash. Two conforming implementations, handed the same curve and message, could produce ciphertexts that would not decrypt for each other.&lt;/p&gt;
&lt;p&gt;None of the four came with shared test vectors, so two libraries had no ground truth against which to check that they agreed. And one standard openly documented that an attacker could take a valid ciphertext and manufacture a &lt;em&gt;second, different&lt;/em&gt; valid ciphertext for the same plaintext. It named the phenomenon &quot;benign malleability&quot; and deemed it harmless [@sec1].&lt;/p&gt;
&lt;p&gt;The hard part of &quot;encrypt to a public key,&quot; it turned out, was never the cryptography. It was getting everyone to do the same correct thing, with a proof that it was correct and a way to check that two implementations interoperated. That failure was tolerable when public-key encryption was a batch operation between parties who could negotiate a format. It stopped being tolerable the moment the modern web needed to encrypt to a server it had never spoken to.&lt;/p&gt;
&lt;p&gt;Consider what TLS Encrypted Client Hello and Oblivious HTTP actually require. A client must encrypt the sensitive part of its connection -- in Encrypted Client Hello, the server name it is trying to reach -- to a public key it fetched moments earlier, for a server it has exchanged no messages with, with no round trip in which to negotiate anything [@rfc9849], [@rfc9458]. There is no handshake to agree on a KDF or a point encoding. The format has to be right the first time, on the first byte, or the connection reveals exactly what it was trying to hide.&lt;/p&gt;
&lt;p&gt;That is the setting Hybrid Public Key Encryption was built for. HPKE, specified in RFC 9180 by the IRTF&apos;s Crypto Forum Research Group in February 2022, is the boring, correct answer [@rfc9180]. Its headline achievements sound almost too unglamorous to matter: it publishes test vectors, and it comes with a proof that it is secure against chosen-ciphertext attack.&lt;/p&gt;
&lt;p&gt;Those two boring facts are exactly why HPKE matters. Once you accept them, the interesting question is not whether HPKE is secure but a sharper one, worth carrying through every section that follows: &lt;em&gt;you have a stranger&apos;s public key and one shot to send them a secret, with no handshake and no reply -- what does HPKE buy you, and what does it silently refuse to give?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is Part 19 of a field guide for people who design protocols, and it is the part where the primitives from earlier parts -- authenticated encryption, key derivation, elliptic curves, the KEM-DEM idea, digital signatures -- stop being separate ingredients and compose into a single shipped building block. By the end you will be able to look at any &quot;encrypt to a recipient&apos;s public key&quot; design and name, on sight, which of HPKE&apos;s modes it is using and which of HPKE&apos;s non-goals it is quietly assuming away.&lt;/p&gt;
&lt;p&gt;The fix is so unglamorous it is tempting to skip straight to the API. Do not: the shape of HPKE is written in the failures of the four standards it replaced, and you cannot use it safely without seeing what &quot;done right&quot; was correcting. Start with the one idea all four were circling and none quite pinned down: the difference between &lt;em&gt;encrypting&lt;/em&gt; a key and &lt;em&gt;encapsulating&lt;/em&gt; one.&lt;/p&gt;
&lt;h2&gt;2. Encapsulate the Key, Do Not Encrypt It&lt;/h2&gt;
&lt;p&gt;RFC 9180 describes what it does differently in a single line, and that line is the whole conceptual pivot of this article. The traditional combination, it says, has been to &quot;encrypt the symmetric key with the public key.&quot; HPKE instead will &quot;generate the symmetric key and its encapsulation with the public key&quot; [@rfc9180]. That one-word swap -- from &lt;em&gt;encrypt&lt;/em&gt; to &lt;em&gt;encapsulate&lt;/em&gt; -- is the difference between the padding-oracle-prone lineage that broke RSA key transport and a clean, composable, provable construction.&lt;/p&gt;

The traditional combination has been &quot;encrypt the symmetric key with the public key.&quot; HPKE instead will &quot;generate the symmetric key and its encapsulation with the public key.&quot; -- RFC 9180
&lt;p&gt;Build the mental model in three moves. Derive only what the rest of the article needs.&lt;/p&gt;
&lt;h3&gt;Move one: KEM-DEM in one picture&lt;/h3&gt;

The scheme standardized in RFC 9180 for encrypting arbitrary-length messages to a recipient&apos;s public key. It is built from three interchangeable ingredients -- a Key Encapsulation Mechanism (KEM), a Key Derivation Function (KDF), and an Authenticated Encryption with Associated Data (AEAD) scheme -- named together as a ciphersuite.
&lt;p&gt;The word &quot;hybrid&quot; means it splits the work between public-key and symmetric cryptography, and the split follows the KEM-DEM pattern. The KEM does the public-key half; the DEM does the symmetric half.&lt;/p&gt;

A public-key primitive that does not transport a key you chose. Called with the recipient&apos;s public key, `Encap(pkR)` *generates* a fresh random shared secret together with an encapsulation `enc` of it, returning the pair `(shared_secret, enc)`. The recipient runs `Decap(skR, enc)` to recover the same shared secret. You never pick the key; the public-key operation produces it.

The symmetric half of a hybrid scheme: an AEAD that bulk-encrypts the actual payload under a key derived from the KEM&apos;s shared secret. In HPKE the DEM is a single AEAD scheme, and its authenticity tag is intrinsic rather than bolted on.
&lt;p&gt;If you have read Part 14 on &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;getting RSA right&lt;/a&gt;, this is the same KEM-DEM skeleton, now instantiated on elliptic curves rather than RSA. The reason to &lt;em&gt;generate&lt;/em&gt; the key rather than &lt;em&gt;encrypt&lt;/em&gt; one is the entire lesson of that part: the moment you encrypt an attacker-influenced key under a public-key operation, you invite a decryption oracle to leak information one query at a time. A KEM removes the attacker&apos;s chosen input from the public-key step. There is nothing to pad, and nothing to probe.&lt;/p&gt;

flowchart TD
    subgraph LEFT[&quot;Encrypt the key (RSA and ElGamal key transport)&quot;]
        A1[&quot;Choose a random symmetric key k&quot;] --&amp;gt; A2[&quot;Encrypt k under the recipient public key&quot;]
        A2 --&amp;gt; A3[&quot;Ship wrapped k plus the symmetric ciphertext&quot;]
    end
    subgraph RIGHT[&quot;Encapsulate the key (KEM-DEM, HPKE)&quot;]
        B1[&quot;Encap(pkR) generates a fresh shared secret&quot;] --&amp;gt; B2[&quot;Returns shared_secret and enc together&quot;]
        B2 --&amp;gt; B3[&quot;Derive keys, then AEAD-seal the payload&quot;]
    end
&lt;h3&gt;Move two: DHKEM, concretely&lt;/h3&gt;
&lt;p&gt;HPKE&apos;s deployed KEM is Diffie-Hellman-based, and it is worth seeing the mechanism once rather than treating it as a black box.&lt;/p&gt;

HPKE&apos;s Diffie-Hellman-based KEM. `Encap(pkR)` generates an ephemeral key pair, computes a Diffie-Hellman value against the recipient&apos;s public key, and derives the shared secret from it; the encapsulation `enc` is the serialized ephemeral public key. The authenticated variants `AuthEncap`/`AuthDecap` additionally fold in a Diffie-Hellman value computed with the sender&apos;s *static* key.
&lt;p&gt;Concretely, &lt;code&gt;Encap(pkR)&lt;/code&gt; generates an ephemeral pair $(sk_E, pk_E)$, computes $\mathrm{DH}(sk_E, pk_R)$, sets &lt;code&gt;enc&lt;/code&gt; to the serialization of $pk_E$, and returns &lt;code&gt;shared_secret = ExtractAndExpand(dh, kem_context)&lt;/code&gt;, where &lt;code&gt;kem_context&lt;/code&gt; already contains both &lt;code&gt;enc&lt;/code&gt; and the recipient key $pk_R$ [@rfc9180]. The recipient recomputes the identical Diffie-Hellman value as $\mathrm{DH}(sk_R, pk_E)$ and derives the same secret. This is exactly the ephemeral-static shape that DHIES introduced in the 1990s, now wearing a modern KDF -- and the decision to fold $pk_R$ into &lt;code&gt;kem_context&lt;/code&gt; is the first appearance of the idea that later kills benign malleability.&lt;/p&gt;

sequenceDiagram
    participant S as Sender (holds pkR)
    participant R as Recipient (holds skR)
    Note over S: generate ephemeral pair skE, pkE
    Note over S: dh holds DH(skE, pkR)
    Note over S: enc holds Serialize(pkE)
    Note over S: shared_secret from ExtractAndExpand(dh, kem_context with enc and pkR)
    S-&amp;gt;&amp;gt;R: send enc (the serialized ephemeral key)
    Note over R: dh holds DH(skR, pkE), the same value
    Note over R: recompute the identical shared_secret
&lt;h3&gt;Move three: the key schedule and the Context&lt;/h3&gt;
&lt;p&gt;The shared secret is not used directly. Both sides run it through a key schedule.&lt;/p&gt;

The HKDF Extract-then-Expand process that turns the KEM&apos;s `shared_secret` into working keys. It *extracts* a pseudorandom key from the shared secret (and, in PSK modes, the PSK), then *expands* that key over a context of the mode identifier, a hash of the pre-shared-key identifier (`psk_id_hash`), and a hash of the application `info` string (`info_hash`) to output the AEAD `key`, a `base_nonce`, and an `exporter_secret`.

A symmetric primitive that provides confidentiality and integrity at once: it encrypts a plaintext and authenticates both the ciphertext and some associated data, so any tampering is detected on decryption. In HPKE it is the DEM that seals the payload; AES-128-GCM, AES-256-GCM, and ChaCha20-Poly1305 are the standard choices.
&lt;p&gt;The HPKE key schedule is, quite literally, a worked example of &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;the HKDF construction from Part 13&lt;/a&gt;: extract a pseudorandom key from the shared secret, then expand it into the specific bytes each downstream primitive needs. Its outputs assemble into a &lt;code&gt;Context&lt;/code&gt;. The &lt;code&gt;Context&lt;/code&gt; exposes &lt;code&gt;Seal&lt;/code&gt;/&lt;code&gt;Open&lt;/code&gt; -- a stateful pair in which an incrementing sequence number is combined with &lt;code&gt;base_nonce&lt;/code&gt; (by exclusive-or) to give every message a distinct nonce -- and &lt;code&gt;Export&lt;/code&gt;.&lt;/p&gt;

An HPKE interface that derives fresh, independent secrets of any length from the established context, for keying *other* protocols. It behaves as a variable-length pseudorandom function, so exported secrets are independent of the AEAD key and of each other. Oblivious HTTP and Oblivious DNS use it to key their own message framing instead of calling `Seal` directly.
&lt;p&gt;For a single message you do not need to hold a &lt;code&gt;Context&lt;/code&gt; open at all: the single-shot &lt;code&gt;Seal&lt;/code&gt;/&lt;code&gt;Open&lt;/code&gt; API wraps setup, one encryption, and teardown into one call. Everything above -- encapsulate, derive, seal -- is what those single-shot calls do underneath.&lt;/p&gt;

flowchart TD
    PKR[&quot;Recipient public key pkR&quot;] --&amp;gt; ENCAP[&quot;Encap&quot;]
    ENCAP --&amp;gt; SS[&quot;shared_secret&quot;]
    ENCAP --&amp;gt; ENCV[&quot;enc (sent to recipient)&quot;]
    SS --&amp;gt; KS[&quot;key_schedule (HKDF Extract-then-Expand)&quot;]
    MODE[&quot;mode identifier&quot;] --&amp;gt; KS
    PSK[&quot;psk_id_hash&quot;] --&amp;gt; KS
    INFO[&quot;info_hash&quot;] --&amp;gt; KS
    KS --&amp;gt; KEY[&quot;AEAD key&quot;]
    KS --&amp;gt; NON[&quot;base_nonce&quot;]
    KS --&amp;gt; EXP[&quot;exporter_secret&quot;]
    KEY --&amp;gt; CTX[&quot;Context&quot;]
    NON --&amp;gt; CTX
    EXP --&amp;gt; CTX
    CTX --&amp;gt; SEAL[&quot;Seal / Open (per-message nonce)&quot;]
    CTX --&amp;gt; EXPORT[&quot;Export (key other protocols)&quot;]
&lt;p&gt;The structural model below is a toy, not real cryptography, but it lets you &lt;em&gt;feel&lt;/em&gt; the difference from wrapping a chosen key: &lt;code&gt;encap&lt;/code&gt; invents a random secret and its encapsulation, folds in the recipient&apos;s public key, and shows that a different recipient key yields a different secret from the same encapsulation.&lt;/p&gt;
&lt;p&gt;{`
// STRUCTURAL MODEL, NOT REAL CRYPTO. It only shows the SHAPE of &quot;generate the key&quot;.
function toyMix(str) {                       // a tiny non-cryptographic mixer
  let h = 2166136261 &amp;gt;&amp;gt;&amp;gt; 0;
  for (const ch of str) { h ^= ch.charCodeAt(0); h = Math.imul(h, 16777619) &amp;gt;&amp;gt;&amp;gt; 0; }
  return (h &amp;gt;&amp;gt;&amp;gt; 0).toString(16);
}
function randInt() { return Math.floor(Math.random() * 1e9); }&lt;/p&gt;
&lt;p&gt;// Encap: the PUBLIC-KEY op GENERATES the shared secret. The caller never picks it.
function encap(pkR) {
  const skE = randInt();                     // fresh ephemeral secret
  const enc = &apos;pkE(&apos; + skE + &apos;)&apos;;            // its public value IS the encapsulation
  const dh  = toyMix(enc + &apos;|&apos; + pkR);       // stands in for DH(skE, pkR)
  // kem_context binds BOTH enc and pkR into the derivation -- the anti-malleability trick
  const shared_secret = toyMix(&apos;ss|&apos; + dh + &apos;|&apos; + enc + &apos;|&apos; + pkR);
  return { shared_secret, enc };
}&lt;/p&gt;
&lt;p&gt;const pkR = &apos;recipient-key-A&apos;;
const { shared_secret, enc } = encap(pkR);
console.log(&apos;enc on the wire     = &apos; + enc);
console.log(&apos;shared_secret       = &apos; + shared_secret);&lt;/p&gt;
&lt;p&gt;// Same enc, a DIFFERENT recipient key -&amp;gt; a DIFFERENT secret. A swapped or mangled
// public key simply cannot derive the key that decrypts, because pkR is bound in.
const swapped = toyMix(&apos;ss|&apos; + toyMix(enc + &apos;|recipient-key-B&apos;) + &apos;|&apos; + enc + &apos;|recipient-key-B&apos;);
console.log(&apos;same enc, key B     = &apos; + swapped);
console.log(&apos;secrets differ      = &apos; + (swapped !== shared_secret));
`}&lt;/p&gt;
&lt;p&gt;Now the payoff, and the first place your understanding should shift.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Encapsulate, do not encrypt. Let the public-key operation &lt;em&gt;generate&lt;/em&gt; the symmetric key and its encapsulation together (a KEM), then AEAD-encrypt under a derived key (a DEM). Because the composite is secure against chosen-ciphertext attack whenever both halves are, &quot;encrypt to a public key&quot; stops being an ad-hoc scheme you must re-prove each time and becomes a composition of two well-understood parts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The modularity is the whole point. Cramer and Shoup proved in 2001 that a KEM and a DEM, each secure against chosen-ciphertext attack, compose into a hybrid scheme that is too [@cs01]; a few years later the converse was pinned down, showing both halves essentially &lt;em&gt;have&lt;/em&gt; to be that strong for the composite to be [@hhk06]. Those results turn &quot;is this encrypt-to-a-public-key scheme secure?&quot; into two smaller, reusable questions.RFC 9180 notes in passing that its Base mode and the original Cramer-Shoup scheme are both &quot;named HPKE&quot; -- the name has KEM-DEM in its bones [@rfc9180], [@cs01].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; RFC 9180 is an &lt;em&gt;Informational&lt;/em&gt; IRTF/CFRG document, not a Standards-Track one. Yet it is normatively &lt;em&gt;required&lt;/em&gt; by Standards-Track protocols -- MLS (RFC 9420), Oblivious HTTP (RFC 9458), and Encrypted Client Hello (RFC 9849) all cite it as a mandatory dependency. Its category label undersells how much of the modern stack rests on it [@rfc9180].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This clean picture -- encapsulate, derive, seal -- looks obvious in hindsight. It was not. The KEM-DEM idea took two decades to take its final shape, and the Diffie-Hellman instantiation it rests on fragmented into the four broken standards from the last section before anyone consolidated it. To understand why HPKE makes the exact choices it does, walk the twenty-five years it is the endpoint of.&lt;/p&gt;
&lt;h2&gt;3. From &quot;Encrypt the Key&quot; to &quot;Encapsulate the Key&quot;&lt;/h2&gt;
&lt;p&gt;The story is a twenty-five-year march from &lt;em&gt;encrypt the key&lt;/em&gt; to &lt;em&gt;encapsulate the key&lt;/em&gt;, starting, as RFC 9180 notes, in the early days of public-key cryptography [@rfc9180]. Diffie and Hellman defined the idea of a public key in 1976 but did not say how to encrypt a long message to one [@dh76]. The answers accumulated in layers, each fixing the previous one&apos;s sharpest failure.&lt;/p&gt;

timeline
    title From encrypt-the-key to encapsulate-the-key, 1976 to 2026
    1976 : Diffie and Hellman define public-key cryptography
    1993 : PEM (RFC 1421) encrypts the content key under the recipient public key
    1999 : DHIES derives the key from Diffie-Hellman instead of encrypting it
    2001 : Cramer and Shoup formalize the KEM-DEM pattern
    2009 : SEC 1 v2 completes the four-way ECIES split and names benign malleability
    2022 : RFC 9180 consolidates KEM-DEM with proofs and test vectors
    2026 : Post-quantum hybrid KEMs such as X-Wing drop into the KEM slot
&lt;p&gt;&lt;strong&gt;PEM (RFC 1421, 1993): the traditional combination.&lt;/strong&gt; Privacy Enhanced Mail defined the canonical early hybrid. Pick a content-encryption key, &lt;em&gt;encrypt that key&lt;/em&gt; under the recipient&apos;s public key, and bulk-encrypt the message symmetrically. RFC 9180 opens by naming this exact tradition and positioning HPKE as the departure from it [@rfc1421], [@rfc9180]. It is the naive model most engineers still carry, and the one the previous section asked you to give up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ElGamal (1985) and why Diffie-Hellman resists &quot;encrypt a chosen key.&quot;&lt;/strong&gt; ElGamal&apos;s public-key encryption predates the hybrid schemes and shows the friction directly [@elgamal85]. To &quot;encrypt&quot; a chosen symmetric key under a Diffie-Hellman public key, you must first encode that key as a group element, then pay a roughly twofold ciphertext expansion -- and the result is malleable rather than secure against chosen-ciphertext attack. That malleability is a reasoned inference here, anchored to DHIES&apos;s own claim to offer &quot;stronger security properties than ElGamal,&quot; not a verbatim quote [@dhies_ms].&lt;/p&gt;
&lt;p&gt;The RSA branch of the same idea failed even more loudly. Encrypting a chosen key under RSA with PKCS#1 v1.5 padding is precisely the construction that Bleichenbacher&apos;s attack turned into a decryption oracle, revived two decades later as ROBOT against nearly a third of the top 100 web domains [@rfc8017], [@robot_site], [@robot_eprint]. The part of this series on getting RSA right tells that story in full; both branches teach the same lesson: stop choosing a key and wrapping it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DHIES and DHAES (Abdalla, Bellare, Rogaway, 1999): derive, do not encrypt.&lt;/strong&gt; The academic root of ECIES made the decisive move. Instead of encrypting a key, run an ephemeral-static Diffie-Hellman exchange, push the result through a KDF to get an encryption key and a MAC key, then Encrypt-then-MAC the payload. No key is ever encrypted; it is &lt;em&gt;agreed&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The authors proved the scheme secure against chosen-ciphertext attack under the Oracle Diffie-Hellman assumption, with &quot;no random oracles involved,&quot; and called it &quot;an attractive starting point for developing public-key encryption standards&quot; [@dhaes], [@dhies_ms].DHIES has no single crisp date: the DHAES manuscript appeared as ePrint 1999/007 in 1999, was later renamed DHIES, and its formal analysis was published at CT-RSA 2001. Cite the range, not one year [@dhaes], [@dhies_ms]. That parenthetical -- &quot;an attractive starting point for developing standards&quot; -- turned out to be prophetic in the worst way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cramer and Shoup (2001): the modular abstraction.&lt;/strong&gt; DHIES hinted at a structure; Cramer and Shoup formalized it. A KEM transports a random key, a DEM encrypts the payload, and the composite is secure against chosen-ciphertext attack if both halves are [@cs01]. A few years later the necessity direction was worked out: for the standard hybrid, both halves essentially have to reach that bar [@hhk06]. This is the theory HPKE would eventually standardize. It existed, complete, in 2001.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; By 2001 the field had both a clean, proof-backed academic scheme (DHIES) and the modular abstraction that made its security compositional (KEM-DEM). The striking fact about HPKE is not that someone invented a new idea in 2022. It is that the correct idea sat published and unadopted &lt;em&gt;as a standard&lt;/em&gt; for two decades [@cs01], [@rfc9180].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So by 2001 everything HPKE needed already existed: a concrete, provably secure ephemeral-static scheme, and an abstraction that made &quot;encrypt to a public key&quot; a matter of plugging two proven pieces together. What happened over the following eight years was not progress. It was fragmentation. The same good idea was handed to four different standards bodies, and it came out four incompatible ways.&lt;/p&gt;
&lt;h2&gt;4. The ECIES Zoo&lt;/h2&gt;
&lt;p&gt;Between 2001 and 2009 the DHIES construction was standardized four times -- ANSI X9.63, IEEE 1363a, ISO/IEC 18033-2, and SEC 1 v2 -- collectively called ECIES, and collectively a mess that RFC 9180 politely calls &quot;numerous competing and non-interoperable standards&quot; [@rfc9180], [@maea10]. This is the hook&apos;s promise, paid off in detail. Three defects run through the era, and HPKE was built to close all three.&lt;/p&gt;

flowchart TD
    D[&quot;DHIES (Abdalla, Bellare, Rogaway)&quot;] --&amp;gt; A[&quot;ANSI X9.63 (2001)&quot;]
    D --&amp;gt; I[&quot;IEEE 1363a (2004)&quot;]
    D --&amp;gt; S[&quot;ISO/IEC 18033-2 (2006)&quot;]
    D --&amp;gt; C[&quot;SEC 1 v2 (2009)&quot;]
    A --&amp;gt; Z[&quot;The ECIES zoo: incompatible KDFs, MACs, point encodings, no shared test vectors&quot;]
    I --&amp;gt; Z
    S --&amp;gt; Z
    C --&amp;gt; Z
    Z --&amp;gt; H[&quot;RFC 9180 (2022): one construction, proven, test-vectored&quot;]
&lt;p&gt;&lt;strong&gt;Fragmentation: the &quot;which ECIES?&quot; problem.&lt;/strong&gt; The four standards disagree on the key-derivation function, the MAC, whether elliptic-curve points are compressed or uncompressed, and whether the optional &lt;code&gt;SharedInfo&lt;/code&gt; fields are present. So two conforming implementations often cannot interoperate: an ECIES ciphertext built with the ANSI X9.63 KDF and one built to ISO/IEC 18033-2, for the same curve and message, are byte-incompatible [@rfc9180], [@maea10].The concrete interoperability failure is not hypothetical: swap the KDF or the point-encoding convention between two of the four standards and a ciphertext valid under one is undecryptable under another, for identical inputs [@maea10].&lt;/p&gt;

The four standards -- ANSI X9.63 (2001), IEEE 1363a (2004), ISO/IEC 18033-2 (2006), and SEC 1 version 2 (2009) -- differ across enough independent knobs (KDF, MAC, point-encoding, `SharedInfo`) that &quot;ECIES&quot; names a family of mutually unintelligible dialects rather than one scheme. RFC 9180 points readers to the Gayoso Martinez et al. survey &quot;for a thorough comparison.&quot; That survey and three of the four standards are paywalled, so this article cites them through RFC 9180&apos;s reference section rather than by live link [@rfc9180], [@maea10], [@sec1].
&lt;p&gt;&lt;strong&gt;Benign malleability.&lt;/strong&gt; SEC 1 version 2 documents a specific defect and then waves it off.&lt;/p&gt;

A property of the ECIES standards in which an attacker can transform a valid ciphertext into a different valid ciphertext for the *same* plaintext -- concretely, by replacing the ephemeral point $R$ with $-R$, which shares the same x-coordinate. SEC 1 called it &quot;benign malleability&quot; and &quot;generally deemed harmless,&quot; and offered binding $R$ into `SharedInfo` only as an *optional* countermeasure. Because a ciphertext can be mauled into a second valid ciphertext, the scheme as written is not strictly secure against chosen-ciphertext attack [@sec1].
&lt;p&gt;The word &quot;benign&quot; is doing a great deal of work. A scheme that lets anyone produce a second valid ciphertext for a plaintext they cannot read is, by definition, malleable, and malleability is exactly what security against chosen-ciphertext attack forbids. SEC 1 made the fix -- include $R$ in the hashed context -- optional. Hold that thought: it is the single design decision the next section shows HPKE making mandatory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No proofs, and no test vectors.&lt;/strong&gt; RFC 9180 is blunt about the state it inherited: prior schemes &quot;rely on outdated primitives, lack proofs of IND-CCA2 security, or fail to provide test vectors&quot; [@rfc9180]. The missing test vectors are not a documentation nicety. Without a published set of known-good inputs and outputs, two implementers have no way to discover that their libraries disagree until a ciphertext fails to decrypt in production -- which is precisely how &quot;conformant&quot; implementations drifted apart.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The ECIES era is the standing proof that a primitive can be widely deployed and still be under-specified, unprovable, and mutually incompatible. &quot;We implemented the standard&quot; told you nothing about whether your ciphertext would decrypt on the other side, or whether the scheme met a modern security definition. Both had to be established separately, and neither was [@rfc9180], [@maea10].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The comparison below is the historical spine as a grid. Read the last two columns -- proof status and test vectors -- and the shape of the fix becomes obvious.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Core idea&lt;/th&gt;
&lt;th&gt;Main weakness&lt;/th&gt;
&lt;th&gt;Proof and test vectors&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;RSA key transport (PKCS#1 v1.5)&lt;/td&gt;
&lt;td&gt;Encrypt a chosen key under RSA&lt;/td&gt;
&lt;td&gt;Padding oracle (Bleichenbacher, ROBOT)&lt;/td&gt;
&lt;td&gt;No CCA2 proof; padding fragile&lt;/td&gt;
&lt;td&gt;Deprecated in TLS 1.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElGamal encryption&lt;/td&gt;
&lt;td&gt;Encode a chosen key as a group element&lt;/td&gt;
&lt;td&gt;Malleable; roughly 2x expansion&lt;/td&gt;
&lt;td&gt;No CCA2 proof&lt;/td&gt;
&lt;td&gt;Historical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DHIES / DHAES&lt;/td&gt;
&lt;td&gt;Derive the key from ephemeral-static DH&lt;/td&gt;
&lt;td&gt;Underspecified as a deployable format&lt;/td&gt;
&lt;td&gt;CCA2 proof (ODH); no shared vectors&lt;/td&gt;
&lt;td&gt;Academic root of ECIES&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ECIES (four standards)&lt;/td&gt;
&lt;td&gt;Standardize DHIES&lt;/td&gt;
&lt;td&gt;Fragmented; benign malleability&lt;/td&gt;
&lt;td&gt;Proof gaps; no shared vectors&lt;/td&gt;
&lt;td&gt;Legacy interop only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPKE (RFC 9180)&lt;/td&gt;
&lt;td&gt;KEM-DEM, all public keys bound in&lt;/td&gt;
&lt;td&gt;Non-goals must be respected&lt;/td&gt;
&lt;td&gt;IND-CCA2 proof; per-suite vectors&lt;/td&gt;
&lt;td&gt;Current default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HPKE + X-Wing&lt;/td&gt;
&lt;td&gt;Swap DHKEM for a hybrid PQ KEM&lt;/td&gt;
&lt;td&gt;Larger ciphertexts; draft-stage&lt;/td&gt;
&lt;td&gt;Hybrid PQ proof; draft vectors&lt;/td&gt;
&lt;td&gt;Emerging&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Every one of these defects points at the same missing discipline: pin &lt;em&gt;one&lt;/em&gt; construction, bind the ambiguous inputs so malleability becomes impossible, prove the result, and publish vectors so anyone can check interoperation. That list is not a wishlist. It is, almost line for line, the changelog of RFC 9180. Here is what &quot;done right&quot; actually changed.&lt;/p&gt;
&lt;h2&gt;5. &quot;Done Right&quot; Was Discipline, Not Genius&lt;/h2&gt;
&lt;p&gt;There is no eureka moment in this story, and that is the point. HPKE&apos;s virtue is not a clever new algorithm -- the algorithm, KEM-DEM, was twenty years old when RFC 9180 shipped. Its virtue is four consolidating decisions, made once, correctly, with the proof and the test vectors published in the same body of work. Each decision closes one of the previous section&apos;s defects.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision one: abstract the KEM so the primitive is agile.&lt;/strong&gt; Instead of freezing one curve, one KDF, and one MAC into a single scheme, HPKE is parameterized over a ciphersuite of three identifiers: &lt;code&gt;kem_id&lt;/code&gt;, &lt;code&gt;kdf_id&lt;/code&gt;, and &lt;code&gt;aead_id&lt;/code&gt; [@rfc9180]. This looks like a small thing and is not. Fragmentation was the disease of the ECIES era, where each standards body froze a different combination.&lt;/p&gt;
&lt;p&gt;HPKE replaces uncontrolled fragmentation with &lt;em&gt;managed&lt;/em&gt; agility: many ciphersuites, but one construction and one set of rules governing all of them. When a component needs replacing -- as the post-quantum transition will require -- you change an identifier, not the scheme.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision two: bind every public key into the key schedule.&lt;/strong&gt; This is the decisive fix, and RFC 9180 states it in one sentence.&lt;/p&gt;

&quot;HPKE mitigates malleability problems (called benign malleability) in prior public key encryption standards based on ECIES by including all public keys in the context of the key schedule.&quot; -- RFC 9180, Section 9.1
&lt;p&gt;What SEC 1 made optional, HPKE makes mandatory [@rfc9180], [@sec1]. Recall the benign-malleability trick: replace the ephemeral point $R$ with $-R$ to get a second valid ciphertext. In HPKE the public keys are inputs to the derived keys, so a mauled encapsulation derives a &lt;em&gt;different&lt;/em&gt; key schedule and simply fails to produce a valid decryption. The attack does not become hard; it becomes impossible to express. The malleability is not patched -- it is designed out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision three: mandate a single AEAD as the DEM.&lt;/strong&gt; Gone is the ad-hoc two-key Encrypt-then-MAC with per-standard knobs for the cipher, the MAC, and the tag order. HPKE specifies one AEAD scheme per ciphersuite, so authenticity is intrinsic to the primitive rather than assembled by the caller [@rfc9180]. If the series&apos; part on &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;safe authenticated-encryption composition&lt;/a&gt; has a single thesis -- compose it wrong and the tag verifies while the forgery goes through -- then HPKE is simply that &quot;compose it correctly&quot; rule frozen into a specification.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decision four: ship proofs and test vectors, developed alongside the standard.&lt;/strong&gt; This is the part the ECIES era never had. HPKE began as an individual draft in 2019 and was analyzed as it was written [@hpke_draft]. Lipp&apos;s CryptoVerif analysis mechanically checked all four modes; the analysis by Alwen, Blanchet, Hauck, Kiltz, Lipp, and Riepel gave exact security bounds for the authenticated mode and was published at EUROCRYPT 2021; and the two pre-shared-key modes were later closed under the Gap Diffie-Hellman assumption [@lipp20], [@abhklr20], [@psk1480]. Every ciphersuite ships machine-checkable test vectors in the RFC itself [@rfc9180].&lt;/p&gt;

The standard security goal for public-key encryption. An adversary who may submit ciphertexts of its choice to a decryption oracle -- adaptively, even after seeing the challenge -- still cannot tell which of two chosen plaintexts a challenge ciphertext encrypts. It is the bar the ECIES standards could not prove and that malleability violates, and the bar HPKE&apos;s composition meets when its KEM and AEAD do [@cs01], [@hhk06].
&lt;p&gt;Put the four together and your sense of what happened should shift.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; &quot;Done right&quot; was consolidation, not invention. Abstract the KEM for agility, bind every public key into the key schedule so benign malleability cannot happen, mandate one AEAD instead of ad-hoc Encrypt-then-MAC, and ship machine-checked proofs &lt;em&gt;and&lt;/em&gt; per-ciphersuite test vectors in the same document. The innovation is rigor -- which is exactly why HPKE is boring, and why boring is the achievement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The KEM-DEM theory was published in 2001 and simply not adopted as the standard until 2022. HPKE is the merge of a two-decade-old proof track with a standards track: it did not out-think the ECIES authors so much as out-&lt;em&gt;discipline&lt;/em&gt; them, pinning one construction and refusing to ship it without proofs and vectors [@cs01], [@rfc9180].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There is even a small tell that HPKE was designed to be built &lt;em&gt;on&lt;/em&gt;, not just used directly.HPKE reserves an export-only AEAD identifier (the value &lt;code&gt;0xFFFF&lt;/code&gt;) for ciphersuites that never call &lt;code&gt;Seal&lt;/code&gt; and only ever use the &lt;code&gt;Export&lt;/code&gt; interface -- a strong hint that the authors expected HPKE to be keyed into other protocols rather than used standalone [@rfc9180].&lt;/p&gt;
&lt;p&gt;Four decisions turn a fragmented, unprovable family into one boring, correct primitive. But &quot;one primitive&quot; is not &quot;one behavior.&quot; HPKE ships a small menu -- four modes -- and choosing the wrong one is the first and most common way to misuse it. The menu is where the design stops being about the math and starts being about what &lt;em&gt;you&lt;/em&gt; are trying to authenticate.&lt;/p&gt;
&lt;h2&gt;6. The Four Modes: HPKE&apos;s Core Menu&lt;/h2&gt;
&lt;p&gt;RFC 9180, Section 5.1, defines exactly four modes, and the difference between them reduces to a single question: &lt;em&gt;what extra secret or identity gets folded into the key schedule?&lt;/em&gt; Get the answer wrong and you either fail to authenticate a sender you needed to, or -- worse -- come to believe you authenticated one you did not [@rfc9180]. There are four modes, never three: the pre-shared-key-plus-authenticated combination is a real, distinct mode that is easy to forget.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Extra input&lt;/th&gt;
&lt;th&gt;What it authenticates&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mode_base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Only that the ciphertext is for this recipient key&lt;/td&gt;
&lt;td&gt;You have just the recipient&apos;s public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mode_psk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x01&lt;/td&gt;
&lt;td&gt;a pre-shared key&lt;/td&gt;
&lt;td&gt;Recipient, plus possession of the PSK&lt;/td&gt;
&lt;td&gt;A symmetric secret already exists out of band&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mode_auth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x02&lt;/td&gt;
&lt;td&gt;sender&apos;s static key&lt;/td&gt;
&lt;td&gt;That a specific sender key holder sent it&lt;/td&gt;
&lt;td&gt;The recipient must know which sender key sealed it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mode_auth_psk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0x03&lt;/td&gt;
&lt;td&gt;static key and PSK&lt;/td&gt;
&lt;td&gt;Both a sender identity and PSK possession&lt;/td&gt;
&lt;td&gt;You need sender identity and a shared secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mode_base&lt;/code&gt; (0x00): nothing extra.&lt;/strong&gt; The sender is anonymous; the derivation authenticates only that the ciphertext was sealed to this recipient&apos;s key. It is the default -- and, tellingly, the mode that every flagship deployment in the next section uses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mode_psk&lt;/code&gt; (0x01): a pre-shared key.&lt;/strong&gt; A symmetric secret known to both parties is folded into the schedule, so a valid decryption additionally proves the sender possessed the PSK.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;mode_psk&lt;/code&gt; authenticates &lt;em&gt;possession of the PSK&lt;/em&gt;, which means a weak PSK is an authentication that anyone who guesses it can forge. RFC 9180 requires a PSK with at least 32 bytes of entropy, and the reason is not arbitrary: a low-entropy PSK opens a partitioning-oracle attack that the non-goals section quantifies. Treat the PSK as key material, never as a human-chosen secret [@rfc9180].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mode_auth&lt;/code&gt; (0x02): the sender&apos;s static key.&lt;/strong&gt; Here &lt;code&gt;Encap&lt;/code&gt;/&lt;code&gt;Decap&lt;/code&gt; are replaced by &lt;code&gt;AuthEncap&lt;/code&gt;/&lt;code&gt;AuthDecap&lt;/code&gt;, which fold a Diffie-Hellman value between the sender&apos;s &lt;em&gt;static&lt;/em&gt; key and the recipient into the shared secret. A valid decryption now proves the message came from the holder of that specific sender key [@rfc9180].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;mode_auth_psk&lt;/code&gt; (0x03): both.&lt;/strong&gt; A static sender identity &lt;em&gt;and&lt;/em&gt; a shared secret, folded together.&lt;/p&gt;

flowchart TD
    B[&quot;mode_base 0x00: nothing extra&quot;] --&amp;gt; KS[&quot;key_schedule context&quot;]
    P[&quot;mode_psk 0x01: plus a pre-shared key&quot;] --&amp;gt; KS
    A[&quot;mode_auth 0x02: plus sender static key via AuthEncap&quot;] --&amp;gt; KS
    AP[&quot;mode_auth_psk 0x03: plus PSK and sender static key&quot;] --&amp;gt; KS
    KS --&amp;gt; OUT[&quot;Same construction, four different derived key schedules&quot;]
&lt;p&gt;Now the subtlety that trips up careful engineers. The authenticated modes are a form of &lt;em&gt;signcryption&lt;/em&gt;, and they carry a security distinction imported from that theory.&lt;/p&gt;

A primitive that combines encryption and sender authentication in one operation, cheaper than encrypt-then-sign. Its authentication is defined relative to *insiders* and *outsiders*: an outsider is any third party, while an insider is a party who holds one of the long-term keys. HPKE&apos;s authenticated modes are signcryption, so their guarantees are stated in these terms rather than as a public, third-party-verifiable signature.
&lt;p&gt;The consequence matters: &lt;code&gt;mode_auth&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; a digital signature. Because the recipient shares the Diffie-Hellman secret that authenticated the sender, the recipient could have produced the same ciphertext, so it cannot prove to any third party who the author was -- there is no non-repudiation. The analysis by Alwen, Blanchet, Hauck, Kiltz, Lipp, and Riepel makes this precise with an insider-versus-outsider treatment lifted from signcryption theory [@abhklr20], [@signcryption]. If you want a third party to verify authorship, you want &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;the digital-signature part of this series&lt;/a&gt;, not HPKE&apos;s &lt;code&gt;auth&lt;/code&gt; mode.&lt;/p&gt;
&lt;p&gt;The security properties proven for each mode, from RFC 9180&apos;s Table 6 via Lipp&apos;s mechanized analysis, are worth seeing side by side [@rfc9180], [@lipp20]:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;PSK&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;th&gt;AuthPSK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Message confidentiality (IND-CCA2)&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;tr&gt;
&lt;td&gt;Export secrecy (variable-length PRF)&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;tr&gt;
&lt;td&gt;Sender authentication&lt;/td&gt;
&lt;td&gt;N/A&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;tr&gt;
&lt;td&gt;Extra secret assumed&lt;/td&gt;
&lt;td&gt;recipient key secret&lt;/td&gt;
&lt;td&gt;plus PSK secret&lt;/td&gt;
&lt;td&gt;plus sender key secret&lt;/td&gt;
&lt;td&gt;plus sender key and PSK&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The structural model below makes the &quot;same machinery, different inputs&quot; claim concrete: it builds a stand-in key-schedule context for each mode and shows the derived key differs, though nothing about the construction changed.&lt;/p&gt;
&lt;p&gt;{`
// STRUCTURAL MODEL, NOT REAL CRYPTO. Shows only that mode changes the derived key.
function toyMix(s){let h=2166136261&amp;gt;&amp;gt;&amp;gt;0;for(const ch of s){h^=ch.charCodeAt(0);h=Math.imul(h,16777619)&amp;gt;&amp;gt;&amp;gt;0;}return (h&amp;gt;&amp;gt;&amp;gt;0).toString(16);}
const shared_secret = &apos;ss-from-KEM&apos;;&lt;/p&gt;
&lt;p&gt;// Real HPKE: Extract a PRK from shared_secret, then Expand over mode + psk_id_hash + info_hash.
// This toy just mixes them (plus the sender key for auth modes) to show the mode changes the key.
function keySchedule(mode, pskId, senderKey) {
  const psk_id_hash = toyMix(&apos;pskid:&apos; + (pskId || &apos;&apos;));
  const info_hash   = toyMix(&apos;info:my-app-v1&apos;);
  const ss = senderKey ? toyMix(shared_secret + &apos;|auth:&apos; + senderKey) : shared_secret;
  return toyMix(mode + &apos;|&apos; + psk_id_hash + &apos;|&apos; + info_hash + &apos;|&apos; + ss);
}&lt;/p&gt;
&lt;p&gt;console.log(&apos;base     -&amp;gt; &apos; + keySchedule(&apos;0x00&apos;, null, null));
console.log(&apos;psk      -&amp;gt; &apos; + keySchedule(&apos;0x01&apos;, &apos;shared-psk&apos;, null));
console.log(&apos;auth     -&amp;gt; &apos; + keySchedule(&apos;0x02&apos;, null, &apos;sender-static-key&apos;));
console.log(&apos;auth_psk -&amp;gt; &apos; + keySchedule(&apos;0x03&apos;, &apos;shared-psk&apos;, &apos;sender-static-key&apos;));
console.log(&apos;all four derive different keys from identical machinery&apos;);
`}&lt;/p&gt;
&lt;p&gt;Choosing a mode answers &quot;who am I authenticating?&quot; It says nothing about which curve, which KDF, which AEAD, and how not to shoot yourself with the nonce. That is the ciphersuite-and-usage layer -- where the test vectors finally earn their keep, and where the known HPKE implementation advisories actually live.&lt;/p&gt;
&lt;h2&gt;7. Ciphersuites and Correct Usage&lt;/h2&gt;
&lt;p&gt;A mode plus a ciphersuite is a complete HPKE configuration. The ciphersuite is three identifiers -- a KEM, a KDF, and an AEAD -- and the correct-usage rules are mostly about a single thing: never let the same nonce protect two messages.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Slot&lt;/th&gt;
&lt;th&gt;Options&lt;/th&gt;
&lt;th&gt;How to choose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;KEM&lt;/td&gt;
&lt;td&gt;DHKEM(X25519, HKDF-SHA256); DHKEM(P-256/P-384/P-521, HKDF-SHA256/384/512); DHKEM(X448, HKDF-SHA512)&lt;/td&gt;
&lt;td&gt;X25519 unless policy requires NIST curves; encapsulation &lt;code&gt;enc&lt;/code&gt; is 32 bytes for X25519, 65 for P-256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KDF&lt;/td&gt;
&lt;td&gt;HKDF-SHA256, HKDF-SHA384, HKDF-SHA512&lt;/td&gt;
&lt;td&gt;Match the KDF hash to the KEM and AEAD strength&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AEAD&lt;/td&gt;
&lt;td&gt;AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305, Export-only (0xFFFF)&lt;/td&gt;
&lt;td&gt;AES-GCM with AES hardware; ChaCha20-Poly1305 without; export-only when you never call &lt;code&gt;Seal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The three menus map onto earlier parts of this series. DHKEM&apos;s curves are &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 of the curves part&lt;/a&gt;; pick X25519 for new designs, NIST curves only when a compliance regime demands them. The KDF slot is the HKDF of the key-derivation part, used exactly as designed. The AEAD slot is &lt;a href=&quot;https://paragmali.com/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/&quot; rel=&quot;noopener&quot;&gt;the decision from the AEAD part&lt;/a&gt;: AES-GCM where the CPU has AES instructions, ChaCha20-Poly1305 where it does not. The tag is 16 bytes in every case.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The nonce and sequence discipline is the crux.&lt;/strong&gt; Inside a &lt;code&gt;Context&lt;/code&gt;, the per-message nonce is computed as &lt;code&gt;nonce = base_nonce XOR seq&lt;/code&gt;, where &lt;code&gt;seq&lt;/code&gt; is an incrementing sequence number. The &lt;code&gt;Context&lt;/code&gt; is therefore &lt;em&gt;stateful&lt;/em&gt;, and the sequence number must never repeat and must never overflow, because AEAD nonce reuse is catastrophic: for AES-GCM it can leak the authentication key, and for any of these AEADs it breaks confidentiality outright [@rfc9180]. This is the same hazard &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;the nonce-reuse part of this series&lt;/a&gt; covers for AES-GCM, arriving here as a counter you must not let wrap.&lt;/p&gt;
&lt;p&gt;The structural model below computes a few per-message nonces and then deliberately overflows a narrow counter so you can watch a nonce repeat -- which is, in miniature, exactly the implementation bug the misuse section names.&lt;/p&gt;
&lt;p&gt;{`
// STRUCTURAL MODEL, NOT REAL CRYPTO. Toy 8-bit nonce; real HPKE nonces are 96 bits.
const base_nonce = 0xA7;
function nonceFor(seq) { return (base_nonce ^ (seq &amp;amp; 255)).toString(16); }&lt;/p&gt;
&lt;p&gt;for (let seq = 0; seq !== 5; seq++) {
  console.log(&apos;seq &apos; + seq + &apos; -&amp;gt; nonce &apos; + nonceFor(seq));
}&lt;/p&gt;
&lt;p&gt;// A deliberately NARROW counter overflows after 256 messages and REPEATS a nonce.
const width = 256;
const seqA = 3;
const seqWrap = seqA + width;                 // a later message with the same low 8 bits
console.log(&apos;seq &apos; + seqA + &apos;   -&amp;gt; nonce &apos; + nonceFor(seqA));
console.log(&apos;seq &apos; + seqWrap + &apos; -&amp;gt; nonce &apos; + nonceFor(seqWrap) + &apos;   (wrapped)&apos;);
console.log(&apos;nonce reused after overflow = &apos; + (nonceFor(seqA) === nonceFor(seqWrap)));
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Prefer single-shot &lt;code&gt;Seal&lt;/code&gt;/&lt;code&gt;Open&lt;/code&gt; for independent messages -- it sets up, encrypts once, and tears down, so there is no counter to mismanage. If you hold a &lt;code&gt;Context&lt;/code&gt; open, &lt;code&gt;Seal&lt;/code&gt; strictly in order, respect the per-ciphersuite AEAD message-count and input-length limits, and never reuse a &lt;code&gt;Context&lt;/code&gt; across two independent streams. A repeated or overflowed sequence number is a repeated nonce, and a repeated nonce is the end of your confidentiality [@rfc9180].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two operational notes round out the usage rules. First, the AEAD message-count and plaintext-length limits are per-ciphersuite constants, and single-shot &lt;code&gt;Seal&lt;/code&gt;/&lt;code&gt;Open&lt;/code&gt; sidesteps the streaming bookkeeping entirely.When you need to protect &lt;em&gt;many&lt;/em&gt; messages or key a different protocol&apos;s framing, use the &lt;code&gt;Export&lt;/code&gt; interface to derive an independent secret rather than looping &lt;code&gt;Seal&lt;/code&gt; on one long-lived &lt;code&gt;Context&lt;/code&gt; -- this is exactly what Oblivious HTTP and Oblivious DNS do [@rfc9180]. Second, the one default to remember: &lt;code&gt;mode_base&lt;/code&gt; with DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM is the interop-safe choice, with ChaCha20-Poly1305 substituted on hardware without AES acceleration.&lt;/p&gt;
&lt;p&gt;These rules read like an interop checklist because that is precisely what they are, and the only reason they &lt;em&gt;can&lt;/em&gt; be a checklist is the test vectors: you can prove your library agrees with the specification byte for byte before you ship. But a checklist for &lt;em&gt;using&lt;/em&gt; HPKE says nothing about &lt;em&gt;where it has already won&lt;/em&gt;. The strongest evidence that HPKE is the right default is that the modern privacy stack is built on it.&lt;/p&gt;
&lt;h2&gt;8. Deployments as the Proof&lt;/h2&gt;
&lt;p&gt;The best argument that HPKE is state of the art in 2026 is not a benchmark. It is a deployment list. Every &quot;encrypt to a party you have never spoken to&quot; corner of the modern IETF stack runs on HPKE, and they almost all run on the &lt;em&gt;same&lt;/em&gt; mode.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Specification&lt;/th&gt;
&lt;th&gt;What HPKE does&lt;/th&gt;
&lt;th&gt;Mode and API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Encrypted Client Hello&lt;/td&gt;
&lt;td&gt;RFC 9849 (2026)&lt;/td&gt;
&lt;td&gt;Encrypts the inner ClientHello, including the server name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mode_base&lt;/code&gt;, &lt;code&gt;SetupBaseS&lt;/code&gt;/&lt;code&gt;SetupBaseR&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messaging Layer Security&lt;/td&gt;
&lt;td&gt;RFC 9420 (2023)&lt;/td&gt;
&lt;td&gt;Encrypts path secrets to tree nodes in TreeKEM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mode_base&lt;/code&gt;, &lt;code&gt;SealBase&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oblivious HTTP&lt;/td&gt;
&lt;td&gt;RFC 9458 (2024)&lt;/td&gt;
&lt;td&gt;Encapsulates requests and responses to a gateway&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mode_base&lt;/code&gt; plus &lt;code&gt;Export&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oblivious DNS over HTTPS&lt;/td&gt;
&lt;td&gt;RFC 9230 (2022)&lt;/td&gt;
&lt;td&gt;Encrypts DNS queries to a target through a proxy&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mode_base&lt;/code&gt; plus &lt;code&gt;Export&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Encrypted Client Hello.&lt;/strong&gt; ECH encrypts the sensitive inner ClientHello -- including the server name a censor would want to read -- to the server&apos;s HPKE public key, published in an &lt;code&gt;ECHConfig&lt;/code&gt; and fetched ahead of time. It uses &lt;code&gt;SetupBaseS&lt;/code&gt; on the client and &lt;code&gt;SetupBaseR&lt;/code&gt; on the server [@rfc9849].Currency pin: ECH is now Standards-Track RFC 9849, published in March 2026 by Rescorla, Oku, Sullivan, and Wood, and formerly known as &lt;code&gt;draft-ietf-tls-esni&lt;/code&gt;. It is an RFC, not a draft, and its text references HPKE throughout [@rfc9849].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Messaging Layer Security.&lt;/strong&gt; MLS uses HPKE inside TreeKEM: when a group member updates the ratchet tree, it encrypts the new path secrets to the public keys of the relevant tree nodes, using &lt;code&gt;SealBase&lt;/code&gt; [@rfc9420]. Group-scale forward secrecy and post-compromise security come from the tree and the ratchet, not from HPKE.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Oblivious HTTP and Oblivious DNS.&lt;/strong&gt; Both split trust between a relay and a gateway so that no single party sees both who you are and what you asked. The client seals its request to the gateway&apos;s HPKE key; the relay forwards ciphertext it cannot read; the gateway decapsulates and, crucially, uses &lt;code&gt;Export&lt;/code&gt; to key the response rather than looping &lt;code&gt;Seal&lt;/code&gt; [@rfc9458], [@rfc9230]. Oblivious DNS, in 2022, was the first shipped RFC deployment of HPKE.&lt;/p&gt;

flowchart LR
    C[&quot;Client seals request with SetupBaseS&quot;] --&amp;gt; REL[&quot;Relay sees ciphertext and client address, not content&quot;]
    REL --&amp;gt; GW[&quot;Gateway runs SetupBaseR, decrypts, Export-keys the reply&quot;]
    GW --&amp;gt; TGT[&quot;Target sees the request, never the client address&quot;]
&lt;p&gt;Privacy Pass and its Private Access Tokens belong on the list too, at one remove: some deployments run token issuance over Oblivious HTTP (RFC 9578 does not require it), and therefore over HPKE [@rfc9578], [@rfc9458].&lt;/p&gt;
&lt;p&gt;Two observations reinforce the article&apos;s spine. First, every marquee deployment uses &lt;code&gt;mode_base&lt;/code&gt;: sender authentication and forward secrecy are supplied &lt;em&gt;at the protocol layer&lt;/em&gt; -- by the TLS handshake, by the MLS ratchet -- leaving HPKE to be the boring sealing primitive it was designed to be. Second, Oblivious HTTP and Oblivious DNS reach for &lt;code&gt;Export&lt;/code&gt; to key their own framing instead of looping &lt;code&gt;Seal&lt;/code&gt;, exactly the extensibility the export interface was built for. The deployments did not stretch HPKE; they used the parts the design anticipated.&lt;/p&gt;

DHKEM is classical-only, which means every HPKE ciphertext sealed today is exposed to &quot;harvest now, decrypt later&quot;: an adversary can store it and decrypt it once a quantum computer can solve the elliptic-curve Diffie-Hellman problem. The fix is the payoff of Decision one -- swap the KEM, do not redesign HPKE. The leading candidate is X-Wing, a hybrid KEM combining X25519 and ML-KEM-768 that stays secure if *either* component holds, and it comes with a dedicated &quot;Use in HPKE&quot; mapping [@xwing], [@xwing_draft], [@fips203]. The cost is size: an X-Wing encapsulation is roughly 1120 bytes against 32 for DHKEM(X25519) [@xwing_draft06]. Draft-stage ciphersuites also include a pure ML-KEM option, a generic hybrid-KEM framework, and a combined post-quantum suite with a SHA-3-based KDF [@mlkem_draft], [@hybrid_draft], [@barnes_pq]. One caveat matters for the modes section: X-Wing is *not* an authenticated KEM, so the post-quantum path powers `base` and `psk` but not yet `auth` or `auth_psk` [@xwing_draft06].
&lt;p&gt;HPKE won the IETF privacy stack. But &quot;won&quot; invites the question a careful engineer actually asks: won &lt;em&gt;against what&lt;/em&gt;, and when would I still reach for something else? The honest comparison comes next -- along with the one construction people keep miscategorizing as HPKE.&lt;/p&gt;
&lt;h2&gt;9. Competing Approaches, and One That Is Not HPKE&lt;/h2&gt;
&lt;p&gt;HPKE is the right default for &lt;em&gt;new protocol design&lt;/em&gt;, but it is not the only encrypt-to-a-public-key tool, and it is not always the right one. Because all of these building blocks cost about the same -- roughly two elliptic-curve scalar multiplications for the sender, plus an ephemeral value and an AEAD tag of overhead -- the honest comparison is not speed. It is security model, agility, and proof discipline.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Construction&lt;/th&gt;
&lt;th&gt;Agility and modes&lt;/th&gt;
&lt;th&gt;Proof and shared vectors&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;HPKE (RFC 9180)&lt;/td&gt;
&lt;td&gt;KEM-DEM (DHKEM + KDF + AEAD)&lt;/td&gt;
&lt;td&gt;Full agility; four modes&lt;/td&gt;
&lt;td&gt;Machine-checked; per-suite vectors&lt;/td&gt;
&lt;td&gt;New protocol design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libsodium &lt;code&gt;crypto_box&lt;/code&gt; / seal&lt;/td&gt;
&lt;td&gt;Fixed X25519 + XSalsa20-Poly1305&lt;/td&gt;
&lt;td&gt;None; mutual-auth, repudiable&lt;/td&gt;
&lt;td&gt;No mechanized proof; partial vectors&lt;/td&gt;
&lt;td&gt;Both ends yours, zero knobs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JOSE &lt;code&gt;ECDH-ES&lt;/code&gt; (RFC 7518)&lt;/td&gt;
&lt;td&gt;Ephemeral-static ECDH + Concat KDF&lt;/td&gt;
&lt;td&gt;Some agility; encryption only&lt;/td&gt;
&lt;td&gt;No mechanized proof; partial vectors&lt;/td&gt;
&lt;td&gt;JWE and JWT stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;age&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;X25519 + HKDF-SHA256 + ChaCha20-Poly1305&lt;/td&gt;
&lt;td&gt;None; file-oriented&lt;/td&gt;
&lt;td&gt;No mechanized proof; has vectors&lt;/td&gt;
&lt;td&gt;Encrypting files to a key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw ECIES&lt;/td&gt;
&lt;td&gt;Ephemeral-static ECDH + Encrypt-then-MAC&lt;/td&gt;
&lt;td&gt;Per-standard knobs&lt;/td&gt;
&lt;td&gt;Proof gaps; no shared vectors&lt;/td&gt;
&lt;td&gt;Legacy and blockchain interop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;libsodium &lt;code&gt;crypto_box&lt;/code&gt; and sealed boxes.&lt;/strong&gt; One fixed suite -- X25519, XSalsa20, Poly1305 -- simple and misuse-resistant, but with no agility, no formal mode menu, and no HPKE-grade test vectors; raw &lt;code&gt;crypto_box&lt;/code&gt; also makes the caller manage nonces, and it is deliberately repudiable and mutually authenticated [@nacl]. Reach for it when you own both ends and want zero knobs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JOSE and COSE &lt;code&gt;ECDH-ES&lt;/code&gt;.&lt;/strong&gt; Ephemeral-static ECDH with the Concat KDF, native to JWE and JWT [@rfc7518]. It has no &lt;code&gt;auth&lt;/code&gt; or &lt;code&gt;psk&lt;/code&gt; modes, uses Concat-KDF rather than HKDF, and ships nothing like HPKE&apos;s vectors. It is chosen by platform gravity, and a COSE-HPKE binding is emerging to bring HPKE &lt;em&gt;into&lt;/em&gt; that world [@cose_hpke].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;age&lt;/code&gt;.&lt;/strong&gt; Wraps a random file key to X25519 recipients with HKDF-SHA256 and ChaCha20-Poly1305 -- the same primitive family as DHKEM(X25519) plus an AEAD, but fixed and file-oriented [@age]. Reach for it to encrypt files to a public key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raw ECIES&lt;/strong&gt; survives only for interop with existing deployments, such as blockchain wallets and legacy PKI [@sec1]. And &lt;strong&gt;RSA-KEM&lt;/strong&gt; (RFC 5990) is worth one line: it proves the KEM-DEM pattern generalizes beyond Diffie-Hellman, but RFC 9180 defines no RSA-KEM ciphersuite, so it stays a niche [@rfc5990].&lt;/p&gt;
&lt;p&gt;Now the boundary marker, because it is the single most common miscategorization in this space.&lt;/p&gt;

Apple&apos;s PQ3 protocol for iMessage is *KEM-based* -- as Apple describes it, the design combines a hybrid post-quantum KEM (ML-KEM/Kyber-1024 with P-256 ECDH) through HKDF-SHA384, inside a triple-ratchet message scheme [@apple_pq3] -- but it is **not** an RFC 9180 HPKE deployment, and it never invokes HPKE. It solves a different problem: stateful, forward-secret, *interactive* messaging with post-compromise security, where HPKE is single-shot public-key encryption to a static key. PQ3 belongs here only to mark the single-shot-versus-ratchet boundary and to keep it off the HPKE deployment list, where it does not belong. Do not cite it as evidence that HPKE ships in iMessage; it does not.
&lt;p&gt;The ranking has a pattern. The fixed-suite tools win on simplicity, &lt;code&gt;ECDH-ES&lt;/code&gt; wins on platform gravity, and everything else loses to HPKE for &lt;em&gt;new&lt;/em&gt; design, because HPKE has the agility, the modes, the proofs, and the vectors. That makes HPKE sound like a strict upgrade. It is not. Its power comes from a set of things it deliberately refuses to do -- and mistaking those refusals for oversights is where every real HPKE incident begins.&lt;/p&gt;
&lt;h2&gt;10. The Refusals That Look Like Features&lt;/h2&gt;
&lt;p&gt;HPKE&apos;s non-goals are not gaps someone forgot to fill. Several are &lt;em&gt;impossibilities&lt;/em&gt; for a single-shot public-key primitive, and RFC 9180 states them plainly so that no one mistakes the boundary for a bug. Knowing them is the entire discipline this article is teaching.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No forward secrecy against recipient compromise, in any mode.&lt;/strong&gt; RFC 9180 is explicit: HPKE ciphertexts &quot;are not forward secret with respect to recipient compromise in any mode,&quot; because only long-term secrets are used on the recipient&apos;s side [@rfc9180]. This is structural, not an oversight.&lt;/p&gt;

The property that compromising a long-term key does not expose past messages. HPKE has none with respect to recipient compromise: because a single-shot ciphertext is encrypted to a static recipient key, anyone who later obtains that key can decrypt every ciphertext ever sent to it. Forward secrecy must come from a ratchet or key rotation *outside* HPKE -- which is exactly why MLS and TLS layer it on top.
&lt;p&gt;&lt;strong&gt;&lt;code&gt;auth&lt;/code&gt; mode is not a signature.&lt;/strong&gt; As the modes section showed, the authenticated modes are signcryption with insider and outsider guarantees, not third-party non-repudiation. The recipient who shares the Diffie-Hellman secret could have produced the same ciphertext, so it cannot prove authorship to anyone else [@abhklr20], [@rfc9180]. If you need a portable proof of who signed, use a digital signature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not key-committing by default.&lt;/strong&gt; HPKE&apos;s AEADs bind a ciphertext to a plaintext, but not to a &lt;em&gt;unique key&lt;/em&gt;.&lt;/p&gt;

A property by which a ciphertext can be opened successfully under exactly one key. HPKE&apos;s standard AEADs are not key-committing by default, which enables a partitioning-oracle attack: an attacker who can submit ciphertexts and observe success or failure can binary-search a low-entropy secret. The eventual fix is a committing AEAD, whose properties are codified in RFC 9771 [@aead_props].
&lt;p&gt;The concrete danger is a low-entropy PSK. RFC 9180, applying the partitioning-oracle result of Len, Grubbs, and Ristenpart, notes that an attacker can recover a PSK drawn from a set of size $m \cdot k$ in roughly $m + \log k$ oracle queries [@rfc9180], [@lgr20]. That logarithmic search is precisely why the PSK requirement is a floor on &lt;em&gt;entropy&lt;/em&gt;, at least 32 bytes, and not a note about password strength. The committing-AEAD countermeasure is standardized as a set of properties in RFC 9771, awaiting a committing &lt;code&gt;aead_id&lt;/code&gt; [@aead_props].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No replay, reordering, downgrade, or metadata protection.&lt;/strong&gt; HPKE does not detect a replayed ciphertext, enforce message ordering across independent contexts, prevent ciphersuite downgrade, or hide the length or existence of a message. Every one of these is deliberately left to the surrounding protocol [@rfc9180].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Classical-only by default.&lt;/strong&gt; This is the single &lt;em&gt;primitive-level&lt;/em&gt; gap -- and, as the deployment section showed, the only non-goal you close by swapping a component rather than redesigning the scheme.&lt;/p&gt;
&lt;p&gt;It is worth being precise about what is and is not finished. The core security question is &lt;em&gt;closed&lt;/em&gt;: sufficiency and necessity together establish that the KEM-DEM composite reaches IND-CCA2 exactly when its halves do, and all four modes have been machine-checked [@cs01], [@hhk06], [@lipp20], [@abhklr20], [@psk1480].&lt;/p&gt;
&lt;p&gt;The residual gaps are about &lt;em&gt;model and API coverage&lt;/em&gt;, not the core: the tightest authenticated-mode proof covers single-shot use rather than many messages, some results live in the random-oracle model rather than the standard model, and the post-quantum composition result is tightest for the authenticated mode, with the &lt;code&gt;psk&lt;/code&gt; and &lt;code&gt;auth_psk&lt;/code&gt; modes since covered by the PSK-modes analysis and a fully general standard-model post-quantum proof across all four modes still open [@abhklr20], [@psk1480]. Even the definition of IND-CCA for KEMs has known subtleties the analyses had to navigate [@rfc9180], [@bhk09].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The math is finished; the risk moved to the interface. HPKE has no known catastrophic break, so its hazards are misuse of its four modes and its non-goals -- and the post-quantum swap, which changes the KEM and not the scheme, is the proof that the architecture was the achievement. The discipline is knowing what HPKE deliberately does not do.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; HPKE&apos;s most important non-goals look like features waiting to be used: there is &lt;strong&gt;no forward secrecy&lt;/strong&gt; against recipient compromise in any mode, &lt;strong&gt;&lt;code&gt;auth&lt;/code&gt; is not a signature&lt;/strong&gt;, and the AEADs are &lt;strong&gt;not key-committing&lt;/strong&gt;. Assuming any one of the three is present is where real incidents begin [@rfc9180].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every non-goal above is a sentence a designer might wrongly assume was a yes. So the honest failure catalog for HPKE is not a list of broken math -- there is none -- it is a list of those wrong assumptions, made in real code. Here they are, named.&lt;/p&gt;
&lt;h2&gt;11. The Failure and Misuse Catalog&lt;/h2&gt;
&lt;p&gt;HPKE itself has no known catastrophic real-world break, and that absence &lt;em&gt;is&lt;/em&gt; the finding, not a gap in the research. This is a deliberately hedged absence-of-evidence claim: it rests on three machine-checked analyses and on the public advisories to date being implementation bugs, not design flaws -- never a claim that HPKE is &quot;proven secure&quot; for all time [@lipp20], [@abhklr20], [@psk1480]. The catalog is two things: the predecessor ECIES defects HPKE fixed, and the misuse patterns that appear when a non-goal is assumed away.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Non-goal assumed away&lt;/th&gt;
&lt;th&gt;Fix or lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Sequence counter overflow&lt;/td&gt;
&lt;td&gt;Fixed-width counter wraps, nonce repeats&lt;/td&gt;
&lt;td&gt;AEAD nonce uniqueness is the caller&apos;s job&lt;/td&gt;
&lt;td&gt;Wide counter; single-shot; the hpke-rs &lt;code&gt;u64&lt;/code&gt; fix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static-key forward-secrecy illusion&lt;/td&gt;
&lt;td&gt;Expecting past secrecy from a static key&lt;/td&gt;
&lt;td&gt;No FS against recipient compromise&lt;/td&gt;
&lt;td&gt;Ratchet or rotate keys above HPKE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Low-entropy PSK&lt;/td&gt;
&lt;td&gt;Guessable PSK enables partitioning oracle&lt;/td&gt;
&lt;td&gt;AEADs are not key-committing&lt;/td&gt;
&lt;td&gt;PSK with at least 32 bytes of entropy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bad ephemeral randomness&lt;/td&gt;
&lt;td&gt;Weak CSPRNG in &lt;code&gt;Encap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HPKE assumes good randomness&lt;/td&gt;
&lt;td&gt;Use a vetted CSPRNG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth&lt;/code&gt; treated as a signature&lt;/td&gt;
&lt;td&gt;Insider can forge to a third party&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auth&lt;/code&gt; is signcryption, not non-repudiation&lt;/td&gt;
&lt;td&gt;Use a digital signature for authorship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Out-of-order or reused &lt;code&gt;Open&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ordering not enforced by HPKE&lt;/td&gt;
&lt;td&gt;No reordering or replay protection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Seal&lt;/code&gt; in order; put ordering in the AAD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export-only misuse&lt;/td&gt;
&lt;td&gt;Calling &lt;code&gt;Seal&lt;/code&gt; on an export-only suite&lt;/td&gt;
&lt;td&gt;Interface contract not enforced&lt;/td&gt;
&lt;td&gt;Error, do not truncate; the hpke-rs fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The first advisory: &lt;code&gt;hpke-rs&lt;/code&gt;, GHSA-g433-pq76-6cmf.&lt;/strong&gt; In February 2026 the &lt;code&gt;hpke-rs&lt;/code&gt; and &lt;code&gt;hpke-rs-rust-crypto&lt;/code&gt; crates received a high-severity advisory bundling four fixes, and the reason it vindicates this article&apos;s spine rather than refuting it is that every fix maps onto a non-goal RFC 9180 had already flagged [@ghsa], [@rfc9180], [@hpke_rs]:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sequence-number overflow leading to nonce reuse.&lt;/strong&gt; The context counter could overflow; the fix widened it to a 64-bit integer. This is exactly the scenario the earlier structural model wraps in miniature, and exactly the AEAD nonce-uniqueness hazard the usage section warned about. It was reported by Kobeissi.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export-only misuse and silent truncation.&lt;/strong&gt; Calling &lt;code&gt;Seal&lt;/code&gt;/&lt;code&gt;Open&lt;/code&gt; on an export-only suite, and requesting an over-long &lt;code&gt;Export&lt;/code&gt;, previously truncated silently; both now return errors. Reported by Arciszewski.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing X25519 zero and low-order key check.&lt;/strong&gt; The library did not reject all-zero keys; it now errors on them, closing a small-subgroup foot-gun.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A post-quantum KEM-identifier mapping bug.&lt;/strong&gt; A confusion in the draft X-Wing KEM-id mapping -- harmless in effect, but a foretaste of the ciphersuite-management care the post-quantum transition will demand.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The second advisory: &lt;code&gt;@hpke/core&lt;/code&gt; / &lt;code&gt;hpke-js&lt;/code&gt;, CVE-2025-64767.&lt;/strong&gt; Three months earlier, in November 2025, the &lt;code&gt;@hpke/core&lt;/code&gt; package -- the core of the &lt;code&gt;hpke-js&lt;/code&gt; library in the survey below -- drew a &lt;em&gt;critical&lt;/em&gt; advisory (GHSA-73g8-5h73-26h4, CVSS 9.1) for reusing AEAD nonces. The root cause was a concurrency race rather than a counter overflow: the public &lt;code&gt;SenderContext&lt;/code&gt; &lt;code&gt;Seal()&lt;/code&gt; API let two concurrent asynchronous &lt;code&gt;seal()&lt;/code&gt; calls read the same sequence number, so both derived the same nonce and reused it in the AEAD, forfeiting confidentiality and integrity outright. Versions &lt;code&gt;@hpke/core&lt;/code&gt; 1.7.4 and earlier are affected; the fix shipped in 1.7.5. The lesson is the misuse table&apos;s first row reached from a new direction -- nonce uniqueness is the caller&apos;s job -- so a single &lt;code&gt;Context&lt;/code&gt; must never have &lt;code&gt;Seal&lt;/code&gt; called on it concurrently: serialize per-context sealing, or prefer single-shot. It is a second, independent instance of the exact nonce-reuse hazard the usage section named, arrived at by a race instead of an overflow [@hpke_core_cve], [@rfc9180], [@hpke_js].&lt;/p&gt;
&lt;p&gt;Notice what is &lt;em&gt;not&lt;/em&gt; on either advisory: no broken cipher, no discrete-log break, no flaw in the KEM-DEM composition -- only dropped or mishandled checks and a concurrency race, each mapping onto a non-goal or requirement the specification had already named.&lt;/p&gt;
&lt;p&gt;The rest of the catalog is misuse in application code, and it maps onto the table above. Expecting &lt;strong&gt;forward secrecy from a static recipient key&lt;/strong&gt; yields none, in any mode [@rfc9180]. A &lt;strong&gt;low-entropy PSK&lt;/strong&gt; turns &lt;code&gt;mode_psk&lt;/code&gt; into a partitioning oracle, so the PSK needs at least 32 bytes of entropy [@rfc9180], [@lgr20]. &lt;strong&gt;&lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;Bad ephemeral randomness&lt;/a&gt;&lt;/strong&gt; can collapse base-mode confidentiality outright, because a predictable ephemeral key undoes the encapsulation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Treating &lt;code&gt;auth&lt;/code&gt; as a signature&lt;/strong&gt; walks into the insider-versus-outsider trap [@abhklr20], and &lt;strong&gt;out-of-order &lt;code&gt;Open&lt;/code&gt; or context reuse&lt;/strong&gt; breaks because applications, not HPKE, must preserve &lt;code&gt;Seal&lt;/code&gt; order and put ordering in the associated data [@rfc9180]. The predecessor ECIES defects -- benign malleability, missing proofs, missing test vectors -- round out the list, each already closed by the four design decisions.&lt;/p&gt;

HPKE has no catastrophic break because the risk moved up a layer. Master the four modes and the non-goals and you have a boring, safe default. The math held; the risk moved to the interface.
&lt;p&gt;Read the catalog back to front and a single pattern falls out: every entry is a non-goal treated as a goal, or an implementation dropping a check the specification demanded. None is a broken cipher. That means the &quot;how to use HPKE safely&quot; guide is not a research problem. It is the non-goals, turned into rules.&lt;/p&gt;
&lt;h2&gt;12. A Practical Decision Guide&lt;/h2&gt;
&lt;p&gt;Everything above collapses into a short decision procedure and a shorter list of things never to do.&lt;/p&gt;

flowchart TD
    START[&quot;Encrypt to a public key&quot;] --&amp;gt; Q1{&quot;New protocol needing agility, modes, proofs?&quot;}
    Q1 --&amp;gt;|Yes| HPKE[&quot;Use HPKE&quot;]
    Q1 --&amp;gt;|No| Q2{&quot;Both ends yours, zero knobs?&quot;}
    Q2 --&amp;gt;|Yes| BOX[&quot;libsodium sealed box&quot;]
    Q2 --&amp;gt;|No| Q3{&quot;JWE or JWT stack?&quot;}
    Q3 --&amp;gt;|Yes| JOSE[&quot;ECDH-ES, or COSE-HPKE as it matures&quot;]
    Q3 --&amp;gt;|No| Q4{&quot;Encrypting files?&quot;}
    Q4 --&amp;gt;|Yes| AGE[&quot;age&quot;]
    Q4 --&amp;gt;|No| ECIES[&quot;Matched ECIES, legacy interop only&quot;]
    HPKE --&amp;gt; M{&quot;Which mode?&quot;}
    M --&amp;gt;|Only the recipient key| BASE[&quot;mode_base&quot;]
    M --&amp;gt;|A shared secret exists| PSK[&quot;mode_psk, PSK at least 32 bytes&quot;]
    M --&amp;gt;|Need a sender identity| AUTH[&quot;mode_auth&quot;]
    M --&amp;gt;|Both| AUTHPSK[&quot;mode_auth_psk&quot;]
&lt;p&gt;&lt;strong&gt;Which building block?&lt;/strong&gt; A new protocol or standard that needs agility, proofs, and modes takes HPKE. Both ends under your control with no options wanted takes a libsodium sealed box. The JWE and JWT world takes &lt;code&gt;ECDH-ES&lt;/code&gt;, or COSE-HPKE as it matures [@cose_hpke]. Encrypting files takes &lt;code&gt;age&lt;/code&gt;. Interop with an existing ECIES deployment takes a matched ECIES configuration and nothing new [@rfc9180].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which HPKE mode?&lt;/strong&gt; Only the recipient&apos;s public key in hand: &lt;code&gt;mode_base&lt;/code&gt;, the deployment default. A pre-shared secret already exists: &lt;code&gt;mode_psk&lt;/code&gt;, with a PSK carrying at least 32 bytes of entropy. The recipient must know that a specific sender key sealed the message: &lt;code&gt;mode_auth&lt;/code&gt;. Both a sender identity and a shared secret: &lt;code&gt;mode_auth_psk&lt;/code&gt;. Never reach for an authenticated mode expecting a third-party-verifiable signature.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Which ciphersuite?&lt;/strong&gt; The interop-safe classical default is DHKEM(X25519, HKDF-SHA256) with HKDF-SHA256 and AES-128-GCM, substituting ChaCha20-Poly1305 where AES is unaccelerated. If you only ever export, use the export-only AEAD (&lt;code&gt;0xFFFF&lt;/code&gt;). If you are planning for quantum resistance, track the X-Wing and ML-KEM drafts, budget roughly 1.1 KB per encapsulation, and remember that the post-quantum suites give you &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;psk&lt;/code&gt; but not yet &lt;code&gt;auth&lt;/code&gt; [@xwing_draft06].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A worked path to shipping.&lt;/strong&gt; Start with a single-shot &lt;code&gt;Seal&lt;/code&gt;/&lt;code&gt;Open&lt;/code&gt; for one message, which removes the entire counter-management problem. Reach for &lt;code&gt;Export&lt;/code&gt; only when you are keying another protocol&apos;s framing. And before you trust your integration, reproduce one of RFC 9180&apos;s published test vectors end to end: if your library produces the specified bytes, you have proof it interoperates -- the very check the ECIES era could not run. The mature libraries below make this easy.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;OpenSSL 3.2+&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Native HPKE via &lt;code&gt;OSSL_HPKE_CTX&lt;/code&gt; [@openssl_blog], [@openssl_api]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BoringSSL&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;HPKE used in the Chrome and ECH stack [@boringssl]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare CIRCL&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;Early production HPKE; powers OHTTP and ODoH [@cloudflare]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rust-hpke&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;RFC 9180-conformant crate [@rust_hpke]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hpke-rs&lt;/td&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;Formally oriented; ships a draft X-Wing KEM id [@hpke_rs]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hpke-js&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;Browser, Node.js, and Deno; full RFC 9180 suites; require &lt;code&gt;@hpke/core&lt;/code&gt; 1.7.5 or later (fixes CVE-2025-64767) [@hpke_js], [@hpke_core_cve]&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; Use &lt;code&gt;mode_base&lt;/code&gt; single-shot with DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM -- or ChaCha20-Poly1305 without AES hardware. Use &lt;code&gt;Export&lt;/code&gt; to key your own framing rather than looping &lt;code&gt;Seal&lt;/code&gt;. Track X-Wing and ML-KEM for the post-quantum transition, and budget about 1.1 KB per encapsulation when it lands [@rfc9180], [@xwing_draft06].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The list of nevers is short and worth memorizing: never expect forward secrecy from a static recipient key; never treat &lt;code&gt;auth&lt;/code&gt; as a signature; never reuse a &lt;code&gt;Context&lt;/code&gt; across independent streams or let the sequence counter overflow; never use a low-entropy PSK; never open one ciphertext under multiple keys and assume it committed to one; and never skip your library&apos;s test-vector check.&lt;/p&gt;

Run your library against RFC 9180&apos;s Appendix A vectors for the exact ciphersuite you ship. A mismatch on the first byte tells you -- before any traffic flows -- that your point encoding, your `info` handling, or your KDF wiring disagrees with the standard. This is the single check the four ECIES standards could never offer, and it is the reason &quot;conformant&quot; now implies &quot;interoperable.&quot;
&lt;p&gt;The checklist is short because the lesson is one sentence. Before restating it, clear away the confident-but-wrong beliefs that keep regenerating the misuse catalog in code review.&lt;/p&gt;
&lt;h2&gt;13. The Misconceptions, Corrected&lt;/h2&gt;
&lt;p&gt;HPKE gets misused mostly because a handful of confident, wrong sentences keep getting said in design meetings. Here they are, corrected.&lt;/p&gt;


No -- not with respect to recipient compromise, in any mode. RFC 9180 states that HPKE ciphertexts are not forward secret against recipient compromise, because only long-term secrets are used on the recipient&apos;s side [@rfc9180]. Forward secrecy has to come from a ratchet or key rotation layered above HPKE, which is exactly what MLS and TLS do.


No. `auth` mode is signcryption, not a third-party-verifiable signature: the recipient cannot prove authorship to anyone else, for the reason the modes section works through. If you need portable proof of who signed, use a digital signature [@abhklr20].


A `Context` is stateful: an incrementing sequence number derives each per-message nonce. Seal strictly in order, never reuse a context across independent streams, respect the per-ciphersuite AEAD message-count limit, and never let the sequence counter overflow -- any of those repeats a nonce, which is catastrophic [@rfc9180]. For independent messages, prefer single-shot `Seal`/`Open` and avoid the bookkeeping entirely.


Not by default -- DHKEM is classical-only. The migration path is to drop a hybrid KEM such as X-Wing (X25519 with ML-KEM-768) into the KEM slot, secure if either component holds [@xwing], [@fips203]. Those suites are still draft-stage, add roughly 1.1 KB per encapsulation, and are not authenticated KEMs yet, so they cover `base` and `psk` but not `auth` [@xwing_draft06].


`crypto_box` is one fixed suite (X25519, XSalsa20, Poly1305), deliberately repudiable and mutually authenticated, with manual nonce handling for the raw API [@nacl]. It is excellent when you own both ends and want no options. HPKE wins for new protocol design, because it brings algorithm agility, four formal modes, machine-checked proofs, and test vectors [@rfc9180].


No -- PQ3 is KEM-based but not HPKE; it is a ratcheted, interactive scheme rather than single-shot encryption, as the earlier PQ3 aside details [@apple_pq3]. Do not list it as an HPKE deployment.


Only if you use low-entropy pre-shared keys or open one ciphertext under multiple candidate keys. HPKE&apos;s AEADs are not key-committing by default, which is why a weak PSK enables a partitioning oracle; keep the PSK entropy at 32 bytes or more [@rfc9180], [@lgr20]. A committing AEAD, whose properties are codified in RFC 9771, may eventually arrive as a new `aead_id` [@aead_props].

&lt;p&gt;Every correction points at the same root: the primitive was never the risk. It is time to say the sentence the whole article was built to earn.&lt;/p&gt;
&lt;h2&gt;14. The Math Held; The Risk Moved to the Interface&lt;/h2&gt;
&lt;p&gt;Return to where this started: an elementary task -- encrypt to a public key -- that the industry got subtly wrong four incompatible times, with no proofs and no shared test vectors, one standard shrugging off a second valid ciphertext as &quot;benign&quot; [@sec1], [@maea10]. HPKE is what that task looks like when it is finally done right.&lt;/p&gt;
&lt;p&gt;The evidence has accumulated into a single claim. HPKE is one KEM-DEM construction: a Key Encapsulation Mechanism generates a shared secret and its encapsulation, an HKDF key schedule with every public key bound in derives the working keys, and an AEAD seals the payload.&lt;/p&gt;
&lt;p&gt;It is proven secure against adaptive chosen-ciphertext attack under standard assumptions, machine-checked across all four modes, test-vectored per ciphersuite, and it designs benign malleability out by making the public-key binding mandatory rather than optional [@rfc9180], [@cs01], [@lipp20], [@abhklr20]. The consequence is the whole thesis: HPKE has no known catastrophic break, so the discipline it demands is not &quot;how do I make it secure&quot; but &quot;do I know its four modes and its non-goals?&quot;&lt;/p&gt;
&lt;p&gt;Every entry in the misuse catalog proved the point. Forward secrecy expected from a static key, a signature expected from &lt;code&gt;auth&lt;/code&gt; mode, safety expected from a non-committing AEAD, a fresh nonce expected from an overflowed counter -- each was a non-goal assumed away, not a broken cipher [@rfc9180], [@ghsa].&lt;/p&gt;
&lt;p&gt;And the post-quantum migration is the architecture&apos;s final vindication: the one primitive-level gap, quantum resistance, is closed by swapping the KEM for a hybrid like X-Wing, not by redesigning the scheme [@xwing]. That is the exact payoff of having abstracted the KEM in the first place. It echoes this series&apos; running lesson from the KEM-DEM and digital-signature parts: the math held, and the interface is where the work -- and the risk -- actually lives.&lt;/p&gt;
&lt;p&gt;So carry one question out of this article and apply it to any encrypt-to-a-public-key design you review.&lt;/p&gt;

Which of the four modes is this, and which of HPKE&apos;s non-goals am I about to assume it has?
&lt;p&gt;The primitive was never the weak link, and the post-quantum swap shows it never will be. That is why this is Part 19 of a field guide to &lt;em&gt;protocol design&lt;/em&gt;, not a chapter about any one cipher. HPKE is the clean seam where the primitives of the earlier parts -- authenticated encryption, key derivation, elliptic curves, KEM-DEM, signatures -- become a protocol. The math is finished, the vectors are published, and the only thing left to get wrong is you.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;hpke-rfc9180-kem-based-hybrid-public-key-encryption&quot; keyTerms={[
  { term: &quot;HPKE&quot;, definition: &quot;The RFC 9180 standard for encrypting to a public key, built from a KEM, a KDF, and an AEAD named together as a ciphersuite.&quot; },
  { term: &quot;KEM&quot;, definition: &quot;A key encapsulation mechanism: Encap generates a fresh shared secret and its encapsulation from the recipient&apos;s public key; you never choose the key.&quot; },
  { term: &quot;DEM&quot;, definition: &quot;The data encapsulation mechanism: an AEAD that bulk-encrypts the payload under a key derived from the KEM&apos;s shared secret.&quot; },
  { term: &quot;DHKEM&quot;, definition: &quot;HPKE&apos;s Diffie-Hellman KEM, using an ephemeral-static exchange and binding the encapsulation and recipient key into the derivation.&quot; },
  { term: &quot;Key schedule&quot;, definition: &quot;HPKE&apos;s HKDF process: Extract a pseudorandom key from the shared secret, then Expand it over mode, psk_id_hash, and info_hash to yield the AEAD key, base_nonce, and exporter_secret.&quot; },
  { term: &quot;AEAD&quot;, definition: &quot;Authenticated encryption with associated data: confidentiality and integrity in one primitive; the DEM half of HPKE.&quot; },
  { term: &quot;IND-CCA2&quot;, definition: &quot;Security against adaptive chosen-ciphertext attack; the bar HPKE meets and that benign malleability violates.&quot; },
  { term: &quot;Benign malleability&quot;, definition: &quot;The ECIES defect where an attacker forges a second valid ciphertext for the same plaintext; HPKE kills it by binding all public keys into the key schedule.&quot; },
  { term: &quot;Signcryption&quot;, definition: &quot;Combined encryption and sender authentication with insider/outsider guarantees; HPKE&apos;s auth modes are signcryption, not third-party signatures.&quot; },
  { term: &quot;Key commitment&quot;, definition: &quot;An AEAD property binding a ciphertext to one key; HPKE is not committing by default, enabling partitioning oracles against low-entropy PSKs.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>hpke</category><category>rfc-9180</category><category>kem-dem</category><category>public-key-encryption</category><category>applied-cryptography</category><category>post-quantum</category><category>aead</category><author>noreply@paragmali.com (Parag Mali)</author></item><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 Math Held; The Interface Leaked: A Field Guide to Digital Signatures</title><link>https://paragmali.com/blog/the-math-held-the-interface-leaked-a-field-guide-to-digital-/</link><guid isPermaLink="true">https://paragmali.com/blog/the-math-held-the-interface-leaked-a-field-guide-to-digital-/</guid><description>ECDLP and RSA held; the nonce, the canonical form, and the verifier broke instead. A field guide to ECDSA, EdDSA, RSA-PSS, determinism, and malleability.</description><pubDate>Sun, 12 Jul 2026 17:41:17 GMT</pubDate><content:encoded>
**A signature scheme&apos;s hardness assumption almost never breaks in the field.** The elliptic-curve discrete logarithm and RSA have held for decades. What breaks instead are the three things the scheme hands the caller: the per-signature **nonce**, the signature&apos;s **canonical form**, and the **verifier&apos;s acceptance rule**.&lt;p&gt;ECDSA outsources all three, and the same untouched-math scheme produced four categorically different disasters: a reused nonce that gave up the PlayStation 3 master key from two signatures [@ps3-27c3-2010], a biased nonce that recovered a PuTTY key in about sixty (a &lt;em&gt;statistical&lt;/em&gt; attack: one few-bit-biased signature does &lt;strong&gt;not&lt;/strong&gt; leak your key) [@cve-2024-31497], a malleable &lt;code&gt;(r, n-s)&lt;/code&gt; that let anyone alter a Bitcoin transaction id [@bip-340], and a verifier that accepted &lt;code&gt;(0, 0)&lt;/code&gt; for any message (&quot;Psychic Signatures&quot;) [@cve-2022-21449].&lt;/p&gt;
&lt;p&gt;The history since ECDSA is one sustained move to pull each surface back &lt;em&gt;inside&lt;/em&gt; the scheme: RFC 6979 derives the nonce deterministically, hedged signing mixes a little randomness back in to survive fault attacks, and Ed25519 and BIP-340 Schnorr make deterministic nonces, a fixed encoding, and a byte-exact verifier native by design -- the clean scheme that sat patent-frozen from 1989 until 2008.&lt;/p&gt;
&lt;p&gt;RSA-PSS answers the same &quot;form&quot; question on the RSA track with the tightest proof of any deployed signature. In 2026 these coexist by niche, and every one of them falls to Shor. Signing safely is a stack: the right scheme, a nonce you need not trust the RNG for, a canonical form, and a strict verifier. The math held; the interface leaked.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h2&gt;1. Two Signatures, One Nonce, and the Master Key Fell Out&lt;/h2&gt;
&lt;p&gt;In December 2010, at the 27th Chaos Communication Congress, a group called fail0verflow put a single slide on the screen that ended the security of the PlayStation 3 [@ps3-27c3-2010]. Sony had signed every piece of PS3 firmware with ECDSA, a scheme whose security rests on a discrete logarithm no one has ever solved for a 256-bit curve.&lt;/p&gt;
&lt;p&gt;Sony&apos;s engineers did not need to fail. They reused the same secret random number -- the per-signature nonce -- for every signature, and any two signatures that share a nonce give up the private key by grade-school algebra. The curve was pristine. The master key fell out of two subtractions.&lt;/p&gt;
&lt;p&gt;Twelve years later, the failure jumped one layer up the stack. Java shipped an ECDSA &lt;em&gt;verifier&lt;/em&gt; that accepted the signature &lt;code&gt;(r, s) = (0, 0)&lt;/code&gt; for any message and any public key -- &quot;Psychic Signatures,&quot; CVE-2022-21449, a CVSS 7.5 flaw that forged TLS handshakes, signed JWTs, SAML assertions, and WebAuthn responses alike [@cve-2022-21449], [@neil-madden-2022]. One break leaked a private key through the &lt;em&gt;nonce&lt;/em&gt;; the other waved a forgery through at the &lt;em&gt;verifier&lt;/em&gt;. Neither touched the underlying hardness assumption. Nobody solved a discrete log. Nobody factored a modulus.&lt;/p&gt;
&lt;p&gt;That is the pattern this article is about, worth stating as a question you can carry through every section: &lt;strong&gt;when a signer or verifier meets a value it did not create, what secret does it leak, or what forgery does it wave through?&lt;/strong&gt; The answer is almost never &quot;the math was weak.&quot; A signature scheme is only as safe as three things it hands the caller and refuses to decide for you: the per-signature &lt;strong&gt;nonce&lt;/strong&gt;, the signature&apos;s &lt;strong&gt;canonical form&lt;/strong&gt;, and the &lt;strong&gt;verifier&apos;s acceptance rule&lt;/strong&gt;. ECDSA outsources all three at once.&lt;/p&gt;
&lt;p&gt;That is why one scheme -- its elliptic-curve discrete logarithm fully intact -- spawned four categorically different disasters: the reused nonce of the PS3, the drained Android Bitcoin wallets, the malleable transactions that unsettled Bitcoin, and the psychic verifier in Java. You half-know these headlines already. This is the one sentence underneath all of them.&lt;/p&gt;

The math held; the interface leaked.
&lt;p&gt;The rest of signature history since ECDSA is one effort to pull those three surfaces back &lt;em&gt;inside&lt;/em&gt; the scheme, so the caller can no longer get them wrong. To see why a shared random number hands over a private key -- and why &quot;just use Ed25519&quot; is a real answer but not the whole one -- you first have to be precise about what a signature promises, and the three jobs ECDSA quietly delegates to you.&lt;/p&gt;
&lt;h2&gt;2. What a Signature Actually Is (and the Three Things ECDSA Hands You)&lt;/h2&gt;
&lt;p&gt;Everyone knows the one-liner: sign with the private key, verify with the public key. That one-liner hides three decisions the scheme refuses to make for you. Start with the part that is simple.&lt;/p&gt;
&lt;p&gt;A digital signature scheme is three algorithms. &lt;code&gt;KeyGen -&amp;gt; (pk, sk)&lt;/code&gt; produces a public verification key and a private signing key. &lt;code&gt;Sign(sk, m) -&amp;gt; sigma&lt;/code&gt; turns a message and the private key into a signature. &lt;code&gt;Verify(pk, m, sigma) -&amp;gt; {accept, reject}&lt;/code&gt; lets anyone holding the public key check the pair. The word &lt;em&gt;anyone&lt;/em&gt; is the whole point.&lt;/p&gt;
&lt;p&gt;A message authentication code also proves a message is authentic, but it verifies with the same secret used to produce it, so only insiders can check it. A signature is &lt;strong&gt;publicly verifiable&lt;/strong&gt; and therefore &lt;strong&gt;non-repudiable&lt;/strong&gt;: the entire world can confirm it, and only the holder of &lt;code&gt;sk&lt;/code&gt; could have produced it.&lt;/p&gt;

A triple of algorithms: KeyGen produces a public key and a private key; Sign maps a private key and a message to a signature; Verify maps a public key, a message, and a signature to accept or reject. Unlike a message authentication code, it is publicly verifiable and non-repudiable, since verification needs only the public key.
&lt;p&gt;Public verifiability is what makes a signature strictly stronger than a MAC, and strictly slower. A MAC can rest on a single shared secret and a fast keyed hash. A signature cannot rest on any shared secret, because the verifier must be the whole world, so it has to be built on a public-key hardness assumption -- and pays for it in both compute and signature size.&lt;/p&gt;
&lt;h3&gt;The bar every scheme is measured against&lt;/h3&gt;
&lt;p&gt;&quot;Secure&quot; needs a precise meaning, and cryptographers settled on one in 1988. Goldwasser, Micali, and Rivest defined &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;&lt;strong&gt;existential unforgeability under adaptive chosen-message attack&lt;/strong&gt;&lt;/a&gt;, or EUF-CMA: an adversary who may ask you to sign any messages it likes, adaptively, still cannot produce a valid signature on a single &lt;em&gt;new&lt;/em&gt; message it never queried [@gmr-1988]. That is a demanding bar, and every scheme in this article is designed to clear it.&lt;/p&gt;
&lt;p&gt;But notice the exact word: a new &lt;em&gt;message&lt;/em&gt;. EUF-CMA says nothing about whether there might be more than one valid signature on a message you &lt;em&gt;did&lt;/em&gt; sign. Tighten the game by one word and you get &lt;strong&gt;strong unforgeability&lt;/strong&gt;, SUF-CMA: the adversary cannot produce any new valid &lt;em&gt;signature&lt;/em&gt;, even on an already-signed message. The gap between &quot;new message&quot; and &quot;new signature&quot; looks pedantic. It is the entire malleability story that unravels a Bitcoin transaction in a later section, so it is worth planting the seed now.&lt;/p&gt;

The standard security goal for signatures: after obtaining signatures on messages of its choice, an adversary still cannot produce a valid signature on any message it did not query. It forbids forging a new message but says nothing about producing a second signature on an old one.

A stronger goal that forbids even a new signature on an already-signed message. The difference from EUF-CMA is one word -- new message versus new signature -- and it is exactly the property that signature malleability violates.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; EUF-CMA does not promise there is only one valid signature per message. A scheme can be perfectly unforgeable in the textbook sense and still let anyone manufacture a second, different signature on a message you already signed. Whether that matters depends on whether your protocol ever treats the signature itself as an identifier.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The thesis, stated as the scheme&apos;s interface&lt;/h3&gt;
&lt;p&gt;Here is the organizing idea of the field guide. A signature scheme hands the caller three things and quietly makes each one the caller&apos;s responsibility. ECDSA is the worked example that outsources all three.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Surface 1, the nonce.&lt;/strong&gt; To sign, ECDSA needs a fresh secret random number for every signature. Supply a bad one and you leak the private key. &lt;strong&gt;Surface 2, the canonical form.&lt;/strong&gt; A valid ECDSA signature &lt;code&gt;(r, s)&lt;/code&gt; has an evil twin &lt;code&gt;(r, n-s)&lt;/code&gt; that also verifies, so the caller must decide whether those are &quot;the same&quot; signature. &lt;strong&gt;Surface 3, the verifier.&lt;/strong&gt; ECDSA&apos;s acceptance rule involves modular inversions and range checks, and the specification left enough slack that two conformant-looking verifiers can disagree about which signatures are valid. Every deployed break in this article lives in one of these three surfaces.&lt;/p&gt;

flowchart TD
    KG[KeyGen] --&amp;gt; SK[Private key sk]
    KG --&amp;gt; PK[Public key pk]
    M[Message] --&amp;gt; SG[Sign]
    SK --&amp;gt; SG
    N[&quot;Surface 1: the per-signature nonce you must supply&quot;] --&amp;gt; SG
    SG --&amp;gt; SIG[&quot;Signature r and s&quot;]
    SIG --&amp;gt; CF[&quot;Surface 2: is r and s the same as r and n minus s?&quot;]
    CF --&amp;gt; VF[Verify]
    PK --&amp;gt; VF
    M --&amp;gt; VF
    VR[&quot;Surface 3: the byte-exact acceptance rule&quot;] --&amp;gt; VF
    VF --&amp;gt; OUT{&quot;Accept or reject&quot;}
&lt;p&gt;The academic description of ECDSA that stands in for the paywalled ANSI standard spells out this division of labor: the scheme defines the arithmetic and leaves nonce generation, encoding choices, and verifier strictness to the implementer [@johnson-menezes-vanstone-2001], [@ansi-x962-1999]. The cleaner schemes we reach later pull these surfaces back inside the definition. BIP-340 Schnorr, for instance, specifies its verifier down to the byte precisely so the caller cannot get surface 3 wrong [@bip-340].&lt;/p&gt;
&lt;p&gt;The table below is the entire article compressed into one grid: read across each row to watch a surface move from &quot;the caller&apos;s problem&quot; to &quot;the scheme&apos;s problem.&quot;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Plain ECDSA&lt;/th&gt;
&lt;th&gt;RFC 6979 / hedged ECDSA&lt;/th&gt;
&lt;th&gt;Ed25519&lt;/th&gt;
&lt;th&gt;BIP-340 Schnorr&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1. Nonce&lt;/td&gt;
&lt;td&gt;Caller supplies from an RNG&lt;/td&gt;
&lt;td&gt;Pulled inside: derived from key and message&lt;/td&gt;
&lt;td&gt;Pulled inside: native deterministic&lt;/td&gt;
&lt;td&gt;Pulled inside: synthetic and hedged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Canonical form&lt;/td&gt;
&lt;td&gt;Caller must enforce low-&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Caller must enforce low-&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pulled inside: strict &lt;code&gt;S &amp;lt; L&lt;/code&gt; on decode&lt;/td&gt;
&lt;td&gt;Pulled inside: fixed 64-byte form&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Verifier rule&lt;/td&gt;
&lt;td&gt;Caller implements, easy to botch&lt;/td&gt;
&lt;td&gt;Caller implements, easy to botch&lt;/td&gt;
&lt;td&gt;Mostly specified (RFC 8032)&lt;/td&gt;
&lt;td&gt;Pulled inside: specified at the byte level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two of those three surfaces were invented before ECDSA existed: the nonce in 1985, the &quot;form&quot; problem in 1978. To understand why a modern scheme still hands them to you, we have to go back to the moment signatures were only an idea.&lt;/p&gt;
&lt;h2&gt;3. Where Signatures Came From (1976 to 1996)&lt;/h2&gt;
&lt;p&gt;In 1976, two Stanford researchers could describe exactly what a signature must do and had no way to build one. Whitfield Diffie and Martin Hellman&apos;s &quot;New Directions in Cryptography&quot; named the requirement -- a private operation that anyone can verify with public information, giving the world a way to hold a signer to their message -- but it offered no algorithm to realize it [@diffie-hellman-1976]. The problem was posed before it was solved, and the two ways it eventually got solved are the two tracks this whole article runs along.&lt;/p&gt;
&lt;h3&gt;Track A: the trapdoor line&lt;/h3&gt;
&lt;p&gt;Two years later, RSA gave the first concrete construction. Signing is the private-key operation, verifying is the public-key operation, and &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 trapdoor&lt;/a&gt; -- factoring is hard, so inverting the public operation without the private exponent is infeasible -- makes the asymmetry real [@rsa-1978].&lt;/p&gt;
&lt;p&gt;It worked, and it &lt;em&gt;immediately&lt;/em&gt; exhibited surface 2. Textbook RSA, which signs the bare message as $\sigma = m^d \bmod N$, is existentially forgeable: pick any $\sigma$, compute $m = \sigma^e \bmod N$, and you have a valid pair for a message you did not choose but that is still &quot;signed.&quot;&lt;/p&gt;
&lt;p&gt;It is also multiplicatively malleable, because $\sigma_1 \cdot \sigma_2 \bmod N$ is a valid signature on $m_1 \cdot m_2 \bmod N$. The raw trapdoor has no notion of which messages are legitimately structured, so the caller must impose that structure. Getting that structure right, with a proof, took fifteen years and is the subject of a later section.&lt;/p&gt;
&lt;h3&gt;Track B: the discrete-log line&lt;/h3&gt;
&lt;p&gt;In 1985, Taher ElGamal built the first signature whose hardness rests on the discrete logarithm problem rather than factoring [@elgamal-1985]. It also introduced the load-bearing footgun that defines the rest of this article: a &lt;strong&gt;fresh secret nonce&lt;/strong&gt; drawn for every single signature. Learn that nonce, or reuse it, and the private key follows. This is where surface 1 enters the lineage, and DSA and ECDSA inherit it unchanged.&lt;/p&gt;
&lt;h3&gt;The engine and the yardstick&lt;/h3&gt;
&lt;p&gt;Two ideas from the late 1980s frame everything after. In 1986, Fiat and Shamir showed how to turn an interactive identification protocol -- prove you know a secret without revealing it -- into a non-interactive signature, by replacing the verifier&apos;s random challenge with a hash of the commitment and the message [@fiat-shamir-1986]. That transform is the engine underneath Schnorr, DSA, and EdDSA, and it comes with a catch we return to in the limits section: its security is argued in the random-oracle model, treating the hash as an ideal random function.&lt;/p&gt;
&lt;p&gt;Then, in 1988, GMR gave the field its yardstick, EUF-CMA, the definition every later scheme is measured against [@gmr-1988].&lt;/p&gt;

A method that converts an interactive public-coin identification protocol into a non-interactive signature by replacing the verifier&apos;s random challenge with a hash of the commitment and the message. Its security is argued in the random-oracle model, which treats the hash function as an ideal random function.
&lt;h3&gt;The clean template, and the patent that froze it&lt;/h3&gt;
&lt;p&gt;In 1989, Claus-Peter Schnorr published the scheme that, in hindsight, got everything right. The signer commits to a random point $R = kG$, computes a challenge $e = H(R \mathbin{|} m)$ by hashing the commitment and message, and answers with $s = k + ex$, where $x$ is the private key. Verification is a single linear check. That linearity is not a detail; it is the reason the scheme is provable, batchable, aggregatable, and non-malleable, and it is the direct ancestor of EdDSA and BIP-340 [@schnorr-1989-1991].&lt;/p&gt;
&lt;p&gt;There was one problem, and it bent the entire history. Schnorr patented the scheme (US 4,995,082, invented 1989, granted 1991, expired 2008) [@patent-us4995082]. NIST, which needed a royalty-free national standard, could not use it. So the best-behaved discrete-log signature ever designed sat frozen for two decades while the government standardized around something else. The clean scheme was invented first and deployed last.&lt;/p&gt;
&lt;p&gt;Schnorr&apos;s work appears twice in the literature: at CRYPTO &apos;89 as &quot;Efficient Identification and Signatures for Smart Cards,&quot; and in the Journal of Cryptology in 1991 as &quot;Efficient Signature Generation by Smart Cards.&quot; They are the same line of work at two venues; cite either, and do not treat one date as a correction of the other.&lt;/p&gt;

timeline
    title From an idea to a standard, 1976 to 2020
    1976 : Diffie and Hellman name the requirement, no algorithm
    1978 : RSA, the first construction, Track A
    1985 : ElGamal introduces the per-signature nonce, Track B
    1986 : Fiat-Shamir turns identification into signatures
    1989 : Schnorr, clean and linear, but patented until 2008
    1994 : DSA standardized as FIPS 186
    1999 : ECDSA moves DSA onto elliptic curves
    2011 : Ed25519 bakes determinism in
    2013 : RFC 6979 retrofits deterministic nonces
    2020 : BIP-340 Schnorr, byte-exact
&lt;p&gt;With the clean scheme locked behind a patent, the U.S. government needed a signature standard of its own. The choices it made under patent pressure are the reason the worse-behaved scheme shipped to the entire internet first.&lt;/p&gt;
&lt;h2&gt;4. Padding, DSA, and the Fateful Nonce&lt;/h2&gt;
&lt;p&gt;Two tracks, two ways to get it wrong first. RSA had to learn not to sign the bare message. The discrete-log camp had to standardize around a secret it could not afford to leak.&lt;/p&gt;
&lt;h3&gt;Track A cleans up its form&lt;/h3&gt;
&lt;p&gt;The fix for textbook RSA&apos;s forgeability is a rule you will hear again: never sign the bare message. Hash it first, then pad the hash into a full-width, unpredictable representative that fills the modulus. Done right, this kills existential forgery -- an attacker can no longer start from a random $\sigma$ and back out a validly structured message -- and it destroys the multiplicative structure that let signatures be multiplied together.&lt;/p&gt;
&lt;p&gt;The workhorse encoding is PKCS#1 v1.5, a deterministic padding standardized in the early 1990s and still ubiquitous [@rfc-8017]. Full-Domain Hash was the conceptual bridge: hash the message across the entire width of the modulus. And in 1996, Bellare and Rogaway&apos;s Probabilistic Signature Scheme (PSS) added a random salt and, with it, the tightest security proof any deployed signature would ever get [@bellare-rogaway-pss-1996], [@rsa-1978].&lt;/p&gt;
&lt;p&gt;We give PSS its own section later; for now, note only that Track A&apos;s first footgun was the &lt;em&gt;form&lt;/em&gt; of what it signed, and Track A fixed it by structuring that form.&lt;/p&gt;
&lt;h3&gt;Track B standardizes, under patent pressure&lt;/h3&gt;
&lt;p&gt;The discrete-log camp went a different way, and politics shaped the outcome. In 1991 NIST proposed the Digital Signature Algorithm, and in 1994 it became FIPS 186 [@wikipedia-dsa]. DSA is an ElGamal and Schnorr relative, deliberately structured to sidestep both the RSA patent and Schnorr&apos;s patent, credited to David Kravitz and assigned to the NSA [@patent-us5231668]. In dodging the clean patented scheme, DSA inherited ElGamal&apos;s design decision without modification: a fresh secret nonce for every signature.&lt;/p&gt;
&lt;p&gt;Keep the DSA dates distinct: proposed in 1991, standardized as FIPS 186 in 1994. The DSA patent (Kravitz, US 5,231,668) was filed in 1991 and granted in 1993. These four dates get conflated constantly.&lt;/p&gt;
&lt;h3&gt;Track B goes to curves&lt;/h3&gt;
&lt;p&gt;Through the late 1990s, Certicom and the ANSI X9F1 committee moved DSA onto elliptic curves. The &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;elliptic-curve discrete logarithm&lt;/a&gt; is harder per bit than the finite-field version, so ECDSA gets equivalent security from much smaller keys -- 256-bit curves for 128-bit security, against 3072-bit fields for RSA and finite-field DSA. The template was otherwise unchanged, and so was the fateful decision: the &lt;strong&gt;same fresh secret nonce, once per signature&lt;/strong&gt; [@johnson-menezes-vanstone-2001].&lt;/p&gt;
&lt;p&gt;The normative ECDSA standard, ANSI X9.62-1999, is paywalled, so this article cites the open Johnson-Menezes-Vanstone description as its stand-in and never quotes X9.62 text directly. One more scoping note that matters for later decision rules: ECDSA lives on two different families of curves in practice -- the NIST P-curves used across web PKI, and secp256k1, the curve Bitcoin and Ethereum chose. The nonce discipline is identical on both.&lt;/p&gt;
&lt;p&gt;By the end of the 1990s, an enormous share of the world&apos;s authentication rested on Track B: DSA and ECDSA, both carrying a per-signature secret that had to be unique, unpredictable, and never leaked -- and that the &lt;em&gt;caller&lt;/em&gt;, not the scheme, was responsible for generating perfectly, every single time. Surface 1 was loaded and handed over to millions of implementers.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; RSA&apos;s first footgun was the &lt;em&gt;form&lt;/em&gt; of what it signed -- the bare message was forgeable and malleable, and the cure was to structure it. The discrete-log camp&apos;s first footgun was the &lt;em&gt;secret&lt;/em&gt; it signed with -- a per-signature nonce the scheme trusted the caller to get right. Hold both in mind: the rest of the story is these two footguns being pulled, one at a time, back inside the scheme.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The discrete-log camp had built its entire deployed base on a secret the caller had to generate perfectly, every single time. It took exactly one game console, one wallet app, and one SSH client to prove how badly that goes.&lt;/p&gt;
&lt;h2&gt;5. The Nonce Catastrophe: ECDSA in Full&lt;/h2&gt;
&lt;p&gt;Here is the mechanism underneath the PS3 break, the drained Bitcoin wallets, and the 2024 PuTTY disclosure. And here is the precision almost every retelling gets wrong.&lt;/p&gt;
&lt;h3&gt;ECDSA, mechanically&lt;/h3&gt;
&lt;p&gt;Key generation is a private scalar $d$ and its public point $Q = [d]G$, where $G$ generates a group of prime order $n$. To sign a message $m$, ECDSA draws a nonce $k$, forms $R = [k]G$, sets $r = x(R) \bmod n$, and computes&lt;/p&gt;
&lt;p&gt;$$s = k^{-1}\bigl(H(m) + r,d\bigr) \bmod n.$$&lt;/p&gt;
&lt;p&gt;Verification recovers $u_1 = H(m),s^{-1}$ and $u_2 = r,s^{-1}$ and accepts exactly when $x\bigl([u_1]G + [u_2]Q\bigr) \bmod n = r$ [@johnson-menezes-vanstone-2001].Throughout, $H(m)$ is the hash truncated to the leftmost $\text{bitlen}(n)$ bits, not the full digest -- which matters whenever the hash is wider than the order $n$, as with the P-521 curve behind the PuTTY story below. Stare at the signing equation and the danger is obvious.&lt;/p&gt;
&lt;p&gt;The nonce $k$ is the load-bearing secret, and $s$ mixes $k$, the message hash, and the private key $d$ into one linear relation. Anything you learn about $k$ becomes a linear equation in $d$.&lt;/p&gt;

The fresh secret integer that a discrete-log signature draws for each signature, used to form the commitment point. It is the load-bearing secret: because the signature value mixes the nonce with the message hash and the private key, anything an attacker learns about the nonce turns into a linear equation in the private key.
&lt;h3&gt;The repeat case: certain from two signatures&lt;/h3&gt;
&lt;p&gt;Sign two different messages with the &lt;em&gt;same&lt;/em&gt; nonce and the two signatures share the same $r$ (because $r$ depends only on $k$). Subtract the two signing equations and $k$ cancels, leaving a single linear equation you can solve directly for the private key:&lt;/p&gt;
&lt;p&gt;$$d = \frac{s_2,H(m_1) - s_1,H(m_2)}{r,(s_1 - s_2)} \bmod n.$$&lt;/p&gt;
&lt;p&gt;This is not statistical. It is exact, from &lt;em&gt;two&lt;/em&gt; signatures. It is the PlayStation 3: Sony used a constant $k$, so every firmware signature shared an $r$, and two of them were enough [@ps3-27c3-2010]. And it is the 2013 Android Bitcoin thefts: a flaw in the Java &lt;code&gt;SecureRandom&lt;/code&gt; implementation caused the nonce to repeat across signatures generated on affected devices, and real coins drained from wallets whose signing keys were exposed by the collision [@bitcoin-android-2013], [@klyubin-android-2013].&lt;/p&gt;
&lt;p&gt;Keep the PS3 disclosures distinct: fail0verflow presented the constant-nonce ECDSA break at 27C3 in December 2010; George Hotz released a separate PS3 root key in January 2011 [@ps3-jailbreak-wikipedia]. They are related events, not the same one.&lt;/p&gt;
&lt;p&gt;The widely cited figure of roughly 55 BTC lost in the 2013 Android incident comes from secondary reporting; the bitcoin.org primary alert states no loss total. Treat the number as reported, not official.&lt;/p&gt;
&lt;p&gt;The recovery really is grade-school algebra. The demonstration below signs two messages under &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;a reused nonce&lt;/a&gt; on toy-sized parameters, then plays the attacker: given only the public signature values, it recovers the nonce and then the private key.&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;// Illustrative toy sizes, NOT secure. Here r stands in for x([k]G) mod n; // a real attacker reads r straight off the two signatures. const n = 1000003n;                       // small prime &quot;group order&quot; const mod = (a, m) =&amp;gt; ((a % m) + m) % m; function inv(a, m) {                       // modular inverse, extended Euclid   let [or, r] = [mod(a, m), m], [os, s] = [1n, 0n];   while (r !== 0n) { const q = or / r; [or, r] = [r, or - q * r]; [os, s] = [s, os - q * s]; }   return mod(os, m); } // Signer&apos;s secrets (the attacker never sees these): const d = 123456n;                        // private key const k = 654321n;                        // per-signature nonce -- REUSED: the whole bug const r = 42n;                            // r = x([k]G) mod n; same k gives the same r const h1 = 111111n, h2 = 222222n;         // two message hashes // ECDSA signing: s = k^{-1} (h + r*d) mod n const s1 = mod(inv(k, n) * mod(h1 + r * d, n), n); const s2 = mod(inv(k, n) * mod(h2 + r * d, n), n); // Attacker sees only (r, s1, s2, h1, h2). Recover k, then d: const kRec = mod(mod(h1 - h2, n) * inv(mod(s1 - s2, n), n), n); const dRec = mod(mod(s1 * kRec - h1, n) * inv(r, n), n); console.log(&apos;recovered nonce k = &apos; + kRec + &apos;  (true &apos; + k + &apos;)&apos;); console.log(&apos;recovered key   d = &apos; + dRec + &apos;  (true &apos; + d + &apos;)&apos;); console.log(dRec === d ? &apos;PRIVATE KEY RECOVERED from two signatures&apos; : &apos;mismatch&apos;);&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;Two subtractions and two inversions. No discrete logarithm was harmed.&lt;/p&gt;
&lt;h3&gt;The bias case: statistical, and it needs many&lt;/h3&gt;
&lt;p&gt;Now change the failure. Instead of repeating the nonce, suppose the nonce generator is subtly &lt;em&gt;biased&lt;/em&gt; -- say the top few bits are always zero. No two signatures share an $r$, so the two-subtraction trick does not apply, and no single signature hands you the key.&lt;/p&gt;
&lt;p&gt;What you have instead is a stream of signatures that each leak a little information about a secret, which is precisely the &lt;strong&gt;Hidden Number Problem&lt;/strong&gt;: recover a hidden value from many samples that each reveal a few high-order bits. Reformulate the biased signatures as an HNP instance, run lattice reduction, and the private key falls out -- but only after you collect &lt;em&gt;many&lt;/em&gt; signatures.&lt;/p&gt;

The problem of recovering a hidden integer from many samples that each leak only a few of its high-order bits. A stream of biased-nonce signatures is an HNP instance, and lattice reduction solves it. This is why a biased nonce is a statistical attack that needs many signatures rather than a single one.
&lt;p&gt;The canonical modern proof is PuTTY. In April 2024, CVE-2024-31497 disclosed that PuTTY&apos;s ECDSA nonce generator for the NIST P-521 curve was biased so that the first 9 bits of each nonce are zero, enabling full secret key recovery in roughly 60 signatures [@cve-2024-31497]. Sixty. Not two.&lt;/p&gt;
&lt;p&gt;The rigor behind that number is a quarter-century of work: Boneh and Venkatesan introduced the Hidden Number Problem in 1996 [@boneh-venkatesan-1996]; Howgrave-Graham and Smart turned it into the first practical lattice recovery of DSA keys from partial nonces in 2001 [@howgrave-graham-smart-2001]; Nguyen and Shparlinski made ECDSA key recovery from a few leaked nonce bits rigorous in 2003 [@nguyen-shparlinski-2003]; and by 2020, LadderLeak recovered keys with under one bit of nonce leakage per signature [@ladderleak-2020].&lt;/p&gt;
&lt;p&gt;Bleichenbacher&apos;s much-cited biased-nonce DSA attack has no standalone peer-reviewed paper; it circulated through talks and other authors&apos; write-ups. Cite the mechanism through the Nguyen-Shparlinski and LadderLeak lattice literature, not a fabricated Bleichenbacher reference.&lt;/p&gt;
&lt;h3&gt;The precision that almost every retelling gets wrong&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A repeated nonce leaks the private key with certainty from just two signatures. A few-bit-biased nonce is different: a statistical lattice attack that needs many signatures, not one. Conflating repeat with bias is the single most corrected misconception in this corner of cryptography.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;Signatures needed&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Certainty&lt;/th&gt;
&lt;th&gt;Canonical example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Repeated nonce&lt;/td&gt;
&lt;td&gt;Two&lt;/td&gt;
&lt;td&gt;Elementary algebra, two linear equations&lt;/td&gt;
&lt;td&gt;Exact, deterministic&lt;/td&gt;
&lt;td&gt;PS3 (2010); Android wallets (2013)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Biased nonce&lt;/td&gt;
&lt;td&gt;Many (tens to thousands)&lt;/td&gt;
&lt;td&gt;Hidden Number Problem, lattice reduction&lt;/td&gt;
&lt;td&gt;Statistical&lt;/td&gt;
&lt;td&gt;PuTTY (roughly 60); LadderLeak (under one bit)&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; A reused nonce is certain from two signatures, and the fix is to never reuse it. A biased nonce is a statistical HNP or lattice attack that needs many signatures, and the fix is unbiased generation or a constant-time implementation. They have different signature counts, different math, and different cures. Do not conflate them, and do not claim that a single few-bit-biased signature leaks the key.&lt;/p&gt;
&lt;/blockquote&gt;

Each biased signature gives a linear relation of the form $k_i \equiv a_i + b_i\,d \pmod n$ where the unknown nonce $k_i$ is known to be small (its top bits are zero). Stack these relations as rows of a lattice whose short vectors correspond to the small nonces. Running LLL or BKZ finds a short vector, which reveals the nonces and hence $d$. The fewer bits each signature leaks, the more signatures and the more lattice-reduction effort you need -- hence &quot;statistical, and it needs many.&quot;
&lt;h3&gt;Two root causes hide under the word &quot;bias&quot;&lt;/h3&gt;
&lt;p&gt;There is a second fork that decides which cure you need. A &lt;em&gt;generation&lt;/em&gt; failure -- a bad RNG that repeats or biases the nonce -- is cured by not depending on the RNG at all, which is the deterministic signing of the next section. But a &lt;em&gt;usage&lt;/em&gt; failure is different: even a perfectly generated nonce leaks if the code that consumes it, the scalar multiplication $[k]G$, runs in time or draws power that depends on the nonce&apos;s bit-length. That is a side channel, and no amount of clever nonce &lt;em&gt;generation&lt;/em&gt; closes it.&lt;/p&gt;
&lt;p&gt;In 2019, both Minerva and TPM-Fail recovered ECDSA keys from exactly this timing leak, then finished the job with the same lattice machinery as the bias case [@minerva-2019], [@tpm-fail-2019]. The cure there is constant-time scalar multiplication.&lt;/p&gt;

flowchart TD
    K[&quot;The per-signature nonce k&quot;] --&amp;gt; G[&quot;Generation failure: how k is produced&quot;]
    K --&amp;gt; U[&quot;Usage failure: how k is consumed&quot;]
    G --&amp;gt; R[&quot;Repeat: same k twice, key from two signatures&quot;]
    G --&amp;gt; B[&quot;Bias: predictable bits, key from many signatures&quot;]
    U --&amp;gt; T[&quot;Timing or EM leak of k during scalar multiplication&quot;]
    R --&amp;gt; C1[&quot;Cure: deterministic nonce, RFC 6979&quot;]
    B --&amp;gt; C1
    T --&amp;gt; C2[&quot;Cure: constant-time scalar multiplication&quot;]
&lt;p&gt;So the random-number generator was the enemy in three of these four stories. The obvious fix is a better RNG. The actual fix, the one the whole field converged on, was to use no randomness at all.&lt;/p&gt;
&lt;h2&gt;6. The Fix for a Randomness Disaster Was Less Randomness&lt;/h2&gt;
&lt;p&gt;The counterintuitive move: if a bad random nonce is what kills you, stop rolling the dice. Derive the nonce deterministically from the one thing that is already secret and the one thing that is already unique.&lt;/p&gt;
&lt;h3&gt;RFC 6979, the retrofit&lt;/h3&gt;
&lt;p&gt;Thomas Pornin&apos;s RFC 6979 does exactly that. It derives the nonce as a deterministic function of the private key and the message hash, driving an HMAC-based deterministic random bit generator so that signing consumes &lt;em&gt;no&lt;/em&gt; runtime entropy. &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;A broken system RNG&lt;/a&gt; can no longer cause a repeat or a generation bias, because there is no RNG in the signing path [@rfc-6979].&lt;/p&gt;
&lt;p&gt;The elegance is that nothing downstream changes: a deterministic signature verifies under the &lt;em&gt;same&lt;/em&gt; verifier as a randomized one, so this is a drop-in with zero protocol change. The RFC states the goal plainly -- signers &quot;do not need access to a source of high-quality randomness&quot; -- and warns that even slight biases in nonce generation may be turned into attacks, exactly the HNP lineage from the previous section [@rfc-6979].&lt;/p&gt;

Deriving the per-signature nonce as a deterministic function of the private key and the message, via an HMAC-based deterministic random bit generator, so that signing needs no runtime entropy and a failing RNG can neither repeat nor bias the nonce. The resulting signature verifies under the ordinary verifier.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The whole class of PS3 and Android failures came from trusting a random-number generator to produce a perfect nonce. Deterministic signing removes the generator from the signing path entirely. Then the field adds a measured dose of randomness back for a different reason.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;But determinism is not unconditionally safer&lt;/h3&gt;
&lt;p&gt;Determinism trades one threat model for another. A fully deterministic signer is a &lt;em&gt;fixed, replayable target&lt;/em&gt;. Feed it the same key and message twice and it does exactly the same computation, which is the ideal setup for a &lt;strong&gt;differential fault attack&lt;/strong&gt;: run the signer twice, glitch one of the two runs with a voltage or clock disturbance, and diff the correct output against the faulted one to recover the key.&lt;/p&gt;
&lt;p&gt;Poddebniak and colleagues demonstrated this against deterministic signature schemes in 2018 [@poddebniak-fault-2018]. NIST took the point: FIPS 186-5 approves deterministic ECDSA yet explicitly flags deterministic schemes as being of particular concern for fault attacks [@fips-186-5].&lt;/p&gt;

Randomized signing is vulnerable to RNG failure but resists differential fault attacks, because two signings of the same message differ. Deterministic signing is immune to RNG failure but hands an attacker a replayable target. Neither extreme dominates. Poddebniak et al. showed the fault channel is real, and the FIPS 186-5 warning is the standards body saying so out loud. The resolution is not to pick a side but to combine them.
&lt;h3&gt;The resolution: hedged signing&lt;/h3&gt;
&lt;p&gt;Hedged signing mixes a little fresh randomness &lt;em&gt;back in&lt;/em&gt; on top of the deterministic derivation. RFC 6979&apos;s own section on variants sanctions adding extra input to the nonce derivation, noting that the result is then no longer deterministic (EC)DSA [@rfc-6979]. The payoff is that a hedged signer survives &lt;em&gt;both&lt;/em&gt; failure modes: if the RNG dies, the deterministic core still produces a safe, distinct nonce; if the RNG works, the fresh entropy makes each signing unrepeatable and defeats the differential fault attack.&lt;/p&gt;
&lt;p&gt;This is where modern libraries land by default. Go&apos;s &lt;code&gt;crypto/ecdsa&lt;/code&gt; hedges: its documentation says signatures &quot;are not deterministic, but entropy is mixed with the private key and the message,&quot; and that &quot;if rand is nil, Sign will produce a deterministic signature according to RFC 6979&quot; [@go-crypto-ecdsa]. BoringSSL mixes in additional entropy the same way [@boringssl-ecdsa], and the security analysis of hedged Fiat-Shamir signatures under faults gives this posture a formal footing [@aranha-hedged-fs-2019].&lt;/p&gt;

Mixing a small amount of fresh randomness into the deterministic nonce derivation, so the signer survives both RNG failure and single fault attacks. It is explicitly not deterministic (EC)DSA, and it is the default in libraries such as Go crypto/ecdsa and BoringSSL.
&lt;p&gt;One caution: it fixes only the &lt;em&gt;generation&lt;/em&gt; surface. The &lt;em&gt;usage&lt;/em&gt; leak from the previous section -- the timing side channel during scalar multiplication -- is untouched by how you derive the nonce, and its cure remains constant-time code. Three cures, three distinct threats.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Nonce strategy&lt;/th&gt;
&lt;th&gt;Nonce source&lt;/th&gt;
&lt;th&gt;Survives RNG failure?&lt;/th&gt;
&lt;th&gt;Resists the fault attack?&lt;/th&gt;
&lt;th&gt;Where it lands&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Randomized (classic ECDSA)&lt;/td&gt;
&lt;td&gt;Fresh RNG each signature&lt;/td&gt;
&lt;td&gt;No, the RNG is the whole risk&lt;/td&gt;
&lt;td&gt;Yes, runs differ&lt;/td&gt;
&lt;td&gt;The PS3 and Android failure mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deterministic (RFC 6979)&lt;/td&gt;
&lt;td&gt;HMAC-DRBG of key and message&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No, a replayable target&lt;/td&gt;
&lt;td&gt;Verifies under the ordinary verifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hedged (RFC 6979 variants)&lt;/td&gt;
&lt;td&gt;Deterministic core plus fresh entropy&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Go and BoringSSL default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;RFC 6979 pulled surface 1 back inside the scheme, but only surface 1, and only as a patch bolted onto a design that still leaks the other two. A cleaner scheme would make deterministic nonces, a fixed encoding, and a byte-exact verifier all native. That scheme already existed. It had been waiting on a patent to expire for two decades.&lt;/p&gt;
&lt;h2&gt;7. The Breakthrough: Schnorr Done Right&lt;/h2&gt;
&lt;p&gt;Everything RFC 6979 retrofitted, one scheme had baked in from birth. It was invented in 1989 and could not be deployed until 2008, because it was patented.&lt;/p&gt;
&lt;h3&gt;Why Schnorr is clean&lt;/h3&gt;
&lt;p&gt;Recall Schnorr&apos;s response, $s = k + ex$. That single linear equation is what makes the scheme provable, batchable, and aggregatable. Provable, because the linear structure is what the Pointcheval-Stern forking lemma needs to reduce forgery to the discrete-log problem in the random-oracle model [@pointcheval-stern-1996-2000]. Batchable, because a linear verification equation lets you check many signatures as one combined equation, the subject of a later section. Aggregatable, because linear things add, which is why threshold and multi-signature schemes like FROST and MuSig2 build directly on Schnorr.&lt;/p&gt;
&lt;p&gt;The Fiat-Shamir transform is still the engine turning the underlying identification protocol into a signature; Schnorr is simply the cleanest identification protocol to feed it.&lt;/p&gt;
&lt;h3&gt;EdDSA and Ed25519 pull all three surfaces inside&lt;/h3&gt;
&lt;p&gt;In 2011, Bernstein, Duif, Lange, Schwabe, and Yang published EdDSA, and its edwards25519 instantiation, Ed25519, is the scheme that pulls all three surfaces inside the definition by design [@bernstein-eddsa-2011].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Surface 1, the nonce, is native and deterministic.&lt;/strong&gt; Ed25519 computes the nonce as $r = H(\text{prefix} \mathbin{|} M) \bmod L$, hashing a secret key-derived prefix together with the message. There is no runtime RNG in the signing path, so the entire PS3 and Android failure class is structurally impossible rather than merely discouraged. The designers said so directly, and named the incident:&lt;/p&gt;

Signatures are generated deterministically ... directly relevant to the recent collapse of the Sony PlayStation 3 security system.
&lt;p&gt;&lt;strong&gt;Surface 2, the encoding, is fixed.&lt;/strong&gt; An Ed25519 signature is a fixed 64-byte string, a point encoding followed by a scalar, and RFC 8032 mandates a strict range check that the scalar satisfy $0 \le S &amp;lt; L$ on decode. Enforcing that check makes a strict Ed25519 implementation strongly unforgeable, closing the malleability surface at the encoding level [@rfc-8032].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Surface 3, the arithmetic, is safe by construction.&lt;/strong&gt; edwards25519 uses a complete twisted-Edwards addition law with no exceptional cases, so the constant-time implementation is the natural one, and there are no special points where a naive verifier misbehaves [@bernstein-eddsa-2011]. Determinism here is &lt;em&gt;structural&lt;/em&gt;, not a patch bolted on afterward, which is the real reason &quot;why determinism helps&quot; is a property of the design rather than a library option.&lt;/p&gt;

A Schnorr signature instantiated on a complete twisted-Edwards curve with a key-prefixed deterministic nonce and a fixed encoding. Ed25519 is its edwards25519 instantiation: constant-time by construction, misuse-resistant by design, and strongly unforgeable under a strict verifier.

A nonce derived by hashing a secret key-derived prefix together with the message, so it is deterministic, unique per message, and never drawn from a runtime random-number generator. Hashing the public key into the challenge additionally hardens the scheme in multi-user and related-key settings.
&lt;p&gt;Key-prefixing -- folding the public key into the hash that produces the challenge -- is a small design choice with outsized value: it hardens the scheme against multi-user and related-key attacks that a bare Schnorr challenge would leave open.&lt;/p&gt;

flowchart TD
    M[Message] --&amp;gt; SCH
    SK[Private key] --&amp;gt; SCH
    subgraph SCH[&quot;Inside EdDSA and BIP-340&quot;]
        N[&quot;Surface 1: deterministic key-prefixed nonce&quot;]
        F[&quot;Surface 2: fixed canonical encoding&quot;]
        V[&quot;Surface 3: byte-exact verifier&quot;]
    end
    SCH --&amp;gt; SIG[&quot;One valid signature, no caller choices&quot;]
    SIG --&amp;gt; OK{&quot;Accept or reject, deterministically&quot;}
&lt;h3&gt;BIP-340: Schnorr over secp256k1, with the verifier pulled inside&lt;/h3&gt;
&lt;p&gt;Bitcoin needed Schnorr on its own curve, secp256k1, and in 2020 BIP-340 delivered it: 64-byte signatures, 32-byte x-only public keys, and, most importantly for surface 3, a verifier specified down to the byte. The specification states that the verification algorithm is &quot;completely specified ... at the byte level,&quot; the guarantee whose full payoff we reach when we return to verifier strictness later [@bip-340].&lt;/p&gt;
&lt;p&gt;BIP-340 also adopts the hedged idea natively: it derives a &lt;em&gt;synthetic&lt;/em&gt; nonce and notes that the added randomness is only supplemental to security, so a faulty RNG cannot break it. Bitcoin&apos;s Taproot upgrade deployed it in November 2021 [@optech-taproot-2021].&lt;/p&gt;
&lt;p&gt;Now the punchline. The scheme that makes deterministic nonces, a fixed encoding, and a byte-exact verifier native is Schnorr, invented in 1989. It could not be deployed until its patent expired in 2008, which is exactly why the worse-behaved ECDSA shipped to the entire internet two decades before its clean successor. Ed25519 arrived in 2011, BIP-340 in 2020. The best-behaved design was first on paper and last in production.&lt;/p&gt;
&lt;p&gt;The two-decade gap between the clean scheme&apos;s invention (US 4,995,082, 1989) and its deployment -- Ed25519 in 2011, BIP-340 in 2020 -- is a patent artifact, not a technical one.&lt;/p&gt;
&lt;p&gt;One honest caveat keeps this from being a silver bullet. Plain Ed25519 has no hedging knob: it is purely deterministic, so on hardware where an attacker can inject faults it is the &lt;em&gt;harder-hit&lt;/em&gt; fault target, and hardened embedded implementations re-inject randomness exactly as the hedged-signing analysis recommends [@poddebniak-fault-2018], [@aranha-hedged-fs-2019]. This is coexistence, not a coronation. Ed25519 and BIP-340 close all three surfaces on the discrete-log track. But the other 1976 track, RSA, had its own reckoning with the &lt;em&gt;form&lt;/em&gt; of a signature, and its answer came with something the discrete-log schemes still lack: a tight proof.&lt;/p&gt;
&lt;h2&gt;8. RSA-PSS: Probabilistic Signing with a Tight Proof&lt;/h2&gt;
&lt;p&gt;On the discrete-log track, determinism was the virtue. On the RSA track, the opposite move -- adding randomness -- bought the best security proof any deployed signature has.&lt;/p&gt;
&lt;h3&gt;PSS, mechanically&lt;/h3&gt;
&lt;p&gt;Never sign the bare message. The EMSA-PSS encoding hashes the message, mixes in a fresh random salt, and builds a full-width encoded message before the RSA private-key operation. Concretely, it forms $H = \mathrm{Hash}(\text{padding} \mathbin{|} \mathrm{Hash}(M) \mathbin{|} \text{salt})$, masks the salt-bearing data block against a mask generated from $H$, and assembles the encoded message as maskedDB followed by $H$ followed by the trailer byte &lt;code&gt;0xbc&lt;/code&gt;. The signature is that encoded message raised to the private exponent modulo $n$ [@rfc-8017], [@bellare-rogaway-pss-1996]. Verification recomputes the whole structure and checks it exactly, rejecting anything that does not fit the padding shape.&lt;/p&gt;

flowchart LR
    M[Message] --&amp;gt; MH[&quot;mHash = Hash of M&quot;]
    SALT[&quot;Random salt&quot;] --&amp;gt; H2[&quot;H = Hash of padding, mHash, salt&quot;]
    MH --&amp;gt; H2
    H2 --&amp;gt; DB[&quot;maskedDB = data block xor mask from H&quot;]
    DB --&amp;gt; EM[&quot;EM = maskedDB then H then 0xbc&quot;]
    H2 --&amp;gt; EM
    EM --&amp;gt; SIG[&quot;Signature = EM to the d mod n&quot;]
&lt;h3&gt;Why probabilistic buys a tight proof&lt;/h3&gt;
&lt;p&gt;Bellare and Rogaway proved PSS existentially unforgeable with a &lt;em&gt;tight&lt;/em&gt; reduction to the RSA problem in the random-oracle model: a forger against PSS can be turned into an algorithm that inverts RSA with essentially the same success probability and running time [@bellare-rogaway-pss-1996].&lt;/p&gt;
&lt;p&gt;That word &lt;em&gt;tight&lt;/em&gt; is the whole prize. A &lt;strong&gt;tight&lt;/strong&gt; reduction loses almost nothing between the scheme&apos;s security and the underlying hard problem, so a concrete key size gives you concrete confidence; a &lt;strong&gt;loose&lt;/strong&gt; reduction loses a large factor, so you either accept a weaker guarantee or inflate parameters to compensate. By contrast, PKCS#1 v1.5&apos;s signature padding has only a heuristic argument, not a tight proof. Among deployed signatures, PSS&apos;s guarantee is the gold standard, and it sets up the proof-quality spectrum we return to in the limits section.&lt;/p&gt;

The Probabilistic Signature Scheme encoding for RSA, which injects a fresh random salt into the padding before the RSA private-key operation and has the verifier recompute and check that structure exactly. The salt is what makes its security reduction to the RSA problem tight, unlike the deterministic PKCS#1 v1.5 padding.
&lt;p&gt;Full-Domain Hash was PSS&apos;s provable but loose predecessor: hash the message across the full width of the modulus, then apply the private operation. PSS keeps that idea and adds the random salt, and the salt is precisely what upgrades the reduction from loose to tight.&lt;/p&gt;
&lt;h3&gt;The deployment reality, stated exactly&lt;/h3&gt;
&lt;p&gt;&quot;Is RSA-PSS widely deployed?&quot; has a split answer, and blurring the split is a common error. TLS 1.3 makes RSA-PSS mandatory to implement for the online handshake signature: the &lt;code&gt;CertificateVerify&lt;/code&gt; message uses the &lt;code&gt;rsa_pss_rsae_*&lt;/code&gt; schemes, and a compliant implementation must support them [@rfc-8446]. That is a specification requirement.&lt;/p&gt;
&lt;p&gt;Separately, as a matter of deployment, most Web-PKI &lt;em&gt;certificates&lt;/em&gt; -- the long-lived signatures a certificate authority puts on the certs in the chain -- are still PKCS#1 v1.5. So the handshake signature your browser makes live and 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;certificate-chain signatures&lt;/a&gt; it validates are &lt;em&gt;different&lt;/em&gt; signatures with &lt;em&gt;different&lt;/em&gt; padding, and the honest answer separates them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The handshake CertificateVerify signature is RSA-PSS by TLS 1.3 requirement. The certificate-chain signatures a certificate authority produces are, in practice, still mostly PKCS#1 v1.5. Same connection, two padding schemes, two different signatures. Do not answer &quot;does the web use PSS?&quot; without saying which of the two you mean.&lt;/p&gt;
&lt;/blockquote&gt;

This is the boundary never to blur. PKCS#1 v1.5 *encryption* padding is the classic Bleichenbacher decryption oracle, a genuine break of the encryption scheme, and a companion part in this series on RSA owns that story. PKCS#1 v1.5 *signatures* are a different animal: the scheme is not broken.&lt;p&gt;Its real-world failures were &lt;em&gt;implementation&lt;/em&gt; bugs in lax verifiers -- most famously the low-exponent forgery that a sloppy parser allows when the public exponent is small, and its 2014 re-occurrence known as BERserk. Those are parsing bugs, not a break of the signature math. That low-exponent forgery has no standalone peer-reviewed paper, so cite the mechanism and the secondary write-ups, never a fabricated primary. The practical rule stands regardless: use RSA-PSS for new designs [@rfc-8017].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;h3&gt;The cost profile that decides where PSS fits&lt;/h3&gt;
&lt;p&gt;RSA&apos;s economics are lopsided and they determine where it belongs. Verification uses a tiny public exponent, usually 65537, so it is very fast. Signing uses the full-width private exponent, so it is slow, and the signatures are large: 256 bytes at RSA-2048, 384 bytes at RSA-3072, an order of magnitude bigger than a 64-byte elliptic-curve signature. That makes RSA-PSS excellent where you verify far more often than you sign, such as certificates and firmware images, and painful where you sign constantly. We put real numbers on this in the state-of-the-art section.&lt;/p&gt;
&lt;p&gt;PSS pinned RSA&apos;s &lt;em&gt;form&lt;/em&gt; with a proof. But the discrete-log schemes still had an unresolved question about form, one that let anyone alter a signature without the key and briefly threatened to break Bitcoin.&lt;/p&gt;
&lt;h2&gt;9. Malleability, Canonicalization, and Strong Unforgeability&lt;/h2&gt;
&lt;p&gt;Recall the promise EUF-CMA does &lt;em&gt;not&lt;/em&gt; make: that there is only one valid signature per message. For ECDSA there are always at least two, and the caller was left to notice.&lt;/p&gt;
&lt;h3&gt;Surface 2: ECDSA malleability, mechanically&lt;/h3&gt;
&lt;p&gt;Take any valid ECDSA signature &lt;code&gt;(r, s)&lt;/code&gt;. Now negate the scalar: &lt;code&gt;(r, n - s)&lt;/code&gt; also verifies, and it is a different signature on the &lt;em&gt;same&lt;/em&gt; message, producible by anyone with no private key at all. The reason is visible in the verification equation. Replacing $s$ with $n - s$ is replacing it with $-s \bmod n$, which flips the signs of both $u_1$ and $u_2$, negating the recovered point to $-R$. On these curves a point and its negation share an $x$-coordinate, so $x(-R) \bmod n = x(R) \bmod n = r$, and the check still passes.&lt;/p&gt;
&lt;p&gt;BIP-340 states the fact plainly: &quot;If (r,s) is a valid ECDSA signature then (r,n-s) is also valid&quot; [@bip-340]. So standard ECDSA meets EUF-CMA but not SUF-CMA -- exactly why that one-word distinction was worth planting seven sections ago.&lt;/p&gt;

The existence of a second valid signature on an already-signed message that anyone can derive without the private key. For ECDSA, whenever (r, s) is valid so is (r, n - s). It is the reason plain ECDSA satisfies EUF-CMA but fails SUF-CMA.
&lt;h3&gt;The protocol fallout: Bitcoin transaction malleability&lt;/h3&gt;
&lt;p&gt;Malleability is harmless until a protocol treats the signature as an identifier. Bitcoin did. A transaction id is a hash computed over the whole transaction &lt;em&gt;including its signatures&lt;/em&gt;, so flipping &lt;code&gt;s&lt;/code&gt; to &lt;code&gt;n - s&lt;/code&gt; changes the txid without changing what the transaction does. Anything that referenced an unconfirmed transaction by its id -- a chain of dependent spends, a naive payment tracker -- could be broken by a third party rewriting the signature in flight [@decker-wattenhofer-2014].&lt;/p&gt;
&lt;p&gt;The honest Mt. Gox nuance: transaction malleability was real and was publicly blamed for the 2014 collapse, but Decker and Wattenhofer measured the malleability activity on the network and found it insufficient to account for the losses, and later analysis attributes most of the missing coins to a wallet compromise around 2011. Real and blamed, but not the sole cause.&lt;/p&gt;
&lt;h3&gt;The fix: low-s canonicalization restores SUF-CMA&lt;/h3&gt;
&lt;p&gt;If two signatures differ only by $s$ versus $n - s$, pick one. The &lt;strong&gt;low-s&lt;/strong&gt; rule requires $s \le n/2$, so exactly one of the twins is standard and the other is rejected on sight. Bitcoin moved this from proposal to policy across BIP-62 and BIP-146, and it restores strong unforgeability in the system that needed it most [@bip-62], [@bip-146]. Ed25519&apos;s analogue is the strict &lt;code&gt;S &amp;lt; L&lt;/code&gt; range check that RFC 8032 mandates on decode, which is what makes a strict Ed25519 implementation SUF-CMA [@rfc-8032].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If any part of your protocol hashes over or deduplicates by the signature itself -- transaction ids, caches, idempotency keys, replay checks -- plain EUF-CMA lets an attacker manufacture a second valid signature and break your assumption. Enforce low-s or a strict &lt;code&gt;S &amp;lt; L&lt;/code&gt; range check, or choose a SUF-CMA-by-design scheme such as BIP-340.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Surface 3: the verifier, which closed last and most embarrassingly&lt;/h3&gt;
&lt;p&gt;The third surface failed in two distinct shapes. The first is a &lt;em&gt;permissive&lt;/em&gt; verifier. In 2022, Java&apos;s ECDSA verification accepted the all-zero signature &lt;code&gt;(r, s) = (0, 0)&lt;/code&gt; for any message and any key, because it skipped the range check that forbids zero. &quot;Psychic Signatures,&quot; CVE-2022-21449, scored CVSS 7.5 and forged anything built on Java&apos;s signatures: TLS, signed JWTs, SAML assertions, OIDC id tokens, and WebAuthn responses [@cve-2022-21449], [@neil-madden-2022].&lt;/p&gt;
&lt;p&gt;The second shape is more subtle: &lt;em&gt;honest disagreement&lt;/em&gt;. &quot;Taming the Many EdDSAs&quot; showed that verifiers all conforming to RFC 8032 nonetheless split on adversarial inputs -- cofactored versus cofactorless verification equations, &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 point encodings&lt;/a&gt;, small-order points -- so the same signature could be valid to one library and invalid to another [@chalkias-many-eddsas-2020]. For a consensus system that is a disaster, and ZIP-215 answers it by pinning a byte-exact validation profile on which single and batch verification always agree [@zip-215].&lt;/p&gt;
&lt;p&gt;Edwards curves have a cofactor, so the verification equation can be written to multiply through by it (cofactored) or not (cofactorless). RFC 8032 notes the cofactorless check is sufficient but not required, and that latitude is exactly what let conformant verifiers disagree until ZIP-215 fixed a single byte-exact rule.&lt;/p&gt;
&lt;p&gt;BIP-340 treats this surface as an engineering requirement rather than an afterthought, which is why its verifier is specified to the byte:&lt;/p&gt;

The verification algorithm must be completely specified at the byte level. This guarantees that nobody can construct a signature that is valid to some verifiers but not all.
&lt;h3&gt;The evidence, in one table&lt;/h3&gt;
&lt;p&gt;Every named break in this article splits cleanly across the three surfaces. Read the &quot;Surface&quot; column and the thesis stops being a slogan and becomes a body count.&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;Surface&lt;/th&gt;
&lt;th&gt;Sub-type&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;PlayStation 3&lt;/td&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;1. Nonce&lt;/td&gt;
&lt;td&gt;Repeat&lt;/td&gt;
&lt;td&gt;One constant nonce across all signatures&lt;/td&gt;
&lt;td&gt;Never reuse a nonce; two signatures leak the key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android Bitcoin wallets&lt;/td&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;1. Nonce&lt;/td&gt;
&lt;td&gt;Generation&lt;/td&gt;
&lt;td&gt;Broken SecureRandom repeated the nonce&lt;/td&gt;
&lt;td&gt;Do not trust the RNG; derive the nonce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitcoin txid, Mt. Gox era&lt;/td&gt;
&lt;td&gt;2014&lt;/td&gt;
&lt;td&gt;2. Canonical form&lt;/td&gt;
&lt;td&gt;Malleability&lt;/td&gt;
&lt;td&gt;(r, n - s) is a free second signature&lt;/td&gt;
&lt;td&gt;Enforce low-s; treat SUF-CMA as the bar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minerva, TPM-Fail&lt;/td&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;1. Nonce&lt;/td&gt;
&lt;td&gt;Usage, timing&lt;/td&gt;
&lt;td&gt;Nonce bit-length leaked during scalar mult&lt;/td&gt;
&lt;td&gt;Constant-time scalar multiplication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many EdDSAs, ZIP-215&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;3. Verifier&lt;/td&gt;
&lt;td&gt;Disagreement&lt;/td&gt;
&lt;td&gt;Conformant verifiers split on edge cases&lt;/td&gt;
&lt;td&gt;Specify the verifier at the byte level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Psychic Signatures&lt;/td&gt;
&lt;td&gt;2022&lt;/td&gt;
&lt;td&gt;3. Verifier&lt;/td&gt;
&lt;td&gt;Permissive&lt;/td&gt;
&lt;td&gt;Verifier accepted (0, 0) for any message&lt;/td&gt;
&lt;td&gt;Range-check the signature; reject zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PuTTY&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;1. Nonce&lt;/td&gt;
&lt;td&gt;Bias&lt;/td&gt;
&lt;td&gt;Top 9 bits of each P-521 nonce were zero&lt;/td&gt;
&lt;td&gt;Unbiased or deterministic nonce generation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Notice what is missing from the root-cause column: not once did anyone solve an elliptic-curve discrete logarithm or factor a modulus. That is Aha 1, complete. Fix the nonce, fix the form, fix the verifier, and you have a signature you can trust one at a time. But the modern world verifies millions at once, and the obvious way to batch them is quietly forgeable.&lt;/p&gt;
&lt;h2&gt;10. Batch Verification, and the One Condition That Makes It Safe&lt;/h2&gt;
&lt;p&gt;Schnorr&apos;s linearity offers a tempting shortcut: instead of checking a thousand signatures one by one, check their sum once. Done naively, that shortcut lets an attacker slip forgeries past you.&lt;/p&gt;
&lt;h3&gt;The technique&lt;/h3&gt;
&lt;p&gt;Because Schnorr, EdDSA, and BIP-340 verification is a single linear equation, a batch of $u$ signatures can be checked as one random linear combination of those equations. That collapses $u$ separate double-scalar multiplications into a single large multi-scalar multiplication, whose cost is sublinear in the number of distinct bases, which is where the throughput win comes from. The original Ed25519 paper reports a batch of 64 signatures verifying at under 134,000 cycles per signature, against 273,364 cycles for a single verification, roughly a twofold speedup [@bernstein-eddsa-2011].&lt;/p&gt;

Checking many signatures with one combined equation instead of one equation each, by exploiting the linearity of Schnorr-type verification. It is safe only when each signature&apos;s contribution is weighted by an independent random coefficient; the naive equal-weight sum is forgeable.
&lt;h3&gt;The non-negotiable safety condition&lt;/h3&gt;
&lt;p&gt;The coefficients must be &lt;strong&gt;randomized&lt;/strong&gt;. Here is why the naive all-ones sum fails. A signature verifies exactly when its verification residual is zero. If you add the residuals with equal weight, an attacker can submit two invalid signatures whose residuals are equal and opposite, so they cancel in the sum: the batch equation holds even though neither signature is valid.&lt;/p&gt;
&lt;p&gt;Weight each residual by an independent random coefficient and the errors no longer cancel except with negligible probability. BIP-340 derives its batch coefficients from a CSPRNG seeded over &lt;em&gt;all&lt;/em&gt; of the inputs, so an attacker cannot predict the weights and engineer a set of residuals that sums to zero without each signature being individually valid [@bip-340].&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;// Illustrative model. A signature is VALID iff its verification residual is 0. const n = 1000003n; const mod = (a, m) =&amp;gt; ((a % m) + m) % m; // Attacker forges two invalid signatures whose residuals are equal and opposite. const D = 777n;                 // the cancelling error the attacker chooses const e1 = mod(D, n);           // residual of forged signature 1 (nonzero: invalid) const e2 = mod(-D, n);          // residual of forged signature 2 (nonzero: invalid) console.log(&apos;signature 1 valid on its own? &apos; + (e1 === 0n));   // false console.log(&apos;signature 2 valid on its own? &apos; + (e2 === 0n));   // false // Naive all-ones batch: accept iff the plain sum of residuals is 0 const allOnes = mod(e1 + e2, n); console.log(&apos;all-ones batch residual = &apos; + allOnes + (allOnes === 0n ? &apos;  -&amp;gt; ACCEPTED, forgery slips through&apos; : &apos;  -&amp;gt; rejected&apos;)); // Randomized batch: weight each residual by a per-input random coefficient const a1 = 31337n, a2 = 424242n;   // seeded by a CSPRNG over ALL inputs const rand = mod(a1 * e1 + a2 * e2, n); console.log(&apos;randomized batch residual = &apos; + rand + (rand === 0n ? &apos;  -&amp;gt; accepted&apos; : &apos;  -&amp;gt; REJECTED, forgery caught&apos;));&lt;/code&gt;}&lt;/p&gt;

flowchart TD
    IN[&quot;u signatures to verify&quot;] --&amp;gt; SEED[&quot;Seed a CSPRNG over all u inputs&quot;]
    SEED --&amp;gt; COEF[&quot;Random coefficients a1 through au&quot;]
    COEF --&amp;gt; MSM[&quot;One multi-scalar multiplication&quot;]
    MSM --&amp;gt; OK{&quot;Batch equation holds?&quot;}
    OK --&amp;gt;|yes| ACC[&quot;Accept all&quot;]
    OK --&amp;gt;|no| FALL[&quot;Fall back to individual verification&quot;]
    IN --&amp;gt; BAD[&quot;Naive all-ones coefficients&quot;]
    BAD --&amp;gt; CANCEL[&quot;Attacker makes two residuals cancel&quot;]
    CANCEL --&amp;gt; FORGE[&quot;Invalid batch accepted, a forgery&quot;]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Naive all-ones batch verification is forgeable, because an attacker can craft invalid signatures whose verification errors cancel in the sum. Randomize the coefficients, seeded by a CSPRNG over every input, or do not batch at all.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;What batching does not give you, and where it is not used&lt;/h3&gt;
&lt;p&gt;A failed batch tells you only that &lt;em&gt;some&lt;/em&gt; signature in it is bad, not which one, so you must fall back to individual verification to find the culprit -- a real cost when batches are large. Batching is also a Schnorr-family privilege: BIP-340 notes that standardized ECDSA &quot;cannot be verified more efficiently in batch,&quot; so the ECDSA deployments that dominate the web get no speedup here [@bip-340]. And adoption is narrower than the technique&apos;s fame suggests.&lt;/p&gt;
&lt;p&gt;Bitcoin Core does not batch-verify signatures in consensus validation as of 2024 through 2026: the determinism and simplicity of validating each signature independently outweigh the throughput gain. Batch verification is a real optimization for specific high-volume verifiers, not a universal default.&lt;/p&gt;
&lt;p&gt;That is the deployed toolkit: ECDSA, Ed25519, BIP-340 Schnorr, and RSA-PSS, each with its surfaces pulled in to a different degree. Which one should you reach for in 2026? The honest answer is not a single winner.&lt;/p&gt;
&lt;h2&gt;11. State of the Art and Competing Approaches (2024 to 2026)&lt;/h2&gt;
&lt;p&gt;There is no single best signature scheme to crown. The state of the art is four deployed constructions, each dominant in a niche, plus a set of signposts pointing at the frontier. This is coexistence with selective supersession, not a ladder where each rung replaces the last.&lt;/p&gt;
&lt;h3&gt;The four, per niche&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Deterministic or hedged ECDSA&lt;/strong&gt; leads by raw deployment. It is what FIPS, TLS, and the secp256k1 chains run, and modern libraries hedge it by default [@rfc-6979], [@go-crypto-ecdsa]. &lt;strong&gt;Ed25519&lt;/strong&gt; is the misuse-resistant greenfield default: deterministic nonce, fixed encoding, constant-time by construction [@rfc-8032], [@bernstein-eddsa-2011]. &lt;strong&gt;BIP-340 Schnorr&lt;/strong&gt; leads for consensus and threshold work, being byte-exact, SUF-CMA by design, and linear enough to aggregate [@bip-340]. &lt;strong&gt;RSA-PSS&lt;/strong&gt; leads verify-heavy and interoperability-bound settings with its tight proof and cheap verification [@rfc-8017], [@rfc-8446].&lt;/p&gt;
&lt;p&gt;Of the classical schemes, only finite-field DSA is withdrawn for new signatures by FIPS 186-5; deterministic ECDSA is &lt;em&gt;approved&lt;/em&gt;, which is not the same as &lt;em&gt;mandated&lt;/em&gt; [@fips-186-5].&lt;/p&gt;

FIPS 186-5 (2023) approves ECDSA, EdDSA, and deterministic ECDSA, and withdraws finite-field DSA for new signatures. Two practical consequences follow. First, &quot;approved&quot; is not &quot;mandated&quot;: you may hedge, and the default posture in compliant libraries is randomized or hedged signing rather than bare determinism. Second, EdDSA is now a first-class FIPS citizen, so choosing Ed25519 no longer forces you outside the compliance boundary the way it once did [@fips-186-5].
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;ECDSA (hedged)&lt;/th&gt;
&lt;th&gt;Ed25519&lt;/th&gt;
&lt;th&gt;BIP-340 Schnorr&lt;/th&gt;
&lt;th&gt;RSA-PSS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Hardness&lt;/td&gt;
&lt;td&gt;ECDLP (P-256, secp256k1)&lt;/td&gt;
&lt;td&gt;ECDLP (edwards25519)&lt;/td&gt;
&lt;td&gt;ECDLP (secp256k1)&lt;/td&gt;
&lt;td&gt;RSA, related to factoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature size&lt;/td&gt;
&lt;td&gt;64 to 72 bytes (DER)&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;td&gt;256 bytes at 2048, 384 at 3072&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public key size&lt;/td&gt;
&lt;td&gt;33 bytes compressed&lt;/td&gt;
&lt;td&gt;32 bytes&lt;/td&gt;
&lt;td&gt;32 bytes, x-only&lt;/td&gt;
&lt;td&gt;256 to 384 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nonce or randomness&lt;/td&gt;
&lt;td&gt;Hedged or RFC 6979&lt;/td&gt;
&lt;td&gt;Native deterministic&lt;/td&gt;
&lt;td&gt;Synthetic, hedged&lt;/td&gt;
&lt;td&gt;Fresh random salt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Malleable by default?&lt;/td&gt;
&lt;td&gt;Yes, enforce low-s&lt;/td&gt;
&lt;td&gt;No under strict S range&lt;/td&gt;
&lt;td&gt;No, byte-exact&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch verification&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proof quality&lt;/td&gt;
&lt;td&gt;Idealized only&lt;/td&gt;
&lt;td&gt;Loose, forking lemma&lt;/td&gt;
&lt;td&gt;Loose, forking lemma&lt;/td&gt;
&lt;td&gt;Tight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sign versus verify&lt;/td&gt;
&lt;td&gt;Balanced&lt;/td&gt;
&lt;td&gt;Balanced&lt;/td&gt;
&lt;td&gt;Balanced&lt;/td&gt;
&lt;td&gt;Slow sign, fast verify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;FIPS, secp256k1 chains&lt;/td&gt;
&lt;td&gt;Greenfield default&lt;/td&gt;
&lt;td&gt;Consensus, threshold&lt;/td&gt;
&lt;td&gt;RSA interop, verify-heavy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;Benchmarks, read correctly&lt;/h3&gt;
&lt;p&gt;The reference measurements everyone quotes for Ed25519 come from the eBATS and eBACS public benchmarking effort [@bernstein-eddsa-2011], [@ebacs-bench].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ed25519 operation&lt;/th&gt;
&lt;th&gt;Cost, eBATS reference measurement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Sign one message&lt;/td&gt;
&lt;td&gt;87,548 cycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify one signature&lt;/td&gt;
&lt;td&gt;273,364 cycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verify in a batch of 64&lt;/td&gt;
&lt;td&gt;under 134,000 cycles per signature&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; Two facts most first courses skip. RSA verifies far faster than it signs, often by more than an order of magnitude, because the public exponent is tiny and the private exponent is full width, whereas elliptic-curve schemes are balanced between the two operations. And rankings flip with the implementation: a build with hand-tuned P-256 assembly can make ECDSA out-run Ed25519, the reverse of the eBATS ordering. Never quote a cross-scheme benchmark as if it were a property of the mathematics. Measure the library you ship, on the CPU you run.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Signposts at the frontier&lt;/h3&gt;
&lt;p&gt;Four constructions sit just outside the four-scheme core and deserve a pointer rather than a section. &lt;strong&gt;XEdDSA&lt;/strong&gt; lets you sign with an X25519 Diffie-Hellman key, a specialized key-reuse trick that is deployed at scale in the Signal protocol and the applications built on it, so it is peripheral by role but anything but academic [@xeddsa-spec].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MuSig2&lt;/strong&gt; and &lt;strong&gt;FROST&lt;/strong&gt; are two-round multi-signature and threshold-Schnorr protocols that produce an ordinary 64-byte signature indistinguishable from a single-signer one [@musig2-2020], [@rfc-9591]. &lt;strong&gt;BLS&lt;/strong&gt; offers pairing-based aggregation and anchors Ethereum consensus; a companion part in this series on hashing to curves covers it.&lt;/p&gt;
&lt;p&gt;And the horizon is &lt;strong&gt;post-quantum&lt;/strong&gt;: NIST finalized &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-DSA&lt;/a&gt; in FIPS 204 and SLH-DSA in FIPS 205 in August 2024, both with signatures ranging from kilobytes to tens of kilobytes, an order of magnitude or more larger than anything in the table above [@fips-204], [@fips-205].&lt;/p&gt;
&lt;p&gt;XEdDSA is de-randomized like plain EdDSA, so absent hedging it inherits the same differential-fault exposure, which the hedged Fiat-Shamir analysis addresses [@aranha-hedged-fs-2019].&lt;/p&gt;
&lt;p&gt;Every row of those tables assumes the hardness holds. That assumption has a precise shape, and a known expiration date.&lt;/p&gt;
&lt;h2&gt;12. Theoretical Limits&lt;/h2&gt;
&lt;p&gt;Here is what a first course skips: none of these schemes has an unconditional proof, their proofs differ enormously in quality, and every one of them dies to the same quantum algorithm.&lt;/p&gt;
&lt;h3&gt;Conditional security, three assumptions&lt;/h3&gt;
&lt;p&gt;Every guarantee in this article is conditional. The elliptic-curve schemes rest on the hardness of the elliptic-curve discrete logarithm; RSA-PSS rests on the RSA problem, which is no harder than factoring; and all of the Fiat-Shamir schemes rest additionally on the &lt;strong&gt;random-oracle model&lt;/strong&gt;, a heuristic that proves security while treating the hash function as an ideal random function that no real hash exactly is. &quot;Provably secure&quot; here always means &quot;secure if these assumptions hold.&quot; That is not a weakness peculiar to signatures; it is the honest status of essentially all deployed public-key cryptography.&lt;/p&gt;
&lt;h3&gt;The tightness spectrum&lt;/h3&gt;
&lt;p&gt;The interesting frontier is not whether the proofs exist but how much they lose. A reduction converts an attacker on the scheme into an algorithm against the hard problem, and the &lt;em&gt;tightness&lt;/em&gt; of that conversion is how much security leaks in the translation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Reduction (random-oracle model)&lt;/th&gt;
&lt;th&gt;Tightness&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;RSA-PSS&lt;/td&gt;
&lt;td&gt;To the RSA problem, Bellare-Rogaway&lt;/td&gt;
&lt;td&gt;Tight&lt;/td&gt;
&lt;td&gt;The best guarantee of any deployed signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schnorr, EdDSA&lt;/td&gt;
&lt;td&gt;To discrete log via the forking lemma, Pointcheval-Stern&lt;/td&gt;
&lt;td&gt;Loose, quadratic loss&lt;/td&gt;
&lt;td&gt;Seurin showed the loss is essentially optimal, a barrier not a gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;(EC)DSA&lt;/td&gt;
&lt;td&gt;Only with an idealized conversion function, Fersch-Kiltz-Poettering&lt;/td&gt;
&lt;td&gt;Idealized&lt;/td&gt;
&lt;td&gt;Secure because ECDLP holds, not because the scheme is clean&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;RSA-PSS has a tight reduction to RSA [@bellare-rogaway-pss-1996]. Schnorr and EdDSA have only a loose, quadratic-loss reduction through the forking lemma [@pointcheval-stern-1996-2000], and Seurin proved that this loss is essentially the best any algebraic reduction can do, so it is a genuine barrier rather than a proof waiting to be improved [@seurin-2012]. Later work made the multi-user Schnorr reduction explicit, with a loss on the order of the number of hash queries [@kiltz-masny-pan-2016]. And (EC)DSA has the weakest guarantee of all: the only known proofs must model its conversion function as an idealized, bijective random oracle [@fersch-kiltz-poettering-2016].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Deployment and proof quality are inversely correlated here. The most-deployed signature in the world, ECDSA, has the least satisfying proof: it is secure because the elliptic-curve discrete logarithm is hard, not because the scheme is clean. And tight proof or loose, every scheme in this article falls to Shor&apos;s algorithm. The destination is post-quantum, not a better curve.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The classical hardness bounds are near-optimal but unproven. Elliptic-curve discrete log costs about 2^128 operations for a 256-bit curve by Pollard&apos;s rho, which is optimal in the generic-group model, while factoring costs sub-exponential effort by the general number field sieve. Neither hardness is proven, and RSA inversion is not even proven equivalent to factoring.&lt;/p&gt;
&lt;h3&gt;The one bound that is not conjectural&lt;/h3&gt;
&lt;p&gt;There is exactly one limit in this section that is not an open question. Shor&apos;s algorithm solves both factoring and the discrete logarithm in quantum polynomial time, so a cryptographically relevant quantum computer breaks &lt;em&gt;every&lt;/em&gt; scheme in this article at once [@shor-1997]. No curve size and no modulus length helps, because Shor&apos;s cost is polynomial in the key size. The elliptic-curve schemes and RSA-PSS fall together.&lt;/p&gt;

For encryption, an adversary can record ciphertext today and decrypt it after building a quantum computer, so the harvest-now-decrypt-later threat is urgent. Signatures authenticate in real time, so a signature forged after a quantum computer exists cannot retroactively forge yesterday&apos;s authenticated session. The exception is long-lived keys: code-signing roots, firmware, and certificate roots that must stay valid for decades have to outlive Shor, which is exactly where post-quantum signatures are being adopted first [@shor-1997].
&lt;p&gt;If the theory is this settled -- conditional proofs of differing quality and a known quantum cliff -- then everything still up to you lives in the code. Here is that part, compressed into rules you can apply on Monday.&lt;/p&gt;
&lt;h2&gt;13. The Practical Guide: Decision Rules&lt;/h2&gt;
&lt;p&gt;Every rule below is one row of the failure catalog, inverted. If you remember nothing else, remember the decision tree.&lt;/p&gt;

flowchart TD
    START{&quot;What constrains you?&quot;} --&amp;gt; G[&quot;Greenfield, no constraint&quot;]
    START --&amp;gt; F[&quot;FIPS or compliance&quot;]
    START --&amp;gt; R[&quot;Must interoperate with RSA&quot;]
    START --&amp;gt; B[&quot;Bitcoin or byte-exact consensus&quot;]
    START --&amp;gt; T[&quot;Threshold or multisig&quot;]
    G --&amp;gt; GE[&quot;Ed25519 with a strict verifier&quot;]
    F --&amp;gt; FE[&quot;Hedged ECDSA P-256 or FIPS-approved Ed25519, enforce low-s&quot;]
    R --&amp;gt; RE[&quot;RSA-PSS as PS256, at least RSA-3072&quot;]
    B --&amp;gt; BE[&quot;BIP-340 Schnorr, or low-s strict-DER ECDSA if legacy&quot;]
    T --&amp;gt; TE[&quot;FROST or MuSig2, never deterministic nonces&quot;]
&lt;h3&gt;Use X, with these parameters, in this case&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Greenfield, no FIPS constraint.&lt;/strong&gt; Reach for &lt;strong&gt;Ed25519&lt;/strong&gt;, verified with a strict profile that enforces the canonical &lt;code&gt;S &amp;lt; L&lt;/code&gt; range and a ZIP-215-style byte-exact rule [@rfc-8032], [@zip-215]. You get deterministic nonces, a fixed encoding, and constant-time arithmetic for free.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;FIPS-constrained.&lt;/strong&gt; Use &lt;strong&gt;deterministic or hedged ECDSA on P-256&lt;/strong&gt; following RFC 6979 and its hedged variant, or FIPS 186-5-approved &lt;strong&gt;Ed25519&lt;/strong&gt;. Enforce low-s, and insist on a constant-time library so you do not reopen the Minerva timing surface [@rfc-6979], [@fips-186-5].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RSA interoperability for a new design.&lt;/strong&gt; Use &lt;strong&gt;RSA-PSS&lt;/strong&gt; (the &lt;code&gt;PS256&lt;/code&gt; scheme), never PKCS#1 v1.5 for new work. Set the salt length equal to the hash length, use a matching MGF hash, and choose at least RSA-3072 for 128-bit security [@rfc-8017].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bitcoin or byte-exact consensus.&lt;/strong&gt; Use &lt;strong&gt;BIP-340 Schnorr&lt;/strong&gt;, as deployed in Taproot. If you are stuck with legacy ECDSA, enforce low-s and strict DER encoding so transactions are not malleable [@bip-340], [@bip-146].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Threshold or multisig.&lt;/strong&gt; Use &lt;strong&gt;FROST&lt;/strong&gt; or &lt;strong&gt;MuSig2&lt;/strong&gt; over BIP-340, and &lt;em&gt;never&lt;/em&gt; use deterministic nonces in multi-party signing -- a deterministic nonce in an interactive protocol can be replayed across differing co-signer inputs and leak the share [@rfc-9591], [@musig2-2020], [@bip-340].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Always.&lt;/strong&gt; Hedge the nonce whenever the platform can face fault injection. Domain-separate your keys per scheme: BIP-340 warns that reusing one key across RFC-6979 ECDSA and BIP-340 can leak it through cross-scheme nonce coincidences [@bip-340]. Verify strictly, and when any part of your protocol hashes over signatures, treat SUF-CMA, not merely EUF-CMA, as the bar.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constraint&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;th&gt;Key parameters&lt;/th&gt;
&lt;th&gt;Because of (named break)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Greenfield, no FIPS&lt;/td&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;td&gt;Strict &lt;code&gt;S &amp;lt; L&lt;/code&gt;, ZIP-215 verifier&lt;/td&gt;
&lt;td&gt;Psychic Signatures, Many EdDSAs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIPS-constrained&lt;/td&gt;
&lt;td&gt;Hedged ECDSA P-256, or FIPS Ed25519&lt;/td&gt;
&lt;td&gt;RFC 6979 or hedged, low-s, constant-time&lt;/td&gt;
&lt;td&gt;PS3, PuTTY, Minerva&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSA interop, new design&lt;/td&gt;
&lt;td&gt;RSA-PSS as PS256&lt;/td&gt;
&lt;td&gt;Salt length equals hash length, RSA-3072 or larger&lt;/td&gt;
&lt;td&gt;v1.5 low-exponent forgeries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bitcoin or consensus&lt;/td&gt;
&lt;td&gt;BIP-340 Schnorr (Taproot)&lt;/td&gt;
&lt;td&gt;Or low-s and strict DER if legacy ECDSA&lt;/td&gt;
&lt;td&gt;Bitcoin txid malleability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threshold or multisig&lt;/td&gt;
&lt;td&gt;FROST or MuSig2&lt;/td&gt;
&lt;td&gt;Never deterministic nonces in multi-party&lt;/td&gt;
&lt;td&gt;Reused-nonce key recovery&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; Ed25519 for anything greenfield. Hedged ECDSA on P-256 for FIPS and secp256k1 chains. RSA-PSS for RSA interoperability. BIP-340 Schnorr for consensus and threshold. Whatever you pick, verify strictly and hedge the nonce wherever faults are possible.&lt;/p&gt;
&lt;/blockquote&gt;

Generate an Ed25519 keypair, then sign and verify a message, with OpenSSL 3.x:&lt;ul&gt;
&lt;li&gt;Create the private key: &lt;code&gt;openssl genpkey -algorithm ed25519 -out sk.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Extract the public key: &lt;code&gt;openssl pkey -in sk.pem -pubout -out pk.pem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Sign: &lt;code&gt;openssl pkeyutl -sign -inkey sk.pem -rawin -in message.txt -out sig.bin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verify: &lt;code&gt;openssl pkeyutl -verify -pubin -inkey pk.pem -rawin -in message.txt -sigfile sig.bin&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To force RSA-PSS rather than PKCS#1 v1.5 when signing with an RSA key: &lt;code&gt;openssl dgst -sha256 -sign rsa.pem -sigopt rsa_padding_mode:pss -sigopt rsa_pss_saltlen:-1 -out sig.bin message.txt&lt;/code&gt;
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;Turn each of those rules around and you get the antipatterns that keep showing up in real code review: the misuse catalog.&lt;/p&gt;
&lt;h2&gt;14. Common Misuse, and the One Sentence Underneath&lt;/h2&gt;
&lt;p&gt;Every break in this article was a violated rule from the last section. Here they are as the antipatterns a reviewer actually finds in real code.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A caller-supplied nonce from a weak or predictable RNG.&lt;/strong&gt; This is the Android wallets and, in its biased form, PuTTY [@bitcoin-android-2013], [@cve-2024-31497].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A nonce reused across messages or keys.&lt;/strong&gt; This is the PS3, and it is certain from two signatures [@ps3-27c3-2010].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-constant-time scalar multiplication.&lt;/strong&gt; This is Minerva and TPM-Fail: even a perfect nonce leaks through timing [@minerva-2019], [@tpm-fail-2019].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skipping the range, subgroup, or canonical-s checks.&lt;/strong&gt; This reopens malleability, and for a permissive verifier it reopens forgery [@bip-340].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treating EUF-CMA as if it meant non-malleability.&lt;/strong&gt; This is Bitcoin transaction-id malleability: the scheme was unforgeable and the protocol still broke [@bip-340].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Naive all-ones batch coefficients.&lt;/strong&gt; A forgeable batch, defeated by errors that cancel in the sum [@bip-340].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A lax or permissive verifier.&lt;/strong&gt; This is Psychic Signatures, which accepted the all-zero signature for any message [@cve-2022-21449].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PKCS#1 v1.5 for new designs, or blurring the encryption-versus-signature split.&lt;/strong&gt; Use RSA-PSS, and never confuse the v1.5 decryption oracle with the unbroken v1.5 signature scheme [@rfc-8017].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signing without domain separation.&lt;/strong&gt; One key across two schemes can leak through cross-scheme nonce coincidences [@bip-340].&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; First, an ECDSA nonce drawn from a dubious or caller-supplied RNG instead of a derived or hedged one. Second, a missing low-s or strict &lt;code&gt;S &amp;lt; L&lt;/code&gt; check, so signatures are malleable. Third, a verifier that does not range-check the signature values and will therefore accept degenerate inputs. Those three account for most of the named breaks in this article.&lt;/p&gt;
&lt;/blockquote&gt;


No, and this is the most repeated mistake in the area. A *repeated* nonce leaks the key with certainty from just two signatures -- that is the PS3 break. A *biased* nonce, where the top few bits are always zero, is a statistical attack: it takes many signatures and lattice reduction, framed as the Hidden Number Problem, to recover the key, as PuTTY&apos;s roughly-sixty-signature break showed. &quot;One few-bit-biased signature leaks your key&quot; is false.


No. Deriving the nonce deterministically removes the RNG-failure disaster, but it turns the signer into a fixed, replayable target for fault attacks: glitch one of two identical signings and diff the outputs. The resolution is hedged signing -- mix a little fresh randomness back in -- which most modern libraries do by default.


No, but it is nuanced. Standard Ed25519 can be malleable at the group level unless the verifier enforces the canonical `S &amp;lt; L` range check that RFC 8032 mandates on decode. Enforce it, as strict libraries do, and Ed25519 is strongly unforgeable; skip it and you inherit a malleability surface.


Encryption. The v1.5 *encryption* padding is the classic Bleichenbacher decryption oracle. The v1.5 *signature* scheme is not broken; its real failures were implementation bugs in lax verifiers, such as low-exponent forgeries, not a break of the signature math. Even so, use RSA-PSS for new designs.


Ed25519 for anything greenfield: deterministic nonces, a fixed 64-byte encoding, constant-time arithmetic, and strong unforgeability under strict verification. Use ECDSA only where a standard or platform compels it, such as FIPS environments or secp256k1 chains, and then with RFC 6979 or hedged nonces, low-s enforcement, and a constant-time library.


If any part of your protocol hashes over or deduplicates by the signature itself -- transaction ids, caches, replay checks -- you need SUF-CMA, because plain EUF-CMA lets an attacker produce a different valid signature on the same message. Enforce low-s or a strict `S &amp;lt; L` range, or choose a SUF-CMA-by-design scheme like BIP-340.


No. Shor&apos;s algorithm breaks both discrete log and factoring in polynomial time, so ECDSA, EdDSA, BIP-340, and RSA-PSS all fall to a cryptographically relevant quantum computer, and no curve or modulus size helps. The destination is post-quantum signatures such as ML-DSA and SLH-DSA, finalized in August 2024 and being adopted first for long-lived keys.

&lt;h3&gt;The one sentence underneath&lt;/h3&gt;
&lt;p&gt;Return to that slide at 27C3. Sony&apos;s curve was pristine; two signatures gave up the master key [@ps3-27c3-2010]. Now line up the rest of the body count. Across three decades, almost no deployed break ever solved an elliptic-curve discrete logarithm or factored a modulus. Attackers reused a nonce, biased one [@cve-2024-31497], leaked one through timing [@minerva-2019], flipped a signature&apos;s &lt;code&gt;s&lt;/code&gt; to &lt;code&gt;n - s&lt;/code&gt;, or accepted the all-zero signature for any message [@cve-2022-21449]. The hardness assumption sat untouched through all of it.&lt;/p&gt;
&lt;p&gt;That is why &quot;signing done right&quot; is not a single choice but a stack: the right scheme for the job, a nonce you do not have to trust the RNG for, a canonical form, and a byte-exact verifier -- and, the deepest lesson the evolution kept teaching, a preference for schemes where the dangerous case simply cannot be reached, which is what Ed25519 and BIP-340 buy you. Remove any one layer and a surface reopens.&lt;/p&gt;
&lt;p&gt;The horizon does not change the shape of the problem, only its arithmetic: post-quantum signatures like ML-DSA and SLH-DSA will hand the same three surfaces to the same callers, at ten to a hundred times the signature size [@fips-204]. The nonce, the form, and the verifier will still be where the security lives.&lt;/p&gt;

A signature is only as safe as the nonce, the form, and the verifier the scheme hands you. The math held; the interface leaked.
&lt;p&gt;Using a signature safely, in the end, is the discipline of never handing an attacker a value your scheme forgot to own.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;digital-signatures-ecdsa-eddsa-rsa-pss&quot; keyTerms={[
  { term: &quot;Per-signature nonce&quot;, definition: &quot;The fresh secret a discrete-log signature draws for each signature; reusing or biasing it leaks the private key.&quot; },
  { term: &quot;EUF-CMA&quot;, definition: &quot;Unforgeability against any new message, even after an adversary obtains signatures on messages of its choice.&quot; },
  { term: &quot;SUF-CMA&quot;, definition: &quot;A stronger goal that also forbids a new signature on an already-signed message; the property malleability violates.&quot; },
  { term: &quot;Signature malleability&quot;, definition: &quot;A second valid signature anyone can derive without the private key, such as the pair with n minus s for ECDSA.&quot; },
  { term: &quot;Hidden Number Problem&quot;, definition: &quot;The lattice problem behind biased-nonce key recovery, and the reason a bias is statistical rather than certain.&quot; },
  { term: &quot;Deterministic signing (RFC 6979)&quot;, definition: &quot;Deriving the nonce from the private key and the message so signing needs no runtime randomness.&quot; },
  { term: &quot;Hedged signing&quot;, definition: &quot;Mixing fresh randomness into a deterministic nonce so the signer survives both RNG failure and fault attacks.&quot; },
  { term: &quot;Low-s canonicalization&quot;, definition: &quot;Requiring the s value to lie in the lower half of its range so only one of the malleable twins is standard.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>digital-signatures</category><category>ecdsa</category><category>eddsa</category><category>rsa-pss</category><category>schnorr</category><category>nonce-reuse</category><category>applied-cryptography</category><category>cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Doorway Into the Group: A Field Guide to Hashing a String Onto an Elliptic Curve (RFC 9380)</title><link>https://paragmali.com/blog/the-doorway-into-the-group-a-field-guide-to-hashing-a-string/</link><guid isPermaLink="true">https://paragmali.com/blog/the-doorway-into-the-group-a-field-guide-to-hashing-a-string/</guid><description>Turning a string into an elliptic-curve point is a two-decade problem behind a one-line API. Why H(m)*G and hunt-and-peck both fail, and how RFC 9380 fixes it.</description><pubDate>Sun, 12 Jul 2026 15:35:47 GMT</pubDate><content:encoded>
**Turning a string into an elliptic-curve point is a two-decade research problem hiding behind a one-line API.** A safe map must produce a point whose discrete log is unknown, in time that does not depend on the input, with a distribution a proof can treat as a random oracle. The two obvious shortcuts each fail one of those tests: $H(m)\cdot G$ publishes the discrete log (forge BLS, break OPRFs), and try-and-increment leaks the input through timing (the Dragonblood attack read WPA3 passwords) [@rfc9380] [@vr20]. RFC 9380 (2023) finally delivers all three at once by hashing to two field elements, mapping each with a per-curve function, and adding the points [@rfc9380]. The standard is now quietly load-bearing under BLS on Ethereum, every elliptic-curve OPRF, OPAQUE, Privacy Pass, and WPA3&apos;s constant-time fix. This guide shows you why the naive maps fail, how the standard works, and exactly which suite to use for your curve and protocol.
&lt;h2&gt;1. They Timed the Handshake and Read the Wi-Fi Password&lt;/h2&gt;
&lt;p&gt;In April 2019, two researchers announced they could recover your Wi-Fi password without guessing it, phishing it, or breaking any cipher. They simply &lt;em&gt;timed&lt;/em&gt; how long WPA3 took to turn the password into a point on an elliptic curve, and the clock spelled out the secret [@vr20] [@dragonblood-site]. The flaw was not in the encryption or the key exchange. It lived in a single, overlooked step that nearly every discrete-log protocol quietly leans on: the map that carries an arbitrary string into the group.&lt;/p&gt;
&lt;p&gt;Get that one step wrong in the &lt;em&gt;other&lt;/em&gt; obvious way, computing the point as $H(m)\cdot G$, and you leak a different secret entirely. Now &lt;em&gt;anyone&lt;/em&gt; can forge the signatures that map was supposed to protect [@rfc9380] [@bls01]. Two shortcuts, two spilled secrets, one primitive. This is the field guide to getting it right.&lt;/p&gt;

A function that maps an arbitrary byte string to a point on an elliptic curve, such that the point&apos;s discrete logarithm (its relationship to the group generator) is unknown even to the party computing it. RFC 9380 standardizes the interface as `hash_to_curve` and `encode_to_curve` [@rfc9380].
&lt;p&gt;The primitive looks trivial. Hash the input, get a point, move on. But &quot;get a point&quot; hides a genuine algorithm, because on an elliptic curve only about half of the field elements are valid $x$-coordinates. The two things every engineer tries first are the two things you must never ship, and each betrays a different secret. So before you write a single line, ask three questions.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; When you turn a string into a curve point, ask: (1) do you know the point&apos;s discrete log; (2) does your running time depend on the input; and (3) is the output distribution one your protocol&apos;s proof can treat as a random oracle? Every safe construction answers &lt;strong&gt;no, no, yes&lt;/strong&gt;. Every named failure in this article is a wrong answer to exactly one of these.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;WPA3-Personal answered the second question wrong. Its handshake, Simultaneous Authentication of Equals (SAE), derives a &quot;password element&quot; by hunt-and-peck: hash the password with a counter, test whether the result is a valid coordinate, and increment until it is [@rfc7664]. The number of iterations depends on the password, so the time the handshake takes is a function of the secret. Mathy Vanhoef and Eyal Ronen showed that an attacker within range can measure that timing and mount an offline dictionary attack, recovering the password without ever seeing a valid login [@vr20] [@dragonblood-site].&lt;/p&gt;

sequenceDiagram
    participant STA as Client (victim)
    participant AP as Access point
    participant Atk as Attacker (passive)
    Note over STA,AP: WPA3-SAE, password shared by both sides
    STA-&amp;gt;&amp;gt;STA: Derive password element by hunt-and-peck
    Note over STA: Iteration count depends on the password
    STA-&amp;gt;&amp;gt;AP: Send SAE Commit after the loop finishes
    Atk--&amp;gt;&amp;gt;STA: Measure time from trigger to Commit
    Note over Atk: Timing reveals the iteration count
    Atk-&amp;gt;&amp;gt;Atk: Offline dictionary attack narrows candidates
&lt;p&gt;RFC 9380, the standard that finally settled this primitive, is blunt about why try-and-increment is unsafe.&lt;/p&gt;

&quot;The running time of this method, which is generally referred to as a probabilistic try-and-increment algorithm, depends on the input string. As such, it is not safe to use in protocols sensitive to timing side channels, as was exemplified by the Dragonblood attack.&quot; -- RFC 9380, Appendix A [@rfc9380]
&lt;p&gt;The story got worse before it got better. The Wi-Fi Alliance&apos;s first attempt at a fix switched SAE to Brainpool curves, which &lt;em&gt;reintroduced&lt;/em&gt; a leak: the encoder now had to loop to find a hash smaller than the prime, and the number of iterations depended on the password &lt;em&gt;and&lt;/em&gt; the client&apos;s MAC address [@dragonblood-site].This second act is the whole lesson in miniature: constant time cannot be patched in curve by curve. The disclosure site concludes that &quot;implementing Dragonfly and WPA3 without side-channel leaks is surprisingly hard&quot; [@dragonblood-site]. Two rounds of the same class of bug, in the handshake now standard across WPA3 Wi-Fi.&lt;/p&gt;
&lt;p&gt;Now hold that failure next to its mirror image. Suppose BLS signatures had mapped a message the naive way, $H(m) = H_n(m)\cdot G$ for a public hash $H_n$. A BLS signature is $\sigma = [sk]\cdot H(m)$, so it would equal $[sk]\cdot H_n(m)\cdot G = H_n(m)\cdot([sk]\cdot G) = H_n(m)\cdot PK$, where $PK$ is the public key [@rfc9380] [@bls01]. Every term on the right is public. Anyone forges any signature with no secret key at all. One shortcut leaked a low-entropy password through the clock; the other would hand out the signing key&apos;s power to the entire world. The primitive that sits between them is where the security of BLS, every elliptic-curve OPRF, and every password PAKE is actually won or lost.&lt;/p&gt;
&lt;p&gt;This is the sibling of a claim Part 15 made about the curve itself: &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-curve discrete-log problem&lt;/a&gt; was hard; the danger was the soft gap around it. Here the gap has a name and a shape. It is the doorway, the map that carries untrusted strings into the group. Get the doorway wrong and you publish the discrete log, leak the password through the clock, or hand your proof a distribution it never agreed to. To see why &quot;hash to a point&quot; is an algorithm and not a one-liner, we have to go back to the moment it stopped being free.&lt;/p&gt;
&lt;h2&gt;2. When Hashing to a Group Stopped Being Free&lt;/h2&gt;
&lt;p&gt;For a generation of cryptographers, &quot;hash into the group&quot; was not a problem worth naming. In a prime field $\mathbb{F}_p^&lt;em&gt;$, the discrete-log setting Diffie and Hellman worked in, you hash your input to an integer and reduce it modulo $p$, and you are already inside the group. Every nonzero residue is a group element, so the map is a single modular reduction with nothing to prove.This is the free lunch elliptic curves took away. In $\mathbb{F}_p^&lt;/em&gt;$ the group &lt;em&gt;is&lt;/em&gt; the set of nonzero field elements, so any nonzero hash output is already a valid element. On a curve, the group is a sparse subset of the plane, and most field elements are not on it.&lt;/p&gt;
&lt;p&gt;Elliptic curves broke the deal. When Victor Miller and Neal Koblitz independently proposed elliptic-curve cryptography in 1985 and 1987, they replaced $\mathbb{F}_p^*$ with the points of a curve $y^2 = x^3 + ax + b$ [@miller85] [@koblitz87]. That change bought smaller keys and harder discrete logs, but it quietly created a new problem: for a random field element $x$, the quantity $x^3 + ax + b$ is a perfect square only about half the time, and only then is $x$ the coordinate of an actual point. &quot;Hash to a group element&quot; stopped being a reduction and became a search.&lt;/p&gt;
&lt;p&gt;Nobody worried, because for a decade nobody needed to hash &lt;em&gt;to a point&lt;/em&gt;. Ordinary ECDSA and ECDH hash to a &lt;em&gt;scalar&lt;/em&gt; and multiply a fixed generator, which never leaves the safe world of exponents.Hashing to a scalar is trivial and always safe: any integer modulo the group order is a valid exponent. Hashing to a &lt;em&gt;point&lt;/em&gt; is the hard problem, because most field elements are not on the curve.&lt;/p&gt;
&lt;p&gt;Then, around the turn of the millennium, the demand arrived from two directions at once, and both directions needed a point.&lt;/p&gt;

A signature scheme by Boneh, Lynn, and Shacham built on a bilinear pairing. The signature on a message $m$ is $\sigma = [sk]\cdot H(m)$, where $H(m)$ is the message hashed *to a curve point* and $sk$ is the secret key. Verification checks a pairing equation. Security requires that $H(m)$ have unknown discrete log, or signatures become forgeable [@bls01].
&lt;p&gt;The first direction was pairing-based cryptography. In 2001 Boneh, Lynn, and Shacham introduced BLS short signatures, whose entire construction rests on hashing the message to a curve point of unknown discrete log, because the signature &lt;em&gt;is&lt;/em&gt; that point scaled by the secret key [@bls01]. The same year, Boneh and Franklin built identity-based encryption on the identical primitive, hashing an identity string to a point [@bf01]. BLS shipped with the first answer anyone reaches for: &lt;code&gt;MapToGroup&lt;/code&gt;, a try-and-increment loop [@bls01]. Boneh and Franklin instead gave a &lt;em&gt;deterministic&lt;/em&gt; map, but only for certain supersingular curves over fields of characteristic $p \equiv 2 \pmod 3$, too narrow a class to serve as general infrastructure [@bf01].&lt;/p&gt;
&lt;p&gt;The second direction was password-authenticated key exchange. In 2008 Dan Harkins proposed Simultaneous Authentication of Equals, a balanced PAKE that maps a &lt;em&gt;password&lt;/em&gt; to a group element and then runs a zero-knowledge proof of it [@sae08]. To do the mapping it introduced hunt-and-peck, the same try-and-increment idea aimed at a secret input. SAE was standardized as RFC 7664 in 2015, including its &quot;Hunting and Pecking with ECC Groups&quot; derivation, and then shipped to consumers in 2018 as the core of WPA3-Personal [@rfc7664] [@vr20].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Pairing signatures and password PAKEs look unrelated, but both need the same thing: a hash whose output is a curve point of unknown discrete log. Pairing crypto needs it so a signature can &lt;em&gt;be&lt;/em&gt; that point; a PAKE needs it so the password element hides the password. The primitive was born load-bearing under two families of protocols in the same decade [@bls01] [@sae08].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A scoping note worth keeping honest: not every PAKE maps a password to a curve point. SPAKE2 uses fixed, independently generated group elements $M$ and $N$ rather than hashing the password onto the curve, and SRP works in an integer group, not on an elliptic curve at all. The &quot;everything depends on hash-to-curve&quot; claim is precise, not universal, and it applies to the named constructions in this article: SAE, CPace, OPAQUE&apos;s OPRF, the RFC 9497 OPRFs, and BLS.The lesson for a field guide: SPAKE2 and SRP are the counterexamples that keep &quot;all PAKEs need hash-to-curve&quot; from being true, and they stop you hunting for the primitive where it is not.&lt;/p&gt;
&lt;p&gt;So by 2008 the problem was named and both naive answers were on the table, shipped in real systems: $H(m)\cdot G$ carried the finite-field intuition onto curves, and try-and-increment searched for a valid coordinate. Each was correct in the sense that it produced a usable point. Each was also a disaster, and for opposite reasons. To see exactly what each one leaks, we have to look at the two shortcuts side by side.&lt;/p&gt;
&lt;h2&gt;3. Two Shortcuts, Two Spilled Secrets&lt;/h2&gt;
&lt;p&gt;Both obvious maps are correct in the ordinary sense: they produce a point on the curve, quickly, with a few lines of code. That is exactly what makes them dangerous. Each satisfies most of the three properties and fails precisely one, and the property each fails is different, which is why they leak different secrets.&lt;/p&gt;
&lt;h3&gt;Shortcut A: compute the point as a known multiple of the generator&lt;/h3&gt;
&lt;p&gt;Hash the message to an integer $H_n(m)$ and scalar-multiply the generator: $P = H_n(m)\cdot G$. The output is uniform on the group, costs one scalar multiplication, and is trivially constant-time. It assumes nothing about the curve. It is the finite-field intuition carried straight onto the curve, and it violates Property 1: whoever computed $P$ knows its discrete log, because they chose it.&lt;/p&gt;
&lt;p&gt;RFC 9380 does not hedge about the consequence.&lt;/p&gt;

&quot;The resulting point has a known discrete log relationship to $P$. Thus, except in cases where this method is specified by the protocol, it must not be used; doing so risks catastrophic security failures.&quot; -- RFC 9380, Appendix A [@rfc9380]
&lt;p&gt;&quot;Catastrophic&quot; is exact, not rhetorical. Take BLS. The public key is $PK = [sk]\cdot G$, and a signature is $\sigma = [sk]\cdot H(m)$. If the map were $H(m) = [H_n(m)]\cdot G$ for a public hash $H_n$, then&lt;/p&gt;
&lt;p&gt;$$\sigma = [sk]\cdot H(m) = [sk]\cdot[H_n(m)]\cdot G = [H_n(m)]\cdot([sk]\cdot G) = [H_n(m)]\cdot PK.$$&lt;/p&gt;
&lt;p&gt;Every quantity on the right -- $H_n(m)$ from the public message, $PK$ from the wire -- is public. Anyone computes the valid signature on any message directly, with no access to $sk$ [@rfc9380] [@bls01]. This is not an edge case or a timing quirk. It is total, structural forgeability on every input. The same knife cuts elsewhere: for an OPRF, knowing the discrete log of the mapped input breaks obliviousness; for a PAKE, a password element with known discrete log no longer hides the password.&lt;/p&gt;
&lt;p&gt;{`
// ILLUSTRATION, NOT AN IMPLEMENTATION.
// We model the group as integers mod a small prime order n, with
// &quot;scalar mult&quot; [k]*P written as (k * P) mod n. Discrete log is trivial
// here on purpose -- the point is the STRUCTURE of the forgery, not hardness.
function scalarMul(k, P, n) { return ((k % n) * (P % n)) % n; }&lt;/p&gt;
&lt;p&gt;const n = 97;          // toy prime group order
const G = 1;           // generator
const sk = 42;         // signer&apos;s SECRET key
const PK = scalarMul(sk, G, n);   // public key = [sk]*G&lt;/p&gt;
&lt;p&gt;// The UNSAFE map: H(m) = [Hn(m)]*G, so its discrete log Hn(m) is public.
const Hn_m = 7;                    // public hash of the message
const Hm = scalarMul(Hn_m, G, n);  // the mapped point (known DL!)&lt;/p&gt;
&lt;p&gt;// Honest signer computes sigma = [sk]*H(m)
const legitSig = scalarMul(sk, Hm, n);&lt;/p&gt;
&lt;p&gt;// Attacker never sees sk. Uses only Hn(m) and PK:
const forgedSig = scalarMul(Hn_m, PK, n);   // [Hn(m)]*PK&lt;/p&gt;
&lt;p&gt;console.log(&quot;legit signature :&quot;, legitSig);
console.log(&quot;forged signature:&quot;, forgedSig, &quot;(computed with NO secret key)&quot;);
console.log(&quot;forgery succeeds:&quot;, legitSig === forgedSig);
`}&lt;/p&gt;
&lt;h3&gt;Shortcut B: search for a valid coordinate&lt;/h3&gt;
&lt;p&gt;The other reflex fixes Property 1 and breaks Property 2. Hash the input together with a counter, test whether the candidate is a valid $x$-coordinate, and loop until it is.&lt;/p&gt;

A map that hashes the input with a counter to a candidate $x$, tests whether $x^3 + ax + b$ is a quadratic residue (a valid coordinate), and increments the counter until it is. A random $x$ works about half the time, so the expected number of tries is two [@rfc9380]. The output has *unknown* discrete log, but the number of iterations depends on the input.
&lt;p&gt;Because nobody chose the output as a multiple of $G$, its discrete log is genuinely unknown, so Property 1 holds. That is exactly why BLS and SAE could ship with it and be functionally correct. The flaw is Property 2: the loop count is a function of the input, so the running time is too. When the input is a secret -- a password, a pre-shared key -- the clock becomes an oracle. That is Dragonblood [@vr20] [@rfc9380].&lt;/p&gt;
&lt;p&gt;{`
// ILLUSTRATION, NOT AN IMPLEMENTATION.
// Count hunt-and-peck iterations for several secret inputs over a toy field.
// A constant-time map would take the SAME work for every input; this does not.
const p = 103;                 // toy prime, p = 3 mod 4
const a = 2, b = 3;            // toy curve y^2 = x^3 + a x + b&lt;/p&gt;
&lt;p&gt;function modpow(base, exp, mod) {
  let r = 1; base = base % mod;
  while (exp &amp;gt; 0) {
    if (exp &amp;amp; 1) r = (r * base) % mod;
    base = (base * base) % mod;
    exp = exp &amp;gt;&amp;gt; 1;
  }
  return r;
}
// Euler&apos;s criterion: x is a nonzero square mod p iff x^((p-1)/2) == 1
function isSquare(x, p) { return x === 0 ? true : modpow(x, (p - 1) / 2, p) === 1; }&lt;/p&gt;
&lt;p&gt;// A stand-in for H(input || ctr): deterministic, input-dependent pseudo-x.
function candidateX(seed, ctr, p) { return ((seed * 131 + ctr * 419 + 17) % p + p) % p; }&lt;/p&gt;
&lt;p&gt;function iterations(seed) {
  for (let ctr = 1; ctr &amp;lt; 200; ctr++) {
    const x = candidateX(seed, ctr, p);
    const g = (((x * x % p) * x % p) + a * x + b) % p;
    if (isSquare(g, p)) return ctr;
  }
  return -1;
}&lt;/p&gt;
&lt;p&gt;for (const pw of [11, 23, 37, 58, 71, 90]) {
  console.log(&quot;secret input &quot; + pw + &quot; -&amp;gt; &quot; + iterations(pw) + &quot; iterations&quot;);
}
console.log(&quot;Different inputs, different work: that difference is the leak.&quot;);
`}&lt;/p&gt;
&lt;p&gt;Here is the table that makes the whole article click. Two maps, three properties, and each map fails a different one.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Map&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Unknown discrete log (Prop 1)&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Constant time (Prop 2)&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Right distribution (Prop 3)&lt;/th&gt;
&lt;th&gt;The secret it spills&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;$H(m)\cdot G$&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;Violated&lt;/strong&gt; (DL is $H_n(m)$)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Satisfied&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Satisfied (uniform)&lt;/td&gt;
&lt;td&gt;The signing key&apos;s power -- universal forgery [@rfc9380] [@bls01]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Try-and-increment&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Satisfied&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;Violated&lt;/strong&gt; (loop depends on input)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;Roughly satisfied&lt;/td&gt;
&lt;td&gt;The secret input -- via handshake timing [@rfc9380] [@vr20]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

Try-and-increment is only a *vulnerability* when the mapped input is secret. A BLS signature over a *public* message runs the same variable-time loop, but the message is public, so the timing reveals nothing an attacker did not already have. The Dragonblood break works because a Wi-Fi password is a low-entropy secret. This is why the accuracy-critical phrasing is &quot;variable time is dangerous when the input is secret,&quot; never &quot;variable time is always dangerous&quot; [@rfc9380] [@vr20].
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If the input to your map is a password, a PSK, or any low-entropy secret, a data-dependent loop over it is a timing oracle. Use a constant-time map. RFC 9380 declines to even specify try-and-increment &quot;because the goal is to specify algorithms that can plausibly be computed in constant time&quot; [@rfc9380].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the requirement is now sharp, and it looks almost contradictory. You need a map whose output has unknown discrete log, like try-and-increment, but whose running time is fixed, unlike try-and-increment. You need the safety of the search without the search. For a few years it was not obvious such a thing existed. The story of how the field built it, one property at a time, is the evolution of the primitive itself.&lt;/p&gt;
&lt;h2&gt;4. Moving Each Property Into the Curve&lt;/h2&gt;
&lt;p&gt;The next thirteen years read like a chain of defenses in which the defenders keep advancing. Each generation takes one property that used to depend on the implementer&apos;s care and locks it into the construction, where it holds by mathematics rather than by discipline. Three of those steps are true property wins. The rest are the engineering that made the winning map cheap enough and general enough to run on every curve real protocols use. Being honest about which is which is the difference between a clean story and a true one.&lt;/p&gt;

flowchart TD
    Naive[&quot;Naive maps: known DL or variable time&quot;] --&amp;gt;|&quot;one leaks the DL, one leaks timing&quot;| G1[&quot;SvdW 2006: constant time, unknown DL&quot;]
    G1 --&amp;gt;|&quot;cheaper per-curve maps&quot;| G2[&quot;Icart and Simplified SWU, 2007 to 2010&quot;]
    G1 --&amp;gt;|&quot;a single map is not a random oracle&quot;| G3[&quot;BCIMRT 2010: sum of two maps&quot;]
    G2 --&amp;gt;|&quot;still a single map&quot;| G3
    G3 --&amp;gt;|&quot;map per curve family&quot;| G4[&quot;Elligator 2, 2013: Montgomery and Edwards&quot;]
    G3 --&amp;gt;|&quot;pairing curves with A zero&quot;| G5[&quot;Isogeny SSWU, 2019: BLS12-381&quot;]
    G4 --&amp;gt; RFC[&quot;RFC 9380, 2023: one interface&quot;]
    G5 --&amp;gt; RFC
&lt;h3&gt;The first constant-time map: Shallue-van de Woestijne (2006)&lt;/h3&gt;
&lt;p&gt;The contradiction from the last section -- unknown discrete log without a search -- was resolved in 2006 by Andrew Shallue and Christiaan van de Woestijne. Their map is the &quot;SW&quot; in &quot;SSWU.&quot; Given a field element $u$, algebraic identities guarantee that among a small fixed set of candidate $x$-values derived from $u$, at least one is a valid coordinate [@sw06]. So you compute the candidates, test them in a fixed order with a constant number of operations, and select a valid one. There is no rejection loop, which means no data-dependent branch, which means constant time by construction [@sw06] [@rfc9380]. The discrete log stays unknown. For the first time, a single map was simultaneously unknown-DL and constant-time on almost any curve with characteristic other than 2 or 3.&lt;/p&gt;
&lt;p&gt;The idea had roots. Schinzel and Skalba had constructed curve points deterministically for a restricted class of curves in 2004, and Skalba generalized it in 2005; Shallue and van de Woestijne further generalized and simplified that line [@ss04] [@s05] [@rfc9380]. Boneh and Franklin&apos;s 2001 supersingular map was a deterministic co-origin, but too narrow to be general infrastructure [@bf01]. SvdW survives today in RFC 9380 as the universal fallback for any curve the specialized maps do not cover [@rfc9380].&lt;/p&gt;
&lt;p&gt;But SvdW carried a flaw that every deterministic map after it would share, and it is the one no amount of cleverness in a single map can dodge.&lt;/p&gt;
&lt;h3&gt;The wall every single map hits&lt;/h3&gt;
&lt;p&gt;RFC 9380 states the obstacle plainly: the deterministic maps &quot;map to some fraction of the points&quot; on the curve, &quot;not the entire curve,&quot; which &quot;means that they cannot be used directly to construct a random oracle that outputs points on the curve&quot; [@rfc9380]. A single SvdW map hits only a constant fraction of the points and is statistically distinguishable from uniform. Any proof that models the map as a random oracle -- BLS&apos;s unforgeability, an OPRF&apos;s pseudorandomness, a password mapping -- does not go through. This is Property 3, and it is missing.&lt;/p&gt;

A formal criterion, defined by Maurer, Renner, and Holenstein in 2004, for when a constructed function can safely stand in for an ideal object such as a random oracle inside a security proof. A hash-to-curve construction is &quot;indifferentiable from a random oracle&quot; when no efficient distinguisher can separate it from a true random oracle, even given access to the underlying building blocks [@mrh04].
&lt;p&gt;Before that wall was scaled, the field spent a few years making the map cheaper.&lt;/p&gt;
&lt;h3&gt;Cheaper maps: Ulas, Icart, Simplified SWU (2007-2010)&lt;/h3&gt;
&lt;p&gt;Ulas simplified SvdW in 2007 (the &quot;U&quot; in SWU) [@u07]. Icart gave an independent closed-form map in 2009 for fields with $p \equiv 2 \pmod 3$, built on a single cube root [@icart09]. Brier and coauthors published the &quot;Simplified SWU&quot; map in 2010 for fields with $p \equiv 3 \pmod 4$ [@bcimrt10]. Each is a fixed rational formula costing roughly one field exponentiation plus a few multiplications and one inversion -- cheaper and easier to implement in constant time than SvdW&apos;s heavier parameterization.&lt;/p&gt;

A closed-form rational map from a field element to a short-Weierstrass curve point $y^2 = x^3 + Ax + B$, descended from the Shallue-van de Woestijne map via Ulas and Brier et al. In its base form it applies when $p \equiv 3 \pmod 4$ and $A\cdot B \neq 0$, and costs about one square root plus constant-time conditional selects [@u07] [@bcimrt10] [@rfc9380].
&lt;p&gt;This was progress, but not on Property 3. Cheaper did not mean uniform: each remained a single map hitting a fraction of the curve, still unusable as a random oracle [@rfc9380]. And the modular conditions left gaps. Icart needs $p \equiv 2 \pmod 3$; Simplified SWU needs $p \equiv 3 \pmod 4$; neither covers curves with $A = 0$ such as secp256k1 and BLS12-381. The deeper question was sharper and universal: could you build something provably indifferentiable out of these non-uniform pieces? The answer arrived, remarkably, in the very same 2010 paper that gave Simplified SWU.&lt;/p&gt;
&lt;h3&gt;The breakthrough: add two maps (2010)&lt;/h3&gt;
&lt;p&gt;Here is the shift the whole standard turns on. A single deterministic map cannot be a random oracle. But the &lt;em&gt;sum of two independent maps&lt;/em&gt; can be. Hash the message to two field elements, map each to a point, and add the points:&lt;/p&gt;
&lt;p&gt;$$H(m) = f\big(H_0(m)\big) + f\big(H_1(m)\big).$$&lt;/p&gt;
&lt;p&gt;Brier, Coron, Icart, Madore, Randriam, and Tibouchi proved this construction indifferentiable from a random oracle, and it recovers Property 3 while keeping Properties 1 and 2 [@bcimrt10] [@mrh04]. All three properties, at last, guaranteed at once.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A single deterministic map can &lt;em&gt;never&lt;/em&gt; be a random oracle: it reaches only a fraction of the curve and is distinguishable from uniform. The fix is structural, not a cleverer single map. Hash to two field elements, map each, and add the two points. The sum is indifferentiable from a random oracle. This is exactly why &lt;code&gt;hash_to_curve&lt;/code&gt; (the &lt;code&gt;_RO_&lt;/code&gt; suites) costs twice what &lt;code&gt;encode_to_curve&lt;/code&gt; (the &lt;code&gt;_NU_&lt;/code&gt; suites) costs: the second map is not waste, it is what buys the distribution [@bcimrt10] [@ffstv13] [@rfc9380].&lt;/p&gt;
&lt;/blockquote&gt;

flowchart TD
    M[&quot;msg and DST&quot;] --&amp;gt; HF[&quot;hash_to_field to two elements&quot;]
    HF --&amp;gt; Q0[&quot;map_to_curve of u0 gives Q0&quot;]
    HF --&amp;gt; Q1[&quot;map_to_curve of u1 gives Q1&quot;]
    Q0 --&amp;gt; ADD[&quot;add points: Q0 plus Q1&quot;]
    Q1 --&amp;gt; ADD
    ADD --&amp;gt; RO[&quot;sum is indifferentiable from a random oracle&quot;]
    RO --&amp;gt; CC[&quot;clear_cofactor&quot;]
    CC --&amp;gt; P[&quot;point in the prime-order subgroup&quot;]
&lt;p&gt;Precision matters here, because it is easy to over-credit. Brier et al. proved the sum-of-two construction indifferentiable &lt;em&gt;specifically for Icart&apos;s map&lt;/em&gt;. The general result -- that summing two copies of essentially &lt;em&gt;any&lt;/em&gt; deterministic map yields a random oracle -- is due to Farashahi, Fouque, Shparlinski, Tibouchi, and Voloch in 2013.Attribute the general theorem to FFSTV13, not to BCIMRT. RFC 9380&apos;s Appendix A is careful about this: BCIMRT proved it for Icart&apos;s method; FFSTV13 &quot;improve the analysis, showing that it applies to essentially all deterministic mappings&quot; [@rfc9380] [@ffstv13]. After 2010, nothing about Property 3 was left to invent. What remained was coverage: the sum-of-two construction needs a concrete constant-time inner map for &lt;em&gt;your&lt;/em&gt; curve family, and two families were still uncovered.&lt;/p&gt;
&lt;h3&gt;Covering the remaining curves: Elligator 2 (2013) and isogeny SSWU (2019)&lt;/h3&gt;
&lt;p&gt;The Montgomery and twisted-Edwards curves -- Curve25519, Curve448, and the Edwards forms behind modern signature and OPRF suites -- needed their own map, because SSWU targets short-Weierstrass curves. Elligator 2 supplied it.&lt;/p&gt;

An efficient, invertible map from a field element to a point on any curve that has a point of order 2 (Montgomery or twisted-Edwards form), introduced by Bernstein, Hamburg, Krasnova, and Lange in 2013. RFC 9380 uses it for the Curve25519/448 and edwards25519/448 families and the ristretto255/decaf448 groups [@bhkl13] [@rfc9380].
&lt;p&gt;Elligator&apos;s origin is a small marvel. It was built for censorship resistance, a way to make a public key look like uniform random bytes so a network censor cannot even tell a key exchange is happening.The elligator.org site records the pivot in its own words: a number of protocols &quot;need a way to map an arbitrary input to an elliptic curve point whose factorisation is unknown (&apos;hashing to an elliptic curve&apos;), such as verifiable random functions (VRFs) and oblivious pseudorandom functions (OPRFs).&quot; A steganography tool became hash-to-curve infrastructure [@elligator-site]. The tool built to &lt;em&gt;hide&lt;/em&gt; a key exchange became the constant-time inner map for an entire curve family&apos;s hashing.&lt;/p&gt;
&lt;p&gt;The last gap was the pairing-friendly curve BLS needs. BLS12-381 has $A = 0$, exactly where Simplified SWU does not apply. In 2019 Riad Wahby and Dan Boneh closed it: map with SSWU onto an &lt;em&gt;isogenous&lt;/em&gt; curve where the parameters are admissible, then push the point back to the target curve through a fixed isogeny of small degree [@wb19] [@rfc9380]. The result is constant-time and works in any characteristic, and it is what made constant-time hashing to BLS12-381 practical at blockchain scale. Their headline measurement is the number that ends an old excuse: the constant-time functions run &quot;within 9% of the fastest, non-constant-time alternatives&quot; [@wb19].&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Key idea&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;What it secured&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;$H(m)\cdot G$&lt;/td&gt;
&lt;td&gt;~2001&lt;/td&gt;
&lt;td&gt;scalar-mult the generator&lt;/td&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;nothing (DL is known)&lt;/td&gt;
&lt;td&gt;never safe as a map [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Try-and-increment&lt;/td&gt;
&lt;td&gt;2001-2008&lt;/td&gt;
&lt;td&gt;search for a valid coordinate&lt;/td&gt;
&lt;td&gt;variable&lt;/td&gt;
&lt;td&gt;unknown discrete log&lt;/td&gt;
&lt;td&gt;deprecated for secret inputs [@bls01] [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SvdW&lt;/td&gt;
&lt;td&gt;2006&lt;/td&gt;
&lt;td&gt;rational parameterization, no loop&lt;/td&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;constant time by construction&lt;/td&gt;
&lt;td&gt;general fallback [@sw06]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ulas / Icart / Simplified SWU&lt;/td&gt;
&lt;td&gt;2007-2010&lt;/td&gt;
&lt;td&gt;cheaper closed-form maps&lt;/td&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;efficiency, per-curve forms&lt;/td&gt;
&lt;td&gt;inner maps [@u07] [@icart09] [@bcimrt10]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BCIMRT sum-of-two&lt;/td&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;add two mapped points&lt;/td&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;random-oracle distribution&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;_RO_&lt;/code&gt; core [@bcimrt10] [@ffstv13]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elligator 2&lt;/td&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;invertible map for order-2 curves&lt;/td&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;Montgomery/Edwards coverage&lt;/td&gt;
&lt;td&gt;active [@bhkl13]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isogeny SSWU&lt;/td&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;map via an isogenous curve&lt;/td&gt;
&lt;td&gt;constant&lt;/td&gt;
&lt;td&gt;$A=0$ pairing-curve coverage&lt;/td&gt;
&lt;td&gt;active [@wb19]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Every curve family that deployed protocols use now has a constant-time, unknown-DL inner map, and the sum-of-two construction wraps any of them into a random oracle. All three properties are achievable everywhere. But an implementer still has to assemble the hashing, the map, the cofactor clearing, the domain separation, and the choice of variant, by hand, and any one of those is a place to slip. What packages the whole lineage so you cannot get it wrong?&lt;/p&gt;
&lt;h2&gt;5. RFC 9380: All Three Properties at Once&lt;/h2&gt;
&lt;p&gt;In August 2023 the CFRG published RFC 9380, &quot;Hashing to Elliptic Curves,&quot; by Armando Faz-Hernandez, Sam Scott, Nick Sullivan, Riad Wahby, and Christopher Wood [@rfc9380]. It invents no new map. It does something more useful: it folds two decades of maps into a single interface an ordinary engineer cannot easily misuse, and it makes every security-relevant choice legible in the name.&lt;/p&gt;
&lt;p&gt;The pipeline has three stages [@rfc9380]:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;hash_to_field&lt;/code&gt;&lt;/strong&gt; turns the input and a domain tag into one or more field elements, using &lt;code&gt;expand_message_xmd&lt;/code&gt; for fixed-output hashes like SHA-256 and &lt;code&gt;expand_message_xof&lt;/code&gt; for extendable-output functions like SHAKE.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;map_to_curve&lt;/code&gt;&lt;/strong&gt; sends each field element to a curve point using the map that fits the curve: Simplified SWU for short-Weierstrass curves, isogeny-SSWU for the $A = 0$ pairing curves, Elligator 2 for Montgomery and Edwards curves, and SvdW as the general fallback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;clear_cofactor&lt;/code&gt;&lt;/strong&gt; forces the result into the prime-order subgroup.&lt;/li&gt;
&lt;/ol&gt;

flowchart TD
    IN[&quot;input msg and DST&quot;] --&amp;gt; HF[&quot;hash_to_field&quot;]
    HF --&amp;gt; BR{&quot;which variant&quot;}
    BR --&amp;gt;|&quot;_RO_ hash_to_curve&quot;| TWO[&quot;two field elements, two maps, add&quot;]
    BR --&amp;gt;|&quot;_NU_ encode_to_curve&quot;| ONE[&quot;one field element, one map&quot;]
    TWO --&amp;gt; CC[&quot;clear_cofactor&quot;]
    ONE --&amp;gt; CC
    CC --&amp;gt; OUT[&quot;point in the prime-order subgroup&quot;]
&lt;h3&gt;The two-element margin is a proven bound&lt;/h3&gt;
&lt;p&gt;The first stage hides a subtlety worth pausing on. Reducing a uniformly random integer modulo $p$ is not exactly uniform, because the top of the range wraps around. RFC 9380 fixes this by oversampling.&lt;/p&gt;

The first pipeline stage: expand the input and the domain tag into field elements. It draws $L = \lceil(\lceil\log_2 p\rceil + k)/8\rceil$ bytes per element and reduces modulo $p$, so each element is within statistical distance $2^{-k}$ of uniform. The standard suites set $k = 128$, giving a $2^{-128}$ margin [@rfc9380].
&lt;p&gt;That $2^{-128}$ is a proven statistical bound, not a hopeful heuristic, and it is exactly why the byte-length formula is what it is [@rfc9380].&lt;/p&gt;
&lt;h3&gt;The variant is in the name&lt;/h3&gt;
&lt;p&gt;The choice between a uniform, proof-usable point and a cheaper nonuniform one is encoded directly in the suite name. &lt;code&gt;hash_to_curve&lt;/code&gt;, the &lt;code&gt;_RO_&lt;/code&gt; family, runs the two-element sum-of-two construction and is indifferentiable from a random oracle. &lt;code&gt;encode_to_curve&lt;/code&gt;, the &lt;code&gt;_NU_&lt;/code&gt; family, runs a single map at about half the cost and produces a nonuniform distribution [@rfc9380]. The trailing tag is not decoration; it tells you whether the output is safe to feed a proof that assumes a random oracle.&lt;/p&gt;
&lt;p&gt;RFC 9380 suite identifiers follow the pattern &lt;code&gt;CURVE_XMD:HASH_MAP_VARIANT_&lt;/code&gt;. Once you can read one, you can read them all.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;In &lt;code&gt;BLS12381G2_XMD:SHA-256_SSWU_RO_&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;What it fixes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Curve / group&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BLS12381G2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the target group: BLS12-381, subgroup G2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expander&lt;/td&gt;
&lt;td&gt;&lt;code&gt;XMD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expand_message_xmd&lt;/code&gt; (a fixed-output hash)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SHA-256&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the underlying hash function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inner map&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SSWU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Simplified SWU (isogeny variant here, since $A=0$)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variant&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_RO_&lt;/code&gt;: uniform, indifferentiable from a random oracle&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read &lt;code&gt;edwards25519_XMD:SHA-512_ELL2_RO_&lt;/code&gt; the same way and it decodes itself: the edwards25519 group, XMD expansion over SHA-512, the Elligator 2 map, uniform variant [@rfc9380]. The name is the spec.&lt;/p&gt;
&lt;p&gt;{`
// Decompose an RFC 9380 suite ID: CURVE_EXPANDER:HASH_MAP_VARIANT_
function parseSuite(id) {
  const [left, right] = id.split(&quot;:&quot;);
  const leftParts = left.split(&quot;&lt;em&gt;&quot;);
  const expander = leftParts.pop();          // XMD or XOF
  const curve = leftParts.join(&quot;&lt;/em&gt;&quot;);         // e.g. BLS12381G2, edwards25519
  const rp = right.split(&quot;&lt;em&gt;&quot;).filter(function (s) { return s.length &amp;gt; 0; });
  const hash = rp[0];                         // SHA-256, SHA-512, SHAKE256
  const variant = rp[rp.length - 1];          // RO or NU
  const map = rp.slice(1, rp.length - 1).join(&quot;&lt;/em&gt;&quot;); // SSWU, ELL2, SVDW
  return { curve: curve, expander: expander, hash: hash, map: map,
           variant: variant,
           uniform: variant === &quot;RO&quot; ? &quot;uniform (random oracle)&quot; : &quot;nonuniform&quot; };
}&lt;/p&gt;
&lt;p&gt;[&quot;BLS12381G2_XMD:SHA-256_SSWU_RO_&quot;,
 &quot;edwards25519_XMD:SHA-512_ELL2_RO_&quot;,
 &quot;P256_XMD:SHA-256_SSWU_NU_&quot;].forEach(function (id) {
  console.log(id);
  console.log(&quot;   &quot;, JSON.stringify(parseSuite(id)));
});
`}&lt;/p&gt;
&lt;h3&gt;The two steps you must not skip&lt;/h3&gt;
&lt;p&gt;Two pipeline stages are easy to forget and dangerous to omit. The first is the domain tag.&lt;/p&gt;

A byte string, unique to a protocol and its version, mixed into `hash_to_field` so the same input maps to different points in different protocols. RFC 9380 makes it mandatory; DSTs longer than 255 bytes are themselves hashed before use. Without a unique DST, two protocols that share a curve can be driven to collide [@rfc9380].
&lt;p&gt;The second is cofactor clearing, the final stage that lands the point where the group arithmetic is safe.&lt;/p&gt;

The final pipeline stage: multiply the mapped point (or apply a faster endomorphism-based equivalent) by the curve&apos;s cofactor to force it into the prime-order subgroup, closing off small-subgroup attacks. edwards25519 has cofactor 8; BLS12-381&apos;s G1 and G2 have large cofactors with fast clearing methods [@rfc9380].
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The everyday act is now tiny: name a suite. The suite ID fixes the curve, the expander, the hash, the inner map, and the distribution variant, and a vetted library does the rest. You are not choosing a construction. You are declaring your curve and your distribution need, and RFC 9380 has already made the safe choice for each [@rfc9380].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Step back and the three properties resolve into one sentence. The mapped point had to be a &lt;em&gt;stranger&lt;/em&gt; -- its discrete log unknown, or forgery follows. It had to &lt;em&gt;arrive on time&lt;/em&gt; -- in work independent of the input, or the clock leaks the secret. And it had to &lt;em&gt;look the part&lt;/em&gt; -- drawn from a distribution the proof can treat as a random oracle, or the proof does not hold.&lt;/p&gt;

The point had to be a stranger, arrive on time, and look the part. RFC 9380 is the standard that guarantees all three, and encodes which ones you asked for in a single suite name.
&lt;p&gt;That framing also closes the loop this article opened. WPA3&apos;s answer to Dragonblood is Hash-to-Element, or H2E.&lt;/p&gt;

After Dragonblood, WPA3-Personal replaced hunt-and-peck with H2E, a constant-time, SSWU-based derivation of the SAE password element, and the Wi-Fi standard was updated to compute the password element &quot;in constant time&quot; [@dragonblood-site]. RFC 9380 supplies the governing principle: a constant-time map is required when the input is secret [@rfc9380]. The hostap and w1.fi advisories track the corresponding constant-time SAE patches shipped to real deployments [@w1fi-security]. The precise IEEE 802.11 clause is not reproduced here; the point is that the fix is exactly the constant-time approach this standard consolidates.
&lt;p&gt;The doorway is finally built to spec. The natural next question is who is actually walking through it, because the answer is: far more of your daily software than you would guess.&lt;/p&gt;
&lt;h2&gt;6. Who Quietly Depends on This Today&lt;/h2&gt;
&lt;p&gt;You have almost certainly relied on hash-to-curve this week without noticing. It sits under signatures that finalize blockchains, under the private authentication that hides your browsing from a token issuer, under the password login protocol that never sees your password, and under WPA3. None of these advertise the primitive. All of them break if it is wrong.&lt;/p&gt;

flowchart TD
    BLS[&quot;BLS signatures on Ethereum&quot;] --&amp;gt; H2C[&quot;RFC 9380 hash-to-curve&quot;]
    OPRF[&quot;OPRF and VOPRF (RFC 9497)&quot;] --&amp;gt;|&quot;one HashToGroup call&quot;| H2C
    OPAQUE[&quot;OPAQUE aPAKE (RFC 9807)&quot;] --&amp;gt;|&quot;indirect&quot;| OPRF
    PP[&quot;Privacy Pass (RFC 9578)&quot;] --&amp;gt; OPRF
    CPace[&quot;CPace PAKE (draft)&quot;] --&amp;gt; H2C
    SAE[&quot;WPA3-Personal SAE&quot;] --&amp;gt;|&quot;H2E&quot;| H2C
&lt;h3&gt;BLS signatures and Ethereum consensus&lt;/h3&gt;
&lt;p&gt;The highest-volume deployment, by signature-verification throughput, is BLS. Every BLS signature hashes its message to a point in G2 and scales it by the secret key, so &lt;code&gt;hash_to_point&lt;/code&gt; is not an accessory but the heart of the scheme [@draft-bls-signature-07]. Ethereum&apos;s consensus specification fixes this for validator signatures: it adopts the suite &lt;code&gt;BLS12381G2_XMD:SHA-256_SSWU_RO_&lt;/code&gt; -- the isogeny-SSWU map wrapped in the &lt;code&gt;_RO_&lt;/code&gt; construction -- so every consensus client hashes exactly this way [@eth-consensus-specs] [@draft-bls-signature-07] [@rfc9380]. The &lt;code&gt;_RO_&lt;/code&gt; variant is not optional here: the unforgeability proof models the map as a random oracle, so a nonuniform &lt;code&gt;_NU_&lt;/code&gt; map would void the guarantee. Notably, the scheme the entire Ethereum validator set relies on is still specified only by an Internet-Draft, which the consensus spec references directly [@eth-consensus-specs] [@draft-bls-signature-07].Unlike ECDSA, BLS is deterministic and uses no per-signature nonce, so it sidesteps the &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 failures that have repeatedly broken ECDSA deployments&lt;/a&gt; (Part 8). Its one hash-to-curve call becomes the security-critical primitive in place of the nonce [@draft-bls-signature-07].&lt;/p&gt;
&lt;h3&gt;OPRFs, and everything built on them&lt;/h3&gt;
&lt;p&gt;The second large consumer is the oblivious pseudorandom function and its verifiable and partially-oblivious variants, standardized in RFC 9497 [@rfc9497].&lt;/p&gt;

A two-party protocol in which a client learns $F(k, x)$ for its input $x$ and the server&apos;s key $k$, while the server learns neither $x$ nor the result. The client&apos;s first step hashes $x$ to a curve point and blinds it; that single `HashToGroup` call is the protocol&apos;s entire hash-to-curve surface [@rfc9497].
&lt;p&gt;The dependency is remarkably concentrated. The client&apos;s &lt;code&gt;Blind(input)&lt;/code&gt; computes &lt;code&gt;HashToGroup(input)&lt;/code&gt; and then blinds the point; every other message is scalar arithmetic and proofs. So the whole hash-to-curve exposure of an OPRF is one call, instantiated with Elligator 2 for ristretto255 and SSWU for the P-256, P-384, and P-521 suites [@rfc9497].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For an OPRF, the entire hash-to-curve attack surface is the single &lt;code&gt;HashToGroup&lt;/code&gt; call in the client&apos;s &lt;code&gt;Blind&lt;/code&gt; step. Get that one call right -- correct suite, correct DST, identity element rejected on the wire -- and the primitive is handled. Get it wrong and obliviousness or correctness collapses [@rfc9497].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because OPRFs are a building block, their hash-to-curve dependency propagates. OPAQUE, the augmented PAKE standardized as RFC 9807 in July 2025, lets a server authenticate a user by password without ever learning the password, and it does so by running an OPRF; its hash-to-curve dependency is indirect but real, flowing through RFC 9497 [@rfc9807] [@rfc9497]. &lt;a href=&quot;https://paragmali.com/blog/the-age-gate-that-doesnt-know-your-age-how-anonymous-credent/&quot; rel=&quot;noopener&quot;&gt;Privacy Pass&lt;/a&gt;, standardized across RFC 9576, RFC 9577, and RFC 9578 in June 2024, issues anonymous authentication tokens; its privately verifiable issuance is &quot;based on the Oblivious Pseudorandom Function,&quot; with token type 0x0001 being a VOPRF over P-384, which is the SSWU path [@rfc9578] [@rfc9576] [@rfc9577].&lt;/p&gt;
&lt;h3&gt;PAKEs and Wi-Fi&lt;/h3&gt;
&lt;p&gt;Two more consumers map a secret rather than a public message. CPace, a balanced PAKE still in CFRG draft with datatracker status &quot;Waiting for IRTF Chair,&quot; maps the password to a session generator using SSWU or Elligator 2 depending on the group [@draft-cpace]. And WPA3-Personal&apos;s SAE now derives its password element with the constant-time H2E map, the fix that closed Dragonblood [@dragonblood-site].&lt;/p&gt;
&lt;p&gt;The whole deployed picture fits in one adoption map.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Consumer&lt;/th&gt;
&lt;th&gt;Where hash-to-curve is called&lt;/th&gt;
&lt;th&gt;Suite / map&lt;/th&gt;
&lt;th&gt;Standard status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;BLS / Ethereum consensus&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hash_to_point(msg)&lt;/code&gt; into G2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BLS12381G2_XMD:SHA-256_SSWU_RO_&lt;/code&gt; [@draft-bls-signature-07]&lt;/td&gt;
&lt;td&gt;Internet-Draft [@draft-bls-signature-07]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPRF / VOPRF / POPRF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Blind&lt;/code&gt; calls &lt;code&gt;HashToGroup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ristretto255 to Elligator 2; P-curves to SSWU [@rfc9497]&lt;/td&gt;
&lt;td&gt;RFC 9497 (2023) [@rfc9497]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OPAQUE (aPAKE)&lt;/td&gt;
&lt;td&gt;indirect, via its OPRF&lt;/td&gt;
&lt;td&gt;inherits the OPRF suite [@rfc9807]&lt;/td&gt;
&lt;td&gt;RFC 9807 (2025) [@rfc9807]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy Pass&lt;/td&gt;
&lt;td&gt;VOPRF token issuance&lt;/td&gt;
&lt;td&gt;P-384 VOPRF to SSWU [@rfc9578]&lt;/td&gt;
&lt;td&gt;RFC 9576/9577/9578 (2024) [@rfc9578]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPace (balanced PAKE)&lt;/td&gt;
&lt;td&gt;password to session generator&lt;/td&gt;
&lt;td&gt;SSWU or Elligator 2 by group [@draft-cpace]&lt;/td&gt;
&lt;td&gt;draft, &quot;Waiting for IRTF Chair&quot; [@draft-cpace]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WPA3-Personal (SAE)&lt;/td&gt;
&lt;td&gt;password element (PWE)&lt;/td&gt;
&lt;td&gt;H2E, constant-time SSWU [@dragonblood-site]&lt;/td&gt;
&lt;td&gt;IEEE 802.11&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;What actually ships&lt;/h3&gt;
&lt;p&gt;Under all of this sit a few load-bearing libraries. The blst library implements BLS12-381 hash-to-curve in C and assembly, is compliant with RFC 9380, was audited by NCC Group in January 2021, and is the subject of an ongoing Galois formal-verification effort [@blst] [@blst-verification]. Cloudflare&apos;s CIRCL provides RFC 9380 hash-to-curve and RFC 9497 OPRFs in Go, though it labels itself experimental and cautions against production use of parts of it [@circl]. In Rust, curve25519-dalek exposes an Elligator-based one-way map through &lt;code&gt;RistrettoPoint::from_uniform_bytes&lt;/code&gt; [@curve25519-dalek].&lt;/p&gt;
&lt;p&gt;The headline is quiet by design: the primitive is standardized and load-bearing under a stack of shipping protocols, and almost nobody who depends on it has to think about it. That is what a well-built doorway looks like. Which raises a fair question: if every consumer just names a suite, is there ever a real choice to make, or has the standard decided everything for you?&lt;/p&gt;
&lt;h2&gt;7. The Decision Matrix&lt;/h2&gt;
&lt;p&gt;There is no &quot;best&quot; hash-to-curve, and that is a feature. Almost every knob is set for you by something structural: the curve you already committed to, or the distribution your proof already assumes. The value of a field guide is naming which axis governs which choice, and where a wrong turn is a vulnerability rather than a mere preference.&lt;/p&gt;
&lt;h3&gt;Distribution need decides the variant&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;_RO_&lt;/code&gt; versus &lt;code&gt;_NU_&lt;/code&gt; choice is governed by one question: does your proof need a random oracle? For BLS, OPRFs, and password mapping, it does, so &lt;code&gt;_RO_&lt;/code&gt; is the default. The &lt;code&gt;_NU_&lt;/code&gt; variant is an optimization you reach for only with a specific proof that non-uniformity is acceptable, because its output is measurably not uniform.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;&lt;code&gt;hash_to_curve&lt;/code&gt; (&lt;code&gt;_RO_&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;encode_to_curve&lt;/code&gt; (&lt;code&gt;_NU_&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Field elements and maps&lt;/td&gt;
&lt;td&gt;2 elements, 2 maps, 1 addition&lt;/td&gt;
&lt;td&gt;1 element, 1 map&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Relative cost&lt;/td&gt;
&lt;td&gt;about twice &lt;code&gt;_NU_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;baseline, about half of &lt;code&gt;_RO_&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output distribution&lt;/td&gt;
&lt;td&gt;indifferentiable from a random oracle [@rfc9380]&lt;/td&gt;
&lt;td&gt;nonuniform: at least one eighth of G, weights vary by up to a factor of four [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proof compatibility&lt;/td&gt;
&lt;td&gt;usable where a proof assumes a random oracle&lt;/td&gt;
&lt;td&gt;only where non-uniformity is provably sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision rule&lt;/td&gt;
&lt;td&gt;default for BLS, OPRF, PAKE mapping&lt;/td&gt;
&lt;td&gt;optimization only, with a proof&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Getting this backward is a shippable misuse: a &lt;code&gt;_NU_&lt;/code&gt; map where the proof assumed &lt;code&gt;_RO_&lt;/code&gt; produces a point from a distribution the proof never agreed to, which is the third of our three failures made concrete [@rfc9380].&lt;/p&gt;
&lt;h3&gt;Curve family decides the map&lt;/h3&gt;
&lt;p&gt;The choice among the inner maps is not a matter of taste at all. It is fixed by the shape of your curve.&lt;/p&gt;

flowchart TD
    Q{&quot;what curve do you have&quot;}
    Q --&amp;gt;|&quot;short Weierstrass, p is 3 mod 4&quot;| SSWU[&quot;Simplified SWU&quot;]
    Q --&amp;gt;|&quot;A is zero: secp256k1, BLS12-381&quot;| ISO[&quot;Isogeny SSWU&quot;]
    Q --&amp;gt;|&quot;Montgomery or Edwards&quot;| ELL[&quot;Elligator 2&quot;]
    Q --&amp;gt;|&quot;none of the above&quot;| SVDW[&quot;SvdW fallback&quot;]
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Map&lt;/th&gt;
&lt;th&gt;Curve condition&lt;/th&gt;
&lt;th&gt;Curves in RFC 9380&lt;/th&gt;
&lt;th&gt;Dominant cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Simplified SWU&lt;/td&gt;
&lt;td&gt;short-Weierstrass, $p \equiv 3 \pmod 4$, $AB \neq 0$&lt;/td&gt;
&lt;td&gt;P-256, P-384, P-521&lt;/td&gt;
&lt;td&gt;about one square root plus selects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isogeny SSWU&lt;/td&gt;
&lt;td&gt;$A = 0$ Weierstrass&lt;/td&gt;
&lt;td&gt;secp256k1, BLS12-381 G1/G2&lt;/td&gt;
&lt;td&gt;SSWU on $E&apos;$ plus one isogeny evaluation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elligator 2&lt;/td&gt;
&lt;td&gt;curve with a point of order 2&lt;/td&gt;
&lt;td&gt;curve25519/448, edwards25519/448, ristretto255/decaf448&lt;/td&gt;
&lt;td&gt;about one exponentiation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SvdW&lt;/td&gt;
&lt;td&gt;any curve, characteristic other than 2 or 3&lt;/td&gt;
&lt;td&gt;fallback when none of the above fit&lt;/td&gt;
&lt;td&gt;a few exponentiations and inversions&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; If you find yourself deliberating between SSWU and Elligator 2, stop: your curve has already decided. SSWU serves short-Weierstrass curves, isogeny-SSWU serves the $A=0$ pairing curves, and Elligator 2 serves Montgomery and Edwards curves. The only choice genuinely left to you is the variant, &lt;code&gt;_RO_&lt;/code&gt; or &lt;code&gt;_NU_&lt;/code&gt;, and the default is &lt;code&gt;_RO_&lt;/code&gt; [@rfc9380].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The two knobs that are real, but small&lt;/h3&gt;
&lt;p&gt;Two remaining choices are genuine but low-stakes. The expander, &lt;code&gt;expand_message_xmd&lt;/code&gt; versus &lt;code&gt;expand_message_xof&lt;/code&gt;, follows your hash family: XMD for SHA-2, XOF for SHAKE, and neither dominates on security, so you pick the hash you already have [@rfc9380]. And within BLS, you can hash to either pairing group.&lt;/p&gt;

BLS can hash to either pairing group. Hashing to G2 is more expensive, because G2 lives over the larger field $\mathbb{F}_{p^2}$ and has a larger cofactor, but the minimal-pubkey-size setting Ethereum uses signs in G2. Minimal-signature-size deployments hash to the cheaper G1 instead. This is a real cost-versus-size trade-off, not a security decision, and it is why fast G2 hashing has its own literature [@rfc9380] [@sbcdk09].
&lt;h3&gt;The number that ends an old argument&lt;/h3&gt;
&lt;p&gt;For years, the excuse for variable-time hunt-and-peck was that constant-time maps were too slow. Wahby and Boneh&apos;s nine-percent measurement retired it: constant time is essentially free here, and simpler to implement correctly, which is why RFC 9380 can require it without asking anyone to pay for the safety [@wb19].&lt;/p&gt;
&lt;p&gt;It is worth remembering how strange this matrix would look to a cryptographer of 1995. In a finite field, hashing into the group is a single modular reduction with no decision matrix at all [@miller85]. Elliptic curves bought hard discrete logs and small keys, and the bill came due as this exact table: a real algorithm, with real parameters, that you must get right. Two maps, an oversampling margin, a distribution that is deliberately &quot;almost uniform&quot;: none of it is arbitrary. The next section proves they are the right choices, not just good ones.&lt;/p&gt;
&lt;h2&gt;8. Why Two Maps, and How Uniform Is &quot;Uniform&quot;&lt;/h2&gt;
&lt;p&gt;The two-map construction can feel like a clever hack. It is not. It is the provable optimum, and the theory that says so also explains the oversampling margin and the one caveat you must respect.&lt;/p&gt;
&lt;h3&gt;The lower bound that forces two maps&lt;/h3&gt;
&lt;p&gt;Start with what a single map cannot do. No efficient deterministic map to a general elliptic curve is both surjective and uniform. Its image is only a constant fraction of the curve, with some points weighted more heavily than others. RFC 9380 quantifies the residual for the single-map case exactly: the output distribution &quot;includes at least one eighth of the points in G,&quot; and the probability of points &quot;varies by at most a factor of four&quot; [@rfc9380]. Those bounds descend from the analyses of the individual maps [@sw06] [@bhkl13] [@ft12]. A distribution that misses seven-eighths of the group in the worst case and re-weights the rest simply cannot be indifferentiable from a uniform random oracle. One map is not enough, and this is why.&lt;/p&gt;
&lt;h3&gt;The matching upper bound&lt;/h3&gt;
&lt;p&gt;Now the other side. Hashing to two field elements, mapping each, and adding the points &lt;em&gt;is&lt;/em&gt; indifferentiable from a random oracle, proven for Icart&apos;s map by Brier et al. and generalized to essentially all maps by Farashahi et al. [@bcimrt10] [@ffstv13] [@mrh04]. The lower bound says one map cannot suffice; the upper bound says two do. The gap is closed. The &lt;code&gt;_RO_&lt;/code&gt; construction is the tight, achievable answer to &quot;produce a uniform, proof-usable point,&quot; and RFC 9380 standardizes precisely that optimum, no more and no less [@rfc9380].&lt;/p&gt;
&lt;h3&gt;Why the oversampling margin is what it is&lt;/h3&gt;
&lt;p&gt;The same &quot;prove it, do not hope it&quot; discipline governs &lt;code&gt;hash_to_field&lt;/code&gt;. Reducing a uniform $L$-byte integer modulo $p$ leaves a small bias from the wrap-around at the top of the range. Oversampling by $k$ bits, with&lt;/p&gt;
&lt;p&gt;$$L = \left\lceil \frac{\lceil \log_2 p \rceil + k}{8} \right\rceil,$$&lt;/p&gt;
&lt;p&gt;drives the statistical distance from uniform below $2^{-k}$. RFC 9380 fixes $k = 128$, so every field element is within $2^{-128}$ of uniform [@rfc9380]. That is a proven bound, which is why truncating the byte length is not a harmless optimization but a real weakening of the margin.&lt;/p&gt;
&lt;h3&gt;The one caveat that survives all of this&lt;/h3&gt;
&lt;p&gt;Here the humility arrives. &quot;Indifferentiable from a random oracle&quot; does not mean &quot;safe to drop into any random-oracle proof.&quot;&lt;/p&gt;

RFC 9380 cites Ristenpart, Shacham, and Shrimpton for the sharpest limit on the whole approach: &quot;not all security proofs that rely on random oracles continue to hold when those oracles are replaced by indifferentiable functionalities&quot; [@rfc9380]. So `_RO_` is the right default, but a protocol&apos;s proof still has to be the composable kind that survives the substitution. Indifferentiability is a strong guarantee, not an unconditional one.
&lt;p&gt;There is a smaller but instructive caveat in the same spirit. Either encoding can, with probability roughly $1/r$ for a random input, output the identity element.That probability is negligible and it is infeasible to find such an input deliberately, so RFC 9380 says protocols &quot;SHOULD NOT add a special case to detect and &apos;fix&apos; the identity element.&quot; The special case is itself a bug surface, more likely to introduce a flaw than to prevent one [@rfc9380]. The right move is to do nothing special, which is counterintuitive enough that people get it wrong by trying too hard.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For classical elliptic curves the core problem is solved in the strongest sense available: a single map provably cannot be a uniform random oracle, the two-map sum provably is, and RFC 9380 standardizes exactly that optimum. What is left is not the classical mathematics. It is implementation assurance and the post-quantum question [@rfc9380] [@ffstv13].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One boundary is worth stating precisely, because it sets up everything still open. The &quot;unknown discrete log&quot; property adds no new hardness assumption: it inherits the elliptic-curve discrete-log hardness of the target group, the same assumption Part 15 examined [@rfc9380]. And so, like all elliptic-curve cryptography, it falls to Shor&apos;s algorithm on a fault-tolerant quantum computer. Post-quantum &lt;em&gt;signatures&lt;/em&gt; sidestep the primitive entirely, because they use no curve. But the &lt;em&gt;consumers that specifically need a point of unknown discrete log&lt;/em&gt; -- OPRFs and VRFs -- have no such escape. That is where the frontier actually is.&lt;/p&gt;
&lt;h2&gt;9. Where It Is Still Moving&lt;/h2&gt;
&lt;p&gt;A solved primitive with live frontiers sounds like a contradiction, but hash-to-curve is exactly that. The classical mathematics is settled; the engineering, the assurance, and the post-quantum future are not.&lt;/p&gt;
&lt;h3&gt;Constant time, all the way down&lt;/h3&gt;
&lt;p&gt;RFC 9380 guarantees constant time at the interface: no data-dependent loop. The guarantee has to hold much further down, though, through the field primitives the map calls -- the square root, the sign function &lt;code&gt;sgn0&lt;/code&gt;, the zero-guarded inversion &lt;code&gt;inv0&lt;/code&gt;, general inversion, and the conditional selects -- each of which can leak through timing, cache, or micro-architecture if written naively [@rfc9380]. And the map runs on secrets: passwords in SAE and CPace, blinded inputs in OPRFs. A leak in one field routine re-opens the exact class of attack Dragonblood exploited.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Constant time must be structural in the map and then verified down to the field primitives, not patched in after the fact. Dragonblood&apos;s second act (Section 1) is the standing proof: fixing timing curve by curve is how you get burned twice [@rfc9380] [@dragonblood-site].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The expensive curves&lt;/h3&gt;
&lt;p&gt;Hashing to G2 of BLS12-381, over the field $\mathbb{F}_{p^2}$ and with a large cofactor, is markedly costlier than hashing to G1. Faster maps and faster cofactor clearing are open engineering targets, and they matter because Ethereum verifies huge volumes of signatures, so every millisecond of G2 hashing multiplies across the network. Wahby and Boneh brought constant-time hashing within nine percent of variable-time, and the fast-G2 cofactor-clearing line descends from work like Scott and coauthors&apos; &quot;Fast Hashing to G2,&quot; but G2 remains the expensive path, and each new pairing-friendly curve keeps the problem alive [@wb19] [@sbcdk09].&lt;/p&gt;
&lt;h3&gt;Machine-checked implementations&lt;/h3&gt;
&lt;p&gt;Because constant time is hard to get right, the field increasingly wants implementations proven correct by machine, not just audited by eye. The leading edge is visible in blst, the highest-volume hash-to-G2 implementation: it was audited by NCC Group in January 2021, and its formal verification by Galois is, in the project&apos;s own words, ongoing [@blst] [@blst-verification]. That is the honest status: verification of a deployed hash-to-curve path is in progress, not finished. Full end-to-end machine-checked hash-to-curve -- map, cofactor clearing, and constant time, across curve families -- remains open.&lt;/p&gt;
&lt;h3&gt;The genuinely open frontier: post-quantum&lt;/h3&gt;
&lt;p&gt;The deepest gap is post-quantum. Signatures have a post-quantum future without curves, so they abandon hash-to-curve. The unknown-DL consumers do not have that luxury. OPRFs and VRFs -- and therefore OPAQUE, Privacy Pass, and anonymous-credential systems built on them -- rest on the hardness of the elliptic-curve discrete log, and a quantum adversary breaks that foundation.Lattice- and isogeny-based OPRF and VRF constructions exist in the research literature, but none is competitive with the elliptic-curve versions in bandwidth or round complexity, and none is standardized. This guide states the frontier without pinning a specific paper, holding to the same citation standard as the rest of the series. There is no drop-in &quot;post-quantum hash-to-curve,&quot; because the whole unknown-DL abstraction has to be rebuilt on different assumptions, and the competitive replacement does not yet exist.&lt;/p&gt;
&lt;h3&gt;Steady-state engineering&lt;/h3&gt;
&lt;p&gt;Finally, the everyday work: as deployments adopt new pairing-friendly curves (BLS12-377 and BW6 are recent examples) and new prime-order abstractions, each needs its admissible map and cofactor-clearing constants worked out and folded into the suite registry [@rfc9380]. This keeps the &quot;which map for which curve&quot; table a living document rather than a finished one. Given all of it, the practical question remains: what should you actually do on Monday?&lt;/p&gt;
&lt;h2&gt;10. The Decision Rules, Made Operational&lt;/h2&gt;
&lt;p&gt;Here is the spine turned into something you can apply to a code review this afternoon. The rules are short because the standard did the hard part; your job is to name the right suite and not undo its work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use a vetted suite, never a hand-rolled map.&lt;/strong&gt; Always call a RFC 9380 suite from a constant-time library for your curve. Never compute $H(m)\cdot G$, and never write your own map. The two most catastrophic errors in this whole area are the two shortcuts from Section 3 [@rfc9380].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Default to &lt;code&gt;_RO_&lt;/code&gt;.&lt;/strong&gt; Use &lt;code&gt;hash_to_curve&lt;/code&gt; (&lt;code&gt;_RO_&lt;/code&gt;) unless you hold a specific proof that non-uniformity is acceptable, in which case &lt;code&gt;encode_to_curve&lt;/code&gt; (&lt;code&gt;_NU_&lt;/code&gt;) halves the cost [@rfc9380].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Always set a unique DST.&lt;/strong&gt; Make the domain-separation tag specific to your protocol and version, hash it if it exceeds 255 bytes, and never share one across protocols. This is &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;domain separation&lt;/a&gt;, the same discipline Part 13 develops for keys [@rfc9380].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Map by curve, not by taste.&lt;/strong&gt; Short-Weierstrass with $p \equiv 3 \pmod 4$ uses SSWU; $A = 0$ uses isogeny-SSWU; Montgomery and Edwards use Elligator 2; anything else uses SvdW [@rfc9380].&lt;/p&gt;
&lt;p&gt;Then the per-protocol specifics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;BLS:&lt;/strong&gt; use &lt;code&gt;BLS12381G2_XMD:SHA-256_SSWU_RO_&lt;/code&gt; for minimal-pubkey-size, or the G1 suite for minimal-signature-size; &lt;code&gt;_RO_&lt;/code&gt; is required for the unforgeability proof; clear the cofactor [@draft-bls-signature-07].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OPRF, VOPRF, OPAQUE:&lt;/strong&gt; call RFC 9497&apos;s &lt;code&gt;HashToGroup&lt;/code&gt; and &lt;code&gt;HashToScalar&lt;/code&gt; at the ciphersuite&apos;s suite; do not substitute your own map; reject the identity element on the wire [@rfc9497].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PAKE mapping a password (SAE, CPace):&lt;/strong&gt; use a constant-time map only, never hunt-and-peck a secret, and pre-hash the password with &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;a slow KDF such as PBKDF2, scrypt, or Argon2&lt;/a&gt; before mapping, so an adversary who sees an intermediate value still faces a slow dictionary attack [@rfc9380]. Part 12&apos;s password-entropy argument is exactly why this matters.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Pick the suite for your curve, default to &lt;code&gt;_RO_&lt;/code&gt;, set a unique DST, and clear the cofactor. Four decisions, each with a safe default, and RFC 9380 has already made three of them for you the moment you name the suite [@rfc9380].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The mirror image of the rules is the list of things that never belong in a diff. Each is a real, shippable bug that maps to exactly one of the three properties, or to the two guardrails around them.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse&lt;/th&gt;
&lt;th&gt;What it violates&lt;/th&gt;
&lt;th&gt;The fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;$P = H(m)\cdot G$ instead of a map&lt;/td&gt;
&lt;td&gt;Unknown discrete log&lt;/td&gt;
&lt;td&gt;use a real RFC 9380 map [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hunt-and-peck over a secret&lt;/td&gt;
&lt;td&gt;Constant time&lt;/td&gt;
&lt;td&gt;constant-time suite; H2E for SAE [@rfc9380] [@dragonblood-site]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_NU_&lt;/code&gt; where the proof needs &lt;code&gt;_RO_&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Random-oracle distribution&lt;/td&gt;
&lt;td&gt;default to &lt;code&gt;_RO_&lt;/code&gt; [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing or shared DST&lt;/td&gt;
&lt;td&gt;Domain separation&lt;/td&gt;
&lt;td&gt;unique per-protocol, per-version DST [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skipped cofactor clearing&lt;/td&gt;
&lt;td&gt;Subgroup safety&lt;/td&gt;
&lt;td&gt;clear the cofactor; check the subgroup [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Truncated &lt;code&gt;hash_to_field&lt;/code&gt; output&lt;/td&gt;
&lt;td&gt;Uniformity margin&lt;/td&gt;
&lt;td&gt;use the full $L$ bytes [@rfc9380]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw password without a slow KDF&lt;/td&gt;
&lt;td&gt;Offline-guessing resistance&lt;/td&gt;
&lt;td&gt;PBKDF2, scrypt, or Argon2 first [@rfc9380]&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; Vetted RFC 9380 suite for the curve. &lt;code&gt;_RO_&lt;/code&gt; unless a proof licenses &lt;code&gt;_NU_&lt;/code&gt;. Unique, versioned DST, hashed if over 255 bytes. Cofactor cleared and subgroup confirmed. Full &lt;code&gt;L&lt;/code&gt;-byte &lt;code&gt;hash_to_field&lt;/code&gt;. For secrets, a constant-time map and a slow KDF first. For OPRFs, identity rejected on the wire. Anything else is a finding [@rfc9380] [@rfc9497].&lt;/p&gt;
&lt;/blockquote&gt;

RFC 9380 ships official test vectors for every suite. Before you trust an implementation, feed it the standard&apos;s sample inputs and DSTs and confirm the output points match byte for byte. A library that reproduces the vectors implements the pipeline correctly; one that does not should never touch a secret [@rfc9380].
&lt;p&gt;A checklist answers &quot;what to do.&quot; The last thing to handle is the set of &quot;but why can&apos;t I just...&quot; questions that every reviewer and implementer keeps asking, because the answers are where the understanding finally sets.&lt;/p&gt;
&lt;h2&gt;11. FAQ and Common Misuse&lt;/h2&gt;


Because its discrete log is $H_n(m)$, which is public, so the point is not a stranger to anyone. In BLS this is fatal: a signature $[sk]\cdot H(m)$ becomes $[H_n(m)]\cdot PK$, which anyone computes from the public message and public key with no secret key. The same known-discrete-log flaw breaks OPRF obliviousness and a PAKE&apos;s password element [@rfc9380] [@bls01].


Only when the input is public and you need neither constant time nor a random-oracle distribution. Over a secret input the loop count is a function of the secret, so the running time leaks it. That is precisely the Dragonblood attack that recovered WPA3 passwords, and it is why RFC 9380 declines to specify the method [@rfc9380] [@vr20].


Yes. The `_NU_` output is provably not a random oracle: it covers at least one eighth of the group with weights varying by up to a factor of four [@rfc9380]. If your proof assumes a random oracle -- as the BLS, OPRF, and PAKE proofs do -- you need `_RO_`. Use `_NU_` only with a specific proof that non-uniformity is acceptable [@rfc9380].


Always. The DST is domain separation, not decoration: without a unique, protocol-and-version-specific tag, two protocols sharing a curve can be driven to collide. RFC 9380 makes it mandatory and hashes tags longer than 255 bytes [@rfc9380].


You don&apos;t; the curve decides. Curve25519 and edwards25519 use Elligator 2, because they have a point of order 2. BLS12-381 uses isogeny-SSWU, because $A = 0$. The map is a structural consequence of the curve, not a preference [@rfc9380].


No. It is bound to elliptic-curve cryptography and falls to Shor&apos;s algorithm. Post-quantum signatures avoid the primitive entirely because they use no curve, but the consumers that need a point of unknown discrete log -- OPRFs and VRFs, and the systems built on them -- have no settled, efficient post-quantum replacement yet [@rfc9380].


No. `encode_to_curve` (`_NU_`) runs one map and produces a nonuniform point at about half the cost. `hash_to_curve` (`_RO_`) hashes to two field elements, maps each, and adds the points, which is indifferentiable from a random oracle. The extra map is what buys the distribution [@rfc9380].

&lt;h2&gt;The Point Had to Be a Stranger, Arrive on Time, and Look the Part&lt;/h2&gt;
&lt;p&gt;Look back at the failures with the whole picture in hand, and they line up with eerie precision. Each was a wrong answer to exactly one of the three questions. Computing $H(m)\cdot G$ published the discrete log: the point was not a &lt;em&gt;stranger&lt;/em&gt;, and universal forgery followed [@rfc9380] [@bls01]. Hunt-and-peck leaked the password through the clock: the point did not &lt;em&gt;arrive on time&lt;/em&gt;, and Dragonblood read it off the handshake [@vr20]. Reaching for &lt;code&gt;_NU_&lt;/code&gt; where a proof needs &lt;code&gt;_RO_&lt;/code&gt; handed the proof a distribution it never agreed to: the point did not &lt;em&gt;look the part&lt;/em&gt; [@rfc9380]. Three questions, three ways to fail, three real systems that failed them.&lt;/p&gt;
&lt;p&gt;That is the sibling claim to the one Part 15 made about the curve itself. The elliptic-curve arithmetic was sound; the danger was the soft gap around it. Here the gap has a name and a shape. It is the doorway, the single step that carries an untrusted string into the group, and getting it right means answering all three questions at once: unknown discrete log, constant time, and a proof-usable distribution. RFC 9380 is the standard that guarantees all three and encodes which ones you asked for in a suite name you can read at a glance.&lt;/p&gt;
&lt;p&gt;For classical curves, the doorway is finally built to spec. The two-map construction is the provable optimum, the oversampling margin is a proven bound, and a handful of audited libraries carry the whole load quietly under BLS, OPRFs, OPAQUE, Privacy Pass, and WPA3. The open frontier has moved elsewhere: to machine-checked implementations of what already ships, to the stubbornly expensive pairing curves, and above all to a post-quantum world where the unknown-DL consumers have no settled replacement.&lt;/p&gt;
&lt;p&gt;So the next time you call a one-line hash-to-curve API, you will know to ask the three questions it answers for you. Do you know the point&apos;s discrete log? Does your running time depend on the input? Can your proof model the output as a random oracle? For any construction worth shipping, the answers are the same three words this whole field spent two decades earning: no, no, yes.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;hashing-to-elliptic-curves-rfc-9380&quot; keyTerms={[
  { term: &quot;Hash-to-curve&quot;, definition: &quot;A map from an arbitrary byte string to a curve point of unknown discrete log; standardized by RFC 9380.&quot; },
  { term: &quot;BLS signature&quot;, definition: &quot;A pairing-based signature where the signature is the hashed message point scaled by the secret key.&quot; },
  { term: &quot;Try-and-increment&quot;, definition: &quot;Hash with a counter until the result is a valid coordinate; unknown DL but variable time.&quot; },
  { term: &quot;Indifferentiability&quot;, definition: &quot;A formal criterion for when a construction can safely replace an ideal object such as a random oracle.&quot; },
  { term: &quot;Simplified SWU&quot;, definition: &quot;A closed-form map to short-Weierstrass curves with p equal to 3 mod 4 and nonzero A and B.&quot; },
  { term: &quot;Elligator 2&quot;, definition: &quot;An invertible map for curves with a point of order 2, used for Montgomery and Edwards curves.&quot; },
  { term: &quot;hash_to_field&quot;, definition: &quot;The first pipeline stage: expand input and DST into field elements within 2^-128 of uniform.&quot; },
  { term: &quot;Domain Separation Tag&quot;, definition: &quot;A mandatory, protocol-and-version-specific tag mixed into hashing to prevent cross-protocol collisions.&quot; },
  { term: &quot;Cofactor clearing&quot;, definition: &quot;The final stage that forces the mapped point into the prime-order subgroup.&quot; },
  { term: &quot;Oblivious PRF&quot;, definition: &quot;A protocol where the client learns a keyed function of its input while the server learns neither input nor output.&quot; }
]} flashcards={[
  { front: &quot;Why is P = H(m) times G unsafe?&quot;, back: &quot;Its discrete log is public, so anyone forges BLS signatures as Hn(m) times PK with no secret key.&quot; },
  { front: &quot;Why does try-and-increment leak?&quot;, back: &quot;Its iteration count depends on the input, so timing reveals a secret input. This is Dragonblood.&quot; },
  { front: &quot;Why does hash_to_curve run two maps?&quot;, back: &quot;A single map cannot be a random oracle; the sum of two mapped points is indifferentiable from one.&quot; },
  { front: &quot;Which map for BLS12-381?&quot;, back: &quot;Isogeny-SSWU, because A is zero. The curve chooses the map, not you.&quot; },
  { front: &quot;Is hash-to-curve quantum-safe?&quot;, back: &quot;No. It falls to Shor. PQ signatures avoid it; PQ OPRFs and VRFs are unsolved.&quot; }
]} questions={[
  { q: &quot;State the three properties a safe hash-to-curve map must satisfy.&quot;, a: &quot;Unknown discrete log, constant running time, and a distribution indifferentiable from a random oracle.&quot; },
  { q: &quot;Which property does each naive map violate, and what does each leak?&quot;, a: &quot;H(m) times G violates unknown discrete log and leaks universal forgery; try-and-increment violates constant time and leaks a secret input by timing.&quot; },
  { q: &quot;Why is the RO variant the provable optimum?&quot;, a: &quot;A single map cannot be uniform and surjective; the sum of two maps is indifferentiable; RFC 9380 standardizes exactly that.&quot; },
  { q: &quot;What is a DST for, and when must it be hashed?&quot;, a: &quot;Domain separation between protocols; it must be hashed when longer than 255 bytes and unique per protocol and version.&quot; },
  { q: &quot;Where is the open frontier?&quot;, a: &quot;Post-quantum OPRFs and VRFs, formal verification of deployed implementations, and efficient hashing to G2.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>hash-to-curve</category><category>elliptic-curves</category><category>rfc-9380</category><category>bls-signatures</category><category>oprf</category><category>pake</category><category>applied-cryptography</category><category>side-channels</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>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><item><title>The One Job of a Password Hash: Why Fast Is the Enemy, and Memory Is the Frontier</title><link>https://paragmali.com/blog/the-one-job-of-a-password-hash-why-fast-is-the-enemy-and-mem/</link><guid isPermaLink="true">https://paragmali.com/blog/the-one-job-of-a-password-hash-why-fast-is-the-enemy-and-mem/</guid><description>A password hash has one job: make each guess expensive on the cheapest attacker hardware. Why fast hashes fail, and how bcrypt, Argon2, and yescrypt fix it.</description><pubDate>Sat, 11 Jul 2026 02:05:14 GMT</pubDate><content:encoded>
**A password hash has exactly one job:** make each attacker guess as expensive as possible on the attacker&apos;s *cheapest* hardware, while staying cheap enough on your own server. That is why a fast, strong hash like SHA-256 is the *wrong* tool. A single gaming GPU tries tens of billions of guesses per second, so a stolen fast-hash dump is a race you have already lost -- as LinkedIn learned in 2012. The fix was never a *stronger* hash but a deliberately, tunably *slower* one, and since 2009 a deliberately *memory-hungry* one, because memory is the resource an attacker cannot buy asymmetrically cheaply. This field guide traces the whole deployed lineage -- DES `crypt` (1979), md5crypt, the twin branch of bcrypt (1999) and PBKDF2 (2000), and the memory-hard generation of scrypt (2009), Argon2 (the 2015 competition winner), and yescrypt (now the Linux `/etc/shadow` default) -- alongside the failure catalog that proves the point (LinkedIn, Adobe, Ashley Madison, Facebook). It ends with exact 2024-2026 parameters, a decision tree, and the one rule beneath them all: your security is the weakest function that ever touches the password. Memory-hardness is the recommended default, not a universal mandate -- the right choice is an economic decision bounded by your own budget.
&lt;h2&gt;1. Your Database Leaked, and the Race Is Already Over&lt;/h2&gt;
&lt;p&gt;In June 2012, roughly six million LinkedIn password hashes appeared on a Russian forum, and within days most were plaintext. Not because anyone had broken SHA-1, and not because there was a key to steal, but because the passwords had been stored with a hash whose whole design goal is to be &lt;em&gt;fast&lt;/em&gt; [@troyhunt].&lt;/p&gt;
&lt;p&gt;A single modern gaming GPU tries tens of billions of SHA-1 guesses per second, so a stolen dump of a fast hash is not ciphertext an attacker must break. It is a race the defender has already lost, for every password common enough to sit in a wordlist. The uncomfortable lesson: the property that makes SHA-1 a &lt;em&gt;good&lt;/em&gt; cryptographic hash -- its blazing speed -- is exactly what makes it a &lt;em&gt;catastrophic&lt;/em&gt; way to store a password.&lt;/p&gt;
&lt;p&gt;Fix that number in your head, because it governs everything that follows: on one NVIDIA RTX 4090, the &lt;code&gt;hashcat&lt;/code&gt; benchmark clocks SHA-1 at about &lt;strong&gt;50.6 billion&lt;/strong&gt; guesses per second and MD5 at &lt;strong&gt;164.1 billion&lt;/strong&gt; [@hashcat-4090]. Nobody attacked the mathematics. The attacker simply guessed, very fast, against a list of hashes that were never designed to make guessing expensive.&lt;/p&gt;

A value derived from a password by a deliberately slow, salted, one-way function and stored so that a leaked database does not directly reveal the passwords. It is distinct from a password-derived *encryption key*, which is computed and used but never stored (LUKS, VeraCrypt, password managers -- the subject of Part 9).

The attack this whole primitive exists to raise the cost of: the adversary holds the stolen verifiers and guesses locally at full hardware speed, with no server and no rate limiter in the loop. Nothing throttles them except the per-guess cost you baked into the hash. This is the offline-attacker threat model the series formalizes in Part 1, &quot;Secure Against Whom?&quot;
&lt;p&gt;So here is the thesis, in your hands from the first page: a password hash has exactly one job -- make each attacker guess as expensive as possible on the attacker&apos;s &lt;em&gt;cheapest&lt;/em&gt; hardware, while staying cheap enough on &lt;em&gt;your&lt;/em&gt; server. The variable that decided LinkedIn was never SHA-1&apos;s strength. It was SHA-1&apos;s &lt;em&gt;speed&lt;/em&gt;, compounded by a missing salt that let identical passwords share a hash and let precomputed tables apply.&lt;/p&gt;
&lt;p&gt;Carry one diagnostic sentence through every scheme in this article: &lt;em&gt;on the cheapest hardware an attacker can rent, how much does one guess at one password cost, and how far above your own per-login cost can you push it?&lt;/em&gt; By the end, you will drop bcrypt, Argon2id, yescrypt, and tomorrow&apos;s breach into that one economic question on sight.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A fast hash&apos;s cryptographic &lt;em&gt;strength&lt;/em&gt; -- collision resistance, preimage resistance -- is almost irrelevant to password storage. The only property that matters is cost per guess. That is why one of the best general-purpose hashes ever designed is one of the worst possible password hashes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One boundary, stated once. This article is about the &lt;strong&gt;stored-verifier&lt;/strong&gt; case: turning a low-entropy password into a value you &lt;em&gt;store&lt;/em&gt; so that a stolen database does not directly yield the passwords. The &lt;em&gt;same&lt;/em&gt; primitives -- PBKDF2, scrypt, Argon2 -- also serve as key-derivation functions whose output becomes an encryption key and where &lt;em&gt;nothing is stored&lt;/em&gt; (full-disk encryption, password managers). That dual use is &lt;a href=&quot;https://paragmali.com/blog/no-room-for-a-nonce-disk-and-length-preserving-encryption-fr&quot; rel=&quot;noopener&quot;&gt;Part 9&apos;s territory&lt;/a&gt;; here it is a pointer, not a detour.&lt;/p&gt;
&lt;p&gt;This is also the handoff &lt;a href=&quot;https://paragmali.com/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp&quot; rel=&quot;noopener&quot;&gt;Part 10, &quot;A Field Guide to Cryptographic Hashes,&quot;&lt;/a&gt; pointed at: a general-purpose hash such as BLAKE3 is engineered to be fast, which makes it exactly wrong for storing a password. Use a purpose-built password KDF instead. This is Part 12 of 25, and it is that return.&lt;/p&gt;

flowchart TD
    A[&quot;Stolen dump: one salt and one hash per user&quot;] --&amp;gt; B[&quot;Load into hashcat or John the Ripper on a rented GPU rig&quot;]
    B --&amp;gt; C[&quot;Take the next guess from a wordlist or a brute-force mask&quot;]
    C --&amp;gt; D[&quot;Compute the hash of the guess combined with the stored salt&quot;]
    D --&amp;gt; E{&quot;Equals a stored hash?&quot;}
    E --&amp;gt;|&quot;No&quot;| C
    E --&amp;gt;|&quot;Yes&quot;| F[&quot;Password recovered, record it and move on&quot;]
    F --&amp;gt; C
    G[&quot;The salt forces a per-user recompute and blocks precomputed tables. It imposes no speed limit. A fast hash runs this entire loop billions of times per second.&quot;] -.-&amp;gt; D
&lt;p&gt;To make the economics visceral, run the numbers yourself. The rates below are the real measured throughputs of one rented RTX 4090, and the wordlist is the published &lt;code&gt;rockyou.txt&lt;/code&gt; -- the first list every attacker loads, whose 14,344,391 entries are reproducible with &lt;code&gt;wc -l&lt;/code&gt; [@seclists-rockyou].&lt;/p&gt;
&lt;p&gt;{`
// Real measured throughputs on one NVIDIA RTX 4090 (hashcat v6.2.6).
var guessesPerSecond = {
  &quot;SHA-1 (fast hash)&quot;:            50600000000, // 50.6 GH/s
  &quot;PBKDF2-HMAC-SHA256, 999 it.&quot;:  8865700,     // 8.87 MH/s
  &quot;bcrypt (cost 5)&quot;:              184000,      // 184 kH/s
  &quot;scrypt (N=16384)&quot;:             7126         // 7,126 H/s
};&lt;/p&gt;
&lt;p&gt;var wordlist = 14344391; // rockyou.txt: 14,344,391 entries&lt;/p&gt;
&lt;p&gt;function human(seconds) {
  if (seconds &amp;lt; 1)     return (seconds * 1000).toFixed(1) + &quot; ms&quot;;
  if (seconds &amp;lt; 60)    return seconds.toFixed(2) + &quot; s&quot;;
  if (seconds &amp;lt; 3600)  return (seconds / 60).toFixed(1) + &quot; min&quot;;
  if (seconds &amp;lt; 86400) return (seconds / 3600).toFixed(1) + &quot; hours&quot;;
  return (seconds / 86400).toFixed(1) + &quot; days&quot;;
}&lt;/p&gt;
&lt;p&gt;for (var scheme in guessesPerSecond) {
  var seconds = wordlist / guessesPerSecond[scheme];
  console.log(scheme.padEnd(30) + human(seconds));
}&lt;/p&gt;
&lt;p&gt;console.log(&quot;&quot;);
console.log(&quot;Now add a per-password salt.&quot;);
console.log(&quot;It blocks precomputed tables and stops two users sharing a hash.&quot;);
console.log(&quot;Effect on the throughputs above: exactly 0 percent. Same rates, same wall clock.&quot;);
`}&lt;/p&gt;
&lt;p&gt;The fast hash exhausts all fourteen million guesses in well under a second. The memory-hard end of the table takes hours to do the same work on the same silicon -- a gap of roughly seven orders of magnitude, bought entirely by making each guess cost more. And notice what the salt does to those rates: nothing. It is a necessary defense against precomputation and hash-sharing, and it is useless against speed. Speed is the whole vulnerability.&lt;/p&gt;

A password hash has one job: make every guess cost as much as possible on the attacker&apos;s cheapest hardware, while staying cheap on yours. A fast hash gets that job exactly backwards.
&lt;p&gt;If the cipher was never the weak link and the key was never touched, then the defense was lost the moment someone chose a &lt;em&gt;fast&lt;/em&gt; function to store a &lt;em&gt;guessable&lt;/em&gt; secret. That trade-off -- store something guessable, so make guessing expensive -- was understood with total clarity in 1979, by two people who assumed from the start that the attacker would read the password file.&lt;/p&gt;
&lt;h2&gt;2. 1979: The Three Rules That Still Govern Everything&lt;/h2&gt;
&lt;p&gt;Bell Labs, 1979. Robert Morris and Ken Thompson publish &quot;Password Security: A Case History&quot; and reason like economists rather than cryptographers [@morris1979]. Their founding assumption is the one every subsequent scheme inherits: assume the attacker can read the password file, because on a shared UNIX system they can. Everything else follows from that single concession.&lt;/p&gt;
&lt;p&gt;Three rules fall out of it, and every scheme in this article -- forty-seven years later -- still obeys all three.&lt;/p&gt;

A per-password random value stored in the clear alongside the hash, so that identical passwords do not produce identical hashes and precomputed tables cannot be reused. A salt is *public*, not secret; its job is to make each stored verifier unique, not to hide anything. Generating salts correctly is the domain of a CSPRNG, covered in Part 2, &quot;Predictable or Repeated.&quot;
&lt;p&gt;&lt;strong&gt;Rule one: never store the password -- store a one-way function of it.&lt;/strong&gt; A stolen file should be a file of hashes, not a file of passwords. &lt;strong&gt;Rule two: salt it.&lt;/strong&gt; Attach a per-password random value -- twelve bits in the original DES &lt;code&gt;crypt&lt;/code&gt; -- so identical passwords do not collide and an attacker cannot precompute one giant table and apply it to every account at once. &lt;strong&gt;Rule three: make it deliberately slow.&lt;/strong&gt; Calibrate the cost so that a legitimate single login is trivial but bulk guessing is not.&lt;/p&gt;
&lt;p&gt;The mechanism was concrete, and you can see all three rules in it without touching DES&apos;s internals. The password was truncated to eight characters and turned into a 56-bit DES key; that key enciphered an all-zero block twenty-five times over; and the twelve-bit salt perturbed DES&apos;s expansion permutation so that off-the-shelf DES hardware could not be pointed straight at the problem [@morris1979]. One-way, because you cannot run DES backward without the key that &lt;em&gt;is&lt;/em&gt; the password. Salted, because of those twelve bits. Slow, because of those twenty-five iterations.&lt;/p&gt;
&lt;p&gt;That salt design is more pointed than it looks. Morris and Thompson perturbed the DES expansion permutation specifically so an attacker could not use stock DES chips to attack the hashes -- the same &quot;make the attacker&apos;s silicon useless&quot; instinct that, thirty years later, becomes memory-hardness [@morris1979].&lt;/p&gt;
&lt;p&gt;The economic point was already fully formed in 1979: the openly readable password file was a &lt;em&gt;design choice&lt;/em&gt;, so the hash itself had to survive offline guessing. That is the offline-attacker model the whole series leans on, and it is why the strength of the underlying cipher was never the interesting question. The interesting question was cost per guess.&lt;/p&gt;

timeline
    title Four decades of stored verifiers
    1979 : Morris and Thompson, DES crypt
    1995 : md5crypt and the Modular Crypt Format
    1999 : bcrypt from the systems world
    2000 : PBKDF2 from the standards world
    2007 : sha256crypt and sha512crypt
    2009 : scrypt, the memory-hard inflection
    2015 : Argon2 wins the PHC, yescrypt recognized
    2016 : Alwen and Blocki bound data-independent MHFs
    2021 : RFC 9106 standardizes Argon2 : yescrypt becomes the Linux shadow default
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Founding rule (1979)&lt;/th&gt;
&lt;th&gt;What it defeats&lt;/th&gt;
&lt;th&gt;How every modern scheme still does it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Store a one-way function, never the password&lt;/td&gt;
&lt;td&gt;A stolen file is not a stolen password list&lt;/td&gt;
&lt;td&gt;bcrypt, Argon2, and yescrypt are all one-way; you verify by recomputing and comparing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Salt it (per-password random value)&lt;/td&gt;
&lt;td&gt;Precomputation and identical-password collisions&lt;/td&gt;
&lt;td&gt;a 16-byte CSPRNG salt embedded directly in the stored hash string&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Make it deliberately slow&lt;/td&gt;
&lt;td&gt;Bulk offline guessing&lt;/td&gt;
&lt;td&gt;a tunable work factor, iteration count, or memory cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

Two neighbors are easy to confuse with this one, so an expert reader does not think they were missed. Password-authenticated key exchange -- SRP, OPAQUE -- avoids *sending* the password to the server at all; it is a different primitive with a different goal, and it still needs a stored verifier underneath. Passkeys and WebAuthn go the other direction and eliminate the shared secret entirely. Both are pointers here, not sections. And the *same* functions in this article (PBKDF2, scrypt, Argon2) are also used to derive encryption keys for disks and vaults, where nothing is stored -- that is [Part 9](/blog/no-room-for-a-nonce-disk-and-length-preserving-encryption-fr), not this one.
&lt;p&gt;Three rules, 1979: one-way, salted, slow. They were right, and they are still the entire skeleton. But two of the three had hard numbers baked in -- an eight-character limit, a twelve-bit salt, twenty-five fixed iterations -- and numbers calibrated to 1979 hardware do not stay calibrated. The next fifteen years are the story of those ceilings collapsing.&lt;/p&gt;
&lt;h2&gt;3. The Antipattern That Would Not Die&lt;/h2&gt;
&lt;p&gt;A design calibrated to a single moment ages at the speed of Moore&apos;s law. DES &lt;code&gt;crypt&lt;/code&gt; was tuned for 1979; by the mid-1990s the same twenty-five iterations were a rounding error, and its eight-character truncation threw away everything a longer password could have bought. The fixes that followed solved real problems -- and, in the same breath, set a trap the industry still steps in today.&lt;/p&gt;
&lt;h3&gt;Removing the ceilings, and inventing migration&lt;/h3&gt;
&lt;p&gt;Poul-Henning Kamp&apos;s &lt;strong&gt;md5crypt&lt;/strong&gt; (&lt;code&gt;$1$&lt;/code&gt;), created in &lt;strong&gt;1995&lt;/strong&gt; in his own words, removed the eight-character and twelve-bit-salt limits with roughly a thousand iterations of MD5 and a larger salt [@phk-eol]. Its most durable gift, though, was not the hash. It was the &lt;em&gt;format&lt;/em&gt;: the &lt;strong&gt;Modular Crypt Format (MCF)&lt;/strong&gt;, a self-describing string of the shape &lt;code&gt;$&amp;lt;id&amp;gt;$&amp;lt;params&amp;gt;$&amp;lt;salt&amp;gt;$&amp;lt;hash&amp;gt;&lt;/code&gt; that lets one password file hold several algorithms at once and migrate between them over time.&lt;/p&gt;

A self-describing hash string of the form `$$$$`. The leading `` names the algorithm, so a verifier can look at a stored value, dispatch to the right function, recompute with the stored parameters, and -- when those parameters fall below today&apos;s target -- transparently rehash. It is the reason a single verifier column can hold `$1$`, `$2b$`, `$6$`, and `$argon2id$` values side by side and upgrade in place.
&lt;p&gt;That registry of &lt;code&gt;&amp;lt;id&amp;gt;&lt;/code&gt; tags is worth memorizing, because it is a map of this entire article:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;MCF prefix&lt;/th&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Status in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$1$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;md5crypt&lt;/td&gt;
&lt;td&gt;legacy, retired by its author [@phk-eol]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$2a$&lt;/code&gt; / &lt;code&gt;$2y$&lt;/code&gt; / &lt;code&gt;$2b$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bcrypt&lt;/td&gt;
&lt;td&gt;active [@cryptbf]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$5$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sha256crypt&lt;/td&gt;
&lt;td&gt;active (older Linux default) [@drepper-shacrypt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$6$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sha512crypt&lt;/td&gt;
&lt;td&gt;active (older Linux default) [@drepper-shacrypt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$7$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;scrypt&lt;/td&gt;
&lt;td&gt;active [@libxcrypt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$y$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yescrypt&lt;/td&gt;
&lt;td&gt;active (current Linux default) [@libxcrypt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$argon2id$&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Argon2id&lt;/td&gt;
&lt;td&gt;active (cross-platform recommendation) [@rfc9106; @argon2-ref]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;{`
// A Modular Crypt Format value is self-describing: $$$$.
// That is exactly what makes in-place migration possible.
function decodeMCF(s) {
  return s.split(&quot;$&quot;).filter(function (p) { return p.length &amp;gt; 0; });
}&lt;/p&gt;
&lt;p&gt;var argon  = &quot;$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0$aGFzaGhhc2hoYXNoaGFzaA&quot;;
var bcrypt = &quot;$2b$12$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm&quot;;&lt;/p&gt;
&lt;p&gt;console.log(&quot;argon2id -&amp;gt;&quot;, JSON.stringify(decodeMCF(argon)));
// [&quot;argon2id&quot;, &quot;v=19&quot;, &quot;m=65536,t=3,p=4&quot;, &quot;&quot;, &quot;&quot;]&lt;/p&gt;
&lt;p&gt;console.log(&quot;bcrypt   -&amp;gt;&quot;, JSON.stringify(decodeMCF(bcrypt)));
// [&quot;2b&quot;, &quot;12&quot;, &quot;&amp;lt;22-char salt and 31-char hash, concatenated&amp;gt;&quot;]&lt;/p&gt;
&lt;p&gt;console.log(&quot;&quot;);
console.log(&quot;The first token names the algorithm. A verifier dispatches on it,&quot;);
console.log(&quot;recomputes with the stored parameters, and rehashes if they are stale.&quot;);
`}&lt;/p&gt;
&lt;p&gt;The other move of this era was the SHA-2 bridge. Ulrich Drepper&apos;s &lt;strong&gt;sha256crypt&lt;/strong&gt; and &lt;strong&gt;sha512crypt&lt;/strong&gt; (2007, &lt;code&gt;$5$&lt;/code&gt; and &lt;code&gt;$6$&lt;/code&gt;) iterated a NIST-approved hash a default of 5,000 rounds, tunable up to just under a billion, with a salt up to sixteen characters [@drepper-shacrypt]. The motive was compliance as much as cryptography: security teams wanted a NIST-tested primitive, which ruled out Blowfish-based bcrypt and made sha512crypt the long-time Linux &lt;code&gt;/etc/shadow&lt;/code&gt; default. Same instinct as md5crypt -- make the cost tunable -- with a FIPS-friendly hash underneath.&lt;/p&gt;
&lt;h3&gt;The antipattern, named and buried&lt;/h3&gt;
&lt;p&gt;Now the recurring villain of the whole story: the &lt;strong&gt;fast, unsalted, general-purpose hash&lt;/strong&gt; -- raw MD5, SHA-1, or SHA-256 -- pressed into service as a password store. It fails on both axes at once. &lt;em&gt;Unsalted&lt;/em&gt; means precomputed rainbow tables apply and identical passwords share a hash. &lt;em&gt;Fast&lt;/em&gt; means billions of guesses per second.&lt;/p&gt;
&lt;p&gt;This is precisely the LinkedIn disaster from Section 1 -- unsalted SHA-1, plaintext in days [@troyhunt] -- and, one rung worse, &lt;strong&gt;RockYou&lt;/strong&gt; in 2009, which stored roughly 32.6 million passwords in &lt;em&gt;plaintext&lt;/em&gt;, no hash at all [@imperva-rockyou]. That dump became &lt;code&gt;rockyou.txt&lt;/code&gt;, the canonical cracking wordlist now itself part of the arms race.&lt;/p&gt;
&lt;p&gt;The unsettling corollary for defenders: if your password is memorable, it is almost certainly already a line in that cracking wordlist, tried in the first seconds of any offline attack.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The fast-hash trap: the property that makes SHA-256 a &lt;em&gt;good&lt;/em&gt; general-purpose hash -- blazing speed -- is exactly what makes it a &lt;em&gt;catastrophic&lt;/em&gt; password hash. A salt defeats precomputation and hash-sharing, but it changes the attacker&apos;s throughput by exactly zero. The attacker never breaks the hash; a stolen fast-hash dump is a race already lost.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Never store a bare fast hash of a password -- not MD5, not SHA-1, not SHA-256, salted or not. A salt is necessary and it stops precomputation, but it does nothing about speed, and speed is the entire vulnerability. This single antipattern is the root cause of most of the failure catalog below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kamp himself buried md5crypt in 2012, and his exact words matter because the field has misquoted them ever since. His notice is titled &quot;Md5crypt Password scrambler is no longer considered safe by author,&quot; and it reads, plainly, &quot;I implore everybody to migrate to a stronger password scrambler without undue delay&quot; [@phk-eol]. The widely repeated phrase &quot;unfit for use&quot; is a paraphrase, not his language. What he &lt;em&gt;did&lt;/em&gt; write is startling in hindsight: the replacement should &quot;soak up area in hardware based attack implementations&quot; -- a demand for area-cost hardening, seven years before scrypt would make it a formal property.&lt;/p&gt;
&lt;p&gt;Kamp&apos;s 2012 retirement of md5crypt cited about one million guesses per second on a commodity 2012 GPU and the vulnerability CVE-2012-3287. Read his phrasing carefully -- &quot;soak up area in hardware based attack implementations&quot; is a plea for area-time cost, the exact idea memory-hardness would formalize [@phk-eol].&lt;/p&gt;

The successor should &quot;soak up area in hardware based attack implementations.&quot; -- Poul-Henning Kamp, retiring md5crypt in 2012, describing memory-hardness before it had a name.
&lt;p&gt;Keep this master catalog in view; the article returns to each entry and unpacks the interesting ones in Sections 9 through 11. Every row is the same lesson from a different angle.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;What was stored&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;The lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;RockYou&lt;/td&gt;
&lt;td&gt;2009&lt;/td&gt;
&lt;td&gt;Plaintext&lt;/td&gt;
&lt;td&gt;No hash at all&lt;/td&gt;
&lt;td&gt;The floor of the failure catalog [@imperva-rockyou]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LinkedIn&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;Unsalted SHA-1&lt;/td&gt;
&lt;td&gt;Fast and unsalted&lt;/td&gt;
&lt;td&gt;Speed is the vulnerability [@troyhunt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adobe&lt;/td&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;Reversible 3DES in ECB mode&lt;/td&gt;
&lt;td&gt;Encryption, not hashing&lt;/td&gt;
&lt;td&gt;Hash, never encrypt, a password [@adobe-ars2013]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ashley Madison&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;bcrypt cost-12, plus an MD5 token&lt;/td&gt;
&lt;td&gt;A strong hash undone by a parallel fast path&lt;/td&gt;
&lt;td&gt;You are only as strong as the weakest function [@cynosure-am]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dropbox&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;SHA-512, then bcrypt-10, then AES-256&lt;/td&gt;
&lt;td&gt;A &lt;em&gt;good&lt;/em&gt; example, shown for contrast&lt;/td&gt;
&lt;td&gt;Layer defenses, keep a separate pepper [@dropbox]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Facebook&lt;/td&gt;
&lt;td&gt;2019&lt;/td&gt;
&lt;td&gt;Plaintext, in log files&lt;/td&gt;
&lt;td&gt;Plaintext logging beside a good hash&lt;/td&gt;
&lt;td&gt;Audit the whole path, not the column [@krebs-fb2019]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password shucking&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bcrypt(md5(pw))&lt;/code&gt;, unsalted inner&lt;/td&gt;
&lt;td&gt;A naive pre-hash&lt;/td&gt;
&lt;td&gt;Never feed an unsalted fast hash into bcrypt [@scottbrady-shucking]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crypt_blowfish bug&lt;/td&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;bcrypt with an 8-bit character flaw&lt;/td&gt;
&lt;td&gt;Sign-extension truncation&lt;/td&gt;
&lt;td&gt;Why bcrypt has &lt;code&gt;$2x$&lt;/code&gt; and &lt;code&gt;$2y$&lt;/code&gt; tags [@cryptbf]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;md5crypt and sha512crypt fixed 1979&apos;s &lt;em&gt;ceilings&lt;/em&gt; but kept its third rule frozen in place: a &lt;em&gt;fixed&lt;/em&gt; iteration count chosen once and forgotten. The next idea was to stop freezing the cost and make it a &lt;em&gt;knob&lt;/em&gt; the defender could turn as hardware improved. That idea arrived twice, independently, in two different communities within about a year -- and the blind spot those twins shared is where the modern story actually begins.&lt;/p&gt;
&lt;h2&gt;4. A Branch, Not a Line: bcrypt and PBKDF2 Are Twins&lt;/h2&gt;
&lt;p&gt;The textbook story is a clean relay race: each generation hands the baton to a stronger successor that retires it. For password hashing, that story is &lt;em&gt;wrong&lt;/em&gt;, and getting it right is the single most useful correction an expert reader can take from this article. Around 1999 and 2000, two communities answered the same question -- &quot;make the cost a tunable knob&quot; -- independently, and &lt;em&gt;neither replaced the other&lt;/em&gt;. They are twins from different families, and both are still alive in 2026.&lt;/p&gt;
&lt;h3&gt;bcrypt, from the systems world (1999)&lt;/h3&gt;
&lt;p&gt;Niels Provos and David Mazieres built bcrypt for OpenBSD on top of &lt;strong&gt;Eksblowfish&lt;/strong&gt;, an &quot;expensive key schedule&quot; variant of the Blowfish cipher [@bcrypt1999]. The construction repeats Blowfish&apos;s key setup $2^{\text{cost}}$ times, then enciphers the constant string &quot;OrpheanBeholderScryDoubt&quot; sixty-four times, salted with 128 bits and encoded as &lt;code&gt;$2a$&amp;lt;cost&amp;gt;$...&lt;/code&gt;. The innovation is the &lt;strong&gt;adaptive cost factor&lt;/strong&gt;: raise &lt;code&gt;cost&lt;/code&gt; as hardware improves, without changing the algorithm or invalidating a single stored hash. In the authors&apos; own framing, &quot;the computational cost of any secure password scheme must increase as hardware improves&quot; [@bcrypt1999].&lt;/p&gt;

A tunable parameter -- bcrypt&apos;s `cost`, PBKDF2&apos;s iteration count, Argon2&apos;s time and memory knobs -- that raises the per-guess cost so a defender can track hardware improvements over time without changing the algorithm or re-hashing existing verifiers with a new scheme. Turning it up is how you &quot;restore the cost ratio&quot; the spine keeps referring to.
&lt;p&gt;bcrypt was also, almost by accident, the first widely deployed scheme to &lt;em&gt;touch memory&lt;/em&gt; in a way that mattered. Blowfish&apos;s key schedule builds about 4 KiB of S-boxes, and that small working set already made bcrypt meaningfully harder to parallelize on a GPU than a bare hash -- a fact scrypt&apos;s paper would later quantify [@scrypt-paper]. Two structural limits recur later: bcrypt silently truncates its input at &lt;strong&gt;72 bytes&lt;/strong&gt;, and its &lt;code&gt;$2a$&lt;/code&gt; / &lt;code&gt;$2x$&lt;/code&gt; / &lt;code&gt;$2y$&lt;/code&gt; / &lt;code&gt;$2b$&lt;/code&gt; version tags are a bug-fix lineage, not a version-number vanity.&lt;/p&gt;
&lt;p&gt;bcrypt&apos;s prefix zoo is a repair history. The &lt;code&gt;$2x$&lt;/code&gt; and &lt;code&gt;$2y$&lt;/code&gt; tags came out of a 2011 fix to an 8-bit-character sign-extension flaw in &lt;code&gt;crypt_blowfish&lt;/code&gt; (CVE-2011-2483) that could truncate the input; &lt;code&gt;$2b$&lt;/code&gt; matches the behavior of OpenBSD 5.5 and later [@cryptbf].&lt;/p&gt;
&lt;h3&gt;PBKDF2, from the standards world (2000)&lt;/h3&gt;
&lt;p&gt;Burt Kaliski&apos;s &lt;strong&gt;PBKDF2&lt;/strong&gt; came out of RSA Laboratories as part of PKCS #5 v2.0, later republished as RFC 2898 and updated in RFC 8018 [@rfc2898; @rfc8018]. Its design is deliberately plain: iterate a pseudorandom function -- in practice HMAC over an approved hash -- a count &lt;code&gt;c&lt;/code&gt; times over the password and salt, concatenating output blocks. Because it is built entirely from NIST-approved hashes, it is the option a &lt;strong&gt;FIPS-constrained&lt;/strong&gt; system can actually deploy, which Blowfish-based bcrypt is not. In 2026 it remains the mandated path wherever FIPS applies.&lt;/p&gt;
&lt;p&gt;Its weakness has to be stated precisely, because the folklore version is wrong. PBKDF2 is not weak because &quot;GPUs are fast.&quot; It is weak because it has &lt;strong&gt;negligible, constant memory&lt;/strong&gt; and is otherwise a bare loop of HMAC, which makes it the &lt;em&gt;most&lt;/em&gt; parallelization-friendly of every deployed scheme. The crux is &lt;strong&gt;low, constant memory implies cheap, massive parallelism&lt;/strong&gt; on GPU, FPGA, and ASIC -- an attacker fits thousands of independent PBKDF2 evaluations onto one chip because each one needs almost no RAM.&lt;/p&gt;
&lt;p&gt;A nuance the OWASP floors encode: PBKDF2-HMAC-&lt;strong&gt;SHA-512&lt;/strong&gt; does 64-bit-word arithmetic, which is somewhat &lt;em&gt;less&lt;/em&gt; GPU-friendly than SHA-256&apos;s 32-bit words. That is exactly why OWASP&apos;s SHA-512 iteration floor (210,000) sits below its SHA-256 floor (600,000) -- fewer iterations buy the same attacker cost [@owasp].&lt;/p&gt;

bcrypt (1999) and PBKDF2 (2000) are *contemporaries* from different communities, not a before-and-after. sha256crypt and sha512crypt (2007) are a third sibling. And **no earlier scheme was ever cleanly retired**: Linux ran DES `crypt`, then md5crypt, then sha512crypt for decades, and PBKDF2 is still the FIPS default today. Read the &quot;generations&quot; of password hashing as geological strata that *accreted*, not as a relay race where each runner sits down. The only true inflection point in the whole history is memory-hardness (scrypt, 2009).
&lt;p&gt;Here is the master head-to-head. The rightmost columns are the ones that decide real deployments; the article earns each judgment in the sections that follow.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;New cost lever&lt;/th&gt;
&lt;th&gt;Memory-hard?&lt;/th&gt;
&lt;th&gt;FIPS lane?&lt;/th&gt;
&lt;th&gt;Side-channel posture&lt;/th&gt;
&lt;th&gt;Status in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Raw fast hash (MD5, SHA-1, SHA-256)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;none; general-purpose speed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;primitive only&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;never use for passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DES &lt;code&gt;crypt(3)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1979&lt;/td&gt;
&lt;td&gt;one-way, salt, 25 iterations [@morris1979]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;data-independent&lt;/td&gt;
&lt;td&gt;historic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;md5crypt (&lt;code&gt;$1$&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;1995&lt;/td&gt;
&lt;td&gt;~1000 iterations, MCF [@phk-eol]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;data-independent&lt;/td&gt;
&lt;td&gt;retired by author&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt&lt;/td&gt;
&lt;td&gt;1999&lt;/td&gt;
&lt;td&gt;adaptive cost, expensive key schedule [@bcrypt1999]&lt;/td&gt;
&lt;td&gt;No (touches ~4 KiB)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;n/a (not an MHF)&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PBKDF2&lt;/td&gt;
&lt;td&gt;2000&lt;/td&gt;
&lt;td&gt;iterated HMAC, tunable count [@rfc2898]&lt;/td&gt;
&lt;td&gt;No (negligible memory)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;data-independent&lt;/td&gt;
&lt;td&gt;active (FIPS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sha256crypt / sha512crypt&lt;/td&gt;
&lt;td&gt;2007&lt;/td&gt;
&lt;td&gt;tunable SHA-2 rounds [@drepper-shacrypt]&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;data-independent&lt;/td&gt;
&lt;td&gt;active (legacy default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scrypt&lt;/td&gt;
&lt;td&gt;2009&lt;/td&gt;
&lt;td&gt;sequential memory-hardness [@scrypt-paper]&lt;/td&gt;
&lt;td&gt;Yes (data-dependent)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;data-dependent&lt;/td&gt;
&lt;td&gt;active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argon2i&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;data-independent MHF [@rfc9106]&lt;/td&gt;
&lt;td&gt;Yes (provably suboptimal)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;data-independent&lt;/td&gt;
&lt;td&gt;superseded by id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argon2id&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;hybrid i-then-d MHF [@rfc9106]&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;hybrid&lt;/td&gt;
&lt;td&gt;recommended default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yescrypt&lt;/td&gt;
&lt;td&gt;2015&lt;/td&gt;
&lt;td&gt;scrypt plus pwxform, optional ROM [@yescrypt]&lt;/td&gt;
&lt;td&gt;Yes (data-dependent)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;data-dependent&lt;/td&gt;
&lt;td&gt;Linux shadow default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;The blind spot all three twins shared&lt;/h3&gt;
&lt;p&gt;Every one of the 1999-2007 schemes stretches &lt;strong&gt;time only&lt;/strong&gt;, with a small fixed memory footprint. Time-hardness has a fatal asymmetry: a function that is slow but tiny in memory is &lt;em&gt;cheap to parallelize&lt;/em&gt;. The defender runs one core per login; the attacker runs thousands of small cores on a GPU or millions in an ASIC. &quot;Slower per guess&quot; is a race the defender loses at scale.&lt;/p&gt;
&lt;p&gt;One benchmark makes the asymmetry impossible to unsee -- a single RTX 4090 under &lt;code&gt;hashcat&lt;/code&gt; v6.2.6 [@hashcat-4090]:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hash mode (one RTX 4090)&lt;/th&gt;
&lt;th&gt;Guesses per second&lt;/th&gt;
&lt;th&gt;Slowdown vs MD5&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;MD5&lt;/td&gt;
&lt;td&gt;164.1 billion&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-1&lt;/td&gt;
&lt;td&gt;50.6 billion&lt;/td&gt;
&lt;td&gt;about 3x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PBKDF2-HMAC-SHA256, 999 iterations&lt;/td&gt;
&lt;td&gt;8.87 million&lt;/td&gt;
&lt;td&gt;about 18,000x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt, cost 5&lt;/td&gt;
&lt;td&gt;184,000&lt;/td&gt;
&lt;td&gt;about 890,000x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

flowchart LR
    A[&quot;Lever 1: iteration TIME&lt;br /&gt;DES crypt 1979, md5crypt 1995&quot;] --&amp;gt;|&quot;eroded by faster CPUs&quot;| B[&quot;Lever 2: expensive KEY SCHEDULE&lt;br /&gt;bcrypt 1999&quot;]
    B --&amp;gt;|&quot;eroded by cheap parallel GPU, FPGA, ASIC&quot;| C[&quot;Lever 3: MEMORY&lt;br /&gt;scrypt 2009, Argon2 2015&quot;]
    C --&amp;gt;|&quot;eroded by botnets and cheap RAM&quot;| D[&quot;Lever 4: MEMORY plus a site ROM&lt;br /&gt;yescrypt&quot;]
&lt;p&gt;Look at the last two rows. PBKDF2 with a thousand iterations barely dents the attacker&apos;s throughput, because it is almost pure compute. bcrypt at a low cost of 5 -- with nothing but its 4 KiB of S-boxes -- already costs roughly &lt;em&gt;five orders of magnitude&lt;/em&gt; more per guess than a raw hash, purely because it touches memory and cannot be trivially unrolled. That gap is the entire clue.&lt;/p&gt;
&lt;p&gt;If a mere 4 KiB of forced memory buys bcrypt that five-orders-of-magnitude edge over a raw hash, the lever is obvious in hindsight: stop making a guess take &lt;em&gt;longer&lt;/em&gt; and start making it take &lt;em&gt;more room&lt;/em&gt;. CPU cycles and logic gates are cheap and endlessly parallel. Memory bandwidth and capacity are not. That inversion -- from time to memory -- is the one genuine conceptual leap in this entire history.&lt;/p&gt;
&lt;h2&gt;5. The Breakthrough: Make Memory the Bottleneck&lt;/h2&gt;
&lt;p&gt;In 2009, Colin Percival stated the idea that splits the timeline in two: force the computation to &lt;em&gt;hold a large array in RAM for a long time&lt;/em&gt;, so that computing the hash cheaply &lt;em&gt;requires buying memory&lt;/em&gt; -- the one resource an attacker with GPUs, FPGAs, and ASICs cannot parallelize away [@scrypt-paper]. This is not a faster hash. It is a differently shaped one.&lt;/p&gt;
&lt;h3&gt;scrypt and sequential memory-hardness&lt;/h3&gt;
&lt;p&gt;The core of scrypt is a routine called &lt;strong&gt;ROMix&lt;/strong&gt;, and you can understand it without touching the Salsa20 mixing inside. It runs in two phases. In the &lt;strong&gt;fill&lt;/strong&gt; phase, it writes an array &lt;code&gt;V[0..N-1]&lt;/code&gt; into memory, where each block is the previous block run through a mixing function -- a strictly sequential chain. In the &lt;strong&gt;mix&lt;/strong&gt; phase, it takes &lt;code&gt;N&lt;/code&gt; more steps, and on each step it reads &lt;code&gt;V[j]&lt;/code&gt; at an index &lt;code&gt;j&lt;/code&gt; that &lt;em&gt;depends on the data computed so far&lt;/em&gt;, mixing that block back into a running state [@scrypt-paper; @rfc7914].&lt;/p&gt;
&lt;p&gt;That second phase is the whole trick. Because the read indices depend on the data, you cannot know in advance which blocks you will need, so computing scrypt with fewer than &lt;code&gt;N&lt;/code&gt; blocks resident forces you to &lt;em&gt;recompute&lt;/em&gt; missing blocks on demand -- and that recomputation cascades.&lt;/p&gt;
&lt;p&gt;To keep the work down, you must hold about &lt;code&gt;N&lt;/code&gt; blocks for about &lt;code&gt;N&lt;/code&gt; steps: an &lt;strong&gt;area times time&lt;/strong&gt; cost of roughly $AT \approx \Theta(N^2)$. Halving the memory you hold roughly &lt;em&gt;doubles&lt;/em&gt; the time, so the AT product stays near $\Theta(N^2)$. Double the cost parameter &lt;code&gt;N&lt;/code&gt; instead and both time and memory double, so the attacker&apos;s hardware cost rises four-fold [@scrypt-paper].&lt;/p&gt;

A function deliberately designed so that computing it cheaply *requires* holding a large amount of memory for a large fraction of its running time. The point is not that it uses memory, but that no attacker strategy avoids the memory without paying more somewhere else -- imposing a cost that cheap, massively parallel hardware cannot dodge.

The attacker&apos;s real cost metric for a memory-hard function: the memory it occupies multiplied by the time it is held, a proxy for the silicon area and energy one guess consumes. Time-hardness alone raises only the time term; memory-hardness raises the product. Maximizing AT-cost per guess is the entire objective of the memory-hard program.

flowchart TD
    A[&quot;Phase 1, FILL. Write N blocks into RAM, each derived from the previous block&quot;] --&amp;gt; B[&quot;The whole array of N blocks now sits resident in memory&quot;]
    B --&amp;gt; C[&quot;Phase 2, MIX. Repeat N times&quot;]
    C --&amp;gt; D{&quot;Read block j, where j depends on the data so far&quot;}
    D --&amp;gt; E[&quot;Mix that block into the running state&quot;]
    E --&amp;gt; C
    D -.-&amp;gt;|&quot;Hold fewer than N blocks and you must recompute them&quot;| F[&quot;Less memory forces far more work, so AT stays high&quot;]
&lt;p&gt;The payoff shows up on the same RTX 4090 that cracked SHA-1 at 50.6 billion guesses per second. scrypt tuned to &lt;code&gt;N&lt;/code&gt; of 16384 runs at just &lt;strong&gt;7,126&lt;/strong&gt; guesses per second on that hardware [@hashcat-4090] -- about 25 times slower than bcrypt at cost 5, and roughly &lt;em&gt;seven orders of magnitude&lt;/em&gt; below raw MD5, because the GPU&apos;s thousands of cores now fight over scarce fast memory instead of streaming through cheap arithmetic.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Time-hardness is cheap to parallelize: the defender runs one core, the attacker runs thousands, so making a guess merely &lt;em&gt;slower&lt;/em&gt; loses the cost race at scale. Making a guess &lt;em&gt;occupy memory for a long time&lt;/em&gt; -- an area-times-time cost -- targets the one resource an attacker cannot buy asymmetrically cheaply. That inversion, from time to memory, is the modern era of password hashing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Institutionalizing it: the PHC and Argon2&lt;/h3&gt;
&lt;p&gt;Faced with a hard design problem, cryptography did what it does best -- it ran an open competition. The &lt;strong&gt;Password Hashing Competition&lt;/strong&gt; ran from 2013 to 2015, initiated by Jean-Philippe Aumasson, drew 24 submissions, and was judged by a panel in the same spirit as NIST&apos;s AES and SHA-3 contests [@phc]. It named one winner, &lt;strong&gt;Argon2&lt;/strong&gt;, and gave special recognition to four finalists.&lt;/p&gt;
&lt;p&gt;The PHC&apos;s four special-recognition finalists were Catena, Lyra2, Makwa, and yescrypt -- the last of which went on to become the Linux &lt;code&gt;/etc/shadow&lt;/code&gt; default. The competition format, modeled on NIST&apos;s, is why the field trusts Argon2: it survived years of public cryptanalysis, not a vendor&apos;s say-so [@phc].&lt;/p&gt;
&lt;p&gt;Argon2 is the purpose-built memory-hard function. It exposes three &lt;em&gt;independent&lt;/em&gt; knobs -- time &lt;code&gt;t&lt;/code&gt; (passes), memory &lt;code&gt;m&lt;/code&gt; (in KiB), and parallelism &lt;code&gt;p&lt;/code&gt; -- fills and mixes a large array using a BLAKE2b-based compression function, and encodes its output in MCF as &lt;code&gt;$argon2id$v=19$m=...,t=...,p=...$salt$hash&lt;/code&gt;, where &lt;code&gt;v=19&lt;/code&gt; is version 0x13. The reference defaults are &lt;code&gt;t=3&lt;/code&gt;, &lt;code&gt;m=4 MiB&lt;/code&gt; (that is $2^{12}$ KiB), and &lt;code&gt;p=1&lt;/code&gt; [@argon2-ref]. Crucially, Argon2 comes in three variants, and choosing among them is a &lt;em&gt;threat-model&lt;/em&gt; decision, not a performance tweak.&lt;/p&gt;

Whether the memory addresses a function reads depend on the password. **Data-independent** (an iMHF) is cache-timing-safe, because the access pattern reveals nothing about the secret -- but, as the next section proves, it cannot be optimally memory-hard. **Data-dependent** (a dMHF) can reach the ideal hardness bound but leaks information through cache-timing side channels. This tension is the reason Argon2id exists.

flowchart TD
    P[&quot;Password plus salt&quot;] --&amp;gt; ID{&quot;Which Argon2 variant?&quot;}
    ID --&amp;gt;|&quot;Argon2d&quot;| D[&quot;Data-dependent indexing. Maximum GPU and trade-off resistance, but the access pattern leaks through cache timing&quot;]
    ID --&amp;gt;|&quot;Argon2i&quot;| I[&quot;Data-independent indexing. Cache-timing safe, but provably attackable, needs more passes&quot;]
    ID --&amp;gt;|&quot;Argon2id&quot;| H[&quot;Hybrid. Data-independent for the first half-pass, then data-dependent&quot;]
    H --&amp;gt; A[&quot;Answers both threat models, so RFC 9106 makes it must-support&quot;]
&lt;p&gt;&lt;strong&gt;Argon2d&lt;/strong&gt; uses data-dependent indexing for maximum resistance to GPU and trade-off attacks, at the cost of a cache-timing side channel. &lt;strong&gt;Argon2i&lt;/strong&gt; uses data-independent indexing, which is cache-timing-safe but needs more passes and is provably weaker. &lt;strong&gt;Argon2id&lt;/strong&gt; is the hybrid: data-independent addressing for the first half-pass, where the side channel matters, then data-dependent addressing for the rest. RFC 9106 makes &lt;strong&gt;Argon2id the MUST-support variant&lt;/strong&gt; [@rfc9106], for reasons the theory section will make exact.&lt;/p&gt;
&lt;p&gt;Memory-hardness works. It is the first idea in thirty years that changes the &lt;em&gt;shape&lt;/em&gt; of the attacker&apos;s cost, not merely its size. So the modern era ought to be simple -- pick the memory-hard winner, turn the memory knob to maximum, and go home. It is not simple, and the two reasons why -- one you meet in production, one buried in complexity theory -- are the rest of this article.&lt;/p&gt;
&lt;h2&gt;6. State of the Art (2024-2026): Two Defaults, Not One&lt;/h2&gt;
&lt;p&gt;Ask a working engineer &quot;what should I use in 2026?&quot; and the honest answer is two names, not one -- and which one depends on what you are protecting and where it runs.&lt;/p&gt;
&lt;h3&gt;Argon2id: the recommended cross-platform default&lt;/h3&gt;
&lt;p&gt;For greenfield application code, Argon2id is the choice. It is the RFC 9106 MUST-support hybrid and the OWASP recommendation, and it meets NIST SP 800-63B-4&apos;s generic requirements -- though NIST itself names no specific memory-hard scheme, and its only approved-scheme pointer is SP 800-132 (PBKDF2) [@nist-63b]. Its one algorithm answers &lt;em&gt;both&lt;/em&gt; threat models at once: the data-independent first half-pass closes the cache-timing side channel, and the data-dependent tail resists GPU and ASIC trade-offs. You get independent time, memory, and parallelism knobs, plus a self-describing MCF encoding that makes migration a matter of reading the stored parameters [@rfc9106; @argon2-ref].&lt;/p&gt;
&lt;p&gt;Two authoritative parameter sets anchor it. RFC 9106&apos;s &lt;strong&gt;first recommended&lt;/strong&gt; option is &lt;code&gt;t=1&lt;/code&gt;, &lt;code&gt;p=4&lt;/code&gt;, &lt;code&gt;m=2 GiB&lt;/code&gt;, a default suitable for all environments; its memory-constrained &lt;strong&gt;second&lt;/strong&gt; option is &lt;code&gt;t=3&lt;/code&gt;, &lt;code&gt;p=4&lt;/code&gt;, &lt;code&gt;m=64 MiB&lt;/code&gt; [@rfc9106]. OWASP publishes a much lower &lt;em&gt;floor&lt;/em&gt; -- &lt;code&gt;m=19 MiB&lt;/code&gt;, &lt;code&gt;t=2&lt;/code&gt;, &lt;code&gt;p=1&lt;/code&gt; -- and it is exactly that: a bare minimum to clear, not a target to aim at [@owasp].&lt;/p&gt;
&lt;p&gt;Where Argon2id struggles is the mirror image of its strength. The defender pays the full memory cost on &lt;em&gt;every&lt;/em&gt; login, so a 2 GiB-per-hash setting is a genuine RAM and denial-of-service hazard -- which is precisely why the deployed floors sit so far below the theoretical ideal. Some language bindings also default to the weaker Argon2i or to under-powered parameters, so &quot;we use Argon2&quot; is not the same claim as &quot;we use Argon2id at sound parameters.&quot;&lt;/p&gt;
&lt;h3&gt;yescrypt: the Linux &lt;code&gt;/etc/shadow&lt;/code&gt; default&lt;/h3&gt;
&lt;p&gt;Alexander Peslyak&apos;s yescrypt, from Openwall, is what actually hashes the login password on most modern Linux systems. It builds on scrypt&apos;s sequential memory-hardness and adds &lt;strong&gt;pwxform&lt;/strong&gt; -- S-box lookups interleaved with integer multiplication that annoy GPUs, FPGAs, and ASICs even at &lt;em&gt;low&lt;/em&gt; memory settings -- plus an optional large, site-specific &lt;strong&gt;ROM&lt;/strong&gt; of tens of gigabytes that throttles &quot;attackers&apos; use of pre-existing hardware such as botnet nodes&quot; [@yescrypt].&lt;/p&gt;
&lt;p&gt;It is engineered so its effective memory cost cannot be driven &quot;unreasonably low&quot; the way plain scrypt&apos;s and Argon2&apos;s can be at high throughput and low latency. That is exactly why it dominates the high-request-rate regime an OS login endpoint lives in [@yescrypt].&lt;/p&gt;
&lt;p&gt;yescrypt is the default &lt;code&gt;/etc/shadow&lt;/code&gt; scheme on ALT Linux, Arch, Debian 11 and later (it became the default in 11, not 12), Fedora 35 and later, Kali 2021.1 and later, and Ubuntu 22.04 and later; it is also &lt;em&gt;supported&lt;/em&gt; but not default on Fedora 29+, RHEL 9+, and Ubuntu 20.04+. The selection mechanism is libxcrypt together with &lt;code&gt;login.defs&lt;/code&gt; [@yescrypt; @libxcrypt].&lt;/p&gt;
&lt;p&gt;Where yescrypt struggles is breadth: it is Linux-centric, comparatively young, and thin on non-Linux bindings, and its ROM feature demands real operational machinery -- the ROM must &lt;em&gt;never&lt;/em&gt; be lost, because losing it invalidates every hash computed against it.&lt;/p&gt;
&lt;p&gt;Beware a naming collision before you grep a Windows codebase: the &lt;code&gt;BCrypt*&lt;/code&gt; functions in Windows CNG (Cryptography API: Next Generation) are &lt;em&gt;not&lt;/em&gt; the bcrypt password hash -- they are the platform&apos;s general crypto API [@microsoft-cng]. See the &lt;a href=&quot;https://paragmali.com/blog/cng-architecture-bcrypt-ncrypt-ksps&quot; rel=&quot;noopener&quot;&gt;CNG architecture post&lt;/a&gt; for what those calls actually do.&lt;/p&gt;
&lt;h3&gt;The honest status line&lt;/h3&gt;
&lt;p&gt;scrypt (still widely deployed, standardized as RFC 7914), bcrypt (ubiquitous and battle-tested), and PBKDF2 (the FIPS lane) are all &lt;strong&gt;active, not retired&lt;/strong&gt;. The branch-not-line reality of Section 4 persists straight into the present: there is no single winner that made the others obsolete, only a recommended default and a set of live alternatives chosen by constraint.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Argon2id at RFC 9106 parameters for greenfield application code. Leave yescrypt alone on modern Linux -- it is already the &lt;code&gt;/etc/shadow&lt;/code&gt; default, so do not reinvent it. bcrypt at work factor 10 or higher for small, legacy, or simplicity-first services. PBKDF2-HMAC-SHA256 at 600,000 iterations only when FIPS forces your hand.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&quot;Use Argon2id, or leave yescrypt alone&quot; is the entire answer for new code on a known platform. But engineers inherit constraints -- a regulator that forbids the primitive, a runtime with no good binding, a login endpoint one traffic spike away from falling over. So the real question is not &quot;what is best&quot; in the abstract. It is &quot;what are my options, ranked, and exactly when does each one apply?&quot;&lt;/p&gt;
&lt;h2&gt;7. The Decision Matrix: Use X With These Parameters in Case Y&lt;/h2&gt;
&lt;p&gt;Five deployed schemes, one page, ranked -- not by fashion but by the economic yardstick this whole article is built on: how much does one attacker guess cost, and what does imposing that cost do to you? The master matrix in Section 4 lays out the axes that decide it -- memory-hardness, standardization and FIPS status, side-channel posture (data-dependent versus data-independent), library breadth, and tunability. What follows turns those axes into rules you can apply on sight.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your binding constraint&lt;/th&gt;
&lt;th&gt;Use this&lt;/th&gt;
&lt;th&gt;At these parameters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Greenfield app, good library&lt;/td&gt;
&lt;td&gt;Argon2id [@rfc9106]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2 GiB, t=1, p=4&lt;/code&gt; if affordable; else &lt;code&gt;64 MiB, t=3, p=4&lt;/code&gt;; never below the &lt;code&gt;19 MiB, t=2, p=1&lt;/code&gt; floor [@owasp]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux OS login&lt;/td&gt;
&lt;td&gt;yescrypt [@yescrypt]&lt;/td&gt;
&lt;td&gt;the distro default; do not hand-roll it [@libxcrypt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huge single-operator, can run a ROM&lt;/td&gt;
&lt;td&gt;yescrypt with ROM [@yescrypt]&lt;/td&gt;
&lt;td&gt;raise the attacker&apos;s &lt;em&gt;capital&lt;/em&gt; cost in scarce large RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need memory-hardness, no Argon2id&lt;/td&gt;
&lt;td&gt;scrypt [@owasp]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;N=2^17, r=8, p=1&lt;/code&gt; (about 128 MiB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIPS-140 or regulatory constraint&lt;/td&gt;
&lt;td&gt;PBKDF2-HMAC-SHA256 [@owasp]&lt;/td&gt;
&lt;td&gt;at least &lt;code&gt;600,000&lt;/code&gt; iterations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy, small, or simplicity-first&lt;/td&gt;
&lt;td&gt;bcrypt [@owasp]&lt;/td&gt;
&lt;td&gt;work factor 10 or higher; enforce the 72-byte cap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read the axes together and the rules explain themselves. If you need cache-timing safety on a shared or multi-tenant host, you want a data-independent or hybrid scheme, which points at Argon2id over Argon2d or scrypt. If a regulator dictates approved primitives, memory-hardness is off the table entirely and PBKDF2 is the only compliant answer, whatever its per-guess weakness.&lt;/p&gt;
&lt;p&gt;If your constraint is a login endpoint hammered at high request rates, yescrypt&apos;s low-memory hardening earns its keep. And if your real constraint is a small team that needs one dependency-light library that has existed for two decades, bcrypt is a defensible engineering choice, not a mistake.&lt;/p&gt;
&lt;p&gt;That last point is the one that makes this section trustworthy, so state it plainly: &lt;strong&gt;the ranking is not absolute.&lt;/strong&gt; A well-tuned bcrypt on a small service can out-secure an &lt;em&gt;under-provisioned&lt;/em&gt; Argon2id that a denial-of-service-averse operator was forced to dial down to the 19 MiB floor. The most credible voice on this is not a bcrypt partisan -- it is the author of yescrypt, the very scheme that sits at the top of the memory-hard heap.&lt;/p&gt;

&quot;For smaller deployments, bcrypt with its simplicity and existing library support is a reasonable short-term choice.&quot; -- Alexander Peslyak, author of yescrypt [@yescrypt]
&lt;p&gt;FIPS deserves one more word, because it does something the rest of the matrix does not: it &lt;em&gt;collapses&lt;/em&gt; the whole decision to a single cell. Under a FIPS-140 constraint, none of the memory-hard reasoning applies -- Argon2, scrypt, and yescrypt are simply not approved -- and you deploy PBKDF2-HMAC-SHA256 at a high iteration count and accept its parallelism weakness as the price of compliance [@owasp]. The economic yardstick still governs; the regulator has just fixed one of its variables for you.&lt;/p&gt;
&lt;p&gt;The matrix ranks the schemes, but notice what it quietly concedes. Every &quot;best&quot; here is bounded by what the &lt;em&gt;defender&lt;/em&gt; can afford to run on every single login, and every entry leans on assumptions about attacker hardware that no one can hold fixed across a decade. Pull on that thread and you arrive at the theory -- where memory-hardness turns out to carry a hard, &lt;em&gt;proven&lt;/em&gt; ceiling that no amount of engineering can lift.&lt;/p&gt;
&lt;h2&gt;8. Theoretical Limits: Memory-Hard by Design Is Not Optimally Memory-Hard&lt;/h2&gt;
&lt;p&gt;Password hashing is one of the rare corners of applied cryptography with real complexity theory bolted to it -- and the theory delivers two humbling results the marketing never mentions.&lt;/p&gt;
&lt;h3&gt;The ideal, and the impossibility&lt;/h3&gt;
&lt;p&gt;The best conceivable attacker cost for a function that touches &lt;code&gt;n&lt;/code&gt; blocks over &lt;code&gt;n&lt;/code&gt; steps is $AT = \Theta(n^2)$: you pay the full memory-time area even with unlimited parallel cores. scrypt&apos;s &lt;em&gt;data-dependent&lt;/em&gt; ROMix is designed to reach it. But you cannot have all three of $\Theta(n^2)$ hardness, cache-timing safety, and free parameter choice at once -- and that is a theorem, not a limitation of any particular design.&lt;/p&gt;
&lt;p&gt;Joel Alwen and Jeremiah Blocki proved that &lt;em&gt;data-independent&lt;/em&gt; memory-hard functions -- the class you are forced into for cache-timing safety, because their access pattern must not depend on the password -- &lt;strong&gt;provably cannot reach the ideal bound&lt;/strong&gt;. Any iMHF can be computed at $O(n^2/\log^{1-\varepsilon} n)$, and Argon2i specifically at $O(n^{7/4}\log n)$ [@alwen-blocki2016]. That sublinear shortfall is small, but it is real, and it is unavoidable for the whole data-independent class.&lt;/p&gt;
&lt;p&gt;This is the single most consequential theorem in the area, because it is the actual reason RFC 9106 prefers the &lt;strong&gt;Argon2id hybrid&lt;/strong&gt; over the safer-&lt;em&gt;looking&lt;/em&gt; pure-data-independent Argon2i. Recall the addressing diagram from Section 5: Argon2id uses data-independent addressing only for the first half-pass, where the side channel would otherwise leak, then switches to data-dependent addressing, which escapes the iMHF impossibility.&lt;/p&gt;
&lt;p&gt;The RFC bakes this in directly. It makes Argon2id MUST-support, notes that Argon2i &quot;makes more passes over the memory to protect from trade-off attacks,&quot; and leans on the Alwen and Blocki attack -- its reference tag &lt;code&gt;[AB16]&lt;/code&gt; -- to conclude that three passes is &quot;almost optimal&quot; for Argon2i [@rfc9106; @alwen-blocki2016].&lt;/p&gt;
&lt;p&gt;The follow-on results fill in the picture, and they are worth one breath each so the section stays proportionate. Blocki and Zhou tightened the Argon2i bound to $O(n^{1.768})$ from above and $\Omega(n^{1.75})$ from below [@blocki-zhou2017]. &lt;strong&gt;DRSample&lt;/strong&gt; is the best practical iMHF construction known, built for high depth-robustness [@drsample2017]. &lt;strong&gt;Balloon&lt;/strong&gt; was the first practical iMHF to ship with a memory-hardness proof [@balloon2016].&lt;/p&gt;
&lt;p&gt;On the data-dependent side, the attacks are only &lt;em&gt;constant-factor&lt;/em&gt; trade-offs found by real cryptanalysis rather than asymptotic breaks -- Biryukov and Khovratovich, for instance, cut Catena&apos;s AT-cost by a factor of about 25 using $M^{4/5}$ memory [@biryukov-khovratovich2015]. The map is detailed, but the headline is simple: the class you need for side-channel safety is the class that cannot be optimal, so the field hybridizes.&lt;/p&gt;
&lt;h3&gt;The second, mundane impossibility: no free lunch&lt;/h3&gt;
&lt;p&gt;The other limit needs no complexity theory at all. The defender pays the &lt;em&gt;same&lt;/em&gt; per-evaluation cost as one attacker guess. So the hardness you can impose is capped by &lt;em&gt;your own&lt;/em&gt; budget: the RAM you can spare per concurrent login, the hash latency your users will tolerate (roughly one second at the very most), and the denial-of-service surface you expose when an attacker hammers your login endpoint to make you do the expensive work.&lt;/p&gt;
&lt;p&gt;RFC 9106 frames the rational defender not as someone climbing an unbounded hardness curve but as someone maximizing attacker cost &lt;em&gt;for a fixed defender time budget&lt;/em&gt; -- an optimization with a ceiling, not a dial that goes to infinity [@rfc9106].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You cannot make a password hash arbitrarily strong. You can only make it as strong as you can afford to run on &lt;em&gt;every&lt;/em&gt; login, because your per-evaluation cost equals one attacker guess. Hardness is bounded by your RAM times concurrency, your latency tolerance, and your denial-of-service budget -- not by the algorithm.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Quantum computing does not change this calculus either. The Section 11 FAQ carries the full argument; in short, Grover&apos;s square-root speedup is no Shor-style break against a one-way hash and is swamped by the per-guess costs a memory-hard scheme already imposes, so post-quantum worry belongs to your key exchange, not your password store [@nist-ir-8105].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The humbling synthesis: &quot;memory-hard by design&quot; is not &quot;optimally memory-hard&quot; -- Alwen and Blocki prove the data-independent class cannot reach the ideal AT bound, which is &lt;em&gt;why&lt;/em&gt; Argon2id is a hybrid. The defender pays the memory too, so hardness is bounded by your own budget. And the hash is rarely the weakest link anyway. The verdict is not &quot;use the strongest function&quot; but &quot;price a guess correctly, on both sides of the ledger&quot; -- an economic decision, not a single winner.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the design problem has a proven best answer -- the id hybrid -- &lt;em&gt;and&lt;/em&gt; a proven ceiling -- your own budget. Yet scanners still find broken password storage in production every year, and the field still argues, in public, about whether any of this memory-hardness is worth paying for. Both of those live on the open frontier.&lt;/p&gt;
&lt;h2&gt;9. Open Problems: The Arms Race, and the Argument About Whether to Fight It&lt;/h2&gt;
&lt;p&gt;The construction-level question is largely settled -- use the id hybrid, and the theory tells you why. The &lt;em&gt;deployment&lt;/em&gt; question is wide open, and one of its honest sub-questions is whether the field&apos;s own default is worth its price.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Principled parameter selection under real budgets.&lt;/strong&gt; The question every engineer actually has -- given RAM times concurrency, a latency target, login queries per second, and a denial-of-service tolerance, output the AT-optimal &lt;code&gt;(m, t, p)&lt;/code&gt; -- has no closed-form answer. RFC 9106 offers two fixed configurations; OWASP offers equivalence classes; neither is a portable optimizer you can point at your server and trust [@rfc9106; @owasp]. In practice you measure, pick a latency target, and revisit it, which is craft, not calculation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The moving hardware economics.&lt;/strong&gt; Every parameter floor silently assumes an attacker hardware model, and that model drifts. When a new GPU generation gains memory bandwidth, yesterday&apos;s &quot;19 MiB minimum&quot; quietly weakens without a line of your code changing. The RTX 4090 benchmark in this article is one snapshot of a gradient that shifts every hardware generation [@hashcat-4090]. yescrypt&apos;s optional ROM is one structural answer: raise the attacker&apos;s &lt;em&gt;capital&lt;/em&gt; cost -- the price of enough scarce large RAM -- rather than only the marginal cost per guess [@yescrypt].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Peppering and secret-salt key management.&lt;/strong&gt; A server-held secret mixed into the hash makes a stolen database uncrackable &lt;em&gt;without&lt;/em&gt; also stealing a separately stored key -- a large practical win. But get the construction wrong and you cannot ever rotate that secret.&lt;/p&gt;

A *secret*, server-held value -- kept in an HSM, KMS, or TEE, separate from the database -- mixed into the hash as defense-in-depth, so that a stolen database *alone* cannot be cracked. It is distinct from the public per-password salt: the salt makes verifiers unique and is stored beside them; the pepper is a secret and must never be.
&lt;p&gt;The engineering answer is to &lt;em&gt;encrypt&lt;/em&gt; the hash under the pepper (which is rotatable, the design Dropbox shipped) rather than to HMAC it into a one-way step (which is not). NIST SP 800-63B-4 recommends exactly this: an additional keyed hashing or encryption operation with the key held in a hardware-protected area such as an HSM or TPM [@dropbox; @nist-63b]. The how-to is in Section 10.&lt;/p&gt;

Present both sides, because reasonable experts disagree. **For:** the AT-cost theory and the benchmark gradient are real -- scrypt costs an attacker orders of magnitude more per guess than PBKDF2, and bcrypt sits in between [@scrypt-paper; @hashcat-4090]. **Against, for small deployments:** yescrypt&apos;s *own author* endorses bcrypt as a reasonable short-term choice, and Dropbox shipped plain bcrypt at cost 10 plus a global pepper and called it among the strongest approaches in production [@yescrypt; @dropbox]. And the sharpest point of all: the failure catalog shows the *usual* weakest link is almost never the hash&apos;s memory parameter. Ashley Madison&apos;s genuinely strong bcrypt at cost 12 fell because the same database also stored a fast MD5 token, `md5(lc($username).&quot;::&quot;.lc($pass))`, that CynoSure Prime cracked in hours and then case-corrected against the bcrypt values [@cynosure-am]. Facebook logged between 200 and 600 million passwords in *plaintext*, searchable by some 20,000 employees [@krebs-fb2019]. Adobe *encrypted* about 130 million passwords with reversible 3DES in ECB mode instead of hashing them [@adobe-ars2013]. And password shucking turns a naive `bcrypt(md5($pw))` into a fast-hash problem: attacking the bcrypt directly runs near 2,000 guesses per second, but the shucked MD5 runs near 64 *billion* [@scottbrady-shucking]. In every case, memory-hardness would have changed nothing. The correct framing is economic, not dogmatic: memory-hardness is the recommended *default*, not a universal mandate.
&lt;p&gt;Every open problem here reduces to the sentence this article began with -- maximize attacker cost per guess without bankrupting the defender -- now carrying one humbling clause: the hash is only &lt;em&gt;one&lt;/em&gt; term in a much larger security budget, and often not the term that decides the outcome. Which means the practical guide is not a victory lap. It is that sentence, made operational.&lt;/p&gt;
&lt;h2&gt;10. The Practical Guide: Decision Rules, Parameters, and Shipping Code&lt;/h2&gt;
&lt;p&gt;Everything above collapses into five short things: a decision procedure, a parameter table, the concrete library call for each scheme, a list of patterns to never ship, and the login-handler code the whole article has been building toward. None of it requires re-deriving the theory. It requires knowing your one binding constraint and reading down from there.&lt;/p&gt;
&lt;p&gt;Start with the constraint, because it -- not fashion -- picks the scheme. Walk this tree top to bottom and stop at the first branch that matches your situation.&lt;/p&gt;

flowchart TD
    S[&quot;Start: choosing a password hash&quot;] --&amp;gt; F{&quot;FIPS or regulatory constraint on primitives?&quot;}
    F --&amp;gt;|&quot;Yes&quot;| FP[&quot;PBKDF2-HMAC-SHA256, at least 600,000 iterations&quot;]
    F --&amp;gt;|&quot;No&quot;| L{&quot;Hashing the Linux /etc/shadow login?&quot;}
    L --&amp;gt;|&quot;Yes&quot;| LY[&quot;yescrypt, the distro default via libxcrypt. Do not reinvent it&quot;]
    L --&amp;gt;|&quot;No&quot;| G{&quot;Greenfield app with a good library?&quot;}
    G --&amp;gt;|&quot;Yes&quot;| GA[&quot;Argon2id. Target 2 GiB, t=1, p=4. Else 64 MiB, t=3, p=4. Never below 19 MiB, t=2, p=1&quot;]
    G --&amp;gt;|&quot;No&quot;| H{&quot;Huge single-operator deployment that can run a multi-GB ROM?&quot;}
    H --&amp;gt;|&quot;Yes&quot;| HR[&quot;yescrypt plus a site ROM, to raise the attacker&apos;s capital cost&quot;]
    H --&amp;gt;|&quot;No&quot;| M{&quot;Need memory-hardness but Argon2id is unavailable?&quot;}
    M --&amp;gt;|&quot;Yes&quot;| MS[&quot;scrypt, N of 2^17, r=8, p=1, about 128 MiB&quot;]
    M --&amp;gt;|&quot;No&quot;| BC[&quot;bcrypt, work factor 10 or higher, 72-byte cap enforced&quot;]
&lt;p&gt;The tree points at a scheme; the table below pins the numbers. Every figure here is drawn from RFC 9106, the OWASP Password Storage Cheat Sheet, yescrypt&apos;s documentation, or NIST SP 800-63B-4 [@rfc9106; @owasp; @yescrypt; @nist-63b]. One trap to name explicitly: OWASP&apos;s iteration counts drift upward over time, so this table prints the values from the dated 2025-06-02 snapshot, not the live page -- if you copy a number, copy it from a dated source so your reviewer can reproduce it.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Parameters to start from (2024-2026)&lt;/th&gt;
&lt;th&gt;Salt&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Argon2id&lt;/td&gt;
&lt;td&gt;RFC 9106 target &lt;code&gt;t=1, p=4, m=2 GiB&lt;/code&gt;; memory-constrained &lt;code&gt;t=3, p=4, m=64 MiB&lt;/code&gt;; OWASP floor &lt;code&gt;t=2, p=1, m=19 MiB&lt;/code&gt; [@rfc9106; @owasp]&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Pin the id variant; never ship bare Argon2i as the default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yescrypt&lt;/td&gt;
&lt;td&gt;The distro default settings; add a multi-GB ROM at scale [@yescrypt]&lt;/td&gt;
&lt;td&gt;library-managed&lt;/td&gt;
&lt;td&gt;Linux &lt;code&gt;/etc/shadow&lt;/code&gt;; &lt;code&gt;$y$&lt;/code&gt; MCF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scrypt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;N&lt;/code&gt; of &lt;code&gt;2^17&lt;/code&gt;, &lt;code&gt;r=8&lt;/code&gt;, &lt;code&gt;p=1&lt;/code&gt; (about 128 MiB) [@owasp]&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Or an equivalent point on the p-for-RAM curve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt&lt;/td&gt;
&lt;td&gt;Work factor 10 or higher; enforce the 72-byte cap [@owasp]&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Safe pre-hash only as &lt;code&gt;bcrypt(base64(hmac-sha384($pw, key=pepper)))&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PBKDF2 (FIPS lane)&lt;/td&gt;
&lt;td&gt;HMAC-SHA256 600,000; HMAC-SHA512 210,000; HMAC-SHA1 1,300,000 (legacy) [@owasp]&lt;/td&gt;
&lt;td&gt;32 bits or more; use 16 bytes&lt;/td&gt;
&lt;td&gt;These are the dated-snapshot figures; the live page has since drifted up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;From a scheme name to a real API call&lt;/h3&gt;
&lt;p&gt;Naming the scheme is not the same as shipping it, and the gap between them is where a surprising amount of security is lost. Here is the bridge from each scheme to the concrete binding a protocol designer actually calls.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Where to call it, in real code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Argon2id&lt;/td&gt;
&lt;td&gt;libsodium &lt;code&gt;crypto_pwhash&lt;/code&gt;, the PHC reference library, &lt;code&gt;argon2-cffi&lt;/code&gt; (Python), PHP &lt;code&gt;PASSWORD_ARGON2ID&lt;/code&gt;, &lt;code&gt;golang.org/x/crypto/argon2&lt;/code&gt;, Rust &lt;code&gt;argon2&lt;/code&gt;, &lt;code&gt;node-argon2&lt;/code&gt;, and .NET wrappers [@argon2-ref]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yescrypt&lt;/td&gt;
&lt;td&gt;&lt;code&gt;libxcrypt&lt;/code&gt; through &lt;code&gt;crypt(3)&lt;/code&gt;, plus Openwall&apos;s reference code [@libxcrypt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;scrypt&lt;/td&gt;
&lt;td&gt;OpenSSL, libsodium, Python &lt;code&gt;hashlib.scrypt&lt;/code&gt;, Node &lt;code&gt;crypto.scrypt&lt;/code&gt; [@rfc7914]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt&lt;/td&gt;
&lt;td&gt;PHP &lt;code&gt;PASSWORD_BCRYPT&lt;/code&gt;, Spring Security, Django, and &lt;code&gt;bcrypt&lt;/code&gt; libraries for Ruby, Node, and Python [@cryptbf]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PBKDF2&lt;/td&gt;
&lt;td&gt;Every standard library: .NET &lt;code&gt;Rfc2898DeriveBytes&lt;/code&gt;, Python &lt;code&gt;hashlib.pbkdf2_hmac&lt;/code&gt;, Django&apos;s default hasher [@rfc8018]&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; The load-bearing warning of this whole section: several Argon2 bindings default to the weaker Argon2i, or to under-powered memory and time settings. &quot;We use Argon2&quot; is not the claim you want -- &quot;we use Argon2id at &lt;code&gt;t&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt; we chose on purpose&quot; is. Read your library&apos;s default constructor, pin the variant and every parameter explicitly, and write a test that fails if the stored cost drops below your target [@argon2-ref].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The four things you always do&lt;/h3&gt;
&lt;p&gt;Whatever the tree selected, four habits are non-negotiable. NIST SP 800-63B-4 section 3.1.1.2 &lt;em&gt;requires&lt;/em&gt; (SHALL) two of them -- salted and hashed storage with an approved one-way function, and verification of the &lt;em&gt;entire&lt;/em&gt; password with no truncation -- and &lt;em&gt;recommends&lt;/em&gt; (SHOULD) a third, a stored algorithm-and-cost reference so you can migrate; it also calls for throttling on repeated failures [@nist-63b].&lt;/p&gt;
&lt;p&gt;First, generate a &lt;strong&gt;16-byte per-password salt from a CSPRNG&lt;/strong&gt;. NIST&apos;s floor is a mere 32 bits; treat that as a floor to clear by a wide margin, not a target, and generate it the way &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand&quot; rel=&quot;noopener&quot;&gt;Part 2, &quot;Predictable or Repeated,&quot;&lt;/a&gt; insists [@nist-63b]. Second, store the algorithm and its parameters in &lt;strong&gt;MCF form&lt;/strong&gt;, so a verifier can dispatch on the scheme and upgrade in place. Third, compare verifiers in &lt;strong&gt;constant time&lt;/strong&gt;, so you never leak how many bytes an attacker has guessed.&lt;/p&gt;
&lt;p&gt;Fourth, &lt;strong&gt;rehash on login&lt;/strong&gt;: when the stored cost sits below today&apos;s target, transparently recompute the verifier from the plaintext you just checked, and write the stronger value back. That last habit turns a parameter choice from a one-time decision into a living one -- and it is short enough to paste in.&lt;/p&gt;
&lt;p&gt;{`
// Mock only -- this illustrates control flow, not real crypto.
// A real system calls its library&apos;s verify(), needsRehash(), and hash().&lt;/p&gt;
&lt;p&gt;var TODAYS_TARGET_COST = 12; // e.g. a bcrypt work factor, or an Argon2 (m,t,p) tuple&lt;/p&gt;
&lt;p&gt;// Constant-time comparison: never return early on the first differing byte,
// or the timing leaks how much of the verifier the attacker has matched.
function constantTimeEquals(a, b) {
  if (a.length !== b.length) return false;
  var diff = 0;
  for (var i = 0; i &amp;lt; a.length; i++) {
    diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }
  return diff === 0;
}&lt;/p&gt;
&lt;p&gt;function mockHash(password, cost) {
  // Stand-in for bcrypt or Argon2id. Real code never rolls its own.
  return &quot;cost=&quot; + cost + &quot;:&quot; + password.split(&quot;&quot;).reverse().join(&quot;&quot;);
}
function costOf(verifier) {
  return parseInt(verifier.split(&quot;:&quot;)[0].split(&quot;=&quot;)[1], 10);
}&lt;/p&gt;
&lt;p&gt;function login(password, stored, updateStore) {
  var candidate = mockHash(password, costOf(stored));
  if (!constantTimeEquals(candidate, stored)) {
    return &quot;reject&quot;; // and throttle repeated failures for this account
  }
  // Verified. If the stored cost is below today&apos;s target, upgrade transparently.
  if (costOf(stored) &amp;lt; TODAYS_TARGET_COST) {
    updateStore(mockHash(password, TODAYS_TARGET_COST));
    return &quot;accept, and rehashed from cost &quot; + costOf(stored) + &quot; to &quot; + TODAYS_TARGET_COST;
  }
  return &quot;accept&quot;;
}&lt;/p&gt;
&lt;p&gt;var stored = mockHash(&quot;correct horse battery staple&quot;, 10); // hashed years ago at cost 10
console.log(login(&quot;correct horse battery staple&quot;, stored, function (h) {
  console.log(&quot;  stored verifier upgraded -&amp;gt;&quot;, h.slice(0, 16) + &quot;...&quot;);
}));
console.log(login(&quot;wrong password&quot;, stored, function () {}));
`}&lt;/p&gt;
&lt;h3&gt;Peppering, and server-cost budgeting&lt;/h3&gt;
&lt;p&gt;Two operational topics finish the guide. The first is peppering -- powerful, and easy to get wrong in a way you cannot undo.&lt;/p&gt;

A pepper is a *secret* mixed into the hash as defense-in-depth, and three things decide whether it helps or hurts. **One: keep the secret out of the database.** It belongs in an HSM, a KMS, or a TEE, never in the same store as the verifiers it is meant to protect -- a pepper sitting next to the hash is not a pepper, it is decoration. **Two: encrypt the hash under the pepper, do not HMAC it in.** Dropbox&apos;s design is the reference: `SHA512(pw)`, then bcrypt at cost 10 with a per-user salt, then `AES-256` encryption of that output under a global pepper held separately [@dropbox]. Because encryption is reversible with the key, you can *rotate* the pepper; an HMAC folded into the one-way step cannot be rotated without every user&apos;s plaintext, which you do not have. **Three: treat it as a bonus layer, never a substitute** for a real password hash. NIST SP 800-63B-4 recommends exactly this shape -- an additional keyed hashing or encryption operation with the key in a hardware-protected area [@nist-63b]. And if you pepper to work around bcrypt&apos;s 72-byte cap, the safe pre-hash is `bcrypt(base64(hmac-sha384($pw, key=pepper)))`: the keyed HMAC defeats password shucking, and the base64 encoding defeats the `$2a$` NUL-truncation bug [@owasp; @scottbrady-shucking].
&lt;p&gt;The second is the budget the whole design lives inside. Memory multiplied by concurrency is the real constraint, not CPU: Argon2id at 1 GiB per hash across 200 simultaneous logins wants roughly 200 GiB of transient RAM. That is the concrete reason the deployed floors of 19 to 64 MiB sit so far below RFC 9106&apos;s 2 GiB ideal [@rfc9106; @owasp].&lt;/p&gt;
&lt;p&gt;Target somewhere between half a second and one second per hash, measured on your own hardware. Rate-limit the login endpoint, because an attacker who floods it makes &lt;em&gt;you&lt;/em&gt; pay the expensive hashing cost -- a denial-of-service lever you handed them. And where the traffic justifies it, put a cheap pre-filter in front of the expensive hash. The parameters are not a trophy you set once; they are a running lease on your own server budget.&lt;/p&gt;

The parameter tables above are starting points, not answers -- the only number that finally matters is how long a hash takes on *your* hardware. The Argon2 reference CLI measures it directly. This runs Argon2id at 64 MiB, three passes, and four lanes, and prints the elapsed time:&lt;p&gt;&lt;code&gt;echo -n &quot;correct horse battery staple&quot; | argon2 mysalt1234 -id -t 3 -m 16 -p 4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here &lt;code&gt;-id&lt;/code&gt; selects the Argon2id variant and &lt;code&gt;-m 16&lt;/code&gt; means &lt;code&gt;2^16&lt;/code&gt; KiB, which is 64 MiB. Raise &lt;code&gt;-m&lt;/code&gt; until the reported time approaches your latency budget -- roughly half a second to a second -- then hold there, and re-measure on the hardware you will actually deploy on, not your laptop [@argon2-ref].
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The checklist is short because the lesson is one sentence. But checklists are not what fail in production -- confident, wrong sentences are, the kind that survive code review because they sound obviously true. Clear those before the conclusion, and the bug that has haunted this entire article finally has nowhere left to live.&lt;/p&gt;
&lt;h2&gt;11. FAQ and Common Misuse: The Confident, Wrong Sentences&lt;/h2&gt;
&lt;p&gt;Broken password storage survives mostly because a handful of plausible-sounding claims keep getting said in design meetings, and a handful of code patterns keep getting shipped past review. Named and corrected, they lose their power. Here is the consolidated catalog of what goes wrong in real code, each pattern mapped to the named breach it reproduces and the one change that fixes it.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse pattern in the code&lt;/th&gt;
&lt;th&gt;The break it reproduces&lt;/th&gt;
&lt;th&gt;The fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;A bare fast hash (MD5, SHA-1, SHA-256) as the password store&lt;/td&gt;
&lt;td&gt;LinkedIn, RockYou&lt;/td&gt;
&lt;td&gt;Use a deliberately slow, ideally memory-hard KDF [@troyhunt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bcrypt silently truncating input at 72 bytes&lt;/td&gt;
&lt;td&gt;Long passphrases lose entropy past byte 72&lt;/td&gt;
&lt;td&gt;Enforce the 72-byte cap explicitly, or pre-hash correctly [@cryptbf; @owasp]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Naive unsalted pre-hash &lt;code&gt;bcrypt(md5($pw))&lt;/code&gt;, and the &lt;code&gt;$2a$&lt;/code&gt; NUL bug&lt;/td&gt;
&lt;td&gt;Password shucking&lt;/td&gt;
&lt;td&gt;Salt or HMAC the inner step and base64-encode it [@scottbrady-shucking]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argon2d (data-dependent) on shared or multi-tenant hosts&lt;/td&gt;
&lt;td&gt;Cache-timing exposure of the access pattern&lt;/td&gt;
&lt;td&gt;Prefer Argon2id, whose first half-pass is data-independent [@rfc9106]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A global salt, a reused salt, or no salt&lt;/td&gt;
&lt;td&gt;LinkedIn: rainbow tables and cross-account hash sharing&lt;/td&gt;
&lt;td&gt;A unique per-password CSPRNG salt, stored in the MCF string [@troyhunt]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A parallel weak code path that also touches the password&lt;/td&gt;
&lt;td&gt;Ashley Madison&apos;s fast MD5 &lt;code&gt;$loginkey&lt;/code&gt; beside strong bcrypt&lt;/td&gt;
&lt;td&gt;Audit every function that sees the password, not just the verifier [@cynosure-am]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypting the password instead of hashing it&lt;/td&gt;
&lt;td&gt;Adobe&apos;s reversible 3DES in ECB mode&lt;/td&gt;
&lt;td&gt;Hash, never encrypt, a stored password [@adobe-ars2013]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storing the pepper next to the hash it protects&lt;/td&gt;
&lt;td&gt;Defeats the entire point of a secret&lt;/td&gt;
&lt;td&gt;Keep the pepper in a separate HSM, KMS, or TEE [@nist-63b]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Parameters chosen once and never raised&lt;/td&gt;
&lt;td&gt;A 2019 cost is weaker every year after&lt;/td&gt;
&lt;td&gt;Rehash on login and lift the floor as hardware improves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No maximum input length feeding PBKDF2&lt;/td&gt;
&lt;td&gt;Django&apos;s 2013 long-password DoS (CVE-2013-1443)&lt;/td&gt;
&lt;td&gt;Cap the accepted input length; Django&apos;s fix rejected inputs over 4096 bytes [@django-cve]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier comparison that short-circuits on the first byte&lt;/td&gt;
&lt;td&gt;A timing side channel on the compare&lt;/td&gt;
&lt;td&gt;Constant-time equality, plus throttling of failed attempts [@nist-63b]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Memory-hardness is always mandatory&quot;&lt;/td&gt;
&lt;td&gt;Over-provisioned Argon2id dialed to a DoS-safe floor&lt;/td&gt;
&lt;td&gt;Treat memory-hardness as the default, not a universal law [@yescrypt]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The Django entry is worth one sentence, because it is the misuse people least expect. Django set no maximum on password length, so an attacker could submit a roughly one-megabyte password -- about a minute of PBKDF2 work -- purely to exhaust server CPU, a self-inflicted denial of service the fix closed by rejecting any input over 4096 bytes [@django-cve].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Your password security is the &lt;em&gt;weakest function that ever touches the password&lt;/em&gt;. A strong bcrypt sitting beside a fast MD5 token (Ashley Madison), a plaintext line in a log file (Facebook), or a naive &lt;code&gt;bcrypt(md5($pw))&lt;/code&gt; (shucking) erases every bit of the verifier&apos;s strength. Audit the whole path, not just the column labeled &quot;password hash.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With the code patterns cleared, here are the sentences themselves -- the confident, wrong claims, and the short answers that retire them.&lt;/p&gt;


Because its strength is not the property that matters, and its speed is the flaw. A password hash exists to make each guess expensive; SHA-256 is engineered to make each evaluation cheap, so one GPU tries tens of billions of guesses per second and a stolen dump falls to a wordlist in minutes [@hashcat-4090]. Use a deliberately slow, ideally memory-hard function -- Argon2id, scrypt, bcrypt, or PBKDF2 -- not a general-purpose hash.


They already salt for you: bcrypt and Argon2id generate a per-password salt and embed it in the MCF string. What you must never do is reuse a salt, use a single global salt, or omit it -- that is precisely the LinkedIn failure, where identical passwords collided and precomputed tables applied [@troyhunt]. The salt is public and stored beside the hash; it is not the secret, and it changes attacker throughput by zero.


No. bcrypt (1999) and PBKDF2 (2000) were never superseded, only joined by the memory-hard generation. bcrypt at work factor 10 or higher is a defensible choice for many deployments -- yescrypt&apos;s own author says as much for smaller ones -- and a FIPS constraint forces PBKDF2 regardless [@yescrypt]. Memory-hardness is the recommended default, not a universal mandate.


A pepper is a *secret*, server-held value -- kept in an HSM or KMS, separate from the database -- mixed into the hash so a stolen database alone cannot be cracked [@nist-63b]. It is defense-in-depth only, never a substitute for a real password hash. If you use one, *encrypt* the hash under it (rotatable, the design Dropbox shipped) rather than HMAC it into the one-way step (not rotatable), and never store it next to the hash [@dropbox].


Only if you do it the careful way. A naive `bcrypt(md5($pw))` with an unsalted inner hash enables password shucking, and a NUL byte in the pre-hash can truncate the input [@scottbrady-shucking]. Salt or HMAC the inner step and base64-encode it -- `bcrypt(base64(hmac-sha384($pw, key=pepper)))` -- or enforce the 72-byte cap and skip pre-hashing entirely.


No. Grover&apos;s algorithm offers only a square-root speedup on guessing, which roughly halves effective bit-strength, and that gain is swamped by the memory-bandwidth and per-guess costs a good password hash already imposes. No Shor-style structural break applies to a one-way hash [@nist-ir-8105]. Your multi-factor and password-reuse posture matter far more to your real risk.


There is no closed-form answer, which is why this is still an open problem. Target roughly half a second to one second per hash, measured on your own hardware, within your RAM-times-peak-concurrency and denial-of-service budget. Start from RFC 9106&apos;s two configurations or OWASP&apos;s floors, and raise them over time with rehash-on-login as your hardware and your traffic allow [@rfc9106; @owasp].

&lt;p&gt;Every correction here points at the same root. The strength of the hash was never the variable that decided the outcome. The cost per guess -- on the attacker&apos;s cheapest hardware, measured against your own server&apos;s budget -- always was. That is the sentence the whole article was built to earn, so it is time to say it in full.&lt;/p&gt;
&lt;h2&gt;One Job, Priced Two Ways&lt;/h2&gt;
&lt;p&gt;Return to the LinkedIn dump from the first page: a strong hash, no key to break, plaintext in days -- because the function was &lt;em&gt;fast&lt;/em&gt;, and fast is the one thing a password hash must never be. Everything between here and there was the field learning, and relearning, that the hash&apos;s cryptographic strength was never the variable. The cost of one guess was.&lt;/p&gt;
&lt;p&gt;Read as one arc, forty-seven years of password hashing is a single economic move, repeated. Morris and Thompson&apos;s 1979 rules -- one-way, salted, slow -- set the terms. Every advance since has restored the &lt;em&gt;same&lt;/em&gt; eroding ratio: the cost of one attacker guess divided by the cost of one honest login.&lt;/p&gt;
&lt;p&gt;Faster CPUs eroded fixed iteration counts, so bcrypt made the count a knob and added an expensive key schedule. Cheap parallel hardware -- GPUs, FPGAs, ASICs -- eroded time-hardness itself, so scrypt and Argon2 changed the &lt;em&gt;shape&lt;/em&gt; of the cost from time to memory, the one resource an attacker cannot buy asymmetrically cheaply. Botnets and cheap RAM erode even that, so yescrypt adds a site-specific ROM. Iteration time, then an expensive key schedule, then memory, then memory plus a ROM: four levers, one idea.&lt;/p&gt;
&lt;p&gt;The failure catalog proves the spine from the other side, because every entry is the ratio collapsing somewhere the verifier column could not see. Ashley Madison ran genuinely strong bcrypt and lost anyway, to a fast MD5 token stored beside it [@cynosure-am]. Facebook&apos;s at-rest hashing was undone by plaintext in a log file [@krebs-fb2019]. Adobe encrypted where it should have hashed [@adobe-ars2013]. Password shucking turned a careless pre-hash back into a fast-hash problem [@scottbrady-shucking]. In none of these did memory-hardness matter, because the weakest function that touched the password was never the one on the spec sheet.&lt;/p&gt;
&lt;p&gt;So the resolution the field converged on is not a single winner. It is a priced menu: Argon2id and yescrypt where you can afford memory-hardness, bcrypt and PBKDF2 where you cannot or where a regulator forbids it -- each choice an economic decision bounded by your own server&apos;s budget, exactly as &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-&quot; rel=&quot;noopener&quot;&gt;Part 1&apos;s attacker model&lt;/a&gt; and &lt;a href=&quot;https://paragmali.com/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp&quot; rel=&quot;noopener&quot;&gt;Part 10&apos;s &quot;a fast hash is the wrong tool&quot;&lt;/a&gt; both predicted. Keep the one-question test where you can reach it.&lt;/p&gt;

On the cheapest hardware an attacker can rent, how much does one guess at one password cost -- and how far above your own per-login cost have you pushed it?
&lt;p&gt;The hash was never the weak link, and no future hash will be, because the weak link is economic, not cryptographic. That is why this is Part 12 of a field guide to &lt;em&gt;protocol design&lt;/em&gt; rather than a catalog of algorithms. The job was never to pick the strongest function. It was to price a guess correctly, on both sides of the ledger.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;password-hashing-bcrypt-scrypt-argon2-yescrypt&quot; keyTerms={[
  { term: &quot;Stored verifier&quot;, definition: &quot;A salted, deliberately slow, one-way transform of a password, stored so that a leaked database does not directly reveal the passwords.&quot; },
  { term: &quot;Offline attack&quot;, definition: &quot;The threat model this primitive answers: the adversary holds the stolen verifiers and guesses locally at full hardware speed, with no server or rate limit in the loop.&quot; },
  { term: &quot;Salt&quot;, definition: &quot;A public per-password random value that makes each verifier unique and defeats precomputed tables; it does not change attacker throughput.&quot; },
  { term: &quot;Modular Crypt Format (MCF)&quot;, definition: &quot;A self-describing hash string, id then params then salt then hash, that lets one column hold many algorithms and migrate in place.&quot; },
  { term: &quot;Work factor (adaptive cost)&quot;, definition: &quot;A tunable knob -- bcrypt cost, PBKDF2 iterations, Argon2 time and memory -- that raises per-guess cost as hardware improves, without changing the algorithm.&quot; },
  { term: &quot;Memory-hard function (MHF)&quot;, definition: &quot;A function whose cheap computation requires holding a large array in memory for much of its runtime, imposing an area-times-time cost.&quot; },
  { term: &quot;AT-cost (area times time)&quot;, definition: &quot;Memory occupied multiplied by time held: the attacker&apos;s real per-guess cost, and the quantity the memory-hard program maximizes.&quot; },
  { term: &quot;Data-independent vs data-dependent access&quot;, definition: &quot;Whether memory addresses depend on the password; independent is cache-timing-safe but provably suboptimal, dependent is optimal but leaks through timing.&quot; },
  { term: &quot;Pepper (secret salt)&quot;, definition: &quot;A secret, server-held value kept in an HSM or KMS, separate from the database, mixed into the hash as defense-in-depth only.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>password-hashing</category><category>argon2</category><category>bcrypt</category><category>scrypt</category><category>yescrypt</category><category>memory-hardness</category><category>pbkdf2</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Tag Verified, the Cipher Held, the Forgery Went Through: A Field Guide to Message Authentication and Safe Composition</title><link>https://paragmali.com/blog/the-tag-verified-the-cipher-held-the-forgery-went-through-a-/</link><guid isPermaLink="true">https://paragmali.com/blog/the-tag-verified-the-cipher-held-the-forgery-went-through-a-/</guid><description>Why HMAC, KMAC, GMAC, and Poly1305 never broke -- but Flickr, Lucky Thirteen, and 184 GCM servers did. A field guide to MACs and Encrypt-then-MAC.</description><pubDate>Sat, 11 Jul 2026 01:12:01 GMT</pubDate><content:encoded>
A MAC makes forgery hard; three deployment assumptions keep it hard, and every famous break -- Flickr [@flickr-advisory], Lucky Thirteen [@cve-2013-0169], POODLE [@cve-2014-3566], the SSH plaintext-recovery attack [@apw-2009], and 184 nonce-repeating HTTPS servers [@woot16-bock] -- broke one of those assumptions, not the math. Pick the right family (PRF-based HMAC and KMAC take a reusable key and need no nonce; Wegman-Carter GMAC and Poly1305 run at line rate but demand a one-time key [@wc-1981]), verify in constant time, and always Encrypt-then-MAC over the ciphertext with an independent key and a strongly unforgeable tag [@bn-2000]. This guide traces the primitive from Carter-Wegman universal hashing in 1979 [@cw-1979] to committing AEAD in 2025 [@rfc-9771], names every failure by root cause, and ends with a decision tree and a paste-in review checklist. The one rule underneath all of it: authenticate the ciphertext, the length, and the context -- not just the message.
&lt;h2&gt;1. A Forgery Without a Broken Cipher&lt;/h2&gt;
&lt;p&gt;In 2009, two researchers forged valid Flickr API signatures without knowing Flickr&apos;s secret key, without finding a single MD5 collision, and without touching a cipher [@flickr-advisory]. Flickr had done what feels obviously correct -- prepend a secret to each request and hash the result -- and that one instinct silently handed signing power to anyone who had ever seen a single signed URL. The hash worked exactly as designed. The way Flickr &lt;em&gt;used&lt;/em&gt; it was the vulnerability.&lt;/p&gt;
&lt;p&gt;That gap -- between a primitive that is sound and a deployment that is not -- is the subject of this article, and it is worth stating as a claim you can test against every page that follows.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A message authentication code makes forgery hard; keeping it hard is the surrounding protocol&apos;s job. Every deployed MAC ships a proof, and every proof quietly assumes three things the deployment must supply: (1) the right &lt;strong&gt;key discipline&lt;/strong&gt; for its family, (2) a &lt;strong&gt;constant-time verify&lt;/strong&gt;, and (3) &lt;strong&gt;composition over the right bytes, in the right order, binding the right context&lt;/strong&gt;. Read that way, the entire failure catalogue is one sentence repeated -- the math never broke; a deployment broke one of these three assumptions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before we can prove that thesis, we need the object it is about. Informally, a MAC is a small tag you attach to a message so the receiver can tell the message came from someone who holds the shared secret key, and that nobody altered it in transit.&lt;/p&gt;

A MAC is a keyed algorithm that takes a secret key and a message and produces a short fixed-size tag. Anyone holding the same key can recompute the tag to verify that the message is authentic and unmodified. Unlike a plain hash, a MAC is keyed, so an attacker without the key cannot produce a valid tag; unlike a digital signature, the same secret both makes and checks the tag, so there is no public verifiability and no non-repudiation.
&lt;p&gt;Now pair Flickr with a second break that fails a &lt;em&gt;different&lt;/em&gt; assumption. In 2013, Lucky Thirteen recovered plaintext from TLS connections that used a perfectly correct HMAC -- the attack timed &lt;em&gt;how&lt;/em&gt; the MAC was checked, not whether the MAC was strong [@cve-2013-0169]. Two production systems, two respected primitives, two failures -- and in neither case did the cryptography break. Flickr fed the hash the wrong bytes in the wrong construction; TLS checked a correct tag on a timing-leaky path.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; MD5 is badly broken for collision resistance, yet the Flickr forgery did not use a collision. HMAC was and is unbroken, yet Lucky Thirteen still bled plaintext. In both cases the primitive met its specification and the deployment violated an assumption the specification silently depended on. Hold onto that pattern -- it is the whole catalogue in miniature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;By the end of this field guide you will have a decision procedure and a review checklist that catch every failure in this article before it ships.The original Flickr advisory by Duong and Rizzo is no longer served from its source host; the citation here points to a 2009 Wayback Machine snapshot, which is the durable primary record [@flickr-advisory]. But a checklist without the underlying model is just cargo-culting. So start with the model. If the hash was not broken, what exactly is a MAC supposed to promise, and against whom?&lt;/p&gt;
&lt;h2&gt;2. What a MAC Actually Is&lt;/h2&gt;
&lt;p&gt;Strip away the mystique and a MAC is three algorithms and one game.&lt;/p&gt;
&lt;p&gt;The three algorithms are &lt;code&gt;KeyGen&lt;/code&gt;, &lt;code&gt;Tag&lt;/code&gt;, and &lt;code&gt;Verify&lt;/code&gt;. &lt;code&gt;KeyGen&lt;/code&gt; samples a secret key $k$. &lt;code&gt;Tag&lt;/code&gt; maps a key and a message $m$ to a tag $t$. &lt;code&gt;Verify&lt;/code&gt; takes $k$, $m$, and a candidate $t$ and returns accept or reject. The whole design question is: how hard is it for someone without $k$ to make &lt;code&gt;Verify&lt;/code&gt; say accept on a message the key-holder never authorized?&lt;/p&gt;
&lt;p&gt;&quot;How hard&quot; was made precise in 1988. Shafi Goldwasser, Silvio Micali, and Ronald Rivest gave the first rigorous adversarial definition of authentication -- existential unforgeability under adaptive chosen-message attack -- while analyzing digital signatures, and MACs inherit it directly [@gmr-1988]. The game is deliberately generous to the attacker.&lt;/p&gt;

The adversary may ask for tags on any messages it likes, adaptively, as many as it wants. It wins if it then outputs a valid tag on any *new* message it never queried. A MAC is EUF-CMA secure if every efficient adversary wins only with negligible probability. The name is exact: &quot;existential&quot; means the forgery can be on any message the attacker chooses, and &quot;adaptive chosen-message&quot; means it sees each answer before picking its next query.
&lt;p&gt;EUF-CMA is the usual bar, but composition needs a slightly stronger one, and the difference will decide whether a whole protocol stands or falls in Section 6.&lt;/p&gt;

SUF-CMA forbids even a *new tag on an old message*. The adversary wins if it outputs any message-tag pair that verifies and was not itself the result of a tag query -- including a second, different valid tag on a message it already asked about. Every SUF-CMA MAC is EUF-CMA, but not conversely: a MAC with malleable tags can be existentially unforgeable yet still let an attacker maul a valid tag into another valid tag.
&lt;p&gt;Formally, if we write the tag oracle as $\mathsf{Tag}_k(\cdot)$ and let $Q$ be the set of message-tag pairs it returned, the strong-forgery advantage is&lt;/p&gt;
&lt;p&gt;$$\mathsf{Adv}^{\text{suf-cma}}_{\mathsf{MAC}}(\mathcal{A}) ;=; \Pr!\left[,(m,t) \leftarrow \mathcal{A}^{\mathsf{Tag}_k(\cdot)} : \mathsf{Verify}_k(m,t)=\text{accept} ;\wedge; (m,t)\notin Q,\right].$$&lt;/p&gt;
&lt;p&gt;A MAC is good when this probability stays negligible for every efficient $\mathcal{A}$. That single inequality is the promise Flickr&apos;s construction could not keep -- an attacker produced an accepted tag on a message that was never in $Q$.&lt;/p&gt;
&lt;p&gt;Four primitives live near this definition, and confusing them is the source of real bugs. An &lt;strong&gt;unkeyed hash&lt;/strong&gt; has no $k$, so anyone can recompute it -- it detects accidental corruption, never a motivated forger; this series covers hashes and their length-extension quirk in Part 10.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;MAC&lt;/strong&gt; is symmetric: the same secret makes and checks the tag. A &lt;strong&gt;digital signature&lt;/strong&gt; is asymmetric -- a private key signs, a public key verifies -- which buys public verifiability and non-repudiation that a MAC deliberately does not provide (Part 3 develops &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;what the authenticated bytes must actually cover&lt;/a&gt;). An &lt;strong&gt;AEAD&lt;/strong&gt; bundles a MAC together with encryption so one call delivers secrecy and integrity at once; Part 7 is &lt;a href=&quot;https://paragmali.com/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/&quot; rel=&quot;noopener&quot;&gt;the decision matrix for choosing among AEADs&lt;/a&gt;, and this article is the authentication half underneath it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Because a MAC key both signs and verifies, a valid tag proves only that &lt;em&gt;someone holding the shared key&lt;/em&gt; produced it -- it cannot tell you &lt;em&gt;which&lt;/em&gt; party, and it cannot be shown to a third party as proof. If you need &quot;Alice, and only Alice, provably sent this,&quot; you need a signature, not a MAC. Reaching for a MAC where you needed non-repudiation is a design error no amount of key length fixes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The definitions say a good MAC is unforgeable. Yet the very first keyed MACs people built were forgeable in ways the definition had not yet been written to outlaw. Stranger still, the airtight, provably unforgeable answer arrived &lt;em&gt;before&lt;/em&gt; the practical, broken ones. Where did the idea come from?&lt;/p&gt;
&lt;h2&gt;3. Origins: Universal Hashing and One-Time Authentication&lt;/h2&gt;
&lt;p&gt;Here is the twist that reorders the whole story. The provably secure MAC was invented first, in 1979 and 1981, and then the field spent two decades shipping forgeable ones anyway. Keep that inversion in mind as the arc unfolds.&lt;/p&gt;

timeline
    title Message authentication, 1979 to 2025
    1979 : Carter-Wegman universal hashing
    1981 : One-time authentication, tag is hash plus one-time pad
    1985 : CBC-MAC standardized in FIPS 113
    1996 : HMAC nests the hash to stop length extension
    2000 : Encrypt-then-MAC formalized by Bellare and Namprempre
    2001 : Krawczyk indicts the SSL and SSH orderings
    2004 : GMAC revives Wegman-Carter at line speed
    2005 : Poly1305 brings it to fast software
    2006 : HMAC proven a PRF without collision resistance
    2009 : Flickr forgery and the SSH plaintext recovery
    2013 : Lucky Thirteen timing oracle
    2016 : KMAC keyed sponge and 184 nonce-repeating servers
    2018 : TLS 1.3 record layer becomes AEAD-only
    2023 : Terrapin breaks a correct channel
    2025 : RFC 9771 standardizes commitment vocabulary
&lt;p&gt;The starting point is a purely combinatorial object. Larry Carter and Mark Wegman, at IBM Research, asked what happens if you pick a hash function at random from a carefully built family, rather than fixing one function everyone knows [@cw-1979].&lt;/p&gt;

A universal family of hash functions has the property that, for any two fixed distinct inputs, a function drawn uniformly at random from the family maps them to the same output with only small, bounded probability. The bound comes from counting the functions in the family, not from any assumption about how much computing power an attacker has. It is combinatorial security, not cryptographic security -- which is exactly why it needs no unproven hardness conjecture.
&lt;p&gt;Two years later, Wegman and Carter turned that object into an authenticator [@wc-1981]. The construction is startlingly small: draw a secret hash $h$ from a strongly universal family, draw a secret one-time pad $s$, and send&lt;/p&gt;
&lt;p&gt;$$t ;=; h(m) ,\oplus, s.$$&lt;/p&gt;
&lt;p&gt;The pad $s$ hides the value $h(m)$ completely, like a one-time pad hides a plaintext, and the universal property bounds an attacker&apos;s chance of guessing a valid tag for a &lt;em&gt;different&lt;/em&gt; message. The result is unconditional: it holds against an adversary with unlimited computing power, assuming nothing about factoring, discrete logs, or the strength of any cipher.&lt;/p&gt;

Authenticate a message as the exclusive-or of a universal hash of the message and a fresh secret pad: the tag reveals nothing about the hash key, and forging a tag on a new message requires guessing a value the universal bound keeps small. The security is information-theoretic and tight -- but the pad must be used exactly once. Authenticate a second message under the same pad and the guarantee evaporates.
&lt;p&gt;Take the smallest instance. Fix a prime $p$, keep a secret pair $(a, b)$, and tag a message $m$ as $t = (a\cdot m + b) \bmod p$. An attacker who sees one pair $(m_1, t_1)$ learns one linear equation in two unknowns; every candidate slope $a$ has exactly one consistent $b$, so the tag on any different $m_2$ stays uniform over all $p$ values. The best forgery succeeds with probability $1/p$. Beautiful, and useless twice: a second pair $(m_2, t_2)$ gives a second equation, the line is pinned, and every future tag is computable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &quot;One-time&quot; is not a restriction bolted onto Wegman-Carter authentication -- it &lt;em&gt;is&lt;/em&gt; the security proof. The pad&apos;s single use is what buys the information-theoretic guarantee. That makes it a feature in 1981 and a landmine in 2004, when the same construction is revived at network line speed and a fast deployment reuses the pad. Every GMAC and Poly1305 nonce rule you will ever read is this 1981 constraint wearing modern clothes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the airtight answer existed. Yet it authenticates only one message per key, which is impractical for a general-purpose MAC.The two ideas are separate papers: the 1979 work introduced universal classes of hash functions as a combinatorial tool [@cw-1979]; the 1981 follow-up applied them to authentication and set equality [@wc-1981]. Universal hashing came first; authentication was the application. If the unconditional construction was this good but this restrictive, what did engineers reach for instead -- and why did the &quot;obvious&quot; keyed hashes they built break?&lt;/p&gt;
&lt;h2&gt;4. Early Approaches and Why They Broke&lt;/h2&gt;
&lt;p&gt;When engineers needed a keyed MAC in the 1990s, most reached for the hash they already had and glued a secret to it. The most natural glue is to put the key in front: compute &lt;code&gt;H(secret || message)&lt;/code&gt; and ship the digest as the tag. It looks unforgeable -- you cannot compute the hash without the secret. It is also exactly what Flickr shipped, and it is broken by a property of the hash that has nothing to do with its collision resistance.&lt;/p&gt;
&lt;p&gt;The property is length extension, and it is a direct consequence of how Merkle-Damgard hashes -- MD5, SHA-1, and most of SHA-2 -- are built [@length-ext-wiki]. Such a hash absorbs the message block by block into an internal state, and it emits &lt;em&gt;that entire internal state&lt;/em&gt; as the digest. The digest is therefore a complete, resumable snapshot: given &lt;code&gt;H(secret || message)&lt;/code&gt;, you can set a fresh hash&apos;s state to that value and keep absorbing more bytes, as if you had known the secret all along.&lt;/p&gt;

Given the digest of `secret || message` and the length of the secret, an attacker can compute the digest of `secret || message || padding || extra` for attacker-chosen `extra`, without knowing the secret. The attack works because a Merkle-Damgard digest is the hash&apos;s full internal state, so it can be resumed. The only wrinkle is the glue `padding` the original hash appended internally, whose bytes are fully determined by the known lengths.
&lt;p&gt;The forgery is mechanical once you see the state as resumable.&lt;/p&gt;

sequenceDiagram
    participant A as Attacker
    participant V as Verifier
    A-&amp;gt;&amp;gt;V: Observe one signed request, message m and tag t
    Note over A: The tag t is the full hash state after absorbing key and m
    A-&amp;gt;&amp;gt;A: Resume from t, append glue padding then chosen extra bytes
    A-&amp;gt;&amp;gt;A: Keep hashing to get t2 for the extended message, no key used
    A-&amp;gt;&amp;gt;V: Submit extended message with tag t2
    Note over V: Verifier recomputes over key plus the extended message
    V--&amp;gt;&amp;gt;A: State matches, forged request accepted
&lt;p&gt;The demonstration below builds a toy Merkle-Damgard hash whose digest is its internal state -- the essential property -- and forges a secret-prefix tag with no secret, then shows that nesting the hash (the HMAC idea) shuts the door.&lt;/p&gt;
&lt;p&gt;{`&lt;/p&gt;
Toy Merkle-Damgard hash: absorb one byte at a time; the digest IS the state.
(The mixer is illustrative, not secure -- the STRUCTURE is the point.)
&lt;p&gt;def compress(state, byte):
    return ((state * 1000003) ^ byte) &amp;amp; 0xFFFFFFFFFFFF   # 48-bit toy state&lt;/p&gt;
&lt;p&gt;def toy_hash(data, iv=0xA5A5A5A5A5A5):
    state = iv
    for byte in data:
        state = compress(state, byte)
    return state                      # digest == resumable internal state&lt;/p&gt;
&lt;p&gt;secret = b&quot;opensesame&quot;                # attacker does NOT know this...
def prefix_mac(msg):                  # ...but does know len(secret) == 10
    return toy_hash(secret + msg)&lt;/p&gt;
1) Attacker observes ONE legitimate (message, tag) pair:
&lt;p&gt;seen = b&quot;amount=10&quot;
tag  = prefix_mac(seen)&lt;/p&gt;
2) Attacker resumes from the tag and appends -- the secret is never touched:
&lt;p&gt;extra = b&quot;&amp;amp;amount=1000000&quot;
forged = tag
for byte in extra:
    forged = compress(forged, byte)&lt;/p&gt;
3) The verifier recomputes over secret + (seen + extra):
&lt;p&gt;print(&quot;secret-prefix forgery accepted:&quot;, forged == prefix_mac(seen + extra))&lt;/p&gt;
The HMAC-style fix: nest, so the outer pass re-absorbs the inner digest.
&lt;p&gt;def nested_mac(msg):
    inner = toy_hash(secret + msg)
    return toy_hash(secret + inner.to_bytes(6, &quot;big&quot;))&lt;/p&gt;
&lt;p&gt;resumed = nested_mac(seen)
for byte in extra:
    resumed = compress(resumed, byte)
print(&quot;nested (HMAC-style) forgery accepted:&quot;, resumed == nested_mac(seen + extra))
`}&lt;/p&gt;
&lt;p&gt;The first line prints &lt;code&gt;True&lt;/code&gt;, the second &lt;code&gt;False&lt;/code&gt;. That single flip is the difference between the Flickr construction and HMAC, and it is Aha number one for this article.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;H(secret || message)&lt;/code&gt; over any Merkle-Damgard hash is not a MAC. It authenticates nothing an attacker who has seen one tag cannot forge, regardless of how strong the underlying hash is. MD5&apos;s collision weakness is irrelevant here -- the Flickr forgery used no collision at all [@flickr-advisory]. If you find &lt;code&gt;md5(secret + data)&lt;/code&gt; or &lt;code&gt;sha256(secret + data)&lt;/code&gt; in a code review, you have found a length-extension forgery waiting to happen.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The mirror construction, secret-suffix &lt;code&gt;H(message || secret)&lt;/code&gt;, resists length extension because the secret is absorbed last.Secret-suffix is not safe either: since an attacker who finds two messages colliding under the raw hash gets a tag collision for free, &lt;code&gt;H(m || k)&lt;/code&gt; reduces to the hash&apos;s collision resistance, so it dies with MD5 and SHA-1 -- the opposite failure mode from secret-prefix, same verdict [@length-ext-wiki]. Neither ad-hoc glue is a MAC.&lt;/p&gt;
&lt;p&gt;The block-cipher world made a parallel mistake. CBC-MAC, standardized as FIPS 113 in 1985, runs a block cipher in CBC mode with a zero IV and keeps the final ciphertext block as the tag [@fips-113]. It carries an unstated assumption: every message is the same fixed length. Bellare, Kilian, and Rogaway turned the failure of that assumption into a theorem in 1994 -- secure for fixed-length messages, trivially forgeable once lengths vary [@bkr-1994].&lt;/p&gt;
&lt;p&gt;The splice is mechanical. Query the tag of a one-block message $m_1$ to get $t_1 = E_k(m_1)$; then the two-block message $m_1 ,\Vert, (m_1 \oplus t_1)$ has tag $E_k\big((m_1 \oplus t_1)\oplus t_1\big) = E_k(m_1) = t_1$ -- a valid tag on a message never queried, produced with no key.&lt;/p&gt;
&lt;p&gt;Both failures teach the same lesson, and it organizes the rest of this guide: &lt;em&gt;a raw hash is not a MAC, and raw CBC-MAC is not a MAC for variable-length messages.&lt;/em&gt; Both fixes point the same direction -- stop exposing resumable internal state, and make the tag depend on length. From here the field split into two lineages. Which one wins?&lt;/p&gt;
&lt;h2&gt;5. The Evolution: Two Lineages Diverge&lt;/h2&gt;
&lt;p&gt;The field did not pick one winner. It split into two families that both survive today, and choosing between them is still the single most consequential decision a practitioner makes about authentication. One family keeps a computational assumption and buys a reusable key; the other keeps the 1981 one-time pad and buys blazing speed.&lt;/p&gt;
&lt;h3&gt;The PRF lineage: HMAC and CMAC&lt;/h3&gt;
&lt;p&gt;Bellare, Canetti, and Krawczyk fixed the secret-prefix disaster in 1996 by nesting the hash so the attacker never sees a resumable inner state [@bck-1996]. Standardized a year later as RFC 2104, HMAC computes two keyed passes [@rfc-2104]:&lt;/p&gt;
&lt;p&gt;$$\mathsf{HMAC}(k, m) ;=; H\big((k \oplus \text{opad}) ,\Vert, H((k \oplus \text{ipad}) ,\Vert, m)\big).$$&lt;/p&gt;
&lt;p&gt;The outer hash re-absorbs the inner digest, so a resumed inner state is useless -- length extension cannot reach the output.&lt;/p&gt;

RFC 2104 also fixes how the key becomes block-sized before those two XORs: a key longer than the hash&apos;s block size is first hashed down to a digest, and a shorter key is zero-padded up to the block size [@rfc-2104]. That is why HMAC accepts a key of any length -- it normalizes every key to exactly one block before mixing in `ipad` and `opad`.

flowchart TD
    K[Secret key k] --&amp;gt; IP[k XOR ipad]
    M[Message m] --&amp;gt; INNER[Inner hash of ipad key then message]
    IP --&amp;gt; INNER
    INNER --&amp;gt; ID[Inner digest, never emitted directly]
    K --&amp;gt; OP[k XOR opad]
    ID --&amp;gt; OUTER[Outer hash of opad key then inner digest]
    OP --&amp;gt; OUTER
    OUTER --&amp;gt; T[HMAC tag]
&lt;p&gt;The deepest fact about HMAC is also the most misunderstood, and it took another decade to prove. In 2006 Bellare showed HMAC is a pseudorandom function under the &lt;em&gt;sole&lt;/em&gt; assumption that the hash&apos;s compression function is a PRF -- collision resistance is not required at all [@bellare-2006].&lt;/p&gt;

A keyed function is a PRF if no efficient adversary, allowed to query it on inputs of its choice, can distinguish its outputs from those of a truly random function. A PRF makes an immediate MAC: to forge a tag on a fresh message is to predict a value that, by assumption, looks uniformly random, which succeeds only with negligible probability. Unforgeability falls out of pseudorandomness.
&lt;p&gt;This is why the collision breaks of MD5 and SHA-1 never touched HMAC-MD5 or HMAC-SHA-1. Collisions attack collision resistance; HMAC never needed it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The folklore &quot;HMAC needs a collision-resistant hash&quot; is false, and it has been false since 2006 [@bellare-2006]. HMAC needs the compression function to behave like a PRF -- a different, and for MD5 and SHA-1 still-standing, assumption. This is not a licence to use MD5 in new systems, but it is why the practical world did not catch fire when SHA-1 collisions arrived.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The block-cipher branch got the same repair. CMAC, from Iwata and Kurosawa&apos;s OMAC and standardized as NIST SP 800-38B, derives two subkeys from the block cipher and mixes one into the final block, killing the variable-length splice that sank raw CBC-MAC while keeping a single key [@omac-2003] [@sp-800-38b]. It is the block-cipher PRF-MAC, useful where a device already has AES hardware but no hash.&lt;/p&gt;
&lt;p&gt;The survival of HMAC-SHA-1 is the cleanest real-world confirmation of the 2006 proof: SHA-1&apos;s collision resistance collapsed publicly, yet no HMAC-SHA-1 forgery followed, because the security reduction never invoked collision resistance in the first place [@bellare-2006].&lt;/p&gt;
&lt;h3&gt;The sponge refinement: KMAC&lt;/h3&gt;
&lt;p&gt;SHA-3 changed the substrate. A Keccak sponge splits its state into a public rate and a secret capacity, and it never emits the capacity [@fips-202]. That single property makes the HMAC nesting trick unnecessary: you can key a sponge by simply absorbing the key first.&lt;/p&gt;

A sponge function absorbs input into a large internal state divided into a public &quot;rate&quot; (the bytes that interact with input and output) and a secret &quot;capacity&quot; (bytes never revealed). Because the capacity is never output, prepending a key and absorbing it yields a keyed authenticator with no length-extension surface and no double hashing -- the key is protected by the same hidden capacity that protects the hash.
&lt;p&gt;NIST SP 800-185 builds KMAC on cSHAKE this way: prepend the key, absorb, squeeze [@sp-800-185]. Keying is native, domain separation is built in through customization strings, and the tag length is a free parameter because the sponge is an extendable-output function. It is, conceptually, the cleanest MAC in the standard catalogue -- but conceptual cleanliness is not deployment.&lt;/p&gt;

KMAC is a real, standardized primitive [@sp-800-185], implemented in OpenSSL 3.0 and later (as the `KMAC-128`/`KMAC-256` EVP_MAC) [@openssl-kmac], as well as BouncyCastle [@bouncycastle-kmac] and Botan [@botan-kmac]. It is not academic-only. But its production footprint is thin next to HMAC, GMAC, and Poly1305, for reasons that are historical inertia rather than cryptography: SHA-2 and HMAC are welded into TLS, IPsec, JWT, HKDF, and hardware everywhere, and SHA-3 adoption trails. This guide gives KMAC full conceptual treatment and makes no claim about its market share -- no reliable quantitative figure exists in a primary source.
&lt;p&gt;Both HMAC and KMAC share a decisive operational property: a reusable key, no nonce, an unbounded stream of messages under one secret. That is the easy side of assumption one. The other lineage trades it away for speed.&lt;/p&gt;
&lt;h3&gt;The Wegman-Carter lineage: GMAC and Poly1305&lt;/h3&gt;
&lt;p&gt;In 2004 and 2005, two designs dragged the 1981 one-time construction into high-speed networking. Both keep the pattern &lt;code&gt;tag = universal_hash(m) + one_time_pad&lt;/code&gt;, and both solve the &quot;fresh pad per message&quot; problem the same way -- derive the pad from a cipher keystream instead of shipping new randomness.&lt;/p&gt;
&lt;p&gt;GHASH, the core of GCM, is a polynomial universal hash over the finite field $\mathrm{GF}(2^{128})$: it evaluates $\sum_i m_i \cdot H^{,L-i+1}$ at a secret point $H = E_k(0)$, then masks the result with an AES-encrypted counter block [@mcgrew-viega-2004]. Run GCM with an empty plaintext and you get GMAC, an authentication-only MAC over associated data, standardized in NIST SP 800-38D [@sp-800-38d]. Poly1305 evaluates a message polynomial modulo the prime $2^{130}-5$ under a secret $r$, then adds a one-time pad $s$; deployed as ChaCha20-Poly1305, it draws a fresh $(r, s)$ from the cipher keystream for every message [@rfc-8439].&lt;/p&gt;

flowchart TD
    N[Nonce and key] --&amp;gt; KS[Cipher keystream block]
    KS --&amp;gt; R[One-time hash key r]
    KS --&amp;gt; S[One-time pad s]
    M[Message m] --&amp;gt; P[Polynomial hash of m under r]
    R --&amp;gt; P
    P --&amp;gt; ADD[Add the one-time pad s]
    S --&amp;gt; ADD
    ADD --&amp;gt; T[Authentication tag]
    T -.reuse the same nonce.-&amp;gt; LEAK[Two tags cancel the pad, leaking hash of m1 minus hash of m2]
&lt;p&gt;This is where the 1981 constraint re-enters as a deployment contract. The pad is safe only if the $(key, nonce)$ pair never repeats. Reuse it and the pad cancels out of the difference of two tags, exactly as the toy line did in Section 3 -- except now the &quot;hash&quot; is a polynomial an attacker can then start solving.&lt;/p&gt;
&lt;p&gt;{`&lt;/p&gt;
A one-time Wegman-Carter MAC: tag = poly_hash(m, r) + s (mod p). Use (r, s) ONCE.
&lt;p&gt;p = (1 &amp;lt;&amp;lt; 130) - 5                          # the Poly1305 prime, 2**130 - 5&lt;/p&gt;
&lt;p&gt;def poly_hash(msg, r):
    acc = 0
    for i in range(0, len(msg), 16):
        chunk = msg[i:i+16]
        limb  = int.from_bytes(chunk, &quot;little&quot;) + (1 &amp;lt;&amp;lt; (8 * len(chunk)))
        acc   = ((acc + limb) * r) % p
    return acc&lt;/p&gt;
In ChaCha20-Poly1305 both r and s come fresh from the keystream, per message.
&lt;p&gt;r = 0x1bf54941aff6bf4afdb1f2a3c4d5e6f7
s = 0x0102030405060708090a0b0c0d0e0f10&lt;/p&gt;
&lt;p&gt;def one_time_tag(msg):
    return (poly_hash(msg, r) + s) % p&lt;/p&gt;
THE MISTAKE: two messages under the SAME (r, s).
&lt;p&gt;m1, m2 = b&quot;transfer 10 dollars now&quot;, b&quot;transfer 9999 bucks now&quot;
t1, t2 = one_time_tag(m1), one_time_tag(m2)&lt;/p&gt;
The pad s cancels in the difference -- a relation that does not depend on s at all:
&lt;p&gt;lhs = (t1 - t2) % p
rhs = (poly_hash(m1, r) - poly_hash(m2, r)) % p
print(&quot;pad cancelled, tag difference is key-independent:&quot;, lhs == rhs)
`}&lt;/p&gt;
&lt;p&gt;It prints &lt;code&gt;True&lt;/code&gt;: subtract two tags produced under the same one-time key and the secret pad vanishes, handing the attacker a clean algebraic relation in the hash key. That is the first step of the &quot;forbidden attack&quot; the failure catalogue will name -- and it is why the distinction below is not pedantry.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;master key versus one-time key&lt;/strong&gt; split is the heart of the fork. HMAC and KMAC take a &lt;em&gt;master&lt;/em&gt; key and authenticate forever under it. GMAC and Poly1305 take a &lt;em&gt;one-time&lt;/em&gt; key per message; the ChaCha20-Poly1305 keystream manufactures that one-time key so the master key stays put, but the per-message pad is still strictly single-use. Confuse the two and you get a catastrophe, not a slow degradation.&lt;/p&gt;
&lt;p&gt;The whole construction catalogue, side by side:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 1 -- MAC constructions, and what each one assumes.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Construction&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Core idea&lt;/th&gt;
&lt;th&gt;Key model&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Weakness&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;Carter-Wegman one-time [@wc-1981]&lt;/td&gt;
&lt;td&gt;1981&lt;/td&gt;
&lt;td&gt;$\mathsf{UH}(m) \oplus \text{pad}$&lt;/td&gt;
&lt;td&gt;One-time pad per message&lt;/td&gt;
&lt;td&gt;Information-theoretic, no hardness assumption&lt;/td&gt;
&lt;td&gt;Pad is strictly single-use&lt;/td&gt;
&lt;td&gt;Foundational, revived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret-prefix $H(k \Vert m)$ [@length-ext-wiki]&lt;/td&gt;
&lt;td&gt;1990s&lt;/td&gt;
&lt;td&gt;Prepend key, hash&lt;/td&gt;
&lt;td&gt;Reusable&lt;/td&gt;
&lt;td&gt;Trivial to build&lt;/td&gt;
&lt;td&gt;Length-extension forgeable&lt;/td&gt;
&lt;td&gt;Superseded, insecure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret-suffix $H(m \Vert k)$ [@length-ext-wiki]&lt;/td&gt;
&lt;td&gt;1990s&lt;/td&gt;
&lt;td&gt;Append key, hash&lt;/td&gt;
&lt;td&gt;Reusable&lt;/td&gt;
&lt;td&gt;No length extension&lt;/td&gt;
&lt;td&gt;Reduces to a hash collision&lt;/td&gt;
&lt;td&gt;Superseded, insecure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBC-MAC [@fips-113]&lt;/td&gt;
&lt;td&gt;1985&lt;/td&gt;
&lt;td&gt;CBC chain, keep last block&lt;/td&gt;
&lt;td&gt;Reusable&lt;/td&gt;
&lt;td&gt;First standardized block-cipher MAC&lt;/td&gt;
&lt;td&gt;Variable-length splicing forgery&lt;/td&gt;
&lt;td&gt;Historical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMAC [@rfc-2104]&lt;/td&gt;
&lt;td&gt;1996&lt;/td&gt;
&lt;td&gt;Nested keyed hash&lt;/td&gt;
&lt;td&gt;Reusable, no nonce&lt;/td&gt;
&lt;td&gt;PRF not collision resistance, length-ext immune&lt;/td&gt;
&lt;td&gt;Serial, no line-rate shortcut&lt;/td&gt;
&lt;td&gt;Active, default PRF-MAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMAC [@sp-800-38b]&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;td&gt;CBC-MAC plus final subkey&lt;/td&gt;
&lt;td&gt;Reusable, no nonce&lt;/td&gt;
&lt;td&gt;Fixes CBC-MAC with one key&lt;/td&gt;
&lt;td&gt;Block-cipher birthday ceiling&lt;/td&gt;
&lt;td&gt;Active, niche&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GMAC / GHASH [@sp-800-38d]&lt;/td&gt;
&lt;td&gt;2004&lt;/td&gt;
&lt;td&gt;Polynomial hash over $\mathrm{GF}(2^{128})$&lt;/td&gt;
&lt;td&gt;One-time pad per message&lt;/td&gt;
&lt;td&gt;Carry-less-multiply hardware (PCLMULQDQ/PMULL)&lt;/td&gt;
&lt;td&gt;Nonce reuse is fatal&lt;/td&gt;
&lt;td&gt;Active, inside GCM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poly1305 [@rfc-8439]&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;td&gt;Polynomial hash mod $2^{130}-5$&lt;/td&gt;
&lt;td&gt;One-time key per message&lt;/td&gt;
&lt;td&gt;Fast in pure software, cipher-agnostic&lt;/td&gt;
&lt;td&gt;One-time key per message&lt;/td&gt;
&lt;td&gt;Active, inside ChaCha20-Poly1305&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KMAC [@sp-800-185]&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;Keyed sponge, cSHAKE&lt;/td&gt;
&lt;td&gt;Reusable, no nonce&lt;/td&gt;
&lt;td&gt;Native keying, domain separation, XOF tag&lt;/td&gt;
&lt;td&gt;Lightly deployed&lt;/td&gt;
&lt;td&gt;Active, niche&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;You can now build an unforgeable tag two ways: a reusable-key PRF or a one-time-key universal hash. But a perfect tag attached in the wrong place, in the wrong order, or over the wrong bytes is still useless. How do you &lt;em&gt;combine&lt;/em&gt; a MAC with a cipher without reopening the door you just closed?&lt;/p&gt;
&lt;h2&gt;6. Composition Done Right, and the Verification Contract&lt;/h2&gt;
&lt;p&gt;SSL chose one order for combining its cipher and its MAC. SSH chose another. Both were quietly indicted by a theorem years before either was broken in the field. This section is where the second aha moment lands: strength was never the variable -- discipline and order were.&lt;/p&gt;
&lt;p&gt;Bellare and Namprempre formalized the question in 2000 by splitting integrity into two notions [@bn-2000]. Integrity of plaintexts (INT-PTXT) says an attacker cannot make the receiver accept a plaintext the sender never sent. Integrity of ciphertexts (INT-CTXT) is stronger: the attacker cannot even produce a &lt;em&gt;new ciphertext&lt;/em&gt; the receiver will accept. Their result is the load-bearing one: applying the MAC to the ciphertext -- Encrypt-then-MAC -- yields both IND-CCA confidentiality and INT-CTXT, and it is the only one of the three generic orders that reaches those strong notions.&lt;/p&gt;

Encrypt the plaintext, then compute the MAC over the resulting *ciphertext* (plus any associated data and length), and send ciphertext and tag together. On receipt, verify the tag *before* touching the ciphertext; if it fails, discard without decrypting. EtM is the only generic composition order that provably delivers integrity of ciphertexts, and it does so only under two preconditions: the MAC must be strongly unforgeable and the encryption and MAC keys must be independent.
&lt;p&gt;Those two preconditions are not decoration. If the MAC is only EUF-CMA rather than SUF-CMA, the guarantee weakens from INT-CTXT down to INT-PTXT, because a malleable tag lets an attacker produce a different valid ciphertext for the same plaintext [@bn-2000]. And the keys must be independent -- reusing one key for both encryption and authentication voids the proof.&lt;/p&gt;
&lt;p&gt;A year later Krawczyk sharpened the verdict [@krawczyk-2001]. He proved that any secure-channel protocol required to work with &lt;em&gt;any&lt;/em&gt; secure cipher and &lt;em&gt;any&lt;/em&gt; secure MAC must use Encrypt-then-MAC, and he built a perfectly secret cipher that becomes totally insecure under MAC-then-Encrypt -- SSL&apos;s order -- and under Encrypt-and-MAC -- SSH&apos;s order.&lt;/p&gt;

&quot;[A]ny secure channels protocol designed to work with any combination of secure encryption ... and secure MAC must use the encrypt-then-authenticate method.&quot; -- Hugo Krawczyk, The Order of Encryption and Authentication for Protecting Communications, 2001 [@krawczyk-2001]
&lt;p&gt;Here is the precision that separates an expert from a slogan-repeater: Krawczyk &lt;em&gt;also&lt;/em&gt; proved that MAC-then-Encrypt is secure for specific cipher modes, namely &lt;a href=&quot;https://paragmali.com/blog/the-ciphertext-was-unbreakable-the-attacker-rewrote-it-anyw/&quot; rel=&quot;noopener&quot;&gt;CBC and CTR&lt;/a&gt; [@krawczyk-2001]. MtE is therefore &lt;em&gt;fragile&lt;/em&gt; and &lt;em&gt;not generically secure&lt;/em&gt; -- it holds only when you can prove something extra about the exact cipher -- but it was never unconditionally &quot;broken.&quot; Say &quot;MtE is broken&quot; in an interview and you are wrong; say &quot;MtE is not generically secure, so do not use it in new designs&quot; and you are exactly right.&lt;/p&gt;

flowchart TD
    subgraph EtM[&quot;Encrypt-then-MAC, generically secure&quot;]
        E1[Plaintext] --&amp;gt; E2[Encrypt]
        E2 --&amp;gt; E3[Ciphertext] --&amp;gt; E4[MAC the ciphertext]
        E4 --&amp;gt; E5[Receiver verifies tag first, then decrypts]
    end
    subgraph MtE[&quot;MAC-then-Encrypt, fragile&quot;]
        M1[Plaintext] --&amp;gt; M2[MAC the plaintext]
        M2 --&amp;gt; M3[Encrypt plaintext and tag together]
        M3 --&amp;gt; M4[Receiver decrypts first, only then verifies]
    end
    subgraph EM[&quot;Encrypt-and-MAC, fragile&quot;]
        A1[Plaintext] --&amp;gt; A2[Encrypt]
        A1 --&amp;gt; A3[MAC the plaintext, tag sent in the clear]
        A2 --&amp;gt; A4[Receiver decrypts, and the tag leaks plaintext]
    end
&lt;p&gt;The diagram shows the mechanism behind every &lt;a href=&quot;https://paragmali.com/blog/they-read-your-plaintext-without-breaking-your-cipher-a-fiel/&quot; rel=&quot;noopener&quot;&gt;padding-oracle&lt;/a&gt; and SSH break at once: in the two fragile orders, the receiver must &lt;em&gt;decrypt or parse before it can verify&lt;/em&gt;, so the very act of processing attacker-chosen bytes leaks information. Encrypt-then-MAC verifies first and refuses to decrypt a single byte of an unauthenticated ciphertext.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Authenticate the ciphertext, not the plaintext. Encrypt-then-MAC over the ciphertext -- with an independent key, a strongly unforgeable tag, and coverage of the associated data and length -- is the only generic order that lets the receiver reject a forgery before it decrypts anything. Every composition break in the catalogue is a system that decrypted or parsed before it verified.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Table 2 -- Composition orders, ranked by what they generically guarantee.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Order&lt;/th&gt;
&lt;th&gt;Idea&lt;/th&gt;
&lt;th&gt;Generic security&lt;/th&gt;
&lt;th&gt;Weakness&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;MAC-then-Encrypt [@krawczyk-2001]&lt;/td&gt;
&lt;td&gt;MAC the plaintext, then encrypt both&lt;/td&gt;
&lt;td&gt;Not generic, secure only for CBC or CTR&lt;/td&gt;
&lt;td&gt;Decrypt-before-verify padding and timing oracles&lt;/td&gt;
&lt;td&gt;Superseded, fragile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt-and-MAC [@apw-2009]&lt;/td&gt;
&lt;td&gt;Encrypt, and MAC the plaintext separately&lt;/td&gt;
&lt;td&gt;Not generic&lt;/td&gt;
&lt;td&gt;Tag over plaintext leaks, MAC-covered length field abused&lt;/td&gt;
&lt;td&gt;Superseded, fragile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encrypt-then-MAC [@bn-2000]&lt;/td&gt;
&lt;td&gt;MAC the ciphertext&lt;/td&gt;
&lt;td&gt;IND-CCA plus INT-CTXT&lt;/td&gt;
&lt;td&gt;Needs SUF-CMA, independent keys, constant-time verify&lt;/td&gt;
&lt;td&gt;Active, canonical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AEAD [@rfc-8446]&lt;/td&gt;
&lt;td&gt;EtM internalized plus nonce management&lt;/td&gt;
&lt;td&gt;IND-CCA plus INT-CTXT by construction&lt;/td&gt;
&lt;td&gt;Key-commitment gap&lt;/td&gt;
&lt;td&gt;Active, default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Committing AEAD [@rfc-9771]&lt;/td&gt;
&lt;td&gt;AEAD plus key and context commitment&lt;/td&gt;
&lt;td&gt;Adds commitment&lt;/td&gt;
&lt;td&gt;Still standardizing, cost profiling&lt;/td&gt;
&lt;td&gt;Frontier&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The order is only half of correct composition. The other half arrived not from a theorem but from a field disclosure. In 2009 Nate Lawson found that Google&apos;s Keyczar library compared HMAC tags byte by byte and returned as soon as two bytes differed [@keyczar-2009]. That early exit turns verification time into a side channel: a forged tag that matches more leading bytes takes measurably longer to reject, so an attacker can recover the correct tag one byte at a time by timing responses.&lt;/p&gt;
&lt;p&gt;{`&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Q0 -- Yes --&amp;gt; ASCON[Ascon, SP 800-232]
Q0 -- No --&amp;gt; Q1&amp;amp;#123;&quot;Is there a ciphertext to authenticate?&quot;&amp;amp;#125;
Q1 -- No --&amp;gt; HMAC[HMAC-SHA-256, reusable key, no nonce]
Q1 -- Yes --&amp;gt; Q2&amp;amp;#123;&quot;Can you guarantee a unique nonce per key?&quot;&amp;amp;#125;
Q2 -- No --&amp;gt; SIV[AES-GCM-SIV, misuse-resistant]
Q2 -- Yes --&amp;gt; Q3&amp;amp;#123;&quot;Is AES-NI hardware available?&quot;&amp;amp;#125;
Q3 -- Yes --&amp;gt; GCM[AES-GCM, rekey before the per-key ceiling]
Q3 -- No --&amp;gt; CHACHA[ChaCha20-Poly1305]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the tree as the three assumptions in order. If there is no ciphertext to bind -- key derivation, a token, a one-time password -- use HMAC-SHA-256 with a key of at least 128 bits, a full-length tag, and a constant-time verify; there is no nonce to mismanage, so assumption one is nearly free. This is the right tool for HKDF [@rfc-5869], JWT&apos;s &lt;code&gt;HS256&lt;/code&gt; [@rfc-7518], TOTP, HOTP, and PBKDF2.&lt;/p&gt;
&lt;p&gt;If there &lt;em&gt;is&lt;/em&gt; a ciphertext, do not hand-roll a MAC plus cipher -- reach for an AEAD, which internalizes Encrypt-then-MAC so you cannot mis-order it. Pick ChaCha20-Poly1305 when you lack AES hardware acceleration, especially on mobile and embedded targets, where its per-key budget is effectively unbounded [@rfc-8439]. Pick AES-GCM when you have AES-NI, and rekey before the $2^{23}$-packet ceiling on high-volume links [@rfc-9001]. If you cannot &lt;em&gt;guarantee&lt;/em&gt; nonce uniqueness at scale, step down to AES-GCM-SIV, which degrades gracefully instead of catastrophically on reuse [@rfc-8452].&lt;/p&gt;
&lt;p&gt;Two niche branches sit off the main tree. Use GMAC for authentication-only data, such as MACsec framing, with a guaranteed-unique nonce [@sp-800-38d] [@ieee-802-1ae]. Use KMAC when you are already in a Keccak or SHA-3 stack, or when you need domain separation or variable-length tags [@sp-800-185]. And on constrained or side-channel-sensitive hardware, use Ascon [@sp-800-232].&lt;/p&gt;
&lt;p&gt;Then there are the rules that apply &lt;em&gt;always&lt;/em&gt;, regardless of which box the tree lands in: Encrypt-then-MAC or an AEAD that internalizes it; independent encryption and MAC keys; a constant-time compare; a tag of at least 128 bits with documented truncation floors [@rfc-4868]; authentication that covers the associated data, the length, the version, and the session context; a committing AEAD when one ciphertext might be opened under multiple keys [@bellare-hoang-2022]; and a misuse-resistant AEAD when nonce uniqueness cannot be guaranteed [@rfc-8452].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Review authentication code against the checklist &lt;em&gt;and its negation&lt;/em&gt;. The checklist tells you what a correct design does; the misuse list tells you what every named CVE did instead. Run any authentication path past both -- if it matches even one line of the misuse list, you have found your next vulnerability before an attacker does.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The negation is the misuse catalogue -- each row is a line of the checklist inverted, and it maps one-to-one onto a named break from Section 7.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 6 -- Common misuse, the break it produced, and the fix.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse in real code&lt;/th&gt;
&lt;th&gt;Named break it caused&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Secret-prefix $H(k \Vert m)$ MAC&lt;/td&gt;
&lt;td&gt;Flickr length extension [@flickr-advisory]&lt;/td&gt;
&lt;td&gt;Use HMAC or KMAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusing a GCM nonce or Poly1305 one-time key&lt;/td&gt;
&lt;td&gt;Joux, Nonce-Disrespecting [@woot16-bock]&lt;/td&gt;
&lt;td&gt;Guarantee uniqueness or use AES-GCM-SIV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt; or &lt;code&gt;memcmp&lt;/code&gt; tag comparison&lt;/td&gt;
&lt;td&gt;Keyczar timing leak [@keyczar-2009]&lt;/td&gt;
&lt;td&gt;Use a constant-time compare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAC-then-Encrypt or Encrypt-and-MAC in new code&lt;/td&gt;
&lt;td&gt;Lucky Thirteen, POODLE, SSH recovery [@cve-2013-0169]&lt;/td&gt;
&lt;td&gt;Use an AEAD (Encrypt-then-MAC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Over-truncated tags&lt;/td&gt;
&lt;td&gt;Forgery near the $2^{-t}$ floor [@rfc-4868]&lt;/td&gt;
&lt;td&gt;Keep the tag at least 128 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One shared key for encryption and MAC&lt;/td&gt;
&lt;td&gt;Voids the Encrypt-then-MAC proof [@bn-2000]&lt;/td&gt;
&lt;td&gt;Derive independent keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MACing the plaintext instead of the ciphertext&lt;/td&gt;
&lt;td&gt;Padding oracles [@vaudenay-2002]&lt;/td&gt;
&lt;td&gt;MAC the ciphertext&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forgetting AD, length, version, context&lt;/td&gt;
&lt;td&gt;Terrapin [@terrapin-site]&lt;/td&gt;
&lt;td&gt;Bind the full context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treating &quot;MAC verified&quot; as &quot;key committed&quot;&lt;/td&gt;
&lt;td&gt;Invisible salamanders [@bellare-hoang-2022]&lt;/td&gt;
&lt;td&gt;Use a committing AEAD&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; Run every authentication path past these ten lines: - Is the tag over the &lt;strong&gt;ciphertext&lt;/strong&gt; (Encrypt-then-MAC), never the plaintext? - Are the encryption and MAC keys &lt;strong&gt;independent&lt;/strong&gt;? - Is the MAC &lt;strong&gt;strongly unforgeable&lt;/strong&gt; (SUF-CMA), not merely EUF-CMA? - Is the tag comparison &lt;strong&gt;constant-time&lt;/strong&gt; (&lt;code&gt;compare_digest&lt;/code&gt;, &lt;code&gt;timingSafeEqual&lt;/code&gt;, &lt;code&gt;ConstantTimeCompare&lt;/code&gt;)? - Is the &lt;strong&gt;whole&lt;/strong&gt; decrypt-and-verify path constant-time, not just the compare? - Is the tag at least &lt;strong&gt;128 bits&lt;/strong&gt;, with any truncation documented against the birthday bound? - For a one-time-key MAC (GMAC, Poly1305), is nonce uniqueness &lt;strong&gt;guaranteed&lt;/strong&gt;, or are you on a misuse-resistant mode? - Does the authenticated data cover &lt;strong&gt;length, version, ordering, and session context&lt;/strong&gt;? - Is there any &lt;strong&gt;&lt;code&gt;H(secret || message)&lt;/code&gt;&lt;/strong&gt; secret-prefix construction? (Replace with HMAC or KMAC.) - Could one ciphertext open under &lt;strong&gt;two keys&lt;/strong&gt;? If so, use a committing AEAD. Any single failure here is a plausible next CVE.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That checklist and its negation are the working payoff of the whole field guide. Before the conclusion ties them back to the thesis, seven specific beliefs produce most of the remaining real-world authentication bugs -- and each is a corollary of the same one sentence.&lt;/p&gt;
&lt;h2&gt;13. Frequently Corrected Misconceptions&lt;/h2&gt;


No. As Section 5 established, Bellare&apos;s 2006 proof rests HMAC&apos;s security on a single property of the underlying compression function -- that it is itself a PRF -- and not on collision resistance [@bellare-2006]. Since a collision attack targets a property the proof never relies on, HMAC-SHA-1 and HMAC-MD5 kept standing when the SHA-1 and MD5 collision breaks landed. Use HMAC-SHA-256 in new systems for margin, but understand that the reason is prudence, not a collision threat.


No, and the difference is the most dangerous one to miss. HMAC takes a reusable key with no nonce. GMAC and Poly1305 are Wegman-Carter one-time-key MACs [@wc-1981]: they need a fresh, unique per-message key, and reusing it is catastrophic, not a gentle degradation. The 184 HTTPS servers caught repeating GCM nonces were forgeable precisely because a one-time construction was treated like a reusable one [@woot16-bock]. Same tag length, completely different key contract.


No. HMAC exists to defeat length extension on Merkle-Damgard hashes, but a Keccak sponge never emits its capacity, so it has no length-extension surface and keys natively -- just use KMAC [@sp-800-185]. In fact you cannot build HMAC on a SHAKE extendable-output function anyway; Python&apos;s `hmac` module notes that XOFs such as SHAKE-128 and SHAKE-256 cannot be used with HMAC [@python-hmac]. KMAC is the SHA-3-native answer.


No -- it is fragile, which is a different and more precise claim. Krawczyk proved MAC-then-Encrypt is *not generically secure*, yet also proved it *is* secure for CBC and CTR modes specifically [@krawczyk-2001]. So it was never unconditionally broken; it is simply deprecated for new designs, where a modern AEAD or Encrypt-then-MAC removes the cipher-specific precondition it depends on. Reach for one of those instead, but do not call MtE &quot;broken.&quot;


Not by itself, and the overstatement matters. A single $(key, nonce)$ reuse immediately leaks a tag relation and the XOR of the two plaintexts [@joux-2006]. Recovering the subkey $H$ outright is a taller order: a single reuse leaves a root-finding equation with several possible solutions for $H$, so narrowing them to the one true value takes at least a second collision, and usually more. So &quot;one reuse recovers the key&quot; is wrong; &quot;one reuse is already a break, and more reuse recovers the key&quot; is right.


No -- there is a real-world counterexample. In 2009 Google&apos;s Keyczar library did a byte-wise tag comparison with an early exit, and that timing difference let an attacker recover valid tags one byte at a time [@keyczar-2009]. The comparison time leaked the secret the MAC existed to protect. Constant-time compare is a correctness requirement, not an optimization, and every major platform ships a named primitive for it.


No. AEAD handles the encrypted channel, but plenty of jobs have no ciphertext to bind and still need authentication. HKDF key derivation [@rfc-5869], JWT&apos;s `HS256` [@rfc-7518], TOTP and HOTP one-time passwords, and PBKDF2 are all standalone HMAC. The standalone MAC did not disappear with AEAD; it moved to the places where you are authenticating keys, tokens, or messages rather than protecting a ciphertext.

&lt;p&gt;Every myth here is a corollary of one sentence, which the conclusion now restates with the whole catalogue behind it.&lt;/p&gt;
&lt;h2&gt;14. The Math Never Broke&lt;/h2&gt;
&lt;p&gt;Return to Flickr, now fully understood. Two researchers forged valid API signatures with no key, no collision, and no cipher work, because Flickr&apos;s secret-prefix &lt;code&gt;H(secret || params)&lt;/code&gt; construction handed out its resumable hash state as the tag [@flickr-advisory]. The hash met its specification exactly. The deployment broke assumption three -- the wrong construction over the wrong bytes -- and that was enough.&lt;/p&gt;
&lt;p&gt;Line up the rest of the catalogue and the pattern is total. Lucky Thirteen and POODLE broke order and timing, decrypting before verifying [@cve-2013-0169] [@cve-2014-3566]. The SSH plaintext-recovery attack broke order, authenticating the plaintext and acting on a MAC-covered length field [@apw-2009]. The 184 nonce-repeating GCM servers broke key discipline, reusing a one-time pad [@woot16-bock]. Terrapin broke context, leaving the transcript and sequence numbers unbound even though the cipher, the MAC, and the order were all correct [@terrapin-site]. Not one of them broke AES, SHA-2, or a polynomial hash. Every single failure broke the contract around the primitive.&lt;/p&gt;
&lt;p&gt;That is the whole thesis, proven by the evidence rather than asserted: a MAC makes forgery hard, and three deployment assumptions -- the right key discipline, a constant-time verify, and composition over the right bytes in the right order binding the right context -- are what keep it hard. The decision tree and the checklist in Section 12 are simply those three assumptions made executable. Carry them, and their negation, into every review.&lt;/p&gt;
&lt;p&gt;The frontier moves the bar again: soon &quot;the tag verified&quot; should also mean &quot;this ciphertext commits to one key and one context,&quot; the committing-AEAD work this series develops further in Part 7. And the deeper machinery lives in the siblings -- &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;the security definitions&lt;/a&gt; of Part 1, &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;the one-time-key generation&lt;/a&gt; of Part 2, the canonicalization discipline of Part 3, the block modes of Part 5, the padding oracles of Part 6, and the AEAD decision matrix of Part 7. This part is the authentication spine that runs through all of them.&lt;/p&gt;

The math never broke; a deployment broke one of three assumptions. Authenticate the ciphertext, the length, and the context -- not just the message.
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;message-authentication-and-safe-composition&quot; keyTerms={[
  { term: &quot;Message Authentication Code (MAC)&quot;, definition: &quot;A keyed algorithm producing a short tag so a shared-key holder can verify a message is authentic and unmodified&quot; },
  { term: &quot;EUF-CMA&quot;, definition: &quot;Existential unforgeability under adaptive chosen-message attack; no forgery on a fresh message after querying tags&quot; },
  { term: &quot;SUF-CMA&quot;, definition: &quot;Strong unforgeability; not even a new valid tag on a previously queried message, the notion Encrypt-then-MAC requires&quot; },
  { term: &quot;Universal hash function&quot;, definition: &quot;A family whose random member collides any fixed pair with small, provable probability, with no hardness assumption&quot; },
  { term: &quot;Pseudorandom function (PRF)&quot;, definition: &quot;A keyed function indistinguishable from random; the sole assumption HMAC&apos;s security rests on&quot; },
  { term: &quot;Keyed sponge&quot;, definition: &quot;A sponge keyed by absorbing the key first; its hidden capacity gives native keying with no length extension, as in KMAC&quot; },
  { term: &quot;Encrypt-then-MAC&quot;, definition: &quot;Compute the tag over the ciphertext and verify before decrypting; the only generically secure composition order&quot; },
  { term: &quot;One-time key&quot;, definition: &quot;The per-message key of a Wegman-Carter MAC (GMAC, Poly1305); reuse is catastrophic, not a slow leak&quot; },
  { term: &quot;Key commitment&quot;, definition: &quot;The property that a ciphertext decrypts validly under only one key; standard AEAD does not provide it&quot; }
]} questions={[
  { q: &quot;What are the three deployment assumptions every MAC relies on?&quot;, a: &quot;Right key discipline for its family, a constant-time verify, and composition over the right bytes and order binding the right context.&quot; },
  { q: &quot;Why did HMAC-SHA-1 survive SHA-1 collisions?&quot;, a: &quot;HMAC&apos;s security reduces to the compression function being a PRF, not to collision resistance, as Bellare proved in 2006.&quot; },
  { q: &quot;Why is reusing a Poly1305 or GMAC nonce catastrophic?&quot;, a: &quot;The one-time pad cancels in the difference of two tags, leaking a key-independent relation that enables forgery.&quot; },
  { q: &quot;Is MAC-then-Encrypt broken?&quot;, a: &quot;No; it is fragile and not generically secure, though Krawczyk proved it secure for CBC and CTR. Avoid it in new designs.&quot; },
  { q: &quot;What does Encrypt-then-MAC require to be secure?&quot;, a: &quot;A strongly unforgeable (SUF-CMA) MAC, independent encryption and MAC keys, a constant-time verify path, and coverage of associated data and length.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>message-authentication</category><category>hmac</category><category>poly1305</category><category>gmac</category><category>authenticated-encryption</category><category>encrypt-then-mac</category><category>applied-cryptography</category><category>aead</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The Fingerprint Two Files Shared: A Field Guide to Cryptographic Hashes</title><link>https://paragmali.com/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp/</link><guid isPermaLink="true">https://paragmali.com/blog/the-fingerprint-two-files-shared-a-field-guide-to-cryptograp/</guid><description>A field guide to cryptographic hashes: the one promise behind SHA-2, SHA-3, and BLAKE3, why MD5 and SHA-1 died, and how to choose one that keeps its promise.</description><pubDate>Fri, 10 Jul 2026 16:04:13 GMT</pubDate><content:encoded>
A cryptographic hash makes one promise: a short fingerprint that stands in, unforgeably and unambiguously, for any input. MD5 and SHA-1 are dead for collision-dependent uses. Their broken collision resistance was weaponized into forged certificates (the 2008 rogue certificate authority [@rogue-ca], Flame in 2012 [@stevens-counter-cryptanalysis-crypto13]) and forged documents and keys (SHAttered in 2017 [@shattered], Shambles in 2020 [@shambles]). Yet HMAC-SHA-1 is not broken [@rfc-2104], and &quot;dead&quot; never meant &quot;extinct.&quot; SHA-2 survived by widening the same Merkle-Damgard margins, while SHA-3&apos;s sponge and the BLAKE3 tree diversified the toolbox rather than replacing the survivor [@fips-202, @blake3-repo]. Correct usage is clause-matching: pick the function and the mode (bare hash, HMAC, KMAC or cSHAKE, an extendable-output function, or a password KDF) that keeps the exact clause your protocol leans on. This guide gives you the promise, the constructions, every named break with its root cause, and a decision tree you can apply the same afternoon.
&lt;h2&gt;1. The Fingerprint Two Files Shared&lt;/h2&gt;
&lt;p&gt;In February 2017, researchers at Google and CWI Amsterdam published two PDF files that looked nothing alike, with different visible content and different bytes, yet when a computer fingerprinted each of them with SHA-1 it returned the very same 40-hex-digit answer: &lt;code&gt;38762cf7f55934b34d179ae6a4c80cadccbb7f0a&lt;/code&gt; [@shattered]. Two different files, one identical fingerprint. Five years earlier, malware named Flame had used the same species of trick against the older MD5 hash to forge a code-signing certificate that chained up to Microsoft and rode Windows Update onto target machines as if Microsoft itself had signed it [@stevens-counter-cryptanalysis-crypto13].&lt;/p&gt;
&lt;p&gt;Neither of those is a stunt. They are the failure mode of one of the quietest, most load-bearing primitives in computing. A cryptographic hash function takes an input of any size and returns a short, fixed-length string of bits, its digest or fingerprint, and it promises something enormous: that fingerprint stands in for the whole input unforgeably and unambiguously.&lt;/p&gt;
&lt;p&gt;Almost everything you trust online leans on that promise. A certificate authority does not sign your certificate; it signs a hash of it. Code signing signs a hash of the program. A TLS 1.3 handshake authenticates a hash of the transcript. Every Git commit names its parent by hash, and every blockchain block commits to its transactions through a tree of hashes. When the promise holds, all of that machinery works. When it breaks, it breaks silently and all at once.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A cryptographic hash is infrastructure hiding in plain sight. Because signatures, certificates, and integrity checks all sign the digest rather than the data, a break in the hash silently breaks every signature built on top of it. The attacker never has to touch your private key.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is what makes the two colliding PDFs unsettling rather than merely clever. If an adversary can produce two inputs with the same fingerprint, then any signature over one of them is also a valid signature over the other, and the entire chain of trust resting on that hash quietly inverts. This guide answers four questions in order: what exactly is the promise a hash makes, how is the fingerprint built, how has it broken in the real world, and how do you choose one today that will not break under you.&lt;/p&gt;

flowchart TD
    A[&quot;One promise: a short, unforgeable, unambiguous fingerprint&quot;] --&amp;gt; B[&quot;How it is built: the Merkle-Damgard chain and the sponge&quot;]
    B --&amp;gt; C[&quot;How it broke: MD5 and SHA-1 collisions weaponized into forgery&quot;]
    C --&amp;gt; D[&quot;What survived: SHA-2 widened the same margins&quot;]
    D --&amp;gt; E[&quot;What diversified: the SHA-3 sponge, the BLAKE3 tree, KangarooTwelve&quot;]
    E --&amp;gt; F[&quot;How to choose: match the function and mode to the clause you rely on&quot;]
&lt;p&gt;A preview of the plot, because it corrects the single most common misconception in the whole subject. Some hashes died and were superseded: MD5 gave way to SHA-1, which gave way to SHA-2. But SHA-2 did not then give way to SHA-3. Instead the field forked. One arc is supersession by failure; the other is deliberate diversification, where several unbroken constructions coexist on purpose so the internet&apos;s trust machinery never again rests on a single design. Telling those two arcs apart is most of what it means to understand this topic.&lt;/p&gt;
&lt;p&gt;Before we can understand how the fingerprint was forged, we have to say, precisely, what it was ever supposed to guarantee.&lt;/p&gt;
&lt;h2&gt;2. What a Hash Actually Promises&lt;/h2&gt;
&lt;p&gt;Ask three engineers what a &quot;secure hash&quot; means and you will get three answers: it is one-way, it has no collisions, it looks random. Cryptographers are more disciplined. They name exactly three properties, plus one more that everyone wishes were true and no real function actually delivers. Getting these four straight is the whole vocabulary, because every break later in this guide is best described as one specific property falling while the others stand.&lt;/p&gt;
&lt;p&gt;Start with the object itself. A hash function $H$ maps an input of essentially unbounded length to an output of some fixed length $n$ bits: 256 bits for SHA-256, 160 for SHA-1, 128 for MD5. Because the input space is infinite and the output space has only $2^n$ points, collisions must exist by counting alone. Security is never about avoiding collisions in principle; it is about making them, and two related feats, computationally unreachable.&lt;/p&gt;
&lt;p&gt;Phillip Rogaway and Thomas Shrimpton gave the field its rigorous, separated definitions of these properties in 2004, and it is worth stating them the way they did, because the separations matter [@rogaway-shrimpton-fse04].&lt;/p&gt;

A deterministic function that maps an input of arbitrary length to a fixed-length digest, engineered so that the digest behaves like a compact, tamper-evident fingerprint of the input: cheap to compute forwards, and infeasible to invert, to steer to a chosen value, or to make two inputs share, except by effort that grows exponentially in the digest length.

Given only a target digest $y$, it is computationally infeasible to find any input $m$ with $H(m) = y$. This is the &quot;one-way&quot; property. For an ideal $n$-bit hash the generic cost is about $2^{n}$ evaluations, because you have no better strategy than trying inputs until one lands on $y$.

Given a specific input $m_1$, it is infeasible to find a different input $m_2 \ne m_1$ with $H(m_2) = H(m_1)$. The target is fixed and not of your choosing. The generic cost is again about $2^{n}$.

It is infeasible to find any two distinct inputs $m_1 \ne m_2$ with $H(m_1) = H(m_2)$. Here you are free to choose both inputs, which is exactly why it is easier to attack. The generic cost is only about $2^{n/2}$, by the birthday bound.
&lt;p&gt;The gap between $2^{n}$ and $2^{n/2}$ is the single most important number in this article. Preimage and second-preimage pit you against a fixed target, so you are searching a space of size $2^n$. Collision lets you range freely over pairs, and pairs accumulate quadratically.&lt;/p&gt;

In a space of $N = 2^{n}$ equally likely values, a randomly drawn set is more likely than not to contain a repeat after only about $\sqrt{N} = 2^{n/2}$ draws, because the number of candidate pairs among $k$ items grows like $k^2/2$. Formally the collision probability after $k$ draws is about $1 - e^{-k(k-1)/(2N)}$.
&lt;p&gt;The name comes from the party trick: in a room of just 23 people, two of them probably share a birthday, even though there are 365 candidate days, because 23 people form 253 pairs. For a 256-bit hash the same arithmetic says a collision surfaces after about $2^{128}$ digests rather than $2^{256}$, so a 256-bit hash buys you only 128 bits of collision resistance. This is why collision is the weakest joint, and, as the failure catalog will show, always the first to fall.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Clause&lt;/th&gt;
&lt;th&gt;What the attacker must find&lt;/th&gt;
&lt;th&gt;Generic cost&lt;/th&gt;
&lt;th&gt;Who leans on it&lt;/th&gt;
&lt;th&gt;First to fall?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Preimage resistance&lt;/td&gt;
&lt;td&gt;Any input hashing to a given digest&lt;/td&gt;
&lt;td&gt;$2^{n}$&lt;/td&gt;
&lt;td&gt;Password hiding, commitments, key derivation&lt;/td&gt;
&lt;td&gt;Last&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second-preimage resistance&lt;/td&gt;
&lt;td&gt;A different input matching one fixed input&lt;/td&gt;
&lt;td&gt;$2^{n}$&lt;/td&gt;
&lt;td&gt;Integrity of a known, published file&lt;/td&gt;
&lt;td&gt;Middle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collision resistance&lt;/td&gt;
&lt;td&gt;Any two distinct inputs that agree&lt;/td&gt;
&lt;td&gt;$2^{n/2}$&lt;/td&gt;
&lt;td&gt;Signatures, certificates, deduplication&lt;/td&gt;
&lt;td&gt;First&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Second-preimage feels like it should be as easy as collision, but it is not. A collision lets you shop for any lucky pair anywhere in the space. A second preimage nails one endpoint to a value someone else chose and dares you to hit it from the other side, which is a $2^n$ problem, not a $2^{n/2}$ one. Attackers who can only find collisions therefore need to control both documents, which is precisely the constraint the forged-certificate attacks had to engineer around.&lt;/p&gt;
&lt;p&gt;The following simulator makes the gap tangible. It uses a toy 24-bit hash so both effects fit in a browser tab: watch how few inputs it takes to stumble into a collision, and how hopeless it is to hit one fixed target by search.&lt;/p&gt;
&lt;p&gt;{`
// A toy 24-bit &quot;hash&quot;: mix the input, then keep the low 24 bits (N = 2^24).
const N = 1 &amp;lt;&amp;lt; 24;                 // 16,777,216 possible digests
function h(x) {
  let v = Math.imul(x ^ 0x9e3779b1, 2654435761) &amp;gt;&amp;gt;&amp;gt; 0;
  v ^= v &amp;gt;&amp;gt;&amp;gt; 15;                    // fold high bits down so the low bits mix
  v = Math.imul(v, 0x85ebca6b) &amp;gt;&amp;gt;&amp;gt; 0;
  v ^= v &amp;gt;&amp;gt;&amp;gt; 13;
  return v &amp;amp; (N - 1);
}&lt;/p&gt;
&lt;p&gt;// Collision search: hash 1, 2, 3, ... and stop at the first repeat.
const seen = new Map();
let collisionAt = null;
for (let i = 1; i &amp;lt; 300000 &amp;amp;&amp;amp; collisionAt === null; i++) {
  const d = h(i);
  if (seen.has(d)) collisionAt = i;
  else seen.set(d, i);
}
console.log(&quot;birthday bound, sqrt(N), is about&quot;, Math.round(Math.sqrt(N)));
console.log(&quot;first collision appeared after only&quot;, collisionAt, &quot;inputs&quot;);&lt;/p&gt;
&lt;p&gt;// Preimage search: how many tries to hit ONE fixed target digest?
const target = h(42), budget = 500000;
let tries = 0, hit = false;
for (let x = 5000000; x &amp;lt; 5000000 + budget; x++) {
  tries++;
  if (h(x) === target) { hit = true; break; }
}
console.log(&quot;preimage search used&quot;, tries, &quot;tries and&quot;, hit ? &quot;got lucky&quot; : &quot;found nothing&quot;);
console.log(&quot;average preimage cost would be about N/2 =&quot;, N / 2, &quot;tries&quot;);
`}&lt;/p&gt;
&lt;p&gt;Collisions surface after only a couple of thousand inputs, while the fixed-target search burns through half a million tries and, on average, would need eight million. That is the $2^{n/2}$ versus $2^{n}$ split in miniature.&lt;/p&gt;
&lt;p&gt;There is a fourth property people silently assume: that a good hash behaves like a random oracle, a magic box that returns a fresh uniformly random answer for every distinct input and is otherwise consistent. It is a wonderful modeling fiction, and much of applied cryptography is proved secure by pretending it is real.&lt;/p&gt;
&lt;p&gt;But no fixed, finite, publicly specified function can actually be a random oracle, and Canetti, Goldreich, and Halevi proved the gap is not merely cosmetic: there exist schemes secure with an ideal oracle yet insecure under every concrete instantiation [@cgh-jacm-2004]. We make that precise in Section 10. For now, hold the three real clauses in mind, plus the knowledge that the fourth is a heuristic.&lt;/p&gt;
&lt;p&gt;Two consequences follow immediately and set up everything after. First, you never sign a document; you sign its digest, so a signature is only as unforgeable as the hash is collision resistant. The formal security goals for signatures and message authentication codes are the subject of Part 1 of this series, &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-&quot; rel=&quot;noopener&quot;&gt;the security-definitions field guide&lt;/a&gt;; here we only need the consequence. Second, the weakest clause is collision resistance, sitting at $2^{n/2}$. To see how that clause snapped in practice, we first have to see how the fingerprint is assembled, because the assembly leaks in ways the three clauses alone do not reveal.&lt;/p&gt;
&lt;h2&gt;3. How the Fingerprint Was Built: The Merkle-Damgard Era&lt;/h2&gt;
&lt;p&gt;The hash function became load-bearing infrastructure almost by accident. When Whitfield Diffie and Martin Hellman introduced public-key cryptography in 1976, their signatures could only operate on short numbers, roughly the size of the key [@diffie-hellman-1976]. But people wanted to sign whole documents, contracts, and programs. The fix was quiet and consequential: do not sign the document, sign a short fingerprint of it. That single move loaded the entire weight of a signature onto the hash. If two documents can share a fingerprint, one signature covers both, so the signature scheme is only as sound as the hash beneath it.&lt;/p&gt;
&lt;p&gt;Ralph Merkle&apos;s 1979 Stanford thesis formalized one-way hash functions and, in its &quot;Tree Authentication&quot; section, introduced the hash tree that now underlies Git, blockchains, and verified downloads [@merkle-thesis-1979].&lt;/p&gt;
&lt;p&gt;The next question was how to build such a fingerprint for an input of any length out of a small, analyzable building block. In 1989, Ralph Merkle and, independently, Ivan Damgard supplied the answer that defined the next two decades [@merkle-crypto89, @damgard-crypto89].&lt;/p&gt;

An iterated hash built by padding the message, splitting it into fixed-size blocks, and folding the blocks one at a time through a compression function, starting from a fixed initial value called the IV. Merkle and Damgard proved that if the compression function is collision resistant and the final block encodes the message length, a trick called MD strengthening, then the whole hash inherits collision resistance from the small compression function.
&lt;p&gt;That theorem is the reason the 1990s could build hashes with confidence: analyze one small function, get a hash for arbitrary inputs for free. The digest is simply the chaining state after the last block has been absorbed.&lt;/p&gt;

flowchart LR
    IV[&quot;IV: fixed start state&quot;] --&amp;gt; C1[&quot;compress&quot;]
    M1[&quot;block m1&quot;] --&amp;gt; C1
    C1 --&amp;gt; C2[&quot;compress&quot;]
    M2[&quot;block m2&quot;] --&amp;gt; C2
    C2 --&amp;gt; C3[&quot;compress&quot;]
    MP[&quot;final block: message plus length padding&quot;] --&amp;gt; C3
    C3 --&amp;gt; D[&quot;digest: the final chaining state&quot;]
&lt;p&gt;Ronald Rivest built the first widely used family on this shape. MD4 arrived in 1990 and the strengthened MD5 in 1992, both producing 128-bit digests from an add-rotate-xor compression function [@rivest-md4-crypto90, @rfc-1320]. RFC 1321 states MD5&apos;s purpose plainly: it compresses a message &quot;in a secure manner before being signed with a private key&quot; [@rfc-1321]. The design DNA, a message schedule feeding an ARX round function, propagated forward. In 1993 the U.S. government standardized a 160-bit hash of the same lineage in FIPS 180, retroactively called SHA-0, and in 1995 replaced it with SHA-1, a one-bit tweak of the same design [@nist-hash-functions]. SHA-1&apos;s specification survives in the current Secure Hash Standard [@fips-180-4], and it went on to hash most of the internet for roughly twenty years.&lt;/p&gt;
&lt;p&gt;The only visible difference between SHA-0 and SHA-1 is a single one-bit rotation added to the message schedule in 1995, a small and initially unexplained change later understood to improve collision resistance. The hedge held for a while, but SHA-0 was later shown to be genuinely fragile: Xiaoyun Wang and colleagues found efficient full collisions for SHA-0 well under the birthday bound in 2005 [@wang-yu-yin-sha0-crypto05]. The quietly patched flaw foreshadowed the cryptanalysis that would soon reach SHA-1 itself.&lt;/p&gt;

This detour, hashing before signing, is so routine that engineers forget it is a security assumption. A signature over `H(m)` is meant to bind you to `m`, but it truly binds you only to `H(m)`. If an adversary finds a second message with the same digest, your signature covers that message too, and no key was ever stolen. Every certificate, code signature, and signed software update in this article inherits this assumption, which is why a collision in the hash is not an academic curiosity but a forged signature waiting to happen.
&lt;p&gt;The Merkle-Damgard theorem promised something precise and limited: the whole hash is as collision resistant as its little compression function. It never promised that the shape wrapped around that function, the chaining, the padding, the exposed final state, was itself sound. That distinction is exactly where the first cracks opened, and they opened even for a hypothetically perfect compression function.&lt;/p&gt;
&lt;h2&gt;4. The Structural Cracks: Why Merkle-Damgard Leaks&lt;/h2&gt;
&lt;p&gt;Here is the unsettling part. You can hand the Merkle-Damgard construction a perfect compression function, one with no weakness whatsoever, and the assembled hash still leaks. It leaks four separate ways, and none of them is a bug in any particular function. They are properties of the shape.&lt;/p&gt;
&lt;p&gt;The first leak is the most practical, and it follows directly from the diagram in the previous section. The digest is the final chaining state. Nothing hides it. So if you know a digest and the length of the message that produced it, you can pick up the computation exactly where it left off.&lt;/p&gt;

Given a valid digest $H(m)$ and the length of $m$, but not $m$ itself, an attacker can compute $H(m \,\|\, \text{pad} \,\|\, s)$ for a suffix $s$ of their choosing. It works because a Merkle-Damgard digest is literally the internal chaining state after the last block, so the attacker resumes from that state and folds in more blocks, without ever knowing $m$.
&lt;p&gt;The mechanism has exactly three moving parts, and it needs neither the message nor any secret. Publishing $H(m)$ publishes a fully loaded, resumable hash engine. From the length of $m$ the attacker rebuilds the glue padding, the &lt;code&gt;0x80&lt;/code&gt; byte, the run of zeros, and the encoded bit-length that Merkle-Damgard appends before the final block [@fips-180-4]. The attacker reloads $H(m)$ as the IV, folds in the glue and then an arbitrary suffix, and reads off a valid digest for the longer message. When the server later hashes &lt;code&gt;secret || m || glue || suffix&lt;/code&gt; it walks the identical chain, passes through the intermediate state $H(m)$, folds in the same suffix, and confirms the forgery.&lt;/p&gt;

flowchart LR
    PUB[&quot;Public: digest H(m) and length of m&quot;] --&amp;gt; REBUILD[&quot;Rebuild glue padding from the length alone&quot;]
    REBUILD --&amp;gt; RELOAD[&quot;Reload H(m) as the compression IV&quot;]
    RELOAD --&amp;gt; FOLD[&quot;Fold in glue, then attacker suffix&quot;]
    FOLD --&amp;gt; FORGE[&quot;Forged digest for m plus glue plus suffix&quot;]
    SERVER[&quot;Server hashes secret plus m plus glue plus suffix&quot;] -.walks the identical chain.-&amp;gt; FORGE
&lt;p&gt;This turns a natural-looking construction into a trap. Many engineers reach for &lt;code&gt;H(secret || message)&lt;/code&gt; as a homemade message authentication code, reasoning that you cannot forge a tag without knowing the secret. Length extension refutes that: given a valid tag for one message, an attacker appends chosen data and computes a valid tag for the longer message, secret still unknown. The toy hash below is deliberately tiny and insecure, but it has the one property that matters, its digest is its whole internal state, so it forges end to end exactly as MD5 or SHA-256 would.&lt;/p&gt;
&lt;p&gt;{`
// A toy Merkle-Damgard hash. Like MD5 and SHA-256, its DIGEST IS ITS FINAL
// INTERNAL STATE -- that single fact is the entire length-extension bug.
const BLK = 8;
const IV = 0x1234abcd &amp;gt;&amp;gt;&amp;gt; 0;
const enc = s =&amp;gt; [...s].map(c =&amp;gt; c.charCodeAt(0));&lt;/p&gt;
&lt;p&gt;function compress(state, byte) {          // one toy ARX step (not secure)
  let s = (state + byte) &amp;gt;&amp;gt;&amp;gt; 0;
  s = ((s &amp;lt;&amp;lt; 7) | (s &amp;gt;&amp;gt;&amp;gt; 25)) &amp;gt;&amp;gt;&amp;gt; 0;
  return Math.imul(s ^ 0x9e3779b1, 2654435761) &amp;gt;&amp;gt;&amp;gt; 0;
}
function fold(state, bytes) {             // raw fold, adds no padding
  let s = state &amp;gt;&amp;gt;&amp;gt; 0;
  for (const b of bytes) s = compress(s, b);
  return s &amp;gt;&amp;gt;&amp;gt; 0;
}
function pad(len) {                       // MD strengthening from the LENGTH ALONE
  const p = [0x80];
  while ((len + p.length + 1) % BLK !== 0) p.push(0x00);
  p.push(len &amp;amp; 0xff);                     // 1-byte toy length encoding
  return p;
}
function toyHash(bytes) {                 // full hash = fold over message + padding
  return fold(IV, bytes.concat(pad(bytes.length)));
}&lt;/p&gt;
&lt;p&gt;// The server publishes (message, tag) where tag = H(secret || message).
const secret  = enc(&quot;SECRET&quot;);            // 6 bytes the attacker never sees
const message = enc(&quot;user=guest&quot;);
const tag = toyHash(secret.concat(message));&lt;/p&gt;
&lt;p&gt;// The attacker knows tag, message, and only the LENGTH of the secret (6).
const secretLen = 6;
const glue   = pad(secretLen + message.length);   // rebuilt from lengths, no secret
const suffix = enc(&quot;&amp;amp;role=admin&quot;);
const total  = secretLen + message.length + glue.length + suffix.length;
const forgedTag = fold(tag, suffix.concat(pad(total)));    // resume FROM the tag
const forgedMessage = message.concat(glue).concat(suffix);&lt;/p&gt;
&lt;p&gt;// The server verifies the forged message with its real secret.
const check = toyHash(secret.concat(forgedMessage));
console.log(&quot;forged tag (secret never used):&quot;, forgedTag.toString(16));
console.log(&quot;server recomputation          :&quot;, check.toString(16));
console.log(&quot;forgery accepted?&quot;, forgedTag === check);
console.log(&quot;smuggled &apos;role=admin&apos; present?&quot;, String.fromCharCode(...forgedMessage).includes(&quot;role=admin&quot;));
`}&lt;/p&gt;
&lt;p&gt;Real tools do this against real hashes. The canonical &lt;code&gt;hash_extender&lt;/code&gt; demonstrates it on MD5: publish &lt;code&gt;signature = MD5(secret || &quot;data&quot;) = 6036708eba0d11f6ef52ad44e8b74d5b&lt;/code&gt;, and knowing only &lt;code&gt;data&lt;/code&gt;, the algorithm, and that signature, an attacker loads the signature back into MD5&apos;s state and continues hashing a chosen suffix to forge a valid tag for &lt;code&gt;secret || data || glue || suffix&lt;/code&gt;, never learning the secret. Its own summary is the whole lesson: &quot;100% of the state needed to continue a hash is in the output of most hashing algorithms&quot; [@hash-extender].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; On any full-width Merkle-Damgard hash, including MD5, SHA-1, SHA-256, and SHA-512, the prefix-secret construction &lt;code&gt;H(secret || message)&lt;/code&gt; is forgeable by length extension. Use HMAC (Section 9) or a length-extension-immune hash instead. This footgun is common enough that it has its own family of real exploits in serialized-signing and token systems, examined in &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;the serialization and canonicalization field guide&lt;/a&gt;; here we own the mechanism, that guide owns the exploitation.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A precise note you should carry forward, because it is the sharpest discriminator between functions in this whole article. Length extension afflicts hashes whose digest exposes the full final state: MD5, SHA-1, and the full-width SHA-256 and SHA-512. Truncation can close the leak, but only if it hides enough of the state. The truncated SHA-2 variants that hide at least 128 bits (SHA-384, SHA-512/224, SHA-512/256), the SHA-3 family and SHAKE, HMAC, and the BLAKE hashes are all immune. The dangerous middle case is SHA-224, which discards only 32 bits and is therefore still vulnerable at about $2^{32}$ work, a distinction Section 6 makes exact. Defer the full table there; just remember that &quot;truncated&quot; does not automatically mean &quot;safe.&quot;&lt;/p&gt;
&lt;p&gt;The other three leaks are subtler but drove the field toward new designs. In 2004, Antoine Joux showed that in any iterated hash, finding $2^k$ messages that all share one digest costs only about $k$ times the work of a single collision, not the vastly larger amount naive counting suggests [@joux-crypto04]. The construction is almost embarrassingly direct: from the starting state, one birthday search finds a colliding pair of blocks that both drive the chain to the same next state; repeat from that state $k$ times; then every one of the $2^k$ ways to pick one block or the other at each step traces the identical chain to the identical final digest.&lt;/p&gt;
&lt;p&gt;These multicollisions demolish a piece of folklore that had reassured a generation of engineers: that concatenating two independent hashes, $H_1(m) ,|, H_2(m)$, multiplies their strength. It does not. Build a large multicollision in the weaker half -- many messages that all share one digest under it, which is cheap for a Merkle-Damgard hash -- then birthday-search that set for a pair that also collides under the stronger half. The combined collision resistance is essentially that of the stronger single hash, not the sum. Hashing twice buys almost nothing.&lt;/p&gt;
&lt;p&gt;Joux&apos;s multicollisions were also the engine for two deeper results. In 2005, John Kelsey and Bruce Schneier used expandable messages built from multicollisions to find second preimages on Merkle-Damgard hashes for far less than the ideal $2^n$: for a target of $2^k$ blocks, the cost falls to roughly $2^{n-k}$ plus overhead [@kelsey-schneier-eurocrypt05]. That matters because second-preimage resistance was supposed to be the sturdy $2^n$ clause, yet the shape leaks it for long messages. In 2006, Kelsey and Tadayoshi Kohno went further with the herding, or Nostradamus, attack: an attacker precomputes a branching diamond structure, publicly commits to a digest as if it were a prediction, and later herds almost any chosen prefix into that exact digest [@kelsey-kohno-eurocrypt06]. It weaponizes the construction into proof-of-prediction fraud.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Length extension, Joux multicollisions, long-message second preimages, and herding are flaws in the shape, not in the function. A stronger or longer compression function cannot fix them, because they do not depend on any weakness in that function. Only a different construction can. Hold that thought: it is exactly why, after the failures, the field did not just build a bigger Merkle-Damgard hash. It built a different machine.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those four are leaks in an idealized world, where the compression function is flawless. They were serious enough to motivate truncated designs and, eventually, entirely new constructions. But they were still theoretical. What turned this subject from a seminar into a decade of emergency migrations was the other kind of failure: the compression function itself fell, and someone chose to weaponize it.&lt;/p&gt;
&lt;h2&gt;5. The Failure Catalog: Every Notable Break, Named&lt;/h2&gt;
&lt;p&gt;A broken hash is a lab curiosity until someone forges a certificate with it. Twice, someone did, and once it was a nation-state. This section is the heart of the guide, because the two failure arcs, MD5 and SHA-1, are the evidence for everything the rest of it recommends. Each arc runs the same course: a theoretical crack, a practical collision, a technique that makes the collision meaningful, and finally a real forged trust anchor.&lt;/p&gt;
&lt;p&gt;The MD5 arc opens with an early warning that the field under-heeded. In 1996, Hans Dobbertin found practical full collisions for MD4 and collisions in MD5&apos;s compression function under a chosen initial value, not yet the full hash but a clear structural crack [@dobbertin-md4]. MD5 stayed deployed anyway.&lt;/p&gt;
&lt;p&gt;The warning became undeniable in August 2004, when Xiaoyun Wang, Dengguo Feng, Xuejia Lai, and Hongbo Yu announced full MD5 collisions computable in minutes, alongside collisions for MD4, HAVAL-128, and RIPEMD [@wang-md5-eprint-2004-199]. The following year, Wang and Yu published the peer-reviewed differential method behind it [@wang-yu-break-md5-eurocrypt05]. That is the theoretical death of MD5. Everything after is engineering.&lt;/p&gt;
&lt;p&gt;The engineering that mattered was learning to choose the colliding inputs.&lt;/p&gt;

A collision between two inputs that begin with two different, attacker-chosen prefixes. The attacker fixes meaningful, distinct starting content for each side, two different names in a certificate, say, and then computes collision blocks that drive both sides to the same digest. This is strictly harder than an identical-prefix collision, where the two inputs share a common start, and strictly more dangerous, because it forges a relationship between two meaningful documents rather than two random blobs.
&lt;p&gt;In 2007, Marc Stevens, Arjen Lenstra, and Benne de Weger built exactly that for MD5, and used it to construct two X.509 certificates with different identities but the same MD5 digest [@stevens-chosen-prefix-md5-eurocrypt07].&lt;/p&gt;
&lt;p&gt;A year later the attack left the lab. At the 2008 Chaos Communication Congress, a seven-person team used a chosen-prefix MD5 collision, about $2^{49}$ MD5 compressions on a cluster of roughly 200 PlayStation 3 consoles, to obtain a genuine commercial certificate-authority signature over a benign certificate that simultaneously validated a forged intermediate certificate authority [@rogue-ca]. The forged certificate carried the basic-constraints CA flag set to TRUE, which meant every browser on earth would trust certificates it issued.&lt;/p&gt;

flowchart TD
    P1[&quot;Benign prefix: an ordinary certificate request&quot;] --&amp;gt; COL[&quot;Collision blocks appended to both sides&quot;]
    P2[&quot;Malicious prefix: a rogue CA certificate&quot;] --&amp;gt; COL
    COL --&amp;gt; SAME[&quot;Both certificates share one MD5 digest&quot;]
    SAME --&amp;gt; SIG[&quot;Commercial CA signs the benign certificate&quot;]
    SIG --&amp;gt; VALID[&quot;The same signature validates the rogue CA:TRUE certificate&quot;]
&lt;p&gt;To make two certificates collide, the attacker needs somewhere to park the roughly 1632-bit collision block without it looking suspicious. In the 2008 rogue CA, that space lived inside a &quot;Netscape Comment&quot; extension, a field parsers ignore, which the researchers wryly described as hiding the collision like a tumor inside an otherwise healthy certificate [@rogue-ca]. The lesson generalizes: any format with an ignorable, attacker-controlled field gives a collision attack room to breathe.&lt;/p&gt;
&lt;p&gt;The arc peaked in June 2012 with Flame, espionage malware that carried a code-signing certificate chaining to Microsoft. Flame&apos;s authors mounted a new chosen-prefix MD5 collision against a Microsoft sub-authority still using MD5 in its licensing service, letting them sign malware as &quot;Microsoft&quot; and distribute it through Windows Update. Marc Stevens later reconstructed the previously unknown collision variant from the certificate itself [@stevens-counter-cryptanalysis-crypto13].&lt;/p&gt;
&lt;p&gt;A nation-state had used a hash collision to subvert the update trust root of a billion machines. After Flame, MD5 in signatures was not deprecated as a matter of hygiene; it was radioactive.&lt;/p&gt;
&lt;p&gt;SHA-1 traveled the same road a decade behind. In 2005, Xiaoyun Wang, Yiqun Lisa Yin, and Hongbo Yu published the first collision attack on the full SHA-1, at about $2^{69}$ operations, comfortably under the $2^{80}$ birthday bound its 160-bit output was supposed to guarantee [@wang-yin-yu-full-sha1-crypto05]. That theoretical break is what prompted NIST to open the SHA-3 competition. The practical break took twelve more years of hardware progress. In February 2017, a team from CWI Amsterdam and Google announced SHAttered: the first practical identical-prefix SHA-1 collision, at roughly $2^{63.1}$ SHA-1 computations, about nine quintillion of them, costing the equivalent of 6,500 CPU-years plus 110 GPU-years [@shattered]. The proof was the pair of PDFs from this article&apos;s opening, sharing the digest &lt;code&gt;38762cf7f55934b34d179ae6a4c80cadccbb7f0a&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In January 2020, Gaetan Leurent and Thomas Peyrin closed the arc with &quot;SHA-1 is a Shambles,&quot; the first practical chosen-prefix SHA-1 collision at about $2^{63.4}$, which along the way cut the identical-prefix cost to about $2^{61.2}$ [@shambles, @shambles-eprint-2020-014]. To prove it mattered, they forged a PGP/GnuPG key certification, tracked as CVE-2019-14855, building on their own EUROCRYPT 2019 precursor that first pushed collisions toward chosen prefixes [@leurent-peyrin-cp-eurocrypt19].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; SHAttered (2017) was an identical-prefix collision: both files began the same way. Shambles (2020) was a chosen-prefix collision: the two inputs started with different, meaningful content, which is what enables impersonation and forged certificates. Different authors, different years, strictly different strength. Conflating them is one of the most common errors in write-ups of SHA-1&apos;s demise.&lt;/p&gt;
&lt;/blockquote&gt;

&quot;All attacks that are practical on MD5 are now also practical on SHA-1.&quot; That is the Shambles team&apos;s own summary of what a chosen-prefix collision means: SHA-1 has caught down to MD5 [@shambles].

timeline
    title Two failure arcs, MD5 and SHA-1
    1996 : Dobbertin cracks MD4, dents the MD5 compression function
    2004 : Wang team announces practical full MD5 collisions
    2005 : Wang, Yin, Yu attack full SHA-1 near 2 to the 69
    2007 : Stevens builds chosen-prefix MD5 collisions
    2008 : Rogue certificate authority forged near 2 to the 49
    2012 : Flame forges a Microsoft-chained code-signing certificate
    2017 : SHAttered, first identical-prefix SHA-1 collision
    2020 : Shambles, first chosen-prefix SHA-1 collision
&lt;p&gt;The attack costs are worth tabulating, because they show how a break moves from theory to a weekend on rented hardware.&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;Attack type&lt;/th&gt;
&lt;th&gt;Approx. cost&lt;/th&gt;
&lt;th&gt;Real-world impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Rogue CA&lt;/td&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;Chosen-prefix MD5&lt;/td&gt;
&lt;td&gt;$2^{49}$&lt;/td&gt;
&lt;td&gt;Forged intermediate CA trusted by browsers [@rogue-ca]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flame&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;Chosen-prefix MD5&lt;/td&gt;
&lt;td&gt;new variant&lt;/td&gt;
&lt;td&gt;Forged Microsoft-chained code-signing cert [@stevens-counter-cryptanalysis-crypto13]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHAttered&lt;/td&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;Identical-prefix SHA-1&lt;/td&gt;
&lt;td&gt;$2^{63.1}$&lt;/td&gt;
&lt;td&gt;Two PDFs, one SHA-1 digest [@shattered]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shambles&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;Chosen-prefix SHA-1&lt;/td&gt;
&lt;td&gt;$2^{63.4}$&lt;/td&gt;
&lt;td&gt;Forged PGP/GnuPG key certification [@shambles]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;There is a discipline the headlines miss, and it protects you from over-correcting. &quot;MD5 and SHA-1 are dead&quot; is true only for uses that depend on collision resistance.&lt;/p&gt;

Every break above attacks collision resistance. None of them touches HMAC-SHA-1 or HMAC-MD5, because HMAC does not rely on collision resistance at all, a point RFC 2104 made in 1997 and Section 9 unpacks [@rfc-2104]. So the correct scoping is narrow: MD5 and SHA-1 are dead where an adversary can influence the hashed input and profit from a collision, above all in signatures and certificates. They are not magically extinct. Old certificate chains, legacy protocols, and Git&apos;s historical use of SHA-1 all persist, which is why NIST set a firm calendar date, the end of 2030, to withdraw SHA-1 from all U.S. government use rather than pretending it vanished overnight [@nist-retires-sha1]. The recurring lesson of both arcs: deprecating an algorithm in new code is not the same as revoking it from live trust anchors.
&lt;p&gt;Every hash in this catalog shared one design shape, the Merkle-Damgard chain of Section 3. The obvious question, and the one the field actually asked, was whether anything built the same way survived. One did, and understanding why is the turn from despair to design.&lt;/p&gt;
&lt;h2&gt;6. The Survivor: SHA-2, and Why It Did Not Fall&lt;/h2&gt;
&lt;p&gt;Here is a paradox worth sitting with. SHA-2 is built the same way as SHA-1: the same Merkle-Damgard skeleton, the same add-rotate-xor compression style, designed by the same institution. By the logic of the last two sections it should have fallen too. Instead, in 2026, SHA-256 is the hash under most of the internet. What did the survivors do differently?&lt;/p&gt;
&lt;p&gt;The family, standardized in FIPS 180-4, has six members: SHA-224, SHA-256, SHA-384, and SHA-512, plus the truncated SHA-512/224 and SHA-512/256 [@fips-180-4]. And the answer to the paradox is almost anticlimactic. SHA-2 did not adopt a new idea. It widened the margins of the old one.&lt;/p&gt;
&lt;p&gt;Larger chaining state, a longer and more diffusing message schedule, more rounds, and distinct per-round constants together mean that the differential paths Wang&apos;s team rode through MD5 and SHA-1 do not propagate far enough to reach the full function. The evidence is two decades of cryptanalysis that keeps stalling short: the best published collision attacks reach only 31 of SHA-256&apos;s 64 rounds in practice [@mendel-nad-schlaffer-eurocrypt13], and 39 rounds in the weaker semi-free-start setting [@li-liu-wang-sha2-2024]. A 2024 effort using programmatic SAT solvers likewise reaches only reduced-round or modified-initial-value variants, never the full function [@sha256-sat-2024]. That wide margin, 33 rounds to the deepest true collision and 25 even against the weaker semi-free-start attack, is the entire reason to trust it.&lt;/p&gt;
&lt;p&gt;There is one wrinkle the designers half-anticipated, and it is the single most useful discriminator you can memorize. Full-width SHA-256 and SHA-512 are length-extension-vulnerable, exactly as Section 4 warned, because their digest is the whole final state. Most of the truncated members are not. But one truncated member still is, and it is the one people most often get wrong.&lt;/p&gt;

A hash whose internal chaining state is wider than its emitted output, so the digest reveals only part of the final state. SHA-384 and SHA-512/256 run the full SHA-512 engine, a 512-bit state, but output only 384 or 256 bits. Because an attacker never sees the whole state, length extension has nothing to resume from, provided enough of the state stays hidden.
&lt;p&gt;The catch is in that last clause. Truncation defeats length extension only when the hidden part of the state is too large to guess. SHA-384 hides 128 bits and SHA-512/256 hides 256; recovering either by brute force is infeasible. SHA-224, however, is SHA-256 with a different initial value and the 256-bit result cut to 224 bits, so it discards only one 32-bit word of state [@rfc-3874].&lt;/p&gt;
&lt;p&gt;An attacker brute-forces that missing 32 bits in about $2^{32}$ compression calls, seconds on commodity hardware, recovers the full internal state, and then length-extends exactly as if nothing were hidden. SHA-224 is therefore length-extension-vulnerable with only a 32-bit margin, categorically different from the truncated variants that hide 128 bits or more.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The move that saves SHA-384 and SHA-512/256 is almost embarrassingly simple: hide part of the final state. But the amount hidden is the whole game. Hide 128 or 256 bits and length extension has nothing feasible to resume from. Hide only 32 bits, as SHA-224 does, and an attacker recovers the state in about $2^{32}$ work and extends normally. &quot;Truncated&quot; is not a synonym for &quot;safe&quot;; &quot;truncated by enough&quot; is. The sponge, next section, takes this instinct to its logical end by never exposing the state at all.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The table below is the sharpest single reference in this guide. When someone asks whether a hash is safe to use as &lt;code&gt;H(secret || message)&lt;/code&gt;, this is the answer.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Length-extension vulnerable&lt;/th&gt;
&lt;th&gt;Length-extension immune&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;MD5, SHA-1&lt;/td&gt;
&lt;td&gt;SHA-384&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-256, SHA-512 (full-width)&lt;/td&gt;
&lt;td&gt;SHA-512/224, SHA-512/256&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-224 (only $2^{32}$ margin)&lt;/td&gt;
&lt;td&gt;SHA-3 family and SHAKE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;HMAC (any hash), BLAKE2, BLAKE3, KangarooTwelve&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;SHA-NI, the x86 hash instructions Intel and AMD ship, accelerate SHA-1 and SHA-256 but not SHA-512 [@intel-sha-extensions]. That inverts an old piece of folklore. For years SHA-512 was &quot;the fast one&quot; on 64-bit CPUs because it processes 64-bit words; on a 2023 AMD Zen 4, SHA-256 now runs at about 2.03 cycles per byte while SHA-512 trails at about 4.19, because only SHA-256 gets the hardware boost [@ebash-zen4]. On CPUs without SHA-NI, the old ordering returns and SHA-512 and SHA-512/256 pull ahead again.&lt;/p&gt;
&lt;p&gt;That hardware acceleration, combined with universal library support and FIPS validation, is why SHA-256 is the correct default almost everywhere its deployment footprint shows: TLS 1.3 computes its handshake transcript hash with the negotiated SHA-256 or SHA-384 [@rfc-8446], Bitcoin hashes every block header with double SHA-256 [@bitcoin-block-hashing], and Git&apos;s next-generation object format is built on SHA-256 [@git-sha256-transition], alongside code-signing digests and certificate fingerprints. It earned that position not by being newest but by being unbroken and fast where it counts.&lt;/p&gt;
&lt;p&gt;Keep one framing straight, because the whole next act depends on it. SHA-2 is a survivor of the supersession arc that ran MD5 to SHA-1 to SHA-2. It is not something a later hash replaced. But a healthy survivor carrying most of the world&apos;s traffic is still a monoculture, and the field had just learned, viscerally, what happens when everything rests on one design. So even though SHA-2 was fine, cryptographers deliberately built something that shared none of its DNA.&lt;/p&gt;
&lt;h2&gt;7. The Different Shape: The Sponge and SHA-3&lt;/h2&gt;
&lt;p&gt;Imagine a hash with no exposed chaining state to steal, that hands you arbitrary-length output for free, and that shares no mathematical DNA with SHA-2. That is not a patch on Merkle-Damgard. It is a different machine.&lt;/p&gt;
&lt;p&gt;NIST went looking for that machine deliberately. In November 2007, stung by the 2004 and 2005 collision attacks, it opened a public SHA-3 competition, explicitly asking for a hash built on a construction unlike SHA-2 so that a future break of one would not endanger the other [@nist-sha3-project]. Five finalists emerged from dozens of submissions: BLAKE, Grostl, JH, Keccak, and Skein. In October 2012, NIST selected Keccak, designed by Guido Bertoni, Joan Daemen, Michael Peeters, and Gilles Van Assche, and standardized it as FIPS 202 in August 2015 [@nist-sha3-project, @fips-202]. Its engine is the sponge.&lt;/p&gt;

A hash built around a single fixed permutation applied to a state of $b$ bits, split into a rate of $r$ bits and a capacity of $c$ bits, with $b = r + c$. In the absorb phase, message blocks are XORed into the rate portion and the whole state is stirred by the permutation. In the squeeze phase, output bytes are read from the rate, with the permutation applied between reads. The capacity is never read from or written to directly. SHA-3 uses the Keccak permutation on a 1600-bit state over 24 rounds.
&lt;p&gt;The capacity is the whole trick. Because those $c$ bits never touch the input or the output, an attacker who sees the digest learns nothing about them, and cannot reconstruct the internal state.&lt;/p&gt;

flowchart LR
    M[&quot;Message blocks&quot;] --&amp;gt; ABS[&quot;Absorb: XOR into rate, then permute&quot;]
    ABS --&amp;gt; STATE[&quot;State: rate lane plus capacity lane&quot;]
    STATE --&amp;gt; SQ[&quot;Squeeze: read the rate, permute, repeat&quot;]
    SQ --&amp;gt; OUT[&quot;Output: fixed digest or an XOF stream&quot;]
    CAP[&quot;Capacity lane, never read or written directly&quot;] -.-&amp;gt; STATE
&lt;p&gt;Two payoffs fall out of that single design decision. First, the sponge is length-extension-immune by construction, not by truncation: there is no exposed final state to resume from, so &lt;code&gt;H(secret || message)&lt;/code&gt; is not forgeable the way it is on full-width SHA-2. Second, the squeeze phase can simply keep going, so the sponge is natively an extendable-output function.&lt;/p&gt;

A hash-like primitive that emits output of any length you ask for from a single input, rather than a fixed-size digest. You request as many bytes as you need and keep squeezing. SHAKE128 and SHAKE256, standardized alongside SHA-3, are the workhorse examples, and they are what modern post-quantum schemes call to expand seeds into keys.
&lt;p&gt;The security is not folklore. Bertoni, Daemen, Peeters, and Van Assche proved the sponge is indifferentiable from a random oracle up to the capacity bound, which is the strongest positive statement you can make about a concrete construction [@bertoni-sponge-indiff-eurocrypt08, @keccak-sponge-duplex]. In two decades of analysis, practical collisions have reached only about 5 of Keccak&apos;s 24 rounds, leaving an enormous margin [@keccak-third-party].&lt;/p&gt;
&lt;p&gt;Now the load-bearing correction this whole guide exists to make. SHA-3 is not the successor to SHA-2 the way SHA-2 succeeded SHA-1. FIPS 202 chooses its words carefully.&lt;/p&gt;

FIPS 202 states that the four SHA-3 functions &quot;supplement the hash functions ... in FIPS 180-4: SHA-1 and the SHA-2 family,&quot; and that together the two standards &quot;provide resilience against future advances&quot; because they &quot;rely on fundamentally different design principles&quot; [@fips-202].
&lt;p&gt;The word is supplement, not supersede, and NIST is even blunter elsewhere: its Policy on Hash Functions says there is &quot;no need to transition applications from SHA-2 to SHA-3&quot; [@nist-policy-hash-functions], and the superseded 2012 version of that policy put it more strongly still, with &quot;no need or plan to transition&quot; [@nist-policy-hash-functions-2012-archived].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; SHA-3 is not the replacement for SHA-2. It is the insurance policy. After watching one construction, Merkle-Damgard, carry MD5, SHA-1, and SHA-2 all at once, the field deliberately funded a second, unrelated construction so that the internet&apos;s trust machinery would never again rest on a single design. SHA-2 and SHA-3 coexist on purpose. That is diversification, not supersession, and it is a different axis from the MD5-to-SHA-1-to-SHA-2 ladder entirely.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ethereum uses &quot;Keccak-256,&quot; which is the original competition-era Keccak with its pre-standard padding, not the FIPS 202 SHA3-256 that NIST later finalized with an extra domain-separation suffix. The two give different digests for the same input, a frequent source of confusion for developers who assume &quot;Keccak&quot; and &quot;SHA-3&quot; are interchangeable. Solidity&apos;s built-in is spelled &lt;code&gt;keccak256&lt;/code&gt; precisely to signal the original variant [@solidity-globals].&lt;/p&gt;
&lt;p&gt;The sponge bought genuine algorithmic diversity and length-extension immunity for free. What it did not buy, at least in plain software, was raw speed: on a Zen 4 core without special instructions, SHA3-256 runs at about 6.8 cycles per byte, roughly three times slower than hardware-accelerated SHA-256 [@ebash-zen4]. That gap set the agenda for the last decade. Could you keep the security and the length-extension immunity and still go an order of magnitude faster? Two answers arrived, from two different shapes.&lt;/p&gt;
&lt;h2&gt;8. The Fast Hashes: BLAKE3, the Tree, and Where K12 Sits&lt;/h2&gt;
&lt;p&gt;The command &lt;code&gt;b3sum&lt;/code&gt; can hash a large file roughly ten times faster than &lt;code&gt;sha256sum&lt;/code&gt;, and it does not cheat to get there [@blake3-repo]. It uses every CPU core and every SIMD lane at once. Understanding how it does that, and when you should let it, means separating two ideas people constantly conflate.&lt;/p&gt;
&lt;p&gt;There are two axes here, and they are independent. The construction axis says how a hash is built: Merkle-Damgard (SHA-2), sponge (SHA-3, SHAKE, and, importantly, KangarooTwelve), and the BLAKE tree family derived from the ChaCha stream cipher. The use-case axis says what job you are hiring it for, and &quot;modern high-throughput hash&quot; is a job, currently filled by BLAKE3 and KangarooTwelve. Notice that those two fast hashes sit on different construction axes: one is a tree over an ARX core, the other is a sponge. Speed is not a construction.&lt;/p&gt;
&lt;p&gt;The BLAKE line began as a SHA-3 finalist, a ChaCha-derived design [@hash-function-blake-book]. It became practical with BLAKE2, specified in RFC 7693, which is directly keyable: feed it a key and it is a message authentication code with no HMAC wrapper needed [@rfc-7693]. Then in 2020, Jack O&apos;Connor, Jean-Philippe Aumasson, Samuel Neves, and Zooko Wilcox-O&apos;Hearn released BLAKE3, which took the decisive structural step: it arranges a round-reduced BLAKE2-style compression function inside a binary Merkle tree [@blake3-repo].&lt;/p&gt;

flowchart TD
    ROOT[&quot;Root chaining value, extensible to an XOF&quot;] --&amp;gt; P0[&quot;Parent node&quot;]
    ROOT --&amp;gt; P1[&quot;Parent node&quot;]
    P0 --&amp;gt; L0[&quot;Chunk 0, 1 KiB&quot;]
    P0 --&amp;gt; L1[&quot;Chunk 1, 1 KiB&quot;]
    P1 --&amp;gt; L2[&quot;Chunk 2, 1 KiB&quot;]
    P1 --&amp;gt; L3[&quot;Chunk 3, 1 KiB&quot;]
&lt;p&gt;The tree is what unlocks the speed. Because each 1 KiB chunk hashes independently, any number of SIMD lanes and threads can work in parallel, and the results combine afterward. The same structure gives BLAKE3 incremental updates and verified streaming, and it collapses five roles into one function: plain hash, keyed MAC, pseudorandom function, key-derivation function via its &lt;code&gt;derive_key&lt;/code&gt; mode, and extendable output. On a Zen 4 core it runs at about 0.60 cycles per byte single-threaded, the fastest secure hash in the eBASH benchmarks, before you even add threads [@ebash-zen4].&lt;/p&gt;

BLAKE3&apos;s own documentation states it plainly: it is &quot;secure against length extension, unlike SHA-2&quot; [@blake3-repo].
&lt;p&gt;The 1 KiB chunk size is not arbitrary. Because every chunk is an independent leaf, a verifier holding the root can check that one specific 1 KiB slice of a huge file is authentic by recomputing only the $O(\log n)$ nodes along its path, never re-hashing the whole file. That is the basis of verified streaming: download a movie and verify each piece as it arrives, aborting instantly if any block is corrupted or tampered [@blake3-repo].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; BLAKE3 is engineered to be fast, which makes it exactly wrong for hashing passwords, where you want to be deliberately slow so an attacker cannot try billions of guesses per second. Its own README says as much and points you to Argon2 [@blake3-repo]. Use a purpose-built password KDF, Argon2id for new systems, or scrypt, bcrypt, or PBKDF2, never a bare fast hash. Section 11 returns to this.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The other fast hash keeps you on the sponge. KangarooTwelve, with its TurboSHAKE core, is a tree hash over a round-reduced Keccak permutation, Keccak-p with 12 rounds instead of 24, published as the Informational RFC 9861 in October 2025 [@rfc-9861, @k12-acns18]. It is genuinely fast: the vendor reports about 0.51 cycles per byte using AVX-512 and 0.75 on an Apple M1, though a generic build measures closer to 3.5 cycles per byte on Zen 4 [@keccak-kangarootwelve, @ebash-zen4]. The point to hold onto, and the reason it gets a signpost rather than a section, is that KangarooTwelve is not a third construction family. It is the sponge again, tuned for throughput. It is standardized but still niche, and, like BLAKE3, not FIPS-approved.&lt;/p&gt;

On a machine with both tools installed, `time b3sum bigfile.iso` against `time sha256sum bigfile.iso` on a multi-gigabyte file shows the order-of-magnitude gap the BLAKE3 documentation advertises, most of it from multithreading. If you only have OpenSSL, `openssl dgst -sha3-256 bigfile.iso` will show you the software sponge penalty on hardware without SHA-3 instructions.
&lt;p&gt;Everything else is a one-line signpost. RIPEMD-160, Whirlpool, China&apos;s SM3, and Russia&apos;s Streebog are real and occasionally mandated regionally, but they are not the default anywhere the reader is likely to design for. The losing SHA-3 finalists, Grostl, JH, and Skein, are well-analyzed but standardized nowhere. None of them changes the decision you are about to make.&lt;/p&gt;
&lt;p&gt;Fast, parallel, and diverse, yes. But so far we have only hashed raw data. Real protocols almost never use a bare hash. They key it, they separate its domains, and they stretch its output, and that is precisely where most real bugs live.&lt;/p&gt;
&lt;h2&gt;9. Beyond the Bare Hash: Keyed, Domain-Separated, and XOF Modes&lt;/h2&gt;
&lt;p&gt;The most common hash bug in production is not a broken algorithm. It is a correct algorithm used in the wrong mode, and the specific mistake, again and again, is pressing &lt;code&gt;H(secret || message)&lt;/code&gt; into service as a message authentication code. Section 4 showed why that is forgeable on full-width SHA-2. Here is what to use instead, and why it holds.&lt;/p&gt;
&lt;p&gt;The canonical fix is HMAC, from Mihir Bellare, Ran Canetti, and Hugo Krawczyk in 1996 and RFC 2104 in 1997 [@bellare-canetti-krawczyk-crypto96, @rfc-2104].&lt;/p&gt;

A message authentication code built from any hash $H$ and a secret key $K$ by nesting two hash calls: $$\mathrm{HMAC}(K, m) = H\big((K \oplus opad) \,\|\, H((K \oplus ipad) \,\|\, m)\big)$$ where $opad$ and $ipad$ are fixed padding constants. Bellare, Canetti, and Krawczyk proved it is a secure pseudorandom function under assumptions weaker than full collision resistance of $H$.
&lt;p&gt;Two things make HMAC the right tool. First, the outer hash of an inner hash structurally defeats length extension: the attacker never sees a resumable state. Second, and this is the payoff of the scoping discipline from Section 5, HMAC does not depend on the collision resistance of its hash. RFC 2104 states directly that the known collision weaknesses of MD5 &quot;do not compromise the use of MD5 within HMAC&quot; [@rfc-2104]. That is why HMAC-SHA-1 and HMAC-MD5 remain unbroken even though bare SHA-1 and MD5 are dead for signatures.&lt;/p&gt;
&lt;p&gt;The keys HMAC needs have to come from somewhere with real entropy, which is the subject of Part 2 of this series, &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand&quot; rel=&quot;noopener&quot;&gt;the field guide to cryptographic randomness&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;HMAC is also the foundation of the most widely deployed key-derivation function, HKDF, which follows an extract-then-expand pattern: an extract step distills a uniform key from messy input keying material, and an expand step stretches it to any needed length [@rfc-5869]. HKDF is worth naming precisely because it is a mode built on a hash by way of HMAC, not a hash itself; when you need to turn a shared secret into keys, reach for it rather than hashing by hand.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you need to authenticate a message with a key, reach for &lt;code&gt;HMAC-SHA-256&lt;/code&gt; or &lt;code&gt;KMAC&lt;/code&gt;. Do not invent &lt;code&gt;H(key || message)&lt;/code&gt;, &lt;code&gt;H(message || key)&lt;/code&gt;, or any variation: the first is length-extension-forgeable on full-width SHA-2, and the others have their own pitfalls that decades of analysis went into avoiding. The whole point of HMAC and KMAC is that someone already did the hard work and proved it correct.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The sponge offers a native alternative that needs no nesting, because it already resists length extension. NIST SP 800-185 defines four such functions: cSHAKE, KMAC, TupleHash, and ParallelHash [@sp-800-185]. KMAC is a keyed MAC that simply absorbs the key into the sponge. The other three are about domain separation.&lt;/p&gt;

Deliberately making the hash of the same bytes come out different depending on the context in which they appear, by mixing in a distinct label or customization string per context. It stops an input crafted for one purpose from being replayed as a valid input to another.

Consider hashing a pair of fields, a username and a message. Concatenating them and hashing, `H(user || message)`, is ambiguous: the pair (`alice`, `bob-says-hi`) and the pair (`aliceb`, `ob-says-hi`) serialize to the same bytes and therefore the same digest. An attacker who controls where the boundary falls can forge equivalences. TupleHash removes the ambiguity by length-encoding each element of the sequence, so the structure, not just the bytes, is bound into the digest. cSHAKE does the same across protocol contexts: a customization string like the ASCII label for &quot;email-signature&quot; versus &quot;file-checksum&quot; guarantees the two contexts can never produce the same output for the same input. Any time distinct inputs could serialize to identical byte streams, you need this, and rolling it yourself is where subtle bugs breed [@sp-800-185].
&lt;p&gt;XOFs change how you get short outputs, too. If you need a 20-byte value at a 128-bit security level, do not hash and chop arbitrary bytes off SHA-256; ask SHAKE128 or a BLAKE3 XOF for exactly 20 bytes. The security level is governed by the capacity, not by how many bytes you happen to read, so requesting the exact length is both cleaner and safer than ad hoc truncation. NIST is even revising SP 800-185 to add streaming specifications for SHAKE, a sign of how central XOFs have become [@nist-fips202-sp800185-update].&lt;/p&gt;
&lt;p&gt;Finally, the oldest mode of all scales integrity to enormous data: the Merkle tree from Ralph Merkle&apos;s 1979 thesis [@merkle-thesis-1979]. Hash the leaves, hash pairs of hashes upward to a single root, and you can prove any one leaf belongs to the root with a path of only $O(\log n)$ hashes. Git object trees, blockchain state commitments, Certificate Transparency logs, and BLAKE3&apos;s verified streaming are all this idea.&lt;/p&gt;
&lt;p&gt;The one rule that separates a correct Merkle tree from a broken one is to domain-separate leaf nodes from internal nodes, so an attacker cannot pass an internal node off as a leaf, exactly the flag-bit discipline BLAKE3 bakes in. The sponge and BLAKE families also power keyed authenticated-encryption modes, which belong to the block-cipher and nonce-reuse parts of this series and are out of scope here.&lt;/p&gt;
&lt;p&gt;We now have functions and modes that keep every clause of the promise. But every one of them still bows to a few hard mathematical ceilings, and to one machine that does not yet exist. How much should you actually worry?&lt;/p&gt;
&lt;h2&gt;10. Theoretical Limits: Bounds, the Random Oracle, and Quantum&lt;/h2&gt;
&lt;p&gt;There is a best-possible hash, and you cannot build it. Understanding exactly why is what separates fear from calibrated caution, especially about quantum computers.&lt;/p&gt;
&lt;p&gt;Start with the ceilings. For an ideal $n$-bit hash, collision resistance cannot exceed about $2^{n/2}$ work and preimage resistance cannot exceed about $2^{n}$. Those are not merely the best known attacks; they are the best any hash of that output length can offer, because the birthday and search arguments apply to a perfect random function. The good news, easy to miss, is that the deployed survivors meet those bounds exactly. The best published attack on full SHA-256, SHA-512, SHA3-256, BLAKE2, and BLAKE3 is the generic one, so there is no gap between what an attacker can do and the theoretical floor.&lt;/p&gt;
&lt;p&gt;When cryptographers talk about a function&apos;s &quot;security margin,&quot; they mean something narrower and attack-specific: the unbroken rounds between the deepest cryptanalysis of a given kind and the full round count. For SHA-256 collisions, published attacks reach 31 of 64 rounds (39 in the weaker semi-free-start setting), leaving a wide margin [@mendel-nad-schlaffer-eurocrypt13, @li-liu-wang-sha2-2024]; for Keccak collisions, only about 5 of 24 rounds fall [@keccak-third-party]. Wide margins are why these functions are trusted, not a gap in their strength.&lt;/p&gt;
&lt;p&gt;The one clause where an ideal is genuinely out of reach is the random oracle. A fixed, finite, publicly specified function cannot actually behave like a fresh random answer for every input, for the plain reason that its code is right there to analyze. Ran Canetti, Oded Goldreich, and Shai Halevi made this precise: there exist signature and encryption schemes provably secure in the random-oracle model for which every concrete instantiation of the oracle by a real hash yields an insecure scheme [@cgh-jacm-2004, @cgh-stoc-1998]. So the random-oracle model is a proof heuristic, not a guarantee.&lt;/p&gt;
&lt;p&gt;The reachable substitute is indifferentiability: the sponge is provably indifferentiable from a random oracle up to its capacity bound, which is the strongest honest claim a real construction can make [@bertoni-sponge-indiff-eurocrypt08]. And a related limit from Section 4 still bites: Antoine Joux&apos;s multicollisions mean concatenating two hashes does not multiply their strength, so there is no cheap &quot;hash twice&quot; route to a stronger oracle [@joux-crypto04].&lt;/p&gt;
&lt;p&gt;Now quantum, where the folklore is loudest and least accurate. Grover&apos;s algorithm gives a square-root speedup for search, so a quantum preimage on an $n$-bit hash costs about $2^{n/2}$ instead of $2^n$ [@grover-1996]. That halves the preimage exponent, which sounds alarming until you plug in numbers: SHA-256 keeps about 128 bits of preimage resistance against an ideal quantum adversary, still far beyond reach.&lt;/p&gt;
&lt;p&gt;Collisions are the subtler story. Brassard, Hoyer, and Tapp gave a quantum collision algorithm at about $2^{n/3}$ queries, but it demands a comparable amount of quantum-accessible memory [@brassard-hoyer-tapp-latin98]. Daniel Bernstein&apos;s cost analysis shows that once you charge honestly for that memory, plain parallel classical collision search at $2^{n/2}$ with cheap hardware wins, so the practical quantum speedup for collisions is marginal [@bernstein-sharcs09]. SHA-256 therefore keeps roughly 128-bit collision resistance in practice, quantum or not.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Classical preimage&lt;/th&gt;
&lt;th&gt;Classical collision&lt;/th&gt;
&lt;th&gt;Quantum preimage (Grover)&lt;/th&gt;
&lt;th&gt;Quantum collision (practical)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;td&gt;$2^{256}$&lt;/td&gt;
&lt;td&gt;$2^{128}$&lt;/td&gt;
&lt;td&gt;$2^{128}$&lt;/td&gt;
&lt;td&gt;about $2^{128}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-384&lt;/td&gt;
&lt;td&gt;$2^{384}$&lt;/td&gt;
&lt;td&gt;$2^{192}$&lt;/td&gt;
&lt;td&gt;$2^{192}$&lt;/td&gt;
&lt;td&gt;about $2^{192}$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-512&lt;/td&gt;
&lt;td&gt;$2^{512}$&lt;/td&gt;
&lt;td&gt;$2^{256}$&lt;/td&gt;
&lt;td&gt;$2^{256}$&lt;/td&gt;
&lt;td&gt;about $2^{256}$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The claim &quot;quantum halves every security level&quot; is right for preimage and wrong for collisions. Halving the collision exponent from $2^{n/2}$ to $2^{n/3}$ would matter only if the required quantum memory were free, and it is not. Charge for it, as Bernstein does, and classical machines are more cost-effective at finding collisions, so the honest planning figure for SHA-256 collision resistance stays near 128 bits [@bernstein-sharcs09].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you want more headroom against a future quantum adversary, you do not need a different construction. You need more output bits. That is the entire reason NIST keeps SHA-384, SHA-512, and the 256-bit-security XOF SHAKE256 in the toolbox: they push both preimage and collision margins up by widening the digest. KT256, defined in the Informational IRTF RFC 9861 rather than a FIPS standard, offers the same raised margin outside the FIPS boundary [@rfc-9861]. The migration story for the rest of cryptography, where the fix really is new algorithms, is covered separately in &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;the post-quantum migration guide&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two frontiers sit just outside this guide&apos;s scope but deserve a pointer, because they are where hashes are heading. The first is arithmetization-oriented hashes such as Poseidon [@poseidon-usenix-sec21] and the Rescue and Vision family [@rescue-tosc-2020], designed to minimize arithmetic-circuit cost inside zero-knowledge proof systems rather than CPU cycles. They are younger, with far less mature cryptanalysis than SHA-2 or SHA-3, so they are a higher-risk choice outside their niche.&lt;/p&gt;
&lt;p&gt;The second runs the other direction: hash-based signatures such as LMS [@rfc-8554], XMSS [@rfc-8391], and the standardized stateless SLH-DSA [@fips-205] build post-quantum signatures purely from hash calls, so they consume this primitive rather than being one. Both belong to the post-quantum posts in this series, not here.&lt;/p&gt;
&lt;p&gt;So the mathematics delivers a calm verdict. The survivors are as strong as any hash of their output length can be, and quantum computers sharpen the analysis rather than overturning it. That means the last question is not &quot;which hash is strongest,&quot; because among the survivors none is meaningfully stronger than another. The question is which function, in which mode, for your specific job.&lt;/p&gt;
&lt;h2&gt;11. The Practical Guide: Choose X with These Params in Case Y&lt;/h2&gt;
&lt;p&gt;Everything so far collapses into one habit. Name the clause your protocol actually leans on, then pick the function and mode that keeps it. Here is that decision on a single page.&lt;/p&gt;

flowchart TD
    START[&quot;What is the job?&quot;] --&amp;gt; Q0{&quot;Passwords or low-entropy secrets?&quot;}
    Q0 --&amp;gt;|Yes| ARGON[&quot;Argon2id, or scrypt, bcrypt, PBKDF2&quot;]
    Q0 --&amp;gt;|No| Q1{&quot;Keyed authentication?&quot;}
    Q1 --&amp;gt;|Yes| HMAC[&quot;HMAC-SHA-256, or KMAC for the sponge&quot;]
    Q1 --&amp;gt;|No| Q2{&quot;FIPS approval required?&quot;}
    Q2 --&amp;gt;|No| BLAKE[&quot;BLAKE3 for throughput, streaming, and KDF&quot;]
    Q2 --&amp;gt;|Yes| Q3{&quot;Length-extension safety needed?&quot;}
    Q3 --&amp;gt;|Yes| TRUNC[&quot;SHA-512/256, or SHA-3 and SHAKE&quot;]
    Q3 --&amp;gt;|No| SHA256[&quot;SHA-256, the default&quot;]
&lt;p&gt;The head-to-head matrix fills in the details behind those leaves. Speeds are single-thread cycles per byte on a 2023 AMD Zen 4, from the eBASH measurements [@ebash-zen4]; every full function listed is unbroken, so the choice is driven by length-extension behavior, speed, FIPS status, and parallelism, not by any strength gap.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Construction&lt;/th&gt;
&lt;th&gt;Output / security&lt;/th&gt;
&lt;th&gt;LE-safe&lt;/th&gt;
&lt;th&gt;Speed (cyc/B)&lt;/th&gt;
&lt;th&gt;FIPS&lt;/th&gt;
&lt;th&gt;Parallel&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;SHA-256&lt;/td&gt;
&lt;td&gt;Merkle-Damgard&lt;/td&gt;
&lt;td&gt;256 / 128 coll&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;2.03&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Default integrity, signatures, interop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-512&lt;/td&gt;
&lt;td&gt;Merkle-Damgard&lt;/td&gt;
&lt;td&gt;512 / 256 coll&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;4.19&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;High margin on 64-bit software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-512/256&lt;/td&gt;
&lt;td&gt;Merkle-Damgard (trunc)&lt;/td&gt;
&lt;td&gt;256 / 128 coll&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;about 4.2&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;LE-immunity plus FIPS in software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA3-256&lt;/td&gt;
&lt;td&gt;Sponge&lt;/td&gt;
&lt;td&gt;256 / 128 coll&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;6.81&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Construction diversity, LE-immune&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHAKE128&lt;/td&gt;
&lt;td&gt;Sponge XOF&lt;/td&gt;
&lt;td&gt;any / 128 ceiling&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;5.43&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Variable-length output, PQC expansion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BLAKE2b&lt;/td&gt;
&lt;td&gt;ARX (HAIFA)&lt;/td&gt;
&lt;td&gt;512 / 256 coll&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;3.54&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Tree mode&lt;/td&gt;
&lt;td&gt;Fast keyed software hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BLAKE3&lt;/td&gt;
&lt;td&gt;ARX Merkle tree&lt;/td&gt;
&lt;td&gt;256, XOF / 128&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High-throughput integrity, KDF, streaming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KT128&lt;/td&gt;
&lt;td&gt;Sponge tree&lt;/td&gt;
&lt;td&gt;any / 128&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;about 3.5 (0.51 with AVX-512)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;High throughput on the Keccak basis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMAC-SHA-256&lt;/td&gt;
&lt;td&gt;Nested keyed MD&lt;/td&gt;
&lt;td&gt;MAC / PRF&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;about 2x SHA-256&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Keyed authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KMAC256&lt;/td&gt;
&lt;td&gt;Keyed sponge&lt;/td&gt;
&lt;td&gt;any / 256&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;about SHAKE256&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Keyed plus domain-separated auth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;And the same advice as a quick &quot;use / never&quot; card, the version you tape above your desk.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use this&lt;/th&gt;
&lt;th&gt;For this job&lt;/th&gt;
&lt;th&gt;Never use here&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;SHA-256&lt;/td&gt;
&lt;td&gt;Default integrity, signatures, interop&lt;/td&gt;
&lt;td&gt;MD5, SHA-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA-512/256&lt;/td&gt;
&lt;td&gt;Length-extension immunity plus FIPS in software&lt;/td&gt;
&lt;td&gt;full-width &lt;code&gt;H(secret||msg)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHA3-256 or SHAKE256&lt;/td&gt;
&lt;td&gt;Construction diversity, XOF, PQC seed expansion&lt;/td&gt;
&lt;td&gt;naive truncation of a fixed hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BLAKE3&lt;/td&gt;
&lt;td&gt;High-throughput, streaming, KDF via &lt;code&gt;derive_key&lt;/code&gt; (non-FIPS)&lt;/td&gt;
&lt;td&gt;passwords&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMAC-SHA-256 or KMAC&lt;/td&gt;
&lt;td&gt;Keyed authentication&lt;/td&gt;
&lt;td&gt;&lt;code&gt;H(key||msg)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Argon2id&lt;/td&gt;
&lt;td&gt;Passwords, low-entropy secrets&lt;/td&gt;
&lt;td&gt;any bare fast hash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD5, CRC&lt;/td&gt;
&lt;td&gt;Non-security checksums only&lt;/td&gt;
&lt;td&gt;signatures, certs, deduplication&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;A few sizing rules turn those tables into correct code. For a target security level of $s$ bits against collisions, make the digest at least $2s$ bits, because collision resistance is only half the output length; 128-bit collision security therefore needs a 256-bit hash.&lt;/p&gt;
&lt;p&gt;When you need a non-standard output length, request it from an XOF (SHAKE, KMAC, or a BLAKE3 XOF) rather than truncating a fixed digest by hand, and remember that reading more XOF bytes never raises you above the capacity ceiling. Add margin, SHA-384, SHA-512, SHAKE256, or KT256, when you want a quantum hedge, per Section 10. And keep the deprecation discipline precise: MD5 and CRC are fine as non-security checksums against accidental corruption, and nowhere an adversary can influence the input.&lt;/p&gt;

On Windows the same primitives appear under specific names. The CNG layer exposes hash and MAC providers through BCrypt, detailed in [the CNG architecture guide](/blog/cng-architecture-bcrypt-ncrypt-ksps); Authenticode computes a hash of a portable-executable file (skipping the fields that signing itself modifies) and signs that digest, which is the exact construction Flame subverted, covered in [the Authenticode and catalog-files guide](/blog/authenticode-and-catalog-files-the-crypto-foundation-under-w); and SChannel, the Windows TLS stack, hashes handshake transcripts and has spent two decades retiring SHA-1 across cipher suites, told in [the SChannel algorithm-agility story](/blog/rotating-every-cipher-schannel-and-the-twenty-year-algorithm). The algorithm choices in this guide map onto those APIs directly.
&lt;p&gt;One operational detail sinks more code than any exotic attack: comparing digests or tags with an ordinary equality check. A loop that returns the moment two bytes differ leaks, through its timing, how many leading bytes matched, which is enough for an attacker to recover a secret tag byte by byte. The fix is a constant-time comparison that always scans the whole buffer. This adjacency between hashing and timing side channels is the same family of bug explored in &lt;a href=&quot;https://paragmali.com/blog/they-read-your-plaintext-without-breaking-your-cipher-a-fiel&quot; rel=&quot;noopener&quot;&gt;the padding-oracle field guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;{`
// Comparing a secret tag with early-exit leaks how many bytes matched.
function naiveEqual(a, b) {
  if (a.length !== b.length) return false;
  for (let i = 0; i &amp;lt; a.length; i++) {
    if (a[i] !== b[i]) return false;      // returns as soon as bytes differ
  }
  return true;
}&lt;/p&gt;
&lt;p&gt;// Constant-time: always scan the whole array, OR the differences together.
function constantTimeEqual(a, b) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i &amp;lt; a.length; i++) {
    diff |= a[i] ^ b[i];                   // no branch on the secret bytes
  }
  return diff === 0;
}&lt;/p&gt;
&lt;p&gt;const tag   = [0x9f, 0x1c, 0x00, 0x42, 0xab];
const guess = [0x9f, 0x1c, 0xff, 0x00, 0x00];
console.log(&quot;naive:&quot;, naiveEqual(tag, guess));
console.log(&quot;constant-time:&quot;, constantTimeEqual(tag, guess));
console.log(&quot;In production call a vetted primitive: crypto.timingSafeEqual, CRYPTO_memcmp, or sodium_memcmp.&quot;);
`}&lt;/p&gt;
&lt;p&gt;Two rules cover most real disasters, and they have nothing to do with picking the &quot;strongest&quot; hash. Never use a bare fast hash for passwords, and never use a bare hash where you meant HMAC. The rest of choosing is about axes that are not strength at all: length-extension behavior, speed, FIPS status, parallelism, and construction diversity. What remains is to clear up the myths that cause the disasters those two rules do not.&lt;/p&gt;
&lt;h2&gt;12. Misconceptions and Real-Code Smells&lt;/h2&gt;
&lt;p&gt;The field&apos;s folklore is mostly half-right, which is worse than fully wrong, because half-right beliefs survive code review. Here are the questions that actually decide those reviews.&lt;/p&gt;


No, and this is the single most common misconception in the subject. FIPS 202 says its SHA-3 functions supplement the SHA-1 and SHA-2 families rather than replacing them [@fips-202], and NIST&apos;s Policy on Hash Functions states plainly that there is &quot;no need to transition applications from SHA-2 to SHA-3&quot; [@nist-policy-hash-functions]. SHA-2 and SHA-3 coexist deliberately, on a diversification axis, so the internet&apos;s trust machinery does not rest on one construction. The supersession ladder ran MD5 to SHA-1 to SHA-2 and ended there.


Yes. Full-width SHA-256 and SHA-512 are length-extension-vulnerable, because their digest is the whole final chaining state. So is SHA-224, despite being truncated: it hides only 32 bits of state, which an attacker recovers in about $2^{32}$ work before extending normally [@rfc-3874]. The genuinely immune options are SHA-384, SHA-512/224, SHA-512/256, the SHA-3 family and SHAKE, HMAC, KMAC, and the BLAKE hashes. If you need `H(secret || message)` behavior, use HMAC or one of those instead.


No. Every collision attack on SHA-1 and MD5 attacks collision resistance, and HMAC does not rely on collision resistance. RFC 2104 states directly that MD5&apos;s weaknesses do not compromise its use within HMAC [@rfc-2104]. HMAC-SHA-1 and HMAC-MD5 remain unbroken, though new systems should still prefer HMAC-SHA-256 for margin.


Only as a non-security checksum guarding against accidental corruption, where no adversary can influence the input. The moment an attacker can choose inputs, MD5 collisions become forged certificates, as the 2008 rogue certificate authority and 2012 Flame both showed [@rogue-ca, @stevens-counter-cryptanalysis-crypto13]. Never use MD5 for signatures, certificates, or deduplication of adversarial data.


No. Grover&apos;s algorithm halves the preimage exponent, leaving SHA-256 with about 128-bit preimage resistance, still infeasible [@grover-1996]. For collisions the quantum speedup is marginal once you charge for quantum memory, so SHA-256 keeps roughly 128-bit collision resistance in practice [@bernstein-sharcs09]. The hedge, if you want more margin, is a longer output such as SHA-384 or SHA-512, not a new construction.


SHA-256 for general integrity, signatures, and interoperability, especially where FIPS validation matters [@fips-180-4]. BLAKE3 when you want maximum throughput, streaming, or a KDF and do not need FIPS [@blake3-repo]. SHA-512/256 when you want length-extension immunity plus FIPS on 64-bit software.


No. Both are built to be fast, which is exactly what you do not want for passwords, where speed helps the attacker guess. Use a purpose-built slow function: Argon2id for new systems, or scrypt, bcrypt, or PBKDF2. BLAKE3&apos;s own README says the same and points to Argon2 [@blake3-repo].

&lt;p&gt;The misuse catalog is the FAQ&apos;s mirror image: the same mistakes, seen from the code side.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;H(secret || message)&lt;/code&gt; pressed into service as a MAC (use HMAC or KMAC). Passwords fed to a bare or unsalted fast hash (use Argon2id). Truncating a fixed digest to an odd length instead of asking an XOF (use SHAKE or a BLAKE3 XOF). Comparing tags with &lt;code&gt;==&lt;/code&gt; or an early-exit loop instead of a constant-time check.These last two, ad hoc truncation and non-constant-time comparison, are the ones that pass functional tests perfectly and fail only under an adversary, which is why they survive so long in production. Rolling your own Merkle tree without domain-separating leaf and internal nodes. Treating a hash as if it provided encryption. And assuming a real hash is a perfect random oracle rather than an approximation of one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Return, finally, to the two files from the opening, the ones that shared the SHA-1 digest &lt;code&gt;38762cf7f55934b34d179ae6a4c80cadccbb7f0a&lt;/code&gt; [@shattered]. You can now read that string precisely. It is not evidence that hashing is broken, or that longer is always safer, or that SHA-3 rode in to replace the fallen. It is a single clause, collision resistance, failing on a single aging construction, and being weaponized because signatures sign the digest, not the file. The answer the field actually gave was not one replacement but two moves at once: widen the margins of the survivor, SHA-2, and diversify into unrelated constructions, the SHA-3 sponge and the BLAKE3 tree, so no future break can take everything down together. A cryptographic hash makes one promise. Mastery is knowing exactly which clause of that promise your protocol leans on, and choosing the function and the mode that keeps it.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;cryptographic-hash-functions-field-guide&quot; keyTerms={[
  { term: &quot;Collision resistance&quot;, definition: &quot;Infeasibility of finding any two distinct inputs with the same digest; generic cost about 2^(n/2) by the birthday bound.&quot; },
  { term: &quot;Length extension&quot;, definition: &quot;Computing the hash of m followed by padding and a chosen suffix from the digest of m and its length, without knowing m; afflicts full-width Merkle-Damgard hashes and SHA-224.&quot; },
  { term: &quot;Merkle-Damgard&quot;, definition: &quot;Iterated hash that folds padded message blocks through a compression function; the digest is the final chaining state.&quot; },
  { term: &quot;Sponge&quot;, definition: &quot;The Keccak and SHA-3 construction with a hidden capacity, which makes it length-extension-immune and a native extendable-output function.&quot; },
  { term: &quot;XOF&quot;, definition: &quot;Extendable-output function that emits output of any requested length, such as SHAKE128 and SHAKE256.&quot; },
  { term: &quot;HMAC&quot;, definition: &quot;Nested keyed message authentication code that authenticates messages without relying on the hash&apos;s collision resistance.&quot; }
]} questions={[
  { q: &quot;Why does a 256-bit hash provide only 128-bit collision resistance?&quot;, a: &quot;By the birthday bound, a repeat appears after only about 2^(n/2) draws in a space of 2^n values.&quot; },
  { q: &quot;Did SHA-3 replace SHA-2?&quot;, a: &quot;No. FIPS 202 states SHA-3 supplements the SHA-2 family, and NIST&apos;s policy says there is no need to transition; they coexist for construction diversity.&quot; },
  { q: &quot;Which SHA-2 variants resist length extension, and why?&quot;, a: &quot;SHA-384, SHA-512/224, and SHA-512/256, because they hide at least 128 bits of the final state; SHA-224 does not, hiding only 32 bits.&quot; },
  { q: &quot;Is HMAC-SHA-1 broken by the SHA-1 collision attacks?&quot;, a: &quot;No. HMAC does not depend on collision resistance, so HMAC-SHA-1 and HMAC-MD5 remain unbroken.&quot; },
  { q: &quot;What is the correct way to hash a password?&quot;, a: &quot;A purpose-built slow function such as Argon2id, scrypt, bcrypt, or PBKDF2, never a bare fast hash.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>cryptography</category><category>hash-functions</category><category>sha-2</category><category>sha-3</category><category>blake3</category><category>collision-resistance</category><category>length-extension</category><category>applied-cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>No Room for a Nonce: Disk and Length-Preserving Encryption, from XTS to FPE</title><link>https://paragmali.com/blog/no-room-for-a-nonce-disk-and-length-preserving-encryption-fr/</link><guid isPermaLink="true">https://paragmali.com/blog/no-room-for-a-nonce-disk-and-length-preserving-encryption-fr/</guid><description>Why disk and format-preserving ciphers are deterministic, unauthenticated permutations, and how block width and domain size decide XTS, Adiantum, HCTR2, or FF1.</description><pubDate>Fri, 10 Jul 2026 11:17:52 GMT</pubDate><content:encoded>
When ciphertext must occupy exactly the plaintext&apos;s space -- a 512-byte disk sector, a 16-digit card number -- there is no room for a fresh nonce and no room for an authentication tag, so encryption collapses back to a deterministic, unauthenticated keyed permutation whose only achievable goal is to look like a random permutation. Its quality is set by two knobs: **block width** (how far one changed bit spreads -- narrow AES-XTS versus wide Adiantum and HCTR2) and **domain size** (how many values it hides among -- huge for a disk, fatally small for format-preserving encryption). Every real-world break sorts onto one knob: XTS malleability and replay come from too-narrow width; the FF3 and FF3-1 message-recovery attacks come from too-small a domain, where recovering the whole codebook costs work merely *polynomial* in the domain rather than the 2^128 the cipher seems to promise. And because a length-preserving cipher can never reject a forgery, integrity must always come from another layer -- dm-integrity, dm-verity, fscrypt&apos;s per-file nonce, or an AEAD wherever you are actually allowed to spend the extra bytes.
&lt;h2&gt;1. Two Failures, One Cause&lt;/h2&gt;
&lt;p&gt;A security team did everything the checklist demanded. They encrypted a server&apos;s disk with AES-XTS, the mode NIST approved for storage devices [@nist-sp800-38e], and confirmed the volume was unreadable without the key. Months later an attacker with write access to that volume rolled the machine back to a vulnerable kernel, flipped a policy byte, and corrupted a specific database page -- never learning a single byte of plaintext, never touching the key, and never tripping an alert. The disk was still, technically, encrypted the entire time.&lt;/p&gt;
&lt;p&gt;Two floors down, a different team faced a legacy analytics pipeline that would only accept six-digit codes -- a short, PIN-length field of exactly the kind format-preserving encryption was built to protect. They enciphered each real code into a different but still-valid six-digit code and moved on.&lt;/p&gt;
&lt;p&gt;An analyst with query access to the encryptor then rebuilt the &lt;em&gt;entire&lt;/em&gt; mapping from fewer query pairs than there are codes -- a few hundred thousand, an afternoon&apos;s compute -- and read every value behind it [@durak-vaudenay2017][@hoang-miller-trieu2019]. That is the same weakness that broke the FF3 standard outright, and Section 8 will put exact numbers on it.&lt;/p&gt;
&lt;p&gt;Two very different systems, two very different failures. They share one root cause, and naming it is the whole point of this article.&lt;/p&gt;

Encryption whose ciphertext occupies exactly the same space as its plaintext, leaving no room for a fresh nonce and no room for an appended authentication tag.
&lt;p&gt;Both teams were forced into length preservation. A disk sector is a fixed 512 or 4096 bytes with no spare room; a code field is exactly six digits wide.&lt;/p&gt;
&lt;p&gt;When you cannot add even one byte, two tools that every modern cipher leans on vanish at once. You lose the nonce, the fresh random input that makes encrypting the same message twice produce different ciphertext. And you lose the tag, the short checksum an authenticated cipher appends so the receiver can reject anything tampered with. Remove both, and encryption collapses back to its oldest form: a single keyed permutation on a fixed set of values.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Length preservation forces two properties at once. The cipher is &lt;strong&gt;deterministic&lt;/strong&gt; -- identical plaintext at the identical position under the identical key always yields identical ciphertext -- so the best security you can even ask for is that it look like a random permutation, never the semantic secrecy of a randomized cipher. And it is &lt;strong&gt;unauthenticated&lt;/strong&gt; -- with no tag there is nothing to reject, so it is malleable by construction.&lt;/p&gt;
&lt;/blockquote&gt;

The property that an attacker can transform a ciphertext into another valid ciphertext, changing the decrypted plaintext in a related or randomized way, with nothing in the cipher able to detect that the change happened.
&lt;p&gt;That is why the first team&apos;s disk could be rewritten by someone who could not read it. There was no tag to say &quot;no.&quot; And it hints at why the second team&apos;s codes fell: a permutation is only as good as the space it hides in is large. Everything that follows lives on exactly two knobs.&lt;/p&gt;

flowchart TD
    Root[&quot;Every length-preserving cipher is one point in width by domain space&quot;]
    Root --&amp;gt; Disk[&quot;Large domain: a whole disk or volume&quot;]
    Root --&amp;gt; Field[&quot;Small domain: a fixed-format field&quot;]
    Disk --&amp;gt; XTS[&quot;Narrow width, AES-XTS, 16-byte diffusion&quot;]
    Disk --&amp;gt; Wide[&quot;Wide width, Adiantum and HCTR2, whole-unit diffusion&quot;]
    Field --&amp;gt; FPE[&quot;Format-preserving, FF1 and FF3-1, domain is the ceiling&quot;]
&lt;p&gt;&lt;strong&gt;Block width&lt;/strong&gt; is how far a single changed input bit diffuses through the output. &lt;strong&gt;Domain size&lt;/strong&gt; is how many possible values the permutation is defined over. Narrow width and a large domain give you AES-XTS; wide width and a large domain give you Adiantum and HCTR2; a deliberately tiny domain, at any width, gives you format-preserving encryption.&lt;/p&gt;
&lt;p&gt;Format-preserving encryption was designed for exactly the small field the second team reached for -- a PIN, a short code, the six middle digits of a card number a processor still has to parse -- which is why that column fell so cheaply. A full sixteen-digit card number, it turns out, is far &lt;em&gt;more&lt;/em&gt; expensive to recover, because the cost scales with the size of the domain, not with the format; Section 8 quantifies both.&lt;/p&gt;
&lt;p&gt;This is Part 9 of the series. It assumes you take AES as a pseudorandom permutation for granted, that you met &lt;a href=&quot;https://paragmali.com/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/&quot; rel=&quot;noopener&quot;&gt;authenticated encryption in Part 7&lt;/a&gt;, and that you have the security definitions from &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;Part 1&lt;/a&gt;. To see why the whole field reduces to these two knobs, start where both problems were born: in a disk sector and a database column that both refused to grow.&lt;/p&gt;
&lt;h2&gt;2. Two Problems With the Same Shape&lt;/h2&gt;
&lt;p&gt;Rewind to the turn of the millennium. Two engineers, in two industries that never spoke to each other, hit the same wall.&lt;/p&gt;
&lt;p&gt;The first was a storage engineer. A disk is an array of fixed sectors, and the encryption layer sits underneath the filesystem, transparent to it. Sector number 8,675,309 must encrypt to exactly 512 bytes and decrypt back in place, with no per-sector header to stash an initialization vector. So where does the IV go?&lt;/p&gt;
&lt;p&gt;The naive answer, Electronic Codebook mode, encrypts each 16-byte block independently under one key. It needs no IV at all -- and it leaks catastrophically. Identical plaintext blocks produce identical ciphertext blocks everywhere on the disk, so the structure of files, repeated records, and zeroed regions survives encryption in plain sight.This is the famous &quot;ECB penguin&quot;: encrypt a bitmap of the Linux mascot Tux in ECB mode and the penguin is still perfectly visible in the ciphertext, because the cipher preserves every repeated block [@wikipedia-ecb-penguin].&lt;/p&gt;
&lt;p&gt;Cipher Block Chaining with a per-sector IV helps, but if that IV is predictable an attacker can mount watermarking attacks -- planting files whose ciphertext betrays their presence -- and CBC on a disk stays malleable end to end. Clemens Fruhwirth&apos;s 2005 survey of hard-disk encryption catalogs exactly these failure modes [@fruhwirth2005].&lt;/p&gt;
&lt;p&gt;The second engineer worked in payments. A legacy mainframe validates that a primary account number is sixteen digits and passes a checksum; it will reject anything else. You must encrypt that field so downstream systems still see a well-formed account number. Practitioners called this &quot;datatype-preserving encryption&quot; and traded folklore recipes through the 1990s.&lt;/p&gt;
&lt;p&gt;In 2002 John Black and Phillip Rogaway gave the problem a real security model in a paper titled &lt;em&gt;Ciphers with Arbitrary Finite Domains&lt;/em&gt; [@black-rogaway2002]. They contributed two reusable techniques that every later scheme inherits: &lt;strong&gt;cycle-walking&lt;/strong&gt; (encipher a value under a larger cipher and re-encrypt until the result lands back in the target set) and a &lt;strong&gt;Feistel network over a radix&lt;/strong&gt; (build a permutation on the integers modulo N from keyed round functions).&lt;/p&gt;

A keyed permutation that no efficient adversary can distinguish from a truly random permutation of the same set. It is a *strong* PRP if that indistinguishability survives even when the adversary may also query the inverse -- the decryption -- direction, not just encryption.
&lt;p&gt;Here is the observation that unifies the two stories. When you cannot spend a single byte, you are not choosing between encryption modes; you are choosing a &lt;strong&gt;permutation on exactly this space&lt;/strong&gt;. And the theory for that had already been written.&lt;/p&gt;
&lt;p&gt;In 1999 Moni Naor and Omer Reingold, revisiting the classic Luby-Rackoff construction, named the right target -- a strong pseudorandom permutation -- and showed that a &lt;em&gt;wide&lt;/em&gt; one, enciphering a whole block at once, could be built cheaply from simpler parts [@naor-reingold1999]. That result sat mostly unused for two decades. Both the disk world and the payments world were, without knowing it, asking Naor and Reingold&apos;s question.&lt;/p&gt;

A cipher whose output has the same format as its input, so a sixteen-digit account number enciphers to another sixteen-digit account number, and a value drawn from a set of size N enciphers to another value in that same set.

timeline
    title The two rails of length-preserving encryption, 1999 to 2025
    1999 : Naor-Reingold, a wide strong PRP can be built cheaply
    2002 : Black-Rogaway formalize FPE : LRW names the tweak
    2004 : XEX fixes the tweak for disks
    2007 : XTS-AES standardized for storage
    2016 : NIST approves FF1 and FF3
    2017 : FF3 broken by Durak-Vaudenay
    2018 : Adiantum published, wide-block from hash-encrypt-hash
    2021 : HCTR2 published : FF3-1 broken by Beyne
    2025 : NIST removes FF3 and FF3-1
&lt;p&gt;The nonce that length preservation takes away is the subject of &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;Part 2&lt;/a&gt;; here it is simply gone. Both camps reached for the same fix at almost the same moment -- a &lt;em&gt;tweak&lt;/em&gt;, a public per-position input that re-keys the permutation cheaply for each sector or each field. It worked. Then it failed, in a way that taught the field its first hard lesson about where key material is allowed to appear.&lt;/p&gt;
&lt;h2&gt;3. The First Answers, and Why They Broke&lt;/h2&gt;
&lt;p&gt;The first generation of real answers shared a blind spot: they confused &lt;em&gt;diffusion&lt;/em&gt; with &lt;em&gt;security&lt;/em&gt;, and &lt;em&gt;format&lt;/em&gt; with &lt;em&gt;strength&lt;/em&gt;. Three named near-misses show the confusion, and each taught a lesson the survivors still carry.&lt;/p&gt;
&lt;h3&gt;ESSIV: a real fix that fixed the wrong thing&lt;/h3&gt;
&lt;p&gt;Fruhwirth&apos;s ESSIV -- Encrypted Salt-Sector IV -- attacks the predictable-IV problem head on. Instead of deriving the CBC initialization vector from the visible sector number, it computes $\mathrm{IV} = E_{\mathrm{salt}}(\text{sector})$ with $\mathrm{salt} = H(K)$, so the IV is a secret function of the key and an attacker can no longer predict it [@fruhwirth2005]. Watermarking dies.&lt;/p&gt;
&lt;p&gt;But ESSIV is a fix to &lt;em&gt;confidentiality leakage&lt;/em&gt;, not to tampering: the underlying CBC is still malleable, and its diffusion runs only forward, so a flipped ciphertext bit corrupts one block and predictably flips the corresponding plaintext bit in the next. ESSIV made the disk harder to read. It did nothing to make it harder to &lt;em&gt;rewrite&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;The Elephant diffuser: a diffuser is not a MAC&lt;/h3&gt;
&lt;p&gt;Microsoft&apos;s first BitLocker, shipped in Windows Vista, saw the malleability problem and answered it with engineering muscle. Niels Ferguson&apos;s 2006 design bolts an unkeyed, invertible &lt;em&gt;diffuser&lt;/em&gt; -- nicknamed Elephant -- onto AES-CBC, explicitly &quot;to improve security against manipulation attacks&quot; [@ferguson2006]. The diffuser spreads any single changed bit across the entire sector, so tampering yields sector-wide noise instead of a clean bit-flip. That feels like integrity. It is not.&lt;/p&gt;

An unkeyed diffuser randomizes the plaintext when the ciphertext is altered, but it authenticates nothing: every altered sector still decrypts to *some* 512 bytes, and there is no tag to reject them. It converts an attacker&apos;s chosen edit into unpredictable garbage -- a real downgrade in attacker control -- but it can never say &quot;this sector was tampered with.&quot; Microsoft eventually accepted the verdict and moved BitLocker to XTS-AES, retiring the Elephant diffuser entirely [@microsoft-bitlocker]. The deployment history is told in full in the [BitLocker architecture post](/blog/bitlocker-on-windows-architecture-attacks-and-the-limits-of-/).
&lt;h3&gt;LRW: a cipher defeated by encrypting its own key&lt;/h3&gt;
&lt;p&gt;The line that actually led somewhere abandoned chaining for a &lt;em&gt;tweak&lt;/em&gt;. In 2002 Moses Liskov, Ronald Rivest, and David Wagner formalized the idea [@liskov-rivest-wagner2002].&lt;/p&gt;

A block cipher with an extra public input, the *tweak*, that cheaply selects a different permutation for each value of the tweak. On a disk the tweak is the position, so every sector or block is enciphered under its own effectively independent permutation without rekeying.
&lt;p&gt;Their LRW mode computes $C = E_{K_1}(P \oplus \Delta) \oplus \Delta$ with the mask $\Delta = K_2 \otimes i$, where $K_2$ is a second key, $i$ is the block index, and $\otimes$ is multiplication in the finite field $\mathrm{GF}(2^{128})$. Each block gets its own location-bound permutation, fully parallel and random-access. It was the right shape -- and it carried a fatal detail.&lt;/p&gt;
&lt;p&gt;The mask is built from &lt;em&gt;raw key material&lt;/em&gt;. On a disk that is not hypothetical: a full-disk-encryption key can be paged or hibernated onto the very disk it protects, so a plaintext block can equal the tweak key $K_2$ itself. And because the mask is a linear function of $K_2$, a handful of such known blocks are enough to recover it.&lt;/p&gt;

It is an unusually instructive failure: the defect is structural, not a tuning mistake. No larger key and no extra round removes it, because the weakness lives in *where the mask comes from* rather than in how strong the underlying cipher is. A construction that a swap file can quietly undo cannot anchor full-disk encryption, so LRW was superseded rather than patched.
&lt;h3&gt;FPE&apos;s first generation: format is not strength&lt;/h3&gt;
&lt;p&gt;On the payments rail, Black and Rogaway&apos;s cycle-walking and Feistel-over-radix were the first rigorous constructions [@black-rogaway2002]. They are elegant and correct -- and they quietly encode the domain problem that will dominate the rest of this article.&lt;/p&gt;

Encipher a value from a small set by encrypting it under a larger cipher and re-encrypting the result until it lands back inside the target set. Each re-encryption is a &quot;walk,&quot; and because the larger cipher is a permutation, the composed map is a permutation on the small set.
&lt;p&gt;The demo below enciphers four-digit values into four-digit values by cycle-walking a toy 16-bit permutation down into the range 0 to 9,999. Notice two things: the output is always a valid four-digit number, and the same input always maps to the same output. Format is preserved, determinism is total -- and the whole security rests on how large that range is.&lt;/p&gt;
&lt;p&gt;{`
// Enciphering 4-digit values using a bigger permutation, then walking back into range.
// The toy 16-bit map is a Feistel network, which is a bijection for ANY round function.
function roundF(half, k) {
  return ((half * 167) ^ (k * 91) ^ ((half &amp;lt;&amp;lt; 3) &amp;amp; 0xFF)) &amp;amp; 0xFF;
}
function feistel16(x, keys) {
  let L = (x &amp;gt;&amp;gt; 8) &amp;amp; 0xFF, R = x &amp;amp; 0xFF;
  for (let r = 0; r &amp;lt; 4; r++) {
    const nL = R, nR = L ^ roundF(R, keys[r]);
    L = nL; R = nR;
  }
  return ((L &amp;lt;&amp;lt; 8) | R) &amp;amp; 0xFFFF;
}
const KEYS = [23, 57, 131, 200];
const N = 10000; // the 4-digit domain: values 0000..9999&lt;/p&gt;
&lt;p&gt;function fpeEncrypt(x) {
  let y = feistel16(x, KEYS), walks = 0;
  while (y &amp;gt;= N) { y = feistel16(y, KEYS); walks++; }
  return { y: y, walks: walks };
}&lt;/p&gt;
&lt;p&gt;for (const v of [1, 42, 1234, 9999]) {
  const r = fpeEncrypt(v);
  console.log(String(v).padStart(4,&apos;0&apos;) + &apos; -&amp;gt; &apos; + String(r.y).padStart(4,&apos;0&apos;) + &apos;   (walks: &apos; + r.walks + &apos;)&apos;);
}
console.log(&apos;Deterministic: encrypting 1234 twice gives &apos; + fpeEncrypt(1234).y + &apos; both times.&apos;);
`}&lt;/p&gt;
&lt;p&gt;LRW&apos;s lesson was precise: never let key material into the mask; derive the offset from the cipher itself. That one idea, written $\Delta = E_K(i) \cdot \alpha^j$, is the hinge on which the entire disk rail now turns.&lt;/p&gt;
&lt;h2&gt;4. The Climb, Generation by Generation&lt;/h2&gt;
&lt;p&gt;Now the climb begins. Two rails, and every generation is motivated by a concrete, named failure of the one before it.&lt;/p&gt;

flowchart TD
    subgraph Disk[&quot;Disk rail: the block-width climb&quot;]
        ECB[&quot;ECB, no diffusion&quot;] --&amp;gt;|&quot;leaks patterns&quot;| CBC[&quot;CBC with ESSIV&quot;]
        CBC --&amp;gt;|&quot;still malleable&quot;| LRW[&quot;LRW tweak&quot;]
        LRW --&amp;gt;|&quot;key material leaks&quot;| XEX[&quot;XEX offsets&quot;]
        XEX --&amp;gt;|&quot;two keys and stealing&quot;| XTS[&quot;XTS-AES&quot;]
        XTS --&amp;gt;|&quot;still too narrow&quot;| WIDE[&quot;Adiantum and HCTR2&quot;]
    end
    subgraph FPErail[&quot;Format-preserving rail: the domain-size limit&quot;]
        BR[&quot;Black-Rogaway model&quot;] --&amp;gt;|&quot;parameterize&quot;| FFX[&quot;FFX and BPS&quot;]
        FFX --&amp;gt;|&quot;standardize&quot;| FF[&quot;FF1 and FF3&quot;]
        FF --&amp;gt;|&quot;FF3 broken 2017&quot;| FF31[&quot;FF3-1&quot;]
        FF31 --&amp;gt;|&quot;broken again 2021&quot;| REMOVED[&quot;Removed by NIST 2025&quot;]
    end
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Key idea&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;ECB per sector&lt;/td&gt;
&lt;td&gt;~1998&lt;/td&gt;
&lt;td&gt;no IV, blocks enciphered independently&lt;/td&gt;
&lt;td&gt;broken, leaks patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBC with ESSIV&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;td&gt;secret sector IV kills watermarking&lt;/td&gt;
&lt;td&gt;superseded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elephant diffuser&lt;/td&gt;
&lt;td&gt;2006&lt;/td&gt;
&lt;td&gt;unkeyed whole-sector diffusion on CBC&lt;/td&gt;
&lt;td&gt;removed for XTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LRW&lt;/td&gt;
&lt;td&gt;2002&lt;/td&gt;
&lt;td&gt;tweak offset from a second key&lt;/td&gt;
&lt;td&gt;superseded, key leak&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XEX&lt;/td&gt;
&lt;td&gt;2004&lt;/td&gt;
&lt;td&gt;offset from the cipher, powers of alpha&lt;/td&gt;
&lt;td&gt;superseded by XTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XTS-AES&lt;/td&gt;
&lt;td&gt;2007&lt;/td&gt;
&lt;td&gt;XEX plus two keys plus ciphertext stealing&lt;/td&gt;
&lt;td&gt;the disk default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CMC / EME / EME2&lt;/td&gt;
&lt;td&gt;2003-2004&lt;/td&gt;
&lt;td&gt;wide-block SPRP, about two passes&lt;/td&gt;
&lt;td&gt;niche, academic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adiantum&lt;/td&gt;
&lt;td&gt;2018&lt;/td&gt;
&lt;td&gt;hash-encrypt-hash, no AES needed&lt;/td&gt;
&lt;td&gt;deployed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HCTR2&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;hash-encrypt-hash, AES-accelerated&lt;/td&gt;
&lt;td&gt;deployed for filenames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Black-Rogaway FPE&lt;/td&gt;
&lt;td&gt;2002&lt;/td&gt;
&lt;td&gt;cycle-walking, Feistel over a radix&lt;/td&gt;
&lt;td&gt;foundation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FF1 (from FFX)&lt;/td&gt;
&lt;td&gt;2010 / 2016&lt;/td&gt;
&lt;td&gt;10-round Feistel, large tweak&lt;/td&gt;
&lt;td&gt;approved, sole survivor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FF3 (from BPS)&lt;/td&gt;
&lt;td&gt;2010 / 2016&lt;/td&gt;
&lt;td&gt;8-round Feistel, 64-bit tweak&lt;/td&gt;
&lt;td&gt;broken 2017, removed 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FF3-1&lt;/td&gt;
&lt;td&gt;2019 draft&lt;/td&gt;
&lt;td&gt;FF3 with tweak shrunk to 56 bits&lt;/td&gt;
&lt;td&gt;broken 2021, removed 2025&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h3&gt;From XEX to XTS: the offset that carries no key&lt;/h3&gt;
&lt;p&gt;Phillip Rogaway&apos;s 2004 XEX construction applies LRW&apos;s lesson exactly [@rogaway-xex2004]. It uses a single key and derives the per-block offset from the cipher: encipher the data-unit number once, then step the offset across block positions by repeatedly multiplying by the field element alpha. No key material ever enters the mask.&lt;/p&gt;

flowchart LR
    I[&quot;Data-unit number i&quot;] --&amp;gt; E2[&quot;Encrypt i under key K2&quot;]
    E2 --&amp;gt; A[&quot;Multiply by alpha, once per block position j&quot;]
    A --&amp;gt; D[&quot;Offset for block j&quot;]
    P[&quot;Plaintext block Pj&quot;] --&amp;gt; X1[&quot;XOR with the offset&quot;]
    D --&amp;gt; X1
    X1 --&amp;gt; E1[&quot;Encrypt under key K1&quot;]
    E1 --&amp;gt; X2[&quot;XOR with the offset again&quot;]
    D --&amp;gt; X2
    X2 --&amp;gt; C[&quot;Ciphertext block Cj&quot;]
&lt;p&gt;The multiply-by-alpha step is why XTS is fast and parallel.Multiplying by alpha in this field is just a one-bit left shift of the 128-bit offset followed by a conditional XOR with the reduction constant 0x87 when the top bit overflows. No general multiplier is needed, and every block&apos;s offset can be computed independently. The simulator below advances the offset exactly this way and enciphers the &lt;em&gt;same&lt;/em&gt; plaintext block at four consecutive positions. The ciphertext differs every time -- position binding -- while re-running the demo reproduces it exactly -- determinism.&lt;/p&gt;
&lt;p&gt;{`
// Advance the per-block offset by multiply-by-alpha in GF(2^128),
// then encipher each block as C = E(P XOR offset) XOR offset.
function mulAlpha(t) {                 // t: 16-byte little-endian offset
  let carry = 0;
  for (let i = 0; i &amp;lt; 16; i++) {
    const b = t[i];
    t[i] = ((b &amp;lt;&amp;lt; 1) | carry) &amp;amp; 0xFF;
    carry = b &amp;gt;&amp;gt;&amp;gt; 7;
  }
  if (carry) t[0] ^= 0x87;            // reduction for the field GF(2^128)
  return t;
}
function toyCipher(block, key) {        // a stand-in permutation with diffusion, NOT real AES
  const out = Uint8Array.from(block);
  for (let r = 0; r &amp;lt; 3; r++) {
    let acc = (key + r * 97 + 1) &amp;amp; 0xFF;
    for (let i = 0; i &amp;lt; 16; i++) {
      acc = (acc + out[i] + key + i * 31) &amp;amp; 0xFF;
      out[i] = (out[i] ^ acc ^ 0x5A) &amp;amp; 0xFF;
    }
    for (let i = 1; i &amp;lt; 16; i++) out[i] = (out[i] + out[i - 1]) &amp;amp; 0xFF;
  }
  return out;
}
function hex(a) { return Array.from(a, b =&amp;gt; b.toString(16).padStart(2, &apos;0&apos;)).join(&apos;&apos;); }&lt;/p&gt;
&lt;p&gt;const seed = new Uint8Array(16); seed[0] = 0x01;   // stands in for Encrypt(i) under K2
let offset = Uint8Array.from(seed);
const plaintext = new Uint8Array(16).fill(0xAA);   // the SAME plaintext at every position&lt;/p&gt;
&lt;p&gt;for (let j = 0; j &amp;lt; 4; j++) {
  const masked = new Uint8Array(16);
  for (let i = 0; i &amp;lt; 16; i++) masked[i] = plaintext[i] ^ offset[i];
  const enc = toyCipher(masked, 0x3C);
  const ct = new Uint8Array(16);
  for (let i = 0; i &amp;lt; 16; i++) ct[i] = enc[i] ^ offset[i];
  console.log(&apos;position &apos; + j + &apos;   offset &apos; + hex(offset).slice(0,12) + &apos;..   ciphertext &apos; + hex(ct).slice(0,12) + &apos;..&apos;);
  mulAlpha(offset);                                 // step to the next position
}
console.log(&apos;Identical plaintext, different position, different ciphertext.&apos;);
`}&lt;/p&gt;
&lt;p&gt;XTS-AES, standardized as IEEE 1619-2007 and adopted by NIST as SP 800-38E, turns XEX into a shippable disk mode [@ieee1619-2007][@nist-sp800-38e]. It uses two independent keys -- one to encipher, one to derive the tweak from the data-unit number -- and adds a trick for data units whose length is not a whole number of blocks.&lt;/p&gt;

A technique that lets a block cipher encrypt a message whose length is not a multiple of the block size without expanding it, by borrowing ciphertext bytes from the second-to-last block to pad the final short block. The output length exactly equals the input length.
&lt;p&gt;NIST caps a single XTS data unit at $2^{20}$ AES blocks, or 16 MiB [@nist-sp800-38e]. And an &quot;AES-256-XTS&quot; key is therefore two 256-bit keys, 512 bits in total -- a fact that trips up more implementers than any other on this list. XTS is excellent at what it does, but three residual limits, all inherent to a &lt;em&gt;narrow&lt;/em&gt; block, drive everything after it: it diffuses across only one 16-byte block, it is unauthenticated, and it leaks equality of identical blocks at identical positions.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; An attacker with write access to an XTS volume cannot paste your plaintext around or flip chosen plaintext bits the way a CTR or CBC gadget allows. What they &lt;em&gt;can&lt;/em&gt; do, all undetected because there is no tag: overwrite a 16-byte block to produce fresh random garbage on decryption, replay an older ciphertext image of the same block at the same position to roll it back, or randomize any block at 16-byte granularity. It is destruction and rollback, not surgical editing -- but it is entirely unauthenticated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;The wide-block ideal, briefly&lt;/h3&gt;
&lt;p&gt;If narrow diffusion is the problem, the obvious cure is to encipher the &lt;em&gt;entire&lt;/em&gt; data unit as one wide block, so that flipping any single bit re-randomizes the whole sector. Shai Halevi and Phillip Rogaway built exactly this: CMC in 2003 and the parallelizable EME in 2004 are wide-block strong PRPs over a whole variable-length unit [@halevi-rogaway-cmc2003][@halevi-rogaway-eme2004]. They are the ideal that XTS only approximates.&lt;/p&gt;
&lt;p&gt;They also cost roughly &lt;em&gt;two&lt;/em&gt; block-cipher passes over every byte, with no hardware primitive to accelerate the whole construction the way AES-NI accelerates XTS, and they carried patent clouds. So they never became the disk default, surviving only as a niche (EME2 lives in IEEE Std 1619.2 [@ieee1619-2-2010]).&lt;/p&gt;
&lt;p&gt;The wide ideal was known for fifteen years before it was cheap enough to ship -- and the thing that made it cheap was the 1999 Naor-Reingold result waiting in the wings [@naor-reingold1999]. Hold that thought; it is the next section.&lt;/p&gt;
&lt;h3&gt;The format-preserving rail: FFX, BPS, and the NIST methods&lt;/h3&gt;
&lt;p&gt;On the payments side, the community turned Black-Rogaway&apos;s Feistel-over-radix into concrete, submittable modes with public tweaks and fixed round counts. Two proposals mattered. FFX, from Mihir Bellare, Phillip Rogaway, and Terence Spies, uses a larger tweak and about ten rounds -- the conservative choice [@ffx-spec2010]. BPS, from Eric Brier, Thomas Peyrin, and Jacques Stern, uses a compact per-message tweak and fewer rounds for speed [@bps-spec].&lt;/p&gt;
&lt;p&gt;Naming trap: BPS is Brier-Peyrin-Stern, the basis of FF3. It is not &quot;Bellare-Rogaway-Spies&quot; -- that trio wrote FFX, the basis of FF1. The initials collide; the papers do not.&lt;/p&gt;
&lt;p&gt;A third submission, VAES3, became the draft method FF2 but was withdrawn before finalization over a security concern.The withdrawal is documented in a 2015 analysis, &quot;Analysis of VAES3 (FF2)&quot; [@dworkin-perlner-vaes3-2015]; when NIST published its standard the next year, only FF1 and FF3 remained. When NIST finalized SP 800-38G in 2016, it approved exactly two methods: &lt;strong&gt;FF1&lt;/strong&gt;, from FFX, with ten Feistel rounds and a large tweak; and &lt;strong&gt;FF3&lt;/strong&gt;, from BPS, with eight rounds and a 64-bit tweak [@nist-sp800-38g-2016]. Both are AES-backed unbalanced Feistel networks over the message radix, and the standard required a minimum domain of at least 100 values, recommending at least one million.&lt;/p&gt;

flowchart TD
    Split[&quot;Digit string split into halves A and B&quot;] --&amp;gt; Round[&quot;Round function: AES-based PRF over round number, tweak, and half B&quot;]
    Round --&amp;gt; Combine[&quot;Add the PRF output into half A, modulo radix to the m, the half&apos;s range&quot;]
    Combine --&amp;gt; Swap[&quot;Swap the halves and repeat for every round&quot;]
    Swap --&amp;gt; Out[&quot;Recombine into a same-length digit string&quot;]
&lt;p&gt;The difference that matters downstream is precisely FF3&apos;s smaller tweak and fewer rounds versus FF1&apos;s larger tweak and ten rounds [@nist-sp800-38g-2016]. FF3 bought speed by spending security margin. Within a year, someone came to collect.&lt;/p&gt;
&lt;p&gt;Each rail had reached a plateau. XTS was narrow; FF3 was fast and fragile. What the field lacked was not another cipher but a &lt;em&gt;reframing&lt;/em&gt; -- one that would explain both plateaus at once and, in the same move, make the wide permutation cheap enough to deploy.&lt;/p&gt;
&lt;h2&gt;5. The Right Target Was a Permutation All Along&lt;/h2&gt;
&lt;p&gt;Step back from the individual ciphers and ask what you are actually allowed to want. Not the secrecy of a randomized message -- there is no randomness. Not integrity -- there is no tag. What you can ask for is the &lt;em&gt;best keyed permutation over this exact space&lt;/em&gt;, one that behaves like a randomly chosen permutation even against an adversary who can query it forward and backward.&lt;/p&gt;
&lt;p&gt;That is a tweakable strong pseudorandom permutation, and once you name it, the whole field snaps into focus. XTS is a narrow approximation of it. CMC and EME are wide but expensive versions of it. Every FF method is a version of it on a tiny domain.&lt;/p&gt;

Whether one changed input bit diffuses across the entire data unit (wide) or only within its own 16-byte block (narrow). A wide-block cipher enciphers the whole sector as a single indivisible permutation.
&lt;p&gt;Two ideas define the modern state of the art, one for each rail. The first is how to make the wide permutation cheap. Naor and Reingold&apos;s 1999 insight was that you do not need two full cipher passes to get a wide strong PRP; you can sandwich a small amount of real cipher work between two passes of fast keyed hashing [@naor-reingold1999]. This is the hash-encrypt-hash shape, and it is the skeleton of both deployed wide-block modes.&lt;/p&gt;

flowchart LR
    P[&quot;Whole data unit, plaintext&quot;] --&amp;gt; H1[&quot;Keyed universal hash over the whole unit&quot;]
    H1 --&amp;gt; Mid[&quot;One narrow primitive call on a short middle block&quot;]
    Mid --&amp;gt; H2[&quot;The same keyed hash, applied again&quot;]
    H2 --&amp;gt; C[&quot;Whole data unit, ciphertext&quot;]
&lt;p&gt;Adiantum instantiates it with NH and Poly1305 hashing wrapped around a single AES call and an XChaCha12 stream cipher [@adiantum2018]. HCTR2 instantiates it with POLYVAL hashing around an XCTR-over-AES core [@hctr2-2021]. The payoff is the second surprise of this article: the wide ideal is not intrinsically expensive. Adiantum runs at 10.6 cycles per byte on an ARM Cortex-A7, over five times faster than AES-256-XTS implemented in software on the same chip [@adiantum2018]. The mode that gives you whole-sector diffusion is &lt;em&gt;faster&lt;/em&gt; than narrow XTS on hardware that lacks an AES instruction.&lt;/p&gt;
&lt;h3&gt;Why one narrow call is enough, and why it is &lt;em&gt;strong&lt;/em&gt;&lt;/h3&gt;
&lt;p&gt;It is easy to assert that Adiantum and HCTR2 are strong pseudorandom permutations. It is more useful to see &lt;em&gt;why&lt;/em&gt;, because the reason is the entire point of the hash-encrypt-hash shape. Take Adiantum concretely. Split the data unit into a long left part $P_L$ (the bulk of the sector) and a single block-sized right part $P_R$. Four steps encipher it:&lt;/p&gt;
&lt;p&gt;$$P_M = P_R \boxplus H_k(T, P_L), \qquad C_M = \mathrm{AES}(P_M),$$&lt;/p&gt;
&lt;p&gt;$$C_L = P_L \oplus \mathrm{stream}(C_M), \qquad C_R = C_M \boxminus H_k(T, C_L).$$&lt;/p&gt;
&lt;p&gt;Here $H_k$ is a fast keyed almost-universal hash (NH plus Poly1305 in Adiantum), $T$ is the tweak, $\boxplus$ and $\boxminus$ are addition and subtraction on one block, and $\mathrm{stream}$ is a stream cipher (XChaCha12) seeded by the single enciphered block. Read it as three beats.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The first hash layer buys unpredictability.&lt;/em&gt; Because $H_k$ is keyed and depends on the entire left bulk, the input $P_M$ to the one real cipher call is a secret function of the whole message. An adversary cannot &lt;em&gt;steer&lt;/em&gt; that middle input: change any part of $P_L$ and $P_M$ jumps unpredictably, so two distinct queries land on essentially independent middle inputs.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The middle is why one narrow pass suffices.&lt;/em&gt; The hash has already done the wide mixing, so the cryptographic core only has to supply pseudorandomness on a &lt;em&gt;single&lt;/em&gt; block. That is why one AES call can stand in for a full wide-block cipher pass -- the expensive part was never the enciphering, it was the diffusion, and cheap keyed hashing does the diffusion.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The second hash layer spreads the result back out.&lt;/em&gt; Running the stream cipher over the bulk and folding the enciphered block back through the hash makes one changed input bit rewrite the entire unit -- wide-block avalanche, built from one block-cipher call and two linear-time hash passes.&lt;/p&gt;
&lt;p&gt;Now the part that makes it a &lt;em&gt;strong&lt;/em&gt; PRP, secure even against an adversary who queries decryption. The construction is symmetric and invertible, so the identical &quot;you cannot steer the middle&quot; argument runs &lt;em&gt;backward&lt;/em&gt; for inverse queries: a decryption query is just as unable to force a chosen middle block as an encryption query is.&lt;/p&gt;
&lt;p&gt;An adversary mixing adaptive forward and inverse queries can win only by making two queries collide on the same middle block $P_M$ -- and because $H_k$ randomizes that block, a collision is a birthday event of probability about $q^2 / 2^b$ for $q$ queries and a $b$-bit block. That two-directional birthday bound &lt;em&gt;is&lt;/em&gt; strong-PRP security; it is Adiantum&apos;s &quot;provable quadratic advantage bound&quot; [@adiantum2018].&lt;/p&gt;
&lt;p&gt;This is Naor and Reingold&apos;s insight in operational form: two universal-hash layers do the work of the outer rounds of a Feistel network, so a single cryptographic core reaches the strong PRP that Luby-Rackoff needs four Feistel rounds -- and CMC or EME need two full cipher passes -- to reach [@naor-reingold1999].&lt;/p&gt;

&quot;HCTR2 is a tweakable super-pseudorandom permutation: any change to the plaintext will result in an unrecognizably different ciphertext.&quot; -- the google/hctr2 project README [@google-hctr2-repo]
&lt;p&gt;The second idea is the hard one, and it lives on the format-preserving rail. There is no reframing that saves small-domain FPE, because the enemy is not the cipher&apos;s construction; it is the size of the set.&lt;/p&gt;

The number of possible inputs a permutation is defined over. For format-preserving encryption it is a hard security parameter, not a tuning knob: below a floor, the permutation can be reconstructed by enumeration no matter how sound the round function is.
&lt;p&gt;So the community&apos;s real progress on FPE was an act of acceptance: minimum-domain floors are not conservative style, they are load-bearing defenses, and a field too small to defend should not be format-preserved at all. That verdict is the subject of Section 8. For now, hold both realizations together.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Wide-block enciphering is the disk field finally &lt;em&gt;reaching&lt;/em&gt; the ideal permutation that XTS only approximated -- and doing it cheaply. But reaching the ideal permutation changes nothing about integrity: even a perfect tweakable SPRP has no tag, so tampering still produces plaintext the layer above will consume. Wide-block fixes diffusion. It does not, and cannot, add authentication.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the wide permutation shipped. Which raises the obvious question, and its genuinely surprising answer: if wide-block is provably better and can run faster, why is the disk in front of you -- and the phone in your pocket -- still enciphering its contents with narrow XTS?&lt;/p&gt;
&lt;h2&gt;6. What Actually Ships in 2026&lt;/h2&gt;
&lt;p&gt;Here is the deployed map, and its most honest surprise: the better cipher shipped, but not where you would guess. The wide-block permutation went to phones -- for filenames and for chips without an AES instruction -- while the contents of nearly every encrypted volume on Earth are still enciphered with narrow XTS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AES-XTS -- the mode, not a fixed key size -- is the universal full-volume default.&lt;/strong&gt; BitLocker has defaulted to XTS-AES since Windows 10 version 1511 [@microsoft-bitlocker]. Apple states plainly that &quot;FileVault uses the AES-XTS data encryption algorithm to protect full volumes on internal and removable storage devices&quot; [@apple-filevault]. On Linux, LUKS and dm-crypt default to &lt;code&gt;aes-xts-plain64&lt;/code&gt; [@luks-cryptsetup-faq]. Three operating systems, three vendors, one narrow-block mode.&lt;/p&gt;
&lt;p&gt;What is &lt;em&gt;not&lt;/em&gt; universal is the key size, and conflating the two is a frequent error: BitLocker and FileVault default to AES-128-XTS -- Microsoft&apos;s own menu lists &quot;XTS-AES 128-bit (default)&quot; [@microsoft-bitlocker-autopilot], and FileVault&apos;s often-quoted &quot;256-bit key&quot; is the two-key XTS total of two 128-bit keys -- while LUKS and dm-crypt default to AES-256-XTS, a 512-bit volume key, because an XTS key size is twice the underlying AES key [@cryptsetup-luksformat-man]. The &lt;a href=&quot;https://paragmali.com/blog/bitlocker-on-windows-architecture-attacks-and-the-limits-of-/&quot; rel=&quot;noopener&quot;&gt;BitLocker architecture post&lt;/a&gt; covers the Windows side of this in depth.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File-based encryption is where wide-block actually ships&lt;/strong&gt;, and its details are the single most mis-stated fact in this area. In Android and Linux fscrypt, on hardware &lt;em&gt;with&lt;/em&gt; AES acceleration, file &lt;strong&gt;contents&lt;/strong&gt; use &lt;code&gt;aes-256-xts&lt;/code&gt; and &lt;strong&gt;filenames&lt;/strong&gt; use &lt;code&gt;aes-256-hctr2&lt;/code&gt;; the AOSP documentation says HCTR2 has been the &quot;preferred mode of filenames encryption for devices with accelerated cryptography instructions&quot; since Android 14, and is planned to become the default [@aosp-fbe]. On hardware &lt;em&gt;without&lt;/em&gt; AES acceleration, &lt;strong&gt;Adiantum&lt;/strong&gt; encrypts both contents and filenames [@aosp-fbe][@aosp-adiantum].The Adiantum announcement is often mis-cited. Its actual title is &quot;Introducing Adiantum: Encryption for the Next Billion Users&quot; [@google-adiantum-blog]; the &quot;entry-level processors&quot; phrasing comes from the research paper&apos;s subtitle, not the blog post.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Read that deployment carefully. HCTR2&apos;s shipped niche is &lt;em&gt;filename&lt;/em&gt; encryption, where its whole-unit diffusion hides the length and structure of names cheaply. It does not encrypt production disk-sector contents on AES hardware -- XTS does. Wide-block did not replace XTS for contents anywhere in mainstream deployment as of 2026.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On the payments rail, the map collapsed to a single survivor. NIST&apos;s second public draft of SP 800-38G Revision 1, dated February 2025, states that &quot;the encryption method FF3 is no longer specified,&quot; leaving &lt;strong&gt;FF1 as the sole NIST-approved format-preserving method&lt;/strong&gt; [@nist-sp800-38gr1-2pd2025].&lt;/p&gt;
&lt;h3&gt;Integrity is a first-class part of the picture, not a footnote&lt;/h3&gt;
&lt;p&gt;Because none of these permutations authenticate anything, real systems bolt integrity on at an adjacent layer, and that layer is genuinely part of the state of the art:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;dm-verity&lt;/strong&gt; builds a Merkle hash tree over a read-only volume and verifies every block on read against a trusted root hash. It is how Android Verified Boot detects any modification of a system image [@kernel-dm-verity].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;dm-integrity&lt;/strong&gt; keeps a per-sector integrity journal for read-write volumes; combined with dm-crypt it detects &lt;em&gt;modification&lt;/em&gt; -- alter the device and the read returns an I/O error instead of forged plaintext -- at a storage and write cost [@kernel-dm-integrity]. It does &lt;em&gt;not&lt;/em&gt; stop rollback or replay: the per-sector tag has no freshness anchor, so restoring a sector&apos;s own earlier authentic ciphertext-and-tag pair still verifies. Anti-rollback needs an externally-anchored root, whose options Section 9 spells out in full.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fscrypt&apos;s per-file nonce&lt;/strong&gt; is a random 16-byte value per file, fed through HKDF-SHA512 to derive a per-file key, so identical plaintext in two different files does not produce identical ciphertext [@kernel-fscrypt]. It restores the per-unit uniqueness that a bare deterministic cipher throws away.&lt;/li&gt;
&lt;/ul&gt;

flowchart TD
    Data[&quot;Data at rest&quot;] --&amp;gt; Conf[&quot;Confidentiality: XTS, Adiantum, or HCTR2, with no tag&quot;]
    Conf --&amp;gt; Q{&quot;Is tamper in scope?&quot;}
    Q --&amp;gt;|&quot;read-only image&quot;| Verity[&quot;dm-verity: read-only Merkle root, resists rollback&quot;]
    Q --&amp;gt;|&quot;read-write volume&quot;| Integ[&quot;dm-integrity: modification only, no freshness anchor&quot;]
    Q --&amp;gt;|&quot;per-file uniqueness&quot;| Nonce[&quot;fscrypt 16-byte nonce&quot;]
    Q --&amp;gt;|&quot;room to expand&quot;| AEAD[&quot;Use an AEAD one layer up&quot;]
&lt;p&gt;The engineering even shows in the key management: an Adiantum key carries more per-key overhead than an AES-256-XTS key, which is why fscrypt offers a &lt;code&gt;DIRECT_KEY&lt;/code&gt; option to amortize it.This is the kind of detail that reveals which mode a platform expects to be common: the optimization exists because Adiantum keys are the expensive case [@kernel-fscrypt]. The map now shows three disk permutations and one surviving FPE method, each optimizing something different. To choose among them you need the head-to-head -- and one decisive question that sits above all four.&lt;/p&gt;
&lt;h2&gt;7. Narrow, Wide, or Authenticated&lt;/h2&gt;
&lt;p&gt;The question everyone asks is &quot;which length-preserving cipher should I use?&quot; The better question sits one layer up. Start with the disk contenders head to head.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;AES-XTS&lt;/th&gt;
&lt;th&gt;Adiantum&lt;/th&gt;
&lt;th&gt;HCTR2&lt;/th&gt;
&lt;th&gt;dm-integrity plus dm-crypt, or an AEAD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Block width&lt;/td&gt;
&lt;td&gt;narrow, 16 bytes&lt;/td&gt;
&lt;td&gt;wide, whole unit&lt;/td&gt;
&lt;td&gt;wide, whole unit&lt;/td&gt;
&lt;td&gt;not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security target&lt;/td&gt;
&lt;td&gt;narrow tweakable SPRP&lt;/td&gt;
&lt;td&gt;wide tweakable SPRP&lt;/td&gt;
&lt;td&gt;wide tweakable SPRP&lt;/td&gt;
&lt;td&gt;authenticated encryption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tamper behavior&lt;/td&gt;
&lt;td&gt;position-bound garbage or rollback&lt;/td&gt;
&lt;td&gt;whole-unit garbage&lt;/td&gt;
&lt;td&gt;whole-unit garbage&lt;/td&gt;
&lt;td&gt;modification detected and rejected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback or replay&lt;/td&gt;
&lt;td&gt;undetected&lt;/td&gt;
&lt;td&gt;undetected&lt;/td&gt;
&lt;td&gt;undetected&lt;/td&gt;
&lt;td&gt;still undetected -- needs an externally-anchored root&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same-offset leakage&lt;/td&gt;
&lt;td&gt;leaks equal 16-byte blocks&lt;/td&gt;
&lt;td&gt;none within a unit&lt;/td&gt;
&lt;td&gt;none within a unit&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardware acceleration&lt;/td&gt;
&lt;td&gt;AES-NI, ARMv8 CE&lt;/td&gt;
&lt;td&gt;none required&lt;/td&gt;
&lt;td&gt;AES-NI, CLMUL, ARMv8 CE&lt;/td&gt;
&lt;td&gt;depends&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ciphertext expansion&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;td&gt;nonzero, a tag or journal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployed status&lt;/td&gt;
&lt;td&gt;universal disk default&lt;/td&gt;
&lt;td&gt;non-AES hardware&lt;/td&gt;
&lt;td&gt;filenames, Android 14+&lt;/td&gt;
&lt;td&gt;integrity layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The table has a punchline. On AES hardware, XTS and HCTR2 are close in speed, and HCTR2&apos;s only advantage is &lt;em&gt;diffusion&lt;/em&gt; -- better tamper behavior and no same-offset leak -- never &lt;em&gt;authentication&lt;/em&gt; [@hctr2-2021][@nist-sp800-38e]. If tampering is in your threat model, none of the three columns on the left answers it. Only the right-hand column does.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; &quot;Which length-preserving cipher?&quot; is downstream of a prior question: am I actually forced to be length-preserving, or can I add a tag one layer up? If you can spend even a few bytes, the honest answer is not a better permutation but authenticated encryption -- the subject of Part 7 of this series. Length-preserving modes are what you reach for only when expansion is genuinely impossible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That framing is exactly why 2026 deployment keeps XTS for contents (fast, accelerated, random-access) and adds wide-block only where it cheaply helps -- HCTR2 for filenames, Adiantum where there is no AES instruction -- while putting authentication in dm-verity, dm-integrity, or a per-file nonce [@kernel-dm-integrity].&lt;/p&gt;
&lt;h3&gt;FF1 versus FF3-1, calibrated&lt;/h3&gt;
&lt;p&gt;On the format-preserving rail the head-to-head is narrower, and it is easy to read it wrong.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;FF1&lt;/th&gt;
&lt;th&gt;FF3-1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Feistel rounds&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tweak&lt;/td&gt;
&lt;td&gt;large&lt;/td&gt;
&lt;td&gt;56-bit, reduced from FF3&apos;s 64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structural weakness&lt;/td&gt;
&lt;td&gt;small-domain recovery, shared by all FPE [@hoang-tessaro-trieu2018]&lt;/td&gt;
&lt;td&gt;a linear distinguisher on the tweak schedule [@beyne2021], plus small-domain recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NIST status, Feb 2025&lt;/td&gt;
&lt;td&gt;approved, sole survivor [@nist-sp800-38gr1-2pd2025]&lt;/td&gt;
&lt;td&gt;removed [@nist-sp800-38gr1-2pd2025]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict&lt;/td&gt;
&lt;td&gt;most defensible FPE, still not for small fields&lt;/td&gt;
&lt;td&gt;migrate off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Do not read this as &quot;FF1 secure, FF3-1 broken, done.&quot; &lt;em&gt;All&lt;/em&gt; small-domain format-preserving encryption leaks; FF1&apos;s larger tweak and ten rounds simply give it a wider margin, not immunity. FF1 is the more defensible choice, not a substitute for a genuinely large-domain permutation. The break that removed FF3-1 was structural to the FF3 family, which is why no tweak-size tuning saved it.&lt;/p&gt;

For a field small enough that any permutation is enumerable -- a PIN, a short code, a low-cardinality category -- the honest engineering answer is not a better cipher but a **tokenization vault**: replace the sensitive value with a random surrogate and keep the mapping in a secured lookup table. A vault sidesteps the domain wall entirely, because a random surrogate reveals nothing about its input. Under compliance regimes like PCI DSS, a vault is frequently the safer and simpler choice than any format-preserving cipher, at the cost of maintaining the lookup [@pci-ssc-tokenization2011].
&lt;p&gt;Every trade-off in both tables bottoms out in the same two limits: block width and domain size. It is time to show that these are not engineering accidents that a cleverer design will someday fix. They are theorems -- and one of them has a measurable height.&lt;/p&gt;
&lt;h2&gt;8. What Length Preservation Makes Impossible&lt;/h2&gt;
&lt;p&gt;Everything so far could be read as &quot;we just need a better cipher.&quot; We do not. Three of these limits are theorems, and the fourth is a wall no cipher climbs -- but it is a wall you can measure. The security definitions behind them come from &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;Part 1&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First theorem: no expansion means no randomness, so the cipher is deterministic.&lt;/strong&gt; Semantic security against a chosen-plaintext attacker -- the IND-CPA notion -- provably requires that encrypting the same message twice can produce different ciphertexts. A length-preserving cipher has nowhere to put the randomness that would make that happen, so IND-CPA is unreachable by construction. The best achievable target is indistinguishability from a random permutation, an SPRP. This is not a weakness of XTS; it is the ceiling for &lt;em&gt;anything&lt;/em&gt; that refuses to expand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second theorem: no expansion means no tag, so there is no integrity.&lt;/strong&gt; With zero expansion there is nothing to carry a MAC, so every ciphertext decrypts to &lt;em&gt;some&lt;/em&gt; plaintext and the cipher can never reject a forgery. A length-preserving cipher can only &lt;em&gt;randomize&lt;/em&gt; on tamper (wide-block) or permit &lt;em&gt;position-bound edits&lt;/em&gt; (narrow-block). It can never say &quot;no.&quot; Authentication is provably outside the primitive and must live at another layer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third theorem: a per-position tweak leaks equality at its own granularity -- but be precise about which positions.&lt;/strong&gt; A deterministic tweakable cipher with a public per-position tweak necessarily reveals when two positions hold identical plaintext. XTS leaks equality of identical 16-byte blocks at the &lt;em&gt;same&lt;/em&gt; key, data unit, and block index. It does &lt;em&gt;not&lt;/em&gt; leak equality of two identical sectors at &lt;em&gt;different&lt;/em&gt; offsets: every data-unit index feeds its own tweak, so the same 512 bytes at sector 10 and sector 20 encipher to completely different ciphertext.&lt;/p&gt;
&lt;p&gt;The one place byte-identical plaintext does collide is &lt;em&gt;file-relative&lt;/em&gt;. In file-based encryption such as fscrypt, where each file&apos;s data-unit index restarts at zero, two byte-identical files would run through the same tweak sequence and encipher identically -- which is exactly why fscrypt derives a distinct per-file key from a random 16-byte nonce, so even identical files no longer collide [@kernel-fscrypt].&lt;/p&gt;
&lt;p&gt;This is not a whole-volume claim: under whole-disk dm-crypt with absolute-sector tweaks, two identical files at different offsets get different data-unit numbers and therefore different tweaks, and do not collide -- the only equality leak there is between data units that share the same key and tweak, such as one logical block rewritten in place. Wide-block modes do not remove this addressing-model leak; they only push its boundary out from a 16-byte block to the whole unit.&lt;/p&gt;
&lt;h3&gt;The fourth limit: the small-domain wall, measured&lt;/h3&gt;
&lt;p&gt;The deepest cut is on the format-preserving rail, and it is not a theorem about any cipher&apos;s construction. It is a theorem about the &lt;em&gt;domain&lt;/em&gt;. For a permutation over a set of size $N$, message recovery and codebook reconstruction are achievable in work that is &lt;em&gt;polynomial&lt;/em&gt; in $N$. Security does not fall off a cliff at some threshold; it degrades smoothly toward zero as the field shrinks.&lt;/p&gt;
&lt;p&gt;The attack line is a decade of steadily sharper results: Bellare, Hoang, and Tessaro gave the first message-recovery attacks that exploit the small domain rather than any flaw in the round function [@bellare-hoang-tessaro2016]; Hoang, Tessaro, and Trieu generalized them in a paper aptly titled &lt;em&gt;The Curse of Small Domains&lt;/em&gt; [@hoang-tessaro-trieu2018]; and Hoang, Miller, and Trieu pushed the FF3 attack out to large domains [@hoang-miller-trieu2019].&lt;/p&gt;
&lt;p&gt;Put numbers on it, because the numbers are the payoff. Durak and Vaudenay&apos;s 2017 break of FF3 -- the one that broke a deployed NIST standard outright -- runs in time about $\mathrm{radix}^{5n/2}$, which for an $n$-digit field of size $D = \mathrm{radix}^{n}$ is $D^{2.5}$, using about $\mathrm{radix}^{11n/12} = D^{11/12}$ adaptively chosen plaintext-ciphertext pairs under two related tweaks [@nist-ff3-news2017][@durak-vaudenay2017].&lt;/p&gt;
&lt;p&gt;Two features make this devastating rather than academic. First, the data cost $D^{11/12}$ is &lt;em&gt;sublinear&lt;/em&gt; in the domain: you need &lt;em&gt;fewer&lt;/em&gt; query pairs than there are values in the field. Second, the attack recovers the round functions themselves, so it yields &quot;any plaintext under any tweak&quot; -- a key-equivalent break, not the exposure of a single record [@nist-ff3-news2017]. NIST&apos;s own note put the cost at roughly $2^{75}$ steps for a nine-digit field such as a Social Security number [@nist-ff3-news2017].&lt;/p&gt;
&lt;p&gt;Two years later Hoang, Miller, and Trieu improved the running time from $O(N^5)$ to $O(N^{17/6})$ -- on the order of $D^{17/12}$, about $D^{1.42}$ -- and, as their title &lt;em&gt;Attacks Only Get Better&lt;/em&gt; promises, extended the break to large domains rather than only tiny ones [@hoang-miller-trieu2019].&lt;/p&gt;
&lt;p&gt;The table makes the scaling visceral, and settles one honesty debt the cold open owed you.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field (domain D)&lt;/th&gt;
&lt;th&gt;Adaptive pairs, about D^(11/12)&lt;/th&gt;
&lt;th&gt;Total work, Durak-Vaudenay, about D^(2.5)&lt;/th&gt;
&lt;th&gt;Total work, Hoang-Miller-Trieu, about D^(17/12)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Six-digit PIN or card-middle, D = 10^6&lt;/td&gt;
&lt;td&gt;about 3.2 x 10^5&lt;/td&gt;
&lt;td&gt;about 2^50&lt;/td&gt;
&lt;td&gt;about 2^30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nine-digit SSN, D = 10^9&lt;/td&gt;
&lt;td&gt;about 1.8 x 10^8&lt;/td&gt;
&lt;td&gt;about 2^75&lt;/td&gt;
&lt;td&gt;about 2^50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sixteen-digit PAN, D = 10^16&lt;/td&gt;
&lt;td&gt;about 4.6 x 10^14&lt;/td&gt;
&lt;td&gt;about 2^133 (exceeds 2^128)&lt;/td&gt;
&lt;td&gt;about 2^75&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;em&gt;A note on the last column: the $2^{50}$ entry for the nine-digit SSN is Hoang, Miller, and Trieu&apos;s concrete reported cost for that imbalanced domain (an uneven split, with constant factors folded in), which sits above the symmetric leading term $D^{17/12} \approx 2^{42}$ at $D = 10^9$. Recomputing the header formula alone therefore gives the smaller exponent; no figure in the table changes [@hoang-miller-trieu2019].&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Read the rows in order. A six-digit field -- a PIN, or the six middle digits of a card, exactly the size FF3 was designed for -- has a million points, and recovering its whole codebook takes a few hundred thousand adaptive pairs and on the order of $2^{30}$ operations, an afternoon of compute. That is the cold open&apos;s second failure, now with numbers.&lt;/p&gt;
&lt;p&gt;But look at the last row: a &lt;em&gt;full&lt;/em&gt; sixteen-digit card number lives in a domain near $10^{16}$, and the same polynomial attack costs about $2^{75}$ by the best known method and about $2^{133}$ by Durak-Vaudenay&apos;s $D^{2.5}$ -- which actually &lt;em&gt;exceeds&lt;/em&gt; a $2^{128}$ key search. The break tracks the &lt;em&gt;field size&lt;/em&gt;, not the cipher and not the format. It is the small field that dies, which is exactly why the honest claim is &quot;a six-digit column falls cheaply,&quot; never &quot;a whole card column falls to a few thousand queries.&quot;&lt;/p&gt;
&lt;p&gt;The crudest version of the attack needs no cleverness at all. If the domain is small, you can simply query every point and write down the codebook.&lt;/p&gt;
&lt;p&gt;{`
// A permutation is a lock only if its domain is astronomically large.
// Here N = 1,000,000 (a six-digit field). We recover the ENTIRE codebook
// by brute enumeration -- the worst case for the attacker, and it still wins.
function makeSecretPermutation(n, seed) {
  const p = new Int32Array(n);
  for (let i = 0; i &amp;lt; n; i++) p[i] = i;
  let s = seed &amp;gt;&amp;gt;&amp;gt; 0;
  for (let i = n - 1; i &amp;gt; 0; i--) {                 // Fisher-Yates shuffle, toy PRNG
    s = (Math.imul(s, 1103515245) + 12345) &amp;amp; 0x7fffffff;
    const j = s % (i + 1);
    const t = p[i]; p[i] = p[j]; p[j] = t;
  }
  return p;                                          // secret to the attacker
}
const N = 1000000;                                   // the six-digit domain
const secret = makeSecretPermutation(N, 20260709);
function oracle(x) { return secret[x]; }             // attacker gets only oracle access&lt;/p&gt;
&lt;p&gt;const recovered = new Int32Array(N);
let queries = 0;
for (let x = 0; x &amp;lt; N; x++) { recovered[x] = oracle(x); queries++; }&lt;/p&gt;
&lt;p&gt;let correct = 0;
for (let x = 0; x &amp;lt; N; x++) if (recovered[x] === secret[x]) correct++;
console.log(&apos;domain size N            = &apos; + N.toLocaleString());
console.log(&apos;queries used (brute)     = &apos; + queries.toLocaleString());
console.log(&apos;codebook entries learned = &apos; + correct.toLocaleString() + &apos; of &apos; + N.toLocaleString());
console.log(&apos;A 128-bit block hides among 2^128 points. This field hides among &apos; + N.toLocaleString() + &apos;.&apos;);
`}&lt;/p&gt;
&lt;p&gt;The demo is deliberately a &lt;em&gt;worst case for the attacker&lt;/em&gt;: brute enumeration spends the full $N$ queries and learns the codebook one point at a time. The real attacks in the table do strictly better on all three axes at once. They use &lt;em&gt;sublinear&lt;/em&gt; data -- about $D^{11/12}$ pairs, fewer than $N$; they recover the round functions rather than individual points, so the whole codebook and every tweak fall together; and they extend to domains far too large to enumerate by hand. Enumeration is only the ceiling. The cryptanalysis lives well beneath it. Either way, the verdict is identical.&lt;/p&gt;

A permutation over a million points is not a lock. It is a puzzle -- and the attacker has all the pieces.
&lt;h3&gt;Why FF3-1 could not be patched a second time&lt;/h3&gt;
&lt;p&gt;NIST had already conceded the problem in public. Its April 2017 note stated that FF3 &quot;is no longer suitable as a general-purpose FPE method&quot; and floated reducing the tweak from eight bytes to six [@nist-ff3-news2017]. NIST&apos;s fix took the same reduce-the-tweak approach but settled on a more modest cut, and it lived only in a draft: the 2019 initial public draft of SP 800-38G Revision 1 defined FF3-1, shrinking the tweak from 64 to 56 bits -- seven bytes, not the six the researchers had floated -- and mandating a million-value minimum domain [@nist-sp800-38gr1-ipd2019]. The only &lt;em&gt;final&lt;/em&gt; NIST format-preserving standard remained the 2016 document, with FF1 and FF3.&lt;/p&gt;
&lt;p&gt;Then Tim Beyne broke FF3-1 again in 2021 with a linear-cryptanalysis distinguisher that exploits the FF3 tweak schedule -- each tweak half influences only alternating rounds -- an attack that hits FF3 and FF3-1 but not FF1, whose larger tweak and ten rounds do not share the structure [@beyne2021].&lt;/p&gt;
&lt;p&gt;The weakness is structural to the FF3 family, so no third round of tweak-size tuning could close it. NIST removed the method entirely rather than shrink the tweak a third time [@nist-sp800-38gr1-2pd2025].&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;Knob&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;ECB penguin&lt;/td&gt;
&lt;td&gt;1990s&lt;/td&gt;
&lt;td&gt;width (none)&lt;/td&gt;
&lt;td&gt;deterministic independent blocks [@fruhwirth2005]&lt;/td&gt;
&lt;td&gt;diffusion is necessary but not sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBC watermarking&lt;/td&gt;
&lt;td&gt;2000s&lt;/td&gt;
&lt;td&gt;width&lt;/td&gt;
&lt;td&gt;predictable per-sector IV [@fruhwirth2005]&lt;/td&gt;
&lt;td&gt;derive the IV secretly, as ESSIV does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LRW key leak&lt;/td&gt;
&lt;td&gt;2002&lt;/td&gt;
&lt;td&gt;key handling&lt;/td&gt;
&lt;td&gt;tweak mask built from raw key material [@liskov-rivest-wagner2002]&lt;/td&gt;
&lt;td&gt;never let key material into the mask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elephant diffuser retired&lt;/td&gt;
&lt;td&gt;2006&lt;/td&gt;
&lt;td&gt;integrity&lt;/td&gt;
&lt;td&gt;a diffuser randomizes but cannot reject [@ferguson2006]&lt;/td&gt;
&lt;td&gt;a diffuser is not a MAC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XTS malleability and replay&lt;/td&gt;
&lt;td&gt;2007&lt;/td&gt;
&lt;td&gt;narrow width, no tag&lt;/td&gt;
&lt;td&gt;16-byte diffusion, no authentication [@nist-sp800-38e]&lt;/td&gt;
&lt;td&gt;integrity is a separate layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FF3 broken&lt;/td&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;small domain&lt;/td&gt;
&lt;td&gt;polynomial codebook recovery, 64-bit tweak [@durak-vaudenay2017]&lt;/td&gt;
&lt;td&gt;domain size is a security parameter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FF3-1 broken&lt;/td&gt;
&lt;td&gt;2021&lt;/td&gt;
&lt;td&gt;structure / tweak schedule&lt;/td&gt;
&lt;td&gt;linear weakness in the tweak schedule [@beyne2021]&lt;/td&gt;
&lt;td&gt;no tweak tuning fixes a structural break&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Every entry tagged &quot;width&quot; or &quot;small domain&quot; is one of the two knobs, and the two tagged &quot;key handling&quot; and &quot;integrity&quot; are the lessons that shaped the survivors. FF3-1 sits apart: its 2021 break is structural -- a linear weakness in the tweak schedule, not a knob you can turn. The disk rail carries an upper-bound limit of its own, from the birthday paradox rather than the domain -- and it is widely misdescribed.Two different numbers get conflated here. XTS, Adiantum, and HCTR2 are birthday-bounded: their distinguishing advantage grows like $q^2 / 2^{128}$ for $q$ enciphered blocks under one key, so collisions become a concern only around $2^{64}$ blocks, and that is a &lt;em&gt;per-key&lt;/em&gt; threshold. NIST&apos;s cap of $2^{20}$ AES blocks on a single XTS data unit is a &lt;em&gt;separate&lt;/em&gt;, far more conservative &lt;em&gt;per-data-unit&lt;/em&gt; ceiling that SP 800-38E attributes to IEEE 1619 section 5.1, with no birthday rationale attached [@nist-sp800-38e]. The cap is not &quot;where XTS breaks,&quot; and the birthday bound is not &quot;why the cap is $2^{20}$&quot;; conflating them invites the false idea that XTS fails at 16 MiB. The format-preserving rail, by contrast, ended exactly where the theorems said it would.&lt;/p&gt;

&quot;The encryption method FF3 is no longer specified.&quot; -- NIST SP 800-38G Revision 1, Second Public Draft, February 2025 [@nist-sp800-38gr1-2pd2025]
&lt;p&gt;If the limits are theorems and the wall has a measured height, then the open questions are not &quot;can we fix these?&quot; They are &quot;given that we cannot, what should we build next, and what does the honest alternative cost?&quot;&lt;/p&gt;
&lt;h2&gt;9. Where This Still Bites&lt;/h2&gt;
&lt;p&gt;The science here is unusually settled, which makes the genuinely open questions sharp and few.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why is XTS still the disk-contents default?&lt;/strong&gt; Wide-block SPRPs are provably better at diffusion and ship in production, yet no mainstream operating system defaults &lt;em&gt;contents&lt;/em&gt; to a wide-block mode on AES hardware. The reasons are performance and inertia: XTS is single-pass, parallel, random-access, and accelerated by AES-NI on x86 and the ARMv8 Cryptography Extensions, so at line rate it is very hard to beat.&lt;/p&gt;
&lt;p&gt;HCTR2&apos;s two POLYVAL passes make it modestly heavier than single-pass XTS on the same accelerated chip, and there is no equally standardized wide-block-for-contents mode for general operating-system use. Android shipped wide-block only where it is cheap and clearly helps -- HCTR2 for filenames, Adiantum for chips without AES -- while keeping XTS for contents [@aosp-fbe]. When, if ever, contents should move to wide-block on accelerated hardware is a real and unresolved deployment question.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can authenticated storage be cheap enough to default?&lt;/strong&gt; Integrity is opt-in today, and it helps to &lt;em&gt;size&lt;/em&gt; why. For read-write volumes, dm-crypt plus dm-integrity attaches a per-sector integrity tag; the kernel&apos;s own example uses a 4-byte &lt;code&gt;crc32c&lt;/code&gt;, but tamper detection against an active attacker needs a keyed MAC such as a 32-byte &lt;code&gt;hmac(sha256)&lt;/code&gt; [@kernel-dm-integrity].&lt;/p&gt;
&lt;p&gt;And note what even a cryptographic tag does and does not buy: dm-integrity detects &lt;em&gt;modification only&lt;/em&gt;. As Section 6 noted, it has no freshness anchor, so an old authentic pair still verifies; rollback resistance is therefore a separate requirement met only by an externally-anchored root: dm-verity&apos;s read-only Merkle root, a TPM or Secure-Boot-measured counter, or per-sector version counters. So the open problem is really two: cheap modification detection &lt;em&gt;and&lt;/em&gt; a cheap freshness anchor for read-write data.&lt;/p&gt;
&lt;p&gt;The tag is not the expensive part. The expensive part is atomicity: a sector and its tag must be updated together or not at all, and dm-integrity&apos;s default journaled mode guarantees that by writing everything twice. The documentation says plainly that it &quot;degrades write throughput twice because the data have to be written twice&quot; [@kernel-dm-integrity] -- roughly a 2x write amplification, and the real reason authenticated read-write storage stays an expert opt-in. A faster bitmap mode skips the double write, but the same documentation warns it &quot;is also less reliable&quot; [@kernel-dm-integrity].&lt;/p&gt;
&lt;p&gt;Contrast dm-verity: it protects &lt;em&gt;read-only&lt;/em&gt; data with a Merkle tree computed once at build time and pinned to an externally-held root -- the freshness anchor dm-integrity structurally lacks -- then merely verified, and cached, on read, with no write path, no journal, no atomic tag update, and optional forward-error-correction adding only about 0.8% space [@kernel-dm-verity]. That asymmetry is exactly why verified boot and system images enable dm-verity by default while mutable user data sits on bare dm-crypt.&lt;/p&gt;
&lt;p&gt;Whether tamper &lt;em&gt;detection&lt;/em&gt; can be made cheap enough to default for read-write data -- rather than paying a doubled write for it -- is the open engineering question. Where expansion is allowed at all, the clean answer stays an AEAD.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is any small-domain FPE safe, or is tokenization the only honest answer?&lt;/strong&gt; The decade-long attack line points one way: shrink the domain and you enumerate the permutation in polynomial work. For genuinely small fields, the consistent verdict is that a tokenization vault, not a cleverer cipher, is the defensible design.&lt;/p&gt;

Mostly no, and it is worth puncturing the &quot;quantum breaks all cryptography&quot; over-generalization. XTS, Adiantum, and HCTR2 are built from symmetric permutations, not from the factoring and discrete-log problems a quantum computer would break. The only quantum lever against them is Grover&apos;s search, which square-roots the effort of a brute-force key hunt -- and a 256-bit key already answers it, leaving an effective 128-bit security level [@nist-ir8105]. AES-256-XTS and 256-bit HCTR2 need no algorithm swap. There is no length-preserving equivalent of the wrenching public-key transition described in the [post-quantum migration post](/blog/post-quantum-cryptography-on-windows-the-thirty-year-migrati/).
&lt;p&gt;&lt;strong&gt;Side channels in the radix arithmetic.&lt;/strong&gt; FPE&apos;s Feistel rounds do modular arithmetic over the message radix, and getting that arithmetic both correct and constant-time is subtle. NIST went so far as to ban floating-point arithmetic in FF1 implementations after a rounding bug in Bouncy Castle&apos;s FF1 code -- reported by Daniel Bleichenbacher and fixed in a commit titled &quot;Fix rounding issue with FF1&quot; -- showed how easily a floating-point shortcut corrupts the cipher [@bouncycastle-ff1-commit][@nist-sp800-38gr1-2pd2025].&lt;/p&gt;
&lt;p&gt;Constant-time radix conversion remains an under-examined implementation area. This kind of slow, standards-driven hardening -- ban the dangerous construct, wait for implementations to catch up -- is the same pattern that governs cipher agility elsewhere.&lt;/p&gt;
&lt;p&gt;Open problems belong to the field. You, though, have a disk to encrypt and a column to tokenize today. So here are the rules.&lt;/p&gt;
&lt;h2&gt;10. A Field Guide: Which Cipher, Which Parameters, Which Layer&lt;/h2&gt;
&lt;p&gt;Every rule below is the thesis made operational: pick the widest permitted permutation for your domain, and name the layer that provides integrity.&lt;/p&gt;

flowchart TD
    Start[&quot;What are you encrypting?&quot;] --&amp;gt; Expand{&quot;Can you spend extra bytes?&quot;}
    Expand --&amp;gt;|&quot;yes&quot;| AEAD[&quot;Use an AEAD, not a length-preserving mode&quot;]
    Expand --&amp;gt;|&quot;no&quot;| Kind{&quot;Volume, files, or a field?&quot;}
    Kind --&amp;gt;|&quot;full volume&quot;| XTS[&quot;AES-XTS-256, plus an integrity layer if tamper matters&quot;]
    Kind --&amp;gt;|&quot;file-based&quot;| FBE[&quot;fscrypt: XTS plus HCTR2 on AES hardware, Adiantum without&quot;]
    Kind --&amp;gt;|&quot;structured field&quot;| Field{&quot;Domain at least a million?&quot;}
    Field --&amp;gt;|&quot;yes&quot;| FF1[&quot;FF1 with a per-context tweak&quot;]
    Field --&amp;gt;|&quot;no&quot;| Vault[&quot;Tokenization vault, not FPE&quot;]
&lt;p&gt;&lt;strong&gt;Full volume or block device.&lt;/strong&gt; Configure AES-256-XTS -- the strongest widely-supported choice. Do not mistake that for the &lt;em&gt;default&lt;/em&gt;, which varies by product: BitLocker and FileVault ship AES-128-XTS, while LUKS ships AES-256-XTS (Section 6), so name the mode, not a universal key size. Respect the 16 MiB data-unit cap of $2^{20}$ AES blocks [@nist-sp800-38e].&lt;/p&gt;
&lt;p&gt;If tampering is in your threat model, add integrity at another layer, and be precise about what each layer buys: dm-integrity detects &lt;em&gt;modification only&lt;/em&gt; -- it has no freshness anchor, so it does not resist rollback or replay -- while rollback resistance needs an externally-anchored root (see Section 9). Budget dm-integrity&apos;s roughly 2x write amplification for read-write volumes. Key management and evil-maid defenses are a separate topic, covered in the &lt;a href=&quot;https://paragmali.com/blog/bitlocker-on-windows-architecture-attacks-and-the-limits-of-/&quot; rel=&quot;noopener&quot;&gt;BitLocker architecture post&lt;/a&gt;; this cipher-level guidance deliberately defers them.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The single most common configuration error in this whole area. &quot;AES-256-XTS&quot; means two independent 256-bit keys -- one to encipher, one to derive the tweak -- for 512 bits total. Linux fscrypt makes this concrete by requiring a 64-byte master key for a v1 AES-256-XTS policy [@kernel-fscrypt]. If your key material is 256 bits, you have half a key.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;File-based encryption on Android or Linux fscrypt.&lt;/strong&gt; On hardware with AES acceleration, use &lt;code&gt;aes-256-xts&lt;/code&gt; for contents and &lt;code&gt;aes-256-hctr2&lt;/code&gt; for filenames. On hardware without AES acceleration, use &lt;code&gt;adiantum&lt;/code&gt; for both. Never hand-roll it; drive it through the platform&apos;s file-based-encryption configuration [@aosp-fbe].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can you expand at all?&lt;/strong&gt; Then stop -- do not use a length-preserving mode. The designers of HCTR2 say so themselves, in the plainest terms.&lt;/p&gt;

&quot;Usually, the right mode to use for encryption is a mode like AES-GCM or AES-GCM-SIV ... However there are some applications, such as disk encryption, where the ciphertext must be the same size as the plaintext and there is no room for such information.&quot; -- the google/hctr2 README [@google-hctr2-repo]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Length preservation is a painful constraint you accept only when it is forced on you. The moment your format allows a nonce and a tag -- a new database column, a wrapping record, a protocol field -- switch to an AEAD and get authentication for free. That is the whole message of Part 7.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Structured field that must keep its format.&lt;/strong&gt; Use FF1, not FF3-1. Require a domain of at least one million values. Use a maximal-entropy, per-context tweak. Never use it for small fields such as PINs or short codes, and prefer a tokenization vault whenever you can add a lookup [@nist-sp800-38gr1-2pd2025].&lt;/p&gt;
&lt;h3&gt;The common-misuse mirror&lt;/h3&gt;
&lt;p&gt;Each row below is a misconception mapped to the failure it causes.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misconception&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;th&gt;Failure it mirrors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&quot;XTS detects tampering&quot;&lt;/td&gt;
&lt;td&gt;No -- XTS is malleable; add dm-integrity or dm-verity [@nist-sp800-38e]&lt;/td&gt;
&lt;td&gt;XTS malleability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;dm-integrity stops rollback and replay&quot;&lt;/td&gt;
&lt;td&gt;No -- it detects modification only, with no freshness anchor; rollback needs an externally-anchored root [@kernel-dm-integrity]&lt;/td&gt;
&lt;td&gt;XTS rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;An AES-256-XTS key is 256 bits&quot;&lt;/td&gt;
&lt;td&gt;No -- two keys, 512 bits; fscrypt v1 wants 64 bytes [@kernel-fscrypt]&lt;/td&gt;
&lt;td&gt;key-size confusion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;AES-256-XTS is the default everywhere&quot;&lt;/td&gt;
&lt;td&gt;No -- the AES-XTS mode is universal, but BitLocker and FileVault default to AES-128-XTS; only LUKS defaults to AES-256-XTS [@microsoft-bitlocker-autopilot][@cryptsetup-luksformat-man]&lt;/td&gt;
&lt;td&gt;mode vs key size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Wide-block means authenticated&quot;&lt;/td&gt;
&lt;td&gt;No -- Adiantum and HCTR2 fix diffusion, not integrity [@hctr2-2021]&lt;/td&gt;
&lt;td&gt;Elephant diffuser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;FF3 was fixed as FF3-1&quot;&lt;/td&gt;
&lt;td&gt;No -- FF3-1 was broken again and removed in 2025 [@nist-sp800-38gr1-2pd2025]&lt;/td&gt;
&lt;td&gt;FF3-1 broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;FPE on a PIN is fine&quot;&lt;/td&gt;
&lt;td&gt;No -- a small domain is enumerable in polynomial work; use a vault [@hoang-tessaro-trieu2018]&lt;/td&gt;
&lt;td&gt;FF3 broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;FPE may use floating-point radix math&quot;&lt;/td&gt;
&lt;td&gt;No -- banned in FF1 after the Bouncy Castle bug [@bouncycastle-ff1-commit]&lt;/td&gt;
&lt;td&gt;radix side channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;Store the disk key on its own disk&quot;&lt;/td&gt;
&lt;td&gt;No -- that is the LRW self-encryption trap [@liskov-rivest-wagner2002]&lt;/td&gt;
&lt;td&gt;LRW key leak&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

On Linux, inspect a LUKS device with `sudo cryptsetup luksDump /dev/sdX` and look at the Cipher line -- you will typically see `aes-xts-plain64`. On Windows, run `manage-bde -status C:` in an elevated prompt and read the Encryption Method field, which reports XTS-AES on any recent install.
&lt;p&gt;The mirror above names the misconceptions. Two practical questions sit outside it -- when to reach for an AEAD instead, and exactly how far XTS malleability reaches -- and the FAQ takes those on.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;


Because the nonce and the authentication tag do not fit. A sector is a fixed size with no spare bytes, and that is the entire reason length-preserving modes exist. Where the extra bytes *do* fit, an AEAD like AES-GCM or AES-GCM-SIV is exactly what you should use instead [@google-hctr2-repo].


No, not in the way a CTR or CBC bit-flipping gadget can. XTS malleability is position-bound: an attacker can overwrite a 16-byte block into random garbage, or replay an old image of the same block at the same position, but cannot make a chosen, surgical edit to your plaintext. It is destruction and rollback, not controlled rewriting [@nist-sp800-38e].

&lt;h2&gt;The two knobs, one last time&lt;/h2&gt;
&lt;p&gt;Return to the two teams from the opening. The first team&apos;s disk was rewritten by someone who could not read it because XTS has no tag and diffuses across only 16 bytes -- too narrow, and unauthenticated by construction. The second team&apos;s six-digit codes were recovered because a million-value field is a domain small enough that rebuilding its whole permutation costs about $2^{30}$ operations and fewer adaptive queries than there are codes -- too small, and deterministic by construction. Neither failure was a bug. Both were the direct, predictable consequence of forcing a cipher to preserve length.&lt;/p&gt;
&lt;p&gt;That is the whole field in one sentence. When ciphertext must occupy exactly the plaintext&apos;s space, encryption becomes a deterministic, unauthenticated keyed permutation, and its quality is set by two knobs: how &lt;em&gt;wide&lt;/em&gt; its diffusion is, and how &lt;em&gt;large&lt;/em&gt; its domain is. Every real break in this article sorts onto one of them, or onto the handful of hard-won lessons the failure catalog logs beside the knobs -- leaky key handling, missing integrity, and a structural tweak-schedule flaw. And the small-domain break, notably, costs work merely polynomial in the domain, not the exponential figure the format advertises.&lt;/p&gt;
&lt;p&gt;Because the primitive has no tag, integrity always lives one layer up: in a Merkle tree, an integrity journal, a per-file nonce, or an AEAD wherever you are finally allowed to spend the bytes. Part 10 turns from confidentiality on stored data to the machinery that negotiates it in motion.&lt;/p&gt;
&lt;p&gt;You can now read any storage or tokenization design at a glance. Ask three questions: how wide is its permutation, how large is its domain, and -- because it has no tag -- what layer above is keeping it from being rewritten?&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;no-room-for-a-nonce-disk-and-length-preserving-encryption-fr&quot; keyTerms={[
  { term: &quot;Length-preserving encryption&quot;, definition: &quot;Encryption whose ciphertext occupies exactly the plaintext&apos;s space, leaving no room for a nonce or a tag.&quot; },
  { term: &quot;Malleability&quot;, definition: &quot;The ability to alter a ciphertext into another valid ciphertext, changing the plaintext with nothing to detect it.&quot; },
  { term: &quot;Pseudorandom permutation (PRP/SPRP)&quot;, definition: &quot;A keyed permutation indistinguishable from random; a strong PRP resists that even under inverse queries.&quot; },
  { term: &quot;Format-preserving encryption (FPE)&quot;, definition: &quot;A cipher whose output has the same format as its input, such as a sixteen-digit number to a sixteen-digit number.&quot; },
  { term: &quot;Tweakable block cipher&quot;, definition: &quot;A block cipher with a public tweak input that cheaply selects a different permutation for each position.&quot; },
  { term: &quot;Cycle-walking&quot;, definition: &quot;Enciphering a small set by re-encrypting under a larger cipher until the result lands back in range.&quot; },
  { term: &quot;Ciphertext stealing&quot;, definition: &quot;A technique to encrypt data that is not a whole number of blocks without expanding its length.&quot; },
  { term: &quot;Wide-block enciphering&quot;, definition: &quot;Enciphering a whole data unit as one permutation so one changed bit diffuses across the entire unit.&quot; },
  { term: &quot;Domain size&quot;, definition: &quot;The number of values a permutation is defined over; for FPE it is a hard security parameter, not a tuning knob.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>disk-encryption</category><category>xts</category><category>adiantum</category><category>hctr2</category><category>format-preserving-encryption</category><category>length-preserving-encryption</category><category>applied-cryptography</category><category>cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>The AEAD Decision Matrix: Seven Ciphers, Three Edges, One Choice</title><link>https://paragmali.com/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/</link><guid isPermaLink="true">https://paragmali.com/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/</guid><description>AES-GCM, ChaCha20-Poly1305, CCM, OCB3, GCM-SIV, AEGIS, and Ascon, compared by the three sharp edges that decide every deployment: nonce, hardware, commitment.</description><pubDate>Thu, 09 Jul 2026 15:04:00 GMT</pubDate><content:encoded>
Every modern AEAD makes the same three promises, and they differ only at the sharp edges where those promises stop. Three edges decide a real deployment: the **nonce contract** (catastrophic on reuse, or misuse-resistant), the **performance and hardware profile** (AES-NI plus CLMUL, constant-time software, or a few thousand gates), and **commitment** (does one ciphertext bind to exactly one key?). &quot;Which AEAD should I use?&quot; is not a ranking from worst to best. It is a function: default to AES-GCM or ChaCha20-Poly1305, and escalate to AES-GCM-SIV, AES-CCM, Ascon, OCB3, or AEGIS only when a specific edge demands it. Every famous break -- invisible salamanders, nonce reuse on live TLS, `CCM_8` forgeries -- was a deployment stepping on an edge, not a broken cipher.
&lt;h2&gt;1. Two Failures That Should Have Been Impossible&lt;/h2&gt;
&lt;p&gt;In 2018, a team of cryptographers handed Facebook a single encrypted file that decrypted to two completely different, perfectly meaningful images, and whose authentication tag verified correctly for &lt;em&gt;both&lt;/em&gt;, under two different keys [@dodis2018-salamander]. The cipher was AES-GCM, the internet&apos;s default authenticated encryption. Nothing about it was broken. That is the unsettling part: AES-GCM did &lt;em&gt;exactly&lt;/em&gt; what it promised. It had just never promised the thing everyone assumed it did.&lt;/p&gt;
&lt;p&gt;The result defeated Facebook Messenger&apos;s abuse-reporting scheme, which relied on the encrypted attachment being &lt;em&gt;bound&lt;/em&gt; to the key that produced it. The authors called their construction &quot;invisible salamanders&quot; [@dodis2018-salamander]. Your reflex on reading this -- &quot;but it is &lt;em&gt;authenticated&lt;/em&gt;, so how did the tag pass?&quot; -- is the entire subject of this article. The word &quot;authenticated&quot; carries less than you think.&lt;/p&gt;
&lt;p&gt;Now the second failure, because one break only shows one edge. In 2016, researchers scanning the live internet found 184 HTTPS servers reusing a &lt;code&gt;(key, nonce)&lt;/code&gt; pair under AES-GCM [@bock2016]. A repeated nonce in GCM is not a hygiene slip that costs you a little margin. It instantly leaks the XOR of the two plaintexts, and -- via an attack Antoine Joux had described to NIST a decade earlier, in 2006 -- it lets an attacker forge arbitrary messages [@joux2006]. The confidentiality half of that failure is easy to see for yourself.&lt;/p&gt;
&lt;p&gt;{`
// A stream-cipher-style AEAD (GCM, ChaCha20-Poly1305) turns the key+nonce
// into a keystream, then XORs it with the plaintext. Reuse the nonce and
// you reuse the keystream -- so it cancels out of the XOR of two ciphertexts.
const keystream = [0x9e, 0x37, 0xb1, 0xf2, 0x4a];   // same (key, nonce) =&amp;gt; same keystream
const xor = (a, b) =&amp;gt; a.map((x, i) =&amp;gt; x ^ b[i]);&lt;/p&gt;
&lt;p&gt;const p1 = [...&apos;HELLO&apos;].map(c =&amp;gt; c.charCodeAt(0));
const p2 = [...&apos;WORLD&apos;].map(c =&amp;gt; c.charCodeAt(0));&lt;/p&gt;
&lt;p&gt;const c1 = xor(p1, keystream);   // ciphertext 1
const c2 = xor(p2, keystream);   // ciphertext 2, SAME nonce&lt;/p&gt;
&lt;p&gt;// Attacker sees only c1 and c2, never the key or keystream:
const leaked = xor(c1, c2);      // == p1 XOR p2, the keystream is gone
const recovered = xor(leaked, p2);  // and if p2 is ever guessed, p1 falls out
console.log(&apos;c1 XOR c2 leaks P1 XOR P2:&apos;, leaked.join(&apos;,&apos;));
console.log(&apos;recovered P1:&apos;, String.fromCharCode(...recovered));
`}&lt;/p&gt;
&lt;p&gt;Neither of these was a broken cipher. AES-GCM met its specification in both cases. The salamander bound nothing to a key because AES-GCM was never designed to; the TLS servers leaked plaintext because they violated the one contract GCM cannot survive. Both failures were the same shape: a deployment reached for an AEAD and then stepped on the one sharp edge its designers had moved somewhere the deployment could not avoid.&lt;/p&gt;
&lt;p&gt;That is the thesis of this field guide. Authenticated encryption with associated data (AEAD) fused confidentiality and integrity so completely that composing them by hand became a solved problem. The remaining hard problem is choosing &lt;em&gt;among&lt;/em&gt; AEADs, and that choice turns on three edges: the &lt;strong&gt;nonce contract&lt;/strong&gt;, the &lt;strong&gt;performance and hardware profile&lt;/strong&gt;, and the &lt;strong&gt;commitment&lt;/strong&gt; axis.&lt;/p&gt;
&lt;p&gt;Part 1 of this series defined an AEAD&apos;s guarantees as &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;IND-CPA plus INT-CTXT&lt;/a&gt;; commitment is a robustness property &lt;em&gt;beyond&lt;/em&gt; both. Part 6 showed the padding-oracle cost of the botched integrity AEAD was built to remove.The nonce-reuse breaks are widely mis-cited to CVE-2016-0270. That CVE actually names IBM Domino, and the NVD entry itself warns it has been &quot;incorrectly used for GCM nonce reuse issues in other products&quot; [@cve-2016-0270].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; &quot;Which AEAD should I use?&quot; is not a ranking. It is a function over three edges: pick the construction whose failure mode your deployment can &lt;em&gt;guarantee&lt;/em&gt; it will never trigger. The salamander triggered the commitment edge; the TLS servers triggered the nonce edge. Same lesson, two edges.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If &quot;authenticated&quot; does not mean &quot;bound to one key,&quot; and &quot;encrypted&quot; does not survive a repeated nonce, then what &lt;em&gt;exactly&lt;/em&gt; does an AEAD promise, and where does each promise stop? To answer that, we have to go back to the moment when &quot;encrypt&quot; and &quot;authenticate&quot; were still two separate calls.&lt;/p&gt;
&lt;h2&gt;2. From Two Calls to One&lt;/h2&gt;
&lt;p&gt;Here is a question that sounds trivial and is not: you have a cipher and you have a message authentication code (MAC). In which order do you apply them? Most engineers guess. The wrong guess shipped in SSL, IPsec, and SSH, and it took the field a decade of theory to sort out which guess was which.&lt;/p&gt;
&lt;p&gt;There are three orderings. &lt;strong&gt;Encrypt-and-MAC&lt;/strong&gt; authenticates the plaintext and appends the tag beside the ciphertext (SSH). &lt;strong&gt;MAC-then-Encrypt&lt;/strong&gt; authenticates the plaintext, then encrypts plaintext and tag together (SSL and, for years, TLS). &lt;strong&gt;Encrypt-then-MAC&lt;/strong&gt; encrypts, then authenticates the &lt;em&gt;ciphertext&lt;/em&gt; (IPsec).&lt;/p&gt;
&lt;p&gt;In 2000, Mihir Bellare and Chanathip Namprempre proved these are not interchangeable: only Encrypt-then-MAC generically preserves both privacy and integrity for &lt;em&gt;any&lt;/em&gt; secure cipher and &lt;em&gt;any&lt;/em&gt; secure MAC [@bn2000]. A year later, Hugo Krawczyk showed the authenticate-then-encrypt method used in SSL is not generically secure while Encrypt-then-MAC is [@krawczyk2001]. Part 6 is the sequel: MAC-then-encrypt is exactly the door a &lt;a href=&quot;https://paragmali.com/blog/they-read-your-plaintext-without-breaking-your-cipher-a-fiel/&quot; rel=&quot;noopener&quot;&gt;padding oracle&lt;/a&gt; walks through.&lt;/p&gt;

One keyed call that provides confidentiality of the plaintext, integrity of the plaintext, and integrity (but not secrecy) of an extra header called the associated data. The uniform interface was standardized in RFC 5116 [@rfc5116].
&lt;p&gt;The lesson cut deeper than &quot;pick Encrypt-then-MAC.&quot; If two correct primitives can combine into an &lt;em&gt;insecure&lt;/em&gt; whole, hand-composition is a footgun no matter how good the parts are. The fix was to make a &lt;em&gt;single&lt;/em&gt; primitive own both goals, so no protocol designer could botch the join.&lt;/p&gt;
&lt;p&gt;Charanjit Jutla showed the way in 2001 with IAPM and IACBC, modes that delivered message integrity in essentially one pass, almost for free [@jutla2001]. The same year, Phillip Rogaway and coauthors published OCB, a one-pass block-cipher mode built for exactly this [@ocb2001]. Then, in 2002, Rogaway named the primitive: real messages carry a header -- routing bytes, version numbers, sequence counters -- that must be &lt;em&gt;authenticated&lt;/em&gt; but not &lt;em&gt;encrypted&lt;/em&gt;, because intermediaries need to read it. His &quot;Authenticated-Encryption with Associated-Data&quot; formalized that extra input [@rogaway2002-ad].&lt;/p&gt;

The header bytes an AEAD authenticates but does not encrypt: routing information, protocol versions, sequence numbers. Tampering with the associated data makes tag verification fail, but the associated data itself travels in the clear [@rogaway2002-ad].
&lt;p&gt;By 2008, David McGrew closed the loop with RFC 5116, which defined the universal interface every construction in this article implements: &lt;code&gt;AEAD-Encrypt(key, nonce, associated_data, plaintext)&lt;/code&gt; returns &lt;code&gt;ciphertext || tag&lt;/code&gt;, and the matching decrypt either returns the plaintext or a single, uninformative failure [@rfc5116]. One call in, one call out, no way to reverse the order of two primitives because there is only one primitive.&lt;/p&gt;

A value that must be unique for every encryption under a given key. The mode, not the label, sets the exact requirement: some AEADs tolerate only uniqueness, others tolerate randomness, and a few tolerate repetition. Nonce generation is the subject of Part 2 of this series.

timeline
    title The road to the AEAD portfolio
    2000 : Encrypt-then-MAC proven the safe order
    2001 : IAPM and OCB, single-pass native AE
    2002 : Rogaway names AEAD and adds associated data
    2003 : CCM standardized in RFC 3610
    2004 : GCM published by McGrew and Viega
    2006 : Joux forbidden attack, SIV misuse-resistance
    2008 : RFC 5116 fixes the universal interface
    2013 : CAESAR opens, AEGIS announced
    2016 : Nonce reuse found on live TLS servers
    2018 : Invisible salamanders break message franking
    2019 : CAESAR portfolio chosen, OCB2 broken
    2021 : Partitioning oracles weaponize non-commitment
    2025 : NIST standardizes Ascon-AEAD128
&lt;p&gt;The interface was settled by 2008. But an interface is a promise about &lt;em&gt;shape&lt;/em&gt;, not about &lt;em&gt;sharp edges&lt;/em&gt;. Two constructions that satisfy &lt;code&gt;AEAD-Encrypt&lt;/code&gt; to the letter can fail in completely different ways, and the very first AEADs to ship proved it. The one that reached the most devices did not even win on technical merit. It won because of a patent.&lt;/p&gt;
&lt;h2&gt;3. The Welded Modes: CCM and GCM&lt;/h2&gt;
&lt;p&gt;The first AEAD to reach a billion devices was not chosen by a cryptographer. It was chosen, in effect, by a patent lawyer. When IEEE 802.11i (the security amendment that became WPA2) needed an authenticated mode, the elegant candidate was OCB -- but OCB was patent-encumbered, and the working group would not build a wireless standard on it. So 802.11i standardized the plodding, unpatented alternative: &lt;strong&gt;AES-CCM&lt;/strong&gt;. By device count, that decision made CCM one of the most widely deployed AEADs in the world.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AES-CCM = Counter mode + CBC-MAC.&lt;/strong&gt; Doug Whiting, Russ Housley, and Niels Ferguson specified it in RFC 3610 in 2003, and NIST blessed it as SP 800-38C the next year [@rfc3610][@sp80038c]. It runs the message through CBC-MAC for the tag, then encrypts with &lt;a href=&quot;https://paragmali.com/blog/the-ciphertext-was-unbreakable-the-attacker-rewrote-it-anyw/&quot; rel=&quot;noopener&quot;&gt;counter (CTR) mode&lt;/a&gt;: two passes, a strictly serial MAC, and the message length needed up front, which makes streaming awkward. What that price buys is frugality -- nothing but AES, a tiny code footprint, no second primitive to implement.&lt;/p&gt;
&lt;p&gt;That profile is why CCM owns the constrained niche -- Bluetooth Low Energy [@bluetooth-core], Zigbee [@ieee802154], and the TLS &lt;code&gt;CCM_8&lt;/code&gt; ciphersuites. IEEE 802.11i also made CCMP the mandatory data-confidentiality protocol for WPA2 [@ieee80211i], and CCM rode WPA2/CCMP into the overwhelming majority of Wi-Fi hardware over the following decade.Zigbee and 802.15.4 use a variant called CCM*, which additionally permits integrity-only or encryption-only operation, unlike the strict RFC 3610 CCM that always does both. So the same plodding, all-AES mode anchors two very different worlds: the smallest constrained devices and the Wi-Fi layer that WPA2 secures. These are the building blocks from Part 5, welded to a MAC.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AES-GCM = Counter mode + GHASH.&lt;/strong&gt; David McGrew and John Viega published Galois/Counter Mode in 2004, and NIST standardized it as SP 800-38D in 2007 [@mcgrew2004-gcm][@sp80038d]. GCM is everything CCM is not: one pass, fully parallelizable, and, on any CPU with AES-NI and CLMUL, blisteringly fast. It is the internet default, and AES-128-GCM is the single mandatory-to-implement (MUST) AEAD cipher suite in TLS 1.3, per RFC 8446 Section 9.1 -- a status the spec grants to no other AEAD [@rfc8446]. To understand every later failure, you need one fact about how its tag works.&lt;/p&gt;

An authenticator of the form $\text{tag} = H_k(\text{message}) + s$, where $H_k$ is a fast keyed universal hash and $s$ is a **one-time** secret mask. It is unforgeable only if $s$ is never reused. Repeat the mask and the algebraic structure of $H_k$ leaks. This is the shared root of both GHASH (AES-GCM) and Poly1305 (ChaCha20-Poly1305) [@bernstein-poly1305].
&lt;p&gt;GCM&apos;s tag is a one-time Wegman-Carter MAC. GHASH evaluates a polynomial over the finite field $\mathrm{GF}(2^{128})$, with the ciphertext and associated-data blocks as coefficients, at a single secret point $H = \operatorname{AES}_k(0^{128})$. It then masks the result with a per-nonce keystream block:&lt;/p&gt;
&lt;p&gt;$$T = \operatorname{GHASH}_H(A, C) \oplus \operatorname{AES}_k(J_0), \qquad H = \operatorname{AES}_k(0^{128})$$&lt;/p&gt;
&lt;p&gt;where $J_0$ is the initial counter block derived from the nonce. The mask $\operatorname{AES}_k(J_0)$ is the one-time secret $s$. It is one-time &lt;em&gt;only because the nonce is unique&lt;/em&gt;. Repeat the nonce and you repeat $J_0$, so you repeat the mask, and now two tags share it -- which turns GHASH&apos;s linear structure into a system of polynomial equations an attacker can solve.&lt;/p&gt;

The universal hash inside AES-GCM: a polynomial in the secret point $H = \operatorname{AES}_k(0^{128})$ evaluated over $\mathrm{GF}(2^{128})$. Because the polynomial is linear in its coefficients, recovering $H$ lets an attacker forge tags for chosen messages [@joux2006].

flowchart LR
    N[&quot;Nonce and counter&quot;] --&amp;gt; CTR[&quot;AES counter mode&quot;]
    K[&quot;Key&quot;] --&amp;gt; CTR
    CTR --&amp;gt; KS[&quot;Keystream&quot;]
    P[&quot;Plaintext&quot;] --&amp;gt; X((&quot;XOR&quot;))
    KS --&amp;gt; X
    X --&amp;gt; C[&quot;Ciphertext&quot;]
    C --&amp;gt; GH[&quot;GHASH polynomial at secret point H&quot;]
    AD[&quot;Associated data&quot;] --&amp;gt; GH
    K --&amp;gt; H[&quot;H is AES of the zero block&quot;]
    H --&amp;gt; GH
    GH --&amp;gt; M((&quot;XOR one-time mask&quot;))
    K --&amp;gt; J[&quot;AES of the first counter block&quot;]
    J --&amp;gt; M
    M --&amp;gt; T[&quot;Authentication tag&quot;]
&lt;p&gt;That single design choice hands GCM two sharp edges the rest of this article pays for. The first is the &lt;strong&gt;nonce contract&lt;/strong&gt;: reuse is not a weakness, it is a detonator.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For every nonce-respecting AEAD -- AES-GCM, ChaCha20-Poly1305, AES-CCM, OCB3, AEGIS, and Ascon -- one repeated &lt;code&gt;(key, nonce)&lt;/code&gt; pair is an immediate, total break of confidentiality for those two messages, and, with a second collision, of authenticity for the whole key [@joux2006].One nonce collision leaks $P_1 \oplus P_2$ right away. Uniquely pinning the secret point $H$ for universal forgery generally needs at least two collisions, since a single pair of equations leaves multiple candidate roots. &quot;One repeat is game over for those messages&quot; is correct; &quot;one repeat exposes $H$&quot; is not quite [@bock2016].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The second edge is quieter and just as dangerous, because it is two different numbers people constantly merge into one.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; SP 800-38D fixes a &lt;strong&gt;per-message&lt;/strong&gt; plaintext limit of $2^{39}-256$ bits (about 64 GiB), set by 32-bit counter-mode block-space exhaustion, and, separately, a &lt;strong&gt;per-key&lt;/strong&gt; invocation cap of roughly $2^{32}$ messages when nonces are random 96-bit values, driven by the birthday bound on nonce collision [@sp80038d]. These are two different mechanisms, not two readings of one bound (the full derivation is in Section 8). The multi-user bounds that justify TLS 1.3&apos;s nonce randomization are tight [@htt2018-gcm]. Enforce both limits. Conflating them is the single most common accuracy error on this primitive.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;GCM gave the internet a one-pass, hardware-fast AEAD, and in the same stroke handed it a mode where one repeated nonce is not a bug but a catastrophe. This is the same GCM negotiated inside shipping Windows protocols such as &lt;a href=&quot;https://paragmali.com/blog/rotating-every-cipher-schannel-and-the-twenty-year-algorithm/&quot; rel=&quot;noopener&quot;&gt;Schannel TLS&lt;/a&gt; and SMB 3 encryption [@ms-schannel-tls][@ms-smb-encryption]. Antoine Joux saw the danger in 2006 [@joux2006]. It took the rest of the world ten years, and a scan of the live internet, to believe him.&lt;/p&gt;
&lt;h2&gt;4. A Trunk With Branches&lt;/h2&gt;
&lt;p&gt;The family tree of AEAD is not a ladder from worst to best. Draw it that way and none of the modern constructions make sense, because none of them strictly dominates GCM. Draw it correctly and the whole field snaps into focus: it is a &lt;strong&gt;trunk with branches&lt;/strong&gt;. The trunk is linear -- generic composition, then single-pass native AE, then the welded deployed modes CCM and GCM. Then, at GCM, the tree &lt;em&gt;splits&lt;/em&gt;, because GCM&apos;s edges each forced a &lt;em&gt;separate&lt;/em&gt; successor. No later generation wins. That is the entire point.&lt;/p&gt;

flowchart TD
    G0[&quot;Generic composition, Encrypt-then-MAC&quot;] --&amp;gt; G1[&quot;Single-pass native AE, IAPM and OCB&quot;]
    G1 --&amp;gt; G2[&quot;Welded deployed modes, CCM and GCM&quot;]
    G2 --&amp;gt; E1[&quot;Nonce edge&quot;]
    G2 --&amp;gt; E2[&quot;Hardware edge&quot;]
    G2 --&amp;gt; E3[&quot;Commitment edge&quot;]
    E1 --&amp;gt; S1[&quot;SIV and AES-GCM-SIV, misuse-resistant&quot;]
    E2 --&amp;gt; S2[&quot;ChaCha20-Poly1305, constant-time in software&quot;]
    E2 --&amp;gt; S4[&quot;AEGIS and Ascon, hardware and footprint co-design&quot;]
    E3 --&amp;gt; S3[&quot;Committing transforms, added on top&quot;]
&lt;h3&gt;The hardware edge: ChaCha20-Poly1305&lt;/h3&gt;
&lt;p&gt;GHASH is hard to make constant-time without the CLMUL instruction, and table-based AES is both slow and timing-leaky without AES-NI. Around 2013, that was the reality on most phones and ARM chips: no AES hardware, so GCM was either slow or a cache-timing side channel. Daniel Bernstein&apos;s answer kept GCM&apos;s exact nonce-respecting contract but threw out the parts that needed special silicon.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ChaCha20&lt;/strong&gt; is an ARX cipher -- addition, rotation, XOR -- with no S-box tables to leak timing, so it is constant-time by construction [@bernstein-chacha]. &lt;strong&gt;Poly1305&lt;/strong&gt; is a one-time Wegman-Carter MAC over a prime field rather than $\mathrm{GF}(2^{128})$, and its one-time key is derived from a ChaCha keystream block [@bernstein-poly1305].Poly1305&apos;s one-time key comes from a ChaCha-derived block, not from AES. ChaCha20-Poly1305 never calls AES at all -- a common misconception, since GCM&apos;s mask does use AES.&lt;/p&gt;
&lt;p&gt;The IETF standardized the pair as an AEAD in RFC 8439 [@rfc8439], and it now protects TLS 1.3, WireGuard, OpenSSH, and the &lt;code&gt;age&lt;/code&gt; file-encryption tool [@wireguard-protocol][@openssh-chacha][@age-spec]. Its normative status is widely overstated: in TLS 1.3, ChaCha20-Poly1305 is a &lt;em&gt;recommended&lt;/em&gt; (SHOULD) cipher suite, not mandatory-to-implement, per RFC 8446 Section 9.1 -- a default by adoption, not by requirement [@rfc8446]. It is not weaker than GCM; it is the same nonce contract on a different, table-free engine.XChaCha20-Poly1305 extends the nonce to 192 bits, large enough that random nonces essentially never collide. That is a &lt;em&gt;mitigation&lt;/em&gt; of nonce-collision anxiety, not misuse-resistance -- reuse the full 192-bit nonce and it fails exactly like GCM.&lt;/p&gt;
&lt;h3&gt;The nonce edge: misuse-resistance and SIV&lt;/h3&gt;
&lt;p&gt;Every mode so far detonates on a repeated nonce, and reuse keeps happening in the real world: cloned virtual machines that resume with identical RNG state, counters reset by a crash, embedded devices with weak entropy at first boot. If the operator cannot guarantee uniqueness, the fix is a mode that &lt;em&gt;survives&lt;/em&gt; the operator&apos;s mistake. In 2006, Phillip Rogaway and Thomas Shrimpton formalized exactly that goal and built the first construction for it [@rs2006].&lt;/p&gt;

An AEAD whose failure under a repeated nonce is graceful, not catastrophic. Repeating a `(key, nonce, associated_data, plaintext)` tuple leaks only whether two encryptions were of the *same* message -- message equality -- and nothing else. It never leaks a plaintext XOR and never hands over the authentication subkey [@rs2006][@rfc8452].
&lt;p&gt;The mechanism is a single clean idea: derive the initialization vector &lt;em&gt;synthetically&lt;/em&gt;, as a pseudorandom function of the entire message, rather than accepting it from the caller. The tag &lt;em&gt;is&lt;/em&gt; the IV.&lt;/p&gt;

A construction in which the initialization vector is computed as a MAC over the associated data and the full plaintext, then reused as the authentication tag. Two different messages under a repeated nonce produce different synthetic IVs, so their keystreams differ and nothing leaks [@rs2006].

flowchart TD
    K[&quot;Key&quot;] --&amp;gt; MAC[&quot;MAC over associated data and full plaintext&quot;]
    AD[&quot;Associated data&quot;] --&amp;gt; MAC
    N[&quot;Nonce&quot;] --&amp;gt; MAC
    P[&quot;Plaintext&quot;] --&amp;gt; MAC
    MAC --&amp;gt; IV[&quot;Synthetic IV, which is also the tag&quot;]
    IV --&amp;gt; CTR[&quot;Counter-mode encryption&quot;]
    K --&amp;gt; CTR
    P --&amp;gt; CTR
    CTR --&amp;gt; C[&quot;Ciphertext&quot;]
    IV --&amp;gt; T[&quot;Tag sent alongside ciphertext&quot;]
&lt;p&gt;That picture also explains the price, which returns as a theorem in Section 8: to compute the IV you must read the whole message first, so SIV cannot emit its first ciphertext byte until it has seen the last plaintext byte. It is inherently two-pass.&lt;/p&gt;
&lt;p&gt;Two deployable forms exist. &lt;strong&gt;AES-SIV&lt;/strong&gt;, standardized by Dan Harkins in RFC 5297, targets deterministic authenticated encryption and key wrapping [@rfc5297]. &lt;strong&gt;AES-GCM-SIV&lt;/strong&gt;, from Shay Gueron, Adam Langley, and Yehuda Lindell in RFC 8452, keeps AES-NI and CLMUL speed while adding misuse-resistance, building on the Gueron-Lindell GCM-SIV design [@rfc8452][@gl2015-gcmsiv].AES-GCM-SIV hashes with POLYVAL, a little-endian sibling of GHASH, and derives a fresh message-authentication and encryption key per nonce -- a structure whose tight multi-user bounds were later established by Bose, Hoang, and Tessaro [@bht2018].&lt;/p&gt;

&quot;...two authenticated encryption algorithms that are nonce misuse resistant -- that is, they do not fail catastrophically if a nonce is repeated.&quot; -- RFC 8452 [@rfc8452]
&lt;p&gt;Read that phrasing carefully, because it is the most misquoted sentence about SIV. &quot;Do not fail catastrophically&quot; is not &quot;do not fail.&quot; A repeated nonce in AES-GCM-SIV still leaks message equality: an attacker learns that two ciphertexts encrypt the same plaintext, which is a real and sometimes serious leak. SIV makes reuse &lt;em&gt;survivable&lt;/em&gt;, never &lt;em&gt;free&lt;/em&gt;.&lt;/p&gt;
&lt;h3&gt;The elegance detour: OCB3 and the patent that chose a standard&lt;/h3&gt;
&lt;p&gt;Two edges now had answers, each on its own branch. A third construction sits off to the side -- not because it is worse, but because history was unkind to it. &lt;strong&gt;OCB3&lt;/strong&gt;, finalized by Ted Krovetz and Phillip Rogaway in 2011 and standardized as RFC 7253, is arguably the most elegant AEAD ever standardized: one block-cipher key, one pass, fully parallel, roughly one cipher call per block [@kr2011-ocb3][@rfc7253].&lt;/p&gt;
&lt;p&gt;Its mechanism is where the elegance lives. Each block is masked by a key-derived offset -- $\text{Offset}&lt;em&gt;i = \text{Offset}&lt;/em&gt;{i-1} \oplus L_{\text{ntz}(i)}$, a Gray-code walk over precomputed $L$ values -- and wrapped as $C_i = \text{Offset}_i \oplus \operatorname{ENCIPHER}(K, P_i \oplus \text{Offset}_i)$, while a &lt;em&gt;single&lt;/em&gt; running plaintext checksum, $\text{Checksum} = P_1 \oplus P_2 \oplus \cdots$, produces the tag in the &lt;em&gt;same pass&lt;/em&gt; [@rfc7253]. No second key, no separate MAC. It matches or beats GCM in software. And it lost anyway -- not for a technical reason.A widely repeated claim is that OCB3 is &lt;em&gt;inverse-free&lt;/em&gt;. It is not. OCB-DECRYPT recovers each block as $P_i = \text{Offset}_i \oplus \operatorname{DECIPHER}(K, C_i \oplus \text{Offset}_i)$, calling the block-cipher inverse $E_K^{-1}$ (RFC 7253 Section 4.3), so a hardware implementation needs both the AES encrypt &lt;em&gt;and&lt;/em&gt; decrypt circuits [@rfc7253]. Inverse-free is the property of the CTR, stream, and sponge modes -- AES-GCM, AES-CCM, ChaCha20-Poly1305, AEGIS, and Ascon -- and OCB is precisely the lineage that trades it away for single-primitive elegance.&lt;/p&gt;

OCB was patented from birth. When the 802.11i working group picked WPA2&apos;s cipher in 2003, it passed over OCB precisely because of those patents and chose the unencumbered CCM instead. Rogaway later granted free licenses for open-source and non-military use, but the damage was done: every major protocol had already standardized on GCM and ChaCha20-Poly1305 in the years the patents were live. In 2021 Rogaway released OCB into the public domain outright -- too late to matter [@kr2021-ocb]. The honest answer to &quot;when should I reach for OCB3 in 2026?&quot; is &quot;almost never, and the reason is a patent grave, not a design flaw.&quot;
&lt;h3&gt;The dead end: OCB2 and the limits of &quot;provably secure&quot;&lt;/h3&gt;
&lt;p&gt;There is one more branch, and it is a warning. Between OCB1 and OCB3 sat OCB2, an ISO-standardized refinement with a security proof. In 2019, Akiko Inoue, Tetsu Iwata, Kazuhiko Minematsu, and Bertram Poettering broke it outright: universal forgery &lt;em&gt;and&lt;/em&gt; full plaintext recovery, because OCB2 used the XEX* tweakable cipher outside the regime its proof actually covered [@inoue2019-ocb2].&lt;/p&gt;

The OCB2 break is the cautionary counterpoint to every &quot;but it has a proof&quot; argument. OCB2 was standardized by ISO/IEC and carried a published security proof, and it was still broken end to end. A proof secures a *model*; if the construction steps outside the model&apos;s assumptions, the proof guarantees nothing. Tellingly, the same attack left OCB1 and OCB3 untouched [@inoue2019-ocb2] -- a dead-end twig on a branch whose siblings survived.
&lt;p&gt;Two of GCM&apos;s three edges now had dedicated successors, and the family tree looked complete. But every construction so far -- GCM, ChaCha20-Poly1305, even the misuse-resistant SIV family -- quietly shared a third assumption nobody had thought to check: that &quot;authenticated&quot; meant &quot;bound to one key.&quot; In 2018, that assumption broke in production, on the desk of a Facebook security engineer holding a single file with two faces.&lt;/p&gt;
&lt;h2&gt;5. The Commitment Reckoning and the CAESAR Crucible&lt;/h2&gt;
&lt;p&gt;Now we can explain the salamanders. Recall the mechanism from Section 3: a GCM tag is a one-time Wegman-Carter value, $\operatorname{GHASH}_H(A,C) \oplus \operatorname{AES}_k(J_0)$, and GHASH is &lt;em&gt;linear&lt;/em&gt; in its coefficients. An attacker who gets to pick two keys, $k_1$ and $k_2$, can treat &quot;make the tag verify under both&quot; as a system of linear equations over $\mathrm{GF}(2^{128})$ and simply solve it, producing one ciphertext that decrypts to two chosen, meaningful plaintexts and passes verification under each key [@dodis2018-salamander].&lt;/p&gt;
&lt;p&gt;Nothing is broken. GCM guarantees confidentiality and integrity; it never promised that a ciphertext binds to a single key. That promise has a name, and until 2018 almost nobody was asking for it.&lt;/p&gt;

A graded robustness property beyond confidentiality and integrity. CMT-1 means a ciphertext binds to exactly one *key*; CMT-4 means it binds to the full *context* -- key, nonce, associated data, and message. Bellare and Hoang proved that CMT-1 does not imply CMT-4: committing to the key is strictly weaker than committing to everything [@bh2022-commit]. RFC 9771 standardized the vocabulary [@rfc9771].
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; &quot;Authenticated&quot; never meant &quot;committed.&quot; An AEAD&apos;s guarantees -- IND-CPA plus INT-CTXT -- say nothing about binding a ciphertext to one key. Commitment is a third, orthogonal axis. That is why every default AEAD was non-committing and nobody noticed for fifteen years: the property was simply outside the definition everyone was proving.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two ideas define the modern state of the art, and both come from &lt;em&gt;refusing&lt;/em&gt; to treat &quot;cipher plus separate hash&quot; as the end of the design space.&lt;/p&gt;
&lt;p&gt;The first is &lt;strong&gt;robustness as an explicit goal&lt;/strong&gt; -- against the operator and against the adversary who supplies the key. The operator side is MRAE from Section 4, elevated from a mode into a principle. The adversary side is committing AEAD, and its timeline is a tidy example of a theoretical curiosity becoming an operational threat.&lt;/p&gt;
&lt;p&gt;Invisible salamanders (2018) showed a non-committing AEAD could defeat message franking [@dodis2018-salamander]. Then, in 2021, Julia Len, Paul Grubbs, and Thomas Ristenpart turned it into a weapon: &lt;strong&gt;partitioning oracle attacks&lt;/strong&gt; use the missing commitment to recover passwords and other low-entropy keys, crafting one ciphertext that decrypts under many candidate keys and using each success or failure to bisect the key space [@len2021-partition].&lt;/p&gt;
&lt;p&gt;Mihir Bellare and Viet Tung Hoang then built the graded CMT-1 through CMT-4 theory and cheap transforms that add full commitment with no ciphertext-size increase [@bh2022-commit]; Ange Albertini and coauthors independently catalogued the abuses and a padding-based fix [@albertini2022], with further theory from John Chan and Phillip Rogaway [@chan2022]. Sanketh Menda and coauthors then showed the gap is portfolio-wide, with context-discovery attacks against CCM, EAX, SIV, GCM, and OCB3 [@menda2023]. RFC 9771 (2025) finally fixed the vocabulary the field had been improvising [@rfc9771].&lt;/p&gt;

A vetted AEAD gives you confidentiality and integrity and stops there. Commitment is a third axis nobody put in the definition -- which is exactly why one ciphertext could wear two faces.
&lt;p&gt;The second idea is &lt;strong&gt;native co-design&lt;/strong&gt;: stop bolting a cipher to a hash and instead build the AEAD &lt;em&gt;around&lt;/em&gt; the hardware or footprint you actually have. Hongjun Wu and Bart Preneel&apos;s AEGIS (2013) is the AES-hardware extreme [@wu2013-aegis].&lt;/p&gt;
&lt;p&gt;It keeps a large internal state -- eight 128-bit words in AEGIS-128L -- seeded directly from the key and nonce; the &lt;strong&gt;AES round function&lt;/strong&gt; updates that state as it absorbs associated data and plaintext, encryption &lt;em&gt;squeezes&lt;/em&gt; a keystream out of the state, and the tag comes from the &lt;em&gt;finalized&lt;/em&gt; state. One primitive, one pass, no separate hash, and the IETF draft records that all variants are inverse-free and built from the AES encryption round [@aegis-draft18].&lt;/p&gt;
&lt;p&gt;That structure is &lt;em&gt;why&lt;/em&gt; AEGIS outruns GCM on AES silicon, and &lt;em&gt;why&lt;/em&gt; nonce reuse is catastrophic: the whole state is a function of &lt;code&gt;(key, nonce)&lt;/code&gt;, so a repeat lets an attacker unwind it. Its commitment story is a nuance, not a headline -- a 128-bit AEGIS tag delivers only about 64-bit committing security, while a 256-bit tag pushes cross-key collisions out of reach, so AEGIS is &lt;em&gt;partially&lt;/em&gt; committing by tag length, neither fully committing nor flatly broken [@aegis-draft18].&lt;/p&gt;
&lt;p&gt;At the other extreme, Ascon -- by Christoph Dobraunig, Maria Eichlseder, Florian Mendel, and Martin Schläffer -- uses a single lightweight permutation in a sponge, giving a tiny gate count and a side-channel-friendly structure for constrained hardware [@ascon2021].&lt;/p&gt;

flowchart LR
    KN[&quot;Key and nonce&quot;] --&amp;gt; ST[&quot;Large internal state, eight 128-bit words&quot;]
    ST --&amp;gt; UP[&quot;AES round function update&quot;]
    AD[&quot;Associated data&quot;] --&amp;gt; UP
    P[&quot;Plaintext&quot;] --&amp;gt; UP
    UP --&amp;gt; KS[&quot;Squeezed keystream&quot;]
    P --&amp;gt; X((&quot;XOR&quot;))
    KS --&amp;gt; X
    X --&amp;gt; C[&quot;Ciphertext&quot;]
    UP --&amp;gt; FIN[&quot;Finalized state&quot;]
    FIN --&amp;gt; T[&quot;Authentication tag&quot;]

A mode built from one public permutation whose state is split into a &quot;rate&quot; (absorbs input and is squeezed for output) and a &quot;capacity&quot; (the hidden security margin). A single permutation absorbs the key, nonce, associated data, and plaintext, then squeezes keystream and a tag -- no separate cipher and hash. Ascon is a sponge [@ascon2021].
&lt;p&gt;What forced this whole portfolio into existence was a contest. The &lt;strong&gt;CAESAR competition&lt;/strong&gt; (2013-2019), organized by Daniel Bernstein, was a public, multi-year, break-it-in-the-open bake-off [@caesar-home]. Its most important decision was structural: rather than crown a single winner, it selected a &lt;em&gt;portfolio across three use cases&lt;/em&gt; -- itself the thesis of this article. The winners were Ascon and ACORN for lightweight use, AEGIS-128 and OCB for high-performance use, and Deoxys-II (first choice) and COLM for defense in depth [@caesar-portfolio].Deoxys-II is built on the TWEAKEY framework for tweakable block ciphers, by Jérémy Jean, Ivica Nikolić, Thomas Peyrin, and Yannick Seurin [@caesar-portfolio].&lt;/p&gt;
&lt;p&gt;Note carefully: &lt;strong&gt;Deoxys-II is a CAESAR winner, not an also-ran&lt;/strong&gt; -- it simply was never deployed at scale. The real also-rans are the candidates eliminated during the competition, including MORUS, which took a certificational break from Tomer Ashur, Maria Eichlseder, and coauthors and did not make the final portfolio [@morus2018].&lt;/p&gt;
&lt;p&gt;The competition ended in 2019 with no single champion, on purpose. So where does that leave a working engineer in 2026, staring at seven names in a crypto library&apos;s documentation? To choose well, you need the current map: exactly what each construction is, where it wins, and where it bites.&lt;/p&gt;
&lt;h2&gt;6. The 2026 Portfolio, Precisely&lt;/h2&gt;
&lt;p&gt;The current portfolio has a stable shape: two defaults and five specialists. By deployment, the two internet defaults are AES-GCM and ChaCha20-Poly1305 -- but normatively they are not equals. In TLS 1.3 only AES-128-GCM is mandatory-to-implement (MUST); AES-256-GCM and ChaCha20-Poly1305 are both &lt;em&gt;recommended&lt;/em&gt; (SHOULD), the same normative level as each other, per RFC 8446 Section 9.1 [@rfc8446]. Everything else is a deliberate escalation. Here is each construction on its edges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AES-GCM.&lt;/strong&gt; Counter mode plus a one-time GHASH tag; one pass, parallel, &lt;code&gt;O(1)&lt;/code&gt; online state [@mcgrew2004-gcm]. Excels on any CPU with AES-NI and CLMUL, which is every server and most modern clients. Struggles on hardware without those instructions and, above all, on nonce discipline: reuse is fatal and the two ceilings are easy to breach [@sp80038d]. Adoption: the internet default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ChaCha20-Poly1305.&lt;/strong&gt; ARX stream cipher plus a prime-field one-time MAC; constant-time in pure software with no lookup tables [@rfc8439]. Excels precisely where GCM struggles -- phones, embedded ARM, any target without AES hardware. Same nonce contract as GCM, so it struggles on the exact same reuse cliff. Adoption: TLS 1.3, WireGuard, OpenSSH [@wireguard-protocol][@openssh-chacha].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AES-CCM.&lt;/strong&gt; Counter mode plus CBC-MAC; two passes, serial, needs the length up front, but all-AES and tiny [@rfc3610]. Excels in constrained stacks (BLE [@bluetooth-core], Zigbee [@ieee802154]) and inside WPA2 [@ieee80211i]. Struggles on throughput and streaming, and the truncated &lt;code&gt;CCM_8&lt;/code&gt; tag trades a real forgery budget for eight bytes [@rfc3610]. Adoption: enormous by device count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AES-GCM-SIV and AES-SIV.&lt;/strong&gt; Synthetic-IV misuse-resistant modes; two-pass by necessity [@rfc8452][@rfc5297]. Excel when nonce uniqueness cannot be guaranteed: reuse degrades to leaking message equality rather than catastrophe. Struggle on streaming (they must buffer) and still leak equality, so they are survivable, not free. AES-GCM-SIV runs near AES-GCM speed on server hardware, around 0.92 cycles per byte on Broadwell in the original design [@gl2015-gcmsiv]. Adoption: growing where reuse risk is real.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OCB3.&lt;/strong&gt; One block-cipher key, one pass, fully parallel, roughly one cipher call per block -- technically superb [@kr2011-ocb3][@rfc7253]. Excels on elegance and software speed. Struggles on adoption, for the patent reasons in Section 4; it is nonce-respecting like GCM; and, unlike the CTR and stream modes, it is &lt;strong&gt;not&lt;/strong&gt; inverse-free -- decryption calls the AES inverse circuit (RFC 7253 Section 4.3), a genuine hardware cost [@rfc7253]. Adoption: minimal, despite standardization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AEGIS.&lt;/strong&gt; Keystream and authentication driven straight from the AES round function over a large nonce-seeded state; the throughput frontier, around 0.48 cycles per byte for AEGIS-128L on 4 KB messages, faster than CCM, GCM, and OCB [@wu2013-aegis][@aegis-v11]. Excels on raw speed when you control both endpoints. Struggles on maturity and contract: nonce-respecting (catastrophic on reuse, since its whole state is nonce-seeded) and only &lt;em&gt;partially&lt;/em&gt; committing -- about 64-bit committing security at a 128-bit tag, stronger at a 256-bit tag [@aegis-draft18].&lt;/p&gt;
&lt;p&gt;Critically, &lt;strong&gt;AEGIS is still an active Internet-Draft, &lt;code&gt;draft-irtf-cfrg-aegis-aead-18&lt;/code&gt; from October 2025, not an RFC&lt;/strong&gt; -- it states plainly that it &quot;is not an IETF product and is not a standard,&quot; and it expires on 8 April 2026 [@aegis-draft18].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ascon-AEAD128.&lt;/strong&gt; A single 320-bit lightweight permutation in a sponge; tiny state, side-channel-friendly [@ascon2021]. Excels on constrained hardware and gate count. Struggles on server throughput, where AEGIS and GCM win. It is nonce-respecting, but -- unlike the CTR and stream modes -- it commits to its inputs &lt;em&gt;without&lt;/em&gt; any added transform: the tag is squeezed from the sponge&apos;s large hidden capacity after that capacity has already absorbed the key, so one ciphertext binds natively to the key it was produced under. RFC 9771 lists Ascon-AEAD128 as both key-committing and full-committing as-is, not merely as a candidate for a bolt-on fix [@rfc9771]. The currency fact matters here: &lt;strong&gt;NIST SP 800-232 is final (August 2025) and standardizes Ascon-AEAD128&lt;/strong&gt;, a &lt;em&gt;tweaked&lt;/em&gt; variant of CAESAR Ascon-128 using a larger data-absorption rate -- widely reported as 128-bit, up from the CAESAR design&apos;s 64-bit, with the exact parameter in the SP 800-232 body [@sp800232][@nist-ascon-2023]. It is not byte-identical to CAESAR Ascon [@ascon-site].&lt;/p&gt;
&lt;p&gt;A profile only helps if you can actually call the construction, so here is where each one ships as of mid-2026, with each cell cited to that project&apos;s official documentation.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Construction&lt;/th&gt;
&lt;th&gt;Where it ships (libraries and frameworks)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;AES-GCM&lt;/td&gt;
&lt;td&gt;OpenSSL/BoringSSL [@openssl-aes][@boringssl-aead], Go &lt;code&gt;crypto/cipher&lt;/code&gt; [@go-cipher], Java JCA [@java-jca], .NET CNG [@dotnet-aesgcm], WebCrypto (the only AEAD in the browser) [@webcrypto], libsodium (hardware-gated) [@libsodium-aegis]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChaCha20-Poly1305 / XChaCha20&lt;/td&gt;
&lt;td&gt;OpenSSL/BoringSSL [@openssl-chacha][@boringssl-aead], libsodium (default for random-nonce APIs) [@libsodium-aegis], Go &lt;code&gt;x/crypto&lt;/code&gt; [@go-chacha], WireGuard [@wireguard-protocol], OpenSSH [@openssh-chacha], &lt;code&gt;age&lt;/code&gt; [@age-spec]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-CCM&lt;/td&gt;
&lt;td&gt;mbedTLS [@mbedtls-ccm], wolfSSL [@wolfssl-ccm], Wi-Fi [@ieee80211i] / BLE [@bluetooth-core] / Zigbee [@ieee802154] firmware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-GCM-SIV&lt;/td&gt;
&lt;td&gt;BoringSSL/OpenSSL [@boringssl-aead][@openssl-aes] and several bindings; less universal than the defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-SIV&lt;/td&gt;
&lt;td&gt;&lt;code&gt;miscreant&lt;/code&gt;-lineage (deterministic / key-wrap) libraries [@miscreant]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCB3&lt;/td&gt;
&lt;td&gt;some libraries (public domain since 2021 [@kr2021-ocb]); rarely a default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AEGIS&lt;/td&gt;
&lt;td&gt;libsodium (&lt;code&gt;crypto_aead_aegis128l&lt;/code&gt; / &lt;code&gt;aegis256&lt;/code&gt;) [@libsodium-aegis] and a growing set of high-performance libraries; not a TLS suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ascon-AEAD128&lt;/td&gt;
&lt;td&gt;reference and third-party implementations [@ascon-site]; adoption ramping since SP 800-232 [@sp800232]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two entries on that map decide real architectures. &lt;strong&gt;In the browser, AES-GCM is the only AEAD you can call.&lt;/strong&gt;WebCrypto&apos;s &lt;code&gt;SubtleCrypto.encrypt&lt;/code&gt; recognizes RSA-OAEP, AES-CTR, AES-CBC, and AES-GCM, and AES-GCM is the only one of those that authenticates. Browser JavaScript that needs an AEAD without bundling a crypto library therefore has exactly one option [@webcrypto]. And AEGIS, still pre-RFC, is nonetheless callable in production today through libsodium&apos;s &lt;code&gt;crypto_aead_aegis128l&lt;/code&gt; and &lt;code&gt;crypto_aead_aegis256&lt;/code&gt; APIs -- deployability and standardization are not the same axis [@libsodium-aegis].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Eight constructions -- the seven cipher families, with the SIV line counted in its two deployable forms (AES-GCM-SIV and AES-SIV) -- all linear-time, all vetted, and not one strictly dominates the others. CAESAR chose a portfolio across three use cases on purpose, and NIST added Ascon for the constrained end. There is no &quot;best AEAD,&quot; and any article or vendor that names one is hiding an assumption about your deployment [@caesar-portfolio].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two more items complete the 2026 map. The CAESAR defense-in-depth winners, Deoxys-II and COLM, remain excellent and essentially undeployed [@caesar-portfolio]. And committing transforms -- the fix for the commitment edge -- exist and are cheap, but as of mid-2026 none is &lt;em&gt;mandated&lt;/em&gt; in a shipping standard; RFC 9771 supplies vocabulary, not a required construction [@rfc9771][@bh2022-commit].&lt;/p&gt;
&lt;p&gt;Laid side by side, those eight look interchangeable. They are not, and the tool that makes the difference legible is a single table built on the three edges.&lt;/p&gt;
&lt;h2&gt;7. The Decision Matrix&lt;/h2&gt;
&lt;p&gt;This is the one screen to memorize. Everything in the previous six sections collapses into two tables and a flowchart, all built on the same three edges.&lt;/p&gt;
&lt;p&gt;The first table answers the only question that matters at selection time: for each construction, what happens on the &lt;em&gt;first repeated nonce&lt;/em&gt;, what hardware does it want, and does one ciphertext bind to one key?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Construction&lt;/th&gt;
&lt;th&gt;First repeated nonce&lt;/th&gt;
&lt;th&gt;Hardware profile&lt;/th&gt;
&lt;th&gt;Commitment&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;AES-GCM&lt;/td&gt;
&lt;td&gt;Catastrophic: leaks P1 XOR P2, enables forgery&lt;/td&gt;
&lt;td&gt;AES-NI + CLMUL fast&lt;/td&gt;
&lt;td&gt;Not committing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChaCha20-Poly1305&lt;/td&gt;
&lt;td&gt;Catastrophic: same one-time-MAC cliff&lt;/td&gt;
&lt;td&gt;Constant-time in software&lt;/td&gt;
&lt;td&gt;Not committing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-CCM&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;td&gt;All-AES, tiny footprint&lt;/td&gt;
&lt;td&gt;Not committing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-GCM-SIV&lt;/td&gt;
&lt;td&gt;Graceful: leaks message equality only&lt;/td&gt;
&lt;td&gt;AES-NI + CLMUL&lt;/td&gt;
&lt;td&gt;Not committing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-SIV&lt;/td&gt;
&lt;td&gt;Graceful: leaks message equality only&lt;/td&gt;
&lt;td&gt;All-AES, two-pass&lt;/td&gt;
&lt;td&gt;Not committing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCB3&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;td&gt;Fast one-pass software&lt;/td&gt;
&lt;td&gt;Not committing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AEGIS&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;td&gt;AES round function, fastest&lt;/td&gt;
&lt;td&gt;Not fully committing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ascon-AEAD128&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;td&gt;A few thousand gates&lt;/td&gt;
&lt;td&gt;Committing from the sponge, no transform&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read down the commitment column and the reckoning of Section 5 gets sharper than a flat &quot;nobody commits.&quot; Every deployed CTR, stream, and welded construction -- AES-GCM, ChaCha20-Poly1305, AES-CCM, the SIV family, and OCB3 -- is non-committing by default, which is exactly what the salamander and the portfolio-wide context-discovery attacks exploit [@dodis2018-salamander][@menda2023]. Two constructions break the pattern, and it is no accident that both fuse the tag with the cipher state instead of bolting on a separate hash: AEGIS partially commits through tag length, and Ascon commits natively from its sponge [@aegis-draft18][@rfc9771]. So commitment is not a uniform failure across the portfolio -- the commitment edge is a real axis of variation, with Ascon at the strong end, AEGIS in between, and the CTR, stream, and welded modes needing a bolt-on transform. Read the nonce column and only the SIV family survives a repeat, and only by leaking equality [@rfc8452]. The second table adds the structural and performance facts you need once the edges are settled.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Construction&lt;/th&gt;
&lt;th&gt;Passes / parallel&lt;/th&gt;
&lt;th&gt;Online state&lt;/th&gt;
&lt;th&gt;Nonce&lt;/th&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Reported speed&lt;/th&gt;
&lt;th&gt;Inverse-free?&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;AES-GCM&lt;/td&gt;
&lt;td&gt;1 pass, parallel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;96-bit&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;~1 cpb with AES-NI&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;The default, server and client&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChaCha20-Poly1305&lt;/td&gt;
&lt;td&gt;1 pass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;96-bit&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Fast in pure software&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Targets without AES hardware&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-CCM&lt;/td&gt;
&lt;td&gt;2 pass, serial&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7 to 13 byte&lt;/td&gt;
&lt;td&gt;128-bit (&lt;code&gt;CCM_8&lt;/code&gt;: 64)&lt;/td&gt;
&lt;td&gt;Modest&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Constrained stacks, WPA2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-GCM-SIV&lt;/td&gt;
&lt;td&gt;2 pass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(n)&lt;/code&gt; buffered&lt;/td&gt;
&lt;td&gt;96-bit&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;~0.92 cpb on Broadwell&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Nonce-reuse risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-SIV&lt;/td&gt;
&lt;td&gt;2 pass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(n)&lt;/code&gt; buffered&lt;/td&gt;
&lt;td&gt;None or supplied&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Modest&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Key wrap, deterministic AE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCB3&lt;/td&gt;
&lt;td&gt;1 pass, parallel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;up to 120-bit&lt;/td&gt;
&lt;td&gt;up to 128-bit&lt;/td&gt;
&lt;td&gt;Fast in software&lt;/td&gt;
&lt;td&gt;No (needs AES decrypt)&lt;/td&gt;
&lt;td&gt;Elegance (now patent-clear)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AEGIS&lt;/td&gt;
&lt;td&gt;1 pass, parallel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128 or 256-bit&lt;/td&gt;
&lt;td&gt;128 or 256-bit&lt;/td&gt;
&lt;td&gt;~0.48 cpb (AEGIS-128L)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Maximum throughput, both endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ascon-AEAD128&lt;/td&gt;
&lt;td&gt;1 pass&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Small, not server-fast&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;New constrained designs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Reported speeds are platform-dependent and should be read against live cross-platform benchmarks rather than a single headline number; the AEGIS and GCM-SIV figures come from their design papers, and the ECRYPT benchmarking project tracks the rest across many CPUs [@wu2013-aegis][@gl2015-gcmsiv][@bench-aead]. One column earns a second look: OCB3 is the only full-treatment construction that is &lt;em&gt;not&lt;/em&gt; inverse-free, so it alone needs the AES decrypt circuit in hardware -- the honest cost behind its elegance [@rfc7253].&lt;/p&gt;
&lt;p&gt;The tables classify. To &lt;em&gt;choose&lt;/em&gt;, turn them into a procedure. The flowchart below is the thesis rendered as a decision tree: branch on the nonce guarantee first (it is the edge that fails hardest), then on hardware, and treat commitment as an orthogonal final step because no default provides it.&lt;/p&gt;

flowchart TD
    Q1{&quot;Can you guarantee a unique nonce per key?&quot;}
    Q1 --&amp;gt;|No| SIV[&quot;AES-GCM-SIV: move on the nonce edge&quot;]
    Q1 --&amp;gt;|Yes| Q2{&quot;AES-NI and CLMUL on both endpoints?&quot;}
    Q2 --&amp;gt;|Yes| GCM[&quot;AES-GCM&quot;]
    Q2 --&amp;gt;|No| Q2b{&quot;Server or mobile software, or a constrained device?&quot;}
    Q2b --&amp;gt;|Software| CC[&quot;ChaCha20-Poly1305&quot;]
    Q2b --&amp;gt;|Constrained| ASC[&quot;Ascon-AEAD128&quot;]
    SIV --&amp;gt; Q3{&quot;Must one ciphertext bind to exactly one key?&quot;}
    GCM --&amp;gt; Q3
    CC --&amp;gt; Q3
    ASC --&amp;gt; Q3
    Q3 --&amp;gt;|Yes| ADD[&quot;Add a key-commitment transform, unless it already commits like Ascon&quot;]
    Q3 --&amp;gt;|No| DONE[&quot;Ship it&quot;]
&lt;p&gt;The argument is now explicit and testable. &quot;Which AEAD?&quot; reduces to &quot;which edge can your deployment &lt;em&gt;guarantee&lt;/em&gt; it never triggers?&quot; If you cannot guarantee nonce uniqueness, move right on the nonce edge to the SIV family. If you have no AES hardware, move on the hardware edge to ChaCha20-Poly1305 or, for a constrained device, Ascon. If a ciphertext must bind to exactly one key, you must &lt;em&gt;add&lt;/em&gt; something on the commitment edge, because none of the defaults gives it to you. You can even write the function down.&lt;/p&gt;
&lt;p&gt;{`
// &quot;Which AEAD?&quot; is a function of three edges, not a ranking.
function chooseAEAD({ nonceUnique, hasAesHardware, constrained, mustCommitToKey }) {
  let pick;
  if (!nonceUnique) {
    pick = &apos;AES-GCM-SIV&apos;;        // edge 1: cannot guarantee unique nonces
  } else if (hasAesHardware) {
    pick = &apos;AES-GCM&apos;;            // the internet default when AES-NI + CLMUL are present
  } else if (constrained) {
    pick = &apos;Ascon-AEAD128&apos;;      // edge 2: no AES hardware, only a few thousand gates
  } else {
    pick = &apos;ChaCha20-Poly1305&apos;;  // edge 2: no AES hardware, constant-time in software
  }
  // edge 3 is orthogonal: most picks do not commit, so you must ADD a transform --
  // except Ascon, which already commits natively from its sponge (RFC 9771).
  const alreadyCommits = pick === &apos;Ascon-AEAD128&apos;;
  return (mustCommitToKey &amp;amp;&amp;amp; !alreadyCommits)
    ? pick + &apos; + key-commitment transform (this construction does not bind to one key)&apos;
    : pick;
}&lt;/p&gt;
&lt;p&gt;console.log(chooseAEAD({ nonceUnique: true,  hasAesHardware: true,  constrained: false, mustCommitToKey: false }));
console.log(chooseAEAD({ nonceUnique: false, hasAesHardware: true,  constrained: false, mustCommitToKey: false }));
console.log(chooseAEAD({ nonceUnique: true,  hasAesHardware: false, constrained: false, mustCommitToKey: false }));
console.log(chooseAEAD({ nonceUnique: true,  hasAesHardware: true,  constrained: false, mustCommitToKey: true  }));
`}&lt;/p&gt;

&quot;Which AEAD should I use?&quot; is not a ranking from worst to best. It is a function: pick the one whose failure mode your deployment can guarantee it will never trigger.
&lt;p&gt;The table tells you which edge to move. It does not tell you why you sometimes &lt;em&gt;cannot&lt;/em&gt; move all of them at once -- why there is no row that is graceful on reuse &lt;em&gt;and&lt;/em&gt; single-pass &lt;em&gt;and&lt;/em&gt; fully committing &lt;em&gt;and&lt;/em&gt; fastest. That absence is not a gap in the engineering. It is a theorem.&lt;/p&gt;
&lt;h2&gt;8. The Limits Are Theorems&lt;/h2&gt;
&lt;p&gt;Everything so far has been a design choice: move an edge here, pay for it there. Now the hard walls -- the places where no cleverness helps, because a proof says so. There are four, and each maps to an edge; the standard graduate references develop these bounds in full [@boneh-shoup][@katz-lindell].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GCM&apos;s two ceilings, correctly attributed (the nonce edge).&lt;/strong&gt; GCM&apos;s two limits from SP 800-38D are not conservative engineering guesses; they are where the security proof runs out -- and they come from two &lt;em&gt;different&lt;/em&gt; mechanisms people constantly merge into one.&lt;/p&gt;
&lt;p&gt;The per-message limit of $2^{39}-256$ bits (about 64 GiB) is &lt;strong&gt;32-bit counter-mode block-space exhaustion&lt;/strong&gt;, not a GHASH property: with a 96-bit nonce GCM forms the initial counter block $J_0 = \mathrm{IV} \parallel 0^{31} \parallel 1$, the increment function advances only the low 32 bits, so counter mode emits at most $2^{32}-2$ keystream blocks, and $(2^{32}-2)\times 128$ bits $= 2^{39}-256$ bits; exceed it and the counter wraps, repeating a keystream block -- a two-time pad (SP 800-38D Section 5.2.1.1) [@sp80038d].&lt;/p&gt;
&lt;p&gt;The per-key cap of roughly $2^{32}$ messages under random 96-bit nonces is a different quantity entirely: the birthday bound on nonce collision, where with $q$ messages the probability of a repeat scales as $q^2 / 2^{96}$, so $q \approx 2^{32}$ keeps it negligible (SP 800-38D Section 8.3) [@sp80038d]. Hoang, Tessaro, and Thiruvengadam proved these multi-user bounds tight, validating the nonce-randomization mechanism TLS 1.3 uses [@htt2018-gcm].&lt;/p&gt;
&lt;p&gt;Tag length obeys its own bound: for a one-time Wegman-Carter MAC, forgery probability grows with the number of attempts and shrinks with tag length -- exactly why a 64-bit &lt;code&gt;CCM_8&lt;/code&gt; tag is a genuine, quantifiable forgery budget rather than a free optimization [@rfc3610].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Misuse-resistance cannot be online (the nonce edge, again).&lt;/strong&gt; This is the deepest limit in the article, and it explains a design decision that otherwise looks like laziness. Rogaway and Shrimpton proved that a deterministic, nonce-misuse-resistant AEAD &lt;em&gt;cannot&lt;/em&gt; be online or single-pass [@rs2006]. The reason is forced by the definition: to be misuse-resistant, the ciphertext must depend on the &lt;em&gt;entire&lt;/em&gt; plaintext (otherwise two messages sharing a prefix under a repeated nonce would leak that prefix), so the encryptor cannot emit even the first ciphertext byte until it has read the last plaintext byte. There is no implementation trick around it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; You cannot have both streaming and misuse-resistance. It is a theorem, not a missing feature: a misuse-resistant AEAD must read the whole message before it can output any ciphertext. That single impossibility is &lt;em&gt;why&lt;/em&gt; AES-GCM-SIV is two-pass, and why &quot;just make GCM misuse-resistant without slowing it down&quot; is a request for something that provably does not exist [@rs2006].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Commitment is an orthogonal axis (the commitment edge).&lt;/strong&gt; A conventional CTR-based AEAD like GCM is provably not key-committing -- the property is simply absent from &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-/&quot; rel=&quot;noopener&quot;&gt;IND-CPA and INT-CTXT&lt;/a&gt;, so no amount of using GCM &quot;correctly&quot; produces it. Worse, commitment is not one property but a lattice: Bellare and Hoang proved CMT-1 (bind to the key) does not imply CMT-4 (bind to the full context), a separation RFC 9771 later codified [@bh2022-commit][@rfc9771].&lt;/p&gt;
&lt;p&gt;And the attacks are cheaper than the naive birthday intuition suggests: Menda and coauthors found context-discovery attacks against CCM, EAX, SIV, GCM, and OCB3, including an $O(2^{n/3})$ attack on SIV using Wagner&apos;s k-tree algorithm -- well below the $2^{n/2}$ a birthday bound would suggest [@menda2023].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Releasing plaintext early is a strictly weaker world (all edges).&lt;/strong&gt; The fourth limit is about what your decrypt function is allowed to do before it finishes.&lt;/p&gt;

What an implementation exposes if it emits decrypted plaintext before checking the authentication tag. The formal integrity notion for this setting, INT-RUP, is codified in RFC 9771 Section 4.3.10; security that survives releasing unverified plaintext is strictly stronger than standard AEAD security, and most fast one-pass modes satisfy it only weakly -- which is why the safe API contract is verify-then-release: never act on plaintext until the tag checks out [@rfc9771].
&lt;p&gt;Now assemble the thought experiment. Imagine the ideal AEAD: graceful on nonce reuse &lt;em&gt;and&lt;/em&gt; fully committing &lt;em&gt;and&lt;/em&gt; online/single-pass &lt;em&gt;and&lt;/em&gt; fastest at both the server and constrained extremes. The second limit rules out &quot;misuse-resistant and online&quot; together. The third makes commitment an add-on that the CTR, stream, and welded defaults lack -- Ascon, uniquely, commits natively. The performance extremes pull in opposite directions -- AES round function for servers, a few thousand gates for sensors. You cannot have all of it in one construction, and this is not because nobody has been clever enough. It is because the properties provably conflict.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; No single construction closes all three edges at once. That is not a temporary state of the art -- it is a set of theorems about online-ness, commitment, and hardware. The portfolio is not a failure to converge on a winner; it is the mathematically necessary shape of the solution [@rs2006][@bh2022-commit].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So that is settled, provably. Which turns the interesting question inside out. Not &quot;which mode wins?&quot; but &quot;can we make the &lt;em&gt;robustness&lt;/em&gt; the default instead of the expert&apos;s opt-in?&quot; That is exactly where the field is fighting right now.&lt;/p&gt;
&lt;h2&gt;9. Where AEAD Still Bites&lt;/h2&gt;
&lt;p&gt;The classical problem is closed. For confidentiality and integrity, the bounds are tight and the constructions are vetted. The entire live frontier is about robustness: turning the properties nobody used to ask for into defaults nobody has to remember. Six problems are open, and every one of them has the same shape.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Committing AEAD by default.&lt;/strong&gt; Salamanders and partitioning oracles still bite wherever a committing transform is omitted, which today means almost everywhere [@dodis2018-salamander][@len2021-partition]. The frustrating part is that the fix is cheap: Bellare and Hoang give transforms that add full commitment with no ciphertext-size increase, and Albertini and coauthors give a padding-based alternative [@bh2022-commit][@albertini2022]. Yet as of mid-2026, none is &lt;em&gt;mandated&lt;/em&gt; by a shipping standard. RFC 9771 gives the field a shared vocabulary for the property but does not require any construction to have it [@rfc9771]. The open question is not &quot;how?&quot; but &quot;why is it still opt-in?&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Misuse-resistance by default.&lt;/strong&gt; If nonce reuse is a recurring operational reality, why is the misuse-resistant mode the escalation rather than the default? The answer is the theorem from Section 8: defaulting to SIV means defaulting to two-pass, non-streaming encryption, and a great deal of infrastructure assumes it can encrypt a stream as it arrives [@rs2006]. Whether that trade is worth making by default is an active argument, not a settled one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The release-of-unverified-plaintext trade space.&lt;/strong&gt; Between &quot;buffer everything and verify first&quot; and &quot;stream plaintext out immediately&quot; is a design space that streaming media, large-file transfer, and constrained receivers all care about, and the security definitions there -- INT-RUP and its relatives -- are still maturing [@rfc9771].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardware co-design versus conservative margin.&lt;/strong&gt; AEGIS already beats AES-GCM on AES-NI hardware, and the appetite for line-rate encryption keeps growing [@wu2013-aegis]. Newer AES-round designs push the throughput frontier even higher, but they trade cryptanalytic maturity for speed, and their standardization status is unsettled -- AEGIS itself is still only an Internet-Draft, not an RFC [@aegis-draft18]. How much margin to spend for how much throughput is an open judgment call.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://paragmali.com/blog/post-quantum-cryptography-on-windows-the-thirty-year-migrati/&quot; rel=&quot;noopener&quot;&gt;Post-quantum reality&lt;/a&gt;, minus the myth.&lt;/strong&gt; This one is mostly a matter of correcting a widespread misconception.&lt;/p&gt;

For symmetric authenticated encryption the honest answer is: no, and you mostly need to change nothing. Grover&apos;s algorithm offers at most a square-root speedup on key search, which halves the effective key length [@grover1996], so a 128-bit key gives about 64 bits of margin against a hypothetical quantum attacker. The remedy is simply to prefer 256-bit keys -- ChaCha20 already uses one, and AES-256 is a configuration flag -- and to size tags with a margin. The post-quantum upheaval lives in key exchange and signatures, not the AEAD record layer, as developed in the post-quantum-cryptography-on-Windows post in this collection. Your record encryption survives the transition largely intact.
&lt;p&gt;&lt;strong&gt;Constant-time without special hardware.&lt;/strong&gt; GHASH is hard to implement in constant time without CLMUL, and table-based AES leaks timing without AES-NI -- the exact side-channel pressure that motivated both ChaCha20-Poly1305 and Ascon [@bernstein-chacha][@ascon2021]. On the smallest devices, where neither special instruction exists and power analysis is a live threat, side-channel-resistant AEAD is still an open engineering problem, and it is much of the reason NIST ran a lightweight competition at all.&lt;/p&gt;
&lt;p&gt;None of these has a finished answer. But notice the shape of every one: they are all attempts to move an &lt;em&gt;edge&lt;/em&gt; to a place where the deployer cannot step on it by mistake -- misuse-resistance you do not have to remember, commitment you do not have to add, side-channel safety you do not have to hand-tune. Which brings us back to the only rule that actually travels.&lt;/p&gt;
&lt;h2&gt;10. The Field Guide: Rules and Parameters&lt;/h2&gt;
&lt;p&gt;Everything above collapses into one default and a short list of deliberate escalations. Follow the ladder and you will never again pick an AEAD by folklore.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Default to &lt;strong&gt;AES-GCM&lt;/strong&gt; with a 96-bit nonce that is unique per key (prefer a counter over a random value), a hard cap well under $2^{32}$ messages per key, at most 64 GiB per message, and a full 128-bit tag. Use &lt;strong&gt;ChaCha20-Poly1305&lt;/strong&gt; under the same contract when you lack AES hardware. Leave the default only when a specific edge forces you, and only in the direction that edge points [@sp80038d][@rfc8439].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The escalation ladder, each rung mapped to the edge it moves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cannot guarantee a unique nonce?&lt;/strong&gt; Escalate on the nonce edge to &lt;strong&gt;AES-GCM-SIV&lt;/strong&gt; [@rfc8452]. Cloned VMs, counters that reset on crash, weak first-boot entropy -- if any of these is in your threat model, the graceful failure is worth the second pass.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A stack mandates it, or the device is tiny?&lt;/strong&gt; Use &lt;strong&gt;AES-CCM&lt;/strong&gt; where WPA2, BLE, or Zigbee require it [@rfc3610], and &lt;strong&gt;Ascon-AEAD128&lt;/strong&gt; for &lt;em&gt;new&lt;/em&gt; constrained designs, now that it is a NIST standard [@sp800232].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic encryption or key wrapping?&lt;/strong&gt; Use &lt;strong&gt;AES-SIV&lt;/strong&gt;, which needs no nonce at all and is built for exactly this [@rfc5297].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You want one-pass, single-primitive elegance?&lt;/strong&gt; &lt;strong&gt;OCB3&lt;/strong&gt; is now patent-clear, though rarely the right call given how entrenched the defaults are [@rfc7253][@kr2021-ocb].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You control both endpoints and need maximum throughput?&lt;/strong&gt; &lt;strong&gt;AEGIS&lt;/strong&gt; is the fastest option, if you can accept a pre-RFC specification [@aegis-draft18][@wu2013-aegis].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Must a ciphertext bind to exactly one key?&lt;/strong&gt; &lt;em&gt;Add&lt;/em&gt; a key-commitment transform -- for password-based encryption, key rotation, message franking, or multi-recipient encryption -- because no default provides it [@bh2022-commit].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/&quot; rel=&quot;noopener&quot;&gt;Nonce generation&lt;/a&gt; itself is the subject of Part 2 of this series; do not re-derive it here, but do treat &quot;unique per key&quot; as a hard contract, not a hope. On Windows, the &lt;a href=&quot;https://paragmali.com/blog/cng-architecture-bcrypt-ncrypt-ksps/&quot; rel=&quot;noopener&quot;&gt;CNG architecture&lt;/a&gt; post in this collection shows AES-GCM exposed through the &lt;code&gt;BCryptEncrypt&lt;/code&gt; API with an authenticated-cipher-mode information structure -- a concrete example of the parameters below appearing in a real API.&lt;/p&gt;
&lt;p&gt;Pair the ladder with the support map from Section 6 when you check feasibility: browser JavaScript means AES-GCM through WebCrypto, and a target with no AES hardware means ChaCha20-Poly1305 or, if it is also tiny, Ascon-AEAD128 [@webcrypto].&lt;/p&gt;
&lt;p&gt;The exact operational contract, per construction:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Construction&lt;/th&gt;
&lt;th&gt;Nonce&lt;/th&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Per-key limit&lt;/th&gt;
&lt;th&gt;Per-message limit&lt;/th&gt;
&lt;th&gt;On nonce reuse&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;AES-GCM&lt;/td&gt;
&lt;td&gt;96-bit, unique&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;~`2^32` msgs (random nonce)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2^39 - 256&lt;/code&gt; bits, ~64 GiB&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChaCha20-Poly1305&lt;/td&gt;
&lt;td&gt;96-bit, unique&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;~`2^32` msgs (random nonce)&lt;/td&gt;
&lt;td&gt;~256 GiB&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-CCM&lt;/td&gt;
&lt;td&gt;7 to 13 byte&lt;/td&gt;
&lt;td&gt;up to 128-bit (&lt;code&gt;CCM_8&lt;/code&gt;: 64)&lt;/td&gt;
&lt;td&gt;Set by nonce length&lt;/td&gt;
&lt;td&gt;Set by length field&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-GCM-SIV&lt;/td&gt;
&lt;td&gt;96-bit&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Improved (per-nonce derived keys)&lt;/td&gt;
&lt;td&gt;~64 GiB&lt;/td&gt;
&lt;td&gt;Graceful: message equality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AES-SIV&lt;/td&gt;
&lt;td&gt;None or supplied&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;Graceful: message equality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCB3&lt;/td&gt;
&lt;td&gt;up to 120-bit&lt;/td&gt;
&lt;td&gt;up to 128-bit&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AEGIS&lt;/td&gt;
&lt;td&gt;128 or 256-bit&lt;/td&gt;
&lt;td&gt;128 or 256-bit&lt;/td&gt;
&lt;td&gt;Large (wide nonce)&lt;/td&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ascon-AEAD128&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;128-bit&lt;/td&gt;
&lt;td&gt;Per SP 800-232&lt;/td&gt;
&lt;td&gt;Per SP 800-232&lt;/td&gt;
&lt;td&gt;Catastrophic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The only two numeric limits you must hard-code are GCM&apos;s, because they are the ones people breach silently: cap messages per key well under $2^{32}$ and messages under 64 GiB each [@sp80038d]. ChaCha20-Poly1305&apos;s own ~256 GiB per-message ceiling comes the same way, from its 32-bit block counter over 64-byte blocks ($2^{32}\times 64$ bytes $= 2^{38}$ bytes), per RFC 8439 [@rfc8439]. AES-GCM-SIV&apos;s per-nonce key derivation buys better multi-user bounds, which is the whole reason RFC 8452 derives fresh keys rather than reusing one [@bht2018][@rfc8452].&lt;/p&gt;
&lt;p&gt;Every rung of that ladder has a mirror image: a misuse pattern that punishes the deployment for stepping on the edge it ignored. Each row below maps a real-code mistake to the named break that catches it.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse seen in real code&lt;/th&gt;
&lt;th&gt;Edge it triggers&lt;/th&gt;
&lt;th&gt;Named break&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Reusing a &lt;code&gt;(key, nonce)&lt;/code&gt; in GCM or AEGIS&lt;/td&gt;
&lt;td&gt;Nonce&lt;/td&gt;
&lt;td&gt;Nonce-Disrespecting Adversaries, 2016&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Assuming an AEAD binds a ciphertext to one key&lt;/td&gt;
&lt;td&gt;Commitment&lt;/td&gt;
&lt;td&gt;Invisible salamanders; partitioning oracles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Truncating the tag to save bytes (&lt;code&gt;CCM_8&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Integrity budget&lt;/td&gt;
&lt;td&gt;Wegman-Carter forgery bound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treating AES-GCM-SIV reuse as &quot;free&quot;&lt;/td&gt;
&lt;td&gt;Nonce&lt;/td&gt;
&lt;td&gt;SIV leaks message equality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-rolling Encrypt-and-MAC&lt;/td&gt;
&lt;td&gt;Composition&lt;/td&gt;
&lt;td&gt;Bellare-Namprempre; Krawczyk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflating GCM&apos;s two ceilings&lt;/td&gt;
&lt;td&gt;Nonce&lt;/td&gt;
&lt;td&gt;The two SP 800-38D limits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The evidence for each is in the sections above: nonce reuse on live servers [@bock2016], the salamander and its weaponization [@dodis2018-salamander][@len2021-partition], the tag-length forgery budget [@rfc3610], the SIV equality leak [@rfc8452], the composition theorems [@bn2000][@krawczyk2001], and the two ceilings [@sp80038d]. One extra caution on attribution: the live-TLS nonce-reuse break is repeatedly mis-cited to CVE-2016-0270, which actually names IBM Domino and which the NVD itself flags as commonly misapplied to other products [@cve-2016-0270].&lt;/p&gt;

Search your codebase for any AEAD decryption that runs under an attacker-influenced key: password-based encryption, key wrapping, token decryption, or multi-recipient envelopes. If a decrypt path lets the caller supply or guess the key and then branches on whether decryption succeeded, you are exposed to a partitioning oracle and need a key-commitment transform on top of the AEAD [@len2021-partition][@bh2022-commit].
&lt;p&gt;That ladder is your inoculation against folklore. But folklore is sticky, so let us take the most common misconceptions head-on.&lt;/p&gt;
&lt;h2&gt;11. Seven Beliefs That Get Deployments Broken&lt;/h2&gt;


No. For every nonce-respecting mode -- AES-GCM, ChaCha20-Poly1305, AES-CCM, OCB3, AEGIS, and Ascon -- a single repeated `(key, nonce)` is an immediate, total break: it leaks the XOR of the two plaintexts, and a second collision hands over the authentication subkey for universal forgery [@joux2006][@bock2016]. Only the SIV family degrades gracefully, and only to leaking message equality [@rfc8452].


No. It makes reuse *survivable*, not free. A repeated nonce still leaks message equality -- an attacker learns that two ciphertexts encrypt the same plaintext -- which can matter a great deal in the right context. Use it as a safety net, not a license to stop managing nonces [@rfc8452][@rs2006].


No to both misreadings. It is a different trade-off on the hardware edge, not a weaker cipher: constant-time in pure software with no lookup tables, which makes it the *stronger* choice on hardware without AES-NI, where table-based AES leaks timing. And it is not mandatory-to-implement in TLS 1.3 -- it is a *recommended* (SHOULD) cipher suite, while only AES-128-GCM is the MUST, per RFC 8446 Section 9.1 [@rfc8446]. Recommended, not weaker, not mandatory.


No. Random 96-bit nonces are collision-safe only up to roughly $2^{32}$ messages per key, by the birthday bound. That per-key invocation cap is a different number from the per-message limit of about 64 GiB; enforce both, and prefer a counter over a random nonce when you can [@sp80038d][@htt2018-gcm].


No. Most AEADs are not key-committing. One AES-GCM ciphertext can be built to decrypt to two different meaningful plaintexts under two different keys, with the tag valid both times -- the invisible-salamanders result. If a ciphertext must bind to one key, add a commitment transform [@dodis2018-salamander][@bh2022-commit].


No. AEGIS leads on raw throughput, but it is nonce-respecting (catastrophic on reuse), not fully key-committing, and still an active Internet-Draft rather than an RFC -- it states plainly that it &quot;is not a standard&quot; and expires on 8 April 2026. Fastest is not the same as best for your deployment [@aegis-draft18][@wu2013-aegis].


No. Symmetric authenticated encryption survives the quantum transition. Grover&apos;s algorithm only halves effective key length [@grover1996], so prefer 256-bit keys and size tags with margin. The post-quantum upheaval is in key exchange and signatures, not the AEAD record layer. Separately, note that NIST&apos;s Ascon-AEAD128 is a tweaked variant of the CAESAR Ascon-128, not byte-identical [@sp800232].

&lt;h2&gt;The Function, Not the Ranking&lt;/h2&gt;
&lt;p&gt;Seven ciphers, one interface, three edges. Every break here was a deployment triggering an edge its designers deliberately moved elsewhere. The invisible salamanders triggered the commitment edge that AEAD never promised to close [@dodis2018-salamander]. The 184 TLS servers triggered the nonce edge that GHASH cannot survive [@bock2016]. The &lt;code&gt;CCM_8&lt;/code&gt; forgery budget is the tag-length edge priced in bits [@rfc3610]. The SIV equality leak is the residue of the one edge SIV &lt;em&gt;does&lt;/em&gt; close -- even the fix has a seam [@rfc8452]. And OCB3&apos;s near-total absence is the patent grave, an edge that was never technical at all [@kr2021-ocb].&lt;/p&gt;
&lt;p&gt;Put them together and the thesis is earned rather than asserted. &quot;Which AEAD should I use?&quot; is not a ranking from worst to best, because Section 8 proved there is no best -- no single construction can be misuse-resistant &lt;em&gt;and&lt;/em&gt; single-pass &lt;em&gt;and&lt;/em&gt; fully committing &lt;em&gt;and&lt;/em&gt; fastest at once. It is a function: pick the construction whose failure mode your deployment can &lt;em&gt;guarantee&lt;/em&gt; it will never trigger.&lt;/p&gt;
&lt;p&gt;Master the three edges and you can place any construction on the map, predict any misuse before it ships, and evaluate whatever the next competition invents -- key-derivation-bound AEADs, the TLS record layer&apos;s next mode, or a lightweight winner that does not exist yet. The names will change. The edges will not.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;the-aead-decision-matrix&quot; keyTerms={[
  { term: &quot;AEAD&quot;, definition: &quot;One keyed call giving confidentiality, plaintext integrity, and associated-data integrity&quot; },
  { term: &quot;Associated Data&quot;, definition: &quot;Header bytes authenticated but not encrypted, travelling in the clear&quot; },
  { term: &quot;Nonce&quot;, definition: &quot;A value that must be unique per key for a nonce-respecting mode&quot; },
  { term: &quot;MRAE&quot;, definition: &quot;Misuse-resistant AE, where nonce reuse leaks only message equality&quot; },
  { term: &quot;Wegman-Carter one-time MAC&quot;, definition: &quot;A tag of the form keyed-hash plus one-time mask, the shared root of GHASH and Poly1305&quot; },
  { term: &quot;GHASH&quot;, definition: &quot;GCM&apos;s linear universal hash at a secret point, recovering which forges tags&quot; },
  { term: &quot;Synthetic IV (SIV)&quot;, definition: &quot;An IV derived as a MAC of the whole message, so the tag is the IV&quot; },
  { term: &quot;Key commitment&quot;, definition: &quot;Binding a ciphertext to one key (CMT-1) up to the full context (CMT-4)&quot; },
  { term: &quot;Sponge&quot;, definition: &quot;One permutation that absorbs input and squeezes keystream and tag, as in Ascon&quot; },
  { term: &quot;Inverse-free&quot;, definition: &quot;A decryption path that never calls the block-cipher inverse, true of the CTR, stream, and sponge modes but not OCB3&quot; },
  { term: &quot;RUP&quot;, definition: &quot;Release of Unverified Plaintext, emitting plaintext before the tag is checked&quot; }
]} questions={[
  { q: &quot;What are the three edges that decide an AEAD choice?&quot;, a: &quot;The nonce contract, the performance and hardware profile, and commitment.&quot; },
  { q: &quot;Why is a repeated nonce catastrophic in AES-GCM?&quot;, a: &quot;It reuses the one-time Wegman-Carter mask, leaking the plaintext XOR and, with a second collision, the GHASH subkey for forgery.&quot; },
  { q: &quot;Why can a misuse-resistant AEAD not be single-pass?&quot;, a: &quot;Its ciphertext must depend on the whole message, so it must read the last plaintext byte before emitting any ciphertext, a Rogaway-Shrimpton theorem.&quot; },
  { q: &quot;Does authenticated mean a ciphertext binds to one key?&quot;, a: &quot;No. Commitment is an orthogonal axis, and most AEADs are not key-committing by default.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>aead</category><category>authenticated-encryption</category><category>aes-gcm</category><category>chacha20-poly1305</category><category>nonce-misuse</category><category>key-commitment</category><category>applied-cryptography</category><category>cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>Predictable or Repeated: The Only Two Ways Cryptographic Randomness Betrays You</title><link>https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/</link><guid isPermaLink="true">https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/</guid><description>Every key, nonce, IV, and salt fails in one of two ways: predictable when it must be unpredictable, or repeated when it must be unique. A field guide.</description><pubDate>Wed, 08 Jul 2026 03:29:38 GMT</pubDate><content:encoded>
Every cryptographic key, nonce, IV, and salt is only as safe as two invisible properties: was it *unpredictable* when it had to be, and *unique* when it had to be? A cryptographically secure pseudorandom number generator (CSPRNG) manufactures unpredictability by stretching a little true entropy into unlimited output, and fails when the seed is guessable, the state cloned, the design back-doored, or the draw biased. But that is only half the story: the four roles each demand a different mix of unique, unpredictable, and secret, so a perfect generator still gets you owned if you reuse a GCM nonce or predict a CBC IV. A field guide to both halves, through every famous break.
&lt;h2&gt;1. Two Lines of Code&lt;/h2&gt;
&lt;p&gt;In 1996, two Berkeley graduate students needed only seconds to reconstruct the secret keys protecting Netscape&apos;s SSL. The RC4 and RSA math was flawless. The browser had simply seeded its random number generator from the time of day and a couple of process IDs, and there were only so many of those [@src-netscape1996]. In 2008, a one-line change to Debian&apos;s OpenSSL quietly collapsed the space of every key it generated, for SSH, SSL, and OpenVPN, down to about 32,768 possibilities, small enough to enumerate on a laptop over lunch [@src-debian2008].&lt;/p&gt;
&lt;p&gt;In both cases the cipher was flawless and the keys were guessable. That is the whole problem with cryptographic randomness: it is the one primitive whose failures are invisible, and there are only two of them.&lt;/p&gt;
&lt;p&gt;Look closely at Netscape first. Ian Goldberg and David Wagner reverse-engineered the Navigator&apos;s seeding code and found that the entire unpredictable input to the session key came from the current time, the process ID, and the parent process ID [@src-netscape1996]. An attacker who knew roughly when you connected, and process IDs live in a small range, could reproduce the seed and therefore the key. The seed &lt;em&gt;was&lt;/em&gt; the secret, and the secret was enumerable. Nothing about RC4 or RSA had to break; the attacker simply recomputed what the generator would produce.&lt;/p&gt;
&lt;p&gt;Debian, twelve years later, is the same mistake wearing a different costume. A developer commented out a line in OpenSSL that fed unpredictable data into the seed, silencing a memory-analysis warning about reading uninitialized bytes.The specific tool that flagged the warning is often reported as Valgrind, sometimes as Purify; the Debian advisory itself names neither, so we will not assert one. The load-bearing fact is the collapsed keyspace, which is not in dispute [@src-debian2008]. With that input gone, the only entropy left was the process ID, so the effective keyspace for roughly two years of Debian-generated keys shrank to about $2^{15}$ values, and CVE-2008-0166 was born [@src-debian2008, @src-cve2008]. Every SSH host key, TLS certificate, and OpenVPN key produced on an affected system could be pre-computed and looked up in a table.&lt;/p&gt;

A deterministic algorithm that stretches a short, truly-random *seed* into an arbitrarily long output stream that no efficient adversary can distinguish from uniform random bits, even after observing part of the stream. The word *cryptographically* is what separates it from an ordinary statistical PRNG: the guarantee is about unpredictability to a computationally bounded attacker, not merely about passing statistical tests.
&lt;p&gt;Neither of these was a broken algorithm. Both were broken randomness, and the reason they feel like the same story is that they &lt;em&gt;are&lt;/em&gt; the same story. A weak or misused random value emits bytes that look perfect, pass every statistical test, and are catastrophically broken anyway. The generator hums along; the ciphertext validates; the signature verifies. The failure is silent until someone else computes what you computed.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A broken cipher raises an error. A bad MAC rejects. A weak or reused random value does neither: it produces output that is statistically indistinguishable from good randomness and passes every test you can run against it, while being fully predictable or fatally repeated to an attacker who knows where to look. You cannot detect the failure by inspecting the output. This is why randomness bugs survive code review, ship to production, and are found years later by the people exploiting them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once you have seen these two breaks side by side, the organizing claim of this article is hard to unsee. A random value can betray you in exactly one of two ways, and the failure can happen in exactly one of two places.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; There are only two ways a random value fails: it is &lt;em&gt;predictable&lt;/em&gt; when it needed to be &lt;em&gt;unpredictable&lt;/em&gt;, or &lt;em&gt;repeated&lt;/em&gt; when it needed to be &lt;em&gt;unique&lt;/em&gt;. And each failure happens in exactly one of two places: the &lt;em&gt;generator&lt;/em&gt; that manufactures the bits, or the &lt;em&gt;role&lt;/em&gt; that consumes them (key, nonce, IV, salt). Two knobs, two places. Every famous break in this article is a single knob turned the wrong way in a single place.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is Part 2 of a field guide for protocol designers. Part 1 argued that the standard security definition, IND-CPA, &lt;em&gt;mandates&lt;/em&gt; randomized encryption: a scheme that encrypts the same message the same way twice cannot hide whether two ciphertexts carry the same plaintext, so a nonce or IV &lt;em&gt;is&lt;/em&gt; the randomness the definition demands. This part supplies, and disciplines, that randomness. By the end you should be able to look at any randomness bug, Netscape or Debian or the ones still shipping today, and name on sight which knob turned and which place failed.&lt;/p&gt;
&lt;p&gt;Both breaks were the generator half failing on the predictability knob. To see why that failure was almost inevitable, we have to go back to the moment cryptography decided to put all of its secrecy into a string of unguessable bits.&lt;/p&gt;
&lt;h2&gt;2. Why Randomness Became a Primitive&lt;/h2&gt;
&lt;p&gt;In 1883, Auguste Kerckhoffs told cryptographers something that sounds obvious now and was radical then: assume the enemy knows your algorithm [@src-kerckhoffs1883]. Do not build your security on the secrecy of the machine, because the machine will be captured, reverse-engineered, or sold. Build it on the &lt;em&gt;key&lt;/em&gt;. That single move relocated all of a system&apos;s secrecy into one place, a string of bits, and quietly made the unpredictability of that string the foundation of everything, without ever saying where the string should come from.&lt;/p&gt;
&lt;p&gt;Sixty-six years later, Claude Shannon made the requirement mathematical. In &lt;em&gt;Communication Theory of Secrecy Systems&lt;/em&gt; (1949), he proved that the one-time pad achieves &lt;em&gt;perfect&lt;/em&gt; secrecy: a ciphertext reveals nothing whatsoever about the plaintext, not even to an adversary with unlimited computing power [@src-shannon1949].&lt;/p&gt;
&lt;p&gt;But the proof comes with a receipt. Perfect secrecy demands a key that is perfectly random and at least as long as the message, used once. Randomness stopped being a nice-to-have and became a first-class, provable requirement, the raw material perfect secrecy is quite literally made of. Hold onto the price tag on that key; it comes back in Section 8 as the gap no engineering closes.&lt;/p&gt;

*Entropy* measures how much true unpredictability a source contains. For cryptography the conservative measure is *min-entropy*, defined for a distribution $X$ as $H_\infty(X) = -\log_2 \max_x \Pr[X = x]$. It is governed by the single most likely outcome, that is, by the attacker&apos;s best single guess. A source with $n$ bits of min-entropy resists guessing about as well as $n$ uniformly random bits. This worst-case framing is deliberate: an average-case measure like Shannon entropy can look healthy while one dominant value makes the source easy to guess.
&lt;p&gt;But where do perfectly random bits come from? In 1951 John von Neumann issued the warning that still hangs over the whole field. Studying ways to generate random digits on the first computers, he tried the &lt;em&gt;middle-square method&lt;/em&gt;, squaring a number and taking the middle digits as the next value, and watched it collapse into short repeating cycles or fall to zero. His verdict was characteristically sharp.&lt;/p&gt;

&quot;Any one who considers arithmetical methods of producing random digits is, of course, in a state of sin.&quot; -- John von Neumann, 1951 [@src-vonneumann1951]
&lt;p&gt;The point was not that deterministic recipes are useless, but that arithmetic alone only &lt;em&gt;simulates&lt;/em&gt; randomness; it cannot manufacture the genuine article [@src-vonneumann1951]. A formula has no unpredictability of its own. Whatever you get out was already determined by what you put in.&lt;/p&gt;
&lt;p&gt;So for a while, randomness was treated as a scarce physical resource you harvested and rationed. In 1955 the RAND Corporation published &lt;em&gt;A Million Random Digits with 100,000 Normal Deviates&lt;/em&gt;, an entire book whose content was a table of random numbers generated from electronic noise, sold so that scientists could look up randomness the way they looked up logarithms [@src-rand1955].Two years later the UK built ERNIE (Electronic Random Number Indicator Equipment) to draw Premium Bond winners from thermal noise. It is a lovely piece of history, but no contemporaneous engineering primary survives that we could verify, so treat it as color, not evidence [@src-ernie1957]. Randomness you could buy in a bookstore is a charming idea and a useless one for cryptography, because a published table is, by definition, known to your adversary, and it cannot produce a fresh, secret, per-session value on demand.&lt;/p&gt;

Von Neumann&apos;s ban is on arithmetic *manufacturing* randomness from nothing. It says nothing against *extracting* clean randomness from a messy physical source that already contains some, which is exactly what a modern entropy source does, and what a [fuzzy extractor](/blog/fuzzy-extractors-windows-hello/) does when it turns a noisy biometric into a stable key. Extraction concentrates existing unpredictability; it never conjures it. Garbage in, garbage out remains the law.
&lt;p&gt;The conceptual breakthrough that made a &lt;em&gt;deterministic&lt;/em&gt; generator respectable arrived in the early 1980s. Andrew Yao (1982) and Manuel Blum with Silvio Micali (1984) redefined what &quot;random enough&quot; should mean for cryptography [@src-yao1982, @src-blummicali1984]. Their answer was not statistical but &lt;em&gt;computational&lt;/em&gt;: a sequence is cryptographically strong if no efficient algorithm, given all the output bits so far, can predict the next bit with probability meaningfully better than one-half.&lt;/p&gt;

A pseudorandom generator passes the *next-bit test* if no probabilistic polynomial-time algorithm, shown any prefix of the output sequence, can guess the following bit with advantage non-negligibly above $1/2$. Yao proved this test is equivalent to indistinguishability from uniform: pass the next-bit test and you pass *every* efficient statistical test at once [@src-yao1982, @src-blummicali1984, @src-katzlindell]. This is unpredictability made rigorous, and it is a yardstick no finite battery of statistical tests can ever meet.
&lt;p&gt;This is knob number one, unpredictability, turned into a precise definition. Notice what it does not promise: it says nothing about where the seed comes from, and it holds only against a &lt;em&gt;bounded&lt;/em&gt; adversary. Both caveats will return to bite. But the intellectual foundation was now in place. Shannon and Kerckhoffs had made unguessable bits the foundation of everything; von Neumann had warned that no formula can produce them from thin air. So engineers reached for the nearest deterministic recipe that merely &lt;em&gt;looked&lt;/em&gt; random, and walked straight into the first disaster.&lt;/p&gt;
&lt;h2&gt;3. Statistical Randomness That Was Not Unpredictable&lt;/h2&gt;
&lt;p&gt;Here is a fact that should bother you. The Mersenne Twister, the default random generator in Python, Ruby, PHP, R, MATLAB, and countless other systems [@src-python-random], passes essentially every statistical randomness test ever devised. Its output is beautifully uniform, its period is astronomically long, and it is &lt;em&gt;completely predictable&lt;/em&gt;. Observe 624 of its 32-bit outputs and you can reconstruct its entire internal state and compute every value it will ever emit, forward and backward [@src-mersenne1998]. That gap, between &quot;looks random&quot; and &quot;is unguessable,&quot; is the mistake that organizes the rest of this article.&lt;/p&gt;
&lt;p&gt;Start with the simplest offender. A &lt;em&gt;linear congruential generator&lt;/em&gt; (LCG) produces its next value with $x_{n+1} = (a \cdot x_n + c) \bmod m$. It is fast, it is fine for shuffling a deck in a video game, and it is fatal for cryptography, because given a handful of outputs you can solve for the hidden constants and then predict everything. There is no unpredictability here at all, only arithmetic dressed up to look busy.&lt;/p&gt;
&lt;p&gt;The Mersenne Twister (MT19937, 1998) is vastly more sophisticated, with 19,937 bits of state and excellent equidistribution, but it shares the same fatal property: it is &lt;em&gt;linear&lt;/em&gt; over the field $\mathbb{F}_2$ [@src-mersenne1998]. Every output bit is a fixed linear function of the state bits. Collect enough outputs and you have a system of linear equations whose solution is the state. Sophistication did not buy unpredictability, because unpredictability was never what the design optimized for.&lt;/p&gt;

A *statistical PRNG* is built to produce output with good distributional properties: uniformity, long period, no detectable correlations. A *cryptographically secure* generator is built to a strictly harder standard: no efficient adversary who has seen past output can predict future output, the next-bit test of Section 2. The two goals are independent. The Mersenne Twister maximizes the first and fails the second completely. Passing statistical batteries is necessary but nowhere near sufficient for cryptography.
&lt;p&gt;The first genuinely &lt;em&gt;cryptographic&lt;/em&gt; generators came from the banking world. The ANSI X9.17 and later X9.31 designs used a block cipher (two-key Triple-DES in X9.17; Triple-DES or AES in X9.31) rather than a linear recurrence [@src-hac, @src-ansix931], so predicting the output required breaking the cipher itself, not solving a linear system. This was real progress.&lt;/p&gt;
&lt;p&gt;But it moved the whole burden onto a single secret: the seed key $K$ baked into the construction. If $K$ leaks, or worse, if it is &lt;em&gt;hard-coded&lt;/em&gt; into shipped firmware, the generator becomes fully predictable to anyone who reads the binary. That is not a hypothetical. It is exactly the DUHK attack we will meet in Section 4, where hard-coded X9.31 keys let researchers decrypt live VPN traffic [@src-duhk2018].&lt;/p&gt;
&lt;p&gt;And then there is the naive-seeding family, the Netscape lineage: take a strong-enough algorithm and feed it a weak seed built from the clock, the process ID, and the parent process ID [@src-netscape1996]. The algorithm is irrelevant if the seed is enumerable. In 2005 the IETF wrote this lesson into a Best Current Practice, RFC 4086, whose Section 3 explicitly warns against seeding from clocks and serial numbers because they are low-entropy and guessable [@src-rfc4086]. The rule was written in blood.&lt;/p&gt;
&lt;p&gt;{`
// The cipher is never touched. Only the seed is guessed.
// We model a small seed space, like Netscape&apos;s time+PID+PPID
// or Debian&apos;s collapsed ~2^15..2^18 keyspace.&lt;/p&gt;
&lt;p&gt;function prng(seed) {                 // a toy stand-in for the real generator
  let s = seed &amp;gt;&amp;gt;&amp;gt; 0;
  s = (Math.imul(1103515245, s) + 12345) &amp;gt;&amp;gt;&amp;gt; 0;   // classic LCG constants
  return s;                            // the victim&apos;s &quot;key material&quot;
}&lt;/p&gt;
&lt;p&gt;const SEED_BITS = 18;                  // pretend this is all the entropy there was
const seedSpace = 1 &amp;lt;&amp;lt; SEED_BITS;      // 262144 possibilities
const secretSeed = Math.floor(Math.random() * seedSpace);
const victimKey = prng(secretSeed);    // a &quot;256-bit key&quot; -- but only 18 bits of real entropy&lt;/p&gt;
&lt;p&gt;// Attacker knows the seed came from a small space and enumerates it:
let tries = 0, recovered = -1;
for (let guess = 0; guess &amp;lt; seedSpace; guess++) {
  tries++;
  if (prng(guess) === victimKey) { recovered = guess; break; }
}&lt;/p&gt;
&lt;p&gt;console.log(&apos;seed space:&apos;, seedSpace, &apos;possibilities&apos;);
console.log(&apos;attacker tries needed:&apos;, tries);
console.log(&apos;key recovered:&apos;, recovered === secretSeed);
console.log(&apos;note: a 256-bit key with 18 bits of seed entropy is an 18-bit key&apos;);
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The clock, the process ID, the MAC address, a counter: all are low-entropy and often knowable to an attacker. A generator seeded from them has only as much unpredictability as the seed, no matter how strong the algorithm downstream. RFC 4086 codifies this as a rule: do not seed from clocks or serial numbers [@src-rfc4086]. In practice you should never seed a CSPRNG by hand at all; ask the operating system, which is the entire lesson of Sections 5 and 10.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This is the first place your mental model has to shift. Statistical quality and cryptographic unpredictability are &lt;em&gt;different properties&lt;/em&gt;, and the second does not follow from the first. The Mersenne Twister is the proof: it sails through the test batteries and collapses after 624 outputs [@src-mersenne1998]. Unpredictability is a &lt;em&gt;computational&lt;/em&gt; claim about what an adversary can compute, not a &lt;em&gt;statistical&lt;/em&gt; claim about how the bits are distributed. Tests can falsify a generator; they can never certify one, a limit we make precise in Section 8.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The theorists had, in fact, already named the generator that gets this right.&lt;/p&gt;

Blum, Blum and Shub (1986) built a generator whose unpredictability *provably* reduces to the hardness of factoring large integers: break the generator and you have factored the modulus [@src-bbs1986]. It is the gold standard on paper. It is also almost never deployed, because it computes only a few bits per modular squaring, orders of magnitude too slow for a system handing out billions of random bytes a day. It is a deliberate non-deployment: the theoretically cleanest answer, benched for being impractical, while the field builds fast generators on other assumptions.
&lt;p&gt;The Mersenne Twister is not a villain, incidentally.For Monte-Carlo simulation, numerical integration, and procedural graphics, the Mersenne Twister is excellent and appropriate. It is disqualified only for cryptography, where its predictability is disqualifying. &quot;Wrong for crypto&quot; is not &quot;wrong for everything.&quot; The lesson is narrower and sharper: statistical excellence is not unpredictability, and unpredictability is the only thing cryptography cares about. Turning that computational ideal into something an operating system can hand out billions of times a day took four decades and a string of named catastrophes. Here is how the generator evolved.&lt;/p&gt;
&lt;h2&gt;4. CSPRNG Design, Generation by Generation&lt;/h2&gt;
&lt;p&gt;No single insight fixed cryptographic randomness. Instead there was a ratchet. Each generation of generator worked until a &lt;em&gt;named&lt;/em&gt; disaster exposed the one thing it got wrong, and that disaster defined the next generation. Walk the ladder and you will notice every rung is one of the two knobs failing in the &lt;em&gt;generator&lt;/em&gt; half of the problem. The consumer-half failures, PS3 and BEAST and the rest, are on the ladder too, because they drove design changes, but their real home is Section 6.&lt;/p&gt;

NIST&apos;s term for the deterministic-expansion core of a modern generator: the algorithm that takes a seed and produces output bits by repeatedly applying a cryptographic function (a block cipher, a hash, or an HMAC). &quot;DRBG&quot; names the *expansion* half specifically; it does not include the entropy source that supplies the seed. A DRBG never creates unpredictability, it only stretches whatever the seed already contained.

The short, high-entropy input a DRBG expands. Its length is set by the target security level (for example, 256 bits of seed for a 256-bit security level). Everything the generator&apos;s output can resist is bounded by the min-entropy of this seed: expand a 15-bit seed into a gigabyte of output and you have a gigabyte with 15 bits of unpredictability, which is exactly what happened to Debian.
&lt;p&gt;&lt;strong&gt;Generation 0, physical and tabular (1949 to 1957).&lt;/strong&gt; Harvest physical noise; if you must reuse it, print it in a book [@src-rand1955]. Superseded because a static table cannot deliver a fresh, secret, per-session value on demand, and von Neumann&apos;s middle-square degenerated to short cycles [@src-vonneumann1951].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 1, statistical PRNGs with ad-hoc seeding (1960s to 1998).&lt;/strong&gt; LCGs and the Mersenne Twister, seeded from the clock and the PID. Two knob-one failures stacked on top of each other: the generator is linearly invertible (624 outputs recover a Mersenne Twister) and the seed is enumerable (Netscape) [@src-mersenne1998, @src-netscape1996].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 2, block-cipher PRNGs (ANSI X9.17 and X9.31, 1985 to 1998).&lt;/strong&gt; A cipher instead of a linear recurrence, so the output is not linearly invertible. It failed because all security rested on a secret seed key $K$, and real products hard-coded $K$ into firmware. In 2017 the DUHK attack recovered full generator state and decrypted VPN traffic on devices such as FortiOS by exploiting a hard-coded X9.31 key, with no entropy input, no reseed, and no forward secrecy to save them [@src-duhk2018].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 3, OS entropy pools (Linux /dev/random, Ts&apos;o 1994).&lt;/strong&gt; The operating system harvests timing from interrupts, keystrokes, and disk activity into a hashed pool, &lt;em&gt;estimates&lt;/em&gt; how much entropy it holds, and blocks when the estimate runs low [@src-random-c, @src-random4man]. This was the first mass-deployed OS CSPRNG and a real advance, but it had three weaknesses: entropy estimation is guesswork; blocking starves fresh systems at boot and spawned years of &lt;code&gt;/dev/random&lt;/code&gt;-versus-&lt;code&gt;/dev/urandom&lt;/code&gt; confusion; and low boot entropy breaks real keys at scale. In 2012, &lt;em&gt;Mining Your Ps and Qs&lt;/em&gt; scanned the internet and found that 5.57% of TLS hosts and 9.60% of SSH hosts shared keys, with thousands of RSA moduli factorable because devices generated keys before their pools had any entropy [@src-heninger2012, @src-factorable]. The independent &lt;em&gt;Ron was wrong, Whit is right&lt;/em&gt; study found shared RSA prime factors across millions of moduli the same year [@src-lenstra2012], and in 2013 researchers factored 184 RSA keys from Taiwan&apos;s government-certified Citizen Digital Certificate smartcards, hardware that had passed FIPS and Common Criteria evaluation [@src-smartfacts, @src-coppersmith2013]. The Linux RNG&apos;s internal state had already been shown recoverable by cryptanalysis in 2006 [@src-gutterman2006], and the Windows generator was analyzed the following year [@src-dorrendorf2007].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 4, standardized DRBGs (NIST SP 800-90A, 2006 onward).&lt;/strong&gt; Hash_DRBG, HMAC_DRBG, and CTR_DRBG: a seed plus a cryptographic function stretched deterministically, with a defined reseed limit (at most $2^{48}$ generate requests) and a backtracking-resistant update step [@src-sp80090a]. And, shipped in the &lt;em&gt;same document&lt;/em&gt;, the field&apos;s most instructive cautionary tale: Dual_EC_DRBG. It had a documented output &lt;em&gt;bias&lt;/em&gt;, distinguishable from uniform independently of any back door (Schoenmakers and Sidorenko, 2006) [@src-schoenmakers2006], and, far worse, a &lt;em&gt;trapdoor&lt;/em&gt;: whoever chose its elliptic-curve points $P$ and $Q$ could recover the internal state from about 32 bytes of output and predict all future output (Shumow and Ferguson, 2007) [@src-shumow2007]. It was reported in 2013 as a deliberate program [@src-reuters2013] and caught weaponized in Juniper&apos;s ScreenOS in 2015 [@src-checkoway2016]. A generator can pass every statistical test and still be &lt;em&gt;owned&lt;/em&gt; by whoever chose its constants.&lt;/p&gt;

flowchart TD
    G0[&quot;Gen 0: physical and tabular (1949 to 1957)&quot;] --&amp;gt;|&quot;a table makes no fresh secret&quot;| G1[&quot;Gen 1: statistical PRNGs, ad-hoc seeds&quot;]
    G1 --&amp;gt;|&quot;MT 624 outputs, Netscape seed&quot;| G2[&quot;Gen 2: block-cipher PRNGs (X9.17, X9.31)&quot;]
    G2 --&amp;gt;|&quot;DUHK hard-coded key&quot;| G3[&quot;Gen 3: OS entropy pools (Linux, 1994)&quot;]
    G3 --&amp;gt;|&quot;boot starvation, Mining Ps and Qs&quot;| G4[&quot;Gen 4: standardized DRBGs (SP 800-90A)&quot;]
    G4 --&amp;gt;|&quot;Dual EC back door&quot;| G5[&quot;Gen 5: accumulation, ChaCha20, getrandom&quot;]
    G5 --&amp;gt;|&quot;fork or VM-snapshot clone&quot;| G6[&quot;Gen 6: converged validated architecture (90B, 90A, 90C)&quot;]
&lt;p&gt;&lt;strong&gt;Generation 5, entropy accumulation, stream-cipher cores, and non-blocking syscalls (1999 to 2022).&lt;/strong&gt; Yarrow (1999) [@src-yarrow1999] led to Fortuna (2003), whose 32 staggered pools &lt;em&gt;abolish entropy estimation&lt;/em&gt; altogether [@src-fortuna2003, @src-freebsd-random]. The output core moved to ChaCha20, Bernstein&apos;s refinement of his earlier Salsa20 cipher [@src-chacha, @src-snuffle]: OpenBSD re-cored &lt;code&gt;arc4random&lt;/code&gt; on it in 2014 [@src-arc4random], and Linux replaced its non-blocking pool with a ChaCha20 CRNG in kernel 4.8, October 2016 [@src-linux48].Pin this milestone at Linux 4.8 (October 2016), commit e192be9d9a30555. A &quot;5.6 / 2020&quot; figure sometimes cited refers to an unrelated change in how &lt;code&gt;/dev/random&lt;/code&gt; handles &lt;code&gt;O_NONBLOCK&lt;/code&gt;, not the ChaCha20 output core [@src-linux48]. Jason Donenfeld&apos;s 5.17 to 5.18 rewrite in 2022 added BLAKE2s extraction, per-CPU fast key erasure, and unified &lt;code&gt;/dev/random&lt;/code&gt; with &lt;code&gt;/dev/urandom&lt;/code&gt; [@src-donenfeld2022]. The &lt;code&gt;getrandom(2)&lt;/code&gt; syscall (2014) blocks &lt;em&gt;once&lt;/em&gt; at boot until the pool is seeded, then never again [@src-getrandom]. One hazard the generator alone cannot fix remained: &lt;code&gt;fork()&lt;/code&gt; and VM-snapshot cloning duplicate the entire generator state and re-emit identical values, a &lt;em&gt;uniqueness&lt;/em&gt; failure measured in the wild by Ristenpart and Yilek (2010) and Everspaugh et al. (2014) [@src-ristenpart2010, @src-everspaugh2014].OpenBSD kept the name &lt;code&gt;arc4random&lt;/code&gt; for source compatibility even though the &quot;RC4&quot; it originally stood for is long gone; since 2014 the bytes come from ChaCha20 [@src-arc4random].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generation 6, the converged validated architecture (2018 to 2026, current state of the art).&lt;/strong&gt; A &lt;em&gt;validated&lt;/em&gt; entropy source (SP 800-90B) [@src-sp80090b] seeds a &lt;em&gt;computational&lt;/em&gt; DRBG (SP 800-90A) [@src-sp80090a], and the two are tied together by SP 800-90C [@src-sp80090c], with continuous reseeding, forward secrecy, clone defense, and no single component trusted on its own. We draw the whole pipeline in Section 5.&lt;/p&gt;

flowchart TD
    K[&quot;State: Key and counter block V&quot;] --&amp;gt; G[&quot;Generate: AES-CTR keystream from Key and V&quot;]
    G --&amp;gt; O[&quot;Hand the requested bytes to the caller&quot;]
    G --&amp;gt; U[&quot;Update: re-derive a fresh Key and V&quot;]
    U --&amp;gt; K2[&quot;New state replaces the old one, so prior output cannot be recomputed (backtracking resistance)&quot;]
    K2 --&amp;gt; R[&quot;After up to 2 to the 48 requests, reseed from the entropy source&quot;]
    R --&amp;gt; K
&lt;p&gt;Now put every break on one page. This is the spine of the article: read down the two middle columns and each disaster resolves to a single knob turned the wrong way in a single place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 1: The Failure Catalog.&lt;/strong&gt;&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;Knob broken&lt;/th&gt;
&lt;th&gt;Place&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Netscape SSL [@src-netscape1996]&lt;/td&gt;
&lt;td&gt;1996&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;generator&lt;/td&gt;
&lt;td&gt;Seed was time + PID + PPID&lt;/td&gt;
&lt;td&gt;Never seed from the clock or a PID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debian OpenSSL [@src-debian2008]&lt;/td&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;generator&lt;/td&gt;
&lt;td&gt;Entropy input removed, keyspace ~2^15&lt;/td&gt;
&lt;td&gt;Never delete an entropy source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PS3 signing key [@src-fail0verflow2010]&lt;/td&gt;
&lt;td&gt;2010&lt;/td&gt;
&lt;td&gt;repeated&lt;/td&gt;
&lt;td&gt;role (nonce)&lt;/td&gt;
&lt;td&gt;Same ECDSA nonce k on every signature&lt;/td&gt;
&lt;td&gt;A signature nonce must be unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android Bitcoin [@src-android2013]&lt;/td&gt;
&lt;td&gt;2013&lt;/td&gt;
&lt;td&gt;repeated&lt;/td&gt;
&lt;td&gt;role (nonce)&lt;/td&gt;
&lt;td&gt;SecureRandom mis-init repeated k&lt;/td&gt;
&lt;td&gt;Seed the RNG before drawing a nonce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mining Ps and Qs [@src-heninger2012]&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;generator&lt;/td&gt;
&lt;td&gt;Keys generated before pool seeded&lt;/td&gt;
&lt;td&gt;Do not make keys at cold boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual_EC_DRBG [@src-shumow2007]&lt;/td&gt;
&lt;td&gt;2007-15&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;generator&lt;/td&gt;
&lt;td&gt;Trapdoor in chosen points P, Q&lt;/td&gt;
&lt;td&gt;Choose constants verifiably; never trust one source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM-snapshot reset [@src-ristenpart2010]&lt;/td&gt;
&lt;td&gt;2010-14&lt;/td&gt;
&lt;td&gt;repeated&lt;/td&gt;
&lt;td&gt;generator&lt;/td&gt;
&lt;td&gt;Snapshot clones generator state&lt;/td&gt;
&lt;td&gt;Force a reseed after any clone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DUHK [@src-duhk2018]&lt;/td&gt;
&lt;td&gt;2017&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;generator&lt;/td&gt;
&lt;td&gt;Hard-coded X9.31 seed key&lt;/td&gt;
&lt;td&gt;Never hard-code the seed key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BEAST [@src-cve2011]&lt;/td&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;role (CBC IV)&lt;/td&gt;
&lt;td&gt;TLS 1.0 chained the previous ciphertext block as IV&lt;/td&gt;
&lt;td&gt;A CBC IV must be unpredictable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCM nonce reuse in TLS [@src-cve2016]&lt;/td&gt;
&lt;td&gt;2016&lt;/td&gt;
&lt;td&gt;repeated&lt;/td&gt;
&lt;td&gt;role (nonce)&lt;/td&gt;
&lt;td&gt;Repeated 96-bit nonce under one key&lt;/td&gt;
&lt;td&gt;Never reuse a GCM nonce per key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CTR_DRBG cache attack [@src-cohney2020]&lt;/td&gt;
&lt;td&gt;2020&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;generator&lt;/td&gt;
&lt;td&gt;AES-table cache timing leaks state&lt;/td&gt;
&lt;td&gt;Prefer a constant-time core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PuTTY P-521 [@src-putty2024]&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;predictable&lt;/td&gt;
&lt;td&gt;role (nonce)&lt;/td&gt;
&lt;td&gt;Biased k, first 9 bits always zero [@src-putty-oss]&lt;/td&gt;
&lt;td&gt;A nonce must be uniform or derived&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two columns, two values each. Every row is one knob times one place. Hold this table in mind; Section 10 hands it back to you as the Common Misuse catalog, the same evidence seen from the developer&apos;s chair.&lt;/p&gt;

Dual_EC did more than embarrass a standard; it changed how the field designs generators. Before it, the working assumption was &quot;trust the standardized construction.&quot; After it, the assumption became &quot;mix multiple sources and trust no single one, and choose your constants so anyone can verify there is no hidden structure.&quot; That is why modern kernels fold hardware RNG output *into* a pool rather than using it raw, why NIST&apos;s newer curves come with published nothing-up-my-sleeve derivations, and why SP 800-90C validates the *whole* pipeline instead of asking you to trust the source and the DRBG separately [@src-sp80090c].&lt;p&gt;The lesson generalizes: a generator that can be back-doored by whoever picks its constants is not secure just because it passes tests. This philosophy shapes Sections 5 and 7.
&lt;/p&gt;&lt;p&gt;&lt;/p&gt;
&lt;p&gt;The most surprising fix of all runs the other direction. After the PlayStation 3, the Bitcoin wallets, and eventually PuTTY, all showed that a &lt;em&gt;random&lt;/em&gt; per-signature nonce is catastrophic when it repeats or leaks,Recovery differs by failure mode. A &lt;em&gt;repeated&lt;/em&gt; nonce means two signatures share $k$, so simple algebra recovers the private key, as on the PlayStation 3. A &lt;em&gt;biased&lt;/em&gt; nonce, like PuTTY&apos;s nine always-zero leading bits, falls to a lattice attack on the &lt;em&gt;Hidden Number Problem&lt;/em&gt;: each signature leaks a few key bits, and lattice reduction solves for the key once about 60 accumulate [@src-putty-oss]. RFC 6979 (2013) responded by &lt;em&gt;removing&lt;/em&gt; the randomness: it derives the ECDSA nonce deterministically from the private key and the message, so that signers, in the RFC&apos;s own words,&lt;/p&gt;

&quot;...do not need access to a source of high-quality randomness.&quot; -- RFC 6979, on deterministic ECDSA nonces [@src-rfc6979]
&lt;p&gt;The fix for a randomness disaster was &lt;em&gt;less&lt;/em&gt; randomness. That inversion is the clue that the generator is only half the story, and often not the interesting half. Seven generations and a dozen named disasters later, the modern architecture is strikingly simple once you see what every break was really teaching. Strip away the history and one design remains, built from two layers most engineers never separate.&lt;/p&gt;
&lt;h2&gt;5. The Modern Architecture, and the Two Layers Underneath It&lt;/h2&gt;
&lt;p&gt;Every fix in Section 4 was pushing toward one shape, and here it is. A &lt;em&gt;true entropy source&lt;/em&gt; seeds a &lt;em&gt;computational DRBG&lt;/em&gt; that stretches a little unpredictability into unlimited output, with continuous reseeding, forward secrecy, and clone defense, and with distrust of any single component built in. Nothing exotic. Just forty years of scar tissue drawn as a single pipeline.&lt;/p&gt;

flowchart LR
    E[&quot;Entropy sources: interrupts, RDSEED, TPM&quot;] --&amp;gt; A[&quot;Accumulate and condition&quot;]
    A --&amp;gt; D[&quot;DRBG expansion: AES-CTR or ChaCha20&quot;]
    D --&amp;gt; I[&quot;OS interface: getrandom, arc4random, BCryptGenRandom&quot;]
    I --&amp;gt; R[&quot;Consumer roles: key, nonce, IV, salt&quot;]
    D -.-&amp;gt;|&quot;reseed and fast key erasure&quot;| A
&lt;p&gt;Read it left to right. Physical noise (interrupt timing, the &lt;code&gt;RDSEED&lt;/code&gt; instruction, a TPM) is accumulated and conditioned into a seed. A DRBG expands that seed into as many bytes as anyone asks for. An operating-system interface hands those bytes to your program, which pours them into one of four roles. The dotted return edge carries the two properties that keep the whole thing honest: reseeding, which periodically mixes fresh entropy back in, and forward secrecy, which we define in a moment.&lt;/p&gt;
&lt;p&gt;The distinction the whole subject turns on is the one from Sections 2 and 3, now with a job. Computational unpredictability is about what an adversary can compute, and the DRBG delivers it by construction, provided the seed carried real entropy. A DRBG does not &lt;em&gt;create&lt;/em&gt; entropy; it stretches it. Garbage seed in, garbage out, which is why the entire left side of the diagram exists.&lt;/p&gt;

A generator has *forward secrecy* if an attacker who compromises its state at some moment still cannot reconstruct output it produced *earlier*. ChaCha20-based generators achieve it by *fast key erasure*: after generating a block, the first bytes of fresh keystream overwrite the generator&apos;s own key, and only the remaining bytes are handed out [@src-donenfeld2022]. The key that produced earlier output no longer exists anywhere, so a later memory capture cannot roll time backward.

Periodically folding fresh entropy into the generator&apos;s state, on a schedule (SP 800-90A mandates a reseed within $2^{48}$ requests) or on an event (after a `fork()`, after a VM resumes from a snapshot) [@src-sp80090a]. Reseeding gives *backward* secrecy: even if the state was compromised, once enough new entropy is mixed in, future output becomes unpredictable again.

sequenceDiagram
    participant S as Generator state
    participant O as Output
    S-&amp;gt;&amp;gt;O: emit one block of keystream
    S-&amp;gt;&amp;gt;S: overwrite the key with fresh keystream
    Note over S: the old key is gone, so earlier output cannot be recomputed
&lt;p&gt;Now the correction that clears away half the folklore in this field: the difference between an &lt;em&gt;OS interface&lt;/em&gt; and a &lt;em&gt;CPU instruction&lt;/em&gt;. These are two different layers, and conflating them is the source of endless bad advice.&lt;/p&gt;
&lt;p&gt;An &lt;strong&gt;OS-provided interface&lt;/strong&gt; is what your code should call: &lt;code&gt;getrandom(2)&lt;/code&gt; or &lt;code&gt;/dev/urandom&lt;/code&gt; on Linux, &lt;code&gt;getentropy(3)&lt;/code&gt; or &lt;code&gt;arc4random(3)&lt;/code&gt; on BSD and macOS, &lt;code&gt;BCryptGenRandom&lt;/code&gt; or &lt;code&gt;ProcessPrng&lt;/code&gt; on Windows [@src-getrandom, @src-arc4random]. On Linux, &lt;code&gt;getrandom&lt;/code&gt; blocks &lt;em&gt;once&lt;/em&gt; until the pool is first seeded and then never again, which is what finally ended the &lt;code&gt;/dev/random&lt;/code&gt;-versus-&lt;code&gt;/dev/urandom&lt;/code&gt; dilemma at the interface level [@src-getrandom, @src-lwn2014].&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;CPU instruction&lt;/strong&gt; is what the operating system &lt;em&gt;consumes&lt;/em&gt;, not what you call. Intel&apos;s &lt;code&gt;RDRAND&lt;/code&gt; returns the output of an on-die hardware DRBG; &lt;code&gt;RDSEED&lt;/code&gt; returns near-raw, seed-grade entropy [@src-intel-drng]. The kernel does not hand these to you and does not trust them on their own. It &lt;em&gt;mixes&lt;/em&gt; them into its pool alongside other sources, precisely the post-Dual_EC discipline: never let one opaque component be the whole answer [@src-donenfeld2022].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The rule is a single sentence: applications call the OS interface; only the kernel touches the CPU instruction. If you find yourself calling &lt;code&gt;RDRAND&lt;/code&gt; directly in application code, you have skipped the layer that mixes, reseeds, and forward-secures the output, and you are trusting one piece of opaque silicon with the whole job, the exact mistake Dual_EC taught the field never to make [@src-shumow2007]. Call &lt;code&gt;getrandom&lt;/code&gt;, &lt;code&gt;getentropy&lt;/code&gt;, or &lt;code&gt;BCryptGenRandom&lt;/code&gt;, and let the kernel decide how much to trust the hardware [@src-getrandom, @src-donenfeld2022].&lt;/p&gt;
&lt;/blockquote&gt;

flowchart TD
    APP[&quot;Your application&quot;] --&amp;gt;|&quot;calls this&quot;| OSI[&quot;OS interface: getrandom, arc4random, BCryptGenRandom&quot;]
    OSI --&amp;gt; POOL[&quot;Kernel entropy pool and DRBG&quot;]
    POOL --&amp;gt; OSI
    RDRAND[&quot;CPU RDRAND: on-die DRBG output&quot;] -.-&amp;gt;|&quot;mixed in, never trusted alone&quot;| POOL
    RDSEED[&quot;CPU RDSEED: seed-grade entropy&quot;] -.-&amp;gt;|&quot;mixed in, never trusted alone&quot;| POOL

So why not skip the pool and read `RDRAND` directly? Because mixing is what turns &quot;trust no single source&quot; from a slogan into a working defense. If the kernel folds `RDRAND` into a pool that also holds interrupt timing and a saved seed, then even a compromised `RDRAND` cannot by itself make the output predictable, because it is only one of several inputs to the mix. Trust is not placed in any one component; it is *diluted* across all of them. We return to the hardware-versus-software question in Section 7.
&lt;p&gt;Seen this way, the generator&apos;s entire job is to manufacture knob number one, unpredictability, and to keep manufacturing it reliably. Everything else in the pipeline, reseeding, forward secrecy, clone defense, architected distrust, exists to stop that one property from silently failing. The four decades of history collapse into a single sentence: keep the seed unguessable, keep the state fresh, and never let one component be the whole answer.&lt;/p&gt;
&lt;p&gt;Windows implements this same pipeline with a CTR_DRBG tree behind &lt;code&gt;BCryptGenRandom&lt;/code&gt; and &lt;code&gt;ProcessPrng&lt;/code&gt;; the platform-specific internals, including how the kernel seeds per-processor generators and defends against VM cloning, are the subject of a dedicated &lt;a href=&quot;https://paragmali.com/blog/a-key-is-only-as-unguessable-as-the-dice-that-made-it-inside/&quot; rel=&quot;noopener&quot;&gt;companion article on the Windows CSPRNG&lt;/a&gt;, and the &lt;a href=&quot;https://paragmali.com/blog/cng-architecture-bcrypt-ncrypt-ksps/&quot; rel=&quot;noopener&quot;&gt;CNG API surface&lt;/a&gt; itself lives in a companion post. We will not re-derive them here.&lt;/p&gt;
&lt;p&gt;That is the generator half, essentially solved as engineering. But a flawless generator is only half the battle, because the same perfect byte stream is catastrophic if you pour it into the wrong role. What, exactly, does each role demand?&lt;/p&gt;
&lt;h2&gt;6. What Ships in 2026, and the Four Roles&lt;/h2&gt;
&lt;p&gt;By 2026 the generator architecture is settled engineering, and most real-world breaks happen &lt;em&gt;after&lt;/em&gt; the generator, when good bytes are used wrong. So we cover what ships in a paragraph, then spend the rest of the section on the practitioner&apos;s heart of the matter: the four roles and the exact property mix each one demands.&lt;/p&gt;
&lt;p&gt;What ships: the SP 800-90A trio (CTR_DRBG, HMAC_DRBG, Hash_DRBG) [@src-sp80090a]; Fortuna on FreeBSD [@src-freebsd-random]; and ChaCha20-based generators reachable through Linux &lt;code&gt;getrandom&lt;/code&gt; and OpenBSD or macOS &lt;code&gt;arc4random&lt;/code&gt; [@src-donenfeld2022, @src-arc4random].&lt;/p&gt;
&lt;p&gt;The 2025 capstone is SP 800-90C, which stops treating &quot;the entropy source&quot; and &quot;the DRBG&quot; as separate acts of faith and validates the whole pipeline as one of four RBG construction classes: RBG1 (seeded once, no ongoing source, for constrained devices), RBG2 (continuous access to a validated SP 800-90B source, the common server case), RBG3 (an enhanced, full-entropy construction combining a physical source directly with a DRBG), and RBGC (chained constructions built from approved components) [@src-sp80090c].&lt;/p&gt;
&lt;p&gt;A pre-draft SP 800-90A Rev. 2 adds an XOF_DRBG built on the SHAKE functions from FIPS 202, forward-looking but not yet final [@src-sp80090a-r2].&lt;/p&gt;
&lt;p&gt;Post-quantum cryptography makes randomness &lt;em&gt;more&lt;/em&gt; foundational, not less. The 2024 standards each draw fresh CSPRNG output per operation: ML-KEM consumes random seeds during key generation and encapsulation [@src-fips203], ML-DSA draws a fresh value per signature [@src-fips204], and SLH-DSA pulls several seeds per operation [@src-fips205]. More draws mean more surface for a weak generator to ruin, which is why the &lt;a href=&quot;https://paragmali.com/blog/post-quantum-cryptography-on-windows-the-thirty-year-migrati/&quot; rel=&quot;noopener&quot;&gt;post-quantum migration guides&lt;/a&gt; for these algorithms treat the CSPRNG as a first-class dependency.&lt;/p&gt;
&lt;p&gt;Now the centerpiece. There are exactly four roles a random value plays in cryptography, and each demands a &lt;em&gt;different&lt;/em&gt; combination of three properties: &lt;em&gt;uniqueness&lt;/em&gt;, &lt;em&gt;unpredictability&lt;/em&gt;, and &lt;em&gt;secrecy&lt;/em&gt;. Getting the combination wrong is how a perfect generator still gets you owned.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 2: The four roles and what each one owes.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Unique?&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Unpredictable?&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Secret?&lt;/th&gt;
&lt;th&gt;Canonical failure if violated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;yes&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;yes&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guessable key: Netscape 1996, Debian 2008 [@src-debian2008]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CBC IV&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;yes&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;no&lt;/td&gt;
&lt;td&gt;Predictable IV enabled BEAST [@src-cve2011]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CTR/GCM nonce&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;no&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;no&lt;/td&gt;
&lt;td&gt;Repeated nonce leaks the auth key: GCM in TLS 2016 [@src-cve2016]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Salt&lt;/strong&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;yes&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;no (but random)&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reused or global salt enables shared precomputed tables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Read the differences, because they are the whole point.&lt;/p&gt;

A *nonce* is a &quot;number used once.&quot; Its one absolute requirement is *uniqueness* per key: no two operations under the same key may ever use the same nonce. A nonce does *not* have to be unpredictable, and it does *not* have to be secret. A simple monotonic counter is a perfectly good nonce, and often the safest one, because a counter cannot accidentally repeat the way random draws can.

An *IV* is the per-message starting value for a block-cipher mode, and its requirement depends entirely on the mode. In CBC, the IV must be both *unpredictable* and *unique*. In CTR and GCM, the value the spec labels &quot;IV&quot; is really a *nonce*, and needs only to be *unique*. The word &quot;IV&quot; therefore does not name a fixed requirement; the *mode* names the requirement. This is the single most common source of confusion in applied randomness.
&lt;p&gt;The nonce-versus-IV correction deserves stating precisely, because folklore gets it backwards. The &lt;em&gt;same value slot&lt;/em&gt; is called an &quot;IV&quot; or a &quot;nonce&quot; depending on the mode, and the &lt;em&gt;mode&lt;/em&gt; dictates the requirement.&lt;/p&gt;
&lt;p&gt;A CBC IV must be unpredictable &lt;em&gt;and&lt;/em&gt; unique. TLS 1.0 chained CBC records by using the previous ciphertext block as the next IV, which is &lt;em&gt;predictable&lt;/em&gt;, and that predictability is exactly what the BEAST attack exploited in 2011 (CVE-2011-3389). TLS 1.1 restored a fresh random IV, and the modern answer is to move to authenticated encryption entirely [@src-cve2011, @src-imperialviolet].&lt;/p&gt;
&lt;p&gt;A CTR or GCM nonce, by contrast, needs &lt;em&gt;only&lt;/em&gt; to be unique; unpredictability buys nothing. But uniqueness is absolute: reuse a GCM nonce under one key even once and you immediately leak the XOR of the two plaintexts, and the reused authentication tags give a polynomial equation whose roots recover the GHASH subkey $H$ (Joux&apos;s forbidden attack), enabling universal forgery. That failure was measured across live TLS servers in 2016 under the name &quot;Nonce-Disrespecting Adversaries&quot; and captured for IBM Domino as CVE-2016-0270 [@src-cve2016].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; GCM&apos;s security collapses the instant a nonce repeats under a given key. Two messages encrypted with the same key and nonce leak the XOR of their plaintexts &lt;em&gt;and&lt;/em&gt;, worse, give up the polynomial equation that recovers the GHASH subkey $H$, after which an attacker can forge arbitrary messages that pass authentication [@src-cve2016]. There is no partial failure here and no recovery: one repeat is game over for that key. Use a counter, or if you cannot guarantee uniqueness across senders, use a nonce-misuse-resistant mode (Section 7).&lt;/p&gt;
&lt;/blockquote&gt;

flowchart TD
    V[&quot;A value in the IV or nonce slot&quot;] --&amp;gt; Q{&quot;Which mode?&quot;}
    Q --&amp;gt;|&quot;CBC&quot;| CBC[&quot;Must be unpredictable AND unique. A predictable IV enabled BEAST&quot;]
    Q --&amp;gt;|&quot;CTR or GCM&quot;| CTR[&quot;Must be unique only. Reuse leaks the authentication key&quot;]
&lt;p&gt;The last role, the salt, is where a different myth does the damage.&lt;/p&gt;

A *salt* is a unique, random value added to a password before hashing, and it is *not secret*: it is stored in plaintext right next to the resulting hash. Its only job is uniqueness, so that identical passwords hash to different values and one precomputed table cannot attack many accounts at once. A salt needs to be unique and random; it does not need to be unpredictable or hidden.
&lt;p&gt;Believing a salt must be secret, or reusing one global salt across every password, is a common and wrong model. A per-password random salt of 16 bytes, stored openly, does its entire job; hiding it adds nothing, and sharing it across users throws the job away, because then one precomputed table works against every account again [@src-nist-63b].The same reuse math retired WEP: a short per-packet IV space made keystream reuse inevitable, which broke the confidentiality of the whole scheme [@src-bgw2001]. That story belongs to &lt;a href=&quot;https://paragmali.com/blog/rotating-every-cipher-schannel-and-the-twenty-year-algorithm/&quot; rel=&quot;noopener&quot;&gt;cipher agility&lt;/a&gt; and is told in the SChannel post.&lt;/p&gt;
&lt;p&gt;{`
const crypto = require(&apos;crypto&apos;);&lt;/p&gt;
&lt;p&gt;// KEY: secret + unpredictable + high-entropy. Straight from the OS CSPRNG.
const key = crypto.randomBytes(32);          // 256 bits, for AES-256 or ChaCha20&lt;/p&gt;
&lt;p&gt;// GCM NONCE: unique only. A monotonic counter guarantees uniqueness by construction.
let counter = 0n;
function nextNonce() {
  const n = Buffer.alloc(12);                // 96-bit GCM nonce
  n.writeBigUInt64BE(counter, 4);            // 64-bit counter in the low bytes
  counter += 1n;                             // never repeats under this key
  return n;
}&lt;/p&gt;
&lt;p&gt;// SALT: unique + random, stored in the clear next to the password hash.
const salt = crypto.randomBytes(16);
console.log(&apos;salt is PUBLIC, stored with the hash:&apos;, salt.toString(&apos;hex&apos;));&lt;/p&gt;
&lt;p&gt;// The WRONG lines, for contrast:
//   const nonce = Buffer.alloc(12);                 // reused -&amp;gt; repeated knob, in a role
//   const salt  = Buffer.from(String(Math.random())); // Math.random is not a CSPRNG -&amp;gt; predictable&lt;/p&gt;
&lt;p&gt;console.log(&apos;key bytes:&apos;, key.length, &apos;nonce bytes:&apos;, nextNonce().length);
console.log(&apos;same generator, three different disciplines&apos;);
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A perfect generator poured into the wrong role is as fatal as a broken generator. The key must be secret, unpredictable, and high-entropy; the CBC IV unpredictable and unique; the CTR or GCM nonce unique and nothing more; the salt unique, random, and public. Four roles, one generator, four different disciplines. Learn which mix each role owes and most of the Failure Catalog stops being possible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now the reader knows what each role demands. But which generator should manufacture the bits, and is a hardware RNG better than a software one? The honest answer overturns the question.&lt;/p&gt;
&lt;h2&gt;7. CTR_DRBG, Fortuna, and ChaCha, and Hardware Versus Software&lt;/h2&gt;
&lt;p&gt;&quot;Which cipher makes the best RNG?&quot; is the wrong question. The real axis is &lt;em&gt;architecture&lt;/em&gt;: how you &lt;em&gt;source&lt;/em&gt; entropy and how you &lt;em&gt;expand&lt;/em&gt; it. Get that framing right and the &quot;competitors&quot; turn out to be solving different halves of the same problem.&lt;/p&gt;
&lt;p&gt;Start with a category correction. Fortuna is largely an &lt;em&gt;accumulator&lt;/em&gt;, the sourcing half: its contribution is how it collects and schedules entropy. CTR_DRBG and ChaCha20 are &lt;em&gt;expanders&lt;/em&gt;, the expansion half: their contribution is how they stretch a seed. A real system runs an accumulator front-end feeding an expander core, so &quot;CTR_DRBG versus Fortuna&quot; is partly a comparison between a fuel tank and an engine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 3: Deployed generators, head to head.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;CTR_DRBG&lt;/th&gt;
&lt;th&gt;HMAC_DRBG&lt;/th&gt;
&lt;th&gt;Fortuna&lt;/th&gt;
&lt;th&gt;ChaCha20&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Output cost&lt;/td&gt;
&lt;td&gt;~1 AES per 128 bits&lt;/td&gt;
&lt;td&gt;~1 HMAC per block&lt;/td&gt;
&lt;td&gt;cipher core per block&lt;/td&gt;
&lt;td&gt;1 ChaCha permutation per 512 bits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State size&lt;/td&gt;
&lt;td&gt;Key + 128-bit V&lt;/td&gt;
&lt;td&gt;two hash-length values&lt;/td&gt;
&lt;td&gt;32 pools + generator key&lt;/td&gt;
&lt;td&gt;256-bit key, per CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forward secrecy&lt;/td&gt;
&lt;td&gt;Update re-derives (Key, V)&lt;/td&gt;
&lt;td&gt;Update re-derives (Key, V)&lt;/td&gt;
&lt;td&gt;frequent generator rekey&lt;/td&gt;
&lt;td&gt;fast key erasure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Side channel&lt;/td&gt;
&lt;td&gt;AES-table lookups can leak [@src-cohney2020]&lt;/td&gt;
&lt;td&gt;HMAC, low risk&lt;/td&gt;
&lt;td&gt;depends on core&lt;/td&gt;
&lt;td&gt;constant-time by design [@src-chacha]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entropy philosophy&lt;/td&gt;
&lt;td&gt;assumes a 90B seed&lt;/td&gt;
&lt;td&gt;assumes a 90B seed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;accumulates&lt;/strong&gt;, no estimate&lt;/td&gt;
&lt;td&gt;assumes a pool seed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reseed&lt;/td&gt;
&lt;td&gt;up to 2^48 requests&lt;/td&gt;
&lt;td&gt;up to 2^48 requests&lt;/td&gt;
&lt;td&gt;32-pool staggered&lt;/td&gt;
&lt;td&gt;continuous, per CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIPS 140&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes, validated&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;not yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;per instance&lt;/td&gt;
&lt;td&gt;per instance&lt;/td&gt;
&lt;td&gt;single system RNG&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;per CPU&lt;/strong&gt; since Linux 2022&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;FIPS boundaries, AES-NI&lt;/td&gt;
&lt;td&gt;deterministic signatures&lt;/td&gt;
&lt;td&gt;accumulator front-end&lt;/td&gt;
&lt;td&gt;modern software default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Two 2026 differences decide most real choices. First, ChaCha20 is &lt;em&gt;constant-time&lt;/em&gt; by design, with no secret-dependent branches or table lookups,Constant-time means the code&apos;s execution path and memory-access pattern do not depend on secret values, so an attacker watching timing or cache behavior learns nothing about the state. so it sidesteps the cache-timing state-recovery that Cohney et al. demonstrated against real CTR_DRBG deployments in 2020 [@src-cohney2020, @src-chacha]. Second, CTR_DRBG holds &lt;em&gt;FIPS 140 validation&lt;/em&gt;, which is exactly why it still dominates inside compliance boundaries and on AES-NI hardware despite the side-channel result [@src-sp80090a]. Linux&apos;s 2022 move to per-CPU ChaCha instances closed the historic concurrency gap that once favored other designs [@src-donenfeld2022].&lt;/p&gt;

Two honest philosophies for the sourcing half. *Estimation* (NIST SP 800-90B) models the noise source, computes a min-entropy estimate, and runs health tests to catch degradation [@src-sp80090b]. *Accumulation* (Fortuna) refuses to estimate at all: it spreads incoming events across many pools and drains them on a schedule that guarantees at least one pool eventually holds enough entropy, whatever the true rate [@src-fortuna2003, @src-cryptoeng]. One measures; the other structures itself so that measurement is unnecessary.

flowchart TD
    EV[&quot;Entropy events arrive&quot;] --&amp;gt; RR[&quot;Round-robin into pools P0 through P31&quot;]
    RR --&amp;gt; P0[&quot;P0 drains at every reseed&quot;]
    RR --&amp;gt; PI[&quot;Pi drains only when 2 to the i divides the reseed number&quot;]
    RR --&amp;gt; P31[&quot;P31 fills rarely but very deeply&quot;]
    P0 --&amp;gt; RK[&quot;Reseed the generator key&quot;]
    PI --&amp;gt; RK
    P31 --&amp;gt; RK
&lt;p&gt;The hardware-versus-software debate resolves the same way: not by choosing, but by mixing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 4: Hardware RNG versus software DRBG.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Hardware (RDRAND/RDSEED, TPM)&lt;/th&gt;
&lt;th&gt;Software DRBG (CTR/ChaCha)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Source of unpredictability&lt;/td&gt;
&lt;td&gt;physical noise on the die&lt;/td&gt;
&lt;td&gt;deterministic expansion of a seed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auditability&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;opaque&lt;/strong&gt;, you cannot inspect silicon&lt;/td&gt;
&lt;td&gt;open, testable, formally analyzable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failure mode&lt;/td&gt;
&lt;td&gt;silent bias or back door (the Dual_EC lesson) [@src-shumow2007]&lt;/td&gt;
&lt;td&gt;bad seed, state clone, side channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Forward secrecy&lt;/td&gt;
&lt;td&gt;vendor-dependent&lt;/td&gt;
&lt;td&gt;explicit (key erasure or Update)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;seed-grade input only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;auditable expansion + forward secrecy&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Hardware supplies physical unpredictability but is a black box you cannot audit; software supplies auditable, forward-secret expansion but only stretches a seed. The field&apos;s resolution is to fold hardware output &lt;em&gt;into&lt;/em&gt; the pool rather than trust it directly [@src-donenfeld2022]. &quot;Trust &lt;code&gt;RDRAND&lt;/code&gt; alone&quot; fell out of favor precisely because Dual_EC proved silicon and standards can both be compromised [@src-shumow2007].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This article cross-references &lt;a href=&quot;https://paragmali.com/blog/a-key-is-only-as-unguessable-as-the-dice-that-made-it-inside/&quot; rel=&quot;noopener&quot;&gt;the Windows CSPRNG&lt;/a&gt; rather than duplicating it. How CNG assembles a per-processor CTR_DRBG tree, seeds it, and defends against VM cloning is covered in full there.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;On the consumer side, the same &quot;which construction&quot; question has its own honest table, this time about how much uniqueness burden you are willing to carry.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 5: Plain GCM versus GCM-SIV versus deterministic.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;AES-GCM (counter nonce)&lt;/th&gt;
&lt;th&gt;AES-GCM (random nonce)&lt;/th&gt;
&lt;th&gt;AES-GCM-SIV&lt;/th&gt;
&lt;th&gt;RFC 6979 (signatures)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Uniqueness burden&lt;/td&gt;
&lt;td&gt;on the caller (counter)&lt;/td&gt;
&lt;td&gt;birthday cap ~2^32 per key&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;tolerates repeats&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none (deterministic)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost vs GCM&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;~2x&lt;/td&gt;
&lt;td&gt;not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Catastrophic on reuse?&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use when&lt;/td&gt;
&lt;td&gt;you control a counter&lt;/td&gt;
&lt;td&gt;low message volume&lt;/td&gt;
&lt;td&gt;uniqueness not guaranteed&lt;/td&gt;
&lt;td&gt;ECDSA/DSA nonces&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;AES-GCM-SIV (RFC 8452) derives its internal inputs from a synthetic value computed over the nonce &lt;em&gt;and&lt;/em&gt; the plaintext, so a repeated nonce with different messages no longer leaks the authentication subkey [@src-rfc8452]. In the RFC&apos;s own words, such schemes are built so that they&lt;/p&gt;

&quot;...do not fail catastrophically if a nonce is repeated.&quot; -- RFC 8452, on AES-GCM-SIV [@src-rfc8452]
&lt;p&gt;It costs roughly twice a plain GCM encryption and is the right answer when you cannot &lt;em&gt;guarantee&lt;/em&gt; unique nonces, for example across many independent senders. RFC 6979 is the other escape hatch, for signatures: derive the nonce deterministically and the uniqueness problem disappears entirely [@src-rfc6979].&lt;/p&gt;

It is tempting to declare a winner between NIST&apos;s estimation and Fortuna&apos;s accumulation, but both ship in 2026 and neither is wrong. Estimation gives you a number you can put in a validation report and a health test that fires when the source degrades [@src-sp80090b]. Accumulation gives you robustness without ever having to trust a number, at the cost of absorbing a burst of fresh entropy more slowly, since high-index pools reseed rarely [@src-fortuna2003]. The mature view is that they are different risk trades over the same unmeasurable quantity, which is the subject of the next section.
&lt;p&gt;The engineering looks won: mix a validated source into a constant-time, forward-secret DRBG behind an un-misusable syscall. So why does the field still call randomness an open problem? Because underneath the engineering sit limits no code can cross.&lt;/p&gt;
&lt;h2&gt;8. What You Cannot Know or Prove&lt;/h2&gt;
&lt;p&gt;Everything so far was engineering, and engineering can be improved. This section is about the floor beneath it: things you fundamentally &lt;em&gt;cannot&lt;/em&gt; do, no matter how good your code is. This is where confidence turns to humility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Min-entropy is unmeasurable.&lt;/strong&gt; You cannot &lt;em&gt;measure&lt;/em&gt; the min-entropy of an unknown physical source from its output; you can only &lt;em&gt;estimate&lt;/em&gt; it under a model of how the source behaves. SP 800-90B&apos;s entire apparatus, its estimators and its health tests, is a disciplined estimate, not a proof [@src-sp80090b]. The whole tower we built in Sections 5 through 7 rests on this one estimate, and if the model is wrong, everything above it is quietly weaker than its paperwork claims.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Statistical tests can falsify, never certify.&lt;/strong&gt; SP 800-22 is explicitly hypothesis testing with P-values: a suite can &lt;em&gt;reject&lt;/em&gt; a generator, but it can never &lt;em&gt;prove&lt;/em&gt; unpredictability [@src-sp80022]. The proof by counterexample is the Mersenne Twister from Section 3: it passes the standard batteries and is fully broken by 624 outputs [@src-mersenne1998].The named suites, SP 800-22, Dieharder, TestU01, are useful for catching gross defects and nothing more. Running more of them does not raise your security; it only lowers the chance you shipped something obviously broken. &quot;More tests&quot; is not &quot;more secure.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Every deployed computational CSPRNG rests on an unproven assumption.&lt;/strong&gt; The best a deterministic generator achieves is the next-bit guarantee, but that guarantee is conditional: cryptographically secure generators exist &lt;em&gt;if and only if&lt;/em&gt; one-way functions exist, an equivalence whose hard direction (any one-way function yields a generator) was proved by Hastad, Impagliazzo, Levin, and Luby [@src-hill1999, @src-katzlindell]. The existence of one-way functions is itself unproven, since it would imply that $P \neq NP$. There is no unconditionally secure &lt;em&gt;pseudorandom&lt;/em&gt; generator. Every CSPRNG you have ever used is secure only relative to a hardness assumption no one has proved.&lt;/p&gt;

For an $m$-bit random value drawn repeatedly, collisions become likely after roughly $2^{m/2}$ draws, not $2^m$, because the chance is dominated by pairs. For a 96-bit GCM nonce, the 50% collision point sits near $2^{48}$ draws. This is a *provable ceiling*, not an implementation defect. It is why SP 800-38D caps random 96-bit nonces well below that point, at about $2^{32}$ messages per key, to keep the repeat probability negligibly small [@src-sp80038d].
&lt;p&gt;&lt;strong&gt;The birthday bound is a hard uniqueness ceiling.&lt;/strong&gt; Randomness cannot buy its way past its own birthday bound. The only escapes are to &lt;em&gt;stop drawing randomly&lt;/em&gt;, using a counter whose uniqueness is structural, or to &lt;em&gt;tolerate&lt;/em&gt; repeats with a nonce-misuse-resistant mode. There is no third option in which random draws stay unique forever.&lt;/p&gt;
&lt;p&gt;{`
// Birthday bound: for an m-bit value, collisions get likely near 2^(m/2).
const m = 96;                              // GCM nonce size in bits&lt;/p&gt;
&lt;p&gt;const fiftyPercentPoint = Math.pow(2, m / 2);   // ~2^48 draws for 96 bits
console.log(&apos;nonce size:&apos;, m, &apos;bits&apos;);
console.log(&apos;~50% collision near 2^&apos; + (m / 2) + &apos; =&apos;, fiftyPercentPoint.toExponential(3), &apos;draws&apos;);&lt;/p&gt;
&lt;p&gt;// Birthday approximation for n draws into a 2^nBits space:
function collisionProb(nBits, n) {
  const space = Math.pow(2, nBits);
  return 1 - Math.exp(-(n * (n - 1)) / (2 * space));
}&lt;/p&gt;
&lt;p&gt;// SP 800-38D caps random 96-bit nonces at ~2^32 messages per key.
// Why 2^32 and not 2^48? Because the cap targets a TINY probability, not 50%:
const cap = Math.pow(2, 32);
console.log(&apos;at 2^32 draws, P(collision) =&apos;, collisionProb(96, cap).toExponential(3));
console.log(&apos;that is the safety margin SP 800-38D buys by capping at 2^32&apos;);
`}&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Back doors are undetectable by construction.&lt;/strong&gt; Dual_EC is the proof once more: a standardized generator can pass every statistical test and still be fully predictable to whoever chose its constants $P$ and $Q$ [@src-shumow2007]. You cannot test your way out of this, which is exactly why the field moved to verifiable constants and architected mixing rather than after-the-fact detection.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Clone defense is mitigation, not elimination.&lt;/strong&gt; &lt;code&gt;fork()&lt;/code&gt; and VM-snapshot cloning duplicate a &lt;em&gt;good&lt;/em&gt; generator [@src-ristenpart2010, @src-everspaugh2014]. VM Generation ID counters, library fork detection, and forced reseeds shrink the window but do not close it; Ferguson&apos;s 2019 Windows RNG whitepaper is explicit that a guest cannot fully solve the snapshot problem on its own, because it may not even know it was cloned until it has already emitted duplicate output [@src-ferguson2019win].&lt;/p&gt;
&lt;p&gt;And beneath all of it, the permanent gap Shannon left us in Section 2. His one-time pad is unconditionally secure but unscalable, a fresh truly-random key as long as every message. The CSPRNG is scalable but assumption-dependent. No cryptography closes that gap; we simply chose the scalable side and spend the rest of our effort defending it.&lt;/p&gt;

flowchart TD
    START[&quot;A randomness bug&quot;] --&amp;gt; K{&quot;Which property failed?&quot;}
    K --&amp;gt;|&quot;predictable&quot;| P{&quot;Where did it fail?&quot;}
    K --&amp;gt;|&quot;repeated&quot;| R{&quot;Where did it fail?&quot;}
    P --&amp;gt;|&quot;generator&quot;| PG[&quot;Netscape, Debian, Dual EC, DUHK, CTR_DRBG cache&quot;]
    P --&amp;gt;|&quot;role&quot;| PR[&quot;BEAST (CBC IV), PuTTY (biased nonce)&quot;]
    R --&amp;gt;|&quot;generator&quot;| RG[&quot;VM-snapshot clone&quot;]
    R --&amp;gt;|&quot;role&quot;| RR[&quot;PS3, Bitcoin, GCM nonce reuse&quot;]
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; You cannot certify unpredictability; you can only fail to falsify it. Min-entropy is estimated, not measured; statistical tests reject but never prove; the whole construction rests on an unproven hardness assumption; and back doors leave no trace in the output. The entire discipline of cryptographic randomness is the management of a risk you cannot directly measure. The surprise is not that randomness sometimes fails. It is that it works as well as it does.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If entropy is unmeasurable and unpredictability uncertifiable, where is randomness still actively failing today?&lt;/p&gt;
&lt;h2&gt;9. Where Randomness Is Still Failing&lt;/h2&gt;
&lt;p&gt;The architecture is settled; the frontier is not. Here are six places where cryptographic randomness is still an open engineering or standards problem in 2026, honestly contested rather than adjudicated.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Early-boot entropy on embedded devices, IoT, and VMs.&lt;/strong&gt; A device that must generate a key seconds after first power-on, before its pool has filled, is the exact condition &lt;em&gt;Mining Your Ps and Qs&lt;/em&gt; measured in 2012, and it still recurs in modern IoT field scans [@src-factorable]. Partial fixes exist: &lt;code&gt;getrandom&lt;/code&gt; blocking once until seeded [@src-getrandom], saved-seed files such as FreeBSD&apos;s &lt;code&gt;/boot/entropy&lt;/code&gt; [@src-freebsd-random], and mixing in &lt;code&gt;RDSEED&lt;/code&gt; or jitter-based entropy at boot. None fully solves a cold device with no stored seed and no timing history.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validating entropy sources at fleet scale.&lt;/strong&gt; SP 800-90B health tests run per device, but proving that a noise-source &lt;em&gt;model&lt;/em&gt; holds across millions of heterogeneous units, over years, across temperature swings and silicon aging, is unsolved [@src-sp80090b]. Fortuna&apos;s accumulation sidesteps &lt;em&gt;estimation&lt;/em&gt; but not &lt;em&gt;validation&lt;/em&gt;: you still have to argue the events carry entropy at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Making nonce-misuse resistance the default.&lt;/strong&gt; Plain AES-GCM fails catastrophically on a single nonce repeat [@src-rfc8452], yet plain AES-GCM, not AES-GCM-SIV, remains the default AEAD offered by most general-purpose libraries. This is an adoption and standards gap, not a research one. TLS 1.3&apos;s fixed-IV construction and the pending SP 800-38D revision help at the protocol layer [@src-sp80038d-news], but general-purpose libraries still hand developers the sharp tool by default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-quantum&apos;s larger draws and wider side-channel surface.&lt;/strong&gt; ML-DSA and its siblings consume more randomness and add rejection-sampling and secret-dependent code paths, which means more surface for a weak generator or a timing leak to do damage [@src-fips204]. 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&lt;/a&gt; is an opportunity to get seeding right and a risk if it is not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trusting hardware RNGs after Dual_EC.&lt;/strong&gt; &quot;Never trust a single hardware source&quot; is settled &lt;em&gt;practice&lt;/em&gt;, but a general, auditable way to &lt;em&gt;earn&lt;/em&gt; trust in opaque silicon is open [@src-shumow2007]. Cohney&apos;s cache attack showed that even a correct-on-paper CTR_DRBG can leak through microarchitectural side channels, so &quot;the math is right&quot; is not the end of the story [@src-cohney2020].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Formally verifying RNG code end to end.&lt;/strong&gt; Proofs typically assume &quot;the implementation does not leak,&quot; but real code leaks through caches, forks, and uninitialized memory. Individual primitives have been verified, but a whole-pipeline proof under a realistic leakage model, from entropy source through DRBG to syscall, is not yet routine.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Notice the pattern across all six. The &lt;em&gt;expansion&lt;/em&gt; problem, how to stretch a good seed into unlimited unpredictable output, is essentially solved: constant-time cores, forward secrecy, validated constructions. What remains open is &lt;em&gt;sourcing&lt;/em&gt; (getting and validating real entropy, especially at boot and at fleet scale) and &lt;em&gt;trust&lt;/em&gt; (how much to believe an opaque hardware source, and how to prove a whole implementation does not leak). The hard part was never the arithmetic. It is the physics and the epistemics around it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Physical noise is quietly back in fashion, incidentally.&lt;code&gt;RDSEED&lt;/code&gt;, on-chip ring-oscillator sources, quantum RNGs, and TPMs are all seeing renewed use as entropy inputs. The difference from the pre-Dual_EC era is that they are &lt;em&gt;mixed in&lt;/em&gt;, never trusted raw. The physics returned; the blind trust did not. These are the field&apos;s problems. But you have to ship code tomorrow. What are the rules, concretely, per role, that keep you out of the Failure Catalog?&lt;/p&gt;
&lt;h2&gt;10. The Field Guide, Made Operational&lt;/h2&gt;
&lt;p&gt;Everything collapses to a short set of rules of the form &quot;use X with these parameters in case Y.&quot; This is the two-knobs thesis turned into a checklist, and its mirror, the Common Misuse catalog, is simply the Failure Catalog seen from the developer&apos;s chair.&lt;/p&gt;
&lt;p&gt;Rule zero for the generator: never roll your own, and never hand-seed. Always call the operating system&apos;s CSPRNG.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 6: Generator, call this, not this.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Call this&lt;/th&gt;
&lt;th&gt;Never this&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getrandom(2)&lt;/code&gt;, &lt;code&gt;/dev/urandom&lt;/code&gt; [@src-getrandom]&lt;/td&gt;
&lt;td&gt;a hand-seeded userspace PRNG, blocking on &lt;code&gt;/dev/random&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BSD, macOS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;getentropy(3)&lt;/code&gt;, &lt;code&gt;arc4random(3)&lt;/code&gt; [@src-arc4random]&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rand()&lt;/code&gt;, &lt;code&gt;random()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BCryptGenRandom&lt;/code&gt;, &lt;code&gt;ProcessPrng&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;anything seeded by hand (see the CNG post)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language stdlib&lt;/td&gt;
&lt;td&gt;Node &lt;code&gt;crypto.randomBytes&lt;/code&gt;, Python &lt;code&gt;secrets&lt;/code&gt; / &lt;code&gt;os.urandom&lt;/code&gt;, Go &lt;code&gt;crypto/rand&lt;/code&gt;, Java &lt;code&gt;SecureRandom&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Math.random()&lt;/code&gt;, Python &lt;code&gt;random&lt;/code&gt;, Mersenne Twister, &lt;code&gt;java.util.Random&lt;/code&gt;, &lt;code&gt;System.Random&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Then, per role, the decision rules:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Key.&lt;/strong&gt; Draw from the OS CSPRNG at the primitive&apos;s exact key length: 32 bytes for AES-256 or ChaCha20. Nothing else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CTR/GCM nonce.&lt;/strong&gt; Prefer a 96-bit monotonic counter (uniqueness by construction). If you must draw randomly, cap usage at fewer than $2^{32}$ messages per key. Never reuse a nonce under one key. If uniqueness cannot be guaranteed across distributed senders, use AES-GCM-SIV [@src-rfc8452].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CBC IV.&lt;/strong&gt; A fresh &lt;em&gt;unpredictable&lt;/em&gt; value per message from the CSPRNG, never a counter and never the previous ciphertext block, or better, migrate to authenticated encryption.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Salt.&lt;/strong&gt; At least 16 bytes from the CSPRNG, unique per password, stored in plaintext. Never a global salt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signature nonce (k).&lt;/strong&gt; Use RFC 6979 deterministic k, or a hedged scheme; never a bare draw whose bias or repetition leaks the private key [@src-rfc6979].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fork or VM clone.&lt;/strong&gt; Rely on kernel reseed (VM Generation ID) plus library fork detection, and force a reseed after any clone or snapshot before emitting security-critical output [@src-ferguson2019win].&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Table 7: The Common Misuse catalog (the mirror of Table 1).&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Misuse&lt;/th&gt;
&lt;th&gt;Knob and place broken&lt;/th&gt;
&lt;th&gt;Named break&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Seeding from &lt;code&gt;time()&lt;/code&gt; or a PID&lt;/td&gt;
&lt;td&gt;predictable, generator&lt;/td&gt;
&lt;td&gt;Netscape [@src-netscape1996]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Removing or starving entropy&lt;/td&gt;
&lt;td&gt;predictable, generator&lt;/td&gt;
&lt;td&gt;Debian, Ps and Qs [@src-debian2008]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reusing a GCM nonce under one key&lt;/td&gt;
&lt;td&gt;repeated, role&lt;/td&gt;
&lt;td&gt;GCM in TLS 2016 [@src-cve2016]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Predictable or chained CBC IV&lt;/td&gt;
&lt;td&gt;predictable, role&lt;/td&gt;
&lt;td&gt;BEAST [@src-cve2011]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static or biased signature k&lt;/td&gt;
&lt;td&gt;repeated or predictable, role&lt;/td&gt;
&lt;td&gt;PS3, PuTTY [@src-putty2024]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret, short, or global salt&lt;/td&gt;
&lt;td&gt;repeated, role&lt;/td&gt;
&lt;td&gt;shared precomputed tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-seeding a userspace PRNG&lt;/td&gt;
&lt;td&gt;predictable, generator&lt;/td&gt;
&lt;td&gt;interface misuse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trusting &lt;code&gt;RDRAND&lt;/code&gt; alone&lt;/td&gt;
&lt;td&gt;predictable, generator&lt;/td&gt;
&lt;td&gt;the Dual_EC lesson [@src-shumow2007]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mersenne Twister or &lt;code&gt;Math.random&lt;/code&gt; for tokens&lt;/td&gt;
&lt;td&gt;predictable, generator&lt;/td&gt;
&lt;td&gt;statistical PRNG used for crypto [@src-mersenne1998]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No reseed after &lt;code&gt;fork()&lt;/code&gt; or snapshot&lt;/td&gt;
&lt;td&gt;repeated, generator&lt;/td&gt;
&lt;td&gt;VM-reset [@src-ristenpart2010]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UUIDv4 as a security token&lt;/td&gt;
&lt;td&gt;predictable, role&lt;/td&gt;
&lt;td&gt;guessable token [@src-rfc9562]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The meta-lesson runs through every row: historically the &lt;em&gt;interface&lt;/em&gt;, not the algorithm, is the bug. Blocking &lt;code&gt;/dev/random&lt;/code&gt;, seeding a userspace PRNG, forgetting to reseed after a fork; these are misuses of how you &lt;em&gt;reach&lt;/em&gt; randomness, not weaknesses in the math. That is exactly why &lt;code&gt;getrandom&lt;/code&gt; and &lt;code&gt;getentropy&lt;/code&gt; were designed to be un-misusable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you are managing entropy in application code, mixing your own sources, maintaining a userspace pool, reseeding on a timer, you have almost certainly introduced a bug the OS already solved. The correct amount of RNG plumbing in an application is zero. Call &lt;code&gt;getrandom&lt;/code&gt;, &lt;code&gt;getentropy&lt;/code&gt;, &lt;code&gt;BCryptGenRandom&lt;/code&gt;, or your language&apos;s CSPRNG wrapper, and stop [@src-getrandom, @src-arc4random].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; A version-4 UUID is 122 random bits formatted for uniqueness, not unpredictability, and its specification never promised it would be unguessable or drawn from a CSPRNG [@src-rfc9562]. Using one as a password-reset token, session ID, or API key is betting your security on an implementation detail the standard does not guarantee. For anything security-sensitive, draw CSPRNG bytes directly [@src-rfc4086].&lt;/p&gt;
&lt;/blockquote&gt;

Draw every random value from the OS CSPRNG (`getrandom` / `getentropy` / `BCryptGenRandom` / `secrets` / `crypto/rand`), never a language `random()` or Mersenne Twister. Keys: exact key length from the CSPRNG. GCM nonces: 96-bit counter, or random with a hard cap below $2^{32}$ per key, and if senders are distributed use AES-GCM-SIV. CBC IVs: fresh unpredictable per message, or move to AEAD. Salts: at least 16 bytes, unique per password, stored in the clear. Signature nonces: RFC 6979 deterministic. After any `fork()` or VM snapshot, force a reseed before emitting keys or nonces. Reject any hand-rolled entropy pool.
&lt;p&gt;The rules are short because the thesis is short: for every value you draw, name the property the role owes, and confirm the bytes came from the OS CSPRNG. Answer both and you are out of the catalog. Before the closing, here are the misconceptions that send good engineers back into it.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;
&lt;p&gt;The most expensive randomness bugs start as reasonable-sounding beliefs. Here are eight, corrected.&lt;/p&gt;


No. After the CSPRNG is seeded once at first boot, both devices draw from the same ChaCha-based generator and are cryptographically identical in the quality of their output; the only remaining difference is that `/dev/random` blocks until that first seeding while `/dev/urandom` does not [@src-donenfeld2022]. The old advice to prefer `/dev/random` for &quot;more secure&quot; keys was based on an entropy-depletion model that does not hold for a modern CSPRNG: a properly seeded generator does not run out of unpredictability by producing output. Use `getrandom()` and stop worrying about which device node to open [@src-random4man].


No, only once. `getrandom()` blocks a single time at first boot until the pool is initially seeded, and then never blocks again for the life of the system [@src-getrandom]. Code that polls an entropy estimate before every draw, or loops waiting for `/dev/random`, is solving a problem that the blocking-once design already solved at the interface.


It depends on the mode, and this is the most common confusion in the field. A CTR or GCM nonce must only be *unique*; unpredictability buys nothing there, and a counter is a fine, often safer, choice [@src-sp80038d]. The role that must be *unpredictable* is the CBC IV, whose predictability is exactly what BEAST exploited [@src-cve2011]. Same-looking value, different requirement, set by the mode.


No. A salt is unique and random but stored in plaintext right next to the password hash. Its only job is to make identical passwords hash differently so one precomputed table cannot attack many accounts. Treating it as a secret, or reusing one global salt, misunderstands the role: uniqueness is what matters, not secrecy.


No. Random 96-bit nonces are bounded by the birthday problem, so a single key should encrypt fewer than about $2^{32}$ messages to keep the repeat probability negligible [@src-sp80038d]. Beyond that, use a monotonic counter, which cannot collide, or AES-GCM-SIV, which tolerates a repeat without catastrophic failure [@src-rfc8452].


Neither extreme is right. Do not trust `RDRAND` *alone*, because it is opaque silicon you cannot audit, but do not refuse it either. The correct posture, the one modern kernels take, is to mix its output into a pool alongside other sources so that no single component is the whole answer [@src-intel-drng]. That is the discipline Dual_EC taught the field [@src-shumow2007].


No. A version-4 UUID is built for uniqueness, and its specification does not require the bits to be unguessable or drawn from a CSPRNG [@src-rfc9562]. For session IDs, reset tokens, and API keys, draw CSPRNG bytes directly instead of relying on a UUID&apos;s format [@src-rfc4086].


No. Statistical suites can falsify a generator but never certify one. The Mersenne Twister passes the standard batteries and is completely broken after 624 outputs [@src-mersenne1998]. Test suites catch gross defects; they say nothing about whether an adversary can predict the next bit, which is the only property that matters for cryptography [@src-sp80022].

&lt;h2&gt;12. Two Knobs, Two Places&lt;/h2&gt;
&lt;p&gt;Go back to where we started. Netscape, 1996: the RC4 and RSA math was flawless, and the keys were guessable because the seed was the clock and a couple of process IDs [@src-netscape1996]. Debian, 2008: the algorithm was fine, and roughly two years of keys collapsed to about 32,768 possibilities because one entropy input was removed [@src-debian2008]. You can now name both on sight: &lt;em&gt;predictable&lt;/em&gt;, in the &lt;em&gt;generator&lt;/em&gt;. That is the whole diagnosis, and it is the same diagnosis for every break in this article.&lt;/p&gt;
&lt;p&gt;By now the two knobs and two places should lock together into a single picture. Every value you draw owes one of two properties, unpredictability or uniqueness, and either can be squandered in one of two places: the &lt;em&gt;generator&lt;/em&gt; that mints the bits, or the &lt;em&gt;role&lt;/em&gt; that spends them (key, nonce, IV, salt). That product, two properties across two places, is the entire fault space, and nothing in this article fell outside it.&lt;/p&gt;
&lt;p&gt;What makes randomness uniquely treacherous is that none of those faults trips an alarm. Hand a cipher the wrong key and it errors; forge a MAC and the receiver rejects it. A predictable or repeated value sets off nothing at all: it clears every statistical test and validates cleanly, yet it is already broken for anyone who thought to recompute it. Each disaster we catalogued was one of those four faults, found only in hindsight.&lt;/p&gt;
&lt;p&gt;Snap the catalog into the grid one last time. Netscape and Debian: predictable, generator. The PS3&apos;s static nonce and the Bitcoin wallets&apos; repeated nonce: repeated, role [@src-fail0verflow2010, @src-android2013]. Dual_EC: predictable, generator, by design [@src-shumow2007]. GCM reuse in TLS: repeated, role [@src-cve2016]. PuTTY&apos;s biased P-521 nonce: predictable, role [@src-putty2024]. Four boxes, and every disaster in four decades fits in one of them.&lt;/p&gt;

Mastering randomness is not about finding a better source of magic. It is about knowing, for every value you draw, which of the two properties it owes and which of the two places can betray it.
&lt;p&gt;That is the discipline. For every value you draw, name the property the role owes (unique? unpredictable? secret?) and confirm the generator is the OS CSPRNG. The generator half is essentially solved engineering; the failures that remain live in sourcing, in trust, and above all in the roles, where a perfect byte stream still gets you owned if you pour it into the wrong one.&lt;/p&gt;
&lt;p&gt;This was Part 2 of the field guide. Part 1 established what &quot;secure&quot; means and why the definitions demand randomized encryption in the first place; later parts take the disciplined bytes you now know how to produce and build the modes, key-encapsulation mechanisms, and signatures that consume them. For the platform-specific instance of everything here, how Windows assembles its CSPRNG and defends it against cloning, the &lt;a href=&quot;https://paragmali.com/blog/a-key-is-only-as-unguessable-as-the-dice-that-made-it-inside/&quot; rel=&quot;noopener&quot;&gt;Windows CSPRNG companion article&lt;/a&gt; is waiting. Every one of those constructions will hand you a value to generate. Now you know the only two questions to ask of it.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;randomness-in-cryptography-csprngs-nonces-ivs-salts&quot; keyTerms={[
  { term: &quot;CSPRNG&quot;, definition: &quot;A generator whose output no efficient adversary can distinguish from uniform, even after seeing part of it.&quot; },
  { term: &quot;DRBG&quot;, definition: &quot;The deterministic expansion core that stretches a seed. It never creates entropy, only stretches it.&quot; },
  { term: &quot;Min-entropy&quot;, definition: &quot;The worst-case unpredictability of a source, set by the attacker&apos;s single best guess.&quot; },
  { term: &quot;Next-bit test&quot;, definition: &quot;A sequence is strong if no efficient algorithm predicts its next bit with advantage over one-half.&quot; },
  { term: &quot;Nonce&quot;, definition: &quot;A number used once. Must be unique per key; need not be unpredictable or secret.&quot; },
  { term: &quot;Initialization Vector&quot;, definition: &quot;A per-message starting value. Unpredictable and unique in CBC; unique-only (a nonce) in CTR and GCM.&quot; },
  { term: &quot;Salt&quot;, definition: &quot;A unique, random, public value that makes each password hash distinct. Not a secret.&quot; },
  { term: &quot;Forward secrecy&quot;, definition: &quot;A later state compromise cannot recover earlier output. Achieved by fast key erasure.&quot; },
  { term: &quot;Reseeding&quot;, definition: &quot;Folding fresh entropy into the state on a schedule or after a fork or snapshot.&quot; },
  { term: &quot;Birthday bound&quot;, definition: &quot;Collisions in an m-bit value get likely near 2 to the m over 2 draws. A provable uniqueness ceiling.&quot; }
]} flashcards={[
  { front: &quot;The two knobs&quot;, back: &quot;Unpredictability and uniqueness.&quot; },
  { front: &quot;The two places&quot;, back: &quot;The generator that makes the bits, and the role that consumes them.&quot; },
  { front: &quot;Why did BEAST work?&quot;, back: &quot;TLS 1.0 used a predictable chained CBC IV, and a CBC IV must be unpredictable.&quot; },
  { front: &quot;Why is a single GCM nonce reuse fatal?&quot;, back: &quot;It leaks the XOR of the two plaintexts and yields the polynomial equation that recovers the GHASH subkey H, enabling forgery.&quot; },
  { front: &quot;RDRAND in one rule&quot;, back: &quot;The kernel mixes it into the pool; applications never call it directly.&quot; }
]} questions={[
  { q: &quot;Why does passing every statistical test not make a generator secure?&quot;, a: &quot;Statistical quality is distributional; cryptographic security is computational unpredictability. The Mersenne Twister passes the batteries yet is recovered from 624 outputs.&quot; },
  { q: &quot;Which role must be unpredictable, and which must only be unique?&quot;, a: &quot;A CBC IV must be unpredictable and unique. A CTR or GCM nonce must only be unique.&quot; },
  { q: &quot;How did RFC 6979 fix repeated-nonce disasters?&quot;, a: &quot;By deriving the signature nonce deterministically from the private key and message, removing the RNG from the signing path.&quot; },
  { q: &quot;What did Dual_EC prove about detectability?&quot;, a: &quot;A standardized generator can pass every test and still be predictable to whoever chose its constants. Back doors are undetectable from output.&quot; },
  { q: &quot;What is the one operational rule for the generator?&quot;, a: &quot;Never roll your own and never hand-seed. Always call the OS CSPRNG.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>cryptography</category><category>randomness</category><category>csprng</category><category>entropy</category><category>nonces</category><category>applied-cryptography</category><category>security</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>