Let's Encrypt 根证书过期引发的问题

Let's Encrypt 曾经通知由于根证书问题导致可能会存在验证失败问题

2021年9月30日起,较旧的浏览器和设备对Let’s Encrypt证书的信任方式将发生一些小变化。如果您运营着一个普通网站,您将不会察觉到任何不同——绝大多数访问者仍会接受您的Let’s Encrypt证书。如果您提供API或必须支持IoT设备,则您可能需要对此变化加以留意。

虽然我只是一个普通网站,原则上应该不会受到影响,但是由于还运行着一个 git 仓库,所以不完全是一个普通网站。在 vultr 的服务器上,以及 Gitee 的仓库克隆,都遇到了提示证书验证失败的问题。而与此同时,电脑浏览器访问却丝毫不受影响。

到底证书是合法,还是不合法呢?

Let's Encrypt 根证书

Let’s Encrypt有一个称为ISRG Root X1的“根证书”。现代浏览器和设备信任您网站上安装的Let’s Encrypt证书,因为它们的根证书列表中包含了ISRG Root X1。同时为了确保我们颁发的证书能够在较旧的设备上依然受信任,Let’s Encrypt同时还具有一个较旧的根证书的“交叉签名”: DST Root CA X3。

当Let’s Encrypt项目启动的时候,这个较旧的根证书DST Root CA X3帮助我们起步,并使Let’s Encrypt的证书几乎立即受到所有设备的信任。较新的根证书(ISRG Root X1)现在也已广受信任——但是某些较旧的设备将永远不会信任它,因为它们没有获得软件更新(例如iPhone 4或HTC Dream)。单击此处以获取信任ISRG Root X1的平台。

DST Root CA X3证书将于2021年9月30日到期。这意味着在此之后那些不信任ISRG Root X1证书的旧设备在访问使用Let’s Encrypt证书的网站时将开始出现证书警告。不过有一个例外很重要:多亏了DST Root CA X3的特殊“交叉签名”机制,Let’s Encrypt的证书依然可以在不信任ISRG Root X1证书的较旧的Android设备上正常工作。交叉签名机制使得Let’s Encrypt的证书的有效期限可以超过根证书的到期时间,此例外仅适用于Android。

简单来说,Let's Encrypt 要证明自己的身份,也需要有一个广受信任的机构来为其证明 —— 大家电脑里都有的根证书 ISRG Root X1。但根证书本身也是有有效期的,这个证书的有效期到 2021 年 9 月 30 日。因此,当证书过期后,就无法证明证书的有效性了

Ubuntu 提示证书错误

既然如此,那理论上在所有地方都不应该有问题,但是如果在一些服务器上,仍然会提示证书不合法

curl: (60) server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
of Certificate Authority (CA) public keys (CA certs). If the default
bundle file isn't adequate, you can specify an alternate file
using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
the bundle, the certificate verification probably failed due to a
problem with the certificate (it might be expired, or the name might
not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
the -k (or --insecure) option.

首先来看我们到底拿到的证书长什么样(省略了无关的内容)

$ openssl s_client -showcerts -host git.ohyee.cc -port 443

Certificate chain
 0 s:CN = *.ohyee.cc
   i:C = US, O = Let's Encrypt, CN = R3

 1 s:C = US, O = Let's Encrypt, CN = R3
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1

 2 s:C = US, O = Internet Security Research Group, CN = ISRG Root X1
   i:O = Digital Signature Trust Co., CN = DST Root CA X3

可以看到证书链是 DST Root CA X3 -> ISRG Root X1 -> Let's Encrypt -> *.ohyee.cc
由于过老的设备没有把 ISRG Root X1 加入到信任的根证书内,因此为了保证兼容性,Let's Encrypt 使用了由 DST Root CA X3 签发的 ISRG Root X1 证书,由于老版 Android 不会检查信任的根证书的有效期,因此签发出的超过 DST Root CA X3 有效期的 ISRG Root X1 证书有效。从而保证了在老版 Android 仍然可以正常访问 Let’s Encrypt 的证书。

Let's Encrypt 证书链Let's Encrypt 证书链

但由于 OpenSSL 1.0.x 存在 Bug,当检查有证书不合法时,就会停止验证,返回错误。而 OpenSSL 1.1.x 后,则会尝试将所有的证书都进行验证。而这里由于存在 DST Root CA X3、ISRG Root X1 两个证书,前者已经过期。因此在低版本 OpenSSL 时,会验证失败。

解决方案

原则上只要保证 OpenSSL 版本在 1.1.0 以上即可,但是实际上由于 OpenSSL 本身是一个比较底层的依赖,在部分系统可能会由于兼容性不支持直接更新到 OpenSSL 1.1.x

一个选择是直接去下载 OpenSSL 1.1.x 然后替换,但是这有概率导致系统出问题,因此另一个选择就是删除掉系统里的 DST Root CA X3,这样 OpenSSL 就会验证 ISRG Root X1 从而达到目的。

首先是修改证书列表 vim /etc/ca-certificates.conf,在 mozilla/DST_Root_CA_X3.crt 前添加 !,而后使用 update-ca-certificates -f 更新证书即可

(但是由于我们无法修改别人服务器的配置,因此仍然无法保证可访问性。如 Gitee 拉取代码,可能会提示证书错误)

参考资料