[译]关于证书和PKI,那些你应该知道但你有不敢问的

Posted by Q on Sunday, November 12, 2023

原文链接:Everything you should know about certificates and PKI but are too afraid to ask, By Mike Malone, updated on May 20, 2024.

证书和公钥基础设施(PKI)很难,对吧? 我认识很多聪明人都避免陷入这个特别的兔子洞。 我个人也回避了很长时间,并且因为不了解更多而感到些许羞愧。 显而易见的结果是一个恶性循环:因为不好意思提问,我从未真正学到这方面的知识。

最终,我不得不学习这些内容,因为它们的强大的作用:PKI 让你可以通过加密的方式定义一个系统。 它是通用且与供应商无关的。 它可以在任何地方工作,使系统的各个部分能够在任何地方运行并进行安全通信。 它在概念上很简单,且非常灵活。 它可以让你使用 TLS 而放弃 VPN。 你可以忽略关于网络的所有事情,但仍能保持强大的安全特性。 这还蛮赞的。

现在我学会了这些知识,我后悔没有早点学习。 PKI 非常强大,也非常有趣。 虽然数学复杂,标准也极其繁琐,但核心概念其实相当简单。 证书是识别代码和设备的最佳方式,而身份识别对安全性、监控、度量以及其他无数事情都非常有用。 使用证书并不难,不比学习一门新语言或数据库更难。 只是稍微有点烦人且文档不好。

这就是缺失的手册。 我认为大多数工程师可以在不到一个小时的时间内掌握所有最重要的概念和常见的怪癖。 这就是我们的目标。 一小时的时间投入来学习一些你无法以其他方式完成的东西,还是相当值得的。

我的动机主要是教学方面的。 但我将在各种演示中使用我们在 smallstep 创建的两个开源项目:step CLIstep certificates。 你可以通过 brew install step 安装这两个工具(这里查看完整的安装说明)。 如果你喜欢简单的方法,可以使用我们的 Certificate Manager 提供的免费托管证书颁发机构。

让我们先来一句tl;dr:证书和 PKI 的目标是将名称与公钥绑定。 就是这样。剩下的只是实现细节。

smallstep/cli smallstep/certificates

概述与一些你应该了解的术语

我要使用一些技术术语,所以在开始之前我们先来定义它们。

**实体(entity)**是任何存在的事物,即使它只是逻辑上或概念上的存在。 你的电脑是一个实体。 你写的一些代码也是。 你也是一个实体。 你午餐吃的卷饼也是。 你六岁时看到的鬼也是,即使你的妈妈是对的,那只是你想象出来的。

每个实体都有一个身份(identity)。 这很难定义。 身份是使你成为你的东西,懂不? 在计算机上,身份通常表示为一堆属性,描述某个实体:群组、年龄、位置、喜欢的颜色、鞋码等等。 标识符(identifier)不同于身份。 相反,它是对某个具有身份的实体的唯一引用。 我是 Mike,但 Mike 不是我的身份。 它是一个名字(name)——标识符和名字是同义词(至少在我们的目的下是这样的)。

实体可以**声称(claim)**他们有某个特定的名字。 其他实体可能能够验证该声称,确认其真实性。 但声称不一定与名字有关:我可以声称任何事情:我的年龄,你的年龄,访问权限,生命的意义等等。 一般来说,**认证(Authentication)**是确认某个声称的真实性的过程。

订阅者(subscriber)终端实体(end entity)是参与 PKI 并且可以成为证书主体(subject)的实体。 证书颁发机构(certificate authority)(CA)是向订阅者颁发证书的实体——证书发行者(issuer)。 订阅者的证书有时被称为终端实体证书或叶子证书(leaf certificates),为啥叫这个在我们讨论证书链时你就知道了。 属于CA 的证书通常被称为根证书(root certificates)中间证书(intermediate certificates),取决于 CA 的类型。 最后,**依赖方(relying party)**是验证和信任 CA 颁发的证书的证书用户。 为了让事情更复杂一点,一个实体可以既是订阅者又是依赖方。 也就是说,单个实体可以拥有自己的证书并使用其他证书来认证远程对等方(例如在双向 TLS 中发生的情况)。

这些信息足以让我们开始,但如果你对“教条”感兴趣,可以考虑将 RFC 4949 放在你的 Kindle 上。 对于其他人,让我们变得具体一些。 我们实际中如何进行声称和认证?让我们谈谈加密。

MACs 和签名用于验证内容

消息认证码(message authentication code) (MAC) 是一种用于验证哪个实体发送了消息并确保消息未被修改的数据。 基本思想是将共享秘钥(密码)与消息一起输入到哈希函数中。 哈希函数的输出就是 MAC。 你将 MAC 与消息一起发送给某个接收者。

同样知道共享秘钥的接收者可以根据消息生成他们自己的 MAC, 并将其与提供的 MAC 进行比较。 哈希函数有一个简单的约定:如果你两次输入相同的内容,你会得到完全相同的输出。 如果输入不同——即使只有一位不同——输出将完全不同。 因此,如果接收者的 MAC 与随消息发送的 MAC 匹配,他们可以确信消息是由另一个知道共享秘钥的实体发送的。 假设只有受信任的实体知道共享秘钥,接收者可以信任该消息。

哈希函数也是单向的:从哈希函数的输出重建其输入在数学上是不可行的。 这对于保持共享秘钥的机密性至关重要:否则某个窥探者可能会窃取你的 MAC,逆向破解你的哈希函数,并找出你的秘钥。 这不好。这种特性是否成立在很大程度上取决于构建 MAC 时使用的哈希函数的微妙细节。 这些微妙的细节在这里我不会涉及。 因此要注意:不要试图自己发明 MAC 算法。使用 HMAC

hmac

所有这些关于 MAC 的讨论都是前奏:我们的真正故事从签名开始。 **签名(signature)在概念上类似于 MAC,但不使用共享秘钥,而是使用密钥对(稍后定义)。 使用 MAC 时,至少两个实体需要知道共享秘钥:发送者和接收者。 有效的 MAC 可以由任何一方生成,你无法分辨是哪一方生成的。 签名则不同。 签名可以用公钥验证,但只能用对应的私钥生成。 因此,公钥的接收者只可以验证签名,但不能生成签名,想要生成的话,只能借助于私钥。 这使你可以更严格地控制谁可以签名。 如果只有一个实体知道私钥,你就获得了一个叫做不可否认性(non-repudiation)**的属性:私钥持有者不能否认(否认)他们签署过某些数据的事实。

如果你已经感到困惑,淡定。 它们之所以被称为签名,是有原因的:它们就像现实世界中的签名。 你有一些东西希望某人同意吗?你想确保以后能够证明他们已经同意了?叼,把它写下来并让他们签名。

公钥密码学让计算机“看见”

证书和公钥基础设施(PKI)建立在公钥加密(public key cryptography)(也称为非对称加密(asymmetric cryptography))之上,使用密钥对(key pairs)。 密钥对包括一个可以分发和共享给全天下的公钥(public key),以及一个应由所有者保密的对应私钥(private key)

让我们再重复一遍最后这部分,因为它很重要:公钥加密系统的安全性取决于私钥的保密性。

使用密钥对有两种操作:

  • 你可以使用公钥**加密(encrypt)**一些数据。解密这些数据的唯一方法是使用对应的私钥。
  • 你可以使用私钥**签署(sign)**一些数据。任何知道对应公钥的人都可以验证签名,证明哪个私钥生成了该签名。

公钥加密是数学赋予计算机科学的神奇礼物。 数学确实很复杂,但你不需要理解它就能欣赏它的价值。 公钥加密让计算机做一些其他方式无法实现的事情:公钥加密让计算机“看见”

