第 2 章 协议

第 2 章 协议

TLS是一种密码学协议,用于保证两个团体之间的会话安全。会话是由任意数量的消息组成的。本章将讨论最新的协议版本TLS 1.2,也会在合适的时候对之前的协议版本进行简要说明。

我的目标是进行宏观概述,让你能够理解其工作原理,而不会因具体的实现细节分心。我尽可能使用消息内容的示例,避免只给出定义。本章中定义用到的语法在本质上与TLS规范完全一致,仅有稍微简化。要得到更多有关语法和完整协议的参考,请从RFC 5246开始了解,它描述了TLS 1.2的规范1,但是并没有完整的细节。还有许多其他的相关RFC,本章会逐一引用到。

1RFC 5246: The Transport Layer Security Protocol Version 1.2,http://tools.ietf.org/html/rfc5246(T. Dierks和E. Rescorla,2008年8月)。

了解TLS的最好方式是观察现实中的网络流量。我最喜欢的方法是使用网络捕获工具Wireshark2,它带有一个TLS协议分析器:用你喜爱的浏览器进入一个安全网站,再启用Wireshark监视连接(最好限制只捕捉一个主机名和端口443),并观察协议消息。

2Wireshark,https://www.wireshark.org(检索于2015年7月13日)。

在理解TLS以后(不用努力了解一切;因为特性太多,理解所有特性非常困难),你可以自由浏览各类RFC,甚至在关键邮件列表中“潜水”。我最喜欢的两处是TLS工作组文档页3和TLS工作组邮件列表4。你可以在前者中找到关键文档和新提案的清单,可以在后者中跟进有关TLS未来发展方向的讨论。

3TLS working group documents,https://datatracker.ietf.org/wg/tls/documents/(IETF,检索于2014年7月19日)。

4TLS working group mailing list archives,http://www.ietf.org/mail-archive/web/tls/current/(IETF,检索于2014年7月19日)。

2.1 记录协议

宏观上,TLS以记录协议(record protocol)实现。记录协议负责在传输连接上交换的所有底层消息,并可以配置加密。每一条TLS记录以一个短标头起始。标头包含记录内容的类型(或子协议)、协议版本和长度。消息数据紧跟在标头之后,如图2-1所示。

{%}

图 2-1 TLS记录

可以更为正式地将TLS记录的字段定义为如下所示。

struct {
    uint8 major;
    uint8 minor;
} ProtocolVersion;

enum {
    change_cipher_spec (20),
    alert (21),
    handshake (22),
    application_data (23)
} ContentType;

struct {
    ContentType type;
    ProtocolVersion version;
    uint16 length; /* 最大长度为2^14(16 384)字节 */
    opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

除了这些可见的字段,还会给每一个TLS记录指定唯一的64位序列号,但不会在线路上传输。任一端都有自身的序列号并跟踪来自另一端记录的数量。这些值是对抗重放攻击的一部分。稍后你可以看到它的工作原理。

记录协议从多个重要的宏观角度对通信进行考量,是一个很有用的协议抽象。

  • 消息传输

    记录协议传输由其他协议层提交给它的不透明数据缓冲区。如果缓冲区超过记录的长度限制(16 384字节),记录协议会将其切分成更小的片段。反过来也是可能的,属于同一个子协议的小缓冲区也可以组合成一个单独的记录。

  • 加密以及完整性验证

    在一个刚建立起来的连接上,最初的消息传输没有受到任何保护(从技术上讲,就是使用了TLS_NULL_WITH_NULL_NULL密码套件)。这是必需的,否则第一次协商就无法进行。但是,一旦握手完成,记录层就开始按照协商取得的连接参数进行加密和完整性验证5

  • 压缩

    理论上,在加密之前透明地对数据进行压缩非常好,可是实践中几乎没有人这样做。这主要是因为每个应用在HTTP层就已经对它们的出口流量进行过压缩。这个特性在2012年遭受过一次严重打击,当时CRIME攻击使压缩成为不安全的特性6,所以现在也不会再被使用。

  • 扩展性

    记录协议只关注数据传输和加密,而将所有其他特性转交给子协议。这个方法使TLS可以扩展,因为可以很方便地添加子协议。伴随着记录协议而被加密,所有子协议都会以协商取得的连接参数自动得到保护。

5大多数情况下,这代表后续流量是加密的并且其完整性已进行过验证。但也存在少量套件未使用加密而仅使用了完整性校验的情况。

6我会在7.3节中讨论CRIME攻击以及各种其他压缩相关的弱点。

TLS的主规格说明书定义了四个核心子协议:握手协议(handshake protocol)、密钥规格变更协议(change cipher spec protocol)、应用数据协议(application data protocol)和警报协议(alert protocol)。

2.2 握手协议

握手是TLS协议中最精密复杂的部分。在这个过程中,通信双方协商连接参数,并且完成身份验证。根据使用的功能的不同,整个过程通常需要交换6~10条消息。根据配置和支持的协议扩展的不同,交换过程可能有许多变种。在使用中经常可以观察到以下三种流程:(1) 完整的握手,对服务器进行身份验证;(2) 恢复之前的会话采用的简短握手;(3) 对客户端和服务器都进行身份验证的握手。

握手协议消息的标头信息包含消息类型(1字节)和长度(3字节),余下的信息则取决于消息类型:

struct {
    HandshakeType msg_type;
    uint24 length;
    HandshakeMessage message;
} Handshake;

2.2.1 完整的握手

每一个TLS连接都会以握手开始。如果客户端此前并未与服务器建立会话,那么双方会执行一次完整的握手流程来协商TLS会话。握手过程中,客户端和服务器将进行以下四个主要步骤。

(1) 交换各自支持的功能,对需要的连接参数达成一致。

(2) 验证出示的证书,或使用其他方式进行身份验证。

(3) 对将用于保护会话的共享主密钥达成一致。

(4) 验证握手消息并未被第三方团体修改。

注意

在实际使用中,第2步和第3步都是密钥交换(更通用的说法是密钥生成)的一部分,密钥交换是一个单独的步骤。我更喜欢将它们分开来说,用以强调协议的安全性取决于正确的身份验证。身份验证有效地在TLS的外层工作。如果没有身份验证,主动攻击者就可以将自身嵌入会话,并冒充会话的另一端。

本节会讨论最常见的TLS握手流程,就是一种在不需要身份验证的客户端与需要身份验证的服务器之间的握手,如图2-2所示。后面几节将介绍其他的流程:客户端身份验证和会话恢复。

{%}

图 2-2 对服务器进行身份验证的完整握手

(1) 客户端开始新的握手,并将自身支持的功能提交给服务器。

(2) 服务器选择连接参数。

(3) 服务器发送其证书链(仅当需要服务器身份验证时)。

(4) 根据选择的密钥交换方式,服务器发送生成主密钥的额外信息。

(5) 服务器通知自己完成了协商过程。

(6) 客户端发送生成主密钥所需的额外信息。

(7) 客户端切换加密方式并通知服务器。

(8) 客户端计算发送和接收到的握手消息的MAC并发送。

(9) 服务器切换加密方式并通知客户端。

(10) 服务器计算发送和接收到的握手消息的MAC并发送。

假设没有出现错误,到这一步,连接就建立起来了,可以开始发送应用数据。现在让我们了解一下这些握手消息的更多细节。

1. ClientHello

在一次新的握手流程中,ClientHello消息总是第一条消息。这条消息将客户端的功能和首选项传送给服务器。客户端会在新建连接后,希望重新协商或者响应服务器发起的重新协商请求(由HelloRequest消息指示)时,发送这条消息。

在下面的例子中,你可以观察到ClientHello消息。为了更简洁,我减少了一些信息展示,但是包含了所有的关键元素。

Handshake protocol: ClientHello
    Version: TLS 1.2
    Random
        Client time: May 22, 2030 02:43:46 GMT
        Random bytes: b76b0e61829557eb4c611adfd2d36eb232dc1332fe29802e321ee871
    Session ID: (empty)
    Cipher Suites
        Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        Suite: TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
        Suite: TLS_RSA_WITH_AES_128_GCM_SHA256
        Suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
        Suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA
        Suite: TLS_RSA_WITH_AES_128_CBC_SHA
        Suite: TLS_RSA_WITH_3DES_EDE_CBC_SHA
        Suite: TLS_RSA_WITH_RC4_128_SHA
    Compression methods
        Method: null
    Extensions
        Extension: server_name
            Hostname: www.feistyduck.com
        Extension: renegotiation_info
        Extension: elliptic_curves
            Named curve: secp256r1
            Named curve: secp384r1
        Extension: signature_algorithms
            Algorithm: sha1/rsa
            Algorithm: sha256/rsa
            Algorithm: sha1/ecdsa
            Algorithm: sha256/ecdsa

可以看到,绝大多数消息字段光看名称就很容易理解,而且消息的结构也很容易理解。

  • Version

    协议版本(protocol version)指示客户端支持的最佳协议版本。

  • Random

