Python 密码学实践指南(二)

原文:Practical Cryptography in Python

协议:CC BY-NC-SA 4.0

四、不对称加密:公钥/私钥

非对称加密是加密安全领域有史以来最重要的进步之一。它是网络、Wi-Fi 连接、安全电子邮件和其他各种通信安全的基础。它无处不在,但也很微妙,很容易被错误地实现或使用,缺乏正确性意味着有时安全性会大大降低。

也许你听说过“公钥”、“公钥基础设施”和/或“公钥加密”实际上,在非对称加密和许多不同的算法中有多种操作。在本章中,我们将专门关注非对称加密,特别是使用一种被称为 RSA 的算法。我们将把其他非对称操作,如签名和密钥交换,留到后面的章节。

事实上,RSA 加密几乎完全过时了。为什么要研究它?因为 RSA 是经典的非对称算法之一,而且在我们看来,它很好地引入了一些核心概念,这些概念将有助于学习更现代的方法。

两把钥匙的故事

东南极洲真相间谍机构(EATSA)给爱丽丝和鲍勃一个新的任务。鲍勃将留在东南极洲(EA)作为爱丽丝的负责人,爱丽丝将在西南极洲政府的小餐馆(WAGGS)得到一个秘密职位。爱丽丝将向鲍勃报告西南极洲(WA)的政客们在吃什么。EATSA 计划要挟这些政客吃多少热食物,而他们的选民却只能吃冷冻晚餐。

然而,EATSA 担心通信受到影响。如果爱丽丝被用对称密钥捕获,西南极洲中央骑士办公室(WACKO)将能够用它来解密他们截获的她发给 EATSA 的任何信息。那会毁了整个计划!

EATSA 决定实施一项新技术:非对称加密。当他们发现有两个密钥的加密方案:用一个密钥加密的东西只能被另一个解密时,他们的集体头脑都炸了!

使用这项新技术,Bob 只需使用两把钥匙中的一把(“公共”钥匙)就可以将 Alice 发送到现场。爱丽丝将能够加密回给鲍勃的消息,即使她也无法解密!只有在 EA 领域内安全并且拥有相应的“私有”密钥的 Bob 可以解密消息。这听起来很完美——如果她的密钥被泄露,至少不会允许她的捕获者解密她写的东西,这比以前严格地说要好。 1 会出什么差错呢?

为了完成这个方案,EATSA 选择使用 RSA 加密,这是一种非对称算法,使用非常大的整数作为密钥和消息,并使用“模幂运算”作为加密和解密的主要数学运算符。该算法易于理解,并且使用现代编程语言,相对容易实现。从各方面来看,这都是烹饪花招的完美配方。

变得紧张

在 RSA 中生成密钥有点棘手,因为它需要找到两个非常大的整数,这两个整数很有可能是互质。对 EATSA 的代理人来说,这看起来像是一大堆数学,所以他们选择只使用现有的库来完成这一部分。清单 4-1 显示了他们放入 Python 3 的包以及他们编写的利用它的代码。

 1   from cryptography.hazmat.backends import default_backend
 2   from cryptography.hazmat.primitives.asymmetric import rsa
 3   from cryptography.hazmat.primitives import serialization
 4
 5   # Generate a private key.
 6   private_key = rsa.generate_private_key(
 7        public_exponent=65537,
 8        key_size=2048,
 9        backend=default_backend()
10   )
11
12   # Extract the public key from the private key.
13   public_key = private_key.public_key()
14
15   # Convert the private key into bytes. We won't encrypt it this time.
16   private_key_bytes = private_key.private_bytes(
17       encoding=serialization.Encoding.PEM,
18       format=serialization.PrivateFormat.TraditionalOpenSSL,
19       encryption_algorithm=serialization.NoEncryption()
20   )
21
22   # Convert the public key into bytes.
23   public_key_bytes = public_key.public_bytes(
24       encoding=serialization.Encoding.PEM,
25       format=serialization.PublicFormat.SubjectPublicKeyInfo
26   )
27
28   # Convert the private key bytes back to a key.
29   # Because there is no encryption of the key, there is no password.
30   private_key = serialization.load_pem_private_key(
31       private_key_bytes,
32       backend=default_backend(),
33       password=None)
34
35   public_key = serialization.load_pem_public_key(
36       public_key_bytes,
37       backend=default_backend())

Listing 4-1
RSA Key Generation

一旦你知道如何使用它,那还不算太坏。这种模式对于任何私钥/公钥的生成都是一样的,所以即使有一些长名字的常量,看起来这个库确实让 EATSA 变得更容易了。

看看 RSA 中的私钥是如何决定一切的吧?公钥就是从它派生出来的。虽然其中一个密钥可用于加密(另一个可用于解密),但由于这个属性,私钥是特殊的。RSA 密钥不仅是不对称的,因为一个加密,另一个解密,它们也是不对称的,因为您可以从私钥导出 RSA 公钥,而不是相反。

private_bytespublic_bytes方法将大整数键转换成标准网络和磁盘编码的字节,称为 PEM。在从磁盘中读取这些字节后,可以使用相应的序列化“load”方法来解码这些字节,这样它们看起来就像加密和解密算法的密钥。

加密私钥本身是可能的(也是一个非常好的想法),但是我们选择不在这里这样做,这就是为什么没有使用密码。

RSA 做错了:第一部分

Alice 和 Bob 将通过探索错误使用 RSA 的所有方法来帮助我们了解 RSA。

对于 EATSA 来说,实际的加密和解密部分看起来非常简单,他们查看的每个库似乎都有许多不必要的额外内容,这使得它更难理解,甚至(气喘吁吁地)降低了速度。由于没有学习过 YANAC 原理,他们决定自己实现加密和解密。他们没有像写的那样使用第三方库,而是选择省略填充。这就产生了一个非常“原始”或基本形式的 RSA,它对我们学习内部机制很有用,即使结果很不完整。

警告:不要滚动您自己的加密

同样,实现你自己的 RSA 加密/解密,而不是使用一个库,根本不是一个好主意。使用没有填充的 RSA 是非常不安全的,原因有很多,我们将在本节中探讨其中的一些。尽管我们将出于教育目的在这里编写我们自己的 RSA 函数,在任何情况下都不要在真实的交流中使用这些代码

下面是加密的数学公式,其中 c 是密文, m 是消息,其余的参数形成公钥和私钥,稍后解释:

)

(4.1)

同样,下面是解密:

)

(4.2)

看起来不算太糟,对吧?模幂运算在大型整数数学库中是一个相当标准的运算, 2 所以这个真的没有太多。

如果你是这方面的新手,不要被。为了简单起见,你通常可以认为它是一个等号。

(4.1)和(4.2)中的运算可以使用gmpy2(一个大数数学库)用 Python 简洁地编写。powmod函数执行必要的模幂运算,如清单 4-2 所示。

 1   #### DANGER ####
 2   # The following RSA encryption and decryption is
 3   # completely unsafe and terribly broken. DO NOT USE
 4   # for anything other than the practice exercise
 5   ################
 6   def simple_rsa_encrypt(m, publickey):
 7       # Public_numbers returns a data structure with the 'e' and 'n' parameters.
 8       numbers = publickey.public_numbers()
 9
10       # Encryption is(m^e) % n.
11       return gmpy2.powmod(m, numbers.e, numbers.n)
12
13   def simple_rsa_decrypt(c, privatekey):
14       # Private_numbers returns a data structure with the 'd' and 'n' parameters.
15       numbers = privatekey.private_numbers()
16
17       # Decryption is(c^d) % n.
18       return gmpy2.powmod(c, numbers.d, numbers.public_numbers.n)
19   #### DANGER ####

Listing 4-2GMPY2

如前所述,现在可能更明显了,RSA 操作的是整数,而不是消息字节。我们如何将消息转换成整数?Python 使这变得很方便,因为它的int类型有to_bytesfrom_bytes方法。让我们让它们在清单 4-3 中使用起来更好一些。

