第六章 非对称加密和解密

在本章中,我们将了解非对称加密asymmetric encryption),它是如何工作的,以及如何使用公钥和私钥来实现加密和解密。在本章的实践部分,我们将学习如何在命令行和C代码中使用非对称加密和解密。

第六章 非对称加密和解密技术要求理解非对称加密理解中间人的攻击亲自会面通过手机验证密钥指纹密钥拆分由受信任的第三方签署密钥OpenSSL中提供了哪种非对称加密?理解会话密钥理解RSA安全如何生成RSA密钥对如何在命令行上使用RSA进行加密和解密如何以编程方式使用RSA加密rsa-encrypt程序的实现运行rsa-encrypt程序了解OpenSSL错误队列如何以编程方式使用RSA解密实现rsa-decrypt程序运行rsa-decrypt程序总结

技术要求

本章将包含可以在命令行上运行的命令以及可以构建和运行的C源代码。对于命令行命令,您将需要带有openssl动态库的openssl命令行工具。为了构建C代码,您需要OpenSSL动态或静态库、库头、C编译器和链接器。

我们将在本章中实施一些示例程序,以练习我们正在学习的内容。这些程序的完整源代码可以在这里找到:

https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/tree/main/Chapter06

理解非对称加密

正如我们在【第2章“对称加密和解密”】中所了解到的,对称加密算法使用相同的密钥进行加密和解密。相反,非对称加密算法Asymmetric encryption algorithms)使用两个密钥——公钥a public key)和私钥a private key)。公钥及其对应的私钥形成密钥对keypair)。公钥用于加密,私钥用于解密。

为什么我们需要两个密钥的复杂性?为什么我们不能总是使用一个密钥的对称加密?简而言之,当难以或不可能发送密钥并确保没有人窃听传输的密钥时,需要非对称加密。让我们想象一下,Alice想通过非安全通道(例如互联网)向Bob发送消息。在互联网上,Alice和Bob之间的中转主机可以窃听经过的流量,甚至可以更改流量。Alice不希望任何人窃听消息,因此她决定对消息进行加密。但是如何传输加密密钥呢?如果Alice和Bob有一个免费可用的安全通信信道来发送密钥,他们就可以通过该信道发送实际消息,而无需加密。因此,Alice和Bob决定使用非对称密码学:

  1. Bob生成一个密钥对,并将他的公钥发送给Alice。
  2. Alice使用Bob的公钥对消息进行加密,并将加密后的消息发送给Bob。
  3. Bob用他的私钥解密了消息。

如果攻击者窃听流量并获得Bob的公钥和消息,他们将无法解密消息。只有Bob的私钥可以解密消息。因此,Bob必须对其私钥保密,不得与任何人共享。

如果潜在的攻击者只能被动地窃听流量,而不能改变流量,那么它就很有效。但如果他们能做到呢?然后,他们可以尝试“中间人”攻击。

理解中间人的攻击

中间人Man in the MiddleMITM)攻击是一种攻击,攻击者监听传输流量并对其进行更改,试图将接收者模拟为发送者,将发送者模拟为接收者。

让我们为前面提到的Alice和Bob的场景演示一种可能的攻击。让我们假设Mallory充当中间人,以便恢复Alice想要发送给Bob的加密消息的明文。那么,攻击场景将如下:

  1. Bob生成一个密钥对,并将他的公钥发送给Alice。
  2. Mallory生成了自己的密钥对。她拦截了Bob发送给Alice的公钥,并将其保存以备将来使用。Mallory将自己的公钥伪装成Bob的密钥发送给Alice,而不是Bob的公钥。
  3. Alice用Mallory的公钥加密她的消息,认为这是Bob的公钥。然后,Alice将加密消息发送给Bob。
  4. Mallory截获了Alice的信息。Mallory用她的私钥解密了消息。然后,她用Bob的公钥对消息进行重新加密,并将加密后的消息发送给Bob。
  5. 结果是,Alice认为她将消息发送给了Bob,但没有其他人阅读。Bob认为他收到了Alice的消息,但没有人阅读。Mallory在未被注意的情况下从消息中窃取了秘密信息。

