TLS协议分析 与 现代加密通信协议设计(三)

发表于2015-09-10
评论0 1.8k浏览

9. handshake -- Client Certificate

ClientCertificate消息是客户端收到ServerHelloDone后,可以发送的第一条消息。仅当服务器要求了一个证书的情况下,客户端才发送ClientCertificate消息,如果没有可用的合适证书,客户端必须发送一条不包含任何证书的ClientCertificate消息(即 certificate_list 结构长度为0)。

如果客户端没有发送任何证书,服务器自行决定,可以放弃要求客户端认证,继续握手;或者发送一条 fatal handshake_failure的alert消息,断开连接。并且,如果证书链的某些方面是不能接受的(比如证书没有被可信任的CA签署),服务器可以自行决定,是继续握手(放弃要求客户端认证),或者发送一条fatal的alert。

客户端证书使用和ServerCertificate相同的结构发送。

ClientCertificate把客户端的证书链发送给服务器。服务器会使用证书链来验证CertificateVerify 消息(如果使用基于签名的客户端认证),或者来计算premaster secret(对于非短暂的 DH)。证书必须和协商出来的CipherSuite的密钥交换算法配套,并和任何协商的扩展配套。

尤其是:

  • 证书必须是X.509v3 类型的。
  • 客户端的末级证书的公钥必须和CertificateRequest里列出的证书类型兼容。
客户端证书类型证书公钥类型
rsa_signRSA公钥;证书必须允许公钥用于certificateVerify消息中的数字签名和hash算法
dss_signDSA 公钥;证书必须允许密钥使用CertificateVerify中的hahs函数做签名;
ecdsa_sign可以用作 ECDSA 的公钥;证书必须允许 公钥用 CertificateVerify中的hash函数做签名;公钥必须使用服务器支持的曲线,和点格式;
rsa_fixed_dh / dss_fixed_dhDiffie-Hellman 公钥; 必须使用和服务器key相同的参数。
rsa_fixed_ecdh / ecdsa_fixed_ecdh可以用作 ECDH 的公钥。必须和服务器的公钥使用同样的曲线,同样的点格式
  • 如果 certificate_authorities 列表不是空的,客户端证书链中的某一个证书必须是CA中的某一个签署的。
  • 证书必须使用 服务器可以接受的 hash/signature 算法对。

类似于Server Certificate,有一些证书目前无法在TLS中使用。

10. handshake -- Client Key Exchange

客户端必须在客户端的Certificate消息之后,立即发送ClientKeyExchange消息。 
或者必须在ServerHelloDone后立即发送ClientKeyExchange消息。

ClientKeyExchange消息中,会设置premaster secret,通过发送 RSA公钥加密premaster secret的密文,或者发送允许双方得出相同的premaster secret的Diffie-Hellman参数。

当客户端使用短暂的 Diffie-Hellman 密钥对时,ClientKeyExchange包含客户端的 Diffie-Hellman 公钥。如果客户端发送一个包含静态 Diffie-Hellman 指数的证书(比如,在使用固定DH的客户端认证),那么这条消息必须被发送,并且必须为空。

消息结构: 
消息的选择取决于选择的密钥交换算法。

  1.  struct {
  2. select (KeyExchangeAlgorithm) {
  3.  case rsa:
  4.  EncryptedPreMasterSecret;
  5.  case dhe_dss:
  6.  case dhe_rsa:
  7.  case dh_dss:
  8.  case dh_rsa:
  9.  case dh_anon:
  10.  ClientDiffieHellmanPublic;
  11.  case ec_diffie_hellman:
  12.  ClientECDiffieHellmanPublic;
  13.  } exchange_keys;
  14.  } ClientKeyExchange;

(1). RSA 加密的 Premaster Secret 消息

如果用RSA做密钥协商和认证,客户端生成 48字节的 premaster secret,使用服务器证书里面的公钥加密,然后把密文EncryptedPreMasterSecret发送给服务器,结构定义如下:

  1.  struct {
  2.  ProtocolVersion client_version;
  3. opaque random[46];
  4.  } PreMasterSecret;
  5. client_version
  6.  客户端支持的最新协议版本号,这个字段用来检测中间人版本回退攻击。T
  7. random
  8.  46 字节的,安全生成的随机值。
  9.  struct {
  10.  public-key-encrypted PreMasterSecret pre_master_secret;
  11.  } EncryptedPreMasterSecret;
  12. pre_master_secret
  13.  这个随机值由客户端生成,用于生成master secret

