5. handshake 协议
handshake protocol重要而繁琐。
TLS 1.3对握手做了大修改,下面先讲TLS 1.2,讲完再介绍一下分析TLS 1.3.
5.1.handshake的总体流程
handshake protocol用于产生给record protocol使用的SecurityParameters。
在handshake中:
- 客户端和服务器端协商TLS协议版本号和一个CipherSuite,
- 认证对端的身份(可选,一般如https是客户端认证服务器端的身份),
- 并且使用密钥协商算法生成共享的master secret。
步骤如下:
- 交换Hello消息,协商出算法,交换random值,检查session resumption.
- 交换必要的密码学参数,来允许client和server协商出premaster secret。
- 交换证书和密码学参数,让client和server做认证,证明自己的身份。
- 从premaster secret和交换的random值 ,生成出master secret。
- 把SecerityParameters提供被record层。
- 允许client和server确认对端得出了相同的SecurityParameters,并且握手过程的数据没有被攻击者篡改。
Handshake的结果是在双方建立相同的Session,Session 包含下列字段:
- session identifier
session id,用来唯一标识一个session,在session 恢复的时候,也要用到 - peer certificate
对端的 X509v3 格式证书. 如果不需要认证对端的身份,就为空。 - compression method
压缩算法,一般被禁用 - cipher spec
CipherSuite,如上文介绍,包含: 用于生成key的pseudorandom function (PRF) , 块加密算法例如AES, MAC算法 (例如 HMAC-SHA256). 还包括一个 mac_length字段,在后文的we握手协议介绍 - master secret
48字节的,client和server共享密钥。 - is resumable
一个标志位,用来标识当前session是否能被恢复。
以上字段,随后被用于生成 record层的SecurityParameters,多个连接可以通过握手协议的session恢复功能来复用同一个session。
握手协议使用 非对称加密/密钥协商/数字签名 3类算法,
因此要求读者对这3类算法概念清晰,能准确区分。
在此澄清一下,:
非对称的算法分为3类:
,
- 非对称加密,有:RSAES-PKCS1-v1_5,RSAES-OAEP ,Rabin-Williams-OAEP, Rabin-Williams-PKCS1-v1_5等
- 非对称密钥协商,有:DH,DHE,ECDH,ECDHE 等
- 非对称数字签名:RSASSA-PKCS1-v1_5,RSASSA-PSS,ECDSA,DSA,ED25519 等
另外,非对称加密算法,可以当作密钥协商算法来用,所以 RSAES-PKCS1-v1_5,RSAES-OAEP 也可以当作密钥协商算法来用。
插播一段 RSA:
RSA的实际工程应用,要遵循PKCS#1 标准,见 https://www.ietf.org/rfc/rfc3447
其中的 RSAES-PKCS1-v1_5 和 RSASSA-PKCS1-v1_5 是使用RSA算法的两种不同scheme(体制)。
RSAES表示 RSA Encryption schemes,即非对称加密,
RSAES有:RSAES-OAEP,RSAES-PKCS1-v1_5两种,其中RSAES-OAEP更新更安全
RSASSA表示 Signature schemes with appendix,即appendix模式(appendix和recovery的区别请参看密码学教材)的非对称数字签名算法。
RSASSA有: RSASSA-PSS, RSASSA-PKCS1-v1_5 两种, 其中RSASSA-PSS更新更安全
RSA还有一个缺陷,就是很容易被时间侧通道攻击,所以现在的RSA实现都要加 blinding ,后文有介绍。
可以看到,RSA是一种很特殊的算法,既可以当非对称加密算法使用,又可以当非对称数字签名使用。这一点很有迷惑性,其实很多用RSA的人都分不清自己用的是RSA的哪种模式。
相比之下,ECC(椭圆曲线)这一块的算法就很清晰,ECDSA只能用作数字签名,ECDH只能用作密钥交换。
分清楚 RSAES-PKCS1-v1_5 和 RSASSA-PKCS1-v1_5 有什么用涅?
PKCS#1规范解释:
A generally good cryptographic practice is to employ a given RSA
key pair in only one scheme. This avoids the risk that
vulnerability in one scheme may compromise the security of the
other, and may be essential to maintain provable security.
FIPS PUB 186-3 美国标准规定:
An RSA key pair used for digital signatures shall only be used for one
digital signature scheme (e.g., ANS X9.31, RSASSA-PKCS1 v1.5 or
RSASSA-PSS; see Sections 5.4 and 5.5). In addition, an RSA digital
signature key pair shall not be used for other purposes (e.g., key
establishment).
一对密钥只做一个用途,要么用作非对称加解密,要么用作签名验证,别混着用!
一对密钥只做一个用途,要么用作非对称加解密,要么用作签名验证,别混着用!
一对密钥只做一个用途,要么用作非对称加解密,要么用作签名验证,别混着用!
这个要求,决定了一个协议的 PFS(前向安全性),在斯诺登曝光NSA的“今日捕获,明日破解”政策后,越发重要。
https://news.ycombinator.com/...
http://news.netcraft.com/arch...
https://lwn.net/Articles/572926/
https://www.eff.org/deeplinks...
http://www.wired.com/2013/10/...
PFS反映到密钥协商过程中,就是:
- 不要使用RSA做密钥协商,一定只用RSA做数字签名。
- 不要把ECDH的公钥固定内置在客户端做密钥协商
后文可以看到这一原则在 TLS 1.3, QUIC,Apple的iMessage等协议中一再贯彻。
非对称RSA/ECC这个话题比较大了,后面有空再写文章吧,读者可以先看一下参考资料,里面有清晰的介绍。
插播结束,继续TLS。
由于设计的时候,就要考虑兼容性,而且实际历史悠久,所以TLS协议90年代曾经使用的一些算法,现在已经被破解了,例如有的被发现漏洞(rc4),有的密钥长度过短(例如曾经美帝有出口限制,限制RSA 在512比特以下,对称加密密钥限制40比特以下,后来2005年限制被取消),但是考虑到兼容,现在的TLS实现中,还是包含了这种已经被破解的老算法的代码。这样,如果攻击者可以干扰握手过程,诱使client和server使用这种已经被破解的算法,就会威胁TLS协议的安全,这被称为“降级攻击”。
为了在握手协议解决降级攻击的问题,TLS协议规定:client发送ClientHello消息,server必须回复ServerHello消息,否则就是fatal error,当成连接失败处理。ClientHello和ServerHello消息用于建立client和server之间的安全增强能力,ClientHello和ServerHello消息建立如下属性:
- Protocol Version
- Session ID
- Cipher Suite
- Compression Method.
另外,产生并交换两个random值 ClientHello.random 和 ServerHello.random
密钥协商使用四条: server的Certificate,ServerKeyExchange,client的Certificate,ClientKeyExchange 。TLS规定以后如果要新增密钥协商方法,可以订制这4条消息的数据格式,并且指定这4条消息的使用方法。密钥协商得出的共享密钥必须足够长,当前定义的密钥协商算法生成的密钥长度必须大于46字节。
在hello消息之后,server会把自己的证书在一条Certificate消息里面发给客户端(如果需要做服务器端认证的话,例如https)。 并且,如果需要的话,server会发送一条ServerKeyExchange消息,(例如如果服务器的证书只用做签名,不用做密钥交换,或者服务器没有证书)。client对server的认证完成后,server可以要求client发送client的证书,如果这是协商出来的CipherSuite允许的。下一步,server会发送ServerHelloDone消息,表示握手的hello消息部分已经结束。然后server会等待一个client的响应。如果server已经发过了CertificateRequest消息,client必须发送Certificate消息。然后发送ClientKeyExchange消息,并且这条消息的内容取决于ClientHello和ServerHello消息协商的算法。如果client发送了有签名能力的证书,就显式发送一个经过数字签名的CertificateVerify消息,来证明自己拥有证书私钥。
然后,client发送一个ChangeCipherSpec消息,并且client拷贝待定的Cipher Spec到当前的Cipher Spec。然后client立即用新算法+新key+新密钥 发送Finished消息。收到后,server发送自己的ChangeCipherSpec消息,作为响应,并且拷贝待定的Cipher Spec到当前的Cipher Spec。此时,握手就完成了,client和server可以开始交换应用层数据(如下图所示)。应用层数据不得在握手完成前发送。
引用一个来自网络的图片:
Client Server
ClientHello -------->
ServerHello
Certificate*
ServerKeyExchange*
CertificateRequest*
<-------- ServerHelloDone
Certificate*
ClientKeyExchange
CertificateVerify*
[ChangeCipherSpec]
Finished -------->
[ChangeCipherSpec]
<-------- Finished
Application Data <-------> Application Data
Figure 1. Message flow for a full handshake
* 表示可选的消息,或者根据上下文在某些情况下会发送的消息。Indicates optional or situation-dependent messages that are not
always sent.
注:为了帮助解决管道阻塞的问题,ChangeCipherSpec是一个独立的TLS protocol content type,并不是一个握手消息。
TLS的完整握手过程,要进行RSA/ECDH/ECDSA等非对称计算,非对称计算是很慢的。关于非对称的性能:
例如在2015年的服务器cpu: Intel(R) Xeon(R) CPU E3-1230 V2 @ 3.30GHz 上,
使用如下命令测试:
openssl speed rsa2048
openssl speed ecdsap256
openssl speed ecdhp256
openssl speed aes-128-cbc
openssl speed -evp aes-128-cbc
结果如下表:
算法 | 性能 | 性能 |
---|---|---|
RSA-2048 | 私钥运算 723.7 次/秒 | 公钥运算 23505.8 次/秒 |
256 bit ecdsa (nistp256) | 签名 8628.4 次/秒 | 验证 2217.0 次/秒 |
256 bit ecdh (nistp256) | ECDH协商 2807.8 次/秒 | |
aes-128-cbc | 加密 121531.39 K/秒 | |
aes-128-cbc 使用aesni硬件加速 | 加密 683682.13 K/秒 |
注:非对称的单位是 次/秒,这是由于非对称一般只用于处理一个block,
对称的单位是 K/秒,因为对称一般用于处理大量数据流,所以单位和流量一样。
可以给非对称的 次/秒 乘以 block size ,就可以和对称做比较了。例如rsa-2048,723.7*2048/8/1024=185.2672 K/秒 ,
故 RSA-2048 私钥运算性能 是aes-128-cbc 的 1.5/1000。是aesni的 2.6/10000。
如上,性能数据惨不忍睹, 简直不能忍!!!
有鉴于此,TLS从设计之初,就采用了万能手段—加cache,有2种cache手段:session id,和session ticket。把握手的结果直接cache起来,绕过握手运算。
当client和server决定恢复一个之前的session,或复用一个已有的session时(可以不用协商一个新的SecurityParameters),消息流程如下:
客户端使用要被恢复的session,发送一个ClientHello,把Session ID包含在其中。server在自己的session cache中,查找客户端发来的Session ID,如果找到,sever把找到的session 状态恢复到当前连接,然后发送一个ServerHello,在ServerHello中把Session ID带回去。然后,client和server都必须ChangeCipherSpec消息,并紧跟着发送Finished消息。这几步完成后,client和server 开始交换应用层数据(如下图所示)。如果server在session cache中没有找到Session ID,那server就生成一个新的session ID在ServerHello里给客户端,并且client和server进行完整的握手。
流程图如下:
Client Server
ClientHello -------->
ServerHello
[ChangeCipherSpec]
<-------- Finished
[ChangeCipherSpec]
Finished -------->
Application Data <-------> Application Data
Figure 2. Message flow for an abbreviated handshake
5.2. handshake 协议外层结构
从消息格式来看,TLS Handshake Protocol 在 TLS Record Protocol 的上层. 这个协议用于协商一个session的安全参数。 Handshake 消息(例如ClientHello,ServerHello等) 被包装进 TLSPlaintext结构里面,传入TLS record层,根据当前session 状态做处理,然后传输。
如下:
enum {
hello_request(0), client_hello(1), server_hello(2),
certificate(11), server_key_exchange (12),
certificate_request(13), server_hello_done(14),
certificate_verify(15), client_key_exchange(16),
finished(20), (255)
} HandshakeType;struct {
HandshakeType msg_type; /* handshake type */
uint24 length; /* bytes in message */
select (HandshakeType) { case hello_request: HelloRequest; case client_hello: ClientHello; case server_hello: ServerHello;
case certificate: Certificate;
case server_key_exchange: ServerKeyExchange;
case certificate_request: CertificateRequest;
case server_hello_done: ServerHelloDone;
case certificate_verify: CertificateVerify;
case client_key_exchange: ClientKeyExchange;
case finished: Finished;
case session_ticket: NewSessionTicket; /* NEW */
} body;
} Handshake;
TLS协议规定,handshake 协议的消息必须按照规定的顺序发,收到不按顺序来的消息,当成fatal error处理。也就是说,TLS协议可以当成状态机来建模编码。
下面按照消息发送必须遵循的顺序,逐个解释每一条握手消息。
handshake协议的外层字段,见这个抓包:
5.3. handshake — ClientHello,ServerHello,HelloRequest
Hello消息有3个:ClientHello, ServerHello,HellloRequest
逐个说明:
5.3.1 Client Hello
当客户端第一次连接到服务器时,第一条message必须发送ClientHello。
另外,rfc里规定,如果客户端和服务器支持重协商,在客户端收到服务器发来的HelloRequest后,也可以回一条ClientHello,在一条已经建立的连接上开始重协商。(重协商是个很少用到的特性。)
消息结构:
struct {
uint32 gmt_unix_time;
opaque random_bytes[28];
} Random;
opaque SessionID<0..32>;
uint8 CipherSuite[2];enum { null(0), (255) } CompressionMethod;struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
CipherSuite cipher_suites<2..2^16-2>;
CompressionMethod compression_methods<1..2^8-1>;
select (extensions_present) { case false: struct {}; case true:
Extension extensions<0..2^16-1>;
};
} ClientHello;
Random 其中:
gmt_unix_time 是 unix epoch时间戳。
random_bytes 是 28字节的,用密码学安全随机数生成器 生成出来的随机数。
密码学安全的随机数生成,这是个很大的话题,也是一个大陷阱,目前最好的做法就是用 /dev/urandom,或者openssl库的 RAND_bytes()
历史上,恰好就在SSL的random_bytes这个字段,NetScape浏览器早期版本被爆出过随机数生成器漏洞。
被爆菊的随机数生成器使用 pid + 时间戳 来初始化一个seed,并用MD5(seed)得出结果。
见 http://www.cs.berkeley.edu/~d...,
建议读者检查一下自己的随机数生成器。
client_version
: 客户端支持的最高版本号。
random
: 客户端生成的random。
ClientHello.session_id 唯一标识一个session,用来做session cache。如果为空,表示不做复用,要求服务器生成新的session。
session_id的来源有:
- 之前的协商好的连接的session_id
- 当前连接的session_id
- 当前也在使用中的另一条连接的session_id
其中第三种允许不做重新握手,就同时建立多条独立的安全连接。这些独立的连接可能顺序创建,也可以同时创建。一个SessionID当握手协商的Finished消息完成后,就合法可用了。存活直到太旧被移除,或者session 关联的某个连接发生fatal error。SessionID的内容由服务器端生成。
注:由于SessionID的传输是不加密,不做MAC保护的,服务器不允许把私密信息发在里面,不能允许伪造的SessionID在服务器造成安全问题。(握手过程中的数据,整体是受Finished消息的保护的)
ClientHello.cipher_suites字段,包含了客户端支持的CipherSuite的列表,按照客户端希望的优先级排序,每个CipherSuite有2个字节,每个CipherSuite由:一个密钥交换算法,一个大量数据加密算法(需要制定key length参数),一个MAC算法,一个PRF 构成。服务器会从客户端发过来的列表中选择一个;如果没有可以接受的选择,就返回一个 handshake failure 的 alert,并关闭连接。如果列表包含服务器不认识,不支持,或者禁用的CipherSuite,服务器必须忽略。
如果SessionID不为空,则cipher_suites里面起码要包含客户端cache的session里面的那个CipherSuite
compression_methods,类似地,ClientHello里面包含压缩算法的列表,按照客户端优先级排序。当然,如前介绍,服务器一般禁用TLS的压缩。
compression_methods 后面可以跟一组扩展(extensions), extensions都是可选的,比较有用的扩展如: SNI, session ticket,ALPN,OCSP 等,后文介绍。
客户端发送了ClientHello后,服务器端必须回复ServerHello消息,回复其他消息都会导致 fatal error 关闭连接。
5.3.2 Server Hello
当收到客户端发来的ClientHello后,正常处理完后,服务器必须回复ServerHello。
消息结构:
struct {
ProtocolVersion server_version;
Random random;
SessionID session_id;
CipherSuite cipher_suite;
CompressionMethod compression_method;
select (extensions_present) { case false: struct {}; case true:
Extension extensions<0..2^16-1>;
};
} ServerHello;
server_version
: 服务器选择 ClientHello.client_version 和 服务器支持的版本号 中的最小的。
random
: 服务器生成的random,必须确保和客户端生成的random没有关联。
session_id
: 服务器为本连接分配的SessionID。如果ClientHello.session_id不为空,服务器会在自己的本地做查找。
- 如果找到了匹配,并且服务器决定复用找到的session建立连接,服务器应该把ClientHello.session_id同样的 session id填入ServerHello.session_id,这表示恢复了一个session,并且双方会立即发送Finished消息。
- 否则,回复一个和ClientHello.random_id不同的Serverhello.session_id,来标识新session。服务器可以回复一个空的session_id,来告诉客户端这个session不要cache,不能恢复。
如果一个session 被恢复了,那必须恢复成之前协商的session里面的 CipherSuite。要注意的是,并不要求服务器一定要恢复session, 服务器可以不做恢复。
在实践中,session cache在服务器端要求key-value形式的存储,如果tls服务器不止一台的话,就有一个存储怎么共享的问题,要么存储同步到所有TLS服务器的内存里,要么专门搞服务来支持存储,并使用rpc访问,
无论如何,都是很麻烦的事情,相比之下,后文要介绍的session ticket就简单多了,所以一般优先使用session ticket。
cipher_suite
: 服务器选定的一个CipherSuite。如果是恢复的session,那就是session里的CipherSuite。
compression_method
: 跟上面类似。
extensions
: 扩展列表。要注意的是,ServerHello.extensions 必须是 ClientHello.extensions的子集。
5.3.3 Hello Extensions
The extension 的格式是:
struct {
ExtensionType extension_type;
opaque extension_data<0..2^16-1>;
} Extension;enum {
signature_algorithms(13), (65535)
} ExtensionType;
其中:
- “extension_type” 标识是哪一个扩展类型。
- “extension_data” 一坨二进制的buffer,扩展的数据体,各个扩展自己做解析。
extension_type 只能出现一次,ExtensionType之间不指定顺序。
extensions 可能在新连接创建时被发送,也可能在要求session恢复的时候被发送。所以各个extension都需要规定自己再完整握手和session恢复情况下的行为。
这些情况比较琐碎而微妙,具体案例要具体分析。
5.3.4 Hello Request
服务器任何时候都可以发送 HelloRequest 消息。
HelloRequest的意思是,客户端应该开始协商过程。客户端应该在方便的时候发送ClientHello。服务器不应该在客户端刚创建好连接后,就发送HelloRequest,此时应该让客户端发送ClientHello。
客户端收到这个消息后,可以直接忽略这条消息。
服务器发现客户端没有响应HelloRequest后,可以发送fatal error alert。
消息结构:
struct { } HelloRequest;
HelloRequest不包含在握手消息的hash计算范围内。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。