好的,让我解释一下…… 公钥加密允许一台计算机(或代码的一部分)向另一台计算机证明它知道一些东西,而无需直接分享这些知识。要证明你知道一个密码,你必须分享它。任何你与之分享的人都可以自己使用它。而私钥则不然。这就像视觉。如果你知道我的长相,你可以通过看我来验证我的身份——认证我的身份。但你不能变形来冒充我。

公钥加密也做了类似的事情。 如果你知道我的公钥(我的长相),你可以用它在网络上“看见”我。 例如,你可以发送给我一个大随机数。 我可以签署你的随机数并将我的签名发送给你。 验证该签名是你在与我对话的有力证据。 这实际上允许计算机在网络上“看见”它们在与谁对话。 这是如此有用,以至于我们在现实世界中认为这是理所当然的。 而在网络上,这简直是魔法。 谢谢数学。

证书:计算机和代码的驾驶证

如果你还不知道我的公钥呢? 这就是证书的用途。

证书本质上非常简单。 证书是一种包含公钥和名称的数据结构。 然后对数据结构进行签名。 签名将公钥与名称绑定在一起。 签署证书的实体称为发行者(或证书颁发机构),而证书中命名的实体称为主体

如果某某发行者为 Bob 签署了一个证书,那么该证书可以解读为这样的一个声明:“某某发行者说 Bob 的公钥是 01:23:42…”。 这是某某发行者关于 Bob 的声明。 声明由某某发行者签名,因此如果你知道某发行者的公钥,你可以通过验证签名来认证它。 如果你信任某某发行者,你就可以信任这个声明。 因此,证书让你可以使用信任和对发行者公钥的了解来学习另一个实体的公钥(在这种情况下是 Bob 的)。 就是这样。 从根本上说,这就是证书的全部内容。

drivers_license_cert

证书就像计算机和代码的驾驶证或护照。 如果你以前从未见过我,但你信任 DMV,你可以使用我的执照进行身份验证:验证驾驶证的有效性(检查全息图等),看照片,看我本人,读名字。 计算机使用证书做同样的事情:如果你以前从未见过某台计算机,但你信任某个证书颁发机构,你可以使用证书进行身份验证:验证证书的有效性(检查签名等),看公钥,“通过网络看私钥”(如上所述),读名字。

让我们快速看一下一个真实的证书:

license_vs_cert

对,我可能把这个故事简化了一点。 就像驾驶执照一样,证书中还有其他内容。 驾驶执照上说你是否是器官捐赠者以及你是否被授权驾驶商业车辆。 证书上说你是否是 CA 以及你的公钥是否用于签名或加密。 两者都有到期日。

这里有很多细节,但它们并不改变我之前所说的内容:从根本上说,证书只是将公钥与名称绑定在一起的东西。

X.509、ASN.1、OID、DER、PEM、PKCS,哦天哪……

让我们看看证书是如何表示为比特和字节的。 这部分确实复杂地令人恼火。 事实上,我怀疑证书和密钥编码方式的晦涩和定义不清是导致 PKI 总体上大多数困惑和挫败感的根源。 这些东西真是蠢。抱歉。

通常,当人们谈论证书而没有额外的限定时,他们指的是 X.509 v3 证书。 更具体地说,他们通常指的是 RFC 5280 中描述的 PKIX 变体,并由 CA/Browser 论坛的基线要求进一步细化。 换句话说,他们指的是浏览器理解并用于 HTTPS(构建在 TLS 之上的 HTTP)的那种证书。 还有其他证书格式。 特别是,SSH 和 PGP 都有它们自己的格式。 但我们将专注于 X.509。如果你能理解 X.509,你就能弄明白其他所有的格式。

由于这些证书被广泛支持——它们有好的库或者类似的东西——它们也常在其他上下文中使用。 它们当然是内部 PKI(稍后定义)颁发证书的最常见格式。 重要的是,这些证书可以开箱即用地与 TLS 和 HTTPS 客户端和服务器一起工作。

如果不了解一点历史,你就无法完全理解 X.509。 X.509 首次于 1988 年作为更广泛的 X.500 项目的一部分由 ITU-T(国际电信联盟的标准组织)标准化。 X.500 是电信公司试图建立一个全球电话簿的产物。 这从未实现,但遗迹仍然存在。 如果你曾经看过 X.509 证书,并想知道为什么一个为网络设计的东西会编码地区、州和国家,答案就在这里:X.509 不是为网络设计的。 它是三十年前为了建立一个电话簿而设计的。

cert_distinguished_name

X.509 构建在 ASN.1 之上,这是另一个 ITU-T 标准(由 X.208 和 X.680 定义)。 ASN(Abstract Syntax Notation) 代表抽象语法标记(1 代表一)。 ASN.1 是用于定义数据类型的一种标记。 你可以把它想象成 X.509 的 JSON,但实际上它更像 protobuf 或 thrift 或 SQL DDL。 RFC 5280 使用 ASN.1 将 X.509 证书定义为一个包含各种信息的对象:名称、密钥、签名等。

ASN.1 有一般的数据类型,如整数、字符串、集合和序列。 它还有一种重要且需要理解的特殊类型:对象标识符(OID)。 OID 类似于 URI,但更烦人。 它们(应该是)通用唯一标识符。 从结构上讲,OID 是一个分层命名空间中的整数序列。 你可以使用 OID 为一段数据标记类型。 一个字符串只是一个字符串,但如果我用 OID 2.5.4.3 标记一个字符串,那么它就不再是普通字符串——它是一个 X.509 公共名。

oids

ASN.1 是抽象的,因为标准并没有规定如何将内容表示为比特和字节。 为此,有各种编码规则指定了 ASN.1 数据值的具体表示。 这是一个应该有用但主要是烦人的额外抽象层。 有点像 Unicode 和 UTF-8 之间的区别(哎呀)。

ASN.1 有很多编码规则,但只有一种广泛地用于 X.509 证书和其他加密内容:差异化编码规则(Distinguished Encoding Rules,DER)(尽管非规范的基本编码规则(Basic Encoding Rules,BER)也偶尔使用)。 DER 是一种非常简单的类型-长度-值编码,但你真的不需要担心,因为库会做大部分繁重的工作。

不幸的是,故事并没有就此结束。 你不必过多担心 DER 的编码和解码,但你绝对需要弄清楚一个特定的证书是纯 DER 编码的 X.509 证书还是更炫酷的东西。 有两个潜在维度炫酷:我们可能看到的不仅仅是原始 DER,我们可能看到的不仅仅是一个证书。

从第一个维度开始,DER 是纯二进制的,而二进制数据很难复制粘贴并在网络上传输。 所以大多数证书都打包在 PEM 文件中(PEM,Privacy Enhanced EMail 代表隐私增强邮件,这是另一个奇怪的上古遗迹)。 如果你曾使用过 MIME,PEM 类似:一个 base64 编码的有效载荷夹在头部和尾部之间。 PEM 头部有一个标签,应该描述有效载荷。 令人震惊的是,这个简单的工作大多被搞砸了,PEM 标签在不同工具之间通常不一致(RFC 7468 试图标准化在此上下文中使用 PEM,但并不完整,也并不总是被遵循)。 不多说了,下面是一个 PEM 编码的 X.509 v3 证书的样子:

-----BEGIN CERTIFICATE-----
MIIBwzCCAWqgAwIBAgIRAIi5QRl9kz1wb+SUP20gB1kwCgYIKoZIzj0EAwIwGzEZ
MBcGA1UEAxMQTDVkIFRlc3QgUm9vdCBDQTAeFw0xODExMDYyMjA0MDNaFw0yODEx
MDMyMjA0MDNaMCMxITAfBgNVBAMTGEw1ZCBUZXN0IEludGVybWVkaWF0ZSBDQTBZ
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABAST8h+JftPkPocZyuZ5CVuPUk3vUtgo
cgRbkYk7Ong7ey/fM5fJdRNdeW6SouV5h3nF9JvYKEXuoymSNjGbKomjgYYwgYMw
DgYDVR0PAQH/BAQDAgGmMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAS
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRc+LHppFk8sflIpm/XKpbNMwx3
SDAfBgNVHSMEGDAWgBTirEpzC7/gexnnz7ozjWKd71lz5DAKBggqhkjOPQQDAgNH
ADBEAiAejDEfua7dud78lxWe9eYxYcM93mlUMFIzbWlOJzg+rgIgcdtU9wIKmn5q
FU3iOiRP5VyLNmrsQD3/ItjUN1f1ouY=
-----END CERTIFICATE-----

PEM 编码的证书通常会带有 .pem.crt.cer 扩展名。 使用 DER 编码的原始证书通常会带有 .der 扩展名。 同样,这里也没有太多的一致性,因此具体情况可能有所不同。

回到我们另外一个炫酷的维度:除了使用 PEM 的炫酷编码外,证书可能还会被包装在更炫酷的封装中。 几种封装格式定义了更大的数据结构(仍使用 ASN.1),可以包含证书、密钥和其他内容。 有些东西要求“一个证书”,实际上是想要一个在这些封装中的证书。 所以要小心。

你可能会遇到的封装格式是由 RSA 实验室发布的一套名为 PKCS(公钥加密标准)的标准的一部分(实际上故事有点复杂,但管他呢)。 第一个是 PKCS#7,被 IETF 重新命名为加密消息语法(CMS,Cryptographic Message Syntax),可以包含一个或多个证书(编码完整的证书链,稍后描述)。 PKCS#7 常用于 Java。 常见的扩展名是 .p7b 和 .p7c。 另一种常见的封装格式是 PKCS#12,它可以包含证书链(如 PKCS#7)以及(加密的)私钥。 PKCS#12 常用于微软产品。 常见的扩展名是 .pfx.p12。 同样,PKCS#7 和 PKCS#12 封装也使用 ASN.1。这意味着两者都可以编码为原始 DER 或 BER 或 PEM。 尽管如此,根据我的经验,它们几乎总是原始 DER。

密钥编码同样复杂,但模式通常是相同的:某些 ASN.1 数据结构描述密钥,DER 作为二进制编码使用,而 PEM(希望有一个有用的头部)可能用作稍微友好的表示。 解读你正在查看的密钥类型是一半艺术、一半科学。 如果你幸运的话,RFC 7468 会给出良好的指导来确定你的 PEM 有效载荷是什么。 椭圆曲线密钥通常会被标记出来,尽管似乎没有任何标准化。 其他密钥在 PEM 中仅被简单标记为“PRIVATE KEY”。这通常表示一个 PKCS#8 有效载荷,包含密钥类型和其他元数据的私钥封装。 以下是一个 PEM 编码的椭圆曲线密钥示例:

$ step crypto keypair --kty EC --no-password --insecure ec.pub ec.prv
$ cat ec.pub ec.prv
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc73/+JOESKlqWlhf0UzcRjEe7inF
uu2z1DWxr+2YRLfTaJOm9huerJCh71z5lugg+QVLZBedKGEff5jgTssXHg==
-----END PUBLIC KEY-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEICjpa3i7ICHSIqZPZfkJpcRim/EAmUtMFGJg6QjkMqDMoAoGCCqGSM49
AwEHoUQDQgAEc73/+JOESKlqWlhf0UzcRjEe7inFuu2z1DWxr+2YRLfTaJOm9hue
rJCh71z5lugg+QVLZBedKGEff5jgTssXHg==
-----END EC PRIVATE KEY-----

也很常见看到私钥使用密码(共享秘密或对称密钥)加密的情况。这些将看起来像这样(Proc-TypeDEK-Info 是 PEM 的一部分,表示这个 PEM 有效载荷使用 AES-256-CBC 加密):

-----BEGIN EC PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,b3fd6578bf18d12a76c98bda947c4ac9

qdV5u+wrywkbO0Ai8VUuwZO1cqhwsNaDQwTiYUwohvot7Vw851rW/43poPhH07So
sdLFVCKPd9v6F9n2dkdWCeeFlI4hfx+EwzXLuaRWg6aoYOj7ucJdkofyRyd4pEt+
Mj60xqLkaRtphh9HWKgaHsdBki68LQbObLOz4c6SyxI=
-----END EC PRIVATE KEY-----

PKCS#8 对象也可以加密,在这种情况下,标题标签应为 “ENCRYPTED PRIVATE KEY”,根据 RFC 7468 的规定。此时不会有 Proc-Type 和 DEK-Info 头信息,因为这些信息被编码在有效载荷中。

公钥通常有 .pub 或 .pem 扩展名。私钥可能有 .prv、.key 或 .pem 扩展名。 再一次,你的具体情况可能有所不同。

快速总结。ASN.1 用于定义证书和密钥等数据类型。 DER 是一组将 ASN.1 转换为比特和字节的编码规则。 X.509 是用 ASN.1 定义的。 PKCS#7 和 PKCS#12 是更大的数据结构,也使用 ASN.1 定义,可以包含证书和其他内容。 它们分别常被 Java 和微软使用。 由于原始二进制 DER 在网络上传输很困难,大多数证书都是 PEM 编码的,即将 DER base64 编码并加上标签。私钥通常表示为 PEM 编码的 PKCS#8 对象。 有时它们也会用密码加密。

如果这些让你困惑,不是你的问题,是这个世界的问题。 我尽力了。

公钥基础设施

了解什么是证书是有用的,但这只是故事的一半不到。 让我们看看证书是如何创建和使用的。

公钥基础设施 (PKI) 是一个总括术语,涵盖了签发、分发、存储、使用、验证、撤销以及管理和交互证书和密钥所需要的一切东西。 这是一个故意模糊的术语,类似于“数据库基础设施”。 证书是大多数 PKI 的构建块,而证书颁发机构 (CA) 是其基础。 话虽如此,PKI 远不止这些。它包括库、定时任务、协议、约定、客户端、服务器、人、过程、名称、发现机制以及所有其他使公钥加密有效使用的内容。

如果你从头开始构建自己的 PKI,你将会不得不各种小心了。 就像你构建自己的数据库基础设施一样。 事实上,许多简单的 PKI 甚至不使用证书。 当你编辑 ~/.ssh/authorized_keys 时,其实你就是在使用纯文本的方式来配置一种供 SSH 使用来将公钥绑定到名字的简单无证书形式的 PKI。PGP 使用证书,但不使用 CA。 相反,它使用信任网模型。 你甚至可以使用区块链来分配名称并将其绑定到公钥。 唯一真正强制性的要求是,如果你从头开始构建 PKI,按定义,你必须使用公钥。 其他一切都可以改变。

话虽如此,你可能并不想完全从头开始构建 PKI。 我们将重点关注网络上使用的 PKI 以及基于 Web PKI 技术并利用现有标准和组件的内部 PKI。

这会儿,请记住证书和 PKI 的简单目标:将名称绑定到公钥。

网络 PKI 与内部 PKI