注:PreMasterSecret里面的 client_version 是 ClientHello.client_version,而不是协商的到的版本号,这个特性用来阻止版本回退攻击。不幸的是,有些不正确的老的代码使用了协商得到的版本号,导致检查client_version字段的时候,和正确的实现无法互通。

客户端实现必须在PreMasterSecret中发送正确的版本号。如果 ClientHello.client_version 的版本号是 TLS 1.1 或者更高,服务器实现必须如下检查版本号。如果版本号是 TLS 1.0 或者更早,服务器必须检查版本号,但是可以通过配置项关闭检查。

要注意的是,如果版本号检查失败了,PreMasterSecret 应该像下面描述的那样填充成随机数。

TLS中的RSA使用的是 PKCS1-V1.5 填充( PKCS1-V1.5也是openssl库RSA的默认填充方式)。Bleichenbacher 在1998年发表了一种针对 PKCS1-V1.5 的选择密文攻击, Klima在2003年发现 PKCS1-V1.5 中 PreMasterSecret 版本号检查的一个侧通道攻击。只要TLS 服务器暴露一条特定的消息是否符合PKCS1-V1.5格式,或暴露PreMasterSecret解密后结构是否合法,或版本号是否合法,就可以用上面2种方法攻击。

Klima 还提出了完全避免这类攻击的方法:对格式不正确的消息,版本号不符的情况,要做出和完全正确的RSA块一样的响应,要让客户端区分不出这3种情况。 
具体地说,要如下:

  1. 生成 46 字节的密码学安全随机值 R
  2. 解密消息,获得明文 M
  3. 如果 PKCS#1 填充不正确,或者 PreMasterSecret 消息的长度不是48字节,则 
    pre_master_secret = ClientHello.client_version || R 
    或者如果 ClientHello.client_version <= TLS 1.0,并且明确禁止了版本号检查,则 
    pre_master_secret = ClientHello.client_version || M[2..47]

注意:明确地用 ClientHello.client_version 构造 pre_master_secret 时,当客户端在原来的 pre_master_secret 中发送了错误的 客户端版本值时,会产生一个不合法的 master_secret 。

另一种解决问题的方法是,把版本号不符,当成 PKCS-1 格式错误来对待,并且完全随机填充 premaster secret。

  1. 生成 48 字节的密码学安全随机值 R
  2. 解密 PreMasterSecret 恢复出明文 M
  3. 如果 PKCS#1 填充不正确,或者消息的长度不是48字节,则 
    pre_master_secret = R 
    或者如果 ClientHello.client_version <= TLS 1.0,并且 明确禁止了版本号检查,则 
    pre_master_secret = M 
    或者如果 M[0..1] != CleintHello.client_version 
    pre_master_secret = R 
    或者 
    pre_master_secret = M

尽管实践中,还没有发现针对这种结构的攻击,Klima 在论文中描述了几种理论上的攻击方式,因此推荐上述的第一种结构。

在任何情况下,一个 TLS 服务器绝对不能在:1. 处理 RSA 加密的 premaster 消息失败, 2.或者版本号检查失败 时产生alert消息。当遇到这两种情况时,服务器必须用随机生成的 premaster 值继续握手。服务器可以把造成失败的真实原因log下来,用于调查问题,但是必须小心确保不能把这种信息泄漏给攻击者(比如通过时间侧通道,log文件,或者其它通道等泄漏)。

RSAES-OAEP 加密体制,更能抵抗 Bleichenbacher 发表的攻击,然而,为了和早期的TLS版本最大程度保持兼容,TLS 仍然规定使用 RSAES-PKCS1-v1_5 体制。只要遵守了上面列出的建议,目前还没有 Bleichenbacher 的变化形式能攻破 TLS 。

