# The Standard Was Boring; That Was the Point: A Field Guide to HPKE (RFC 9180)

> 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.

*Published: 2026-07-13*
*Canonical: https://paragmali.com/blog/the-standard-was-boring-that-was-the-point-a-field-guide-to-*
*© Parag Mali. All rights reserved.*

---
<TLDR>
**"Encrypt to a public key" 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 "benign malleability" [@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.
</TLDR>

## 1. An Elementary Task the Industry Got Wrong Four Times

"Encrypt this to their public key" 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.

At least four "standard" 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'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.

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 *second, different* valid ciphertext for the same plaintext. It named the phenomenon "benign malleability" and deemed it harmless [@sec1].

The hard part of "encrypt to a public key," 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.

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.

That is the setting Hybrid Public Key Encryption was built for. HPKE, specified in RFC 9180 by the IRTF'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.

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: *you have a stranger'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?*

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 "encrypt to a recipient's public key" design and name, on sight, which of HPKE's modes it is using and which of HPKE's non-goals it is quietly assuming away.

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 "done right" was correcting. Start with the one idea all four were circling and none quite pinned down: the difference between *encrypting* a key and *encapsulating* one.

## 2. Encapsulate the Key, Do Not Encrypt It

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 "encrypt the symmetric key with the public key." HPKE instead will "generate the symmetric key and its encapsulation with the public key" [@rfc9180]. That one-word swap -- from *encrypt* to *encapsulate* -- is the difference between the padding-oracle-prone lineage that broke RSA key transport and a clean, composable, provable construction.

<PullQuote>
The traditional combination has been "encrypt the symmetric key with the public key." HPKE instead will "generate the symmetric key and its encapsulation with the public key." -- RFC 9180
</PullQuote>

Build the mental model in three moves. Derive only what the rest of the article needs.

### Move one: KEM-DEM in one picture

<Definition term="Hybrid Public Key Encryption (HPKE)">
The scheme standardized in RFC 9180 for encrypting arbitrary-length messages to a recipient'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.
</Definition>

The word "hybrid" 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.

<Definition term="Key Encapsulation Mechanism (KEM)">
A public-key primitive that does not transport a key you chose. Called with the recipient'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.
</Definition>

<Definition term="Data Encapsulation Mechanism (DEM)">
The symmetric half of a hybrid scheme: an AEAD that bulk-encrypts the actual payload under a key derived from the KEM's shared secret. In HPKE the DEM is a single AEAD scheme, and its authenticity tag is intrinsic rather than bolted on.
</Definition>

If you have read Part 14 on [getting RSA right](/blog/rsa-is-a-trapdoor-not-a-cryptosystem-oaep-pss-and-the-25-yea/), this is the same KEM-DEM skeleton, now instantiated on elliptic curves rather than RSA. The reason to *generate* the key rather than *encrypt* 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's chosen input from the public-key step. There is nothing to pad, and nothing to probe.

<Mermaid caption="The one-word swap the whole article turns on: encrypting a chosen key (left) versus letting the public-key operation generate the key (right)">
flowchart TD
    subgraph LEFT["Encrypt the key (RSA and ElGamal key transport)"]
        A1["Choose a random symmetric key k"] --> A2["Encrypt k under the recipient public key"]
        A2 --> A3["Ship wrapped k plus the symmetric ciphertext"]
    end
    subgraph RIGHT["Encapsulate the key (KEM-DEM, HPKE)"]
        B1["Encap(pkR) generates a fresh shared secret"] --> B2["Returns shared_secret and enc together"]
        B2 --> B3["Derive keys, then AEAD-seal the payload"]
    end
</Mermaid>

### Move two: DHKEM, concretely

HPKE's deployed KEM is Diffie-Hellman-based, and it is worth seeing the mechanism once rather than treating it as a black box.

<Definition term="DHKEM">
HPKE's Diffie-Hellman-based KEM. `Encap(pkR)` generates an ephemeral key pair, computes a Diffie-Hellman value against the recipient'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's *static* key.
</Definition>

Concretely, `Encap(pkR)` generates an ephemeral pair $(sk_E, pk_E)$, computes $\mathrm{DH}(sk_E, pk_R)$, sets `enc` to the serialization of $pk_E$, and returns `shared_secret = ExtractAndExpand(dh, kem_context)`, where `kem_context` already contains both `enc` 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 `kem_context` is the first appearance of the idea that later kills benign malleability.

<Mermaid caption="DHKEM Encap and Decap: the sender's ephemeral key produces both the shared secret and its encapsulation, and the recipient recomputes the same Diffie-Hellman value">
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->>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
</Mermaid>

### Move three: the key schedule and the Context

The shared secret is not used directly. Both sides run it through a key schedule.

<Definition term="Key schedule (HPKE)">
The HKDF Extract-then-Expand process that turns the KEM'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`.
</Definition>

<Definition term="AEAD (Authenticated Encryption with Associated Data)">
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.
</Definition>

The HPKE key schedule is, quite literally, a worked example of [the HKDF construction from Part 13](/blog/one-secret-is-not-one-key-the-discipline-of-key-derivation-w/): extract a pseudorandom key from the shared secret, then expand it into the specific bytes each downstream primitive needs. Its outputs assemble into a `Context`. The `Context` exposes `Seal`/`Open` -- a stateful pair in which an incrementing sequence number is combined with `base_nonce` (by exclusive-or) to give every message a distinct nonce -- and `Export`.

<Definition term="Export (secret export)">
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.
</Definition>

For a single message you do not need to hold a `Context` open at all: the single-shot `Seal`/`Open` API wraps setup, one encryption, and teardown into one call. Everything above -- encapsulate, derive, seal -- is what those single-shot calls do underneath.

<Mermaid caption="The HPKE construction end to end, showing where the public keys are bound into the derivation">
flowchart TD
    PKR["Recipient public key pkR"] --> ENCAP["Encap"]
    ENCAP --> SS["shared_secret"]
    ENCAP --> ENCV["enc (sent to recipient)"]
    SS --> KS["key_schedule (HKDF Extract-then-Expand)"]
    MODE["mode identifier"] --> KS
    PSK["psk_id_hash"] --> KS
    INFO["info_hash"] --> KS
    KS --> KEY["AEAD key"]
    KS --> NON["base_nonce"]
    KS --> EXP["exporter_secret"]
    KEY --> CTX["Context"]
    NON --> CTX
    EXP --> CTX
    CTX --> SEAL["Seal / Open (per-message nonce)"]
    CTX --> EXPORT["Export (key other protocols)"]
</Mermaid>

The structural model below is a toy, not real cryptography, but it lets you *feel* the difference from wrapping a chosen key: `encap` invents a random secret and its encapsulation, folds in the recipient's public key, and shows that a different recipient key yields a different secret from the same encapsulation.

<RunnableCode lang="js" title="Structural model: Encap generates the key, and the recipient key is bound into it (NOT real crypto)">{`
// STRUCTURAL MODEL, NOT REAL CRYPTO. It only shows the SHAPE of "generate the key".
function toyMix(str) {                       // a tiny non-cryptographic mixer
  let h = 2166136261 >>> 0;
  for (const ch of str) { h ^= ch.charCodeAt(0); h = Math.imul(h, 16777619) >>> 0; }
  return (h >>> 0).toString(16);
}
function randInt() { return Math.floor(Math.random() * 1e9); }

// 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 = 'pkE(' + skE + ')';            // its public value IS the encapsulation
  const dh  = toyMix(enc + '|' + 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('ss|' + dh + '|' + enc + '|' + pkR);
  return { shared_secret, enc };
}

const pkR = 'recipient-key-A';
const { shared_secret, enc } = encap(pkR);
console.log('enc on the wire     = ' + enc);
console.log('shared_secret       = ' + shared_secret);

// Same enc, a DIFFERENT recipient key -> a DIFFERENT secret. A swapped or mangled
// public key simply cannot derive the key that decrypts, because pkR is bound in.
const swapped = toyMix('ss|' + toyMix(enc + '|recipient-key-B') + '|' + enc + '|recipient-key-B');
console.log('same enc, key B     = ' + swapped);
console.log('secrets differ      = ' + (swapped !== shared_secret));
`}</RunnableCode>

Now the payoff, and the first place your understanding should shift.

> **Key idea:** Encapsulate, do not encrypt. Let the public-key operation *generate* 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, "encrypt to a public key" stops being an ad-hoc scheme you must re-prove each time and becomes a composition of two well-understood parts.

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 *have* to be that strong for the composite to be [@hhk06]. Those results turn "is this encrypt-to-a-public-key scheme secure?" into two smaller, reusable questions.<MarginNote>RFC 9180 notes in passing that its Base mode and the original Cramer-Shoup scheme are both "named HPKE" -- the name has KEM-DEM in its bones [@rfc9180], [@cs01].</MarginNote>

> **Note:** RFC 9180 is an *Informational* IRTF/CFRG document, not a Standards-Track one. Yet it is normatively *required* 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].

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.