每当你通过浏览器访问 HTTPS URL 时,你就在与网络 PKI 进行交互——就像你加载这个网页时一样。 这是许多人(至少模糊地)熟悉的唯一 PKI。 尽管它有些问题,但它基本上运作正常。 尽管存在一些问题,它显著提高了网络安全性,并且对用户来说基本上是透明的。在你的系统通过互联网与外界通信的所有地方都应该使用它。

网络 PKI 主要由 RFC 5280 定义,并由 CA/浏览器论坛(也称为 CA/B 或 CAB 论坛)进一步优化。 有时它被称为“互联网 PKI”或 PKIX(以创建它的工作组命名)。 PKIX 和 CAB 论坛文件覆盖了很多基础内容。 它们定义了我们在上一节中讨论的各种证书。 它们还定义了“名称”是什么以及它在证书中的位置、可以使用哪些签名算法、依赖方如何确定证书的颁发者、证书的有效期(签发和到期日期)如何指定、撤销和证书路径验证的工作原理、CA 用于确定某人是否拥有域名的过程等等。

网络 PKI 之所以重要,是因为网络 PKI 证书可以默认在浏览器和几乎所有使用 TLS 的东西上工作。

内部 PKI 是你自己运行的 PKI,用于你自己的东西:生产基础设施,如服务、容器和虚拟机;企业 IT 应用程序;企业终端,如笔记本电脑和手机;以及你希望识别的任何其他代码或设备。 它允许你进行身份验证并建立加密通道,以便你自己的东西可以在任何地方运行并安全通信,即使在公共互联网上也是如此。

既然网络 PKI 已经存在,那么为什么还要运行你自己的内部 PKI? 简单的答案是网络 PKI 不是为支持内部使用场景而设计的。 即使使用 Let’s Encrypt 这样的 CA,它提供免费证书和自动化创建,你也必须应对速率限制可用性问题。 如果你有大量经常部署的服务,这是不好的。

此外,使用网络 PKI 你对证书生命周期、撤销机制、更新过程、密钥类型和算法等重要细节几乎没有控制权(所有这些都是我们稍后会解释的重要内容)。

最后,CA/浏览器论坛基本要求实际上禁止网络 PKI CA 绑定内部 IP(例如,10.0.0.0/8 范围内的 IP)或不完全限定且不可在公共全球 DNS 中解析的内部 DNS 名称(例如,你不能绑定一个像 foo.ns.svc.cluster.local 这样的 Kubernetes 集群 DNS 名称)。 如果你想在证书中绑定这种名称、签发大量证书或控制证书细节,你需要自己的内部 PKI。

在下一节中,我们将看到信任(或缺乏信任)是避免将网络 PKI 用于内部用途的另一个原因。简而言之,使用网络 PKI 处理你的网站和公共 API。使用你自己的内部 PKI 处理其他所有东西。

信任与可信度

信任存储

我们早些时候学习了如何将证书解释为一种陈述或者是一种声明,例如:“颁发者说这个主体的公钥是某某某”。 这种声明由颁发者签名,以便依赖方可以对其进行认证。 在这个描述中我们略过了一些重要的东西:“依赖方如何知道颁发者的公钥?”

答案很简单,尽管不一定令人满意:依赖方会在信任存储中预先配置一些列受信任的根证书(或信任锚)。 这种预配置发生的方式是任何 PKI 的一个重要方面。 一个选项是从另一个 PKI 引导:你可以让一些自动化工具使用 SSH 将根证书复制到依赖方,利用前面描述的 SSH PKI。 如果你在云上运行,你的 SSH PKI 反过来是由网络 PKI 外加你创建账号并提供信用卡的时候你的云供应商提供的某个验证方法共同生成的。 如果你沿着这条信任链追溯得足够远,你总是会找到人:每条信任链最终都结束于现实世界。

chain_of_trust

信任存储中的根证书是自签名的。 发行者和主体是相同的。 从逻辑上讲,这就像是一个声明:“Mike 说 Mike 的公钥是啥啥啥”。 自签名证书上的签名保证了主体/发行者知道相关的私钥,但任何人都可以生成包含他们想要的任何名称的自签名证书。 因此,来源至关重要:自签名证书只有在其进入信任存储的过程值得信任时才应该被信任。 在 macOS 上,信任存储由 Keychain 管理。 在许多 Linux 发行版中,它只是 /etc 或磁盘上其他地方的一些简单的文件。 如果你的用户可以修改这些文件,你最好信任所有用户。

那么,信任存储来自哪里呢? 对于 Web PKI,最重要的依赖方是网页浏览器。 默认情况下,绝大多数浏览器和几乎所有使用 TLS 的其他程序使用的信任存储由四个组织维护:

操作系统的信任存储通常随操作系统一起发布。 Firefox 附带自己的信任存储(通过 mozilla.org 使用 TLS 分发——使用其他信任存储从 Web PKI 引导)。 编程语言和其他非浏览器应用(如 curl)通常默认使用操作系统的信任存储。 因此,几乎所有默认使用的信任存储都是预装的,并通过软件更新进行更新(通常使用另一个 PKI 对代码进行签名)。

这些程序维护的信任存储中通常包含超过 100 个证书颁发机构。 你可能知道一些大的机构:Let’s Encrypt、Symantec、DigiCert、Entrust 等。 浏览这些机构可能会很有趣。 如果你想以编程方式进行浏览,Cloudflare 的 cfssl 项目在 GitHub 上维护了一个包含各种信任存储中受信任证书的存储库,以帮助进行证书打包(我们稍后会讨论)。 如果你想要更人性化的体验,可以查询 Censys 以查看 MozillaAppleMicrosoft 信任的证书。

可信度

这100多个证书颁发机构说起来是被信任的——浏览器和其他工具默认信任这些CA颁发的证书。 但这并不意味着它们在道德层面是值得信任的。 相反,有记录表明,一些 Web PKI 证书颁发机构向政府提供欺诈性的证书以便监视流量和冒充网站。 这些“受信任”的 CA 中有些在中国等司法比较严格的国家运作。 司法宽松在这一点上也没有道德高地。 NSA在每个可能的机会下都会暗中破坏 Web PKI。 2011年,“受信任”的 DigiNotar 和 Comodo 这两个证书颁发机构都被攻破了。 DigiNotar 的遭受攻击背后的凶手很可能是NSA。 此外,还有许多例子表明CA错误地颁发了格式错误或不合规的证书。 因此,虽然这些CA实际上是被信任的,但作为一个群体,它们实际上并不值得信任。 我们很快会看到,Web PKI大体上仅和最不安全的CA有差不多的安全性,所以这并不是好事。

浏览器社区已采取一些措施来解决这一问题。 CA/浏览器论坛基准要求理顺了这些受信任证书颁发机构在颁发证书之前应该遵循的规则。 作为 WebTrust 审计计划的一部分,CA需要接受对这些规则合规性的审计,这对于某些根证书组织来说是CA能否被包括在其信任存储中的必要条件(例如Mozilla的)。

那么,如果你在使用TLS进行内部业务,你大概不会再信任这些 CA 了,除了不得已。 如果你信任他们了,你可能正在为NSA和其他人打开了大门。 你接受了你的安全性将依附于100多个其他组织的纪律性和操守这一事实。 也许你压根不在乎,但这是一个公平的警告。

联盟

更糟的是,Web PKI的依赖方(RP)信任其信任存储中的每个CA来为任何订阅者签发证书。 这导致 Web PKI 的整体安全性仅与最不安全的 Web PKI CA 相当。 2011年的 DigiNotar 攻击展示了这个问题:作为攻击内容的一部分,一个伪造的证书被签发给google.com。 这个证书被主要的网页浏览器和操作系统所信任,尽管 Google 与 DigiNotar 没有任何关系。 数十个其他伪造的证书被签发给 Yahoo!、Mozilla 和 The Tor Project 等公司。 最终,DigiNotar 的根证书从主要的信任存储中移除,但很可能已经造成了大量损害。

