Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an unix implementation of TLS using Unix.file_descr #443

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions unix/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
(library
(name tls_unix)
(public_name tls.unix)
(wrapped false)
(libraries tls unix ptime.clock.os mirage-crypto-rng.unix))
206 changes: 206 additions & 0 deletions unix/tls_unix.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
type error =
| Alert of Tls.Packet.alert_type
| Failure of Tls.Engine.failure
| Unix_error of Unix.error * string * string
| Closed

let pp_error ppf = function
| Alert alert ->
Fmt.pf ppf "TLS alert: %s" (Tls.Packet.alert_type_to_string alert)
| Failure failure ->
Fmt.pf ppf "TLS failure: %s" (Tls.Engine.string_of_failure failure)
| Unix_error (err, f, arg) ->
Fmt.pf ppf "%s(%s): %s" f arg (Unix.error_message err)
| Closed -> Fmt.pf ppf "Connection closed by peer"

(* syscalls *)

let rec fully_write socket str off len =
if len > 0
then
let len' = Unix.write socket (Bytes.unsafe_of_string str) off len in
fully_write socket str (off + len') (len - len')

let fully_write socket ({ Cstruct.len; _ } as cs) =
try
fully_write socket (Cstruct.to_string cs) 0 len ;
Ok ()
with Unix.Unix_error (err, f, arg) -> Error (Unix_error (err, f, arg))

let read socket =
let buf = Bytes.create 0x1000 in
match Unix.read socket buf 0 (Bytes.length buf) with
| 0 -> Ok `Eof
| len -> Ok (`Data (Cstruct.of_bytes ~off:0 ~len buf))
| exception Unix.Unix_error (err, f, arg) ->
Error (Unix_error (err, f, arg))

