打开网易新闻 查看精彩图片

导读:通过 OpenSSL 深入了解密码学的细节:哈希值、数字签名、数字证书等。

本文字数:11502,阅读时长大约: 16分钟

https://linux.cn/article-12511-1.html
作者:Marty Kalin
译者:Xingyu.Wang

通过 OpenSSL 库和命令行实用程序介绍了哈希、加密/解密、数字签名和数字证书。这第二篇文章将对细节进行深入探讨。让我们从计算中无处不在的哈希开始,并考虑是什么使哈希函数具备密码学意义。

密码学哈希

OpenSSL 源代码的包含了一个带有最新版本的表格。每个版本都有两个哈希值(hash):160 位 SHA1 和 256 位 SHA256。这些值可以用来验证下载的文件是否与存储库中的原始文件相匹配:下载者在本地重新计算下载文件的哈希值,然后将结果与原始文件进行比较。现代系统有计算这种哈希值的实用程序。例如,Linux 有 和 。OpenSSL 本身也提供了类似的命令行实用程序。

哈希值被用于计算的许多领域。例如,比特币区块链使用 SHA256 哈希值作为区块标识符。挖比特币就是生成一个低于指定阈值的 SHA256 哈希值,也就是至少有 N 个前导零的哈希值。(N 的值可以上升或下降,这取决于特定时间的挖矿生产力)。作为一个兴趣点,如今的矿机是为并行生成 SHA256 哈希值而设计的硬件集群。在 2018 年的一个高峰期,全球的比特币矿工每秒产生约 7500 万个太哈希值(terahash) —— 这真是一个不可思议的数字。

网络协议也使用哈希值(在这里通常叫做“校验和(checksum)”)来支持消息的完整性;也就是说,保证收到的消息与发送的消息是一样的。消息发送者计算消息的校验和,并将结果与消息一起发送。当消息到达时,接收方重新计算校验和。如果发送的校验和与重新计算的校验和不一致,那么消息在传输过程中可能出现了一些问题,或者发送的校验和出现了问题,或者两者都出现了问题。在这种情况下,应该重新发送消息和它的校验和,或者至少应该触发一个错误情况。(如 UDP 这样的低级网络协议不会理会校验和。)

哈希的其他例子大家都很熟悉。比如一个网站,要求用户用密码进行验证,用户在浏览器中输入密码,然后,他们通过 HTTPS 连接到服务器,密码从浏览器加密发送到服务器。一旦密码到达服务器,就会被解密,然后进行数据库表的查询。

在这个查询表中应该存储什么?存储密码本身是有风险的。风险要小得多的方式是存储一个由密码生成的哈希值,也许在计算哈希值之前“加一些盐(salt)(额外的位)改善口味”。你的密码可能会被发送到 Web 服务器上,但网站可以向你保证,密码不会存储在那里。

哈希值还出现在安全的各个领域。例如,基于哈希值的消息认证码(hash-based message authentication code)()使用一个哈希值和一个秘密的加密密钥(cryptographic key)来认证通过网络发送的消息。HMAC 码轻量级且易于在程序中使用,在 Web 服务中很受欢迎。一个 X509 数字证书包括一个称为指纹(fingerprint)的哈希值,它可以方便证书验证。一个存放于内存中的可信存储(truststore)可以实现为一个以这种指纹为键的查找表 —— 作为一个支持恒定查找时间的哈希映射(hash map)。来自传入的证书的指纹可以与可信存储中的密钥进行比较,以确定是否匹配。

密码学哈希函数(cryptographic hash function)应该具有什么特殊属性?它应该是单向(one-way)的,这意味着很难被逆转。一个加密哈希函数应该是比较容易计算的,但是计算它的反函数(将哈希值映射回输入位串的函数)在计算上应该是困难的。下面是一个描述,用chf作为加密哈希函数,我的密码foobar作为样本输入。

  1. +---+

  2. foobar—>|chf|—>hash value ## 简单直接

  3. +--–+

相比之下,逆向操作是不可行的:

  1. +-----------+

  2. hash value—>|chf inverse|—>foobar ## 棘手困难

  3. +-----------+

