从里到外的HTTPS证书(X509)详解

关于什么是 HTTPS 证书,以及证书是怎么工作的文章已经有很多,而本文将从另一个角度来讲解一个 X509 证书究竟是怎么工作的,证书文件里面究竟存放了什么内容,以及证书是如何互相签名信任的。

在分析证书文件结构和内容之前,需要先了解一个前置知识点,那就是 ASN.1

阅读小提示

本文先从最基本的知识讲起,逐渐覆盖到 X509。因此尽管前面的内容看起来没有什么关联,但还是推荐从头到尾顺序阅读。如果前面的部分确实看不懂,也可以先短暂跳过,再回头复习。

ASN.1

ASN.1的全称是Abstract Syntax Notation One,这是一个就像字面意思一样抽象的东西,它本身是一个接口描述语言,用来描述如何定义一个可序列化的结构体。它广泛的应用与密码学和通信等行业。

就算这样说也是有点难以理解,有一个和ASN.1类似的东西,大家应该都听过,那就是 Google 推出的 Protocol Buffer,以下简称 protobuf。如果你没有听说过的话,可以花费几分钟利用搜索引擎简单了解下。

protobuf大体上可以由两部分组成,第一部分是使用专用语言 proto 编写的描述文件,它描述了一个结构体对象具有哪些属性,或者称为字段,以及这些字段是什么类型,它是数字还是字符串。例如直接从官网复制一份例子:

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
}

非常简单明了的一份描述文件,它声明了一个对象 Person 具有三个属性,以及它们的类型。

第二部分则是 proto 文件的编译器,又或者叫做代码生成器。它的作用是将上述的描述文件编译成对应语言的代码。生成的代码功能则是实现了如何将一个 Person 对象序列化成二进制数据。

ASN.1 就是类似这样的一个东西,例如从 Wikipedia 上复制的一个例子:

FooProtocol DEFINITIONS ::= BEGIN

    FooQuestion ::= SEQUENCE {
        trackingNumber INTEGER,
        question       IA5String
    }

    FooAnswer ::= SEQUENCE {
        questionNumber INTEGER,
        answer         BOOLEAN
    }

END

这份描述文件定义了两个结构体,FooQuestionFooAnswer,以及他们对应的属性。

不同于 protobuf 的地方是,protobuf 中定义与编码是一体的。我们一谈到 protobuf,就指的是 proto 语言编写的描述文件,和 protobuf 独特的二进制编码方案。当然如果你愿意,也可以自己发明一个新的编码方案,例如你可以编码成一个 JSON

而在 ASN.1 中,编码方案则是可以有多种方案的。例如:

以及还有更多的方案,可以在 Wikipedia 上的 ASN.1 词条查看。

关于链接

本文有一些链接利用了 chrome 浏览器的高亮功能,使用 chrome 浏览器点击后,可以自动跳转到相关联的位置并且高亮显示。

如果读者不使用 chrome,也不会影响阅读。

这个时候如果你比较细心,应该会发现上述方案中,有一个叫做 DER 的方案似乎非常眼熟。没错,这个就是我们常见的证书文件格式之一,另一种则是 PEM

DER

之前提到 DER 的全称是 Distinguished Encoding Rules,它是一种二进制编码规则,就像 protobuf 的二进制编码。它规定了 ASN.1 中的数据结构应该如何序列化。

DER 是一种典型的 type-length-value 的格式,即编码时按照 类型-长度-值 的方式逐个编码。

同样从 Wikipedia 上复制一个例子,接着上文定义的 FooQuestion 来说,现在有一个 FooQuestion 对象:

myQuestion FooQuestion ::= {
    trackingNumber     5,
    question           "Anybody there?"
}

它的两个字段值分别是 5Anybody there?。将他按照 DER 格式编码得到的结果以16进制表示如下:

30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f

一个字节一个字节的来阅读,就是这样的:

30 — type tag indicating SEQUENCE
13 — length in octets of value that follows
  02 — type tag indicating INTEGER
  01 — length in octets of value that follows
    05 — value (5)
  16 — type tag indicating IA5String 
     (IA5 means the full 7-bit ISO 646 set, including variants, 
      but is generally US-ASCII)
  0e — length in octets of value that follows
    41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f — value ("Anybody there?")

虽然说注释都是英文,但是都是很简单的基础英文,相信大家都没有什么问题吧。

DER 的具体编码方法是有点复杂,而且本文重点也不是如何解析或者如何编码。只需要了解 DER 是一种编码 ASN.1 数据的格式 这一简单事实就好。

也有一些在线的工具可以辅助大家理解 ASN.1DER

第一个工具功能上比较有限,只支持 DER,而第二个工具支持非常广泛,然而大部分功能都是收费的。

而证书的其中一个常见格式就 DER,或许大家可能没见过这种格式的证书,也可能见过,但是打开之后发现都是“乱码”,误以为可能是加密过的之类的,但其实它就是这么一种二进制的格式。