1   def int_to_bytes(i):
2       # i might be a gmpy2 big integer; convert back to a Python int
3       i = int(i)
4       return i.to_bytes((i.bit_length()+7)//8, byteorder="big")
5
6   def bytes_to_int(b):
7       return int.from_bytes(b, byteorder="big")

Listing 4-3
Integer/Byte Conversion

重要的

因为 RSA 处理的是整数,而不是字节,所以默认实现会丢失前导零。就整数而言,01 和 1 是同一个数。如果您的字节序列以任意数量的零开头,它们将无法通过加密/解密。对于我们的例子,我们发送文本,所以它永远不会是一个问题。然而,对于二进制数据传输,它可能是。这个问题用填充就解决了。

EATSA 现在拥有了创建一个简单的 RSA 加密/解密应用所需的所有组件。在查看清单 4-4 中的代码之前,尝试创建自己的版本。

  1   # FOR TRAINING USE ONLY! DO NOT USE THIS FOR REAL CRYPTOGRAPHY
  2
  3   import gmpy2, os, binascii
  4   from cryptography.hazmat.backends import default_backend
  5   from cryptography.hazmat.primitives.asymmetric import rsa
  6   from cryptography.hazmat.primitives import serialization
  7
  8   #### DANGER ####
  9   # The following RSA encryption and decryption is
 10   # completely unsafe and terribly broken. DO NOT USE
 11   # for anything other than the practice exercise
 12   ################
 13   def simple_rsa_encrypt(m, publickey):
 14       numbers = publickey.public_numbers()
 15       return gmpy2.powmod(m, numbers.e, numbers.n)
 16
 17   def simple_rsa_decrypt(c, privatekey):
 18       numbers = privatekey.private_numbers()
 19       return gmpy2.powmod(c, numbers.d, numbers.public_numbers.n)
 20   #### DANGER ####
 21
 22   def int_to_bytes(i):
 23       # i might be a gmpy2 big integer; convert back to a Python int
 24       i = int(i)
 25       return i.to_bytes((i.bit_length()+7)//8, byteorder="big")
 26
 27   def bytes_to_int(b):
 28       return int.from_bytes(b, byteorder="big")

 29
 30   def main():
 31       public_key_file = None
 32       private_key_file = None
 33       public_key = None
 34       private_key = None
 35       while True:
 36           print("Simple RSA Crypto")
 37           print("--------------------")
 38           print("\tprviate key file: {}".format(private_key_file))
 39           print("\tpublic key file: {}".format(public_key_file))
 40           print("\t1\. Encrypt Message.")
 41           print("\t2\. Decrypt Message.")
 42           print("\t3\. Load public key file.")
 43           print("\t4\. Load private key file.")
 44           print("\t5\.  Create and load new public and private key files.")
 45           print("\t6\. Quit.\n")
 46           choice = input(" >> ")
 47           if choice == '1':
 48               if not public_key:
 49                   print("\nNo public key loaded\n")
 50               else:
 51                   message = input("\nPlaintext: ").encode()
 52                   message_as_int = bytes_to_int(message)
 53                   cipher_as_int = simple_rsa_encrypt(message_as_int, public_key)
 54                   cipher = int_to_bytes(cipher_as_int)
 55                   print("\nCiphertext (hexlified): {}\n".format(binascii.hexlify(cipher)))
 56           elif choice == '2':
 57               if not private_key:
 58                   print("\nNo private key loaded\n")
 59               else:
 60                    cipher_hex = input("\nCiphertext (hexlified): ").encode()
 61                   cipher = binascii.unhexlify(cipher_hex)
 62                   cipher_as_int = bytes_to_int(cipher)
 63                   message_as_int = simple_rsa_decrypt(cipher_as_int, private_key)
 64                   message = int_to_bytes(message_as_int)
 65                   print("\nPlaintext: {}\n".format(message))
 66           elif choice == '3':
 67               public_key_file_temp = input("\nEnter public key file: ")
 68               if not os.path.exists(public_key_file_temp):
 69                   print("File {} does not exist.")
 70               else:
 71                   with open(public_key_file_temp, "rb") as public_key_file_object:
 72                       public_key = serialization.load_pem_public_key(
 73                                        public_key_file_object.read(),
 74                                        backend=default_backend())
 75                       public_key_file = public_key_file_temp

 76                       print("\nPublic Key file loaded.\n")
 77
 78                       # unload private key if any
 79                       private_key_file = None
 80                       private_key = None
 81           elif choice == '4':
 82               private_key_file_temp = input("\nEnter private key file: ")
 83               if not os.path.exists(private_key_file_temp):
 84                   print("File {} does not exist.")
 85               else:
 86                   with open(private_key_file_temp, "rb") as private_key_file_object:
 87                       private_key = serialization.load_pem_private_key(
 88                                        private_key_file_object.read(),
 89                                        backend = default_backend(),
 90                                        password = None)
 91                       private_key_file = private_key_file_temp
 92                       print("\nPrivate Key file loaded.\n")
 93
 94                       # load public key for private key
 95                       # (unload previous public key if any)
 96                       public_key = private_key.public_key()
 97                       public_key_file = None
 98           elif choice == '5':
 99               private_key_file_temp = input("\nEnter a file name for new private key: ")
100               public_key_file_temp = input("\nEnter a file name for a new public key: ")
101               if os.path.exists(private_key_file_temp) or os.path.exists(public_key_file_temp):
102                   print("File already exists.")
103               else:
104                   with open(private_key_file_temp, "wb+") as private_key_file_obj:
105                       with open(public_key_file_temp, "wb+") as public_key_file_obj:
106
107                           private_key = rsa.generate_private_key(
108                                             public_exponent =65537,
109                                             key_size =2048,
110                                             backend = default_backend()
111                                         )
112                           public_key = private_key.public_key()
113
114                           private_key_bytes = private_key.private_bytes(
115                               encoding=serialization.Encoding.PEM,
116                               format=serialization.PrivateFormat.TraditionalOpenSSL,

117                               encryption_algorithm=serialization.NoEncryption()
118                           )
119                           private_key_file_obj.write(private_key_bytes)
120                           public_key_bytes = public_key.public_bytes(
121                               encoding=serialization.Encoding.PEM,
122                               format=serialization.PublicFormat.SubjectPublicKeyInfo
123                           )
124                           public_key_file_obj.write(public_key_bytes)
125
126                           public_key_file = None
127                           private_key_file = private_key_file_temp
128           elif choice == '6':
129               print("\n\nTerminating. This program will self destruct in 5 seconds.\n")
130               break
131           else:
132               print("\n\nUnknown option {}.\n".format(choice))
133
134   if __name__ == '__main__':
135       main()

Listing 4-4RSA Done Simply

在我们一起练习之前,花几分钟时间自己尝试一下这个练习。顺便注意,因为公钥可以从私钥派生出来,所以加载私钥的同时也加载了公钥。

当你准备好了,继续读!你可能想不时地回头参考清单 4-4 。我们随后的许多清单将重用这些导入和函数定义。为了节省空间,我们一般不会重印它们,所以这个列表也是一个有用的模板。

练习 4.1。简单 RSA 加密

使用前面的应用,建立从 Alice 到 Bob 的通信,然后从 Alice 向 Bob 发送一些加密的消息进行解密。

填充发件箱

一旦 EATSA 建立了 RSA 加密应用,他们就把它交给 Alice 和 Bob,并命令他们开始这项任务。爱丽丝将渗透到 WAGGS,并发送更新给鲍勃。爱丽丝和鲍勃首先需要做什么?

公钥/私钥对的神奇之处在于,为了让 Alice 向 Bob 发送安全消息,它们在分开之前不需要就任何事情达成一致! 3 只要爱丽丝知道去哪里找,鲍勃就可以在任何地方向她的发布公钥。他可以把它登在报纸上,在电话里背诵给她听,或者在环绕西南极洲飞行的固特异飞艇上宣传它。关键是。如果西南极洲反情报部门看到了也没关系:他们将无法解密爱丽丝的信息。

正确

爱丽丝离开 EATSA 总部,穿过边境,来到西南极洲城市,在那里她渗透到 WAGGS。当她从事秘密烹饪活动时,Bob 生成了一对公钥/私钥。他保留私钥并公布公钥给 Alice 看。

让我们跟着走。启动代表 Bob 版本的应用实例,并选择选项 5,这会生成新的密钥对并将它们保存到磁盘。完成后,您将有两个可以在编辑器中检查的文件。

看一下公钥文件(在出现提示时为它选择了名称)。它的内容应该是这样的:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGFr+NV3cMu2pdl+i52J
XkYwwSHgZvA0FyIPsZ/rp6Ts5iBTkpymt7cf+cQCQro4FSw+udVt4A8wvZcppnBZ
h+17ZZ6ZZfj0LCr/3sJw8QfZwuaX5TZxFbJDxWWwsR4jLHsiGsPNf7nzExn7yCSQ
sXLNqc+mLKP3Ud9ta14bTQ59dZIKKDHVGlQ1iLlhjcE1dhOAjWlsdCVfE+L/bSQk
Ld9dWKCM57y5tiMsoqnVjl28XcsSuiOd4QPGITprsX0jb7/p/rzXc9OQHHGyAQzs
WTAbZNaQxf9AY1AhE4wgMVwhnrxJA2g+DpY1yXUapOIH/hpD0sMH56IGcMx9oV/y
SwIDAQAB
-----END PUBLIC KEY-----

那是一个 PEM 格式的公钥。恭喜你!鲍勃可以拿着这把钥匙,在分类广告栏里把它发表给西南极洲的一家报纸。

与此同时,爱丽丝一直在仔细观察西南极洲的政客们喜欢吃什么。多么不像南极人!当她看着他们吃着热狗热巧克力时,她心想。然后,回头看了一眼手中的报纸,她找到了她一直在寻找的分类广告!公钥已经到了!她小心翼翼地将它复制到一个文件中,现在已经能够为 Bob 的眼睛加密信息了。

接下来,让我们将刚刚生成的公钥复制到一个新文件中。这代表 Alice 从分类广告中复制文本后创建的文件。现在启动应用的一个新实例,它代表程序的 Alice 副本。选择选项 3 来加载她的公钥。

爱丽丝需要给鲍勃发回一条消息。这是我们计划中的选项 1。运行它,选择选项 1,并在明文字段中输入文本“热狗”。加密信息突然出现。 4 如果您使用前面的公钥,您将得到以下输出:

Plaintext: hot dogs

Ciphertext (hexlified): b'56d5586cab1764fae575bc5815115f1c5d759
daddccbd6c9cb4a077026e2616dfca756ffa7733538e66997f06ebbbb853028
3926383a6bb80b7145990a29236d042048eed8eb7607bd35fcafe3dadd5d60a
1f8694192bddedac5728061234ffbb7a407155844a7e79b3dbc9704df0de818
d24acad32ccd6d2afe2d0734199c76e5c5c770fa8c3c208eceae00554aa2f29
9a8510121d388d85f35fa49c08f3e9d7540f22fe5eb4ea15da5f387dbdd0e00
6710aa9031b885094773ef3329cde91dbede53ed77b96483d34daa4fedbf5bc
d95e95b6b482a7decbf47fe2df0e309d706ab9c73ce73a2bdef33b786dd12e9
8a9ce34bbc1847f36e13ae9eea4007b616'

我们再来一次,但这次是“热巧克力”如果您使用我们向您展示的前面的公钥这样做,您将得到这个输出(但是继续使用您自己生成的公钥):

Plaintext: hot chocolate

Ciphertext (hexlified): b'4d1e544e71c4cb15636ef4b0d629294538a05
979db762952cc5f0fc494f71535dff326dbb8543d0f2ace51a2279f65c2a76b
2a5ca5a3ee151e65e516afcb1d4da9ca9871dc7ce1dd4361a3b49def05c5089
99f5fab81b869b251ba8694fb171ab56ca1cde7cef0ac3934da4c28f7bfbb65
b03afa9cff30db974f0bd4fb8dee7fac75c99cd4def94ca8de83d46fffa092a
90642c9cfbfbf07c371f5aa3a62dc997d20e9959fcbec7dd0b434709b679619
ea195008a9a12eaa7462ffdbe8e6f765dd86b21f0f1d9b8b2b523ca7f11785e
fc6da84ec717bd1f0e2191e5a3bef74e489b5e396c49bd8f222ccd89984dbec
8b5e4cbb23ba739637d3307bca4e9f57e7'

同样,Alice 不能解密这些消息,即使她自己加密了它们:她没有私钥。至少,理论是这么告诉他们的。

她对自己的“可食用间谍”充满信心,带着这些信息,通过一只不安全的企鹅把它们发送给鲍勃。Bob 收到消息并重新加载他的应用。首先,他使用选项 4 加载私钥文件,然后选择选项 2 尝试解密。果不其然,当他把消息复制给爱丽丝时,它正确地解密了:

Ciphertext (hexlified): 56 d5586cab1764fae575bc5815115f1c5d759da
ddccbd6c9cb4a077026e2616dfca756ffa7733538e66997f06ebbbb85302839
26383a6bb80b7145990a29236d042048eed8eb760735fcafe3dadd5d60a1f86
94192bddedac5728061234ffbb7a407155844a7e79b3dbc9704df0de818d24a
cad32ccd6d2afe2d0734199c76e5c5c770fa8c3c208eceae00554aa2f299a85
10121d388d85f35fa49c08f3e9d7540f22fe5eb4ea15da5f387dbdd0e006710
aa9031b885094773ef3329cde91dbede53ed77b96483d34daa4fedbf5bcd95e
95b6b482a7decbf47fe2df0e309d706ab9c73ce73a2bdef33b786dd12e98a9c
e34bbc1847f36e13ae9eea4007b616

Plaintext: b'hot dogs'

“热狗!”鲍勃惊呼道。“不光彩!”

Ciphertext (hexlified): 4d1e544e71c4cb15636ef4b0d629294538a05979
db762952cc5f0fc494f71535dff326dbb8543d0f2ace51a2279f65c2a76b2a5c
a5a3ee151e65e516afcb1d4da9ca9871dc7ce1dd4361a3b49def05c508999f5f
ab81b869b251ba8694fb171ab56ca1cde7cef0ac3934da4c28f7bfbb65b03afa
9cff30db974f0bd4fb8dee7fac75c99cd4def94ca8de83d46fffa092a90642c9
cfbfbf07c371f5aa3a62dc997d20e9959fcbec7dd0b434709b679619ea195008
a9a12eaa7462ffdbe8e6f765dd86b21f0f1d9b8b2b523ca7f11785efc6da84ec
717bd1f0e2191e5a3bef74e489b5e396c49bd8f222ccd89984dbec8b5e4cbb23
ba739637d3307bca4e9f57e7

Plaintext: b'hot chocolate'

鲍勃的眼睛眯了起来。“热巧克力?!他们没有羞耻心吗?!"

到目前为止,一切顺利!鲍勃收到了爱丽丝的信息。它们被特工伊芙·瓦克截获了,但她应该读不出来,尽管她也有公钥。如果爱丽丝不能阅读她自己的信息,为什么夏娃可以?

爱丽丝和鲍勃不知道的是,夏娃即将造成各种破坏。在本章的其余部分,我们将介绍 RSA 可能受到攻击的一些方式以及如何正确应对。但是首先,练习!

练习 4.2。谁鲍勃。是你吗?

假设 Eve 的角色,想象你知道 Alice 和 Bob 操作的一切,除了私钥。也就是说,假设你知道分类广告,载体企鹅,甚至加密程序。他们的方案通过使用非对称加密得到了加强,但是仍然容易受到 MITM(中间人)攻击。伊芙如何定位自己,让她可以欺骗爱丽丝发送伊芙可以解密的信息,而鲍勃只能从伊芙而不是爱丽丝那里收到假信息?

练习 4.3。生命,宇宙,一切的答案是什么?

我们已经在前一章讨论过选择明文攻击。这里可以使用相同的攻击。再次承担伊夫的作用,古怪的代理人。你在报纸上截获了鲍勃的公开密钥,你可以进入 RSA 加密程序。如果你怀疑你知道爱丽丝在她的加密信息中发送了什么,解释或演示你将如何验证你的猜测。

非对称加密有何不同?

正如您在本节中已经了解到的,RSA 是非对称加密的一个例子。如果您之前没有听说过非对称加密,希望您刚刚完成的练习已经让您了解了关键概念。现在让我们明确一些事情。

在对称加密中,有一个单独的共享密钥对消息进行加密和解密。这意味着任何有能力创建加密消息的人都有同样的能力解密相同的消息。给某人解密对称加密的消息的权力而不给他们加密同类消息的能力是不可能的,反之亦然。

在非对称加密中,总有一个绝对不能公开的私钥和一个可以广泛公开的公钥。密钥对到底能做什么取决于算法。在本章中,我们一直关注 RSA 加密。我们将在本节中作为一个具体的例子来回顾 RSA 的运算,但是请记住,它们可能不适用于其他非对称算法和运算。

具体来说,RSA 支持非对称加密方案,在该方案中,您可以使用一个密钥来加密消息,而使用另一个密钥来解密消息。通常,任一密钥都可以充当任一角色:私钥可以加密可以被公钥解密的消息,反之亦然。当然,对于 RSA,一个密钥显然是私有密钥,因为公共密钥可以从私有密钥中导出,而不是相反。有 RSA 私钥而没有与之匹配的公钥是不可能的。因此,一个密钥被明确地指定为“私有的”,另一个是“公共的”

受适当保护的 RSA 私钥和足够健壮的协议的拥有者可以出于两个目的使用非对称加密:

  1. 加密收存箱:任何有公钥的人都可以加密一条消息,并将其发送给私钥的所有者。只有拥有私钥的人才能解密这条消息。

  2. 签名:任何有公钥的人都可以解密用私钥加密的消息。这显然无助于保密(任何人都可以解密该消息),但它有助于证明发送者的身份,或者至少发送者拥有私钥;否则,他们就不能加密一个可用公钥解密的消息。这是一个加密签名的例子,我们稍后会谈到。

注意:RSA 加密小东西

我们现在正在学习的加密 dropbox 操作几乎从未被用来以这种方式发送完整的消息。RSA 加密最常用的方式(同样,它正在被淘汰)是加密一个对称密钥,以便从一方传输到另一方。这是另一个概念,我们将留到下一章讨论。

RSA 加密的非对称本质的真正奇妙之处在于,双方不需要见面就可以开始交换消息。在我们的例子中,Alice 和 Bob 不需要一起创建任何共享密钥。爱丽丝甚至不需要认识鲍勃。只要 Alice 有 Bob 的公钥,她就可以加密只有 Bob 能读懂的消息。

不幸的是,只为一个人加密的能力并不是现实生活中唯一重要的事情。如练习中所示,非对称加密的优势也是其弱点。没有任何先前互动的交流能力也意味着,在没有额外信息的情况下,没有办法知道你正在与正确的人交流。

如果你已经完成了前面的练习,你会发现对于疯子来说,通过截取信息和密钥欺骗双方来读取和修改爱丽丝和鲍勃之间的通信是非常简单的。

  1. 他们可以通过截取和修改报纸上公布的公钥来欺骗爱丽丝。通过插入他们自己的公钥——爱丽丝现在误认为是鲍勃的——他们可以读取爱丽丝发送给鲍勃的所有信息。没有附加信息,Alice 无法知道公钥已经被泄露。

  2. 然后,他们可以通过阻止 Alice 不正确加密的消息到达 Bob,并向他发送用正确的公钥加密的假消息来欺骗 Bob,他们截获了这些消息。没有附加信息,Bob 无法知道是谁在发送消息。

这是对称密钥和非对称密钥之间的一个关键区别。事实上,一些密码学家区分“秘密的”对称密钥和“私有的”非对称密钥。两个人可以共享一个秘密,但是只有一个人知道他们自己的私钥。这在实践中意味着,如果对称密钥对双方都是保密的,那么它可以用来确定你正在与正确的人(即,与你创建共享密钥的人)交谈,而非对称密钥则不能。 6

让我们暂时回避这个问题,留待以后解决,因为在证书的上下文中确实讨论了这个问题的解决方案。

传递填料

回想一下前面的内容,EATSA 选择在没有任何填充的情况下实现 RSA。他们真的不应该那样做;这是一个相当严重的错误。事实上,它是如此严重,以至于cryptography模块甚至不允许你用 RSA 无填充加密!

那么,什么是填充,为什么填充如此重要?

解释这一点的最佳方式是演示如何读取用公钥加密的消息,即使您没有私钥,只要这些消息没有被填充。另一个很好的练习是在互联网上搜索 RSA 填充攻击。使用无填充明文有许多问题。

确定性输出

先说最基本的问题。RSA 本身就是一种确定性算法。这意味着,给定相同的密钥和消息,您将总是得到相同的密文,一个字节一个字节地。回想一下,我们在 AES 之类的对称密钥加密算法上也有同样的问题。有必要使用初始化向量(IV) 来防止确定性输出。你还记得为什么确定性输出如此糟糕吗?

确定性输出的问题是,它们使被动窃听者(如 Eve)能够进行一些密码逆向工程。因为加密是确定性的,如果伊芙知道 m 加密到 c ,那么任何时候伊芙看到 c 她就知道明文是什么。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1

如果 RSA 的输出是确定性的,那么发现明文和对应密文之间映射的对手可以将其记录到查找表中以备后用。这个图看起来眼熟吗?

Eve 既有公钥又有算法(你永远不能假设一个密码算法是秘密的)。她可以加密任意数量的潜在消息,并存储预加密值的查找表。图 4-1 看着眼熟吗?我们在第三章中展示了同样的图像,来讨论对称密码的 ECB 模式及其存在的问题。

但是确定性非对称加密会更糟。与对称加密不同,我们必须假设对手拥有(公共)密钥。在我们假设的南极冲突中,Eve 可能会发现,或者简单地猜测,Alice 正在根据她对自助餐厅的监视发送信息。如果她试图通过列出房间内发现的东西来加密几百个单词(例如,在自助餐厅吃饭的政治家的名字、谈话的主题和正在吃的食物),一旦她加密了“热狗”或“热巧克力”,加密的值就会与在返回给 Bob 的消息中截取的内容完全匹配。对于像这样的短消息,尤其是如果 EA Intelligence 总是用小写字母写单词,那么只有不到 3 亿条 8 个字符长的消息可以尝试。创建这么多消息的密文表并不太麻烦。使用这个查找表,Eve 可以相对快速地识别“热狗”。

即使夏娃不能猜出这个信息,仍然有各种各样的分析可以做。假设爱丽丝继续日复一日地发送同样的信息。虽然 Eve 可能无法解密这条消息,但她仍然能够自信地声明这是同一条消息。在前几章中,我们已经考虑了许多利用这种“信息泄露”的例子。

练习 4.4。强力 RSA

编写一个程序,使用蛮力解密一个 RSA 加密的全小写(无空格)少于四个字符的字。该程序应该将公钥和 RSA 加密的密文作为输入。使用 RSA 加密程序生成四个或更少字母的几个单词,并用您的暴力程序破解这些代码。

练习 4.5。等待是最难的部分

修改蛮力程序,尝试五个或更少字母的所有可能单词。测量暴力破解一个四个字母的单词和一个五个字母的单词所花费的时间(最坏情况下)。大约需要多长时间,为什么?尝试所有可能的六个字母单词需要多长时间?

练习 4.6。字典攻击

很明显,尝试所有可能的长度远大于四或五的小写 ASCII 单词将花费比你可能的注意力跨度更长的时间。但是我们在前面的章节中已经看到了同样的问题。让我们尝试相同的解决方案。修改你的强力程序,将字典作为输入来尝试任意的英语单词。

选择密文攻击

没有填充的 RSA 也容易受到所谓的“选择密文攻击”当你能让受害者代表你解密你选择的一些密文时,这种类型的攻击就起作用了。这听起来可能违背直觉。为什么有人会为你解密任何东西?例如,为什么鲍勃要为伊芙解密任何东西?

请记住,许多计算机安全都与心理学、诡计和人类思维有关。2].鲍勃在找什么?Bob 假设他正在解密来自 Alice 的可读信息。如果他收到了人类无法阅读的信息呢?例如,假设在解密一条消息(假设来自 Alice)时,他得到以下输出:

b'\xe8\xca\xe6\xe8'

完全有可能,这只是假设由于传输错误。这些事情在现实生活中无时无刻不在发生。这可能是一个小错误,或者是一只载体企鹅弄脏了墨水。Bob 可能会看到许多无法正确解密的消息。

鲍勃是做什么的?如果他没有很好的安全控制,他可能会把它扔掉。但是如果爱丽丝能渗透到敌人内部,它也能以另一种方式工作。你觉得哪个更容易被 Eve 弄到手?被发送到指挥链进行分析的绝密信息,还是被扔进垃圾桶的“不正确”信息?如果 Eve 在门卫工作人员中有自己的秘密特工,很有可能会得到丢弃的纸张或未完全销毁的数据。

让我们假设这个场景:Eve 可以向 Bob 发送任意的密文。出于我们的目的,Eve 看不到任何人类可读的消息,但是可以恢复被 Bob 丢弃的假定错误的消息,因为它们看起来毫无意义。

不幸的是,对于爱丽丝和鲍勃来说,伊芙可以用这个技巧解密爱丽丝发送回她基地的几乎所有信息。这个技巧背后的数学知识非常酷,在本章的多个例子中都有使用。所以让我们暂停一分钟来谈谈加密中的同态

加密同态的基本概念是,如果您对密文执行某种计算,结果会反映在明文中。不是所有的密码系统都具有同态性质,但 RSA 在一定程度上具有同态性质。在 RSA 中,我们将看到对密文进行乘法运算的方法会导致对明文进行乘法运算。目前还有其他一些特殊的同态加密技术正在开发中,这些技术使第三方能够在无法读取数据的情况下提供数据服务。你可能听说过其中的一些;如果没有,可以试着在网上搜索“同态加密”。这是非常有趣的东西。

虽然 RSA 不是一个同态加密方案,但这种乘法特性非常有趣(也造成了许多漏洞)。还记得代数课上说的(ac)(bc)=(abc)?模幂运算也是如此,如下式所示:

)

(4.3)

这个等式的任何部分看起来熟悉吗?回头看看(4.1)。你现在明白了吗?

任何时候我们在 RSA 中加密一个值( m ),最终都会得到memodn。在(4.3)的左侧,我们有两个加密*,一个是 m 1 一个是 m 2 ,两者都使用相同的公共指数 e 并且两者都取相同的模数 n 。*

在右手边,我们有一个单次加密的值 m 1m2。这个等式告诉我们的是,如果将这些单独加密的值相乘(mod n ),就可以得到相乘的加密结果!

换句话说,两个密文(在同一公钥下加密)的乘积解密为两个明文的乘积。在我们开始之前,请尝试自己完成以下练习。

练习 4.7。无填充 RSA 的同态性质

使用(4.3)将两个 RSA 加密的数字相乘,并解密结果以验证等式。

这个练习的代码非常简单,所以一定要先自己尝试一下。当你准备好了,我们的解决方案就在清单 4-5 中。

 1   # FOR TRAINING USE ONLY! DO NOT USE THIS FOR REAL CRYPTOGRAPHY
 2
 3   import gmpy2, sys, binascii, string, time
 4   from cryptography.hazmat.backends import default_backend
 5   from cryptography.hazmat.primitives import serialization
 6   from cryptography.hazmat.primitives.asymmetric import rsa
 7
 8   #### DANGER ####
 9   # The following RSA encryption and decryption is
10   # completely unsafe and terribly broken. DO NOT USE
11   # for anything other than the practice exercise
12   ################
13   def simple_rsa_encrypt(m, publickey):
14       numbers = publickey.public_numbers()
15       return gmpy2.powmod(m, numbers.e, numbers.n)
16
17   def simple_rsa_decrypt(c, privatekey):
18       numbers = privatekey.private_numbers()
19       return gmpy2.powmod(c, numbers.d, numbers.public_numbers.n)

20
21   private_key = rsa.generate_private_key(
22         public_exponent=65537,
23         key_size=2048,
24         backend=default_backend()
25   )
26   public_key = private_key.public_key()
27
28   n = public_key.public_numbers().n
29   a = 5
30   b = 10
31
32   encrypted_a = simple_rsa_encrypt(a, public_key)
33   encrypted_b = simple_rsa_encrypt(b, public_key)
34
35   encrypted_product = (encrypted_a * encrypted_b) % n
36
37   product = simple_rsa_decrypt(encrypted_product, private_key)
38   print("{} x {} = {}".format(a,b, product))

Listing 4-5Solution

如果这种数学没有太大的意义,在这一点上不要太担心它。即使你不完全确定它是如何工作的,也要试着理解它是如何被使用的。

回到我们当前的例子,假设 Eve 有一个通过 m 的 RSA 公钥加密获得的密文 c 。没有私钥,Eve 应该无法解密。想必鲍勃也不会为她解密。然而,如果他将解密它的一个倍数,伊芙就能恢复原来的。

对于我们的例子,让我们选择我们的倍数为 2。Eve 首先使用(4.1)和公钥加密 2,得到cr。

为清楚起见,我们称原始密文为c0。如果我们将 c 0c r (模 n )相乘,我们将得到一个新的密文,我们称之为 c 1

)

从(4.3)式可以看出,这是

)

那么 Eve 怎么用这个呢?假设伊芙截获了爱丽丝的一个密文 c 。Eve 将她计算的cr(同样,这只是在公钥下加密的值 2)然后将两个加密值相乘(模 n )。Eve 将这个新的密文发送给 Bob。

鲍勃接收到 c 1 并将其解密给 mr 并将整数转换成字节。他发现它不能解密成任何清晰可辨的东西,并认为某些东西在运输过程中被损坏了。他耸耸肩,把纸揉成一团,扔进了废纸篓。那天晚上晚些时候,伊芙的经纪人在垃圾桶里找到了那张皱巴巴的纸。她快速复制了一份,通过秘密的载体送回给伊芙。

夏娃现在有了先生,需要提取 m 。没问题。她选择 r 为 2。在熟悉的算法中,你将除以 r 得到 m。但是在用模运算做这个算术的时候,你必须使用一个不同的逆运算:r1(modn)。幸运的是,有一些库可以为我们计算这类数字,比如gmpy2

r_inv_modulo_n = gmpy2.powmod(r, -1, n)

练习 4.8。夏娃的门徒

重现夏娃选择的密文攻击。像前面一样,用 Python 创建一个示例消息,使用公钥对其进行加密。然后,加密一个值 r (比如 2)。将密文的两个数字版本相乘,不要忘记取模 n 的答案。解密这个新的密文,并尝试将其转换为字节。它不应该是人类可读的东西。取这个解密的数字版本,乘以 r (mod n )的倒数。你应该回到原来的数字。将其转换为字节以查看原始消息。

共模攻击

没有填充的 RSA 的另一个问题是“共模”攻击。回想一下, n 参数是模数,包含在公钥和私钥中。出于超出本书范围的数学原因,如果相同的 RSA 消息由两个不同的公钥加密,并且具有相同的 n 模数,那么该消息可以在没有私钥的情况下被解密。

在选择密文的例子中,我们详细地研究了数学,因为它可以相对容易地描述,也因为它对于多重攻击是至关重要的。对于这个例子,为了简单和节省空间,我们不会进入数学细节。相反,使用清单 4-6 中的代码来测试和探索攻击。如果你对数学的细节感兴趣,你可以阅读 Hinek 和 Lam 的“对小私有指数 RSA 和一些快速变体的共模攻击(实践)”。

 1   # Partial Listing: Some Assembly Required
 2
 3   # Derived From: https://github.com/a0xnirudh/Exploits-and-Scripts/tree/master/RSA At tacks
 4   def common_modulus_decrypt(c1, c2, key1, key2):
 5       key1_numbers = key1.public_numbers()
 6       key2_numbers = key2.public_numbers()
 7
 8       if key1_numbers.n != key2_numbers.n:
 9           raise ValueError("Common modulus attack requires a common modulus")

10       n = key1_numbers.n
11
12       if key1_numbers.e == key2_numbers.e:
13           raise ValueError("Common modulus attack requires different public exponents")
14
15       e1, e2 = key1_numbers.e, key2_numbers.e
16       num1, num2 = min(e1, e2), max(e1, e2)
17
18       while num2 != 0:
19           num1, num2 = num2, num1 % num2
20       gcd = num1
21
22       a = gmpy2.invert(key1_numbers.e, key2_numbers.e)
23       b = float(gcd - (a*e1))/float(e2)
24
25       i = gmpy2.invert(c2, n)
26       mx = pow(c1, a, n)
27       my = pow(i, int(-b), n)
28       return mx * my % n

Listing 4-6Common Modulus

注意,为了测试这种攻击,您需要两个具有相同模数( n 值)和不同公共指数( e 值)的公钥。回想一下 e 建议总是 65537。但是很明显,在这个例子中,你不会对两个键都使用它。

如何创建公钥?到目前为止,在我们所有的例子中,我们要么生成新的键,要么从磁盘加载它们。

回想一下, ne定义了公钥。其他一切都只是为了方便而包装。cryptography模块提供了一个 API,用于直接从这些值创建一个键。RSA 私钥对象有一个名为private_numbers的方法,RSA 公钥对象有一个名为public_numbers的方法。这些方法返回带有数据元素的数据结构,如 nde 。这些“数字”对象也可以用来创建关键对象。

