Quick walkthrough: Encrypting connections with ocaml-tls


#1

Following up on this tweet

Indeed, it was a fun evening live coding a simple TLS server with a bunch of friends. This post would hopefully serve as a quick tutorial on getting started with ocaml-tls.

To follow this tutorial, you’ll need esy and the pesy@next.

npm i -g esy pesy@next 

If all you’re looking for is the code from the call, here you go.

Getting started

We used the unstable release of pesy to bootstrap a new project.

pesy -d my-new-project

Promises

We chose lwt because ocaml-tls has useful helpers. Async, while having more interesting features, would require more effort to get started.

Adding the dependency

esy add @opam/lwt

Importing the dependency

Configure Dune, the build tool, to link Lwt from the package.json.

   "buildDirs": {
     "library": {},
     "bin": {
-      "imports": [ "Library = require('my-new-project/library')" ],
+      "imports": [
+        "Lwt = require('@opam/lwt')",
+        "LwtUnix = require('@opam/lwt/unix')",
+        "Library = require('my-new-project/library')"
+      ],
       "bin": { "MyNewProjectApp": "MyNewProjectApp.re" }
     }
   }

async/await syntax for promises - Lwt.Syntax

We open Lwt.Syntax for convenient async/await like experience from the JS world.

Quick sample of what it looks like

// In bin/MyNewProjectApp.re
open Lwt.Syntax;

let main = () => {
  let* num = Lwt.return(10);
  Lwt.return(num + 1);
};

main() |> Lwt_main.run |> print_int;
print_newline();

Running esy start should display 11.

Adding ocaml-tls

Use esy to install the dependency. The package is available as tls on opam registry

esy add @opam/tls

Along with tls, we link in the lwt sub-library too. So, in the package.json

     "bin": {
       "imports": [
         "Lwt = require('@opam/lwt')", "LwtUnix = require('@opam/lwt/unix')",
+       "Tls = require('@opam/tls')", "TlsLwt = require('@opam/tls/lwt')",
         "Library = require('my-new-project/library')"
       ],

Writing a simple TCP Server

TLS can be used to make protocols. In this tutorial, we’ll only try to encrypt a TCP server.
A simple echo server looks like this.

open Lwt.Syntax;
open Lwt_unix;
let backlog = 10;
let serverPort = 8000;

let rec unEncryptedHandle = (outputChannel, inChannel) => {
  let* userInput = Lwt_io.read_line(inChannel);
  let* () =
    Lwt_io.write_line(outputChannel, "Server also replied: " ++ userInput);
  unEncryptedHandle(outputChannel, inChannel);
};

let rec processConnection = unEncryptedSocket => {
  let* (fd, _sockaddr) = accept(unEncryptedSocket);
  let outputChannel = Lwt_io.of_fd(~mode=Lwt_io.output, fd);
  let inputChannel = Lwt_io.of_fd(~mode=Lwt_io.input, fd);
  unEncryptedHandle(outputChannel, inputChannel);
};

let main = () => {
  let sockaddr = Unix.ADDR_INET(Unix.inet_addr_any, serverPort);
  let socket = socket(PF_INET, SOCK_STREAM, 0);
  setsockopt(socket, SO_REUSEADDR, true);
  let* () = bind(socket, sockaddr);
  listen(socket, backlog);
  processConnection(socket);
};

Lwt_main.run(main());

This is standard socket program.

  1. It creates an address for the server - an INET address on 0.0.0.0 bound to the specified port number.
  2. Creates a socket (read: special file descriptor that can be read and written into) bound to this address
  3. listen and accept incoming connections - every time something is read, it is written back.

We can use telnet to test the echo interaction.

telnet 0.0.0.0 8000

Type away anything and have it echoed back!

Encrypting those IO channels

This is what you’ve been waiting for!

Well frankly, ocaml-tls does all the heavy lifting behind the scenes if all you want to do is play with a toy TLS server.

Creating Certificates

TLS is sophisticated protocol layer that is capable of a lot of things - the Diffie Hellman Ephemeral exchange of keys, RSA public and private keys, optional x509 authentication and negotiation. This warrants an entire post altogether. We’ll simply create a public/private key pair, a certifcate and sign it ourselves. This is enough of local testing.

  1. Get OpenSSL for your distro

  2. Use the following command to create the key pair, the signing request and the CA file.

    openssl req \
      -x509 \
      -sha256 
      -nodes \
      -days 365 \
      -newkey rsa:4096 \
      -keyout myserver.pem \
      -out myserver.crt
    

    Brief explanation
    When a TLS connection is initiated, the server and a client need a way exchange secret keys. It does so initially with asymmetric encryption but later switches to the much faster symmetric AES encryption.

    The initial encrypted channel needs public keys and public keys are to be treated with suspicion. There are two known ways to “bootstrapping” trust.

    1. Public Key Infrastructure (PKI)
    2. Web of Trust

    I have seen many assign attributes of centralisation of trust to PKI and Web of Trust as it’s decentralised alternative. I think it’s fair approximation.

    TLS uses PKI and requires a trusted authority to sign what you claim is your public key. For local testing, we sign the public key ourselves. The command does exactly that - a self signed certificate that is valid for 365 days.

Tls_lwt.accept_ext

If the key take away from this post is to be summarised in one-line, that would be: ocaml-tls provides that single function to turn unencrypted socket file descriptor to an encrypted one. Thats it!

processConnection in the earlier snippet needs essentially this one line.

let* (encryptedLwtIOChannels, _socketAddr) =
    Tls_lwt.accept_ext(config, unEncryptedSocket);

Of course, we need to configure Tls_lwt.accept_ext with your keys and some other essential properties.

let rec processConnection = unEncryptedSocket => {
  let serverCert = "./certs/myserver.crt";
  let serverKey = "./certs/myserver.pem";
  let* cert = X509_lwt.private_of_pems(~cert=serverCert, ~priv_key=serverKey);
  let config =
    Tls.Config.server(
      ~reneg=true, 
      ~certificates=`Single(cert), 
      ~authenticator=nullAuth, // optional x509 authentication
      (),
    );
  let* (encryptedLwtIOChannels, _socketAddr) =
    Tls_lwt.accept_ext(config, unEncryptedSocket);
  let (encrypedInputChannel, encryptedOutputChannel) = encryptedLwtIOChannels;
  encryptedHandle(encryptedOutputChannel, encrypedInputChannel);
};

Tls.Config.server asks for a Tls__Config that negotiates for the highest available encryption algorithms and avoids the optional x509 authenticator.

encryptedHandle is the same echo logic we saw with the TCP server.

let rec encryptedHandle = (encryptedOutputChannel, encryptedInputChannel) => {
  let* msg = Lwt_io.read_line(encryptedInputChannel);
  let* () =
    Lwt_io.write_line(encryptedOutputChannel, "SERVER also said: " ++ msg);
  encryptedHandle(encryptedOutputChannel, encryptedInputChannel);
};

That’s it!

TLS servers are usually tested with the following

openssl s_client -connect localhost:4433

Any input at the prompt would be echoed back.

But since we use self signed certificates, we’d need the certificate we in the openssl command

 openssl s_client  \
  -connect localhost:4433 \
  -CAfile /path/to/project/root/certs/myserver.crt

Complete source code - https://github.com/ManasJayanth/reason-ocaml-tls-tutorial-meet

Happy hacking!