Skip to the content.
« Preventing timing attacks

Erlang standard library: ssl

TLS clients

TLS servers

Server certificate verification

The default value for the verify option in the ssl application is verify_none. While this is an appropriate value for most servers, it presents a significant risk for clients: with the default value clients silently ignore the server’s certificate, making them vulnerable to man-in-the-middle (MitM) attacks. Except under very specific circumstances, any TLS client should set the verify option to verify_peer.

In order for client connections to succeed in verify_peer mode, a few more ssl options must be set:

%% Erlang
ssl:connect("example.net", 443, [
    {verify, verify_peer},
    {cacertfile, "/etc/ssl/cert.pem"},
    {depth, 3},
    {customize_hostname_check, [
        {match_fun, public_key:pkix_verify_hostname_match_fun(https)}
    ]}
]).
# Elixir
:ssl.connect('example.net', 443,
  verify: :verify_peer,
  cacertfile: '/etc/ssl/cert.pem',
  depth: 3,
  customize_hostname_check: [
    match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
  ]
)

Make sure to test the selected options against test endpoints, such as those provided by https://badssl.com. Negative testing, i.e. making sure the connection fails when it should, is arguably more important than positive (interoperability) testing.

Revocation check

One scenario that’s not handled by the above examples is certificate revocation: no revocation check is performed, and therefore a revoked but otherwise valid certificate would be accepted. It is possible to check certificates against the CA’s Certificate Revocation List (CRL) by setting the crl_check option to true. This also requires the crl_cache to be configured:

%% Erlang
ssl:connect("revoked.badssl.com", 443, [
    {verify, verify_peer},
    {cacertfile, "/etc/ssl/cert.pem"},
    {depth, 3},
    {customize_hostname_check, [
        {match_fun, public_key:pkix_verify_hostname_match_fun(https)}
    ]},
    {crl_check, true},
    {crl_cache, {ssl_crl_cache, {internal, [{http, 1000}]}}}
]).
# Elixir
:ssl.connect('revoked.badssl.com', 443,
  verify: :verify_peer,
  cacertfile: '/etc/ssl/cert.pem',
  depth: 3,
  customize_hostname_check: [
    match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
  ],
  crl_check: true,
  crl_cache: {:ssl_crl_cache, {:internal, [http: 1000]}}
)

However, please note that the ssl_crl_cache module does not actually cache the CRL contents, so each handshake will trigger a new CRL lookup, which impacts the performance and reliability of TLS connections. In applications that require revocation checks as well as high throughput a custom CRL cache implementation will be needed.

Selecting protocol versions and ciphers

Recent versions of Erlang/OTP disable most weak, legacy SSL/TLS protocol versions and cipher suites. For instance, Erlang/OTP 24 receives an ‘A’ score on the Qualys SSL Labs ‘SSL Server Test’, without any further tuning.

Further hardening of the TLS parameters to comply with the Mozilla ‘Server Side TLS’ “Intermediate compatibility” recommendations can be achieved as described below. These recommendations were written for servers, but the same settings may be used for client-side hardening, depending on the configuration of the TLS server(s) the client is expected to connect to.

%% Erlang

PreferredCiphers = [
  %% Cipher suites (TLS 1.3): TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
  #{cipher => aes_128_gcm, key_exchange => any, mac => aead, prf => sha256},
  #{cipher => aes_256_gcm, key_exchange => any, mac => aead, prf => sha384},
  #{cipher => chacha20_poly1305, key_exchange => any, mac => aead, prf => sha256},
  %% Cipher suites (TLS 1.2): ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:
  %% ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:
  %% ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
  #{cipher => aes_128_gcm, key_exchange => ecdhe_ecdsa, mac => aead, prf => sha256},
  #{cipher => aes_128_gcm, key_exchange => ecdhe_rsa, mac => aead, prf => sha256},
  #{cipher => aes_256_gcm, key_exchange => ecdhe_ecdsa, mac => aead, prf => sha384},
  #{cipher => aes_256_gcm, key_exchange => ecdhe_rsa, mac => aead, prf => sha384},
  #{cipher => chacha20_poly1305, key_exchange => ecdhe_ecdsa, mac => aead,prf => sha256},
  #{cipher => chacha20_poly1305, key_exchange => ecdhe_rsa, mac => aead, prf => sha256},
  #{cipher => aes_128_gcm, key_exchange => dhe_rsa, mac => aead, prf => sha256},
  #{cipher => aes_256_gcm, key_exchange => dhe_rsa, mac => aead, prf => sha384}
],
Ciphers = ssl:filter_cipher_suites(PreferredCiphers, []),