最近, Sennheiser 因为通过他们自己的 HeadSetup 应用在用户的信任存储中安装了一个自签名的根证书并在这个应用的配置中内嵌了其对应的私钥而广受诟病。 任何人都可以提取这个私钥并用它为任何域名签发证书。 任何信任存储中包含 Sennheiser 证书的计算机都会信任这些伪造的证书。 这把 TLS 的棺材板都给掀了。 天哪。

有一些缓解机制可以帮助减少这些风险。 证书颁发机构授权(CAA)允许您使用特殊的DNS记录限制哪些CA可以为您的域名签发证书。 证书透明度(CT)(RFC 6962)要求CA提交他们签发的每个证书给一个保持公共证书日志的公正观察者,以侦测伪造的证书。 向CT提交后的加密回执包含在签发的证书中。 HTTP公钥钉扎(HPKP 或简称为“钉扎”)让订阅者(网站)告诉RP(浏览器)只接受某个域名证书中的特定公钥。

这些措施的问题是RP的支持不足。 CAB论坛现在要求浏览器进行CAA检查。 一些浏览器也部分支持CT和HPKP。 对于其他RP(例如,大多数TLS标准库实现),这些措施几乎从未被强制执行。 这个问题会反复出现:许多证书策略必须由RP强制执行,而RP却很少会屈尊过问。 如果RP不检查CAA记录、不要求提供CT提交的证明,这些措施就没多大用处。

无论如何,如果您使用自己的内部PKI,您应该为内部这些玩意维护一个单独的信任存储。 也就是说,不要将您的根证书添加到现有的系统信任存储中,而是配置内部TLS请求仅使用您自己的根证书。 如果您希望内部有更好的联邦(例如,您希望限制内部CA可以签发的证书),您可以尝试CAA记录和适当配置的RP。 您可能还想查看 SPIFFE,这是一个正在发展的标准化项目,解决了这个问题以及其他一些与内部PKI相关的问题。

什么是证书颁发机构

我们已经讨论了很多关于证书颁发机构(CAs),但实际上还没有定义什么是CA。 CA是一个受信任的证书签发者。 它通过签署一张证书,从而为一个公钥和一个名称之间的绑定作担保。 基本上,证书颁发机构只是另一个证书和相应的私钥,用来签署其他证书。

显然地,这些玩意儿还需要一些逻辑和过程来包装。 CA需要将其证书分发到信任存储中,接受和处理证书请求,并向订阅者颁发证书。 如果一个CA可以通过暴露一些API来自动化这些操作, 那么这些CA称为在线CA(Online CA)。 一个在信任存储中包含自签名根证书的CA称为根CA(Root CA)。

中间证书、链和证书包

CAB 论坛基线要求规定,Web PKI 根 CA 所属的根私钥只能通过发出直接命令来签署证书(见第4.3.1节)。 换句话说,Web PKI 根 CA 不可以自动化证书签署流程。 他们不可以在线。 这对于任何大规模 CA 操作来说都是一个问题。 你总不能让某人手动在机器上输入命令来完成每个证书订单吧。

这一规定的出发点是安全性。 Web PKI 根证书广泛分布在广大用户的信任存储中,因而难以吊销。 根 CA 私钥如果泄露了会影响数十亿人和设备。 因此,最佳实践是将根私钥保持离线,理想情况下保存在某些连接到隔离网络机器的专用硬件上,并且具有良好的物理安全性和严格执行的使用流程。

许多内部 PKI 也遵循这些实践,尽管这远没有那么必要。 如果你可以自动化根证书轮换(例如,使用配置管理或编排工具更新信任存储),那么可以轻松轮换被泄露的根密钥。 人们对内部 PKI 的根私钥管理过于痴迷,以至于延迟或阻碍了内部 PKI 的部署。 你的 AWS 根账户凭证至少跟这差不多敏感,甚至比之更甚。 你是如何管理这些凭证的?

为了在根 CA 不在线的情况下使证书签发具有规模化(即使自动化成为可能),根私钥仅仅用于签署少量中间证书。 相应的中间私钥由中间 CA(也称为下级 CA)用于签署和颁发给订阅者的叶子证书。 中间证书通常不包含在信任存储中,使其更容易吊销和轮换,因此从中间 CA 颁发的证书通常是在线和自动化的。

这个证书包——叶证书、中间证书、根证书——形成了一条链(称为证书链)。 叶子证书由中间证书签署,中间证书由根证书签署,根证书自签名。

cert_chain

技术上来说,这又是一个简化。 没有什么能阻止你创建更长的链和更复杂的图(例如,通过交叉认证)。 不过,这通常不被鼓励,因为它很快就会变得非常复杂。 不管怎样,终端实体证书是这个图中的叶子节点。 因此得名“叶子证书”。

当你配置一个订阅者(例如,一个像 Apache、Nginx、Linkerd 或 Envoy 这样的网络服务器)时,你通常需要提供的不仅仅是叶证书,还需要包含中间证书的证书包。 这里有时候会使用PKCS#7 和 PKCS#12,因为它们可以包含完整的证书链。 更多情况下,证书链会编码为一个简单的行分隔 PEM 对象序列。 一些使用方期望证书按从叶到根的顺序排列,另一些使用方期望从根到叶,还有一些使用方无所谓哪样。 甚至还有更多恼人的不一致。 这种情况下,搜索Google 和 Stack Overflow 会有所帮助。 或者通过反复试错。

不管怎样,下面是一个例子:

$ cat server.crt
-----BEGIN CERTIFICATE-----
MIICFDCCAbmgAwIBAgIRANE187UXf5fn5TgXSq65CMQwCgYIKoZIzj0EAwIwHzEd
MBsGA1UEAxMUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMTgxMjA1MTc0OTQ0WhcN
MTgxMjA2MTc0OTQ0WjAUMRIwEAYDVQQDEwlsb2NhbGhvc3QwWTATBgcqhkjOPQIB
BggqhkjOPQMBBwNCAAQqE2VPZ+uS5q/XiZd6x6vZSKAYFM4xrYa/ANmXeZ/gh/n0
vhsmXIKNCg6vZh69FCbBMZdYEVOb7BRQIR8Q1qjGo4HgMIHdMA4GA1UdDwEB/wQE
AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFHee
8N698LZWzJg6SQ9F6/gQBGkmMB8GA1UdIwQYMBaAFAZ0jCINuRtVd6ztucMf8Bun
D++sMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDBWBgwrBgEEAYKkZMYoQAEERjBEAgEB
BBJtaWtlQHNtYWxsc3RlcC5jb20EK0lxOWItOEdEUWg1SmxZaUJwSTBBRW01eHN5
YzM0d0dNUkJWRXE4ck5pQzQwCgYIKoZIzj0EAwIDSQAwRgIhAPL4SgbHIbLwfRqO
HO3iTsozZsCuqA34HMaqXveiEie4AiEAhUjjb7vCGuPpTmn8HenA5hJplr+Ql8s1
d+SmYsT0jDU=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBuzCCAWKgAwIBAgIRAKBv/7Xs6GPAK4Y8z4udSbswCgYIKoZIzj0EAwIwFzEV
MBMGA1UEAxMMVGVzdCBSb290IENBMB4XDTE4MTIwNTE3MzgzOFoXDTI4MTIwMjE3
MzgzOFowHzEdMBsGA1UEAxMUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwWTATBgcqhkjO
PQIBBggqhkjOPQMBBwNCAAT8r2WCVhPGeh2J2EFdmdMQi5YhpMp3hyVZWu6XNDbn
xd8QBUNZTHqdsMKDtXoNfmhH//dwz78/kRnbka+acJQ9o4GGMIGDMA4GA1UdDwEB
/wQEAwIBpjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/
BAgwBgEB/wIBADAdBgNVHQ4EFgQUBnSMIg25G1V3rO25wx/wG6cP76wwHwYDVR0j
BBgwFoAUcITNjk2XmInW+xfLJjMYVMG7fMswCgYIKoZIzj0EAwIDRwAwRAIgTCgI
BRvPAJZb+soYP0tnObqWdplmO+krWmHqCWtK8hcCIHS/es7GBEj3bmGMus+8n4Q1
x8YmK7ASLmSCffCTct9Y
-----END CERTIFICATE-----