例如,回忆一下 SHA256 哈希函数。对于一个任意长度为 N > 0 的输入位串,这个函数会生成一个 256 位的固定长度的哈希值;因此,这个哈希值甚至不会反映出输入位串的长度 N,更不用说字符串中每个位的值了。顺便说一下,SHA256 不容易受到(length extension attack)。唯一有效的逆向工程方法是通过蛮力搜索将计算出的 SHA256 哈希值逆向返回到输入位串,这意味着需要尝试所有可能的输入位串,直到找到与目标哈希值匹配的位串。这样的搜索在 SHA256 这样一个完善的加密哈希函数上是不可行的。

现在,最后一个回顾的知识点是有序(in order)。加密哈希值是统计学上的唯一,而不是无条件的唯一,这意味着两个不同的输入位串产生相同的哈希值是不太可能的,但也不是不可能的 —— 这称之为碰撞(collision)。提供了一个很好的反直觉的碰撞例子。对各种哈希算法的抗碰撞性(collision resistance)有着广泛的研究。例如,MD5(128 位哈希值)在大约 221 次哈希之后,抗碰撞能力就会崩溃。对于 SHA1(160 位哈希值),大约在 261 次哈希后开始崩溃。

对于 SHA256 的抗碰撞能力的剖析,目前还没有一个很好的估计。这个事实并不奇怪。SHA256 有 2256 个不同的哈希值范围,这个数字的十进制表示法有 78 位之多!那么,SHA256 哈希会不会发生碰撞呢?当然可能,但可能性极小。

在下面的命令行示例中,有两个输入文件被用作位串源:hashIn1.txthashIn2.txt。第一个文件包含abc,第二个文件包含1a2b3c

为了便于阅读,这些文件包含的是文本,但也可以使用二进制文件代替。

在命令行(百分号%是提示符)使用 Linux 实用程序对这两个文件进行处理产生以下哈希值(十六进制):

  1. % sha256sum hashIn1.txt

  2. 9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3 hashIn1.txt

  3. % sha256sum hashIn2.txt

  4. 3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13 hashIn2.txt

OpenSSL 哈希对应的结果与预期相同:

  1. % openssl dgst -sha256 hashIn1.txt

  2. SHA256(hashIn1.txt)= 9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3

  3. % openssl dgst -sha256 hashIn2.txt

  4. SHA256(hashIn2.txt)= 3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13

这种对密码学哈希函数的研究,为我们仔细研究数字签名及其与密钥对的关系奠定了基础。

数字签名

顾名思义,数字签字(digital signature)可以附在文件或其他一些电子工件(artifact)(如程序)上,以证明其真实性。因此,这种签名类似于纸质文件上的手写签名。验证数字签名就是要确认两件事:第一,被担保的工件在签名被附上后没有改变,因为它部分是基于文件的加密学哈希值。第二,签名属于一个人(例如 Alice),只有她才能获得一对密钥中的私钥。顺便说一下,对代码(源码或编译后的代码)进行数字签名已经成为程序员的普遍做法。

让我们来了解一下数字签名是如何创建的。如前所述,没有公钥和私钥对就没有数字签名。当使用 OpenSSL 创建这些密钥时,有两个独立的命令:一个是创建私钥,另一个是从私钥中提取匹配的公钥。这些密钥对用 base64 编码,在这个过程中可以指定它们的大小。

私钥(private key)由数值组成,其中两个数值(一个模数(modulus)和一个指数(exponent))组成了公钥。虽然私钥文件包含了公钥(public key),但提取出来的公钥并不会透露相应私钥的值。

因此,生成的带有私钥的文件包含了完整的密钥对。将公钥提取到自己的文件中是很实用的,因为这两把钥匙有不同的用途,而这种提取方式也将私钥可能被意外公开的危险降到最低。

接下来,这对密钥的私钥被用来生成目标工件(如电子邮件)的哈希值,从而创建签名。在另一端,接收者的系统使用这对密钥的公钥来验证附在工件上的签名。

现在举个例子。首先,用 OpenSSL 生成一个 2048 位的 RSA 密钥对:

  1. openssl genpkey -out privkey.pem -algorithm rsa 2048