像这种二进制的格式,在传输上总会有一些不便的地方。于是证书还有另一种常见的格式,即 PEM

PEM

PEM 全称是 Privacy-Enhanced Mail,它现在已经成为了一种储存各种密钥、证书等的事实标准。之所以称为事实标准,是因为它原本设计出来并不是做这个的,是给 PGPS/MIME 等服务的。然而后者最终没有采用这种方案。

由于上面提到的 DER 传输和使用问题,PEM 采用了 base64 的方案。简单的来说,我们将一个 DER 格式的二进制进行 base64 编码,然后附加上头和结尾,即生成一个 PEM 文件。

而头和尾就是类似:

-----BEGIN PRIVATE KEY-----

-----END PRIVATE KEY-----

其中-----中间的文字就是用来指示其中储存的具体类型,它是一个证书,还是一个私钥,又或者是一个证书请求文件。

那么它的格式标准的定义差不多是这样的,可以在 RFC7468 中查看

textualmsg = preeb *WSP eol
             *eolWSP
             base64text
             posteb *WSP [eol]

preeb      = "-----BEGIN " label "-----"   ; unlike [RFC1421] (A)BNF,
                                           ; eol is not required (but
posteb     = "-----END " label "-----"     ; see [RFC1421], Section 4.4)

一个典型的 PEM 格式证书内容可能是这样的:

-----BEGIN CERTIFICATE-----
MIIFNzCCBNygAwIBAgIQAmqyUtzsfDRp4nmTEJIfAjAKBggqhkjOPQQDAjBKMQsw
CQYDVQQGEwJVUzEZMBcGA1UEChMQQ2xvdWRmbGFyZSwgSW5jLjEgMB4GA1UEAxMX
.....
kW857zAKBggqhkjOPQQDAgNJADBGAiEA5Kjlfd/bn3kuK47LXVNaqbhofcw+avXz
nIJHTgY/eTkCIQCMmdP4k8lljdyAv3kmMAkgiAjKez+Dz2yzOWKh64fQQQ==
-----END CERTIFICATE-----

我们可以利用 openssl 工具来理解 PEMDER 文件。

首先任意准备一个证书文件,例如可以利用 chrome 浏览器的证书导出功能,将一个网站的证书储存起来,储存格式就选择默认的单一证书PEM类型。

导出证书

然后我们尝试将他转换成 DER 格式:

openssl x509 -in sni.cloudflaressl.pem -outform der -out sni.cloudflaressl.der

然后把储存好的新的 DER 格式的证书用 base64 进行编码:

base64 sni.cloudflaressl.der

当我们比对 base64 的结果和之前 PEM 格式中间部分时,就会发现的确是一样的内容。

PEM or DER

不管是 PEM 也好还是 DER,都是一种容器,它里面的内容可以是不同的。就好比 zip 格式是压缩包,但它的压缩算法可以的 deflate,也可以是 bzip2,或者是 lzma。

X509 证书

在了解完前面的几个小知识之后,终于来到了今天的主题,X509。

X509是定义了公钥证书格式的一个标准,其中重点是证书,而不是公钥。毕竟公钥没什么好说的,对于加密算法来说,无非就是一些数字。而证书之所以叫做证书,它包含了例如ID、签发者、签名、有效期之类的各种信息。这些证书现在广泛的应用在各种场景,常见的比如 HTTPS 证书,代码签名证书等。

只要了解了 X509 协议中是如何规定证书储存的,自然而然就会了解我们开头的问题,即证书文件里面有什么,证书是怎么签名的,是如何验证一个证书是谁颁发的。

X509 证书可以说有两大组成部分,基础证书字段和证书拓展。

我们首先来看看证书的 ASN.1 是怎么定义的,从 RFC5280 中摘抄。

Certificate  ::=  SEQUENCE  {
        tbsCertificate       TBSCertificate,
        signatureAlgorithm   AlgorithmIdentifier,
        signatureValue       BIT STRING  }

TBSCertificate  ::=  SEQUENCE  {
        version         [0]  EXPLICIT Version DEFAULT v1,
        serialNumber         CertificateSerialNumber,
        signature            AlgorithmIdentifier,
        issuer               Name,
        validity             Validity,
        subject              Name,
        subjectPublicKeyInfo SubjectPublicKeyInfo,
        issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3
        subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                             -- If present, version MUST be v2 or v3
        extensions      [3]  EXPLICIT Extensions OPTIONAL
                             -- If present, version MUST be v3
        }

Validity ::= SEQUENCE {
        notBefore      Time,
        notAfter       Time }

SubjectPublicKeyInfo  ::=  SEQUENCE  {
        algorithm            AlgorithmIdentifier,
        subjectPublicKey     BIT STRING  }

Extensions  ::=  SEQUENCE SIZE (1..MAX) OF Extension

Extension  ::=  SEQUENCE  {
        extnID      OBJECT IDENTIFIER,
        critical    BOOLEAN DEFAULT FALSE,
        extnValue   OCTET STRING
                    -- contains the DER encoding of an ASN.1 value
                    -- corresponding to the extension type identified
                    -- by extnID
        }