再次强调,这令人烦恼且复杂,但并不是火箭科学。

证书路径验证

由于中间证书不包含在信任存储中,它们需要像叶证书一样被分发和验证。 正如前面描述的,当你配置订阅者时,需要提供这些中间证书。 然后订阅者将它们发送给依赖方。 在 TLS 中,这发生在建立 TLS 连接的握手过程中。 当订阅者将其证书发送给依赖方时,它会包含任何必要的中级证书,以便回溯到受信任的根证书。 依赖方通过一个称为证书路径验证的过程来验证叶证书和中间证书。

cert_path_validation

完整的证书路径验证算法很复杂。 它包括检查证书的过期时间、吊销状态、各种证书策略、密钥使用限制以及其他一堆东西。 PKI 依赖方能否正确实现这个算法是绝对关键的。 令人震惊的是,人们对禁用证书路径验证非常随意(例如,通过传递 -k 标志给 curl)。 别这样做。

不要禁用证书路径验证。 正确地使用 TLS 并不难,而且证书路径验证是 TLS 中进行身份验证的部分。 有人可能会争辩说通道仍然是加密的,所以没关系。 这是错误的。 这有关系的。 没有身份验证的加密几乎是没有价值的。 这就像一个匿名忏悔室:你的对话是私密的,但你不知道幕布另一边是谁。 不同的仅仅是这不是教堂,这是互联网。 所以不要禁用证书路径验证

密钥和证书的生命周期

在您可以使用诸如 TLS 之类的协议来使用证书之前,您需要弄清楚如何从 CA 获取一个证书。 抽象地说,这是一个非常简单的过程:想要证书的订阅者生成一个密钥对并向证书颁发机构提交请求。 CA 确认将在证书中绑定的名称是正确的,如果正确,则签署并返回证书。

证书会过期,过期后它们将不再被依赖方信任。 如果您仍在使用即将过期的证书,你需要更新并替换它。 如果您希望依赖方在证书到期之前停止信任某个证书,它可以(有时)被吊销。

就像 PKI 的许多方面一样,这个看起来简单的过程实际上却非常复杂。 细节中隐藏了两个计算机科学中最难的问题:缓存失效和对象命名。 然而,一旦了解了其中的原理,这一切都很容易理解了。

对象命名

历史上,X.509 使用 X.500 的可分辨名称(distinguished names) (DN) 来命名证书的主体(订阅者)。 DN 包括一个通用名称(common name)(对我来说,那就是 “Mike Malone”)。 它还可以包括所在地、国家、组织、组织单位以及其他一大堆无关紧要的信息(回想一下,这些东西最初是为数字电话簿设计的)。 没有人真正理解可分辨名称。 它们对网络来说是没有意义的。 回避他们吧。 如果你想使用他们,请保持简单。 你不必使用每一个字段。 事实上,你也不应该。 一个通用名称可能就已经足够,如果你想找点刺激,或许可以加上一个组织名称。

PKIX 最初规定,网站的 DNS 主机名应绑定在 DN 的通用名称中。 最近,CAB 论坛已经弃用了这种做法,并使整个 DN 成为可选项(参见基线要求第 7.1.4.2 节)。 相反,现代最佳实践是利用主体备选名称(subject alternative name) (SAN) X.509 扩展在证书中绑定名称。

常用的四种 SAN 都绑定了广泛使用和理解的名称:域名(DNS)、电子邮件地址、IP 地址和 URI。 这些在我们关心的上下文中应该都是唯一的,并且它们很好地映射到我们要标识的内容:电子邮件地址用于人,域名和 IP 地址用于机器和代码,如果您想变得更高级就使用 URI。 使用 SAN 吧。

inspect_san_dns

请注意,Web PKI 还允许在证书中绑定多个名称,并允许名称中包含通配符。 一个证书可以有多个 SAN,并且可以有类似于 *.smallstep.com 的 SAN。 这对于响应多个名称的网站(例如,smallstep.comwww.smallstep.com)非常有用。

生成密钥对

一旦我们有了一个需要绑定证书的名称,就需要生成密钥对,才能创建证书。 回顾一下,PKI 的安全性关键依赖于一个简单的不变量:知道某个私钥的唯一实体是对应证书中命名的订阅者。 为了确保这一不变量成立,最佳实践是让订阅者自己生成密钥对,这样他就是唯一知道这个私钥的人了。 一定要避免通过网络传输私钥。

你需要决定使用哪种类型的密钥。 这是另一个完整的主题,但这里有一些快速指导(截至2023年5月)。 RSA 向椭圆曲线密钥(ECDSAEdDSA)的过渡虽然缓慢但在进行中。 如果你决定使用 RSA 密钥,至少要使用 2048 位的,别搞得超过 4096 位就好。 另外使用 RSA-PSS,而不是 RSA PKCS#1。 如果使用 ECDSA,P-256 曲线可能是最佳选择(在 openssl 中是 secp256k1prime256v1)… 除非你担心 NSA,那么你可能会选择使用更高级的,如 Curve25519 的 EdDSA(尽管这些密钥的支持度不是很好)。

以下是使用 openssl 生成椭圆曲线 P-256 密钥对的示例:

openssl ecparam -name prime256v1 -genkey -out k.prv
openssl ec -in k.prv -pubout -out k.pub

这里是使用 step 生成同样类型的密钥对的示例:

step crypto keypair --kty EC --curve P-256 k.pub k.prv

你也可以通过编程方式完成这个过程,永远别让你的私钥接触到磁盘。

选择你的方式。

颁发(Issuance)

一旦订阅者拥有了一个需要绑定的名称和密钥对,下一步就是从证书颁发机构(CA)获取一个叶子证书。 CA希望验证(证明)两件事:

  • 要绑定在证书中的公钥是订阅者的公钥(也就是说,订阅者知道相应的私钥)
  • 要绑定在证书中的名称是订阅者的名称

前者通常通过一个简单的技术机制实现:证书签名请求。 而后者难一点了。 大体上说来,这个过程称为身份验证或注册。

证书签名请求(Certificate Signing Requests)

为了获得一个证书,订阅者需要向证书颁发机构提交证书签名请求(certificate signing request,CSR)。 CSR是另一种ASN.1结构,由PKCS#10定义。

和证书一样,CSR是一个数据结构,包含一个公钥、一个名称和一个签名。 它使用与 CSR 中公钥对应的私钥进行自签名。 这个签名证明了创建CSR的实体知道其私钥。 它还允许 CSR 在不可能被外部人员修改的情况下,被复制粘贴和转储。

CSR 包含许多选项来指定证书的详细信息。 实际上,大多数这些信息会被CA忽略掉。 相反,大多数CA使用模板或提供管理接口来收集这些信息。