在清单 4-7 中,我们生成一个私钥,然后手动创建另一个具有相同模数和不同公共指数的密钥。

 1   # Partial Listing: Some Assembly Required
 2
 3   private_key1 = rsa.generate_private_key(
 4       public_exponent =65537,
 5       key_size=2048,
 6       backend = default_backend()
 7   )
 8   public_key1 = private_key1.public_key()
 9
10   n = public_key1.public_numbers().n
11   public_key2 = rsa.RSAPublicNumbers(3, n).public_key(default_backend())

Listing 4-7Common Modulus Key Generation

现在,您应该有了测试这种攻击所需的所有 Python 代码。

此时,您可能会问自己,“这种攻击有多实际?”为了实现它,你必须有相同的消息两个具有相同模数的密钥下加密。为什么同一条消息会在两个不同的密钥下被加密两次,为什么两个不同的密钥会有相同的模数?

在处理密码学的时候,千万不要依赖这种思维。如果有办法利用加密技术,坏人就会想出办法来利用它。让我们首先考虑如何用两个不同的密钥加密相同的消息。

一种可能性是让 Alice 相信已经创建了新的公钥,并且她需要进行交换。如果我们控制了新的公钥,我们可以给她一个我们选择的具有 ne 值的密钥。

但是如果我们能控制她的密钥,为什么我们需要使用共模攻击呢?为什么不直接给她一个我们创建的公钥,并且我们有配对的私钥呢?

的确,一个新的私钥/公钥对将允许 Eve 在将来解密 Alice 发送的任何消息。但是通用模数攻击将允许 Eve 潜在地确定在过去发送的一些消息。在我们的例子中,爱丽丝渗透进自助餐厅,食物服务可能有规律地重复。事实上,正如我们之前所讨论的,即使 Eve 不能解密,她也已经可以知道相同的消息是否被重发。如果 Eve 观察到相同的消息被一遍又一遍地发送,则共模攻击提供了关于发送内容的历史以及关于将来发送的消息的信息的更大视图。

练习 4.9。共模攻击

通过创建一个通用模数攻击演示来测试本节中的代码。

练习 4.10。常见模数用例

写出一个附加场景,说明使用共模攻击可能对攻击者有用。

证据就在衬垫里

正如我们刚刚演示的,这种非常原始的 RSA 形式,有时被称为“教科书式 RSA”,相对容易被破解。有两个关键问题。正如我们已经看到的,教科书 RSA 的一个问题是输出是确定的。这使得需要对同一消息加密两次的普通模数攻击变得更加容易。

也许更大的问题是这些信息的可塑性有多大。我们在前一章讨论了对称加密的可扩展性。对于 RSA,我们有类似的问题,例如,将 RSA 密文相乘并得到一个可解密的值。

尝试加密微小的消息也有潜在的问题,比如我们在练习中加密的一些小消息。除了练习中的强力方法之外,还有一些方法可以破解较小的消息,特别是使用较小的公共指数(例如, e = 3)。

为了减少或消除这些问题,RSA 的实际使用总是利用填充有随机元素的填充。RSA 填充在加密前通过我们一直在处理的原始 RSA 计算应用于明文消息。填充确保消息不会太小,并提供一定的结构来降低延展性。此外,随机化元素的操作与对称加密的 IV 没有什么不同:良好的随机化填充确保 RSA 加密操作生成的每个密文(即使对于相同的明文)都是唯一的(概率非常高)。

没有填充的 RSA 足够危险,cryptography模块甚至没有无填充的 RSA 操作。你应该非常清楚,你不能在没有填充的情况下使用 RSA 进行加密。虽然cryptography模块不允许这样做,但其他库允许。值得注意的是,这包括 OpenSSL。

在撰写本文时,通常使用两种填充方案。旧的方案被称为 PKCS #1 v1.5,另一个是 OAEP,代表最佳非对称加密填充。清单 4-8 中所示的cryptography模块可以使用这些填充方案中的任何一种。

 1   from cryptography.hazmat.backends import default_backend
 2   from cryptography.hazmat.primitives.asymmetric import rsa
 3   from cryptography.hazmat.primitives import serialization
 4   from cryptography.hazmat.primitives import hashes
 5   from cryptography.hazmat.primitives.asymmetric import padding
 6
 7   def main():
 8       message = b'test'
 9
10       private_key = rsa.generate_private_key(
11             public_exponent =65537,
12             key_size=2048,
13             backend=default_backend()
14         )
15       public_key = private_key.public_key()
16
17       ciphertext1 = public_key.encrypt(
18           message,
19           padding.OAEP(
20               mgf = padding.MGF1(algorithm = hashes.SHA256()),

21               algorithm = hashes.SHA256(),
22               label = None # rarely used. Just leave it 'None'
23           )
24       )
25
26       ###
27       # WARNING: PKCS #1 v1.5 is obsolete and has vulnerabilities
28       # DO NOT USE EXCEPT WITH LEGACY PROTOCOLS

29       ciphertext2 = public_key.encrypt(
30           message,
31           padding.PKCS1v15()
32       )
33
34       recovered1 = private_key.decrypt(
35       ciphertext1,
36       padding.OAEP(
37           mgf=padding.MGF1(algorithm=hashes.SHA256()),
38           algorithm=hashes.SHA256(),
39           label=None # rarely used.Just leave it 'None'
40       ))
41
42       recovered2 = private_key.decrypt(
43       ciphertext2,
44        padding.PKCS1v15()
45     )
46
47       print("Plaintext: {}".format(message))
48       print("Ciphertext with PKCS #1 v1.5 padding(hexlified): {}".format(ciphertext1.hex()))
49       print("Ciphertext with OAEP padding (hexlified): {}".format(ciphertext2.hex()))
50       print("Recovered 1: {}".format(recovered1))
51       print("Recovered 2: {}".format(recovered2))
52
53   if __name__=="__main__":
54       main()

Listing 4-8RSA Padding

如果您重复运行这个演示脚本,您会发现两种填充方案的密文都会导致输出在每次时发生变化。因此,像 Eve 这样的对手既不能执行选择密文攻击,也不能执行本章前面演示的共模攻击。她也无法使用 RSA 的确定性加密来分析消息模式、频率等等。

填充还解决了加密过程中丢失前导零的问题。填充确保输入总是固定的大小:模数的比特大小。因此,例如,使用填充,模数大小为 2048 的 RSA 加密的输入将始终是 256 字节(2048 位)。因为输出的大小是已知的,所以它还允许明文以前导零开始。不管组合消息是否以 0 开始,已知的大小意味着可以附加零,直到达到正确的大小。

所以现在一切都好了,对吗?Alice 和 Bob 将切换到使用填充,Eve 将被关在他们的通信之外?

首先,请注意填充不能解决中间人或认证问题。Eve 仍然可以截获并更改公钥,从而完全解密 Alice 的消息。鲍勃仍然不知道是谁在给他发信息。这些问题将在下一章讨论。

其次,敏锐的读者可能注意到了源代码清单中的警告。以防你没有注意就浏览了一遍,我们将再次强调它。

警告:对 PKCS 1 号说“不”

不要使用 PKCS #1 v1.5,除非你必须这样做以兼容传统协议。它已经过时并且存在漏洞(包括我们将在下一节测试的一个漏洞)!对于加密,尽可能使用 OAEP。

在离开这一节之前,关于 OAEP 的使用,还有两个评论是适当的:

  1. 你可能已经注意到 OAEP 的“标签”参数。这很少使用,通常可以保留为None。使用标签不会增加安全性,所以现在忽略它。

  2. OAEP 要求使用哈希算法。在这个例子中,我们使用了 SHA-256。为什么不是 SHA-1?这与 SHA-1 已知的弱点有关吗?不。事实上,没有已知的针对 OAEP 的攻击依赖于 SHA-1 的弱点。因为 SHA-1 被认为是过时的,所以在编写自己的代码时最好不要使用它,但是如果出于兼容性原因或者为了维护别人的代码而不得不将 OAEP 与 SHA-1 一起使用,那么在撰写本文时,它还不知道是否不如 SHA-256 安全。

练习 4.11。获得升级

帮助爱丽丝和鲍勃。重写 RSA 加密/解密程序,使用cryptography模块代替gmpy2操作。

利用 PKCS #1 v1.5 填充的 RSA 加密

这一部分将会令人兴奋和有趣!Eve 不是密码学家,而你——因为你正在读这本书——可能也不是密码专家。然而,你和伊芙将要实施一个由杰出的密码学家设计的攻击,并用它来破解爱丽丝和鲍勃的密码。

这次攻击不仅好玩,而且非常真实。它不仅在过去是一种真正的攻击,而且今天仍然被用来攻击配置不佳的 TLS 服务器。它既是历史的又是当代的。

这篇论文是由 Daniel Bleichenbacher 撰写的“针对基于 RSA 加密标准 PKCS #1 的协议的选择密文攻击”[2]。您可以在网上找到这篇论文,一些读者可能对攻击背后的数学原理感兴趣。在接下来的章节中,我们将通过这篇文章创建一个攻击的实现。同时,我们会尝试给出某些关键概念背后的一些直觉。如果您发现深入的细节令人沮丧或不感兴趣,您应该能够忽略大部分解释,只需从源代码清单中整理出一个可用的 RSA 破解程序。我们不会被冒犯。

这个例子会有很多代码片段。您应该从清单 4-9 开始,它初始化了一些导入。不要忘记本章中我们已经看到的对其他函数的依赖。当我们处理新的片段时,将它们添加到这个框架中。

 1   from cryptography.hazmat.primitives.asymmetric import rsa, padding
 2   from cryptography.hazmat.primitives import serialization
 3   from cryptography.hazmat.primitives import hashes
 4   from cryptography.hazmat.backends import default_backend
 5
 6   import gmpy2
 7   from collections import namedtuple
 8
 9   Interval = namedtuple('Interval', ['a','b'])
10   # Imports and dependencies for RSA Oracle Attack
11   # Dependencies: simple_rsa_encrypt(), simple_rsa_decypt()
12   #                bytes_to_int()

Listing 4-9RSA Padding Oracle Attack

爱丽丝和鲍勃又在吵架了。不过,这一次,他们使用了带填充的 RSA。但是 EATSA 仍然在做错误的决定。他们决定使用 PKCS # 1 1.5 版,因为它不需要参数。最初他们打算使用 OAEP,但东南极洲工作队为现代操作 RSA 就业和更好的加密,特别是在外地(EATMOREBEEF)显然争论了几个星期的工作队名称。时间紧迫,无法就哪种哈希算法应该用于 OAEP,以及“EATMOREBEEF”是否应该用于标签达成一致,他们举手说,“我们非常确定 PKCS #1 v1.5 足够好了。”

我们再一次发现爱丽丝在西南极洲监视她的邻居。然而,这一次,爱丽丝假扮成一家制冰公司的首席执行官,在南极洲西部城市的一次会议上与制冰行业的其他高管会面。在过去的几年里,冰毒的销售已经融化,而政府面临着自身的资产冻结和流动性下降的问题,既不能也不愿意提供补贴。爱丽丝的任务是继续明确反对当前执政党的不同意见,试图在下次选举中巩固影响力。

会议结束后,Alice 需要向 Bob 发送一份她已说服向反对党大量捐款的首席执行官的报告。爱丽丝使用 PKCS #1 v1.5 的 RSA 传输以下消息:“简·温特斯、f·罗·曾和约翰·怀特。”

爱丽丝迅速拿出一部翻盖手机(他们在技术上正慢慢赶上来…还没有智能手机,但他们最终摆脱了企鹅)。她把信息输入给 Bob,它自动把它转换成一个数字,加密,然后发送出去。几秒钟后,她的手机震动了,出现了一条新信息:

Received: OK

在城市的其他地方,伊芙观察着这种交流。她从穿越边境开始就一直在追踪爱丽丝。但是她不能解密这些信息。爱丽丝甚至带着已经安装在手机里的公钥来了,所以伊芙也不能给她一个假密钥。她能做什么?

对 Eve 来说幸运的是,她通过自己的情报机构发现 Alice 和 Bob 正在使用 PKCS #1 v1.5 进行 RSA 填充。伊芙很惊讶。在经历了本章前面的所有事件之后,Eve 已经对 RSA 有了相当多的了解,她知道这种填充方案有已知的漏洞。她想知道他们为什么要用它。他们没收到备忘录吗?

伊芙拿了一份布莱肯巴赫的报纸,开始阅读。该白皮书解释说,PKCS #1 v1.5 填充可以被类似于我们在上一章中看到的 oracle 攻击破坏。

在这种情况下,Eve 需要一个神谕来告诉她一个给定的密文(一个数字)是否可以解密成具有适当填充的东西。神谕当然不会告诉她密文解密到了什么;关于填充,它只需要说“是”或“否”。

幸运的是,Eve 一直在监控 EA 的通讯,看起来他们在他们的技术中建立了一个错误报告系统。当 Alice 发送有效消息时,她会返回

Received: OK

但是当 Eve 发送一个随机数(密文)时,她几乎总能得到回复

Failed: Padding

在发送了成千上万个随机数之后,她最终还是收到了一个回复“OK”的消息。据她所知,这不是一个“真正的”消息(人类可读的,或者 Bob 理解的),但它确实有自动处理系统报告的正确填充。

这是夏娃的神谕。这是她完全解密密文信息所需要的。

为了方便编写她的攻击程序,Eve 将首先用自己生成的私钥破解本地加密的消息。Eve 将使用可插拔的 oracle 配置,以便在攻击 Bob 时,她可以简单地关闭用于支持攻击的 oracle。测试 oracle 使用真实私钥解密消息,并检查消息是否具有正确的格式。

伊芙开始阅读 PKCS 1.5 版,并开始尝试自己的实验。她创建了自己的密钥对,用填充符加密消息,然后检查输出。她加密消息“test ”,然后在不移除填充符的情况下解密消息*。清单 4-10 显示了她使用的代码的关键片段。*

 1   # Partial Listing: Some Assembly Required
 2
 3   from cryptography.hazmat.primitives.asymmetric import rsa, padding
 4   from cryptography.hazmat.primitives import hashes
 5   from cryptography.hazmat.backends import default_backend
 6   import gmpy2
 7
 8   # Dependencies: int_to_bytes(), bytes_to_int(), and simple_rsa_decrypt()
 9
10   private_key = rsa.generate_private_key(
11         public_exponent=65537,
12         key_size=2048,
13         backend=default_backend()
14     )
15   public_key = private_key.public_key()
16
17   message = b'test'
18
19   ###
20   # WARNING: PKCS #1 v1.5 is obsolete and has vulnerabilities
21   # DO NOT USE EXCEPT WITH LEGACY PROTOCOLS
22   ciphertext = public_key.encrypt(
23       message,
24       padding.PKCS1v15()
25   )
26
27   ciphertext_as_int = bytes_to_int(ciphertext)
28   recovered_as_int = simple_rsa_decrypt(ciphertext_as_int, private_key)
29   recovered = int_to_bytes(recovered_as_int)
30
31   print("Plaintext: {}".format(message))
32   print("Recovered: {}".format(recovered))

Listing 4-10Encrypt with Padding

您可以看到她正在使用cryptography模块创建加密。但是她使用自己的simple_rsa_decrypt操作进行解密,以便保留填充。

这是她所看到的:

Plaintext: b'test'
Recovered: b'\x02@&\x1cC\xb1\xe4\x0f\x14\xd9\x93oU
\x07\x1b\xfdC\xe1\xe2K\xeeP\xdd\x8b\x10\xf9cZJ\x0c
42\x8e\xbblZ\xfb\x80\x8b\xfcA?p\xac\xba\xf7I\x9e\x
11\x1cn&t\xb8\x15\xbfo\xfe\xcc\xdf\xe7=\xc2\x9e\x
ca<v\xcd\x9ep\xd8\x1c\xf6b2"\x8c\xc0\x1e\xb8\xdb\x
97\x89\xfauj\x8f``\x99m~,\x18h\xc2k6d~qr-\x0c\xb9\
xfe?\xf9\xf9\xa6o\x05\\ZV\xfd4?\x0e;y\xf3\xd3q\xb2
\x94\xf6\xf8~a\xc1eA\xe4\x14\xce\x82\xdcc\xbf4e\xa
e\xa3<"\xcb,L\xd8\xed\xca}\xeb\x82\xa67\x1a\xd1\xc
7)\x13\xc1D)\xe8\x05h\xbe/\x97\xdf>\xf0\xef\xeb\xe
4Q\xc2\x85(*\xdcE\x9ct\x08c0\xb1\x80la\x94_/2\xd4y
\xc7\x95\x01\x90@\xea\x92\xaa\xb8\x18!\xc7\xff\xab
\x03\xea\x8b\xa3\xb4\xf6\xf2\xd6GH\x98-fM\x1c\x99\
x84\x8d4\xaf"\x95\xa7XR(M\x836\xd4\x17\x99m\xa8\x1
a\xb3\x00test'

Eve 注意到实际的消息在填充的末尾,符合 PKCS #1 v1.5 标准。(在本节的其余部分,我们将只说“PKCS”)

她注意到恢复文本的第一个字节是 2。她觉得这很奇怪,因为标准规定填充应该以 0 和 2 开始。最初的 0 去哪了?

然后夏娃记得!当然可以!因为 RSA 处理的是整数而不是字节,所以任何前导零都会被删除。幸运的是,当使用 RSA 填充时,字节的大小固定为密钥的大小。Eve 决定用清单 4-11 中所示的可选参数 8 来更新她的转换函数。

 1   # Partial Listing: Some Assembly Required
 2
 3   # RSA Oracle Attack Component
 4   def int_to_bytes(i, min_size = None):
 5       # i might be a gmpy2 big integer; convert back to a Python int
 6       i = int(i)
 7       b = i.to_bytes((i.bit_length()+7)//8, byteorder="big")
 8       if min_size != None and len(b) < min_size:
 9           b = b'\x00'*(min_size-len(b)) + b
10       return b

Listing 4-11Integer to Bytes

现在适当地更新,Eve 写了她的“假”神谕,她将只用于测试。清单 4-12 中的代码执行简单的 RSA 解密,将结果转换为字节(使用我们刚刚实现的最小大小参数),并检查第一个和第二个字节是否分别为 0 和 2。确保新的int_to_bytes正常工作。旧版本将总是删除前导零,oracle 将总是报告错误。

 1   # Partial Listing: Some Assembly Required
 2
 3   # RSA Oracle Attack Component
 4   class FakeOracle:
 5       def __init__(self, private_key):
 6           self.private_key = private_key
 7
 8       def __call__(self, cipher_text):
 9           recovered_as_int = simple_rsa_decrypt(cipher_text, self.private_key)
10           recovered = int_to_bytes(recovered_as_int, self.private_key.key_size //8)
11           return recovered [0:2] == bytes([0, 2])

Listing 4-12
Fake Oracle

有了神谕,Eve 准备攻击论文中描述的算法。该算法分四步描述。我们将逐个审查每一个,并逐步开发代码。

第一步:失明

Bleichenbacher 的算法要求设置和“隐蔽”消息的隐蔽步骤。但是,算法末尾的备注部分解释说,对于我们的情况来说,这大部分是不必要的:

  • 如果 c 已经是符合 PKCS 的(即当 c 是加密的消息时),可以跳过步骤 1。在这种情况下,我们设置s0←1。

在这一步中有三个值需要配置。因为我们正在处理一个已经用 PKCS 填充的加密消息,所以我们只需要将这些值设置为规定的默认值:

)

因为s0= 1,我们可以将第一次赋值简化为

)

显然,1 的任何次方仍然只是 1,所以无论是幂还是模都没有任何影响。

M 参数将会是一个区间列表的列表(稍后会有更多关于区间的内容)。该算法包括由 i 标识的重复步骤。 M 0 记录由 i = 1 标识的步骤中标识的区间列表。在这种情况下,只有单个区间[2 B ,3B–1]。

什么是 B ?正如本文前面所解释的, B 是具有适当填充的合法值的数量。它被定义为

)

基本上, k 是以字节为单位的密钥大小。因此,如果我们使用 2048 位密钥,k = 256。但是为什么要减去 2 呢?

让我们这样分解它。对于带填充的 RSA,我们的明文大小(以字节为单位)应该总是与密钥大小相同。如果我们使用 2048 位的密钥,我们的填充明文也必须是 2048 位(256 字节)。这意味着有 2 个 2048 个可能的明文值。

不过,这不是真的,对吧?我们知道,前两个字节必须是 0 和 2,这将合法值的数量减少了 2 × 8 = 16 位。因此, B 是在考虑前两个固定字节时该密钥大小的最大值。

