为什么我强烈建议你使用ECC 证书

代码语言:javascript
复制
作者:Jerry Qu
原文:https://imququ.com/post/ecc-certificate.html

本文包含三部分内容:1)简单介绍 ECC 证书是什么;2)介绍如何申请 ECC 证书;3)以 Nginx 为例介绍如何使用 ECC 证书。

简单介绍

HTTPS 通过 TLS 层和证书机制提供了内容加密、身份认证和数据完整性三大功能,可以有效防止数据被监听或篡改,还能抵御 MITM(中间人)攻击。TLS 在实施加密过程中,需要用到非对称密钥交换和对称内容加密两大算法。

对称内容加密强度非常高,加解密速度也很快,只是无法安全地生成和保管密钥。在 TLS 协议中,应用数据都是经过对称加密后传输的,传输中所使用的对称密钥,则是在握手阶段通过非对称密钥交换而来。常见的 AES-GCM、ChaCha20-Poly1305,都是对称加密算法。

非对称密钥交换能在不安全的数据通道中,产生只有通信双方才知道的对称加密密钥。目前最常用的密钥交换算法有 RSA 和 ECDHE:RSA 历史悠久,支持度好,但不支持 PFS(Perfect Forward Secrecy);而 ECDHE 是使用了 ECC(椭圆曲线)的 DH(Diffie-Hellman)算法,计算速度快,支持 PFS。要了解更多 RSA 和 ECDHE 密钥交换的细节,可以阅读 Cloudflare 的这篇文章:https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/。

只有非对称密钥交换,依然无法抵御 MITM 攻击,还得引入身份认证机制。对于大部分 HTTPS 网站来说,服务端一般通过 HTTP 应用层的帐号体系就能完成客户端身份认证;而浏览器想要验证服务端身份,需要用到服务端提供的证书。

浏览器会在两个步骤中用到证书:1)证书合法性校验。确保证书由合法 CA 签署,且适用于当前网站;2)使用证书提供的非对称加密公钥,完成密钥交换和服务端认证。

证书合法性校验的原理,简单总结如下:

  • 根据版本号、序列号、签名算法标识、发行者名称、有效期、证书主体名、证书主体公钥信息、发行商唯一标识、主体唯一标识、扩展等信息,生成 TBSCertificate(To Be Signed Certificate)信息;
  • 签发数字签名:使用 HASH 函数对 TBSCertificate 计算得到消息摘要,再用 CA 的私钥进行加密,得到签名;
  • 校验数字签名:使用相同的 HASH 函数对 TBSCertificate 计算得到消息摘要,与使用 CA 公钥解密签名得到内容相比较;

可以看到校验证书需要同时用到签名和非对称加密算法:目前必须使用 SHA-2 做为证书签名函数(没有打 XP SP3 补丁的 IE6 不支持);目前一般使用 RSA 算法对 TBSCertificate 进行非对称加密。可以通过 openssl 工具来查看证书签名算法:

代码语言:javascript
复制
$ openssl x509 -in chained.pem -noout -text | grep 'Signature Algorithm'

Signature Algorithm: sha256WithRSAEncryption

大部分 CA 都有证书链,浏览器对于收到的多级证书,需要从站点证书开始逐级验证,直至出现操作系统或浏览器内置的受信任 CA 根证书。

浏览器还需要校验当前访问的域名是否存在于证书 TBSCertificate 的 Common NameSubject Alternative Name 字段之中。

在 RSA 密钥交换中,浏览器使用证书提供的 RSA 公钥加密相关信息,如果服务端能解密,意味着服务端拥有证书对应的私钥,同时也能算出对称加密所需密钥。密钥交换和服务端认证合并在一起。

在 ECDHE 密钥交换中,服务端使用证书私钥对相关信息进行签名,如果浏览器能用证书公钥验证签名,就说明服务端确实拥有对应私钥,从而完成了服务端认证。密钥交换和服务端认证是完全分开的。

可用于 ECDHE 数字签名的算法主要有 RSA 和 ECDSA,也就是目前密钥交换 + 签名有三种主流选择:

  • RSA 密钥交换(无需签名);
  • ECDHE 密钥交换、RSA 签名;
  • ECDHE 密钥交换、ECDSA 签名;

以下是 Chrome 中这三种密钥交换方式的截图(截图来自于早期 Chrome,新版 Chrome 查看位置有了变化):

内置 ECDSA 公钥的证书一般被称之为 ECC 证书,内置 RSA 公钥的证书就是 RSA 证书。由于 256 位 ECC Key 在安全性上等同于 3072 位 RSA Key,加上 ECC 运算速度更快,ECDHE 密钥交换 + ECDSA 数字签名无疑是最好的选择。由于同等安全条件下,ECC 算法所需的 Key 更短,所以 ECC 证书文件体积比 RSA 证书要小一些。以下是本站的对比,可以看到左侧的 ECC 证书要小 1/3:

RSA 证书可以用于 RSA 密钥交换(RSA 非对称加密)或 ECDHE 密钥交换(RSA 非对称签名);而 ECC 证书只能用于 ECDHE 密钥交换(ECDSA 非对称签名)。