小提示

上面的描述文件进行了一定程度的精简,删除了一些一眼就能看出作用和无关紧要的类型声明。例如 Version 是数字 0/1/2 这种不重要的声明。

这个描述看起来非常的复杂,但仔细观察第一部分,一个证书有三大部分:

  • 证书 – tbsCertificate
  • 签名算法 – signatureAlgorithm
  • 签名 – signatureValue

首先来看证书部分的内容。

tbsCertificate

这部分虽然看起来很复杂,一层嵌套一层,但其实只从表面来看还是非常好理解的。

这里面包含了之前提到的一些基础信息,例如版本,序列号,颁发者,主体之类的信息。这些信息实际上并不会很严格的参与到验证证书签名的步骤。毕竟你不可能在颁发者里面填上“Google”,这个证书就真的是Google颁发的。但其中的例如 Subject 的部分可能会因不同应用而有不同的验证方法。

首先版本就不多说了,虽然版本也是重要信息,但是并不影响我们理解证书本身。

接下来就是序列号,一个CA签发的证书里面,每个证书的序列号必须是唯一的,至于序列号怎么生成,就没有什么关系了,这当然是为了辨别每个证书的。

接下来的 signature 字段并不是签名内容,而是签名算法,这个签名算法和外层的签名算法作用是一样的,都是用来指示当CA签发证书的时候,用的是什么签名算法。

根据密钥类型的不同,例如有 RSA, ECDSA 等密钥,所采用的签名算法也是不一样的。

然后就是 issuer 属性了,这个就是签发者,它的值一般来说就是父证书,或者CA证书的 subject 属性,而不管是 issuer 也好还是 subject,它只是一个名字而已。这意味着名字是可以重复的,因此它不能作为判定父证书的依据。例如在上面导出过的证书,它的 issuer 是如下内容:

CN = Cloudflare Inc ECC CA-3
O = Cloudflare, Inc.
C = US

validity 属性则是关于时间的属性,说他的保质期也不为过。一个证书为了安全性,一般不会设置成永不过期。而较短的过期时间又会造成频繁的更换新证书,从而带来一定的不便利。

通常情况下,一个CA根证书的过期时间可能是二十年,或者三十年这种设定。这时候我们很难保证在这种时间跨度上,CA证书的私钥会不会丢失、泄露等。因为CA证书总是要办发新的证书的,每天签发次数可能几千几万都会有可能,这意味着它的私钥需要时刻保持在线。

为了解决这个问题,普遍的方案是,由CA根证书来签发一个中间证书,它的有效期就比较短了,比如五年这种设定,CA证书离线储存,所有的子证书都由中间证书来签发,形成一个证书链的关系。这样就算中间证书丢失或者泄露,只需要吊销中间证书再重新签发一个中间证书即可,CA根证书的在线时间大大缩短。

那么在这里插一段题外话,或多或少大家都用过 Let's Encrypt 的证书。在早期也就是大概2015年刚起步的时候,配置一个 Let's 证书时,除了签好的证书之外,还需要配置一个中间证书。如果不配置这个中间证书的话,在一些系统上就会出现证书不被信任的问题。

这是因为CA根证书都会在系统里面预装,以及一些中间证书,方便我们的系统验证完整的证书链。而 Let's 证书作为刚起步,没有办法做到根证书的等级,只能借助于中间证书的帮助,来实现签发新的证书。而这个新的中间证书没有在系统里面预装,因此在验证过程中,一个链条上少了一环,自然没有办法验证。

小提示

现在的 Let's Encrypt 证书也可以不单独配置中间证书,但同样为了兼容性 问题还是用 fullchain.cer 证书即可。

说回证书的结构,subject 属性和 issuer 属性的类型是一样的,都是名字。subject 则是当前证书的主体,它声明了这个证书归属于谁。对于 HTTPS 场景用到的证书来说,subject 当中会包含网站的域名。例如之前证书的 subject 内容是:

CN = sni.cloudflaressl.com
O = Cloudflare, Inc.
L = San Francisco
ST = California
C = US

在这里 CN 的全称是 common name,其内容就是网站域名。浏览器在收到证书之后,会检查 CN 的内容是否和当前正在访问的域名一致,如果不一致,则说明服务端发来的证书有问题。就好比有人向你出示身份证,上面写着男,照片也是男性,而面前的人却是实实在在的女性。

有了这些内容还不够,证书除了它本身,还必须要有配套的私钥。既然有私钥的存在,那一定有公钥。于是 subjectPublicKeyInfo 属性就是用来储存对应的公钥信息的。包含了公钥类型,它是 RSA 还是 ECDSA 等,以及公钥本身。

换句话说,一个证书对应一个唯一的公钥和私钥,而一个公钥可以用来生成多个证书。当你的证书过期后,重新签发的时候并不需要连私钥一起更换,而是只需要重新用你的公钥请求CA来生成新的证书即可。

