一行 Golang 代码引起的血案——全网最详细分析 2020 年 3 月 Let’s Encrypt 证书吊销事故

Let's Encrypt 做为一家免费提供 SSL 证书的组织,旨在推动互联网向更安全的 HTTPS 迁移,受到了大量小型网站的支持和承认。然而不少站长在 3 月 3 日收到了来自 Let's Encrypt 名为 ACTION REQUIRED: Renew these Let's Encrypt certificates by March 4 的邮件,警告站长尽快更新证书。那么为何须要更新证书?不更新证书有什么危害?如何更新证书?本文将为读者分析本次 Let's Encrypt 证书漏洞事故的真相。

1、事故概览

首先摘录一下邮件中的部份内容:html

We recently discovered a bug in the Let's Encrypt certificate authority code,described here:https://community.letsencrypt.org/t/2020-02-29-caa-rechecking-bug/114591Unfortunately, this means we need to revoke the certificates that were affectedby this bug, which includes one or more of your certificates. To avoiddisruption, you'll need to renew and replace your affected certificate(s) byWednesday, March 4, 2020. We sincerely apologize for the issue.If you're not able to renew your certificate by March 4, the date we arerequired to revoke these certificates, visitors to your site will see securitywarnings until you do renew the certificate. Your ACME client documentationshould explain how to renew.

邮件大意为:Let's Encrypt 的证书校验代码中存在一个 BUG,部分证书受到了这个 BUG 的影响。咱们将会在 3 月 4 日(周三)开始吊销受影响的证书,若是你的证书在吊销列表中,请当即更新证书到最新版本。git

那么问题来了:发生了什么 BUG 致使这一后果?证书吊销是什么?如何更新证书?这就是接下来要讲解的内容。程序员

2、事故详情

首先我来说解一下到底发生了什么 BUG 引发了如此大的事故。算法

根据邮件中的连接原文,Let's Encrypt 使用了自行研发的一款证书签发软件称为 Boulder,但该软件在 2019 年 7 月 25 日引入了一个 BUG,致使 CAA 记录认证出现错误。浏览器

原文以下:安全

On 2020-02-29 UTC, Let’s Encrypt found a bug in our CAA code. Our CA software, Boulder, checks for CAA records at the same time it validates a subscriber’s control of a domain name. Most subscribers issue a certificate immediately after domain control validation, but we consider a validation good for 30 days. That means in some cases we need to check CAA records a second time, just before issuance. Specifically, we have to check CAA within 8 hours prior to issuance (per BRs §3.2.2.8), so any domain name that was validated more than 8 hours ago requires rechecking.The bug: when a certificate request contained N domain names that needed CAA rechecking, Boulder would pick one domain name and check it N times. What this means in practice is that if a subscriber validated a domain name at time X, and the CAA records for that domain at time X allowed Let’s Encrypt issuance, that subscriber would be able to issue a certificate containing that domain name until X+30 days, even if someone later installed CAA records on that domain name that prohibit issuance by Let’s Encrypt.We confirmed the bug at 2020-02-29 03:08 UTC, and halted issuance at 03:10. We deployed a fix at 05:22 UTC and then re-enabled issuance.Our preliminary investigation suggests the bug was introduced on 2019-07-25. We will conduct a more detailed investigation and provide a postmortem when it is complete.

1. CAA 是什么?

那么 CAA 是什么呢?根据维基百科的解释,CAA 全称为『DNS 证书颁发机构受权』,用于避免非受权的证书生成。服务器

看完定义,可能一些读者依旧对 CAA 的概念感到模糊。这里咱们要明确一下证书的做用以及证书在 SSL 数据传输中所起的角色:app

SSL 数据传输有两个做用,一个是加密,这依靠的是非对称加密,准确说是公私钥构成的加密体系即公开密钥加密,避免互联网中传输的数据被中间人窃听;另外一个是鉴证,也就是依靠数字签名和证书链实现的身份鉴别,若是出现中间人对数据进行窃听或重定向,因为证书包含数字签名,这样客户就能区分什么证书是可信的、什么是不可信的。dom

互联网中大部分证书都是由可信第三方,即证书颁发机构(Certificate Authority)简称 CA 签发。但若是你不须要验证身份,只须要加密数据就可使用不包含可信第三方的自签名证书,这时候证书的鉴证效果再也不存在,只起到了加密的效果。大部分浏览器都不会认可自签名证书,所以会使用醒目的红色标识告诉用户:没法肯定证书是否有效。ide