## 3. From "Encrypt the Key" to "Encapsulate the Key"

The story is a twenty-five-year march from *encrypt the key* to *encapsulate the key*, 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's sharpest failure.

<Mermaid caption="The twenty-five-year arc from encrypting a chosen key to encapsulating a generated one, ending in RFC 9180 and the post-quantum drop-in">
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
</Mermaid>

**PEM (RFC 1421, 1993): the traditional combination.** Privacy Enhanced Mail defined the canonical early hybrid. Pick a content-encryption key, *encrypt that key* under the recipient'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.

**ElGamal (1985) and why Diffie-Hellman resists "encrypt a chosen key."** ElGamal's public-key encryption predates the hybrid schemes and shows the friction directly [@elgamal85]. To "encrypt" 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's own claim to offer "stronger security properties than ElGamal," not a verbatim quote [@dhies_ms].

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'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.

**DHIES and DHAES (Abdalla, Bellare, Rogaway, 1999): derive, do not encrypt.** 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 *agreed*.

The authors proved the scheme secure against chosen-ciphertext attack under the Oracle Diffie-Hellman assumption, with "no random oracles involved," and called it "an attractive starting point for developing public-key encryption standards" [@dhaes], [@dhies_ms].<Sidenote>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].</Sidenote> That parenthetical -- "an attractive starting point for developing standards" -- turned out to be prophetic in the worst way.

