0%

PSK相关

1. 为什么

TLS通常用证书体系(PKI)进行认证,先回顾一下传统的RSA证书认证过程,

  1. server发送证书给client, 证书中包含RSA的公钥,只有server有证书中公钥对应的私钥;
  2. client收到证书后,对证书进行校验(PKI体系一层层往CA验证,直到根CA),校验通过后,用证书中的公钥对预主密钥(client每次随机生成)进行加密,将加密后的数据发送给server;
  3. 只有server能解密数据,取出其中的预主密钥。这样,server和client之间就有了共同的秘钥,可以用于后续的加密了。 这里的关键是只有server有对应的RSA私钥,然后server的RSA公钥可以由信任的根CA一层层校验,这样,client就可以认为它正在通信的对方是自己想要通信的server。server对client进行的过程认证基本一样。

但使用证书有几个问题:

  1. 证书需要非对称算法,不管是RSA还是ECC,加密相同强度都比对称算法慢;
  2. 证书的PKI体系部署管理比较麻烦,需要根CA、子CA、server证书、证书私钥,client证书等;
  3. 认证过程需要发送证书,证书通常比较大,需要传输时间比较长。

所以,PSK就解决这样的问题:

  1. PSK可以只需要对称算法,不使用非对称算法;
  2. PSK在可控的封闭环境中容易部署;如果应用使用其他办法能生成一个共享的秘钥,PSK就可以用该秘钥建立TLS连接了;
  3. 认证时PSK可以传输较小的数据量。

2. 是什么

PSK——pre-shared key,预共享密钥——就是通信双方使用提前部署好的预共享密钥(用于对称算法)建立TLS连接的机制。

PSK的主要逻辑是:

  1. 分别在client和server部署相同的对称加密秘钥(psk_key);
  2. 每个psk_key对应有一个psk_id, client发送该psk_id给server,server通过psk_id查找对应的psk_key,如果查到,就用该psk_key去直接加密后续的数据,不再进行非对称加密。

3. TLS1.2及之前的PSK

PSK机制需要使用加密套件来确定是否使用PSK机制。
最开始的3组加密套件为:

ciphersuite key exchange cipher hash
TLS_PSK_WITH_RC4_128_SHA PSK RC4_128 SHA
TLS_PSK_WITH_3DES_EDE_CBC_SHA PSK 3DES_EDE_CBC SHA
TLS_PSK_WITH_AES_128_CBC_SHA PSK AES_128_CBC SHA
TLS_PSK_WITH_AES_256_CBC_SHA PSK AES_256_CBC SHA
TLS_DHE_PSK_WITH_RC4_128_SHA DHE_PSK RC4_128 SHA
TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA DHE_PSK 3DES_EDE_CBC SHA
TLS_DHE_PSK_WITH_AES_128_CBC_SHA DHE_PSK AES_128_CBC SHA
TLS_DHE_PSK_WITH_AES_256_CBC_SHA DHE_PSK AES_256_CBC SHA
TLS_RSA_PSK_WITH_RC4_128_SHA RSA_PSK RC4_128 SHA
TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA RSA_PSK 3DES_EDE_CBC SHA
TLS_RSA_PSK_WITH_AES_128_CBC_SHA RSA_PSK AES_128_CBC SHA
TLS_RSA_PSK_WITH_AES_256_CBC_SHA RSA_PSK AES_256_CBC SHA

是按照秘钥交换算法进行的分组,分别为PSK, DHE_PSK, RSA_PSK

  • PSK是只是用对称加密算法,适合要求性能的地方;
  • DHE_PSK同时使用PSK和DHE算法,可以抵御字典攻击,也提供前向安全性;
  • RSA_PSK同时使用PSK和RSA算(还是使用RSA证书),可以提供额外的认证。

不同的秘钥交换算法使用的握手消息不太一样,下面分别详细说明。

3.1. PSK