那么 CAA 究竟是作什么用的呢?我举一个例子好了。若是我在甲 CA 凭借正当身份注册了http://example.comHTTP),对于自签名证书,前面提到了浏览器会拦截,而对于 HTTP 的降级一样有 HSTS 协议能够保证其绝对不可能发生。的证书,那么全部访问我网站的用户均可以经过证书中的信息(例如申请单位、签发单位等)了解这个证书的可信性。若是有攻击者企图拦截数据,就必定会破坏证书(好比使用自签名证书或降级到

但不要忘了,CA 的本质也是企业。不一样的 CA 之间不太可能共享信息,也就是说假若有一个乙 CA 对客户信息鉴别不充分,攻击者就能够在乙 CA 上假冒个人身份也注册一个http://example.comhttp://example.com 的有效证书就有两份,攻击者能够在拦截数据后向用户返回来自乙 CA 的那一份证书,用户依旧没法鉴别是否遭受中间人攻击。证书。这时候关于


图 1. 两个 CA 为同一个域名生成了两份证书


图 2. 普通的中间人攻击,自签名证书将不被用户信任


图 3. 如何使用图 1 的漏洞实现用户无感知的中间人攻击

从图中能够看出:不一样的 CA 没法共享信息形成了中间人攻击的隐患,而这就是 CAA 存在的目的。

2. CAA 有什么用?

CAA 和 A 记录同样,都是 DNS 记录的一部分,若是一个 CA 接受到了证书生成的请求,它首先会访问这个域名在 DNS 中对应的 CAA 记录,查看其中包含的信息。若是 CAA 记录容许这个 CA 生成证书,它才会进行接下来的操做,不然将会拒绝证书申请。除此以外,CAA 记录还支持在证书申请时告知特定邮箱(好比域名持有者),警戒持有者:有用户正在伪造你的身份。

CAA 记录并不是强制标准,但绝大多数的 CA 都遵照了这一规定,毕竟由于违反规定致使证书被滥用,浏览器厂商是有权利吊销 CA 的根证书的。举一个例子:中国沃通由于违规签发证书,致使其根证书被吊销,全部新签发的证书都再也不获得主流操做系统和浏览器的认可,相关新闻能够查看这篇知乎问答。根证书被吊销将会毁灭一家 CA 的信誉和所有业务,这也是为何 CA 如此少、证书申请如此麻烦、Let’s Encrypt 官方对这次漏洞如此重视的主要缘由。

3、事故分析

上面提到了,本次事故出在 CAA 部分代码。那么 Let’s Encrypt 应该如何校验 CAA,又如何进行了错误校验呢?

根据官方说明:Let’s Encrypt 的服务器会在用户申请证书的八小时内对证书对应域名的 CAA 记录进行检查,若是检查经过,接下来的 30 天内都不会对其进行从新检查。

这里的规则实际上不是 Let’s Encrypt 本身制定的,而是来源于 CA/Browser Forum,一个制定 CA 和浏览器关于证书处理规范的论坛。CA/Browser Forum 提供了一份规范 (Baseline Requirements),要求全部 CA 按照规范中的内容进行证书签发和吊销,其中在§3.2.2.8:CAA Records 要求了如下内容:

As part of the issuance process, the CA MUST check for CAA records and follow the processing instructions found, for each dNSName in the subjectAltName extension of the certificate to be issued, as specified in RFC 6844 as amended by Errata 5065 (Appendix A). If the CA issues, they MUST do so within the TTL of the CAA record, or 8 hours, whichever is greater. This stipulation does not prevent the CA from checking CAA records at any other time.

大致上就是:CA 须要在签发证书的八小时内对所签发域名的 CAA 记录进行核查,除此以外还能够在任什么时候间进行其余核查以进一步确保安全。

结合上面官方的说明能够了解到,Let’s Encrypt 严格遵照了这一标准。但 Let’s Encrypt 的 CA 系统犯了一个错误,若是一个证书包含 N 个域名,CA 系统应该对每一个域名都单独进行 CAA 检查,结果却将 N 个域名中的某一个检查了 N 次,其余 N-1 个域名均未被检查而直接经过。

也就是说:若是攻击者发现了这一漏洞,它就能够经过申请多域名证书的方式来绕过 CAA 记录对证书申请的限制。举例而言,攻击者能够申请包含如下域名的证书:

  • example.com
  • some-domain-controlled-by-hacker.com
  • another-domain-controlled-by-hacker.com

在没有以上漏洞的状况下,CA 软件会对三个域名的 CAA 进行检查,这时若是第一个域名的 CAA 记录拒绝 Let’s Encrypt 签发证书,签发流程会所以停止,攻击者没法获得证书。

