写点什么

TLS 协议分析 (五) handshake 协议 证书与密钥交换

用户头像
OpenIM
关注
发布于: 4 小时前

5.4. 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 格式] 链接 https://tools.ietf.org/html/rfc5081 )。

  • 服务器证书的公钥,必须和选择的密钥交换算法配套。



  • “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 密钥交换算法,也会指定证书格式和要求的密钥编码方法。

5.5. handshake — Server Key Exchange

服务器会在 server Certificate 消息之后,立即发送 Server Key Exchange 消息。(如果协商出的 CipherSuite 不需要做认证,即 anonymous negotiation,会在 ServerHello 之后立即发送 Server Key Exchange 消息)


只有在 server Certificate 消息没有足够的信息,不能让客户端完成 premaster 的密钥交换时,服务器才发送 server Key Exchange, 主要是对前向安全的几种密钥协商算法,列表如下:


  1. DHE_DSS

  2. DHE_RSA

  3. DH_anon

  4. ECDHE_ECDSA

  5. ECDHE_RSA

  6. ECDH_anon


对下面几种密钥交换方法,发送 ServerKeyExchange 消息是非法的:


  1. RSA

  2. DH_DSS

  3. DH_RSA

  4. ECDH_ECDSA

  5. 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 ] 链接 http://www.ietf.org/mail-archive/web/tls/current/msg12266.html


消息格式:


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 和签名算法,必须和服务器的证书里面的公钥兼容。

5.6. 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,服务器不能要求客户端认证。

5.7. handshake — Server Hello Done

在 ServerHello 和相关消息已经处理结束后,服务器发送 ServerHelloDone。在发送 ServerHelloDone 后,服务器开始等待客户端的响应。


ServerHelloDone 消息表示,服务器已经发送完了密钥协商需要的消息,并且客户端可以开始客户端的密钥协商处理了。


收到 ServerHelloDone 后,客户端应该确认服务器提供了合法的证书,并且确认服务器的 ServerHello 消息里面的参数是可以接受的。


消息格式:


  struct { } ServerHelloDone;
复制代码

5.8. 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 里列出的证书类型兼容。



  • 如果 certificate_authorities 列表不是空的,客户端证书链中的某一个证书必须是 CA 中的某一个签署的。

  • 证书必须使用 服务器可以接受的 hash/signature 算法对。


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

5.9. 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 的客户端认证),那么这条消息必须被发送,并且必须为空。


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


struct {    select (KeyExchangeAlgorithm) {        case rsa:            EncryptedPreMasterSecret;        case dhe_dss:        case dhe_rsa:        case dh_dss:        case dh_rsa:        case dh_anon:            ClientDiffieHellmanPublic;        case ec_diffie_hellman:             ClientECDiffieHellmanPublic;    } exchange_keys;} ClientKeyExchange;
复制代码

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

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


struct {    ProtocolVersion client_version;    opaque random[46];} PreMasterSecret;
client_version 客户端支持的最新协议版本号,这个字段用来检测中间人版本回退攻击。Trandom 46 字节的,安全生成的随机值。struct { public-key-encrypted PreMasterSecret pre_master_secret;} EncryptedPreMasterSecret;
pre_master_secret 这个随机值由客户端生成,用于生成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_versionpre_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

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

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


Yc 的编码方式由 PublicValueEncoding 决定。


消息的结构:


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

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

struct {    select (PublicValueEncoding) {        case implicit:          struct { };       case explicit:           ECPoint ecdh_Yc;    } ecdh_public;} ClientECDiffieHellmanPublic;
复制代码


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


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

5.10. handshake — Cerificate Verify

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


消息结构:Structure of this message:


struct {     digitally-signed struct {         opaque handshake_messages[handshake_messages_length];     }} CertificateVerify;
复制代码


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


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


本文转自微信后台团队,如有侵犯,请联系我们立即删除


OpenIMgithub 开源地址:


https://github.com/OpenIMSDK/Open-IM-Server


OpenIM 官网 : https://www.rentsoft.cn


OpenIM 官方论坛: https://forum.rentsoft.cn/


更多技术文章:


开源 OpenIM:高性能、可伸缩、易扩展的即时通讯架构https://forum.rentsoft.cn/thread/3


【OpenIM 原创】简单轻松入门 一文讲解 WebRTC 实现 1 对 1 音视频通信原理https://forum.rentsoft.cn/thread/4


【OpenIM 原创】开源 OpenIM:轻量、高效、实时、可靠、低成本的消息模型https://forum.rentsoft.cn/thread/1

用户头像

OpenIM

关注

还未添加个人签名 2021.08.30 加入

还未添加个人简介

评论

发布
暂无评论
TLS协议分析 (五) handshake协议 证书与密钥交换