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.
- It creates an address for the server - an
INET
address on0.0.0.0
bound to the specified port number. - Creates a socket (read: special file descriptor that can be read and written into) bound to this address
-
listen
andaccept
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.
-
Get
OpenSSL
for your distro -
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.
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!