接下来的两个 issuerUniqueIdsubjectUniqueId 是可选项,也确实没怎么见到有证书用到了,就不做解释。

而同样是可选的 extensions 部分则是非常重要的,它不光在验证证书链的时候起到作用,还同时在 HTTPS 中域名验证时起到辅助作用。

extensions

所谓拓展,就是对原本的功能进行增加,让他具有新的功能。X509 的拓展也是同样的意义,它实现了仅靠上面简单的属性无法做到的功能。

拓展部分就是由一个一个拓展组成的数组,每一个拓展至多只能出现一次,在不需要的时候也可以不出现。

拓展拥有很多种,那么这里只介绍一些起到关键性作用的几个拓展。

Subject Key Identifier

这一拓展起到识别一个包含公钥的证书的目的,对于所有的CA证书来说,为了正确的构建证书链,这一拓展是必须有的。

如果两个证书的这一属性相同,那就代表这两个证书的公钥是相同的,但证书本身可能是不同的,例如过期时间可能不一样,序列号可能不一样,甚至可能不是同一个CA签发的。

这个标识的生成方法一般来说是根据公钥的哈希值来计算的,但并不是强制要求的。

Authority Key Identifier

这一拓展和上面的类似,不同点在于,这个 ID 起到的是识别签发本证书的父证书的公钥的目的。也就是说这个 ID 是父证书的 Subject Key Identifier

同时规定所有有CA证书签发的子证书必须有这一拓展种的 keyIdentifier 属性,这也是为了验证证书路径所作的规定。

但是不光如此,它的完整定义如下

AuthorityKeyIdentifier ::= SEQUENCE {
      keyIdentifier             [0] KeyIdentifier           OPTIONAL,
      authorityCertIssuer       [1] GeneralNames            OPTIONAL,
      authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL  }

也就是还可能会包含另外的信息,根据字面意思,父证书的的签发者,和父证书的序列号。实际上不愧是是可选参数,目前也是基本没有见过后两个的,大部分情况只有一个 keyIdentifier

Key Usage

就如字面意思,这个拓展描述的是本证书的用途,它更像是一种约束。它的类型算是 bitmap,也就是可以拥有多种用途。有如下几种

  • digitalSignature
  • nonRepudiation – 新版本改名成 contentCommitment
  • keyEncipherment
  • dataEncipherment
  • keyAgreement
  • keyCertSign
  • cRLSign
  • encipherOnly
  • decipherOnly

这些用途就没有必要一一解释,只需要大概了解下证书是可以限制用途的即可。

例如如果拥有digitalSignature 用途,代表此证书的公钥可以用来验证数字签名,而不是验证证书的签名或者CRL的签名。

再比如 keyEncipherment,代表此证书的公钥可以用来在传输密钥的过程中对密钥进行加密。例如证书公钥类型是 RSA 时,这个证书包含的公钥可以用来进行非对称加密文件等。

Subject Alternative Name

这个拓展是用来描述额外的 subject的,常见的用途就是多域名证书。由于 subject 里面只能有一个 CN,当我们需要有多个域名的时候,就需要写在这一拓展中。

例如

DNS 名称: sni.cloudflaressl.com
DNS 名称: snowstar.org
DNS 名称: *.snowstar.org

这在 CDN 中非常常见,一个证书里面可能有几十上百个 DNS=xxxx 的内容。当浏览器验证证书时,只要其中有一个值匹配到当前域名,那么这个证书在 subject 的验证中就算通关了。

Basic Constraints

这一拓展是区分是否是 CA 的关键,也就是当前证书能否用来签发新的子证书。它的定义如下

BasicConstraints ::= SEQUENCE {
        cA                      BOOLEAN DEFAULT FALSE,
        pathLenConstraint       INTEGER (0..MAX) OPTIONAL }

对于我们普通用户申请到的网站证书来讲,全部都是CA:FALSE的,代表我们的这个证书在证书路径上走到头了,它不能用来签发新的证书。

同时还有一个路径长度的约束,它的作用则是用来限制中间能够有多少中间证书,也可以是无限制。

证书的签名

在上面这么多内容之后,我们就可以理解证书其实是具有一定格式的文件,每一部分都有它的作用,详细描述了这个证书的各方面属性。

除此之外还需要最重要的一个部分,那就是签名。

签名分成两种情况,一种是自签名证书,另一种就是由父证书给子证书进行签名。

自签名常见在CA根证书上,根证书作为最顶层的证书,自然没有父证书这一说法,没有人能给他签名,因此只能自己给自己签。

又或者是开发途中需要用一个证书做内部测试,不需要考虑信任问题,我们也是可以自己签一个证书做测试。当然这个证书是不会被任何人信任的。

另一种就是当我们申请证书的时候,CA证书作为父证书,给我们的子证书签名。签名起到一个信任作用,即CA证书认可子证书的内容,给这个子证书打包票。因此CA证书在签名时,要起到审查证书申请内容的工作。

