339 lines
11 KiB
OCaml
339 lines
11 KiB
OCaml
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 _10s = Ptime.Span.of_int_s 10
|
|
|
|
let make domain_name seed =
|
|
let valid_from = Option.get Ptime.(sub_span (v (Ptime_clock.now_d_ps ())) _10s) in
|
|
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
|
|
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
|