Save Ukraine

Debugging a TLS problem

Christian Kruse,

Starting with a simple cURL request

The first few tests with cURL were successful. I had to use the „insecure“ flag (-k) because of a self-signed certificate, but I was able to authorize (via HTTP basic authentication) and send simple API requests like „show me the server capabilities“ – nice:

curl -X POST -u 'user:password' -H 'Content-Type: application/json' -d ' {"version":"1.1", "method": "MyMethod", "params": []}' -k https://example.org/json
{"version":"1.1", "result": {…}}

Nice, next step!

Implementing the API in Elixir

The next step would be to implement the API in Elixir. To do that I chose Tesla as my HTTP client. The code to do basic requests was writte pretty fast, but I wasn't able to send a successful API request. Tesla failed every HTTP request with an error:

   [info] TLS :client: In state :hello received SERVER ALERT: Fatal - Handshake Failure

   ** (Tesla.Error) {:tls_alert, {:handshake_failure, 'TLS client: In state hello received SERVER ALERT: Fatal - Handshake Failure\n'}} (POST https://example.org/json)
       (tesla 1.4.1) lib/tesla.ex:320: Tesla.execute!/3
       (termitool 23.34.10) lib/termitool/greyhound.ex:50: Termitool.Greyhound.get_server_infos/2

First I thought this is because the certificate validation fails. So I was looking how to disable the validation, and it is documented surprisingly bad. Tesla can use different backends to do the actual HTTP requests, and it seems to differ from backend to backend, there is no unified API for this. Since I was using Hackney (the recommended backend) I had to look for a solution for Hackney, and I found it in a closed issue on Github: I had to add an insecure: true flag when defining the middleware:

client = Tesla.client(middleware, {Tesla.Adapter.Hackney, insecure: true})

But adding that didn't help, I still got the same error. I tried to Google the problem, but I only found not very helpful comments and no solutions. After thinking a bit about the message „TLS client: In state hello received SERVER ALERT: Fatal - Handshake Failure“ it clicked: the problem occured even before the certificate should be validated. Hm, might this be a TLS version problem? So I researched how to define the TLS version Hackney should use and pinned it to TLS 1.2:

client =
  Tesla.client(
    middleware,
    {Tesla.Adapter.Hackney,
     insecure: true,
     ssl_options: [versions: [:"tlsv1.2"]]}
  )

But no luck. After racking my brain over this problem I thought that this might be a cipher problem…? Well, that is a theory worth testing. Nmap can tell me what ciphers the server supports:

nmap --script ssl-enum-ciphers -p 443 example.org
   Starting Nmap 7.91 ( https://nmap.org ) at 2021-04-26 10:05 CEST
   Nmap scan report for example.org (93.184.216.34)
   Host is up (0.017s latency).

   PORT     STATE SERVICE
   443/tcp open  vop
   | ssl-enum-ciphers:
   |   SSLv3:
   |     ciphers:
   |       TLS_RSA_WITH_3DES_EDE_CBC_SHA - F
   |       TLS_RSA_WITH_AES_128_CBC_SHA - F
   |       TLS_RSA_WITH_AES_256_CBC_SHA - F
   |       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA - F
   |       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA - F
   |       TLS_RSA_WITH_IDEA_CBC_SHA - F
   |       TLS_RSA_WITH_RC4_128_MD5 - F
   |       TLS_RSA_WITH_RC4_128_SHA - F
   |       TLS_RSA_WITH_SEED_CBC_SHA - F
   |     compressors:
   |       NULL
   |     cipher preference: client
   |     warnings:
   |       64-bit block cipher 3DES vulnerable to SWEET32 attack
   |       64-bit block cipher IDEA vulnerable to SWEET32 attack
   |       Broken cipher RC4 is deprecated by RFC 7465
   |       CBC-mode cipher in SSLv3 (CVE-2014-3566)
   |       Ciphersuite uses MD5 for message integrity
   |       Forward Secrecy not supported by any cipher
   |       Insecure certificate signature: MD5
   |   TLSv1.0:
   |     ciphers:
   |       TLS_RSA_WITH_3DES_EDE_CBC_SHA - F
   |       TLS_RSA_WITH_AES_128_CBC_SHA - F
   |       TLS_RSA_WITH_AES_256_CBC_SHA - F
   |       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA - F
   |       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA - F
   |       TLS_RSA_WITH_IDEA_CBC_SHA - F
   |       TLS_RSA_WITH_RC4_128_MD5 - F
   |       TLS_RSA_WITH_RC4_128_SHA - F
   |       TLS_RSA_WITH_SEED_CBC_SHA - F
   |     compressors:
   |       NULL
   |     cipher preference: client
   |     warnings:
   |       64-bit block cipher 3DES vulnerable to SWEET32 attack
   |       64-bit block cipher IDEA vulnerable to SWEET32 attack
   |       Broken cipher RC4 is deprecated by RFC 7465
   |       Ciphersuite uses MD5 for message integrity
   |       Forward Secrecy not supported by any cipher
   |       Insecure certificate signature: MD5
   |   TLSv1.1:
   |     ciphers:
   |       TLS_RSA_WITH_3DES_EDE_CBC_SHA - F
   |       TLS_RSA_WITH_AES_128_CBC_SHA - F
   |       TLS_RSA_WITH_AES_256_CBC_SHA - F
   |       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA - F
   |       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA - F
   |       TLS_RSA_WITH_IDEA_CBC_SHA - F
   |       TLS_RSA_WITH_RC4_128_MD5 - F
   |       TLS_RSA_WITH_RC4_128_SHA - F
   |       TLS_RSA_WITH_SEED_CBC_SHA - F
   |     compressors:
   |       NULL
   |     cipher preference: client
   |     warnings:
   |       64-bit block cipher 3DES vulnerable to SWEET32 attack
   |       64-bit block cipher IDEA vulnerable to SWEET32 attack
   |       Broken cipher RC4 is deprecated by RFC 7465
   |       Ciphersuite uses MD5 for message integrity
   |       Forward Secrecy not supported by any cipher
   |       Insecure certificate signature: MD5
   |   TLSv1.2:
   |     ciphers:
   |       TLS_RSA_WITH_3DES_EDE_CBC_SHA - F
   |       TLS_RSA_WITH_AES_128_CBC_SHA - F
   |       TLS_RSA_WITH_AES_128_CBC_SHA256 - F
   |       TLS_RSA_WITH_AES_128_GCM_SHA256 - F
   |       TLS_RSA_WITH_AES_256_CBC_SHA - F
   |       TLS_RSA_WITH_AES_256_CBC_SHA256 - F
   |       TLS_RSA_WITH_AES_256_GCM_SHA384 - F
   |       TLS_RSA_WITH_CAMELLIA_128_CBC_SHA - F
   |       TLS_RSA_WITH_CAMELLIA_256_CBC_SHA - F
   |       TLS_RSA_WITH_IDEA_CBC_SHA - F
   |       TLS_RSA_WITH_RC4_128_MD5 - F
   |       TLS_RSA_WITH_RC4_128_SHA - F
   |       TLS_RSA_WITH_SEED_CBC_SHA - F
   |     compressors:
   |       NULL
   |     cipher preference: client
   |     warnings:
   |       64-bit block cipher 3DES vulnerable to SWEET32 attack
   |       64-bit block cipher IDEA vulnerable to SWEET32 attack
   |       Broken cipher RC4 is deprecated by RFC 7465
   |       Ciphersuite uses MD5 for message integrity
   |       Forward Secrecy not supported by any cipher
   |       Insecure certificate signature: MD5
   |_  least strength: F

   Nmap done: 1 IP address (1 host up) scanned in 2.05 seconds

Oh wow, a lot of very old and weak ciphers… Hm, what ciphers does Erlang/OTP support?

iex(1)> :ssl.cipher_suites()
[
  {:any, :aes_256_gcm, :aead, :sha384},
  {:any, :aes_128_gcm, :aead, :sha256},
  {:any, :chacha20_poly1305, :aead, :sha256},
  {:any, :aes_128_ccm, :aead, :sha256},
  {:any, :aes_128_ccm_8, :aead, :sha256},
  {:ecdhe_ecdsa, :aes_256_gcm, :aead, :sha384},
  {:ecdhe_rsa, :aes_256_gcm, :aead, :sha384},
  {:ecdhe_ecdsa, :aes_256_ccm, :aead},
  {:ecdhe_ecdsa, :aes_256_ccm_8, :aead},
  {:ecdhe_ecdsa, :aes_256_cbc, :sha384, :sha384},
  {:ecdhe_rsa, :aes_256_cbc, :sha384, :sha384},
  {:ecdhe_ecdsa, :chacha20_poly1305, :aead, :sha256},
  {:ecdhe_rsa, :chacha20_poly1305, :aead, :sha256},
  {:ecdhe_ecdsa, :aes_128_gcm, :aead, :sha256},
  {:ecdhe_rsa, :aes_128_gcm, :aead, :sha256},
  {:ecdhe_ecdsa, :aes_128_ccm, :aead},
  {:ecdhe_ecdsa, :aes_128_ccm_8, :aead},
  {:ecdh_ecdsa, :aes_256_gcm, :aead, :sha384},
  {:ecdh_rsa, :aes_256_gcm, :aead, :sha384},
  {:ecdh_ecdsa, :aes_256_cbc, :sha384, :sha384},
  {:ecdh_rsa, :aes_256_cbc, :sha384, :sha384},
  {:ecdh_ecdsa, :aes_128_gcm, :aead, :sha256},
  {:ecdh_rsa, :aes_128_gcm, :aead, :sha256},
  {:ecdhe_ecdsa, :aes_128_cbc, :sha256, :sha256},
  {:ecdhe_rsa, :aes_128_cbc, :sha256, :sha256},
  {:ecdh_ecdsa, :aes_128_cbc, :sha256, :sha256},
  {:ecdh_rsa, :aes_128_cbc, :sha256, :sha256},
  {:dhe_rsa, :aes_256_gcm, :aead, :sha384},
  {:dhe_dss, :aes_256_gcm, :aead, :sha384},
  {:dhe_rsa, :aes_256_cbc, :sha256},
  {:dhe_dss, :aes_256_cbc, :sha256},
  {:dhe_rsa, :aes_128_gcm, :aead, :sha256},
  {:dhe_dss, :aes_128_gcm, :aead, :sha256},
  {:dhe_rsa, :chacha20_poly1305, :aead, :sha256},
  {:dhe_rsa, :aes_128_cbc, :sha256},
  {:dhe_dss, :aes_128_cbc, :sha256},
  {:ecdhe_ecdsa, :aes_256_cbc, :sha},
  {:ecdhe_rsa, :aes_256_cbc, :sha},
  {:dhe_rsa, :aes_256_cbc, :sha},
  {:dhe_dss, :aes_256_cbc, :sha},
  {:ecdh_ecdsa, :aes_256_cbc, :sha},
  {:ecdh_rsa, :aes_256_cbc, :sha},
  {:ecdhe_ecdsa, :aes_128_cbc, :sha},
  {:ecdhe_rsa, :aes_128_cbc, :sha},
  {:dhe_rsa, :aes_128_cbc, :sha},
  {:dhe_dss, :aes_128_cbc, :sha},
  {:ecdh_ecdsa, :aes_128_cbc, :sha},
  {:ecdh_rsa, :aes_128_cbc, ...}
]

Oh. Yeah, OTP disabled several legacy ciphers in OTP-19 and dropped SSLv3 support completely in OTP-23. Sigh.

Of course I notificed the client about the unsecure TLS, but for the meantime luckily I can enable old ciphers again and continue, err, start with my work for implementing the API:

client =
  Tesla.client(
    middleware,
    {Tesla.Adapter.Hackney,
     insecure: true,
     ssl_options: [
       verify: :verify_none,
       versions: [:"tlsv1.2"],
       ciphers: :ssl.cipher_suites() ++ [{:rsa, :aes_256_cbc, :sha256}]
     ]}
  )

And heureka! Finally I was able to perform HTTP requests against this host 🎉

Update: a few days ago Erlang/OTP 24 was released and the API of the ssl application changed. The :ssl.cipher_suites function now expects at least two arguments and returns a list of maps. You've got to change the call to this to support the old cipher:

:ssl.cipher_suites(:all, :"tlsv1.2") ++ [%{key_exchange: :rsa, cipher: :aes_256_cbc, mac: :sha256}]