跳到主要内容

TLS1.3

tls1.3 酝酿了十年,和前面的tls版本有了较大的改动 —— 但是整体流程还是类似的。

Handshake Protocol

握手协议的目的还是为 record layer 层协商出相应的安全参数。

其类型为:

enum {
- hello_request(0),
client_hello(1),
server_hello(2),
+ new_session_ticket(4),
+ end_of_early_data(5),
+ encrypted_extensions(8),
certificate(11),
- server_key_exchange (12),
certificate_request(13),
- server_hello_done(14),
certificate_verify(15),
- client_key_exchange(16),
finished(20),
+ key_update(24),
+ message_hash(254),
(255)
} HandshakeType;

- 表示旧的存在,在1.3中被移除的消息类型
+ 表示旧的不存在,在1.3中被加入的消息类型

全流程的握手为:

       Client                                           Server

Key ^ 1 ClientHello
Exch | + key_share*
| + signature_algorithms*
| + psk_key_exchange_modes*
v + pre_shared_key* -------->

------------↓ 异常分支:不匹配参数的握手 ↓-------------------
HelloRetryRequest 2.1
<-------- + key_share
3 ClientHello
+ key_share -------->
------------↑ 异常分支:不匹配参数的握手 上-------------------


ServerHello 2.2 ^ Key
+ key_share* | Exch
+ pre_shared_key* v
{EncryptedExtensions} 4 ^ Server
{CertificateRequest*} 5 v Params
{Certificate*} 6 ^
{CertificateVerify*} 7 | Auth
{Finished} 8 v
<-------- [Application Data*] 9
^ 10 {Certificate*}
Auth | 11 {CertificateVerify*}
v 12 {Finished} -------->

[Application Data] <-------> [Application Data]


+表示之前提到消息中发送的重要扩展。
*表示不经常发送的可选或者特定情况下的消息或扩展。
{} 表示由[sender]_handshake_traffic_secret 导出的秘钥加密的消息。
[] 表示由[sender]_application_traffic_secret_N 导出的秘钥加密的消息。

说明如下:

步骤1 + 3 [客户端 ClientHello] 客户端主动发起握手,进行加密协商 或者 在服务端发送 HelloRequestRequest 消息后(3),客户端会发送 ClientHello。 数据格式伪代码如下:

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;

对比 tls < 1.3 的数据格式伪代码:

struct {
uint32 gmt_unix_time;
opaque random_bytes[28];
} Random;

struct {
uint8 major, minor;
} ProtocolVersion;

uint8 CipherSuite[2];

struct {
// 客户端期望使用的协议版本,值为其支持的最高的版本
ProtocolVersion client_version;

// 客户端生成的随机数
Random random;

// 客户端希望当前连接的ID
// 如果没有就为空
// 如果有,说明希望会话恢复。
SessionID session_id;

// 客户端支持的加密套件列表。如果希望会话恢复,需要传递上一次会话一样的值
CipherSuite cipher_suites<2..2^16-1>;

// 客户端支持的压缩算法列表。如果希望会话恢复,需要传递上一次会话一样的值
CompressionMethod compression_methods<1..2^8-1>;
} ClientHello;

新旧版本对比可以发现:

  • 版本号的格式有所不同。旧的版本号被废弃了,使用 legacy_version 字段接收,并且要求值必须是 "1.2"。新增 supported_versions 字段来接收。(原因是在事实表明很多服务端并没有正确的实现版本协商)
  • SessionID 字段 (和SessionTicket扩展)被移除,使用了 legacy_session_id 字段接收。会话恢复的功能被合并到 pre-sahred keys 中。
  • CompressionMethod 被移除(1.3不再压缩),使用了 legacy_compression_methods 接收(安全漏洞)
  • 新加了扩展字段(一直都有,1.3直接作为结构的一部分)

可以发现:版本号、SessionID、CompressionMethod 都被废弃了,Extension 则是被扶正。

