Skip to content

Commit 18c6b52

Browse files
Add an unix implementation of TLS (#518)
* Add an unix implementation of TLS using Unix.file_descr * Take tls-miou as new base for tls.unix --------- Co-authored-by: Romain Calascibetta <romain.calascibetta@gmail.com>
1 parent 92bd5f7 commit 18c6b52

File tree

5 files changed

+443
-0
lines changed

5 files changed

+443
-0
lines changed

miou/tls_miou_unix.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
(* NOTE: the unix/tls_unix.ml is mostly copied from here, so any change should be synchronized. *)
2+
13
let src = Logs.Src.create "tls-miou"
24

35
module Log = (val Logs.src_log src : Logs.LOG)

tls.opam

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ depends: [
2929
"ipaddr"
3030
"ohex" {>= "0.2.0"}
3131
"digestif" {>= "1.2.0"}
32+
"ptime" {>= "1.2.0"}
3233
"alcotest" {with-test}
3334
"cmdliner" {with-test & >= "1.3.0"}
3435
]

unix/dune

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(library
2+
(name tls_unix)
3+
(public_name tls.unix)
4+
(wrapped false)
5+
(libraries tls unix ptime.clock.os mirage-crypto-rng.unix))

unix/tls_unix.ml

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
(* NOTE: mostly copied from miou/tls_miou_unix.ml, so any change should be synchronized. *)
2+
3+
let src = Logs.Src.create "tls-unix"
4+
5+
module Log = (val Logs.src_log src : Logs.LOG)
6+
7+
external reraise : exn -> 'a = "%reraise"
8+
9+
let ( $ ) f x = f x
10+
11+
exception Tls_alert of Tls.Packet.alert_type
12+
exception Tls_failure of Tls.Engine.failure
13+
exception Closed_by_peer
14+
15+
let () =
16+
Printexc.register_printer @@ function
17+
| Closed_by_peer -> Some "Connection closed by peer"
18+
| Tls_alert alert -> Some (Tls.Packet.alert_type_to_string alert)
19+
| Tls_failure failure -> Some (Tls.Engine.string_of_failure failure)
20+
| _ -> None
21+
22+
type state =
23+
[ `Active of Tls.Engine.state
24+
| `Read_closed of Tls.Engine.state
25+
| `Write_closed of Tls.Engine.state
26+
| `Closed
27+
| `Error of exn ]
28+
29+
type t = {
30+
role : [ `Server | `Client ];
31+
fd : Unix.file_descr;
32+
mutable state : state;
33+
mutable linger : string option;
34+
read_buffer_size : int;
35+
buf : bytes;
36+
mutable rd_closed : bool;
37+
}
38+
39+
let file_descr { fd; _ } = fd
40+
41+
let half_close state mode =
42+
match (state, mode) with
43+
| `Active tls, `read -> `Read_closed tls
44+
| `Active tls, `write -> `Write_closed tls
45+
| `Active _, `read_write -> `Closed
46+
| `Read_closed tls, `read -> `Read_closed tls
47+
| `Read_closed _, (`write | `read_write) -> `Closed
48+
| `Write_closed tls, `write -> `Write_closed tls
49+
| `Write_closed _, (`read | `read_write) -> `Closed
50+
| ((`Closed | `Error _) as e), (`read | `write | `read_write) -> e
51+
52+
let inject_state tls = function
53+
| `Active _ -> `Active tls
54+
| `Read_closed _ -> `Read_closed tls
55+
| `Write_closed _ -> `Write_closed tls
56+
| (`Closed | `Error _) as e -> e
57+
58+
let tls_alert a = Tls_alert a
59+
let tls_fail f = Tls_failure f
60+
let inhibit fn v = try fn v with _ -> ()
61+
62+
let rec unix_write fd str off len =
63+
let written = Unix.write_substring fd str off len in
64+
if not (Int.equal written len) then
65+
unix_write fd str (off + written) (len - written)
66+
67+
let write flow str =
68+
Log.debug (fun m -> m "try to write %d byte(s)" (String.length str));
69+
try unix_write flow.fd str 0 (String.length str) with
70+
| Unix.Unix_error ((Unix.EPIPE | Unix.ECONNRESET), _, _) ->
71+
flow.state <- half_close flow.state `write;
72+
raise Closed_by_peer
73+
| Unix.Unix_error (_, _, _) as exn ->
74+
flow.state <- `Error exn;
75+
reraise exn
76+
77+
let handle flow tls str =
78+
match Tls.Engine.handle_tls tls str with
79+
| Ok (state, eof, `Response resp, `Data data) ->
80+
Log.debug (fun m -> m "We handled %d byte(s)" (String.length str));
81+
let state = inject_state state flow.state in
82+
let state = Option.(value ~default:state (map (fun `Eof -> half_close state `read) eof)) in
83+
flow.state <- state;
84+
let to_close = flow.state = `Closed in
85+
Option.iter (inhibit $ write flow) resp;
86+
(* NOTE(dinosaure): [write flow] can set [flow.state]. So we must
87+
check if the actual [flow.state] or the [flow.state] after [write flow]
88+
want to close the underlying file-descriptor. *)
89+
if to_close || flow.state = `Closed then Unix.close flow.fd;
90+
data
91+
| Error (fail, `Response resp) ->
92+
let exn = match fail with
93+
| `Alert a -> tls_alert a | f -> tls_fail f in
94+
flow.state <- `Error exn;
95+
let _ = inhibit (write flow) resp in
96+
raise exn
97+
98+
let read flow =
99+
match Unix.read flow.fd flow.buf 0 (Bytes.length flow.buf) with
100+
| 0 -> Ok String.empty
101+
| len -> Ok (Bytes.sub_string flow.buf 0 len)
102+
| exception Unix.Unix_error (Unix.ECONNRESET, _, _) -> Ok String.empty
103+
| exception exn -> Error exn
104+
105+
let not_errored = function `Error _ -> false | _ -> true
106+
107+
let garbage flow = match flow.linger with
108+
| Some "" | None -> false
109+
| _ -> true
110+
111+
let read_react flow =
112+
match flow.state with
113+
| `Error exn -> raise exn
114+
| `Read_closed _ | `Closed when garbage flow ->
115+
(* XXX(dinosaure): [`Closed] can appear "at the same time" than some
116+
application-data. In that case, we stored them into [t.linger]. Depending
117+
on who closed the connection, [read_react] gives this /garbage/ in any
118+
situation (even if the user closed the connection).
119+
120+
An extra layer with [read] below check if [`Read_closed]/[`Close] comes
121+
from the network (the peer closed the connection) or the user. In the
122+
first case, we must give pending application-data. In the second case,
123+
we must return [0] (or raise [End_of_file]). *)
124+
let mbuf = flow.linger in
125+
flow.linger <- None;
126+
mbuf
127+
| `Read_closed _ | `Closed ->
128+
(* XXX(dinosaure): the goal of [read_react] is to read some encrypted bytes
129+
and try to decrypt them with [handle]. If the linger is empty, this means
130+
that we're trying to get more data (to decrypt) when we can't get any
131+
more. From this point of view, it's an error that needs to be notified.
132+
However, this error can be interpreted in 2 ways:
133+
- we want to have more data decrypted. In this case, this error is
134+
expected and may result in the user being told that there is nothing
135+
left to read (for example, returning 0).
136+
- we attempt a handshake. In this case, we are dealing with an unexpected
137+
error. *)
138+
raise End_of_file
139+
| `Active _ | `Write_closed _ ->
140+
Log.debug (fun m -> m "read something from the TLS session");
141+
match read flow with
142+
| Error exn ->
143+
if not_errored flow.state then flow.state <- `Error exn;
144+
raise exn
145+
| Ok "" ->
146+
(* XXX(dinosaure): see [`Read_closed _ | `Closed] case. *)
147+
raise End_of_file
148+
| Ok str ->
149+
Log.debug (fun m -> m "got %d byte(s)" (String.length str));
150+
match flow.state with
151+
| `Active tls | `Read_closed tls | `Write_closed tls -> handle flow tls str
152+
| `Closed -> raise End_of_file
153+
| `Error exn -> raise exn
154+
[@@ocamlformat "disable"]
155+
156+
let rec read_in flow ?(off= 0) ?len buf =
157+
let len = Option.value ~default:(Bytes.length buf - off) len in
158+
let write_in res =
159+
let rlen = String.length res in
160+
let mlen = min len rlen in
161+
Bytes.blit_string res 0 buf off mlen;
162+
let linger = if mlen < rlen
163+
then Some (String.sub res mlen (rlen - mlen))
164+
else None in
165+
flow.linger <- linger; mlen
166+
in
167+
match flow.linger with
168+
| Some res -> write_in res
169+
| None -> (
170+
match read_react flow with
171+
| None -> read_in ~off ~len flow buf
172+
| Some res -> write_in res)
173+
174+
let writev flow bufs =
175+
match flow.state with
176+
| `Closed | `Write_closed _ -> raise Closed_by_peer
177+
| `Error exn -> reraise exn
178+
| `Active tls | `Read_closed tls -> (
179+
match Tls.Engine.send_application_data tls bufs with
180+
| Some (tls, answer) ->
181+
flow.state <- inject_state tls flow.state;
182+
write flow answer
183+
| None -> assert false)
184+
185+
let rec drain_handshake flow =
186+
let push_linger flow mcs =
187+
match (mcs, flow.linger) with
188+
| None, _ -> ()
189+
| scs, None -> flow.linger <- scs
190+
| Some cs, Some l -> flow.linger <- Some (l ^ cs)
191+
in
192+
match flow.state with
193+
| `Active tls when not (Tls.Engine.handshake_in_progress tls) -> flow
194+
| (`Read_closed _ | `Closed) when garbage flow -> flow
195+
| _ ->
196+
Log.debug (fun m -> m "start to read something from the TLS session");
197+
let mcs = read_react flow in
198+
push_linger flow mcs;
199+
drain_handshake flow
200+
201+
let close flow =
202+
match flow.state with
203+
| `Active tls | `Read_closed tls ->
204+
let tls, str = Tls.Engine.send_close_notify tls in
205+
flow.rd_closed <- true;
206+
flow.state <- inject_state tls flow.state;
207+
flow.state <- `Closed;
208+
inhibit (write flow) str;
209+
Unix.close flow.fd
210+
| `Write_closed _ ->
211+
flow.rd_closed <- true;
212+
flow.state <- `Closed;
213+
Unix.close flow.fd
214+
| `Closed -> flow.rd_closed <- true
215+
| `Error _ ->
216+
flow.rd_closed <- true;
217+
Unix.close flow.fd
218+
219+
let closed_by_user flow = function
220+
| `read | `read_write -> flow.rd_closed <- true
221+
| `write -> ()
222+
223+
let shutdown flow mode =
224+
closed_by_user flow mode;
225+
match (flow.state, mode) with
226+
| `Active tls, `read ->
227+
Log.debug (fun m -> m "shutdown `read");
228+
flow.state <- inject_state tls (half_close flow.state mode)
229+
| (`Active tls | `Read_closed tls), (`write | `read_write) ->
230+
let tls, str = Tls.Engine.send_close_notify tls in
231+
flow.state <- inject_state tls (half_close flow.state mode);
232+
(* NOTE(dinosaure): [write flow] can set [flow.state]. So we must
233+
check if the actual [flow.state] or the [flow.state] after [write flow]
234+
want to close the underlying file-descriptor. *)
235+
let to_close = flow.state = `Closed in
236+
inhibit (write flow) str;
237+
if to_close || flow.state = `Closed then Unix.close flow.fd
238+
| `Write_closed tls, (`read | `read_write) ->
239+
flow.state <- inject_state tls (half_close flow.state mode);
240+
if flow.state = `Closed then Unix.close flow.fd
241+
| `Error _, _ -> Unix.close flow.fd
242+
| `Read_closed _, `read -> ()
243+
| `Write_closed _, `write -> ()
244+
| `Closed, _ -> ()
245+
246+
let client_of_fd conf ?(read_buffer_size = 0x1000) ?host fd =
247+
let conf' =
248+
match host with None -> conf | Some host -> Tls.Config.peer conf host
249+
in
250+
let tls, init = Tls.Engine.client conf' in
251+
let tls_flow =
252+
{
253+
role = `Client;
254+
fd;
255+
state = `Active tls;
256+
linger = None;
257+
read_buffer_size;
258+
buf = Bytes.make read_buffer_size '\000';
259+
rd_closed = false;
260+
}
261+
in
262+
write tls_flow init;
263+
drain_handshake tls_flow
264+
265+
let server_of_fd conf ?(read_buffer_size = 0x1000) fd =
266+
let tls = Tls.Engine.server conf in
267+
let tls_flow =
268+
{
269+
role = `Server;
270+
fd;
271+
state = `Active tls;
272+
linger = None;
273+
read_buffer_size;
274+
buf = Bytes.make read_buffer_size '\000';
275+
rd_closed = false;
276+
}
277+
in
278+
drain_handshake tls_flow
279+
280+
let write flow ?(off = 0) ?len str =
281+
let len = Option.value ~default:(String.length str - off) len in
282+
if off < 0 || len < 0 || off > String.length str - len
283+
then invalid_arg "Tls_unix.write";
284+
if len > 0 then writev flow [ String.sub str off len ]
285+
286+
let read t ?(off= 0) ?len buf =
287+
let len = Option.value ~default:(Bytes.length buf - off) len in
288+
if off < 0 || len < 0 || off > Bytes.length buf - len
289+
then invalid_arg "Tls_unix.read";
290+
if t.rd_closed then 0
291+
else try read_in t ~off ~len buf with End_of_file -> 0
292+
293+
let rec really_read_go t off len buf =
294+
let len' = read t buf ~off ~len in
295+
if len' == 0 then raise End_of_file
296+
else if len - len' > 0
297+
then really_read_go t (off + len') (len - len') buf
298+
299+
let really_read t ?(off= 0) ?len buf =
300+
let len = Option.value ~default:(Bytes.length buf - off) len in
301+
if off < 0 || len < 0 || off > Bytes.length buf - len
302+
then invalid_arg "Tls_unix.really_read";
303+
if len > 0 then really_read_go t off len buf
304+
305+
let resolve host service =
306+
let tcp = Unix.getprotobyname "tcp" in
307+
match Unix.getaddrinfo host service [ AI_PROTOCOL tcp.p_proto ] with
308+
| [] -> Fmt.invalid_arg "No address for %s:%s" host service
309+
| ai :: _ -> ai.ai_addr
310+
311+
let connect authenticator (v, port) =
312+
let conf =
313+
match Tls.Config.client ~authenticator () with
314+
| Ok config -> config
315+
| Error `Msg msg -> Fmt.invalid_arg "Configuration failure: %s" msg
316+
in
317+
let addr = resolve v (string_of_int port) in
318+
let fd =
319+
match addr with
320+
| Unix.ADDR_UNIX _ -> invalid_arg "Tls_unix.connect: Invalid UNIX socket"
321+
| Unix.ADDR_INET (inet_addr, _) ->
322+
if Unix.is_inet6_addr inet_addr then
323+
Unix.socket Unix.PF_INET6 Unix.SOCK_STREAM 0
324+
else
325+
Unix.socket Unix.PF_INET Unix.SOCK_STREAM 0
326+
in
327+
let host = Result.to_option Domain_name.(Result.bind (of_string v) host) in
328+
match Unix.connect fd addr with
329+
| () -> client_of_fd conf ?host fd
330+
| exception exn ->
331+
Unix.close fd;
332+
raise exn
333+
334+
let epoch flow = match flow.state with
335+
| `Active tls | `Read_closed tls | `Write_closed tls ->
336+
( match Tls.Engine.epoch tls with
337+
| Error () -> assert false
338+
| Ok data -> Some data )
339+
| _ -> None

0 commit comments

Comments
 (0)