Search code examples
ssltcpopensslgdax-api

Why would BIO_do_connect() from OpenSSL not work right with GDAX (a.k.a. cloudflare) sandbox?


I wrote some software in C++ and I'm trying to get the GDAX /products list for now (mainly as a test at this time.)

UPDATE: I wanted to add that the connection is actually to cloudflare and not directly to GDAX. So it is likely a problem with cloudflare and not directly GDAX servers.

Only, the BIO_do_connect() function returns -1 each time. It does not give me much to go on with it. I write the following in my log. So the main info is the error occurs on line 794 of s23_clnt.c...

OpenSSL: [336031996/20|119|252]:[]:[]:[]:[s23_clnt.c]:[794]:[(no details)]

I can tell that this means the TCP connection itself happens, but somehow it's not able to get an acceptable secure connection. I've see similar behaviors before when a machine would only some old encryption methods. But I checked with nmap and the connection definitely supports TLS 1.2. I ran the following command and got:

nmap --script ssl-enum-ciphers api-public.sandbox.gdax.com

And I get the following output which proves that port 443 is open and has all the necessary encryption schemes necessary.

Starting Nmap 7.01 ( https://nmap.org ) at 2018-03-24 21:57 PDT
Nmap scan report for api-public.sandbox.gdax.com (104.28.30.142)
Host is up (0.016s latency).
Other addresses for api-public.sandbox.gdax.com (not scanned): 104.28.31.142
Not shown: 996 filtered ports
PORT     STATE SERVICE
80/tcp   open  http
443/tcp  open  https
| ssl-enum-ciphers: 
|   TLSv1.0: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|     compressors: 
|       NULL
|     cipher preference: server
|   TLSv1.1: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|   TLSv1.2: 
|     ciphers: 
|       TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA256 (rsa 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|_  least strength: C
8080/tcp open  http-proxy
8443/tcp open  https-alt
| ssl-enum-ciphers: 
|   TLSv1.0: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C
|     compressors: 
|       NULL
|     cipher preference: server
|   TLSv1.1: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|   TLSv1.2: 
|     ciphers: 
|       TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (secp256r1) - A
|       TLS_RSA_WITH_AES_128_GCM_SHA256 (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA256 (rsa 2048) - A
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (secp256r1) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (secp256r1) - A
|       TLS_RSA_WITH_AES_256_GCM_SHA384 (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA256 (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|_  least strength: C

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

Now, I tested my code against the normal REST API address (api.gdax.com) and my own website (www.m2osw.com) and the encryption part works just fine. I really don't see what I would be doing wrong that it would fail like that with the sandbox URL (api-public.sandbox.gdax.com) unless its SSL setup is weird.

Note that when I try to connect to port 80 (which is wrong, I know), it works as expected. That is, I get a 301 with a Location to the same address with protocol HTTPS.

Anyone has had some problem with connecting to the sandbox?

There are all the functions that get called. The full implementation is available on github in the libsnapwebsites around line 1111 at this point (bio_client constructor).

// called once on initialization
SSL_library_init();
ERR_load_crypto_strings();
ERR_load_SSL_strings();
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
crypto_thread_setup();

// call each time we connect
SSL_CTX * ssl_ctx = SSL_CTX_new(SSLv23_client_method();
SSL_CTX_set_verify_depth(ssl_ctx, 4);
SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_COMPRESSION);

// just in case I tried with "ALL", but no difference
//SSL_CTX_set_cipher_list(ssl_ctx, "ALL");
SSL_CTX_set_cipher_list(ssl_ctx, "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4");

SSL_CTX_load_verify_locations(ssl_ctx, NULL, "/etc/ssl/certs");
BIO * bio = BIO_new_ssl_connect(ssl_ctx);
SSL * ssl(nullptr);
BIO_get_ssl(bio, &ssl);
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
BIO_set_conn_hostname(bio, const_cast<char *>(addr.c_str()));
BIO_set_conn_int_port(bio, &port);

int const cr(BIO_do_connect(bio));
// here cr == -1 when I use api-public.sandbox.gdax.com

Again, this code works find and cr > 0 if I use api.gdax.com so I'm really at a loss at this point!? And I know that the TCP connection itself happens since it gets in that s23_clnt.c which is after that part happens.


Solution

  • Okay, I spent the whole day (okay about half a day) working on this one comparing my code with libcurl's code which also uses the SSL_CTX and SSL structures of OpenSSL. The code looks very much the same... except that the libcurl version includes this:

    [...]
    switch(data->set.ssl.version) {
    case CURL_SSLVERSION_DEFAULT:
      [...]
      use_sni(TRUE);
      break;
    
    [...]
    if((0 == Curl_inet_pton(AF_INET, conn->host.name, &addr)) &&
       (0 == Curl_inet_pton(AF_INET6, conn->host.name, &addr)) &&
       sni &&
       !SSL_set_tlsext_host_name(connssl->handle, conn->host.name))
      infof(data, "WARNING: failed to configure server name indication (SNI) "
            "TLS extension\n");
    [...]
    

    As we can see, they have something called SNI and if true they set the TLS extension called Hostname. If that Hostname parameter is not included in the SSL HELLO message, then the GDAX server (or most probably the cloudflare one) refuses the connection immediately.

    So, on my end I will be forcing the SNI (Server Name Identification) and that way it is likely to work on way more servers. libcurl allows to not include it, but it looks like you should always have it. It shouldn't hurt, at least.

    BIO_get_ssl(bio, &ssl);
    SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
    SSL_set_tlsext_host_name(ssl, const_cast<char *>(addr.c_str()));
    BIO_set_conn_hostname(bio, const_cast<char *>(addr.c_str()));
    

    Note that the SSL_set_tlsext_host_name() function must be given the correct hostname, not an IPv4 or IPv6 address.