TLS协议分析 与 现代加密通信协议设计(二)
3. 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协议的外层字段,见这个抓包:
4. handshake -- ClientHello,ServerHello,HelloRequest
Hello消息有3个:ClientHello, ServerHello,HellloRequest
逐个说明:
4.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/~daw/papers/ddj-netscape.html,
建议读者检查一下自己的随机数生成器。
- 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 关闭连接。
4.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的子集。
4.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恢复情况下的行为。
这些情况比较琐碎而微妙,具体案例要具体分析。
4.4 Hello Request
服务器任何时候都可以发送 HelloRequest 消息。
HelloRequest的意思是,客户端应该开始协商过程。客户端应该在方便的时候发送ClientHello。服务器不应该在客户端刚创建好连接后,就发送HelloRequest,此时应该让客户端发送ClientHello。
客户端收到这个消息后,可以直接忽略这条消息。
服务器发现客户端没有响应HelloRequest后,可以发送fatal error alert。
消息结构:
struct { } HelloRequest;
HelloRequest不包含在握手消息的hash计算范围内。
5. handshake -- Server Certificate
当服务器确定了CipherSuite后,根据CipherSuite里面的认证算法,如果需要发送证书给客户端,那么就发送 Server Certificate消息给客户端。Server Certificate总是在ServerHello之后立即发送,所以在同一个RTT里。
Server Certificate里面包含了服务器的证书链。
消息结构:
opaque ASN.1Cert<1..2^24-1>;
struct {
ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;
- certificate_list
- 证书列表,发送者的证书必须是第一个,后续的每一个证书都必须是前一个的签署证书。根证书可以省略
证书申请的时候,一般会收到好几个证书,有的需要自己按照这个格式来拼接成证书链。
如果服务器要认证客户端的身份,那么服务器会发送Certificate Request消息,客户端应该也以 这条Server Certificate消息的格式回复。
服务器发送的证书必须:
证书类型必须是 X.509v3。除非明确地协商成别的了(比较少见,rfc里提到了例如 OpenPGP格式)。
服务器证书的公钥,必须和选择的密钥交换算法配套。
密钥交换+认证算法 | 配套的证书中公钥类型 |
---|---|
RSA / RSA_PSK | RSA 公钥;证书中必须允许私钥用于加密 (即如果使用了X509V3规定的key usage扩展, keyEncipherment比特位必须置位) 这种用法没有前向安全性,因此在 TLS 1.3中被废弃了 |
DHE_RSA / ECDHE_RSA | RSA 公钥;证书中必须允许私钥用于签名(即如果使用了X509V3规定的key usage扩展, digitalSignature比特位必须置位),并且允许server key exchange消息将要使用的签名模式(例如 PKCS1_V1.5 ,OAEP等)和hash算法(例如sha1, sha256等) |
DHE_DSS | DSA 公钥; 历史遗留产物,从来没有被大规模用过,安全性差,废弃状态。证书必须允许私钥用于签名,必须允许server key exchange消息中使用的hash算法。 |
DH_DSS / DH_RSA | Diffie-Hellman 公钥; 要求key usage里面的keyAgreement比特位必须置位。 这种用法没有前向安全性,因此在 TLS 1.3中被废弃了 |
ECDH_ECDSA / ECDH_RSA | 能做 ECDH 用途的公钥;公钥必须使用 客户端支持的ec曲线和点格式。这种用法没有前向安全性,因此在 TLS 1.3中被废弃了 |
ECDHE_ECDSA | ECDSA用途的公钥;证书必须运输私钥用作签名,必须允许server key exchange消息里面要用到的hash算法。公钥必须使用客户端支持的ec曲线和点格式。 |
- "server_name" 和 "trusted_ca_keys" 扩展用于引导证书选择。
其中有5种是ECC密钥交换算法:ECDH_ECDSA, ECDHE_ECDSA, ECDH_RSA, ECDHE_RSA, ECDH_anon。
ECC(椭圆曲线)体制相比RSA,由于公钥更小,性能更高,所以在移动互联网环境下越发重要。
以上ECC的5种算法都用ECDH来计算premaster secret, 仅仅是ECDH密钥的生命周期和认证算法不同。
其中只有 ECDHE_ECDSA 和 ECDHE_RSA 是前向安全的。
如果客户端在ClientHello里提供了 "signature_algorithms" 扩展,那么服务器提供的所有证书必须用 "signature_algoritms"中提供的 hash/signature算法对 之一签署。要注意的是,这意味着,一个包含某种签名算法密钥的证书,可能被另一种签名算法签署(例如,一个RSA公钥可能被一个ECDSA公钥签署)。(这在TLS1.2和TLS1.1中是不一样的,TLS1.1要求所有的算法都相同。)注意这也意味着DH_DSS,DH_RSA,ECDH_ECDSA,和ECDH_RSA 密钥交换不限制签署证书的算法。固定DH证书可能使用"signature_algorithms"扩展列表中的 hash/签名算法对 中的某一个签署。名字 DH_DSS, DH_RSA, ECDH_ECDSA, 和 ECDH_RSA 只是历史原因,这几个名字的后半部分中指定的算法,并不会被使用,即DH_DSS中的DSS并不会被使用,DH_RSA中并不会使用RSA做签名,ECDH_ECDSA并不会使用ECDSA算法。。。
如果服务器有多个证书,就必须从中选择一个,一般根据服务器的外网ip地址,SNI中指定的hostname,服务器配置来做选择。如果服务器只有一个证书,那么要确保这一个证书符合这些条件。
要注意的是,存在一些证书使用了TLS目前不支持的 算法组合。例如,使用 RSASSA-PSS签名公钥的证书(即证书的SubjectPublicKeyInfo字段是id-RSASSA-PSS)。由于TLS没有给这些算法定义对应的签名算法,这些证书不能在TLS中使用。
如果一个CipherSuite指定了新的TLS密钥交换算法,也会指定证书格式和要求的密钥编码方法。
6. handshake -- Server Key Exchange
服务器会在 server Certificate 消息之后,立即发送 Server Key Exchange消息。
(如果协商出的CipherSuite不需要做认证,即anonymous negotiation,会在ServerHello之后立即发送Server Key Exchange消息)
只有在server Certificate 消息没有足够的信息,不能让客户端完成premaster的密钥交换时,服务器才发送 server Key Exchange, 主要是对前向安全的几种密钥协商算法,列表如下:
- DHE_DSS
- DHE_RSA
- DH_anon
- ECDHE_ECDSA
- ECDHE_RSA
- ECDH_anon
对下面几种密钥交换方法,发送ServerKeyExchange消息是非法的:
- RSA
- DH_DSS
- DH_RSA
- ECDH_ECDSA
- ECDH_RSA
需要注意的是,ECDH和ECDSA公钥的数据结构是一样的。所以,CA在签署一个证书的时候,可能要使用 X.509 v3 的 keyUsage 和 extendedKeyUsage 扩展来限定ECC公钥的使用方式。
ServerKeyExchange传递足够的信息给客户端,来让客户端交换premaster secret。一般要传递的是:一个 Diffie-Hellman 公钥,或者一个其他算法(例如RSA)的公钥。
在TLS实际部署中,我们一般只使用这4种:ECDHE_RSA, DHE_RSA, ECDHE_ECDSA,RSA
其中RSA密钥协商(也可以叫密钥传输)算法,由于没有前向安全性,在TLS 1.3里面已经被废除了。参见: Confirming Consensus on removing RSA key Transport from TLS 1.3
消息格式:
enum { dhe_dss, dhe_rsa, dh_anon, rsa, dh_dss, dh_rsa,ec_diffie_hellman
} KeyExchangeAlgorithm;
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 */
dh_p
Diffie-Hellman密钥协商计算的大质数模数。
dh_g
Diffie-Hellman 的生成元,
dh_Ys
服务器的Diffie-Hellman公钥 (g^X mod p).
struct {
opaque point <1..2^8-1>;
} ECPoint;
enum { explicit_prime (1), explicit_char2 (2),
named_curve (3), reserved(248..255) } ECCurveType;
struct {
ECCurveType curve_type;
select (curve_type) {
case named_curve:
NamedCurve namedcurve;
};
} ECParameters;
struct {
ECParameters curve_params;
ECPoint public; //ECDH的公钥
} ServerECDHParams;
struct {
select (KeyExchangeAlgorithm) {
case dh_anon:
ServerDHParams params;
case dhe_dss:
case dhe_rsa:
ServerDHParams params;
digitally-signed struct {
opaque client_random[32];
opaque server_random[32];
ServerDHParams params;
} signed_params;
case ec_diffie_hellman:
ServerECDHParams params;
Signature signed_params;
case rsa:
case dh_dss:
case dh_rsa:
struct {} ;
/* message is omitted for rsa, dh_dss, and dh_rsa */
/* may be extended, e.g., for ECDH -- see [TLSECC] */
};
} ServerKeyExchange;
params
服务器的密钥交换参数。
signed_params
对需要认证的(即非anonymous的)密钥交换,对服务器的密钥交换参数的数字签名。
ECParameters 结构比较麻烦,其中ECCurveType是支持3种曲线类型的,可以自行指定椭圆曲线的多项式系数,基点等参数。但是,我们基本不会用到这种功能,因为一般部署都是使用 NamedCurve,即参数已经预先选定,各种密码学库普遍都支持的一组曲线,其中目前用的最广的是 secp256r1 (还被称为 P256,或 prime256v1)
NamedCurve 列表中比较重要的曲线(在TLS1.3中,只保留了这几条曲线。),定义如下:
enum {
...
secp256r1 (23), secp384r1 (24), secp521r1 (25),
reserved (0xFE00..0xFEFF),
(0xFFFF)
} NamedCurve;
ECDHE_RSA 密钥交换算法的 SignatureAlgorithm 是 rsa 。
ECDHE_RSA 密钥交换算法的 SignatureAlgorithm 是 ecdsa。
如果客户端提供了 "signature_algorithms" 扩展, 则签名算法和hash算法必须是列在扩展中的算法。
要注意的是,这个地方可能有不一致,例如客户端可能提供了 DHE_DSS 密钥交换,但是 "signature_algorithms"扩展中没有DSA算法,在这类情况下,为了正确地协商,服务器必须确保满足自己选择的CipherSuite满足 "signature_algorithms" 的限制。这不优雅,但是是为了把对原来的CipherSuite协商的设计的改动减到最小,而做的妥协。
并且,hash和签名算法,必须和服务器的证书里面的公钥兼容。
7. handshake -- Certificate Request
TLS规定了一个可选功能:服务器可以认证客户端的身份,这通过服务器要求客户端发送一个证书实现,服务器应该在ServerKeyExchange之后立即发送CertificateRequest消息。
消息结构:
enum {
rsa_sign(1), dss_sign(2), rsa_fixed_dh(3),dss_fixed_dh(4),
rsa_ephemeral_dh_RESERVED(5),dss_ephemeral_dh_RESERVED(6),
fortezza_dms_RESERVED(20),
ecdsa_sign(64), rsa_fixed_ecdh(65),
ecdsa_fixed_ecdh(66),
(255)
} ClientCertificateType;
opaque DistinguishedName<1..2^16-1>;
struct {
ClientCertificateType certificate_types<1..2^8-1>;
SignatureAndHashAlgorithm
supported_signature_algorithms<2^16-1>;
DistinguishedName certificate_authorities<0..2^16-1>;
} CertificateRequest;
- certificate_types
客户端可以提供的证书类型。
- rsa_sign 包含RSA公钥的证书。
- dss_sign 包含DSA公钥的证书。
- rsa_fixed_dh 包含静态DH公钥的证书。
- dss_fixed_dh 包含静态DH公钥的证书。
- supported_signature_algorithms
- 服务器支持的 hash/signature 算法的列表。
- certificate_authorities
- 服务器可以接受的CA(certificate_authorities)的 distinguished names 的列表 DER编码格式.
这些 distinguished names 可能为root CA或者次级CA指定了想要的 distinguished name ,因此,这个消息可以用来描述已知的root,或者希望的授权空间。
如果 certificate_authorities 列表是空的,那么客户端可以发送任何适当的 ClientCertificateType 类型的证书,如果没有别的限制的话。
certificate_types 和 supported_signature_algorithms 字段的交叉选择很复杂。 certificate_types 这个字段从SSLv3时代就定义了,但是一直都没有详细定义,其大多数功能都被 supported_signature_algorithms 代替了。
有如下规则:
客户端提供的任何证书,必须用一个supported_signature_algorithms 中出现过的 hash/signature 算法对 签名.
客户端提供的末端证书必须提供一个和 certificate_types 兼容的key。 如果这个key是一个签名key,那必须能和 supported_signature_algorithms 中提供的某个 hash/signature 算法对配合使用。
由于历史原因,某些客户端证书类型的名字,包含了证书的签名算法,例如,早期版本的TLS中, rsa_fixed_dh 意思是一个被RSA算法签署,并且包含一个固定DH密钥的证书。在TLS1.2中,这个功能被 supported_signature_algorithms 淘汰,并且证书类型不再限制用来签署证书的算法。例如,如果服务器发送了 dss_fixed_dh 证书类型,和 { {sha1, dsa}, {sha1,rsa} } 签名类型,客户端可以回复一个 包含静态DH密钥,用RSA-sha1签署的证书。
如果协商出来的是匿名CipherSuite,服务器不能要求客户端认证。
8. handshake -- Server Hello Done
在 ServerHello和相关消息已经处理结束后,服务器发送ServerHelloDone。在发送ServerHelloDone后,服务器开始等待客户端的响应。
ServerHelloDone消息表示,服务器已经发送完了密钥协商需要的消息,并且客户端可以开始客户端的密钥协商处理了。
收到ServerHelloDone后,客户端应该确认服务器提供了合法的证书,并且确认服务器的ServerHello消息里面的参数是可以接受的。
消息格式:
struct { } ServerHelloDone;