使用单独的PSK(plain PSK)进行协商的主要步骤如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Client                              Server
------ ------
ClientHello -------->
ServerHello
(Certificate)
ServerKeyExchange*
(CertificateRequest)
<-------- ServerHelloDone
(Certificate)
ClientKeyExchange
(CertificateVerify)
ChangeCipherSpec
Finished -------->
ChangeCipherSpec
<-------- Finished
Application Data <-------> Application Data
  1. client如果希望使用PSK进行握手,就在CH中加入PSK加密套件发送给server;
  2. server如果也希望使用PSK,就在SH中回复相应的PSK加密套件;
  3. server不再发送CertificateCertificateRequest,因为这时候不需要证书了;
  4. client和server都可能有多组psk_key,server可以在SKE中发送一个psk identity hint的东西(类似索引之类的)帮助client去选择相应的psk_key;如果没有psk identity hint,就不发送SKE了;
  5. client把希望使用的psk_key对应的psk_id放到CKE中,发送给server;
  6. server收到CKE后,根据其中的psk_id选择相应的psk_key,然后将psk_key当做预主密钥(pre master secret)进行后续的秘钥推导;

相关的消息格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct {
select (KeyExchangeAlgorithm) {
/* other cases for rsa, diffie_hellman, etc. */

case psk: /* NEW */
opaque psk_identity_hint<0..2^16-1>;
};
} ServerKeyExchange;

struct {
select (KeyExchangeAlgorithm) {
/* other cases for rsa, diffie_hellman, etc. */

case psk: /* NEW */
opaque psk_identity<0..2^16-1>;
} exchange_keys;
} ClientKeyExchange;

3.2. DHE_PSK

协商的步骤跟plain PSK大体一致,其中:
\4. server不管有没有psk identity hint,都需要发送SKE,其中一定要包含server的DH秘钥交换参数;
\5. client把希望使用的psk_key对应的psk_id放到CKE中,并将client的DH秘钥交换参数一并放入发送给server;
\6. server收到CKE后,根据其中的psk_id选择相应的psk_key,并使用收到的DH参数生成DH的共享秘钥,然后跟psk_key一起构建出预主密钥(pre master secret)进行后续的秘钥推导;

并且消息格式有差别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct {
select (KeyExchangeAlgorithm) {
/* other cases for rsa, diffie_hellman, etc. */

case diffie_hellman_psk: /* NEW */
opaque psk_identity_hint<0..2^16-1>;
ServerDHParams params;
};
} ServerKeyExchange;

struct {
select (KeyExchangeAlgorithm) {
/* other cases for rsa, diffie_hellman, etc. */

case diffie_hellman_psk: /* NEW */
opaque psk_identity<0..2^16-1>;
ClientDiffieHellmanPublic public;
} exchange_keys;
} ClientKeyExchange;

其中:

  • ServerDHParams:保存的是server临时生成的DH公开参数(其中dh_Ys相当于server的公钥)。格式为:
    1
    2
    3
    4
    5
    struct {
    opaque dh_p<1..2^16-1>;
    opaque dh_g<1..2^16-1>;
    opaque dh_Ys<1..2^16-1>;
    } ServerDHParams; /* Ephemeral DH parameters */
  • ClientDiffieHellmanPublic: client生成的DH公开参数(其中dh_Yc相当于client的公钥),格式为:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct {
    select (PublicValueEncoding) {
    case implicit:
    struct { };

    case explicit:
    opaque dh_Yc<1..2^16-1>;
    } dh_public;
    } ClientDiffieHellmanPublic;

3.3. RSA_PSK

协商的步骤跟plain PSK大体一致,其中:
\3. server需要发送包含RSA证书的Certificate
\5. client把希望使用的psk_key对应的psk_id放到CKE中,并生成一串随机数secret,用serve证书中的公钥加密,一并发送给server;
\6. server收到CKE后,根据其中的psk_id选择相应的psk_key,并使用RSA私钥对其中的加密数据解密,得到一个跟client共享的秘钥,然后跟psk_key一起构建出预主密钥(pre master secret)进行后续的秘钥推导;

消息格式为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct {
select (KeyExchangeAlgorithm) {
/* other cases for rsa, diffie_hellman, etc. */

case rsa_psk: /* NEW */
opaque psk_identity_hint<0..2^16-1>;
};
} ServerKeyExchange;

struct {
select (KeyExchangeAlgorithm) {
/* other cases for rsa, diffie_hellman, etc. */

case rsa_psk: /* NEW */
opaque psk_identity<0..2^16-1>;
EncryptedPreMasterSecret;
} exchange_keys;
} ClientKeyExchange;

其中EncryptedPreMasterSecret格式见rfc2246-tls1.0

3.4. 使用PSK构建预主密钥

使用PSK构建预主密钥的通用格式为:

1
2
3
4
struct {
opaque other_secret<0..2^16-1>;
opaque psk<0..2^16-1>;
};

