本系列的第一篇文章通过 OpenSSL 库和命令行实用程序介绍了哈希、加密/解密、数字签名和数字证书。第二篇文章将深入探讨细节。让我们从计算中无处不在的哈希开始,并考虑是什么使哈希函数成为密码学。
密码学哈希
OpenSSL 源代码的下载页面 (https://www.openssl.org/source/) 包含一个带有最新版本的表格。每个版本都带有两个哈希值:160 位 SHA1 和 256 位 SHA256。这些值可用于验证下载的文件是否与存储库中的原始文件匹配:下载器在本地重新计算下载文件上的哈希值,然后将结果与原始文件进行比较。现代系统具有用于计算此类哈希的实用程序。例如,Linux 有 md5sum 和 sha256sum。OpenSSL 本身也提供类似的命令行实用程序。
哈希用于计算的许多领域。例如,比特币区块链使用 SHA256 哈希值作为区块标识符。挖掘比特币是生成一个低于指定阈值的 SHA256 哈希值,这意味着一个至少有 N 个前导零的哈希值。(N 的值可能会根据特定时间的挖掘效率而上升或下降。)值得一提的是,今天的矿工是专为并行生成 SHA256 哈希而设计的硬件集群。在 2018 年的峰值时期,全球的比特币矿工每秒产生大约 7500 万太哈希——又一个难以理解的数字。
网络协议也使用哈希值——通常称为 校验和——以支持消息完整性;也就是说,确保接收到的消息与发送的消息相同。消息发送者计算消息的校验和并将结果与消息一起发送。接收者在消息到达时重新计算校验和。如果发送的校验和与重新计算的校验和不匹配,则消息在传输过程中或发送的校验和或两者都发生了某些情况。在这种情况下,应再次发送消息及其校验和,或者至少应引发错误条件。(低级网络协议,如 UDP,不关心校验和。)
哈希的其他示例也很常见。考虑一个需要用户使用密码进行身份验证的网站,用户在其浏览器中输入密码。然后,他们的密码通过 HTTPS 连接从浏览器加密发送到服务器。一旦密码到达服务器,它就会被解密以进行数据库表查找。
应该将什么存储在此查找表中?存储密码本身是有风险的。风险较低的做法是存储从密码生成的哈希,可能在计算哈希值之前添加一些盐(额外的位)以调味。您的密码可能会发送到 Web 服务器,但该站点可以向您保证密码不会存储在那里。
哈希值也出现在各种安全领域。例如,基于哈希的消息身份验证代码 (HMAC) 使用哈希值和秘密加密密钥来验证通过网络发送的消息。HMAC 代码轻量级且易于在程序中使用,在 Web 服务中很受欢迎。X509 数字证书包括一个称为指纹的哈希值,可以方便证书验证。内存信任存储可以实现为一个以这种指纹为键的查找表——作为一个哈希映射,它支持常数时间查找。可以将传入证书的指纹与信任存储密钥进行比较以找到匹配项。
一个密码学哈希函数应该有什么特殊的属性?它应该是单向的,这意味着很难反转。计算密码学哈希函数应该相对简单,但是计算它的逆函数——将哈希值映射回输入位串的函数——应该是计算上难以处理的。这是一个描述,其中 chf 是密码学哈希函数,我的密码 foobar 是示例输入
+---+
foobar—>|chf|—>hash value ## straightforward
+--–+
相比之下,逆运算是不可行的
+-----------+
hash value—>|chf inverse|—>foobar ## intractable
+-----------+
例如,回想一下 SHA256 哈希函数。对于任何长度 N > 0 的输入位串,此函数都会生成一个固定长度的 256 位哈希值;因此,此哈希值甚至不会显示输入位串的长度 N,更不用说字符串中每个位的值了。顺便说一句,SHA256 不容易受到 长度扩展攻击 的影响。将计算出的 SHA256 哈希值逆向工程回输入位串的唯一有效方法是通过暴力搜索,这意味着尝试每个可能的输入位串,直到找到与目标哈希值的匹配项。在像 SHA256 这样的健全的密码学哈希函数上,这种搜索是不可行的。
现在,最后的回顾点是有序的。密码学哈希值在统计上而不是无条件地唯一,这意味着两个不同的输入位串产生相同的哈希值(冲突)的可能性很小但并非不可能。生日问题 提供了一个很好的反直觉的冲突示例。对各种哈希算法的抗冲突性进行了广泛的研究。例如,MD5(128 位哈希值)在大约 221 个哈希后会出现抗冲突性崩溃。对于 SHA1(160 位哈希值),崩溃从大约 261 个哈希开始。
SHA256 抗冲突性崩溃的良好估计尚未掌握。这个事实并不奇怪。SHA256 具有 2256 个不同的哈希值的范围,这个数字的十进制表示形式高达 78 位!那么,SHA256 哈希会发生冲突吗?当然,但是它们极不可能发生。
在下面的命令行示例中,两个输入文件用作位串源:hashIn1.txt 和 hashIn2.txt。第一个文件包含 abc,第二个文件包含 1a2b3c。
这些文件包含文本以提高可读性,但可以使用二进制文件代替。
在命令行上对这两个文件使用 Linux sha256sum 实用程序——以百分号 (%) 作为提示符——会产生以下哈希值(以十六进制表示)
% sha256sum hashIn1.txt
9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3 hashIn1.txt
% sha256sum hashIn2.txt
3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13 hashIn2.txt
正如预期的那样,OpenSSL 哈希对应的结果产生相同的结果
% openssl dgst -sha256 hashIn1.txt
SHA256(hashIn1.txt)= 9e83e05bbf9b5db17ac0deec3b7ce6cba983f6dc50531c7a919f28d5fb3696c3
% openssl dgst -sha256 hashIn2.txt
SHA256(hashIn2.txt)= 3eaac518777682bf4e8840dd012c0b104c2e16009083877675f00e995906ed13
对密码学哈希函数的这种检查为更仔细地了解数字签名及其与密钥对的关系奠定了基础。
数字签名
顾名思义,数字签名可以附加到文档或一些其他电子工件(例如,程序)以保证其真实性。因此,这种签名类似于纸质文档上的手写签名。验证数字签名是确认两件事。首先,自签名附加后,受保证的工件没有更改,因为它部分基于文档的密码学哈希。其次,签名属于可以单独访问一对私钥的人(例如,Alice)。顺便说一句,对代码(源代码或编译后的代码)进行数字签名已成为程序员中的一种常见做法。
让我们逐步了解如何创建数字签名。如前所述,没有公钥和私钥对就没有数字签名。当使用 OpenSSL 创建这些密钥时,有两个单独的命令:一个用于创建私钥,另一个用于从私钥中提取匹配的公钥。这些密钥对以 base64 编码,并且可以在此过程中指定它们的大小。
私钥由数值组成,其中两个值(一个模数和一个指数)构成公钥。虽然私钥文件包含公钥,但提取的公钥不显示相应私钥的值。
因此,包含私钥的生成文件包含完整的密钥对。将公钥提取到其自己的文件中是实用的,因为这两个密钥具有不同的用途,但这种提取也可以最大程度地减少私钥可能意外公开的危险。
接下来,该对的私钥用于处理目标工件(例如,电子邮件)的哈希值,从而创建签名。另一方面,接收者的系统使用该对的公钥来验证附加到工件的签名。
现在来看一个例子。首先,使用 OpenSSL 生成一个 2048 位的 RSA 密钥对
openssl genpkey -out privkey.pem -algorithm rsa 2048
在此示例中,我们可以删除 -algorithm rsa 标志,因为 genpkey 默认为 RSA 类型。文件的名称 (privkey.pem) 是任意的,但隐私增强邮件 (PEM) 扩展名 pem 是默认 PEM 格式的习惯。(如果需要,OpenSSL 具有在格式之间进行转换的命令。)如果需要更大的密钥大小(例如,4096),则可以将 2048 的最后一个参数更改为 4096。这些大小始终是 2 的幂。
这是生成的 privkey.pem 文件的一部分,该文件采用 base64 编码
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBANnlAh4jSKgcNj/Z
JF4J4WdhkljP2R+TXVGuKVRtPkGAiLWE4BDbgsyKVLfs2EdjKL1U+/qtfhYsqhkK
…
-----END PRIVATE KEY-----
然后,下一个命令从私钥中提取该对的公钥
openssl rsa -in privkey.pem -outform PEM -pubout -out pubkey.pem
生成的 pubkey.pem 文件足够小,可以完整显示在此处
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZ5QIeI0ioHDY/2SReCeFnYZJY
z9kfk11RrilUbT5BgIi1hOAQ24LMilS37NhHYyi9VPv6rX4WLKoZCmkeYaWk/TR5
4nbH1E/AkniwRoXpeh5VncwWMuMsL5qPWGY8fuuTE27GhwqBiKQGBOmU+MYlZonO
O0xnAKpAvysMy7G7qQIDAQAB
-----END PUBLIC KEY-----
现在,有了密钥对,数字签名就很容易了——在本例中,将源文件 client.c 作为要签名的工件
openssl dgst -sha256 -sign privkey.pem -out sign.sha256 client.c
client.c 源文件的摘要是 SHA256,私钥驻留在先前创建的 privkey.pem 文件中。生成的二进制签名文件是 sign.sha256,这是一个任意名称。要获取此文件的可读(如果为 base64)版本,后续命令为
openssl enc -base64 -in sign.sha256 -out sign.sha256.base64
文件 sign.sha256.base64 现在包含
h+e+3UPx++KKSlWKIk34fQ1g91XKHOGFRmjc0ZHPEyyjP6/lJ05SfjpAJxAPm075
VNfFwysvqRGmL0jkp/TTdwnDTwt756Ej4X3OwAVeYM7i5DCcjVsQf5+h7JycHKlM
o/Jd3kUIWUkZ8+Lk0ZwzNzhKJu6LM5KWtL+MhJ2DpVc=
或者,可以签名可执行文件 client,并且生成的 base64 编码的签名将按预期有所不同
VMVImPgVLKHxVBapJ8DgLNJUKb98GbXgehRPD8o0ImADhLqlEKVy0HKRm/51m9IX
xRAN7DoL4Q3uuVmWWi749Vampong/uT5qjgVNTnRt9jON112fzchgEoMb8CHNsCT
XIMdyaPtnJZdLALw6rwMM55MoLamSc6M/MV1OrJnk/g=
此过程的最后一步是使用公钥验证数字签名。用于签名工件的哈希(在本例中为可执行的 client 程序)应作为验证中的一个重要步骤进行重新计算,因为验证过程应指示自签名以来工件是否已更改。
这里有两个用于此目的的 OpenSSL 命令。 第一个命令解码 base64 签名
openssl enc -base64 -d -in sign.sha256.base64 -out sign.sha256
第二个命令验证签名
openssl dgst -sha256 -verify pubkey.pem -signature sign.sha256 client
第二个命令的输出应该是:
Verified OK
为了理解验证失败时会发生什么,一个简短但有用的练习是将最后一个 OpenSSL 命令中的可执行文件 client 替换为源文件 client.c,然后尝试验证。 另一个练习是稍微修改 client 程序,然后再次尝试。
数字证书
数字证书将到目前为止分析的片段整合在一起:哈希值、密钥对、数字签名以及加密/解密。 获得生产级证书的第一步是创建证书签名请求 (CSR),然后将其发送到证书颁发机构 (CA)。 要使用 OpenSSL 为该示例执行此操作,请运行
openssl req -out myserver.csr -new -newkey rsa:4096 -nodes -keyout myserverkey.pem
此示例生成一个 CSR 文档,并将该文档存储在文件 myserver.csr(base64 文本)中。 这里的目的是:CSR 文档请求 CA 保证与指定域名关联的身份的真实性——在 CA 术语中称为通用名称 (CN)。
此命令还会生成一个新的密钥对,尽管也可以使用现有的密钥对。 请注意,在 myserver.csr 和 myserverkey.pem 等名称中使用 server 暗示了数字证书的典型用途:作为与诸如 www.google.com 之类的域名关联的 Web 服务器的身份凭证。
但是,无论数字证书的用途如何,相同的命令都会创建一个 CSR。 它还会启动一个交互式问答会话,提示您输入与要链接到请求者的数字证书的域名相关的相关信息。 可以通过将必要信息作为命令的一部分提供,并使用反斜杠作为跨行的延续,来绕过此交互式会话。 -subj 标志引入所需的信息
% openssl req -new
-newkey rsa:2048 -nodes -keyout privkeyDC.pem
-out myserver.csr
-subj "/C=US/ST=Illinois/L=Chicago/O=Faulty Consulting/OU=IT/CN=myserver.com"
生成的 CSR 文档可以在发送到 CA 之前进行检查和验证。 此过程会创建具有所需格式(例如,X509)、签名、有效日期等的数字证书
openssl req -text -in myserver.csr -noout -verify
这是输出的一部分
verify OK
Certificate Request:
Data:
Version: 0 (0x0)
Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73:
…
Exponent: 65537 (0x10001)
Attributes:
a0:00
Signature Algorithm: sha256WithRSAEncryption
…
自签名证书
在 HTTPS 网站的开发过程中,无需通过 CA 流程即可获得数字证书非常方便。 自签名证书在 HTTPS 握手的身份验证阶段可以满足需求,尽管任何现代浏览器都会警告说此类证书毫无价值。 继续该示例,用于自签名证书(有效期为一年,带有 RSA 公钥)的 OpenSSL 命令是
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:4096 -keyout myserver.pem -out myserver.crt
下面的 OpenSSL 命令提供生成证书的可读版本
openssl x509 -in myserver.crt -text -noout
这是自签名证书的部分输出
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 13951598013130016090 (0xc19e087965a9055a)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com
Validity
Not Before: Apr 11 17:22:18 2019 GMT
Not After : Apr 10 17:22:18 2020 GMT
Subject: C=US, ST=Illinois, L=Chicago, O=Faulty Consulting, OU=IT, CN=myserver.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:ba:36:fb:57:17:65:bc:40:30:96:1b:6e:de:73:
…
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91
X509v3 Authority Key Identifier:
keyid:3A:32:EF:3D:EB:DF:65:E5:A8:96:D7:D7:16:2C:1B:29:AF:46:C4:91
X509v3 Basic Constraints:
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
3a:eb:8d:09:53:3b:5c:2e:48:ed:14:ce:f9:20:01:4e:90:c9:
...
如前所述,RSA 私钥包含从中生成公钥的值。 但是,给定的公钥不会泄露匹配的私钥。 有关基础数学的介绍,请参见 https://simple.wikipedia.org/wiki/RSA_algorithm。
数字证书和用于生成证书的密钥对之间存在重要的对应关系,即使证书只是自签名的
- 数字证书包含构成公钥的指数和模数值。 这些值是原始生成的 PEM 文件中的密钥对的一部分,在本例中为文件 myserver.pem。
- 指数几乎总是 65,537(就像本例一样),因此可以忽略。
- 密钥对中的模数应与数字证书中的模数匹配。
模数是一个很大的值,为了便于阅读,可以对其进行哈希处理。 这是两个 OpenSSL 命令,用于检查相同的模数,从而确认数字证书是基于 PEM 文件中的密钥对的
% openssl x509 -noout -modulus -in myserver.crt | openssl sha1 ## modulus from CRT
(stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769
% openssl rsa -noout -modulus -in myserver.pem | openssl sha1 ## modulus from PEM
(stdin)= 364d21d5e53a59d482395b1885aa2c3a5d2e3769
生成的哈希值匹配,从而确认数字证书基于指定的密钥对。
回到密钥分发问题
让我们回到第一部分结尾提出的一个问题:client 程序和 Google Web 服务器之间的 TLS 握手。 有多种握手协议,即使 client 示例中使用的 Diffie-Hellman 版本也提供了回旋余地。 尽管如此,client 示例还是遵循一个常见的模式。
首先,在 TLS 握手期间,client 程序和 Web 服务器会就密码套件达成一致,该套件由要使用的算法组成。 在这种情况下,该套件是 ECDHE-RSA-AES128-GCM-SHA256。
现在感兴趣的两个要素是 RSA 密钥对算法和 AES128 块密码,用于在握手成功时对消息进行加密和解密。 关于加密/解密,此过程有两种形式:对称和非对称。 在对称形式中,使用相同的密钥进行加密和解密,这首先提出了密钥分发问题:如何将密钥安全地分发给双方? 在非对称形式中,一个密钥用于加密(在本例中为 RSA 公钥),但另一个密钥用于解密(在本例中为来自同一对的 RSA 私钥)。
client 程序具有来自身份验证证书的 Google Web 服务器的公钥,并且 Web 服务器具有来自同一对的私钥。 因此,client 程序可以将加密的消息发送到 Web 服务器,只有 Web 服务器才能轻松解密此消息。
在 TLS 情况下,对称方法具有两个显着的优势
- 在 client 程序和 Google Web 服务器之间的交互中,身份验证是单向的。 Google Web 服务器将三个证书发送到 client 程序,但 client 程序不会将证书发送到 Web 服务器; 因此,Web 服务器没有来自客户端的公钥,并且无法将消息加密给客户端。
- 使用 AES128 的对称加密/解密比使用 RSA 密钥的非对称替代方案快近一千倍。
TLS 握手以巧妙的方式组合了两种加密/解密形式。 在握手期间,client 程序生成随机位,称为预主密钥 (PMS)。 然后,client 程序使用服务器的公钥加密 PMS,并将加密的 PMS 发送到服务器,服务器反过来使用其来自 RSA 对的私钥解密 PMS 消息
+-------------------+ encrypted PMS +--------------------+
client PMS--->|server’s public key|--------------->|server’s private key|--->server PMS
+-------------------+ +--------------------+
在此过程结束时,client 程序和 Google Web 服务器现在具有相同的 PMS 位。 每一方都使用这些位来生成主密钥,并在短时间内生成一个对称加密/解密密钥,称为会话密钥。 现在有两个不同但相同的会话密钥,每个连接端一个。 在 client 示例中,会话密钥是 AES128 类型的。 一旦在 client 程序端和 Google Web 服务器端生成,每一端上的会话密钥都会保持双方对话的机密性。 如果任何一方(例如,client 程序)或另一方(在本例中为 Google Web 服务器)要求重新启动握手,则像 Diffie-Hellman 这样的握手协议允许重复整个 PMS 过程。
总结
通过底层库的 API 也可以使用在命令行中演示的 OpenSSL 操作。 这两篇文章都强调了这些实用程序,以保持示例的简短性并专注于加密主题。 如果您对安全问题感兴趣,OpenSSL 是一个不错的起点 - 并且可以一直保持下去。
评论已关闭。