回到区间,2 B 和 3 B 是什么?该数据结构中的区间代表实际明文消息所在的 PKCS 数的合法值。因为开头的字节是最高有效字节,所以 0 对整数没有影响(例如,0020 = 20)。但是 2 意味着任何合法的数字必须至少是 2 B 但是必须小于 3 B

这么想吧。如果我告诉你一个两位数的数字必须在 20 和 30 之间,你会知道它可能有 10 个可能的值。而且,你知道最小值是 2 × 10。这是同样的想法。

这种算法的工作方式是通过缩小合法区间,直到它只是一个单一的数字。这个数字就是明文信息!

Eve 决定为算法的每一步创建一个函数。假设有状态数据需要在这些函数之间共享(例如, BM 等)。),她决定使用一个类来存储状态。构造函数接受一个公钥和一个预言。请记住,oracle 只是将密文作为输入,如果密文解密为适当的 PKCS 填充明文,则返回 true。

现在,Eve 为算法的这一步(步骤 1)编写代码。该步骤需要一个密文作为输入( c ,并初始化c0、 BsM 的值。Eve 还在一个名为_step1_blinding的便利函数中从公钥中复制出 n ,如清单 4-13 所示。

 1   # Partial Listing: Some Assembly Required
 2
 3   class RSAOracleAttacker:
 4       def __init__(self, public_key, oracle):
 5           self.public_key = public_key
 6           self.oracle = oracle
 7
 8       def _step1_blinding(self, c):
 9           self.c0 = c
10
11           self.B = 2**(self.public_key.key_size-16)
12           self.s = [1]
13           self.M = [ [Interval(2*self.B, (3*self.B)-1)] ]
14
15           self.i = 1
16           self.n = self.public_key.public_numbers().n

Listing 4-13RSA Oracle Attack: Step 1

B 的值直接从位计算,而不是从字节转换。其他一切都是按照论文中描述的那样精确计算的。

这段代码中的Interval数据结构是使用collections.namedtuple工厂创建的。它的两个值是 a (下限)和 b (上限)。

步骤 2:搜索符合 PKCS 的邮件

对于这一节,我们需要从乘法 RSA 密文中重新学习数学。花一点时间复习(4.3)。

从概念上讲,步骤 2 是在MI–1间隔内搜索新的符合 PKCS 的消息,这些消息是原始明文消息 m 和某个其他整数 s i 的倍数。

图 4-2 描绘了所有可能的 RSA 密文值内的 PKCS 一致性空间的(简化)视图。RSA 加密的输出范围从 0 到 2k–1,其中 k 是以位为单位的密钥大小。不管密钥大小如何,每个数字(十六进制)都以 0 到f的 16 位数字中的 1 开始。2 和 3 之间突出显示的部分表示具有适当 PKCS 填充的 RSA 密文值。(这个视图过于简化,因为在现实中,正确的切片应该是从00ff范围内的从 02 到 03,所以它实际上只是 256 个切片中的 1 个。)

消息空间显示为一个环的原因是因为我们正在处理模块化(回绕)算法。如果你在这个空间内取两个数,并把它们相乘(取模 n ),如果乘积大于 n ,它就绕回。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-2

PKCS 共形空间的简化视图

这让我们回到将明文消息 m 乘以另一个数字。在图 4-2 的简化视图中, m 一定在高亮区域的某处。如果我们使用模乘,用特定的数乘以 m 会产生同样在同一区域内的其他数。

当然,我们不知道确切的在哪里 m 因为我们所有的都是加密版本 c 。我们所知道的是,因为它符合 PKCS,所以它在这个区域内的某个地方。同样,因为我们不知道 m 在哪里,我们也不知道 m 的倍数会落在环中的什么地方。当然,例外的是,使用我们的神谕,我们可以确定多次波是否落在 PKCS 整合区域内!

然后,使用神谕,我们将搜索一个 s i 值,当该值乘以 m (模 n )时,是 PKCS 一致的,因此在 RSA 消息空间的 PKCS 一致区域内。我们仍然不知道 m 在哪里,但是知道它有一个倍数落在某个区域内会引入对包含它的区间的额外约束。我们将在第 3 步中详细讨论这些约束以及如何使用它们。不过现在,还是先找 s i

Bleichenbacher 将寻找 s i 分成三个子步骤:

  1. 2 a 开始搜索是我们第一次做这个操作(即当 i = 1)。

  2. 2 b

  3. 2 c 剩余一个区间搜索用于只有一个区间且 i 不为 1 时。其他情况应该都是这样。

这些子步骤中的每一个都需要搜索一系列可能的 s i 值,以查看它是否产生一致的密文。

具体来说,对于每个候选人sI,我们用 RSA 加密产生 c i

)

我们将加密的 s i 值乘以我们的原始密文 c 0 来创建一个测试密码 c t 。因为 c 0 是未知明文的加密m09,我们得到

)

我们向 oracle 发送 c t 来测试它是否符合。对于我们的假甲骨文,它只是简单地用私钥解密 c t 并检查明文是否以字节 0 和 2 开头。(记住,要破解 Alice 的消息,我们不会有一个支持私钥的 oracle。相反,我们将把密文发送给 Bob,并检查填充错误消息响应。)

因为每个子步骤都需要能够以这种方式检查一系列的 s i 值,所以 Eve 决定创建一个助手函数来执行搜索。它接受一个起始值和一个可选的包含上限(如清单 4-14 )。

 1   # Partial Listing: Some Assembly Required
 2
 3   # RSA Oracle Attack Component, part of class RSAOracleAttacker
 4       def _find_s(self, start_s, s_max = None):
 5           si = start_s
 6           ci = simple_rsa_encrypt(si, self.public_key)
 7       while not self.oracle((self.c0 * ci) % self.n):
 8           si += 1
 9           if s_max and (si > s_max):
10               return None
11           ci = simple_rsa_encrypt(si, self.public_key)
12       return si

Listing 4-14Find “s”

使用这个助手函数,前两个子步骤非常简单。步骤 2 a 需要测试sIn/(3B)的所有值,直到其中一个值符合为止。Eve 对这个步骤进行编码,如清单 4-15 所示。

1   # Partial Listing: Some Assembly Required
2
3   # RSA Oracle Attack Component, part of class RSAOracleAttacker
4       def _step2a_start_the_searching(self):
5           si = self._find_s(start_s=gmpy2.c_div(self.n, 3*self.B))
6           return si

Listing 4-15Step 2a

请注意,使用来自gmpy2模块的c_div函数,起始 s 值被计算为 n /(3 B )。因为我们正在处理如此大的数字,所以我们不能信任 Python 的内置浮点。我们正在计算的许多值只是范围,并不保证是整数,因此小数值是可能的。gmpy2模块为我们提供了对非常大的数字的快速运算,包括浮点。

c_div函数本身提供了向上舍入到上限的除法。因此,例如,c_div(3,4)计算 3/4 并向上舍入,返回 1。

使用这些 RSA 概念,该步骤搜索将 c 乘以另一个 PKCS 一致性值的 s i 的值。具体来说,对于一个 s i 的候选值,我们 RSA 加密,然后乘以原始密文。我们使用上限是因为 s i 必须是一个整数,并且必须大于或等于初始值。无论起始值是否为整数,下一个整数(即上限)都是 s i 的起点。

子步骤 2 b 也相当容易做到。该子步骤处理罕见的情况,其中 m 0 的间隔被一分为二。当这种情况发生时,我们向前迭代 s i ,直到我们找到另一个符合的值(清单 4-16 )。

1   # Partial Listing: Some Assembly Required
2
3   # RSA Oracle Attack Component, part of class RSAOracleAttacker
4       def _step2b_searching_with_more_than_one_interval(self):
5       si = self._find_s(start_s=self.s[-1]+1)
6       return si

Listing 4-16Step 2b

我们将保存在self.s数组中找到的每个值,以便能够访问这些值。事实上,我们只需要前一个值,但是我们使用这个习语来匹配论文的写作方式。

最后,最后一个子步骤,2 c ,稍微复杂一点。它需要在一系列可能的值中搜索 s 。回想一下,在上一步中只找到了一个区间,我们将下限作为 a ,上限作为 b 。接下来,我们必须迭代通过 r i 值:

)

我们用这些rI 的值来绑定两边的 s i 搜索:

)

我们在这里所做的是挑选特定范围内的 s i 值,这将帮助我们继续缩小解决方案的范围。Bleichenbacher 在他的论文中解释了为什么这些界限有效,我们在这里不再重复他的评论。当我们谈到第 3 步时,我们将给出整个算法的一些进一步的直觉,这将有助于澄清正在发生的事情。

同时,Eve 将这个算法编码为清单 4-17 。

 1   # Partial Listing: Some Assembly Required
 2
 3   # RSA Oracle Attack Component, part of class RSAOracleAttacker
 4       def _step2c_searching_with_one_interval_left(self):
 5           a,b = self.M[-1][0]
 6           ri = gmpy2.c_div(2*(b*self.s[-1] - 2*self.B),self.n)
 7           si = None
 8
 9           while si == None:
10               si = gmpy2.c_div((2*self.B+ri*self.n),b)
11
12               s_max = gmpy2.c_div((3*self.B+ri*self.n),a)

Listing 4-17Step 2c

13               si = self._find_s(start_s=si, s_max=s_max)
14               ri += 1
15           return si

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-3

描述布莱肯巴赫的攻击

与之前的计算一样,除法是使用gmpy2.c_div来处理的。这一点非常重要。如果只是使用 Python 的除法运算符,很可能会得到不完整的结果。

步骤 3:缩小解决方案的范围

一旦从步骤 2 中找到了一个 s i 值,我们就更新我们在 m 的位置上的界限。在讨论数学之前,我们先来讨论一下这个算法是怎么回事。

在图 4-3 中,我们再次可视化了包含合法 PKCS 填充值的 RSA 消息空间环的切片。这个空间的下限是从 000200 开始的数字…00,并且包含上限是 0002 FFFF。明文消息 m 0 在这里的某个地方*。在算法开始时,我们不知道在哪里。*

然而,对于我们发现一致的每个 s i 值,我们了解到新的值m*0sI也在该区域内(因为模运算而绕回)。我们知道m0sI(模 n )落在特定范围内的事实引入了关于 m 0 可以在哪里的新约束。我们能够使用这些约束来计算新的区间 ab ,在该区间内 m 0 必须是

一旦我们更新了界限,我们可以使用进一步收紧界限的sI的新值来重复该过程。最终,边界会将m0 限制为单一值。那个就是我们要找的明文!

希望这种直觉会有所帮助,即使下面的公式没有多大意义。或者,如果你真的试着去理解 Bleichenbacher 的论文,那会很有帮助。在任何情况下,我们计算新的上限和下限如下。

对于前一个M0 中的每个 ab (通常会有一个,但有时会有两个),找出 r 的所有整数值,使得

)

对于 abr 中的每一个值,我们计算一个新的间隔。首先,我们如下计算下限候选者:

)

和上限候选者

)

我们定义一个新的区间为[ max ( aa i ), min ( bb**I)]。

将所有音程的集合插入到MI 中。同样,通常只有一个间隔。

Eve 按照清单 4-18 对算法的这一步进行编码。

 1   # Partial Listing: Some Assembly Required
 2
 3   # RSA Oracle Attack Component, part of class RSAOracleAttacker
 4       def _step3_narrowing_set_of_solutions(self, si):
 5           new_intervals = set()
 6           for a,b in self.M[-1]:
 7               r_min = gmpy2.c_div((a*si - 3*self.B + 1),self.n)
 8               r_max = gmpy2.f_div((b*si - 2*self.B),self.n)
 9
10               for r in range(r_min, r_max+1):
11                   a_candidate = gmpy2.c_div((2*self.B+r*self.n),si)
12                   b_candidate = gmpy2.f_div((3*self.B-1+r*self.n),si)
13
14                   new_interval = Interval(max(a, a_candidate), min(b, b_candidate))
15                   new_intervals.add(new_interval)
16           new_intervals = list(new_intervals)
17           self.M.append(new_intervals)
18           self.s.append(si)
19
20           if len(new_intervals) == 1 and new_intervals[0].a == new_intervals[0].b:
21               return True
22           return False

Listing 4-18Step 3

在这段代码中,注意r_max是使用f_div计算的。这将计算舍入到地板而不是天花板的除法。我们使用这个值是因为 r 是一个整数,并且必须小于或等于这个值。

一旦计算出间隔,代码就将它们添加到self.M数据结构中,并将sI值添加到self.s中。

最后,它检查我们是否找到了解决方案。伊芙在这里想得太多了。这是第 4 步的一部分,但是放在这里更方便。

步骤 4:计算解决方案

正如前面几节所暗示的,这个算法有终止条件。希望,考虑到前面的讨论,这是相当明显的。也

  • MI 只包含一个音程,或者

  • M i 的区间上下限相同。

简而言之,当限制 m 的位置的区间减少到一个数字时,我们终止。

我们已经在步骤 3 的末尾看到了 Eve 检查这个条件的代码。Bleichenbacher 的步骤 4 也处理了一个比我们的更普遍的问题,包括当 s 0 为 1 时不必要的步骤。回想一下,为了处理明文已经被 PKCS 填充的 RSA 加密消息, s 0 被设置为 1。

尽管有些不必要,但为了完整性和一致性,Eve 确实为步骤 4 创建了一个方法(清单 4-19 )。

1   # Partial Listing: Some Assembly Required
2
3   # RSA Oracle Attack Component, part of class RSAOracleAttacker
4       def _step4_computing_the_solution(self):
5           interval = self.M[-1][0]
6           return interval.a

Listing 4-19Step 4

就这样!这就是整个算法!Eve 将这些步骤组合成清单 4-20 的攻击方法。

 1   # Partial Listing: Some Assembly Required
 2
 3   # RSA Oracle Attack Component, part of class RSAOracleAttacker
 4       def attack(self, c):
 5           self._step1_blinding(c)
 6
 7           # do this until there is one interval left
 8           finished = False
 9           while not finished:
10               if self.i == 1:
11                   si = self._step2a_start_the_searching()
12               elif len(self.M[ -1]) > 1:
13                   si = self._step2b_searching_with_more_than_one_interval()
14               elif len(self.M[-1]) == 1:
15                   interval = self.M[-1][0]
16                   si = self._step2c_searching_with_one_interval_left()
17
18               finished = self._step3_narrowing_set_of_solutions(si)
19               self.i += 1
20
21           m = self._step4_computing_the_solution()
22           return m

Listing 4-20Attack!

请注意,attack()方法的输入是密文,但它必须已经是整数形式。别忘了先调用密文上的bytes_to_int()

练习 4.12。发动进攻!

使用前面的代码,运行一些用 PKCS 填充破解 RSA 加密的实验。你应该使用cryptography模块来创建加密的消息,将加密的消息转换成整数,然后使用你的攻击程序(和假 oracle)来破解加密。首先,在大小为 512 的 RSA 密钥上测试你的程序。这将更快地中断,并使您能够更快地验证您的代码。

练习 4.13。花时间

攻击需要多长时间?使用计时检查和 oracle 函数调用次数的计数来检测您的代码。对一组输入运行攻击,并确定破解大小为 512、1024 和 2048 的密钥所需的平均时间。

练习 4.14。保持最新状态

尽管这种攻击已经有 20 多年的历史了,但它仍继续困扰着互联网。做一些谷歌搜索,找出这种攻击的当前状态,包括预防和更新的变种。一定要弄清楚机器人袭击的事。这个我们讨论 TLS 的时候再讲。

关于 RSA 的附加说明

在这一章中,我们在 RSA 上花了很多时间,我们甚至还没有深入了解它在实践中的实际应用。像大多数非对称密码一样,RSA 几乎从未被用来加密信息,就像我们在本章中让 Alice 和 Bob 做的那样。当使用它时,它通常用于加密一个对称密码或签名的会话密钥。

然而,理解非对称密码的工作原理以及如何破解它们是至关重要的。尽管有这些缺点,RSA 仍然被广泛使用,而且经常是不正确的。浏览本章中的漏洞应该有助于您走上正确的道路。

这里有几个其他项目需要考虑。

密钥管理

和所有的密码一样,它们的安全性很大程度上取决于正确地创建和保护密钥。

创建 RSA 密钥时,请确保使用库。不要试图自己生成公钥和私钥。同时,留意你所使用的库的任何错误报告。例如,已经发现一些库在没有足够随机性的情况下生成 RSA 私钥,从而产生易受各种攻击的私钥。您不可能预料到所有会出错的事情,或者您使用的库或算法何时会暴露为易受攻击的,因此您必须通过更新已知的漏洞来“维护”您的加密技术。

漏洞可能是特定于系统的。例如,ROCA 漏洞主要局限于某些硬件芯片。

创建 RSA 密钥时,使用正确的参数也很重要。密钥大小通常应该至少为 2048 位,除非遗留约束迫使您选择更小的值。并且公共指数 e 的值应该总是 65537。

您还必须小心地守护和保护私钥及其秘密。显然,私钥本身应该安全地存储,并具有适当的权限。您的私钥至少应该以绝对最小的权限存储在文件系统中。非常敏感的密钥可能需要脱机存储。

您还应该考虑以加密形式存储私钥。这将需要一个密码来解密密钥,这在全自动系统中有其自身的困难。但是,如果使用得当,如果攻击者获得主机系统的访问权限,它可以降低私钥泄露的风险。

此外,私钥由许多组成值组成。在我们的例子中,我们可以认为 d 是私钥,因为这是我们用来实际解密的值。但是除了 d 之外,还必须小心不要暴露用来生成它的秘密。例如,模数 n 本身并不是秘密,而是产生它的两个大素数 pq

创建私钥时会生成额外的值,如果泄露,会危及安全性。与 pq 一起,这些值在密钥生成后并不是严格必需的,因为一切都可以从 edn 中计算出来。然而,大多数库确实将它们作为私钥的一部分保存在内存和磁盘上。您应该阅读关于私钥生成的库文档,并遵循推荐的处理过程。

非对称加密的弱点之一是无法“撤销”私钥。如果 Bob 的私钥泄露,Alice 如何知道停止发送在相关公钥下加密的数据?在实践中,您的 RSA 密钥可能会与证书一起使用,证书可以包括一个证书和密钥的层次结构,允许一些密钥不如其他密钥敏感,还包括一个过期日期,以限制泄露的密钥。关于这一点,其他地方说得更多。

练习 4.15。分解 RSA 密钥

在本节中,我们建议使用 2048 位密钥。在这个练习中,在互联网上搜索一下,找出当前容易被分解的键的大小。例如,搜索“保理服务”,看看保理一个 512 位的密钥要花多少钱。

练习 4.16。ROCA 脆弱键

除非您的 RSA 密钥是由某些 RSA 硬件模块生成的,否则您为本章练习生成的密钥应该不会受到 ROCA 的攻击,但检查一下也无妨。在这个练习中,访问在线 ROCA 漏洞检查网站 https://keychest.net/roca#/ 并测试几个关键点。

算法参数

如果有什么东西是你应该从本章中吸取的,那就是这个:特别注意 RSA 的填充参数。在撰写本文时,您应该对加密操作使用 OAEP 填充方案,对签名使用 PSS 填充方案。不要不要使用 PKCS #1 v1.5,除非它对于遗留应用是绝对必要的。

量子密码术

在本书中,我们没有足够的篇幅来深入研究量子密码术,但是我们不能在不提及 RSA 的情况下结束对它的讨论。当量子计算到来时,我们目前的大多数非对称算法都将变得不可破解。RSA 已经很容易受到一些当代攻击,但当量子计算变得可行时,它将被彻底打破。因此,在未来十年左右,RSA 将完全无用。

非常短的附录

如果从这一章中可以得出什么,那就是:参数很重要,正确的实现是微妙的,并且随着时间的推移而发展。非对称加密如何工作以及如何使用的直觉很容易解释,但是有许多细节可以使一种实现安全,而另一种实现非常容易受到攻击。

为工作选择正确的工具,并为工具选择正确的参数。

哦,砸东西很有趣!

他们仍然可以伪装成她发送假消息,但对称加密也是可能的。

2

在 PKI 发明之后,它当然变得流行起来。

3

Bob 不一定知道它们来自谁,但这是一个独立的(也是非常有趣的)问题。至少他会知道他是唯一能读懂它们的人。

4

消息显示为其字节的十六进制表示,以便于选择和粘贴到其他地方。

5

还记得克霍夫原理吗?又来了!

6

除非公钥保证是秘密的,但是这样我们就在某种程度上挫败了非对称密钥的目的,因为我们需要一个安全的共享通道来进行密钥交换。

7

