Published at 2021-10-07 | Last Update 2021-10-07
本文翻译自 2018 年的一篇英文博客: Everything you should know about certificates and PKI but are too afraid to ask, 作者 MIKE MALONE。
这篇长文并不是枯燥、零碎地介绍 PKI、X.509、OID 等概念,而是从前因后果、历史沿革 的角度把这些东西串联起来,逻辑非常清晰,让读者知其然、知其所以然。
证书和 PKI 的目标其实很简单:将名字关联到公钥(bind names to public keys)。
加密方式的演进:
MAC 最早的验证消息是否被篡改的方式,发送消息时附带一段验证码
| 双方共享同一密码,做哈希;最常用的哈希算法:HMAC
|
\/
Signature 解决 MAC 存在的一些问题;双方不再共享同一密码,而是使用密钥对
|
|
\/
PKC 公钥加密,或称非对称加密,最常用的一种 Signature 方式
| 公钥给别人,私钥自己留着;
| 发送给我的消息:别人用 *我的公钥* 加密;我用我的私钥解密
\/
Certificate 公钥加密的基础,概念:CA/issuer/subject/relying-party/...
| 按功能来说,分为两种
|
|---用于 *签名*(签发其他证书) 的证书
|---用于 *加解密* 的证书
证书(certificate)相关格式及其关系(沉重的历史负担):
最常用的格式 | 信息比 X.509 更丰富的格式 | 其他格式
mTLS 等常用 Java 常用 微软常用
.p7b .p7c .pfx .p12
X.509 v3 PKCS#7 PKCS#12 SSH 证书 PGP 证书 =====> 证书格式
\ | / (封装格式,证书结构体)
\ | /
\ | /
\ | /
\-------------+----------------/
|
ASN.1 (类似于 JSON、ProtoBuf 等) =====> 描述格式
|
/-------------+----------------\
/ | \
/ | \
/ | \
/ | \
DER PEM =====> 编码格式
二进制格式 文本格式 (序列化)
.der .pem .crt .cer
一些解释:
X.509 从结构上定义证书中应该包含的信息,例如签发者、秘钥等等;但使用哪个格式 (例如 JSON 还是 YAML 还是 ASN.1)来描述,并不属于 X.509 的内容;
ASN.1 是 X.509 的描述格式(或者说用 ASN.1 格式来定义 X.509),类似于现在的 protobuf;
2.5.4.3
,有点像 URI 或 IP 地址,在设计上是全球唯一标识符,ASN.1 与其编码格式的关系,与 unicode 与 utf8 的关系类似。
.pem
、.crt
或 .cer
为后缀。某些场景下,X.509 信息不够丰富,因此又设计了一些信息更丰富(例如可以包含证书 链、秘钥)的证书封装格式,包括 PKCS #7 和 #12。
以上提到的东西,再加上 CA、信任仓库、信任链、certificate path validation、证书生命周期管理、 SPIFFE 等还没有提到但也与加密相关的东西,统称为公钥基础设施(PKI)。
翻译时调整了一些配图,也加了几张新图,以方便展示和理解。
由于译者水平有限,本文不免存在遗漏或错误之处。如有疑问,请查阅原文。
以下是译文。
证书(certificates)与 PKI(public key infrastructure,公钥基础设施)很难。我认识的很多非常聪明的人也会绕过这一主题。 我个人也很长时间没去碰这些内容,但说起来很讽刺,我没去碰的原因是 不懂,所以不好意思问 —— 然后更不懂,自然更不好意思问 —— 如此形成恶性循环。
最终我还是硬着头皮学习了这些东西,因为我觉得 PKI 能使一个人在加解密层面(乃至更大的安全层面)去思考如何定义一个系统。 具体来说,PKI 技术,
总之一句话:非常强大!
在深入理解了 PKI 之后,我很后悔没有早点学这些东西。
那为什么大家对这些内容望而却步呢?我认为主要是缺少很好的文档,所以经常看地云里雾里,半途而弃。
本文试图弥补这一缺失。我认为大部分工程师花一个小时读完本文后,头脑中都将有具备了那些 关于加解密的最重要概念和最常见使用场景 —— 这正是本文的目的 —— 一小时只是很小的一个投资,而且这些内容是无法通过其他途径学到的。
本文将用到以下两个开源工具:
证书和 PKI 的目的是:将名字关联到公钥(bind names to public keys)。
这是关于证书和 PKI 的最高抽象,其他都属于实现细节。
本文将用到以下术语。
Entity 是任何存在的东西(anything that exists) —— 即使 只在逻辑或概念上存在(even if only exists logically or conceptually)。 例如,
每个 entity(实体)都有一个 identity(身份)。 要精确定义这个概念比较困难,这么来说吧:identity 是使你之所以为你 (what makes you you)的东西,懂吗?
具体到计算机领域,identity 通常用一系列属性来表示,描述某个具体的 entity, 这里的属性包括 group、age、location、favorite color、shoe size 等等。
Identifier 跟 identity 还不是一个东西:每个 identifier 都是一个唯一标识符, 也唯一地关联到某个有 identity 的 entity。
例如,我是 Mike,但 Mike 并不是我的 identity,而只是个 name —— 虽然二者在我们 小范围的讨论中是同义的。
其他 entity 能够对这个 claim 进行认证(authenticate),以确认这份声明的真假。
一般来说,认证的目的是确认某些 claim 的合法性。
能作为一个证书的 subject 的 entity,称为 subscriber(证书 owner)或 end entity。
对应地,subscriber 的证书有时也称为 end entity certificates 或 leaf certificates, 原因在后面讨论 certificate chains 时会介绍。
CA(certificate authority,证书权威)是给 subscriber 颁发证书的 entity,是一种 certificate issuer(证书颁发者)。
CA 的证书,通常称为 root certificate 或 intermediate certificate,具体取决于 CA 类型。
Relying party 是 使用证书的用户(certificate user),它验证由 CA 颁发(给 subscriber)的证书是否合法。
一个 entity 可以同时是一个 subscriber 和一个 relying party。 也就是说,单个 entity 既有自己的证书,又使用其他证书来认证 remote peers, 例如双向 TLS(mutual TLS,mTLS)场景。
对于我们接下来的讨论,这些术语就够了。下面将进入正题,看如何在实际中实现 证书的声明和认证。
想了解更多相关术语,可参考 RFC 4949。
MAC(消息认证码)是一小段数据,用于验证某个 entity 发送的消息未被篡改。 其基本原理如下图所示:
MAC/HMAC 原理。图片来自:okta.com
关于哈希:
讨论 MAC 其实是为了引出 signature(签名)这一主题。
签名在概念上与 MAC 类似,但不是用共享 secret 的方式, 而是使用一对秘钥(key pair):
MAC 方式中,至少有两个 entity 需要知道共享的 secret,也就是消息的发送方和接 收方。双方都可以生成 MAC,因此给定一个合法的 MAC,我们是 无法知道是谁生成的。
签名就不同了。签名能用公钥(public key)验证,但只能用相应的 私钥(private key)生成。 因此对于接收方来说,它只能验证签名是否合法,而无法生成同样的签名。
如果只有一个 entity 知道秘钥,那这种特性称为 non-repudiation (不可否认性):持有私钥的人无法否认(repudiate)数据是由他签名的这一事实。
MAC 与 signature 都叫做签名,是因为它们和现实世界中的签名是很像的。例如,如果想 让某人同意某事,并且事后还能证明他们当时的确同意了,就把问题写下来,然后让他们 手写签字(签名)。
证书和 PKI 的基础是公钥加密(public key cryptography), 也叫非对称加密(asymmetric cryptography)。
公钥加密系统使用秘钥对(key pair)加解密。一个秘钥对包含:
一个私钥(private key):owner 持有,解密用,不要分享给任何人;
这一点非常重要,值得重复一遍:公钥加密系统的安全性取决于私钥(private key)的机密性。
一个公钥(public key):加密用,可分发和共享给别人;
秘钥可以做的事情:
公钥加密是数学给计算机科学的神秘礼物, 其数学基础 显然很复杂,但如果只是使用,那并不理解它的每一步数学原理。 公钥加密使计算机能做一些之前无法做的事情:它们现在能看到对方是谁了。
这句话的意思是说,公钥加密使一台计算(或代码)能向其他计算机或程序证明, 不用直接分享某些信息,它也能知道该信息。更具体来说,
私钥却与此不同。你能通过私钥对我的身份进行认证(authenticate my identity),但却无法假冒我。
例如,你发给我一个大随机数,我对这个随机数进行签名,然后将再发送给你。 你能用公钥对这个签名进行认证,确认这个签名(消息)确实来自我。 这就是一种证明你在和我(而不是别的其他的人)通信的很好证据。这使得网络上的 计算机能有效地知道它们在和谁通信。
这听起来是一件如此理所当然的事情,但仔细地想一下,网络上只有流动的 0 和 1, 你怎么知道消息来自谁,在和谁通信?因此公钥加密系统是一个非常伟大的发明。
前面说道,公钥加密系统使我们能知道和谁在通信,但这个的前提是: 要知道(有)对方的公钥。
那么,如果对方不知道我的公钥怎么办? 这就轮到证书出场了。
想一下,我们需求其实非常简单:
但光有这个信息还不行,还要让对方相信这些信息;
证书就是用来解决这个问题的,解决方式是请一个双方都信任的权威机构 对以上信息作出证明(签名)。
权威机构对证书进行签名,签名的大概意思是:public key xxx 关联到了 name xx;
对证书进行签名的 entity 称为 issuer(或 certificate authority, CA), 证书中的 entity 称为 subject。
举个例子,如果某个 Issuer 为 Bob 签发了一张证书,其中的内容就可以解读如下:
Some Issuer says Bob’s public key is 01:23:42…
证书是权威机构颁发的身份证明,并没有什么神奇之处
其中 Some Issuer
是证书的签发者(证书权威),证书是为了证明这是 Bob
的公钥,
Some Issuer
也是这个声明的签字方。
由上可知,如果知道 Some Issuer 的公钥,就可以通过验证签名的方式来 对它(用私钥)签发的证书进行认证(authenticate)。 如果如果你信任 Some Issuer,那你就可以信任这个声明。
因此,证书使大家能基于对 issuer 公钥的信任和知识,来学习到其他 entity 的公钥 (上面的例子中就是 Bob)。这就是证书的本质。
证书就像是计算机/代码的驾照或护照。如果你之前从未见过我,但信任车管局,那你可以 用我的驾照做认证:
计算机用证书做类似的事情:如果之前从未和其他电脑通信,但信任 一些证书权威,那可以用证书来认证:
下面是个真实的证书:
还是与驾照类比:
上图中有大量的细节,很多东西将在下面讨论到。但归根结底还是本文最开始总结的那句话 :证书不过是一个将名字关联到公钥(bind names to public keys)的东西。 其他都是实现细节。
接下来看一看证书在底层的表示(represented as bits and bytes)。
这部分内容复杂且相当令人沮丧。事实上,我怀疑证书和秘钥诡异的编码方式 是导致 PKI 如此混乱和令人沮丧的根源。
一般来说,人们提到“证书”而没有加额外限定词时,指的都是 X.509 v3 证书。
也有其他的证书格式,例如著名的 SSH 和 PGP 都有它们各自的格式。
本文主要关注 X.509,理解了 X.509,其他格式都是类似的。 由于这些证书使用广泛,因此有很好的函数库,而且也用在浏览器之外的场景。毫无疑问,它们是 internal PKI 颁发的最常见证书格式。重要的是,这些证书在很多 TLS/HTTPS 客户端/服 务端程序中都是开箱即用的。
了解一点 X.509 的历史对理解它会有很大帮助。
X.509 在 1988 年作为国际电信联盟(ITU)X.500 项目的一部分首次标准化。 这是通信(telecom)领域的标准,想通过它构建一个全球电话簿(global telephone book)。 虽然这个项目没有成功,但却留下了一些遗产,X.509 就是其中之一。
如果查看 X.509 的证书,会看到其中包含了 locality、state、country 等信息, 之前可能会有疑问为什么为 web 设计的证书会有这些东西,现在应该明白了,因为 X.509 并不是为 web 设计的。
X.509 构建在 ASN.1 (Abstract Syntax Notation,抽象语法标注)之上,后者是另一个 ITU-T 标准 (X.208 and X.680)。
ASN.1 定义数据类型,
RFC 5280 用 ASN.1 来定义 X.509 证书,其中包括名字、秘钥、签名等信息。
ASN.1 除了有常见的数据类型,如整形、字符串、集合、列表等, 还有一个不常见但很重要的类型:OID(object identifier,对象标识符)。
2.5.4.3
)。可以用 OID 来 tag 一段数据的类型。例如,一个 string 本来只是一个 string,但可
以 tag 一个 OID 2.5.4.3
,然后就变成了一个特殊 string:这是
X.509 的通用名字(common name) 字段。
ASN.1 只是抽象(abstract),因为这个标准并未定义在数据层应该如何表示(represented as bits and bytes)。 ASN.1 与其编码格式的关系,就像 unicode 与 utf8 的区别。 因此,有很多种编码规则(encoding rules),描述具体如何表示 ASN.1 数据。 原以为增加这层额外的抽象会有所帮助,但实际证明大部分情况下反而徒增烦恼。
ASN.1 有很多种编码规则, 但用于 X.509 和其他加密相关的,只有一种常见格式:DER —— 虽然有时也会用到 non-canonical 的 basic encoding rules (BER,基础编码规则) 。
DER 是非常简单的 TLV(type-length-value)编码,但实际上用户无需 关心这些,因为函数库封装好了。但不要高兴得太早 —— 虽然我们不必关心 DER 的编解码, 但要能判断给定的某个 X.509 证书是 DER 还是其他类型编码的。这里的其他类型包括:
DER 编码的证书通常以 .der
为后缀。
DER 是二进制格式,不便复制粘贴。因此大部分证书都是以 PEM 格式打包的,这是 另一个历史怪胎。
如果你熟悉 MIME 的话,二者是比较类似的: 由 header、base64 编码的 payload、footer 三部分组成。 header 中有标签(label)来描述 payload。例如下面是一个 PEM 编码的 X.509 证书:
-----BEGIN CERTIFICATE-----
MIIBwzCCAWqgAwIBAgIRAIi5QRl9kz1wb+SUP20gB1kwCgYIKoZIzj0EAwIwGzEZ
MBcGA1UEAxMQTDVkIFRlc3QgUm9vdCBDQTAeFw0xODExMDYyMjA0MDNaFw0yODEx
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRc+LHppFk8sflIpm/XKpbNMwx3
SDAfBgNVHSMEGDAWgBTirEpzC7/gexnnz7ozjWKd71lz5DAKBggqhkjOPQQDAgNH
ADBEAiAejDEfua7dud78lxWe9eYxYcM93mlUMFIzbWlOJzg+rgIgcdtU9wIKmn5q
FU3iOiRP5VyLNmrsQD3/ItjUN1f1ouY=
-----END CERTIFICATE-----
但令人震惊的时,即便如此简单的功能,在实现上也已经出现混乱:PEM labels 在不同工具之间是不一致的。 RFC 7468 试图标准化 PEM 的使用规范, 但也并不完整,不是所有工具都遵循这个规范。
PEM 编码的证书通常以 .pem
、.crt
或 .cer
为后缀。
再次提醒,这只是“通常”情况,实际上某些工具可能并不遵循这些惯例。
下面介绍几个前面提到的“其他类型的打包格式”。
X.509 只是一种常用的证书格式,但有人觉得这种格式能装的信息不够多,因此 又定义了一些比 X.509 更大的数据结构(但仍然用 ASN.1), 能将证书、秘钥以及其他东西封装(打包)到一起。因此,有时说我需要“一个证书”时,其 实真正说的是包(package)中包含的那个“证书”(a certificate in one of these envelopes),而不是这个包本身。
你可能会遇到的是一个称为 PKCS(Public Key Cryptography Standards,公钥加密标准)的标准的一部分, 它由 RSA labs 发布(真实历史要 更加复杂一些,本文不展开)。
其中的第一个标准是 PKCS#7,后面被 IETF 重新冠名为 Cryptographic Message Syntax (CMS) ,其中可以包含多个证书(以 full certificate chain 方式编码,后面会看到)。
PKCS#7 在 Java 中使用广泛。常见扩展名是 .p7b
and .p7c
。
另一个常见的打包格式 <a href=https://tools.ietf.org/html/rfc7292>PKCS#12</a>, 它能将一个证书链(这一点与 PKCS#7 类似)连同一个(加密之后的)私钥打包到一起。
微软的产品多用这种格式,常见后缀.pfx
and .p12
。
再次说明,PKCS#7 和 PKCS#12 envelopes 仍然使用 ASN.1,这意味着 它们都能以原始 DER、BER 或 PEM 的格式编码。 从我个人的经验来看,二者几乎都是 DER 编码的。
秘钥编码(Key encoding)的过程与以上描述的类似(复杂):
秘钥的解密过程(deciphering),一半是是科学,一半是艺术。
如果足够幸运,根据 RFC 7468 就能找到其中的 PEM payload;
椭圆曲线秘钥通常符合 RFC 7468 规范,虽然 这里看起来似乎也并没有什么标准。
下面是一个 PEM 编码的椭圆曲线秘钥(PEM-encoded elliptic curve key):
$ 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-----
其他秘钥,通常用 PEM label “PRIVATE KEY” 描述
PEM label “PRIVATE KEY” 描述的秘钥,通常暗示这是一个 PKCS#8 payload, 这是一种私钥(private key)封装格式,其中包含秘钥类型和其他 metadata。
用密码来加密私钥也很常见(private keys encrypted using a
password),这里的密码可以是 a shared secret or symmetric key。
看起来大致如下(Proc-Type
and DEK-Info
是 PEM 的一部分,表示这个 PEM 的 payload 是用 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 对象也能被加密,这种情况下 header label 应该是 "ENCRYPTED PRIVATE KEY" per RFC 7468。
这种情况下不会看到 Proc-Type
和 Dek-Info
headers,因为这些信息此时编码到了 payload 中。
.pub
or .pem
,.prv,
.key
, or .pem
。但再次说明,有些工具或组织可能并不遵循业界惯例。
如果觉得以上内容理解起来很杂乱,那并不是你的问题,而是加密领域的现状就是如此。我已经尽力了。
至此我们已经知道了证书的来历和样子,但这仅仅是本文的一半。 下面看证书是如何创建和使用的。
Public key infrastructure (PKI) 是一个统称,包括了我们在 如下与证书和秘钥管理及交互操作时需要用到的所有东西:签发、分发、存放、使用、验证、撤回等等。 就像“数据库基础设施” 一样,这个名词是有意取的这样模糊的。
自己从头开始构建一个 PKI 是一件极其庞大的工作, 但实际上 一些简单的 PKI 甚至并不使用证书。例如,
~/.ssh/authorized_keys
文件时,就是在配置
一个简单的无证书形式的(certificate-less)PKI,SSH 通过这种方式在扁平文件内
实现 public key 和 name 的绑定;如果从头开始构建一个 PKI,唯一确定的事情是:你需要用到公钥(public keys), 其他东西都随设计而异。
下文将主要关注 web 领域使用的 PKI,以及基于 Web PKI 技术、遵循现有标准的 internal PKI。
证书和 PKI 的目标其实很简单:将名字关联到公钥(bind names to public keys)。 在下面的内容中,不要忘了这一点。
浏览器访问 HTTPS 链接时会用到 Web PKI。虽然也有一些问题,但它大大提升了 web 的安全性,而且基本上对用户透明。在访问互联网 web 服务时,应该在所有可能的情 况下都启用它。
PKIX 和 CAB Forum 文档涵盖了很大内容。 它们定义了前面讨论的各种证书、还定义什么是 “name” 以及位于证书中什么位置、能使用什么签名算法、 RP 如何判断 issuer 的证书、如何指定证书的 validity period (issue and expiry dates)、 撤回、certificate path validation、CA 判断某人是否拥有一个域名等等。
Web PKI 很重要,是因为浏览器默认使用 Web PKI 证书。
Internal PKI 是用户为自己的产品基础设施使用的 PKI,这些产品包括
Internal PKI 使你能认证和建立加密通道,这样你的服务就可以安全地在公网上的任意位置互相通信了。
首先,简单来说:Web PKI 设计中并没有考虑内部使用场景。 即使有了 Let’s Encrypt 这样的提供免费证书和自动化交付的 CA, 用户还是需要自己处理 rate limits 和 availability 之类的事情。 如果有很多 service,部署很频繁,就非常不方便。
另外,Web PKI 中,用户对证书生命周期、撤回机制、续约过程、秘钥类型、算法等等很 多重要的细节都没有控制权,或只有很少控制权。而下文将会看到,这些都是非常重要的东西。
最后,CA/Browser Forum Baseline Requirements
实际上禁止将 Web PKI CA 关联到 internal IPs (e.g., 10.0.0.0/8
)
及 internal DNS names that aren’t fully-qualified and
resolvable in public global DNS (e.g., you can’t bind a kubernetes cluster DNS
name like foo.ns.svc.cluster.local
)。
如果需要在证书中绑定到这些 name,或者签发大量证书,或者控制证书细节,就需要自己的 internal PKI.
下面一节将看到,信任(或缺乏信任)是避免将 Web PKI 用于内部场景的另一个原因。
总结起来,建议:
前面介绍到,证书可解读为一个 statement 或 claim,例如:
Issuer(签发者)说,该 subject 的公钥是 xxx。
Issuer 会对这份声明进行签名,relying party 能(通过 issuer 的公钥)验证(authenticate)签名是否合法。 但这里其实跳过了一个重要问题:relying party 是如何知道 issuer 的公钥的?
答案其实很简单:relying parties 在自己的 trust store(信任库)预先配置了一个它 信任的根证书(trusted root certificates,也称为 trust anchors)列表,
预配置的具体方式(the manner in which this pre-configuration occurs), 是 PKI 非常重要的一面:
如果沿着这个信任链(chain of trust)回溯足够远,最后总能找到人(people):每个 信任链都终结在现实世界(meatspace)。
信任仓库中的根证书是自签名的(self-signed):issuer 和 subject 相同。逻辑上,这种 statement 表示的是:
Mike 说:Mike 的公钥是 xxx。
自签名的证书保证了该证书的 subject/issuer 知道对应的私钥, 但任何人都可以生成一个自签名的证书,这个证书中可以写任何他们想写的名字(name)。
因此证书的起源(provenance)就非常关键:一个自签名的证书,只有 当它进入信任仓库的过程是可信任时,才应该信任这个根证书。
/etc
或其他路径下面的一些文件。所以,信任仓库又从哪里来?对于 Web PKI 来说,最重要的 relying parties 就是浏览器。主流浏览器默认使用的信任仓库 —— 及其他任何使用 TLS 的东西 —— 都是由四个组织维护的:
操作系统中的信任仓库通常都是系统自带的。
curl
,通过默认用操作系统的信任仓库。因此,这个信任仓库通常情况下,会被该系统上预装的很多东西默认使用;通过软件更新( 通常使用另一个 PKI 来签名)而更新。
信任仓库中通常包含了超过 100 个由这些程序维护的常见证书权威(certificate authorities)。 其中一些著名的:
如果想编程控制:
这 100 多个证书权威在理论上是可信的(trusted) —— 浏览器和其他 一些软件默认情况下信任由这些权威颁发的证书。
但是,这并不意味着它们是可靠的(trustworthy)。 已经出现过 Web PKI 证书权威向政府机构提供假证书的事故,以便 窥探流量(snoop on traffic)或仿冒某些网站。 这类“受信任的” CA 中,其中在司法管辖权之外的地方运营 —— 包括民主国家和专制国家。
我们很快就会看到,Web PKI 的安全性取决于安全性最弱的权威(the least secure CA)的安全性。 这显然不是我们希望的。
浏览器社区已经在采取行动来解决这些问题。 CA/Browser Forum Baseline Requirements 规定了这些受信的证书权威在签发证书时应该遵守的规则。 作为 WebTrust audit 项目的一部分,在将 CA 加入到某些信任仓库(例如 Mozilla 的)之前,会对 CA 合规性进行审计。
如果内部场景(internal stuff)已经在使用 TLS,你可能大部分情况下 并不需要信任这些 public CA。 如果信任了,就为 NSA 和其他组织打开了一扇地狱之门:你的系统安全性将取决于 100 多 个组织中安全性最弱的那一个。
令事情更糟糕的是,Web PKI relying parties (RPs) 信任它们的信任仓库中任何 CA 签发给任何 subscriber 的证书。结果是 Web PKI 整体的安全性取决于所有 Web PKI CA 中最弱的那个。 2011 DigiNotar 攻击就说明了这个问题:
最近,森海塞尔(Sennheiser)因为在它们的 HeadSetup APP 信任仓库中 安装了一个自签名的根证书 引起了一次重大安全事故,
这完全摧毁了 TLS 带来的好处,太糟糕了!
已经有一些机制来减少此类风险:
这里存在的问题是:缺少 RP 端的支持。CAB Forum now mandates CAA checks in browsers. Some browsers also have some support for CT and HPKP. 但对于 其他 RPs (e.g., most TLS standard library implementations) 这些东西几乎都是没有 贯彻执行的。This issue will come up repeatedly: a lot of certificate policy must be enforced by RPs, and RPs can rarely be bothered. If RPs don’t check CAA records and don’t require proof of CT submission this stuff doesn’t do much good.
在任何情况下,如果使用自己的 internal PKI,都应该为 internal 服务维护一个单独的信任仓库。 即,
如果想在内部实现更好的联邦(federation) —— 例如限制 internal CA 能签发哪些证书,
前面已经讨论了很多 CA 相关的东西,但我们还没定义什么是 CA。
显然需要一些逻辑和过程来将这些东西串联起来。CA 需要将它的证书分发到信任仓库,接受和处理 证书请求,颁发证书给 subscriber。
CAB Forum Baseline Requirements 4.3.1 明确规定:一个 Web PKI CA 的 root private key 只能通过 issue a direct command 来签发证书。
这样规定是出于安全考虑。
一些 internal PKI 也遵循类似的实践,但实际上并没有这个必要。
在 root CA offline 的前提下,为使证书 issuance 可扩展(例如,使自动化成为可能), root private key 只在很少情况下使用,
Intermediates 通常并不包含在信任仓库中,所以撤回或 roate 比较容易, 因此通过 intermediate CA,就实现了 certificate issuance 的在线和自动化(online and automated)。
这种 leaf、intermediate、root 组成的证书捆绑(bundle)机制, 形成了一个证书链(certificate chain)。
如下图所示:
技术上来说,上图是一个简化的例子。你可以创建更长的 chain 和更复杂的图(例如, cross-certification)。 但不推荐这么做,因为复杂性很快会失控。在任何情况下, end entity certificates 都是叶子节点,这也是称为叶子证书(leaf certificate)的原因。
当配置一个 subscriber 时(例如,Apache、Nginx、Linkderd、Envoy), 通常不仅需要叶子证书,还需要一个包含了 intermediates 的 certificate bundle。
PKCS#7
和 PKCS#12
,因为它们能包含一个完整的证书链(certificate chain)。更多情况下,证书链编码成一个简单的空行隔开的 PEM 对象(sequence of line-separated PEM objects)。
Some stuff expects the certs to be ordered from leaf to root, other stuff expects root to leaf, and some stuff doesn’t care. More annoying inconsistency. Google and Stack Overflow help here. Or trial and error.
下面是一个例子:
$ cat server.crt
-----BEGIN CERTIFICATE-----
MIICFDCCAbmgAwIBAgIRANE187UXf5fn5TgXSq65CMQwCgYIKoZIzj0EAwIwHzEd
...
MBsGA1UEAxMUVGVzdCBJbnRlcm1lZGlhdGUgQ0EwHhcNMTgxMjA1MTc0OTQ0WhcN
HO3iTsozZsCuqA34HMaqXveiEie4AiEAhUjjb7vCGuPpTmn8HenA5hJplr+Ql8s1
d+SmYsT0jDU=
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIBuzCCAWKgAwIBAgIRAKBv/7Xs6GPAK4Y8z4udSbswCgYIKoZIzj0EAwIwFzEV
...
BRvPAJZb+soYP0tnObqWdplmO+krWmHqCWtK8hcCIHS/es7GBEj3bmGMus+8n4Q1
x8YmK7ASLmSCffCTct9Y
-----END CERTIFICATE-----
Again, annoying and baroque, but not rocket science.
由于 intermediate certificates 并未包含在信任仓库中,因此需要与 leaf certificates 一样分发和验证。
完整的 certificate path validation 算法比较复杂。包括了
显然,PKI RP 准确实现这个算法是非常关键的。
curl -k
),用户将面临重大风险,所以不要关闭。可能有人会说,channel 已经是加密的了,因此关闭没关系 —— 错,有关系。 没有认证(authentication)的加密是毫无价值的 —— 这就像在教堂忏悔: 你说的话都是私密的,但却并不知道帘幕后面的人是谁 —— 只不过这里不是教堂,而是互联网。
在能通过 TLS 等协议使用证书之前,要先配置如何从 CA 获取一个证书。 逻辑上来说这是一个相当简单的过程:
证书会过期,过期的证书就不会被 RP 信任了。如果证书快过期了而还想继续用它,就需要 续期(renew )并轮转(rotate)它。如果想在一个证书过期之前就让 RP 停止信任它,就需要执行撤销(revoke)。
与 PKI 相关的大部分东西一样,这些看似简单的过程实际上都充满坑。 其中也隐藏了计算机科学中最难的两个问题:缓存一致性和命名(naming)。 但另一方面,一旦理解了背后的原来,再反过来看实际在用的一些东西就简单多了。
历史上,X.509 使用 X.500 distinguished names (DN) 来命名证书的使用者(name the subject of a certificate),即 subscriber。 一个 DN 包含了一个 common name (对作者我来说,就是 “Mike Malone”),此外还可以包含 locality、country、organization、organizational unit 及其他一些东西(数字电话簿相关)。
PKIX 规定一个网站的 DNS hostname 应该关联到 DN common name。最近,CAB Forum 已 经废弃了这个规定,使整个 DN 字段变成可选的(Baseline Requirements, sections 7.1.4.2)。
现代最佳实践使用 subject alternative name (SAN) X.509 extension 来 bind 证书中的 name。
常用的 SAN 有四种类型,绑定的都是广泛使用的名字:
在我们讨论的上下文中,这些都是唯一的,而且它们能很好地映射到我们想识别的东西:
应该使用 SAN。
注意,Web PKI 允许在一个证书内 bind 多个 name,name 也也允许通配符。也就是说,
*.smallstep.com
这样的 SAN。有了 name 之后,需要先生成一个密钥对,然后才能创建证书。前面提到:PKI 的安全性 在根本上取决于一个简单的事实:只有与证书中的 subscriber name 对应的 entity,才应该拥有与该证书对应的私钥。 为确保这个条件成立,
生成证书时使用什么类型的秘钥?这一主题值得单独写一篇文章,这里 只提供一点快速指导(截止 2018.12)。
secp256k1
or prime256v1
in openssl),
除非你担心 NSA,这种情况下你可以选择更 fancier 一些的东西,例如 EdDSA with Curve25519(但对这些秘钥的支持还不是太好)。下面是用 openssl
生成一个椭圆曲线 P-256 key pair 的例子:
$ 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
还可以通过编程来生成这些证书,这样能做到证书不落磁盘。
subscriber 有了一个 name 和一对 key 之后,下一步就是从 CA 获取一个 leaf certificate。 对 CA 来说,它需要认证(证明)两件事情:
subscriber 证书中的公钥,确实是该 subscriber 的公钥(例如,验证该 subscriber 知道对应的私钥);
这一步通常通过一个简单的技术机制实现:证书签名请求(certificate signing request, CSR)。
证书中将要绑定的 name,确实是该 subscriber 的 name。
这一步要难很多。抽象来说,这个过程称为 identity proofing(身份证明)或 registration(注册).
Subscriber 请求一个证书时,会向 CA 会提交一个 certificate signing request (CSR)。
CSR 是自签名的,用与 CRS 中公钥对应的私钥自签名。
用 step
命令创建一个密钥对和 CSR 的例子:
$ step certificate create -csr test.smallstep.com test.csr test.key
OpenSSL 功能也非常强大,但 用起来不够方便。
CA 收到一个 CSR 并验证签名之后,接下来需要确认证书中绑定的 name 是否真的 是这个 subscriber 的 name。这项工作很棘手。 证书的核心功能是能让 RP 对 subscriber 进行认证。因此, 如果一个证书都还没有颁发,CA 如何对这个 subscriber 进行认证呢?
答案是:分情况。
Web PKI 有三种类型的证书,它们最大的区别就是如何识别 subscriber, 以及它们所用到的 identity proofing 机制。
这三种证书是:
domain validation (DV,域验证)
DV 证书绑定的是 DNS name,CA 在颁发时需要验证的这个 domain name 确实是由该 subscriber 控制的。
证明过程通常是通过一个简单的流程,例如
ACME protocol (最初由 Let’s Encrypt 开发和使用)改进了这种方式,更加自动化:不再用邮件验证 ,而是由 ACME CA 提出一个 challenge,该 subscriber 通过完成这个问题来证明它拥有 这个域名。challenge 部分属于 ACME 规范的扩展部门,常见的包括:
organization validation (OV,组织验证)
extended validation (EV,扩展验证)
这些完成之后,当相应网站时,某些浏览器会在 URL 栏中显示该组织的名称。例如:
但除了这个场景之外,EV certificates 并未得到广泛使用,Web PKI RP 也未强依赖它。
本质上来说,每个 Web PKI RP 只需要 DV 级别的 assurance 就行了, 也就是确保域名是被该 subscriber 控制的。重要的是能理解一个 DV 证书在设计上的意思和在实际上做了什么:
但话说回来,DNS、电子邮件和 BGP 这些底层基础设施本身的安全性也并没有做到足够好, 针对这些基础设施的攻击还是 时有发生, 目的之一就是获取证书。
上面是 Web PKI 的身份证明过程,再来看 internal PKI 的身份证明过程。
实际上,用户可以使用任何方式来做 internal PKI 的 identity proofing, 并且效果可能比 Web PKI 依赖 DNS 或邮件方式的效果更好。
乍听起来好像很难,但其实不难,因为可以利用已有的受信基础设施: 用来搭建基础设施的工具,也能用来为这些基础设施之上的服务创建和证明安全身份。
provisioning infrastructure 必须理解 identity 的概念,这样才能将正确的代码放到正确的位置。 此外,用户必须信任这套机制。基于这些知识和信任,才能配置 RP 信任仓库、将 subscribers 纳入你的 internal PKI 管理范围。 而完成这些功能全部所需做的就是:设计和实现某种方式,能让 provisioning infrastructure 在每个服务启动时,能将它们的 identity 告诉你的 CA。 顺便说一句,这正是 step certificates 解决的事情。
证书通常都会过期。虽然这不是强制规定,但一般都这么做。设置一个过期时间非常重要,
因此,设置过期时间非常重要。具体来说,X.509 证书中包含一个有效时间范围:
这个机制看起来设计良好,但实际上也是有一些不足的:
1970.1.1
),此时它无法信任任何证书。在 subscriber 侧,证书过期后,私钥要处理得当:
如果一个密钥对之前是用来签名/认证的(例如,基于 TLS),
如果密钥对是用来加密的,情况就不同了。
这就是为什么很多人会说,不要用同一组秘钥来同时做签名和加密(signing and encryption)。 因为当一个用于签名的私钥过期时,无法实现秘钥生命周期的最佳管理: 最终不得不保留着这个私钥,因为解密还要用它。
证书快过期时,如果还想继续使用,就需要续期。
Web PKI 实际上并没有标准的续期过期:
对于 internal PKI 我们能做的更好。
最简单的方式是:
证书的续期过程其实并不是太难,最难的是记得续期这件事。
几乎每个管理过公网证书的人,都经历过证书过期导致的生产事故,例如这个。 我的建议是:
Let’s Encrypt 使自动化非常容易,它签发 90 天有效期的证书,因此对 Web PKI 来说非常合适。 对于 internal PKI,建议有效期签的更短:24 小时或更短。有一些实现上的挑战 —— hitless certificate rotation 可能比较棘手 —— 但这些工作是值得的。
用
step
检查证书过期时间:step certificate inspect cert.pem --format json | jq .validity.end step certificate inspect https://smallstep.com --format json | jq .validity.end
将这种命令行封装到监控采集脚本,就可以实现某种程度的监控和自动化。
如果一个私钥泄露了,或者一个证书已经不再用了,就需要撤销它。即希望:
但实际上,撤销证书过程也是一团糟。
除非显式配置,否则大部分 Web PKI TLS RP 并不关注撤销状态。换句话说,默认情况下, 大部分 TLS 实现都乐于接受已经撤销的证书。
Internal PKI 的趋势是接受这个现实,然后试图通过被动撤销(passive revocation)机制来弥补, 具体来说就是签发生命周期很短的证书,这样就使撤销过程变得不再那么重要了。 想撤销一个证书时,直接不给它续期就行了,过一段时间就会自动过期。
可以看到,这个机制有效的前提就是使用生命周期很短的证书。具体有多短?
对于 web 和其他的被动撤销不适合的场景,如果认真思考之后发现真的 需要撤销功能,那有两个选择:
CRL 定义在 RFC 5280 中,这是一个相当庞杂的 RFC,还定义了很多其他东西。 简单来是,CRL 是一个有符号整数序列,用来识别已撤销的证书。
这个维护在一个 CRL distribution point 服务中,每个证书中都包含指向这个服务的 URL。 工作流程:每个 RP 下载这个列表并缓存到本地,在对证书进行验证时,从本地缓存查询撤销状态。 但这里也有一些明显的问题:
因此,即使已经在用 CRL,也应该考虑使用短时证书来保持 CRL size 比较小。 CRL 只需要包含已撤销但还未过期的证书的 serial numbers,因此 证书生命周期越短,CRL 越短。
主动检查机制除了 CRL 之外,另一个选择是 OCSP,它允许 RP 实时查询一个 OCSP responder: 指定证书的 serial number 来获取这个证书的撤销状态。
与 CRL distribution point 类似,OCSP responder URL 也包含在证书中。 这样看,OCSP 似乎更加友好,但实际上它也有自己的问题。对于 Web PKI,它引入了验证的隐私问题:
OCSP stapling 是 OCSP 的一个变种,目的是解决以上提到的那些问题。
相比于让 RP 每次都去查询 OCSP responder,OCSP stapling 中让证书的 subscriber 来做这件事情。 OCSP response 是一个经过签名的、时间较短的证词(signed attestation),证明这个证书未被撤销。
attestation 包含在 subscriber 和 RP 的 TLS handshake (“stapled to” the certificate) 中。 这给 RP 提供了相对比较及时的撤销状态,而不用每次都去查询 OCSP responder。 subscriber 可以在 signed OCSP response 过期之前多次使用它。这减少了 OCSP 的负担,也解决了 OCSP 的隐私问题。
但是,所有这些东西其实最终都像是一个 鲁布·戈德堡装置(Rube Goldberg Device) ,
鲁布·戈德堡机械(Rube Goldberg machine)是一种被设计得过度复杂的机械组合,以 迂回曲折的方法去完成一些其实是非常简单的工作,例如倒一杯茶,或打一只蛋等等。 设计者必须计算精确,令机械的每个部件都能够准确发挥功用,因为任何一个环节出错 ,都极有可能令原定的任务不能达成。
解释来自 知乎。
如果让 subscribers 去 CA 获取一些生命周期很短的证词(signed attestation)来证明对应的证书并没有过期, 为什么不直接干掉中间环节,直接使用生命周期很短的证书呢?
虽然理解 PKI 需要以上长篇大论,但在实际中用证书其实是非常简单的。
下面以 TLS 为例,其他方式也是类似的:
配置 PKI relying party 使用哪个根证书;
对于 Web PKI,通常已经默认配置了正确的根证书,这一步可以跳过。
配置 PKI subscriber 使用哪个证书和私钥(或如何生成自己的密钥对、如何提交 CSR);
某个 entity (code, device, server, etc) 既是 RP 又是 subscriber 是很常见的。 这样的 entities 需要同时配置根证书、证书和私钥。
下面是个完整例子,展示 certificate issuance, root certificate distribution, and TLS client (RP) and server (subscriber) configuration:
希望这展示了使用 internal PKI 和 TLS 是如何简单直接。
curl -k
)。公钥加密系统使计算机能在网络上看到对方(”see” across networks)。
现实中,
加密领域有沉重的历史包袱,使当前的这些东西学起来、用起来非常让人沮丧,这比一项技术因为太难而不想学更加令人沮丧。
PKI 是使用公钥基础设施时涉及到的所有东西的统称:names, key types, certificates, CAs, cron jobs, libraries 等。
要获得一个证书,需要命令和生成证书。建议 name 用 SAN:
秘钥类型(key type)是很大一个主题,但几乎不重要:你可以随便修改秘钥类型, 而且实际上加密本身(crypto)并不是 PKI 中最弱的一环。
要从 CA 获取一个证书,需要提交一个 CSR 并证明申请者的身份(identity)。 使用生命周期较短的证书和 passive revocation。 自动化证书续期过程。不要禁用 certificate path validation。
最后还是那句话:证书和 PKI 将名字关联到公钥(bind names to public keys)。 其他都是细节。