Translation
This post is also available in
Simplified Chinese.
The final TLS 1.3 specification, RFC 8446, was published in August 2018. Out of curiosity, I installed OpenSSL 1.1.1 to enable TLS 1.3 support. Recently, however, I noticed that Nginx always selected TLS_AES_256_GCM_SHA384 under TLS 1.3, regardless of the cipher-suite order in ssl_ciphers; even explicitly requesting AES-128 made no difference. This was not a serious problem, but to a would-be perfectionist it made the configuration feel incomplete.
Searching online revealed little investigation of this behavior. BoringSSL, with its equal-preference cipher groups, seemed to be the only implementation offering arbitrary selection. The OpenSSL Wiki explains under “Ciphersuites” that applications should configure TLS 1.3 suites with SSL_CTX_set_ciphersuites() or SSL_set_ciphersuites(). The older SSL_CTX_set_cipher_list() interface configures TLS 1.2 and earlier instead. The command-line tools likewise provide a separate -ciphersuites option containing a colon-separated preference list.
With that distinction established, we can modify the Nginx code that configures cipher suites. In src/http/modules/ngx_http_ssl_module.c, ngx_http_ssl_merge_srv_conf() configures the certificate, private key, and ciphers:
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 is the ngx_str_t holding the value of ssl_ciphers. The following ngx_ssl_ciphers() function calls OpenSSL’s SSL configuration API:
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;
}
|
My somewhat destructive solution splits the configured suites at |, placing TLS 1.3 suites first. SSL configuration for HTTP, mail, stream, and HTTP gRPC ultimately reaches ngx_ssl_ciphers() in src/event/ngx_event_openssl.c, so changing that function covers all of them. The function separates the two lists and passes each to the appropriate OpenSSL API:
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);
|
After recompiling, configure ssl_ciphers as follows, using | to separate TLS 1.3 from the older suites:
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;
|
Finally, test it with Chromium:

The selected suite is now the first configured entry, TLS_AES_128_GCM_SHA256.
Warning
My programming ability is limited. I accept no responsibility if this causes your server to misbehave in the future.
The patch is available in this GitHub Gist. Suggestions and corrections are welcome.