选择密文攻击(CCA)比我们在这里讨论的空间要复杂得多。请认为我们的 CCA 讨论过于简单。如果你想了解更多关于 CCA 和 CCA 下的不可区分性,Matthew Green 博士有一些很棒的博客文章[7]。

8

在大多数源代码中,因为大小是固定的,所以它被指定为预期的大小,代码检查以确保它不会太大。

9

我们只是把这个 m ,但是为了把它与c0 值联系起来,我们将把它称为m0。

*

五、消息完整性、签名和证书

在这一章中,我们将讨论“密钥哈希”以及如何使用非对称加密技术通过数字签名不仅提供消息隐私,还提供消息完整性和 ?? 真实性。我们还将讨论证书与密钥有何不同,以及这种区别为何如此重要。让我们深入一个例子和一些代码!

过于简单的消息认证码(MAC)

在爱丽丝和鲍勃的报道中,我们的东南极间谍二人组最近在西部敌对领土的冒险中遇到了一些麻烦。显然,伊芙设法截获了他们之间的一些通信。这些用对称加密法加密的信息是不可读的,但是 Eve 想出了如何改变它们,插入一些错误的指令和信息。爱丽丝和鲍勃根据错误的情报采取行动,差点中了埋伏。幸运的是,由于全球变暖,一堆冰融化了,他们设法游回了安全的地方!

他们很快从千钧一发中吸取教训,在总部花了一点时间擦干身体,设计新的通信机制,以防止他们的加密数据被未经授权的修改。

最终,东南极洲真相间谍机构(EATSA)发现了一个新概念:“消息认证码”或“MAC”。

Alice 和 Bob 被告知,MAC 是与消息一起传输的任何“代码”或数据,可以对其进行评估以确定消息是否被更改。这是出于直觉目的的非正式定义。在 Alice 和 Bob 学习这个介绍性的错误起点时,请耐心等待。这个过于简单的 MAC 的基本思想是这样的:

  1. 发送方使用函数f(M1)为给定的消息M1 计算代码C1。

  2. 发送者将M?? 1 和C1 发送给接收者。

  3. 接收者以 MC 的形式接收数据,但不知道它们是否已被修改。

  4. 接收者重新计算 f ( M )并将输出与 C 进行比较,以验证消息未被更改。

假设夏娃截获了爱丽丝发给鲍勃的 M 1C 1 。如果 Eve 想要将消息 M 1 更改为 M 2 ,她必须同时重新计算C2=f(M2)并发送 M 2否则,由于 f ( M )和 C 不匹配,Bob 将会检测到某些东西已经被改变。

如果你在问,“那又怎样?Eve 可以重新计算 MAC,对吗?”那么你就看到了我们过于简单的设置的问题。我们必须假设 Eve 拥有除键之外的所有东西*,但是这个例子也假设她没有 f 。我们会尽快解决这个问题。敬请关注!*

现在,Alice 和 Bob 只是假设 Eve 不会计算,或者很容易计算函数 f 。如果这个假设是真的(事实并非如此),那么几乎任何创建指纹的机制都可以工作。东南极洲间谍机构决定将消息哈希作为消息的附件发送。因此,在这种情况下,MAC 是一个哈希。

让我们深入一些代码,看看这个简单的想法是如何形成的。当我们这样做的时候,我们可以把我们新的假 MAC 技术和第三章的对称加密结合起来。这在清单 5-1 中有所展示。

 1   # THIS IS NOT SECURE. DO NOT USE THIS!!!
 2   from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
 3   from cryptography.hazmat.backends import default_backend
 4   import os, hashlib
 5
 6   class Encryptor:
 7       def __init__ (self, key, nonce):
 8           aesContext = Cipher(algorithms.AES(key),
 9                                modes.CTR(nonce),
10                                backend=default_backend())
11           self.encryptor = aesContext.encryptor()
12           self.hasher = hashlib.sha256()
13
14       def update_encryptor(self, plaintext):
15           ciphertext = self.encryptor.update(plaintext)
16           self.hasher.update(ciphertext)
17           return ciphertext
18
19       def finalize_encryptor(self):
20           return self.encryptor.finalize() + self.hasher.digest()
21
22   key = os.urandom(32)
23   nonce = os.urandom(16)
24   manager = Encryptor(key, nonce)
25   ciphertext = manager.update_encryptor(b"Hi Bob, this is Alice !")
26   ciphertext += manager.finalize_encryptor()

Listing 5-1Fake MAC with Symmetric Encryption

回想一下,“计数器模式”不需要填充,在我们前面的例子中,“finalize”函数实际上没做什么。但是现在,当我们完成我们的管理器时,它不仅完成了加密,还返回了计算出的哈希,作为附加到加密数据的最后几个字节。因此,最终的加密消息将我们的简单 MAC 附加到它的末尾。

练习 5.1。信任但核实

完成简单加密加哈希系统的代码,并增加一个解密操作。解密操作应该在完成后重新计算密文的哈希,并将其与发送过来的哈希进行比较。如果哈希值不匹配,应该会引发一个异常。小心点!MAC 没有加密,不应该解密!如果您不仔细考虑这一点,您可能会解密不存在的数据!

练习 5.2。永远邪恶的夏娃

继续“拦截”一些由您在本节中编写的代码加密的消息。修改截获的消息,并验证您的解密机制是否正确地报告了错误。

麦克,HMAC 和 CBC-MAC

Alice 和 Bob 的支持人员告诉他们,任何验证消息的机制都是消息验证代码(MAC)。正如我们所暗示的,这不是一个完整的定义。真正的 MAC 也需要一个1

我们已经使用密钥进行加密,但是到目前为止,我们还没有在其他方面使用它们。正如您可能已经猜到的那样,MAC 密钥与加密根本没有关系。相反,它确保消息认证码只能由知道密钥的各方计算。

在我们的例子中,爱丽丝和鲍勃必须假设伊芙不会计算函数 f ( M )。当然,这是不合理的。艾丽丝和鲍勃利用 SHA-256 得到了一个指纹,所以显然伊芙也可以用它来计算她自己的认证码。假设她可以决定性地修改密文,正如我们在前一章中看到的,在某些情况下,她可以插入一条新消息一个新的假 MAC。

然而,真正的 MAC 依赖于一个密钥,不可能由 Eve 生成,除非她已经泄露了密钥!请记住,良好的安全性意味着除了密钥之外的一切都可以被知道,并且它仍然正常工作。 2

MAC 保护消息的完整性。没有密钥的攻击者无法不被察觉地更改数据。此外,如果密钥仍然保密,MAC 还提供真实性:接收者知道只有共享密钥的其他人才能发送 MAC,因为只有拥有密钥的人才能生成合法的 MAC。

虽然有许多 MAC 算法,我们将着眼于两个易于理解的方法:HMAC 和 CBC-MAC。这些算法在教授 MAC 如何工作以及为什么工作方面做得很好。它们在实践中也是有用的。

HMAC

HMAC 是一种“基于哈希的消息验证码”事实上,你已经知道 HMAC 最复杂的特征:哈希。HMAC 通常只是一个由键控的哈希。

“被键控”是什么意思?为了说明这一点,让我们首先回顾一下没有密钥的标准加密哈希。对于这样的哈希,如果输入不变,输出也不变。它们完全是确定性的,只基于一个输入:消息内容。如果你重温“谷歌知道!”在第二章中,你会回忆起我们实际上可以在 Google 中输入一些哈希值并找到匹配的输入。

打开 Python shell,再测试一两次:

>>> import hashlib
>>> hashlib.sha256(b"hello world").hexdigest()
'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'
>>> hashlib.sha256(b"happy birthday").hexdigest()
'd7469a66c4bb97c09aa84e8536a85f1795761f5fe01ddc8139922b6236f4397d'

“hello world”和“happy birthday”的 SHA-256 输出在每台计算机上永远都是这些值。他们将永远不会改变。您可以通过自己运行代码来验证这一点。SHA-256 定义要求如此。你也可以尝试在线搜索哈希值。

重复一遍,使用非密钥算法,相同的输入总是产生相同的输出。

当一个算法被键控时,意味着输出依赖于输入和一个键。但是如何对哈希算法进行加密呢?

从概念上讲,这其实很容易。因为即使对哈希算法输入的微小改变也会完全改变输出,所以我们可以让密钥成为输入本身的一部分!

虽然下面的例子不是真正的 HMAC,并且不是被认为足够安全,但是它说明了这个想法:

>>> import hashlib
>>>
>>> password1 = b"CorrectHorseBatteryStaple" # See XKCD 936
>>> password2 = b"LiverKiteWorkerAgainst"
>>>
>>> # This is not really HMAC, it is for illustration ONLY:
>>> hashlib.sha256(password1 + b"hello world").hexdigest()
'ca7d4abd13bceb305eef2738e3592da77ed826aa1665ba684b80f36bd7522b32'
>>>
>>> hashlib.sha256(password2 + b"hello world").hexdigest()
'b22786bc894c8bb27d1e7e698a9bddfd6b95f35dcd063e37d764fa296216408a'

在这个例子中,我们使用人类可读的密码作为密钥。我们将输入的“hello world”哈希了两次,但每次都插入不同的密码作为前缀*。基本上,我们用密钥来改变我们所哈希的内容。每个密码会产生完全不同的输出,这意味着有人要重新创建消息“hello world”的输出 MAC 的唯一方法是也知道密码(或者通过暴力破解)。与任何其他加密算法一样,密钥/密码必须足够大并且足够随机。*

说到大小,值得注意的是,密码的大小并不是它如何有效地改变哈希输出的一个因素。你还记得雪崩原理吗?改变哈希函数的输入的单个位*会完全改变输出哈希值。您可以拥有一个万亿字节的文档,只改变其中的一个字符,并产生一个与未改变的文档的哈希无关的新哈希。类似地,您的密码可以是单个字符,它将有效地“扰乱”任何给定输入的输出,不管输入有多大。您需要担心的是,您的密码长度(和随机性)足够强大,可以防止暴力攻击。

练习 5.3。又是暴力

您应该已经在前面的章节中完成了一些强力攻击,但是重要的是重复这个练习,直到您对这个概念有了直觉。使用我们前面的假 HMAC,让计算机生成一个特定大小的随机密码,并使用暴力方法找出它是什么。更具体地说,假设你已经知道消息是什么(例如,“你好,世界”,“生日快乐”,或者你选择的消息)。编写一个程序来创建一个字符的随机密码,将密码添加到消息的前面,然后打印出 MAC(哈希)。获取输出,遍历所有可能的密码,直到找到正确的密码。从简单的单字母字符测试开始,然后尝试两个字符,以此类推。通过使用不同的字符集来混淆事物,例如全部小写、小写和大写,或者大写加数字,等等。

练习 5.4。暴力破解四字密码

重复之前的练习。但是不要用从一个字母来源中提取的字母,而要用从一个单词来源中提取的单词。查找或创建一个包含常用单词列表的文本文件。至少要 2000 字。使用这个字典,通过选择 n 个随机单词来创建密码。通过尝试字典中所有可能的组合,尝试暴力破解该密码。从 n = 1(一个字的密码)开始,从那里往上走。

即使前面的方法也不够好,所以让我们来谈谈真正的 HMAC。我们已经反复说过,仅仅预先设置密码是不够安全的。“HMAC”是标准文档“RFC 2104”中定义的算法的正式名称如果您以前从未看过 RFC,这些是来自互联网工程任务组(IETF)的文档,代表了互联网协议和算法的标准、最佳实践、实验和讨论。它们都是免费的,可以在网上找到。RFC 2104 可以在 https://tools.ietf.org/html/rfc2104 找到。

该文件的摘要指出:

  • 本文档描述了 HMAC,一种使用加密哈希函数进行消息认证的机制。HMAC 可以与任何迭代加密哈希函数(例如,MD5、SHA-1)结合秘密共享密钥一起使用。

这部分应该已经说得通了。我们已经做的实验使用了 SHA-256 和一个秘密共享密钥,但我们显然可以使用 SHA-1 或 MD5。不过,提醒一下,那些哈希算法被认为是“坏的”,除非必要,否则不应该用于遗留应用。

回到 RFC 的第 3 页,我们看到一旦选择了哈希函数 H ,输入文本的 HMAC 就被计算出来,因此

  • h(k xorg opad,h(k xorg ipad,文本))

让我们来看看这些术语。我们已经知道H;这是底层的哈希函数。术语“文本”指的是输入,但不必像任何“明文”消息那样由可读的文本字符组成:它可以是任意的二进制数据。哦,我们需要解决逗号。因为 H 是一个函数,您可能会认为这个定义显示了一个带两个参数的哈希函数。但是在 RFC 的这个定义中,逗号可以被认为是连接。和我们所有的例子一样,哈希函数只接受一个输入。

术语 K 指的是密钥,但不能只是任何东西。RFC 对密钥有许多要求,通常需要一些预处理。这些要求大多与 H 的块大小有关。回想一下第三章中的内容,我们在分组密码中使用了术语“块大小”来描述分组密码一次操作的数据大小。例如,AES 的块大小为 16 字节(128 位)。哈希算法可以哈希任何大小的输入,那么哈希算法的块大小是多少呢?

实际上,哈希函数通常一次对一个块进行操作,但是将一个块的哈希输出输入到下一个块的哈希计算中。例如,SHA-1 的块大小为 64 字节(512 位),而 SHA-256 的块大小为 128 字节(1024 位)。RFC 将 H 的块大小称为 B (字节)。

我们的密钥的第一个要求是,如果它比块大小 B个字节,它必须用零填充,直到它的长度为 B 个字节。

第二个要求是,如果密钥比 B 长*,那么首先通过用 H 哈希密钥来减少它。不要对此感到惊讶。我们将在一次 HMAC 操作中多次使用 H 。*

*综上所述,如果 K 太短就用零填充,如果 K 太长就用 H ( K )代替。

眼尖的读者会注意到,哈希的长度可能也可能比块大小短。SHA-1 的哈希是 20 字节长,它的块大小是 64 字节。SHA-256 的哈希是 32 字节长,但是它的块大小是 128 字节。在用哈希函数减少了太长的关键字之后,它通常会太短,然后将需要填充。

最后,我们应该有一个长度正好为 B 字节的密钥。

接下来,我们需要计算 K ⊕ ipad (XOR)。术语“ipad”代表“内部填充”,因为这是 HMAC 中的内部哈希操作。RFC 将 ipad 定义为“重复 B 次的字节 0x36”,将“opad”定义为“重复 B 次的字节 0x5c”。为 ipad 和 opad 选择的值是任意选取的。最重要的是它们是不同的。

填充的原因超出了本书的范围,但是它们给了 HMAC 一些额外的安全性,以防底层的哈希函数被破坏。因此,举例来说,这些填充使得 HMAC-MD5 相对较强,即使 MD5 已经被破解。这很有帮助,但不是对新应用使用 HMAC-MD5 的好理由。请不要。HMAC 的填充意味着 HMAC-SHA256 将是一个相当强大的 MAC,即使有人发现 SHA-256 哈希函数中的漏洞,这可以帮助保持现有的使用(可能不容易立即升级到更好的哈希函数)相对安全。

⊕ ipad 的计算非常简单,因为它们大小相同。随后的值被添加到输入“文本”的前面,组合的数据由 H 哈希。我们现在已经计算出了 H ( K ⊕ (ipad text))。同样,这是内部哈希计算。

现在,对于外部哈希,我们计算 K ⊕ opad。随后的值被添加到内部哈希的输出中,聚合的字节再次被哈希。外部函数的哈希是输入文本在 K 上的 HMAC。

幸运的是,加密库几乎总是将 HMAC 作为原语。

>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.hazmat.primitives import hashes, hmac
>>>
>>> key = b"CorrectHorseBatteryStaple"
>>> h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend())
>>> h.update(b"hello world")
>>> h.finalize().hex()
'd14110a202b607dc9243f83f5e0b1f4a1e59fba572fc5ea5f41d263dd4e78608'

为什么要大费周章去学习 HMAC 内部是如何工作的,而不仅仅是学习如何使用一个附带的库呢?有几个原因。首先,至少对事物如何运作有一点点了解是有好处的。它有助于直觉和推理何时使用它以及为什么使用它。

第二,也许是最重要的,它提醒你 YANAC(你不是一个密码学家…还没!).你一定要记住这个原则!尽可能多地使用加密库,不要试图想出自己的“聪明”算法。再看一眼 HMAC。它建立在一些与简单地在输入前加一个键相同的概念上,但是有更高的复杂性。这种复杂性来自更深层次和更微妙的目标,包括在哈希函数被破坏的情况下的前向安全性。这种复杂性不是任意的;HMAC 行动是基于密码学家的一篇研究论文,该论文从数学上证明了某些安全特性。除非你是一名密码学家,发布你的作品(通常有正式的证明)供公众同行审查、测试和辩论,否则你真的不应该创建自己的算法,除非是为了教育或演示的目的。

练习 5.5。测试 Python 的 HMAC

虽然你不应该推出自己的加密,但这并不意味着你不应该验证实现!按照 RFC 2104 的说明创建您自己的 HMAC 实现,并用您的实现和 Python 的cryptography库的实现测试一些输入和键。确保它们产生相同的输出!

CBC-MAC 电脑

HMAC 是一个非常受欢迎的 MAC,例如在 TLS 中使用,但也有其他方法来创建 MAC。例如,我们可以将您在第三章中学到的关于密码块链接(CBC)模式的知识作为推导安全 MAC 的另一种方法。

让我们快速介绍一些新的术语。MAC 有时也称为“标签”当我们创建一个消息的 MAC 时,我们可以称之为该消息的“标签”;这就像一件礼物或一件衣服上的标签:它是附在主文章上的一点点信息。在数学符号中,标签通常表示为 t 。这样,一个 MAC over 消息 m 1 产生一个标签t1,该对( m 1 ,t 1 )被发送到接收方进行验证。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1

因为所有的消息都影响最后一个加密数据块的值,所以 C[n]是所有 P 的 MAC…有一些瑕疵。

回想一下,当使用 AES 加密时,我们一次只能加密 128 位。如果我们单独加密每个 128 位数据块,仍然有信息可能会“泄露”整个数据。例如,大的图像特征可能仍然是可识别的。解决这个问题的一个方法是“连锁”加密,这样来自一个块的输入就可以延续并影响下一个块的加密。换句话说,开始时一个比特的变化会产生级联效应,一直到最后一个块。

换句话说,密文的最后一个块由链中每隔一个块的值决定:输入中任何地方的任何变化都将反映在最后一个块中!这使得 CBC 加密模式的最后一个块成为整个数据的 MAC,如图 5-1 所示。

希望您已经从本书中学到了这一点,所有的加密技术都有限制和关键参数。和 HMAC 一样,我们将首先做一些简单的例子,看看 CBC-MAC 算法背后的基本概念以及简单的方法是如何被利用的。

让我们从获取一条消息并通过 AES-CBC 加密来运行它开始。出于我们稍后将解释的安全原因,我们将把初始化向量固定为零。为了使我们的消息是块大小的倍数,我们还将使用用于加密的相同的 PKCS7 填充。为了简化下一个练习,我们需要一些没有填充的完整块消息,所以我们包含了一个关闭填充的标志。

 1   # WARNING! This is a fake CBC–MAC that is broken and insecure!!!
 2   # DO NOT USE!!!
 3   from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
 4   from cryptography.hazmat.backends import default_backend
 5   from cryptography.hazmat.primitives import padding
 6   import os
 7
 8   def BROKEN_CBCMAC1(message, key, pad=True):
 9       aesCipher = Cipher(algorithms.AES (key),
10                          modes.CBC(bytes(16)), # 16 zero bytes
11                          backend=default_backend())
12       aesEncryptor = aesCipher.encryptor()
13
14       if pad:
15           padder = padding.PKCS7(128).padder()
16           padded_message = padder.update(message)+padder.finalize()
17       elif len(message) % 16 == 0:
18           padded_message = message
19       else:
20            raise Exception("Unpadded input not a multiple of 16!")
21       ciphertext = aesEncryptor.update(padded_message)
22       return ciphertext[-16:] # the last 16 bytes are the last block
23
24   key = os.urandom(32)
25   mac1 = BROKEN_CBCMAC1(b"hello world, hello world, hello world, hello world", key)
26   mac2 = BROKEN_CBCMAC1(b"Hello world, hello world, hello world, hello world", key)

Listing 5-2Fake MAC with CBC

清单 5-2 中的代码虽然不安全,但确实展示了 MAC 背后的基本概念。一段数据首先被填充,然后被加密。然而,不管它有多长,最后一个块(16 字节)都是由所有前面的输入决定的。把第一个字母从“H”改成“H”,MAC 就完全不一样了。

