使用 openssl 进行 DH 密钥交换

在我之前的博客中聊到了 DH 密钥交换的原理 . 这里来讨论如何使用 OPENSSL 进行 DH 密钥交换。

一 原理验证

OPENSSL 提供了一系列API, 可以在方便地进行大数计算。在这里我们先对 DH 密钥交换的原理 中讲到的原理进行代码级别的验证。对原理不感兴趣的可以跳过这一小节。
由于在 DH 密钥交换过程中,大部分的操作过程都是一样的,可以定义一个基类来抽象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class DHBase {
public:
DHBase() : m_pri_key(NULL) {}

virtual ~DHBase() {
if (m_pri_key)
BN_free(m_pri_key);
};

const BIGNUM *GetSharedNum() {
if (!m_pri_key)
GenRandomData();

BN_CTX *ctx = BN_CTX_new();
const BIGNUM *g = DH_get0_g(m_dh);
const BIGNUM *p = DH_get0_p(m_dh);
BIGNUM *r = BN_new();

// r = g ^ m_pri_key % p
int rst = BN_mod_exp(r, g, m_pri_key, p, ctx);
assert(rst == 1);
BN_CTX_free(ctx);
return r;
}

BIGNUM *GenKey(const BIGNUM *shared_num) {
if (!m_pri_key)
GenRandomData();

BIGNUM *key = BN_new();
BN_CTX *ctx = BN_CTX_new();

// key = shared_num ^ m_pri_key % p
int rst = BN_mod_exp(key, shared_num, m_pri_key, DH_get0_p(m_dh), ctx);
assert(rst == 1);
BN_CTX_free(ctx);

std::cout << m_name << ": received shared num: " << GetNumString(shared_num) << std::endl;
std::cout << m_name << ": generated KEY: " << GetNumString(key) << std::endl;
return key;
}

std::string GetName() {
return m_name;
}

static std::string GetNumString(const BIGNUM *num) {
return BN_bn2hex(num);
}

private:
void GenRandomData() {
assert(m_dh);
m_pri_key = BN_new();
int rst = BN_rand_range(m_pri_key, DH_get0_p(m_dh));
assert(rst == 1);
while (BN_is_zero(m_pri_key)) {
BN_rand_range(m_pri_key, DH_get0_p(m_dh));
}
std::cout << m_name << ": generated the private num" << std::endl;
}

protected:
DH *m_dh;
std::string m_name;

private:
BIGNUM *m_pri_key;
};
  • m_pri_key 是双方生成的私密随机数。

  • GetSharedNum() 返回双方交换的中间变量 G^A mod P

  • GenKey() 即完成了密钥交换,返回交换成功后的密钥。

DH交换发起方的类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DHRequest : public DHBase {
public:
DHRequest()
: m_dh_nid(NID_ffdhe8192) {
m_dh = DH_new_by_nid(m_dh_nid);
m_name = "REQUEST";
}

int GetDHNid() {
return m_dh_nid;
}

private:
int m_dh_nid;
};

m_dh_nidOID (ObjectID 对象标识) 的延伸。OID 是由国际电信联盟(ITU)和ISO/IEC标准化的标识符机制,用于以全局明确的持久名称命名任何对象、概念或“事物”的一组数字。NID (NameID) 则是与 OID 相对应的一个字符串名称;在 DH 密钥交换中,NID 可以是 NID_ffdhe2048, NID_ffdhe3072, NID_ffdhe4096, NID_ffdhe6144NID_ffdhe8192 中的一种。每一种 NID 背后都是由密码学专家或组织精心设计的一组数据 DH(内容包括 G,P等)。当然,我们也可以设计使用自己的数据。

密钥交换接受方的类如下:

1
2
3
4
5
6
7
8
9
class DHResponse : public DHBase {
public:
DHResponse(int dh_nid) {
m_name = "RESPONSE";
m_dh = DH_new_by_nid(dh_nid);

std::cout << m_name << ": received DH param (G,P)" << std::endl;
}
};

最后来看 DH 密钥交换的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {
init_openssl();

DHRequest request;
DHResponse response(request.GetDHNid());

const BIGNUM *req_shared_num = request.GetSharedNum();
const BIGNUM *res_shared_num = response.GetSharedNum();

BIGNUM *request_key = request.GenKey(res_shared_num);
BIGNUM *response_key = response.GenKey(req_shared_num);

int cmp = BN_cmp(request_key, response_key);
assert(cmp == 0);

clean_openssl();
return 0;
}

可以看到,整个过程中,交换的数据只有 nidshared_num 。最终双方生成的 key 是相同的,完成了密钥交换。