我们如何防御MITM攻击?有不同的方法,但每种方法都需要一个可信的通信信道,至少在主通信发生之前的短时间内可用。

亲自会面

双方(Alice和Bob)可以亲自会面,交换或验证他们的公钥。这种方法虽然简单,但往往不太实用,尤其是当Alice和Bob住得很远的时候。然而,有时它是实用的。例如,一些IT会议包括密钥签署聚会key signing parties)。来自世界各地的人们聚集在一起,互相出示身份证,如护照或驾驶执照,以及他们的相当好的隐私Pretty Good PrivacyPGP)密钥指纹。聚会的参与者还可以签署彼此的钥匙;这就是为什么它被称为密钥签署聚会。

通过手机验证密钥指纹

公钥指纹基本上是其完整或部分加密哈希,十六进制或base64编码。在通过互联网交换公钥后,例如通过电子邮件,Alice和Bob打了个电话,验证了对方的指纹。在这种情况下,Alice和Bob的声音可以作为身份验证的一种形式。不幸的是,如今,这种密钥验证方法比以前更不实用和可靠。更快的CPU和先进的密码分析方法迫使Alice和Bob使用更长的指纹,而deepfake技术的最新发展使实时伪造人声成为可能。即便如此,语音验证仍然是一种很好的身份验证方法。为了伪造密钥交换,攻击者需要在两个不同的通信信道上完成MITM攻击,并采用先进的深度伪造技术。

密钥拆分

密钥可以分成几个部分,每个部分都可以使用不同的传递方式发送给收件人,例如电子邮件、即时通讯、在文件交换服务器上发布以及通过物理邮件发送闪存驱动器。这个想法是,攻击者很难在所有渠道上拦截和伪造消息。收件人重新组装钥匙后,还可以通过电话验证其指纹。

由受信任的第三方签署密钥

Alice和Bob都信任的第三方(我们称之为Thomas)可以对Bob的公钥进行数字签名,证明它确实是Bob的密钥。然后,Alice可以验证签名。这种方法要求Alice必须首先通过某个可信通道获取Thomas的公钥。PGP的信任网络web of trust)和基于X.509证书的公钥基础设施Public Key InfrastructurePKI)中使用了类似但更复杂的密钥验证模型。有关数字签名的更多信息,请参阅【第7章“数字签名及其验证”】,有关X.509证书的更多详细信息,请参见【第8章“X.509证书和PKI”】。

Alice和Bob现在知道如何安全地交换公钥。但是,他们应该使用什么非对称加密算法来加密他们想要安全地发送给彼此的消息呢?让我们在下一节中了解一下。

OpenSSL中提供了哪种非对称加密?

OpenSSL库实现了几种非对称加密算法,但其中只有一种算法允许您 直接directly)加密数据。它是Rivest-Shamir-AdlemanRSA)算法。

其他可用的非对称加密算法,如数字签名算法Digital Signature AlgorithmDSA)和椭圆曲线数字签名算法Elliptic Curve Digital Signature AlgorithmECDSA),可用于数字签名。Diffie-HellmanDH)和 椭圆曲线Diffie-HellmanElliptic Curve Diffie–HellmanECDH)算法可用于传输层安全Transport Layer SecurityTLS)协议中的密钥交换。

众所周知,对称密码密钥没有结构;它们只是随机比特的数组。相反,非对称加密算法使用结构化密钥,这意味着一个密钥可以有多个组件,而一个组件可能有某些要求——例如,它必须是素数而不是随机比特数组。每种非对称加密算法都有自己的公钥和私钥结构。因此,存在不同格式的非对称密钥,例如RSA密钥和ECDSA密钥。