其中psk就是根据psk_id查找出来的对应的psk_key,而other_secret是根据不同的秘钥交换算法而定的:

  • PSK: 跟psk相同长度的0字节串;
  • DHE_PSK: client和server使用DH交换参数生成了共享秘钥Z,去掉Z的前导0,将其填入other_secret中;
  • RSA_PSK: client生成的随机secret,用RSA公钥加密后发送给server,这样就得出了共享秘钥,将其填入other_secret中。

4. TLS1.3的PSK

TLS1.3的PSK机制改变很大,虽然也有一部分之前TLS版本的PSK的功能,但还整合了其他功能:

  1. TLS1.2的PSK相当于外部导入PSK机制,用户预先在client和server部署psk_key(手动部署或使用其他秘钥交换算法进行)。TLS1.3也保留了这部分功能,支持外部导入;
  2. TLS1.2使用的session ID(server有状态)和ticket(server无状态)的会话恢复机制,现在统一整合到PSK中,这时候相当于psk_key从上次完整会话中生成。
  3. PSK还用于支持TLS1.3特有的0-RTT早期数据,通过牺牲前向安全性,来在client发起连接时,使用PSK的psk_key直接加密用户数据,发给server,server可以直接使用查找得到的psk_key解密数据。加快应用数据的交换过程。

用于不同目的的PSK在细节处有些不一样,下面分别说明。

4.1. 外部导入PSK

4.1.1 协商过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
client                                      server
ClientHello
+ key_share*
+ psk_key_exchange_modes
+ pre_shared_key -------->
ServerHello
+ pre_shared_key
+ key_share*
{EncryptedExtensions}
{Finished}
<-------- [Application Data*]
{Finished} -------->

[Application Data] <-------> [Application Data]
  1. client如果希望使用PSK进行握手,就在CH中加入psk_key_exchange_modespre_shared_key扩展项,其中psk_key_exchange指示是使用单纯的PSK还是同时使用PSK和DHE进行秘钥交换;pre_shared_key中存放的就是之前的psk_id和一些相关信息;如果是psk_ke就不需要发送key_share扩展了,如果是psk_dhe_ke,还需要发送key_share扩展,以携带DHE相关的参数;
  2. server如果也希望使用PSK,就在SH中回复选择哪个psk_id
  3. server不再发送CertificateCertificateRequest,因为这时候不需要证书了;
  4. client和server可以从psk_kepsk_dhe_ke两种秘钥交换算法中得出共享的秘钥,然后使用它进行其他秘钥推导。

相关的消息格式为:

4.1.2. psk_key_exchange_modes扩展项格式

1
2
3
4
5
enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;

struct {
PskKeyExchangeMode ke_modes<1..255>;
} PskKeyExchangeModes;

其中:

  • psk_ke:类似于于TLS1.2的plain PSK机制,就是只使用PSK进行认证协商,不使用其他秘钥交换算法。这时候不能发送key_share扩展了。
  • psk_dhe_ke:类似于TLS1.2的DHE_PSK机制,同时使用DH和PSK进行认证协商。必须同时发送key_share

4.1.3. pre_shared_key扩展项格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct {
select (Handshake.msg_type) {
case client_hello:
OfferedPsks;
case server_hello:
uint16 selected_identity;
};
} PreSharedKeyExtension;

struct {
PskIdentity identities<7..2^16-1>;
PskBinderEntry binders<33..2^16-1>;
} OfferedPsks;

struct {
opaque identity<1..2^16-1>;
uint32 obfuscated_ticket_age;
} PskIdentity;

opaque PskBinderEntry<32..255>;

其中:

  • identity: 跟TLS1.2中psk_id效果类似,都是用于查找相应的psk_key的;但由于TLS1.3集成了会话恢复的功能,会有一些其他内容:1)如果是外部导入PSK,这个值就是TLS1.2一样的psk_id,用以查找相应的psk_key。2)如果是用于会话恢复的PSK,分为session ID和ticket实现机制,如果是session ID机制,这里存放的相当于TLS1.2中的session ID,用于指导server查找相应的session加密状态;如果是ticket实现机制,这里存放的就是TLS1.2的ticket,server收到之后,使用ticket中的key_name查找对应的解密秘钥,解密出ticket中的内容,进而恢复session加密状态。但总体来看,client和server可以通过identity得到相应的加密状态——不管是外部导入,还是会话恢复。
  • obfuscated_ticket_age: client计算psk_key已存活的时间,使用一种混淆方式。如果是外部导入PSK,值是0;如果是用于会话恢复的PSK,值的计算见下边NewSessionTicket
  • identities: client可能会提供多个psk供server选择,所以这是个列表。如果client同时发送了0-RTT数据,那应用数据就必须使用第1个psk去加密(编号从0开始)。
  • binders: 使用每个PSK的psk_key对当前trans-hash计算的一个HMAC值,用于将PSK和当前握手绑定到一起。计算顺序跟identities中的一样。
  • selected_identity: server希望使用哪个psk进行后续握手,填编号,编号从0开始。

