0%

使用OpenSSL指定加密套件使用外部PSK进行握手(TLS1.3,OpenSSL3.0)

实验室有TLS相关的工作,我自己对TLS协议方面也并没有特别深入的了解。中文的OpenSSL相关资料很少,而与此工作特别相关的英文文章也并不多,尤其是可用代码非常少,很多代码运行后都会出错。因此做这个工作的时候也是摸着石头过河,踩了很多坑,最后很多代码都是参考的OpenSSL的部分源码。这一篇是这个工作的前篇,也是探索阶段,工作的后半阶段会在后一篇进行介绍。

本人对协议细节没有很深入的理解,因此有部分缺漏也在所难免,还请各位见谅,不过本人尽量保证本文。也欢迎各位私信我的微博对部分问题进行更深入的探讨。

简介

PSK,即Pre-shared Key。一般来讲PSK有两种形式,其一是通讯双方在通讯前就协商好的密钥,指定此密钥通信双方可以不借助证书进行通信;另一种则是在通信结束时双方会协商获得PSK,以便下次更高效地进行通讯。

在TLS1.2中就已经可以使用PSK进行通讯了,但在TLS1.3中对PSK进行了更正式的支持。而TLS1.3也相对TLS1.2进行了大幅的修改。而反映到最为广泛使用的TLS库——OpenSSL上,则是更改了很多API。对新特性更是如此。

使用外部PSK进行握手的协议部分

这里使用了TLS 1.3科普——新特性与协议实现的制图与讲解。感谢该文章给我的帮助。
handshake

  • +表示该报文中值得注意的extension
  • * 表示该内容也可能不被发送
  • {} 表示该内容使用handshake_key加密
  • [] 表示该内容使用application_key加密

当我们使用PSK进行握手时,handshake_key是由我们使用的PSK与前两次报文,使用HKDF(内建的某函数,其中会使用到加密套件指定的哈希算法,加密套件会在下一篇文章中提到)导出而来的。
而application_key则是以整个握手阶段的报文作为输入,计算四次HKDF导出而来。

当然,client不会将PSK明文发送给server,而是会发送哈希后的PSK,因此server与client需要使用相同的哈希算法。

OpenSSL与部分命令行工具

OpenSSL库是开源的最为广泛使用的TLS标准实现,这里使用OpenSSL进行开发。我这里的环境为CentOS 7.3。在Linux上OpenSSL的部署相对简单,不进行赘述。我也尝试过Windows上的OpenSSL部署,不过部分代码经过修改后还是无法运行,因此这里不作讲解。此外需要一提的是我这里使用的OpenSSL版本为OpenSSL 3.0.0-alpha7-dev,因此部分执行结果可能会与低版本的OpenSSL不同。

OpenSSL自带的命令行工具附带了非常多的实用功能,比如我们可以使用命令行工具搭建server或client,便于我们进行测试。这里我们可以直接使用其命令行进行PSK功能的相关测试。

1
2
3
4
5
# s_server命令用于建立服务器 -psk 指定使用的psk,注意密钥位数需要满足条件否则会报错
# -port 指定监听的端口 -ciphersuites tls1.3中指定加密套件,tls1.2的api不同
# -nocert 不使用证书 -psk_identity 指定psk对应的psk_identity 服务器利用此字段寻找对应的PSK

openssl s_server -psk 123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678 -port 4433 -ciphersuites TLS_AES_128_GCM_SHA256 -nocert -psk_identity Client1
1
2
3
# client 端的命令 参数与上述说明类似

openssl s_client -psk 123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678 -port 4433 -ciphersuites TLS_AES_128_GCM_SHA256 -psk_identity Client1

在实际写握手代码时我也主要是参考了s_client与s_server的实现。其中有部分小坑,接下来会说明。

关键API说明

server端官方文档client端官方文档中提及了PSK的使用方法,原文如下:

A server application wishing to use TLSv1.3 PSKs should set a callback using either SSL_CTX_set_psk_find_session_callback() or SSL_set_psk_find_session_callback() as appropriate.

A client application wishing to use TLSv1.3 PSKs should use either SSL_CTX_set_psk_use_session_callback() or SSL_set_psk_use_session_callback() as appropriate. These functions cannot be used for TLSv1.2 and below PSKs.

以上是server端和client端的说明。也就是说server端需要完成并设置关于PSK的回调函数SSL_CTX_set_psk_find_session_callback()SSL_set_psk_find_session_callback()。同样的,在client端也需要完成对应的回调函数SSL_CTX_set_psk_use_session_callbackSSL_set_psk_use_session_callback。这里列出二者详细的定义。

