为什么来自 OpenSSL 的 BIO_do_connect() 不能与 GDAX (a.k.a.cloudflare) 沙箱一起工作?

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

我用 C++ 写了一些软件,现在正在尝试获取 GDAX /products 列表(目前主要作为测试。)

更新: 我想补充一点,连接实际上是连接到 cloudflare,而不是直接连接到 GDAX。所以这可能是 cloudflare 的问题,而不是直接的 GDAX 服务器。

只是,BIO_do_connect()函数每次returns-1。它并没有给我太多继续下去的机会。我在日志中写下以下内容。所以主要信息是错误发生在 s23_clnt.c...

的第 794 行

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

我可以看出这意味着 TCP 连接本身发生了,但不知何故它无法获得可接受的安全连接。我以前见过类似的行为,当时机器只会使用一些旧的加密方法。但我检查了 nmap,连接肯定支持 TLS 1.2。我 运行 以下命令并得到:

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

我得到以下输出,证明端口 443 已打开并且具有所有必要的加密方案。

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

现在,我针对普通 REST API 地址 (api.gdax.com) 和我自己的网站 (www.m2osw.com) 测试了我的代码,加密部分工作正常。我真的不明白我会做错什么,除非它的 SSL 设置很奇怪,否则它会像沙箱 URL (api-public.sandbox.gdax.com) 那样失败。

请注意,当我尝试连接到端口 80 时(我知道这是错误的),它按预期工作。也就是说,我得到一个 301,其 Location 指向使用协议 HTTPS 的同一地址。

有人在连接到沙箱时遇到过问题吗?

调用了所有函数。完整的实现在 libsnapwebsites 中的 github 上可用,大约在第 1111 行(bio_client 构造函数)。

// 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

同样,如果我使用 api.gdax.com,这段代码可以找到 cr > 0,所以我现在真的很茫然!?而且我知道 TCP 连接本身会发生,因为它进入 s23_clnt.c 之后发生的部分。

好吧,我花了一整天(好吧大约半天)来研究这个,将我的代码与 libcurl 的代码进行比较,libcurl 的代码也使用 SSL_CTXSSL OpenSSL 的结构。代码看起来非常相似...除了 libcurl 版本包含以下内容:

[...]
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");
[...]

正如我们所见,他们有一个名为 SNI 的东西,如果为真,他们会设置名为 Hostname 的 TLS 扩展名。如果 Hostname 参数未包含在 SSL HELLO 消息中,则 GDAX 服务器(或者很可能是 cloudflare 服务器)立即拒绝连接。

因此,就我而言,我将强制使用 SNI(服务器名称标识),这样它可能会在更多服务器上运行。 libcurl 允许不包含它,但看起来您应该始终拥有它。至少应该不会痛。

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()));

请注意,必须为 SSL_set_tlsext_host_name() 函数提供正确的主机名,而不是 IPv4 或 IPv6 地址。