2

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?

  • Since it’s [tag:erlang], I bet one should use charlists instead of binaries. Simply change double quotes to single ones there and everywhere: `keyfile: "key.pem"` → `keyfile: 'key.pem'`. – Aleksei Matiushkin Feb 24 '23 at 13:59

1 Answers1

1

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.

7stud
  • 46,922
  • 14
  • 101
  • 127
  • Thanks, this fixed the problem form me on MacOS. On Ubuntu, I am running into following error ```== Compilation error in file lib/tls_question.ex == ** (exit) :badarg (kernel 8.2) inet_tcp.erl:145: :inet_tcp.listen/2 (ssl 10.6.1) tls_socket.erl:79: :tls_socket.listen/3 (ssl 10.6.1) ssl.erl:631: :ssl.listen/2 lib/tls_question.ex:7: TlsQuestion.main/0``` – Freddie Woodruff Feb 24 '23 at 19:36
  • @FreddieWoodruff -- It's a bit confounding how you can get a `badarg` error when running on Ubunto but not macOS. Are you sure it's the exact same code? Can you post your Ubuntu code and the error message at the bottom of your original post and use code tags to preserve the indenting? Also, put a comment in your code to indicate which line the error message is referring to. Thanks. – 7stud Feb 25 '23 at 20:15
  • I posted the question to elixirforum.com and got the answer. I was running OTP version 21 on the Linux OSes and OTP version 25 on MacOS. The solution is to pass the :keyfile and :certfile arguments directly rather than in a :certs_keys map. – Freddie Woodruff Feb 26 '23 at 11:28
  • @FreddieWoodruff -- If you don't have the erlang documentation on your OS, you can go here: https://www.erlang.org/docs. Then click on `Download` at the top (you don't have to download anything to view the docs). Then click on `Erlang/OTP 21` on the right. Then click on whatever version you have installed (or the highest version). Then click on the `View Documentation` button. Then cick on `Expand all` on the left. Then look down the table of contents on the left and click on `ssl`. Then click on `ssl` again. Then click on `listen/2`. There are links for the types. – 7stud Feb 26 '23 at 17:02
  • You can see that there is no `certs_keys` or map in the `Options` for that version of erlang. – 7stud Feb 26 '23 at 17:03