    随机数(random)字段包含32字节的数据。当然,只有28字节是随机生成的;剩余的4字节包含额外的信息,受客户端时钟的影响。准确来说,客户端时间与协议不相关,而且协议规格文档中言及此事时也很清楚(“基本的TLS协议不需要正确设置时钟,更高层或应用协议可以定义额外的需求项。”);该字段是1994年在Netscape Navigator中发现了一个严重故障之后,为了防御弱随机数生成器而引入的7。尽管这个字段曾经一直含有精确时间的部分,但现在仍然有人担心客户端时间可能被用于大规模浏览器指纹采集8,所以一些浏览器会给它们的时间添加时钟扭曲(正如你在示例中所看到的那样),或者简单地发送随机的4字节。

    在握手时,客户端和服务器都会提供随机数。这种随机性对每次握手都是独一无二的,在身份验证中起着举足轻重的作用。它可以防止重放攻击,并确认初始数据交换的完整性。

  • Session ID

    在第一次连接时,会话ID(session ID)字段是空的,这表示客户端并不希望恢复某个已存在的会话。在后续的连接中,这个字段可以保存会话的唯一标识。服务器可以借助会话ID在自己的缓存中找到对应的会话状态。典型的会话ID包含32字节随机生成的数据,这些数据本身并没有什么价值。

  • Cipher Suites

    密码套件(cipher suite)块是由客户端支持的所有密码套件组成的列表,该列表是按优先级顺序排列的。

  • Compression

    客户端可以提交一个或多个支持压缩的方法。默认的压缩方法是null,代表没有压缩。

  • Extensions

    扩展(extension)块由任意数量的扩展组成。这些扩展会携带额外数据。我会在本章后面对最常见的扩展进行讨论。

7如果想得到有关这个问题的更多信息,请参考6.2.1节。

8Deprecating gmt_unix_time in TLS,https://tools.ietf.org/id/draft-mathewson-no-gmtunixtime-00.txt(N. Mathewson和B. Laurie,2013年12月)。

2. ServerHello

ServerHello消息的意义是将服务器选择的连接参数传送回客户端。这个消息的结构与ClientHello类似,只是每个字段只包含一个选项。

Handshake protocol: ServerHello
    Version: TLS 1.2
    Random
        Server time: Mar 10, 2059 02:35:57 GMT
        Random bytes: 8469b09b480c1978182ce1b59290487609f41132312ca22aacaf5012
    Session ID: 4cae75c91cf5adf55f93c9fb5dd36d19903b1182029af3d527b7a42ef1c32c80
    Cipher Suite: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    Compression method: null
    Extensions
        Extension: server_name
        Extension: renegotiation_info

服务器无需支持客户端支持的最佳版本。如果服务器不支持与客户端相同的版本,可以提供某个其他版本以期待客户端能够接受。

3. Certificate

典型的Certificate消息用于携带服务器X.509证书链。证书链是以ASN.1 DER编码的一系列证书,一个接着一个组合而成。主证书必须第一个发送,中间证书按照正确的顺序跟在主证书之后。根证书可以并且应该省略掉,因为在这个场景中它没有用处。

服务器必须保证它发送的证书与选择的算法套件一致。比方说,公钥算法与套件中使用的必须匹配。除此以外,一些密钥交换算法依赖嵌入证书的特定数据,而且要求证书必须以客户端支持的算法签名。所有这些都表明服务器需要配置多个证书(每个证书可能会配备不同的证书链)。

Certificate消息是可选的,因为并非所有套件都使用身份验证,也并非所有身份验证方法都需要证书。更进一步说,虽然消息默认使用X.509证书,但是也可以携带其他形式的标志;一些套件就依赖PGP密钥9

9RFC 5081: Using OpenPGP Keys for TLS Authentication,http://tools.ietf.org/html/rfc5081(N. Mavrogiannopoulos,2007年11月)。

4. ServerKeyExchange

ServerKeyExchange消息的目的是携带密钥交换的额外数据。消息内容对于不同的协商算法套件都会存在差异。在某些场景中,服务器不需要发送任何内容,这意味着在这些场景中根本不会发送ServerKeyExchange消息。

5. ServerHelloDone

ServerHelloDone消息表明服务器已经将所有预计的握手消息发送完毕。在此之后,服务器会等待客户端发送消息。

6. ClientKeyExchange

ClientKeyExchange消息携带客户端为密钥交换提供的所有信息。这个消息受协商的密码套件的影响,内容随着不同的协商密码套件而不同。

7. ChangeCipherSpec

ChangeCipherSpec消息表明发送端已取得用以生成连接参数的足够信息,已生成加密密钥,并且将切换到加密模式。客户端和服务器在条件成熟时都会发送这个消息。

注意

ChangeCipherSpec不属于握手消息,它是另一种协议,只有一条消息,作为它的子协议进行实现。这个设计的结果是这条消息不是握手完整性验证算法的一部分,这使得正确实现TLS更为困难。在2014年6月,人们发现OpenSSL对于ChangeCipherSpec消息的处理不正确,使得OpenSSL为主动网络攻击敞开了大门10

同样的问题也出现在其他所有子协议中。主动网络攻击者利用缓冲机制在首次握手时发送未经验证的警报消息,更可以在开始加密以后破环真正的警报消息11。为了避免更严重的问题,应用数据协议消息必须等到首次握手完成以后才能开始发送。

10你可以在6.1.1节中找到有关此缺陷的更多信息。

11The Alert attack,http://www.mitls.org/wsgi/alert-attack(miTLS,2012年2月)。

8. Finished

Finished消息意味着握手已经完成。消息内容将加密,以便双方可以安全地交换验证整个握手完整性所需的数据。

这个消息包含verify_data字段,它的值是握手过程中所有消息的散列值。这些消息在连接两端都按照各自所见的顺序排列,并以协商新得到的主密钥计算散列。这个过程是通过一个伪随机函数(pseudorandom function,PRF)来完成的,这个函数可以生成任意数量的伪随机数据。我将在本章的后续部分中对其进行介绍。散列函数与PRF一致,除非协商的套件指定使用其他算法。两端的计算方法一致,但会使用不同的标签:客户端使用client finished,而服务器则使用server finished。

verify_data = PRF(master_secret, finished_label, Hash(handshake_messages))

因为Finished消息是加密的,并且它们的完整性由协商MAC算法保证,所以主动网络攻击者不能改变握手消息并对vertify_data的值造假。

理论上攻击者也可以尝试找到一组伪造的握手消息,得到的值与真正消息计算出的verity_data的值完全一致。这种攻击本身就非常不容易,而且因为散列中混入了主密钥(攻击者不知道主密钥),所以攻击者根本不会尝试。

在TLS 1.2版本中,Finished消息的长度默认是12字节(96位),并且允许密码套件使用更长的长度。在此之前的版本,除了SSL 3使用36字节的定长消息,其他版本都使用12字节的定长消息。

2.2.2 客户端身份验证

尽管可以选择对任意一端进行身份验证,但人们几乎都启用了对服务器的身份验证。如果服务器选择的套件不是匿名的,那么就需要在Certificate消息中跟上自己的证书。

相比之下,服务器通过发送CertificateRequest消息请求对客户端进行身份验证。消息中列出所有可接受的客户端证书。作为响应,客户端发送自己的Certificate消息(使用与服务器发送证书相同的格式),并附上证书。此后,客户端发送CertificateVerify消息,证明自己拥有对应的私钥。完整的握手如图2-3所示。

{%}

图 2-3 完整的握手,在此期间客户端和服务器都会进行身份验证

只有已经过身份验证的服务器才被允许请求客户端身份验证。基于这个原因,这个选项被称为相互身份验证(mutual authentication)。

1. CertificateRequest

服务器使用CertificateRequest消息请求对客户端进行身份验证,并将其接受的证书的公钥和签名算法传送给客户端。它也可以选择发送一份自己接受的证书颁发机构列表,这些机构都用其可分辨名称来表示:

struct {
    ClientCertificateType certificate_types;
    SignatureAndHashAlgorithm supported_signature_algorithms;
    DistinguishedName certificate_authorities;
} CertificateRequest;

2. CertificateVerify

客户端使用CertificateVerify消息证明自己拥有的私钥与之前发送的客户端证书中的公钥相对应。消息中包含一条到这一步为止的所有握手消息的签名:

struct {
    Signature handshake_messages_signature;
} CertificateVerify;

2.2.3 会话恢复

完整的握手协议非常复杂,需要很多握手消息和两次网络往返才能开始发送客户端应用数据。此外,握手执行的密钥学操作通常需要密集的CPU处理。身份验证通常以客户端和服务器证书验证(以及证书吊销检查)的形式完成,需要更多的工作。这其中的许多消耗都可以通过简短握手的方式节约下来。

最初的会话恢复机制是,在一次完整协商的连接断开时,客户端和服务器都会将会话的安全参数保存一段时间。希望使用会话恢复的服务器为会话指定唯一的标识,称为会话ID。服务器在ServerHello消息中将会话ID发回客户端(请参见2.2.2节中的示例)。

希望恢复早先会话的客户端将适当的会话ID放入ClientHello消息,然后提交。服务器如果愿意恢复会话,就将相同的会话ID放入ServerHello消息返回,接着使用之前协商的主密钥生成一套新的密钥,再切换到加密模式,发送Finished消息。客户端收到会话已恢复的消息以后,也进行相同的操作。这样的结果是握手只需要一次网络往返。简短握手如图2-4所示。

{%}

图 2-4 简短握手,用于恢复已经建立的会话

用来替代服务器会话缓存和恢复的方案是使用会话票证(session ticket)。它是2006年引入的(参见RFC 4507),随后在2008年进行了更新(参见RFC 5077)。使用这种方式,除了所有的状态都保持在客户端(与HTTP Cookie的原理类似)之外,其消息流与服务器会话缓存是一样的。

2.3 密钥交换

密钥交换是握手过程中最引人入胜的部分。在TLS中,会话安全性取决于称为主密钥(master secret)的48字节共享密钥。密钥交换的目的是计算另一个值,即预主密钥(premaster secret)。这个值是组成主密钥的来源。

TLS支持许多密钥交换算法,能够支持各种证书类型、公钥算法和密钥生成协议。它们之中有一些在TLS协议主规格书中定义,但更多的则是在其他规格说明中定义。你可以在表2-1中找到最常用的算法。

表2-1 最常用的密钥交换算法概览

密钥交换

描述

dh_anon

Diffie-Hellman(DH)密钥交换,未经身份验证

dhe_rsa

临时DH密钥交换,使用RSA身份验证

ecdh_anon

临时椭圆曲线DH(elliptic curve DH,ECDH)密钥交换,未经身份验证(RFC 4492)

ecdhe_rsa

临时ECDH密钥交换,使用RSA身份验证(RFC 4492)

ecdhe_ecdsa

临时ECDH密钥交换,使用ECDSA身份验证(RFC 4492)

krb5

Kerberos密钥交换(RFC 2712)

rsa

RSA密钥交换和身份验证

psk

预共享密钥(pre-shared key,PSK)密钥交换和身份验证(RFC 4279)

dhe_psk

临时DH密钥交换,使用PSK身份验证(RFC 4279)

rsa_psk

PSK密钥交换,使用RSA身份验证(RFC 4279)

srp

安全远程密码(secure remote password,SRP)密钥交换和身份验证(RFC 5054)

使用哪一种密钥交换由协商的套件所决定。一旦套件决定下来,两端都能了解按照哪种算法继续操作。实际使用的密钥交换算法主要有以下4种。