然而,它可以被利用。回想一下,对于给定的消息和密钥对,MAC 必须是唯一的。如果攻击者可以用相同的密钥为不同的消息生成相同的 MAC,那么 MAC 算法就被破解了。

事实证明,对于 CBC-MAC 的这个幼稚版本,你完全可以做到这一点。让我们先用代码来做,看看你能不能猜出是怎么回事。请注意,清单 5-3 旨在与清单 5-2 合并。

 1   # Partial Listing: Some Assembly Required
 2
 3   # Dependencies: BROKENCBCMAC1
 4   def prependAttack(original, prependMessage, key):
 5       # assumes prependMessage is multiple of 16
 6       # assumes original is at least 16
 7       prependMac = BROKEN_CBCMAC1(prependMessage, key, pad = False)
 8       newFirstBlock = bytearray(original [:16])
 9       for i in range (16):
10           newFirstBlock[i] ^= prependMac[i]
11       newFirstBlock = bytes(newFirstBlock)
12       return prependMessage + newFirstBlock + original [16:]
13
14   key = os.urandom(32)
15   originalMessage = b"attack the enemy forces at dawn!"
16   prependMessage = b"do not attack. (End of message, padding follows)"
17   newMessage = prependAttack(originalMessage, prependMessage, key)
18   mac1 = BROKEN_CBCMAC1(originalMessage, key)
19   mac2 = BROKEN_CBCMAC1(newMessage, key)
20   print("Original Message and mac:", originalMessage, mac1.hex())
21   print("New message and mac     :", newMessage, mac2.hex())
22   if mac1 == mac2:
23       print("\tTwo messages with the same MAC! Attack succeeded!!")

Listing 5-3MAC Prepend Attack

上市 5-3 生产的两款 MAC 是一模一样的。我们的攻击将我们选择的另一个消息添加到原始消息中,并且破坏了第一个块。对前置消息的唯一限制是,它还必须在同一密钥下具有前置消息的 CBC-MAC 值。我们关闭了这个前置消息的填充,以使攻击变得稍微容易一些,但这只是为了我们的方便,并不是攻击成功的先决条件。

对攻击者来说,可悲的是,原始消息需要修改第一个块;否则,袭击可能会更严重。攻击者然后可以创建消息说“不要在黎明攻击敌军!”攻击者也不能擦除第一块以外的任何数据。在运行代码时,您可能注意到“部队在黎明!”在新邮件中仍然可读。即便如此,这仍然很糟糕:我们添加了一个完全不同的消息,而没有改变 MAC 的值!

对于这个简单的例子,我们假设一个人正在阅读输出,我们希望我们的消息说,其余的数据是填充将足以说服发送者不要进一步阅读。在实际攻击中,传输数据长度和其他类似的机制通常可以用来达到相同的效果。如果我们成功了,我们基本上可以用原来的 MAC 发送任意消息。

出了什么问题?在我们给你解释之前,看看你自己能不能搞清楚。您可能需要重新了解 CBC 模式是如何工作的。如果你需要额外的提示,记住abb=a

