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 the miou implementation #503

Merged
merged 11 commits into from
Aug 21, 2024
4 changes: 4 additions & 0 deletions miou/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(library
(name tls_miou_unix)
(public_name tls-miou-unix)
(libraries miou.unix tls))
15 changes: 15 additions & 0 deletions miou/tests/dune
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
(test
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure that you want to execute this executable every time, it takes a very long time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how long is "very long"? And what are the options? marking it as executable? then we can't attach it to a package without a public_name...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how long is "very long"?

2mins on my (good) machine.

And what are the options? marking it as executable? then we can't attach it to a package without a public_name...

We can set it as a simple executable if the test bother you too much (because it takes to much time).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine leaving it as test. I usually develop in ocaml 4 land where I don't have miou anyways, and I prefer to have tests that are useful enabled and running (otherwise we hit the "what was the last time that feedback.exe worked" again).

(name fuzz)
(package tls-miou-unix)
(libraries
mirage-crypto-rng-miou-unix
ohex
rresult
ptime
ptime.clock.os
crowbar
hxd.core
hxd.string
tls-miou-unix)
(instrumentation
(backend bisect_ppx)))
338 changes: 338 additions & 0 deletions miou/tests/fuzz.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,338 @@
let rec random_path ?(tries = 10) fmt =
if tries <= 0 then failwith "Impossible to generate an available random path";
let res = Bytes.create 6 in
for i = 0 to Bytes.length res - 1 do
let chr =
match Random.int (10 + 26 + 26) with
| n when n < 10 -> Char.chr (Char.code '0' + n)
| n when n < 10 + 26 -> Char.chr (Char.code 'a' + n - 10)
| n -> Char.chr (Char.code 'A' + n - 10 - 26)
in
Bytes.set res i chr
done;
let path = Fmt.str fmt (Bytes.unsafe_to_string res) in
if Sys.file_exists path then random_path ~tries:(pred tries) fmt else path

let unlink_if_exists path =
try Unix.unlink path with Unix.Unix_error (Unix.ENOENT, _, _) -> ()

let bind_and_listen ?(backlog = 16) () =
let tmp = random_path "socket-%s.socket" in
unlink_if_exists tmp;
let socket = Unix.socket ~cloexec:true Unix.PF_UNIX Unix.SOCK_STREAM 0 in
let addr = Unix.ADDR_UNIX tmp in
Unix.bind socket addr;
Unix.listen socket backlog;
(Miou_unix.of_file_descr ~non_blocking:true socket, addr, tmp)

module Ca = struct
open Rresult

let prefix =
X509.Distinguished_name.
[ Relative_distinguished_name.singleton (CN "Fuzzer") ]

let cacert_dn =
X509.Distinguished_name.(
prefix
@ [ Relative_distinguished_name.singleton (CN "Ephemeral CA for fuzzer") ])

let cacert_lifetime = Ptime.Span.v (365, 0L)

let make domain_name seed =
Domain_name.of_string domain_name >>= Domain_name.host
>>= fun domain_name ->
let private_key =
let seed = Base64.decode_exn ~pad:false seed in
let g = Mirage_crypto_rng.(create ~seed (module Fortuna)) in
Mirage_crypto_pk.Rsa.generate ~g ~bits:2048 ()
in
let valid_from = Ptime.v (Ptime_clock.now_d_ps ()) in
Ptime.add_span valid_from cacert_lifetime
|> Option.to_result ~none:(R.msgf "End time out of range")
>>= fun valid_until ->
X509.Signing_request.create cacert_dn (`RSA private_key) >>= fun ca_csr ->
let extensions =
let open X509.Extension in
let key_id =
X509.Public_key.id X509.Signing_request.((info ca_csr).public_key)
in
empty
|> add Subject_alt_name
( true,
X509.General_name.(
singleton DNS [ Domain_name.to_string domain_name ]) )
|> add Basic_constraints (true, (false, None))
|> add Key_usage
(true, [ `Digital_signature; `Content_commitment; `Key_encipherment ])
|> add Subject_key_id (false, key_id)
in
X509.Signing_request.sign ~valid_from ~valid_until ~extensions
ca_csr (`RSA private_key) cacert_dn
|> R.reword_error (R.msgf "%a" X509.Validation.pp_signature_error)
>>= fun certificate ->
let fingerprint = X509.Certificate.fingerprint `SHA256 certificate in
let time () = Some (Ptime_clock.now ()) in
let authenticator =
X509.Authenticator.cert_fingerprint ~time ~hash:`SHA256
~fingerprint
in
Ok (certificate, `RSA private_key, authenticator)
end