  • RSA

    RSA是一种事实上的标准密钥交换算法,它得到了广泛的支持。但它受到一个问题的严重威胁:它的设计使被动攻击者可以解码所有加密数据,只要她能够访问服务器的私钥。因此,RSA密钥交换正慢慢被其他支持前向保密(forward secrecy)的算法所替代。RSA密钥交换是一种密钥传输(key transport)算法,这种算法由客户端生成预主密钥,并以服务器公钥加密传送给服务器。

  • DHE_RSA

    临时Diffie-Hellman(ephemeral Diffie-Hellman,DHE)密钥交换是一种构造完备的算法。它的优点是支持前向保密,缺点是执行缓慢。DHE是一种密钥协定算法,进行协商的团体都对密钥生成产生作用,并对公共密钥达成一致。在TLS中,DHE通常与RSA身份验证联合使用。

  • ECDHE_RSAECDHE_ECDSA

    临时椭圆曲线Diffie-Hellman(ephemeral elliptic curve Diffie-Hellman,ECDHE)密钥交换建立在椭圆曲线加密的基础之上。椭圆曲线算法是相对较新的算法。大家认可它执行很快而且提供了前向保密。但是只有较新的客户端才能较好地支持。ECDHE也是一种密钥协定算法,其理论原理与DHE类似。在TLS中,ECDHE可以与RSA或者ECDSA身份验证一起使用。

不论使用哪一种密钥交换,服务器都有机会发送ServerKeyExchange消息率先发话:

struct {
    select (KeyExchangeAlgorithm) {
        case dh_anon:
            ServerDHParams    params;
        case dhe_rsa:
            ServerDHParams    params;
            Signature         params_signature;
        case ecdh_anon:
            ServerECDHParams  params;
        case ecdhe_rsa:
        case ecdhe_ecdsa:
            ServerECDHParams  params;
            Signature         params_signature;
        case rsa:
        case dh_rsa:
            /* 无消息 */
    };
} ServerKeyExchange;

你可以在上面的消息定义中发现,在某些算法内,服务器不发送任何信息。原因是在这些情况下,所有需要的信息已经通过其他消息得到;不然,服务器就会在此发送其密钥交换的参数。关键的是,服务器也会发送参数的签名用于身份验证。使用签名,客户端得以确认它正在与持有私钥对应证书中的公钥的团体进行通信。

客户端会发送ClientKeyExchange消息传送它的密钥交换参数,这个消息总是必需的:

struct {
    select (KeyExchangeAlgorithm) {
        case rsa:
            EncryptedPreMasterSecret;
        case dhe_dss:
        case dhe_rsa:
        case dh_dss:
        case dh_rsa:
        case dh_anon:
            ClientDiffieHellmanPublic;
        case ecdhe:
            ClientECDiffieHellmanPublic;
    } exchange_keys;
} ClientKeyExchange;

2.3.1 RSA密钥交换

RSA密钥交换的过程十分直截了当。客户端生成预主密钥(46字节随机数),使用服务器公钥对其加密,将其包含在ClientKeyExchange消息中,最后发送出去。服务器只需要解密这条消息就能取出预主密钥。TLS使用的是RFC 344712定义的RSAES-PKCS1-v1_5加密方案。

12RFC 3447: RSA Cryptography Specifications Version 2.1,http://tools.ietf.org/html/rfc3447(Jonsson和Kaliski,2003年2月)。

注意

因为RSA算法可以同时用于加密和数字签名,所以RSA密钥交换可以按照这种方式工作。其他流行的密钥类型,比如DSA(DSS)和ECDSA,只能用于签名。

RSA密钥交换的简单性也是它最大的弱点。用于加密预主密钥的服务器公钥,一般会保持多年不变。任何能够接触到对应私钥的人都可以恢复预主密钥,并构建相同的主密钥,从而危害到会话安全性。

对目标的攻击并不需要实时进行,强大的对手可以制定长期行动。攻击者会记录所有加密的流量,耐心等待有朝一日可以得到密钥。比如,计算机能力的进步使暴力破解成为可能;也可以通过法律强制力、政治高压、贿赂或强行进入使用该密钥的服务器来取得密钥。只要密钥泄露,就可以解密之前记录的所有流量了。

TLS中其他常见的密钥交换方式都不受这个问题的影响,被称为支持前向保密。使用那些密钥交换时,每个连接使用的主密钥相互独立。泄露的服务器密钥可以用于冒充服务器,但不能用于追溯解密任何流量。

2.3.2 Diffie-Hellman密钥交换

Diffie-Hellman(DH)密钥交换是一种密钥协定的协议,它使两个团体在不安全的信道上生成共享密钥成为可能13

13Diffie-Hellman key exchange,https://en.wikipedia.org/wiki/Diffie-Hellman_key_exchange(维基百科,检索于2014年6月18日)。

注意

以这种方式协商共享密钥时不会受到被动攻击的威胁,但主动攻击者却可以劫持通信信道,冒充对端。这就是DH密钥交换通常与身份验证联合使用的原因。

抛开算法的细节,DH的诀窍是使用了一种正向计算简单、逆向计算困难的数学函数,即使交换中某些因子已被知晓,情况也是一样。最恰当的类比示例是混色:如果有两种颜色,那么很容易将其混在一起得到第三种颜色;但是如果只有第三种颜色的话,就很难确定究竟它是由哪两种颜色混合而成的14

14Public Key Cryptography: Diffie-Hellman Key Exchange,https://www.youtube.com/watch?v=3QnD2c4Xovk(YouTube,检索于2014年6月26日)。

DH密钥交换需要6个参数:其中两个(dh_pdh_g)称为域参数,由服务器选取。协商过程中,客户端和服务器各自生成另外两个参数,相互发送其中一个参数(dh_Ysdh_Yc)到对端,再经过计算,最终得到共享密钥。

临时Diffie-Hellman(ephemeral Diffie-Hellman,DHE)密钥交换中没有任何参数被重复使用。与之相对,在一些DH密钥交换方式中,某些参数是静态的,并被嵌入到服务器和客户端的证书中。这样的话,密钥交换的结果是一直不变的共享密钥,就无法具备前向保密的能力。

TLS支持静态DH密钥交换,但无人使用。在协商DHE套件时,服务器将其所有参数填入ServerDHParams块并发送:

struct {
    opaque dh_p;
    opaque dh_g;
    opaque dh_Ys;
} ServerDHParams;

客户端响应并发送其公开参数(dh_Yc):

struct {
    select (PublicValueEncoding) {
        case implicit:
            /* 空的,当客户端公共参数嵌入其客户端时 */
        case explicit:
            opaque dh_Yc;
    } dh_public;
} ClientDiffieHellmanPublic;

当前使用的DH交换存在以下这些现实问题。

  • DH参数的安全性

    DH密钥交换的安全性取决于域参数的质量。服务器发送弱的或者不安全的参数,将对会话的安全性造成损害。这个问题在一篇研究论文“Triple Handshake Attack”中进行过重点论述,弱DH参数被用作一种攻击向量15