1
2
3
4
5
6
7
8
9
10
11
// client端回调函数的格式
typedef int (*SSL_psk_use_session_cb_func)(SSL *ssl, const EVP_MD *md, const unsigned char **id, size_t *idlen, SSL_SESSION **sess);
// client 端需要完成的回调函数
void SSL_CTX_set_psk_use_session_callback(SSL_CTX *ctx, SSL_psk_use_session_cb_func cb);
void SSL_set_psk_use_session_callback(SSL *s, SSL_psk_use_session_cb_func cb);

//server端回调函数的格式
typedef int (*SSL_psk_find_session_cb_func)(SSL *ssl, const unsigned char *identity, size_t identity_len, SSL_SESSION **sess);
//server端需要完成的回调函数
void SSL_CTX_set_psk_find_session_callback(SSL_CTX *ctx, SSL_psk_find_session_cb_func cb);
void SSL_set_psk_find_session_callback(SSL *s, SSL_psk_find_session_cb_func cb);

这里需要说明的是,TLS1.2到TLS1.3在协议上有非常大的改动,相应的,OpenSSL的api也发生了相当大的变化,旧版中有不同的调用方式,在TLS1.3中仍然可以使用,不过这里不再提旧api的用法。

在回调函数内,client端与server端还需要进行繁琐的设置:client端需要设置key、加密套件与协议版本,此外还可以设置early data。而server端则对应地要对client端设置的参数进行验证。
验证的核心过程非常容易理解,也正是前文中提到的:client端将算出PSK哈希后将该值与字段psk identity发送给server端,server端会存储着若干psk identity - psk对(OpenSSL没有实现此部分,此部分由用户自己编码完成),根据传来的psk identity就可以找出存储的psk,计算哈希后进行比对即可确认身份。

这里需要着重说明的是设置的参数——加密套件,这也是我踩的最大的坑。实际上文档中说得很清楚:

A ciphersuite

Only the handshake digest associated with the ciphersuite is relevant for the PSK (the server may go on to negotiate any ciphersuite which is compatible with the digest). The application can use any TLSv1.3 ciphersuite. If md is not NULL the handshake digest for the ciphersuite should be the same. The ciphersuite can be set via a call to <SSL_SESSION_set_cipher(3)>. The handshake digest of an SSL_CIPHER object can be checked using <SSL_CIPHER_get_handshake_digest(3)>.

在这里设置的加密套件只有与加密套件相关的哈希算法会被使用(举例来说就是TLS_AES_128_GCM_SHA256加密套件会使用其中的sha256哈希算法),而server端与client端的哈希算法需要设置得一致才能正确地进行PSK的验证。TLS标准中说明该值默认使用SHA256进行计算。而还需要注意的一点是:在这里设置了加密套件后并不代表着设置了通讯使用的加密套件设置通讯使用的加密套件还需要调用额外的apiSSL_CTX_set_ciphersuites
而根据我的测试,在OpenSSL的实现中,如果server端与client端设置一致,但是通讯使用的加密套件与PSK设置中使用的不一致,PSK认证依然能通过,但是server端会调用自己的证书进行一部分操作(因此耗时也会对应增加),如果server端没有设置证书则会报错no suitable signature algorithm。设置一致时server端不需要证书也可以顺利进行通讯。

不过仅仅靠文档说明还是很难完成这部分代码的编写,不过在copy s_clients_server中大段的代码后我的程序还是顺利跑起来了。但是在s_clients_server中,psk默认设置了加密套件为aes128sha256,因此当server端不进行证书设置时,将二端的加密套件均设置为aes256sha384会导致报错。如下:

1
openssl s_server -psk 123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678 -port 4433 -ciphersuites TLS_AES_256_GCM_SHA384 -nocert -psk_identity Client1 -psk_identity Client1
1
openssl s_client -psk 123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678 -port 4433 -ciphersuites TLS_AES_256_GCM_SHA384 -psk_identity Client1

在编码时通过设置一致的加密套件可以避免这一点。

代码

这里给出拼拼凑凑的二端代码,这部分工作是进行下一步开发的前置工作,下一步会利用OpenSSL已实现的密码算法添加新的加密套件。这部分代码需要注意的是我们使用了BIO进行通信,如果不使用这个接口可能会导致程序报错或乱码。

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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
//server端
//g++ server-simple.cpp -o server-simple -I /usr/local/include -L /usr/local/lib64 -lssl -lcrypto
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define PSK_KEY "123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678"
#define PSK_ID "Client1"