但若是以上漏洞存在,CA 软件可能只会对 another-domain-controlled-by-hacker.com 或 some-domain-controlled-by-hacker.com 进行 CAA 记录检查(并且是检查三次),由于这个域名被攻击者所控制,所以他能够容许 Let’s Encrypt 进行证书签发,这样就绕过了 example.com 的 CAA 记录限制。

固然,这并不意味着 Let’s Encrypt 的这一漏洞可让攻击者随意伪造身份进行证书申请,由于解除 CAA 限制只是破除 CA 众多检查中的一个,Let’s Encrypt 的 HTTP 验证、DNS 验证分别须要对服务器或 DNS 进行实质性控制。

须要注意的是,利用难度大,也不意味着这一漏洞的存在是合理的。

首先 Let’s Encrypt 是一家 CA,必须遵照相关规定,且为客户的安全负责(尽管客户并未付费);更重要的是,Let’s Encrypt 并不是一个独立组织,而是隶属于互联网安全研究小组,致力于加强全互联网的信息安全,做为互联网安全的推动者绝对不能首先破除规则。

其次,若是攻击者恰好拿到了服务器控制权,那么有 CAA 的限制,攻击者依旧没法成功申请证书。但若是 Let’s Encrypt 未能合理对 CAA 进行检查,即攻击者不只发现了此漏洞,还拿到了服务器控制权,那么伪造身份将会变得易如反掌。至于控制 DNS,攻击者彻底能够删除 CAA 记录,所以引起的事故属于 CA 能力范围之外,CA 也无需为此负责。

这就是为何 Let's Encrypt 对这一事故的处理如此严肃,甚至在事故发生后马上关闭了受影响的两台 CA 服务器,还发布了所受影响 300 万个证书的 Hash(压缩包高达 300MB+),同时向全部在申请证书时附带邮件地址的用户紧急发送邮件。做为一家 CA,Let's Encrypt 无疑是负责的;做为互联网安全研究小组的项目,Let's Encrypt 对事故的处理态度无疑也为其余 CA 起到了模范做用。

图 4. Let’s Encrypt 官方的服务中断公告,在事故发生后马上关闭了受影响的 CA 服务器

图 5. 用户反馈申请了 100 个域名的证书后,发现出现了 100 次如出一辙的报错,全部报错都由于其中 一个域名的 CAA 记录不容许 Let’s Encrypt 签发证书。而 Let’s Encrypt 收到用户的 BUG 反馈后马上意识到这是一个安全事故,进行了相关处理。

4、一行 Golang 代码引起的血案

Let’s Encrypt 的态度无疑让人对其肃然起敬,但这并不意味着 Let’s Encrypt 不须要为此负责。

阅读完上面的事故分析,可能仍是有不少读者不清楚:明明应该校验每一个域名,究竟是什么 BUG 致使了 Let’s Encrypt 只校验了其中一个呢?

在文章的最开始,我提到了 Let’s Encrypt 使用了一款叫作 Boulder 的软件。其实这是一款开放源代码的软件,地址为 letsencrypt/boulder。

该软件使用 Golang 开发,旨在实现一个 ACME 协议的 CA 服务器,Let’s Encrypt 的官方 CA 服务器运行着该软件。

那么这个软件到底出现了什么问题才会致使如此滑稽的故障?我翻看着 Let’s Encrypt 最近的 commit,找到了一个 Pull Request:#4690。看完这个 Pull Request 后,我立刻意识到问题所在:Golang 最经典的错误——循环迭代变量陷阱。

对于不熟悉 Golang 的读者,可能不知道我在说什么,这里我使用 C 语言举一个例子:

int main() {    int* arr[3];    for (int i = 0; i < 3; i++) {        arr[i] = &i;    }    printf("%d %d %d", *arr[0], *arr[1], *arr[2]);    return 0;}

大部分读者应该都熟悉 C 语言,应该能够看出上面的例子返回的结果是3 3 3而非1 2 3,由于arr的三个元素都是i的地址,而i最终的值为3

做为『21 世纪的 C 语言』,Golang 一样存在这一问题:

func main() {    var out []*int    for i := 0; i < 3; i++ {        out = append(out, &i)    }    fmt.Println("Values:", *out[0], *out[1], *out[2])    fmt.Println("Addresses:", out[0], out[1], out[2])}

输出结果为:

Values: 3 3 3Addresses: 0x40e020 0x40e020 0x40e020

因为这一问题过于广泛,Golang 甚至将其写入了文档的『常见错误』部分:文档

而这一『常见错误』,就出如今 Let’s Encrypt 的代码中。