  • DH参数协商

    TLS并没有为客户端提供传递期望使用的DH参数的强度的设施。比如,客户端可能希望避免使用弱参数,抑或可能不支持强参数。因此,选择DHE套件的服务器事实上只能期待DH参数可以被客户端接受。

  • 参数强度不够

    以历史角度来说,DH参数很大程度上被忽略了,其安全性也被忽视了。许多库和服务器默认使用弱DH参数,而且经常不提供配置DH参数强度的方法。因此,服务器使用1024位弱参数、768位非安全参数,更有甚者使用512位参数,这些情况都很常见。直到最近,一些平台才开始使用2048位或者更高位数的强参数。

    2015年5月披露的Logjam攻击表明,512位的DH参数在使用合适资源的情况下可以被攻击者在很短的时间内成功利用,同时可以估计厉害的攻击者甚至可能利用768位的参数。同样的研究还强调非常厉害的攻击者甚至有可能可以攻破那些被广泛使用的、长度为1024位的标准参数组,从而以被动方式入侵数以百万计的互联网服务器。在6.5节中可以找到有关此问题的更多信息。

15要想了解有关三次握手攻击的更多信息,请参考7.6节。

这些问题可以通过对不同强度的域参数定义来进行标准化,或者扩展TLS允许客户端告知其偏好的方法来解决16

16Negotiated Finite Field Diffie-Hellman Ephemeral Parameters for TLS,https://datatracker.ietf.org/doc/draft-ietf-tls-negotiated-ff-dhe/(D. Gillmor,2015年6月)。

2.3.3 椭圆曲线Diffie-Hellman密钥交换

临时椭圆曲线Diffie-Hellman(elliptic curve Diffie-Hellman,ECDH)密钥交换原理与DH相似,但它的核心使用了不同的数学基础。正如名称所示,ECDHE基于椭圆曲线(elliptic curve,EC)加密。

ECDH密钥交换发生在一条由服务器定义的特定的椭圆曲线上。这条曲线代替了DH中域参数的角色。理论上,ECDH支持静态的密钥交换,但实际使用时,只使用了这种临时的变种(ECDHE)。

密钥交换由服务器发起,它选择一条椭圆曲线和公开参数(EC point)并提交:

struct {
    ECParameters curve_params;
    ECPoint public;
} ServerECDHParams;

服务器可以为密钥交换明确指定任意一条曲线,但TLS并未使用这个功能。作为替代,在TLS中,服务器通过指定某个名称引用一条可能预先定义好参数的曲线(命名曲线,named curve):

struct {
    ECCurveType curve_type;
    select (curve_type) {
        case explicit_prime:
            /* 为了清晰,略去 */
        case explicit_char2:
            /* 为了清晰,略去 */
        case named_curve:
            NamedCurve namedcurve;
    };
} ECParameters;

然后客户端提交自己的公开参数。在那以后,就可以计算预主密钥:

struct {
    select (PublicValueEncoding) {
        case implicit:
            /* 空的 */
        case explicit:
            ECPoint ecdh_Yc;
    } ecdh_public;
} ClientECDiffieHellmanPublic;

使用预定义参数,以及ellipic_curve扩展(客户端可以提交支持的曲线),可以使服务器选择一条双方都支持的曲线。你可以在2.12.3节中找到更多有关命名曲线的可用信息。

2.4 身份验证

在TLS中,为了避免重复执行密码操作造成巨大开销,身份验证与密钥交换紧紧捆绑在一起。大多数场景中,身份验证的基础是证书支持的公钥密码(最常见的是RSA,有时也用ECDSA)。一旦证书验证通过,客户端就知道了使用的公钥。在此之后,客户端将公钥交给指定的密钥交换算法,并由它负责以某种方式使用公钥验证另一端。

在RSA密钥交换的过程中,客户端生成一个随机值作为预主密钥,并以服务器公钥加密后发送出去。拥有对应私钥的服务器解码消息得到预主密钥。身份验证原理很清楚:只有拥有对应私钥的服务器才能取得预主密钥,构造正确的会话密钥,并生成正确的Finished消息。

在DHE和ECDHE的交换过程中,服务器为密钥交换提供自己的参数,并使用自己的私钥签名。客户端持有对应的公钥(从已验证的证书中获得),可以验证参数是否真正出自期望的服务器。

注意

服务器参数是与客户端和服务器随机值连在一起进行签名的,而客户端和服务器随机值对于握手来说是唯一的。因而,即使签名是以明文方式发送的,它也只对当前握手有效,这意味着攻击者无法重用该签名。Logjam攻击显示了这种将签名绑定在握手过程上面的弊端;主动网络攻击者可以同步产生这个随机值,并且在某些情况下再次利用服务器签名。

2.5 加密

TLS可以使用各种方法加密数据,比如使用3DES、AES、ARIA、CAMELLIA、RC4或者SEED等算法。目前使用最为广泛的加密算法是AES。TLS支持三种加密类型:序列密码、分组密码和已验证的加密。在TLS中,完整性验证是加密处理的一部分;它要么在协议级中显式处理,要么由协商的密码隐式处理。

2.5.1 序列加密

使用序列密码时,加密由两步组成。第一步,计算MAC值,范围包含记录序列号、标头、明文。MAC包含标头能确保未进行加密的标头不会遭受篡改。MAC包括序列号,能确保消息不被重放。第二步,加密明文和MAC,生成密文。整个过程如图2-5所示。

{%}

图 2-5 序列加密

注意

使用完整性验证但不进行加密的套件与使用序列密码加密的套件以相同的方式实现。明文被简单地复制到TLS记录中,而MAC则以这里描述的方法计算。

2.5.2 分组加密

使用分组密码时,加密会涉及更多内容,因为需要为分组加密的特性准备解决方案。具体来说,需要以下几个步骤,如图2-6所示。

{%}

图 2-6 分组加密

(1) 计算序列号、标头和明文的MAC。

(2) 构造填充,确认加密前的数据长度是分组大小(通常16字节)的整数倍。

(3) 生成一个长度与分组大小一致的不可预期的初始向量(initialization vector,IV)。IV能保证加密是不确定的。

(4) 使用CBC分组模式加密明文、MAC和填充。

(5) 将IV和密文一起发送。

注意

可以在1.4.1节找到有关CBC分组模式、填充和初始向量的更多信息。

这种处理方式被称为先计算MAC再加密(MAC-then-encrypt),而它也是很多问题的源头。在TLS 1.1和更新的版本中,每条记录中都包含显式IV;而TLS 1.0和以往的版本则使用隐式IV(使用前一个TLS记录中的加密块作为下一块的IV),但这在2011年被发现并不安全17

17这个问题首先被称为BEAST的攻击所利用。关于BEAST,我将在7.2节中进行讨论。

另一个问题是MAC计算不包括填充,这给主动网络攻击者进行填充预示攻击(padding oracle attack)提供了机会(有成功攻击TLS的示例18)。这里的问题其实是,协议定义的分组加密方式在现实中很难安全地实现。就我们所知,现在的实现并没有明显表现出易受攻击,但仍然不能对这个弱点掉以轻心。

18我将在7.4节中讨论填充预示攻击。

另一种处理安排方式的提案称为先加密再计算MAC(encrypt-then-MAC),最近才被公开提出19。在这种替代方案中,首先对明文和填充进行加密,再将结果交给MAC算法。这可以保证主动网络攻击者不能操纵任何加密数据。

19RFC 7366: Encrypt-then-MAC for TLS and DTLS,https://datatracker.ietf.org/doc/rfc7366/(Peter Gutmann,2014年9月)。

2.5.3 已验证的加密

已验证的密码将加密和完整性验证合二为一,全名是使用关联数据的已验证加密(authenticated encryption with associated data,AEAD)。表面上,它看起来是序列密码和分组密码的交叉。它不用填充20,也不用初始向量,而是使用一个特殊的值,称为nonce(在加密通信中仅使用一次的密钥)。这个值必须唯一。加密过程比使用分组密码要简单一些,如图2-7所示。

20实际上,它们不一定会使用填充。即使它们使用的话,也是实现细节,不会暴露给TLS协议。

{%}

图 2-7 已验证加密

(1) 生成一个唯一的64位nonce。

(2) 使用已验证加密算法加密明文;同时也将序列号和记录标头作为完整性验证依据的额外数据交给算法。

(3) 将nonce和密文一起发送。

已验证加密被认为是当前TLS中可用的加密模式中最好的一种,因为它可以避免MAC-then- encrypt方式带来的问题。虽然TLS当前定义基于GCM和CCM块模式的已验证套件,实际上仅支持GCM套件。基于ChaCha20流密码的全新已验证套件当前正在进行标准化21

21The ChaCha20-Poly1305 AEAD Cipher for TLS,https://datatracker.ietf.org/doc/draft-ietf-tls-chacha20-poly1305/(Langley等,2015年6月)。

2.6 重新协商

大部分TLS连接都以握手作为起点,经过应用数据的交换,最后关闭会话。但如果请求重新协商,就会发起一次新的握手,对新的连接安全参数达成一致。这个功能在以下这些情形下很有用。