**Cramer and Shoup (2001): the modular abstraction.** 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.

> **Note:** 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 *as a standard* for two decades [@cs01], [@rfc9180].

So by 2001 everything HPKE needed already existed: a concrete, provably secure ephemeral-static scheme, and an abstraction that made "encrypt to a public key" 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.

## 4. The ECIES Zoo

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 "numerous competing and non-interoperable standards" [@rfc9180], [@maea10]. This is the hook's promise, paid off in detail. Three defects run through the era, and HPKE was built to close all three.

<Mermaid caption="The single DHIES idea, standardized four incompatible ways between 2001 and 2009, then consolidated by RFC 9180 in 2022">
flowchart TD
    D["DHIES (Abdalla, Bellare, Rogaway)"] --> A["ANSI X9.63 (2001)"]
    D --> I["IEEE 1363a (2004)"]
    D --> S["ISO/IEC 18033-2 (2006)"]
    D --> C["SEC 1 v2 (2009)"]
    A --> Z["The ECIES zoo: incompatible KDFs, MACs, point encodings, no shared test vectors"]
    I --> Z
    S --> Z
    C --> Z
    Z --> H["RFC 9180 (2022): one construction, proven, test-vectored"]
</Mermaid>

**Fragmentation: the "which ECIES?" problem.** The four standards disagree on the key-derivation function, the MAC, whether elliptic-curve points are compressed or uncompressed, and whether the optional `SharedInfo` 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].<Sidenote>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].</Sidenote>

<Aside label="The ECIES zoo, quantified">
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 "ECIES" names a family of mutually unintelligible dialects rather than one scheme. RFC 9180 points readers to the Gayoso Martinez et al. survey "for a thorough comparison." That survey and three of the four standards are paywalled, so this article cites them through RFC 9180's reference section rather than by live link [@rfc9180], [@maea10], [@sec1].
</Aside>

**Benign malleability.** SEC 1 version 2 documents a specific defect and then waves it off.

<Definition term="Benign malleability">
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 "benign malleability" and "generally deemed harmless," 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].
</Definition>

The word "benign" 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.

**No proofs, and no test vectors.** RFC 9180 is blunt about the state it inherited: prior schemes "rely on outdated primitives, lack proofs of IND-CCA2 security, or fail to provide test vectors" [@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 "conformant" implementations drifted apart.

> **Note:** The ECIES era is the standing proof that a primitive can be widely deployed and still be under-specified, unprovable, and mutually incompatible. "We implemented the standard" 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].

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.

| Approach | Core idea | Main weakness | Proof and test vectors | Status |
|---|---|---|---|---|
| RSA key transport (PKCS#1 v1.5) | Encrypt a chosen key under RSA | Padding oracle (Bleichenbacher, ROBOT) | No CCA2 proof; padding fragile | Deprecated in TLS 1.3 |
| ElGamal encryption | Encode a chosen key as a group element | Malleable; roughly 2x expansion | No CCA2 proof | Historical |
| DHIES / DHAES | Derive the key from ephemeral-static DH | Underspecified as a deployable format | CCA2 proof (ODH); no shared vectors | Academic root of ECIES |
| ECIES (four standards) | Standardize DHIES | Fragmented; benign malleability | Proof gaps; no shared vectors | Legacy interop only |
| HPKE (RFC 9180) | KEM-DEM, all public keys bound in | Non-goals must be respected | IND-CCA2 proof; per-suite vectors | Current default |
| HPKE + X-Wing | Swap DHKEM for a hybrid PQ KEM | Larger ciphertexts; draft-stage | Hybrid PQ proof; draft vectors | Emerging |

Every one of these defects points at the same missing discipline: pin *one* 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 "done right" actually changed.

## 5. "Done Right" Was Discipline, Not Genius

There is no eureka moment in this story, and that is the point. HPKE'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's defects.

**Decision one: abstract the KEM so the primitive is agile.** Instead of freezing one curve, one KDF, and one MAC into a single scheme, HPKE is parameterized over a ciphersuite of three identifiers: `kem_id`, `kdf_id`, and `aead_id` [@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.

HPKE replaces uncontrolled fragmentation with *managed* 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.

**Decision two: bind every public key into the key schedule.** This is the decisive fix, and RFC 9180 states it in one sentence.

<PullQuote>
"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." -- RFC 9180, Section 9.1
</PullQuote>

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 *different* 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.

**Decision three: mandate a single AEAD as the DEM.** 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' part on [safe authenticated-encryption composition](/blog/the-tag-verified-the-cipher-held-the-forgery-went-through-a-/) has a single thesis -- compose it wrong and the tag verifies while the forgery goes through -- then HPKE is simply that "compose it correctly" rule frozen into a specification.

**Decision four: ship proofs and test vectors, developed alongside the standard.** 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'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].

<Definition term="IND-CCA2 (indistinguishability under adaptive chosen-ciphertext attack)">
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's composition meets when its KEM and AEAD do [@cs01], [@hhk06].
</Definition>

Put the four together and your sense of what happened should shift.

> **Key idea:** "Done right" 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 *and* 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.

> **Note:** 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-*discipline* them, pinning one construction and refusing to ship it without proofs and vectors [@cs01], [@rfc9180].

There is even a small tell that HPKE was designed to be built *on*, not just used directly.<Sidenote>HPKE reserves an export-only AEAD identifier (the value `0xFFFF`) for ciphersuites that never call `Seal` and only ever use the `Export` interface -- a strong hint that the authors expected HPKE to be keyed into other protocols rather than used standalone [@rfc9180].</Sidenote>

Four decisions turn a fragmented, unprovable family into one boring, correct primitive. But "one primitive" is not "one behavior." 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 *you* are trying to authenticate.

## 6. The Four Modes: HPKE's Core Menu

RFC 9180, Section 5.1, defines exactly four modes, and the difference between them reduces to a single question: *what extra secret or identity gets folded into the key schedule?* 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.

| Mode | ID | Extra input | What it authenticates | Use when |
|---|---|---|---|---|
| `mode_base` | 0x00 | none | Only that the ciphertext is for this recipient key | You have just the recipient's public key |
| `mode_psk` | 0x01 | a pre-shared key | Recipient, plus possession of the PSK | A symmetric secret already exists out of band |
| `mode_auth` | 0x02 | sender's static key | That a specific sender key holder sent it | The recipient must know which sender key sealed it |
| `mode_auth_psk` | 0x03 | static key and PSK | Both a sender identity and PSK possession | You need sender identity and a shared secret |

**`mode_base` (0x00): nothing extra.** The sender is anonymous; the derivation authenticates only that the ciphertext was sealed to this recipient's key. It is the default -- and, tellingly, the mode that every flagship deployment in the next section uses.

**`mode_psk` (0x01): a pre-shared key.** A symmetric secret known to both parties is folded into the schedule, so a valid decryption additionally proves the sender possessed the PSK.

> **Note:** `mode_psk` authenticates *possession of the PSK*, 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].

**`mode_auth` (0x02): the sender's static key.** Here `Encap`/`Decap` are replaced by `AuthEncap`/`AuthDecap`, which fold a Diffie-Hellman value between the sender's *static* 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].

