Sending / receiving TCP requests with SSL certificate

Hello,

I’m attempting to communicate with the Verisign EPP server over a TCP / SSL connection. This connection requires an SSL certificate, but I’m having trouble with the SSL certificate. I am inexperienced in working with anything like this, in the past all I’ve done is use Req to send various requests of different verbs. So has anyone written a library to make this easier? Or can anyone assist in the correct configuration of including an SSL certificate, sending a request and listening for a response? Here’s what I have so far, just shooting in the dark:

  def ssl_client() do
    host = Application.get_env(:appname, :epp_host) |> String.to_charlist()
    port = Application.get_env(:appname, :epp_port)
    cert = File.cwd!() <> "/ssl/cert.chain.pem"

    {:ok, connect_socket} =
      :ssl.connect(host, port, [verify: :verify_none, cacertfile: cert, active: true], :infinity)

    connect_socket
  end

  defp listen_ssl(socket) do
    case :ssl.recv(socket, 0) do
      {:ok, line} ->
        IO.puts(~s(Client got: "#{String.trim(line)}"))
        :ok = :ssl.close(socket)

      {:error, :closed} ->
        IO.puts("Server closed socket.")

      {:error, :enotconn} ->
        IO.puts("Server is not connected.")

      {:error, reason} ->
        IO.puts("Server errored with code: #{reason}")
    end
  end

  def send_ssl_request(line) do
    socket = ssl_client()
    :ssl.send(socket, line)
    listen_ssl(socket)
  end

The response that I get when I attempt to call send_ssl_request() is:
TLS :client: In state :connection received SERVER ALERT: Fatal - Bad Certificate

Thanks!

Your configuration for the certificate looks correct, however any reason you are setting verify: :verify_none instead of verify: :verify_peer ?

Also if you try cacerts: :public_key.cacerts_get() instead of passing in your own cert what happens?

I used :verify_none because if I use :verify_peer, I get the following error: ~c"TLS client: In state wait_cert at ssl_handshake.erl:2180 generated CLIENT ALERT: Fatal - Unknown CA\n"

If I try cacerts: :public_key.cacerts_get() I get the same error: TLS :client: In state :connection received SERVER ALERT: Fatal - Bad Certificate
I assume that’s because it doesn’t have a certificate, or the certificate it does have isn’t for the domain I need to authenticate.
If I do :public_key.cacerts_load(cert), I get :ok, but then with cacerts: :public_key.cacerts_get() I still get the same error: TLS :client: In state :connection received SERVER ALERT: Fatal - Bad Certificate

A couple of things that have caught me out in the past, one being when using :public_key_cacerts_get() I didn’t have the certificate store configured correctly on my host machine.