1
2
3
4
5
6
7
RESPONSE: received DH param (G,P)
REQUEST: generated the private num
RESPONSE: generated the private num
REQUEST: received shared num: 04066791E05CEDC8C630544043CA2FE9 ... 2B541ED73
REQUEST: generated KEY: 56E45AFDF14856C858899D5E13890693 ... 19066BBD1
RESPONSE: received shared num: 4A7B96C1D952DEC5486FF95DC3FF749D ... B80492A83
RESPONSE: generated KEY: 56E45AFDF14856C858899D5E13890693 ... 19066BBD1

二. OPENSSL 进行 DH 密钥交换

最主要的结构体为 typedef struct dh_st DH; , 原型 dh_st 定义在文件 dh_locl.h 中:

1
2
3
4
5
6
7
8
struct dh_st {
// ...
BIGNUM *p;
BIGNUM *g;
BIGNUM *pub_key; /* g^x % p */
BIGNUM *priv_key; /* x */
// ...
};

DH 的生成有多种方式,如前面说的 DH_new_by_nid, 还有后面用到的 DH_generate_parameters_ex .此外还有从 BIO 等生成 DH 的方法。

1
2
3
int DH_generate_parameters_ex(DH *dh, int prime_len,int generator, BN_GENCB *cb);
int DH_generate_key(DH *dh);
int DH_compute_key(unsigned char *key, BIGNUM *pub_key, DH *dh);
  • DH_generate_parameters_ex : 用于生成一个随机的 DH 结构体

    • 参数 prime_len 是生成的质数 p 的位长度。

    • 参数 generator 是一个大于 1 的很小的数,一般是 2 或 5

  • DH_generate_key : 用于生成公钥和私钥, 不要和 RSA 的公私钥弄混,这里的公钥是指和双方交换的数 G^a % P, 这里的私钥指双方保留的私密数字

  • DH_compute_key 用于计算最后交换的结果密钥

    • 参数 key 即是结果密钥。其长度可以使用 DH_size() 计算

    • 参数 pub_key 是从对方获取的交换数。

一个密钥交换的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <openssl/dh.h>
#include <openssl/evp.h>
#include <openssl/bn.h>

class Request {
public:
Request() {
m_dh = DH_new();
int rst = DH_generate_parameters_ex(m_dh, 512, 2, NULL);
rst = DH_check(m_dh, &rst);
assert(rst == 1);
DH_generate_key(m_dh);
}

~Request() {
if (m_dh)
DH_free(m_dh);
}

void GetSharedNums(BIGNUM *&p, BIGNUM *&g) {
assert(m_dh);
p = BN_dup(DH_get0_p(m_dh));
g = BN_dup(DH_get0_g(m_dh));
}

void GetPubKey(BIGNUM *&pubkey) {
assert(m_dh);
pubkey = BN_dup(DH_get0_pub_key(m_dh));
}

std::string GetPriKey(const BIGNUM *pubkey) {
int sz = DH_size(m_dh);
unsigned char *key = (unsigned char *) malloc(sz);
DH_compute_key(key, pubkey, m_dh);

std::string ret(reinterpret_cast<char *>(key));
free(key);

return ret;
}

private:
DH *m_dh;
};

class Response {
public:
Response() : m_dh(NULL) {}

~Response() {
if(m_dh)
DH_free(m_dh);
}

bool Init(BIGNUM *p, BIGNUM *g) {
if (!m_dh) {
m_dh = DH_new();
}
int rst = DH_set0_pqg(m_dh, p, 0, g);
assert(rst == 1);
rst = DH_generate_key(m_dh);
return rst == 1;

}

void GetPubKey(BIGNUM *&pubkey) {
assert(m_dh);
assert(DH_get0_priv_key(m_dh));
pubkey = BN_dup(DH_get0_pub_key(m_dh));
}

std::string GetPriKey(const BIGNUM *pubkey) {
assert(m_dh);
unsigned char *key = (unsigned char *) malloc(DH_size(m_dh));
int rst = DH_compute_key(key, pubkey, m_dh);
if(rst < 0){
printf("%s", ERR_reason_error_string(ERR_get_error()));
}
std::string ret(reinterpret_cast<char *>(key));
free(key);

return ret;
}

private:
DH *m_dh;
};

int main(void) {
//init_openssl();

DH *m_dh = DH_new();
int rst = DH_generate_parameters_ex(m_dh, 512, 2, NULL);
rst = DH_check(m_dh, &rst);
assert(rst == 1);

const BIGNUM *ppp = DH_get0_p(m_dh);
BIGNUM *x = BN_dup(ppp);

Request request;
BIGNUM *p = NULL, *g = NULL;
request.GetSharedNums(p, g);

Response response;
bool init = response.Init(p, g);
assert(init);

BIGNUM *pubkey_of_request = NULL, *pubkey_of_response = NULL;
request.GetPubKey(pubkey_of_request);
response.GetPubKey(pubkey_of_response);

std::string key1 = request.GetPriKey(pubkey_of_response);
std::string key2 = response.GetPriKey(pubkey_of_request);

int cmp = key1.compare(key2);
assert(cmp == 0);

//clean_openssl();
return 0;
}