**`mode_auth_psk` (0x03): both.** A static sender identity *and* a shared secret, folded together.

<Mermaid caption="The four modes are the same machinery with different inputs folded into the key schedule">
flowchart TD
    B["mode_base 0x00: nothing extra"] --> KS["key_schedule context"]
    P["mode_psk 0x01: plus a pre-shared key"] --> KS
    A["mode_auth 0x02: plus sender static key via AuthEncap"] --> KS
    AP["mode_auth_psk 0x03: plus PSK and sender static key"] --> KS
    KS --> OUT["Same construction, four different derived key schedules"]
</Mermaid>

Now the subtlety that trips up careful engineers. The authenticated modes are a form of *signcryption*, and they carry a security distinction imported from that theory.

<Definition term="Signcryption">
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's authenticated modes are signcryption, so their guarantees are stated in these terms rather than as a public, third-party-verifiable signature.
</Definition>

The consequence matters: `mode_auth` is *not* 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 [the digital-signature part of this series](/blog/the-math-held-the-interface-leaked-a-field-guide-to-digital-/), not HPKE's `auth` mode.

The security properties proven for each mode, from RFC 9180's Table 6 via Lipp's mechanized analysis, are worth seeing side by side [@rfc9180], [@lipp20]:

| Property | Base | PSK | Auth | AuthPSK |
|---|---|---|---|---|
| Message confidentiality (IND-CCA2) | Yes | Yes | Yes | Yes |
| Export secrecy (variable-length PRF) | Yes | Yes | Yes | Yes |
| Sender authentication | N/A | Yes | Yes | Yes |
| Extra secret assumed | recipient key secret | plus PSK secret | plus sender key secret | plus sender key and PSK |

The structural model below makes the "same machinery, different inputs" 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.

<RunnableCode lang="js" title="Structural model: the four modes differ only in what is folded into the key schedule (NOT real crypto)">{`
// STRUCTURAL MODEL, NOT REAL CRYPTO. Shows only that mode changes the derived key.
function toyMix(s){let h=2166136261>>>0;for(const ch of s){h^=ch.charCodeAt(0);h=Math.imul(h,16777619)>>>0;}return (h>>>0).toString(16);}
const shared_secret = 'ss-from-KEM';

// 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('pskid:' + (pskId || ''));
  const info_hash   = toyMix('info:my-app-v1');
  const ss = senderKey ? toyMix(shared_secret + '|auth:' + senderKey) : shared_secret;
  return toyMix(mode + '|' + psk_id_hash + '|' + info_hash + '|' + ss);
}

console.log('base     -> ' + keySchedule('0x00', null, null));
console.log('psk      -> ' + keySchedule('0x01', 'shared-psk', null));
console.log('auth     -> ' + keySchedule('0x02', null, 'sender-static-key'));
console.log('auth_psk -> ' + keySchedule('0x03', 'shared-psk', 'sender-static-key'));
console.log('all four derive different keys from identical machinery');
`}</RunnableCode>

Choosing a mode answers "who am I authenticating?" 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.