#define AES256SHA384 1
#define AES128SHA256 2
#define CURR_CIPHERSUITES AES256SHA384
// #define CURR_CIPHERSUITES AES128SHA256


void report_error(const char* e_log) {
perror(e_log);
exit(EXIT_FAILURE);
}

int create_socket(int port){
int s;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));

addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = htonl(INADDR_ANY);

s = socket(PF_INET, SOCK_STREAM, 0);
if (s < 0) {
perror("Unable to create socket");
exit(EXIT_FAILURE);
}

if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("Unable to bind");
exit(EXIT_FAILURE);
}

if (listen(s, 1) < 0) {
perror("Unable to listen");
exit(EXIT_FAILURE);
}

return s;
}

static int psk_find_session_cb(SSL *ssl, const unsigned char *identity,
size_t identity_len, SSL_SESSION **sess)
{
SSL_SESSION *tmpsess = NULL;
unsigned char *key;
long key_len;

if (strlen(PSK_ID) != identity_len
|| memcmp(PSK_ID, identity, identity_len) != 0) {
printf("PSK ID len %d", strlen(PSK_ID));
*sess = NULL;
return 1;
}

key = OPENSSL_hexstr2buf(PSK_KEY, &key_len);
if (key == NULL) {
report_error("Could not convert PSK key to buffer\n");
return 0;
}

const SSL_CIPHER *cipher = NULL;
const unsigned char tls13_aes128gcmsha256_id[] = { 0x13, 0x01 };
const unsigned char tls13_aes256gcmsha384_id[] = { 0x13, 0x02 };
const unsigned char tls13_aes256gcmsha512_id[] = { 0x13, 0x06 };

// use default sha256 on psk transfer or use corresponding ciphersuites
// cipher here determines which hash alg to use on psk, but not ciphersuite for communication
// if cipher is not consistent with ciphersuite, then server needs to set certificate <- I don't know why but it is true

if (CURR_CIPHERSUITES == AES256SHA384) {
cipher = SSL_CIPHER_find(ssl, tls13_aes256gcmsha384_id);
}
else if (CURR_CIPHERSUITES == AES128SHA256) {
cipher = SSL_CIPHER_find(ssl, tls13_aes128gcmsha256_id);
}
else {
report_error("Error ciphersuites define setting\n");
}


if (cipher == NULL) {
report_error("Error finding suitable ciphersuite\n");
OPENSSL_free(key);
return 0;
}

tmpsess = SSL_SESSION_new();
if (tmpsess == NULL
|| !SSL_SESSION_set1_master_key(tmpsess, key, key_len)
|| !SSL_SESSION_set_cipher(tmpsess, cipher)
|| !SSL_SESSION_set_protocol_version(tmpsess, SSL_version(ssl))) {
OPENSSL_free(key);
return 0;
}
OPENSSL_free(key);
*sess = tmpsess;

return 1;
}


SSL_CTX *create_context()
{
const SSL_METHOD *method;
SSL_CTX *ctx;

method = TLS_server_method();
ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}

return ctx;
}

void configure_context(SSL_CTX *ctx)
{

// determine ciphersuite to use
if (CURR_CIPHERSUITES == AES256SHA384) {
SSL_CTX_set_ciphersuites(ctx, "TLS_AES_256_GCM_SHA384");
}
else if (CURR_CIPHERSUITES == AES128SHA256) {
SSL_CTX_set_ciphersuites(ctx, "TLS_AES_128_GCM_SHA256");
}
else {
report_error("Error ciphersuites define setting\n");
}
}

int main(int argc, char **argv)
{
int sock;
SSL_CTX *ctx;

ctx = create_context();
configure_context(ctx);
sock = create_socket(4433);
printf("Server starting up...\n");
int count = 0;

/* Handle connections */
while (1) {

count += 1;
const char reply[] = "test\n";

SSL_CTX_set_psk_find_session_callback(ctx, psk_find_session_cb);

struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int client = accept(sock, (struct sockaddr*)&addr, &len);
if (client < 0) {
perror("Unable to accept");
exit(EXIT_FAILURE);
}

SSL *ssl;
ssl = SSL_new(ctx);
SSL_set_fd(ssl, client);

BIO *accept_bio = BIO_new_socket(client, BIO_CLOSE);
SSL_set_bio(ssl, accept_bio, accept_bio);

BIO *bio = BIO_pop(accept_bio);


if (SSL_accept(ssl) <= 0) {
ERR_print_errors_fp(stderr);
}
else {
SSL_write(ssl, reply, strlen(reply));
printf("receive a connection: %d\n", count);
}

SSL_shutdown(ssl);
printf("connection closed.\n");

BIO_free_all(bio);
BIO_free_all(accept_bio);
}
}
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
//client 端
//g++ client-simple.cpp -o client-simple -I /usr/local/include -L /usr/local/lib64 -lssl -lcrypto
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define HOST "0.0.0.0:4433"

