package hpke
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page
Idiomatic RFC 9180 Hybrid Public Key Encryption for OCaml
Install
dune-project
Dependency
Authors
Maintainers
Sources
v0.3.0.tar.gz
md5=c8ffbe6a03d19841a9665e07c46db509
sha512=caa62cfe83178d0b623a16785abf6f93745a4aa4a1216e4983254c4c3527f0d239e0d0dd57d8794fe9aaecf28d22aff405fd4eac7869566f02132b571972b307
doc/CHANGES.html
Changelog
0.3.0 — 2026-09-25
Adds X448, the RFC 9180 Auth and AuthPSK modes, and the post-quantum ML-KEM KEMs. Two changes can break an upgrade from 0.2.0: Kem.id and Error.t gain constructors, which exhaustive matches must handle, and hpke is no longer installable on 32-bit architectures.
- Add
Kem.X448, DHKEM(X448, HKDF-SHA512), identifier0x0021, with 56-byte keys and a 64-byte shared secret. Key derivation and clamping follow the X25519 path with the X448 rules of RFC 7748, and a low-order public value is rejected when it is used. The new constructor breaks exhaustive matches onKem.id, which is why this is not a patch release. - Depend on
curve448for the X448 primitive.hpkedepends on the plaincurve448library, so the pure OCaml implementation links by default and an application selects the C one by addingcurve448.cto its own libraries.curve448needs a 64-bit OCaml, sohpkeis no longer installable on 32-bit architectures, whichever KEM an application uses. - Add the RFC 9180 Auth and AuthPSK modes, which also authenticate the sender with a static KEM key pair:
Rfc9180.setup_auth_sender,setup_auth_receiver,setup_auth_psk_sender, andsetup_auth_psk_receiver, and the single-shotseal_auth,open_auth,seal_auth_psk, andopen_auth_psk. The sender passes itsPrivate_key.tand the recipient the sender'sPublic_key.t, both as~sender; a key of another KEM is aKey_mismatch. Sender authentication is not a signature: whoever holds the recipient's private key, and in AuthPSK mode the PSK as well, can seal as any sender. The additions are new functions only, so existing callers are unaffected. - Add
setup_auth_senderandsetup_auth_psk_sendertohpke.for_testing. - Grow the pinned RFC 9180 corpus from 48 to 128 cases, every vector of the same CFRG commit: the 16 X448 Base and PSK vectors, and the 64 Auth and AuthPSK vectors over P-256, P-521, X25519, and X448. The 48 existing cases are unchanged.
- Add
Kem.Mlkem512,Kem.Mlkem768, andKem.Mlkem1024, the post-quantum ML-KEM KEMs of FIPS 203 with identifiers0x0040to0x0042, asdraft-ietf-hpke-pq-05specifies them. They work in the Base and PSK modes, where a suite with an HKDF runs the RFC 9180 key schedule unchanged. A private key is the 64-byte seedd || z, andgenerate_key_pairtakes it from the generator asML-KEM.KeyGendoes. A public key must pass the modulus check of FIPS 203. Keys are parsed once and kept: parsing a private key runs key generation, and parsing a public key expands its matrix. An encapsulated key is an ML-KEM ciphertext, soKem.encapsulated_key_sizeis no longerKem.public_key_sizefor every KEM. One of the right length never fails to decapsulate: a forged one yields an unrelated secret (implicit rejection), and the failure surfaces asOpen_errorat the first open. The draft is not yet an RFC, andderive_key_pairfor these KEMs follows it. - Add
Kem.supports_authandError.Unsupported_mode. ML-KEM has no authenticated encapsulation, so on an ML-KEM suite the Auth and AuthPSK functions returnUnsupported_modebefore they look at a key or draw randomness, and the single-shot opens leave it distinguishable, as they doKey_mismatch. The new constructor breaks exhaustive matches onError.t. - Depend on
mlkemfor the ML-KEM primitive, and for the SHAKE256 that deriving an ML-KEM key pair takes:mlkemexports the one ML-KEM itself runs on asMlkem.Fips202, so the library still implements no primitive of its own. hpke.for_testingrefuses an ML-KEM suite, which has no ephemeral key to choose: its Base and PSK senders returnInvalid_private_key, and its Auth and AuthPSK sendersUnsupported_mode, as the ordinary Auth and AuthPSK functions do. A generator that returns the fixed encapsulation randomness, passed as~rngto the ordinary setup functions, reproduces such vectors.- Pin the
draft-ietf-hpke-pq-05corpus: its three ML-KEM vectors that use an HKDF, replayed in full, and the fourth as far as the KEM goes. - Parse private keys through one exhaustive match on the KEM. The previous wildcard would have let a new KEM skip clamping without a compiler warning.
- Release a context when an exception from a signal handler interrupts
Sender.sealorReceiver.open_while it marks the context busy. Since 0.1.0,Fun.protectallocated before installing its handler, and on an exception again before releasing the context, and an exception raised at either allocation left the context marked busy for good, so that every later call returnedConcurrent_use. - Check X25519 and X448 private-key clamping against literal bytes. The vector corpus cannot: it compares serializations that have both passed through the library's own clamp, and the primitives clamp again when they use a scalar.
- Never let an interrupted
Sender.sealorReceiver.open_rewind a context's sequence number. Since 0.1.0, an exception raised by a signal handler, such asSys.Breakor a timeout, could stop the increment between two byte stores and leave the sequence 255 or more below the number just used. A caller that caught it and kept the context then sealed under nonces it had already used, and a receiver accepted replays. The increment now stores the carried byte first, so an interrupted one can at most skip sequence numbers. - Parse the Diffie-Hellman secret once per private key, and not on every exchange. Parsing derives the public key, a scalar multiplication whose result every exchange discarded. X25519 and X448 have no shortcut for the base point, so there it cost as much as the exchange itself: setting up a receiver is up to twice as fast and setting up a sender up to half again as fast. The NIST curves multiply the base point from a table and gain a little over a tenth.
- Refuse an AES-GCM plaintext of 2^36 - 31 bytes as
Plaintext_too_long. Since 0.1.0 the limit was RFC 5116's, one byte beyond NIST SP 800-38D (RFC 5116 erratum 5219). mirage-crypto refuses that byte itself, soAead.sealandRfc9180.Sender.sealreturnedInternal_errorfor it, where the interface promisesPlaintext_too_long. Every shorter plaintext seals as before. - Keep the RFC 9180 wire behavior of the existing KEMs and modes unchanged from 0.2.0.
0.2.0 — 2026-09-18
- Export the suite's unlabeled primitives for protocols layered on HPKE, such as Oblivious HTTP (RFC 9458):
Kdf.extractandKdf.expand(plain RFC 5869 HKDF) and the single-shotAead.sealandAead.open_under a key prepared byAead.keyand an explicit nonce. Wrong-sized keys, nonces, pseudorandom keys, and output lengths are reported asInvalid_length. - Expand the AEAD key once per context, and not on every seal and open. Without hardware support, deriving the GHASH tables of an AES-GCM key costs more than sealing several kilobytes, so contexts that carry many messages are several times faster.
- Export the RFC 9180 parameters
Aead.key_size,Aead.nonce_size,Aead.tag_size,Kdf.hash_size, andKem.secret_size. - Add the separate
hpke.for_testinglibrary, which sets up a Base or PSK sender from a caller-chosen ephemeral private key so that vectors which fixskEcan be reproduced. It must never be linked outside a test suite. - Retain the key-schedule intermediates in the pinned RFC 9180 corpus and use them, the published
skEm, and every encryption record as known answers for the new entry points. - Keep the existing API and RFC 9180 wire behavior unchanged from 0.1.1.
0.1.1 — 2026-08-29
- Expand known-answer coverage to all 48 supported Base and PSK combinations in the pinned RFC 9180 corpus, including representative 257-message sequences and the independent Go P-384 fixture.
- Broaden malformed-input, mismatch, concurrency, property, and Crowbar fuzz coverage across every supported KEM and AEAD.
- Test OCaml 4.14, 5.2, and 5.5 on Linux, OCaml 4.14 and 5.5 on macOS, declared dependency lower bounds, and isolated OPAM package installation.
- Accommodate OCaml 5.5's stricter GADT exhaustiveness analysis.
- Keep the public API and RFC 9180 wire behavior unchanged from 0.1.0.
- Remain an unaudited, non-production release intended for interoperability review.
0.1.0 — 2026-08-23
- Add RFC 9180 Base and PSK setup, stateful contexts, single-shot APIs, and export-only suites.
- Add P-256, P-384, P-521, and X25519 DHKEM; SHA-2 HKDFs; AES-GCM and ChaCha20-Poly1305.
- Add abstract validated keys, deterministic
DeriveKeyPair, explicit-RNG key generation, and minimum-length PSK values. - Add atomic context-use guards, 96-bit sequence counters, bounds checks, and normalized single-shot open errors.
- Add pinned RFC vectors, all-suite round trips, adversarial/state tests, QCheck properties, and Crowbar fuzz targets.
- Add a security policy and release-readiness checks.
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page