## 7. Ciphersuites and Correct Usage

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.

| Slot | Options | How to choose |
|---|---|---|
| KEM | DHKEM(X25519, HKDF-SHA256); DHKEM(P-256/P-384/P-521, HKDF-SHA256/384/512); DHKEM(X448, HKDF-SHA512) | X25519 unless policy requires NIST curves; encapsulation `enc` is 32 bytes for X25519, 65 for P-256 |
| KDF | HKDF-SHA256, HKDF-SHA384, HKDF-SHA512 | Match the KDF hash to the KEM and AEAD strength |
| AEAD | AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305, Export-only (0xFFFF) | AES-GCM with AES hardware; ChaCha20-Poly1305 without; export-only when you never call `Seal` |

The three menus map onto earlier parts of this series. DHKEM's curves are [the elliptic curves of the curves part](/blog/the-curve-was-hard-the-gap-was-soft-a-field-guide-to-using-e/); 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 [the decision from the AEAD part](/blog/the-aead-decision-matrix-seven-ciphers-three-edges-one-choic/): AES-GCM where the CPU has AES instructions, ChaCha20-Poly1305 where it does not. The tag is 16 bytes in every case.

**The nonce and sequence discipline is the crux.** Inside a `Context`, the per-message nonce is computed as `nonce = base_nonce XOR seq`, where `seq` is an incrementing sequence number. The `Context` is therefore *stateful*, 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 [the nonce-reuse part of this series](/blog/one-number-used-twice-how-a-repeated-nonce-hands-over-your-p/) covers for AES-GCM, arriving here as a counter you must not let wrap.

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.

<RunnableCode lang="js" title="Structural model: per-message nonce is base_nonce XOR seq, and overflow repeats a nonce (NOT real crypto)">{`
// 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 & 255)).toString(16); }

for (let seq = 0; seq !== 5; seq++) {
  console.log('seq ' + seq + ' -> nonce ' + nonceFor(seq));
}

// 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('seq ' + seqA + '   -> nonce ' + nonceFor(seqA));
console.log('seq ' + seqWrap + ' -> nonce ' + nonceFor(seqWrap) + '   (wrapped)');
console.log('nonce reused after overflow = ' + (nonceFor(seqA) === nonceFor(seqWrap)));
`}</RunnableCode>

> **Note:** Prefer single-shot `Seal`/`Open` for independent messages -- it sets up, encrypts once, and tears down, so there is no counter to mismanage. If you hold a `Context` open, `Seal` strictly in order, respect the per-ciphersuite AEAD message-count and input-length limits, and never reuse a `Context` 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].

Two operational notes round out the usage rules. First, the AEAD message-count and plaintext-length limits are per-ciphersuite constants, and single-shot `Seal`/`Open` sidesteps the streaming bookkeeping entirely.<Sidenote>When you need to protect *many* messages or key a different protocol's framing, use the `Export` interface to derive an independent secret rather than looping `Seal` on one long-lived `Context` -- this is exactly what Oblivious HTTP and Oblivious DNS do [@rfc9180].</Sidenote> Second, the one default to remember: `mode_base` 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.

These rules read like an interop checklist because that is precisely what they are, and the only reason they *can* 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 *using* HPKE says nothing about *where it has already won*. The strongest evidence that HPKE is the right default is that the modern privacy stack is built on it.

## 8. Deployments as the Proof

The best argument that HPKE is state of the art in 2026 is not a benchmark. It is a deployment list. Every "encrypt to a party you have never spoken to" corner of the modern IETF stack runs on HPKE, and they almost all run on the *same* mode.

| Deployment | Specification | What HPKE does | Mode and API |
|---|---|---|---|
| Encrypted Client Hello | RFC 9849 (2026) | Encrypts the inner ClientHello, including the server name | `mode_base`, `SetupBaseS`/`SetupBaseR` |
| Messaging Layer Security | RFC 9420 (2023) | Encrypts path secrets to tree nodes in TreeKEM | `mode_base`, `SealBase` |
| Oblivious HTTP | RFC 9458 (2024) | Encapsulates requests and responses to a gateway | `mode_base` plus `Export` |
| Oblivious DNS over HTTPS | RFC 9230 (2022) | Encrypts DNS queries to a target through a proxy | `mode_base` plus `Export` |

**Encrypted Client Hello.** ECH encrypts the sensitive inner ClientHello -- including the server name a censor would want to read -- to the server's HPKE public key, published in an `ECHConfig` and fetched ahead of time. It uses `SetupBaseS` on the client and `SetupBaseR` on the server [@rfc9849].<Sidenote>Currency pin: ECH is now Standards-Track RFC 9849, published in March 2026 by Rescorla, Oku, Sullivan, and Wood, and formerly known as `draft-ietf-tls-esni`. It is an RFC, not a draft, and its text references HPKE throughout [@rfc9849].</Sidenote>

**Messaging Layer Security.** 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 `SealBase` [@rfc9420]. Group-scale forward secrecy and post-compromise security come from the tree and the ratchet, not from HPKE.