实现的时候要注意:公钥加密的数据用 字节数组 <0..2^16-1> 的形式表示。因此,ClientKeyExchange中的 RSA加密的PreMasterSecret 前面有2个字节用来表示长度。这2个字节在使用RSA做密钥协商时,是冗余的,因为此时 EncryptedPreMasterSecret 是 ClientKeyExchange 中的唯一字段,因此可以无歧义地得出 EncryptedPreMasterSecret 的长度。因此更早的 SSLv3 规范没有明确规定 public-key-encrypted 数据的编码格式,因此有一些SSLv3的实现没有包含 长度字段,这些实现直接把 RSA 加密的数据放入了 ClientKeyExchange消息里面。 
TLS规范要求 EncryptedPreMasterSecret 字段包含长度字段。因此得出的结果会和一些 SSLv3 的实现不兼容。实现者从 SSLv3 升级到 TLS 时,必须修改自己的实现,以接受并且生成带长度的格式。如果一个实现要同时兼容 SSLv3 和 TLS,那就应该根据协议版本确定自己的行为。

注意:根据 Boneh 等在2003年USENIX Security Symposium上发表的论文 "Remote timing attacks are practical",针对 TLS RSA密钥交换的远程时间侧通道攻击,是实际可行的,起码当客户端和服务器在同一个LAN里时是可行的。因此,使用静态 RSA 密钥的实现,必须使用 RSA blinding,或者Boneh论文中提到的,其他抵抗时间侧通道攻击的技术。

openssl中的RSA blinding,参见:http://linux.die.net/man/3/rsa_blinding_on

(2). 客户端 Diffie-Hellman 公钥

这条消息把客户端的 Diffie-Hellman 公钥 ( Yc ) 发送给服务器。

Yc的编码方式由 PublicValueEncoding 决定。

消息的结构:

  1.  enum { implicit, explicit } PublicValueEncoding;
  2. implicit
  3.  如果客户端已经发送了一个包含合适的 DH 公钥的证书(即 fixed_dh 客户端认证方式),那么Yc已经隐式包含了,不需要再发送。这种情况下,ClientKeyExchange消息必须发送,并且必须是空的。
  4.  explicit
  5.  表示Yc需要发送。
  6.  struct {
  7. select (PublicValueEncoding) {
  8.  case implicit: struct { };
  9.  case explicit: opaque dh_Yc<1..2^16-1>;
  10.  } dh_public;
  11.  } ClientDiffieHellmanPublic;
  12. dh_Yc
  13.  客户端的 Diffie-Hellman 公钥 Yc.

(3). 客户端 EC Diffie-Hellman 公钥

  1.  struct {
  2. select (PublicValueEncoding) {
  3.  case implicit: struct { };
  4.  case explicit: ECPoint ecdh_Yc;
  5.  } ecdh_public;
  6.  } ClientECDiffieHellmanPublic;

Diffie-Hellman 推广到椭圆曲线群上,就是 EC Diffie-Hellman ,简称 ECDH,其它的计算,和一般的 DH 计算类似。

ECDH 是目前最重要的密钥协商算法 http://vincent.bernat.im/en/blog/2011-ssl-perfect-forward-secrecy.html

11. handshake -- Cerificate Verify

当需要做客户端认证时,客户端发送CertificateVerify消息,来证明自己确实拥有客户端证书的私钥。这条消息仅仅在客户端证书有签名能力的情况下发送(就是说,除了含有固定 Diffie-Hellman 参数的证书以外的证书)。CertificateVerify必须紧跟在ClientKeyExchange之后发送。

消息结构: 
Structure of this message:

  1.  struct {
  2. digitally-signed struct {
  3. opaque handshake_messages[handshake_messages_length];
  4.  }
  5.  } CertificateVerify;

此处, handshake_messages 表示所有发送或者接收的握手消息,从client hello开始,一直到CertificateVerify之前的所有消息,包括handshake消息的type和length字段,这是之前所有握手结构体的拼接。要注意,这要求双方在握手过程中,都得缓存所有消息,或者在握手过程中,用每一种可能的hash算法计算到CeritificateVerify为止的hash值。

signature中用的hash和签名算法必须是 CertificateRequest 的 supported_signature_algorithms 中的某一种。另外,hash和签名算法必须和客户端的证书的算法兼容。 
RSA公钥可能被用于任何允许的hash函数,只要遵循证书中的限制。

12. handshake -- Finished

在 ChangeCipherSpec 消息之后,应该立即发送 Finished 消息,来确认密钥交换和认证过程已经成功了。ChangeCipherSpec 必须在其它握手消息和 Finished 消息之间。

Finished 消息是第一条用刚刚协商出来的参数保护的消息。接收方必须确认Finished消息的内容是正确的。一旦某一方发送了,并且确认了对端发来的Finished消息,就可以开始在连接上发送和接收应用数据了。

