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