Search code examples
ssltcperlangelixirtls1.3

Simple SSL server-client connection in Elixir


I'm working on a toy networking project and I want to add a TLS layer between the server and the client. I'm getting handshake errors that I'm trying to figure out how to debug.

The TL;DR is probably: 'what arguments do I pass to :ssl.listen/2' but here is the minimal example.

First I create a new project with mix new tls_question.

I have added :crypto and :ssl to mix.exs like so:

def application do
    [
      extra_applications: [:logger, :crypto, :ssl]
    ]
end

I have then generated an SSL certificate with

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365

and moved key.pem and cert.pem into the project folder.

I then have the following minimal program

defmodule TlsQuestion do
  @ip {127,0,0,1}
  @port 4343
  def main do
    :ssl.start()
    {:ok, listen_socket} = :ssl.listen(@port,
      [ certs_keys: [
          keyfile: "key.pem",
          certfile: "cert.pem",
          password: "CorrectHorseBatteryStaple"
        ],
        reuseaddr: true
      ])
    spawn(fn -> client() end)
    {:ok, accept_socket} = :ssl.transport_accept(listen_socket)
    {:ok, accept_socket} = :ssl.handshake(accept_socket)
    :ssl.send(accept_socket, "Hello World")
  end
  def client() do
    {:ok, connect_socket} = :ssl.connect(@ip, @port,
                              [verify: :verify_peer,
                              cacertfile: "cert.pem",
                              active: :once], :infinity)
    message = :ssl.recv(connect_socket, 0)
    IO.puts(message)
  end
end
TlsQuestion.main()

From which I call mix run.

The error message might be enlightening for some but hasn't helped me

== Compilation error in file lib/tls_question.ex ==
** (exit) exited in: :gen_statem.call(#PID<0.164.0>, {:start, :infinity}, :infinity)
    ** (EXIT) an exception was raised:
        ** (FunctionClauseError) no function clause matching in :ssl_config.key_conf/1
            (ssl 10.8.7) ssl_config.erl:181: :ssl_config.key_conf({:keyfile, "key.pem"})
            (ssl 10.8.7) ssl_config.erl:72: :ssl_config.cert_key_pair/3
            (stdlib 4.2) lists.erl:1315: :lists.map/2
            (ssl 10.8.7) ssl_config.erl:56: :ssl_config.init_certs_keys/3
            (ssl 10.8.7) ssl_config.erl:51: :ssl_config.init/2
            (ssl 10.8.7) ssl_gen_statem.erl:164: :ssl_gen_statem.ssl_config/3
            (ssl 10.8.7) tls_connection.erl:150: :tls_connection.init/1
            (stdlib 4.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
    (stdlib 4.2) gen.erl:243: :gen.do_call/4
    (stdlib 4.2) gen_statem.erl:900: :gen_statem.call_dirty/4
    (ssl 10.8.7) ssl_gen_statem.erl:1239: :ssl_gen_statem.call/2
    (ssl 10.8.7) ssl_gen_statem.erl:234: :ssl_gen_statem.handshake/2
    lib/tls_question.ex:16: TlsQuestion.main/0

It looks like it's complaining about something I'm doing with the certificate and key files?

I've passed the certificate to the client as the CA certificate chain (since a self-signed certificate is its own certificate chain). Could that be the issue?


Solution

  • The TL;DR is probably: 'what arguments do I pass to :ssl.listen/2'

    listen(Port, Options) -> {ok, ListenSocket} | {error, reason()}
    

    Port is defined to be an integer:

    0...65535
    

    Options is defined to be a list:

    Options = [tls_server_option()]
    
    tls_server_option() = 
        server_option() |
        common_option() |
        socket_option() |
        transport_option()
    
    common_option() = ...| {certs_keys, certs_keys()} | ...
    
    certs_keys() = [cert_key_conf()]
    
    cert_key_conf() = 
        #{cert => cert(),
          key => key(),
          certfile => cert_pem(),
          keyfile => key_pem(),
          password => key_pem_password()}
    

    Note that cert_key_conf() is an erlang map, giving you this structure in elixir:

      {:ok, listen_socket} = :ssl.listen(@port,
          [ certs_keys: [%{
    
             
            }],
            reuseaddr: true
          ]) 
    

    Continuing with the type descriptions:

     cert_pem() = file:filename() = string() => list of integers
     key_pem() = filename() = string() => list of integers
     key_pem_pasword() = io_list() => possibly nested list of integers and/or binaries
    

    In erlang, the string() type is a list of integers.

                           elixir             erlang
                           ------             ------
     list of itegers:      single quotes      double quotes
     binaries:             double quotes,     the syntax <<1,34,97>>
                           or <<97,98,99>>
    

    Starting at the bottom of the type specifications listed above and substituting upwards, gives you:

      {:ok, listen_socket} = :ssl.listen(@port,
          [ certs_keys: [%{
              keyfile: 'key.pem',
              certfile: 'cert.pem',
              password: 'CorrectHorseBatteryStaple'
            }],
            reuseaddr: true
          ])
    

    I'm not sure whether you can omit the keys cert: and key: in the map. Also, the docs don't list reuseaddr as a valid 2-tuple in the Options list.