4.1.4. binders计算

主要使用HMAC计算,HMAC详细过程见HKDF算法
这里,需要计算的数据需要注意:

  1. 如果是计算第一个ClientHello中的psk binder,需要先计算Truncate(ClientHello1)——pre_shared_key扩展项按要求必须放在ClientHello最后,也就是说,其中的psk_binder是在最后的最后,只去掉binders列表,剩下的前边的ClientHello就是Truncate(ClientHello)了。这里ClientHello消息中前边的长度值也需要填好,因为最后计算的binder都是按照HMAC中的hash算法输出的长度,所以长度在计算HMAC值之前就可以确定下来了。在计算Transcript-Hash(Truncate(ClientHello1)),然后使用HMAC对结果进行计算,得到一个个binder值;
  2. 如果是server发送了HRR,需要对Transcript-Hash(ClientHello, HelloRetryRequest, Truncate(ClientHello2)))进行HMAC,这里的ClientHello1是重组后的"message_hash"结构,见TLS1.3的RFC。

计算时使用的hash算法:

  1. 如果是外部导入的PSK,需要用户提供一个对应的HASH算法,没有提供就默认SHA256;
  2. 如果是上次会话生成的PSK,就用上次会话使用的加密套件中的HASH算法。

使用的秘钥如何生成后续再完整描述。

4.2. 会话恢复PSK

4.2.1. 协商过程

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
// 第一次协商
client server
ClientHello
+ key_share -------->
ServerHello
+ key_share
{EncryptedExtensions}
{CertificateRequest*}
{Certificate*}
{CertificateVerify*}
{Finished}
<-------- [Application Data*]
{Certificate*}
{CertificateVerify*}
{Finished} -------->
<-------- [NewSessionTicket]
[Application Data] <-------> [Application Data]

// 第二次协商
client server
ClientHello
+ key_share*
+ psk_key_exchange_modes
+ pre_shared_key -------->
ServerHello
+ pre_shared_key
+ key_share*
{EncryptedExtensions}
{Finished}
<-------- [Application Data*]
{Finished} -------->

[Application Data] <-------> [Application Data]
  1. 第一次完整握手,server会发送证书等相关信息,进行认证握手。在协商完成后,server会发送NewSessionTicket给client,其中携带的有session ID或ticket;
  2. 第二次会话恢复,流程跟外部导入PSK一致,只不过其中PSK中的psk_key不再是外部导入,而是上次会话生成的。

4.2.2. NewSessionTicket消息格式

ticket相关

4.3. 支持早期数据

4.3.1. 协商流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Client                                      Server
ClientHello
+ early_data
+ key_share*
+ psk_key_exchange_modes
+ pre_shared_key
(Application Data*) -------->
ServerHello
+ pre_shared_key
+ key_share*
{EncryptedExtensions}
+ early_data*
{Finished}
<-------- [Application Data*]
(EndOfEarlyData)
{Finished} -------->

[Application Data] <-------> [Application Data]
  1. client使用pre_shared_key中的第一个psk_key对用户数据进行加密。其他流程不变。

4.4. 其他

4.4.1. PSK跟server_name的关系

TLS1.3之前的版本中,SNI应该是跟session状态绑定到一起的,也就是说,每次会话恢复的时候,还是使用之前保存的session状态中的SNI,用户不能再重新设置。但很多实现没有处理好,导致恢复的SNI和用户提供的SNI不一致,这样SNI的最终确定是由client进行判断的。
TLS1.3中的SNI强制跟session绑定到一起,在会话恢复中必须明确带上SNI,而server不需要将SNI存储到ticket中,每次都根据CH去重新选择。

5. openssl关于PSK的API

这里只是有关外部的导入PSK的相关操作。会话恢复的一些操作见ticket相关

5.1. 设置client期望使用的PSK