**Oblivious HTTP and Oblivious DNS.** 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's HPKE key; the relay forwards ciphertext it cannot read; the gateway decapsulates and, crucially, uses `Export` to key the response rather than looping `Seal` [@rfc9458], [@rfc9230]. Oblivious DNS, in 2022, was the first shipped RFC deployment of HPKE.

<Mermaid caption="Where HPKE sits in an Oblivious HTTP relay and gateway split: the relay sees the client but not the content, the gateway sees the content but not the client">
flowchart LR
    C["Client seals request with SetupBaseS"] --> REL["Relay sees ciphertext and client address, not content"]
    REL --> GW["Gateway runs SetupBaseR, decrypts, Export-keys the reply"]
    GW --> TGT["Target sees the request, never the client address"]
</Mermaid>

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].

Two observations reinforce the article's spine. First, every marquee deployment uses `mode_base`: sender authentication and forward secrecy are supplied *at the protocol layer* -- 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 `Export` to key their own framing instead of looping `Seal`, exactly the extensibility the export interface was built for. The deployments did not stretch HPKE; they used the parts the design anticipated.

<Aside label="The post-quantum migration, honestly">
DHKEM is classical-only, which means every HPKE ciphertext sealed today is exposed to "harvest now, decrypt later": 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 "Use in HPKE" 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].
</Aside>

HPKE won the IETF privacy stack. But "won" invites the question a careful engineer actually asks: won *against what*, and when would I still reach for something else? The honest comparison comes next -- along with the one construction people keep miscategorizing as HPKE.

## 9. Competing Approaches, and One That Is Not HPKE

HPKE is the right default for *new protocol design*, 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.

| Tool | Construction | Agility and modes | Proof and shared vectors | Best suited for |
|---|---|---|---|---|
| HPKE (RFC 9180) | KEM-DEM (DHKEM + KDF + AEAD) | Full agility; four modes | Machine-checked; per-suite vectors | New protocol design |
| libsodium `crypto_box` / seal | Fixed X25519 + XSalsa20-Poly1305 | None; mutual-auth, repudiable | No mechanized proof; partial vectors | Both ends yours, zero knobs |
| JOSE `ECDH-ES` (RFC 7518) | Ephemeral-static ECDH + Concat KDF | Some agility; encryption only | No mechanized proof; partial vectors | JWE and JWT stacks |
| `age` | X25519 + HKDF-SHA256 + ChaCha20-Poly1305 | None; file-oriented | No mechanized proof; has vectors | Encrypting files to a key |
| Raw ECIES | Ephemeral-static ECDH + Encrypt-then-MAC | Per-standard knobs | Proof gaps; no shared vectors | Legacy and blockchain interop |

**libsodium `crypto_box` and sealed boxes.** 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 `crypto_box` 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.

**JOSE and COSE `ECDH-ES`.** Ephemeral-static ECDH with the Concat KDF, native to JWE and JWT [@rfc7518]. It has no `auth` or `psk` modes, uses Concat-KDF rather than HKDF, and ships nothing like HPKE's vectors. It is chosen by platform gravity, and a COSE-HPKE binding is emerging to bring HPKE *into* that world [@cose_hpke].

**`age`.** 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.

**Raw ECIES** survives only for interop with existing deployments, such as blockchain wallets and legacy PKI [@sec1]. And **RSA-KEM** (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].

Now the boundary marker, because it is the single most common miscategorization in this space.

<Aside label="Not HPKE: Apple's PQ3">
Apple'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.
</Aside>

The ranking has a pattern. The fixed-suite tools win on simplicity, `ECDH-ES` wins on platform gravity, and everything else loses to HPKE for *new* 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.

## 10. The Refusals That Look Like Features

HPKE's non-goals are not gaps someone forgot to fill. Several are *impossibilities* 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.

**No forward secrecy against recipient compromise, in any mode.** RFC 9180 is explicit: HPKE ciphertexts "are not forward secret with respect to recipient compromise in any mode," because only long-term secrets are used on the recipient's side [@rfc9180]. This is structural, not an oversight.

<Definition term="Forward secrecy">
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.
</Definition>

**`auth` mode is not a signature.** 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.

**Not key-committing by default.** HPKE's AEADs bind a ciphertext to a plaintext, but not to a *unique key*.

<Definition term="Key commitment (committing AEAD)">
A property by which a ciphertext can be opened successfully under exactly one key. HPKE'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].
</Definition>

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 *entropy*, 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 `aead_id` [@aead_props].

**No replay, reordering, downgrade, or metadata protection.** 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].

**Classical-only by default.** This is the single *primitive-level* gap -- and, as the deployment section showed, the only non-goal you close by swapping a component rather than redesigning the scheme.

It is worth being precise about what is and is not finished. The core security question is *closed*: 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].

The residual gaps are about *model and API coverage*, 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 `psk` and `auth_psk` 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].

> **Key idea:** 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.

> **Note:** HPKE's most important non-goals look like features waiting to be used: there is **no forward secrecy** against recipient compromise in any mode, **`auth` is not a signature**, and the AEADs are **not key-committing**. Assuming any one of the three is present is where real incidents begin [@rfc9180].

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.

## 11. The Failure and Misuse Catalog

HPKE itself has no known catastrophic real-world break, and that absence *is* 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 "proven secure" 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.