消息结构:

  1.  struct {
  2. opaque verify_data[verify_data_length];
  3.  } Finished;
  4. verify_data
  5. PRF(master_secret, finished_label,Hash(handshake_messages))
  6.  [0..verify_data_length-1];
  7. finished_label
  8.  对客户端发的Finished消息来说,固定是字符串 "client finished".
  9.  对服务器发的Finished消息来说,固定是字符串 "server finished".

Hash表示握手消息的hash。hash函数是前文 PRF 的hash 函数。或者 CipherSuite 规定的用于 Finished 计算的hash函数。

在TLS的之前版本中,verify_data 总是 12 字节。在TLS 1.2中,这取决于CipherSuite。如果CipherSuite没有显式规定 verify_data_length ,就当成12字节处理。将来的CipherSuite可能会规定别的长度,但是不能小于12字节。

Finished 消息必须跟在 ChangeCipherSpec 消息之后,如果顺序错乱,就是 fatal error.

handshake_message 的内容包含从 ClientHello开始,直到 本条Finished之前的所有消息,只包含handshake层的消息体,不包含record层的几个消息头字段。包括CertificateVerify 消息。同时,对客户端和服务器来说,handshake_message 的内容不同, 后发送者必须包含前发送者的 Finished 消息。

注意:ChangeCipherSpec 消息,alert,和其它的record 类型不是握手消息,不包含在 hash计算中。同时,HelloRequest 消息也不算在内。

13. handshake -- NewSessionTicket

SessionTicket 定义在 RFC5077 标准里面,2008年发布。

SessionTicket是一种不需要服务器端状态的,恢复TLS session的方式。 
SessionTicket可以用于任何CipherSuite。 TLS 1.0, TLS 1.1, TLS 1.2 都适用。

在下面这些场景下,尤其有用:

用户量巨大,session id的方式耗费服务器内存过多 
服务器希望长时间缓存session 
服务器有多台,不希望服务器间有共享状态 
服务器内存不足 
客户端在 ClientHello中设置一个 SessionTicket 扩展来标识自己支持 SessionTicket。如果客户端本地没有存之前收到的ticket,就把这个扩展设为空。

如果服务器希望使用 SessionTicket 机制,服务器把本地的 session 状态存入一个ticket中,ticket会被加密,并被MAC保护,无法篡改,加密和算MAC用的key只有服务器知道。 
加密并MAC过的ticket用 NewSessionTicket 消息分发给客户端,NewSessionTicket 消息应该在 ChangeCipherSpec 消息之前,在服务器验证通过客户端的Finished消息之后发送。

  1.  Client Server
  2.  ClientHello
  3.  (empty SessionTicket extension)------->
  4.  ServerHello
  5.  (empty SessionTicket extension)
  6.  Certificate*
  7.  ServerKeyExchange*
  8.  CertificateRequest*
  9.  <-------- ServerHelloDone
  10.  Certificate*
  11.  ClientKeyExchange
  12.  CertificateVerify*
  13.  [ChangeCipherSpec]
  14.  Finished -------->
  15.  NewSessionTicket
  16.  [ChangeCipherSpec]
  17.  <-------- Finished
  18.  Application Data <-------> Application Data
  19.  Figure 1: Message flow for full handshake issuing newsession ticket

客户端把收到的ticket和master secret等其它与当前session有关的参数一起,缓存起来。 
单客户端希望恢复会话时,就把ticket包含在 ClientHello 的 SessionTicket 扩展中发给服务器。 
服务器收到后,解密ticket,算MAC确认ticket没有被篡改过,然后从解密的内容里面,获取session 状态,用来恢复会话。如果服务器成功地验证了ticket,可以在 ServerHello 之后返回一个 NewSessionTicket 消息来更新ticket。

显然,这种情况下,相比完整握手,可以省掉1个RTT。如下图:

  1.  Client Server
  2.  ClientHello
  3.  (SessionTicket extension) -------->
  4.  ServerHello
  5.  (empty SessionTicket extension)
  6.  NewSessionTicket
  7.  [ChangeCipherSpec]
  8.  <-------- Finished
  9.  [ChangeCipherSpec]
  10.  Finished -------->
  11.  Application Data <-------> Application Data
  12.  Figure 2: Message flow for abbreviated handshake using new
  13. session ticket

如果服务器不能,或者不想使用客户端发来的ticket,那服务器可以忽略ticket,启动一个完整的握手流程。