为什么我们之前说RSA是OpenSSL中唯一可以直接directly)加密数据的非对称加密算法?直接是什么意思?这意味着仅RSA算法就足以进行加密。其他算法可以间接用于加密吗?是的,这是可能的。ElGamal 算法可以使用带有DSA密钥的DH算法或带有ECDSA密钥的ECDH算法进行非对称加密。ElGamal涉及除了要用于加密的DSA/ECDSA密钥外,还创建一个临时的DAS/ECDSA密钥,在两个密钥之间进行DH/EDCH密钥交换以获得共享密钥,从共享密钥中导出对称加密密钥,最后用该对称密钥加密数据。正如我们所看到的,它比使用RSA更复杂。还有其他方案可以使用定义的非对称密钥和临时非对称密钥来推导对称密钥,如集成加密方案Integrated Encryption SchemeIES)和混合公钥加密Hybrid Public Key EncryptionHPKE)。OpenSSL不实现ElGamal、IES、HPKE或类似算法。因此,对于本章中的非对称加密示例,我们将使用RSA。

ElGamal算法中使用的对称加密密钥称为会话密钥session key)。使用会话密钥的概念并不特定于ElGamal。会话密钥也用于RSA和安全网络协议,如TLS、SSH和IPsec。接下来让我们详细看看会话密钥。

理解会话密钥

了解非对称加密在实践中的使用方式非常重要。非对称密码,如RSA,比对称密码(如AES)慢得多。因此,通常,发送方想要加密的实际数据不是由RSA加密的。相反,发送方生成对称会话密钥。然后,使用由RSA加密的会话密钥,通过对称算法(如AES)对实际数据进行加密。解密时,接收方首先通过RSA解密会话密钥,然后使用会话密钥通过AES解密实际数据。这种加密方案通常被称为混合加密方案hybrid encryption scheme)。

在TLS、SSH和IPsec等安全网络协议中,通信会话从握手handshaking)操作开始,其中一部分是密钥交换key exchange)操作。在旧版本的协议中,密钥交换操作涉及通信的一方生成会话密钥,由RSA加密,并将加密后的密钥发送给另一方。在当前版本的协议中,通信方使用DH或ECDH密钥交换方法,其中通信双方导出相同的会话密钥,然后使用该密钥对有用数据进行加密。有关TLS握手的更多信息,请参阅【第9章“建立TLS连接并通过它们发送数据”】。

为了确保混合加密的安全性,会话密钥的对称加密和RSA密钥的非对称加密都必须是安全的。下一节将深入介绍RSA安全性。

理解RSA安全

RSA的安全性依赖于整数分解问题(integer factorization problem)的难度。提醒一下,因式分解操作是将一个正整数分解为素数的乘法。目前,没有可以由经典计算机运行的整数分解多项式时间算法,因此,这个问题在计算上仍然非常昂贵。RSA中使用的整数有多大?通常,它们是数百或数千个十进制数字,或数千或数万位。显然,C语言中常见的整数类型不足以执行RSA中使用的大数字的数学运算。因此,OpenSSL包含一个大数字子库来处理大数字。该大数子库的类型和函数具有 BN_ 前缀。

RSA公钥或私钥由几个大整数或小整数组成,称为模、指数,以及可选的素数和系数。如前所述,其中一个数字是模数。RSA密钥大小被认为是模数的大小。例如,如果RSA密钥包含2048位模数,则该RSA密钥的大小也为2048位。

重要的是要记住,RSA密钥的安全级别远低于密钥大小。例如,2048位RSA密钥的安全级别仅为112位。RSA密钥的安全级别不会根据密钥大小线性增长。密钥大小必须大幅增加,才能只获得几个安全位。

下表列出了美国国家标准与技术研究院(NIST)对不同大小的RSA密钥估计的安全级别:

RSA密钥尺寸(bits)安全级别(bits)
1,02480
2,048112
3,072128
4,096152
7,680192
8,192200
15,360256

正如我们所看到的,为了达到与AES-256相同的安全级别,我们需要15360位的RSA密钥!长RSA密钥有什么缺点吗?不幸的是,有:

正如我们所看到的,我们在安全性和性能之间进行了权衡。RSA密钥、加密块或签名占用的空间量就不那么重要了。不幸的是,为了获得更多的安全性,我们必须将密钥大小增加更多的比特,并且我们必须付出巨大的性能代价。幸运的是,CPU,包括移动和嵌入式设备中的CPU,正变得越来越快。