否则如果我将 subject 设置成 CN=google.com,而CA证书不检查我是否真的拥有 google.com 域名的所有权就颁发了证书。那我就可以光明正大的进行中间人攻击,所有人看到证书都会认为我的确拥有 google.com 域名的所有权。

那么签名的具体步骤是什么。根据前面的知识,我们将这一系列属性填入 tbsCertificate 对象中,然后按照 DER 编码规则对其编码,就可以得到一串二进制数据,签名就是对这一串二进制进行的。

例如常见的 sha256WithRSAEncryption 来说,首先将编码后的二进制数据进行 sha256 摘要计算,得到它的哈希值,然后再使用对应的私钥对这个哈希值签名。

在得到签名值之后,我们再将 tbsCertificatesignatureAlgorithmsignatureValue 三者组成真正的 Certificate 对象,并再一次根据 DER 编码规则生成最终的证书文件。

PEM 如何来的

还是那个原因,DER 作为二进制格式,并不适用于在文本环境中传输。因此我们大多时候收到的证书文件都是先编码成 DER,再转换成 PEM的。

验证证书

好了,现在可以回答一开始的问题,证书是如何验证的。

足够聪明的你应该已经得到了答案。

父证书和子证书之间的关联,在上面的内容中有这么几种:

  • Issuer
  • Authority Key Identifier

假如我们的子证书C没有 Authority Key Identifier,那么在验证过程中,根据子证书C里面的 Issuer 属性,查找系统中受信任的根证书中,有没有那么一个证书F,它的 Subject 匹配这个 Issuer,那么我们就用这个证书F里面的 subjectPublicKeyInfo 中储存的公钥信息,对子证书C里面包含的签名验证,检查这个签名是否是由证书F生成的。

而如果我们的子证书C包含 Authority Key Identifier 拓展,那么同样的道理,只需要找到一个证书F,它的Subject Key Identifier 与之匹配,并且签名验证也通关,那么就可以认定从属关系。此时 Issuer 即使不匹配,那么在Windows平台上,也无关紧要了。

如果刚好我们的子证书C没有 Authority Key Identifier 拓展,并且它的 Issuer 没有匹配到任何一个证书,即使在系统中的确存在一个证书G,C的签名的确是可以由G的公钥验证通关,那么一般来说也会认为这个子证书C和证书G没有关系,因而不会被信任。

实际更复杂

在实际情况下证书的验证要更复杂,不光要验证签名上是否正确,时间是否过期,证书用途是否正确,有没有被吊销等。

实地操作下

在上面纸上谈兵了这么久,或许还是会不太明白,还是直接实践一下会更简单。

依旧以上面导出的证书为材料,我们可以借助 openssl 工具来解析证书内容。

openssl x509 -in sni.cloudflaressl.pem -text -noout

