diff --git a/.gitignore b/.gitignore index fcf4f65..178f008 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.byte *.native _build -awa-ssh.install +*.install +.merlin diff --git a/.merlin b/.merlin deleted file mode 100644 index caf6047..0000000 --- a/.merlin +++ /dev/null @@ -1,18 +0,0 @@ -S . -S lib -B _build/lib -B _build/test -B _build/lwt -B _build/lwt/test -PKG ppx_deriving -PKG ppx_sexp_conv -PKG cstruct -PKG cstruct.ppx -PKG cstruct-unix -PKG ipaddr -PKG tcpip -PKG rresult -PKG nocrypto -PKG x509 -PKG lwt lwt.unix -PKG mtime mtime.clock.os diff --git a/_tags b/_tags deleted file mode 100644 index 3a990a4..0000000 --- a/_tags +++ /dev/null @@ -1,12 +0,0 @@ -true : warn(+A-4-41-42-44-45) - -true: debug, bin_annot, strict_sequence, safe_string -true: package(nocrypto rresult cstruct cstruct.ppx sexplib ppx_sexp_conv) -true: package(ipaddr zarith x509 mtime) - : include -: package(cstruct-unix io-page.unix) - and not : for-pack(Awa) - -: include -: package(lwt lwt.unix mtime.clock.os) -: package(lwt lwt.unix mtime.clock.os) diff --git a/awa-lwt.opam b/awa-lwt.opam new file mode 100644 index 0000000..a4b3857 --- /dev/null +++ b/awa-lwt.opam @@ -0,0 +1,25 @@ +opam-version: "2.0" +maintainer: "Christiano F. Haesbaert " +authors: "Christiano F. Haesbaert " +license: "ISC" +homepage: "https://github.com/haesbaert/awa-ssh" +bug-reports: "https://github.com/haesbaert/awa-ssh/issues" +dev-repo: "git+https://github.com/haesbaert/awa-ssh.git" +doc: "https://mirage.github.io/awa-ssh/api" + +build: [ + ["dune" "subst"] {pinned} + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] + +depends: [ + "ocaml" {>= "4.04.2"} + "dune" {build & >= "1.0"} + "awa" {= version} + "cstruct" {>= "1.9.0"} + "mtime" + "lwt" +] +synopsis: "SSH implementation in OCaml" +description: """The OpenSSH protocol implemented in OCaml.""" \ No newline at end of file diff --git a/awa-mirage.opam b/awa-mirage.opam new file mode 100644 index 0000000..78eced5 --- /dev/null +++ b/awa-mirage.opam @@ -0,0 +1,28 @@ +opam-version: "2.0" +maintainer: "Christiano F. Haesbaert " +authors: "Christiano F. Haesbaert " +license: "ISC" +homepage: "https://github.com/haesbaert/awa-ssh" +bug-reports: "https://github.com/haesbaert/awa-ssh/issues" +dev-repo: "git+https://github.com/haesbaert/awa-ssh.git" +doc: "https://mirage.github.io/awa-ssh/api" + +build: [ + ["dune" "subst"] {pinned} + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] + +depends: [ + "ocaml" {>= "4.04.2"} + "dune" {build & >= "1.0"} + "awa" {= version} + "cstruct" {>= "1.9.0"} + "mtime" + "lwt" + "mirage-flow" {>= "2.0.0"} + "mirage-clock" {>= "3.0.0"} + "logs" +] +synopsis: "SSH implementation in OCaml" +description: """The OpenSSH protocol implemented in OCaml.""" \ No newline at end of file diff --git a/awa.opam b/awa.opam new file mode 100644 index 0000000..d560a5e --- /dev/null +++ b/awa.opam @@ -0,0 +1,35 @@ +opam-version: "2.0" +maintainer: "Christiano F. Haesbaert " +authors: "Christiano F. Haesbaert " +license: "ISC" +homepage: "https://github.com/haesbaert/awa-ssh" +bug-reports: "https://github.com/haesbaert/awa-ssh/issues" +dev-repo: "git+https://github.com/haesbaert/awa-ssh.git" +doc: "https://mirage.github.io/awa-ssh/api" + +build: [ + ["dune" "subst"] {pinned} + ["dune" "build" "-p" name "-j" jobs] + ["dune" "runtest" "-p" name "-j" jobs] {with-test} +] + +depends: [ + "ocaml" {>= "4.04.2"} + "dune" {build & >= "1.0"} + "ppx_sexp_conv" + "ppx_cstruct" + "nocrypto" + "x509" + "cstruct" {>= "1.9.0"} + "cstruct-unix" + "cstruct-sexp" + "sexplib" + "rresult" + "mtime" + "logs" + "fmt" + "cmdliner" + "base64" {>= "3.0.0"} +] +synopsis: "SSH implementation in OCaml" +description: """The OpenSSH protocol implemented in OCaml.""" \ No newline at end of file diff --git a/build.sh b/build.sh deleted file mode 100755 index 6d025f8..0000000 --- a/build.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -# -# Copyright (c) 2016 Christiano F. Haesbaert -# -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -# - -ocaml pkg/pkg.ml build && ocaml pkg/pkg.ml test diff --git a/dune-project b/dune-project new file mode 100644 index 0000000..ff634d6 --- /dev/null +++ b/dune-project @@ -0,0 +1,2 @@ +(lang dune 1.0) +(name awa) diff --git a/lib/auth.ml b/lib/auth.ml index 316a3a5..5136d7b 100644 --- a/lib/auth.ml +++ b/lib/auth.ml @@ -48,6 +48,22 @@ let by_password name password db = | None -> false | Some user -> user.password = Some password +let to_hash name pubkey session_id service = + let open Wire in + put_cstring session_id (Dbuf.create ()) |> + put_message_id Ssh.MSG_USERAUTH_REQUEST |> + put_string name |> + put_string service |> + put_string "publickey" |> + put_bool true |> + put_string (Hostkey.sshname pubkey) |> + put_pubkey pubkey |> + Dbuf.to_cstruct + +let sign name key session_id service = + let data = to_hash name (Hostkey.pub_of_priv key) session_id service in + Hostkey.sign key data + let by_pubkey name pubkey session_id service signed db = match lookup_user_key name pubkey db with | None -> false @@ -55,16 +71,5 @@ let by_pubkey name pubkey session_id service signed db = if pubkey = Hostkey.Unknown then false else - let unsigned = - let open Wire in - put_cstring session_id (Dbuf.create ()) |> - put_message_id Ssh.MSG_USERAUTH_REQUEST |> - put_string name |> - put_string service |> - put_string "publickey" |> - put_bool true |> - put_string (Hostkey.sshname pubkey) |> - put_pubkey pubkey |> - Dbuf.to_cstruct - in + let unsigned = to_hash name pubkey session_id service in Hostkey.verify pubkey ~unsigned ~signed diff --git a/lib/awa.mllib b/lib/awa.mllib deleted file mode 100644 index a3f1074..0000000 --- a/lib/awa.mllib +++ /dev/null @@ -1 +0,0 @@ -Awa diff --git a/lib/awa.mlpack b/lib/awa.mlpack deleted file mode 100644 index 1b2bae5..0000000 --- a/lib/awa.mlpack +++ /dev/null @@ -1,13 +0,0 @@ -Ssh -Packet -Wire -Cipher -Hmac -Kex -Server -Channel -Auth -Driver -Hostkey -Util -Dbuf diff --git a/lib/channel.ml b/lib/channel.ml index 03628a8..1c15fe6 100644 --- a/lib/channel.ml +++ b/lib/channel.ml @@ -34,7 +34,7 @@ type channel = { us : channel_end; them : channel_end; state : state; - tosend: Cstruct.t; + tosend: Cstruct_sexp.t; } [@@deriving sexp] let compare a b = @@ -57,7 +57,10 @@ let to_string t = Sexplib.Sexp.to_string_hum (sexp_of_channel t) let input_data t data = (* Normalize data, discard if greater than window *) let len = min (Cstruct.len data |> Int32.of_int) t.us.win in - let data = Cstruct.set_len data (Int32.to_int len) in + let data, left = Cstruct.split data (Int32.to_int len) in + if Cstruct.len left > 0 then + Printf.printf "channel input_data: discarding %d bytes (window size)\n%!" + (Cstruct.len left); let new_win = Int32.sub t.us.win len in Util.guard Int32.(new_win >= zero) "window underflow" >>= fun () -> let win, adjust = @@ -78,7 +81,7 @@ let input_data t data = let output_data t data = let fragment data = - let max_pkt = t.them.max_pkt |> Int32.to_int in + let max_pkt = Int32.to_int t.them.max_pkt in let i = Cstruct.iter (fun buf -> @@ -95,8 +98,7 @@ let output_data t data = in let tosend = Util.cs_join t.tosend data in let len = min (Cstruct.len tosend) (Int32.to_int t.them.win) in - let data = Cstruct.set_len tosend len in - let tosend = Cstruct.shift tosend len in + let data, tosend = Cstruct.split tosend len in let win = Int32.sub t.them.win (Int32.of_int len) in Util.guard Int32.(win >= zero) "window underflow" >>= fun () -> let t = { t with tosend; them = { t.them with win } } in @@ -120,6 +122,8 @@ type db = channel Channel_map.t let empty_db = Channel_map.empty +let is_empty = Channel_map.is_empty + (* Find the next available free channel *) let next_free db = let rec linear lkey = function diff --git a/lib/cipher.ml b/lib/cipher.ml index 3cdac37..3a1b8df 100644 --- a/lib/cipher.ml +++ b/lib/cipher.ml @@ -28,7 +28,7 @@ type t = type cipher_key = | Plaintext_key - | Aes_ctr_key of (CTR.key * Nocrypto.Cipher_block.Counters.C128be.t) + | Aes_ctr_key of (CTR.key * Nocrypto.Cipher_block.AES.CTR.ctr) | Aes_cbc_key of (CBC.key * Cstruct.t) type key = { diff --git a/lib/client.ml b/lib/client.ml new file mode 100644 index 0000000..44e2853 --- /dev/null +++ b/lib/client.ml @@ -0,0 +1,284 @@ +(* + * Copyright (c) 2019 Hannes Mehnert + * + * All rights reversed! +*) + +open Rresult.R +open Util + +let service = "ssh-connection" + +let src = Logs.Src.create "awa.client" ~doc:"AWA client" +module Log = (val Logs.src_log src : Logs.LOG) + +type event = [ + | `Established of int32 + | `Channel_data of int32 * Cstruct.t + | `Channel_eof of int32 + | `Channel_exit_status of int32 * int32 + | `Disconnected +] + +let pp_event ppf = function + | `Established id -> Format.fprintf ppf "established id %lu" id + | `Channel_data (id, data) -> + Format.fprintf ppf "data %lu: %s" id (Cstruct.to_string data) + | `Channel_eof id -> Format.fprintf ppf "eof %lu" id + | `Channel_exit_status (id, r) -> Format.fprintf ppf "exit %lu with %lu" id r + | `Disconnected -> Format.fprintf ppf "disconnected" + +type state = + | Init of string * Ssh.kexinit + | Received_version of string * Ssh.kexinit * string + | Negotiated_kex of string * Ssh.kexinit * string * Ssh.kexinit * Kex.negotiation * Nocrypto.Dh.secret * Ssh.mpint + | Newkeys_before_auth of Kex.keys * Kex.keys + | Requested_service of string + | Userauth_request of Ssh.auth_method + | Userauth_requested + | Opening_channel of Channel.channel_end + | Established + +type t = { + state : state ; + session_id : Cstruct.t option; + keys_ctos : Kex.keys; + keys_stoc : Kex.keys; + keying : bool; + key_eol : Mtime.t option; + channels : Channel.db; + linger : Cstruct.t; + user : string ; + key : Hostkey.priv ; + authenticator : Keys.authenticator ; +} + +let established t = match t.state with Established -> true | _ -> false + +let rotate_keys_ctos t new_keys_ctos = + let open Kex in + let new_mac_ctos = { new_keys_ctos.mac with seq = t.keys_ctos.mac.seq } in + let new_keys_ctos = { new_keys_ctos with mac = new_mac_ctos } in + { t with keys_ctos = new_keys_ctos } + +let rotate_keys_stoc t new_keys_stoc = + let open Kex in + let new_mac_stoc = { new_keys_stoc.mac with seq = t.keys_stoc.mac.seq } in + let new_keys_stoc = { new_keys_stoc with mac = new_mac_stoc } in + { t with keys_stoc = new_keys_stoc; keying = false } + +let debug_msg prefix = function + | Ssh.Msg_channel_data (id, data) -> + Log.debug (fun m -> m "%s (Msg_data %d bytes for %lu)" prefix + (Cstruct.len data) id) + | msg -> Log.debug (fun m -> m "%s %s" prefix (Ssh.message_to_string msg)) + +let output_msg t msg = + let buf, keys_ctos = Common.output_msg t.keys_ctos msg in + let t = { t with keys_ctos } in + debug_msg ">>>" msg; + (* Do state transitions *) + match t.state with + | Newkeys_before_auth (my_keys, _) -> + Log.debug (fun m -> m "rotating ctos keys"); + let t' = rotate_keys_ctos t my_keys in + t', buf + | _ -> t, buf + +let output_msgs t msgs = + let t', data = List.fold_left (fun (t, acc) msg -> + let t', buf = output_msg t msg in + (t', buf :: acc)) + (t, []) msgs + in + t', List.rev data + +let make ?(authenticator = `No_authentication) ~user key = + let open Ssh in + let client_kexinit = Kex.make_kexinit () in + let banner_msg = Ssh.Msg_version version_banner in + let kex_msg = Ssh.Msg_kexinit client_kexinit in + let t = { state = Init (version_banner, client_kexinit); + session_id = None; + keys_ctos = Kex.make_plaintext (); + keys_stoc = Kex.make_plaintext (); + keying = true; + key_eol = None; + linger = Cstruct.empty; + channels = Channel.empty_db; + user ; key ; authenticator + } + in + output_msgs t [ banner_msg ; kex_msg ] + +let handle_kexinit t c_v ckex s_v skex = + Kex.negotiate ~s:skex ~c:ckex >>= fun neg -> + let secret, my_pub = Kex.Dh.secret_pub neg.kex_alg in + ok ({ t with state = Negotiated_kex (c_v, ckex, s_v, skex, neg, secret, my_pub) }, + [ Ssh.Msg_kexdh_init my_pub], []) + +let handle_kexdh_reply t now v_c ckex v_s skex neg secret my_pub k_s theirs signed = + Kex.Dh.shared neg.Kex.kex_alg secret theirs >>= fun shared -> + let h = + Kex.Dh.compute_hash + ~v_c ~v_s ~i_c:(Wire.blob_of_kexinit ckex) ~i_s:skex.Ssh.rawkex + ~k_s ~e:my_pub ~f:theirs ~k:shared + in + if Keys.hostkey_matches t.authenticator k_s && Hostkey.verify k_s ~unsigned:h ~signed then begin + Log.info (fun m -> m "verified kexdh_reply!"); + let session_id = match t.session_id with None -> h | Some x -> x in + Kex.Dh.derive_keys shared h session_id neg now + >>| fun (new_keys_ctos, new_keys_stoc, key_eol) -> + { t with + state = Newkeys_before_auth (new_keys_ctos, new_keys_stoc) ; + session_id = Some session_id ; key_eol = Some key_eol }, + [ Ssh.Msg_newkeys ], [] + end else + Error "couldn't verify kex" + +let handle_newkeys_before_auth t keys = + Log.debug (fun m -> m "rotating stoc keys"); + let t' = rotate_keys_stoc t keys in + let service = "ssh-userauth" in + Ok ({ t' with state = Requested_service service }, + [ Ssh.Msg_service_request service ], []) + +let service_accepted t = function + | "ssh-userauth" -> + Ok ({ t with state = Userauth_request Authnone }, + [ Ssh.Msg_userauth_request (t.user, service, Authnone) ], + []) + | service -> Error ("unknown service: " ^ service) + +let handle_auth_failure t _ = function + | [] -> Error "no authentication method left" + | xs when List.mem "publickey" xs -> + let pub = Hostkey.pub_of_priv t.key in + let met = Ssh.Pubkey (pub, None) in + Ok ({ t with state = Userauth_request met }, + [ Ssh.Msg_userauth_request (t.user, service, met) ], + []) + | _ -> Error "no supported authentication methods left" + +let handle_pk_ok t m pk = match m with + | Ssh.Pubkey (pub, None) when pub = pk -> + let session_id = match t.session_id with None -> assert false | Some x -> x in + let signed = Auth.sign t.user t.key session_id service in + let met = Ssh.Pubkey (Hostkey.pub_of_priv t.key, Some signed) in + Ok ({ t with state = Userauth_requested }, + [ Ssh.Msg_userauth_request (t.user, service, met) ], + []) + | _ -> Error "not sure how we ended in pk ok now" + +let open_channel t = + if Channel.is_empty t.channels then + let channel, msg = + let id = 0l + and win = Ssh.channel_win_len + and max_pkt = Ssh.channel_max_pkt_len + in + Channel.make_end id win max_pkt, + (id, win, max_pkt, Ssh.Session) + in + Ok ({ t with state = Opening_channel channel }, [ Ssh.Msg_channel_open msg ], []) + else + Error "not sure what to do, there's already a channel" + +let open_channel_success t us our_id their_id win max_pkt _data = + if us.Channel.id = our_id then + let them = Channel.make_end their_id win max_pkt in + let c = Channel.make ~us ~them in + let channels = Channel.update c t.channels in + Ok ({ t with channels ; state = Established }, [], [ `Established our_id ]) + else + Error (Printf.sprintf "channel ids do not match (our %lu their %lu)" + us.Channel.id our_id) + +let input_msg t msg now = + let open Ssh in + match t.state, msg with + | Init (cv, ckex), Msg_version v -> + Ok ({ t with state = Received_version (cv, ckex, v) }, [], []) + | Received_version (cv, ckex, sv), Msg_kexinit skex -> + handle_kexinit t cv ckex sv skex + | Negotiated_kex (cv, ckex, sv, skex, neg, sec, mypub), + Msg_kexdh_reply (pub, theirs, signed) -> + handle_kexdh_reply t now cv ckex sv skex neg sec mypub pub theirs signed + | Newkeys_before_auth (_, keys), Msg_newkeys -> + handle_newkeys_before_auth t keys + | Requested_service s, Msg_service_accept s' when s = s' -> + service_accepted t s + | Userauth_request m, Msg_userauth_failure (methods, _) -> + handle_auth_failure t m methods + | Userauth_request m, Msg_userauth_pk_ok pk -> handle_pk_ok t m pk + | Userauth_request _, Msg_userauth_success -> open_channel t + | Userauth_requested, Msg_userauth_success -> open_channel t + | Opening_channel us, Msg_channel_open_confirmation (oid, tid, win, max, data) -> + open_channel_success t us oid tid win max data + | _, Msg_global_request (_, want_reply, Unknown_request _) -> + Log.info (fun m -> m "ignoring unknown global request (want reply %B)" + want_reply); + Ok (t, [], []) + | _, Msg_debug (_, msg, lang) -> + Log.info (fun m -> m "ignoring debug %s (lang %s)" msg lang); + Ok (t, [], []) + | Established, Msg_channel_data (id, data) -> + guard_some (Channel.lookup id t.channels) "no such channel" >>= fun c -> + Channel.input_data c data >>| fun (c, data, adjust) -> + let channels = Channel.update c t.channels in + let out = match adjust with None -> [] | Some e -> [ e ] in + { t with channels }, out, [ `Channel_data (Channel.id c, data) ] + | Established, Msg_channel_window_adjust (id, len) -> + guard_some (Channel.lookup id t.channels) "no such channel" >>= fun c -> + Channel.adjust_window c len >>| fun (c, msgs) -> + let channels = Channel.update c t.channels in + { t with channels }, msgs, [] + | Established, Msg_channel_eof id -> + guard_some (Channel.lookup id t.channels) "no such channel" >>| fun c -> + t, [], [ `Channel_eof (Channel.id c) ] + | Established, Msg_channel_request (id, false, Exit_status r) -> + guard_some (Channel.lookup id t.channels) "no such channel" >>| fun c -> + t, [], [ `Channel_exit_status (Channel.id c, r) ] + | Established, Msg_channel_close id -> + guard_some (Channel.lookup id t.channels) "no such channel" >>| fun c -> + let channels = Channel.remove (Channel.id c) t.channels in + let msg = "all the channels are closed now, nothing left to do here" in + { t with channels }, + [ Msg_channel_close (Channel.id c) ; + Msg_disconnect (DISCONNECT_BY_APPLICATION, msg, "") ], + [ `Disconnected ] + | _, _ -> + debug_msg "unexpected" msg; + Error "unexpected state and message" + +let rec incoming t now buf = + let buf = Cstruct.append t.linger buf in + (match t.state with + | Init _ -> + Common.version buf >>| fun (msg, buf) -> + { t with linger = buf }, msg + | _ -> + Common.decrypt t.keys_stoc buf >>| fun (keys_stoc, msg, buf) -> + { t with keys_stoc ; linger = buf }, msg) >>= fun (t, msg) -> + match msg with + | None -> Ok (t, [], []) + | Some msg -> + debug_msg "<<<" msg; + input_msg t msg now >>= fun (t', replies, events) -> + let t'', replies = output_msgs t' replies in + incoming t'' now Cstruct.empty >>| fun (t''', replies', events') -> + t''', replies @ replies', events @ events' + +let outgoing_request t ?(id = 0l) ?(want_reply = false) req = + guard (established t) "not yet established" >>= fun () -> + guard_some (Channel.lookup id t.channels) "no such channel" >>| fun _ -> + let msg = Ssh.Msg_channel_request (id, want_reply, req) in + output_msg t msg + +let outgoing_data t ?(id = 0l) data = + guard (established t) "not yet established" >>= fun () -> + guard (Cstruct.len data > 0) "empty data" >>= fun () -> + guard_some (Channel.lookup id t.channels) "no such channel" >>= fun c -> + Channel.output_data c data >>| fun (c, frags) -> + let t' = { t with channels = Channel.update c t.channels } in + output_msgs t' frags diff --git a/lib/client.mli b/lib/client.mli new file mode 100644 index 0000000..4d01725 --- /dev/null +++ b/lib/client.mli @@ -0,0 +1,29 @@ +(* + * Copyright (c) 2019 Hannes Mehnert + * + * All rights reversed! +*) + +type t + +val make : ?authenticator:Keys.authenticator -> user:string -> Hostkey.priv -> + t * Cstruct.t list + +type event = [ + | `Established of int32 + | `Channel_data of int32 * Cstruct.t + | `Channel_eof of int32 + | `Channel_exit_status of int32 * int32 + | `Disconnected +] + +val pp_event : Format.formatter -> event -> unit + +val incoming : t -> Mtime.t -> Cstruct.t -> + (t * Cstruct.t list * event list, string) result + +val outgoing_request : t -> ?id:int32 -> ?want_reply:bool -> + Ssh.channel_request -> (t * Cstruct.t, string) result + +val outgoing_data : t -> ?id:int32 -> Cstruct.t -> + (t * Cstruct.t list, string) result diff --git a/lib/common.ml b/lib/common.ml new file mode 100644 index 0000000..5c5ae4e --- /dev/null +++ b/lib/common.ml @@ -0,0 +1,20 @@ +open Rresult.R + +let output_msg keys = function + | Ssh.Msg_version v -> Cstruct.of_string (v ^ "\r\n"), keys + | msg -> Packet.encrypt keys msg + +let version buf = + Wire.get_version buf >>= fun (version, input_buffer) -> + match version with + | None -> Ok (None, input_buffer) + | Some v -> + let msg = Ssh.Msg_version v in + Ok (Some msg, input_buffer) + +let decrypt ?(ignore_packet = false) keys buf = + Packet.decrypt keys buf >>= function + | None -> ok (keys, None, buf) + | Some (pkt, input_buffer, keys) -> + Packet.to_msg pkt >>= fun msg -> + ok (keys, (if ignore_packet then None else Some msg), input_buffer) diff --git a/lib/dbuf.ml b/lib/dbuf.ml index 0c5ca82..087f206 100644 --- a/lib/dbuf.ml +++ b/lib/dbuf.ml @@ -25,7 +25,7 @@ let chunk_size = 1024 let create () = { tlen = chunk_size; coff = 0; cbuf = Cstruct.create chunk_size } -let to_cstruct t = Cstruct.set_len t.cbuf t.coff +let to_cstruct t = Cstruct.sub t.cbuf 0 t.coff let left t = t.tlen - t.coff diff --git a/lib/driver.ml b/lib/driver.ml index bfc1823..1f90cd3 100644 --- a/lib/driver.ml +++ b/lib/driver.ml @@ -54,11 +54,13 @@ let rekey t = | Some (server, kexinit) -> send_msg { t with server } kexinit let rec poll t = + Printf.printf "poll called, input buffer %d\n%!" (Cstruct.len t.input_buffer); let now = t.time_cb () in let server = t.server in Server.pop_msg2 server t.input_buffer >>= fun (server, msg, input_buffer) -> match msg with | None -> + Printf.printf "no msg :/, input %d\n%!" (Cstruct.len input_buffer); let input_buffer = cs_join input_buffer (t.read_cb ()) in poll { t with server; input_buffer } | Some msg -> diff --git a/lib/dune b/lib/dune new file mode 100644 index 0000000..642e77a --- /dev/null +++ b/lib/dune @@ -0,0 +1,5 @@ +(library + (name awa) + (public_name awa) + (preprocess (pps ppx_cstruct ppx_sexp_conv)) + (libraries cstruct cstruct-sexp nocrypto zarith x509 mtime sexplib rresult logs base64)) diff --git a/lib/hmac.ml b/lib/hmac.ml index a3ba7a6..7a6d9f1 100644 --- a/lib/hmac.ml +++ b/lib/hmac.ml @@ -76,10 +76,10 @@ let preferred = [ Md5; Sha1; Sha2_256; let hmacv hmac ~key data = let take_96 buf = - if (Cstruct.len buf) < 12 then + if Cstruct.len buf < 12 then failwith "digest is too short." else - Cstruct.set_len buf 12 + Cstruct.sub buf 0 12 in match hmac with | Plaintext -> Cstruct.create 0 diff --git a/lib/hostkey.ml b/lib/hostkey.ml index ce6cdab..1aaa4fb 100644 --- a/lib/hostkey.ml +++ b/lib/hostkey.ml @@ -15,7 +15,6 @@ *) open Nocrypto -open Sexplib.Conv type priv = | Rsa_priv of Rsa.priv @@ -27,10 +26,7 @@ type pub = let pub_of_priv = function | Rsa_priv priv -> Rsa_pub (Rsa.pub_of_priv priv) -let sexp_of_pub = function - | Rsa_pub pub -> Nocrypto.Rsa.sexp_of_pub pub - | Unknown -> sexp_of_string "Unknown" - +let sexp_of_pub _ = Sexplib.Sexp.Atom "Hostkey.sexp_of_pub: TODO" let pub_of_sexp _ = failwith "Hostkey.pub_of_sexp: TODO" (* diff --git a/lib/kex.ml b/lib/kex.ml index f03964d..45792a4 100644 --- a/lib/kex.ml +++ b/lib/kex.ml @@ -56,7 +56,7 @@ let group_of_alg = function | Diffie_hellman_group14_sha1 -> Dh.Group.oakley_14 | Diffie_hellman_group1_sha1 -> Dh.Group.oakley_2 -let preferred = [ Diffie_hellman_group14_sha1; Diffie_hellman_group1_sha1 ] +let preferred = [ Diffie_hellman_group14_sha1 ] let make_kexinit () = let k = @@ -226,7 +226,7 @@ let derive_keys digesti k h session_id neg now = let x = Cstruct.create 1 in Cstruct.set_char x 0 ch; let k1 = digesti (fun f -> List.iter f [k; h; x; session_id]) in - Cstruct.set_len (expand k1) need + Cstruct.sub (expand k1) 0 need in let key_of cipher iv secret = let open Cipher_block in @@ -234,7 +234,7 @@ let derive_keys digesti k h session_id neg now = match cipher with | Plaintext -> invalid_arg "Deriving plaintext, abort at all costs" | Aes128_ctr | Aes192_ctr | Aes256_ctr -> - let iv = Counters.C128be.of_cstruct iv in + let iv = AES.CTR.ctr_of_cstruct iv in { cipher; cipher_key = Aes_ctr_key ((AES.CTR.of_secret secret), iv) } | Aes128_cbc | Aes192_cbc | Aes256_cbc -> @@ -269,25 +269,33 @@ module Dh = struct let compute_hash ~v_c ~v_s ~i_c ~i_s ~k_s ~e ~f ~k = let open Wire in - put_cstring v_c (Dbuf.create ()) |> - put_cstring v_s |> + put_cstring (Cstruct.of_string v_c) (Dbuf.create ()) |> + put_cstring (Cstruct.of_string v_s) |> put_cstring i_c |> put_cstring i_s |> - put_cstring k_s |> + put_cstring (Wire.blob_of_pubkey k_s) |> put_mpint e |> put_mpint f |> put_mpint k |> Dbuf.to_cstruct |> Hash.SHA1.digest - let generate alg peer_pub = + let secret_pub alg = + let secret, pub = Dh.gen_key (group_of_alg alg) in + secret, Numeric.Z.of_cstruct_be pub + + let shared alg secret recv = let g = group_of_alg alg in - let secret, my_pub = Dh.gen_key g in guard_some - (Dh.shared g secret (Numeric.Z.to_cstruct_be peer_pub)) + (Dh.shared g secret (Numeric.Z.to_cstruct_be recv)) "Can't compute shared secret" >>= fun shared -> + ok (Numeric.Z.of_cstruct_be shared) + + let generate alg peer_pub = + let secret, my_pub = secret_pub alg in + shared alg secret peer_pub >>= fun shared -> (* my_pub is f or e, shared is k *) - ok Numeric.Z.(of_cstruct_be my_pub, of_cstruct_be shared) + ok (my_pub, shared) end diff --git a/lib/keys.ml b/lib/keys.ml new file mode 100644 index 0000000..1cc9901 --- /dev/null +++ b/lib/keys.ml @@ -0,0 +1,67 @@ + +open Rresult.R.Infix + +type authenticator = [ + | `No_authentication + | `Key of Nocrypto.Rsa.pub + | `Fingerprint of string +] + +let hostkey_matches a = function + | Hostkey.Unknown -> false + | Hostkey.Rsa_pub pub -> + let hash = Nocrypto.Hash.SHA256.digest (Wire.blob_of_pubkey (Hostkey.Rsa_pub pub)) in + Logs.app (fun m -> m "authenticating RSA server fingerprint SHA256:%s" + (Base64.encode_string ~pad:false (Cstruct.to_string hash))); + match a with + | `No_authentication -> + Logs.warn (fun m -> m "NO AUTHENTICATOR"); + true + | `Key pub' -> + if pub = pub' then begin + Logs.app (fun m -> m "host RSA key verification successful!"); + true + end else begin + Logs.err (fun m -> m "host RSA key verification failed"); + false + end + | `Fingerprint s -> + if Cstruct.(equal (Cstruct.of_string s) hash) then begin + Logs.app (fun m -> m "host fingerprint verification successful!"); + true + end else begin + Logs.err (fun m -> m "host fingerprint verification failed"); + false + end + +let authenticator_of_string str = + if str = "" then + Ok `No_authentication + else + match Astring.String.cut ~sep:":" str with + | Some ("SHA256", fp) -> + begin match Base64.decode ~pad:false fp with + | Error (`Msg m) -> + Error ("invalid authenticator (bad b64 in fingerprint): " ^ m) + | Ok fp -> Ok (`Fingerprint fp) + end + | _ -> + match Base64.decode ~pad:false str with + | Ok k -> + (Wire.pubkey_of_blob (Cstruct.of_string k) >>= function + | Hostkey.Rsa_pub key -> Ok (`Key key) + | Hostkey.Unknown -> Error "invalid authenticator") + | Error (`Msg msg) -> + Error (str ^ " is invalid or unsupported authenticator, b64 failed: " ^ msg) + +let of_seed seed = + let g = + let seed = Cstruct.of_string seed in + Nocrypto.Rng.(create ~seed (module Generators.Fortuna)) + in + let key = Nocrypto.Rsa.generate ~g 2048 in + let public = Nocrypto.Rsa.pub_of_priv key in + let pubkey = Wire.blob_of_pubkey (Hostkey.Rsa_pub public) in + Logs.info (fun m -> m "using ssh-rsa %s" + (Cstruct.to_string (Nocrypto.Base64.encode pubkey))); + Hostkey.Rsa_priv key diff --git a/lib/packet.ml b/lib/packet.ml index 1a055b4..be0f0a7 100644 --- a/lib/packet.ml +++ b/lib/packet.ml @@ -27,13 +27,13 @@ let hmac mac buf = digest, Hmac.{ mac with seq = Int32.succ seq } let peek_len cipher block_len buf = - assert (block_len <= (Cstruct.len buf)); - let buf = Cstruct.set_len buf block_len in + assert (block_len <= Cstruct.len buf); + let buf = Cstruct.sub buf 0 block_len in let hdr, _ = Cipher.decrypt cipher buf in Ssh.get_pkt_hdr_pkt_len hdr |> Int32.to_int let partial buf = - if (Cstruct.len buf) < Ssh.max_pkt_len then + if Cstruct.len buf < Ssh.max_pkt_len then ok None else error "Buffer is too big" @@ -47,21 +47,20 @@ let decrypt keys buf = let mac = keys.Kex.mac in let block_len = max 8 (Cipher.block_len cipher.Cipher.cipher) in let digest_len = Hmac.(digest_len mac.hmac) in - if (Cstruct.len buf) < (max sizeof_pkt_hdr (digest_len + block_len)) then + if Cstruct.len buf < max sizeof_pkt_hdr (digest_len + block_len) then partial buf else let pkt_len = peek_len cipher block_len buf in guard (pkt_len > 0 && pkt_len < max_pkt_len) "decrypt: Bogus pkt len" >>= fun () -> (* 4 is pkt_len field itself *) - if (Cstruct.len buf) < (pkt_len + 4 + digest_len) then + if Cstruct.len buf < pkt_len + 4 + digest_len then partial buf else - let pkt_enc = Cstruct.set_len buf (pkt_len + 4) in + let pkt_enc, digest1 = Cstruct.split buf (pkt_len + 4) in let tx_rx = Int64.(add keys.Kex.tx_rx (Cstruct.len pkt_enc |> of_int)) in let pkt_dec, cipher = Cipher.decrypt cipher pkt_enc in - let digest1 = Cstruct.shift buf (pkt_len + 4) in - let digest1 = Cstruct.set_len digest1 digest_len in + let digest1 = Cstruct.sub digest1 0 digest_len in let digest2, mac = hmac mac pkt_dec in guard (Cstruct.equal digest1 digest2) "decrypt: Bad digest" >>= fun () -> @@ -86,7 +85,7 @@ let encrypt keys msg = in assert (padlen >= 4 && padlen <= 255); let pkt = Wire.put_random padlen buf |> Dbuf.to_cstruct in - Ssh.set_pkt_hdr_pkt_len pkt (Int32.of_int ((Cstruct.len pkt) - 4)); + Ssh.set_pkt_hdr_pkt_len pkt (Int32.of_int (Cstruct.len pkt - 4)); Ssh.set_pkt_hdr_pad_len pkt padlen; let digest, mac = hmac mac pkt in let enc, cipher = Cipher.encrypt cipher pkt in diff --git a/lib/server.ml b/lib/server.ml index c7487bb..0e7eaf8 100644 --- a/lib/server.ml +++ b/lib/server.ml @@ -119,22 +119,12 @@ let maybe_rekey t now = if should_rekey t now then rekey t else None let pop_msg2 t buf = let version t buf = - Wire.get_version buf >>= fun (client_version, input_buffer) -> - match client_version with - | None -> ok (t, None, input_buffer) - | Some v -> - let msg = Ssh.Msg_version v in - ok (t, Some msg, input_buffer) + Common.version buf >>| fun (v, i) -> + (t, v, i) in let decrypt t buf = - Packet.decrypt t.keys_ctos buf >>= function - | None -> ok (t, None, buf) - | Some (pkt, input_buffer, keys_ctos) -> - let ignore_packet = t.ignore_next_packet in - Packet.to_msg pkt >>= fun msg -> - ok ({ t with keys_ctos; ignore_next_packet = false }, - (if ignore_packet then None else Some msg), - input_buffer) + Common.decrypt ~ignore_packet:t.ignore_next_packet t.keys_ctos buf >>| fun (keys_ctos, msg, buf) -> + { t with keys_ctos; ignore_next_packet = false }, msg, buf in match t.client_version with | None -> version t buf @@ -323,14 +313,17 @@ let input_msg t msg now = Kex.(Dh.generate neg.kex_alg e) >>= fun (f, k) -> let pub_host_key = Hostkey.pub_of_priv t.host_key in let h = Kex.Dh.compute_hash - ~v_c:(Cstruct.of_string client_version) - ~v_s:(Cstruct.of_string t.server_version) + ~v_c:client_version + ~v_s:t.server_version ~i_c:c.rawkex ~i_s:(Wire.blob_of_kexinit t.server_kexinit) - ~k_s:(Wire.blob_of_pubkey pub_host_key) + ~k_s:pub_host_key ~e ~f ~k in let signature = Hostkey.sign t.host_key h in + Format.printf "shared is %a signature is %a (hash %a)\n%!" + Cstruct.hexdump_pp (Nocrypto.Numeric.Z.to_cstruct_be f) + Cstruct.hexdump_pp signature Cstruct.hexdump_pp h; let session_id = match t.session_id with None -> h | Some x -> x in Kex.Dh.derive_keys k h session_id neg now >>= fun (new_keys_ctos, new_keys_stoc, key_eol) -> @@ -398,14 +391,8 @@ let input_msg t msg now = | msg -> error ("unhandled msg: " ^ (message_to_string msg)) let output_msg t msg = - let t, buf = - match msg with - | Ssh.Msg_version v -> - t, Cstruct.of_string (v ^ "\r\n") - | msg -> - let enc, keys = Packet.encrypt t.keys_stoc msg in - { t with keys_stoc = keys }, enc - in + let buf, keys_stoc = Common.output_msg t.keys_stoc msg in + let t = { t with keys_stoc } in (* Do state transitions *) match msg with | Ssh.Msg_newkeys -> of_new_keys_stoc t >>= fun t -> ok (t, buf) diff --git a/lib/ssh.ml b/lib/ssh.ml index b759fca..6632900 100644 --- a/lib/ssh.ml +++ b/lib/ssh.ml @@ -75,7 +75,7 @@ type message_id = [@@uint8_t][@@sexp]] type kexinit = { - cookie : Cstruct.t; + cookie : Cstruct_sexp.t; kex_algs : string list; server_host_key_algs : string list; encryption_algs_ctos : string list; @@ -87,7 +87,7 @@ type kexinit = { languages_ctos : string list; languages_stoc : string list; first_kex_packet_follows : bool; - rawkex : Cstruct.t; (* raw kexinit *) + rawkex : Cstruct_sexp.t; (* raw kexinit *) } [@@deriving sexp] [%%cenum @@ -130,6 +130,7 @@ let sexp_of_mpint mpint = sexp_of_string (Z.to_string mpint) type global_request = | Tcpip_forward of (string * int32) | Cancel_tcpip_forward of (string * int32) + | Unknown_request of string [@@deriving sexp] type channel_request = @@ -144,7 +145,7 @@ type channel_request = | Signal of string | Exit_status of int32 | Exit_signal of (string * bool * string * string) - | Raw_data of Cstruct.t + | Raw_data of Cstruct_sexp.t [@@deriving sexp] type channel_open = @@ -152,7 +153,7 @@ type channel_open = | X11 of (string * int32) | Forwarded_tcpip of (string * int32 * string * int32) | Direct_tcpip of (string * int32 * string * int32) - | Raw_data of Cstruct.t + | Raw_data of Cstruct_sexp.t [@@deriving sexp] (* @@ -164,9 +165,9 @@ let sexp_of_password _ = sexp_of_string "????" let password_of_sexp _ = failwith "password_of_sexp: TODO" type auth_method = - | Pubkey of (Hostkey.pub * Cstruct.t option) + | Pubkey of (Hostkey.pub * Cstruct_sexp.t option) | Password of (password * password option) - | Hostbased of (string * Cstruct.t * string * string * Cstruct.t) (* TODO *) + | Hostbased of (string * Cstruct_sexp.t * string * string * Cstruct_sexp.t) (* TODO *) | Authnone [@@deriving sexp] @@ -198,7 +199,7 @@ type message = | Msg_service_accept of string | Msg_kexinit of kexinit | Msg_newkeys - | Msg_kexdh_reply of (Hostkey.pub * mpint * Cstruct.t) + | Msg_kexdh_reply of (Hostkey.pub * mpint * Cstruct_sexp.t) | Msg_kexdh_init of mpint | Msg_userauth_request of (string * string * auth_method) | Msg_userauth_failure of (string list * bool) @@ -206,14 +207,14 @@ type message = | Msg_userauth_banner of (string * string) | Msg_userauth_pk_ok of Hostkey.pub | Msg_global_request of (string * bool * global_request) - | Msg_request_success of Cstruct.t option + | Msg_request_success of Cstruct_sexp.t option | Msg_request_failure | Msg_channel_open of (int32 * int32 * int32 * channel_open) - | Msg_channel_open_confirmation of (int32 * int32 * int32 * int32 * Cstruct.t) + | Msg_channel_open_confirmation of (int32 * int32 * int32 * int32 * Cstruct_sexp.t) | Msg_channel_open_failure of (int32 * int32 * string * string) | Msg_channel_window_adjust of (int32 * int32) - | Msg_channel_data of (int32 * Cstruct.t) - | Msg_channel_extended_data of (int32 * int32 * Cstruct.t) + | Msg_channel_data of (int32 * Cstruct_sexp.t) + | Msg_channel_extended_data of (int32 * int32 * Cstruct_sexp.t) | Msg_channel_eof of int32 | Msg_channel_close of int32 | Msg_channel_request of (int32 * bool * channel_request) diff --git a/lib/wire.ml b/lib/wire.ml index 0f13ea6..e132d70 100644 --- a/lib/wire.ml +++ b/lib/wire.ml @@ -54,8 +54,7 @@ let get_cstring buf = trap_error (fun () -> let len = Cstruct.BE.get_uint32 buf 0 |> Int32.to_int in Ssh.guard_sshlen_exn len; - (Cstruct.set_len (Cstruct.shift buf 4) len, - Cstruct.shift buf (len + 4))) + Cstruct.split (Cstruct.shift buf 4) len) let put_cstring s t = let len = Cstruct.len s in @@ -178,10 +177,8 @@ let openssh_of_pubkey key = Cstruct.of_string " awa-ssh\n" ] let privkey_of_pem buf = - trap_error (fun () -> - let open X509.Encoding.Pem in - match Private_key.of_pem_cstruct1 buf with - `RSA key -> Hostkey.Rsa_priv key) + X509.Private_key.decode_pem buf >>| fun (`RSA key) -> + Hostkey.Rsa_priv key let put_kexinit kex t = let open Ssh in @@ -290,7 +287,7 @@ let get_message buf = get_nl buf >>= fun (languages_stoc, buf) -> get_bool buf >>= fun (first_kex_packet_follows, _) -> ok (Msg_kexinit - { cookie = Cstruct.set_len cookiebegin 16; + { cookie = Cstruct.sub cookiebegin 0 16; kex_algs; server_host_key_algs; encryption_algs_ctos; @@ -372,7 +369,9 @@ let get_message buf = get_string buf >>= fun (address, buf) -> get_uint32 buf >>= fun (port, buf) -> ok (Cancel_tcpip_forward (address, port), buf) - | _ -> error ("Unknown request " ^ request)) + | _ -> + get_string buf >>= fun (data, buf) -> + ok (Unknown_request data, buf)) >>= fun (global_request, _) -> ok (Msg_global_request (request, want_reply, global_request)) | MSG_REQUEST_SUCCESS -> @@ -403,7 +402,7 @@ let get_message buf = (send_channel, init_win, max_pkt, Forwarded_tcpip (con_address, con_port, origin_address, origin_port))) - | _ -> error ("Unknown request " ^ request)) + | _ -> error ("Unknown channel open " ^ request)) | MSG_CHANNEL_OPEN_CONFIRMATION -> get_uint32 buf >>= fun (recp_channel, buf) -> get_uint32 buf >>= fun (send_channel, buf) -> @@ -507,7 +506,7 @@ let get_message buf = get_string buf >>= fun (lang, _) -> ok (Msg_channel_request (channel, want_reply, Exit_signal (name, core_dumped, message, lang))) - | _ -> error ("Unknown request " ^ request)) + | _ -> error ("Unknown channel request " ^ request)) | MSG_CHANNEL_SUCCESS -> get_uint32 buf >>= fun (channel, _) -> ok (Msg_channel_success channel) @@ -616,7 +615,8 @@ let put_message msg buf = put_uint32 port | Cancel_tcpip_forward (address, port) -> put_string address buf |> - put_uint32 port) + put_uint32 port + | Unknown_request _ -> assert false) | Msg_request_success (req_data) -> let buf = put_id MSG_REQUEST_SUCCESS buf in (match req_data with @@ -737,37 +737,33 @@ let put_message msg buf = (* XXX Maybe move this to Packet *) let get_payload buf = let open Ssh in - guard ((Cstruct.len buf) >= 5) "Buf too short" - >>= fun () -> + guard (Cstruct.len buf >= 5) "Buf too short" >>= fun () -> let pkt_len = get_pkt_hdr_pkt_len buf |> Int32.to_int in let pad_len = get_pkt_hdr_pad_len buf in - guard (pkt_len > 0 && pkt_len < max_pkt_len) "Bogus pkt len" - >>= fun () -> - guard (pad_len < pkt_len) "Bogus pad len" - >>= fun () -> - guard ((Cstruct.len buf) = (pkt_len + 4)) "Bogus buf len" - >>= fun () -> + guard (pkt_len > 0 && pkt_len < max_pkt_len) "Bogus pkt len" >>= fun () -> + guard (pad_len < pkt_len) "Bogus pad len" >>= fun () -> + guard (Cstruct.len buf = pkt_len + 4) "Bogus buf len" >>= fun () -> let payload_len = pkt_len - pad_len - 1 in - guard (payload_len > 0) "Bogus payload_len" - >>= fun () -> - let payload = Cstruct.shift buf 5 in - let payload = Cstruct.set_len payload payload_len in + guard (payload_len > 0) "Bogus payload_len" >>= fun () -> + let payload = Cstruct.sub buf 5 payload_len in ok payload let get_version buf = (* Fetches next line, returns maybe a string and the remainder of buf *) let fetchline buf = - if (Cstruct.len buf) < 2 then + if Cstruct.len buf < 1 then None else let s = Cstruct.to_string buf in let n = try String.index s '\n' with Not_found -> 0 in - if n = 0 || ((String.get s (pred n)) <> '\r') then + if n = 0 then None else - let line = String.sub s 0 (pred n) in + let off = if String.get s (pred n) = '\r' then 1 else 0 in + let line = String.sub s 0 (n - off) in let line_len = String.length line in - Some (line, Cstruct.shift buf (line_len + 2)) + let v = Cstruct.shift buf (line_len + 1 + off) in + Some (line, v) in (* Extract SSH version from line *) let processline line = @@ -795,7 +791,7 @@ let get_version buf = (* Scan all lines until an error or SSH version is found *) let rec scan buf = match fetchline buf with - | None -> if (Cstruct.len buf) > 1024 then + | None -> if Cstruct.len buf > 1024 then error "Buffer is too big" else ok (None, buf) @@ -803,7 +799,7 @@ let get_version buf = processline line >>= function | Some peer_version -> ok (Some peer_version, buf) | None -> - if (Cstruct.len buf) > 2 then + if Cstruct.len buf > 2 then scan buf else ok (None, buf) diff --git a/lwt/awa_lwt.mllib b/lwt/awa_lwt.mllib deleted file mode 100644 index 68c7534..0000000 --- a/lwt/awa_lwt.mllib +++ /dev/null @@ -1 +0,0 @@ -Awa_lwt diff --git a/lwt/dune b/lwt/dune new file mode 100644 index 0000000..c852cee --- /dev/null +++ b/lwt/dune @@ -0,0 +1,4 @@ +(library + (name awa_lwt) + (public_name awa-lwt) + (libraries awa cstruct lwt lwt.unix mtime.clock.os)) diff --git a/mirage/awa_mirage.ml b/mirage/awa_mirage.ml new file mode 100644 index 0000000..dae5af9 --- /dev/null +++ b/mirage/awa_mirage.ml @@ -0,0 +1,136 @@ +open Lwt + +module Make (F : Mirage_flow.S) (M : Mirage_clock.MCLOCK) = struct + + module FLOW = F + + type error = [ `Msg of string + | `Read of F.error + | `Write of F.write_error ] + type write_error = [ Mirage_flow.write_error | error ] + + let pp_error ppf = function + | `Msg e -> Fmt.string ppf e + | `Read e -> F.pp_error ppf e + | `Write e -> F.pp_write_error ppf e + + let pp_write_error ppf = function + | #Mirage_flow.write_error as e -> Mirage_flow.pp_write_error ppf e + | #error as e -> pp_error ppf e + + type flow = { + flow : FLOW.flow ; + mutable state : [ `Active of Awa.Client.t | `Eof | `Error of error ] + } + + let write_flow t buf = + FLOW.write t.flow buf >>= function + | Ok () -> Lwt.return (Ok ()) + | Error w -> t.state <- `Error (`Write w) ; Lwt.return (Error (`Write w)) + + let writev_flow t bufs = + Lwt_list.fold_left_s (fun r d -> + match r with + | Error e -> Lwt.return (Error e) + | Ok () -> write_flow t d) + (Ok ()) bufs + + let read_react t = + match t.state with + | `Eof | `Error _ -> Lwt.return (Error ()) + | `Active _ -> + FLOW.read t.flow >>= function + | Error e -> t.state <- `Error (`Read e) ; Lwt.return (Error ()) + | Ok `Eof -> t.state <- `Eof ; Lwt.return (Error ()) + | Ok (`Data data) -> + match t.state with + | `Active ssh -> + begin match Awa.Client.incoming ssh (Mtime.of_uint64_ns (M.elapsed_ns ())) data with + | Error msg -> t.state <- `Error (`Msg msg) ; Lwt.return (Error ()) + | Ok (ssh', out, events) -> + let state' = if List.mem `Disconnected events then `Eof else `Active ssh' in + t.state <- state'; + writev_flow t out >>= fun _ -> + Lwt.return (Ok events) + end + | _ -> Lwt.return (Error ()) + + let rec drain_handshake t = + read_react t >>= function + | Ok es -> + begin match t.state, List.filter (function `Established _ -> true | _ -> false) es with + | `Eof, _ -> Lwt.return (Error (`Msg "disconnected")) + | `Error e, _ -> Lwt.return (Error e) + | `Active _, [ `Established id ] -> Lwt.return (Ok id) + | `Active _, _ -> drain_handshake t + end + | Error () -> match t.state with + | `Error e -> Lwt.return (Error e) + | `Eof -> Lwt.return (Error (`Msg "disconnected")) + | `Active _ -> assert false + + let rec read t = + read_react t >>= function + | Ok events -> + let r = List.fold_left (fun acc e -> + match acc, e with + | `Data d, `Channel_data (_, more) -> `Data (Cstruct.append d more) + (* TODO verify that received on same channel! *) + | `Data d, _ -> `Data d + | `Nothing, `Channel_data (_, data) -> `Data data + | `Nothing, `Channel_eof _ -> `Eof + | `Nothing, `Disconnected -> `Eof + | a, _ -> a) + `Nothing events + in + begin match r with + | `Nothing -> read t + | `Data _ | `Eof as r -> Lwt.return (Ok r) + end + | Error () -> match t.state with + | `Error e -> Lwt.return (Error e) + | `Eof -> Lwt.return (Ok `Eof) + | `Active _ -> assert false + + let close _ = + Logs.err (fun m -> m "ignoring close for now"); + Lwt.return_unit + + let writev t bufs = + let open Lwt_result.Infix in + match t.state with + | `Active ssh -> + Lwt_list.fold_left_s (fun r data -> + match r with + | Error e -> Lwt.return (Error e) + | Ok ssh -> + match Awa.Client.outgoing_data ssh data with + | Ok (ssh', datas) -> + t.state <- `Active ssh'; + writev_flow t datas >|= fun () -> + ssh' + | Error msg -> + t.state <- `Error (`Msg msg) ; + Lwt.return (Error (`Msg msg))) + (Ok ssh) bufs >|= fun _ -> () + | `Eof -> Lwt.return (Error `Closed) + | `Error e -> Lwt.return (Error (e :> write_error)) + + let write t buf = writev t [buf] + + let client_of_flow ?authenticator ~user key req flow = + let open Lwt_result.Infix in + let client, msgs = Awa.Client.make ?authenticator ~user key in + let t = { + flow = flow ; + state = `Active client ; + } in + writev_flow t msgs >>= fun () -> + drain_handshake t >>= fun id -> + (* TODO that's a bit hardcoded... *) + let ssh = match t.state with `Active t -> t | _ -> assert false in + (match Awa.Client.outgoing_request ssh ~id req with + | Error msg -> t.state <- `Error (`Msg msg) ; Lwt.return (Error (`Msg msg)) + | Ok (ssh', data) -> t.state <- `Active ssh' ; write_flow t data) >|= fun () -> + t +end diff --git a/mirage/awa_mirage.mli b/mirage/awa_mirage.mli new file mode 100644 index 0000000..6a0a0be --- /dev/null +++ b/mirage/awa_mirage.mli @@ -0,0 +1,30 @@ +(** Effectful operations using Mirage for pure SSH. *) + +(** SSH module given a flow *) +module Make (F : Mirage_flow.S) (M : Mirage_clock.MCLOCK) : sig + + module FLOW : Mirage_flow.S + + (** possible errors: incoming alert, processing failure, or a + problem in the underlying flow. *) + type error = [ `Msg of string + | `Read of F.error + | `Write of F.write_error ] + + type write_error = [ `Closed | error ] + (** The type for write errors. *) + + (** we provide the FLOW interface *) + include Mirage_flow.S + with type error := error + and type write_error := write_error + + (** [client_of_flow ~authenticator ~user key channel_request flow] upgrades the + existing connection to SSH, mutually authenticates, opens a channel and + sends the channel request. *) + val client_of_flow : ?authenticator:Awa.Keys.authenticator -> user:string -> + Awa.Hostkey.priv -> Awa.Ssh.channel_request -> FLOW.flow -> + (flow, error) result Lwt.t + +end + with module FLOW = F diff --git a/mirage/dune b/mirage/dune new file mode 100644 index 0000000..c5ee28d --- /dev/null +++ b/mirage/dune @@ -0,0 +1,5 @@ +(library + (name awa_mirage) + (public_name awa-mirage) + (wrapped false) + (libraries awa mirage-flow mirage-clock lwt mtime logs)) diff --git a/opam b/opam deleted file mode 100644 index 42675d3..0000000 --- a/opam +++ /dev/null @@ -1,39 +0,0 @@ -opam-version: "1.2" -name: "awa-ssh" -maintainer: "Christiano F. Haesbaert " -authors: "Christiano F. Haesbaert " -license: "ISC" -homepage: "https://github.com/haesbaert/awa-ssh" -bug-reports: "https://github.com/haesbaert/awa-ssh/issues" -dev-repo: "https://github.com/haesbaert/awa-ssh.git" -doc: "https://mirage.github.io/awa-ssh/api" - -available: [ocaml-version >= "4.03" & opam-version >= "1.2"] - -build: [ - [ "ocaml" "pkg/pkg.ml" "build" "--pinned" "%{pinned}%" "--tests" "false" ] -] - -build-test: [ - [ "ocaml" "pkg/pkg.ml" "build" "--pinned" "%{pinned}%" "--tests" "true" ] - [ "ocaml" "pkg/pkg.ml" "test" ] -] - -depends: [ - "ocamlfind" {build} - "ocamlbuild" {build} - "topkg" {build} - "topkg-care" {build} - "ppx_sexp_conv" {build} - "ppx_tools" {build} - "nocrypto" - "x509" - "cstruct" {>= "1.9.0"} - "cstruct-unix" - "sexplib" - "ipaddr" {>= "2.5.0"} - "tcpip" {>= "3.0.0"} - "rresult" - "io-page" {test} - "mtime" -] diff --git a/pkg/META b/pkg/META deleted file mode 100644 index 9a0300b..0000000 --- a/pkg/META +++ /dev/null @@ -1,8 +0,0 @@ -version = "%%VERSION_NUM%%" -description = "Pure SSH library implmentation in ocaml" -requires = "cstruct sexplib result nocrypto ipaddr lwt rresult x509 mtime" -archive(byte) = "awa.cma" -plugin(byte) = "awa.cma" -archive(native) = "awa.cmxa" -plugin(native) = "awa.cmxs" -exists_if = "awa.cma" diff --git a/pkg/pkg.ml b/pkg/pkg.ml deleted file mode 100644 index 3c6ac56..0000000 --- a/pkg/pkg.ml +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env ocaml -#use "topfind" -#require "topkg" -open Topkg - -let () = - let opams = - [ Pkg.opam_file "opam" - ~lint_deps_excluding:(Some ["ppx_tools" ; "ppx_sexp_conv"]) ] - in - - Pkg.describe ~opams "awa-ssh" @@ fun c -> - let exts = Exts.(cmx @ library @ exts [".cmi" ; ".cmt" ]) in - Ok [ - Pkg.lib ~exts "lib/awa" ; - Pkg.test "test/test"; - Pkg.test ~run:false "test/awa_test_server"; - - (* Lwt bindings *) - Pkg.lib ~exts "lwt/awa_lwt"; - Pkg.test ~run:false "test/awa_lwt_server" - ] diff --git a/test/awa_gen_key.ml b/test/awa_gen_key.ml new file mode 100644 index 0000000..5801a40 --- /dev/null +++ b/test/awa_gen_key.ml @@ -0,0 +1,26 @@ + +let gen_key seed = + Nocrypto_entropy_unix.initialize (); + let b64s x = Cstruct.to_string (Nocrypto.Base64.encode x) in + let seed = match seed with + | None -> b64s (Nocrypto.Rng.generate 30) + | Some x -> x + in + Printf.printf "seed is %s\n" seed; + let hostkey = Awa.Keys.of_seed seed in + let pub = Awa.Hostkey.pub_of_priv hostkey in + let public = Awa.Wire.blob_of_pubkey pub in + Printf.printf "ssh-rsa %s awa@awa.local\n" (b64s public); + Ok () + +open Cmdliner + +let seed = + let doc = "Seed for private key." in + Arg.(value & opt (some string) None & info [ "seed" ] ~doc) + +let cmd = + Term.(term_result (const gen_key $ seed)), + Term.info "albatross_stat_client" ~version:"%%VERSION_NUM%%" + +let () = match Term.eval cmd with `Ok () -> exit 0 | _ -> exit 1 diff --git a/test/awa_test_client.ml b/test/awa_test_client.ml new file mode 100644 index 0000000..98643ad --- /dev/null +++ b/test/awa_test_client.ml @@ -0,0 +1,103 @@ +(* + * Copyright (c) 2019 Hannes Mehnert + * + * All rights reversed + *) + +let () = Printexc.record_backtrace true + +open Rresult.R +open Awa + +let read_cstruct fd = + let len = Ssh.max_pkt_len in + let buf = Bytes.create len in + let n = Unix.read fd buf 0 len in + if n = 0 then + failwith "got EOF" + else + let cbuf = Cstruct.create n in + Cstruct.blit_from_bytes buf 0 cbuf 0 n; + Logs.debug (fun m -> m "read %d bytes" (Cstruct.len cbuf)); + cbuf + +let write_cstruct fd buf = + let len = Cstruct.len buf in + let bytes = Bytes.create len in + Cstruct.blit_to_bytes buf 0 bytes 0 len; + let n = Unix.write fd bytes 0 len in + assert (n > 0) + +let jump _ user seed authenticator host port = + Nocrypto_entropy_unix.initialize (); + let fd = Unix.(socket PF_INET SOCK_STREAM 0) in + Unix.(connect fd (ADDR_INET (inet_addr_of_string host, port))); + match + Keys.authenticator_of_string authenticator >>= fun authenticator -> + let t, out = Client.make ~authenticator ~user (Keys.of_seed seed) in + List.iter (write_cstruct fd) out; + let rec read_react t = + let data = read_cstruct fd in + let now = Mtime_clock.now () in + Client.incoming t now data >>= fun (t, replies, events) -> + List.iter (write_cstruct fd) replies; + let t, cont = List.fold_left (fun (t, cont) -> function + | `Established id -> + begin match Client.outgoing_request t ~id (Ssh.Exec "ls\ /tmp/bla") with + | Error e -> + Logs.err (fun m -> m "couldn't request ls: %s" e) ; t, cont + | Ok (t', data) -> write_cstruct fd data ; t', cont + end + | `Disconnected -> Unix.close fd ; t, false + | `Channel_data (_, data) -> + Logs.app (fun m -> m "channel data: %s" (Cstruct.to_string data)) ; + t, cont + | e -> + Logs.info (fun m -> m "received event %a" Client.pp_event e) ; + t, cont) + (t, true) events + in + if cont then read_react t else Ok "disconnected" + in + read_react t + with + | Ok x -> Logs.app (fun m -> m "all good %s" x) ; Ok () + | Error msg -> Error (`Msg msg) + +let setup_log style_renderer level = + Fmt_tty.setup_std_outputs ?style_renderer (); + Logs.set_level level; + Logs.set_reporter (Logs_fmt.reporter ~dst:Format.std_formatter ()) + +open Cmdliner + +let user = + let doc = "username to use" in + Arg.(value & opt string "hannes" & info [ "user" ] ~doc) + +let seed = + let doc = "private key seed" in + Arg.(value & opt string "180586" & info [ "seed" ] ~doc) + +let authenticator = + let doc = "authenticator" in + Arg.(value & opt string "" & info [ "authenticator" ] ~doc) + +let host = + let doc = "remote host" in + Arg.(value & opt string "127.0.0.1" & info [ "host" ] ~doc) + +let port = + let doc = "remote port" in + Arg.(value & opt int 22 & info [ "port" ] ~doc) + +let setup_log = + Term.(const setup_log + $ Fmt_cli.style_renderer () + $ Logs_cli.level ()) + +let cmd = + Term.(term_result (const jump $ setup_log $ user $ seed $ authenticator $ host $ port)), + Term.info "awa_test_client" ~version:"%%VERSION_NUM" + +let () = match Term.eval cmd with `Ok () -> exit 0 | _ -> exit 1 diff --git a/test/awa_test_server.ml b/test/awa_test_server.ml index c7452d2..7808a42 100644 --- a/test/awa_test_server.ml +++ b/test/awa_test_server.ml @@ -32,6 +32,7 @@ let read_cstruct fd () = else let cbuf = Cstruct.create n in Cstruct.blit_from_bytes buf 0 cbuf 0 n; + Format.printf "read %d bytes\n%!" (Cstruct.len cbuf); cbuf let write_cstruct fd buf = @@ -71,6 +72,7 @@ let rec serve t cmd = | Disconnected s -> ok (printf "Disconnected: %s\n%!" s) | Channel_eof id -> ok (printf "Channel %ld EOF\n%!" id) | Channel_data (id, data) -> + printf "channel data %d\n%!" (Cstruct.len data); (match cmd with | None -> serve t cmd | Some "echo" -> @@ -80,7 +82,9 @@ let rec serve t cmd = echo t id data >>= fun t -> serve t cmd | Some "bc" -> bc t id data >>= fun t -> serve t cmd | _ -> error "Unexpected cmd") - | Channel_exec (id, exec) -> match exec with + | Channel_exec (id, exec) -> + printf "channel exec %s\n%!" exec; + match exec with | "suicide" -> Driver.disconnect t >>= fun _ -> ok () | "ping" -> Driver.send_channel_data t id (Cstruct.of_string "pong\n") >>= fun t -> @@ -96,7 +100,7 @@ let user_db = (* User foo auths by passoword *) let foo = Auth.make_user "foo" ~password:"bar" [] in (* User awa auths by pubkey *) - let fd = Unix.(openfile "test/awa_test_rsa.pub" [O_RDONLY] 0) in + let fd = Unix.(openfile "test/data/awa_test_rsa.pub" [O_RDONLY] 0) in let file_buf = Unix_cstruct.of_fd fd in let key = get_ok (Wire.pubkey_of_openssh file_buf) in Unix.close fd; diff --git a/test/awa_test_rsa b/test/data/awa_test_rsa similarity index 100% rename from test/awa_test_rsa rename to test/data/awa_test_rsa diff --git a/test/awa_test_rsa.pub b/test/data/awa_test_rsa.pub similarity index 100% rename from test/awa_test_rsa.pub rename to test/data/awa_test_rsa.pub diff --git a/test/kex.packet b/test/data/kex.packet similarity index 100% rename from test/kex.packet rename to test/data/kex.packet diff --git a/test/dune b/test/dune new file mode 100644 index 0000000..842b06f --- /dev/null +++ b/test/dune @@ -0,0 +1,39 @@ +(executable + (name test) + (public_name test) + (modules test) + (package awa) + (libraries awa mtime.clock.os cstruct-unix)) + +(alias + (name runtest) + (deps (source_tree data) (:< test.exe)) + (action (run %{<}))) + +(executable + (name awa_test_server) + (public_name awa_test_server) + (modules awa_test_server) + (package awa) + (libraries awa mtime.clock.os cstruct-unix)) + +(executable + (name awa_test_client) + (public_name awa_test_client) + (modules awa_test_client) + (package awa) + (libraries awa nocrypto.unix mtime.clock.os cmdliner fmt.tty logs.fmt logs.cli fmt.cli)) + +(executable + (name awa_lwt_server) + (public_name awa_lwt_server) + (modules awa_lwt_server) + (package awa-lwt) + (libraries awa awa-lwt mtime.clock.os cstruct-unix)) + +(executable + (name awa_gen_key) + (public_name awa_gen_key) + (modules awa_gen_key) + (package awa) + (libraries awa nocrypto.unix cmdliner)) diff --git a/test/test.ml b/test/test.ml index 3059f1f..94b3085 100644 --- a/test/test.ml +++ b/test/test.ml @@ -46,7 +46,7 @@ let cipher_key_of cipher key iv = | Plaintext -> { cipher = Plaintext; cipher_key = Plaintext_key } | Aes128_ctr | Aes192_ctr | Aes256_ctr -> - let iv = Nocrypto.Cipher_block.Counters.C128be.of_cstruct iv in + let iv = Nocrypto.Cipher_block.AES.CTR.ctr_of_cstruct iv in { cipher; cipher_key = Aes_ctr_key ((CTR.of_secret key), iv) } | Aes128_cbc | Aes192_cbc | Aes256_cbc -> @@ -261,7 +261,7 @@ let t_parsing () = let t_key_exchange () = (* Read a pcap file and see if it makes sense. *) - let file = "test/kex.packet" in + let file = "data/kex.packet" in let fd = Unix.(openfile file [O_RDONLY] 0) in let buf = Unix_cstruct.of_fd fd in let pkt, _ = get_some @@ get_ok @@ decrypt_plain buf in @@ -384,7 +384,7 @@ let t_crypto () = test_ok let t_openssh_pub () = - let fd = Unix.(openfile "test/awa_test_rsa.pub" [O_RDONLY] 0) in + let fd = Unix.(openfile "data/awa_test_rsa.pub" [O_RDONLY] 0) in let file_buf = Unix_cstruct.of_fd fd in let key = get_ok (Wire.pubkey_of_openssh file_buf) in let buf = Wire.openssh_of_pubkey key in @@ -550,8 +550,8 @@ let t_channel_output () = let t_openssh_client () = let s1 = "Georg Wilhelm Friedrich Hegel" in let s2 = "Karl Marx" in - let ossh_cmd = "ssh -p 18022 awa@127.0.0.1 -i test/awa_test_rsa echo" in - let awa_cmd = "./_build/test/awa_test_server.native" in + let ossh_cmd = "ssh -p 18022 awa@127.0.0.1 -i data/awa_test_rsa echo" in + let awa_cmd = "./_build/default/test/awa_test_server.exe" in let awa_args = Array.of_list [] in let null = Unix.openfile "/dev/null" [ Unix.O_RDWR ] 0o666 in ignore @@ Unix.system "pkill awa_test_server"; @@ -604,5 +604,5 @@ let all_tests = [ let _ = Nocrypto.Rng.reseed (Cstruct.of_string "180586"); Sys.set_signal Sys.sigalrm (Sys.Signal_handle (fun _ -> failwith "timeout")); - Unix.chmod "test/awa_test_rsa" 0o600; + Unix.chmod "data/awa_test_rsa" 0o600; List.iter run_test all_tests;