但什么是可接受的妥协?我们选择什么RSA密钥大小?NIST建议RSA密钥使用以下大小:

在撰写本文时,2048位是RSA密钥最受欢迎和最低可接受的大小。web上大多数基于RSA的web服务器TLS证书使用2048位密钥。下一步可能是4096位证书。互联网上的许多旧文章都主张折衷3072位密钥和证书,但这一想法并没有真正流行起来。目前的文章通常比较2048位和4096位证书。

我个人认为有一些安全裕度是很好的,因此我更喜欢4096位作为RSA密钥大小。为什么不少呢?因为我使用的计算机使用这样的RSA密钥工作得很快。为什么不多?因为4096位提供了足够的安全裕度,而且一些软件,特别是旧软件,不支持长度超过4096位的RSA密钥。例如,即使是现代的GnuPG也不允许创建长度超过4096位的RSA密钥对。此外,就其价值而言,4096是一个不错的数字,因为它是2的幂(a power of 2),与3072不同。

我还认为,在ECC的使用与RSA的使用一样简单的情况下,使用椭圆曲线密码学Elliptic Curve CryptographyECC)而不是RSA会更好——例如,在TLS证书和SSH密钥中。但对于加密,RSA比ECC更容易使用。我们将在【第7章“数字签名及其验证”】中了解更多关于ECC的信息。

量子计算quantum computing)到来时,RSA安全会发生什么?不幸的是,我们所知道的非对称密码学,无论是RSA还是ECC,都将被量子计算完全打破,我们还没有一个标准化和实践证明的替代品。好消息是,许多密码学家正在积极研究后量子密码算法。在撰写本文时,NIST正在进行后量子密码学标准化,这是一场对抗量子密码算法的竞赛,类似于AES和SHA-3竞赛。比赛的获胜者将被标准化为标准的后量子密码算法。最乐观的预测是,能够打破当前不对称密码学的量子计算机将在2030年代问世。因此,我们很希望在强大的量子计算机甚至可供政府和公司等大公司使用之前,后量子加密算法将被标准化并包含在流行的加密库中,如OpenSSL。

量子计算和OpenSSL对后量子加密算法的支持是我们未来所期待的。但我们现在能做什么呢?了解如何使用RSA。让我们从生成第一个RSA密钥对开始。

如何生成RSA密钥对

openssl 工具提供了两个子命令来生成RSA密钥对—— genrsagenpkey 。前者只能生成RSA密钥对,而后者是一个更通用的子命令,可以生成OpenSSL支持的任何类型的密钥对。自OpenSSL 3.0以来, genrsa 已被宣布弃用,因此我们将使用 genpkey

openssl genpkey 子命令的文档可以在openssl-genpkey手册页上找到:

为什么叫这样的名字,genpkey ?OpenSSL有一个公钥或私钥Public or Private KeyPKEY)的概念。在这里,澄清一个混淆是很重要的。在OpenSSL文档中,您会发现关于公钥和私钥的提及。在提到私钥时,文档通常指的是密钥对。它适用于命令行工具文档和OpenSSL API文档。例如,openssl-genpkey 手册页的描述description)部分说:genpkey 命令生成私钥。如果只生成私钥,我将如何生成补充公钥?OpenSSL文档建议我可以从私钥中提取公钥,这对于没有经验的OpenSSL用户来说可能会非常困惑。更具体地说,OpenSSL和PKCS标准通常称之为私钥的是一种数据结构,其中包含足以构建私钥和公钥的信息。其中一些信息在公钥和私钥之间共享,特别是RSA的模数部分。公钥是一种类似的数据结构,只包含公钥信息。因此,为了简单起见,我们可以假设当提到私钥时,OpenSSL意味着密钥对,当提到公钥时,OpenSSL意味着它所说的——公钥。

我希望我能澄清比我制造的更多的困惑!现在,让我们生成第一个RSA密钥对:

使用 -pkeyopt rsa_keygen_bits:4096 开关,我们指定要生成4096位密钥对。生成后,密钥对已写入名为 rsa_keypair.pem 的文件中。

