298 lines
10 KiB
OCaml
298 lines
10 KiB
OCaml
|
|
(* Fuzz testing for tls-eio.
|
||
|
|
|
||
|
|
This code picks two random strings, one for the client to send and one for
|
||
|
|
the server. It then starts a send and receive fiber for each end.
|
||
|
|
|
||
|
|
A dispatcher fiber then sends commands to these worker fibers
|
||
|
|
(see [action] for the possible actions).
|
||
|
|
|
||
|
|
This is intended to check for bugs in the Eio wrapper (rather than in Tls itself).
|
||
|
|
At the moment, it's just checking that tls-eio works when used correctly.
|
||
|
|
Each endpoint overlaps reads with writes (but not reads with other reads or
|
||
|
|
writes with other writes).
|
||
|
|
|
||
|
|
Some possible future improvements:
|
||
|
|
|
||
|
|
- It currently only checks the basic read/write/close operations.
|
||
|
|
It should be extended to check [reneg], etc too.
|
||
|
|
|
||
|
|
- Currently, cancelling a read operation marks the Tls flow as broken.
|
||
|
|
We should allow resuming after a cancelled read, and test that here.
|
||
|
|
|
||
|
|
- We should try injecting faults and make sure they're handled sensibly.
|
||
|
|
|
||
|
|
- It would be good to get coverage reports for these tests.
|
||
|
|
However, this requires changes to crowbar:
|
||
|
|
https://github.com/stedolan/crowbar/issues/4#issuecomment-1310277551
|
||
|
|
(a patched version reported 54% coverage of Tls_eio.ml) *)
|
||
|
|
|
||
|
|
open Eio.Std
|
||
|
|
|
||
|
|
let src = Logs.Src.create "fuzz" ~doc:"Fuzz tests"
|
||
|
|
module Log = (val Logs.src_log src : Logs.LOG)
|
||
|
|
|
||
|
|
module W = Eio.Buf_write
|
||
|
|
|
||
|
|
type transmit_amount = Mock_socket.transmit_amount
|
||
|
|
|
||
|
|
type op =
|
||
|
|
| Send of int (* The application sends some bytes to Tls *)
|
||
|
|
| Transmit of transmit_amount (* The network sends some types to the peer *)
|
||
|
|
| Recv (* The application tries to read some data *)
|
||
|
|
| Shutdown_send (* The application shuts down the sending side *)
|
||
|
|
|
||
|
|
let label name gen =
|
||
|
|
Crowbar.with_printer Fmt.(const string name) gen
|
||
|
|
|
||
|
|
let op =
|
||
|
|
Crowbar.choose @@ [
|
||
|
|
Crowbar.(map [range 4096]) (fun n -> Send n);
|
||
|
|
Crowbar.(map [range ~min:1 4096]) (fun n -> Transmit (`Bytes n));
|
||
|
|
label "recv" @@ Crowbar.const Recv;
|
||
|
|
label "shutdown-send" @@ Crowbar.const Shutdown_send;
|
||
|
|
]
|
||
|
|
|
||
|
|
type dir = To_client | To_server
|
||
|
|
|
||
|
|
let pp_dir f = function
|
||
|
|
| To_server -> Fmt.string f "client-to-server"
|
||
|
|
| To_client -> Fmt.string f "server-to-client"
|
||
|
|
|
||
|
|
let dir =
|
||
|
|
Crowbar.choose [
|
||
|
|
label "server-to-client" @@ Crowbar.const To_client;
|
||
|
|
label "client-to-server" @@ Crowbar.const To_server;
|
||
|
|
]
|
||
|
|
|
||
|
|
(* A test case is a random sequence of [action]s, followed by party shutting
|
||
|
|
down the sending side of the connection (if it hasn't already done so) and
|
||
|
|
the network draining any queued traffic.
|
||
|
|
|
||
|
|
Once all fibers have finished, we check that what was sent matches the data
|
||
|
|
that has been received. *)
|
||
|
|
|
||
|
|
let action =
|
||
|
|
Crowbar.option (Crowbar.pair dir op) (* None means yield *)
|
||
|
|
|
||
|
|
(* A [Path] is one direction (either server-to-client or client-to-server).
|
||
|
|
The two paths can be tested mostly independently (except for shutdown at the moment). *)
|
||
|
|
module Path : sig
|
||
|
|
type t
|
||
|
|
|
||
|
|
val create :
|
||
|
|
sender:(Tls_eio.t, exn) result Promise.t ->
|
||
|
|
receiver:(Tls_eio.t, exn) result Promise.t ->
|
||
|
|
transmit:(transmit_amount -> unit) ->
|
||
|
|
dir -> string -> t
|
||
|
|
(** Create a test driver for one direction, from [sender] to [receiver].
|
||
|
|
[transmit n] causes [n] bytes to be transferred over the mock network. *)
|
||
|
|
|
||
|
|
val close : t -> unit
|
||
|
|
(** [close t] causes the sender to close the socket for sending.
|
||
|
|
Futher send operations will be ignored. *)
|
||
|
|
|
||
|
|
val run : t -> unit
|
||
|
|
(** Run the send and receive fibers. Returns once the receiver has read EOF. *)
|
||
|
|
|
||
|
|
val enqueue : t -> op -> unit
|
||
|
|
(** Send a command to the send or receive fiber (depending on [op]). *)
|
||
|
|
end = struct
|
||
|
|
type t = {
|
||
|
|
dir : dir;
|
||
|
|
message : string; (* The complete message to be transmitted over this path. *)
|
||
|
|
(* We need to construct [t] before the handshake is done, so these are promises: *)
|
||
|
|
sender : Tls_eio.t Promise.or_exn;
|
||
|
|
receiver : Tls_eio.t Promise.or_exn;
|
||
|
|
mutable sent : int; (* Bytes of [message] sent so far *)
|
||
|
|
mutable recv : int; (* Bytes of [message] received so far *)
|
||
|
|
send_commands : [`Send of int | `Exit] Eio.Stream.t; (* Commands for the sending fiber *)
|
||
|
|
recv_commands : [`Recv | `Drain] Eio.Stream.t; (* Commands for the receiving fiber *)
|
||
|
|
transmit : transmit_amount -> unit;
|
||
|
|
}
|
||
|
|
|
||
|
|
let pp_dir f t =
|
||
|
|
pp_dir f t.dir
|
||
|
|
|
||
|
|
let create ~sender ~receiver ~transmit dir message =
|
||
|
|
let send_commands = Eio.Stream.create max_int in
|
||
|
|
let recv_commands = Eio.Stream.create max_int in
|
||
|
|
{ dir; message; sender; receiver; sent = 0; recv = 0;
|
||
|
|
send_commands; recv_commands; transmit }
|
||
|
|
|
||
|
|
let shutdown t =
|
||
|
|
Eio.Stream.add t.send_commands `Exit
|
||
|
|
|
||
|
|
let close t =
|
||
|
|
shutdown t; (* Sender stops sending *)
|
||
|
|
t.transmit `Drain; (* Network transmits everything *)
|
||
|
|
Eio.Stream.add t.recv_commands `Drain (* Receiver reads everything *)
|
||
|
|
|
||
|
|
let run_send_thread t =
|
||
|
|
let sender = Promise.await_exn t.sender in
|
||
|
|
Logs.info (fun f -> f "%a: sender ready" pp_dir t);
|
||
|
|
let rec aux () =
|
||
|
|
match Eio.Stream.take t.send_commands with
|
||
|
|
| `Exit ->
|
||
|
|
Log.info (fun f -> f "%a: shutdown send (Tls level)" pp_dir t);
|
||
|
|
Eio.Flow.shutdown sender `Send
|
||
|
|
| `Send len ->
|
||
|
|
let available = String.length t.message - t.sent in
|
||
|
|
let len = min len available in
|
||
|
|
if len > 0 then (
|
||
|
|
let msg = Cstruct.of_string ~off:t.sent ~len t.message in
|
||
|
|
t.sent <- t.sent + len;
|
||
|
|
Log.info (fun f -> f "%a: sending %S" pp_dir t (Cstruct.to_string msg));
|
||
|
|
Eio.Flow.write sender [msg];
|
||
|
|
);
|
||
|
|
aux ()
|
||
|
|
in
|
||
|
|
aux()
|
||
|
|
|
||
|
|
let run_recv_thread t =
|
||
|
|
let recv = Promise.await_exn t.receiver in
|
||
|
|
Logs.info (fun f -> f "%a: receiver ready" pp_dir t);
|
||
|
|
try
|
||
|
|
let drain = ref false in
|
||
|
|
while true do
|
||
|
|
if !drain = false then (
|
||
|
|
begin match Eio.Stream.take t.recv_commands with
|
||
|
|
| `Recv -> ()
|
||
|
|
| `Drain -> drain := true
|
||
|
|
end
|
||
|
|
);
|
||
|
|
let buf = Cstruct.create 4096 in
|
||
|
|
let got = Eio.Flow.single_read recv buf in
|
||
|
|
let received = Cstruct.to_string buf ~len:got in
|
||
|
|
Log.info (fun f -> f "%a: received %S" pp_dir t received);
|
||
|
|
let expected = String.sub t.message t.recv got in
|
||
|
|
if received <> expected then
|
||
|
|
Fmt.failwith "%a: excepted %S but got %S!" pp_dir t expected received;
|
||
|
|
t.recv <- t.recv + got
|
||
|
|
done
|
||
|
|
with End_of_file ->
|
||
|
|
if t.recv <> t.sent then (
|
||
|
|
Fmt.failwith "%a: Sender sent %d bytes, but receiver got EOF after reading only %d"
|
||
|
|
pp_dir t
|
||
|
|
t.sent
|
||
|
|
t.recv
|
||
|
|
);
|
||
|
|
Log.info (fun f -> f "%a: recv thread done (got EOF)" pp_dir t)
|
||
|
|
|
||
|
|
let run t =
|
||
|
|
Fiber.both
|
||
|
|
(fun () -> run_send_thread t)
|
||
|
|
(fun () -> run_recv_thread t)
|
||
|
|
|
||
|
|
let pp_amount f = function
|
||
|
|
| `Bytes n -> Fmt.pf f "%d bytes" n
|
||
|
|
| `Drain -> Fmt.string f "all bytes"
|
||
|
|
|
||
|
|
let enqueue t = function
|
||
|
|
| Send i->
|
||
|
|
Log.info (fun f -> f "%a: enqueue send %d bytes of plaintext" pp_dir t i);
|
||
|
|
Eio.Stream.add t.send_commands @@ `Send i;
|
||
|
|
| Recv ->
|
||
|
|
Log.info (fun f -> f "%a: enqueue read from Tls" pp_dir t);
|
||
|
|
Eio.Stream.add t.recv_commands @@ `Recv;
|
||
|
|
| Transmit i ->
|
||
|
|
Log.info (fun f -> f "%a: enqueue transmit %a over network" pp_dir t pp_amount i);
|
||
|
|
t.transmit i
|
||
|
|
| Shutdown_send ->
|
||
|
|
Log.info (fun f -> f "%a: enqueue shutdown send" pp_dir t);
|
||
|
|
shutdown t
|
||
|
|
end
|
||
|
|
|
||
|
|
module Config : sig
|
||
|
|
val client : Tls.Config.client
|
||
|
|
val server : Tls.Config.server
|
||
|
|
end = struct
|
||
|
|
let null_auth ?ip:_ ~host:_ _ = Ok None
|
||
|
|
|
||
|
|
let client =
|
||
|
|
Result.get_ok (Tls.Config.client ~authenticator:null_auth ())
|
||
|
|
|
||
|
|
let read_file path =
|
||
|
|
let ch = open_in_bin path in
|
||
|
|
let len = in_channel_length ch in
|
||
|
|
let data = really_input_string ch len in
|
||
|
|
close_in ch;
|
||
|
|
data
|
||
|
|
|
||
|
|
let server =
|
||
|
|
let certs = Result.get_ok (X509.Certificate.decode_pem_multiple (read_file "server.pem")) in
|
||
|
|
let pk = Result.get_ok (X509.Private_key.decode_pem (read_file "server.key")) in
|
||
|
|
let certificates = `Single (certs, pk) in
|
||
|
|
Result.get_ok Tls.Config.(server ~version:(`TLS_1_0, `TLS_1_3) ~certificates ~ciphers:Ciphers.supported ())
|
||
|
|
end
|
||
|
|
|
||
|
|
let dispatch_commands ~to_server ~to_client actions =
|
||
|
|
let rec aux = function
|
||
|
|
| [] ->
|
||
|
|
Log.info (fun f -> f "dispatch_commands: done");
|
||
|
|
Path.close to_client;
|
||
|
|
Path.close to_server
|
||
|
|
| None :: xs ->
|
||
|
|
Fiber.yield (); aux xs
|
||
|
|
| Some (dir, op) :: xs ->
|
||
|
|
let path =
|
||
|
|
match dir with
|
||
|
|
| To_server-> to_server
|
||
|
|
| To_client -> to_client
|
||
|
|
in
|
||
|
|
Path.enqueue path op;
|
||
|
|
aux xs
|
||
|
|
in
|
||
|
|
aux actions
|
||
|
|
|
||
|
|
(* In some runs we automatically perform these actions first, which allows the handshake to complete.
|
||
|
|
This lets the fuzz tester get to the interesting cases more quickly. *)
|
||
|
|
let quickstart_actions = [
|
||
|
|
Some (To_server, Transmit (`Bytes 4096));
|
||
|
|
None; (* Client sends handshake *)
|
||
|
|
None; (* Server reads handshake *)
|
||
|
|
Some (To_client, Transmit (`Bytes 4096));
|
||
|
|
None; (* Server replies to handshake *)
|
||
|
|
None; (* Client reads reply *)
|
||
|
|
Some (To_server, Transmit (`Bytes 4096));
|
||
|
|
None; (* Client sends final part *)
|
||
|
|
None; (* Server receives it *)
|
||
|
|
Some (To_client, Recv);
|
||
|
|
Some (To_server, Recv);
|
||
|
|
]
|
||
|
|
|
||
|
|
let main client_message server_message quickstart actions =
|
||
|
|
let actions =
|
||
|
|
if quickstart then quickstart_actions @ actions
|
||
|
|
else actions
|
||
|
|
in
|
||
|
|
Eio_mock.Backend.run @@ fun () ->
|
||
|
|
Switch.run @@ fun sw ->
|
||
|
|
let insecure_test_rng = Mirage_crypto_rng.create (module Test_rng) in
|
||
|
|
Mirage_crypto_rng.set_default_generator insecure_test_rng;
|
||
|
|
let client_socket, server_socket = Mock_socket.create_pair () in
|
||
|
|
let server_flow = Fiber.fork_promise ~sw (fun () -> Tls_eio.server_of_flow Config.server server_socket) in
|
||
|
|
let client_flow = Fiber.fork_promise ~sw (fun () -> Tls_eio.client_of_flow Config.client client_socket) in
|
||
|
|
let to_server =
|
||
|
|
Path.create
|
||
|
|
~sender:client_flow
|
||
|
|
~receiver:server_flow
|
||
|
|
~transmit:(Mock_socket.transmit client_socket)
|
||
|
|
To_server client_message in
|
||
|
|
let to_client =
|
||
|
|
Path.create
|
||
|
|
~sender:server_flow
|
||
|
|
~receiver:client_flow
|
||
|
|
~transmit:(Mock_socket.transmit server_socket)
|
||
|
|
To_client server_message
|
||
|
|
in
|
||
|
|
Fiber.all [
|
||
|
|
(fun () -> dispatch_commands actions ~to_server ~to_client);
|
||
|
|
(fun () -> Path.run to_server);
|
||
|
|
(fun () -> Path.run to_client);
|
||
|
|
]
|
||
|
|
|
||
|
|
let () =
|
||
|
|
Logs.set_level (Some Warning);
|
||
|
|
Logs.set_reporter (Logs_fmt.reporter ());
|
||
|
|
Crowbar.(add_test ~name:"random ops" [bytes; bytes; bool; list action] main)
|