在本章中,我们将学习X.509证书(X.509 certificates)。证书是用于身份表示和验证的数据结构。X.509证书对于TLS和基于TLS的协议(如HTTPS)的运行至关重要,在HTTPS中,证书用于证明网站的身份。证书也用于安全消息传递标准,如S/MIME;VPN解决方案,如OpenVPN;智能卡、软件签名等。X.509证书也可用于IPsec。
我们将了解证书的组成、证书验证链的构建方式以及公钥基础设施(Public Key Infrastructure,PKI)的工作原理。在本章的实践部分,我们将学习如何在命令行上生成证书和验证证书链,并使用C代码进行编程。
第八章 X.509证书和PKI技术要求X.509证书是什么?了解证书签名链X.509证书是如何颁发的?X509v3扩展是什么?了解X.509公钥基础架构如何生成自签名证书如何生成非自签名证书如何在命令行上验证证书如何以编程方式验证证书实现x509-verify程序运行x509-verify程序总结
本章将包含可以在命令行上运行的命令以及可以构建和运行的C源代码。对于命令行命令,您将需要带有OpenSSL动态库的 openssl
命令行工具。为了构建C代码,您需要OpenSSL动态或静态库、库头、C编译器和链接器。
我们将在本章中实施一个示例程序,以练习我们正在学习的内容。该程序的完整源代码可以在这里找到:https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/tree/main/Chapter08 。
X.509证书是一种用于身份表示(identity presentation)和身份验证(identity verification)的数据结构。证书可以保存到文件中或通过网络传输,也可以作为安全网络协议(如TLS)的一部分。X.509证书使用数字签名将身份绑定到公钥。
以下是X.509证书的示例文本表示:
xxxxxxxxxx
Certificate:
Data:
Version: 3 (0x2)
Serial Number: ... hex bytes ...
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = Let's Encrypt, CN = R3
Validity
Not Before: Mar 4 12:43:52 2022 GMT
Not After : Jun 2 12:43:51 2022 GMT
Subject: CN = www.openssl.org
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus: ... hex bytes ...
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication,
TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
... hex bytes ...
X509v3 Authority Key Identifier:
... hex bytes ...
... more X509v3 extensions ...
正如我们所观察到的,X.509证书包含以下字段:
Not Before
和 Not After
,定义证书的有效时间。subject
字段也以可分辨名称格式表示。与公钥和私钥类似,X.509证书使用抽象语法表示法一(Abstract Syntax Notation One,ASN.1)编码为可分辨编码规则(Distinguished Encoding Rules,DER)或隐私增强邮件(Privacy-Enhanced Mail,PEM)格式。
X.509证书的 Subject
和 Issuer
字段以DN格式表示。可分辨名称如下:C=US
,O=Let’s Encrypt
,CN=R3
。正如我们所观察到的,DN由键值对组成。这些键有明确的含义,例如,C
表示Country
,O
表示Organization
。最重要的关键字是CN
,意思是Common Name
。它是证书所标识实体的主要名称。它可以是一个网站名称,例如www.openssl.org;某人的名字,如无名氏(John Doe);或者,如果出于技术目的需要证书,则证书本身的名称,例如 Technical certificate
或仅 R3
。
每个X.509证书都有相应的私钥。该私钥不包含在证书中,但与证书中包含的公钥匹配,这意味着该私钥生成的数字签名可以通过证书中的公钥进行验证。该私钥通常被称为证书的私钥。证书的所有者需要证书的私钥来证明他们确实拥有证书,而不仅仅是复制了证书。这种证明可以通过所有者使用私钥对某些数据进行签名,验证方使用从证书中提取的公钥验证签名来建立。
X.509证书通常不是秘密信息,可以像公钥一样自由分发。相反,证书的私钥是只有证书所有者才应该拥有的秘密信息,因为拥有私钥的人可以声称证书所有者的身份,并通过密码证明这种身份。私钥被盗可能导致身份盗用。例如,如果有人窃取了www.openssl.org网站证书的私钥,并进行DNS中毒(DNS poisoning)或 中间人(Man in the Middle,MITM)攻击,他们可能会建立一个虚假的www.openssl.com网站,该网站将通过证书验证。有一些方法可以减轻所述的身份盗用,例如证书吊销列表(Certificate Revocation Lists,CRL)和在线证书状态协议(Online Certificate Status Protocol,OCSP),但攻击者在吊销受损证书之前仍可能有一个攻击时间窗口。这是另一个例子:如果证书的私钥用于对电子邮件进行签名,那么窃取私钥的人将能够代表证书所有者对电子邮件进行签署。
当一些数据由证书的私钥签名时,通常说数据是由证书签名的。证书本身也可以签名。事实上,每个X.509证书都由某个证书签名,这意味着由该证书的私钥签名。该签名证书在DN格式的 Issuer
字段中指定。证书可以用它自己的私钥签名。这样的证书称为自签名证书。自签名证书在 Subject
和 Issuer
字段中具有相同的DN。一个证书也可以由另一个证书签名,另一个认证可以由第三个证书签名等等。在这种情况下,我们有一个证书签署链。
证书签名链(certificate signing chain),也称为证书验证链(certificate verification chain),简称为证书链(certificate chain)或信任链(chain of trust),是一个有序的证书集合,其中每个证书都由集合中的下一个证书签名。当然,除了最后一张证书。最后一个证书是自签名的。
为什么需要证书签名链?为了验证证书的有效性。好奇的读者可能会问,证书的私钥不是解决了这个问题吗?不,没那么容易。当使用X.509证书验证身份时,我们必须验证两个声明:
这类似于你如何用护照来识别自己。您只能使用自己的护照来识别自己,并且您的护照必须有效。
证书验证是如何工作的?为了理解这一点,我们需要了解证书是如何生成的,或者根据X.509术语是如何颁发的。
互联网上的大多数网站证书都是由称为证书颁发机构(Certificate Authorities,CA)的组织颁发的。CA是一个颁发证书并通常收取费用的组织。一些CA,例如Let's Encrypt,免费颁发证书。CA将自己视为值得信赖的第三方(trusted third parties)。其想法是,您信任CA颁发证书,并认为这些CA颁发的证书是有效的。您相信CA会检查从CA订购证书的人的身份,并仅向身份与证书 Subject
字段中写入的身份匹配的人颁发证书。
当你不认识在那里工作的人时,你为什么要信任一些你从未见过、几乎没听说过的CA组织?CA辩称信任是他们的业务;他们不能颁发与主体身份不匹配的流氓证书,否则,没有人会信任CA,他们将破产。因此,你必须信任他们。我个人认为这种论证是有问题的。但你没有太多选择。您(或者更确切地说,您的操作系统或浏览器)要么信任这些CA颁发的证书,要么假装信任它们,要么您无法访问使用这些CA颁发证书的HTTPS网站,这些CA是互联网上大多数网站。这就是现代万维网的工作原理。
为了验证证书,您必须使用受信任的证书构建一个证书签名链。让我们考虑一个流行的网站证书验证案例。在这种情况下,链中的第一个证书将是标识网站的证书。该证书没有签署或颁发任何其他证书。不签署其他证书的证书称为叶子证书(leaf certificates)。网站证书将由链中的第二个证书签名。在大多数情况下,第二个证书不是自签名证书,因此不会完成证书链。既签署其他证书又不是自签名的证书称为中间CA证书(intermediate CA certificates)。以网站证书开始的典型证书链将有一个或两个中间CA证书。在所有中间CA证书之后,链中的最后一个证书将进行自签名。对其他证书进行签名的自签名证书称为根CA证书(root CA certificates)。
证书链看起来像这样:
在互联网上的各种文章中,垂直绘制证书链也很流行,如下所示:
请注意,水平表示看起来像一个链表,第一个元素是要验证的证书,最后一个元素是受信任的证书。这也是OpenSSL表示证书签名链的方式。
在我们的描述和图表中,我们提到了中间CA证书。但为什么需要它们?为什么不直接用根证书签署所有叶子(最终实体)证书呢?这有几个原因:
正如我们刚刚了解到的,证书验证需要证书链,可能包含叶子(最终实体)、中间和根证书。但是,谁来验证这些证书链,验证者如何获得所有证书?任何能够获得所需证书并建立证书签名链的人都可以验证证书。让我们回到网站证书验证的流行案例。在这种情况下,验证证书的是浏览器或其他HTTPS客户端。浏览器在TLS握手期间从服务器获取服务器证书。浏览器如何获取根CA证书?从浏览器证书存储区或操作系统证书存储区。
大多数操作系统都有一个操作系统范围的证书存储区,用于存储证书并将其分为不同类别,例如受信任的CA证书、不受信任的证书或具有私钥的客户端证书。这些证书存储区通过操作系统更新机制定期更新。一些web浏览器也有自己的证书存储,可以作为操作系统证书存储的补充或替代。浏览器存储由相应的浏览器更新,不一定是在发布新的浏览器版本时,但存储更新可以定期下载。
谁决定哪些证书被认为是可信的?维护证书存储的人决定了存储最初是如何填充的,以及更新中包含了对存储的哪些更改(可以是添加和删除)。对于操作系统存储,维护者是操作系统供应商,对于浏览器存储,它是浏览器供应商。操作系统和浏览器用户还可以在存储中添加和删除证书。然而,没有那么多用户有足够的知识、需求和愿望来更改其证书存储的内容。因此,在大多数情况下,证书存储仅包含操作系统或浏览器供应商决定包含在其中的证书。因此,受信任的CA证书列表将由存储供应商为您选择信任的证书组成。
大多数操作系统和浏览器证书存储的内容相同或几乎相同。这些存储由知名CA的根CA证书填充。如果您访问的网站拥有其中一个CA颁发的证书,您的浏览器将能够验证网站证书,并很乐意向您显示网页。但是,如果您访问的是使用非知名CA颁发的证书的业余网站,您的浏览器将无法验证证书,并会向您显示一条警告,这对不懂技术的人来说可能会很可怕。为了避免警告,您必须将直接或通过中间CA证书签署网站证书的CA证书作为可信证书添加到操作系统或浏览器证书存储中。对于一个不懂技术的人来说,这项任务可能很困难。因此,互联网上的大多数HTTPS站点都有由知名CA颁发的证书,这些CA的根CA证书存在于操作系统或浏览器证书存储中,被认为是可信的。对于小型业余网站来说,为网站证书付费可能会带来太大的负担。幸运的是,自2015年以来,可以从非商业的Let's Encrypt CA获得免费的站点证书。一些大型商业CA有时也会颁发免费的HTTPS证书,通常是作为试用。
好奇的读者可能已经注意到,证书存储区有一个不受信任的CA证书的特殊类别。为什么需要它?答案很简单。不受信任的CA证书是中间CA证书,可用于构建从网站证书到受信任CA证书的证书签名链。然而,操作系统或浏览器证书存储中包含不受信任的CA证书的情况非常罕见。那么浏览器如何获得所需的中间证书呢?通常,web服务器会将中间CA证书与站点证书一起发送。
在验证证书时,如何构建证书签名链?很明显,链中的第一个证书将是要验证的证书,例如网站证书。但是,您如何决定接下来将哪个证书放入链中?可以使用两种方法查找下一个证书:
Issuer
字段,并在 Subject
字段中查找具有相同DN的证书。Issuer
字段始终存在于证书中,但有时可以找到具有相同主题的多个证书。因此,按 Issuer
字段搜索是不明确的。因此,由于歧义,有时为了构建有效的证书签名链,有必要分析几个证书签名路径。如果至少有一条路径允许我们构建一个通往受信任证书的证书链,并且满足所有约束,如有效期和X509v3扩展所施加的约束,那么我们可以认为该证书已成功验证。OpenSSL在构建证书签名链时支持对多个签名路径进行分析。
在大多数情况下,证书链以自签名的根CA证书结束。但是,严格来说,这不是强制性的。OpenSSL支持以非自签名证书结束的证书签名链。然而,使用这种签名链被认为是一种糟糕的做法。
我们现在已经了解了很多关于证书验证的知识。X.509证书是如何颁发的?
X.509证书生成过程由几个阶段组成:
请注意,在此过程中,没有人将私钥暴露给另一方。
让我们来谈谈CA如何在为网站颁发证书的常见情况下验证申请人的身份。有些人认为CA总是对网站及其所有者进行彻底检查,扫描网站是否有恶意软件,亲自访问网站所有者,检查他们的大量文件等等。通常情况并非如此。大多数颁发给网站的证书的身份都是自动在线检查的,网站所有者和CA组织工作人员之间没有任何互动。不检查任何文件,如所有者的护照、网络或DNS托管合同或其他文件。然后检查什么?检查申请人是否控制网站和网站域的DNS。检查是自动的,可以是这样的:
这就是为什么通常的HTTPS证书被称为域验证(Domain-Validated,DV)证书。
这样的DV证书在网站上使用时证明了什么?用户的浏览器连接到证书中指定的域名的网站,如www.openssl.org——不多也不少。DV证书并不断言网站代表特定的组织,如OpenSSL项目。有些人认为,如果一个网站有证书,那么它就是经过认证的,因此是一个好网站,不能代表一个坏组织,不能包含恶意软件,不能攻击其用户,并且该网站上的所有信息都是真实的。不幸的是,事实并非如此。网络钓鱼(phishing)、分发恶意软件(malware)或进行其他犯罪活动的网站也可以拥有有效的HTTPS证书,与这些网站的连接将被浏览器视为“安全”的。重要的是要明白,只有连接是安全的。网站本身可能非常不安全。
为什么DV证书仍然有用?因为它们保护网络浏览器用户免受MITM和DNS中毒攻击。用户可以很有可能确信他们真的访问了他们在浏览器地址行中看到的网站,并且这不是一个模仿原始网站的虚假网站。
是否有证书证明网站代表特定组织,而不仅仅是特定域名?幸运的是,是的。此类证书称为扩展验证(Extended Validation,EV)证书。在颁发EV证书时,CA会对CSR进行更多的检查。CA检查CSR是否来自拥有该域名的组织,CSR Subject
字段中的组织名称是否与真实组织名称匹配,该组织是否存在并在政府组织登记簿中正确注册,该组织的地址和电话号码是否真实,请求证书的人是否有权代表该组织这样做,等等。EV证书仅颁发给组织,而不是个人。
EV证书比DV证书贵得多。例如,在撰写本文时,一家知名的CA以每年8美元的价格出售DV证书,以每年90美元的价格销售EV证书。互联网上的大多数网站都使用DV证书。EV证书由身份特别重要的网站使用,例如银行网站。
已颁发证书的EV状态由X509v3扩展发出信号。遗憾的是,目前还没有用于EV状态指示的标准化X509v3扩展;不同的CA使用不同的X509v3扩展。因此,浏览器必须实现对几个EV指示X509v3扩展的支持。
以前,主要的网络浏览器通过地址栏中的绿色背景和地址栏中显示网站所有者公司的名称来表示网站拥有EV证书。自2019年以来,主要的网络浏览器停止使用这种强烈的视觉指示。不再有绿色条,只有当浏览器用户点击地址栏中的挂锁图标时,才会显示公司名称。大多数其他软件,而不是网络浏览器,从未有过EV证书的强烈视觉指示。
还有组织验证(Organization Validation,OV)和个人验证(Individual Validation,IV)证书。它们是DV和EV证书之间的中间解决方案。颁发此类证书时,CA会验证组织或个人的身份,颁发的证书在证书的 Subject
字段中包含组织或个人名称。与EV证书的不同之处在于,当CA颁发OV或IV证书时,CSR不会像EV那样经过彻底的验证过程——进行的检查更少。OV和IV证书不如EV证书受欢迎。如果一个组织想要比DV更高级别的证书,它通常会选择EV证书而不是OV。
我们已经多次提到X509v3扩展。让我们更多地了解他们。
X509v3扩展是可以添加到X.509证书中的附加字段。X509v3扩展可以对证书使用施加限制,或提供有关证书的其他信息。例如,让我们浏览一下www.openssl.org网站证书中的X509v3扩展:
X509v3扩展不是X.509证书格式的强制性部分,但它们的添加使证书的使用更加方便。此外,一些验证证书的软件,如web浏览器,可能会期望证书中包含某些X509v3扩展,并根据扩展或其缺失更改证书验证结果。
X.509证书很有用,但单独使用它们不会很有用。这些证书是一个更大的基础设施的一部分,称为公钥基础设施。
X.509 PKI 是创建、存储、传输、使用、撤销和管理X.509证书和证书密钥所需的标准、算法、数据结构、软件、硬件、组织和程序的组合。
听起来很复杂,但我们刚刚了解了X.509 PKI在万维网上的工作原理。CA颁发证书,供网站使用并由网络浏览器验证。这就是数百万人每天使用网络自动验证网站身份的方式。一些网站支持使用客户端证书对用户进行身份验证。在这种情况下,不仅网站会提供其证书,用户的网络浏览器还会向网站提供客户端证书,以便网站可以验证客户端证书、验证和授权用户。
X.509 PKI不仅用于网络。X.509证书用于邮件传输、自动化计算机系统的通信、软件签名等。
有些人认为PKI是神秘和难以理解的,但实际上并非如此。它只是管理密钥和身份。
我们现在已经了解了X.509证书、证书签名链、CA和PKI。本章的理论部分已经完成。让我们继续进行实践部分并生成一些证书。
为了生成证书,我们必须生成一个密钥对、一个CSR,最后是一个证书。openssl
工具可以通过多种方式生成自签名证书。其中一种方法是使用单个命令生成密钥对和自签名证书。但我们将使用单独的命令,因为这是一种更通用的方式。
我们将使用以下 openssl
子命令: genpkey
、 pkey
、 req
和 x509
。他们的文档可在以下手册页上找到:
xxxxxxxxxx
$ man openssl-genpkey
$ man openssl-pkey
$ man openssl-req
$ man openssl-x509
以下是生成自签名证书的方法:
首先,让我们生成一个密钥对。这次让我们使用 ED448
作为密钥类型:
xxxxxxxxxx
$ openssl genpkey -algorithm ED448 -out root_keypair.pem
我们没有输出,这意味着我们没有错误。
让我们检查一下新创建的密钥:
xxxxxxxxxx
$ openssl pkey -in root_keypair.pem -noout –text
ED448 Private-Key:
priv:
e2:62:21:f0:32:25:20:ca:84:f9:b8:4f:0a:9f:51:
51:3b:68:d0:0d:3a:91:c9:68:38:b4:2f:d0:53:af:
62:5a:06:9d:b0:f5:86:11:73:f5:be:39:9a:78:be:
ec:a2:53:d8:91:ad:8b:e5:2e:e2:b3:a3
pub:
70:3c:22:d9:9f:f8:d6:76:e0:4f:46:e8:74:7b:5f:
98:98:ee:90:49:af:07:ba:05:a4:3b:b3:2c:e3:20:
1a:00:cf:11:5c:76:93:32:0a:91:14:98:fa:dd:83:
7b:9c:00:46:c8:d3:df:67:23:ea:e1:80
看起来很好。
接下来,让我们生成一个CSR。为了简单起见,我们不会添加太多X509v3扩展。我们将在【第12章“运行迷你CA”】中了解更复杂的证书生成:
xxxxxxxxxx
$ openssl req \
-new \
-subj "/CN=Root CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-key root_keypair.pem \
-out root_csr.pem
请注意,我们添加了 CA:TRUE
扩展名。我们必须为CA证书添加它。
我们没有输出,只有一个新文件 root_csr.pem ,其中包含我们的CSR。让我们检查一下:
xxxxxxxxxx
$ openssl req -in root_csr.pem -noout –text
Certificate Request:
Data:
Version: 1 (0x0)
Subject: CN = Root CA
Subject Public Key Info:
Public Key Algorithm: ED448
ED448 Public-Key:
pub:
... hex bytes ...
Attributes:
Requested Extensions:
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: ED448
Signature Value:
... hex bytes ...
现在,让我们使用CSR和密钥对生成一个自签名证书。我们的新证书的有效期为3650天,约为10年:
xxxxxxxxxx
$ openssl x509 \
-req \
-in root_csr.pem \
-copy_extensions copyall \
-key root_keypair.pem \
-days 3650 \
-out root_cert.pem
Certificate request self-signature ok
subject=CN = Root CA
注意 -copy_extensions copyall
开关。添加该开关很重要,因为默认情况下, openssl x509
命令不会将X509v3扩展从CSR复制到生成的证书中。
我们没有输出【实操在FreeBSD下运行上面的命令,会有两行提示】,只有一个新文件root_cert.pem,其中包含我们新的自签名证书。让我们检查一下:
xxxxxxxxxx
$ openssl x509 -in root_cert.pem -noout –text
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
... hex bytes ...
Signature Algorithm: ED448
Issuer: CN = Root CA
Validity
Not Before: Mar 27 22:25:17 2022 GMT
Not After : Mar 24 22:25:17 2032 GMT
Subject: CN = Root CA
Subject Public Key Info:
Public Key Algorithm: ED448
ED448 Public-Key:
pub:
... hex bytes ...
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
... hex bytes ...
Signature Algorithm: ED448
Signature Value:
... hex bytes ...
请注意,证书中的 Issuer
和 Subject
字段是相同的。这是自签名证书的指示。
我们现在已经成功生成了一个自签名证书,我们将使用它作为根CA证书。现在让我们生成几个非自签名证书。
在生成自签名证书时,我们使用了一种通用方法,而不是长组合命令。因此,非自签名证书的生成将非常相似。我们现在将生成几个非自签名证书。我们将使用一个作为中间CA证书,另一个作为最终实体叶子证书。
让我们继续生成证书:
首先,让我们为中间CA证书生成一个密钥对:
xxxxxxxxxx
$ openssl genpkey \
-algorithm ED448 \
-out intermediate_keypair.pem
接下来,生成CSR:
xxxxxxxxxx
$ openssl req \
-new \
-subj "/CN=Intermediate CA" \
-addext "basicConstraints=critical,CA:TRUE" \
-key intermediate_keypair.pem \
-out intermediate_csr.pem
请注意,我们为中间CA证书使用了不同的 Subject
。
现在,我们将颁发中间CA证书,并使用根证书的私钥对其进行签名:
xxxxxxxxxx
$ openssl x509 \
-req \
-in intermediate_csr.pem \
-copy_extensions copyall \
-CA root_cert.pem \
-CAkey root_keypair.pem \
-days 3650 \
-out intermediate_cert.pem
请注意,我们使用了 -CA
和 -CAkey
开关,而不是 -key
开关。如果使用相应的X509v3扩展名,则需要 -CA
开关将CA证书的 Subject
值复制到已颁发证书的 Issuer
字段中,以及从颁发证书中获取其他必要信息,例如授权密钥标识符(Authority Key Identifier)。
让我们检查一下颁发的中间CA证书:
xxxxxxxxxx
$ openssl x509 -in intermediate_cert.pem -noout –text
Certificate:
Data:
Version: 1 (0x0)
Serial Number:
... hex bytes …
Signature Algorithm: ED448
Issuer: CN = Root CA
Validity
Not Before: Mar 27 23:11:35 2022 GMT
Not After : Mar 24 23:11:35 2032 GMT
Subject: CN = Intermediate CA
Subject Public Key Info:
Public Key Algorithm: ED448
ED448 Public-Key:
pub:
... hex bytes ...
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
... hex bytes ...
X509v3 Authority Key Identifier:
... hex bytes ...
Signature Algorithm: ED448
Signature Value:
... hex bytes ...
请注意,此证书具有不同的 Issuer
和 Subject
字段,表明它不是自签名证书。
现在,让我们颁发一个叶子证书,并用中间CA证书的私钥对其进行签名。我们将按照与颁发中间CA证书非常相似的方式进行操作:
xxxxxxxxxx
$ openssl genpkey -algorithm ED448 -out leaf_keypair.pem
$ openssl req \
-new \
-subj "/CN=Leaf" \
-addext "basicConstraints=critical,CA:FALSE" \
-key leaf_keypair.pem \
-out leaf_csr.pem
$ openssl x509 \
-req \
-in leaf_csr.pem \
-copy_extensions copyall \
-CA intermediate_cert.pem \
-CAkey intermediate_keypair.pem \
-days 3650 \
-out leaf_cert.pem
请注意,这次我们设置 CA:FALSE
而不是 CA:TRUE
,因为叶子证书不应该颁发其他证书。
让我们检查一下生成的叶子证书:
xxxxxxxxxx
$ openssl x509 -in leaf_cert.pem -noout –text
Certificate:
Data:
Version: 1 (0x0)
Serial Number:
... hex bytes ...
Signature Algorithm: ED448
Issuer: CN = Intermediate CA
Validity
Not Before: Mar 27 23:34:19 2022 GMT
Not After : Mar 24 23:34:19 2032 GMT
Subject: CN = Leaf
Subject Public Key Info:
Public Key Algorithm: ED448
ED448 Public-Key:
pub:
... hex bytes ...
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
... hex bytes ...
X509v3 Authority Key Identifier:
... hex bytes ...
Signature Algorithm: ED448
Signature Value:
... hex bytes ...
看起来不错,叶子证书是由中间CA证书颁发的。
现在让我们使用另外两个证书来验证叶子证书。
可以使用 openssl verify
命令在命令行上进行证书验证。其文档可以在 openssl-verify
手册页上找到。
让我们验证一下刚才生成的叶子证书。我们将把根CA证书视为可信证书。我们的中间CA证书将被视为不受信任,但它将帮助我们构建证书签名链。
以下是我们如何在命令行上验证叶子证书:
xxxxxxxxxx
$ openssl verify \
-verbose \
-show_chain \
-trusted root_cert.pem \
-untrusted intermediate_cert.pem \
leaf_cert.pem
leaf_cert.pem: OK
Chain:
depth=0: CN = Leaf (untrusted)
depth=1: CN = Intermediate CA (untrusted)
depth=2: CN = Root CA
注意 -trusted
和 -untrusted
开关。 -trusted
开关用于指定包含一个或多个受信任证书的文件。为了成功验证证书, openssl verify
必须从被验证的证书到受信任的证书构建一个证书签名链。-untrusted
开关用于指定具有一个或多个不可信证书的文件。不受信任的证书可用作证书链中的中间证书。 -trusted
和 -untrusted
开关都可以多次用于指定多个文件。
openssl verify
已报告 OK
,这意味着证书验证已成功。 openssl verify
还打印了它为验证证书而构建的证书链。
我们已在命令行上成功验证了证书。现在让我们学习如何以编程方式验证证书。
OpenSSL 3.0仅提供一个用于证书验证的API。API由以 X509_
前缀开头的函数组成。
我们将开发一个小程序来验证我们刚刚生成的叶子证书,类似于 openssl verify
的方式。
以下是我们将要使用的API的一些相关手册页面:
xxxxxxxxxx
$ man X509_STORE_new
$ man X509_STORE_load_file
$ man DEFINE_STACK_OF
$ man PEM_read_x509
$ man X509_STORE_CTX_new
$ man X509_verify_cert
$ man X509_STORE_CTX_get_error
$ man X509_free
我们的程序将接受三个命令行参数:
我们的高层实施计划如下:
现在是实施计划的时候了。
让我们根据计划开发我们的 x509-verify
程序:
首先,我们必须加载可信证书。我们需要将一组受信任的证书作为 X509_STORE
容器提供给验证函数。幸运的是,OpenSSL提供了一个方便的函数 X509_STORE_load_file()
,用于将证书从PEM文件加载到 X509_STORE
:
xxxxxxxxxx
const char* trusted_cert_fname = argv[1];
X509_STORE* trusted_store = X509_STORE_new();
X509_STORE_load_file(trusted_store, trusted_cert_fname);
接下来,我们必须加载不受信任的证书。我们需要将不受信任的证书作为 STACK_OF(X509)
对象提供给验证函数。X509
是一种OpenSSL类型,代表X.509证书。 STACK_OF
是一个构造OpenSSL堆栈类型的宏。 STACK_OF
宏类似于C++中的 std::stack
模板类型。宏将堆栈元素类型作为参数。OpenSSL在其API和内部代码中大量使用 STACK_OF
堆栈。 STACK_OF(X509)
对象通常用作证书签名链或证书列表。使用堆栈的OpenSSL函数,如 sk_X509_new_null()
和 sk_X509_push()
,也是宏。它们有 sk_
前缀,它们的名称用堆栈元素类型名称参数化。例如,使用 STACK_OF(X509)
的 sk_
函数的名称中有 X509
子字符串。换句话说,这些函数的名称不仅以 sk_
开头,而且以 sk_X509_
开头。由于X509
子字符串在这种情况下只是堆栈函数名中的一个参数,因此您不会找到名为 sk_X509_push
的手册页。但是,您将找到 sk_TYPE_push
手册页,它是 DEFINE_STACK_OF
手册页的别名。
不幸的是,OpenSSL中没有一个方便的函数可以在一次操作中将证书加载到 STACK_OF(X509)
。因此,我们必须使用 PEM_read_X509()
函数逐一加载证书。首先,让我们找出包含不受信任证书的文件的长度:
xxxxxxxxxx
const char* untrusted_cert_fname = argv[2];
FILE* untrusted_cert_file =
fopen(untrusted_cert_fname, "rb");
fseek(untrusted_cert_file, 0, SEEK_END);
long untrusted_cert_file_len = ftell(untrusted_cert_file);
fseek(untrusted_cert_file, 0, SEEK_SET);
然后,让我们将证书逐一加载到 STACK_OF(X509)
结构中:
xxxxxxxxxx
STACK_OF(X509)* untrusted_stack = sk_X509_new_null();
while (ftell(untrusted_cert_file) < untrusted_cert_file_len){
X509* untrusted_cert =
PEM_read_X509(untrusted_cert_file, NULL, NULL, NULL);
sk_X509_push(untrusted_stack, untrusted_cert);
}
我们不能只使用 while (!feof(unsisted_cert_file))
循环条件,因为 feof()
函数仅在试图读取文件末尾以外的内容时检测文件的末尾。如果我们在检测到文件结束条件之前在文件末尾调用 PEM_read_X509()
函数,则 PEM_read_6509()
函数将失败。因此,我们依赖于循环中的文件位置。
加载受信任和不受信任的证书后,是时候加载必须验证的目标证书了。这只是一张证书;因此,我们可以使用一个 PEM_read_X509()
函数调用方便地加载它:
xxxxxxxxxx
const char* target_cert_fname = argv[3];
FILE* target_cert_file = fopen(target_cert_fname, "rb");
X509* target_cert =
PEM_read_X509(target_cert_file, NULL, NULL, NULL);
所有证书都已加载。现在是时候创建和初始化证书验证上下文了,它是 X509_STORE_CT
X类型的对象:
xxxxxxxxxx
X509_STORE_CTX* ctx = X509_STORE_CTX_new();
X509_STORE_CTX_init(
ctx, trusted_store, target_cert, untrusted_stack);
决定性时刻——使用验证上下文进行证书验证:
xxxxxxxxxx
int err = X509_verify_cert(ctx);
如果证书已成功验证,则函数返回 1
。其他返回值表示失败。
如果发生故障,可以使用 X509_STORE_CTX_get_error()
和 X509_verify_cert_error_string()
函数获得确切的错误:
xxxxxxxxxx
if (err != 1) {
int error_code = X509_STORE_CTX_get_error(ctx);
const char* error_string =
X509_verify_cert_error_string(error_code);
fprintf(
stderr,
"X509 verification error: %s\n",
error_string);
}
完成验证后,必须释放正在使用的对象:
xxxxxxxxxx
X509_STORE_CTX_free(ctx);
X509_free(target_cert);
sk_X509_pop_free(untrusted_stack, X509_free);
X509_STORE_free(trusted_store);
我们的 x509-verify
程序的完整源代码可以在GitHub上的 x509-verify.c 文件中找到:
让我们运行 x509-verify
程序并验证叶子证书,使用生成的根CA证书作为可信证书,中间CA证书作为不可信证书:
xxxxxxxxxx
$ ./x509-verify \
root_cert.pem \
intermediate_cert.pem \
leaf_cert.pem
Verification succeeded
正如我们所观察到的,证书验证已经成功,正如我们所料。
让我们来看看一个失败的案例。这一次,我们不会向我们的程序提供中间CA证书,因此它将无法构建指向受信任证书的证书签名链。我们仍需为该计划提供三个论据;因此,我们将只提供两次 leaf_cert.pem
参数:
xxxxxxxxxx
$ ./x509-verify \
root_cert.pem \
leaf_cert.pem \
leaf_cert.pem
X509 verification error: unable to get local issuer certificate
Verification failed
正如预期的那样,我们在这种情况下观察到验证失败。
正如我们所看到的,我们的 x509-verify
程序支持成功和失败的情况。
本节总结了本章的实践部分。让我们继续进行总结。
在本章中,我们了解了X.509证书的概念,为什么需要它们,以及它们包含什么样的信息。我们还了解了证书签名链及其在证书验证中的作用。然后我们了解了CA,以及根CA证书和中间CA证书之间的区别。我们还了解了颁发X.509证书的过程和几种类型的证书,如域验证和EV类型。然后我们学习了X509v3扩展。我们通过学习PKI的概念完成了本章的理论部分。
在本章的实践部分,我们学习了如何生成自签名和非自签名证书。然后,我们学习了如何在命令行和使用C代码编程验证证书。
在下一章中,我们将学习如何设置TLS连接并通过它们发送数据。