250 lines
12 KiB
Markdown
250 lines
12 KiB
Markdown
|
|
### The OCaml-TLS architecture
|
||
|
|
|
||
|
|
The OCaml ecosystem has several distinct ways of interacting with the outside world
|
||
|
|
(and the network in particular): straightforward [unix][ocaml-unix] interfaces
|
||
|
|
and the asynchronous programming libraries [lwt][] and [async][]. One of the
|
||
|
|
early considerations was not to restrict ourselves to any of those -- we wanted
|
||
|
|
to support them all.
|
||
|
|
|
||
|
|
There were also two distinct basic "platforms" we wanted to target from the
|
||
|
|
outset: the case of a simple executable, and the case of `Mirage` unikernels.
|
||
|
|
|
||
|
|
So one of the first questions we faced was deciding how to represent
|
||
|
|
interactions with the network in a portable way. This can be done by
|
||
|
|
systematically abstracting out the API boundary which gives access to network
|
||
|
|
operations, but we had a third thing in mind as well: we wanted to exploit the
|
||
|
|
functional nature of OCaml to its fullest extent!
|
||
|
|
|
||
|
|
Our various prior experiences with Haskell and Idris convinced us to adopt
|
||
|
|
what is called "purely functional" technique. We believe it to be an approach
|
||
|
|
which first forces the programmer to give principled answers to all the
|
||
|
|
difficult design questions (errors and global data-flow) *in advance*, and then
|
||
|
|
leads to far cleaner and composable code later on. A purely functional system
|
||
|
|
has all the data paths made completely explicit in the form of function
|
||
|
|
arguments and results. There are no unaccounted-for interactions between
|
||
|
|
components mediated by shared state, and all the activity of the parts of the
|
||
|
|
system is exposed through types since, after all, it's only about computing
|
||
|
|
values from values.
|
||
|
|
|
||
|
|
For these reasons, the library is split into two parts: the directory `/lib`
|
||
|
|
(and the corresponding findlib package `tls`) contains the core TLS logic, and
|
||
|
|
`/mirage` and `/lwt` (packaged as `tls.mirage` and `tls.lwt` respectively)
|
||
|
|
contain front-ends that tie the core to `Mirage` and `Lwt_unix`.
|
||
|
|
|
||
|
|
[ocaml-unix]: http://caml.inria.fr/pub/docs/manual-ocaml/libref/Unix.html
|
||
|
|
[lwt]: http://ocsigen.org/lwt/
|
||
|
|
[async]: https://realworldocaml.org/v1/en/html/concurrent-programming-with-async.html
|
||
|
|
|
||
|
|
### Core
|
||
|
|
|
||
|
|
The [core][tls-engine-mli] library is purely functional. A TLS session is represented by the
|
||
|
|
abstract type `Tls.Engine.state`, and various functions consume this session
|
||
|
|
type together with raw bytes (`Cstruct.t` -- which is by itself mutable, but
|
||
|
|
`ocaml-tls` eschews this) and produce new session values and resulting buffers.
|
||
|
|
|
||
|
|
The central entry point is [handle_tls][], which transforms an input state and a
|
||
|
|
buffer to an output state, a (possibly empty) buffer to send to the
|
||
|
|
communication partner, and an optional buffer of data intended to be received by
|
||
|
|
the application:
|
||
|
|
|
||
|
|
```OCaml
|
||
|
|
type state
|
||
|
|
|
||
|
|
type ret = [
|
||
|
|
| `Ok of [ `Ok of state | `Eof | `Alert of alert ] *
|
||
|
|
[ `Response of Cstruct.t ] * [ `Data of Cstruct.t option ]
|
||
|
|
| `Fail of alert * [ `Response of Cstruct.t ]
|
||
|
|
]
|
||
|
|
|
||
|
|
val handle_tls : state -> Cstruct.t -> ret
|
||
|
|
```
|
||
|
|
|
||
|
|
As the signature shows, errors are signalled through the `ret` type, which is a [polymorphic variant][poly]. This
|
||
|
|
reflects the actual internal structure: all the errors are represented as
|
||
|
|
values, and operations are composed using an error [monad][monad-ml].
|
||
|
|
|
||
|
|
Other entry points share the same basic behaviour: they transform the prior
|
||
|
|
state and input bytes into the later state and output bytes.
|
||
|
|
|
||
|
|
Here's a rough outline of what happens in `handle_tls`:
|
||
|
|
|
||
|
|
- TLS packets consist of a header, which contains the protocol
|
||
|
|
version, length, and content type, and the payload of the given
|
||
|
|
content type. Once inside our [main handler][handle_tls], we
|
||
|
|
[separate][separate_records] the buffer into TLS records, and
|
||
|
|
[process][handle_raw_record] each individually. We first check that
|
||
|
|
the version number is correct, then [decrypt][decrypt], and [verify
|
||
|
|
the mac][verify_mac].
|
||
|
|
|
||
|
|
- Decrypted data is then [dispatched][handle_packet] to one of four
|
||
|
|
sub-protocol handlers (Handshake, Change Cipher Spec, Alert and
|
||
|
|
Application Data). Each handler can [return][return_types] a new
|
||
|
|
handshake state, outgoing data, application data, the new decryption
|
||
|
|
state or an error (with the outgoing data being an interleaved list
|
||
|
|
of buffers and new encryption states).
|
||
|
|
|
||
|
|
- The outgoing buffers and the encryption states are
|
||
|
|
[traversed][encrypt] to produce the final output to be sent to the
|
||
|
|
communication partner, and the final encryption, decryption and
|
||
|
|
handshake states are combined into a new overall state which is
|
||
|
|
returned to the caller.
|
||
|
|
|
||
|
|
Handshake is (by far) the most complex TLS sub-protocol, with an elaborate state
|
||
|
|
machine. Our [client][client_handshake] and [server][server_handshake] encode
|
||
|
|
this state as a "flat" [sum type][handshake_states], with exactly one incoming
|
||
|
|
message allowed per state. The handlers first [parse][parse_handshake] the
|
||
|
|
handshake packet (which fails in case of malformed or unknown data) and then
|
||
|
|
dispatch it to the handling function. The [handshake state][handshake_state] is
|
||
|
|
carried around and a fresh one is returned from the handler in case it needs
|
||
|
|
updates. It consists of a protocol version, the handshake state, configuration,
|
||
|
|
renegotiation data, and possibly a handshake fragment.
|
||
|
|
|
||
|
|
Logic of both handshake handlers is very localised, and does not mutate any
|
||
|
|
global data structures.
|
||
|
|
|
||
|
|
[poly]: https://realworldocaml.org/v1/en/html/variants.html#polymorphic-variants
|
||
|
|
[monad-ml]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/control.ml
|
||
|
|
[return_types]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/state.ml#L109
|
||
|
|
[encrypt]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L48
|
||
|
|
[handle_packet]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L240
|
||
|
|
[verify_mac]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L85
|
||
|
|
[decrypt]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L95
|
||
|
|
[handle_tls]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L321
|
||
|
|
[handle_raw_record]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L275
|
||
|
|
[separate_records]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L150
|
||
|
|
|
||
|
|
[handshake_state]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/state.ml#L92
|
||
|
|
[parse_handshake]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/reader.ml#L361
|
||
|
|
[separate_handshakes]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.ml#L217
|
||
|
|
[handshake_states]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/state.ml#L61
|
||
|
|
[server_handshake]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/handshake_server.ml#L247
|
||
|
|
[client_handshake]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/handshake_client.ml#L285
|
||
|
|
|
||
|
|
### Core API
|
||
|
|
|
||
|
|
OCaml permits the implementation a module to be exported via a more
|
||
|
|
abstract *signature* that hides the internal representation
|
||
|
|
details. Our public API for the core library consists of the
|
||
|
|
[Tls.Engine][tls-engine-mli] and [Tls.Config][tls-config-mli] modules.
|
||
|
|
|
||
|
|
`Tls.Engine` contains the basic reactive function `handle_tls`, mentioned above,
|
||
|
|
which processes incoming data and optionally produces a response, together with
|
||
|
|
several operations that allow one to initiate message transfer like
|
||
|
|
`send_application_data` (which processes application-level messages for
|
||
|
|
sending), `send_close_notify` (for sending the ending message) and `reneg`
|
||
|
|
(which initiates full TLS renegotiation).
|
||
|
|
|
||
|
|
The module also contains the only two ways to obtain the initial state:
|
||
|
|
|
||
|
|
```OCaml
|
||
|
|
val client : Config.client -> (state * Cstruct.t)
|
||
|
|
val server : Config.server -> state
|
||
|
|
```
|
||
|
|
|
||
|
|
That is, one needs a configuration value to create it. The `Cstruct.t`
|
||
|
|
that `client` emits is the initial Client Hello since in TLS,
|
||
|
|
the client starts the session.
|
||
|
|
|
||
|
|
`Tls.Config` synthesizes configurations, separately for client and server
|
||
|
|
endpoints, through the functions `client_exn` and `server_exn`. They take a
|
||
|
|
number of parameters that define a TLS session, check them for consistency, and
|
||
|
|
return the sanitized `config` value which can be used to create a `state` and,
|
||
|
|
thus, a session. If the check fails, they raise an exception.
|
||
|
|
|
||
|
|
The parameters include the pair of a certificate and its private key for the
|
||
|
|
server, and an `X509.Authenticator.t` for the client, both produced by our
|
||
|
|
[ocaml-x509][] library and described in a [previous article][x509-intro].
|
||
|
|
|
||
|
|
This design reflects our attempts to make the API as close to "fire and forget"
|
||
|
|
as we could, given the complexity of TLS: we wanted the library to be relatively
|
||
|
|
straightforward to use, have a minimal API footprint and, above all, fail very
|
||
|
|
early and very loudly when misconfigured.
|
||
|
|
|
||
|
|
[tls-engine-mli]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/engine.mli
|
||
|
|
|
||
|
|
[tls-config-mli]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lib/config.mli
|
||
|
|
|
||
|
|
[ocaml-x509]: https://github.com/mirleft/ocaml-x509
|
||
|
|
|
||
|
|
|
||
|
|
### Effectful front-ends
|
||
|
|
|
||
|
|
Clearly, reading and writing network data *does* change the state of the world.
|
||
|
|
Having a pure value describing the state of a TLS session is not really useful
|
||
|
|
once we write something onto the network; it is certainly not the case that we
|
||
|
|
can use more than one distinct `state` to process further data, as only one
|
||
|
|
value is in sync with the other endpoint at any given time.
|
||
|
|
|
||
|
|
Therefore we wrap the core types into stateful structures loosely inspired by
|
||
|
|
sockets and provide IO operations on those. The structures of `mirage` and `lwt`
|
||
|
|
front-ends mirror one another.
|
||
|
|
|
||
|
|
In both cases, the structure is pull-based in the sense that no processing is
|
||
|
|
done until the client requires a read, as opposed to a callback-driven design
|
||
|
|
where the client registers a callback and the library starts spinning in a
|
||
|
|
listening loop and invoking it as soon as there is data to be processed. We do
|
||
|
|
this because in an asynchronous context, it is easy to create a callback-driven
|
||
|
|
interface from a demand-driven one, but the opposite is possible only with
|
||
|
|
unbounded buffering of incoming data.
|
||
|
|
|
||
|
|
One exception to demand-driven design is the initial session creation: the
|
||
|
|
library will only yield the connection after the first handshake is over,
|
||
|
|
ensuring the invariant that it is impossible to interact with a connection if it
|
||
|
|
hasn't already been fully established.
|
||
|
|
|
||
|
|
**Mirage**
|
||
|
|
|
||
|
|
The `Mirage` [interface][tls_mirage_types_mli] matches the [FLOW][flow]
|
||
|
|
signature (with additional TLS-specific operations). We provide a functor that
|
||
|
|
needs to be applied to an underlying TCP module, to obtain a TLS transport on
|
||
|
|
top. For example:
|
||
|
|
|
||
|
|
```OCaml
|
||
|
|
module Server (Stack: STACKV4) (KV: KV_RO) =
|
||
|
|
struct
|
||
|
|
|
||
|
|
module TLS = Tls_mirage.Make (Stack.TCPV4)
|
||
|
|
module X509 = Tls_mirage.X509 (KV) (Clock)
|
||
|
|
|
||
|
|
let accept conf flow =
|
||
|
|
TLS.server_of_tcp_flow conf flow >>= function
|
||
|
|
| `Ok tls ->
|
||
|
|
TLS.read tls >>= function
|
||
|
|
| `Ok buf ->
|
||
|
|
TLS.write tls buf >>= fun () -> TLS.close buf
|
||
|
|
|
||
|
|
let start stack e kv =
|
||
|
|
lwt authenticator = X509.authenticator kv `Default in
|
||
|
|
let conf = Tls.Config.server_exn ~authenticator () in
|
||
|
|
Stack.listen_tcpv4 stack 4433 (accept conf) ;
|
||
|
|
Stack.listen stack
|
||
|
|
|
||
|
|
end
|
||
|
|
```
|
||
|
|
|
||
|
|
**Lwt**
|
||
|
|
|
||
|
|
The `lwt` interface has [two layers][tls_lwt_mli]. `Tls_lwt.Unix` is loosely based
|
||
|
|
on read/write operations from `Lwt_unix` and provides in-place update of
|
||
|
|
buffers. `read`, for example, takes a `Cstruct.t` to write into and returns the
|
||
|
|
number of bytes read. The surrounding module, `Tls_lwt`, provides a simpler,
|
||
|
|
`Lwt_io`-compatible API built on top:
|
||
|
|
|
||
|
|
```OCaml
|
||
|
|
let main host port =
|
||
|
|
lwt authenticator = X509_lwt.authenticator (`Ca_dir nss_trusted_ca_dir) in
|
||
|
|
lwt (ic, oc) = Tls_lwt.connect ~authenticator (host, port) in
|
||
|
|
let req = String.concat "\r\n" [
|
||
|
|
"GET / HTTP/1.1" ; "Host: " ^ host ; "Connection: close" ; "" ; ""
|
||
|
|
] in
|
||
|
|
Lwt_io.(write oc req >>= fun () -> read ic >>= print)
|
||
|
|
```
|
||
|
|
|
||
|
|
We have further plans to provide wrappers for [`Async`][async] and plain [`Unix`][ocaml-unix] in a
|
||
|
|
similar vein.
|
||
|
|
|
||
|
|
[tls_mirage_types_mli]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/mirage/tls_mirage_types.mli
|
||
|
|
[flow]: https://github.com/mirage/mirage/blob/ae3c966f8d726dc97208595b8005e02e39478cb1/types/V1.mli#L136
|
||
|
|
[example_unikernel]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/mirage/example/unikernel.ml
|
||
|
|
[tls_lwt_mli]: https://github.com/mirleft/ocaml-tls/blob/6dc9258a38489665abf2bd6cdbed8a1ba544d522/lwt/tls_lwt.mli
|