可以看到类似这样的输出

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            02:6a:b2:52:dc:ec:7c:34:69:e2:79:93:10:92:1f:02
        Signature Algorithm: ecdsa-with-SHA256
        Issuer: C = US, O = "Cloudflare, Inc.", CN = Cloudflare Inc ECC CA-3
        Validity
            Not Before: Jun 12 00:00:00 2022 GMT
            Not After : Jun 12 23:59:59 2023 GMT
        Subject: C = US, ST = California, L = San Francisco, O = "Cloudflare, Inc.", CN = sni.cloudflaressl.com
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:56:5a:53:49:11:e5:e1:e4:2f:45:37:c1:bb:15:
                    a7:3b:8c:65:a7:5e:ce:a0:1f:2c:e4:37:99:78:d8:
                    19:21:e8:ba:81:4c:28:53:cc:95:dc:fc:2a:47:0d:
                    f1:c2:79:c5:68:8b:b4:c9:b7:99:e5:74:67:dc:07:
                    12:d9:20:e9:19
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Authority Key Identifier:
                keyid:A5:CE:37:EA:EB:B0:75:0E:94:67:88:B4:45:FA:D9:24:10:87:96:1F

            X509v3 Subject Key Identifier:
                D7:8D:C3:7E:21:6A:FA:F4:DF:EE:7B:5D:62:87:A4:5E:C7:20:61:2D
            X509v3 Subject Alternative Name:
                DNS:sni.cloudflaressl.com, DNS:snowstar.org, DNS:*.snowstar.org
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl3.digicert.com/CloudflareIncECCCA-3.crl

                Full Name:
                  URI:http://crl4.digicert.com/CloudflareIncECCCA-3.crl

            X509v3 Certificate Policies:
                Policy: 2.23.140.1.2.2
                  CPS: http://www.digicert.com/CPS

            Authority Information Access:
                OCSP - URI:http://ocsp.digicert.com
                CA Issuers - URI:http://cacerts.digicert.com/CloudflareIncECCCA-3.crt

            X509v3 Basic Constraints: critical
                CA:FALSE
            CT Precertificate SCTs:
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : E8:3E:D0:DA:3E:F5:06:35:32:E7:57:28:BC:89:6B:C9:
                                03:D3:CB:D1:11:6B:EC:EB:69:E1:77:7D:6D:06:BD:6E
                    Timestamp : Jun 12 01:50:16.534 2022 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:46:02:21:00:93:81:90:82:98:B1:07:3B:99:E4:B7:
                                CE:1F:60:C3:DF:D5:08:37:47:15:B3:2D:03:F9:75:1B:
                                AA:38:38:E5:AA:02:21:00:A4:3E:52:27:C0:1E:A5:1A:
                                B0:DB:14:99:9A:FA:C8:D5:74:5C:43:9B:1E:2B:3C:97:
                                8D:51:5C:47:20:89:18:D5
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : 35:CF:19:1B:BF:B1:6C:57:BF:0F:AD:4C:6D:42:CB:BB:
                                B6:27:20:26:51:EA:3F:E1:2A:EF:A8:03:C3:3B:D6:4C
                    Timestamp : Jun 12 01:50:16.549 2022 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:44:02:20:73:92:A7:D6:B5:5B:9C:6D:58:F3:B9:2F:
                                88:59:CC:CF:8B:A5:17:5E:C7:CC:9A:C8:1A:E7:69:D1:
                                9B:52:80:33:02:20:04:6D:EA:F6:E1:84:9B:87:95:D5:
                                31:CC:20:55:6D:9F:3C:AC:F6:5A:3D:77:AD:2F:02:35:
                                88:2E:3A:AE:96:09
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : B3:73:77:07:E1:84:50:F8:63:86:D6:05:A9:DC:11:09:
                                4A:79:2D:B1:67:0C:0B:87:DC:F0:03:0E:79:36:A5:9A
                    Timestamp : Jun 12 01:50:16.595 2022 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:44:02:20:62:8B:AA:5F:A4:1B:05:A6:9B:CE:00:E7:
                                8D:58:27:10:65:15:DB:DC:55:43:60:88:16:CD:A0:0E:
                                35:44:03:36:02:20:6D:39:22:DD:FF:49:8A:06:79:98:
                                08:03:EE:F7:E1:6C:6B:FA:FB:C8:EE:C3:B1:C8:BA:41:
                                16:6A:91:6F:39:EF
    Signature Algorithm: ecdsa-with-SHA256
         30:46:02:21:00:e4:a8:e5:7d:df:db:9f:79:2e:2b:8e:cb:5d:
         53:5a:a9:b8:68:7d:cc:3e:6a:f5:f3:9c:82:47:4e:06:3f:79:
         39:02:21:00:8c:99:d3:f8:93:c9:65:8d:dc:80:bf:79:26:30:
         09:20:88:08:ca:7b:3f:83:cf:6c:b3:39:62:a1:eb:87:d0:41

在输出中,可以看到就像上面讲到的,有 subjectissuer,以及各种拓展。为了验证 Authority Key Identifier的关系,我们再把这个证书的父证书也导出以下,一起用 openssl 检查下。

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            0a:37:87:64:5e:5f:b4:8c:22:4e:fd:1b:ed:14:0c:3c
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = IE, O = Baltimore, OU = CyberTrust, CN = Baltimore CyberTrust Root
        Validity
            Not Before: Jan 27 12:48:08 2020 GMT
            Not After : Dec 31 23:59:59 2024 GMT
        Subject: C = US, O = "Cloudflare, Inc.", CN = Cloudflare Inc ECC CA-3
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:b9:ad:4d:66:99:14:0b:46:ec:1f:81:d1:2a:50:
                    1e:9d:03:15:2f:34:12:7d:2d:96:b8:88:38:9b:85:
                    5f:8f:bf:bb:4d:ef:61:46:c4:c9:73:d4:24:4f:e0:
                    ee:1c:ce:6c:b3:51:71:2f:6a:ee:4c:05:09:77:d3:
                    72:62:a4:9b:d7
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                A5:CE:37:EA:EB:B0:75:0E:94:67:88:B4:45:FA:D9:24:10:87:96:1F
            X509v3 Authority Key Identifier:
                keyid:E5:9D:59:30:82:47:58:CC:AC:FA:08:54:36:86:7B:3A:B5:04:4D:F0

            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
            X509v3 Extended Key Usage:
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
            Authority Information Access:
                OCSP - URI:http://ocsp.digicert.com

            X509v3 CRL Distribution Points:

                Full Name:
                  URI:http://crl3.digicert.com/Omniroot2025.crl

            X509v3 Certificate Policies:
                Policy: 2.16.840.1.114412.1.1
                  CPS: https://www.digicert.com/CPS
                Policy: 2.16.840.1.114412.1.2
                Policy: 2.23.140.1.2.1
                Policy: 2.23.140.1.2.2
                Policy: 2.23.140.1.2.3

    Signature Algorithm: sha256WithRSAEncryption
         05:24:1d:dd:1b:b0:2a:eb:98:d6:85:e3:39:4d:5e:6b:57:9d:
         82:57:fc:eb:e8:31:a2:57:90:65:05:be:16:44:38:5a:77:02:
         b9:cf:10:42:c6:e1:92:a4:e3:45:27:f8:00:47:2c:68:a8:56:
         99:53:54:8f:ad:9e:40:c1:d0:0f:b6:d7:0d:0b:38:48:6c:50:
         2c:49:90:06:5b:64:1d:8b:cc:48:30:2e:de:08:e2:9b:49:22:
         c0:92:0c:11:5e:96:92:94:d5:fc:20:dc:56:6c:e5:92:93:bf:
         7a:1c:c0:37:e3:85:49:15:fa:2b:e1:74:39:18:0f:b7:da:f3:
         a2:57:58:60:4f:cc:8e:94:00:fc:46:7b:34:31:3e:4d:47:82:
         81:3a:cb:f4:89:5d:0e:ef:4d:0d:6e:9c:1b:82:24:dd:32:25:
         5d:11:78:51:10:3d:a0:35:23:04:2f:65:6f:9c:c1:d1:43:d7:
         d0:1e:f3:31:67:59:27:dd:6b:d2:75:09:93:11:24:24:14:cf:
         29:be:e6:23:c3:b8:8f:72:3f:e9:07:c8:24:44:53:7a:b3:b9:
         61:65:a1:4c:0e:c6:48:00:c9:75:63:05:87:70:45:52:83:d3:
         95:9d:45:ea:f0:e8:31:1d:7e:09:1f:0a:fe:3e:dd:aa:3c:5e:
         74:d2:ac:b1