%% Protocols: TLS 1.2, TLS 1.3
Versions = ['tlsv1.2', 'tlsv1.3'],

%% TLS curves: X25519, prime256v1, secp384r1
PreferredEccs = [secp256r1, secp384r1],
Eccs = ssl:eccs() -- (ssl:eccs() -- PreferredEccs),

SslOpts = [
  {ciphers, Ciphers},
  {versions, Versions},
  {eccs, Eccs}
].
# Elixir

preferred_ciphers = [
  # Cipher suites (TLS 1.3): TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
  %{cipher: :aes_128_gcm, key_exchange: :any, mac: :aead, prf: :sha256},
  %{cipher: :aes_256_gcm, key_exchange: :any, mac: :aead, prf: :sha384},
  %{cipher: :chacha20_poly1305, key_exchange: :any, mac: :aead, prf: :sha256},
  # Cipher suites (TLS 1.2): ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:
  # ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:
  # ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
  %{cipher: :aes_128_gcm, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256},
  %{cipher: :aes_128_gcm, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256},
  %{cipher: :aes_256_gcm, key_exchange: :ecdh_ecdsa, mac: :aead, prf: :sha384},
  %{cipher: :aes_256_gcm, key_exchange: :ecdh_rsa, mac: :aead, prf: :sha384},
  %{cipher: :chacha20_poly1305, key_exchange: :ecdhe_ecdsa, mac: :aead, prf: :sha256},
  %{cipher: :chacha20_poly1305, key_exchange: :ecdhe_rsa, mac: :aead, prf: :sha256},
  %{cipher: :aes_128_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha256},
  %{cipher: :aes_256_gcm, key_exchange: :dhe_rsa, mac: :aead, prf: :sha384}
]
ciphers = :ssl.filter_cipher_suites(preferred_ciphers, [])

# Protocols: TLS 1.2, TLS 1.3
versions = [:"tlsv1.2", :"tlsv1.3"]

# TLS curves: X25519, prime256v1, secp384r1
preferred_eccs = [:secp256r1, :secp384r1]
eccs = :ssl.eccs() -- (:ssl.eccs() -- preferred_eccs)

ssl_opts = [
  {:ciphers, ciphers},
  {:versions, versions},
  {:eccs, eccs}
]

Notes:

Consider making the protocol version and cipher suite configuration part of the application’s runtime configuration, instead of hardcoding the values: it should be possible to remove or add a protocol version or cipher suite without rebuilding the application.

Other options

The client_renegotiation server-side option can be set to false to disable client-initiated session renegotiation, to prevent it from being used as a DoS vector by malicious clients. Note that very long-lived TLS connections sending large data volumes may require periodic renegotiation to prevent sequence numbers (nonce) from wrapping. If this happens when client_renegotiation is set to false, the connection will be terminated.

This option is relevant only for TLS version 1.2 and earlier, as in 1.3 renegotiation is not supported and nonce wrapping is handled by rekeying.

TLS client and server libraries

Finally, when using standard library or third party packages that use ssl to implement TLS clients or servers, verify whether secure defaults are used. See also Erlang standard library: inets, for information about the ‘httpc’ HTTP client.

Next: Erlang standard library: inets »