在这个例子中,我们可以舍去-algorithm rsa标志,因为genpkey默认为 RSA 类型。文件的名称(privkey.pem)是任意的,但是隐私增强邮件(Privacy Enhanced Mail)(PEM)扩展名.pem是默认 PEM 格式的惯用扩展名。(如果需要的话,OpenSSL 有命令可以在各种格式之间进行转换。)如果需要更大的密钥大小(例如 4096),那么最后一个参数2048可以改成4096。这些大小总是二的幂。

下面是产生的privkey.pem文件的一个片断,它是 base64 编码的:

  1. -----BEGIN PRIVATE KEY-----

  2. MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANnlAh4jSKgcNj/Z

  3. JF4J4WdhkljP2R+TXVGuKVRtPkGAiLWE4BDbgsyKVLfs2EdjKL1U+/qtfhYsqhkK

  4. ...

  5. -----END PRIVATE KEY-----

接下来的命令就会从私钥中提取出这对密钥的公钥:

  1. openssl rsa -in privkey.pem -outform PEM -pubout -out pubkey.pem

由此产生的pubkey.pem文件很小,可以在这里完整地显示出来:

  1. -----BEGIN PUBLIC KEY-----

  2. MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZ5QIeI0ioHDY/2SReCeFnYZJY

  3. z9kfk11RrilUbT5BgIi1hOAQ24LMilS37NhHYyi9VPv6rX4WLKoZCmkeYaWk/TR5

  4. 4nbH1E/AkniwRoXpeh5VncwWMuMsL5qPWGY8fuuTE27GhwqBiKQGBOmU+MYlZonO

  5. O0xnAKpAvysMy7G7qQIDAQAB

  6. -----END PUBLIC KEY-----

现在,有了密钥对,数字签名就很容易了 —— 在本例中,源文件client.c是要签名的工件:

  1. openssl dgst -sha256 -sign privkey.pem -out sign.sha256 client.c

client.c源文件的摘要是 SHA256,私钥在前面创建的privkey.pem文件中。由此产生的二进制签名文件是sign.sha256,这是一个任意的名字。要得到这个文件的可读版本(比如 base64),后续命令是:

  1. openssl enc -base64 -in sign.sha256 -out sign.sha256.base64

文件sign.sha256.base64现在包含如下内容:

  1. h+e+3UPx++KKSlWKIk34fQ1g91XKHOGFRmjc0ZHPEyyjP6/lJ05SfjpAJxAPm075

  2. VNfFwysvqRGmL0jkp/TTdwnDTwt756Ej4X3OwAVeYM7i5DCcjVsQf5+h7JycHKlM

  3. o/Jd3kUIWUkZ8+Lk0ZwzNzhKJu6LM5KWtL+MhJ2DpVc=

或者,可执行文件client也可以被签名,由此产生的 base64 编码签名将如预期的不同:

  1. VMVImPgVLKHxVBapJ8DgLNJUKb98GbXgehRPD8o0ImADhLqlEKVy0HKRm/51m9IX

  2. xRAN7DoL4Q3uuVmWWi749Vampong/uT5qjgVNTnRt9jON112fzchgEoMb8CHNsCT

  3. XIMdyaPtnJZdLALw6rwMM55MoLamSc6M/MV1OrJnk/g=

这一过程的最后一步是用公钥验证数字签名。作为验证的一个重要步骤,应重新计算用于签署工件(在本例中,是可执行的client程序)的哈希值,因为验证过程应表明工件在签署后是否发生了变化。

有两个 OpenSSL 命令用于这个目的。第一条命令是对 base64 签名进行解码。

  1. openssl enc -base64 -d -in sign.sha256.base64 -out sign.sha256

第二条是核实签名:

  1. openssl dgst -sha256 -verify pubkey.pem -signature sign.sha256 client

第二条命令的输出,应该是这样的:

  1. Verified OK

为了了解验证失败时的情况,一个简短但有用的练习是将最后一个 OpenSSL 命令中的可执行的client文件替换为源文件client.c,然后尝试验证。另一个练习是改变client程序,无论多么轻微,然后再试一次。

数字证书