并不是所有浏览器都支持 ECDHE 密钥交换,也就是说 ECC 证书的兼容性要差一些。例如在 Windows XP 中,使用 ECC 证书的网站只有 Firefox 能访问(Firefox 的 TLS 自己实现,不依赖操作系统);Android 平台中,也需要 Android 4+ 才支持 ECC 证书。

好消息是,Nginx 1.11.0 开始提供了对 RSA/ECC 双证书的支持。它的实现原理是:分析在 TLS 握手中双方协商得到的 Cipher Suite,如果支持 ECDSA 就返回 ECC 证书,否则返回 RSA 证书。

也就是说,配合最新的 Nginx,我们可以使用 ECC 证书为现代浏览器提供更好的体验,同时老旧浏览器依然会得到 RSA 证书,从而保证了兼容性。这一次,鱼与熊掌可以兼得。

如何申请

如果你的 CA 支持签发 ECC 证书,使用以下命令生成 CSR(Certificate Signing Request,证书签名请求)文件并提交给提供商,就可以获得 ECC 证书:

代码语言:javascript
复制
openssl ecparam -genkey -name secp256r1 | openssl ec -out ecc.key
openssl req -new -key ecc.key -out ecc.csr

以上命令中可供选择的算法有 secp256r1 和 secp384r1,secp521r1 已被 Chrome 和 Firefox 废弃。

我目前在用的 Let’s Encrypt,也支持签发 ECC 证书。我使用了 acme.sh 这个小巧的工具来签发证书,指定 -k ec-256 就可以将证书类型改为 ECC:

代码语言:javascript
复制
"/root/.acme.sh"/acme.sh --issue --dns dns_cx -d imququ.com -d www.imququ.com -k ec-256

目前 Let’s Encrypt 只提供 RSA 中间证书,官方预计会在 2017 年 3 月底提供 ECC 中间证书(via)。

如何使用

有了 RSA/ECC 双证书之后,还需要安装 Nginx 1.11.x。这部分内容我之前详细写过,请点击查看。

一切准备妥当后,将证书配置改为双份即可:

代码语言:javascript
复制
ssl_certificate     example.com.rsa.crt;ssl_certificate_key example.com.rsa.key;ssl_certificate     example.com.ecdsa.crt;ssl_certificate_key example.com.ecdsa.key;

问题来了!本站使用 Cloudflare 提供的 Cipher Suites 配置,在 Nginx 中配置了双证书并重启,用 Chrome 测试发现仍然没有采用 ECC 证书。这是为什么呢?

代码语言:javascript
复制
# https://github.com/cloudflare/sslconfig/blob/master/confssl_ciphers                 EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;ssl_prefer_server_ciphers   on;

研究发现,Chrome 与服务端协商到的 Cipher Suites 是 ECDHE-RSA-AES128-GCM-SHA256,来自于 ssl_ciphers 配置中的 EECDH+AES128 这部分。我们通过 openssl 工具看一下 EECDH+AES128 具体包含哪些 Cipher Suites:

代码语言:javascript
复制
openssl ciphers -V 'EECDH+AES128' | column -t

0xC0,0x2F - ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD
0xC0,0x2B - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
0xC0,0x27 - ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256
0xC0,0x23 - ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256
0xC0,0x13 - ECDHE-RSA-AES128-SHA SSLv3 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA1
0xC0,0x09 - ECDHE-ECDSA-AES128-SHA SSLv3 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA1

可以看到,使用 RSA 做为签名认证算法(Au=RSA)的加密套件排到了前面,导致 Nginx 作出了错误判断。

知道原因就好办了,将这段配置改为 EECDH+ECDSA+AES128:EECDH+aRSA+AES128,再看一下:

代码语言:javascript
复制
openssl ciphers -V 'EECDH+ECDSA+AES128:EECDH+aRSA+AES128' | column -t

0xC0,0x2B - ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(128) Mac=AEAD
0xC0,0x23 - ECDHE-ECDSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA256
0xC0,0x09 - ECDHE-ECDSA-AES128-SHA SSLv3 Kx=ECDH Au=ECDSA Enc=AES(128) Mac=SHA1
0xC0,0x2F - ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(128) Mac=AEAD
0xC0,0x27 - ECDHE-RSA-AES128-SHA256 TLSv1.2 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA256
0xC0,0x13 - ECDHE-RSA-AES128-SHA SSLv3 Kx=ECDH Au=RSA Enc=AES(128) Mac=SHA1

这下就没问题了。

并不是所有加密套件都需要把 ECDSA 和 aRSA 分开写,例如 EECDH+CHACHA20 就不需要,ECDSA 默认就在前面:

代码语言:javascript
复制
openssl ciphers -V 'EECDH+CHACHA20' | column -t

0xCC,0xA9 - ECDHE-ECDSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=ECDSA Enc=ChaCha20-Poly1305 Mac=AEAD
0xCC,0xA8 - ECDHE-RSA-CHACHA20-POLY1305 TLSv1.2 Kx=ECDH Au=RSA Enc=ChaCha20-Poly1305 Mac=AEAD

最终,我的 Cipher Suites 配置如下,供参考:

代码语言:javascript
复制
ssl_ciphers                EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AES128:EECDH+aRSA+AES128:RSA+AES128:EECDH+ECDSA+AES256:EECDH+aRSA+AES256:RSA+AES256:EECDH+ECDSA+3DES:EECDH+aRSA+3DES:RSA+3DES:!MD5;