TLS1.3的最终版本,在2018年8月发布,它包含着很多不同以往版本的改进,相对于之前版本安全性以及性能具有极大的提高,同时它也具备了更多的扩展和握手模式,那么从实现完整TLS1.3结构的角度去学习TLS,我们应该从哪些方面入手呢?
TLS代表传输层安全性,并且是SSL(安全套接字层)的后继者。TLS提供了Web浏览器和服务器之间的安全通信。连接本身是安全的,因为使用对称密码术对传输的数据进行加密。密钥是为每个连接唯一生成的,并且基于在会话开始时协商的共享机密(也称为TLS握手)。HTTP + TLS = HTTPS
观察图片我们可以看出,TLS1.2整个握手过程需要2个RTT时间,而且每次握手都用到了非对称加密算法签名或者解密的操作,比较耗时和耗 CPU,每次都要传输证书,证书比较大会消耗带宽。
在TLS1.3中没有了SessionID这种会话恢复模式,但是在client Hello中还会存在该字段,主要是为了兼容版本。并且在SessionTicket模式中,添加了Ticket age,指的是会话是存在时间限制的,如果超过了该时间,那么也就不能进行会话恢复。
我们可以看到,使用SessionID恢复会话的时候,需要花费1个RTT的时间,在TLS1.3中一定情况下恢复会话只需要花费0个RTT!
在握手的过程中很多数据都会临时计算,如果我们把这些数据提前计算出来,然后存入扩展当中,这样就可以减少握手的时间为1个RTT,TLS1.3实现的主要思想就是这样的。
更快的访问速度
这是一张TLS1.2的握手过程图片,前面也分析过,它需要2个RTT的时间才能完成整个握手过程,下面我们看一下TLS1.3的握手过程图:
我们会发现,其中存在一些以前版本从来没有出现过的extension,比如:key_share、signature_algorithms等等,这只是一部分,还包含很多扩展,我会一一细说。正因为这些扩展才使得TLS1.3的握手速度大大提高。
注:
当client第一次连接server的时候,它需要向server发送client Hello 消息。
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;
简单介绍一下比较重要的几个字段的含义:
还有cipher_suites、legacy_compression_methods,包含的是Client支持的密码套件和压缩算法,压缩算法TLS1.3也已经不再支持了,这个字段主要还是为了兼容版本,对于每个 ClientHello,该向量必须包含一个设置为 0 的一个字节,它对应着 TLS 之前版本中的 null 压缩方法。
这个扩展表明了 Client 支持的用于密钥交换的命名组。按照优先级从高到低。这个扩展中的 “extension_data” 字段包含一个 “NamedGroupList” 值:
enum {
/* Elliptic Curve Groups (ECDHE) */
secp256r1(0x0017), secp384r1(0x0018), secp521r1(0x0019),
x25519(0x001D), x448(0x001E),
/* Finite Field Groups (DHE) */
ffdhe2048(0x0100), ffdhe3072(0x0101), ffdhe4096(0x0102),
ffdhe6144(0x0103), ffdhe8192(0x0104),
/* Reserved Code Points */
ffdhe_private_use(0x01FC..0x01FF),
ecdhe_private_use(0xFE00..0xFEFF),
(0xFFFF)
} NamedGroup;
struct {
NamedGroup named_group_list<2..2^16-1>;
} NamedGroupList;
这个扩展我觉得是TLS1.3的重大改变,它里面包含了Client对应于supported_groups中参数的公钥集,如果使用了曲线,则会表明所使用的曲线以及对应的公钥。
struct {
NamedGroup group;
opaque key_exchange<1..2^16-1>;
} KeyShareEntry;
在 ClientHello 消息中,“key_share” 扩展中的 “extension_data” 包含 KeyShareClientHello 值:
struct {
KeyShareEntry client_shares<0..2^16-1>;
} KeyShareClientHello;
在golang中该结构的实现:
type KeyShareEntry struct {
group NamedGroup
length uint16
keyExchange []byte
}
type KeyShareClientHello struct {
length uint16
clientShares []KeyShareEntry
}
如果我们只实现椭圆曲线的话,首先需要选择要使用的椭圆曲线,之后再选取随机数生成公钥,将公钥存入keyExchange字段中,也就是说这个扩展已经将Client用于协商会话密钥的参数提前计算出来,并存储了起来,这与之前版本的形式server先选择参数,然后发给Client,然后Client再计算相比较,极大的节省了时间和握手过程中占用的CPU。
Client 可以提供与其提供的 support groups 一样多数量的 KeyShareEntry 的值。每个值都代表了一组密钥交换参数。例如,Client 可能会为多个椭圆曲线或者多个 FFDHE 组提供 shares。每个 KeyShareEntry 中的 key_exchange 值必须独立生成。Client 不能为相同的 group 提供多个 KeyShareEntry 值。Client 不能为,没有出现在 Client 的 “supported_group” 扩展中列出的 group 提供任何 KeyShareEntry 值。Server 会检查这些规则,如果违反了规则,立即发送 “illegal_parameter” alert 消息中止握手。
当选用PSK密钥协商模式时,即使在supported_groups中不存在支持的算法也不会终止握手,这时候,server会向Client发送和serverhello具有相同结构的消息:HelloRetryRequest,它的目的主要是想让Client作出一些改变以使得握手正常进行,在这种情况下,在 HelloRetryRequest 消息中,“key_share” 扩展中的 “extension_data” 字段包含 KeyShareHelloRetryRequest 值。
struct {
NamedGroup selected_group;
} KeyShareHelloRetryRequest;
Client收到此消息之后也会对其进行验证,selected_group是否在NamedGroup中出现了,selected_group 没有在原始的 ClientHello 中的 “key_share” 中出现过。如果上面 的检查都失败了,那么 Client 必须通过 “illegal_parameter” alert 消息来中止握手。否则,在发送新的 ClientHello 时,Client 必须将原始的 “key_share” 扩展替换为仅包含触发 HelloRetryRequest 的 selected_group 字段中指示的组,这个组中只包含新的 KeyShareEntry。
在 ServerHello 消息中,“key_share” 扩展中的 “extension_data” 字段包含 KeyShareServerHello 值。
struct {
KeyShareEntry server_share;
} KeyShareServerHello;
我们再来看一下ECDHE的参数,对于 secp256r1,secp384r1 和 secp521r1,内容是以下结构体的序列化值:
struct {
uint8 legacy_form = 4;
opaque X[coordinate_length];
opaque Y[coordinate_length];
} UncompressedPointRepresentation;
对端还要验证对方的公钥以确保为有效的点:
TLS 1.3 中优化握手:
在本文前面我提到过TLS1.3已经不再使用SessionID进行会话恢复了,现在主要使用的是SessionTicket进行会话恢复,但是又不同于TLS1.2中使用SessionTicket进行会话恢复的过程,也做出了一些改变,或者说是进行了一些更新(PSK)。
会话恢复所花费的时间是1个RTT,这与整个的握手时间是一样的。在TLS1.3中采用的会话恢复机制是PSK它与SessionTicket有些类似,Client通过PSK发送被server加密的会话缓存参数,如果server解密成功就可以直接复用会话,不需要再重新传输证书和协商密钥了。
在使用PSK密钥交换模式时我们首先要了解几个ClientHello的其它扩展:
为了使用PSK,client还需要发送Pre-Shared Key Exchange Modes扩展,它的含义是Client 仅支持使用具有这些模式的 PSK,这就限制了在这个 ClientHello 中提供的 PSK 的使用,也限制了 Server 通过 NewSessionTicket 提供的 PSK 的使用。
如果Client提供了 pre_shared_key扩展,那么就必须提供该扩展
enum { psk_ke(0), psk_dhe_ke(1), (255) } PskKeyExchangeMode;
struct {
PskKeyExchangeMode ke_modes<1..255>;
} PskKeyExchangeModes;
这样的话就可以进行模式选择,并作出相应的改变。未来分配的任何值都必须要能保证传输的协议消息可以明确的标识 Server 选择的模式。目前 Server 选择的值由 ServerHello 中存在的 key_share 表示。
该扩展是用来协商标识的,该标识是与PSK密钥相关联的给定握手所使用的预共享密钥的标识。或者说是New Session Ticket+binders,由于在TLS1.3中,New Session Ticket可以在握手结束后可能多次发送,所以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;
前面已经提到过TLS1.3已经将握手时间优化到了1-RTT,对比之前版本的速度已经快了很多,但是TLS1.3最终极的做法是0-RTT,即握手的时间是0-RTT。
Client发送ClientHello消息,除了在PSK模式中提到的那些扩展外,还应该具有一个early_data扩展,同理server发送serverHello中也应该包括该扩展,并表示其愿意接受early_data,client发送完early_data后,发送End_Of_Early_Data报文表示client自己发送完了early_data。
如果 Server 提供了 early_data 扩展,Client 必须验证 Server 的 selected_identity 是否为 0。如果返回任何其他值,Client 必须使用 “illegal_parameter” alert 消息中止握手。下面我们看一下它的结构:
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;
其中的max_early_data_size字段表明,允许Client发送的最大0-RTT的数据量。
发生错误会导致0-RTT降级到1-RTT。
Post-Handshake Messages在 Server 接收到 Client 的 Finished 消息以后的任何时刻,它都可以发送 NewSessionTicket 消息。此消息在 ticket 值和从恢复主密钥派生出来的 PSK 之间创建了唯一的关联。
struct {
uint32 ticket_lifetime;
uint32 ticket_age_add;
opaque ticket_nonce<0..255>;
opaque ticket<1..2^16-1>;
Extension extensions<0..2^16-2>;
} NewSessionTicket;
主要通过随机数来实现,当协商TLS1.2或更老的版本,为了响应ClientHello在random后8个字节填入特定的随机值,若为TLS1.2则后8个字节的值为:
44 4F 57 4E 47 52 44 01
主要功能的话前面也有提到,对Client标明所支持的TLS版本,对Server标明正在使用的TLS版本,如果协商TLS之前的版本,这个扩展必须带上,若不存在该扩展,server要协商之前的版本,则中止握手,若存在server将禁止使用ClientHello中的legacy_version作为版本协商的值,只能使用supported_versions中的值。
对于server:
struct {
select (Handshake.msg_type) {
case client_hello:
ProtocolVersion versions<2..254>;
case server_hello: /* and HelloRetryRequest */
ProtocolVersion selected_version;
};
} SupportedVersions;