密钥对文件可以用文本编辑器打开,它将包含以下内容:

密钥对以隐私增强邮件Privacy Enhanced MailPEM)格式保存。PEM是OpenSSL用于存储密钥或证书的默认格式。即使在PEM格式名称中提到了邮件,该格式也不仅仅用于邮件。PEM格式实际上是一个围绕一些二进制数据的Base64,有一个文本标题(BEGIN 行)和一个文本页脚(END 行)。如果从密钥对PEM文件中删除页眉和页脚并对其进行Base64解码,则将获得区分编码规则Distinguished Encoding RulesDER)格式的密钥对。DER是存储OpenSSL支持的密钥和证书的另一种流行格式。DER是抽象语法表示法一Abstract Syntax Notation OneASN.1)描述的数据结构的二进制表示。ASN.1是一种符号,允许您以独立于平台的方式描述序列化的数据结构,例如,ASN.1描述的RSA密钥对是ASN.1整数的ASN.1序列。

我们可以使用 openssl pkey 子命令检查生成的密钥对的结构。 openssl pkey 子命令的文档可以在 openssl-pky 手册页上找到:

还有一个类似的RSA特定子命令 openssl rsa 。自OpenSSL 3.0以来,它与 openssl genrsaopenssl rsautl 和其他特定于密钥类型的子命令一起被弃用。

以下是我们检查键对结构的方法:

我们生成了一个密钥对,这非常好。现在,我们必须从中提取公钥,以便与为我们加密数据的人共享公钥,因为我们永远不应该共享我们的密钥对;我们应该只共享公钥。

以下是我们从密钥对中提取公钥的方法:

注意 -pubout 开关。这就是我们如何指定输出中只需要公钥,而不是整个密钥对。

生成的 rsa_public_key.pem 文件不包含秘密信息,可以自由共享。我们可以检查公钥文件的结构:

注意 -pubin 开关。这就是我们如何指定 openssl pkey 子命令必须将输入视为公钥,而不是密钥对。

正如我们所观察到的,公钥文件包含的信息比密钥对文件少得多,只有模数和公共指数。

我们在密钥生成上花了足够的时间。现在,让我们利用这些钥匙。

如何在命令行上使用RSA进行加密和解密

openssl 工具提供了两个子命令用于使用RSA进行加密—— pkeyutl 和已弃用的RSA特定的 rsautl 子命令。当然,我们将使用 pkeyutl 。该子命令的文档可以在 openssl-pkeyutl 手册页上找到:

如前所述,RSA通常用于加密会话密钥,然后会话密钥将用于加密有用数据。让我们生成一个256位的会话密钥:

现在,让我们使用 openssl pkeyutl 和我们的公共RSA密钥来加密会话密钥:

注意 -pkeyopt rsa_padding_mode:oaep 开关。它指示 openssl pkeyutl 对RSA加密使用PKCS#1 v2.0 最优非对称加密填充Optimal Asymmetric Encryption PaddingOAEP)填充类型。强烈建议使用该填充类型,因为它是最安全的。如果您使用默认的PKCS#1 v1.5填充,您的密文可能容易受到Bleichenbacher填充预言攻击。

让我们检查一下我们创建的文件:

请注意,输入会话密钥文件的长度仅为32字节(256位),但其加密版本的长度为512字节(4096位)。这是因为RSA的输出密文块与使用的RSA密钥大小相同。

尝试多次加密,并注意每次加密后加密文件的大小相同,但校验和不同。虽然RSA加密本身是确定性的,但使用的OAEP填充不是确定性的,这导致了非确定性密文。

我们已成功加密了一个小文件。但是,如果我们试图加密一个更大的文件,会发生什么?让我们试试。首先,让我们生成一个更大的文件:

我们有一个大约100 KB大小的文件。让我们尝试加密它:

我们遇到了一个错误,告诉我们输入数据对于密钥大小来说太大了。这是因为RSA在一次操作中不能加密超过其密钥长度的数据。如果使用填充,则可接受的最大明文长度甚至更短。当使用OAEP填充时,最大输入数据长度是 key size – 42 ——在我们的例子中,4096–42=4054 字节。如果需要加密较长的数据,则应使用对称会话密钥对其进行加密。

是时候解密我们加密的内容并将其与原始内容进行比较了。为了解密,我们需要向 openssl pkeyutl 提供密钥对,而不仅仅是公钥:

让我们检查校验和:

正如我们所看到的,session_key.binsession_key.bin.decrypted 的大小和校验和匹配,这意味着解密后的文件等于原始文件。解密总是确定性的,这绝对很棒!

我们已经学习了如何在命令行上加密和解密。现在,让我们学习如何以编程方式使用RSA进行加密。

如何以编程方式使用RSA加密

OpenSSL 3.0为RSA加密提供了以下API:

我们将开发 rsa-encrypt 程序,该程序使用rsa加密一小部分数据,例如会话密钥,类似于我们在上一节中使用 openssl pkeyutl 加密的方式。

我们的程序将与 pkeyutl 互操作,这意味着 pkeyutl 将能够解密我们程序生成的密文。

如前所述,我们将使用 EVP_PKEY API。以下是我们将要使用的功能的相关手册页列表:

我们的程序将接受三个命令行参数:

  1. 输入文件名
  2. 输出文件名
  3. RSA公钥文件名

让我们像往常一样制定一个高级实施计划。我们的程序需要执行以下步骤:

  1. 从RSA公钥文件加载RSA公钥。
  2. 从键创建 EVP_PKEY 上下文。
  3. 初始化加密的 EVP_PKEY 上下文并设置OAEP填充模式。
  4. 从输入文件中读取明文。
  5. 加密明文。
  6. 将生成的密文写入输出文件。

rsa-encrypt程序的实现

让我们继续执行:

  1. 首先,加载RSA公钥:

  2. 接下来,根据加载的密钥创建 EVP_PKEY 上下文:

  3. 然后,初始化 EVP_PKHKEY_CTX 并设置填充模式:

    您还可以在新的OpenSSL 3.0样式中使用带参数的 OSSL_PARAM 进行初始化:

    有趣的是,OpenSSL 3.0文档(在 provider-sym_cipher 手册页上)将OSSL_ASYM_CIPHER_PARAM_PAD_MODE 参数定义为整数,但在OpenSSL源代码(在 rsa_ec.c 文件中)中,相同的参数被定义为 UTF-8 字符串。该代码接受整数值和字符串值,并调用传统的PAD模式数字legacy pad mode number)整数值。当使用字符串设置填充模式时,上下文初始化代码将如下所示:

    我更喜欢旧的初始化风格,因为它是最可读、最简洁、最类型安全的,尽管它不那么通用。OpenSSL也更喜欢旧风格。OpenSSL 3.0源代码包含一些 EVP_PKEY_CTX_set_rsa_padding() 调用,但除了rsa实现代码外,没有涉及 OSSL_ASYM_CIPHER_PARAM_PAD_MODE 的调用。

  4. 接下来我们应该做的是读取输入数据。但是我们应该读多少呢?我们知道,带有填充的RSA可以在一次加密操作中加密略小于 密钥大小key size)的字节。我们可以尝试读取并加密 密钥大小key size)字节。如果加密操作因输入太长而失败,我们将从加密错误中发现它。

    让我们发现密钥大小,分配适当大小的输入和输出缓冲区,并读取输入数据:

  5. 输入数据的非对称加密是使用 EVP_PKEY_encrypt() 函数完成的:

  6. 加密数据应写入输出文件:

  7. 工作完成后,我们必须释放已使用的缓冲区和对象:

我们的rsa加密程序的完整源代码可以在GitHub上找到,文件名为 rsa encrypt.chttps://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter06/rsa-encrypt.c

运行rsa-encrypt程序

让我们运行 rsa-encrypt 程序,对之前创建的32字节会话密钥文件进行加密:

我们的项目取得了成功;让我们检查一下文件:

到目前为止一切顺利——我们的程序已经创建了大小合适的 session_key.bin.encrypted 文件。我承诺我们的程序将与 openssl pkeyutl 互操作。让我们检查一下:

未报告任何错误。

让我们检查文件及其校验和:

正如我们所观察到的,session_key.binsession_key.bin.decrypted 的大小和校验和匹配,这意味着 openssl pkeyutl 可以成功解密由 rsa-encrypt 程序加密的文件。

我提到了错误处理,但到目前为止还没有显示任何错误处理代码。我这样做是因为我不想让错误处理代码弄乱代码片段,尽管GitHub上的完整源代码包含错误处理。OpenSSL的错误处理本身就是一个主题,值得在本章中用一整节来讨论。让我们进入本节,我们还将讨论OpenSSL错误队列。

了解OpenSSL错误队列

在运行 rsa-encrypt 程序时,可能会出现许多问题,例如:

我们如何处理这些错误?那些可能失败的OpenSSL函数通常通过返回 NULL0 或负数来表示失败。返回 1 通常表示成功。某些函数还会在失败时将错误添加到OpenSSL错误队列中。

OpenSSL错误队列是OpenSSL库想要报告的错误的容器。进程的每个线程都有自己的OpenSSL错误队列Every thread of the process has its own OpenSSL error queue.)。错误队列不需要初始化或取消初始化;OpenSSL会自动处理它。每个线程都以一个空的错误队列开始。错误队列存储错误,直到它们被移出队列或线程完成执行。因此,最好检查错误队列,处理错误,如果需要,在调用OpenSSL后清除队列。

并非所有函数在失败时都会将错误放入错误队列中——例如,在对称解密失败和HMAC计算初始化失败时,我无法将任何错误放入OpenSSL错误队列中。但是,处理非对称加密、X.509证书或TLS的OpenSSL函数通常支持错误队列,并在失败时将错误放入其中。一些函数,特别是那些验证X.509证书的函数,在一次调用中可能会将多个错误放入错误队列。

以下是一些与OpenSSL错误队列交互的常用函数:

重要的是要理解,OpenSSL函数返回的退出代码与放入错误队列的错误中包含的错误代码不同——例如,如果RSA加密的输入太长, EVP_PKEY_encrypt() 函数将返回0退出代码,并将错误放入错误队列,错误代码为 0x200006E ,错误队列由以下组件组成:

以下是从错误队列中获取最早错误并打印错误信息的示例代码:

提供的示例代码将以数字形式打印库编号和原因代码。但是,我们在哪里可以找到它们的符号表示,如 ERR_R_RSA_LIBRSA_R_DATA_TOO_LARGE_FOR_KEY_SIZE ?不幸的是,我在OpenSSL文档中找不到答案,但我可以在OpenSSL库头中找到答案。库编号列表可以在 err.h 标头中找到,库特定原因代码列表可以在库特定错误标头文件中找到,例如,对于RSA子库,库特定错误标题文件是 rsaerr.h

ERR_get_error()ERR_peek_error()ERAR_GET_LIB()ERAR_GET_REASON() 函数为OpenSSL调用的复杂错误处理提供了一种方法。但是,如果我们想要一些快速简单的调试工具呢?然后,我们可以使用 ERR_print_error_fp() 函数。我们可以这样做:

错误队列将被打印到 stderr 并清除。

这就是在我们的 rsa-encrypt 程序的源代码中使用 ERR_print_error_fp() 函数的方式:

让我们尝试在输入过长的情况下运行 rsa-encrypt

我们从OpenSSL错误队列中得到的打印输出与 openssl pkeyutl 在相同错误上打印的打印输出相同。一个错误有很多信息,但长错误字符串的某些部分是人类可读的:

您可以在OpenSSL手册页上找到有关OpenSSL调用错误处理的更多信息:

当然,如何处理OpenSSL调用中的错误取决于您。但作为一名负责任的程序员,您不应该忘记在失败后处理和清除OpenSSL错误队列。

