1. 为什么
TLS通常用证书体系(PKI)进行认证,先回顾一下传统的RSA证书认证过程,
- server发送证书给client, 证书中包含RSA的公钥,只有server有证书中公钥对应的私钥;
- client收到证书后,对证书进行校验(PKI体系一层层往CA验证,直到根CA),校验通过后,用证书中的公钥对预主密钥(client每次随机生成)进行加密,将加密后的数据发送给server;
- 只有server能解密数据,取出其中的预主密钥。这样,server和client之间就有了共同的秘钥,可以用于后续的加密了。 这里的关键是只有server有对应的RSA私钥,然后server的RSA公钥可以由信任的根CA一层层校验,这样,client就可以认为它正在通信的对方是自己想要通信的server。server对client进行的过程认证基本一样。
但使用证书有几个问题:
- 证书需要非对称算法,不管是RSA还是ECC,加密相同强度都比对称算法慢;
- 证书的PKI体系部署管理比较麻烦,需要根CA、子CA、server证书、证书私钥,client证书等;
- 认证过程需要发送证书,证书通常比较大,需要传输时间比较长。
所以,PSK就解决这样的问题:
- PSK可以只需要对称算法,不使用非对称算法;
- PSK在可控的封闭环境中容易部署;如果应用使用其他办法能生成一个共享的秘钥,PSK就可以用该秘钥建立TLS连接了;
- 认证时PSK可以传输较小的数据量。
2. 是什么
PSK——pre-shared key,预共享密钥——就是通信双方使用提前部署好的预共享密钥(用于对称算法)建立TLS连接的机制。
PSK的主要逻辑是:
- 分别在client和server部署相同的对称加密秘钥(
psk_key
); - 每个
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 | Client Server |
- client如果希望使用PSK进行握手,就在
CH
中加入PSK加密套件发送给server; - server如果也希望使用PSK,就在
SH
中回复相应的PSK加密套件; - server不再发送
Certificate
和CertificateRequest
,因为这时候不需要证书了; - client和server都可能有多组
psk_key
,server可以在SKE
中发送一个psk identity hint
的东西(类似索引之类的)帮助client去选择相应的psk_key
;如果没有psk identity hint
,就不发送SKE
了; - client把希望使用的
psk_key
对应的psk_id
放到CKE
中,发送给server; - server收到
CKE
后,根据其中的psk_id
选择相应的psk_key
,然后将psk_key
当做预主密钥(pre master secret)进行后续的秘钥推导;
相关的消息格式:
1 | struct { |
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 | struct { |
其中:
ServerDHParams
:保存的是server临时生成的DH公开参数(其中dh_Ys
相当于server的公钥)。格式为:1
2
3
4
5struct {
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
9struct {
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 | struct { |
其中EncryptedPreMasterSecret
格式见rfc2246-tls1.0
。
3.4. 使用PSK构建预主密钥
使用PSK构建预主密钥的通用格式为:
1 | struct { |
其中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的功能,但还整合了其他功能:
- TLS1.2的PSK相当于外部导入PSK机制,用户预先在client和server部署
psk_key
(手动部署或使用其他秘钥交换算法进行)。TLS1.3也保留了这部分功能,支持外部导入; - TLS1.2使用的session ID(server有状态)和ticket(server无状态)的会话恢复机制,现在统一整合到PSK中,这时候相当于
psk_key
从上次完整会话中生成。 - PSK还用于支持TLS1.3特有的
0-RTT
早期数据,通过牺牲前向安全性,来在client发起连接时,使用PSK的psk_key
直接加密用户数据,发给server,server可以直接使用查找得到的psk_key
解密数据。加快应用数据的交换过程。
用于不同目的的PSK在细节处有些不一样,下面分别说明。
4.1. 外部导入PSK
4.1.1 协商过程
1 | client server |
- client如果希望使用PSK进行握手,就在
CH
中加入psk_key_exchange_modes
和pre_shared_key
扩展项,其中psk_key_exchange
指示是使用单纯的PSK还是同时使用PSK和DHE进行秘钥交换;pre_shared_key
中存放的就是之前的psk_id
和一些相关信息;如果是psk_ke
就不需要发送key_share
扩展了,如果是psk_dhe_ke
,还需要发送key_share
扩展,以携带DHE相关的参数; - server如果也希望使用PSK,就在
SH
中回复选择哪个psk_id
; - server不再发送
Certificate
和CertificateRequest
,因为这时候不需要证书了; - client和server可以从
psk_ke
和psk_dhe_ke
两种秘钥交换算法中得出共享的秘钥,然后使用它进行其他秘钥推导。
相关的消息格式为:
4.1.2. psk_key_exchange_modes
扩展项格式
1 | enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode; |
其中:
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 | struct { |
其中:
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算法。
这里,需要计算的数据需要注意:
- 如果是计算第一个
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值; - 如果是server发送了HRR,需要对
Transcript-Hash(ClientHello, HelloRetryRequest, Truncate(ClientHello2)))
进行HMAC,这里的ClientHello1
是重组后的"message_hash"
结构,见TLS1.3的RFC。
计算时使用的hash算法:
- 如果是外部导入的PSK,需要用户提供一个对应的HASH算法,没有提供就默认SHA256;
- 如果是上次会话生成的PSK,就用上次会话使用的加密套件中的HASH算法。
使用的秘钥如何生成后续再完整描述。
4.2. 会话恢复PSK
4.2.1. 协商过程
1 | // 第一次协商 |
- 第一次完整握手,server会发送证书等相关信息,进行认证握手。在协商完成后,server会发送
NewSessionTicket
给client,其中携带的有session ID或ticket; - 第二次会话恢复,流程跟外部导入PSK一致,只不过其中PSK中的
psk_key
不再是外部导入,而是上次会话生成的。
4.2.2. NewSessionTicket
消息格式
见ticket相关。
4.3. 支持早期数据
4.3.1. 协商流程
1 | Client Server |
- 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 | #include <openssl/ssl.h> |
这俩函数也能用于TLS1.3,但建议限于TLS1.2及之前的client的PSK设置。
回调函数在TLS1.2的触发时机:
- client发送
SKE
的时候,会触发该回调。其中hint
是server发给client的psk_identity_hint
字符串,应用根据hint
挑选相应的PSK,把选中的PSK的psk_id
放入*identity
中,在psk
中放入相应的psk_key
。
回调函数返回0表示成功,同样设置了有效的psk
的话,可以使用PSK。返回其他值会导致握手失败。
1 | #include <openssl/ssl.h> |
这俩函数只限于TLS1.3的PSK。
回调函数SSL_psk_use_session_cb_func()
触发时机:
- client发送
CH
的时候,在组建PSK相关扩展项时,会触发一次,这时候md
是NULL
, 用户需要在回调中将希望使用的psk_id
放到**id
中,将PSK的其他相关信息放到**sess
中。*id
指向的内存还是由应用管理,但用户在握手完成之后才能删除它。 - 在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 | #include <openssl/ssl.h> |
TLS1.2及以前用这俩函数。
了解PSK机制和client端的设置的话,这里的就很容易看懂了。
1 | #include <openssl/ssl.h> |
TLS1.3尽量用这俩函数。
回调函数在server收到CH
时处理,lib会将psk_id
放到identity
中,返回给应用,应用根据identity
去选择相应的PSK,将PSK的相关信息存储到sess
中,返回给lib。sess
中相关信息见上边client的配置。
6. 其他
- 以上只是针对正常逻辑,具体的异常逻辑需要查阅相应RFC。
- 理解这些协议机制,其实就是了解过程和相应的数据结构,就差不多了。
7. 参考
- PSK的RFC: RFC4279-Pre-Shared Key Ciphersuites for Transport Layer Security (TLS)
ServerDHParams
格式: RFC2246-tls1.0