type flow = {
role : [ `Server | `Client ] ;
flow : Unix.file_descr ;
mutable state : [ `Active of Tls.Engine.state
| `Eof
| `Error of error ] ;
mutable linger : Cstruct.t list ;
}

let tls_alert a = `Error (Alert a)
let tls_fail f = `Error (Failure f)

let list_of_option = function None -> [] | Some x -> [x]

let lift_read_result = function
| Ok (`Data _ | `Eof as x) -> x
| Error e -> `Error e

let lift_write_result = function
| Ok () -> `Ok ()
| Error e -> `Error e

let check_write flow f_res =
let res = lift_write_result f_res in
( match flow.state, res with
| `Active _, (`Eof | `Error _ as e) ->
flow.state <- e ; Unix.close flow.flow
| _ -> ()) ;
match f_res with
| Ok () -> Ok ()
| Error e -> Error e

let read_react flow =

let handle tls buf =
match Tls.Engine.handle_tls tls buf with
| Ok (res, `Response resp, `Data data) ->
flow.state <- ( match res with
| `Ok tls -> `Active tls
| `Eof -> `Eof
| `Alert alert -> tls_alert alert );
ignore ( match resp with
| None -> Ok ()
| Some buf -> fully_write flow.flow buf |> check_write flow ) ;
ignore ( match res with
| `Ok _ -> ()
| _ -> Unix.close flow.flow ) ;
`Ok data
| Error (fail, `Response resp) ->
let reason = tls_fail fail in
flow.state <- reason ;
fully_write flow.flow resp |> fun _ -> Unix.close flow.flow |> fun () -> reason
in
match flow.state with
| `Eof | `Error _ as e -> e
| `Active _ ->
read flow.flow |> lift_read_result |>
function
| `Eof | `Error _ as e -> flow.state <- e ; e
| `Data buf -> match flow.state with
| `Active tls -> handle tls buf
| `Eof | `Error _ as e -> e

let rec read flow =
match flow.linger with
| [] ->
( read_react flow |> function
| `Ok None -> read flow
| `Ok (Some buf) -> Ok (`Data buf)
| `Eof -> Ok `Eof
| `Error e -> Error e )
| bufs ->
flow.linger <- [] ;
Ok (`Data (Cstruct.concat @@ List.rev bufs))

let writev flow bufs =
match flow.state with
| `Eof -> Error Closed
| `Error e -> Error e
| `Active tls ->
match Tls.Engine.send_application_data tls bufs with
| Some (tls, answer) ->
flow.state <- `Active tls ;
fully_write flow.flow answer |> check_write flow
| None ->
(* "Impossible" due to handshake draining. *)
assert false

let write flow buf = writev flow [buf]

(*
* 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 flow =
match flow.state with
| `Active tls when not (Tls.Engine.handshake_in_progress tls) ->
Ok flow
| _ ->
(* read_react re-throws *)
read_react flow |> function
| `Ok mbuf ->
flow.linger <- list_of_option mbuf @ flow.linger ;
drain_handshake flow
| `Error e -> Error e
| `Eof -> Error Closed

let reneg ?authenticator ?acceptable_cas ?cert ?(drop = true) flow =
match flow.state with
| `Eof -> Error Closed
| `Error e -> Error e
| `Active tls ->
match Tls.Engine.reneg ?authenticator ?acceptable_cas ?cert tls with
| None ->
(* XXX make this impossible to reach *)
invalid_arg "Renegotiation already in progress"
| Some (tls', buf) ->
if drop then flow.linger <- [] ;
flow.state <- `Active tls' ;
fully_write flow.flow buf |> fun _ ->
drain_handshake flow |> function
| Ok _ -> Ok ()
| Error _ as e -> e

let key_update ?request flow =
match flow.state with
| `Eof -> Error Closed
| `Error e -> Error e
| `Active tls ->
match Tls.Engine.key_update ?request tls with
| Error _ -> invalid_arg "Key update failed"
| Ok (tls', buf) ->
flow.state <- `Active tls' ;
fully_write flow.flow buf |> check_write flow

let close flow =
match flow.state with
| `Active tls ->
flow.state <- `Eof ;
let (_, buf) = Tls.Engine.send_close_notify tls in
fully_write flow.flow buf |> fun _ -> Unix.close flow.flow
| _ -> ()

let client_of_flow conf ?host flow =
let conf' = match host with
| None -> conf
| Some host -> Tls.Config.peer conf host
in
let (tls, init) = Tls.Engine.client conf' in
let tls_flow = {
role = `Client ;
flow = flow ;
state = `Active tls ;
linger = [] ;
} in
fully_write flow init |> fun _ -> drain_handshake tls_flow

let server_of_flow conf flow =
let tls_flow = {
role = `Server ;
flow = flow ;
state = `Active (Tls.Engine.server conf) ;
linger = [] ;
} in
drain_handshake tls_flow

65 changes: 65 additions & 0 deletions unix/tls_unix.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
(** Effectful operations using [Unix] for pure TLS. *)

(** possible errors: incoming alert, processing failure, or a
problem in the underlying [Unix] flow. *)
type error =
| Alert of Tls.Packet.alert_type
| Failure of Tls.Engine.failure
| Unix_error of Unix.error * string * string
| Closed

val pp_error : Format.formatter -> error -> unit
(** Pretty-printer of {!val:error}. *)

type flow
(** The type of flows. *)

val read : flow -> ([ `Data of Cstruct.t | `Eof ], error) result
(** [read flow] blocks until some data is available and returns a
fresh buffer containing it.

If the remote endpoint calls [close] then calls to [read] will
keep returning data until all the {i in-flight} data has been read.
[read flow] will return [`Eof] when the remote endpoint has
called [close] and when there is no more (i in-flight} data.
*)

val write : flow -> Cstruct.t -> (unit, error) result
(** [write flow buffer] writes a buffer to the TLS flow. There is no
indication when the buffer has actually been read and, therefore,
it must not be reused. The result [Ok ()] indicates success,
[Error Closed] indicates that the connection is now closed and
therefore the data could not be written. Other errors are possible.
*)

val writev : flow -> Cstruct.t list -> (unit, error) result
(** [writev flow bufs] is a successive call of {!val:write} with
given [bufs]. *)

val close : flow -> unit
(** [close flow] sends a close notification to the peer and close the
underlying [Unix] socket. *)

(** [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 -> flow -> (unit, error) result

(** [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 -> flow -> (unit, error) result

(** [client_of_flow client ~host socket] upgrades the existing connection
to TLS using [client] configuration, using [host] as peer name. *)
val client_of_flow : Tls.Config.client -> ?host:[ `host ] Domain_name.t ->
Unix.file_descr -> (flow, error) result

(** [server_of_flow server flow] upgrades the flow to a TLS
connection using the [server] configuration. *)
val server_of_flow : Tls.Config.server -> Unix.file_descr ->
(flow, error) result