Second, I’ve found that I also need to include the server_name_indication (SNI) in my ssl opts (some info here on SNI: https://www.cloudflare.com/learning/ssl/what-is-sni/).

1 Like

What type of value should I provide for the SNI? The only thing I see listed in the Erlang docs are disable :sweat_smile:
I tried providing a string with the domain (server_name_indication: "domain.ext"), but that produces an error.
This could be what I need, though. I’ve added certs_keys: [...] to the options and I’m getting TLS :client: In state :connection received SERVER ALERT: Fatal - Certificate Unknown

Yep you have it right, but I think you have to provide it as a charlist, so: ~c"foo.bar.com" (ref: Binaries, strings, and charlists — Elixir v1.17.2)

This worked for the SNI value, however; it didn’t resolve the problem :slightly_frowning_face: I’m still getting Fatal - Certificate Unknown

How do I specify the version of TLS to use? I’ve tried appending the following to the options to no avail: versions: ["tlsv1.2"], versions: [~c"tlsv1.2"], versions: ["tlsv1_2"], versions: [~c"tlsv1_2"]

// Update
Finally found the correct format, it’s: versions: [:"tlsv1.2"]

Sadly, I’m still getting errors. When I try verify: :verify_peer, cacerts: :public_key.cacerts_get()
I get this error:
{:tls_alert, {:unknown_ca, ~c"TLS client: In state certify at ssl_handshake.erl:2180 generated CLIENT ALERT: Fatal - Unknown CA\n"}}

When I try verify: :verify_none, certs_keys: [%{certfile: cert, keyfile: key}],
I get this error:
{:tls_alert, {:certificate_unknown, ~c"TLS client: In state cipher received SERVER ALERT: Fatal - Certificate Unknown\n"}}

I don’t think it’s an issue with the cert / key, as when I test them with the openssl command, I seem to get a successful response, so I’m at a loss:
openssl s_client -connect domain.com:700 -cert cert.pem -key key.pem -tls1_2

---
SSL handshake has read 10844 bytes and written 1790 bytes
Verification: OK
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: AD15B5B774CE0C8D76B920B165F72C03DE1F1B0CEF8CCD8FC229A3008E6C0497
    Session-ID-ctx: 
    Master-Key: RedactedReallyLongString
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1724396369
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
    Extended master secret: yes
---

This is my current function code:

  def ssl_start() do
    host = Application.get_env(:appname, :epp_host) |> String.to_charlist()
    port = Application.get_env(:appname, :epp_port)
    cert = File.cwd!() <> "/ssl/server/cert.pem"
    key = File.cwd!() <> "/ssl/server/key.pem"
    :public_key.cacerts_load(cert)

    opts = [
      # verify: :verify_peer,
      # cacerts: :public_key.cacerts_get(),
      verify: :verify_none, certs_keys: [%{certfile: cert, keyfile: key}],
      reuseaddr: true,
      server_name_indication: ~c"domain.ext",
      versions: [:"tlsv1.2"]
    ]

    :ssl.start()

    case :ssl.connect(host, port, opts, 5000) do
      {:ok, socket} ->
        socket

      {:error, err} ->
        dbg(err)
        nil
    end
  end

On what are you running this server? It might be possible that you are missing the client certificates on your deployed system, the symptom usually is that it works on dev envs but fails on deployed server.

To check that fast, you can try adding castore to your project and use the provided certs by castore with: CAStore.file_path()

2 Likes

Had a chance to test it myself.

It worked using the options setup below and I didn’t need to use the SNI. I think as mentioned before and as @D4no0 indicated, there might be an issue with the application host certificate store i.e. wherever you’re running this from (obviously, I don’t know where you’re connecting to, so I used a fairly well-known address!).

def ssl_start() do
    host = ~c"www.google.com"
    port = 443

    opts = [
      verify: :verify_peer,
      cacerts: :public_key.cacerts_get()
    ]

    :ssl.start()

    case :ssl.connect(host, port, opts, 5000) do
      {:ok, socket} ->
        socket
      {:error, error} ->
        dbg(error)
        nil
    end
  end

Edit: you might still need the SNI depending on what you’re connecting to.

For example, say you’re using maps.google.com, you’re SNI value could be *.google.com that’s just a contrived example.

This isn’t deployed anywhere, I’m working on my localhost. So I can’t even get it to work locally :sweat_smile:

Good to know about the SNI value, I misunderstood what was needed there so I’ll modify that and give it a try. I’ll also try reducing the options and seeing if that has any effect. Thanks!

I finally found the right combination of options to get it working. What a pain! Somehow this ended up working:

  def ssl_start() do
    host = Application.get_env(:appname, :epp_host) |> String.to_charlist()
    port = Application.get_env(:appname, :epp_port)
    certs = File.cwd!() <> "/ssl/certs.pem"
    key = File.cwd!() <> "/ssl/key.pem"
    :public_key.cacerts_load(certs)

    opts = [
      cacerts: :public_key.cacerts_get(),
      verify: :verify_none,
      certfile: certs,
      keyfile: key
    ]

    :ssl.start()

    case :ssl.connect(host, port, opts, 5000) do
      {:ok, socket} ->
        socket

      {:error, err} ->
        dbg(err)
        nil
    end
  end

I received a file with 1 key & 3 certs. I tried separating each cert into it’s own file, but the working combination was to have the 3 certs in 1 file, and the key in another.

1 Like