  • 客户端证书

    客户端证书并不常用,但因为它可以提供双因素身份验证,所以还是有一些网站在使用它。部署客户端证书有两种方法。你可以要求连接到网站的所有连接都需要客户端证书,但是这种方式对那些(还)没有证书的使用者并不友好;在连接成功以前,你无法向它们发送任何信息和说明。处理错误的情况同样不可能。因此,许多操作员会选择允许连接到网站根路径的连接不携带证书,并设计一个需要提供客户端证书的子区域。当用户打算浏览子区域时,服务器发起重新协商请求,要求客户端提供证书。

  • 隐藏消息

    上述的双步控制方式使客户端证书具备了一个额外的优势:因为第二次握手是加密的,这意味着被动攻击者无法监视协商,而且最关键的是攻击者根本无法观察到客户端证书。这解决了非常重要的潜在隐私问题,因为客户端证书通常包含身份识别信息。比如,Tor协议就是用这种方式进行重新协商的22

  • 改变加密强度

    以前,当网站加密刚刚出现(而且是CPU密集的)的时候,经常可以发现网站将加密配置分成两个级别。你可以默认使用比较弱的加密,而在特定区域使用强加密23。如果要使用客户端证书,则通过重新协商实现这个功能。当你尝试进入网站中更安全的子区域时,服务器将请求更强的安全性。

22Tor Protocol Specification,https://gitweb.torproject.org/torspec.git?a=blob_plain;hb=HEAD;f=tor-spec.txt(Dingledine和Mathewson,检索于2014年6月30日)。

23这种想法本身就是问题。你的加密要么足够安全要么就是不安全。如果你的敌对方可以攻入较弱的配置,他们就可以完全控制牺牲者的浏览器。做到那样的话,他们可以欺骗牺牲者将所有秘密揭示出来(比如密码)。

此外,在以下两种情况下协议需要使用重新协商,不过这两种情况在实际工作中都不大可能发生。

  • 服务网关密码

    20世纪90年代,美国还未允许出口强密码算法,一个称为服务网关密码(Server-Gated Crypto,SGC)的功能使美国供应商能够将强密码算法输出到世界范围,但仅对选定的(主要是金融业的)美国网站有效。浏览器默认使用的是弱加密,遇到特定证书以后升级至强证书。整个升级过程由客户端控制,通过重新协商实现。只有少数选定的CA获得允许签发这种特定证书。密码输出限制于2000年解除,SGC随之废弃。

  • TLS记录的计数器溢出

    TLS内部将数据包装成记录,并为每个记录指定唯一的64位序列号。每当发生记录交换时,序列号就随之增长。一旦序列号接近溢出,协议就会强制执行重新协商。然而,因为这个计数器本身的数字就非常大,所以实践中不太可能出现溢出。

协议允许客户端在任意时间简单地发送新的ClientHello消息请求重新协商,就如同建立一个全新的连接一样,这被称为客户端发起的重新协商(client-initiated renegotiation)。

如果服务器希望重新协商,它会发送HelloRequest协议消息给客户端。这个消息通知客户端停止发送应用数据,并开始新的握手,这被称为服务器发起的重新协商(server-initiated renegotiation)。

正如原本设计的那样,重新协商并不安全,并且可被主动网络攻击者以很多方式滥用。它的弱点于2009年被发现24,然后通过引进renegotiation_info扩展得以修正。我将在本章后面对这个扩展进行讨论。

24要了解更多信息,请参考7.1节。

2.7 应用数据协议

应用数据协议携带着应用消息,只以TLS的角度考虑的话,这些就是数据缓冲区。记录层使用当前连接安全参数对这些消息进行打包、碎片整理和加密。

2.8 警报协议

警报的目的是以简单的通知机制告知对端通信出现异常状况。它通常会携带close_notify异常,在连接关闭时使用,报告错误。警报非常简单,只有两个字段:

struct {
    AlertLevel level;
    AlertDescription description;
} Alert;

AlertLevel字段表示警报的严重程度,可取值warning或者fatalAlertDescription直接表示警报代码。不论这种设计是好是坏,警报都没有表达任意信息的能力,比如实际的错误提示。

严重程度为fatal的消息会立即终止当前连接并使会话失效(相同会话的其他正在进行的连接会继续,但会话绝不可能恢复了)。发送警告通知的一端不会主动终止连接,而是交由接收端通过发送它自己的严重警报对该警告自行作出反应。

2.9 关闭连接

关闭连接警报(closure alert)用于以有序的方式关闭TLS连接。一旦一端决定关闭连接,就会发送一个close_notify警报。另一端收到这个警报以后,会丢弃任何还未写出的数据,并发送自己的close_notify警报。在警报之后到来的任何消息都将被忽略。

这个关闭协议虽然简单,但它可以避免截断攻击,也就是主动攻击者打断通信过程,阻断所有后续消息的攻击,因而是必需的。如果没有关闭协议,通信双方就无法确认是遭到攻击还是通信真正结束。

注意

虽然协议自身不易遭到截断攻击,但是其许多实现却容易遭到攻击,因为连接关闭协议的冲突非常普遍。我将在6.7节中讨论这个问题。

2.10 密码操作

本节会对协议中一些重要的方面进行简单讨论:伪随机函数、构建主密钥和生成连接密钥。

2.10.1 伪随机函数

在TLS中,伪随机函数(pseudorandom function,PRF)用于生成任意数量的伪随机数据。PRF使用一条秘密、一颗种子和一个唯一标签。从TLS 1.2起,所有的算法套件都需要明确指定它们的PRF。所有TLS 1.2套件都使用基于HMAC和SHA256的PRF。以TLS 1.2协商的过往老套件也会使用与此相同的PRF。

TLS 1.2定义的PRF基于数据扩展函数P_hash,这个函数使用了HMAC和一个任意散列函数:

P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
                       HMAC_hash(secret, A(2) + seed) +
                       HMAC_hash(secret, A(3) + seed) + ...

A(i)函数定义如下:

A(1) = HMAC_hash(secret, seed)
A(2) = HMAC_hash(secret, A(1))
...
A(i) = HMAC_hash(secret, A(i-1))

PRF则是结合标签和种子对P_hash的封装:

PRF(secret, label, seed) = P_hash(secret, label + seed)

引入种子和标签允许在不同的环境中重用相同的秘密,而且能够生成不同的输出(因为标签和种子不同)。

2.10.2 主密钥

前面大家已经看到,密钥交换过程的输出是预主密钥。对这个值进行进一步加工,就是使用PRF生成48字节(384位)主密钥:

master_secret = PRF(pre_master_secret, "master secret",
                    ClientHello.random + ServerHello.random)

因为使用不同的密钥交换方法,得到的预主密钥长度可能不同,所以需要执行这个步骤。同时,因为客户端和服务器的随机字段被用作种子,所以主密钥实际上也是随机的25,且与协商握手绑定。

25虽然绝大多数平时常用的密钥交换机制每次都会生成不同的预主密钥,但也有一些机制依赖长期密钥,因此会重用相同的预主密钥。所以必须使用随机化以确保密钥不会重复。

注意

主密钥和握手之间的结合点只是计算主密钥依赖交换的随机数,并已经表现出这是不充分的。攻击者可以观察并复制这些值,从而创建共用相同主密钥的多个会话。这个弱点已被之前提到的三次握手攻击所利用26

26要想了解有关三次握手攻击的更多信息,请参考7.6节。

2.10.3 密钥生成

连接所需的密钥材料是用单一的PRF调用基于主密钥和客户端、服务器的随机数生成的:

key_block = PRF(master_secret, "key expansion",
                server_random + client_random)

密钥块的长度根据协商的参数而有所不同。密钥块分为六个密钥:两个MAC密钥、两个加密密钥和两个初始向量(只在必要时生成;序列密码不会使用IV)。AEAD套件不使用MAC密钥。不同的密钥用于不同的操作,这样可以预防当共享相同密钥时,密钥学基元之间出现不可预见的交互。同样,因为客户端和服务器都拥有各自的一组密钥,由其中一方产生的消息不会被解释成是由另一方产生的。这个设计决策使协议更加可靠。

注意

当恢复会话时,在生成密钥块时使用相同的主密钥,但PRF以当前握手时客户端和服务器的随机值进行种子设定。因为每次握手时的随机值都不同,所以密钥每次也不同。

2.11 密码套件

如你所见,TLS为实现所需的安全属性提供了非常大的灵活性。它是一个创造实际密码协议的框架。虽然以往版本将某些加密基元硬编码到了协议中,但TLS 1.2是完全可配置的。密码套件是一组选定的加密基元和其他参数,它可以精确定义如何实现安全。套件大致由以下这些属性定义。

  • 身份验证方法

  • 密钥交换方法

  • 加密算法

  • 加密蜜钥大小

  • 密码模式(可应用时)

  • MAC算法(可应用时)

  • PRF(只有TLS 1.2一定使用,其他版本取决于各自协议)

  • 用于Finished消息的散列函数(TLS 1.2)

