diff --git a/async/dune b/async/dune new file mode 100644 index 00000000..d80ffcb7 --- /dev/null +++ b/async/dune @@ -0,0 +1,5 @@ +(library + (name tls_async) + (public_name tls-async) + (preprocess (pps ppx_jane)) + (libraries async core cstruct-async mirage-crypto-rng-async tls)) diff --git a/async/examples/dune b/async/examples/dune new file mode 100644 index 00000000..3446effc --- /dev/null +++ b/async/examples/dune @@ -0,0 +1,11 @@ +(executable + (name test_client) + (modules test_client) + (preprocess (pps ppx_jane)) + (libraries async core tls-async)) + +(executable + (name test_server) + (modules test_server) + (preprocess (pps ppx_jane)) + (libraries async core tls-async)) diff --git a/async/examples/test_client.ml b/async/examples/test_client.ml new file mode 100644 index 00000000..4b0f934d --- /dev/null +++ b/async/examples/test_client.ml @@ -0,0 +1,30 @@ +open! Core +open! Async +open Deferred.Or_error.Let_syntax + +let config = Tls.Config.client ~authenticator:(fun ~host:_ _ -> Ok None) () + +let test_client () = + let host = "127.0.0.1" in + let port = 8443 in + let hnp = Host_and_port.create ~host ~port in + let%bind (_ : Tls_async.Session.t), rd, wr = + Tls_async.connect config (Tcp.Where_to_connect.of_host_and_port hnp) ~host:(Some host) + in + let req = + String.concat + ~sep:"\r\n" + [ "GET / HTTP/1.1"; "Host: " ^ host; "Connection: close"; ""; "" ] + in + Writer.write wr req; + let%bind () = Writer.flushed wr |> Deferred.ok in + let%bind () = + match%map Reader.read_line rd |> Deferred.ok with + | `Ok str -> print_endline str + | `Eof -> print_endline "Eof reached" + in + Writer.close wr |> Deferred.ok +;; + +let cmd = Command.async_or_error ~summary:"test client" (Command.Param.return test_client) +let () = Command.run cmd diff --git a/async/examples/test_server.ml b/async/examples/test_server.ml new file mode 100644 index 00000000..a0e28946 --- /dev/null +++ b/async/examples/test_server.ml @@ -0,0 +1,74 @@ +open! Core +open! Async + +let server_cert = "./certificates/server.pem" +let server_key = "./certificates/server.key" + +module X509_async = struct + let lift_of_result_msg : ('a, [< `Msg of string ]) result -> 'a Or_error.t = + Result.map_error ~f:(fun (`Msg message) -> Error.of_string message) + ;; + + let x509_of_pem pem = + Cstruct.of_string pem |> X509.Certificate.decode_pem_multiple |> lift_of_result_msg + ;; + + let certs_of_pems ca_file = Reader.file_contents ca_file >>| x509_of_pem + + let private_of_pems ~cert ~priv_key = + let open Deferred.Or_error.Let_syntax in + let%bind certs = certs_of_pems cert in + let%map priv_key = + let%bind priv = + Reader.file_contents priv_key |> Deferred.ok >>| Cstruct.of_string + in + X509.Private_key.decode_pem priv |> lift_of_result_msg |> Deferred.return + in + certs, priv_key + ;; +end + +let serve_tls port handler = + let%bind certificate, priv_key = + X509_async.private_of_pems ~cert:server_cert ~priv_key:server_key + |> Deferred.Or_error.ok_exn + in + let config = + Tls.Config.( + server + ~version:(`TLS_1_0, `TLS_1_2) + ~certificates:(`Single (certificate, priv_key)) + ~ciphers:Ciphers.supported + ()) + in + let where_to_listen = Tcp.Where_to_listen.of_port port in + let on_handler_error = `Ignore in + Tls_async.listen ~on_handler_error config where_to_listen handler +;; + +let test_server port = + let handler (_ : Socket.Address.Inet.t) (_ : Tls_async.Session.t) rd wr = + let pipe = Reader.pipe rd in + let rec read_from_pipe () = + (match%map Pipe.read pipe with + | `Ok line -> Writer.write wr line + | `Eof -> ()) + >>= read_from_pipe + in + read_from_pipe () + in + serve_tls port handler +;; + +let cmd = + let open Command.Let_syntax in + Command.async + ~summary:"test server" + (let%map_open port = anon ("PORT" %: int) in + fun () -> + let open Deferred.Let_syntax in + let%bind server = test_server port in + Tcp.Server.close_finished server) +;; + +let () = Command.run cmd diff --git a/async/io.ml b/async/io.ml new file mode 100644 index 00000000..7a177ba3 --- /dev/null +++ b/async/io.ml @@ -0,0 +1,193 @@ +open! Core +open! Async +include Io_intf + +module Tls_error = struct + type t = + | Tls_alert of Tls.Packet.alert_type + (** [Tls_alert] exception received from the other endpoint *) + | Tls_failure of Tls.Engine.failure + (** [Tls_failure] exception while processing incoming data *) + | Connection_closed + | Connection_not_ready + | Unexpected_eof + | Unable_to_renegotiate + | Unable_to_update_key + [@@deriving sexp_of] +end + +module Make (Fd : Fd) : S with module Fd := Fd = struct + open Deferred.Or_error.Let_syntax + + module State = struct + type t = + | Active of Tls.Engine.state + | Eof + | Error of Tls_error.t + end + + type t = + { fd : Fd.t + ; mutable state : State.t + ; mutable linger : Cstruct.t option + ; recv_buf : Cstruct.t + } + + let tls_error = Fn.compose Deferred.Or_error.error_s Tls_error.sexp_of_t + + let rec read_react t = + let handle tls buf = + match Tls.Engine.handle_tls tls buf with + | Ok (state, `Response resp, `Data data) -> + t.state + <- (match state with + | `Ok tls -> Active tls + | `Eof -> Eof + | `Alert a -> Error (Tls_alert a)); + let%map () = + match resp with + | None -> return () + | Some resp -> Fd.write_full t.fd resp + in + `Ok data + | Error (alert, `Response resp) -> + t.state <- Error (Tls_failure alert); + let%bind () = Fd.write_full t.fd resp in + read_react t + in + match t.state with + | Error e -> tls_error e + | Eof -> return `Eof + | Active _ -> + let%bind n = Fd.read t.fd t.recv_buf in + (match t.state, n with + | Active _, `Eof -> + t.state <- Eof; + return `Eof + | Active tls, `Ok n -> handle tls (Cstruct.sub t.recv_buf 0 n) + | Error e, _ -> tls_error e + | Eof, _ -> return `Eof) + ;; + + let rec read t buf = + let writeout res = + let open Cstruct in + let rlen = len res in + let n = min (len buf) rlen in + blit res 0 buf 0 n; + t.linger <- (if n < rlen then Some (sub res n (rlen - n)) else None); + return n + in + match t.linger with + | Some res -> writeout res + | None -> + (match%bind read_react t with + | `Eof -> return 0 + | `Ok None -> read t buf + | `Ok (Some res) -> writeout res) + ;; + + let writev t css = + match t.state with + | Error err -> tls_error err + | Eof -> tls_error Connection_closed + | Active tls -> + (match Tls.Engine.send_application_data tls css with + | Some (tls, tlsdata) -> + t.state <- Active tls; + Fd.write_full t.fd tlsdata + | None -> tls_error Connection_not_ready) + ;; + + (* + * XXX bad XXX + * This is a point that should particularly be protected from concurrent r/w. + * Doing this before a `t` is returned is safe; redoing it during rekeying is + * not, as the API client already sees the `t` and can mistakenly interleave + * writes while this is in progress. + * *) + let rec drain_handshake t = + let push_linger t mcs = + match mcs, t.linger with + | None, _ -> () + | scs, None -> t.linger <- scs + | Some cs, Some l -> t.linger <- Some (Cstruct.append l cs) + in + match t.state with + | Active tls when not (Tls.Engine.handshake_in_progress tls) -> return t + | _ -> + (match%bind read_react t with + | `Eof -> tls_error Unexpected_eof + | `Ok cs -> + push_linger t cs; + drain_handshake t) + ;; + + let reneg ?authenticator ?acceptable_cas ?cert ?(drop = true) t = + match t.state with + | Error err -> tls_error err + | Eof -> tls_error Connection_closed + | Active tls -> + (match Tls.Engine.reneg ?authenticator ?acceptable_cas ?cert tls with + | None -> tls_error Unable_to_renegotiate + | Some (tls', buf) -> + if drop then t.linger <- None; + t.state <- Active tls'; + let%bind () = Fd.write_full t.fd buf in + let%bind _ = drain_handshake t in + return ()) + ;; + + let key_update ?request t = + match t.state with + | Error err -> tls_error err + | Eof -> tls_error Connection_closed + | Active tls -> + (match Tls.Engine.key_update ?request tls with + | Error _ -> tls_error Unable_to_update_key + | Ok (tls', buf) -> + t.state <- Active tls'; + Fd.write_full t.fd buf) + ;; + + let close_tls t = + match t.state with + | Active tls -> + let _, buf = Tls.Engine.send_close_notify tls in + t.state <- Eof; + Fd.write_full t.fd buf + | _ -> return () + ;; + + let server_of_fd config fd = + drain_handshake + { state = Active (Tls.Engine.server config) + ; fd + ; linger = None + ; recv_buf = Cstruct.create 4096 + } + ;; + + let client_of_fd config ?host fd = + let config' = + match host with + | None -> config + | Some host -> Tls.Config.peer config host + in + let t = { state = Eof; fd; linger = None; recv_buf = Cstruct.create 4096 } in + let tls, init = Tls.Engine.client config' in + let t = { t with state = Active tls } in + let%bind () = Fd.write_full t.fd init in + drain_handshake t + ;; + + let epoch t = + match t.state with + | Active tls -> + (match Tls.Engine.epoch tls with + | `InitialEpoch -> assert false (* can never occur! *) + | `Epoch data -> Ok data) + | Eof -> Or_error.error_string "TLS state is end of file" + | Error _ -> Or_error.error_string "TLS state is error" + ;; +end diff --git a/async/io.mli b/async/io.mli new file mode 100644 index 00000000..acbfcac9 --- /dev/null +++ b/async/io.mli @@ -0,0 +1,6 @@ +open! Core + +module type Fd = Io_intf.Fd +module type S = Io_intf.S + +module Make (Fd : Fd) : S with module Fd := Fd diff --git a/async/io_intf.ml b/async/io_intf.ml new file mode 100644 index 00000000..44292555 --- /dev/null +++ b/async/io_intf.ml @@ -0,0 +1,60 @@ +open! Core +open! Async + +module type Fd = sig + type t + + val read : t -> Cstruct.t -> [ `Ok of int | `Eof ] Deferred.Or_error.t + val write_full : t -> Cstruct.t -> unit Deferred.Or_error.t +end + +module type S = sig + module Fd : Fd + + (** Abstract type of a session *) + type t + + (** {2 Constructors} *) + + (** [server_of_fd server fd] is [t], after server-side TLS + handshake of [fd] using [server] configuration. *) + val server_of_fd : Tls.Config.server -> Fd.t -> t Deferred.Or_error.t + + (** [client_of_fd client ~host fd] is [t], after client-side + TLS handshake of [fd] using [client] configuration and [host]. *) + val client_of_fd : Tls.Config.client -> ?host:string -> Fd.t -> t Deferred.Or_error.t + + (** {2 Common stream operations} *) + + (** [read t buffer] is [length], the number of bytes read into + [buffer]. *) + val read : t -> Cstruct.t -> int Deferred.Or_error.t + + (** [writev t buffers] writes the [buffers] to the session. *) + val writev : t -> Cstruct.t list -> unit Deferred.Or_error.t + + (** [close t] closes the TLS session by sending a close notify to the peer. *) + val close_tls : t -> unit Deferred.Or_error.t + + (** [reneg ~authenticator ~acceptable_cas ~cert ~drop t] renegotiates the + session, and blocks until the renegotiation finished. Optionally, a new + [authenticator] and [acceptable_cas] can be used. The own certificate can + be adjusted by [cert]. If [drop] is [true] (the default), + application data received before the renegotiation finished is dropped. *) + val reneg + : ?authenticator:X509.Authenticator.t + -> ?acceptable_cas:X509.Distinguished_name.t list + -> ?cert:Tls.Config.own_cert + -> ?drop:bool + -> t + -> unit Deferred.Or_error.t + + (** [key_update ~request t] updates the traffic key and requests a traffic key + update from the peer if [request] is provided and [true] (the default). + This is only supported in TLS 1.3. *) + val key_update : ?request:bool -> t -> unit Deferred.Or_error.t + + (** [epoch t] returns [epoch], which contains information of the + active session. *) + val epoch : t -> Tls.Core.epoch_data Or_error.t +end diff --git a/async/session.ml b/async/session.ml new file mode 100644 index 00000000..2d9065cb --- /dev/null +++ b/async/session.ml @@ -0,0 +1,27 @@ +open! Core +open! Async + +module Fd = struct + type t = Reader.t * Writer.t + + let read (reader, (_ : Writer.t)) buf = + Deferred.Or_error.try_with (fun () -> Async_cstruct.read reader buf) + ;; + + let write ((_ : Reader.t), writer) buf = + Deferred.Or_error.try_with (fun () -> + Async_cstruct.schedule_write writer buf; + Writer.flushed writer) + ;; + + let rec write_full fd buf = + let open Deferred.Or_error.Let_syntax in + match Cstruct.len buf with + | 0 -> return () + | len -> + let%bind () = write fd buf in + write_full fd (Cstruct.shift buf len) + ;; +end + +include Io.Make (Fd) diff --git a/async/session.mli b/async/session.mli new file mode 100644 index 00000000..88a17ac1 --- /dev/null +++ b/async/session.mli @@ -0,0 +1 @@ +include Io.S with type Fd.t = Async.Reader.t * Async.Writer.t diff --git a/async/tls_async.ml b/async/tls_async.ml new file mode 100644 index 00000000..e29743f5 --- /dev/null +++ b/async/tls_async.ml @@ -0,0 +1,141 @@ +open! Core +open! Async +module Session = Session + +let try_to_close t = + match%map Session.close_tls t with + | Ok () -> () + | Error tls_close_error -> Log.Global.error_s [%sexp (tls_close_error : Error.t)] +;; + +let pipe t = + let b_reader = Cstruct.create 0x8000 in + let rec f_reader writer = + match%bind Session.read t b_reader with + | Ok 0 -> + Pipe.close writer; + return () + | Ok len -> + let%bind () = Pipe.write writer (Cstruct.to_string (Cstruct.sub b_reader 0 len)) in + f_reader writer + | Error read_error -> + Log.Global.error_s [%sexp (read_error : Error.t)]; + Pipe.close writer; + return () + in + let rec f_writer reader = + let%bind pipe_read = Pipe.read reader in + match pipe_read with + | `Ok s -> + (match%bind Session.writev t [ Cstruct.of_string s ] with + | Ok () -> f_writer reader + | Error (_ : Error.t) -> try_to_close t) + | `Eof -> try_to_close t + in + Pipe.create_reader ~close_on_exception:false f_reader, Pipe.create_writer f_writer +;; + +let upgrade_connection tls_session ((_ : Reader.t), outer_writer) = + let pipe_r, pipe_w = pipe tls_session in + let%bind inner_reader = Reader.of_pipe (Info.of_string "tls_reader") pipe_r in + let%map inner_writer, `Closed_and_flushed_downstream inner_cafd = + Writer.of_pipe (Info.of_string "tls_writer") pipe_w + in + Writer.set_raise_when_consumer_leaves inner_writer false; + let outer_cafd = + (* Ordering is important here to ensure no data is lost during the session shutdown *) + let%bind () = Writer.close_finished inner_writer in + let%bind () = inner_cafd in + let%bind () = try_to_close tls_session in + Writer.flushed outer_writer + in + tls_session, inner_reader, inner_writer, `Tls_closed_and_flushed_downstream outer_cafd +;; + +let upgrade_server_reader_writer_to_tls config rw = + let open Deferred.Or_error.Let_syntax in + let%bind tls_session = Session.server_of_fd config rw in + upgrade_connection tls_session rw |> Deferred.ok +;; + +let upgrade_client_reader_writer_to_tls ?host config rw = + let open Deferred.Or_error.Let_syntax in + let%bind tls_session = Session.client_of_fd ?host config rw in + upgrade_connection tls_session rw |> Deferred.ok +;; + +let listen + ?buffer_age_limit + ?max_connections + ?max_accepts_per_batch + ?backlog + ?socket + ~on_handler_error + config + where_to_listen + handle_client + = + let tls_handler sock outer_reader outer_writer = + let%bind ( tls_session + , inner_reader + , inner_writer + , `Tls_closed_and_flushed_downstream inner_cafd ) + = + upgrade_server_reader_writer_to_tls config (outer_reader, outer_writer) + |> Deferred.Or_error.ok_exn + in + Monitor.protect + (fun () -> handle_client sock tls_session inner_reader inner_writer) + ~finally:(fun () -> + Deferred.all_unit + [ Reader.close inner_reader; Writer.close inner_writer; inner_cafd ]) + in + Tcp.Server.create + ?buffer_age_limit + ?max_connections + ?max_accepts_per_batch + ?backlog + ?socket + ~on_handler_error + where_to_listen + tls_handler +;; + +let connect + ?socket + ?buffer_age_limit + ?interrupt + ?reader_buffer_size + ?writer_buffer_size + ?timeout + config + where_to_connect + ~host + = + let open Deferred.Or_error.Let_syntax in + let%bind (_ : ([ `Active ], 'a) Socket.t), outer_reader, outer_writer = + Tcp.connect + ?socket + ?buffer_age_limit + ?interrupt + ?reader_buffer_size + ?writer_buffer_size + ?timeout + where_to_connect + |> Deferred.ok + in + let%bind ( tls_session + , inner_reader + , inner_writer + , `Tls_closed_and_flushed_downstream inner_cafd ) + = + upgrade_client_reader_writer_to_tls ?host config (outer_reader, outer_writer) + in + don't_wait_for + (let%bind.Deferred () = inner_cafd in + Deferred.all_unit [ Writer.close outer_writer; Reader.close outer_reader ]); + return (tls_session, inner_reader, inner_writer) +;; + +(* initialized RNG early to maximise available entropy. *) +let () = Mirage_crypto_rng_async.initialize (module Mirage_crypto_rng.Fortuna) diff --git a/async/tls_async.mli b/async/tls_async.mli new file mode 100644 index 00000000..568ec675 --- /dev/null +++ b/async/tls_async.mli @@ -0,0 +1,37 @@ +open! Core +open! Async + +(** Low-level API for working with TLS sessions. + Most applications should use the high-level API below *) +module Session = Session + +(** [listen] creates a [Tcp.Server.t] with the requested parameters, including those + specified in [Tls.Config.server]. The handler function exposes the low-level + [Session.t] to accommodate cases like interrogating a client certificate *) +val listen + : ?buffer_age_limit:Writer.buffer_age_limit + -> ?max_connections:int (** defaults to [10_000]. *) + -> ?max_accepts_per_batch:int (** defaults to [1]. *) + -> ?backlog:int (** defaults to [64]. *) + -> ?socket:([ `Unconnected ], ([< Socket.Address.t ] as 'address)) Socket.t + -> on_handler_error:[ `Call of 'address -> exn -> unit | `Ignore | `Raise ] + -> Tls.Config.server + -> ('address, 'listening_on) Tcp.Where_to_listen.t + -> ('address -> Session.t -> Reader.t -> Writer.t -> unit Deferred.t) + -> ('address, 'listening_on) Tcp.Server.t Deferred.t + +(** [connect] behaves similarly to [Tcp.connect], exposing a cleartext reader and writer. + Callers should ensure they close the [Writer.t] and wait for the [unit Deferred.t] + returned by [`Closed_and_flushed_downstream] to completely shut down the TLS connection + + [host] is used for peer name verification and should generally be provided. Passing + [None] will disable peer name verification unless [peer_name] was provided in the + [Tls.Config.client]. If both are present [host] overwrites [peer_name]. +*) +val connect + : ?socket:([ `Unconnected ], 'addr) Socket.t + -> (Tls.Config.client + -> 'addr Tcp.Where_to_connect.t + -> host:string option + -> (Session.t * Reader.t * Writer.t) Deferred.Or_error.t) + Tcp.with_connect_options diff --git a/tls-async.opam b/tls-async.opam new file mode 100644 index 00000000..de9af2b3 --- /dev/null +++ b/tls-async.opam @@ -0,0 +1,33 @@ +opam-version: "2.0" +homepage: "https://github.com/mirleft/ocaml-tls" +dev-repo: "git+https://github.com/mirleft/ocaml-tls.git" +bug-reports: "https://github.com/mirleft/ocaml-tls/issues" +doc: "https://mirleft.github.io/ocaml-tls/doc" +author: ["David Kaloper " "Hannes Mehnert " "Eric Ebinger " "Calascibetta Romain "] +maintainer: ["Hannes Mehnert " "David Kaloper "] +license: "BSD-2-Clause" + +build: [ + ["dune" "subst"] {dev} + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] + +depends: [ + "ocaml" {>= "4.08.0"} + "dune" {>= "1.0"} + "tls" {= version} + "x509" {>= "0.13.0"} + "ptime" {>= "0.8.1"} + "async" + "async_unix" + "core" + "cstruct-async" + "ppx_jane" + "mirage-crypto-rng-async" +] +tags: [ "org:mirage"] +synopsis: "Transport Layer Security purely in OCaml, Async layer" +description: """ +Tls-async provides Async-friendly tls bindings +"""