1
2
3
4
5
6
7
8
9
10
11
#include <openssl/ssl.h>

typedef unsigned int (*SSL_psk_client_cb_func)(SSL *ssl,
const char *hint,
char *identity,
unsigned int max_identity_len,
unsigned char *psk,
unsigned int max_psk_len);

void SSL_CTX_set_psk_client_callback(SSL_CTX *ctx, SSL_psk_client_cb_func cb);
void SSL_set_psk_client_callback(SSL *ssl, SSL_psk_client_cb_func cb);

这俩函数也能用于TLS1.3,但建议限于TLS1.2及之前的client的PSK设置。
回调函数在TLS1.2的触发时机:

  1. client发送SKE的时候,会触发该回调。其中hint是server发给client的psk_identity_hint字符串,应用根据hint挑选相应的PSK,把选中的PSK的psk_id放入*identity中,在psk中放入相应的psk_key

回调函数返回0表示成功,同样设置了有效的psk的话,可以使用PSK。返回其他值会导致握手失败。

1
2
3
4
5
6
7
8
9
10
#include <openssl/ssl.h>

typedef int (*SSL_psk_use_session_cb_func)(SSL *ssl, const EVP_MD *md,
const unsigned char **id,
size_t *idlen,
SSL_SESSION **sess);

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);

这俩函数只限于TLS1.3的PSK。
回调函数SSL_psk_use_session_cb_func()触发时机:

  1. client发送CH的时候,在组建PSK相关扩展项时,会触发一次,这时候mdNULL, 用户需要在回调中将希望使用的psk_id放到**id中,将PSK的其他相关信息放到**sess中。*id指向的内存还是由应用管理,但用户在握手完成之后才能删除它。
  2. 在client收到SH后,如果server也期望使用PSK,则会回复pre_share_key扩展,这时候会再触发一次,这时候server已经协商出了一个加密套件,其中的摘要算法会在md中给应用提供,应用提供的PSK中的摘要算法必须跟给出的一样,否则就不能使用。两次触发时应用返回的PSK可以不一样。

**sess指向的SSL_SESSION中至少要有以下信息:

  • master key:可以用SSL_SESSION_set1_master_key()去设置,就是设置psk_key的;
  • ciphersuite:一个加密套件,PSK只关心其中的摘要算法(hash),因为后续计算PSK binder的时候会用到,server也会根据相应的摘要算法选择最后协商的加密套件。可以设置任何TLS1.3的加密套件。
  • protocol version: 只能是TLS1_3_VERSION
  • 如果需要支持PSK,需要用 SSL_SESSION_set_max_early_data()设置早期数据最大长度。

在回调中返回正确,但不返回sess,说明应用想继续握手,但不想用PSK。返回失败会导致握手失败。

5.2. 在server中选择PSK

1
2
3
4
5
6
7
8
9
10
11
#include <openssl/ssl.h>
typedef unsigned int (*SSL_psk_server_cb_func)(SSL *ssl,
const char *identity,
unsigned char *psk,
unsigned int max_psk_len);

int SSL_CTX_use_psk_identity_hint(SSL_CTX *ctx, const char *hint);
int SSL_use_psk_identity_hint(SSL *ssl, const char *hint);

void SSL_CTX_set_psk_server_callback(SSL_CTX *ctx, SSL_psk_server_cb_func cb);
void SSL_set_psk_server_callback(SSL *ssl, SSL_psk_server_cb_func cb);

TLS1.2及以前用这俩函数。
了解PSK机制和client端的设置的话,这里的就很容易看懂了。

1
2
3
4
5
6
7
8
9
10
11
#include <openssl/ssl.h>

typedef int (*SSL_psk_find_session_cb_func)(SSL *ssl,
const unsigned char *identity,
size_t identity_len,
SSL_SESSION **sess);


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.3尽量用这俩函数。
回调函数在server收到CH时处理,lib会将psk_id放到identity中,返回给应用,应用根据identity去选择相应的PSK,将PSK的相关信息存储到sess中,返回给lib。sess中相关信息见上边client的配置。

6. 其他

  1. 以上只是针对正常逻辑,具体的异常逻辑需要查阅相应RFC。
  2. 理解这些协议机制,其实就是了解过程和相应的数据结构,就差不多了。

7. 参考

  1. PSK的RFC: RFC4279-Pre-Shared Key Ciphersuites for Transport Layer Security (TLS)
  2. ServerDHParams格式: RFC2246-tls1.0