  • verify_data结构的长度(TLS 1.2)

密码套件都倾向于使用较长的描述性名称,并且相当一致:它们都由密钥交换方法、身份验证方法、密码定义以及可选的MAC或PRF算法组合而成27,如图2-8所示。

27TLS套件会使用TLS_前缀,而SSL 3套件使用SSL_前缀,SSL 2套件使用SSL_CK_前缀。在所有场景中,这个命名方法大致都是相同的。可是,并非所有供应商都使用标准套件命名。OpenSSL和GnuTLS就使用不同的命名。Microsoft基本上会使用标准命名,但有时使用前缀扩展它们,以指明ECDHE密钥交换的强度。

{%}

图 2-8 密码套件名称构成

虽然套件名称传达所有安全性参数时并不充分,但借其可以轻松推断出最重要的那些参数。此外,其他参数则可以在RFC上通过套件定义找到。你可以在表2-2中看到少量精选套件的安全性属性。在撰写这本书的时候,已经有超过300种官方密码套件,所以不能全部罗列在这里。如果想得到完整列表,请访问IANA上的TLS官方页面28

28TLS Parameters,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml(IANA,检索于2014年6月30日)。

表2-2 密码套件的名称和其安全性属性的示例

密码套件名称

身份验证

密钥交换

密码

MAC

PRF

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

RSA

ECDHE

AES-128-GCM

SHA256

TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384

ECDSA

ECDHE

AES-256-GCM

SHA384

TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA

RSA

DHE

3DES-EDE-CBC

SHA1

协议

TLS_RSA_WITH_AES_128_CBC_SHA

RSA

RSA

AES-128-CBC

SHA1

协议

TLS_ECDHE_ECDSA_WITH_AES_128_CCM

ECDSA

ECDHE

AES-128-CCM

SHA256

随着TLS 1.2的引进(允许定义额外定制的参数,比如PRF)以及已验证套件的引进,为了全面解析密码套件名称,需要在某种程度上理解其实现。

  • 已验证套件结合了密码学的身份验证和加密,这意味TLS这一层无需执行完整性验证。GCM套件使用名称的最后一段指明使用的PRF算法,而非MAC算法。另外CCM套件则将最后一段完全省略。

