mirage / ocaml-cohttp

An OCaml library for HTTP clients and servers using Lwt or Async
Other
696 stars 172 forks source link

Provide an example of a tls client #479

Open zoggy opened 8 years ago

zoggy commented 8 years ago

Hello,

It would be great to have an example of a client (not a server) connecting to a server and authenticating with certificates.

This is partly related to https://github.com/mirage/ocaml-cohttp/issues/471 .

cc @hannesm

hannesm commented 8 years ago

while I won't have time for this, here are some pointers (first, conduit would need support for certificates):

(or am I misguided and is there a cohttp API not involving conduit to do HTTP client connections? if so, you could use tls_lwt).

A simple example using OCaml-TLS and client certificates is available at https://github.com/mirleft/ocaml-tls/blob/master/lwt/examples/echo_client.ml -- just read private key and certificate chain (line 14-16) and pass them to the client config (line 19).

zoggy commented 8 years ago

Thanks for your answer. Indeed I found no way in cohttp API to plug TLS connection.

Maybe a simple way would be to add a connected_call function to Cohttp_lwt.Make_client which would be like call but would take (ic, oc) in parameter, rather than ctx and uri ?

rgrinberg commented 8 years ago

That definitely sounds like the way forward. I'd like to make cohttp usable without conduit anyway.

zoggy commented 8 years ago

I'll try to set up an example with such a change.

zoggy commented 8 years ago

@hannesm I'm starting from the https://github.com/mirleft/ocaml-tls/blob/master/lwt/examples/echo_client.ml example but I'm getting tls errors:

 (record-in (((content_type ALERT) (version (Supported TLS_1_2))) "\002("))

(alert-in (FATAL HANDSHAKE_FAILURE))

(record-out (ALERT "\001\000"))

(ok-alert-out HANDSHAKE_FAILURE)

TLS ALERT (remote end): HANDSHAKE_FAILURE
Fatal error: exception Tls_lwt.Tls_alert(6)

Since I don't known which files to use as server certificates, I commented out some code:

 ...
   | Some f      -> `Ca_file f) >>= fun authenticator ->
  (*X509_lwt.private_of_pems
    ~cert:server_cert
    ~priv_key:server_key >>= fun certificate ->*)
  Tls_lwt.connect_ext
    ~trace:eprint_sexp
    Tls.Config.(client ~authenticator (*~certificates:(`Single certificate)*) ~ciphers:Ciphers.supported ())
    (host, port) >>= fun (ic, oc) ->
...

but this does not solve the problem.

Are these server certificates required ?

hannesm commented 8 years ago

not sure what your other endpoint is. the client uses server_cert, defined in ex_common.ml in the same directory, which read the (nowadays invalid ./certificates/server.pem -- assuming your cwd is a ocaml-tls git checkout).

you can generate your own certificate and private key and either overwrite server.pem and server.key or change the server_cert / server_key in echo_client.ml with full paths to your custom ones.

zoggy commented 8 years ago

But are these server certificates required ? By now I just use

Tls_lwt.connect ~trace:eprint_sexp authenticator (host, port) >>= fun (ic, oc) ->

so no server certificate is involved. My problem seems to be that the PEM file I use as authentication certificate is not valid/recognized. This PEM file is exported from firefox, but as you may have guessed already I'm not a TLS specialist and I wonder if this is a correct certificate to use.

hannesm commented 8 years ago

I've no clue about your scenario, and your paste doesn't include the actual error.

Depending on your scenario, you need on the client:

if you provide only an authenticator (as mentionde above), there won't be any client certificate involved. You'll have to use Tls_lwt.connect_ext (and provide a Tls.Config.client manually).

zoggy commented 8 years ago

From what you say, the certificates/server* files are used to authenticate the client. I thought they were used to authenticate the server :-) And I thought the authenticator was used to authenticate the client, not the server... Thanks for the explanation.