#define PSK_KEY "123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678"
#define PSK_ID "Client1"

#define AES256SHA384 1
#define AES128SHA256 2
#define CURR_CIPHERSUITES AES256SHA384
// #define CURR_CIPHERSUITES AES128SHA256


void report_error(const char* e_log){
perror(e_log);
exit(EXIT_FAILURE);
}

int create_socket(int port)
{
int s;
s = socket(PF_INET, SOCK_STREAM, 0);
if (s < 0) {
perror("Unable to create socket");
exit(EXIT_FAILURE);
}

return s;
}

SSL_CTX *create_context()
{
const SSL_METHOD *method;
SSL_CTX *ctx;

method = TLS_client_method();

ctx = SSL_CTX_new(method);
if (!ctx) {
perror("Unable to create SSL context");
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}

return ctx;
}

static int psk_use_session_cb (SSL *ssl, const EVP_MD *md,
const unsigned char **id,
size_t *idlen,
SSL_SESSION **sess)
{
SSL_SESSION *usesess = SSL_SESSION_new();
long key_len;
unsigned char *key = OPENSSL_hexstr2buf(PSK_KEY, &key_len);

if (key == NULL)
{
report_error("Could not convert PSK key to buffer\n");
return 0;
}

const SSL_CIPHER *cipher = NULL;
const unsigned char tls13_aes128gcmsha256_id[] = { 0x13, 0x01 };
const unsigned char tls13_aes256gcmsha384_id[] = { 0x13, 0x02 };
const unsigned char tls13_aes256gcmsha512_id[] = { 0x13, 0x06 };

if (CURR_CIPHERSUITES == AES256SHA384) {
cipher = SSL_CIPHER_find(ssl, tls13_aes256gcmsha384_id);
}
else if (CURR_CIPHERSUITES == AES128SHA256) {
cipher = SSL_CIPHER_find(ssl, tls13_aes128gcmsha256_id);
}
else {
report_error("Error ciphersuites define setting\n");
}

if (usesess == NULL
|| !SSL_SESSION_set1_master_key(usesess, key, key_len)
|| !SSL_SESSION_set_cipher(usesess, cipher)
|| !SSL_SESSION_set_protocol_version(usesess, TLS1_3_VERSION))
{
OPENSSL_free(key);
goto err;
}

cipher = SSL_SESSION_get0_cipher(usesess);
if (cipher == NULL)
goto err;

if (md != NULL && SSL_CIPHER_get_handshake_digest(cipher) != md) {
goto err;
} else {
*sess = usesess;
*id = (unsigned char *)PSK_ID;
*idlen = strlen(PSK_ID);
}
return 1;
err:
SSL_SESSION_free(usesess);
report_error("error happens\n");
return 0;

}

int main(int argc, char **argv)
{
SSL_CTX *ctx;
ctx = create_context();
SSL_CTX_set_psk_use_session_callback(ctx, psk_use_session_cb);

if (CURR_CIPHERSUITES == AES256SHA384) {
SSL_CTX_set_ciphersuites(ctx, "TLS_AES_256_GCM_SHA384");
}
else if (CURR_CIPHERSUITES == AES128SHA256) {
SSL_CTX_set_ciphersuites(ctx, "TLS_AES_128_GCM_SHA256");
}
else {
report_error("Error ciphersuites define setting\n");
}

BIO *bio = BIO_new_ssl_connect(ctx);
SSL *ssl;
BIO_get_ssl(bio, &ssl);
BIO_set_ssl_mode(bio, 1);
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
BIO_set_conn_hostname(bio, HOST);

if (BIO_do_connect(bio) <= 0) {
BIO_free_all(bio);
printf("errored; unable to connect.\n");
ERR_print_errors_fp(stderr);
return -2;
}

char tmpbuf[1024+1];
for (;;) {
int len = BIO_read(bio, tmpbuf, 1024);
if (len == 0) {
break;
}
else if (len < 0) {
if (!BIO_should_retry(bio)) {
printf("errored; read failed.\n");
ERR_print_errors_fp(stderr);
break;
}
}
else {
tmpbuf[len] = 0;
printf("%s", tmpbuf);
}
}


BIO_free_all(bio);

return 0;
}

参考资料

TLS 1.3科普——新特性与协议实现
HTTPS 温故知新(四) —— 直观感受 TLS 握手流程(下)