不管怎样,让我们一起努力吧。假设我们有一个消息 M 由任意数据块M1 到Mn 组成。在下面的公式中,让 E 表示 AES 加密操作,让 t 表示对数据计算的 CBC-MAC 标签:

  • t=e(me(m】-我…。e(me*(, k】-我…。, kk***

*请注意,消息的第一块 m 1 由 AES 在密钥 k 下加密,加密前输出与 m 2 进行异或运算。

假设我们预先计划了一条消息 P ,其长度正好是一个块。那会怎样改变事情?CBC-MAC 显然会产生一些不同的东西,因为我们改变了第一个计算:

  • t【p】=e(【m】**-我…。e(m(-我…。, kk**

结局是应该的。改变消息(即,预先考虑新的块)改变了标签。但是如果我们已经知道了前置块 E ( P,k )的 AES 加密的输出会怎么样呢?姑且称之为 C 。如果 E ( P,k ) = C ,那么我们可以将 P 前置到链上而不改变最后的标签如果我们也将原来的第一块 m 1 破坏为m1c

  • t=e(mn-我…。e(m【2】e(-我…。, kk

当 CBC 在这个被破坏的链上操作时,它试图将前置块的加密输出( C )异或成被破坏的第一个块的明文(m1⊕c)。但是损坏的第一个块已经混合了 C 的 XOR 运算,因此 C 值取消!这就简化为

  • t = e(m【n】【e】(】n-1-我…。e(m2e(1-我…。, kk**

*实际上,我们已经取消了最后一个标签上的前置块的输入!我们回到了消息的原始 MAC!

  • t=e(me(m】-我…。e(me*(, k】-我…。, kk***

这个例子只是针对单个块。但事实证明,无论前置消息有多长,我们只关心在加密之前将与 m 1 进行异或运算的部分。在任意长度的 CBC 链中,唯一延续到下一个块的部分是链的最后一个加密块。换句话说,CBC-MAC 操作的 MAC 输出, t ,是前置消息中唯一会影响其后内容的部分!*

然后,假设您有两个消息 M 1M 2 以及两个相应的标签 t 1t 2 ,这两个标签都是使用我们的破解 CBC-MAC 算法在同一密钥下生成的。为了创建一个伪造的消息,首先将第一个块M2 与第一个块t1 进行异或运算,以产生M2’。现在创建M3=M1+M2’(加号表示串联)。 M 3 的 CBC-MAC 也会是t2 因为(用 C ()表示“MAC”):

  • t=e(【m】【2】,e-我…。e(m,1】-我…。, kk****

由于 M 1 的 MAC 是 t 1 ,它与另一个 t 1 相抵消,剩下的 MAC 就是 M 2 的 MAC。

图 5-2 描述了这种攻击的可视化,以及我们刚刚研究过的数学方法。

重要的是,你不需要钥匙来进行这次攻击。在我们的代码示例中,我们自己拥有密钥并生成了一条任意的消息。这仍然是一种攻击,因为即使是共享密钥的拥有者也不能用同一个 MAC 发送两条消息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-2

攻击者可以在不改变(简单的)CBC-MAC 的情况下,通过破坏第一个块来预先计划消息

但是利用这种攻击,没有密钥的攻击者可以从两个现有的消息(例如,由受害者生成的)和相应的标签中生成新的消息和伪造的标签。

这个问题有各种各样的解决方案,但是我们在这里提到的唯一一个是强制每个消息都加上消息的长度,如清单 5-4 所示。

 1   # Reasonably secure concept. Still, NEVER use it for production code.
 2   # Use a crypto library instead!
 3   from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
 4   from cryptography.hazmat.backends import default_backend
 5   from cryptography.hazmat.primitives import padding
 6   import os
 7
 8   def CBCMAC(message, key):
 9       aesCipher = Cipher(algorithms.AES(key),
10                           modes.CBC(bytes(16)), # 16 zero bytes
11                           backend=default_backend())
12       aesEncryptor = aesCipher.encryptor()
13       padder = padding.PKCS7(128).padder()
14
15       padded_message = padder.update(message)
16       padded_message_with_length = len(message).to_bytes(4, "big") + padded_message
17       ciphertext = aesEncryptor.update(padded_message_with_length)
18       return ciphertext[-16:]

Listing 5-4
Prepend Message Length

为了安全地使用 CBC-MAC,有一些额外的注意事项:

  1. 如果您也使用 AES-CBC 加密数据,则不能对加密和 MAC 使用相同的密钥。

  2. 静脉注射应该固定为零。

对这些问题的全面解释超出了本书的范围。然而,假设您遵循了它们,包含的 CBC-MAC 代码是相当安全的。我们仍然不推荐使用它,因为创建你自己的加密算法或者你自己的已知加密算法的实现总是很危险。相反,请始终使用可信加密库中的算法。

我们在示例代码中使用的库包括 CMAC。该算法是 RFC 4493 中定义的 CBC-MAC 的更新和改进。CMAC 或 HMAC 是 MAC 算法的好选择;在没有专门的 AES 加密硬件的大多数系统上,HMAC 可能会更快。

使用图书馆的 CMAC 很简单。以下内容直接摘自在线文档:

>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.hazmat.primitives import cmac
>>> from cryptography.hazmat.primitives.ciphers import algorithms
>>> c = cmac.CMAC(algorithms.AES(key), backend=default_backend())
>>> c.update(b"message to authenticate")

加密和标记

在许多情况下,需要对消息进行加密并防止修改。在本章的第一个代码示例中,Alice 和 Bob 使用了一个未加密的哈希来保护加密的消息。显然,这是行不通的,因为没有密钥,任何人都可以生成相应的哈希。既然我们无畏(或卑怯)的二人组知道如何使用 HMAC 和 CMAC,他们可以更新他们的代码。

练习 5.6。加密然后 MAC

更新本章开头的代码,将 SHA-256 操作替换为 HMAC 或 CMAC 操作,以实现正确的 MAC。使用两个键。

请注意在前面的练习中,您何时使用 MAC 以及在什么设备上使用它。您会注意到 MAC 应用的是密文,而不是明文。正如这个练习的名字所暗示的,这叫做加密然后 MAC 。过去有两种发送加密和认证消息的方法。

一种是先 MAC 后加密。在这个版本中,MAC 应用于明文、,然后明文和 MAC 一起加密。早期版本的 TLS(用于 HTTPS 连接)采用了这种方法。

另一种方法叫做加密和 MAC。为了采用这种方法,MAC 也是在明文上计算的,但是 MAC 本身没有加密。它与密文一起发送(未加密)。如果您曾经使用过安全 Shell (SSH 或 PuTTY),它会使用 Encrypt-And-MAC。

大多数密码学家强烈推荐使用 Encrypt-Then-MAC3而不是其他两种方法,当然也有一些人反对。事实上,针对 MAC-Then-Encrypt 的某些组合,已经发现了某些实际的漏洞。你已经演示过一次了!前一章中针对 CBC 的填充 oracle 攻击仅适用于 MAC-Then-Encrypt 场景。

还有一种更好的方法叫做 AEAD(带附加数据的认证加密),我们将在第七章中了解到,它将加密和消息完整性结合到一个操作中。无论出于何种原因,如果您需要将加密和 MAC 结合起来,请确保选择 Encrypt-Then-MAC(即,加密明文,然后根据密文计算 MAC)。

我们不会深入讨论为什么 Encrypt-Then-MAC 通常被认为更好,但有一点值得一提。正如我们在其他情况下讨论过的,我们通常不希望坏人篡改我们的密文。这可能不直观,因为我们倾向于考虑最终目标:保护明文。但是,当坏人可以在我们无法检测到的情况下更改密文时,坏事就会发生。当您先加密再加密 MAC 时,应该保护密文不被修改。

练习 5.7。了解你的弱点

建议使用 Encrypt-Then-MAC 方法将加密和 MAC 结合起来。然而,理解所有这三种方法是有好处的。不说别的,如果您曾经不得不维护您没有编写的代码,或者不得不与遗留系统兼容,您将来可能会遇到这种情况。修改您的(强烈推荐)先加密后 MAC 系统,创建一个先 MAC 后加密的变体。最后,创建一个 MAC-And-Encrypt 版本。

数字签名:认证和完整性

Alice 和 Bob 喜欢用 hmac 发送加密消息(使用 Encrypt-Then-MAC)。在他们目前在西南极洲的任务中,他们每个人都有四把钥匙。一对允许它们相互发送加密和 MAC 保护的消息(记住,一个密钥用于加密,一个密钥用于 MAC 生成),第二对允许它们向位于东南极洲的总部发送和接收加密和 MAC 保护的消息。

不幸的是,有一天爱丽丝被抓获,因为她试图渗透到西南极雪球测试大厦。瞬间,一切都陷入混乱,因为夏娃现在可以使用她所有的钥匙。

这是一个可怕的妥协。Eve 现在可以发送消息了,就好像它们是来自 Alice 或 HQ 一样!试图减轻这种保密性和身份验证的损失是一场噩梦。鲍勃的情况很糟糕。他需要两把新钥匙来与总部沟通,也许还需要两把新钥匙来与现场的新合作伙伴沟通。这只能通过返回总部来完成,这意味着将他从现场拉出来,可能会浪费他花在渗透目标和收集数据上的时间和资源。更糟糕的是,他甚至不能被可靠地告知正在发生的事情!如果他没有爱丽丝被捕的第一手资料,总部向他发送的任何通知事件或指示他回家的消息都可以被拦截和更改。

尽管事情对鲍勃来说很糟糕,但总部的情况更糟。他们使用相同的共享密钥对所有邮件进行加密和标记。现场的每个特工都有爱丽丝丢失的密钥。伊芙可以在他们任何一个人面前冒充总部。Eve 可以像任何代理一样向总部发送消息,因为他们没有自己的密钥来与总部通信。

共享密钥的丢失使 EATSA 倒退了至少 12 个月。

更糟糕的是,Eve 可以使用加密密钥读取 HQ 和他们的代理之间的通信,更糟糕的是,她可以使用 MAC 密钥伪装成任何一方发送消息。重复我们之前的一个评论,当人们第一次开始学习密码学时,他们通常认为“加密”是其主要目的或特征。正如我们虚构的例子所示,身份验证——知道谁发送了消息——至少同样重要,甚至可以说更重要。

即使一旦 EATSA 设法得到他们所有代理的家,并且不再使用旧密钥(旧密钥因此被“撤销”),他们也有提出密钥管理系统以避免将来出现相同问题的问题。他们考虑的一个选择是让每个代理拥有他们自己的单独密钥。如果 HQ 或代理想要发送消息,他们使用各自的密钥对其进行标记。

问题是 MAC 需要共享密钥。消息的接收方必须与发送方拥有相同的密钥。他们将如何获得它?每个代理都有其他代理的钥匙吗?如果是这样,代理的捕获就像只有一把钥匙一样糟糕。更糟糕的是,没有什么能阻止一个代理使用另一个代理的密钥(冒充他们),无论是意外还是因为他们变得无赖。

最终,其中一名科学家记起了第四章中的不对称加密,特别是它可以用于一种叫做的数字签名。与消息身份验证代码一样,数字签名旨在提供真实性(您可以知道谁发送了消息)和消息完整性(消息不能被不可察觉地更改)。此外,因为他们使用非对称加密,所以没有共享密钥。当 EA 开始尝试非对称加密的时候,他们变得非常非常关注消息的加密(保密性),而数字签名则被抛到了一边。

现在是补救的时候了。

到底什么是数字签名?首先,让我们回顾一下不对称加密是如何为我们在第四章中学习的 RSA 算法工作的。与各方之间只有一个共享密钥的对称加密不同,RSA 的非对称加密包含一对密钥:公钥和私钥。这些密钥以相反的方式工作:一个加密,另一个解密。此外,RSA 公钥可以从私钥导出,但不能反过来。

顾名思义,一方应该保持 RSA 私钥的私密性,永远不要向任何人透露。另一方面,RSA 公钥可以而且通常应该广泛传播。这个设置支持两个非常有趣的操作。

首先,因为 RSA 公钥由任何人持有(并且可能由每个人持有!),世界上任何一个人都很容易将加密的消息发送给相应 RSA 私钥的所有者。任何人都可以用公钥加密信息,但是只有拥有私钥的一方才能解密。

这很重要!发送加密消息的人知道只有拥有私钥的一方可以解密消息。这是一种不同的反向真实性。消息的接收者不知道是谁发送的,但是发送者可以确定(如果密钥是安全的)只有预定的一方可以阅读消息。我们在第四章中对 RSA 非对称加密的介绍主要集中在这个用例上。

但是,加密的方向可以反过来*:RSA 私钥也可以用来加密消息。因此,拥有私钥的一方可以用它来加密只能用公钥解密的东西。那有什么好处呢?任何人(大家!)可能有公钥。这种加密当然不会让数据保密!*

这是真的!但是,在 RSA 私钥下加密发送的消息只能由拥有该私钥的某人加密。即使每个人都能解密它,它能被特定的公钥解密的事实是发送者持有私钥证明。换句话说,如果你收到一条可以用我的公钥解密的消息,你就知道它来自我;没人能加密它。听起来很有用!*

让我们假设环境局想要向全世界发布一份关于西南极洲犯罪的宣言。首先,他们可以到处传播他们的 RSA 公钥,然后用相关的私钥加密文档。现在,当他们分发文档时,世界上的任何人都可以解密它,这个事实向他们证明了它来自 EA。

这个系统很棒,但是它有几个重要的缺陷。首先,世界如何知道 RSA 公钥真的属于 EA(而不是来自 WA 的赝品)?这是一个非常重要的问题,我们稍后会谈到它。现在,我们假设接收者拥有合法的、可信的 RSA 公钥。

另一个问题是效率。RSA 加密。解密长文档来验证发送者并不是一个非常有效的方法。更糟糕的是,一些非对称算法没有任何内置的消息完整性。哦,当我们谈论 RSA 的局限性时,它不能加密像文档一样长的东西。

幸运的是,效率和完整性这后两个问题很容易解决。回想一下,我们不是为了保密而加密,而是为了证明来源或真实性。不加密消息本身,而是加密消息的一个哈希怎么样?

这是对任意数据进行 RSA 数字签名的基本思想。它包括两个步骤。首先,哈希数据。第二,用私钥加密哈希。加密哈希是应用于数据的发送方签名。现在,签名可以与原始(可能未加密)数据一起传输。当接收方收到数据和签名时,接收方生成哈希,用公钥解密签名,并验证两个哈希(生成的和解密的)是否相同。

下面是密码学家可能如何表示这一点。首先,对于一个消息 M ,我们使用哈希函数生成一个哈希: h = H ( M )。

一旦我们有了哈希值 h ,我们就用 RSA 私钥对它进行加密。为了描述这个操作,我们将使用加密协议中常用的一些符号。具体来说,我们将使用{}来表示 RSA 加密的数据。大括号内的所有内容都是明文,但是大括号表明明文在某个加密信封内。大括号还会有一个表示键的下标。所以比如密文 C 是在某个密钥 K 下加密的明文 P ,这个被描绘成C= {P}K

从这一点开始,在本书中,两方之间的共享密钥将用一个表示双方的下标来描述。所以,举例来说,爱丽丝和鲍勃之间的一个键可以描述为KA,B 。这是对称密钥的一个例子。

诸如 RSA 公钥之类的公钥将由仅具有一个识别方的密钥来表示。例如,爱丽丝的公钥可以表示为 K A ,而鲍勃的公钥同样可以表示为 K B 。因为公钥是被分发的,所以它是被命名的。私钥改为表示为公钥的逆:K1A和K*–*1B)。

在本章中,我们通常还会使用字母 t 来表示 RSA 签名,因为签名有时也被称为标签,就像 MAC 一样。因此,我们表示一个 R:

tM= {H(M)}K—1

当拥有 RSA 公钥的另一方 K 接收到 M、{H’(M)}K—1时,用公钥解密签名,恢复H’(M)。接收方生成自己的 H ( M ),如果H’(M)=H(M),则签名被认为是真实的。

冒着重复的风险,请记住 RSA 公钥加密与私钥加密用于不同的用途。用 RSA 公钥加密使消息保密:只有私钥拥有者才能阅读它。用 RSA 私钥加密证明了的真实性:只有所有者才能创作它。

在 EA 间谍机构,这似乎是奇迹!代理为自己生成一个 RSA 密钥对,并让所有代理生成一个 RSA 密钥对。代理保存所有代理的所有公共密钥的副本,并且每个代理获得代理的公共密钥的副本。

当代理机构向 Alice 发送加密消息时,他们使用她的公钥对其进行加密,只有 Alice 能够对其进行解密。他们用他们的私钥签署消息,并且 Alice 可以使用代理公钥来验证消息是真实的并且没有被破坏。只要 Alice 和 Bob 有对方公钥的副本,他们同样可以互相发送加密和认证的消息。

这是一个很大的进步,看起来很棒。

确实是这样,但正如 EA 的加密体验经常发生的那样,有复杂之处、警告和微妙之处。然而,在我们开始之前,让我们帮助 Alice 和 Bob 学习如何相互发送一些签名通信。为了简单起见,我们不打算加密它们。

再一次,cryptography库用它的签名和验证功能拯救了我们:我们不需要,也不应该试图自己实现数字签名。相反,使用我们的库,我们将生成一些 RSA 签名。

 1   from cryptography.hazmat.backends import default_backend
 2   from cryptography.hazmat.primitives.asymmetric import rsa
 3   from cryptography.hazmat.primitives import hashes
 4   from cryptography.hazmat.primitives.asymmetric import padding
 5
 6   private_key = rsa.generate_private_key(
 7       public_exponent=65537,
 8       key_size=2048,
 9       backend=default_backend()
10   )
11   public_key = private_key.public_key()
12
13   message = b"Alice, this is Bob. Meet me at Dawn"
14   signature = private_key. sign(
15       message,
16       padding.PSS(
17           mgf=padding.MGF1(hashes.SHA256()),
18           salt_length=padding.PSS.MAX_LENGTH
19       ),
20       hashes.SHA256()
21   )
22
23   public_key.verify(
24       signature,
25       message,
26       padding.PSS(
27           mgf=padding.MGF1(hashes.SHA256()),
28           salt_length=padding.PSS.MAX_LENGTH
29       ),
30       hashes.SHA256()
31   )
32   print("Verify passed! (On failure, throw exception)")

Listing 5-5Sign Unencrypted Data

清单 5-5 中的内容可能比预期的要多一点,尤其是在填充配置中。让我们从头到尾走一遍。

首先,我们生成一个密钥对。对于 RSA,公钥是从私钥派生出来的,因此生成私钥会生成密钥对。API 包括一个从私钥获取公钥的调用。在这个例子中,两个密钥都被使用。在真实的例子中,签名和验证代码将存在于完全不同的程序中,并且验证程序将只能访问公钥,而不能访问私钥。

在第四章中,我们还学习了如何从磁盘中序列化和反序列化这些 RSA 密钥。

在代码的下一部分,我们对消息进行签名。您会注意到,我们在这里使用填充,就像我们在 RSA 加密中使用的一样,但这是一个不同的方案。RSA 的推荐填充是加密的 OAEP 和签名的 PSS。考虑到 RSA 签名是通过加密哈希生成的,这可能会让您感到惊讶。如果无论如何都是加密,为什么我们需要不同的填充方案?

答案是,因为签名是在哈希上操作的,所以数据的某些特征必须是真实的。任意数据加密与哈希加密的本质决定了两种不同的填充方案。

与第四章中使用的 OAEP 填充一样,PSS 填充功能也需要使用“屏蔽生成功能”在撰写本文时,只有一个这样的函数,MGF1。

最后,签名算法需要哈希函数。在本例中,我们使用 SHA-256。

验证算法的参数应该是不言自明的。请注意,验证函数不会返回 true 或 false,而是在数据与签名不匹配时引发异常。

重要的

请仔细注意下一段。这一点非常重要,也有些反直觉。

如果要加密签名,应该先签名再加密,还是先加密再签名?在前一节讨论了先加密后 MAC 之后,您可能会想到先加密后签名。

但是签名是而不是 MAC,你一般应该而不是使用先加密后签名。有两个非常重要的原因。

首先,记住签名的目标不仅仅是消息的完整性,还包括发送者的 ?? 认证。假设爱丽丝正在给鲍勃发送一条加密的消息,她在签名之前对消息进行了加密。任何人都可以截取消息,去掉签名,然后用自己的密钥发送重新签名的消息。哎呀。

目前还不清楚这种攻击有多实际,因为数据是在每个人都有的接收者的公钥下加密的。无论如何,攻击者可以发送他们自己的加密消息给 Bob(用 Bob 的公钥加密)。攻击者甚至无法解密 Alice 的消息,以查看他/她是否想邀功。但关键是,明文和签名之间没有关联,确实需要有关联:Bob 有兴趣知道他能读到的消息来自 Alice 而不是其他人。如果加密数据被签名而不是明文,当 Bob 收到密文和签名时,他不能可靠地确定原始消息的作者。

简而言之,如果你签署了一个加密的消息,它很容易被别人截取和签署,这就损害了它的真实性。签名应该应用于明文。

第二,也是更重要的一点,签名不能防止坏人篡改密文。请记住,使用 Encrypt-Then-MAC 的首要原因是防止加密数据被篡改。例如,通过“先加密后签名”,Eve 可以截取 Alice 发给 Bob 的消息,去掉 Alice 的签名,修改密文,然后用她自己的密钥对修改后的数据进行签名。你可能会问,这有什么好处?毕竟,Bob 将会看到邮件现在是由 Eve 而不是 Alice 签名的。他为什么要相信它?

鲍勃会接受签名的原因有很多。例如,伊芙可能泄露了另一个特工的钥匙。使用 RSA 加密的全部原因是为了防止一个代理的密钥泄露危及另一个代理的通信。但是如果 Eve 得到了合法的签名密钥,她就可以剥去 Alice 的签名,修改密文,用 Bob 会接受的东西重新签名。

一旦发生这种情况,Eve 可以观察 Bob 的行为来了解 Alice 的信息。正如我们在前面的例子中所使用的,即使 Bob 丢弃了一条消息,Eve 也可以利用这条信息(例如,她知道她发送给他的消息是不可读的)。

这听起来是不是很牵强?嗯,正是苹果 iMessage 的这种漏洞被马特·格林发现了。你可以在他的博客[6]上读到。我们不会在这里详细讨论他的攻击,只是说这种攻击实际上非常实用。

所以,请不要加密然后签名。

为什么这与 MAC 电脑如此不同?为什么先加密后 MAC 行得通?最根本的区别还是在于按键。对于 MAC,有一个共享密钥,通常只在双方之间共享。没有人能够替换由 Alice 和 Bob 共享的密钥创建的 MAC,因为其他人不应该拥有该密钥。然而,用于创建数字签名的私钥是不共享的,并且不将任何一方绑定在一起。

你应该做什么?首先,似乎没有太多的加密系统适用于此。如果您使用对称加密,包含对称 MAC 通常没有问题。如果苹果做到了这一点,我们提到的 iMessage 攻击就不可能发生。不对称加密通常不用于批量加密。当需要加密大量数据时,通常的方法是使用非对称加密交换或创建对称密钥,然后切换到对称算法。我们将在下一章谈到这一点。

如果您必须在没有对称 MAC(例如,RSA 加密加上一些签名)的情况下进行签名和加密,则应对明文消息进行签名,并对明文和签名进行加密(先签名后加密)。尽管这意味着攻击者可以试图篡改密文,但像 OAEP 这样的好的 RSA 填充方案应该会使这变得非常困难。

虽然目前还没有已知的针对先签名后加密的攻击,但一些最偏执的人仍然先签名后加密,然后再签名。内部签名在明文上,证明作者身份,外部签名在密文上,确保消息的完整性。另一种选择是所谓的“签密”因为 Python cryptography库不支持签密,所以我们在这里不花时间讨论它,但是好奇的人可以看看这篇关于它的文章: www.cs.bham.ac.uk/ ~ mdr/teaching/modules04/security/students/SS3/IntroductiontoSigncryption.pdf

现在,我们将坚持使用稍微不那么偏执的先签名后加密策略。但是,请记住,RSA 加密只能加密非常有限的字节数。当 OAEP 填充与 SHA-256 一起使用时,可以加密的最大明文只有 190 字节!如果您开始加密签名,可能就没有多少空间来做其他事情了。如果你的信息太长,你将不得不把它分成 190 字节的块进行加密。这就是我们在下一章将要看到的使用非对称和对称操作的更多原因。

练习 5.8。RSA 回归!

为 Alice、Bob 和 EATSA 创建一个加密和认证系统。该系统需要能够生成密钥对,并以不同的操作员名称将它们保存到磁盘。要发送一条消息,需要加载操作者的私钥和接收者的公钥。然后,要发送的消息由运营商的私钥签名。然后,发送者姓名、消息和签名的连接被加密。

为了接收消息,系统加载操作者的私钥并解密数据,提取发送者的名字、消息和签名。加载发送者的公钥来验证消息上的签名。

练习 5.9。MD5 返回!

在第二章中,我们讨论了一些破解 MD5 的方法。特别是,我们强调 MD5 仍然没有被破解(在实践中)来寻找原像(即,向后工作)。但是在寻找碰撞方面,它破的。这在涉及签名时非常重要,因为签名通常是通过数据的哈希而不是数据本身来计算的。

在本练习中,修改您的签名程序,使用 MD5 代替 SHA-256。找到两条具有相同 MD5 和的数据。您可以在或通过快速搜索互联网找到一些示例。一旦有了数据,验证这两个文件的哈希是否相同。现在,为这两个文件创建一个签名,并验证它们是否相同。

最后要提一件事。在某些情况下,您可能无法一次对所有数据进行签名。sign函数不像哈希函数那样有一个update方法。但是,它有一个 API 来提交预先哈希的数据。这允许您对需要单独签名的数据进行哈希处理。以下是摘自cryptography模块文档的一个示例:

>>> from cryptography.hazmat.primitives.asymmetric import utils
>>> chosen_hash = hashes.SHA256()
>>> hasher = hashes.Hash(chosen_hash, default_backend())
>>> hasher.update(b"data & ")
>>> hasher.update(b"more data")
>>> digest = hasher.finalize()
>>> sig = private_key.sign(
...     digest,
...     padding.PSS(
...         mgf=padding.MGF1(hashes.SHA256()),
...         salt_length=padding.PSS.MAX_LENGTH
...     ),
...     utils.Prehashed(chosen_hash)
... )

椭圆曲线:RSA 的替代方案

是时候告诉你不对称加密的真相了。到目前为止,我们告诉您的一切都是特定于 RSA 的,RSA 的很多工作实际上都是独一无二的。

当我们谈到非对称或公钥加密时,我们指的是涉及公钥和私钥对的任何加密操作。在第四章中,我们几乎专门研究了 RSA 加密,在这一章中,我们探讨了 RSA 签名。方便的是,RSA 签名也基于 RSA 加密(即对要签名的数据的哈希进行加密)。但是大多数其他非对称算法甚至根本不支持加密作为一种操作模式,并且不使用加密来生成签名。例如,其他非对称算法生成不涉及任何加密的签名或标签,并且在没有任何种类的可逆操作(例如解密)的情况下验证签名。

这就是我们在本书中试图通过具体提及“RSA 公钥”、“RSA 加密”和“RSA 非对称运算”来限定我们关于非对称加密的对话的原因之一。您不应该假设其他非对称算法提供相同的操作或以相同的方式执行它们。

为什么如此关注 RSA 加密?我们在这里这样做是因为几十年来 RSA 一直是不对称运算最流行的算法之一。它仍然随处可见,你很难不在某个地方碰到它。DSA(数字签名算法)是另一种非对称算法,但它只能用于签名,不能用于加密。出于教育和实践的目的,RSA 是一个很好的起点。

也就是说,RSA 正在慢慢被淘汰。人们发现它有许多弱点,其中一些我们已经探索过了。基于“椭圆曲线” 4 的加密技术已被用于签署数据和交换密钥。在本章中,我们将研究 ECDSA 的签名功能。在第六章中,我们将会看到一种叫做椭圆曲线 Diffie-Hellman (ECDH)的东西,它被用来创建和协商会话密钥。ECDH 的密钥协议为 RSA 加密所支持的密钥传输功能提供了一种替代方案(可以说是一种更好的替代方案)。

要使用椭圆曲线对数据进行签名,可以使用 ECDSA 算法。正如您必须为 RSA 选择参数(例如 e ,公共指数),您也必须在基于 EC 的操作中选择参数。其中最明显的是潜在曲线。同样,实际的数学在本书中没有讨论,所以我们可以满足于说不同的椭圆曲线可以用于这些算法中。

对于 ECDSA,cryptography库提供了许多 NIST 批准的曲线。需要注意的是,一些密码学家对这些曲线很警惕,因为有可能美国政府推荐了它知道可以被破解的曲线。也就是说,这些是该库目前提供的唯一曲线。如果您在生产中使用这些,您应该留意关于安全漏洞和潜在替代品的其他信息。

对于这个测试,我们将使用 NIST 的 P-384 曲线,在库中称为 SECP384r1。来自cryptography文档

>>> from cryptography.hazmat.backends import default_backend
>>> from cryptography.hazmat.primitives import hashes
>>> from cryptography.hazmat.primitives.asymmetric import ec
>>> private_key = ec.generate_private_key(
...     ec.SECP384R1(), default_backend()
... )
>>> data = b"this is some data I'd like to sign"
>>> signature = private_key.sign(
...     data,
...     ec.ECDSA(hashes.SHA256())
... )
>>> public_key = private_key.public_key()
>>> public_key.verify(signature, data, ec.ECDSA(hashes.SHA256()))

与 RSA 签名一样,您必须选择一个哈希函数。我们再次选择了 SHA-256。你会注意到,尽管选择一个曲线函数看起来令人畏惧,但一旦完成,剩下的操作就非常简单了。

ECDSA 也有与 RSA 相同的预哈希 API,用于处理大量数据。

证书:证明公钥的所有权

在我们关于 Alice 和 Bob 以及公钥的例子中,我们假设每个相关方都拥有其他相关方的公钥。在我们的场景中,这个可能是可能的。总部可以把所有的间谍聚集在一起,让每个人交换公钥。 5

然而,随着时间的推移,这可能不可行。

如果诺埃尔,一个新的间谍,在其他人之后进入这个领域会怎么样?假设特工查理被抓了,诺埃尔被派去接替他的位置。爱丽丝和鲍勃已经有了查理的钥匙,但是他们还没有诺尔的钥匙。

当然,Noel 不能就这样出现并分发公钥。否则,Eve 可能会派假特工来分发公钥,声称自己是真正的 EA 特工。她可以像 HQ 一样轻松地创建证书。爱丽丝和鲍勃怎样才能认出诺埃尔是一个真正的 EATSA 特工,而不是为伊芙工作?

一种可能是让 HQ 向 Alice 和 Bob 发送一条消息,其中包含新代理的名称和公钥。Alice 和 Bob 已经信任 HQ,并且已经有了 HQ 的公钥。HQ 可以在他们和 Noel 之间充当可信的第三方。在 PKI 的早期,这正是为了建立信任而提出的。这个模型被称为“注册表”注册中心将是身份到公钥映射的中央存储库。注册中心自己的公钥将被传播到任何地方:报纸、杂志、教科书、实体邮件等等。只要每个人都有注册中心密钥的真实副本,他们就可以查找在注册中心注册的任何人的公钥。

当时的问题是规模,尽管现在这个问题不那么严重了。尽管当代计算设想世界上的谷歌、亚马逊和微软每时每刻都在处理来自世界各地的数十亿次连接,但在 20 世纪 90 年代并非如此。人们认为,网上登记处根本无法扩展。

就我们的间谍而言,他们必须假设他们可能与总部失去了联系。他们可能不得不深入隐藏,或者他们可能正在躲避 Eve,或者 EA 想要暂时否认他们的任何活动。由于任何或所有这些原因,他们可能无法从总部得到及时的消息。如果他们在躲避夏娃,如果他们能知道在安全屋遇到他们的间谍是否站在他们一边,那就太好了。

这就把我们带到了证书。公钥证书只是数据;它通常包括一个公钥、与密钥所有权相关的元数据,以及一个已知“发行者”对所有内容的签名。元数据包括诸如所有者身份、发行者身份、截止日期、序列号等信息。其概念是将元数据(尤其是身份的元数据)绑定到公钥。身份可以是姓名、电子邮件地址、URL 或任何其他约定的标识符。

总部现在可以分发证书,而不是简单地将公钥分发给他们的代理。 6 首先,代理生成自己的密钥对(任何人,甚至是 HQ,都不应该拥有代理的私钥)。接下来,HQ 获取代理的公钥,并开始创建一个证书,其中包括代理的标识信息,例如他们的代码名称。 7 为了完成证书,HQ 用 HQ 私钥对其进行签名,成为发行方。

重复一遍,证书中的公钥属于代理。代理保持他们自己的私钥是私有的。 8 如图 5-3 所示,证书上的签名是由颁发者的私钥(本例中为 HQ 的私钥)生成的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-3

证书的主要目的是将身份和公钥绑定在一起。发行者可以签署证书数据以防止修改并提供信任。

让我们回到我们的场景,爱丽丝在南极洲西部逃亡,夏娃的特工紧追不舍。她来到一个安全屋,看到了一个她从未见过的特工:查理。为了证明他就是他所说的那个人,查理出示了他的证明。Alice 检查身份数据是否与他的声明匹配(例如,证书中的身份是“charlie”)。接下来,Alice 检查证书的颁发者是 HQ,然后验证证书中包含的签名。记住,证书中的签名是由发布者 (HQ)签署的。使用 HQ 在她执行任务前发给她的公钥,Alice 的签名检查成功。因此,Alice 知道证书一定是由 HQ 颁发的,因为没有其他人能够生成有效的签名。该证书是真实的,并且 Alice 现在拥有(并且信任)Charlie 的公钥用于将来的通信。

当然,还有一个问题。查理的证!没有什么能阻止伊芙拿一份拷贝给爱丽丝本人。爱丽丝怎么知道门口那个手里拿着证件自称查理的人真的是查理?

查理现在必须通过为爱丽丝签署一些数据来证明他的身份。爱丽丝给了他某种测试信息,查理用他的私钥签名。Alice 使用来自其证书的公钥来验证该数据上的签名。签名检查通过,因此 Alice 知道 Charlie 一定是证书的所有者。只有主人有(或者应该有!)与签名数据所需的公钥相关联的私钥。当然,如果查理被抓获,他的私人密钥泄露,所有的赌注都将关闭!

总之,Charlie 用他的私钥签名以证明这是他的证书,但是 Alice 检查证书中的签名以确保证书本身是由她信任的人签发的。Alice 对这一过程的观点如图 5-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-4

谁在敲门?爱丽丝想知道在她让谁进来之前是谁!

让我们通过一些例子来看看这是如何工作的。对于第一个练习,我们不打算使用真实的证书,至少现在还没有。现在,我们将使用一个简单的字典作为我们的证书数据结构,并使用 Python json 模块将其转换为字节。

警告:不用于生产

天啊,我们经常说“不用于生产用途”,不是吗?我们不得不这么做。密码学是独特的,同时也是微妙而诱人的:概念描述起来相对简单,但微小的细节可以决定良好的安全性和不安全性。这些细节有时很难发现,证明它们是正确的也很难。

不要在产品中使用本书中的任何非库实现,也不要假设我们使用库是一个合适的解决方案。不要假设一个例子已经教会了你足够的知识来开发你自己的密码,也不要假设你已经掌握了库的正确用法。甚至不要认为我们的出错清单是完整的!

记住,YANAC(你不是一个密码学家…还没!).我们会再说一遍。这是我们的工作。

我们要研究的例子有三方:声明身份的一方(Charlie),也称为主体,验证声明的一方(Alice),以及发布证书的可信第三方(HQ)。其中两方,Charlie 和 HQ,将需要 RSA 密钥对。您可以生成 RSA 密钥对,并使用第四章中的rsa_simple.py脚本将它们保存到磁盘。在本练习的其余部分,我们将假设 HQ 的密钥保存在hq_public.keyhq_private.key中,Charlie 的密钥保存在charlie_public.keycharlie_private.key中。

此外,为了清楚起见,我们为每一方创建了三个单独的脚本。发布者 (HQ)使用第一个脚本从现有的公钥生成证书。

 1   from cryptography.hazmat.backends import default_backend
 2   from cryptography.hazmat.primitives.asymmetric import rsa
 3   from cryptography.hazmat.primitives.asymmetric import padding
 4   from cryptography.hazmat.primitives import hashes
 5   from cryptography.hazmat.primitives import serialization
 6
 7   import sys, json
 8
 9   ISSUER_NAME = "fake_cert_authority1"
10
11   SUBJECT_KEY = "subject"
12   ISSUER_KEY = "issuer"
13   PUBLICKEY_KEY = "public_key"
14
15   def create_fake_certificate(pem_public_key, subject, issuer_private_key):
16       certificate_data = {}
17       certificate_data[SUBJECT_KEY] = subject
18       certificate_data[ISSUER_KEY] = ISSUER_NAME
19       certificate_data[PUBLICKEY_KEY] = pem_public_key.decode('utf-8')
20       raw_bytes = json.dumps(certificate_data).encode('utf-8')
21       signature = issuer_private_key.sign(
22           raw_bytes,
23           padding.PSS(
24               mgf=padding.MGF1(hashes.SHA256()),
25               salt_length=padding.PSS.MAX_LENGTH
26           ),
27           hashes.SHA256()
28       )
29       return raw_bytes + signature
30
31   if __name__=="__main__":
32       issuer_private_key_file = sys.argv[1]
33       certificate_subject = sys.argv[2]
34       certificate_subject_public_key_file = sys.argv[3]

35       certificate_output_file = sys.argv[4]
36
37       with open(issuer_private_key_file, "rb") as private_key_file_object:
38           issuer_private_key = serialization.load_pem_private_key(
39                            private_key_file_object.read(),
40                            backend=default_backend(),
41                            password=None)
42
43       with open(certificate_subject_public_key_file, "rb") as public_key_file_object:
44           certificate_subject_public_key_bytes = public_key_file_object.read()
45
46       certificate_bytes = create_fake_certificate(certificate_subject_public_key_bytes,
47                                                   certificate_subject,
48                                                   issuer_private_key)
49
50       with open(certificate_output_file, "wb") as certificate_file_object:
51           certificate_file_object.write(certificate_bytes)

Listing 5-6
Fake Certificate Issuer

让我们浏览一下清单 5-6 。只有一个功能:create_fake_certificate。我们使用“假”这个名称不是为了表明欺诈,而是表明这不是一个真正的证书。同样,请不要在生产中使用它。 9

该函数创建一个字典并加载三个字段:主题名称(身份)、发布者名称和公钥。请注意,该文件中使用了两个密钥对(的一部分)。有一个发行者私钥和主体公钥。证书中存储的是主体的私钥。这个公钥在许多方面代表了主体,因为它将被用来证明他或她的身份。这就是证书签名如此重要的原因。另外,任何人都可以创建一个证书来宣称他们喜欢的任何身份。

一旦加载了字典,我们使用json将字典序列化为一个字符串。JSON 是一种常见的标准格式,但是在 Python 3.x 中,它不能直接对字节进行编码,而是输出一个文本字符串。为了与 Python cryptography库兼容,我们将 PEM 编码的键作为二进制字节而不是文本来加载。要存储在这个 JSON 证书中的公钥必须首先转换成一个字符串,但是因为它是 PEM 编码的(也就是说,它已经是明文),所以我们可以安全地将其转换成 UTF-8。类似地,json.dumps()操作的整个输出通过安全的 UTF-8 转换被转换成字节。

然后使用发布者的私钥对字节进行签名。只有颁发者可以访问这个私钥,因为这是颁发者向世界证明它(颁发者)已经创建了证书的方式。我们的最终证书是来自 json 的原始字节和来自签名的字节。

在我们假设的例子中,charlie 想要声明身份“Charlie”Charlie 从生成密钥对开始。公钥(而不是私钥)被发送到 HQ 证书颁发部门,并请求制作证书。发行部门中的人应该验证查理有权声明身份“查理”例如,负责的官员可能会要求查看查理的机构 ID,审查上级官员的文书工作,检查指纹等等,以确保真正的查理将获得证书。

颁发者脚本接受四个参数:颁发者私钥文件、将放入证书的声明身份、与身份相关联的公钥以及证书的输出文件名。使用您在本练习中生成的密钥,运行如下所示的脚本:

python fake_certs_issuer.py \
  hq_private.key \
  charlie \
  charlie_public.key \
  charlie.cert

这将为 Charlie 生成一个(伪造的)证书,其中包含声明的身份和相关的公钥,所有这些都由 HQ 签名。

现在查理可以向爱丽丝证明他拥有“查理”这个身份。他首先给她声称的身份(“查理”)并提供证书。

这里的第二个脚本是让 Alice 验证 Charlie 声称的身份。

 1   from cryptography.hazmat.backends import default_backend
 2   from cryptography.hazmat.primitives.asymmetric import rsa
 3   from cryptography.hazmat.primitives.asymmetric import padding
 4   from cryptography.hazmat.primitives import hashes
 5   from cryptography.hazmat.primitives import serialization
 6
 7   import sys, json, os
 8
 9   ISSUER_NAME = "fake_cert_authority1"
10
11   SUBJECT_KEY = "subject"
12   ISSUER_KEY = "issuer"
13   PUBLICKEY_KEY = "public_key"
14
15   def validate_certificate(certificate_bytes, issuer_public_key):
16       raw_cert_bytes, signature = certificate_bytes[:-256], certificate_bytes [-256:]
17
18       issuer_public_key.verify(
19           signature,
20           raw_cert_bytes,
21           padding.PSS(
22               mgf=padding.MGF1(hashes.SHA256()),
23               salt_length=padding.PSS.MAX_LENGTH
24           ),
25           hashes.SHA256())
26       cert_data = json.loads(raw_cert_bytes.decode('utf-8'))
27       cert_data[PUBLICKEY_KEY] = cert_data[PUBLICKEY_KEY].encode('utf-8')
28       return cert_data
29
30   def verify_identity(identity, certificate_data, challenge, response):

31       if certificate_data[ISSUER_KEY] != ISSUER_NAME:
32           raise Exception("Invalid (untrusted) Issuer!")
33
34       if certificate_data[SUBJECT_KEY] != identity:
35           raise Exception("Claimed identity does not match")
36
37       certificate_public_key = serialization.load_pem_public_key(
38           certificate_data[PUBLICKEY_KEY],
39           backend=default_backend())
40
41       certificate_public_key.verify(
42           response,
43           challenge,
44           padding.PSS(
45               mgf=padding.MGF1(hashes.SHA256()),
46               salt_length=padding.PSS.MAX_LENGTH
47           ),
48           hashes.SHA256())
49
50   if __name__ == "__main__":
51       claimed_identity = sys.argv[1]
52       cert_file = sys.argv[2]
53       issuer_public_key_file = sys.argv[3]
54
55       with open(issuer_public_key_file, "rb") as public_key_file_object:
56           issuer_public_key = serialization.load_pem_public_key(
57                            public_key_file_object.read(),
58                               backend=default_backend())
59
60       with open(cert_file, "rb") as cert_file_object:
61           certificate_bytes = cert_file_object.read()
62
63       cert_data = validate_certificate(certificate_bytes, issuer_public_key)
64
65       print("Certificate has a valid signature from {}".format(ISSUER_NAME))
66
67       challenge_file = input("Enter a name for a challenge file: ")
68       print("Generating challenge to file {}".format(challenge_file))
69
70       challenge_bytes = os.urandom(32)
71       with open(challenge_file, "wb+") as challenge_file_object:
72           challenge_file_object.write(challenge_bytes)
73
74       response_file = input("Enter the name of the response file: ")
75
76       with open (response_file, "rb") as response_object:
77           response_bytes = response_object.read()
78
79       verify_identity(
80           claimed_identity,
81           cert_data,
82           challenge_bytes,
83           response_bytes)
84       print("Identity validated")

Listing 5-7Verify Identity in a Fake Certificate

清单 5-7 需要三个参数:声明的一方身份、出示的证书和发行者的公钥。

对所声称的身份的验证必须分两部分进行。首先,它加载证书以查看它是否由 HQ 的公钥签名。这由verify_certificate功能执行。请记住,如果签名检查失败,签名验证函数会引发异常。您会注意到,为了获得签名,脚本只需要证书的最后 256 个字节。因为签名连接在末尾,并且因为我们总是使用 2048 位密钥的 RSA 签名,所以签名总是 256 字节。

如果签名通过验证,我们使用json模块将其他字节加载到字典中(再次将 JSON 操作的字节转换为字符串,然后将公钥数据的字节转换为字符串)。

爱丽丝运行脚本:

python fake_certs_verify_identity.py \
  charlie \
  charlie.cert \
  hq_public.key

此时,Alice 的脚本已经给了她一些信息,但是它还在等待更多的输入。在这个过程的这个阶段,Alice 现在知道什么?她知道她得到了一份由 HQ 签名的真实证书。接下来会发生什么?她还不知道出示证书的人是否真的是查理。为此,她需要测试他或她是否有私钥。

她生成一条随机消息,并将其保存到文件charlie.challenge中,她将要求自称是查理的人用他的私钥签名。脚本正在等待这个随机消息,所以 Alice 提供了她刚刚创建的文件的名称,charlie.challenge

尽管 Alice 没有完成,我们现在需要切换到 Charlie 的操作。让爱丽丝的剧本一直运行到我们回来。Charlie 将使用另一个脚本和他的私钥来回答 Alice 的挑战。

 1   from cryptography.hazmat.backends import default_backend
 2   from cryptography.hazmat.primitives.asymmetric import rsa
 3   from cryptography.hazmat.primitives.asymmetric import padding
 4   from cryptography.hazmat.primitives import hashes
 5   from cryptography.hazmat.primitives import serialization
 6
 7   import sys
 8
 9   def prove_identity(private_key, challenge):
10       signature = private_key.sign(
11           challenge,
12           padding.PSS(
13               mgf = padding.MGF1(hashes.SHA256()),
14               salt_length = padding.PSS.MAX_LENGTH
15           ),
16           hashes.SHA256()
17       )
18       return signature
19
20   if __name__ == "__main__":
21       private_key_file = sys.argv[1]
22       challenge_file = sys.argv[2]
23       response_file = sys.argv[3]
24
25       with open(private_key_file, "rb") as private_key_file_object:
26           private_key = serialization.load_pem_private_key(
27                            private_key_file_object.read(),
28                            backend=default_backend(),
29                            password=None)
30
31       with open(challenge_file, "rb") as challenge_file_object:
32           challenge_bytes = challenge_file_object.read()
33
34       signed_challenge_bytes = prove_identity(
35           private_key,
36           challenge_bytes)
37
38       with open(response_file, "wb") as response_object:
39           response_object.write(signed_challenge_bytes)

Listing 5-8Prove Identity on a Fake Certificate

清单 5-8 中 Charlie 的脚本很简单。它接受三个参数:证书主体的私钥、质询文件名和将用于存储响应的响应文件名。只需获取挑战字节并用私钥对它们进行签名,就可以生成响应。如下所示运行该脚本(在与 Alice 不同的终端中):

python fake_certs_prove_identity.py \
  charlie_private.key \
  charlie.challenge \
  charlie.response

Charlie 因此回答了 Alice 的挑战,并将响应放入文件charlie.response。现在我们终于可以完成 Alice 的脚本了,它正在等待响应文件名。输入由 Charlie ( charlie.response)生成的文件名以继续。

Alice 的脚本加载响应并验证它。为此,Alice 的脚本现在移到了verify_identity函数。它首先检查证书中的名称是否与所声明的身份(例如“charlie”)相匹配,以及颁发者是否为 HQ。接下来,它从证书中加载公钥,并验证挑战字节上的签名是否有效。

这向 Alice 证明了不仅 Charlie 出示的证书是有效的,而且 Charlie 是主体(所有者)。声称是查理的人必须有相关的私钥,否则他将不能回答她的挑战。

练习 5.10。检测假查理

使用前面的脚本进行实验,检查出试图欺骗 Alice 时出现的各种错误。创建一个假的颁发者并用这个私钥签署证书。让拿错私钥的人出示查理的证书。确保理解代码中执行的所有不同的检查。

虽然我们的证书是“假的”,但它们旨在教授证书概念背后的基本原则。真正的证书通常使用称为 X.509 的格式。我们将在第八章中详细讨论 X.509。

证书和信任

你可能会问自己的一个问题是,我们为什么要给发行者命名?毕竟,如果 Alice、Bob 和所有其他代理总是信任 HQ,那么为什么要求在证书中列出颁发者的名字呢?

在我们假设的南极洲陷入内战的世界中,可能有许多证书的发行者。例如,除了间谍单位之外的其他机构可能要出具证明。EA 军方开始发证怎么办?EA 教育部开始发证怎么办?爱丽丝和鲍勃也应该相信这些吗?也许他们会想相信军官证而不是学历证?

在证书术语中,我们也称发行者为“认证机构”(CA),证书验证者必须决定他们将信任哪个认证机构。事实上,ca 也有它们自己的证书,包括它们的身份名称和它们的公钥。因此,证书的发布者字段应该与 CA 证书中的主体相同。

如果 CA 有证书,谁给签那个?有一个概念叫做“中间”CA。中级 CA 的证书由“更高级”的 CA 签署。在 EA 政府中,可能会有一个顶级 CA 来签署国防、教育、间谍等所有其他 CA。这将创建一个层次证书链,其中最高级别的证书称为“根”证书。

谁签署这个最终的根 CA?

答案是:本身。这个 CA 的证书被称为自签名证书。请注意,任何人都可以生成自签名证书,因此在决定信任哪个自签名根证书时必须非常小心。基本上,他们和他们签署的所有证书一起成为公认可信的*!*

虽然这看起来有点复杂,但它确实让事情变得更容易管理。整个 EA 政府可能只有一个顶级 CA。所有雇员、代理甚至公民只需要拥有最顶层的根 CA 证书。所有其他身份都可以在一个链中进行验证。例如,Charlie 可能持有三个证书:他的个人证书、为其签名的间谍 CA 的中间证书以及根 EA 证书本身。Charlie 可以将这三个证书呈现给任何其他 EA 员工,并让他或她验证链的根。

当有多个根时,事情会变得稍微复杂一些(并引入潜在的安全风险)。例如,也许 EA 政府没有一个单一的顶级根。毕竟,你真的希望你的间谍命令由一个可以追溯到政府的 CA 签署吗?假设 EA 政府有两个根基:一个是“公开”运作的部门和组织,另一个是秘密运作的团体和个人。

查理和其他特工应该信任这两个根源吗?

练习 5.11。我们在生活中锻造的锁链

修改身份验证程序以支持信任链。首先,为 EA 政府创建一些自签名证书(至少两个,如前所述)。现有的 issuer 脚本已经可以做到这一点。只需使自签名证书的颁发者私钥成为组织自己的私钥。因此,该组织正在签署其自己的证书,并且用于签署证书的私钥与证书中的公钥匹配。

接下来,为中级 ca 创建证书,如“教育部”、“国防部”、“间谍机构”等等。这些证书应该由上一步中的自签名证书进行签名。

最后,由间谍 CA 为 Alice、Bob 和 Charlie 签署证书。也许可以为国防部和教育部门的员工制作一些证书。这些证书应该由适当的中间 CA 签名。

现在修改验证程序,以获取一系列证书,而不仅仅是一个证书。去掉颁发者公钥的命令行参数,代之以硬编码哪些根证书文件名是可信的。要指定证书链,让程序将声明的身份作为第一个输入(已经这样做了),然后是任意数量的证书。每个证书的颁发者字段应该指示证书链中的下一个证书。例如,为了验证查理,可能有三个证书:charlie.certespionage.certcovert_root.certcharlie.cert的发行者应该与espionage.cert拥有相同的主题名称,以此类推。如果证书链中的最后一个证书已经被信任,验证程序应该只接受一个身份。

证书对于现代密码学和计算机安全非常重要。在第八章中,我们将介绍 real X.509 证书,并讨论 real CAs 如何运行以及其他问题和解决方案,作为学习 TLS 的一部分。

撤销和私钥保护

证书及其包含的公钥非常强大。同时,它们也有一个非常危险的致命弱点。如果相关的私钥泄露了,如何禁用它们呢?

我们这里说的是一个叫做“撤销”的概念撤销证书就是撤销发行人的背书。HQ 可能已经向 Charlie 颁发了证书,但是如果 Charlie 被抓获并且丢失了他的私钥,HQ 需要找到一种方法来告诉所有其他代理不要再信任该证书。

不幸的是,这并不容易做到。如果你还记得,CAs 而不是在线注册的出现的原因之一是对离线验证的渴望。离线验证过程如何提供实时撤销数据?

简单的回答是,“不能。”只有两个选择。要么验证过程必须具有实时部分,要么撤销不能实时更新。目前,这两种选项都可以用于在线证书状态协议(OCSP)和证书撤销列表(CRL)形式的证书,前者可以实时检查证书的状态,后者是不定期发布的带有已撤销证书的列表。我们将在第八章更详细地回顾这两者。

由于撤销证书的难度,私有密钥必须得到最大限度的保护。当不需要实时签名时,私钥应该脱机保存在安全的环境中。如果必须实时使用证书,并且必须将其存储在服务器上,则应该以必要的最低权限存储证书,并且在严格保密的基础上可读。对于最终用户密钥,例如用于电子邮件和其他应用的密钥,存储在磁盘上的私钥应该通过具有强密码的对称加密来充分保护。理想情况下,避免将私钥存储在台式机和服务器上(尤其是在连续备份的现代时代),而是将私钥存储在硬件安全模块中。

保留相对较短到期日期的证书并在必要时轮换它们可能不是一个坏主意。

重放攻击

在讨论消息完整性之前,还有最后一个安全问题需要解决。它同样适用于 MAC 和签名。问题是重放攻击

当先前通信中的合法消息在以后不再有效时被攻击者使用,就会发生重放攻击。

让我们考虑下面的信息:“我们在黎明时进攻!”

我们可以保护此消息不被修改,并用 MAC 或签名来验证发送者。但是什么能阻止 Eve 截取这条信息并在不同的日子发送出去呢?也许她会选择在 EA 正在而不是策划攻击的那一天发送它?Eve 可能无法更改消息内容;也许她甚至不能阅读它们,但这并不能阻止她随时重新发送(重放)信息。

出于这个原因,几乎所有加密保护的消息通常都需要某种独特的组件来将它们与所有其他消息区分开来。这段数据通常被称为随机数。在许多情况下,随机数可以是一个随机数。如果你快速回头看一下第三章,你会发现传递给 AES 计数器模式的 IV 值被称为 nonce。随机数,尤其是随机数随机数,也用于防止消息相同,如果这样做会引入安全漏洞的话。

然而,为了防止重放攻击,简单地使用随机数是不行的。为了检测重放,接收器必须跟踪已经使用的随机数,并在第二次看到它们时拒绝它们。

这可能会有很大的问题。应该保留多大的随机数列表?一百?一千?过了一段时间后,你会从列表中删除一个随机数吗?如果你这么做了,并且攻击者知道了,攻击者现在可以在重放中使用它。例如,如果攻击者知道您只跟踪最近 5 分钟内收到的随机数,那么攻击者可以重放从 6 分钟前开始的内容,并获得一定的成功。

一些系统使用时间戳而不是随机随机数。使用时间戳,接收者可以拒绝太旧的数据。这种方法的问题是所有的计算机都必须有同步的时钟才能可靠地工作。另外,带有“旧”时间戳的数据必须在某个窗口内被接受。毕竟,信息不会瞬间到达。你允许多大的窗户?不管它有多大,坏人都会想出办法用它来对付你。

将两种方法结合在一起是可能的。你可以发送带有时间戳和随机数的数据。时间戳用于删除真正旧的数据,随机数用于防止在允许的时间窗口内重放。这意味着时钟只需要相对接近(甚至可能在 24 小时内),并且要存储的随机数列表是有界的。

现在您已经看到了需要考虑在消息中发送的两个元数据:防止重放的 nonce 和/或时间戳以及发送者/接收者名称。通常,您应该将所有相关的上下文放入消息中,这样就不能在上下文之外使用它。

练习 5.12。山姆,再放一遍!

使用 MAC 或签名从 Alice 向 Bob 发送消息,反之亦然。在消息中包含一个 nonce,以防止使用本节中描述的所有三种机制进行重放。发一些 Eve 的回放,试着绕过 Alice 和 Bob 的防守。

总结-然后-MAC

新的一章,新的信息来源!在本章中,我们介绍了消息认证码,它是通过一系列数据计算得到的键控代码。没有密钥,就不可能不被察觉地更改数据。此外,当两方共享一个 MAC 密钥时,他们可以确定(除非共享密钥已经泄露)如果其中一方接收到正确的消息,它来自另一方。

使用非对称操作,可以使用私钥在一段数据上创建签名(通常在数据的哈希上)。与 MAC 操作不同,MAC 操作只能确保共享密钥的个人的正确性和真实性,理论上,任何人(信任它的人)都可以使用广泛分发的公钥来验证数据上的签名。

我们还提供了基本证书操作的快速概述。

而现在我们的总结已经完成,这里是 HMAC-SHA256(以十六进制表示)对前面三段(即来自“另一章……”通过”…证书操作。”)使用我们两次引用的 XKCD 密码:

c4d60c7336911cd0a23132f11ae1ca8ba392a05ae357c81bc995876693886b9e

现在你有办法知道在我们提交给他们之后,我们的编辑是否对摘要做了任何修改或变更!

这仍然只是一个非正式的定义。小气有正式的定义。 9

2

克霍夫原理再次来袭!

3

你困惑了吗?“Encrypt- -MAC”的意思是将它们都应用到明文上,而“Encrypt- Then -MAC”的意思是将 MAC 应用到密文上:加密后的*。*

4

椭圆曲线加密所基于的数学超出了本书的范围。这一节的目的只是让你了解算法,并向你展示如何使用它们。

5

现实世界中也发生了类似的情况:PGP 签名聚会。你可能想用你最喜欢的网络搜索引擎来寻找更多的信息。

6

记住,这些包含公钥,但也是签名的,等等。

7

虽然,记住证书是 public !不要将不想让其他人看到的信息放在证书中。也许那不是代号的正确位置?

8

显然,一些 web 服务器要求安装一个“证书”,但同时需要证书和私钥。这是对有明确含义的词的一种不幸的误用。证书是公共的,只包含公钥。私钥是私有的,不是证书的一部分。

9

告诉过你。

10

一个可信的权威。

******

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值