let fuzz_coop = "fuzz.coop"
let mutex = Miou.Mutex.create ()
let epr fmt = Miou.Mutex.protect mutex @@ fun () -> Fmt.epr fmt

type operation =
| Send of string
| Recv of int
| Shutdown of [ `read | `write ]
| Close
| Noop

module Stop = struct
type t = {
mutex : Miou.Mutex.t;
condition : Miou.Condition.t;
mutable stop : bool;
}

let create () =
let mutex = Miou.Mutex.create () in
let condition = Miou.Condition.create () in
{ mutex; condition; stop = false }

let stop t =
Miou.Mutex.protect t.mutex @@ fun () ->
t.stop <- true;
Miou.Condition.broadcast t.condition

let wait t =
Miou.Mutex.protect t.mutex @@ fun () ->
while t.stop = false do
Miou.Condition.wait t.condition t.mutex
done
end

let inhibit fn = try fn () with _exn -> ()

let run ~role:_ actions tls =
let rec go buf tls = function
| [] -> Buffer.contents buf
| Noop :: actions ->
Miou.yield ();
go buf tls actions
| Send str :: actions ->
Tls_miou_unix.write tls str;
go buf tls actions
| Close :: actions ->
Tls_miou_unix.close tls;
go buf tls actions
| Shutdown cmd :: actions ->
Tls_miou_unix.shutdown tls (cmd :> [ `read | `write | `read_write ]);
go buf tls actions
| Recv len :: actions ->
let tmp = Bytes.make len '\000' in
Tls_miou_unix.really_read tls tmp;
Buffer.add_subbytes buf tmp 0 len;
go buf tls actions
in
let buf = Buffer.create 0x100 in
try go buf tls actions with
| End_of_file | Tls_miou_unix.Closed_by_peer | Tls_miou_unix.Tls_alert _
| Tls_miou_unix.Tls_failure _ ->
inhibit (fun () -> Miou_unix.close (Tls_miou_unix.file_descr tls));
Buffer.contents buf
| exn ->
inhibit (fun () -> Miou_unix.close (Tls_miou_unix.file_descr tls));
raise exn

let run_client ~to_client:actions cfg addr =
let domain = Unix.domain_of_sockaddr addr in
let socket = Unix.socket ~cloexec:true domain Unix.SOCK_STREAM 0 in
Unix.connect socket addr;
let fd = Miou_unix.of_file_descr ~non_blocking:true socket in
let tls = Tls_miou_unix.client_of_fd cfg fd in
let finally () =
inhibit (fun () -> Unix.close socket)
in
Fun.protect ~finally @@ fun () -> run ~role:"client" actions tls

let rec cleanup orphans clients =
match Miou.care orphans with
| None | Some None -> clients
| Some (Some prm) ->
let clients = Miou.await prm :: clients in
cleanup orphans clients

let rec terminate orphans clients =
match Miou.care orphans with
| None -> List.rev clients
| Some None ->
Miou.yield ();
terminate orphans clients
| Some (Some prm) ->
let clients = Miou.await prm :: clients in
terminate orphans clients

exception Stop

let run_server ~to_server:actions ~stop fd cfg =
let rec go orphans clients =
let clients = cleanup orphans clients in
let accept = Miou.async @@ fun () -> Miou_unix.accept ~cloexec:true fd in
let stop =
Miou.async @@ fun () ->
Stop.wait stop;
raise Stop
in
match Miou.await_first [ accept; stop ] with
| Error _ ->
inhibit (fun () -> Miou_unix.close fd);
terminate orphans clients
| Ok (fd, _) ->
ignore
( Miou.async ~orphans @@ fun () ->
match Tls_miou_unix.server_of_fd cfg fd with
| tls ->
let str = run ~role:"server" actions tls in
inhibit (fun () -> Miou_unix.close fd); str
| exception _ ->
Miou_unix.close fd;
String.empty );
go orphans clients
in
go (Miou.orphans ()) []

let compile to_client to_server =
let close_client close = function
| Close -> close lor 0b1100
| Shutdown `read -> close lor 0b1000
| Shutdown `write -> close lor 0b0100
| _ -> close
in
let close_server close = function
| Close -> close lor 0b0011
| Shutdown `read -> close lor 0b0010
| Shutdown `write -> close lor 0b0001
| _ -> close
in
let client = Buffer.create 0x100 in
let server = Buffer.create 0x100 in
let rec go close to_client to_server =
match (close, to_client, to_server) with
| _, [], _ | _, _, [] -> ()
| close, ((Shutdown _ | Close) as operation) :: to_client, _ ->
go (close_client close operation) to_client to_server
| close, _, ((Shutdown _ | Close) as operation) :: to_server ->
go (close_server close operation) to_client to_server
| close, Noop :: to_client, to_server | close, to_client, Noop :: to_server
->
go close to_client to_server
| close, Send str :: to_client, Recv n :: to_server ->
assert (String.length str = n);
if close land 0b0100 = 0 && close land 0b0010 = 0 then
Buffer.add_string server str;
if close land 0b0100 = 0 && close land 0b0010 = 0 then
go close to_client to_server
| close, Recv n :: to_client, Send str :: to_server ->
assert (String.length str = n);
if close land 0b1000 = 0 && close land 0b0001 = 0 then
Buffer.add_string client str;
if close land 0b1000 = 0 && close land 0b0001 = 0 then
go close to_client to_server
| _, Send _ :: _, Send _ :: _ | _, Recv _ :: _, Recv _ :: _ ->
assert false (* GADT? *)
in
go 0x0 to_client to_server;
(Buffer.contents client, Buffer.contents server)

let pp_exn ppf exn = Fmt.string ppf (Printexc.to_string exn)
let pp_str ppf str = Hxd_string.pp Hxd.default ppf str

let run seed operations =
Miou_unix.run ~domains:1 @@ fun () ->
let rng = Mirage_crypto_rng_miou_unix.(initialize (module Pfortuna)) in
let fd, addr, path = bind_and_listen () in
let finally () = Unix.unlink path in
Fun.protect ~finally @@ fun () ->
let cert, pk, authenticator =
Rresult.R.failwith_error_msg (Ca.make fuzz_coop seed)
in
let cfg_server =
Result.get_ok (Tls.Config.server ~certificates:(`Single ([ cert ], pk)) ())
in
let cfg_client = Result.get_ok (Tls.Config.client ~authenticator ()) in
let to_client, to_server = List.split operations in
let stop = Stop.create () in
let prm0 = Miou.async @@ fun () -> run_server ~to_server ~stop fd cfg_server in
let prm1 =
Miou.async @@ fun () ->
let finally () = Stop.stop stop in
Fun.protect ~finally @@ fun () -> run_client ~to_client cfg_client addr
in
let send_to_client, send_to_server = compile to_client to_server in
match (Miou.await prm0, Miou.await prm1) with
| Ok [ Ok send_to_server' ], Ok send_to_client' ->
Crowbar.check (String.equal send_to_client send_to_client');
Crowbar.check (String.equal send_to_server send_to_server');
let n = String.length send_to_client in
let m = String.length send_to_server in
Mirage_crypto_rng_miou_unix.kill rng;
epr "[%a] %db %db transmitted\n%!" Fmt.(styled `Green string) "OK" n m
| a, b ->
Mirage_crypto_rng_miou_unix.kill rng;
Crowbar.failf "[%a] Unexpected result: %a & %a\n%!"
Fmt.(styled `Red string) "ERROR"
Fmt.(Dump.result ~error:pp_exn ~ok:Fmt.(Dump.list (Dump.result ~error:pp_exn ~ok:pp_str))) a
Fmt.(Dump.result ~error:pp_exn ~ok:pp_str) b

let label name gen = Crowbar.with_printer Fmt.(const string name) gen

let direction =
let open Crowbar in
choose
[
label "server-to-client" (const `To_client);
label "client-to-server" (const `To_server);
]

let shutdown =
let open Crowbar in
choose
[
label "close" (const Close);
label "shutdown-recv" (const (Shutdown `read));
label "shutdown-send" (const (Shutdown `write));
label "noop" (const Noop);
]

let operation =
let open Crowbar in
map [ direction; bytes ] @@ fun direction str ->
match (direction, str) with
| _, "" -> (Noop, Noop)
| `To_server, str -> (Send str, Recv (String.length str))
| `To_client, str -> (Recv (String.length str), Send str)

let counter = Atomic.make 0

let operations =
let open Crowbar in
fix @@ fun m ->
let continue (to_client, to_server) =
if Atomic.fetch_and_add counter 1 >= 4 then const [ (Close, Close) ]
else map [ m ] @@ fun ops -> (to_client, to_server) :: ops
in
map
[ list1 operation; dynamic_bind (pair shutdown shutdown) continue ]
List.rev_append

let seed = Crowbar.(map [ bytes ] Base64.encode_exn)

let () =
Sys.set_signal Sys.sigpipe Sys.Signal_ignore;
Crowbar.add_test ~name:"run" Crowbar.[ seed; operations ] @@ fun seed operations ->
run seed operations;
Atomic.set counter 0
Loading