数字证书(digital certificate)汇集了到目前为止所分析的各个部分:哈希值、密钥对、数字签名和加密/解密。生产级证书的第一步是创建一个证书签名请求(certificate signing request)(CSR),然后将其发送给证书颁发机构(certificate authority)(CA)。在 OpenSSL 的例子中,要做到这一点,请运行:

  1. openssl req -out myserver.csr -new -newkey rsa:4096 -nodes -keyout myserverkey.pem

这个例子生成了一个 CSR 文档,并将该文档存储在文件myserver.csr(base64 文本)中。这里的目的是:CSR 文档要求 CA 保证与指定域名相关联的身份,域名也就是 CA 所说的通用名(common name)(CN)。

尽管可以使用现有的密钥对,但这个命令也会生成一个新的密钥对。请注意,在诸如myserver.csrmyserverkey.pem等名称中使用server暗示了数字证书的典型用途:作为与 www.google.com 等域名相关的 Web 服务器的身份担保。

然而,无论数字证书如何使用,同样使用这个命令都会创建一个 CSR。它还会启动一个问题/回答的交互式会话,提示有关域名的相关信息,以便与请求者的数字证书相连接。这个交互式会话可以通过在命令中提供基本的信息,用反斜杠来续行一步完成。-subj标志提供了所需的信息。

  1. % openssl req -new \

  2. -newkey rsa:2048 -nodes -keyout privkeyDC.pem \

  3. -out myserver.csr \

  4. -subj "/C=US/ST=Illinois/L=Chicago/O=Faulty Consulting/OU=IT/CN=myserver.com"

产生的 CSR 文件在发送给 CA 之前可以进行检查和验证。这个过程可以创建具有所需格式(如 X509)、签名、有效期等的数字证书。

  1. openssl req -text -in myserver.csr -noout -verify

这是输出的一个片断:

  1. verify OK

  2. Certificate Request:

  3. Data:

  4. Version: 0 (0x0)

  5. Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com

  6. Subject Public Key Info:

  7. Public Key Algorithm: rsaEncryption

  8. Public-Key: (2048 bit)

  9. Modulus:

  10. 00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73:

  11. Exponent: 65537 (0x10001)

  12. Attributes:

  13. a0:00

  14. Signature Algorithm: sha256WithRSAEncryption

自签证书

在开发 HTTPS 网站的过程中,手头有一个不用经过 CA 流程的数字证书是很方便的。在 HTTPS 握手的认证阶段,自签证书(self-signed certificate)就能满足要求,尽管任何现代浏览器都会警告说这样的证书毫无价值。继续这个例子,自签证书的 OpenSSL 命令(有效期为一年,使用 RSA 公钥)如下:

  1. openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout myserver.pem -out myserver.crt

下面的 OpenSSL 命令呈现了生成的证书的可读版本:

  1. openssl x509 -in myserver.crt -text -noout

这是自签证书的部分输出:

  1. Certificate:

  2. Data:

  3. Version: 3 (0x2)

  4. Serial Number: 13951598013130016090 (0xc19e087965a9055a)

  5. Signature Algorithm: sha256WithRSAEncryption

  6. Issuer: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com

  7. Validity

  8. Not Before: Apr 11 17:22:18 2019 GMT

  9. Not After : Apr 10 17:22:18 2020 GMT

  10. Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com

  11. Subject Public Key Info:

  12. Public Key Algorithm: rsaEncryption

  13. Public-Key: (4096 bit)

  14. Modulus:

  15. 00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73:

  16. ...

  17. Exponent: 65537 (0x10001)

  18. X509v3 extensions:

  19. X509v3 Subject Key Identifier:

  20. 3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91

  21. X509v3 Authority Key Identifier:

  22. keyid:3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91

  23. X509v3 Basic Constraints:

  24. CA:TRUE

  25. Signature Algorithm: sha256WithRSAEncryption

  26. 3a:eb:8d:09:53:3b:5c:2e:48:ed:14:ce:f9:20:01:4e:90:c9:

  27. ...

如前所述,RSA 私钥包含的值是用来生成公钥的。但是,给定的公钥不会泄露匹配的私钥。关于底层数学理论的介绍,见 。