消息包括扩展部分说明:

  • (密钥如何生成和使用)客户端支持的 AEAD算法/HKDF哈希对 的加密套件
  • (密钥如何协商)supported_groups 表明 客户端(EC)DHE组,key_share 包括这些组的 (EC)DHE共享
  • (密钥如何协商)pre_shared_key 包括客户端知道的对称密钥标识,psk_key_exchange_modes 说明 PSKs的密钥交换模式(PSK由服务端后续发送的 NewSessionTicket消息中的ticket派生出来。所以第一次握手一定不是PSK交换模式)
  • (数据如何签名)signature_algorithms 表明 客户端支持的签名算法,signature_algorithms_cert 用来进一步指定证书类的签名算法

注:

  • Authenticated Encryption with Associated Data 算法:它是一个规范,规定了一种同时提供认证和加密的算法模式,具体算法比如 AES-GCM(AES的GCM模式,可以类比 AES-CBC)
  • HMAC-based Extract-and-Expand Key Derivation Function:基于HAMC的压缩和扩展密钥派生函数。是一种基于哈希函数的密钥派生函数,用于从一个长密钥生成一到多个加密密钥。
  • DHE:DHE denotes ephemeral Diffie-Hellman, where the Diffie-Hellman parameters are signed by a DSS or RSA certificate, which has been signed by the CA —— tls1.0。是一种临时的DH,使用DH计算出来的共享密钥是临时的,不会在多个会话中多次使用,也就是说DH加上了这种特性后计算出来的相关密钥是动态的,是会不断的变化。所以说DHE是具备完美向前安全性(PFS)。TLS1.3中已经将DH和ECDH这些静态的DH密钥交换算法剔除了。

key_share 扩展

这个扩展 和旧版本的 ServerKeyExchange 有点类似。新版本主要用来补充 (EC)DHE的客户端参数。

这个扩展的数据格式为:

struct {
// 名字,客户端 服务端使用这个标识是哪组的
NamedGroup group;
// DHE 或者 ECDHE encode之后的内容
opaque key_exchange<1..2^16-1>;
} KeyShareEntry;

这个扩展客户端可能发送零到多个。零就代表着客户端请求 HelloRetryRequest,让服务端选择NamedGroup。

服务端的ServerHello 或者 HelloRetryRequest 返回一个 KeyShareEntry。

pre_shared_key扩展

pre_shared_key扩展用于协商PSK密钥建立相关联握手使用的预共享密钥标识。数据结构为:

struct {
// 秘钥标签
opaque identity<1..2^16-1>;
// 密钥生存时间的混淆版本。
uint32 obfuscated_ticket_age;
} PskIdentity;

opaque PskBinderEntry<32..255>;

struct {
// 客户端想要与服务器协商的标识列表。
PskIdentity identities<7..2^16-1>;
// 一系列HMAC值,每个标识值一个,并且以相同顺序排列
PskBinderEntry binders<33..2^16-1>;
} OfferedPsks;

struct {
select (Handshake.msg_type) {
case client_hello: OfferedPsks;
// 服务器选择的标识,以客户端列表中的标识表示为(0-based)的索引
case server_hello: uint16 selected_identity;
};
} PreSharedKeyExtension;

key交换模式除了 (EC)DHE,就是 PSK了(客户端预生成的被剔除)。如果客户端想使用PSK,就需要发送 pre_shared_key + psk_key_exchange_modes 扩展。

psk_key_exchange_modes 的数据格式为:

enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;

struct {
PskKeyExchangeMode ke_modes<1..255>;
} PskKeyExchangeModes;
  • psk_ke: PSK-only密钥建立。 在这种模式下,服务器不得提供key_share值。
  • psk_dhe_ke: PSK和(EC)DHE的秘钥建立。 在这种模式下,客户端和服务器必须提供key_share值

early_data扩展

如果使用了PSK,客户端就能够在 第一个消息中发送应用数据,实现0-RTT握手。

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]

0-RTT的握手消息流

+ 表明是在之前提到的消息中发送的重要扩展
* 表明可选的或者特定条件下发送的消息或扩展
() 表示消息由client_early_traffic_secret导出的密钥保护
{} 表示消息由 [sender]_handshake_traffic_secret导出的密钥保护
[] 表示消息由[sender]_application_traffic_secret_N导出的密钥保护