你可以使用step在一条命令中生成密钥对并创建CSR,如下所示:

step certificate create --csr test.smallstep.com test.csr test.key

OpenSSL非常强大,但使用起来更加繁琐

身份证明

一旦CA收到了CSR并验证了其签名,下一步需要做的是确定将绑定在证书中的名称是否确实是订阅者的真实名称。 这很棘手。 证书的终极目标是方便RP对订阅者进行身份鉴定,但在证书被签发之前,CA如何能够验证订阅者的身份呢?

答案是:得看情况。 对于Web PKI,有三种类型的证书,它们在如何识别订阅者以及所采用的身份验证方式上有很大差异。 它们分别是:域名验证(domain validation,DV)证书、组织验证(organization validation,OV)证书和扩展验证(extended validation,EV)证书。

DV证书绑定了一个DNS名称,签发这个证书是为了证明请求证书的实体控制了这个域名。 证明通常通过简单的仪式进行,比如向 WHOIS 记录中列出的管理联系人发送确认电子邮件。 最初由Let’s Encrypt开发和使用的ACME协议通过更好的自动化改进了这一过程:ACME CA不使用电子邮件验证,而是发出一个挑战(challenge),订阅者必须完成以证明其都能控制这个域名。 ACME规范的挑战规范部分是一个扩展点,但常见的挑战包括在指定URL上提供一个随机数(HTTP挑战)和将一个随机数放置在DNS TXT记录中(DNS挑战)。

OV和EV证书建立在了DV证书的基础上,它包括了拥有绑定域名的组织的名称和位置。 它们不仅将证书与域名关联起来,还与控制该域名的法律实体相关联。 OV证书的验证过程在各个CA之间并不一致。 为了解决这个问题,CAB Forum引入了EV证书。 它们包含相同的基本信息,但强制要求严格的验证(身份证明)。 EV的签发过程可能需要几天甚至几周,可能包括公共记录的搜索和由公司高管签署的声明(书面形式)。 然而,到最后,网络浏览器并没有突出显示EV证书的任何差异。 因此,EV证书在Web PKI依赖方中并不广泛使用。

事实上,每个Web PKI RP只需要DV级别的保证,即基于对域名控制的“证明”。 重要的是要考虑DV证书实际上证明了什么。 它应该证明请求证书的实体拥有相关域名。 实际上,它证明了在某个时间点,请求证书的实体能够读取电子邮件、配置DNS或通过HTTP提供秘钥。 这些过程底层所依赖的DNS、电子邮件和BGP的基础安全性并不强。 已经发生过攻击这些基础设施的事件,目的是获取欺诈性证书。

对于内部PKI,你可以使用任何你想要的身份验证过程。 你可能比Web PKI使用DNS或电子邮件做得更高明。 起初这可能看起来很难,但实际上并不是。 你可以利用现有的受信任基础设施:用于配置你的东西的任何工具也都应该能够衡量和证明正在配置的内容的身份。 如果你信任用Chef、Puppet、Ansible或Kubernetes来将代码放到服务器上,你也可以信任它们进行身份证明。 如果你在AWS上使用原始AMI,你可以使用实例身份文档GCPAzure具有类似的功能)。

你的配置基础设施必须具有某种身份概念,以便将正确的代码放到正确的位置并启动它们。 而且你必须信任它。 你可以利用这些知识和信任来配置RP信任存储和将订阅者引导到你的内部PKI中。 你只需想出一种方式,让你的配置基础设施告诉你的CA正在启动的这些玩意儿的身份。 顺便说一句,这正是step证书设计用来填补的空白。

证书的过期

证书会过期……通常是这样。 这并不是一个严格的要求,但几乎总是如此。 在证书中包含一个过期日期是很重要的,因为证书是要分散使用的:一般情况下,在RP验证证书时没有一个中心化的权威可供查验。 如果没有过期日期,证书将永远被信任。 安全性的经验法则是,随着我们接近永远,凭证被攻破的概率就会接近100%。 因此,证书要可以过期。

特别是,X.509证书包括一个有效期:签发时间、生效时间和失效时间。 时间不断前进,最终超过了失效时间,证书就会消亡。 这个看似无害的必然性有几个重要的细微之处。

首先,并没有什么能阻止某个特定的依赖方(RP)错误地(或由于糟糕的设计)接受一个已过期的证书。 再次强调,证书的使用是分散的。 检查证书是否已经过期是每个RP的责任,有时候它们会搞砸。 这可能会发生在你的代码依赖于一个没有正确同步的系统时钟的情况下。 一个常见的场景是一个时钟被重置到Unix零点的系统,它不会信任任何证书,因为它认为现在是1970年1月1日——远早于任何近期签发的证书的有效开始日期。 所以要确保你的时钟是同步的!

在订阅者方面,在证书过期后需要妥善处理私钥。 如果密钥对用于签名/认证(例如,与TLS配合使用),一旦不再需要,你就应该删除私钥。 保留一个签名密钥是一种不必要的安全风险项:除了可以用于伪造签名之外,它一无是处。 然而,如果你的密钥对用于加密情况则不同。 只要仍然有数据被该密钥加密,你就需要继续保留该私钥。 如果你曾经被告知不要为签名和加密使用相同的密钥对,这就是主要原因。 为签名和加密使用相同的密钥会使你在不再需要用于签名的私钥时无法实施最佳的密钥生命周期管理实践:如果仍然需要解密某些内容,这就迫使你必须比必要的时间更长地保留签名密钥。

续订

如果你仍在使用即将过期的证书,你需要在此之前进行续订。 事实上,Web PKI 没有标准的续订流程——没有正式的方法来延长证书的有效期。 相反,你只需用新证书替换即将过期的证书。 因此,续订过程与签发过程相同:生成并提交CSR,并完成所有身份验证的义务。

对于内部PKI,我们可以做得更好点。 最简单的方法莫过于使用旧证书通过诸如mTLS之类的协议进行续订。 CA可以验证订阅者携带的客户端证书,使用延长的有效期重新签名,并返回新的证书作为响应。 这使得自动续订非常简单,并仍然强制订阅者定期向中心化的权威服务器进行检查。 你可以利用这个检查过程轻松构建监控和吊销设施。

无论哪种情况,最难的部分莫过于记住在证书过期之前续订它们。 几乎所有管理公共网站证书的人都曾经历过证书意外过期,导致类似这样的错误。 我在这里的最佳建议是:如果你被一件事伤到了,就让它多伤你几次就好了。 比如说,使用短寿命证书。 这将迫使你改进流程并自动化解决这个问题。 Let’s Encrypt使自动化变得简单,并颁发90天的证书,这对于Web PKI来说相当不错。 对于内部PKI,你应该考虑更短的有效期:二十四小时或更短。 存在一些实施挑战——无损证书轮换可能有些棘手——但这是值得努力的。

小提示,你可以使用step命令行检查证书的到期时间:

step certificate inspect cert.pem --format json | jq .validity.end
step certificate inspect https://smallstep.com --format json | jq .validity.end

这虽然简单,但如果你将其与一个小bash脚本中的DNS区域传输结合起来,你可以为你所有域名的证书过期建立优雅的监控,帮助在问题成为故障之前捕捉到它们。

吊销

如果私钥被泄露或者证书不再需要,你可能希望将其撤销。 也就是说,你可能希望主动将其标记为无效,以使其立即停止被RPs信任,即使它还没有到期。 撤销X.509证书非常混乱。 与证书到期相同的是,RP有责任来执行撤销。 与证书到期不同的是,撤销状态不能编码在证书中。 RP必须通过一些带外过程确定证书的撤销状态。 除非显式地配置,不然得话大多数Web PKI TLS RPs都不会费心去检查证书过期状态。 换句话说,默认情况下,大多数TLS实现将乐意接受已撤销的证书。