数字证书与用于生成该证书的密钥对之间存在着重要的对应关系,即使证书只是自签的:

数字证书包含构成公钥的指数和模数值。这些值是最初生成的 PEM 文件中密钥对的一部分,在本例中,是文件myserver.pem

指数(exponent)几乎总是 65,537(如本例中),所以可以忽略。

密钥对的 模数(modulus)应该与数字证书的模数相匹配。

模数是一个很大的值,为了便于阅读,可以进行哈希处理。下面是两个 OpenSSL 命令,它们检查相同的模数,从而确认数字证书是基于 PEM 文件中的密钥对。

  1. % openssl x509 -noout -modulus -in myserver.crt | openssl sha1 ## 证书中的模数

  2. (stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769

  3. % openssl rsa -noout -modulus -in myserver.pem | openssl sha1 ## 密钥中的模数

  4. (stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769

所产生的哈希值匹配,从而确认数字证书是基于指定的密钥对。

回到密钥分发问题上

让我们回到第一部分末尾提出的一个问题:client程序和 Google Web 服务器之间的 TLS 握手。握手协议有很多种,即使是用在client例子中的 Diffie-Hellman 版本也有不同的方式。尽管如此,client例子遵循了一个共同的模式。

首先,在 TLS 握手过程中,client程序和 Web 服务器就加密套件(cipher suite)达成一致,其中包括要使用的算法。在本例中,该套件是ECDHE-RSA-AES128-GCM-SHA256

现在值得关注的两个要素是 RSA 密钥对算法和 AES128 块密码,用于在握手成功的情况下对消息进行加密和解密。关于加密/解密,这个过程有两种流派:对称(symmetric)和非对称(asymmetric)。在对称流派中,加密和解密使用的是相同的密钥,这首先就引出了密钥分发问题(key distribution problem)。如何将密钥安全地分发给双方?在非对称流派中,一个密钥用于加密(在这种情况下,是 RSA 公钥),但另一个密钥用于解密(在这种情况下,是来自同一对密钥的 RSA 私钥)。

client程序拥有来认证证书的 Google Web 服务器的公钥,而 Web 服务器拥有来自同一对密钥的私钥。因此,client程序可以向 Web 服务器发送加密信息,而 Web 服务器可以单独对该通信进行解密。

在 TLS 的情况下,对称方式有两个显著的优势:

client程序与 Google Web 服务器之间的互动中,认证是单向的。Google Web 服务器向client程序发送三张证书,但client程序并没有向 Web 服务器发送证书,因此,Web 服务器没有来自客户端的公钥,无法加密发给客户端的消息。

使用 AES128 的对称加密/解密比使用 RSA 密钥的非对称加密/解密快了近千倍

TLS 握手将两种加密/解密方式巧妙地结合在一起。在握手过程中,client程序会生成随机位,即所谓的预主密(pre-master secret)(PMS)。然后,client程序用服务器的公钥对 PMS 进行加密,并将加密后的 PMS 发送给服务器,服务器再用 RSA 密钥对的私钥对 PMS 信息进行解密:

  1. +-------------------+ encrypted PMS +--------------------+

  2. +-------------------+ +--------------------+

在这个过程结束时,client程序和 Google Web 服务器现在拥有相同的 PMS 位。每一方都使用这些位生成一个主密码(master secret),并立即生成一个称为会话密钥(session key)的对称加密/解密密钥。现在有两个不同但等价的会话密钥,连接的每一方都有一个。在client的例子中,会话密钥是 AES128 类的。一旦在client程序和 Google Web 服务器两边生成了会话密钥,每一边的会话密钥就会对双方的对话进行保密。如果任何一方(例如,client程序)或另一方(在这种情况下,Google Web 服务器)要求重新开始握手,握手协议(如 Diffie-Hellman)允许整个 PMS 过程重复进行。

总结

在命令行上说明的 OpenSSL 操作也可以通过底层库的 API 完成。这两篇文章重点使用了这个实用程序,以保持例子的简短,并专注于加密主题。如果你对安全问题感兴趣,OpenSSL 是一个很好的开始地方,并值得深入研究。

via:

作者: 选题: 译者: 校对:

本文由 原创编译, 荣誉推出