Now when I use the server.* files from https://github.com/mirleft/ocaml-tls/tree/master/certificates, it seems that they are validated. Thanks, I will now struggle with openssl to create these server.key and server.pem files from my certificate stored in firefox... The aim is be able to retrieve some of my data using

 ./cohttp_lwt_client zoggy.databox.me 443
zoggy commented 8 years ago

In fact https://databox.me seems to be the only host for which the connection fails with these errors:

(record-in (((content_type ALERT) (version (Supported TLS_1_2))) "\002("))

(alert-in (FATAL HANDSHAKE_FAILURE))

(record-out (ALERT "\001\000"))

(ok-alert-out HANDSHAKE_FAILURE)

TLS ALERT (remote end): HANDSHAKE_FAILURE
Fatal error: exception Tls_lwt.Tls_alert(6)

Don't know why but it's no luck as it is the one I'm interested in.

hannesm commented 8 years ago

could you provide a bit more output please? the lines you pasted do not include the actual problem... otoh I can try to connect myself to databox and debug... maybe tomorrow..

zoggy commented 8 years ago

These lines are the only output I have :-/ Is there a way to get more ? I will post the code somewhere tomorrow if you need it.

zoggy commented 8 years ago

@rgrinberg The solution to add a connected_call function to Cohttp_lwt_s.Client was not a great idea, because other modules use this module signature, and some cannot have such a function in their interface (XHR modules, for example).

But exploring the Cohttp code, I came with another solution which does not require modifying Cohttp. I define a new Net_tls module, with same interface as Cohttp_lwt_unix_net. I also need a IO module not using Conduit:

module IO =
  struct
    type 'a t = 'a Lwt.t
    let (>>=) = Lwt.bind
    let return = Lwt.return

    type ic = Lwt_io.input_channel
    type oc = Lwt_io.output_channel
    type conn = ic * oc

    let read_line ic = Lwt_io.read_line_opt ic
    let read ic count = Lwt_io.read ~count ic
    let write oc buf = Lwt_io.write oc buf
    let flush oc = Lwt_io.flush oc
  end