如此可见,这两个 ID 都是 A5:CE:37:EA:EB:B0:75:0E:94:67:88:B4:45:FA:D9:24:10:87:96:1F

用代码来生成证书

说了这么多文字内容后,还是来试试自己生成一个证书更好理解以下。但是从头到尾实现 X509 和 DER 的确不太现实,因此直接使用 openssl 的库来帮助就很轻松了。

那么在代码方面直接就用熟悉的 rust,不了解的也没关系,就当作伪代码来看就好,唯一可能会有问题的就是 ? 操作符,这是和错误处理有关的。

生成 CA 证书

当然第一步首先是生成一个私钥,例如 RSA 类型的私钥

let pkey = Rsa::generate(4096)?;
let pkey = PKey::from_rsa(pkey)?;

我们的 CA 证书还需要有一个用于 subjectissuer 的名字,简单的设置一个 CN

let mut name = X509NameBuilder::new()?;
name.append_entry_by_text("CN", "snowstar")?;
let name = name.build();

以及一个序列号,序列号生成方法就简单的按照随机数来,不考虑冲突什么的

let serial = BigNum::from_u32(rand::random())?;
let serial = serial.to_asn1_integer()?;

这些准备好之后,可以开始构建 X509 证书本体

let mut x509 = X509::builder()?;
x509.set_version(2)?;
x509.set_subject_name(&name)?;
x509.set_issuer_name(&name)?;
x509.set_not_before(&*Asn1Time::days_from_now(0)?)?;
x509.set_not_after(&*Asn1Time::days_from_now(3650)?)?;
x509.set_serial_number(&serial)?;
x509.set_pubkey(&rsa)?;

在上述代码片段中,我们只是设置了一些在之前提到的基本属性,版本号这里虽然是 2,但他其实是 版本3,想不到吧。

接着需要设置一些拓展

首先是基本拓展(Basic Constraints),并且设置 CA:TRUE

let bc = BasicConstraints::new().ca().build()?;
x509.append_extension(bc)?;

然后就是用途拓展(Key Usage),由于是 CA 证书,他需要签发子证书,因此需要给他加上 keyCertSign 的用途

let usage = KeyUsage::new()
    .digital_signature()
    .crl_sign()
    .key_cert_sign()
    .build()?;
x509.append_extension(usage)?;

最后就是两个 identifier,首先需要生成 SubjectKeyIdentifier,用来标识这个证书。由于生成的 CA 证书属于自签名的,于是它的 AuthorityKeyIdentifier 就是它自己。体现在代码中就是,在生成好前者后,context 里面已经有了 SubjectKeyIdentifier的信息,于是再后面添加 AuthorityKeyIdentifier 的时候就可以自动设置好。

let sid = SubjectKeyIdentifier::new().build(&x509.x509v3_context(None, None))?;
x509.append_extension(sid)?;

let aid = AuthorityKeyIdentifier::new().keyid(true).build(&x509.x509v3_context(None, None))?;
x509.append_extension(aid)?;

完成这些设置之后,最后步骤当然就是签名并保存

x509.sign(&pkey, MessageDigest::sha256())?;
let x509 = x509.build();
std::fs::write("ca.crt", x509.to_pem()?)?;
std::fs::write("ca.key", pkey.private_key_to_pem_pkcs8()?)?;

检查一下生成好的证书

自签名 CA 证书

生成子证书

有了 CA 证书后,就可以开始生成子证书,在流程上基本上是一样的。

x509.set_issuer_name(&ca.subject_name())?;

