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
| Secrets are abstract | Private 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 encrypt | A 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 separate | A sender context only seals and a receiver context only opens, so a context cannot be reused in the wrong direction. |
| Randomness is explicit | There is no ambient generator to fall back on. A test generator cannot silently become the production one. |
| Registries are closed | Algorithm identifiers are variants. Unknown codepoints are unrepresentable; of_int rejects them at the boundary. |
| One nonce per success | A 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 detected | State-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 time | NIST 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
- Audit. No independent cryptographic review has been performed.
- Zeroization. OCaml's garbage collector cannot guarantee that key material is erased. Processes holding long-lived secrets should minimize retention and consider process isolation appropriate to their threat model.
- Message loss and reordering. HPKE contexts do not recover from either. A stream that skips a message cannot be resynchronized; prefer the single-shot API at protocol boundaries.
- PSK entropy.
Psk.createenforces a 32-byte minimum. No length check can establish that a secret is unpredictable. - Key-compromise impersonation. The Auth and AuthPSK modes authenticate the sender, but not as a signature does. Whoever holds the recipient's private key, and in AuthPSK mode the PSK as well, can seal a message that opens as coming from any sender (RFC 9180, Section 9.1.1), so a recipient also cannot prove to a third party who sent a message. Where either matters, sign the encapsulated key and ciphertext as well.
- Primitive side channels. The timing behavior of the curve, hash, and AEAD implementations is Mirage Crypto's and Digestif's responsibility, not this library's.
- Key management. Storage, distribution, rotation, and revocation are out of scope. So is transport framing.
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.
| Source | Covers |
|---|---|
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
- Initialize a cryptographically secure Mirage Crypto RNG and pass it explicitly. Deterministic or test generators must never be used in production.
- Serialize access to a context at the application layer.
Concurrent_usemeans no cryptography was performed by that call, but retrying still requires knowing which operation owns the next sequence number. - Treat
Open_erroruniformly at protocol boundaries. Do not build an oracle by translating lower-level setup errors into distinguishable responses. - Do not log private-key or PSK byte strings. The public API intentionally offers no secret printers or equality helpers.
- Assume secrets may persist in memory after use, and design retention accordingly.
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.