方法就是使用 early_data扩展 + pre_shared_key扩展。early_data的数据结构为:

struct {} Empty;

struct {
select (Handshake.msg_type) {
case new_session_ticket: uint32 max_early_data_size;
case client_hello: Empty;
case encrypted_extensions: Empty;
};
} EarlyDataIndication;

步骤2.2 [服务端 HelloRetryRequest*] 如果服务端选择了一个(EC)DHE组但是客户端没有提供兼容的 key_share 扩展,服务端必须回复 HelloRetryRequest 消息。这时候客户端需要重新发送 ClientHello

HelloRetryRequest 的数据格式和字段和 ServerHello 是一样的。

功能上则和之前版本的 hello_request 很类似。

步骤2.1 [服务端 ServerHello] 如果服务端选择参数成功,将回复 ServerHello。数据结构伪代码如下:

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;

对比旧的数据格式:

struct {
// 并客户端建议低的服务端最高支持的版本
ProtocolVersion server_version;

// 服务端生成的随机数
Random random;

// 连接的会话ID
// 如果客户端传递了,而且会话缓存也有效,服务端返回相同的值,
// 不然就是不同的值,表示新的会话
// 服务端也可能返回空,表示不希望会话被缓存
SessionID session_id;

// 服务端从客户端支持的列表中挑选出来的 加密套件
CipherSuite cipher_suite;

// 服务端从客户端支持的列表中挑选出来的 压缩算法
CompressionMethod compression_method;
} ServerHello;

变化和ClientHello是一样的。

扩展里面可以包括如下信息:

  • pre_shared_key 扩展:如果使用了PSK,服务端会提供本扩展,选择一个key
  • key_share 扩展: 如果 (EC)DHE 被使用,服务端会提供本扩展(如果没有使用PS,那么 (EC)DHE 和 基于证书的认证总是被使用)

步骤4 [服务端 EncryptedExtensions] 服务端会马上使用 server_handshake_traffic_secret 衍生出的密钥 加密发送 本消息(不需要建立加密上下文但不与各个证书相关联的扩展)。数据格式伪代码为:

struct {
Extension extensions<0..2^16-1>;
} EncryptedExtensions;

步骤5 [服务端 CertificateRequest*] 如果服务端需要对客户端认证,本消息将发送。消息格式如下:

struct {
// 一个字符串,会在客户端的响应中返回
opaque certificate_request_context<0..2^8-1>;
Extension extensions<2..2^16-1>;
} CertificateRequest;

对比旧格式:

struct {
// enum { rsa_sign(1), dss_sign(2), rsa_fixed_dh(3), dss_fixed_dh(4), (255) } ClientCertificateType;
// 按照服务端喜好排序的一系列的证书类型的请求
ClientCertificateType certificate_types<1..2^8-1>;

// opaque DistinguishedName<1..2^16-1>;
// 可接受的证书机构(CA)的名字
DistinguishedName certificate_authorities<3..2^16-1>;
} CertificateRequest;

可以发现,一切都可以扩展化。

相关扩展:post_handshake_auth

这个扩展只能客户端发送,表示愿意执行握手的认证:

  • 如果客户端没有发送,服务端不能发送CertificateRequest
  • 如果发送了,服务器可以在握手完成后的任何时候通过发送CertificateRequest消息来请求客户端认证。 客户端必须用适当的Authentication消息来响应

步骤6 [服务端 Certificate*] 当使用证书作为密钥交换方法进行认证(除了PSK外都是)时,服务端必须发送证书。数据格式为:

enum {
X509(0),
RawPublicKey(2),
(255)
} CertificateType;

struct {
select (certificate_type) {
case RawPublicKey:
/* From RFC 7250 ASN.1_subjectPublicKeyInfo */
opaque ASN1_subjectPublicKeyInfo<1..2^24-1>;

case X509:
opaque cert_data<1..2^24-1>;
};
// 目前,有效的扩展为 OCSP Status 和 SignedCertificateTimestamp(确保 Certificate Transparency 证书透明度的一个机制)
Extension extensions<0..2^16-1>;
} CertificateEntry;

