2018年8月,TLS 1.3的最终版本RFC 8446终于发布了,我这种菜鸡出于好奇试着安装了OpenSSL 1.1.1来开启TLS 1.3的支持。但是最近几天我发现,使用TLS 1.3的时候,不管怎么设置Nginx里的ssl_ciphers中的密码套件的顺序,最终采用的密码套件总是TLS_AES_256_GCM_SHA384,就算直接指明使用AES128也无济于事,虽然并不是什么大的问题,但是在一个伪完美主义者眼中,总感觉像是残废了一般(果然强迫症发作就没法治)。

在网上搜索了一番,并没有人对于这个问题进行过探究,能够任意选择密码套件的貌似只有Google的具有等效密码组功能的BoringSSL,于是又查看了OpenSSL Wiki,在Ciphersuites一章里有这样一段:

Applications should use the SSL_CTX_set_ciphersuites() or SSL_set_ciphersuites() functions to configure TLSv1.3 ciphersuites. Note that the functions SSL_CTX_get_ciphers() and SSL_get_ciphers() will return the full list of ciphersuites that have been configured for both TLSv1.2 and below and TLSv1.3.

For the OpenSSL command line applications there is a new “-ciphersuites” option to configure the TLSv1.3 ciphersuite list. This is just a simple colon (":") separated list of TLSv1.3 ciphersuite names in preference order. Note that you cannot use the special characters such as “+”, “!", “-” etc, that you can for defining TLSv1.2 ciphersuites. In practice this is not likely to be a problem because there are only a very small number of TLSv1.3 ciphersuites.

For example:

$ openssl s_server -cert mycert.pem -key mykey.pem -cipher ECDHE -ciphersuites “TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256”

This will configure OpenSSL to use any ECDHE based ciphersuites for TLSv1.2 and below. For TLSv1.3 the TLS_AES_256_GCM_SHA384 and TLS_CHACHA20_POLY1305_SHA256 ciphersuites will be available.

提到了设置TLS 1.3密码套件时应该使用SSL_CTX_set_ciphersuites(),而TLS 1.2及以下版本使用的是SSL_CTX_set_cipher_list(),入口并不一样。

知道了这一点之后就可以试着修♂改Nginx里的设置密码套件的代码了,可以从src/http/modules/ngx_http_ssl_module.c处入手,找到ngx_http_ssl_merge_srv_conf()中设置证书、私钥和密码套件的部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static char *
ngx_http_ssl_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ...    
    if (ngx_ssl_certificates(cf, &conf->ssl, conf->certificates,
                             conf->certificate_keys, conf->passwords)
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }

    if (ngx_ssl_ciphers(cf, &conf->ssl, &conf->ciphers,
                        conf->prefer_server_ciphers)
        != NGX_OK)
    {
        return NGX_CONF_ERROR;
    }
    ...    
    return NGX_CONF_OK;
}

conf->ciphers就是配置文件中ssl_ciphers一项后面跟的值对应的ngx_str_t结构体。后面的ngx_ssl_ciphers()函数内部即为对OpenSSL的SSL设置函数的调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
ngx_int_t
ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers,
    ngx_uint_t prefer_server_ciphers)
{
    if (SSL_CTX_set_cipher_list(ssl->ctx, (char *) ciphers->data) == 0) {
        ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
                      "SSL_CTX_set_cipher_list(\"%V\") failed",
                      ciphers);
        return NGX_ERROR;
    }

    if (prefer_server_ciphers) {
        SSL_CTX_set_options(ssl->ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);
    }

#if (OPENSSL_VERSION_NUMBER < 0x10100001L && !defined LIBRESSL_VERSION_NUMBER)
    /* a temporary 512-bit RSA key is required for export versions of MSIE */
    SSL_CTX_set_tmp_rsa_callback(ssl->ctx, ngx_ssl_rsa512_key_callback);
#endif

    return NGX_OK;
}

我的破坏性修改方案是将配置文件中的密码套件用'|'分成两部分,TLS 1.3的放在前面。除了http,mail、stream以及http_grpc中的SSL设置也最终会调用位于src/event/ngx_event_openssl.c中的ngx_ssl_ciphers(),因此可以修改里面的代码,将参数分成两部分,分别调用对应的设置函数,修改后的patch如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
index a281fba..7e2809b 100644
--- a/src/event/ngx_event_openssl.c
+++ b/src/event/ngx_event_openssl.c
@@ -662,7 +662,35 @@ ngx_int_t
 ngx_ssl_ciphers(ngx_conf_t *cf, ngx_ssl_t *ssl, ngx_str_t *ciphers,
     ngx_uint_t prefer_server_ciphers)
 {
-    if (SSL_CTX_set_cipher_list(ssl->ctx, (char *) ciphers->data) == 0) {
+    u_char *ciphers_data;
+
+    ciphers_data = ciphers->data;
+#if (OPENSSL_VERSION_NUMBER >= 0x10101000L)
+    /* Find TLS 1.3 ciphersuite config */
+    size_t i;
+    for (i = 0; i < ciphers->len; i++)
+    {
+        if (ciphers->data[i] == '|')
+        {
+            break;
+        }
+    }
+    if (i < ciphers->len)
+    {
+        ciphers->data[i] = '\0';
+        if (SSL_CTX_set_ciphersuites(ssl->ctx, (char *)ciphers->data) == 0)
+        {
+            ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
+                          "SSL_CTX_set_ciphersuites(\"%V\") failed",
+                          ciphers);
+            return NGX_ERROR;
+        }
+        ciphers->data[i] = '|';
+        ciphers_data = &(ciphers->data[i + 1]);
+    }
+#endif
+
+    if (SSL_CTX_set_cipher_list(ssl->ctx, (char *) ciphers_data) == 0) {
         ngx_ssl_error(NGX_LOG_EMERG, ssl->log, 0,
                       "SSL_CTX_set_cipher_list(\"%V\") failed",
                       ciphers);

重新编译后,在Nginx的配置文件中将ssl_ciphers的设置改成了下面这样(用'|'把密码套件分成了两部分):

1
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384|ECDHE+AESGCM:CHACHA20+ECDHE:+AES256:HIGH:!aNULL:!eNULL:!MD5;

最后用Chromium访问一下试试:

可以看到使用的密码套件已经变成了设置的第一项TLS_AES_128_GCM_SHA256

warning

鉴于本人的代码能力实在有限… 将来如果服务器运行出了偏差我不负责的,明白吗。

相关代码的patch已经上传至Github Gist。欢迎提出意见和建议。