在设置 issuer 的时候,设置的就是之前 CA 证书的 subject

同时基础约束也不能设置 CA:TRUE

let bc = BasicConstraints::new().build()?;
x509.append_extension(bc)?;

证书用途也有所不同

let usage = KeyUsage::new().key_encipherment().digital_signature().build()?;
x509.append_extension(usage)?;

当然 identifier 也会有一些不一样,context 当中包含了 CA 证书的信息,因此 AuthorityKeyIdentifier 就是 CA 证书的 SubjectKeyIdentifier 了。

let ctx = x509.x509v3_context(Some(&ca), None);

let aid = AuthorityKeyIdentifier::new().keyid(true).build(&ctx)?;
let sid = SubjectKeyIdentifier::new().build(&ctx)?;

最后在签名的时候,与 CA 不同的是,签名用的私钥不是子证书的私钥,而是 CA 证书的私钥。

x509.sign(&ca_key, MessageDigest::sha256())?;

最后检查下生成好的子证书,没有什么问题。

PKCS8 和 PKCS12

上面分析了 X509 证书的格式,除此之外,有时候还会遇到类似 PKCS8 和 PKCS12 的格式。与 X509 类似的,PKCS8 是储存私钥的格式。

非对称加密算法有很多,比如常见的 RSA,ECDSA,Ed25519 之类的,而他们的私钥组成也是不一样的。

例如 RSA 来说,它的私钥有 n,e,d,p,q等一系列参数。而对于 ECDSA 来说,私钥是由椭圆曲线上的点组成的,长度要短很多。

为了将不同的私钥能装入同一种格式,于是有了 PKCS8 标准。

PKCS8 早期的定义是这样的

PrivateKeyInfo ::= SEQUENCE {
        version                   Version,
        privateKeyAlgorithm       PrivateKeyAlgorithmIdentifier,
        privateKey                PrivateKey,
        attributes           [0]  IMPLICIT Attributes OPTIONAL }

Version ::= INTEGER

PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier

PrivateKey ::= OCTET STRING

Attributes ::= SET OF Attribute

也就是通过一个 PrivateKeyAlgorithmIdentifier 来标识这个私钥里面具体是什么格式,接下来才是实际的私钥内容。

可以通过 openssl 来查看它的结构。首先生成一个 RSA 私钥

openssl genrsa -out rsa.pem

然后解析一下它的内容

openssl asn1parse -in rsa.pem

这时候可以看到它的内容是这样的

    0:d=0  hl=4 l=1189 cons: SEQUENCE
    4:d=1  hl=2 l=   1 prim: INTEGER           :00
    7:d=1  hl=4 l= 257 prim: INTEGER           :CE80FF52C06...
  268:d=1  hl=2 l=   3 prim: INTEGER           :010001
  273:d=1  hl=4 l= 257 prim: INTEGER           :8B77A7F95...
  534:d=1  hl=3 l= 129 prim: INTEGER           :FE531E249...
  666:d=1  hl=3 l= 129 prim: INTEGER           :CFDD3C9...
  798:d=1  hl=3 l= 129 prim: INTEGER           :F45403C4...
  930:d=1  hl=3 l= 128 prim: INTEGER           :712F80...
 1061:d=1  hl=3 l= 129 prim: INTEGER           :8E8E298A...

此时这个私钥还不是 PKCS8 格式的,他只是普普通通的 RSA 私钥而已。

然后将它转换成 PKCS8 格式

openssl pkcs8 -in rsa.pem -topk8 -nocrypt -out rsa.pk8

然后再来检查下内容

openssl asn1parse -in rsa.pk8

可以看到输出的内容已经完全不一样了

 0:d=0  hl=4 l=1215 cons: SEQUENCE
 4:d=1  hl=2 l=   1 prim: INTEGER           :00
 7:d=1  hl=2 l=  13 cons: SEQUENCE
 9:d=2  hl=2 l=   9 prim: OBJECT            :rsaEncryption
20:d=2  hl=2 l=   0 prim: NULL
22:d=1  hl=4 l=1193 prim: OCTET STRING      [HEX DUMP]:308204A5020100....

对比一下 ECDSA 的结构

 0:d=0  hl=3 l= 135 cons: SEQUENCE
 3:d=1  hl=2 l=   1 prim: INTEGER           :00
 6:d=1  hl=2 l=  19 cons: SEQUENCE
 8:d=2  hl=2 l=   7 prim: OBJECT            :id-ecPublicKey
17:d=2  hl=2 l=   8 prim: OBJECT            :prime256v1
27:d=1  hl=2 l= 109 prim: OCTET STRING      [HEX DUMP]:306B02010....

是不是就逐渐理解什么是 PKCS8了。

那么 PKCS12 又是什么呢,其实就是将私钥和公钥打包在一个文件里面的格式。常见的拓展名就是 .pfx.p12 这样的。那具体的格式就不做分析了。

结语

基本上以上就是本文的全部内容了,如果有什么疑问可以在评论区发表看法。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注