  • TLS 1.2是唯一允许套件定义其PRF的协议。对于TLS 1.2以前就已定义的套件,由协商的协议版本控制PRF。以TLS_RSA_WITH_AES_128_CBC_SHA套件为例,当协商后的协议是TLS 1.2时,它使用的PRF是基于HMAC-SHA256;当协议是TLS 1.0时,PRF则是基于HMAC-MD5/HMAC-SHA1。另一方面,SHA384 GCM套件(只在TLS 1.2及更高版本中可用)则会一直使用HMAC-SHA384作为PRF。

注意

密码套件名称使用简化符号表示MAC算法,只指定其散列函数。这经常会在散列函数存在弱点时引发大家的迷惑。比方说,虽然已知SHA1在选择前缀攻击面前是弱点,但它在TLS中却并不存在此弱点,因为它是用在HMAC体系中。目前尚无值得注意的针对HMAC-SHA1的攻击。

密码套件并未完全掌控其安全参数。它们只是定义了最关键的身份验证和密钥交换算法,而对这些算法的实际参数并没有控制能力(比如密钥和参数强度)。

注意

密码套件只能与其预期的特定身份验证机制一起使用。比方说,名称含有ECDSA的套件需要ECDSA密钥。如果一台服务器中只有一个RSA密钥,那么它会展示自己不支持任何ECDSA套件。

对于身份验证,其强度主要依靠证书,更确切地说是证书中的密钥长度和签名算法。RSA密钥交换的强度也依赖证书。可以为DHE和ECDHE密钥交换配置不同的强度,这通常是在服务器级别的配置中完成的。某些服务器将这些配置暴露给最终用户,而另一些不会。我会在第8章及其后的技术性章节中更详细地讨论这些方面。

2.12 扩展

TLS扩展是一种通用目的的扩展机制,使用这种机制可以在不修改协议本身的条件下为TLS协议增加功能。它在2003年作为一个单独的规格说明(RFC 3456)首次出现,但随后即被加入到TLS 1.2中。

扩展以扩展块的形式加在ClientHelloServerHello消息的末尾:

Extension extensions;

扩展块由所需数量的扩展一个个堆叠而成。每一个扩展标头是2字节扩展类型(唯一标志),后接扩展数据:

struct {
    ExtensionType extension_type;
    opaque extension_data;
} Extension;

扩展的格式和期望的行为由每个扩展自己决定。在实践中,扩展通常用于通知支持某些新功能(因此改变了协议),以及用于在握手阶段传递所需的额外数据。自从扩展被引入TLS,它就成为了TLS演进的主要载体。

在本节中,我将讨论最常见的TLS扩展(如表2-3所示)。因为IANA一直保持对扩展类型的跟踪,所以可以从其网站上获得官方的扩展列表29

29TLS Extensions,https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml(IANA,检索于2014年6月30日)。

表2-3 常见的TLS扩展

类型

名称

描述

0

server_name

包含连接欲访问的安全虚拟主机

5

status_request

指示支持OCSP stapling

13 (0x0d)

signature_algorithms

包含支持的签名算法/散列函数对

15 (0x0f)

heartbeat

指示支持心跳协议

16 (0x10)

application_layer_protocol_negotiation

包含客户端希望协商的并且支持的应用层协议

18 (0x12)

signed_certificate_timestamp

服务器用来提交证据,以证明证书已被公众共享;是证书透明度的一部分

21 (0x15)

padding

用于解决F5负载均衡设备a中的特定bug

35 (0x23)

session_ticket

指示支持无状态会话恢复

13172 (0x3374)

next_protocol_negotiation

指示支持次协议协商

65281 (0xff01)

renegotiation_info

指示支持安全重新协商

a A TLS padding extension,https://datatracker.ietf.org/doc/draft-ietf-tls-padding/(Internet-Draft,A. Langley,2015年2月)。

2.12.1 应用层协议协商

应用层协议协商(application layer protocol negotiation,ALPN)协议扩展能够在TLS连接上协商不同的应用层协议30。使用ALPN,一个监听443端口的服务器可以默认提供HTTP 1.1,并允许协商其他协议,比如SPDY或者HTTP 2.0。

30RFC 7301: TLS Application-Layer Protocol Negotiation Extension,http://tools.ietf.org/html/rfc7301(Friedl等,2014年7月)。

支持ALPN的客户端在application_layer_protocol_negotiation扩展中提交自己支持的应用层协议列表给服务器。兼容的服务器会决定使用的协议并使用相同扩展向客户端通知其决定。

ALPN与其老前辈NPN(会在本节中稍后进行讨论)提供的主要功能一致,只是在次要属性上有所不同。NPN偏爱将协议选择结果加密,而ALPN则是以明文传输,使中间设备可以检查它们并根据观察所得信息为流量选择路由。

2.12.2 证书透明度

证书透明度(certificate transparency)31还是一个提案,目的是通过保持所有公开的服务器证书来改进互联网PKI。它的基本想法是CA将每一张证书都提交给一组公开的日志服务器,反过来,这些CA将收到提交的证明,称为已签名证书时间戳(signed certificate timestamp,SCT),并中继给最终用户。有一些选项用来传送SCT,其中之一就是新的TLS扩展signed_certificate_timestamp

31Certificate Transparency,http://www.certificate-transparency.org/(Google,检索于2014年6月30日)。

2.12.3 椭圆曲线功能

RFC 4492提出了两个扩展,可以在握手时通告客户端的EC功能。elliptic_curves扩展在ClientHello中列出支持的曲线名称,使服务器可以在其中选择一条双方都支持的曲线。

struct {
    NamedCurve elliptic_curve_list
} EllipticCurveList;

主要的曲线在RFC 449232中说明。所有的曲线都在标准主体(比如NIST33)之上定义,通过参数来实现:

32RFC 4492: ECC Cipher Suites for TLS,http://tools.ietf.org/html/rfc4492(S. Blake-Wilson等,2006年5月)。

33FIPS 186-3: Digital Signature Standard,http://csrc.nist.gov/publications/fips/fips186-3/fips_186-3.pdf(NIST,2009年6月)。

enum {
    sect163k1 (1), sect163r1 (2), sect163r2 (3),
    sect193r1 (4), sect193r2 (5), sect233k1 (6),
    sect233r1 (7), sect239k1 (8), sect283k1 (9),
    sect283r1 (10), sect409k1 (11), sect409r1 (12),
    sect571k1 (13), sect571r1 (14), secp160k1 (15),
    secp160r1 (16), secp160r2 (17), secp192k1 (18),
    secp192r1 (19), secp224k1 (20), secp224r1 (21),
    secp256k1 (22), secp256r1 (23), secp384r1 (24),
    secp521r1 (25),
    reserved (0xFE00..0xFEFF),
    arbitrary_explicit_prime_curves(0xFF01),
    arbitrary_explicit_char2_curves(0xFF02)
} NamedCurve;

Brainpool曲线在稍后的RFC 702734中定义。另外两条曲线Curve25519和 Curve448,当前正在进行标准化,以在TLS中使用35

34RFC 7027: ECC Brainpool Curves for TLS,http://tools.ietf.org/html/rfc7027(J. Merkle和M. Lochter,2013年10月)。

35urve25519 and Curve448 for TLS,https://datatracker.ietf.org/doc/draft-ietf-tls-curve25519/(S. Josefsson和M. Pegourie-Gonnard,2015年7月)。

现在,只有两种曲线得到了广泛支持:secp256r1secp384r1。一般来讲,根本不可能支持任意曲线36

36生成性质良好且不限数量的椭圆曲线是一项复杂并且很容易出错的工作,大多数开发人员都会选择敬而远之。此外,命名曲线是可以进行优化以使之运行更快速的。

NIST椭圆曲线

NIST椭圆曲线有时会受到怀疑,因为它们没有解释参数是如何选择出来的37。尤其是在Dual EC DRBG后门曝光以后,任何没有解释的曲线都会受到怀疑。大家担心那些命名曲线存在只有设计者才知道而不为大众所知的缺陷。因此,要扩展TLS支持,其他曲线还需要做很多工作。

37SafeCurves: choosing safe curves for elliptic-curve cryptography,http://safecurves.cr.yp.to/(D. J. Bernstein,检索于2014年6月30日)。

另一个定义的扩展是ec_point_formats。这个扩展可以在协商时对椭圆曲线顶点进行可选压缩。理论上,使用压缩的顶点格式可以在受限的环境下节省宝贵的带宽资源,但实际使用中节省的量不大(比如为256位的曲线节约64字节)38,因此压缩格式一般未被使用。

38这里想当然地未提及未压缩时需要的字节数,所以这个例子说明不了什么问题。——译者注

2.12.4 心跳

心跳(Heartbeat)39是一个协议扩展,添加了支持连接保活的功能(检查对端是否仍然可用),以及为TLS和DTLS发现路径最大传输单元(path maximum transmission unit,PMTU)40。虽然TLS通常用于TCP协议之上,而且TCP本身已经具备连接保活的功能,但是心跳的目标定位是DTLS,因为其工作在不可依赖的协议(比如UDP)之上。

39RFC 6520: TLS and DTLS Heartbeat Extension,https://tools.ietf.org/html/rfc6520(R. Seggelmann等,2012年2月)

40最大传输单元(maximum transmission unit,MTU)是能作为一个整体发送的数据最大长度。两端在直接通信时,它们可以交换其MTU。但是,当通信穿越许多跃点以后,有时需要渐进发送更大的数据帧来发现适用于整个路径的MTU。

注意

有些人建议使用0长度的TLS记录(协议允许)来实现连接保活功能。但在试图减轻BEAST攻击的实际过程中显示了大量应用无法容忍没有任何数据的记录。并且,因为PMTU需要各种长度的负载,所以0长度TLS记录对其没有任何帮助。

首先,客户端和服务器通过心跳扩展相互通告支持心跳。在协商过程中,一方通过发送带有HeartbeatMode参数的心跳请求给另一方授权:

struct {
    HeartbeatMode mode;
} HeartbeatExtension;

enum {
    peer_allowed_to_send (1),
    peer_not_allowed_to_send (2)
} HeartbeatMode;

心跳是作为TLS的子协议实现的,这意味着心跳消息可能与应用数据甚至其他协议消息相互交错。按照RFC的说明,只允许在握手完成后才能发送心跳消息,但实际上,OpenSSL在TLS扩展交换完成以后就立即允许其发送。

现在还不清楚生产中有无使用心跳。但是,OpenSSL支持它并且默认启用。GnuTLS也实现了这个扩展。2014年4月以前,几乎没有人知道心跳是什么;在那之后,由于OpenSSL实现遭受针对其一个严重弱点的攻击,使得敏感数据从服务器进程的内存空间中外泄,人们才发现心跳的存在。那次利用这一漏洞的攻击被称为心脏出血(Heartbleed),也许是发生在TLS上的最严重的攻击。你可以在6.3节中阅读到更多相关信息。

2.12.5 次协议协商

当Google开始设计SPDY41(这个协议将比HTTP有所提升)时,它需要一种可靠的协议协商机制,可以与严格的防火墙和有问题的代理一起工作。因为SPDY本来就会使用TLS,所以它们决定为TLS提供一个协商应用层协议的扩展。其最终结果是次协议协商(next protocol negotiation,NPN)。

41SPDY,https://en.wikipedia.org/wiki/SPDY(维基百科,检索于2014年6月30日)。

注意

如果你调查NPN,可能会碰到许多不同版本的规格说明书。其中一些版本是TLS工作组在标准化讨论时生成的,而生产中使用的是一个更老的版本42

42Google Technical Note: TLS Next Protocol Negotiation Extension,https://web.archive.org/web/20150529234248/ https://technotes.googlecode.com/git/nextprotoneg.html(Adam Langley,2012年5月)。

启用SPDY支持的客户端提交的TLS握手中集成了一个空的next_protocol_negotiation扩展,但这仅在握手中也包含server_name扩展表明其需要访问的主机名时才行。兼容的服务器会返回含有next_protocol_negotiation扩展的响应,这次会包含服务器所支持的应用层协议的列表。

客户端通过一个新的握手消息NextProtocol表明期望的应用层信息:

struct {
    opaque selected_protocol;
    opaque padding;
} NextProtocol;

为了避免被动攻击者得到客户端的选择,提交的消息被加密,这意味着客户端必须在ChangeCipherSpec消息以后才能发送它,而且还要在Finished消息之前。这就与标准握手的消息流有所不同。客户端不仅可以从服务器提供的列表中选择所期望的协议名称,而且也可以自由提交不在服务器通知中的协议。扩展会使用填充,这样可以隐藏其真实长度,这样敌对方无法通过观察加密消息的长度猜测选择的协议。

NPN已提交到TLS工作组,希望成为标准43。但即使其在业界得到广泛支持(比如Chrome、Firefox和OpenSSL),它仍然未被接受。引入新的握手消息,改变通常的握手流程,被认定是破坏性的并且过度复杂。大家也担心其不具备让中间设备了解协商的协议的能力,并且在实践中可能也存在问题。最终,工作组采用的是与之竞争的ALPN提案44。Google现在同时支持ALPN和NPN,而且将在未来切换到只支持ALPN45

43Next Protocol Negotiation 03,http://www.ietf.org/mail-archive/web/tls/current/msg08678.html(Adam Langley,2012年4月24日)。

44Some missing context (was: Confirming consensus for ALPN),http://www.ietf.org/mail-archive/web/tls/current/msg09344.html(Yoav Nir,2013年3月15日)。

45NPN and ALPN,https://www.imperialviolet.org/2013/03/20/alpn.html(Adam Langley,2013年3月20日)。

2.12.6 安全重新协商

renegotiation_info扩展以验证重新协商的双方仍是先前完成握手的两个团体方式来改进TLS。

开始(在某个连接的第一次握手期间),这个扩展用于双方相互通知对方自己支持安全重新协商;为了做到这一点,他们简单地发送不带数据的扩展。SSL 3不支持扩展,为了使其支持这种安全性,作为替代,客户端会发送特殊的通知套件TLS_EMPTY_RENEGOTIATION_INFO_SCSV (0xff)

在后续的握手中,扩展用于提交先前握手的一些信息作为证明。客户端以先前的Finished消息作为verify_data的值发送出去。而服务器会发送两个值:首先是客户端的verify_data,接下来是服务器自己的。因为Finished消息总是加密的,所以攻击者得不到这些值。

2.12.7 服务器名称指示

服务器名称指示(server name indication,SNI)通过server_name扩展实现46,它可以为客户端提供一种机制,利用这种机制客户端可以告知服务器它希望与之建立连接的服务器的名称。换言之,这个扩展为安全虚拟主机提供支持:它为服务器提供足够的信息,使之可以在所有可用的安全虚拟主机中寻找到匹配的证书。如果没有这种机制,每个IP地址上只能部署一张证书47。因为SNI在TLS中添加得比较晚(2006),所以仍然存在很多陈旧的产品(比如Windows XP和一些早期安卓版本)不支持这个扩展。因此,为大量受众提供开放站点建设服务的虚拟安全托管方案仍然不现实。

46RFC 6066: TLS Extensions: Extension Definitions,http://tools.ietf.org/html/rfc6066(D. Eastlake 3rd,2011年1月)。

47虽然HTTP也具有通过Host请求头发送主机信息的能力,但这是在应用层发送的,只能在TLS握手成功以后才能通告至服务器。

2.12.8 会话票证

会话票证(session ticket)引入了一种新的会话恢复机制,这种机制不需要任何服务器端存储48。其思想是服务器取出它的所有会话数据(状态)并进行加密,再以票证的方式发回客户端。在接下来的连接中,客户端将票证提交回服务器,由服务器检查票证的完整性,解密其内容,再使用其中的信息恢复会话。这种方法有可能使扩展服务器集群更为简单,因为如果不使用这种方式,就需要在服务集群的各个节点之间同步会话。

48RFC 5077: TLS Session Resumption without Server-Side State,http://tools.ietf.org/html/rfc5077(Salowey等,2008年1月)。

警告

会话票证破坏了TLS安全模型。它使用票证密钥加密的会话状态并将其暴露在线路上。有些实现中的票证密钥可能会比连接使用的密码要弱。比如,OpenSSL使用128位的AES密钥。同时,相同的票证密钥在许多会话中重用,这就与RSA密钥交换的情形类似,无法提供前向保密;如果票证密钥被暴露,就可以解密连接上的全部数据。因此,使用会话票证时,票证密钥需要频繁轮换。

客户端可以使用一个空的会话票证扩展指示支持这种恢复方式。如果它希望恢复此前的会话,它应该将票证放置在扩展中代替空值。兼容服务器如希望发起一个新的票证,则应在其ServerHello中包含一个空session_ticket扩展。之后服务器便会等待客户端的Finished消息,验证它,将票证置入NewSessionTicket握手消息中发回客户端。如果服务器希望恢复此前的会话,它会按照简短握手的方式响应客户端,并且按照标准的会话恢复方式处理自身逻辑。

注意

当服务器决定使用会话票证进行会话恢复时,它会发回一个空的会话ID字段(在其ServerHello消息中)。就这一点来说,这个会话不具有唯一的标识。然而,票证规格说明允许客户端在后续使用票证的握手中选择并提交一个会话ID(在其ClientHello中)。服务器如果接受票证,那么也必须以同样的会话ID作为响应。这就是为什么即使使用会话票证作为会话恢复机制,TLS Web服务器日志中也出现会话ID的原因。

2.12.9 签名算法

signature_algoritms扩展是在TLS 1.2中定义的。它使客户端可以通告自己支持的各种签名和散列算法。TLS规格说明书中列出了RSA、DSA和ECDSA签名算法,以及MD5、SHA1、SHA224、SHA256、SHA384和SHA512这些散列算法。使用signature_algorithms扩展,客户端可以提交其支持的签名—散列算法对。

这个扩展是可选的;如果未设置,服务器会根据客户端提供的密码套件推断其支持的签名算法(比如RSA套件可以推断出RSA签名,ECDSA套件支持ECDSA,等等),而且假定其支持SHA1。

2.12.10 OCSP stapling

客户端使用status_request扩展49指示支持OCSP stapling。服务器使用这个特性发送最新的证书吊销信息给客户端。(我会在5.10节对吊销进行详细讨论)。支持OCSP stapling的服务器在其ServerHello中返回一个空的status_request扩展,并在Certificate消息之后紧跟一条CertificateStatus握手消息,将OCSP响应(使用DER格式)包含在这条消息中。

49RFC 6066: TLS Extensions: Extension Definitions,http://tools.ietf.org/html/rfc6066(D. Eastlake 3rd,2011年1月)。

OCSP stapling只支持一个OCSP响应,只能用于检测一张服务器证书的吊销状态。这个限制在RFC 696150中得到了解决,因为它添加了对多个OCSP响应的支持(使用status_request_v2扩展来表明对它的支持)。但是,直到现在,这个改进版仍然没有得到客户端和服务器软件的完善支持。

50RFC 6961: TLS Multiple Certificate Status Request Extension,http://tools.ietf.org/html/rfc6961(Y. Pettersen,2013年6月)。

2.13 协议限制

因为TLS在OSI层次中的定位和某些设计安排,除了那些偶然的弱点(那些弱点我将在后续章节中详细讨论)之外,当前它仍有几处众所周知的限制。

