根证书存储的供应链安全

·6 分钟阅读时长

不同系统与运行环境中的根证书存储的安全性如何?应用开发中证书验证实现是否完全可信?结合对一篇论文的概要解读与 go 语言 x509 标准库的实现细节,谈谈根证书存储的供应链信任。

论文概要

之前在如何检测与清理不可信的 CA 证书 中提到过 Tracing Your Roots: Exploring the TLS Trust Anchor Ecosystem 这篇论文。

根据论文作者的研究,用来进行 TLS 服务器身份验证的根存储,主要来自于三个根程序:Apple、Microsoft、Mozilla NSS。Apple 和 Microsoft 集中在两家公司的产品中应用,而 NSS 除了 Mozilla 旗下产品,还有着众多的产品使用了从 NSS 衍生的根存储,如 Linux 各发行版、Android、NodeJS 等等。

该论文写作时 Chrome 的独立根程序 还在过渡期,主要用在 ChromeOS/Linux 中。当前根证书列表 更新日期为 2022-02-16。

Root Store Ecosystem

根程序比较

Apple、Microsoft、Mozilla NSS 这三个主要根程序之间的比较:

  • 管理:在清理不安全的(弱加密、过期等)证书方面,NSS 最好,Apple 次之,Microsoft 最差。
  • 排他性差异:Microsoft 对来自国家政府的超级 CA 更宽容,因而安全风险也最高。
  • 高风险证书的移除:对多个高风险根证书处理时间与策略的对比中,总体而言 NSS 做得更好(但需要注意的是,这不等同于 NSS 衍生的根存储,后面会提到)。

关于从 NSS 衍生的根存储:

  • Alpine Linux 的更新最频繁,最接近 NSS。
  • NSS 与其衍生者之间的根本区别之一是 NSS 能够为每个根指定信任目的,以及逐渐不信任。对于出现风险的根证书,多数衍生者无法提供精确的处理。例如需要对某个 CA 指定日期之前和之后颁发进行不同的信任、对 CA 证书用途的限制(TLS 身份验证、代码签名、电子邮件签名)等。
  • 衍生者会进行自定义修改,有些修改是及时处理风险,也有是为自身的用户需求(与兼容)而放宽安全限制。

问题与解决建议

论文提到了几个主要问题的解决建议:

  • NSS 衍生者,无法高保真地复制它,例如部分信任特性。需要更现代化的根存储格式和开发者易用性。
  • 多用途根存储会导致非预期信任。例如电子邮件签名根用于 TLS 服务器身份验证,或 TLS 服务器身份验证用于代码签名验证等。一是避免多用途根证书颁发,二是使用单一用途的根存储(RHEL 和 AmazonLinux 最近开始实施)。长远来看 PKI 需要更具可扩展性、跨平台的设计。
  • 根信任的数据透明度。根程序对具体根 CA 授予信任的原因、审核过程、CA 的行为监控等都是透明度范畴,CCADB 等开始有相关改进。

开发中要关注的问题

论文中主要涉及的是根程序和根存储,在实际应用的开发与运行中,还有一些问题需要考虑。前段时间写了一个命令行程序 ctlcheck ,用来检测操作系统中的根存储是否有意外安装或未及时删除的不可信根证书。过程中有一些有趣地发现。

golang 的 TLS 验证

golang 对 TLS 连接时的证书进行验证,使用了标准库 x509

// If opts.Roots is nil, the platform verifier might be used, and
// verification details might differ from what is described below. If system
// roots are unavailable the returned error will be of type SystemRootsError.
func (c *Certificate) Verify(opts VerifyOptions) (chains [][]*Certificate, err error)
  1. 如果 opts 参数没有提供自定义的根证书(opts.Rootsnil),将会使用平台(操作系统)的根存储进行验证。在源码实现中,可能发生两种情况。

    • A: 程序从系统中能获取到根存储中的根证书列表,然后使用标准库中的验证逻辑对证书进行验证。
    • B: 程序直接调用系统提供的编程接口对证书进行验证,由系统返回验证的结果。*uix 系统不支持系统验证。
  2. 如果提供自定义根证书,常见的开发实践中会使用 gocertifi 库。这个库可以为应用内置一个根证书列表(来自于 Mozilla,需要显式调用 gocertifi.CACerts() 并赋给 tls.Config.RootCAs 或 上述的 opts.Roots)。

