mirleft/ocaml-tls
> TLS 1.0–1.3 implemented purely in OCaml — a memory-safe protocol engine with > no I/O, born from the "Not-Quite-So-Broken TLS" research project.
GitHub repo · API docs · License: BSD-2-Clause
Overview
ocaml-tls is a from-scratch implementation of Transport Layer Security in OCaml, started in 2014 by David Kaloper-Meršinjak and Hannes Mehnert and described in a Usenix Security 2015 paper1. The motivating thesis — written in the shadow of Heartbleed — was that a TLS stack in a memory-safe, expressive language, structured as a pure function from (state, bytes) to (state, bytes), would avoid both the buffer-overflow class of C bugs and the state-machine confusion bugs (SMACK, EarlyCCS) that plagued OpenSSL-era implementations. The authors backed the claim with the Bitcoin Piñata (2015): a public MirageOS unikernel holding bitcoin whose private key could be taken by anyone who broke the stack. It was not broken2.
The defining design decision is that the core (Tls.Engine, opam package tls) performs no I/O and no mutation. You feed it received bytes and a state value; it returns a new state, bytes to write, and data for the application. Effectful adapters — tls-lwt, tls-mirage, tls-eio, tls-async, tls-miou-unix, and a plain-Unix layer — wire that engine to an actual scheduler and socket. This makes the protocol logic testable and composable (e.g. STARTTLS layering in SMTP) at the cost of a less familiar API than "wrap this socket".
At ~321 stars it is niche by raw numbers, but the number understates its footprint: it is the TLS stack of the entire MirageOS unikernel ecosystem and is maintained today by the Robur cooperative. Maintenance is demonstrably active — two CVEs reported in 2026 (missing keyUsage/extendedKeyUsage validation, CVE-2026-45388/45389) were fixed and released in v2.1.0 within the disclosure cycle3, and the last push was 2026-06-29.
Getting Started
opam install tls tls-lwt ca-certs
(* client.ml — HTTPS GET over tls-lwt (tls >= 1.0, result-based Config) *)
let tls_config =
let authenticator =
match Ca_certs.authenticator () with
| Ok a -> a | Error (`Msg m) -> failwith m
in
match Tls.Config.client ~authenticator () with
| Ok cfg -> cfg | Error (`Msg m) -> failwith m
let () =
Lwt_main.run
(let open Lwt.Infix in
Tls_lwt.connect tls_config ("mirage.io", 443) >>= fun (ic, oc) ->
Lwt_io.write oc "GET / HTTP/1.1\r\nHost: mirage.io\r\n\r\n" >>= fun () ->
Lwt_io.read ~count:512 ic >>= Lwt_io.print)
Ca_certs loads the operating system's trust anchors; without it you must construct an X509.Authenticator.t yourself — there is no implicit default.
Architecture / How It Works
The repository ships one engine and several thin effectful shells:
tls(core) — handshake state machines for TLS 1.0/1.1/1.2 (client and
server) and TLS 1.3, record layer framing, and configuration validation. Tls.Engine.handle_tls : state -> string -> ... is the single entry point: a value-passing step function. Illegal state transitions are unrepresentable or rejected explicitly rather than discovered in an interop matrix — the Usenix paper's central claim1.
- Crypto is delegated to
mirage-crypto(AES-GCM/CCM,
ChaCha20-Poly1305, ECDH/ECDSA/EdDSA via mirage-crypto-ec, which vendors code generated by fiat-crypto)4. Certificate parsing and chain validation live in the sibling ocaml-x509 library. ocaml-tls is therefore a protocol engine, not a crypto library — its safety story is only as good as those dependencies.
- Effectful layers (
tls-lwt,tls-mirage,tls-eio,tls-async,
tls-miou-unix, tls.unix) each own a read/write loop and expose the idioms of their scheduler. They are small; the protocol logic is never duplicated.
Two consequences of the pure core are worth understanding. First, composability: anything that shuttles bytes can host a TLS session (SMTP STARTTLS via colombe/sendmail, MirageOS Mirage_flow.S stacking). Second, defaults are opinionated and move: v0.13.0 (2021) dropped static-RSA and CBC ciphersuites and SHA1 signatures from the default config, leaving (EC)DHE + AEAD only3 — old peers may need explicit config to interoperate.
Production Notes
- Performance is adequate, not competitive with C. The v1.0.0 rewrite
from Cstruct to bytes/string improved bandwidth up to 3× and handshakes ~2× (measured on an i7-5600U)3, and v2.0.1 cut allocation in send_application_data. There is no kTLS offload and no assembly fast path beyond what mirage-crypto's C stubs (AES-NI, PCLMULQDQ) provide. For a TLS-terminating proxy at high throughput, this is the wrong tool.
- Secrets live on a GC-managed heap. OCaml's moving collector copies
values; reliable zeroization of key material is not achievable the way it is in C or Rust. Memory-safety cuts one attack class while making another mitigation unavailable — know which you care about.
- API churn across majors is real. 1.0.0 (2024) changed error types,
made Tls.Config.{client,server} return result instead of raising, and removed Cstruct from the interface; 0.16.0 renamed tls.lwt to the separate tls-lwt opam package. Pinned tutorials from the 0.x era will not compile.
- Old bugs do surface. v2.0.4 (2026) fixed an exception on empty
change-cipher-spec input present since 0.1.0, and v2.1.0's CVEs were missing certificate-extension validation, not memory errors3 — a reminder that language safety does not equal protocol completeness. The project does run interop suites (bettertls support landed in v2.1.1).
When to Use / When Not
Use when:
- You are building MirageOS unikernels — it is the native and only real TLS
option there.
- You are writing OCaml services and want TLS without C bindings in your
trust base (capnp-rpc, albatross, miragevpn all do this).
- You need to compose TLS inside another protocol (STARTTLS, custom tunnels)
and benefit from an I/O-free engine, or want a readable reference implementation of the TLS state machines.
Avoid when:
- Raw throughput or handshake rate is the bottleneck — OpenSSL-class stacks
with kTLS and assembly crypto are markedly faster.
- You need FIPS certification or formally audited crypto primitives.
- You are not in the OCaml ecosystem, or depend on long-tail TLS features
(exotic extensions, PKCS#11 HSM integration) a small team has not prioritized.
Alternatives
- savonet/ocaml-ssl — OpenSSL bindings for OCaml; use instead when you need
maximum interop/performance and accept C in the trust base.
- rustls/rustls — memory-safe TLS in Rust; use instead outside OCaml when
the "safe-language TLS" argument matters, with far wider deployment.
- openssl/openssl — the C default; use instead when certification, hardware
offload, or ubiquity outweigh memory-safety concerns.
- haskell-tls/hs-tls — the closest analogue: a pure functional-language TLS
stack, for Haskell codebases.
History
| Version | Date | Notes |
|---|---|---|
| 0.1.0 | 2014-07-08 | Initial beta release3. |
| 0.12.0 | 2020-05-12 | TLS 1.3 support3. |
| 0.13.0 | 2021-04-14 | AEAD-only defaults; ECDSA/EdDSA via mirage-crypto-ec. |
| 0.15.4 | 2022-09-27 | tls-eio package added. |
| 0.16.0 | 2023-02-14 | tls.lwt split into the tls-lwt opam package. |
| 1.0.0 | 2024-08-21 | Cstruct removed; result-based config; up to 3× bandwidth; tls-miou-unix3. |
| 2.0.0 | 2025-02-05 | Dune variants (mirage-ptime) replace PCLOCK functors. |
| 2.0.3 | 2025-09-26 | Plain-Unix implementation (tls.unix). |
| 2.1.0 | 2026-05-20 | keyUsage/extendedKeyUsage validation; CVE-2026-45388/45389 fixed3. |
| 2.1.1 | 2026-06-29 | TLS 1.3 KeyUpdate state fix; IP checking in certificates. |
References
- ^ Kaloper-Meršinjak, Mehnert, Madhavapeddy, Sewell, "Not-Quite-So-Broken
TLS: Lessons in Re-Engineering a Security Protocol Specification and Implementation" — Usenix Security 2015. https://www.usenix.org/conference/usenixsecurity15/technical-sessions/presentation/kaloper-mersinjak
- ^ MirageOS blog, "Announcing the Bitcoin Piñata" — 2015-02.
https://mirage.io/blog/announcing-bitcoin-pinata
- ^ ocaml-tls CHANGES.md (release notes and dates).
https://github.com/mirleft/ocaml-tls/blob/main/CHANGES.md
- ^ mirage-crypto — cryptographic primitives used by ocaml-tls.
https://github.com/mirage/mirage-crypto
Tags
ocaml, tls, cryptography, security, networking, protocol-implementation, mirageos, unikernel, functional-programming, memory-safety