Usage
Start with one message. Use contexts when your protocol sends several
messages in order. Fallible operations return ('a, Hpke.Error.t) result.
Install · Example · Keys · PSK · Auth · Contexts · Export · Errors
Install
Requires an initialized opam switch with 64-bit OCaml 4.14+ and Dune 3.12+.
opam install hpke
These docs cover 0.4.0. The basic example also works with 0.3.0.
opam-repository takes a release only after review, so it can trail the
newest tag. To use the hybrid KEMs and Draft_hpke_04 before
0.4.0 lands there, pin the tagged source:
opam pin add hpke git+https://github.com/thevilledev/ocaml-hpke.git#v0.4.0
Encrypt and decrypt one message
Create an empty directory with these three files.
dune-project:
(lang dune 3.12)
dune:
(executable
(name main)
(libraries hpke mirage-crypto-rng.unix))
main.ml:
open Hpke
let ( let* ) = Result.bind
let round_trip ~rng =
let suite =
Suite.create ~kem:Kem.X25519 ~kdf:Kdf.Hkdf_sha256
~aead:Aead.Chacha20_poly1305
in
let* private_key, public_key =
generate_key_pair ~rng Kem.X25519
in
let info = "example-v1" and aad = "message-1" in
let* ciphertext =
Rfc9180.seal_base ~rng suite ~recipient:public_key ~info ~aad
~plaintext:"Hello, HPKE."
in
Rfc9180.open_base suite ~recipient:private_key ~info ~aad
~ciphertext
let () =
Mirage_crypto_rng_unix.use_default ();
let rng = Mirage_crypto_rng.default_generator () in
match round_trip ~rng with
| Ok plaintext -> print_endline plaintext
| Error error ->
Format.eprintf "%a@." Error.pp error;
exit 1
Run it:
opam exec -- dune exec ./main.exe
Output: Hello, HPKE.
The example keeps both keys in one process. In an application, give the sender a trusted copy of the recipient's public key and keep the private key with the recipient. Base mode does not authenticate the sender.
encapsulated_keyandciphertextare separate byte strings. Send both using your protocol's framing.infoidentifies the protocol or purpose;aadis authenticated message metadata. Neither is encrypted or sent by the library. Both must match exactly when opening.- Both peers must agree on the suite and mode. The library does not negotiate them.
The snippets below assume open Hpke and let ( let* ) = Result.bind.
Randomness
Call Mirage_crypto_rng_unix.use_default () once at startup,
as above. Pass ~rng to key generation and sender operations.
Receivers need no RNG. Deterministic generators are for tests only.
Keys
let parse_peer encoded =
Public_key.of_bytes ~kem:Kem.X25519 encoded
let encode_public public_key =
Public_key.to_bytes public_key
Private_key.of_bytes and to_bytes handle private
keys; keep those bytes secret. A key's KEM must match the suite or the
operation returns Key_mismatch. Parsing checks the encoding;
it does not establish who owns the key.
derive_key_pair kem ~ikm derives a pair deterministically
from high-entropy keying material. It is not a password KDF. For encoding
rules and sizes, see key validation.
Pre-shared keys
PSK mode requires both the recipient's private key and a shared secret to decrypt. Both peers use the same secret and identifier:
let psk_round_trip ~rng suite ~secret
~recipient_public ~recipient_private =
let* psk = Psk.create ~secret ~id:"tenant-42" in
let* ciphertext =
Rfc9180.seal_psk ~rng suite ~recipient:recipient_public ~psk
~info:"example-v1" ~aad:"" ~plaintext:"Hello, HPKE."
in
Rfc9180.open_psk suite ~recipient:recipient_private ~psk
~info:"example-v1" ~aad:"" ~ciphertext
Psk.create requires at least 32 secret bytes and a non-empty
identifier. Use a high-entropy secret; a length check cannot establish entropy.
Authenticated senders
Auth mode uses the sender's static key pair. The sender supplies its private key; the recipient supplies a trusted copy of the sender's public key:
let auth_round_trip ~rng suite ~recipient_public ~recipient_private
~sender_private ~sender_public =
let* ciphertext =
Rfc9180.seal_auth ~rng suite ~recipient:recipient_public
~sender:sender_private ~info:"example-v1" ~aad:""
~plaintext:"Hello, HPKE."
in
Rfc9180.open_auth suite ~recipient:recipient_private
~sender:sender_public ~info:"example-v1" ~aad:"" ~ciphertext
AuthPSK adds ~psk through seal_auth_psk and
open_auth_psk. All keys must match the suite's KEM.
Only DHKEMs support these modes; check Kem.supports_auth.
ML-KEM and hybrids return Unsupported_mode.
Auth is not a signature: a holder of the recipient's private key can impersonate any sender. AuthPSK also requires the PSK for that impersonation. See the authentication limits.
Post-quantum and hybrid KEMs
Choose the KEM in both the suite and key generation:
let kem = Kem.Mlkem768_x25519 (* X-Wing; requires 0.4.0 *)
let suite =
Suite.create ~kem ~kdf:Kdf.Hkdf_sha256 ~aead:Aead.Chacha20_poly1305
let make_keys ~rng = generate_key_pair ~rng kem
Use the same Base or PSK functions. Keys and encapsulations are larger; private keys serialize as seeds. Parse keys once and reuse them. See ML-KEM, hybrids, and their security limits.
Several ordered messages
Set up one context at each end. Send the encapsulated key once, then send each ciphertext in order. This example sends two messages:
let send_two ~rng suite ~recipient =
let* { Rfc9180.encapsulated_key; context } =
Rfc9180.setup_base_sender ~rng suite ~recipient ~info:"stream-v1"
in
let* first = Rfc9180.Sender.seal context ~aad:"" ~plaintext:"one" in
let* second = Rfc9180.Sender.seal context ~aad:"" ~plaintext:"two" in
Ok (encapsulated_key, first, second)
let receive_two suite ~recipient (encapsulated_key, first, second) =
let* context =
Rfc9180.setup_base_receiver suite ~recipient ~encapsulated_key
~info:"stream-v1"
in
let* first = Rfc9180.Receiver.open_ context ~aad:"" ~ciphertext:first in
let* second = Rfc9180.Receiver.open_ context ~aad:"" ~ciphertext:second in
Ok (first, second)
- Each successful seal or open advances the nonce sequence once. Failed opens leave it unchanged.
- Messages must arrive in order. Contexts cannot skip a lost message or recover from reordering.
- Serialize context access. Overlapping calls can return
Concurrent_usebefore doing cryptography; retries still need application-level ordering. - A second binding to a context shares its state. It does not copy it.
PSK, Auth, and AuthPSK have corresponding setup_* functions.
For independent messages, use the single-shot API.
Export secrets
Both sender and receiver contexts can export the same secret using the
same label and length. Exporting does not change the message sequence.
Use Suite.export_only if you need no message encryption:
let exporter =
Suite.export_only ~kem:Kem.X25519 ~kdf:Kdf.Hkdf_sha256
let export_sender ~rng ~recipient =
let* { Rfc9180.encapsulated_key; context } =
Rfc9180.setup_base_sender ~rng exporter ~recipient ~info:"export-v1"
in
let* key =
Rfc9180.Sender.export context ~context:"traffic key" ~length:32
in
Ok (encapsulated_key, key)
let export_receiver ~recipient ~encapsulated_key =
let* context =
Rfc9180.setup_base_receiver exporter ~recipient ~encapsulated_key
~info:"export-v1"
in
Rfc9180.Receiver.export context ~context:"traffic key" ~length:32
Export-only contexts cannot be passed to seal or open; the type checker rejects them. HKDF export limits are 255 times the hash size; SHAKE exports allow up to 65,535 bytes. See KDF limits.
ML-KEM and hybrid decapsulation can succeed with a secret the sender does not share. An export-only protocol must confirm that secret with its peer before relying on it.
Primitives for layered protocols
Kdf.extract and Kdf.expand expose unlabeled HKDF.
Aead.key prepares a reusable key; Aead.seal and
Aead.open_ take an explicit nonce. These functions support
protocols that derive keys from HPKE exports. Follow that protocol's
derivation and framing, and never encrypt twice with the same key and nonce.
Handle errors
Use Error.pp to format an error. Error values contain no key
material. The main cases are:
| Error | Meaning |
|---|---|
Open_error | Decryption or authentication failed. Single-shot opens also map invalid encapsulations to this error. |
Key_mismatch | A key's KEM differs from the suite's. |
Unsupported_mode | Auth or AuthPSK was requested for an ML-KEM or hybrid KEM. |
Concurrent_use | Another call held the context. This call did no cryptography. |
Message_limit_reached | The context's sequence is exhausted. Establish a new context. |
Treat peer failures uniformly in protocol responses. Context setup
exposes more specific errors than single-shot opens. For every
constructor and its contract, see
hpke.mli.