本文翻译自 https://www.cryptopp.com/wiki/Diffie-Hellman,本人英文水平有限,如有翻译不当之处请给出修改建议!
1. Diffie-Hellman 密钥协商算法
Diffie-Hellman 是一个密钥协商算法,它允许双方建立一个安全的通信通道。最原始的
Diffie-Hellman 是一个异步协议,也即它是一个未经认证的协议,因此它容易受到中间人攻击的方式攻击。Crypto++通过DH类暴露未认证的DH算法。原始Diffie-Hellman的扩展包含加固交换协议以防止中间人攻击的认证。认证版本的DH协议通常被称为统一Diffie-Hellman。Crypto++通过DH2类提供统一Diffie-Hellman。
当数据在运转,密钥交换和传输通常是起点。不幸的是,交换和传输通常在一个项目之后被思考,尤其是一个密码爱好者或者译解密码者是不可用的。结果导致系统很脆弱不可靠,此时由于脆弱和不可靠的交换和传输导致敌对者能够在通道建立过程中恢复会话秘钥(为什么攻击AES算法的时候在秘钥交换的早期攻击有缺陷的交换)。
下面的例子使用1024位的模运算来加速计算并且保持输出是可控的。对于非对称秘钥,对称密钥,哈希值以及MAC一个合适的安全等级需要被选定。这通常意味着传输一个128位的AES密钥使用一个最小长度为3072的素数组,或者是一个最小尺寸为256位的素数域上的椭圆曲线。
2. Crypto++ 验证
Crypto++在 validate2.cpp中展示Diffie-Hellman算法的使用,其中主要的函数是: SimpleKeyAgreementValidate 和 AuthenticatedKeyAgreementValidate。函数具有如下的签名:
bool SimpleKeyAgreementValidate(SimpleKeyAgreementDomain &d)
以及:
bool AuthenticatedKeyAgreementValidate(AuthenticatedKeyAgreementDomain &d)
SimpleKeyAgreementDomain是未认证的Diffie-Hellman并且使用DH类。AuthenticatedKeyAgreementDomain 是认证的 Diffie-Hellman (统一 Diffie-Hellman) 并且使用类 DH2.
头两个例子产生DH参数并且加载标准的参数(也就是说,使用标准的参数初始化一个Crypto++对象)。标准的参数通常由 ANSI, IEEE, IETF 和 NIST的主体提供。
3. 产生参数(MODP)
Crypto++产生一个安全的素数以供DH算法使用。产生一个安全的素数是非常消耗时间的。当产生程序运行的时候程序似乎被挂起的情况屡见不鲜。
下面的例子产生典型的参数p和g。接下来程序验证素数和产生器,计算并验证q,并且打印子组的顺序。
DH dh;
AutoSeededRandomPool rnd;
dh.AccessGroupParameters().GenerateRandomWithKeySize(rnd, 1024);
const Integer& p = dh.GetGroupParameters().GetModulus();
cout << "P: " << p << endl;
Integer q = (p-1)/2;
cout << "Q: " << q << endl;
const Integer& g = dh.GetGroupParameters().GetGenerator();
cout << "G: " << g << endl;
Integer r = dh.GetGroupParameters().GetSubgroupOrder();
cout << "Subgroup order: " << r << endl;
你也可以使用PrimeAndGenerator类来产生域参数:
AutoSeededRandomPool prng;
Integer p, q, g;
PrimeAndGenerator pg;
pg.Generate(1, prng, 512, 511);
p = pg.Prime();
q = pg.SubPrime();
g = pg.Generator();
DH dh(p, q, g);
SecByteBlock t1(dh.PrivateKeyLength()), t2(dh.PublicKeyLength());
dh.GenerateKeyPair(prng, t1, t2);
Integer k1(t1, t1.size()), k2(t2, t2.size());
cout << "Private key:\n";
cout << hex << k1 << endl;
cout << "Public key:\n";
cout << hex << k2 << endl;
4. 参数初始化(MODP)
很多时候,密钥交换发生在使用标准的参数。例如: RFC 3526。标准提供{p,g}或者{p,q,g}。p和g是习惯的素数和产生器。q是子组的顺序。
注意,在下面的例子中我们不强制使用安全的素数。我们选择的参数由IETF提供规定的安全水平。这样我们就可以通过减少模乘法来提高效率。
// http://tools.ietf.org/html/rfc5114#section-2.1
Integer p("0xB10B8F96A080E01DDE92DE5EAE5D54EC52C99FBCFB06A3C6"
"9A6A9DCA52D23B616073E28675A23D189838EF1E2EE652C0"
"13ECB4AEA906112324975C3CD49B83BFACCBDD7D90C4BD70"
"98488E9C219A73724EFFD6FAE5644738FAA31A4FF55BCCC0"
"A151AF5F0DC8B4BD45BF37DF365C1A65E68CFDA76D4DA708"
"DF1FB2BC2E4A4371");
Integer g("0xA4D1CBD5C3FD34126765A442EFB99905F8104DD258AC507F"
"D6406CFF14266D31266FEA1E5C41564B777E690F5504F213"
"160217B4B01B886A5E91547F9E2749F4D7FBD7D3B9A92EE1"
"909D0D2263F80A76A6A24C087A091F531DBF0A0169B6A28A"
"D662A4D18E73AFA32D779D5918D08BC8858F4DCEF97C2A24"
"855E6EEB22B3B2E5");
Integer q("0xF518AA8781A8DF278ABA4E7D64B7CB9D49462353");
DH dh;
dh.AccessGroupParameters().Initialize(p, q, g);
p = dh.GetGroupParameters().GetModulus();
cout << "P : " << p << endl;
g = dh.GetGroupParameters().GetGenerator();
cout << "G : " << std::dec << g << endl;
q = dh.GetGroupParameters().GetSubgroupOrder();
cout << "Subgroup order: " << q << endl;
5. 参数的产生和初始化的细节
参数的产生和初始化(MODP)是相当简单的——他们产生和初始化DH参数。不幸的是,很多的细节没有及时的出现(或者说丢失了),并且丢失的细节导致系统整体的不安全。
在产生参数(MODP)的过程中,Crypto++产生一个安全的素数。一个安全的素数的形式为 p = 2q + 1,其中p和q都是素数。安全素数p提供一个大小约为模数的子组。也就是说一个1024位的素数通常产生一个1023位大小的子组。子群的顺序意味着数据或者密文的大小能够在区间[2,1023]位之间。由于没有免费的午餐,这种类型的Diffie-Hellman群将会在求幂的过程中导致1023次的平方和乘法运算。
作为对比,参数初始化不使用安全的素数,因此数据或者密文的大小将会被削减。这种情况下的素数的形式为: p = qr + 1,其中p,q是素数(这就是众所周知的Schnorr 群)。从上面的例子中 , IETF 提供的参数的子群的大小为 160 位。这就意味着数据或者密文的大小能够在区间[2,160]位之间。子群的顺序被减少也有一个好的一面:此时在求幂的过程中只会有160次的平方和乘法运算,这样会大大提高算法的效率。
虽然效率是设计算法的一个重要的考虑方面,但是密码学首先要保证安全。这里有两个广为人知的Diffie-Hellman被攻击的例子:第一个攻击的是典型的离散对数(有限域上的对数)并且经常做索引计算,修改Pollard的rho或者一小步一大步。该方法大约运行在亚指数的时间(而不是多项式的时间)。第二种攻击,由于 Pohlig 和 Hellman 通过利用循环子群的结构尝试危害系统。也就是说攻击是针对参数q的。 Pohlig-Hellman 算法的时间复杂度为 O(q√) ,其中q是子群的次序。
这有一个浅显的例子:假设你要交换一个128位的AES密钥,它是用来进行批量加密的。基于NIST安全等级,你至少需要一个3072素数模运算并且需要一个至少为256位的素数子群的次序。3072位的模数用于抵抗亚指数复杂度的离散对数的攻击,而256位的子群提供必要的安全性来抵制 Pohlig-Hellman算法。
如果你使用安全的素数,你浪费了由于大约3072次的平方和乘法的周期。你不能够使用RFC5114 MODP 群,由于RFC不能够提供带有256位的素数次序子群的3072位MODP群。你有三种可选方案:(1)转到椭圆曲线并且使用RFC5114的256位的随机ECP群;(2)接受时间周期的浪费;(3)找到一个合适的替代算法。
6. 参数验证
无论在什么时候,只要参数从外部源中加载,参数都必须被验证。参数可能是其他方不良方法产生的(例如,一个随机的素数而不是一个安全的素数),或者参数是能够被一个敌对者(例如中间人)操作。如果验证失败了,立即终止进一步的处理。
下面感兴趣的调用是ValidateGroup,它带有一个RandomNumberGenerator参数和一个int参数来指定级别。验证能够在Crypto++产生参数后或者是从对等端接收参数之后。
// RFC 5114, 1024-bit MODP Group with 160-bit Prime Order Subgroup
Integer p = ...
Integer g = ...
Integer q = ...
DH dh;
dh.AccessGroupParameters().Initialize(p, q, g);
if(!dh.GetGroupParameters().ValidateGroup(rnd, 3))
throw runtime_error("Failed to validate prime and generator");
p = dh.GetGroupParameters().GetModulus();
cout << "P: " << p << endl;
q = dh.GetGroupParameters().GetSubgroupOrder();
cout << "Subgroup order: " << q << endl;
g = dh.GetGroupParameters().GetGenerator();
cout << "G: " << g << endl;
Integer v = ModularExponentiation(g, q, p);
if(v != Integer::One())
throw runtime_error("Failed to verify order of the subgroup");
当执行一个额外的测试: gq≡1modp ,它是在调用 ModularExponentiation(g, q, p)。该测试验证g有一个次序q由于它是不可见的,Crypto++中在 ValidateGroup中执行 该测试。
当追踪整数上的测试,我们进入gfpcrypt.cpp类中的 DL_GroupParameters_IntegerBased
bool DL_GroupParameters_IntegerBased::ValidateGroup(RandomNumberGenerator &rng, unsigned int level) const
{
const Integer &p = GetModulus(), &q = GetSubgroupOrder();
bool pass = true;
pass = pass && p > Integer::One() && p.IsOdd();
pass = pass && q > Integer::One() && q.IsOdd();
if (level >= 1)
pass = pass && GetCofactor() > Integer::One() && GetGroupOrder() % q == Integer::Zero();
if (level >= 2)
pass = pass && VerifyPrime(rng, q, level-2) && VerifyPrime(rng, p, level-2);
return pass;
}
最后, nbtheory.cpp中的VerifyPrime如下所示。默认情况下,Crypto++调用 Rabin-Miller 10次。如果迭代计数似乎太低,可以直接调用RabinMillerTest
bool VerifyPrime(RandomNumberGenerator &rng, const Integer &p, unsigned int level)
{
bool pass = IsPrime(p) && RabinMillerTest(rng, p, 1);
if (level >= 1)
pass = pass && RabinMillerTest(rng, p, 10);
return pass;
}
7. 产生密钥(MODP)
适当的初始化和验证DH对象之后,产生一个用于交换的秘钥对的代码是微不足道的。它是这么的微不足道以致于没有相关的下载。
DH dh;
dh.AccessGroupParameters().Initialize(p, q, g);
...
SecByteBlock privKey(dh.PrivateKeyLength());
SecByteBlock pubKey(dh.PublicKeyLength());
dh.GenerateKeyPair(rnd, privKey, pubKey);
通常把私钥从* SecByteBlock* 中移出来不是一个好的想法,但是下面的代码将会打印密钥用于调试的目的:
Integer a, b;
a.Decode(sharedA.BytePtr(), sharedA.SizeInBytes());
cout << "Shared secret (A): " << std::hex << a << endl;
b.Decode(sharedB.BytePtr(), sharedB.SizeInBytes());
cout << "Shared secret (B): " << std::hex << b << endl;
8. 密钥协商和传输
在以上的例子之后,我们最终准备好执行密钥交换和达到共享密钥的目的。尽管Diffie-Hellman 在绝大多数的场景下被简单地描述为 gab ,该协议(以及Crypto++)使用两部分、非对称的的密钥。私钥a和b是指数,而 ga 和 gb 是公钥。
这里有两个例子可以应用密钥协商协议。第一个使用传统的未认证的Diffie-Hellman(通常被称为DH1),第二个描述的是经过认证的Diffie-Hellman(被称为DH2)。经过认证的Diffie-Hellman也被称为 统一Diffie-Hellman 。传统的Diffie-Hellman交换的双方在交换密钥的过程中都使用一个秘钥对;而统一Diffie-Hellman要求交换的双方都具有两个密钥对。在统一的模型中,一个密钥对被用于传统的Diffie-Hellman,并且第二对密钥用于确保可靠性(也就是签名)。
共享的密钥通常被称为密钥加密密钥 或者 KEK (key encryption key)。这是因为共享的密钥通常不用于批量加密。KEK通常被用于传输实际的用于批量加密的对称密钥(笔者解释:KEK其实就是一个用于加密对称加密密钥的公钥密钥)。在讨论Diffie-Hellman算法的时候,被KEK传输的对称的密钥通常被称为内容加密秘钥 或者 CEK(content encryption key)。
8.1 未认证的密钥协商
下面的密钥交换生成一个双方可共享的密文,A和B,使用的是DH类。如果有第三方进行攻击,也就是说有三个以上的成员参加了。
任意一方呼叫同意,这时就将自己的私钥和另一方的公钥进行结合。对于那些参考了Crypto++库validate2.cpp 的测试程序代码 SimpleKeyAgreementValidate。
// RFC 5114, 1024-bit MODP Group with 160-bit Prime Order Subgroup
Integer p = ...
Integer g = ...
Integer q = ...
DH dhA, dhB;
AutoSeededRandomPool rndA, rndB;
dhA.AccessGroupParameters().Initialize(p, q, g);
dhB.AccessGroupParameters().Initialize(p, q, g);
if(!dhA.GetGroupParameters().ValidateGroup(rndA, 3) ||
!dhB.GetGroupParameters().ValidateGroup(rndB, 3))
throw runtime_error("Failed to validate prime and generator");
...
//////////////////////////////////////////////////////////////
SecByteBlock privA(dhA.PrivateKeyLength());
SecByteBlock pubA(dhA.PublicKeyLength());
dhA.GenerateKeyPair(rndA, privA, pubA);
SecByteBlock privB(dhB.PrivateKeyLength());
SecByteBlock pubB(dhB.PublicKeyLength());
dhB.GenerateKeyPair(rndB, privB, pubB);
//////////////////////////////////////////////////////////////
SecByteBlock sharedA(dhA.AgreedValueLength());
SecByteBlock sharedB(dhB.AgreedValueLength());
if(dhA.AgreedValueLength() != dhB.AgreedValueLength())
throw runtime_error("Shared secret size mismatch");
if(!dhA.Agree(sharedA, privA, pubB))
throw runtime_error("Failed to reach shared secret (1)");
if(!dhB.Agree(sharedB, privB, pubA))
throw runtime_error("Failed to reach shared secret (2)");
count = std::min(dhA.AgreedValueLength(), dhB.AgreedValueLength());
if(!count || !VerifyBufsEqual(sharedA.BytePtr(), sharedB.BytePtr(), count))
throw runtime_error("Failed to reach shared secret (3)");
在生产环境中,sharedA 和 sharedB不能够在测试中被执行,因为它们的值是在不同的主机上协商过程中产生的问题将不会被检测到,指导数据开始。
8.2 经认证的密钥协商
统一Diffie-Hellman,生成双方共享的秘密,A和B,使用的是DH2类。尝试交换被双方检测到的经过认证确保交换参数正确的密钥。
统一Diffie-Hellman的细节能够在IEEE的P1363、ANSI X9.63和 NIST的 SP 800-56上找到。在统一的模型中,交换的每一方拥有两个密钥对:其中一个密钥对用于传统得DH密钥交换;第二对密钥用于签名以确保认证。在统一DH算法中传统的密钥对被称为 ephemeral(短暂的) 因为它是一个暂时的密钥对,仅仅用于当前的交换。签名密钥对是静态密钥对。签名密钥的共有部分可以被发布在一个公共的目录下以方便访问,由于签名密钥对很少被改变。
// RFC 5114, 1024-bit MODP Group with 160-bit Prime Order Subgroup
Integer p = ...
Integer g = ...
Integer q = ...
DH dh;
AutoSeededRandomPool rnd;
dh.AccessGroupParameters().Initialize(p, q, g);
...
//////////////////////////////////////////////////////////////
DH2 dhA(dh), dhB(dh);
SecByteBlock sprivA(dhA.StaticPrivateKeyLength()), spubA(dhA.StaticPublicKeyLength());
SecByteBlock eprivA(dhA.EphemeralPrivateKeyLength()), epubA(dhA.EphemeralPublicKeyLength());
SecByteBlock sprivB(dhB.StaticPrivateKeyLength()), spubB(dhB.StaticPublicKeyLength());
SecByteBlock eprivB(dhB.EphemeralPrivateKeyLength()), epubB(dhB.EphemeralPublicKeyLength());
dhA.GenerateStaticKeyPair(rnd, sprivA, spubA);
dhA.GenerateEphemeralKeyPair(rnd, eprivA, epubA);
dhB.GenerateStaticKeyPair(rnd, sprivB, spubB);
dhB.GenerateEphemeralKeyPair(rnd, eprivB, epubB);
//////////////////////////////////////////////////////////////
if(dhA.AgreedValueLength() != dhB.AgreedValueLength())
throw runtime_error("Shared secret size mismatch");
SecByteBlock sharedA(dhA.AgreedValueLength()), sharedB(dhB.AgreedValueLength());
if(!dhA.Agree(sharedA, sprivA, eprivA, spubB, epubB))
throw runtime_error("Failed to reach shared secret (A)");
if(!dhB.Agree(sharedB, sprivB, eprivB, spubA, epubA))
throw runtime_error("Failed to reach shared secret (B)");
count = std::min(dhA.AgreedValueLength(), dhB.AgreedValueLength());
if(!count || !VerifyBufsEqualp(sharedA.BytePtr(), sharedB.BytePtr(), count))
throw runtime_error("Failed to reach shared secret");
8.3 密钥传输
有了密钥加密密钥(KEK),我们现在可以传输一个内容加密密钥(CEK)。CEK是通过接下来的安全的会话来进行批量加密的。
// Take the leftmost 'n' bits for the KEK
SecByteBlock kek(sharedA.BytePtr(), AES::DEFAULT_KEYLENGTH);
// CMAC key follows the 'n' bits used for KEK
SecByteBlock mack(&sharedA.BytePtr()[AES::DEFAULT_KEYLENGTH], AES::BLOCKSIZE);
CMAC<AES> cmac(mack.BytePtr(), mack.SizeInBytes());
// Generate a random CEK
SecByteBlock cek(AES::DEFAULT_KEYLENGTH);
rnd.GenerateBlock(cek.BytePtr(), cek.SizeInBytes());
// AES in ECB mode is fine - we're encrypting 1 block, so we don't need padding
ECB_Mode<AES>::Encryption aes;
aes.SetKey(kek.BytePtr(), kek.SizeInBytes());
// Will hold the encrypted key and cmac
SecByteBlock xport(AES::BLOCKSIZE /*ENC(CEK)*/ + AES::BLOCKSIZE /*CMAC*/);
byte* const ptr = xport.BytePtr();
// Write the encrypted key in the first 16 bytes, and the CMAC in the second 16 bytes
// The logical layout of xport:
// [ Enc(CEK) ][ CMAC(Enc(CEK)) ]
aes.ProcessData(&ptr[0], cek.BytePtr(), AES::BLOCKSIZE);
cmac.CalculateTruncatedDigest(&ptr[AES::BLOCKSIZE], AES::BLOCKSIZE, &ptr[0], AES::BLOCKSIZE);
8.4 产生密钥
接下来的例子阐述两个主机,Alice和Bob,使用DH密钥交换算法产生密钥。Alice产生一个素数和一个基数来共享给Bob(这个素数和基数可以简单的硬编码在应用程序的代码中)。Alice然后生成一对公私整数,共有的整数用于和Bob共享;Bob利用从Alice传输过来的素数和基数来生成一对公私整数,共有的整数和Alice共享。
//////////////////////////////////////////////////////////////////////////
// Alice
// Initialize the Diffie-Hellman class with a random prime and base
AutoSeededRandomPool rngA;
DH dhA;
dh.Initialize(rngA, 128);
// Extract the prime and base. These values could also have been hard coded
// in the application
Integer iPrime = dhA.GetGroupParameters().GetModulus();
Integer iGenerator = dhA.GetGroupParameters().GetSubgroupGenerator();
SecByteBlock privA(dhA.PrivateKeyLength());
SecByteBlock pubA(dhA.PublicKeyLength());
SecByteBlock secretKeyA(dhA.AgreedValueLength());
// Generate a pair of integers for Alice. The public integer is forwarded to Bob.
dhA.GenerateKeyPair(rngA, privA, pubA);
//////////////////////////////////////////////////////////////////////////
// Bob
AutoSeededRandomPool rngB;
// Initialize the Diffie-Hellman class with the prime and base that Alice generated.
DH dhB(iPrime, iGenerator);
SecByteBlock privB(dhB.PrivateKeyLength());
SecByteBlock pubB(dhB.PublicKeyLength());
SecByteBlock secretKeyB(dhB.AgreedValueLength());
// Generate a pair of integers for Bob. The public integer is forwarded to Alice.
dhB.GenerateKeyPair(rngB, privB, pubB);
//////////////////////////////////////////////////////////////////////////
// Agreement
// Alice calculates the secret key based on her private integer as well as the
// public integer she received from Bob.
if (!dhA.Agree(secretKeyA, privA, pubB))
return false;
// Bob calculates the secret key based on his private integer as well as the
// public integer he received from Alice.
if (!dhB.Agree(secretKeyB, privB, pubA))
return false;
// Just a validation check. Did Alice and Bob agree on the same secret key?
if (VerifyBufsEqualp(secretKeyA.begin(), secretKeyB.begin(), dhA.AgreedValueLength()))
return false;
return true;
8.5 使用Diffie-Hellman生成AES密钥
在以上例子的基础上,注意该例子使用的是就地加解密,其中的输入和输出是相同的:
int aesKeyLength = SHA256::DIGESTSIZE; // 32 bytes = 256 bit key
int defBlockSize = AES::BLOCKSIZE;
// Calculate a SHA-256 hash over the Diffie-Hellman session key
SecByteBlock key(SHA256::DIGESTSIZE);
SHA256().CalculateDigest(key, secretKeyA, secretKeyA.size());
// Generate a random IV
byte iv[AES::BLOCKSIZE];
arngA.GenerateBlock(iv, AES::BLOCKSIZE);
char message[] = "Hello! How are you.";
int messageLen = (int)strlen(plainText) + 1;
//////////////////////////////////////////////////////////////////////////
// Encrypt
CFB_Mode<AES>::Encryption cfbEncryption(key, aesKeyLength, iv);
cfbEncryption.ProcessData((byte*)message, (byte*)message, messageLen);
//////////////////////////////////////////////////////////////////////////
// Decrypt
CFB_Mode<AES>::Decryption cfbDecryption(key, aesKeyLength, iv);
cfbDecryption.ProcessData((byte*)message, (byte*)message, messageLen);