Security

No released version is currently supported for production use. The 0.1.x series is for interoperability review and has not received an independent cryptographic audit. Security fixes land on the latest 0.1.x release until a stable policy is published with 1.0.0.

HPKE is easy to implement and easy to misuse. The parts of this library that are not RFC 9180 exist to make the second harder: what the type system can prevent, it prevents; what it cannot, it reports as an error rather than guessing.

What the API makes hard

Misuse resistance by construction
Secrets are abstractPrivate keys, PSKs, and contexts are abstract types with no printer and no equality function. There is no convenient way to log a secret or compare one in variable time.
Export-only cannot encryptA suite carries a capability parameter, so an export-only suite passed to seal is a compile error rather than a run-time surprise.
Roles are separateA sender context only seals and a receiver context only opens, so a context cannot be reused in the wrong direction.
Randomness is explicitThere is no ambient generator to fall back on. A test generator cannot silently become the production one.
Registries are closedAlgorithm identifiers are variants. Unknown codepoints are unrepresentable; of_int rejects them at the boundary.
One nonce per successA successful seal or open advances the sequence exactly once, a failed open does not advance it at all, and exhaustion returns Message_limit_reached instead of wrapping.
Contention is detectedState-changing operations take an atomic guard. The losing caller gets Concurrent_use before any cryptography is performed, so no nonce is consumed.
Scalars compare in constant timeNIST private-scalar range checks go through eqaf.

Error normalization

At protocol boundaries, open_base, open_psk, open_auth, and open_auth_psk collapse every peer-controlled failure — malformed encapsulation, decapsulation failure, AEAD authentication failure, a message from the wrong sender — into a single Open_error. An attacker who can submit ciphertexts learns only that the open failed.

Key_mismatch is the deliberate exception: a key whose KEM differs from the suite's is a local configuration mistake, not something a peer can provoke, so it stays distinguishable and debuggable. The context-level setup_base_receiver likewise reports malformed encapsulations structurally, and setup_auth_receiver a sender key that fails validation, because at that level the caller is assumed to be diagnosing its own inputs.

What it does not defend against

Assurance

Interoperability is claimed only where a pinned, citable vector backs it. The fixtures are committed, and their provenance is recorded in test-vectors/PROVENANCE.md.

Test evidence
SourceCovers
RFC 9180 Appendix A, pinned at CFRG commit 5f503c5 A.1.1 X25519/HKDF-SHA256/AES-128-GCM Base, A.1.2 to A.1.4 the same suite in the PSK, Auth, and AuthPSK modes, A.2.1 X25519/HKDF-SHA256/ChaCha20-Poly1305 Base. Every vector of the same commit's machine-readable corpus is replayed as well: 128 over the four modes, P-256, P-521, X25519, X448, and every AEAD.
draft-ietf-hpke-hpke-04, pinned at HPKE working-group commit 4abc37e Appendix C.8 export-only, and Appendix D vectors for P-256 rejection sampling, empty inputs, embedded zero bytes, and empty info.
Go 1.26.5 standard-library crypto/hpke An independently generated P-384/HKDF-SHA384/AES-256-GCM fixture. The generator is kept at tools/differential/go/main.go; its outputs are pinned in the OCaml test.
Suite coverage All 45 Base combinations round-tripped, PSK, Auth, and AuthPSK across every KEM, and sender/receiver agreement on export-only suites.
Invariants A failed open retains sequence state; malformed encodings, off-curve points, invalid scalars, low-order X25519 and X448 values, and tampering are rejected; X25519 and X448 private keys are clamped as RFC 7748 specifies; single-shot errors are normalized; suite and key mismatches are reported; a message opens only in the mode it was sealed in, and only from its own sender.
Property tests (QCheck) Round-trip correctness, and totality on peer-controlled input — no input shape escapes as an exception.
Fuzzing (Crowbar) Key parsing across all five KEMs, receiver setup in all four modes on arbitrary encapsulations, arbitrary Auth-mode sender keys, and opening arbitrary ciphertexts. CI fuzzes briefly on every pull request and push to main, and for longer each week.
Concurrency Two OCaml 5 domains contend on one sender context until the guard fires. The surviving ciphertext must then open on a fresh receiver, which holds only if the losing call consumed nothing.

Vectors are never silently refreshed from a moving branch. Changing a fixture requires citing a standards revision and an immutable source commit.

Operational cautions

Reporting a vulnerability

Do not open a public issue for a suspected vulnerability. Use GitHub's private security-advisory flow on the repository. Include the affected version or commit, the suite and mode, a minimal reproduction, and your assessment of confidentiality and nonce-reuse impact. Maintainers aim to acknowledge a report within seven days and to coordinate disclosure after a fix is available.

The full policy is in SECURITY.md.