对 x509 源码进一步梳理发现,不同操作系统下的实现还有以下区别:

golang 版本Windows*nixmacOSiOS
1.13.15 - 1.16.15AA(不支持B)A(不支持B)A(不支持B)
1.17.8AA(不支持B)A(不支持B)A(不支持B)
1.18rc1BA(不支持B)BB
  • *nix 系统不存在平台级的证书验证接口,所以依赖于开发时使用的库,golang 标准库有内置的验证逻辑,其它程序可能依赖于 openssl/libressl 等。
  • 1.13 - 1.17 的当前最新的版本
    • 除了 Windows 系统在 opts.Rootsnil 时,会调用系统 API 进行验证,macOS 和 iOS 都不支持 B(systemVerify 直接返回 nil,nil
    • iOS使用了内置到标准库中的根证书列表,不是实时从系统中获取。有些类似 gocertifi 库,只不过证书列表是从 Apple 网站获取。
  • 1.18rc1 开始有了变化,Windows/macOS/iOS 都不再获取系统根证书列表,而是直接使用系统的验证接口。

golang 1.18 更新

golang 的 commit 3544082 更新了证书验证逻辑,对 windows/macOS/iOS,先用平台接口验证,再使用自定义证书验证。(此前的提交中已经完善了 macOS 和 iOS 系统中的平台接口验证代码)

该提交关联的主要问题是 Issue#46287 。上一节论文概要中提到的 NSS 衍生根存储存在的问题,在 golang 开发中也存在。Issue 中 Filippo 对问题有详细的描述:单纯获取系统的根存储静态列表,不等于高保真地复制了它的验证功能。许多系统(平台)中都有附加规则,例如“这个根只能签署以 .fr 结尾的证书”或“这个根只能签署这组特定的证书”或“这个根只能签署记录到 CT 的证书”。(CT 是证书透明度)

问题与处理

上述 golang 开发中 TLS 验证的情况让我们知道,除了要关注系统的根存储以及供应商的根程序,验证逻辑也是关键的一环。对于 golang:

  • 升级到 v1.18 (也可能官方会合并 x509 相关提交到之前的版本?)能尽可能利用平台的 TLS 验证逻辑。
  • gocertifi 库能在 Windows 等系统环境下也使用 Mozilla NSS 的审核更严格和透明的根存储。缺点是 gocertifi 也只是衍生根存储形式,不包含 NSS 的其他附加验证规则。

其它开发语言或环境也会存在类似的问题,需要针对性的审计测试。例如现在 Electron 应用越来越广泛,它组合了浏览器内核和 NodeJS,在不同的业务逻辑部分实际上有两套 TLS 验证实现。

总结

在高安全等级要求的环境中,对 TLS 服务器验证需要关注供应链安全的以下两个方面:

  1. 所依赖的系统根存储、供应商的根程序是否能满足安全要求?Microsoft\Apple\Mozilla 的根程序对 CA 的审核标准不一,许多 Mozilla NSS 的衍生根存储(如 Ubuntu/Debian 等)引入了进一步的安全风险。对于高等级安全要求的环境,如 Google Chrome 一样创建独立的根程序/根存储可能是必要的。

  2. 编程语言、运行时库等其中的证书验证逻辑,是否存在风险?对 golang 标准库源码分析发现 TLS 的具体实现需要注意,v1.18(目前为 rc1 )改进了 Windows/macOS/iOS 等系统下的证书验证,与系统(平台)保持一致。其它开发语言与运行环境也需要类似的审计,降低中间人攻击风险。

以上仅对 TLS 服务器身份验证举例。根证书的常见用途还有两个,代码签名验证和电子邮件签名验证。代码签名主要影响恶意代码的检测,电子邮件签名影响身份冒用等。安全类软件研发尤其需要关注。

补充一下,360 浏览器也有自己的根证书计划 👈。同一页面还有“信创 SM2 根证书计划”,国产化系统如统信和麒麟等内置的根存储也值得关注。

参考阅读

转载许可声明

CC-BY 4.0本作品采用知识共享-署名-相同方式共享 4.0 国际许可协议 进行许可,转载时请注明原文链接。