对于内部PKI,趋势是接受这一现实并使用被动撤销。 也就是说,颁发很快到期的证书,快到连撤销都不需要了。 如果你想“撤销”某个证书,你只需禁止它续订然后等待它到期。 为了使这样的机制得以正常运行,你就需要使用有效期短的证书。 多短呢? 这取决于你的威胁模型(这是安全专家说的¯(ツ)/¯)。 24小时是相当典型的,但更短的到期时间,如5分钟,也很常见。 如果你将生命周期缩短得太短,存在明显的可扩展性和可用性挑战:每次续订都需要与在线CA交互,因此你的CA基础设施最好是可扩展和高可用的。 随着证书生命周期的缩短,请记住保持所有时钟同步,否则你就麻烦了。

对于Web和其他无法使用被动撤销的场景,你应该停下来重新考虑被动撤销。 如果你确实需要撤销,你有两个选择:

  1. 证书吊销列表(Certificate Revocation Lists, CRLs)
  2. 在线证书签名协议(Online Certificate Signing Protocol, OCSP)

CRLs是和其他无数的东西一起被定义在了RFC 5280。 它们只是标识撤销证书的序列号的签名列表。 该列表从CRL分发点提供:一个包含在证书中的URL。 期望的场景是,依赖方将下载此列表,并在验证证书时定期查询其撤销状态。 这里有一些明显的问题:CRL可以很大,分发点可能会出现故障。 如果RP检查CRL,它们将大量缓存来自分发点的响应,并定期同步。 在Web上,CRL经常被缓存几天。 如果CRL需要这么长时间来传播,你可能还不如使用被动撤销。 RP也经常打开失败——如果CRL分发点关闭,它们将接受证书。 这可能是一个安全问题:你可以通过对CRL分发点发起拒绝服务(DOS)攻击来欺骗RP接受已撤销的证书。

值得注意的是,即使你使用CRLs,你也应考虑使用短寿命证书以减小CRL的大小。 CRL只需要包括已撤销但尚未过期的证书的序列号。 如果你的证书寿命较短,你的CRL将更短。

如果你不喜欢CRL,另一个选择是OCSP,它允许RP查询OCSP服务器以获取特定证书的撤销状态。 与CRL分发点类似,OCSP服务器的URL包含在证书中。 OCSP听起来很好(也很明显),但它也有自己的问题。 它对Web PKI提出了严重的隐私问题:OCSP服务器可以根据我提交的证书状态检查请求看到我访问的网站。 它还增加了每个TLS连接的开销:必须发出额外的请求来检查撤销状态。 像CRL一样,许多RP(包括浏览器)在OCSP服务器关闭或返回错误时也会打开失败,并假设证书有效。

OCSP印章是OCSP的一种变体,旨在解决这些问题。 订阅证书的所有者不再需要询问OCSP服务器,OCSP响应是一个签名的陈述,陈述有效期短,指明证书未被撤销。 该陈述包含在订阅者和RP之间的TLS握手中(“附加到”证书)。 这样,RP就能得到一个相对最新的撤销状态,而无需直接查询OCSP服务器。 订阅者可以多次使用签名的OCSP响应,直到它过期。 这减少了OCSP服务器的负载,基本上消除了性能问题,并解决了OCSP的隐私问题。 然而,所有这些都有点像鲁布·戈尔德伯格的装置。 如果订阅者要求一个短期有效的签名陈述以证明证书未过期,为什么不跳过中间商而去直接使用短寿命证书呢?

使用证书

有了这些背景知识,实际上使用证书非常简单。 我们将以TLS为例进行演示,但大多数其他使用场景都是相似的。

  • 要配置一个PKI依赖方,你得告诉它要使用哪些根证书
  • 要配置一个PKI订阅者,你得告诉它要使用哪个证书和私钥(或者告诉它如何生成自己的密钥对,并提交CSR以获取证书)

一个实体(代码、设备、服务器等)通常会既是依赖方又是订阅者。 这些实体需要配置根证书和证书及私钥。 最后,对于Web PKI,正确的根证书通常是默认被信任的,所以你可以跳过这部分。

以下是一个完整的示例,演示证书发行、根证书分发以及TLS客户端(依赖方)和服务器(订阅者)的配置:

step_ca_certificate_flow

希望这足以说明正确和适当的内部PKI和TLS可以多么简单直接。 你不需要使用自签名证书或者做危险的事情,比如禁用证书路径验证(通过给curl加上-k参数)。

几乎所有TLS客户端和服务器都接受相同的参数。 它们中几乎所有都回避了密钥和证书生命周期的部分:它们通常假设证书会是从天上掉在了磁盘上的,会变戏法似的自行轮换等等。 那才是最难的部分。 再次强调,如果你需要这样的功能,那就是step certificates的用武之地。

总结

公钥密码学使计算机能够在网络上拥有了“视觉”。 如果我有一个公钥,我能"看见"你有对应的私钥,但我自己不能使用它。 如果我没有你的公钥,证书可以帮忙。 证书将公钥与拥有对应私钥的所有者的名称绑定在一起。 它们就像是计算机和代码的驾驶执照。 证书颁发机构(CA)用他们的私钥签署证书,为这些绑定背书。 它们就像是车管所。 如果你是唯一一个看起来像你的人,并且你向我展示了一张我信任的车管所颁发的驾驶执照,我就能知道你的名字。 如果你是唯一一个知道私钥的人,并且你向我展示了一个我信任的CA颁发的证书,我就能知道你的名字。

在现实世界中,大多数证书都是X.509 v3证书。 它们使用ASN.1定义,并通常以PEM编码的DER格式序列化。 相应的私钥通常表示为PKCS#8对象,也以PEM编码的DER格式序列化。 如果你使用Java或Microsoft,你可能会遇到PKCS#7和PKCS#12封装格式。 这里有很多历史包袱,可能会让这些东西变得相当令人沮丧,但它只是让人感到很烦,难倒是不难。

公钥基础设施是一个总称,指的是为了有效使用公钥而需要构建和达成一致的所有内容:名称、密钥类型、证书、CA、定时作业、库等等。 Web PKI是公共PKI的一个子集,它默认由Web浏览器和几乎所有使用TLS的其他服务使用。 Web PKI的CA是受信任的,但不一定值得信任。 内部PKI是你自己建立和运行的PKI。 你需要它,因为Web PKI并不是为内部用例设计的,而且内部PKI更容易自动化、扩展,并且能够更好地控制像命名和证书生命周期等重要内容。 对于公共事务,请使用Web PKI。 对于内部事务(例如,使用TLS替代VPN),请使用你自己的内部PKI。 Smallstep证书管理器使构建内部PKI变得相当简单。

要获得证书,你需要为想出个名字并生成密钥。 对于名称,请使用SANs:DNS SANs是给代码和机器用的,EMAIL SANs是给人用的。 如果这些不适用,使用URI SANs。 密钥类型是一个大话题,但大部分情况下并不重要:你可以更改密钥类型,而实际的加密不会成为你PKI中最薄弱的环节。 要从CA获取证书,你需要提交CSR并证明你的身份。 使用短期证书和被动吊销。 自动化证书更新。不要禁用证书路径验证。

记住:证书和PKI将名字与公钥绑定在一起。 其他都只是细节。

「你的肯定是我不懈前行的动力」

Q先生的世界

你的肯定是我不懈前行的动力

使用微信扫描二维码完成支付