  • 加密保护TCP连接的内容,但TCP和所有其他更低层的协议的元数据仍然是明文传输。因此,被动观察者可以确定源和目标的IP地址。这类信息的泄露不是TLS的责任,而是我们当前的分层网络模型导致的限制。

  • 即使在TLS层,也有很多信息以明文形式暴露出去。第一次握手一定是非加密的,可以让被动观察者:(1) 了解客户端的功能,并使用其作为指纹;(2) 检查SNI信息确定期望访问的虚拟主机;(3) 检查主机证书,以及何时会使用客户端证书;(4) 存在得到足够信息以识别用户身份的可能性。这些问题有方法可以避免,但是这些方法都没有被主流实现所采用。

  • 当启动加密以后,某些协议信息仍能被清楚地探查到:观察者可以了解到子协议和每条消息的长度。根据不同的协议,这些长度可以揭示某些底层通信的线索。比如,有一些研究尝试根据指明的请求和相应长度推断HTTP访问的是哪些资源。如果没有隐藏长度,不可能安全地在加密之前使用压缩(现在最常见的实践方法)。

网络层元数据的泄露只能在网络层解决。而其他限制是可以修复的,确实有一些解决问题的提案和讨论。你可以在本书后面的部分对这些问题有更多了解。

2.14 协议版本间的差异

本节描述SSL和TLS协议的各版本之间的主要差别。自从SSL 3以来,协议核心并没有大幅改变。TLS 1.0为了迎合使用另一个名称进行了有限的改变,发布TLS 1.1的首要目标是为了解决几个安全性问题。TLS 1.2引入了已验证加密,清理了散列,另外去掉了协议中的硬编码基元。

2.14.1 SSL 3

SSL 3于1995年年末发布。为了弥补先前协议版本的诸多弱点,SSL 3从头开始设计了一套协议,并一直沿用到了最新版本的TLS。如果你想更好地了解SSL 3作出了哪些改变以及作出改变的原因,我推荐Wagner和Schneier的协议分析论文51

51Analysis of the SSL 3.0 protocol,https://www.schneier.com/paper-ssl-revised.pdf(David Wagner和Bruce Schneier,Proceedings of the Second USENIX Workshop on Electronic Commerce,1996年)。

2.14.2 TLS 1.0

TLS 1.0于1999年1月发布。与SSL 3相比,它包含了以下改进。

  • 这是定义基于标准HMAC的PRF的第一个版本。它将PRF以HMAC-MD5和HMAC-SHA的结合(XOR)实现。

  • 生成主密钥使用PRF,而不是定制的构造方法。

  • verify_data的值基于PRF,而不是定制的构造方法。

  • 使用官方HMAC作为完整性验证(MAC)。SSL 3使用的是更早的、已被废弃的HMAC版本。

  • 修改填充格式,使其更为可靠。2014年10月,被称为POODLE的攻击暴露了SSL 3的填充机制不安全。

  • 去掉了FORTEZZA52套件。

52Fortezza,https://en.wikipedia.org/wiki/Fortezza(维基百科,检索于2014年6月30日)。

协议清理的结果是TLS 1.0得到了FIPS的批准,允许其用于美国政府机构,这是现实中一个非常重要的事件。

如果你想研究TLS 1.0和之前版本的协议,我推荐Eric Rescorla的SSL and TLS: Designing and Building Secure Systems一书(Addison-Wesley,2001年出版)。我发现这本书对于理解TLS某些决定背后的理由,以及跟进其设计的演变,有着非常宝贵的价值。

2.14.3 TLS 1.1

TLS 1.1于2006年4月发布。与TLS 1.0相比,它包含以下主要改进。

  • CBC加密使用包含在每个TLS记录中的显式IV。这弥补了IV可预测的弱点。不然这个弱点后面会被BEAST攻击所利用。

  • 为了抵抗填充攻击,要求实现使用bad_record_mac警报作为填充问题的响应。不再赞成使用decryption_failed警报。

  • 这个版本引用包含了TLS扩展(RFC 3546)。

2.14.4 TLS 1.2

TLS 1.2于2008年8月发布。与TLS 1.1相比,它包含以下主要改进。

  • 添加已验证加密支持。

  • 添加对HMAC-SHA256密码套件的支持。

  • 删除IDEA和DES密码套件。

  • 虽然大部分扩展的实际文档还是在其他地方,但TLS将扩展和协议的主规格说明书进行了集成。

  • 客户端可以使用一种新的扩展(signature_algorithms)来通报它愿意接受的散列和签名算法。

  • 当使用TLS 1.2套件或者以协商协议是TLS 1.2为条件使用之前的套件时,在PRF中使用SHA256代替MD5/SHA1组合。

  • 允许密码套件定义其自身的PRF。

  • 使用单一散列代替用于数字签名的MD5/SHA1组合。默认使用SHA256,并且密码套件可以指定其自身使用的散列。签名散列算法以往是由协议强制指定,现在是散列函数式签名结构中的一部分,而且在实施启用中可以选择最佳算法。

  • 密码套件可以显式指定Finished消息中的verify_data成员的长度。

目录

  • 版权声明
  • 前言
  • 第 1 章 SSL、TLS和密码学
  • 第 2 章 协议
  • 第 3 章 公钥基础设施
  • 第 4 章 攻击PKI
  • 第 5 章 HTTP和浏览器问题
  • 第 6 章 实现问题
  • 第 7 章 协议攻击
  • 第 8 章 部署
  • 第 9 章 性能优化
  • 第 10 章 HTTP严格传输安全、内容安全策略和钉扎
  • 第 11 章 OpenSSL
  • 第 12 章 使用OpenSSL进行测试
  • 第 13 章 配置Apache
  • 第 14 章 配置Java和Tomcat
  • 第 15 章 配置Microsoft Windows和IIS
  • 第 16 章 配置Nginx
  • 第 17 章 总结