如果服务器此时不希望下发新的ticket,那就可以不回复 SessionTicket 扩展,或者不回复 NewSessionTicket 消息。 
此时除了 ClientHello里面的 SessionTicket扩展,就和一般的TLS流程一样了。

如果服务器拒绝收到的ticket,服务器可能仍然希望在完整的握手之后,下发新的ticket。 
此时流程和全新 ticket 生成下发的区别,就是ClientHello的SessionTicket不是空的。

NewSessionTicket 消息 
服务器在握手过程中,发ChangeCipherSpec之前发送NewSessionTicket消息。 
如果服务器在ServerHello中包含了一个SessionTicket扩展,那就必须发送NewSessionTicket消息。 
如果服务器没有包含SessionTicket扩展,那绝对不能发送NewSessionTicket消息。 
如果服务器在包含了SessionTicket扩展之后,不想发送ticket,那可以发送一个长度为0的NewSessionTicket消息。

在完整握手的情况下,客户端必须在确认服务器的Finished消息正确之后,才能认为NewSessionTicket 里面的ticket合法。

服务器可以NewSessionTicket消息中更新 ticket。

ticket_lifetime_hint 字段包含一个服务器的提示,提示客户端本ticket应该存多长时间就失效。单位是秒,网络字节序。当时间到期时,客户端应该删掉ticket和关联的状态。客户端也可以提前删除。服务器端也可以提前认为ticket失效。

  1.  struct {
  2.  uint32 ticket_lifetime_hint;
  3. opaque ticket<0..2^16-1>;
  4.  } NewSessionTicket;

SessionTicket 和 Session ID 之间的关系比较繁琐。感兴趣的自行去看RFC吧。

对于客户端来说,ticket就是一块二进制buffer,客户端并不管里面的内容。所以ticket具体怎么加密加MAC服务器可以为所欲为,无需顾及客户端的感受。

RFC5077中推荐了一种ticket的加密保护方法: 
服务器使用2个key,一个 aes-128-cbc的key,一个 HMAC-SHA-256 的key。

ticket的格式像这样:

  1.  struct {
  2. opaque key_name[16];
  3. opaque iv[16];
  4. opaque encrypted_state<0..2^16-1>;
  5. opaque mac[32];
  6.  } ticket;

其中,key_name 用来标识一组key,这样服务器端就可以使用多组key。

加密过程,首先随机生成IV,然后用 aes-128-cbc 加密 session 的序列化结果, 
然后用 HMAC-SHA-256 对 key_name,IV,encrypted_data 的长度(2字节),encrypted_data 计算MAC。 
最好把各个字段填入上面ticket结构体。 
显然,此处是 Encrypt-then-MAC的方式,是最安全的。

实际在openssl 中的session,用asn1格式序列化保存了下面这些字段:

  1.  typedef struct ssl_session_asn1_st {
  2. ASN1_INTEGER version;
  3. ASN1_INTEGER ssl_version;
  4. ASN1_OCTET_STRING cipher;
  5. ASN1_OCTET_STRING master_key;
  6. ASN1_OCTET_STRING session_id;
  7. ASN1_OCTET_STRING session_id_context;
  8. ASN1_INTEGER time;
  9. ASN1_INTEGER timeout;
  10. ASN1_INTEGER verify_result;
  11. ASN1_OCTET_STRING tlsext_hostname;
  12. ASN1_INTEGER tlsext_tick_lifetime;
  13. ASN1_OCTET_STRING tlsext_tick;
  14.  } SSL_SESSION_ASN1;

6. ChangeCipherSpec 协议

ChangeCipherSpec用来通知对端,开始启用协商好的Connection State做对称加密,内容只有1个字节。 
这个协议是冗余的,在TLS 1.3里面直接被删除了。

changeCipherSpec协议抓包: 

7. Alert 协议

一种返回码机制,简单

 enum { warning(1), fatal(2), (255) } AlertLevel;
  struct {
      AlertLevel level;
      AlertDescription description;
  } Alert; 

其中level是等级,不同等级要求不同的处理。

其中有一种:close_notify,用来通知对端,我不会再发送更多数据了。这个可以让对端主动close fd,这样可以减少我方tcp timewait状态的socket 量。

alert协议: 

8. application data协议

application data协议,就是把应用数据直接输入record层,做分段,算MAC,加密,传输。 
抓包举例如下:


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引