Apache + TLS 1.3

10月23日, Apache更新了正式支持TLS 1.3的2.4.37版本。(可能成为了Nginx后第二个支持TLS 1.3且被广泛使用的HTTP服务器)。

经过一番尝试,我成功地在自己的VPS上令Apache支持TLS 1.3(RFC8446)。简记其过程与踩到的坑。

编译安装

支持TLS 1.3需要OpenSSL 1.1.1, 而大多数发行版的源并未提供该版本,故需要自行编译。

$ cd openssl-1.1.1/
$ ./config --prefix=/opt/openssl-1.1.1 --openssldir=/etc/ssl \
enable-tls1_3 shared \
{your_own_compile_arguments} \
-Wl,-rpath=/opt/openssl-1.1.1/lib

$ make -j4
# make install

之后,编译安装Apache
(注:某些老旧系统的源提供的aprapr-util,可能版本过低而无法使用。亦需自行编译后,在Apache编译参数中用--with-apr=--with-apr-util=指定其路径。)

// Debian / Ubuntu
# apt install libapr1-dev libaprutil1-dev
// Fedora(<22) / RHEL / CentOS
# yum install apr-devel apr-util-devel
$ cd httpd-2.4.37/
$ ./configure --prefix=/opt/httpd --enable-so --enable-mpms-shared=all \
--enable-ssl --with-ssl=/opt/openssl-1.1.1 --with-crypto \
{your_own_compile_arguments} \
LDFLAGS="-Wl,-rpath=/opt/openssl-1.1.1/lib"
# make install

配置文件

编译安装完成后,修改配置文件。

// 仅启用安全的TLS 1.3和TLS 1.2协议。
SSLProtocol -All +TLSv1.3 +TLSv1.2
// TLS 1.3加密套件
SSLCipherSuite TLSv1.3 TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
// SSLv3 ~ TLS 1.2加密套件
SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384
// 椭圆曲线(Named curve)和DH生成元(Diffie-Hellman Parameter)
SSLOpenSSLConfCmd Curves X25519:P-256:P-384
SSLOpenSSLConfCmd DHParameters /etc/ssl/private/dhparam_4096.pem
// 在不支持AES-NI的设备上优先使用CHACHA20-POLY1305
SSLOpenSSLConfCmd Options "+PrioritizeChaCha"

  1. TLS 1.3不支持重协商,因此无法在连接建立后修改加密套件。因此,SSLCipherSuite TLSv1.3 {ciphers}不能置于<Directory /path></Directory>之间。
  2. SSLOpenSSLConfCmd SignatureAlgorithms {sig_algs}会令TLS 1.3连接建立失败。此参数用于设定对临时密钥(DHE/ECDHE)签名所用的算法,TLS 1.3的算法命名和以前版本的"非对称加密算法+哈希算法"格式不一样。前者如rsa_pss_pss_sha256,ecdsa_secp256r1_sha256;后者则类似RSA+SHA384格式。
  3. SSLOpenSSLConfCmd ClientSignatureAlgorithms {sig_algs}会令使用TLS 1.3时无法完成双向验证(客户端验证)。该参数制定客户端所被允许使用的签名算法,故障原因与上条同理。

填坑

OpenSSL的官网上只提供了如下几个适用于TLS 1.3的签名算法:

  • ecdsa_secp256r1_sha256
  • ed25519
  • rsa_pss_pss_sha256

而这些都没被浏览器(Google Chrome & Mozilla Firefox)普遍支持,这便是上文中指定签名算法后TLS 1.3握手失败的原因。

而根据SSL Labs的SSL Client Test提供的结果,Chrome和Firefox均支持如下TLS 1.3签名算法:

  • RSA_PSS_SHA256
  • RSA_PSS_SHA384
  • RSA_PSS_SHA512

通过grep -rnwI /path/to/openssl-source --include=*.c --exclude-dir=doc/ -Ee 'rsa_pss_pss_sha256'检索OpenSSL官网上提供的一个签名算法,得到如下结果:

./ssl/t1_trce.c:559:    {TLSEXT_SIGALG_rsa_pss_pss_sha256, "rsa_pss_pss_sha256"},
./ssl/t1_lib.c:713:    {"rsa_pss_pss_sha256", TLSEXT_SIGALG_rsa_pss_pss_sha256,

其中,ssl/t1_trce.c的548-575行定义了一个数组,里面包含所有签名算法的名称:

$ sed -n 548,575p ssl/t1_trce.c
static const ssl_trace_tbl ssl_sigalg_tbl[] = {
    {TLSEXT_SIGALG_ecdsa_secp256r1_sha256, "ecdsa_secp256r1_sha256"},
    {TLSEXT_SIGALG_ecdsa_secp384r1_sha384, "ecdsa_secp384r1_sha384"},
    {TLSEXT_SIGALG_ecdsa_secp521r1_sha512, "ecdsa_secp521r1_sha512"},
    {TLSEXT_SIGALG_ecdsa_sha224, "ecdsa_sha224"},
    {TLSEXT_SIGALG_ed25519, "ed25519"},
    {TLSEXT_SIGALG_ed448, "ed448"},
    {TLSEXT_SIGALG_ecdsa_sha1, "ecdsa_sha1"},
    {TLSEXT_SIGALG_rsa_pss_rsae_sha256, "rsa_pss_rsae_sha256"},
    {TLSEXT_SIGALG_rsa_pss_rsae_sha384, "rsa_pss_rsae_sha384"},
    {TLSEXT_SIGALG_rsa_pss_rsae_sha512, "rsa_pss_rsae_sha512"},
    {TLSEXT_SIGALG_rsa_pss_pss_sha256, "rsa_pss_pss_sha256"},
    {TLSEXT_SIGALG_rsa_pss_pss_sha384, "rsa_pss_pss_sha384"},
    {TLSEXT_SIGALG_rsa_pss_pss_sha512, "rsa_pss_pss_sha512"},
    {TLSEXT_SIGALG_rsa_pkcs1_sha256, "rsa_pkcs1_sha256"},
    {TLSEXT_SIGALG_rsa_pkcs1_sha384, "rsa_pkcs1_sha384"},
    {TLSEXT_SIGALG_rsa_pkcs1_sha512, "rsa_pkcs1_sha512"},
    {TLSEXT_SIGALG_rsa_pkcs1_sha224, "rsa_pkcs1_sha224"},
    {TLSEXT_SIGALG_rsa_pkcs1_sha1, "rsa_pkcs1_sha1"},
    {TLSEXT_SIGALG_dsa_sha256, "dsa_sha256"},
    {TLSEXT_SIGALG_dsa_sha384, "dsa_sha384"},
    {TLSEXT_SIGALG_dsa_sha512, "dsa_sha512"},
    {TLSEXT_SIGALG_dsa_sha224, "dsa_sha224"},
    {TLSEXT_SIGALG_dsa_sha1, "dsa_sha1"},
    {TLSEXT_SIGALG_gostr34102012_256_gostr34112012_256, "gost2012_256"},
    {TLSEXT_SIGALG_gostr34102012_512_gostr34112012_512, "gost2012_512"},
    {TLSEXT_SIGALG_gostr34102001_gostr3411, "gost2001_gost94"},
};

我尝试了rsa_pss_rsae_sha256,发现这个签名算法不会令TLS 1.3协商失败。由此看来,OpenSSL对签名算法的命名或与SSL Labs的命名不同。

由此地,在mod_ssl配置文件中添加如下两行,既停用了(TLS 1.2及更低版本)所用的不安全签名算法(e.g. SHA1+RSA),也不会令TLS 1.3握手失败。

SSLOpenSSLConfCmd SignatureAlgorithms rsa_pss_rsae_sha512:rsa_pss_rsae_sha256:ECDSA+SHA512:ECDSA+SHA256:RSA+SHA512:RSA+SHA256
SSLOpenSSLConfCmd ClientSignatureAlgorithms rsa_pss_rsae_sha512:rsa_pss_rsae_sha256:ECDSA+SHA512:ECDSA+SHA256:RSA+SHA512:RSA+SHA256

经测试,添加该参数后TLS 1.3握手正常;TLS 1.3下的客户端认证也不受影响。

备注:

本文所提及内容在Ubuntu 16.04, Ubuntu 18.04, Debian GNU/Linux 9, CentOS 7 (201804)上测试无问题,不保证其他发行版/操作系统也正常。

上述所有操作系统皆使用官方源提供的内核。

参考资料:

  1. www.apache.org/dist/httpd/CHANGES_2.4.37
  2. mod_ssl - Apache HTTP Server Version 2.4
  3. /docs/manmaster/man3/SSL_CONF_cmd.html