module Tls_net =
  struct
    module IO = IO
    type ctx = Tls.Config.client Lwt.t
    let sexp_of_ctx _ = Sexplib.Sexp.Atom "tls ctx"
    let default_ctx =
      X509_lwt.authenticator `No_authentication_I'M_STUPID >>=
      fun authenticator ->
          Lwt.return
            (Tls.Config.(client
              ~authenticator ~ciphers:Ciphers.supported ()))

    let connect_uri ~ctx uri =
      let host = match Uri.host uri with None -> "" | Some s -> s in
      let port = match Uri.port uri with None -> 443 | Some n -> n in
      ctx >>= fun client ->
      Tls_lwt.connect_ext
        ~trace:eprint_sexp
        client (host, port)
        >>= fun (ic, oc) -> Lwt.return ((ic, oc), ic, oc)

    let close c = Lwt.catch (fun () -> Lwt_io.close c) (fun _ -> return_unit)
    let close_in ic = ignore_result (close ic)
    let close_out oc = ignore_result (close oc)
    let close ic oc = ignore_result (close ic >>= fun () -> close oc)
end

Type ctx uses Lwt.t because default_ctx require a default authenticator which cannot be obtained without Lwt. @hannesm Would it be possible to have a value like default_authenticator to avoid this ?

Then I get a Client module:

module Client = Cohttp_lwt.Make_client (IO) (Tls_net)

I can then define my own function call which uses the Client.call function provided by Cohttp:

let call meth ?ca ?body ?chunked ?headers iri =
  let%lwt authenticator = X509_lwt.authenticator
    (match ca with
     | None        -> `Ca_dir server_cert_dir
     | Some "NONE" -> `No_authentication_I'M_STUPID
     | Some f      -> `Ca_file f)
  in
  let%lwt certificate =
    X509_lwt.private_of_pems ~cert:server_cert ~priv_key:server_key
  in
  let ctx = Lwt.return
    (Tls.Config.client
     ~authenticator ~certificates:(`Single certificate)
       ~ciphers:Tls.Config.Ciphers.supported ()
    )
  in
  Client.call ~ctx ?body ?chunked ?headers meth
    (Uri.of_string (Iri.to_uri iri))

let get = call `GET
let delete = call `DELETE
let post = call `POST
let put = call `PUT
let patch = call `PATCH

This function can be defined differently according to your application context: here each connection creates a new context (i.e. a Tls.Config.client) by looking at the ?ca argument and certificate on disk.

Finally, I could tls-authenticate successfully to my account on https://rww.io to create a ressource by a POST request :-)

zoggy commented 8 years ago

Regarding the default authenticator, now I use X509.Authenticator.null so that type ctx can be defined without Lwt.t:

type ctx = Tls.Config.client [@@deriving sexp_of]
let default_ctx =
   let authenticator = X509.Authenticator.null in
   Tls.Config.(client ~authenticator ~ciphers:Ciphers.supported ())
hannesm commented 8 years ago

@zoggy both are not good ideas: the null authenticator always returns true, don't do that. both chain_of_trust (which takes a time and a list of trusted CA certificates) and server_key_fingerprint (look here) are not inside of Lwt.t (but those which read files etc. over here are inside of Lwt.t).

you shouldn't pass ~ciphers to client unless you are really sure what you are doing. the default list of ciphers in OCaml-TLS is well curated.

EDIT: and there is no such thing as a default authenticator: depending on your application you have to choose which strategy to use (and which CA certificates are trustworthy).

zoggy commented 8 years ago

Ok, thanks. So I could use a default authenticator built with chain_of_trust [] so that it will always fail, forcing the developer to specify a context (a default context is required to conform to Cohttp_lwt_unix_net interface).

Ok for the ciphers, I had just copy-pasted from your example.

hannesm commented 8 years ago

oh, thanks... I just removed the ~ciphers from our echo_client example.

providing a default context with an empty set of trust anchors in chain_of_trust sounds sensible for now

zoggy commented 8 years ago

Did you try your echo example on https://databox.me ?

hannesm commented 8 years ago

@zoggy I just tried echo_client, and https://www.ssllabs.com/ssltest/analyze.html?d=databox.me tells me that they only support 3 ciphersuites, all using ECDHE for the key exchange (and unfortunately neither nocrypto nor OCaml-TLS has EC support at the moment, it is planned)

talex5 commented 4 years ago

I spent a bit of time last week investigating how to use cohttp with https in 0install. Here are my notes on how to get it working using cohttp_lwt_unix.

First, you must not use ocaml-tls in the default configuration, because conduit disables certificate validation in this mode and you cannot override it. See https://github.com/mirage/ocaml-conduit/blob/2aa5d06fc81cd74dc56553cd490a9c23cb538680/lwt-unix/conduit_lwt_tls_real.ml#L33

To avoid this, call Cohttp_lwt.Make_client with your own Net implementation that overrides connect_uri with one that forces the use of Conduit_lwt_unix_ssl, or invokes ocaml-tls itself.

By default, openssl uses CA certificate paths hard-coded at compile time. As these are different on every Linux system, if you want portable binaries then you will need to search for the right paths yourself. Here's the code I use for that:

https://github.com/0install/0install/blob/6c0f5c51bc099370a367102e48723a42cd352b3b/ocaml/zeroinstall/http.cohttp.ml#L4-L66

To use your custom context with the correct CAs you'll need to avoid Conduit_lwt_unix and go directly to Conduit_lwt_unix_ssl. Unfortunately, there is no way to turn the result of this into a Conduit_lwt_unix.flow as required by cohttp (since the flow type is private). You will hit the same problem if you used ocaml-tls directly.

Luckily, cohttp doesn't actually use the flow for anything, so you can override the IO module with your own implementation that doesn't use it. See https://github.com/0install/0install/blob/6c0f5c51bc099370a367102e48723a42cd352b3b/ocaml/zeroinstall/http.cohttp.ml#L68-L111 for some suitable Net and IO modules.

mseri commented 3 years ago

@samoht did your change to use ca-cert somewhat solve this issue?