struct {
// 服务端认证为空,客户端认证为CertificateRequest.certificate_request_context
opaque certificate_request_context<0..2^8-1>;
// CertificateEntry结构的序列(链),每个结构包含一个证书和一组扩展。
CertificateEntry certificate_list<0..2^24-1>;
} Certificate;

对比旧格式:

opaque ASN.1Cert<1..2^24-1>;

struct {
ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;

步骤7 [服务端 CertificateVerify*] 用于明确证明端点拥有与其证书相对应的私钥。CertificateVerify消息也为截至当前的握手提供完整性。 服务器在通过证书进行验证时必须发送此消息,客户端在通过证书进行验证时必须发送此消息。

计算方式就是将当前所有的握手消息都使用私钥进行签名,供对端校验。

旧版本并没有这一步。

步骤8 [服务端 Finished] 数据格式如下:

finished_key = HKDF-Expand-Label(BaseKey, "finished", "", Hash.length)

struct {
// verify_data =
// HMAC(
// finished_key,
// Transcript-Hash(Handshake Context, Certificate*, CertificateVerify*)
// )
opaque verify_data[Hash.length];
} Finished;

和旧版本对比:

struct {
// verify_data =
// PRF(master_secret, finished_label, MD5(handshake_messages) +
// SHA-1(handshake_messages)) [0..11];
// finished_label
// For Finished messages sent by the client, the string "client
// finished". For Finished messages sent by the server, the
// string "server finished".
// handshake_messages
// All of the data from all handshake messages up to but not
// including this message. This is only data visible at the
// handshake layer and does not include record layer headers.
opaque verify_data[12];
} Finished;

签名算法和源数据都不一样了。

步骤9 [服务端 Application Data] 服务端可以在此刻发送数据,但是由于握手还没有完成,所以既不能保证对端的身份,也不能保证它的活性(如ClientHello可能已经被重放)

步骤10 [客户端 Certificate*] 同步骤6。

步骤11 [客户端 CertificateVerify*] 同步骤7。

步骤12 [客户端 Finished] 同步骤8。

其他消息: Handshake前的消息

TLS还允许在主握手之后发送其他消息。这些消息使用握手内容类型,并由合适的应用流量密钥进行加密。主要以下三个。

NewSessionTicket消息 在收到客户端Finished消息后,服务端任何时候都可以发送NewSessionTicket消息。 该消息在ticket值和从恢复主秘钥(resumption_master_secret)中导出的秘密PSK之间建立了对应关系。

服务器可以在一个连接上发送多个ticket,可以是紧接着发送,也可以是在特定事件之后。

多tikcet对客户端有多种用途,包括:

  • 打开多个并行HTTP连接。
  • 通过(例如)Happy Eyeballs [RFC8305]或相关技术跨接口和地址族执行连接竞速。

其数据结构为:

struct {
// 表示从发布ticket时间开始的32位无符号整数,以秒为单位,网络序。 服务器不得使用任何大于604800秒(7天)的值。
uint32 ticket_lifetime;

// 一个安全生成的随机32位值,用于掩盖客户端pre_shared_key 扩展中的ticket年龄。
uint32 ticket_age_add;

// 每个ticket对应一个值,使ticket在这个连接上唯一。
opaque ticket_nonce<0..255>;

// 作为PSK标识的ticket值。
opaque ticket<1..2^16-1>;

Extension extensions<0..2^16-2>;
} NewSessionTicket;

ticket关联的PSK的计算方法为:

PSK = HKDF-Expand-Label(resumption_master_secret,
"resumption", ticket_nonce, Hash.length)

原则上可以持续发行新的ticket,它可以无限期地延长最初从initial non-PSK握手(很可能与对端证书绑定)中派生的密钥材料的寿命。

任何ticket必须只用建立原始连接时使用的KDF哈希算法相同的密码套件来恢复。