想象一下,您使用OpenSSL执行了几个操作。对于大多数操作,函数退出代码对您来说已经足够了,您忘记清除错误队列,但对于最后一个失败的操作,您希望从队列中获取错误。但是,队列现在包含所有已执行操作的错误。您将如何在所有其他错误中找到最后一个操作的错误?没有好办法。因此,您应该从一个空的错误队列开始操作。这里的操作不一定是指对OpenSSL的一次调用。一个操作可能包含多个调用,但它应该代表一个逻辑工作,这样你就可以一次处理所有操作的错误。

何时清除OpenSSL错误队列更好——在操作之前还是之后?不同的人对此有不同的看法。一种意见是,错误队列应该在操作后清除,因为负责任的程序员应该自己清理,不要泄露错误。另一种观点认为,在操作之前清除错误队列更好,因为它确保了操作之前有一个空的错误队列。我更喜欢在操作之前和之后清除队列——之后是因为它是负责任的,之前是因为在许多人参与的复杂项目中,一个人或多个人有时会忘记在自己之后清除错误队列。人类会犯错;这是生活和软件开发的悲哀事实。

好了,哲学够了。现在是时候学习如何使用C代码使用RSA解密了。

如何以编程方式使用RSA解密

在本节中,我们将开发一个小型的 rsa-decrypt 程序,以便学习如何进行RSA解密。

我们的程序将接受三个与 rsa-encrypt 类似的命令行参数,但请注意,第三个参数是包含密钥对的文件的名称,而不仅仅是公钥:

  1. 输入文件名
  2. 输出文件名
  3. RSA密钥对文件名

当然,现在输入文件应该包含密文,密文将被解密,生成的明文将被写入输出文件。

我们的 rsa-decrypt 高级实现计划将包含与 rsa-encrypt 相反的操作序列,如下所示:

  1. 从RSA密钥对文件加载RSA密钥对。我们需要一个RSA私钥进行解密;公钥是不够的。
  2. 从密钥创建 EVP_PKEY 上下文。
  3. 初始化 EVP_PKEY 上下文以进行解密,并设置OAEP填充模式。
  4. 从输入文件中读取密文。
  5. 解密密文。
  6. 将生成的明文写入输出文件。

实现rsa-decrypt程序

让我们根据我们的计划逐步实现 rsa-decrypt 程序:

  1. 首先,加载RSA密钥对:

    请注意,相同的类型EVP_PKEY用于持有公钥或整个密钥对。

  2. 接下来,从加载的键对创建 EVP_PKEY 上下文:

  3. 然后,初始化 EVP_PKEY_CTX 并设置填充模式:

  4. 找出密钥大小,分配适当大小的输入和输出缓冲区,并读取输入数据:

  5. 使用 EVP_PKY_Decrypt() 函数对获得的密文进行解密:

  6. 将解密后的数据写入输出文件:

  7. 工作完成后,我们必须释放已使用的缓冲区和对象:

我们的rsa解密程序的完整源代码可以在GitHub上找到,作为 rsa-decrypt.c 文件:https://github.com/PacktPublishing/Demystifying-Cryptography-with-OpenSSL-3/blob/main/Chapter06/rsa-decrypt.c

运行rsa-decrypt程序

让我们运行 rsa-decrypt 程序,解密之前创建的加密会话密钥文件:

我们的项目取得了成功。让我们检查文件及其校验和:

正如我们所看到的,session_key.binsession_key.bin.decrypted 的大小和校验和匹配,这意味着我们的 rsa-decrypt 可以成功解密会话密钥文件。

总结

在本章中,我们学习了非对称密码学的概念。然后,我们了解了MITM攻击的概念以及如何减轻这种攻击。之后,我们学习了会话密钥是什么。我们通过学习RSA安全性、安全性和性能之间的权衡以及选择RSA密钥大小来完成理论部分。

在实践部分,我们学习了如何在命令行上使用RSA进行加密和解密。然后,我们学习了如何在C代码中以编程方式使用RSA进行加密和解密。我们还了解了OpenSSL错误队列以及如何检查错误。

在下一章中,我们将继续学习非对称密码学。特别是,我们将学习数字签名及其验证。