| Misuse | Root cause | Non-goal assumed away | Fix or lesson |
|---|---|---|---|
| Sequence counter overflow | Fixed-width counter wraps, nonce repeats | AEAD nonce uniqueness is the caller's job | Wide counter; single-shot; the hpke-rs `u64` fix |
| Static-key forward-secrecy illusion | Expecting past secrecy from a static key | No FS against recipient compromise | Ratchet or rotate keys above HPKE |
| Low-entropy PSK | Guessable PSK enables partitioning oracle | AEADs are not key-committing | PSK with at least 32 bytes of entropy |
| Bad ephemeral randomness | Weak CSPRNG in `Encap` | HPKE assumes good randomness | Use a vetted CSPRNG |
| `auth` treated as a signature | Insider can forge to a third party | `auth` is signcryption, not non-repudiation | Use a digital signature for authorship |
| Out-of-order or reused `Open` | Ordering not enforced by HPKE | No reordering or replay protection | `Seal` in order; put ordering in the AAD |
| Export-only misuse | Calling `Seal` on an export-only suite | Interface contract not enforced | Error, do not truncate; the hpke-rs fix |

**The first advisory: `hpke-rs`, GHSA-g433-pq76-6cmf.** In February 2026 the `hpke-rs` and `hpke-rs-rust-crypto` crates received a high-severity advisory bundling four fixes, and the reason it vindicates this article's spine rather than refuting it is that every fix maps onto a non-goal RFC 9180 had already flagged [@ghsa], [@rfc9180], [@hpke_rs]:

- **Sequence-number overflow leading to nonce reuse.** 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.
- **Export-only misuse and silent truncation.** Calling `Seal`/`Open` on an export-only suite, and requesting an over-long `Export`, previously truncated silently; both now return errors. Reported by Arciszewski.
- **Missing X25519 zero and low-order key check.** The library did not reject all-zero keys; it now errors on them, closing a small-subgroup foot-gun.
- **A post-quantum KEM-identifier mapping bug.** 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.

**The second advisory: `@hpke/core` / `hpke-js`, CVE-2025-64767.** Three months earlier, in November 2025, the `@hpke/core` package -- the core of the `hpke-js` library in the survey below -- drew a *critical* 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 `SenderContext` `Seal()` API let two concurrent asynchronous `seal()` calls read the same sequence number, so both derived the same nonce and reused it in the AEAD, forfeiting confidentiality and integrity outright. Versions `@hpke/core` 1.7.4 and earlier are affected; the fix shipped in 1.7.5. The lesson is the misuse table's first row reached from a new direction -- nonce uniqueness is the caller's job -- so a single `Context` must never have `Seal` 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].

Notice what is *not* 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.

The rest of the catalog is misuse in application code, and it maps onto the table above. Expecting **forward secrecy from a static recipient key** yields none, in any mode [@rfc9180]. A **low-entropy PSK** turns `mode_psk` into a partitioning oracle, so the PSK needs at least 32 bytes of entropy [@rfc9180], [@lgr20]. **[Bad ephemeral randomness](/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand/)** can collapse base-mode confidentiality outright, because a predictable ephemeral key undoes the encapsulation.

**Treating `auth` as a signature** walks into the insider-versus-outsider trap [@abhklr20], and **out-of-order `Open` or context reuse** breaks because applications, not HPKE, must preserve `Seal` 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.

<PullQuote>
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.
</PullQuote>

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 "how to use HPKE safely" guide is not a research problem. It is the non-goals, turned into rules.

## 12. A Practical Decision Guide

Everything above collapses into a short decision procedure and a shorter list of things never to do.

<Mermaid caption="The 2026 decision flow: pick a building block, then an HPKE mode, from a stranger's public key to a shipped configuration">
flowchart TD
    START["Encrypt to a public key"] --> Q1&#123;"New protocol needing agility, modes, proofs?"&#125;
    Q1 -->|Yes| HPKE["Use HPKE"]
    Q1 -->|No| Q2&#123;"Both ends yours, zero knobs?"&#125;
    Q2 -->|Yes| BOX["libsodium sealed box"]
    Q2 -->|No| Q3&#123;"JWE or JWT stack?"&#125;
    Q3 -->|Yes| JOSE["ECDH-ES, or COSE-HPKE as it matures"]
    Q3 -->|No| Q4&#123;"Encrypting files?"&#125;
    Q4 -->|Yes| AGE["age"]
    Q4 -->|No| ECIES["Matched ECIES, legacy interop only"]
    HPKE --> M&#123;"Which mode?"&#125;
    M -->|Only the recipient key| BASE["mode_base"]
    M -->|A shared secret exists| PSK["mode_psk, PSK at least 32 bytes"]
    M -->|Need a sender identity| AUTH["mode_auth"]
    M -->|Both| AUTHPSK["mode_auth_psk"]
</Mermaid>

**Which building block?** 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 `ECDH-ES`, or COSE-HPKE as it matures [@cose_hpke]. Encrypting files takes `age`. Interop with an existing ECIES deployment takes a matched ECIES configuration and nothing new [@rfc9180].

