构建您自己的加密安全服务器/客户端协议
本文档介绍了实现您自己的安全协议所需的所有内容,包括可变密钥长度 RSA 加密/解密、数字签名、多精度库、Diffie-Hellman 密钥交换、Rijndael 等。所有这些都集成到一个安全的 IOCP 客户端/服务器聊天服务器中。
1. 摘要
信息时代也是数字信息资产的时代,专业程序员必须处理密码学。本文档介绍了可变密钥长度 RSA 加密/解密、数字签名、多精度库、Diffie-Hellman 密钥交换、熵收集、伪随机数生成器等的理论、源代码和实现。本文档通过提供安全的聊天客户端/服务器解决方案实现,介绍了如何使用 IOCP 技术实现您自己的安全协议。
2. 要求
- 本文档假定读者熟悉 C++、TCP/IP、套接字编程、MFC 和多线程。
- 需要一些关于初等数论和统计学的数学知识。
- 源代码使用 Winsock 2.0 和 IOCP 技术,需要
- Windows NT/2000 或更高版本:需要 Windows NT 3.5 或更高版本。
- Windows 95/98/ME:不支持。
- Visual C++ .NET 或完全更新的 Visual C++ 6.0。
3. 引言
信息时代也是数字信息资产的时代。专业开发人员必须处理密码学,以确保数据存储和传输的安全。本文档的目的不是“重新发明轮子”或实现不安全自制的加密算法。本文档侧重于加密安全协议的实际细节,并提供用于任何类型客户端/服务器应用程序的安全客户端/服务器解决方案的理论和源代码。
了解密码学的理论对开发人员至关重要,但实际实现却很困难。存在许多安全漏洞,如缓冲区溢出 [1] 等,这些漏洞是在实现理论上安全的算法时出现的。市场上存在许多商业/免费的高质量密码学库,如 Crypto++ [2]、OpenSSL [3] 和 Crypttool [4]。要使用这些库,开发人员必须了解其背后的密码学理论,并意识到“库的内部发生了什么”。这并非易事,因为这些库的内部结构可能很复杂,并且库包含的附加功能并非总是必需的。
本文档简要解释了涉及加密安全通信协议的密码学理论,并介绍了如何通过提供安全的聊天客户端/服务器解决方案来实现。
4. 背景
本节解释了理解文章其余部分(即第 5 节)所需的密码学理论和术语。如果您认为自己经验丰富且知识渊博,并希望动手实践,请继续阅读下一节(5. 实现)。
通过使用加密算法,可以安全地进行数据存储和传输。使用算法更改数据,使其只有授权接收者能够重新构造数据,这称为加密。对于未经授权的接收者,加密数据看起来就像一串无意义的随机比特。密码学算法,也称为密码,是一种数学函数,它以明文(数据)作为输入,并生成密文(加密数据)作为输出(反之亦然)。使用秘密密钥和密码来加密明文,并使用相同的密钥或另一个密钥来解密密文以恢复明文。
不同的密码学算法或密码具有不同的数学特性和弱点。密码的细节通常是公开的。现代密码的安全性在于秘密密钥,而不是密码的细节。如需更多安全信息和知识,请阅读并尝试 Crypttool [4]。Crypttool 软件演示了多种加密攻击实现,并为您提供了有关本文档中使用算法的附加信息。
4.1. 对称加密
在对称加密中,发送者和接收者使用相同的秘密密钥来加密和解密明文。这意味着发送者和接收者必须在实际开始通信之前,已经通过安全的通道交换了公共(秘密)密钥。
+
对称算法的优点是数据加密和解密速度快。
-
一个缺点是密钥管理的需求。为了保密通信,发送者和接收者在实际开始通信之前,必须通过安全的通道交换密钥。
大多数现代对称算法都操作明文块。这些过程通常包含许多复杂的比特移位和转换轮,以防止各种数学分析攻击。在第 5 节中,我们将根据其属性和密码强度选择对称加密。
4.2. 非对称加密
与非对称加密不同,每个订阅者都有一个包含秘密密钥和公钥的个人密钥对。公钥是公开的,这意味着任何人都可以获得它。密钥在数学上相关,但从一个推导出另一个在计算上是困难的。使用公钥,任何人都可以加密明文,只有拥有私钥的人才能解密消息。
使用非对称加密,网络上的两个实体(称它们为 Alice 和 Bob)可以通过以下简单协议安全地通信:
- Bob 和 Alice 交换公钥。
- Alice 使用 Bob 的公钥加密她的消息并发送给 Bob。
- Bob 使用 Alice 的公钥加密他的消息并发送给 Alice。
- Bob 使用他的私钥解密 Alice 的消息,Alice 使用她的私钥解密 Bob 的消息。
现在,他们可以通信了,因为他们知道自己的私钥,而且没有人可以解密消息,因为他们没有 Alice 和 Bob 的私钥。由于“中间人攻击”和我们将在第 4.3 节和 4.5 节中讨论的实际原因,这并非完全属实。
+
优点是,在传输消息之前不需要安全的通道,因为所有保密通信所需的信息都可以公开发送。
-
缺点是,纯粹的非对称过程比对称过程耗时更长。因此,非对称加密用于交换用于对称加密的会话密钥。
最著名的非对称算法是 RSA 算法 [可在 5, 10 中找到],以其开发者 Ronald Rivest、Adi Shamir 和 Leonard Adleman 的名字命名。RSA 算法于 1978 年发布。RSA 算法可用于公钥加密和数字签名。其安全性基于分解大整数的难度。我们将在第 4.6 节讨论 RSA 加密算法及其实现。
4.3. 中间人攻击
Alice 和 Bob 之间上面第 4.2 节描述的公钥非对称加密安全通信协议容易受到中间人攻击。
假设 Mallory 是一个敌对的黑客,并且可以
- 监听 Alice 和 Bob 之间的流量。
- 可以修改、删除和替换 Alice 和 Bob 的消息,还可以插入新消息。
Mallory 可以冒充 Alice 与 Bob 对话,也可以冒充 Bob 与 Alice 对话。这在通过 Internet 进行的真实通信中是可能的。
攻击者示例
- Bob 发送他的公钥给 Alice。Mallory 截获该密钥并将他自己的公钥发送给 Alice。
- Alice 生成一个随机会话密钥,用 Bob 的公钥(实际上是 Mallory 的)加密它,然后发送给 Bob。
- Mallory 截获消息。他用自己的私钥解密会话密钥,用 Bob 的公钥加密它,然后发送给 Bob。
- Bob 收到消息,以为它来自 Alice。他用自己的私钥解密它,并获得会话密钥。
Alice 和 Bob 开始使用会话密钥交换消息。Mallory 也有会话密钥,现在可以解密整个对话。当然,这可以通过使用单向哈希函数(第 4.4 节)对消息包进行数字签名(第 4.5 节和 5.3 节)来解决。
4.4. 消息摘要
消息摘要,也称为单向哈希函数,是一种数学函数,它接收可变长度的输入并将其转换为固定长度的二进制序列,称为哈希值。消息摘要的另一个重要属性是其逆向过程的困难性。给定哈希值,很难或不可能找到输入。此外,好的哈希函数也使得找到两个产生相同哈希值但输入不同的数据变得困难。即使输入数据发生微小变化,哈希值也会发生剧烈变化,因此,消息摘要用于检查数字数据的完整性/正确性。
这使得单向哈希函数成为公钥密码学中的核心概念,并在生成消息的数字签名(第 4.5 节和 5.3 节)时使用。消息摘要也用于分发随机性。例如,一个 8 位的真随机数被一个消息摘要消耗,并变成 160 位,其中随机性分布在这 160 位中。最流行的单向哈希算法是 MD4 和 MD5(两者都产生 128 位哈希值),以及 SHA,也称为 SHA1(产生 160 位哈希值)。
4.5. 数字签名
数字签名主要概念是接收者可以验证文档或消息是否来自特定个人或公司。数字签名使用非对称加密(通常是 RSA)和消息摘要来签名和验证消息,如下面的过程所示。为了做到这一点,我们需要一个我们信任的强非对称公钥 (n,e)。如果您使用私钥加密消息,任何人都可以使用您的公钥解密它。这有什么用?嗯,它证明是您加密的,并且自您加密以来没有被更改。这构成了数字签名。的基础。
通常,一个大家都信任的独立第三方,其职责是颁发证书,称为证书颁发机构(CA)。证书是一个完全标识实体的数据包,并且仅在 CA 验证了实体的身份后才由该机构颁发。该证书可以包含每个人都信任的非对称公钥。
在本文档中,我们将可信的非对称公钥 (n,e) 硬编码到客户端软件中(也将在 5.6.3 节中讨论),而不使用其他方。稍后将在第 5.3.4 节讨论这一点。下面描述了数字签名过程。
4.5.1. 数字签名假设
签名者 A 拥有私钥 (d,e) 和公钥 (n,e)。接收者 B 已经信任 A 的公钥 (n,e)。
4.5.2. 数字签名过程
- 创建要发送的信息的消息摘要或哈希,我们称之为 m(假设 m<n)。
- 签名者 A 使用私钥计算签名(s = m^d mod n)。
- 将此签名 s 和消息发送给接收者 B。
4.5.3. 签名验证过程
接收者 B 执行以下操作:
- 使用发送者 A 的公钥 (n, e) 计算一个整数(v = s^e mod n)。
- 独立计算已签名信息的哈希值。
- 如果两个消息摘要相同,则签名有效。
该实现可以在提供的源代码的 MyCryptLib::DemoDSA(..)
中找到。
4.6. RSA 算法
在本节中,我们将简要介绍 RSA 算法的工作原理,稍后将讨论其实现。RSA 非对称算法 [可在 5 中找到] 以其开发者 Ronald Rivest、Adi Shamir 和 Leonard Adleman 的名字命名。几乎所有已知的商业安全协议都依赖于 RSA 公钥交换。RSA 的强大之处在于分解两个非常大的素数的难度,p 和 q。
4.7. RSA 过程
RSA 算法的安全性,与其他所有公钥方法一样,取决于从公钥 (n, e) 计算私钥 d 的难度。这是通过使分解公钥 n 中的两个素数 (p,q) 的乘积变得困难来实现的(下面第 4.7.1 节)。因此,素数 (p,q) 必须非常大。
4.7.1. 标准 RSA 算法描述
n = pq where p and q are distinct primes.
phi, φ = (p-1)(q-1)
e < n such that gcd(e, phi)=1, usually e=3.
d = e^-1 mod phi, d is the private key.
Encryption:
c = m^e mod n, 1<m<n.
Decryption:
m = c^d mod n.
这意味着我们需要处理大整数,并且还需要生成大素数。我们将在稍后第 5.3 节讨论此算法的实现。
4.8. 随机数 - 所有密码学安全性的基础
随机数用于生成密钥、素数等。因此,随机数或随机比特是所有密码学安全性的基础。如果数字不是真正的随机,安全性很容易被打破。计算机是确定性的(计算机器),无法产生或处理真正的随机性,这是核心问题。在本节中,我们将讨论随机性是什么,以及计算机如何生成伪随机数。
4.8.1. 熵,统计随机性的数学名称
统计学告诉我们一些关于随机比特的信息。零的出现频率应该与一的出现频率大致相同。存在统计检验,例如卡方检验,您可以在数字流上执行这些检验以显示其随机程度。比特流遵循此统计形式的程度,就是其熵的程度。这是严格的数学定义。
4.8.2. 伪随机数生成器
伪随机数生成器是一种算法,它生成一串数字,这些数字表现得像随机数,但实际上并非如此。该算法从一个“种子”开始,这是一个起始数字,然后使用该种子生成一系列“看起来”随机的数字。
通常,所有伪随机数生成器都有一个周期。这意味着,过一段时间后,它们就会开始遵循某种模式。还有其他“糟糕”的特性,例如:
- 某些种子状态的周期比预期短(周期取决于种子值)
- 维度分布较差
- 值之间相互依赖
- 某些比特比其他比特“更随机”
- 缺乏均匀性(并非所有情况都同样可能)
1999 年末,ASF 软件 Texas Hold'em 扑克应用程序使用了标准的 rand()
来洗牌。Cigital [12] 发现,只需知道五张牌就可以预测剩余的牌。用于生成随机值的伪随机数生成器算法非常重要,因为我们已经讨论了这些方面。在此实现中,我们使用 Makoto Matsumoto 和 Takuji Nishimura 于 1997 年开发的 Mersenne Twister 伪随机数生成器,稍后将在第 5 节详细介绍。
4.8.3. 收集熵
收集真正的随机数很难,但却是必需的。存在生成真实噪声的硬件设备,如观测宇宙射线通量等。然而,我们需要使用我们拥有的设备,即计算机和用户。指示用户在屏幕上随机移动鼠标(如源代码中的 RandDialog
)或键入一些随机字母,是一种生成数据的不错方法,但这需要花费太多时间,而且我们通常没有用户(对于服务器)。使用计算机中已有的具有一定熵(随机性)的硬件也是另一种方法。
- 计算机中的时钟是一个相当不错的熵源,分辨率更高的时钟更好。但是,如果我们使用,例如,
TickCount()
来获取随机种子,搜索很快就能找到。一年只有 3150 万个滴答。这是在使用硬件的不同部分时。 - 在 1994 年的 Crypto 会议上,Davis、Ihaka 和 Fenstermach 表明,硬盘驱动器内部的空气湍流产生的随机性足以生成加密强度的随机数。
- 使用麦克风。读取许多计算机内置的麦克风可以提供看似随机的数据源。
一旦收集了随机数据,我们就必须分发随机性,或者以一种好的方式将其提炼出来,以便所有比特都同样随机。最好的方法是使用某种消息摘要(在第 4.4 节中讨论)。
4.8.4. 总结
对称加密 |
当使用相同的密钥进行加密/解密时。对称密码速度快,但问题是通信实体需要交换密钥。 |
非对称加密 |
使用公钥加密数据,使用另一个秘密密钥解密数据。主要用于此的算法是 RSA,它基于分解大素数的难度。很难从私钥推导出秘密密钥。通信可以在公开环境中进行,但对于“中间人攻击”不安全。 非对称加密速度慢,仅用于交换密钥。 |
中间人攻击 |
可以监听、修改和删除网络中两个实体之间的数据包。因此,黑客有可能在网络中冒充他人成为“中间人”。解决方案是数字签名。 |
消息摘要 |
是一个非常重要的一元函数,它接收可变大小的数据并产生固定大小的哈希值。这用于数字签名数据、计算数据校验和、保护明文密码,以及分发数据中的随机性(熵)。 |
数字签名 |
可以基于 RSA 算法。是一种确定数据确实来自发送者的方法(消除中间人攻击)。 |
随机数 - 所有密码学安全性的基础 |
随机数用于生成素数、密钥等。因此,它们在密码学中扮演着重要角色。生成真正的随机数既困难又耗时。而且通常需要特殊设备。可以使用具有良好统计特性的带种子的伪随机数生成器代替。种子可以使用不同的源收集大量熵,并用消息摘要进行提炼。 |
5. 实现
在本节中,我们将描述实现安全协议的步骤。讨论了所选方法的理由、算法及其可靠性和性能。此外,简要解释并展示了源代码。
5.1. 协议设计 – 总体概述
要实现安全协议,我们需要一个对称密码(第 4.1 节)和一个使用非对称加密的密钥交换过程(第 4.2 节)。
图 1。 该图表示实现中涉及的类之间的依赖关系。
该协议使用三个类实现,即 MyCryptLib
、CRijndael
和 IOCPS/SecureChatIOCP
(见图 1)。MyCryptLib
类包含密钥交换过程和数字签名周围所有细节的源代码,CRijndael
是对称密码类,而 IOCPS/SecureChatIOCP
是一个全面的 IOCP 服务器类 [6]。有关 IOCP 技术,请阅读我的另一篇文章 “一个简单的 IOCP 服务器/客户端类” [6]。本节将详细介绍实现细节。
5.2. 对称密码
Rijndael 被选为高级加密标准(2000-10-02)[7]。该算法的设计符合密码学研究的最新水平。该算法速度快,并且能抵抗线性密码分析、差分密码分析等密码学攻击。然而,Rijndael 算法可以写成一个多元二次方程组。由于没有有效的算法来求解这些方程组,因此求解它们以获得密钥或明文是困难的。不过,这个领域的研究很多,已经提出了许多方法,如 XL 和 XLS,来求解大型多元二次方程组 [8]。因此,128 位 Rijndael 可能不再安全。也有可能破解 256 位 Rijndael 的算法提案,但这不确定 [8]。目前,(2005 年)256 位 Rijndael 对于我们的任务来说足够安全,但未来可能并非如此。
对称的 256 位 Rijndael 密码使用 CRijndael
类实现。该密码使用 256 位密钥,并加密/解密 16 字节大小的块。
图 2: 该图显示了数据如何封装在加密包中。包大小表示包的大小。里面的数据大小表示实际数据的大小。如果数据不适合 16 字节块,则用随机数据填充。此外,还会添加 CRC16 校验和到块中以增加安全性。
数据被打包成多个块(图 2),如果数据不适合块,则用随机数填充。还会添加 CRC16 校验和到块中,以避免攻击者更改加密数据,并确定解密是否成功。加密和解密的实现由函数 SecureChatIOCP::EnCryptBuffer(..)
和 SecureChatIOCP::DeCryptBuffer(..)
完成。
5.2.1. 实现源代码
对于每个包,都使用密码块链接(CBC)来加密数据。在 CBC 模式下,通过首先将数据块与前一个加密块进行 XOR 运算,然后加密结果值来获得加密块。这意味着我们需要输入一个 16 字节块和密钥来进行加密/解密。每个连接/客户端在其上下文中都有一个 CRijndael
类的实例,该类使用密钥和初始链块进行初始化,通过函数
// key - The 128/192/256-bit user-key to use. // chain - initial chain block for CBC and CFB modes. // keylength - 16, 24 or 32 bytes // blockSize - The block size in bytes // of this Rijndael (16, 24 or 32 bytes). void CRijndael ::MakeKey(char const* key, char const* chain, int keylength=DEFAULT_BLOCK_SIZE, int blockSize=DEFAULT_BLOCK_SIZE) { …. }
SecureChatIOCP
类中的函数 EnCryptBuffer(CIOCPBuffer *pBuff, ClientContext *pContext)
和 DeCryptBuffer(CIOCPBuffer *pBuff, ClientContext *pContext)
用于加密/解密缓冲区,如图 2 所示。有关更多信息和细节,请参阅源代码。
5.3. 非对称加密 – RSA/DSA 实现
在第 4.5-4.8 节中,我们讨论了 RSA/DSA 是什么,并且我们知道要使用非对称加密,我们需要进行非常大的数字计算。然而,这是不可能的,因为硬件只支持 32 位或 64 位操作。这就是为什么我们需要回到我们旧的、普通的计算书籍中,寻找数值算法来实现使用现有硬件进行任意位长计算。这就是为什么我们实现了一个多精度库。此外,我们需要生成非常大的素数(第 4.6 节)。
5.3.1. 多精度库
通过使用数值算法 [9],在 MyCryptLib
类中实现了一组函数,称为 BN*(…)
(BN 代表 Big Number)。实现的重点是简单性,而不是性能。这些函数使用操作类型为 DWORD
的现有硬件实现了我们任务所需的所有操作。BN*(…)
函数可以处理任意位长的数字。了解内部工作原理非常重要,因为可以使用不同的实现漏洞来入侵系统(第 2 节中讨论)。
用于实现加法、除法和减法等不同算术运算的算法很复杂,因此我们只在此解释其中一个,即加法。
让我们先从加两个二进制位开始。由于每个位只有两个可能的值,0 或 1,因此只有四种可能的输入组合。这四种可能性和结果总和是:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 01
第四行表明我们有溢出!我们需要两位来存储输出!因此,可以使用图 3 来对两位数进行加法。进位(表示一位加法器的溢出)被馈送到加法器 *2 * 以便加到 *S2 *。通过这样做,我们可以使用两个 1 位加法器来相加两个位。
图 3。 该图说明了一个两位加法器。A、B 是两位数,S 是两位数。计数表示是否有溢出。
以类似的方式,我们可以使用 32 位硬件来处理任意长度的数字。请参见下面的源代码,关于加法。
DWORD MyCryptLib::BNAdd(DWORD C[], const DWORD A[], const DWORD B[], const UINT nSize) { DWORD k=0; // carry for (UINT i = 0; i < nSize; i++) { C[i] = A[i] + k; if(C[i]>=k) // detect overflow and set k k=0; else k=1; C[i] += B[i]; if (C[i] < B[i]) // Detect overflow again. k++; } return k; }
有关实现的更多信息,请参阅源代码。另外,请查看 MyCryptLib
类中的 DemoSimpleTest(..)
函数。
5.3.2. 素数生成
- 要生成素数,我们需要以下内容:
- 我们需要收集熵(真随机性)来为随机生成器提供输入。
- 一个具有良好统计特性的快速伪随机生成器。
- 最后需要的是一个快速的函数来确定一个数字是否为素数。
收集熵
如第 4.8.3 节所述,收集熵并非易事。在我们的应用程序中,我们使用 CRanDialog
类从用户那里收集熵,该类在用户随机移动鼠标时收集随机数据。如果用户不在,则从不同的硬件组件收集熵,如下面的源代码所示。
BOOL MyCryptLib::MTCollectEntropy(BYTE *pRandomPool, UINT nSize) { //… while ( nSize-nCollected>0 ) { // Hash the previus entropy Bucket.. SHA1_Hash(pEntropyBucket,SHA1_DIGEST_SIZE,&csha1); // Destill The process ID dwRes=GetCurrentProcessId(); SHA1_Hash((BYTE*)&dwRes,sizeof(DWORD),&csha1); // Destill The thread ID dwRes=GetCurrentThreadId(); SHA1_Hash((BYTE*)&dwRes,sizeof(DWORD),&csha1); // Destill The system time. GetSystemTime(&st); SystemTimeToFileTime(&st, &ft); SHA1_Hash((BYTE*)&ft,sizeof(FILETIME),&csha1); //Destill The processors tickcount. dwTick = GetTickCount(); SHA1_Hash((BYTE*)&dwTick,sizeof(DWORD),&csha1) // Destill The memory allocated // GlobalMemoryStatus(&ms); SHA1_Hash((BYTE*)&ms, sizeof(MEMORYSTATUS),&csha1); // Put it inside the Bucket. SHA1_Finish(EntropyBucket,&csha1); // Copy the Entropy to the pool if ( nSize-nCollected < SHA1_DIGEST_SIZE ) { memcpy(pRandomPool+nCollected, pEntropyBucket,nSize-nCollected); nCollected+=nSize-nCollected; } else { memcpy(pRandomPool+nCollected, pEntropyBucket,SHA1_DIGEST_SIZE); nCollected+=SHA1_DIGEST_SIZE; } } return TRUE; }
随机生成器
收集到的熵(来自用户或硬件,使用 MTCollectEntropy(..)
)被馈送到 Mersenne Twister 随机生成器 [11]。
该生成器具有令人难以置信的周期 2^(219937 – 1)。这是一个有 6000 位十进制数字的数字。宇宙中的基本粒子数量“仅”估计为 80 位数字。该算法速度也非常快,并且具有非常好的统计特性。32 位随机数在高达 623 的维度上表现出最佳的等分布特性。请参阅源代码 DWORD MyCryptLib::MTRandom()
和 BOOL MyCryptLib::MTInit(BYTE *pRandomPool, UINT nSize)
以获取更多信息。)
Rabin-Miller 概率素性检验
Rabin Miller [11] 是一种算法,用于确定给定的数字是否为素数。该算法是概率性的,这意味着它不是 100% 安全的,但速度非常快。我们将使用此算法来查找素数,如下所示:
- 从一个大小为
nSize
的随机数 A 开始。 - 使其变为奇数。
- 使用一小部分素数和 Rabin-Miller 来检查该数字是否不是素数。
- 如果该数字不是素数,则加 2,直到找到素数为止。
请参阅函数 BNMakePrime(…)
、BNMakeRSAPrime(..)
和 RSAGenerateKey(..)
以获取有关素数生成的更多信息。正如我们在图 4 中看到的,找到更大的素数需要更长的时间,原因有两个:查找素数的计算时间更长,并且素数在变大时密度降低。在 Centrino 笔记本电脑上生成 4096 位 RSA 密钥大约需要 73 秒。
图 4。 该图表示生成 RSA 密钥所需的时间。X 轴表示比特大小,Y 轴表示时间(秒)。
5.3.3. RSA 加密/解密
RSA 加密/解密在第 4.7 节中进行了讨论。如需更多实现细节,请阅读 MyCryptLib::DemoRSA(..)
函数。此处讨论的一个重要方面是性能。使用不同密钥长度加密数据的时间在 0.01 毫秒到 1 毫秒之间,具体取决于密钥长度。
然而,解密消息比加密消息耗时更长(图 5)。使用一种称为中国剩余定理(CRT)[10] 的不同解密方法可以显著减少时间。该算法(图 5 中的红线)比第 4.7 节中讨论的标准解密(m = c^d mod n)(图 5 中的蓝线)快得多。
图 5。 在图中,我们可以观察到 RSA 算法的解密对于不同的密钥长度需要多长时间。可以看出,与标准解密算法 (m = c^d mod n) 相比,CRT 提供了更好的性能。
在 CRT 算法中,私钥表示为一个五元组 (p, q, dP, dQ, qInv),其中 p 和 q 是 n 的素数因子,dP 和 dQ 称为 CRT 指数,qInv 是 CRT 系数。CRT 解密方法比计算 m = c^d mod n(对于大密钥长度)整体快四倍。
私钥的额外值是:
dP = (1/e) mod (p-1)
dQ = (1/e) mod (q-1)
qInv = (1/q) mod p where p > q
这些值是预先计算并与 p 和 q 一起保存为私钥的。为了计算消息 m,给定 c,执行以下操作:
m1 = c^dP mod p
m2 = c^dQ mod q
h = qInv(m1 - m2) mod p
m = m2 + hq
即使此过程涉及更多步骤,需要执行的模幂运算使用的指数也短得多,因此总体成本较低。此实现的实现在 MyCryptLib::RSADecryptCRT(…)
中。
5.3.4. 数字签名实现
数字签名的详细信息在第 4.5 节中讨论。根据第 4.5.2 和 4.5.3 节,数字签名是通过 MyCryptLib
类中的函数 DigitalSignSHA1rDSA(..)
和 DigitalVerifySHA1rDSA(..)
实现的。该函数使用 SHA1 消息摘要来计算消息的校验和,然后对其进行签名/验证。如需更多信息和示例,请参阅 MyCryptLib
类中的 DemoDSA(..)
函数,或使用 Server->CryptoLibrary->GenerateKey。
验证签名并不消耗 CPU(耗时不到 1 毫秒,取决于密钥大小),但计算签名非常耗时,如下面的图 6 所示。我们可以观察到(图 6)使用 2688 位密钥长度签名一个消息需要 1 秒。因此,不可能使用该大小签名发送给客户端的每一条消息。
图 6。 该图显示了使用 DigitalSignSHA1rDSA(..)
函数和不同密钥大小计算签名的时间(以毫秒为单位)。使用 2688 位密钥长度签名消息需要 1 秒。
5.4. 密钥交换过程
Whitfield Diffie、Martin E. Hellman 和 Ralph Merkle 于 1976 年在斯坦福大学开发了 Diffie-Hellman (D-H) 密钥交换协议。该协议可防止“窃听”,这意味着没有人可以知道交换的秘密密钥,但无法防止“中间人”攻击(第 4.3 节讨论)。该算法的安全性在于解决离散对数问题的难度,即:如果 g= 2 或 5,p 是公用素数,并且已知 A=g^a mod(p),则计算秘密随机密钥 a。如果 p 大于 1024 位,这非常困难,但是,如果秘密随机密钥不包含足够的熵,则可以预测该密钥,从而破坏算法。
算法描述
- 数字 g、p、A、B 是公开的。
- Alice 生成一个秘密密钥 a,并计算 A= g^ a mod(p)。
- Bob 生成一个秘密密钥 b,并计算 B=g^b mod(p)。
- 密钥 A、B 被交换。
- Alice 计算 S=B^a=g^(b*a) mod (p)。
- Bob 计算 S=A^b=g^(a*b) mod (p)。
- 由于数学交换律 g^(b*a) mod (p) = [a*b=b*a] = g^(a*b) mod(p),Alice 和 Bob 拥有相同的秘密密钥。该算法的实现和使用可以在
MyCrypt
类中的DemoDiffieHellman(..)
函数中找到。在下图,我们可以看到根据此算法交换密钥所需的计算时间。
图 7。 该图显示了安全密钥交换(以毫秒为单位,Y 轴)与公钥 p 的长度相比所需的时间。我们可以看到,对于 2048 位密钥,交换密钥需要 600 毫秒。
5.5. 密钥长度
上一篇文章中介绍的所有算法的安全性都取决于密钥长度和密钥生成算法。经验法则是拥有密钥大小的 10%-20% 的熵(真随机性)比特。1999 年,使用由 Buhler、Lenstra 和 Pomerance 开发的数域筛法 (GNFS) 的实现分解了一个 512 位公钥。破解该密钥所需的时间是五个月。这意味着 512 位长度的密钥不再安全。
这清楚地表明,512 位的模块长度不再能防御攻击者。在过去的 20 年里,取得了很大的进展。关于公钥分解能力未来发展的估计各不相同,并且取决于一些假设:
- 计算性能的发展(摩尔定律:每 18 个月,计算能力翻倍)和网格计算。
- 素数分解是数论和计算机科学中许多热门研究领域的一部分。由于对素数的新认识,进展比预期的要大。
实际上,普通人要破解 512 位密钥仍然几乎不可能,但像 NSA 这样拥有超级计算机的组织可能可以破解密钥。安全密钥的长度通常为 1024 位(2005 年),并且应每年增加 24 位。在第 5.3 节的图 5-7 中,可以看到使用更大的素数或密钥时,不对称加密的计算时间呈指数增长。
“哦,我们的系统使用了 4096 位安全”,听起来很强大,但使用过大的密钥会降低性能,而且由于密钥生成过程中的安全问题(密钥大小的 10%-20% 熵比特、随机数生成器的属性等),还会产生虚假的安全感。
在我们的系统中,我们将使用大于 2048 位密钥长度来生成数字签名。数字签名用于防止“中间人攻击”(第 4.5.2 和 4.5.3 节讨论),并且需要在服务器端有效很长时间,因为它用于客户端验证发件人。对于密钥交换过程(第 5.4 节),服务器接受大于 1024 位的密钥。
这意味着总的密钥交换过程,包括数字签名,在 Centrino 笔记本电脑上大约需要 600 毫秒(来自第 5.4 节图 7 的 100 毫秒,加上第 5.3.4 节图 6 的 500 毫秒)。
5.6. 整合
服务器使用 IOCP 技术实现 [6]。服务器和客户端演示中的通信协议在 SecureChatIOCP
类中实现。该类继承自使用 IOCP 技术的 IOCPS [6]。一个存储结构(ClientContext
,在 iocps.h 中找到)与 IOCP 的每个连接相关联。从多个连接接收的所有包仅由一个或多个称为 IOWorker
的线程处理。这些线程调用不同的函数,如 NotifyReceivedPackage(..)
、NotifyNewConnection(..)
等,这些函数的实现定义了通信协议。通常,使用更复杂的学术有限状态机模型来实现与 IOCP 的复杂协议,但我们将保持尽可能简单。请阅读我的文章 “一个简单的 IOCP 服务器/客户端类” 以获取更多信息和有关 IOCP 技术的详细信息。
所有实现都在 IOCPS
类和 SecureChatIOCP
中。通过定义 _IOCPClientMode_
,我们将协议行为从服务器更改为客户端。
我们首先定义一些包,以及它们在 NotifyReceivedPackage(..)
中如何被处理。
5.6.1. 定义包
下表描述了不同的包结构。包的第一个字节从表顶部(黄色)开始。
包结构 1 |
包结构 2 |
包结构 3 |
4 字节,定义包的大小 |
4 字节,定义包的大小 |
4 字节,定义包的大小 |
1 字节定义包类型。 |
1 字节定义包类型。 |
1 字节定义包类型。 |
可变大小,负载,通常包含另一个加密的包或一个以 null 结尾的字符串。 |
4 字节定义负载大小(以 DWORD 为单位)。 |
4 字节定义字符串(payload1)的大小(以字节为单位)。 |
可变大小的负载(通常包含公钥或签名)。 |
大小可变的 Payload1(通常包含一个以 null 结尾的字符串(用户名和密码))。 | |
4 字节定义字符串(payload2)的大小(以字节为单位)。 | ||
可变大小的 Payload2(通常包含一个以 null 结尾的字符串)。 | ||
表 1: 描述了客户端和服务器之间传输的各种包结构。 |
这些包具有表中描述的结构之一,并且通常在 BuildAndSend(..)
、SendPublicKey(..)
、SendTextMessage(..)
和 SendErrorMessageTo(..)
函数内部构建,位于 SecureChatIOCP
类中。
包类型在下表中定义,并使用相应的函数 OnXXXX(..)
进行处理,其中 XXXX
表示包类型(例如,OnPKG_PUBLIC_KEYA(..)
)。
包(枚举类型 PackageTypes) |
如何处理包 | |
封装在结构 1(表 1)中 |
这种类型的消息包含一个将发送到客户端的错误消息。该包由客户端接收,并显示错误消息。发送包后,连接将立即关闭。 | |
封装在结构 2(表 1)中 |
此包由客户端发送。根据 Diffie-Hellman (D-H) 密钥交换协议(第 5.4 节),该包包含公钥 P。当服务器收到此包时,它会保存公钥,生成一个 512 位私钥,并通过 | |
封装在结构 2(表 1)中 |
此包由服务器发送,根据 Diffie-Hellman (D-H) 密钥交换协议(第 5.4 节),包含公钥 A。此包由客户端接收,客户端生成私钥,计算会话密钥( | |
封装在结构 2(表 1)中 |
此包由客户端发送到服务器(参见 | |
封装在结构 2(表 1)中 |
此包由服务器发送,包含一个签名,用于确认包 | |
封装在结构 1(表 1)中 |
此包总是在密钥交换过程之后发送,并封装在 当客户端收到时,内容显示在聊天客户端的文本框中。但是,如果服务器收到,则将其发送给所有人。 | |
封装在结构 3(表 1)中 |
此包始终作为客户端接受签名的响应发送。此包封装在 | |
|
此包在密钥交换过程之后发送,并在 |
表 2: 描述了不同类型的包及其处理方式。
5.6.2. 密钥交换细节
用于实现安全聊天的技术包括数字签名/验证(第 5.3.4 节)、Diffie-Hellman (D-H) 密钥交换协议(第 5.4 节)和第 5.2 节中解释的 Rijndael。
密钥交换过程如下进行(有关实现细节,请阅读前面的第 5.6.1 节,表 1 和表 2)。
- 客户端生成公钥 p;公钥 g 始终等于五。将类型为
PKG_PUBLIC_KEYP
的包发送到服务器。这在客户端连接到服务器时完成,并在NotifyNewConnection(..)
函数中执行。 - 服务器接收包
PKG_PUBLIC_KEYP
并发送包PKG_PUBLIC_KEYA
(参见第 5.6.1 节)。 - 客户端接收
PKG_PUBLIC_KEYA
。客户端计算会话密钥,并根据第 5.6.1 节将PKG_PUBLIC_KEYB
发送到服务器。 - 服务器接收
PKG_PUBLIC_KEYB
。服务器计算公共会话密钥,并在PKG_SIGNATURE
中发送签名到客户端。 - 客户端验证签名,并将
PKG_USERNAME_PASSWORD
包发送到服务器。
现在,客户端和服务器都拥有一个共同的秘密会话密钥,根据 Diffie-Hellman (D-H) 密钥交换算法,并且客户端也受到“中间人攻击”的保护。
5.6.3. 数字签名细节
为了对消息进行数字签名,客户端必须信任一个公钥。此受信任的公钥已硬编码在软件中(即使它是公开的),作为 SecureChatIOCP
类中声明的全局常量 SecureChatIOCP::m_PubN
。相同的公钥和秘密密钥也已硬编码到服务器 SecureChatIOCP::m_PrivD
和 SecureChatIOCP::m_PubN
中。从 SecureChatIOCP.h 中删除 #define USE_SIGNATURE
以使用不带“中间人”保护和数字签名的协议。
注意!演示中的数字签名不安全,因为下载此演示的每个人都知道用于签名数据的私钥。
使用服务器演示中的“Demo”部分和“Generate DSA key”按钮来生成您自己的私钥和公钥,用于数字签名。将生成的密钥复制到 SecureChatIOCP::m_PrivD
和 SecureChatIOCP::m_PubN
以获得您自己的安全客户端-服务器框架。
5.6.4. 不同的实现方法
正如我们在前面几节中讨论过的,有许多不同的实现方法。密钥交换的实现相当慢;进行安全的密钥交换大约需要 600 毫秒(第 5.5 节)。这是因为数字签名计算。比 RSA 递归更有效的替代数字签名算法可以用来减少密钥交换时间。
一个很好的替代方法是服务器使用相同的公钥 P 和公钥 A,并且只签名公钥 A。通过这样做,从服务器的角度来看,密钥交换过程将快得多,因为计算只需要在服务器启动时或在特定间隔(几小时)进行一次。
因此,可以轻松使用更大的密钥大小,因为我们不必每次都进行计算。但是,使用这种方法会损害协议的安全性,即使它是一个相当不错的解决方案。例如,假设公钥 A 被破解。
5.6.5. 特殊注意事项
在为 64 位处理器编译此源代码时,请确保在 MyCryptLib.h 中更改参数(例如:_HIBITMASK_
、MAXIMUMNR
)为适当的值。
对于商业发行版,保护此软件免受缓冲区溢出攻击 [1] 非常重要,尤其是在 MyCryptLib
类方面。使用 Visual C++ 6.0 中的 /GZ
开关,以及 Visual C++ .NET 编译器的 /GS
开关,以防止缓冲区溢出攻击 [1]。
6. 未来工作
- 优化用于实现非对称加密和密钥交换的多精度库。
- 客户端授权,使用 SHA256 哈希密码/用户名。
- 用户之间的文件传输功能。
7. 常见问题解答
问: 该协议确保客户端免受“中间人攻击”,方法是使用数字签名。在服务器端,客户端未以类似方式进行授权,为什么?
答: 实际上,最重要的是保护客户端免受黑客攻击。此外,通常服务器会使用用户名和密码来授权客户端。在这里,重要的是客户端不会被“中间人”攻击“欺骗”以将密码等信息发送给攻击者。
8. 修订历史
- 首次发布 - 2006-06-08。
9. 参考文献
- 编译器安全检查深入分析, 2006-05-02
- Crypto++® 库 5.2.1, 2006-05-02
- OpenSSL 项目, 2006-05-02
- CrypTool, 2006-05-02
- RSA Security, 2006-05-02
- 一个简单的 IOCP 服务器/客户端类,2006-05-02,Amin Gholiha
- “对分组密码进行密码分析(或 XSL 攻击分组密码)”,Nicolas Courtois、Josef Pieprzyk,Asiacrypt 2002,LNCS 2501,第 267-287 页,Springer
- “关于 Courtois 和 Pieprzyk 对 AES 攻击的报告”,Leah McFall,Asiacrypt 2002 会议,2002 年 12 月 3 日星期二
- 计算机程序艺术,Knuth,Donald。1968
- RSA 加密标准,RSA 实验室。PKCS #1 v2.1:2002 年 6 月。
- 计算机程序艺术,卷 2:半数值算法,Knuth,Donald。E. Addison-Wesley,Reading,Mass.,1981
- 可靠软件技术或 Cigital,2006-06-07