咱们倒回这个 Pull Request 以前的代码,来看看这一错误如何在 Boulder 中重现:

// authzModelMapToPB converts a mapping of domain name to authzModels into a// protobuf authorizations mapfunc authzModelMapToPB(m map[string]authzModel) (*sapb.Authorizations, error) {    resp := &sapb.Authorizations{}    for k, v := range m {        // Make a copy of k because it will be reassigned with each loop.        kCopy := k        authzPB, err := modelToAuthzPB(&v)        if err != nil {            return nil, err        }        resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{Domain: &kCopy, Authz: authzPB})    }    return resp, nil}// ...func modelToAuthzPB(am * authzModel)( * corepb.Authorization, error) {    expires: = am.Expires.UTC().UnixNano()    id: = fmt.Sprintf("%d", am.ID)    status: = uintToStatus[am.Status]    pb: = & corepb.Authorization {            Id: & id,            Status: & status,            Identifier: & am.IdentifierValue,            RegistrationID: & am.RegistrationID,            Expires: & expires,        }        //...}

看到这里,眼尖的读者可能已经意识到问题了。对于循环变量 k,该函数拷贝了一份(甚至还贴心的加了一个注释),而后再在resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{Domain: &kCopy, Authz: authzPB})将其以引用的方式传递出去。须要注意的 Golang 对于重复声明的变量会使用不一样地址,所以每次循环传递出去的地址都不同。

但滑稽的是,另外一个循环变量 v 却未能获得宠幸,开发者不知道什么缘由忘记对其进行拷贝。代码中的authzPB, err := modelToAuthzPB(&v)这部分,传递出去的是未经复制的引用,形成了 resp 中全部的 authzPB 数据都被设定为循环的最后一个 v,其中包含对应域名的全部信息。

更多代码能够在这里看到。

那么这个 BUG 是如何引入的呢?我使用 git blame 对附近的代码进行检查,发现这段代码在2019年4月24日随着 Pull Request #4134 带入。

此次 Pull Request 新增代码量高达 2750 行,并且几乎全是新增功能,在测试不充分的状况下的确容易将这一 BUG 遗漏。有趣的是:2019 年引入 BUG 的做者和 2020 年 Merge 对应代码的人是同一人,即_@_rolandshoemaker。

看来就算是顶尖的程序员,也没法保证写出彻底没有 BUG 的软件🤣。

5、解决事故

写到这里,相信你们应该对此次事故有着很是详细的了解了。接下来咱们要谈的是如何解决这次事故的影响。

根据官方描述,这次受到影响的域名签发日期在 2019-12-04 到 2020-02-29 之间。你能够在浏览器中点击域名左边的小锁图标来查看签发时间:

若是你的域名签发时间在此日期以外,那么基本无需担忧,但若是签发时间在此日期以内,请接着往下读:

对于收到警告邮件的读者,请留意邮件中的域名。我收到的邮件中就有我本身博客的域名。

若是没有收到警告邮件,但不肯定本身的域名是否受影响,有两种方式能够验证:

  1. 在这里下载全部受影响证书列表,解压后在命令行执行如下代码获取域名对应证书 hash,再在列表进行查询。 go openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | openssl x509 -text -noout | grep -A 1 Serial\ Number | tr -d :
  2. 在这个网站输入你的域名便可检查域名所带证书是否受到影响。

我我的推荐第二种,第一种更适合代理分发 Let’s Encrypt 证书的第三方如宝塔面板等。

若是你的域名不在受影响范围内,不用进行任何操做。不过其实就算是在受影响范围内也无需进行操做,由于这一次吊销列表实在太大,几乎全部的浏览器都不会根据这一列表对证书进行吊销,而 Let’s Encrypt 的证书三个月以后就会过时,过时后从新申请是不会出现任何问题的。

但本着安全起见,最好使用你的证书申请客户端对证书进行强制重申请。这里我以 certbot-auto 为例:

certbot-auto renew --force-renewal

执行后全部的证书都会从新进行签发。

签发完成后,记得重启你的 Web 服务器如 Nginx,以确保新的证书被正确装载,这样就能让本身的域名完全免遭这次事故影响。

6、避免事故

在事故状态更新的帖子下面,Let’s Encrypt 官方向用户保证了接下来将会对其余模块进行一样的安全检查:

Improve TestGetValidOrderAuthorizations2 unittest (3 weeks).Implement modelToAuthzPB unittest (3 weeks).Productionize automated logs verification for CAA behavior (8 weeks).Review code for other examples of loop iterator bugs (4 weeks).Evaluate adding stronger static analysis tools to our automated linting suite and add one or more if we find a good fit (4 weeks).Upgrade to proto3 (6 months).