       Client                                               Server

Initial Handshake:
ClientHello
+ key_share -------->
ServerHello
+ key_share
{EncryptedExtensions}
{CertificateRequest*}
{Certificate*}
{CertificateVerify*}
{Finished}
<-------- [Application Data*]
{Certificate*}
{CertificateVerify*}
{Finished} -------->
<-------- [NewSessionTicket]
[Application Data] <-------> [Application Data]


Subsequent Handshake:
ClientHello
+ key_share*
+ pre_shared_key -------->
ServerHello
+ pre_shared_key
+ key_share*
{EncryptedExtensions}
{Finished}
<-------- [Application Data*]
{Finished} -------->
[Application Data] <-------> [Application Data]

会话恢复和PSK的消息流

CertificateRequest消息

当客户端发送了post_handshake_auth扩展后,服务器可以在握手完成后的任何时候通过发送CertificateRequest消息来请求客户端认证。 客户端必须用适当的Authentication消息来响应。

KeyUpdate消息

KeyUpdate握手消息用于指示发送方正在更新其发送的加密密钥。这个消息可以由任何一端在Finished消息后发送。

在一组给定的密钥下,可以安全加密的明文数量是有密码学限制的。 [AEAD-LIMITS]提供了对这些限制的分析,其假设是底层基元(AES或ChaCha20)没有弱点。在达到这些限制之前,实现应该进行密钥更新。

  • 对于AES-GCM来说,在给定的连接上,最多可加密2^24.5大小的记录(约2400万条),同时保持约2^-57的安全系数,以保证验证加密(AE,Authenticated Encryption)的安全性。
  • 对于ChaCha20/Poly1305,记录序列号将在达到安全限值之前被wrap。

Record Protocol

数据结构和旧版本是一样的(兼容性),但是流程从 TLSPlaintext -压缩-> TLSCompressed(+签名) -加密-> TLSCiphertext 变为:TLSPlaintext -AEAD-> TLSCiphertext

密钥生成算法则有了较大的改动。 明文和密文相关转换关系为:

AEADEncrypted =
AEAD-Encrypt(write_key, nonce, additional_data, plaintext)

plaintext of encrypted_record =
AEAD-Decrypt(peer_write_key, nonce, additional_data, AEADEncrypted)

nonce算法

每条记录都有一个int64的序列化,0开始,i++。将其按网络序编码,左边用0填充到iv_length,在与静态的 client_write_iv或server_write_iv(取决于角色)进行异或 得到。

write_key算法

[sender]_write_key = HKDF-Expand-Label(Secret, "key", "", key_length)
[sender]_write_iv = HKDF-Expand-Label(Secret, "iv", "", iv_length)

Secret 来自于:
+-------------------+---------------------------------------+
| Record Type | Secret |
+-------------------+---------------------------------------+
| 0-RTT Application | client_early_traffic_secret |
| | |
| Handshake | [sender]_handshake_traffic_secret |
| | |
| Application Data | [sender]_application_traffic_secret_N |
+-------------------+---------------------------------------+

对于各种 Secret (traffic_secret),计算逻辑比较复杂,按照如下格式约定:

  • HKDF-Extract从上取Salt参数,从左取IKM参数,其输出在底部,输出的名称在右侧。
  • Derive-Secret的Secret参数用进位箭头表示。例如,Early Secret是生成client_early_traffic_secret的Secret。
  • "0"表示一串Hash.length字节设置为0。

有如下推导图:

          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 = Master 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_master_secret
|
+-----> Derive-Secret(., "res master",
ClientHello...client Finished)
= resumption_master_secret

其中的 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)

其中 Input Keying Material有两个来源:

  • PSK (外部建立的预共享密钥,或从以前连接中的resumption_master_secret值导出)
  • (EC)DHE共享secret

如果给定的secret不可用,则使用Hash.length字节的0值,并不意味着跳过一轮。所以如果没有使用PSK,Early Secret仍将是HKDF-Extract(0,0)。

Alert Protocol

和旧的类似,但是更细化了,细节不展开描述。


参考