这篇blog是关于tls1.3协议handshake部分的介绍和分析,主要内容来自于rfc8446,这里主要侧重tls1.3区别于tls1.2的部分,并尝试搞清楚tls1.3这些变更的设计动机。
tls1.3将握手概括为三个阶段
可以看出tls1.3在第一个rtt就开始进行密钥交换以及server端的身份认证了,一个rtt后就可以开始application data的传输了,之所以能做到这点,是因为tls1.3只保留(EC)DHE方式的密钥交换,其不依赖于server端证书。
此外tls1.3还允许server端在第一个rtt完成同时就先向client端传输加密的application data,对于应用层是http协议而言,这个特性可能用处不大。
tls1.3废弃了1.2中的session resumption机制,采用更为的通用的PSK机制,PSK在原理上很类似于tls1.2的session ticket。一个带有PSK扩展的握手过程如下:
可以看出PSK的handshake过程也是1-rtt,但是相比于full handshake省去了身份认证的过程。另外PSK可以和(EC)DHE密钥交换结合,做到前向安全。
有关PSK机制的更多内容后面再介绍。
tls1.3另外一个重要的升级是支持0-rtt模式,所谓0-rtt就是client在第一个rtt基于PSK握手的同时带上加密的application data,server也是在第一个rtt完成的同时响应application data,协议称之为early data。0-rtt模式下加密application data的密钥仅能由PSK密钥派生,也没有server端握手信息参与,因此本身不能防止被重放。一个0-rtt的握手和数据交换过程如下:
uint16 ProtocolVersion;
opaque Random[32];
uint8 CipherSuite[2]; /* Cryptographic suite selector */
struct {
ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */
Random random;
opaque legacy_session_id<0..32>;
CipherSuite cipher_suites<2..2^16-2>;
opaque legacy_compression_methods<1..2^8-1>;
Extension extensions<8..2^16-1>;
} ClientHello;
1.3的ClientHello在设计上吸取了当年部署tls1.2时候教训,不采用此前tls基于ProtocolVersion的版本协商机制。此外由于需要兼容各种中间件,在协议结构上保持和tls1.2完全一致。体现在legacy_version设置为1.2的值,保留legacy_session_id字段,为了防止僵化,要求实现设置一个随机值。保留legacy_compression_methods字段。random和cipher_suites字段依旧生效,其他协议版本、加密材料、密钥参数、握手相关协商等都放到了扩展中。
supported_versions
是强制存在的扩展, 为了避免tls1.2版本协商机制带来的问题,tls1.3采用的是在client hello的supported_versions扩展里带上自己所支持的versions,在server hello的supported_versions选择一个version。
由于去掉了rsa-based key exchange,只保留DHE-based key exchange,使得tls1.3在client hello里就开始进行密钥交换,通过key_share
扩展将自己的DHE公钥发给server端。
tls1.3中废弃了tls1.2的session resumption和session ticket机制,采用更为通过的pre_shared_key机制(可以视作session ticket的升级版)。具体来说client在hello消息里可以通过pre_shard_key
扩展带上自己支持的key标识列表(可能是之前的tls session里生成的,也可能是其他外部方式预先配置的),同时通过psk_key_exchange_modes
扩展指定支持的psk的使用方式(目前有两种方式:PSK-only和PSK-with-DHE)。server端选择也通过pre_shard_key
扩展选择一个PSK,通过psk_key_exchange_modes
选择一种mode,双端达成对PSK的一致。
tls1.3还有个重要的改动是摒弃了之前异常复杂的各种算法排列组合形成的cipher-suite-list,转而采用各种算法各自正交协商,其中cipher_suites字段只是用来协商对称加密算法,并且只保留了AEAD系列算法。密钥交换算法只支持DHE-based算法,通过supported_groups
扩展来协商用什么椭圆曲线, 通过signature_algorithms
来协商签名算法。
tls1.3支持0-rtt,即紧随clientHello后就开始发送Application data,0-rtt要求client在ClientHello里至少需要带上early_data
扩展以及pre_shared_key
扩展,early_data
用于告知server接下来会有early application data,这些data使用的是pre_shared_key
扩展中第一个PSK对应的密钥加密的(client_early_traffic_secret)。如果server端接受early data,那么也需要在消息里带上early_data
扩展,告诉client端自己接受并处理early data,tls1.3规定server端的early_data
在Server的EncryptedExtensions消息里带上。注意early data使用的密钥和后面正常的application data使用的密钥是不一样的,所以client在收到server的Finished消息后要切换密钥,并且发送一个EndOfEarlyData消息告知对段后续数据用正常密钥加密。
pre-shared-key
扩展数据字段定义如下:
struct {
opaque identity<1..2^16-1>;
uint32 obfuscated_ticket_age;
} PskIdentity;
opaque PskBinderEntry<32..255>;
struct {
PskIdentity identities<7..2^16-1>;
PskBinderEntry binders<33..2^16-1>;
} OfferedPsks;
struct {
select (Handshake.msg_type) {
case client_hello: OfferedPsks;
case server_hello: uint16 selected_identity;
};
} PreSharedKeyExtension;
pre-shared-key
扩展里提供了一个PSK的列表供server选择,对于server来说选择一个PSK在ServerHello扩展里返回给client struct {
ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */
Random random;
opaque legacy_session_id_echo<0..32>;
CipherSuite cipher_suite;
uint8 legacy_compression_method = 0;
Extension extensions<6..2^16-1>;
} ServerHello;
HelloRetryRequest
的SHA-256值来区别是ServerHello还是HelloRetryRequest。所谓的downgrade protection在tls1.3中有个正式的定义
The cryptographic parameters should be the same on both sides and should be the same as if the peers had been communicating in the absence of an attack
在tls1.2以及之前版本的tls协议存在多种downgrade攻击,比如FREAK、logjam等,攻击者在中间通过篡改handshake消息让双方协商出一个弱密钥,然后break掉master secret,从而能改篡改为了防止downgrade的Finished消息,以使得双方继续以被破解的密钥进行通信。另一方面,防止downgrade的另一个有效机制——签名算法在tls1.2以及之前并没有得到合理利用,整个握手过程的消息并没有签名保护(tls1.2只在ServerKeyExchange里用签名保护了DH公钥和random)。
因此tls1.3在downgrade这块一个关键的改动是签名覆盖整个握手消息(server 在CertificateVerify里面将整个handshake消息进行签名发给client端,如果有篡改,client终止连接)。这点阻隔了大部分的downgrade attacks,但是仅仅是这一点还不够,因为协议需要后向兼容的原因downgrade attack依旧存在。攻击者可以篡改握手让都支持tls1.3的client和server双方版本降级到tls1.2或之前上,然后再利用低版本的协议进行downgrade攻击。
因此tls1.3为了防止这个版本降级,在server random里嵌入了一个降级保护机制,具体来说,如果支持tls1.3的server被协商以低版本tls通信,会在random的最后8个字节里注入一个标识(如果协商的是tls1.2,注入44 4F 57 4E 47 52 44 01
; 如果协商的是tls1.1,注入44 4F 57 4E 47 52 44 00
),tls1.3的client会去检测random里有没有这个标识,如果有的话,表示存在版本降级攻击,中止连接。另外协议还建议最高支持tls1.2的client也去检测random,这样tls1.2的client也能够防止被降级到tls1.1或之前。至于为什么在random里面去注入这个降级标识呢,因为tls1.2和之前在ServerKeyExchange消息中有对random的签名保护,所以不用担心MITM偷偷修去掉random里的降级标识。
至此,tls1.3 key exchange阶段结束。
Encrypted Extensions紧随Server Hello之后,这是tls1.3中首个加密的消息,加密采用的key基于server_handshake_traffic_secret派生得到。专门用于Server发送一些加密扩展的消息
和tls1.2中的同名消息类似,作用也一样。区别是
signature_algorithms
、signature_algorithms_cert
和"certificate_authorities
扩展给出。至此,tls1.3 Server Parameters阶段结束。
和tls1.2的同名消息区别不大。tls1.3的Certificate消息还支持扩展,比如在扩展里将各证书的OCSP状态返回。
验证证书等内容不在tls协议标准范围内
这个消息是tls1.3和之前一个重要升级的地方。在tls1.2中只有client端在提供了client 证书后才需要发送,server则不需要依赖这个消息来证明自己确实拥有Certificate消息中的证书(因为ServerKeyExchange消息server需要证书的私钥去签名DH公钥匙+random)。
在tls1.3中,Server端将也需要发送这个消息,一方面是为了证明自己拥有证书,另一方面通过对整个handshake消息计算transcript-hash,然后进行签名,保护握手免受FREAK、logjam这类MITM攻击。
Finished消息是Authentication阶段的最后一个消息。作用和之前一样,区别不大。
TLS支持在handshake完成后,继续发送一些类型属于Handshake的消息,主要有:
NewSessionTicket作用和tls1.2 session ticket对应的基本一致,都是用于server生成一个ticket给client作为PSK,以恢复后续握手。
为什么会有这个机制?
- servers that have the ability to serve requests from multiple domains over the same connection but do not have a certificate that is simultaneously authoritative for all of them
- servers that have resources that require client authentication to access and need to request client authentication after the connection has started
- clients that want to assert their identity to a server after a connection has been established
- clients that want a server to re-prove ownership of their private key during a connection
- clients that wish to ask a server to authenticate for a new domain not covered by the certificate included in the initial handshake
quote from http://www.watersprings.org/pub/id/draft-sullivan-tls-post-handshake-auth-00.html
这里几个场景我认为很典型,首先对于server,可能有一些资源访问是需要认证client的,但是tls连接建立的一开始不需要认证client,这时候post-handshake认证就有用了。要支持Post-Handshake Authentication, Client首先要在ClientHello里带上post_handshake_auth
表示自己支持Post-Handshake Authentication, 然后server就可以在handshake建立后的任意时候通过发送CertificateRequest消息来要求client验证身份。Client则需要回复依次回复Certificate, CertificateVerify, Finished消息。
KeyUpdate消息作用是让两端同步更新key/IV, 由一端发起,另一端ack
对于没有进行加密保护前的Record层的结构和tls1.2一致,如下TLSPlaintext的定义所示:
enum {
invalid(0),
change_cipher_spec(20),
alert(21),
handshake(22),
application_data(23),
(255)
} ContentType;
struct {
ContentType type;
ProtocolVersion legacy_record_version;
uint16 length;
opaque fragment[TLSPlaintext.length];
} TLSPlaintext;
对于需要加密保护的record,相对于tls1.2,tls1.3对record层密码学保护做的最重要的改动是只保留AEAD加密。此外tls1.3选择将除了ClientHello、ServerHello外的握手消息都采用加密传输,为了前向兼容,这些加密的握手消息在record层的type都是application_data。因此对于需要加密保护的record来说,结构如下:
struct {
opaque content[TLSPlaintext.length];
ContentType type;
uint8 zeros[length_of_padding];
} TLSInnerPlaintext;
struct {
ContentType opaque_type = application_data; /* 23 */
ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
uint16 length;
opaque encrypted_record[TLSCiphertext.length];
} TLSCiphertext;
aead 的additional_data构造、加密、解密过程如下:
additional_data = TLSCiphertext.opaque_type ||
TLSCiphertext.legacy_record_version ||
TLSCiphertext.length
AEADEncrypted = AEAD-Encrypt(write_key, nonce, additional_data, plaintext)
plaintext of encrypted_record =
AEAD-Decrypt(peer_write_key, nonce, additional_data, AEADEncrypted)
可以看出,和tls1.2不同的是additional_data没有seq_num的参与,tls1.3将seq_num编码进pre_record的nonce中。nonce通过seq_num XOR (client_write_iv / server_write_iv) 得到。在tls1.2中nonce则有显式部分和隐式部分拼接构成。
相比于tls1.2自制的PRF,tls1.3采用的HKDF标准,HKDF标准中派生密钥分为两个部分:
HKDF_Extract(salt, IKM) -> PRK
//salt: 可选的“盐”,如果不提供,则默认为0串
//IKM: 初始密钥材料
//PRK: 定长的伪随机密钥
HKDF_Expand(PRK, info, L) -> OKM
//PRK:HKDF_Extract的输出
//info:可选的上下文信息,默认是空字符串“”,当IKM被用于多种业务时,就可以用info来保证导出不一样的OKM
//L:指定输出的OKM的字节长度,不能超过255*HashLen
//OKM: 输出密钥材料
tls1.3在HKDF_Expand基础之上定义了Derive_Secret函数:
HKDF-Expand-Label(Secret, Label, Context, Length) =
HKDF-Expand(Secret, HkdfLabel, Length)
Where HkdfLabel is specified as:
struct {
uint16 length = Length;
opaque label<7..255> = "tls13 " + Label;
opaque context<0..255> = Context;
} HkdfLabel;
Derive-Secret(Secret, Label, Messages) =
HKDF-Expand-Label(Secret, Label,
Transcript-Hash(Messages), Hash.length)
Derive_Secret将当前handshake消息的Transcript-Hash作为HKDF_Expand的上下文。
整个key派生过程就是对两个原始密钥(PSK密钥和ECDHE协商出的密钥)使用HKDF-Extract、Derive-Secret两个进行密钥生成和派生过程。
0
|
v
PSK -> HKDF-Extract = Early Secret
|
+-----> Derive-Secret(., "ext binder" | "res binder", "")
| = binder_key
|
+-----> Derive-Secret(., "c e traffic", ClientHello)
| = client_early_traffic_secret
|
+-----> Derive-Secret(., "e exp master", ClientHello)
| = early_exporter_master_secret
v
Derive-Secret(., "derived", "")
|
v
(EC)DHE -> HKDF-Extract = Handshake Secret
|
+-----> Derive-Secret(., "c hs traffic",
| ClientHello...ServerHello)
| = client_handshake_traffic_secret
|
+-----> Derive-Secret(., "s hs traffic",
| ClientHello...ServerHello)
| = server_handshake_traffic_secret
v
Derive-Secret(., "derived", "")
|
v
0 -> HKDF-Extract = Main Secret
|
+-----> Derive-Secret(., "c ap traffic",
| ClientHello...server Finished)
| = client_application_traffic_secret_0
|
+-----> Derive-Secret(., "s ap traffic",
| ClientHello...server Finished)
| = server_application_traffic_secret_0
|
+-----> Derive-Secret(., "exp master",
| ClientHello...server Finished)
| = exporter_secret
|
+-----> Derive-Secret(., "res master",
ClientHello...client Finished)
= resumption_secret
有了各阶段的secret后,再派生出对应的对称密钥和iv
[sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
[sender]_write_iv = HKDF-Expand-Label(Secret, "iv", "", iv_length)
[sender] denotes the sending side. The value of Secret for each
record type is shown in the table below.
+-------------------+---------------------------------------+
| Record Type | Secret |
+-------------------+---------------------------------------+
| 0-RTT Application | client_early_traffic_secret |
| | |
| Handshake | [sender]_handshake_traffic_secret |
| | |
| Application Data | [sender]_application_traffic_secret_N |
+-------------------+---------------------------------------+
TLS在0-rtt模式下协议本身不能防重放,需要应用层措施来防止重放,这里列举几个措施
顾名思义PSK只能使用一次,这需要server端存储所有颁发的并且还在生效中的PSK,一旦某个PSK使用了,从存储里删除这个PSK,0-rtt建连时候检查有没有对应的PSK,有的话接受连接和数据,没有则中断或者fallback到完全握手
server端记录此前一个时间窗口内所有带PSK的Client Hello的唯一标识(可能是random字段也可以是PSK binder字段),如果后面带有同样标识的Client hello过来,直接拒绝掉或者fallback到完全握手,Client Hello标识没有出现过才接受连接和数据。时间窗口大小设置和PSK的有效期保持一致就行。
提法有些奇怪,协议原文叫做Freshness Checks。说的是server通过高效检查ClientHello里PSK是否足够新鲜来决定是否接受PSK。之前在介绍pre_shared_key扩展时留了个疑问,为啥扩展里需要带上client视角的ticket_age,这时候就用上了。如果攻击者复制了某个0-rtt的ClientHello过一段时间后重放这个ClientHello,server可以很简单地通过校验这个client视角下的ticket_age是否和server视角的ticket age匹配,如果差距很大的话(比如server视角下ticket age要比client视角的ticket_age大很多,表示不新鲜了)就可以拒绝接受这个PSK。server可以通过将ticket的createtime编码在ticket做到无状态计算这个server视角的ticket_age。
https://blog.cloudflare.com/rfc-8446-aka-tls-1-3/
https://www.ietf.org/id/draft-ietf-tls-rfc8446bis-02.html
https://datatracker.ietf.org/doc/html/rfc8446
https://blog.cloudflare.com/why-tls-1-3-isnt-in-browsers-yet/
https://tls13.ulfheim.net/