**Which HPKE mode?** Only the recipient's public key in hand: `mode_base`, the deployment default. A pre-shared secret already exists: `mode_psk`, with a PSK carrying at least 32 bytes of entropy. The recipient must know that a specific sender key sealed the message: `mode_auth`. Both a sender identity and a shared secret: `mode_auth_psk`. Never reach for an authenticated mode expecting a third-party-verifiable signature.

**Which ciphersuite?** 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 (`0xFFFF`). 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 `base` and `psk` but not yet `auth` [@xwing_draft06].

**A worked path to shipping.** Start with a single-shot `Seal`/`Open` for one message, which removes the entire counter-management problem. Reach for `Export` only when you are keying another protocol's framing. And before you trust your integration, reproduce one of RFC 9180'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.

| Library | Language | Notes |
|---|---|---|
| OpenSSL 3.2+ | C | Native HPKE via `OSSL_HPKE_CTX` [@openssl_blog], [@openssl_api] |
| BoringSSL | C | HPKE used in the Chrome and ECH stack [@boringssl] |
| Cloudflare CIRCL | Go | Early production HPKE; powers OHTTP and ODoH [@cloudflare] |
| rust-hpke | Rust | RFC 9180-conformant crate [@rust_hpke] |
| hpke-rs | Rust | Formally oriented; ships a draft X-Wing KEM id [@hpke_rs] |
| hpke-js | JavaScript | Browser, Node.js, and Deno; full RFC 9180 suites; require `@hpke/core` 1.7.5 or later (fixes CVE-2025-64767) [@hpke_js], [@hpke_core_cve] |

> **Note:** Use `mode_base` single-shot with DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, and AES-128-GCM -- or ChaCha20-Poly1305 without AES hardware. Use `Export` to key your own framing rather than looping `Seal`. 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].

The list of nevers is short and worth memorizing: never expect forward secrecy from a static recipient key; never treat `auth` as a signature; never reuse a `Context` 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's test-vector check.

<Spoiler kind="hint" label="Why reproducing a test vector is the highest-value five minutes you will spend">
Run your library against RFC 9180'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 "conformant" now implies "interoperable."
</Spoiler>

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.

## 13. The Misconceptions, Corrected

HPKE gets misused mostly because a handful of confident, wrong sentences keep getting said in design meetings. Here they are, corrected.

<FAQ title="Frequently asked questions about HPKE">
<FAQItem question="Is HPKE forward-secret?">
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'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.
</FAQItem>
<FAQItem question="Is auth mode a digital signature?">
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].
</FAQItem>
<FAQItem question="Can I reuse a Context, and how many messages can one seal?">
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.
</FAQItem>
<FAQItem question="Is HPKE post-quantum?">
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].
</FAQItem>
<FAQItem question="HPKE or libsodium crypto_box?">
`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].
</FAQItem>
<FAQItem question="Does iMessage PQ3 use HPKE?">
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.
</FAQItem>
<FAQItem question="Do I need to worry about partitioning oracles and key commitment?">
Only if you use low-entropy pre-shared keys or open one ciphertext under multiple candidate keys. HPKE'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].
</FAQItem>
</FAQ>

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.

## 14. The Math Held; The Risk Moved to the Interface

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 "benign" [@sec1], [@maea10]. HPKE is what that task looks like when it is finally done right.

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.

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 "how do I make it secure" but "do I know its four modes and its non-goals?"

Every entry in the misuse catalog proved the point. Forward secrecy expected from a static key, a signature expected from `auth` 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].

And the post-quantum migration is the architecture'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' 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.

So carry one question out of this article and apply it to any encrypt-to-a-public-key design you review.

<PullQuote>
Which of the four modes is this, and which of HPKE's non-goals am I about to assume it has?
</PullQuote>

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 *protocol design*, 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.

<StudyGuide slug="hpke-rfc9180-kem-based-hybrid-public-key-encryption" keyTerms={[
  { term: "HPKE", definition: "The RFC 9180 standard for encrypting to a public key, built from a KEM, a KDF, and an AEAD named together as a ciphersuite." },
  { term: "KEM", definition: "A key encapsulation mechanism: Encap generates a fresh shared secret and its encapsulation from the recipient's public key; you never choose the key." },
  { term: "DEM", definition: "The data encapsulation mechanism: an AEAD that bulk-encrypts the payload under a key derived from the KEM's shared secret." },
  { term: "DHKEM", definition: "HPKE's Diffie-Hellman KEM, using an ephemeral-static exchange and binding the encapsulation and recipient key into the derivation." },
  { term: "Key schedule", definition: "HPKE'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." },
  { term: "AEAD", definition: "Authenticated encryption with associated data: confidentiality and integrity in one primitive; the DEM half of HPKE." },
  { term: "IND-CCA2", definition: "Security against adaptive chosen-ciphertext attack; the bar HPKE meets and that benign malleability violates." },
  { term: "Benign malleability", definition: "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." },
  { term: "Signcryption", definition: "Combined encryption and sender authentication with insider/outsider guarantees; HPKE's auth modes are signcryption, not third-party signatures." },
  { term: "Key commitment", definition: "An AEAD property binding a ciphertext to one key; HPKE is not committing by default, enabling partitioning oracles against low-entropy PSKs." }
]} />
