:ssl.connect/3 ignoring certfile?

Hi all,

I am trying to connect to an SSL Server with a certificate and a private key.

defmodule SSLTest do
  @certfile ~c"/path/to/cert.cer"
  @keyfile ~c"/path/to/key.pem"

  def ssl_options do
    [
      certfile: @certfile,
      keyfile: @keyfile,
      verify: :verify_none
    ]
  end

  def connect() do
    :ssl.connect(~c"api-test.host.com", 443, ssl_options())
  end
end

This returning the following error:

{:error,
 {:tls_alert,
  {:handshake_failure,
   ~c"TLS client: In state cipher received SERVER ALERT: Fatal - Handshake Failure\n"}}}

If I run this with log_level: :debug I can see the following:

09:44:26.103 [debug] [message: {:server_hello_done}, protocol: :handshake, direction: :inbound]
<<< Handshake, ServerHelloDone
[]

09:44:26.103 [debug] [message: {:certificate, []}, protocol: :handshake, direction: :outbound]
>>> Handshake, Certificate
[{asn1_certificates,[]}]

Does this mean that the client is not providing any certificates during the handshake?

If I set @certfile to a path that does not exist I see the same handshake error, i.e. it seems that the file is not being accessed at all.

If I set @keyfile to a path that does not exists, I get the error I would expect:

{:error,
 {:options,
  {:keyfile, ~c"/missing.pem", {:error, :enoent}}}}

Am I using the certfile and keyfile options wrong?

Best,
Nickolay.

1 Like

You did not specify which version of Erlang/OTP you are using, this may be helpful. However, let’s assume you are on OTP 25 or OTP 26 and this is using TLS 1.3, you probably need to provide {certificate_authorities, true} (or certificate_authorities: true in elixir). This may not be your ultimate problem, but it sounds like you’re trying to engage in mutual TLS auth, and if so, that option is required (I believe) with TLS 1.3.

To note : At a glance (key words), it seems, the ssl application will indeed not yell at you right away if you provide a path to an invalid cert. In fact, there’s a bit of code in ssl that has a catch all if the role is a client and an exception occurs when initializing certificates. This initialization happens along with key initialization, so it is a bit confusing, yet there is probably some history there which would explain why that is. I’m quite interested to know myself :slight_smile: It would be rather nice if this were not the case, as short feedback loops are valuable. I suspect that if it can’t be fundamentally changed right now, perhaps an option could be introduced that hints to ssl to return an error in all cases, however, we do need to know this history first.

Also, curious, if you are trying to perform mutual tls, why are you setting verify to verify_none? It seems in this situation you’d want to verify the other server side, but I understand if you’re just trying to keep and running first :slight_smile:

1 Like

I should have mentionend the OTP version of course - 26.1.2 on macOS.

I got it working yesterday by restricting the TLS version to 1.2 (versions: [:"tlsv1.2"]).

Using certificate_authorities: true and TLS 1.3 did not work unfortunately.

:verify_none was an attempt to exclude any problems with the certificate chain. :slight_smile:

While experimenting with this, I found a bit of surprising behaviour. Restricting the TLS versions once affects all subsequent connection attempts.

iex(1)> Application.ensure_all_started(~w[ssl public_key]a)
{:ok, []}


## No version restriction

iex(2)> Keyword.keys SSLTest.ssl_options()
[:certfile, :keyfile, :verify, :cacerts, :server_name_indication]


## Connection fails

iex(3)> :ssl.connect(~c"api-test.host.com", 443, SSLTest.ssl_options())

05:41:15.471 [notice] TLS :client: In state :cipher received SERVER ALERT: Fatal - Handshake Failure
{:error,
 {:tls_alert,
  {:handshake_failure,
   ~c"TLS client: In state cipher received SERVER ALERT: Fatal - Handshake Failure\n"}}}


## Restrict TLS version in options variable

iex(4)> ssl_options_tlsv12 = [{:versions, [:"tlsv1.2"]} | SSLTest.ssl_options()]; nil
nil


## Connection succeeds with TLS 1.2

iex(5)> :ssl.connect(~c"api-test.host.com", 443, ssl_options_tlsv12)
{:ok,
 {:sslsocket, {:gen_tcp, #Port<0.6>, :tls_connection, :undefined},
  [#PID<0.205.0>, #PID<0.204.0>]}}


## Retry again with the original options (no version restriction)
## I would expect it to fail, but it suceeds

iex(6)> :ssl.connect(~c"api-test.host.com", 443, SSLTest.ssl_options())
{:ok,
 {:sslsocket, {:gen_tcp, #Port<0.7>, :tls_connection, :undefined},
  [#PID<0.210.0>, #PID<0.209.0>]}}

Does the ssl application keep track of how it succeeded to connect to a particular host and reuse that information on subsequent attempts?

That sounds like session reuse . You can try disabling it and see what you get.

Edit:

While you have figured out the remote endpoint is wanting to use tls 1.2 (it seems), for posterity I wanted to double check something I suggested… {certificate_authorities, true} is not always needed with TLS 1.3, (but perhaps in some cases).

All this has got me thinking there should be some more examples around :slight_smile: There are some in OTP, I didn’t call this out but I’d be remiss not to :

Perhaps the community needs a more diverse set of examples though, covering different scenarios (though, it would not be feasible to cover them all).

Have you tried specifying cacerts?

{cacerts, public_key:cacerts_get()} (available since OTP-25) to access the CA certificates provided by the OS.