做为一家 CA,咱们其实无需过多担忧 Let’s Encrypt 从此是否会出现相似事故,由于它们对于此次事故的重视程度实在使人惊叹,这也是我决定撰写这篇文章的缘由。

经历这次事故后的 Let’s Encrypt 不只没有像其余 CA 同样损失用户,反而更进一步赢得了用户的信赖:不管是开源 CA 服务器、是公开服务状态、是主动复盘故障、是故障后马上中止服务且提示受影响用户仍是快速解决问题,这都让 Let’s Encrypt 与其余不负责任的 CA 不一样。

的确,Let’s Encrypt 的 SSL 证书只起到了加密的做用,对于鉴证不如其余商业 CA 有效,但在过去的数年里,Let’s Encrypt 用本身的行动让全互联网变得更加安全——三年前我还在吐槽各个浏览器使用『使人恶心』的方式警告 HTTP 站点,三年后竟很难找到一个不是 HTTPS 的网站。

这就是互联网开放之魅力:任何人均可以参与到互联网基础设施的建设中,而这偏偏是在其余传统行业所很难见到的。开放意味着人人平等,意味着每一个人均可以发现问题、能够参与到问题的分析中、甚至能够帮助解决问题。互联网的建设者们也和现实世界的官僚不一样,他们不多表露出傲慢,不管是规范的制定、是开放源代码软件的开发仍是社区的讨论,你的贡献和你所获得的声望永远是对等的。

互联网为何如此有魅力?魅力在于人人生来平等。


那么,对于咱们普通用户,此次事故有哪些值得吸收的教训呢?

最浅显的教训应该是在申请证书时附上本身正确的邮箱地址。在我收到这一封邮件后,我和其余几个好友分享了邮件内容,他们却表示申请时乱填了一个地址,致使没有收到警告。这一次事故可能比较小,但若是下一次事故是能够无条件伪造身份呢?

目前的邮箱都有着很复杂的 spam 识别规则,所以个人建议不只是在申请证书时,而是在进行任何注册操做时都附上正确邮箱地址。不用担忧 spam 骚扰——按规则将它们拉入垃圾箱便可。

可能一些读者还会吸收另外一个教训:对本身的域名在 DNS 中增长 CAA 记录:这是一个很是好的习惯,但若是是小型网站,在确保不包含关键业务,且没有潜在竞争者状况下,基本上无需担忧。

但我以为教训应该不止于此。在我从事软件开发工做的两年半时间里,不少比我资历还要久的前辈常常会感到困惑:为何我能在这么短的时间里学习这么多东西?个人回答其实很简单:不要放过任何一点细节。

我其实不是很是聪明的人,我一直以来对本身的要求就是『笨鸟先飞』,我在算法、理论、计算机基础方面相比其余同龄从业者都属于底层。但我永远相信勤能补拙,我坚信学习的力量,认为肤浅的了解不如不学,认同终生学习。

实际上在写到这里的时候(本文从收到邮件开始撰写,花了三天时间撰写 + 校对,写到这里的时候是 3 月 7 日)我查了一下国内外的新闻。事故发生已经超过三天,竟然没有一个媒体/博客对这个事故的根源进行分析!这太使人感到遗憾了。

但我相信,读到这里的读者必定已经收获了不少新知识。写到这里的我一样学习了很是多——在写这篇文章以前,我其实对于 SSL 证书的概念处于只知其一;不知其二状态,因而我边写边理顺思路,边写边查资料,全文写完以后,我对于 SSL 证书的理解就已经彻底不一样于写做前。阅读和写做都是学习的一种过程,前者被动学习、后者主动学习,除此以外并没有高下之分。

是否只有阅读和写做才能帮助本身?不彻底是。由于平常事务太多,个人博客中所分享的实际上是平常所掌握知识的不多一部分,也不多阅读那些『计算机经典书籍』。

而学习应该是随时随地的,是不放过任何一点细节的,是无论内容是否与本身所从事事业相关,只要是未知领域都勇于探索的。若是只是为了从事某项特化的岗位而成为软件工程师,未免太没劲了。

有一句成语叫作『求贤若渴』,我想把它改造一下,做为本文的结束,那就是:

求学若渴

感谢读者们能耐心读完本文,也但愿读到这里的读者能有所启发。

本文受权转载自:公众号GoCN(GolangChina 论坛原创投稿)
做者博客地址: https://untitled.pw/software/...
论坛阅读连接: https://gocn.vip/topics/9967
相关文章
相关标签/搜索