Development

The library is one module. lib/hpke.mli is the contract and lib/hpke.ml is the whole implementation: DHKEM, the RFC 9180 key schedule, contexts, and the single-shot API over Mirage Crypto primitives.

Set up

git clone https://github.com/thevilledev/ocaml-hpke.git
cd ocaml-hpke
opam install . --deps-only --with-test --with-doc

Build and test

dune build @all @doc
dune runtest

dune runtest runs the Alcotest suite and, on OCaml 5.0 or newer, the two-domain concurrency check. Generated odoc lands in _build/default/_doc/_html/.

Alcotest groups
RFC 9180 vectorsX25519 Base, PSK, Auth, and AuthPSK, and ChaCha20-Poly1305 Base fixtures.
Successor draft vectorsP-256 rejection sampling, empty and zero-byte AEAD inputs, empty and embedded-zero info, embedded-zero PSK inputs, and export-only.
Differential fixturesThe P-384 fixture generated by Go's crypto/hpke.
SuitesAll 45 Base combinations, PSK, Auth, and AuthPSK across every KEM, and export-only agreement.
InvariantsSequence retention after a failed open, malformed inputs, X25519 and X448 private-key clamping, single-shot error normalization, adversarial mismatches, and mismatched modes and senders.
Suite primitivesAlgorithm sizes, bounds of the unlabeled KDF, and single-shot AEAD round trips, tampering, and length checks.
For testingThe deterministic senders: agreement with an ordinary receiver in all four modes, repeatability, and ephemeral, recipient, and sender keys of another KEM.
PropertiesQCheck round-trip and peer-input totality.

A second executable, test_vectors, replays the pinned RFC 9180 corpus four ways: through the public API with fixed randomness, through hpke.for_testing with the published ephemeral private key, against the published key-schedule intermediates for Kdf.extract and Kdf.expand, and against every encryption record for Aead.seal and Aead.open_.

Deterministic senders

HPKE requires a fresh, secret ephemeral key for every sender context, and the public API always draws it from the generator it is given. Some published vectors fix the ephemeral private key itself, including those of protocols layered on HPKE such as Oblivious HTTP (RFC 9458). The separate hpke.for_testing library reproduces them:

Hpke_for_testing.setup_base_sender suite ~ephemeral ~recipient ~info

setup_psk_sender, setup_auth_sender, and setup_auth_psk_sender do the same for the other modes.

It is a separate findlib library so that a dependency on it is visible in a dune file. Reusing or disclosing an ephemeral key breaks the confidentiality of everything sealed under it; nothing outside a test suite should link hpke.for_testing.

Fuzzing

dune build --profile fuzz fuzz/fuzz_hpke.exe
_build/default/fuzz/fuzz_hpke.exe --repeat 1000

The Crowbar targets cover the code paths an attacker controls: key parsing for every KEM, receiver setup in all four modes on arbitrary encapsulations, Auth-mode sender keys, and opening arbitrary ciphertexts against a valid encapsulation. The executable is behind the fuzz profile so a normal build does not need Crowbar.

Formatting and metadata

dune fmt
opam lint hpke.opam

Formatting is ocamlformat 0.29.0 with the conventional profile, installed through opam install . --deps-only --with-dev-setup. CI runs dune fmt and then requires a clean git diff, so unformatted code fails rather than being reformatted for you.

Layout

lib/
  hpke.mli              public API and contract
  hpke.ml               DHKEM, key schedule, contexts
  hpke_for_testing.mli  hpke.for_testing: deterministic senders
test/
  test_hpke.ml          vectors, suites, invariants, properties
  test_vectors.ml       the pinned RFC 9180 corpus
  concurrency/          two-domain contention, OCaml 5.0+
fuzz/
  fuzz_hpke.ml          Crowbar targets for peer input
tools/differential/go/  generator for the Go crypto/hpke fixture
test-vectors/           vector provenance
website/                this static documentation site

Continuous integration

The exact OCaml versions, fuzzing parameters, and dependency pins are recorded in .github/workflows/ci.yml.

Jobs, on every pull request and push to main plus a weekly schedule
testBuilds @all @doc and runs the suite with several OCaml versions on Linux and macOS.
fuzzRuns the Crowbar targets with a fixed seed, briefly on every pull request and push to main and for longer on the weekly schedule.
packagingRuns opam lint, then installs the package itself with opam install, tests and docs included.
hygieneopam lint, dune fmt, and a clean working tree.
lower-boundsInstalls the exact declared minimums from hpke.opam on the oldest supported OCaml and runs the suite against them.

The lower-bounds job is why the version constraints in hpke.opam mean something: a floor that is never built is a guess.

Test-vector policy

Fixtures are pinned to immutable commits and are never refreshed from a moving branch. Changing one requires citing a standards revision and its source commit; the current pins are recorded in test-vectors/PROVENANCE.md.

Contributing

Changes to wire behavior need a vector, not an argument. A new suite needs a pinned fixture from an independent implementation or a published standard; a new mode belongs in the versioned module that names its standard. Additions to the public API should say what they make impossible, not only what they make possible.

Use issues for bugs and proposals, and the private advisory flow described on the security page for anything exploitable. Contributions are ISC.