跳到主要内容

TLS1.0/1/2

前置基本概念

Transport Layer Security 协议实现互联网 客户端/服务端 通信的 反监听、防篡改和防伪装。它在传输层之上,保证传输层的安全。

理解TLS,应该提前理解 Connection states。TLS连接状态指明了 压缩算法、加密算法和数字签名算法及其相关的参数(比如协商得到的主密钥)。伪代码表示如下:

struct {
// 连接终端
// enum { server, client } ConnectionEnd;
ConnectionEnd entity;

// 数据加密相关
// 加密算法
// enum { null, rc4, rc2, des, 3des, des40 } BulkCipherAlgorithm;
BulkCipherAlgorithm bulk_cipher_algorithm;
// 加密类型
// enum { stream, block } CipherType;
CipherType cipher_type;
// 密钥的长度
uint8 key_size;
uint8 key_material_length;
// enum { true, false } IsExportable;
IsExportable is_exportable;

// 消息认证相关
// 认证算法
// enum { null, md5, sha } MACAlgorithm;
MACAlgorithm mac_algorithm;
// 返回的code长度
uint8 hash_size;

// 压缩算法
// enum { null(0), (255) } CompressionMethod;
CompressionMethod compression_algorithm;

// 协商的结果,应用数据加密的密钥
opaque master_secret[48];
opaque client_random[32];
opaque server_random[32];
} SecurityParameters;

这里提前引出加密套件(Cipher Suite)的概念。加密套件是TLS协议中用来约定客户端和服务端通信的信息,由一系列的密码基元组成,指导协商和后续的数据传输。

举例说明,在 1.2版本里面有如下密码套件:

Cipher Suite                            Key        Cipher         Mac
Exchange
----------------------------------- -------- -------------- -------
TLS_RSA_WITH_AES_256_CBC_SHA256 RSA AES_256_CBC SHA256
TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA DH_RSA 3DES_EDE_CBC SHA
TLS_DH_anon_WITH_AES_128_CBC_SHA256 DH_anon AES_128_CBC SHA256

其格式的为:

TLS_{密钥协商算法[_身份验证算法_]}_WITH_{数据加密算法}_{消息认证码算法}

身份验证算法指的是证书中包含的服务器公钥的算法。并不是证书本身的签名算法。

各个阶段可能的算法有:

Key
Exchange
Algorithm Description Key size limit
--------- -------------------------------- --------------
DHE_DSS Ephemeral DH with DSS signatures None
DHE_RSA Ephemeral DH with RSA signatures None
DH_anon Anonymous DH, no signatures None
DH_DSS DH with DSS-based certificates None
DH_RSA DH with RSA-based certificates None
RSA = none
NULL No key exchange N/A
RSA RSA key exchange None

Key IV Block
Cipher Type Material Size Size
------------ ------ -------- ---- -----
NULL Stream 0 0 N/A
RC4_128 Stream 16 0 N/A
3DES_EDE_CBC Block 24 8 8
AES_128_CBC Block 16 16 16
AES_256_CBC Block 32 16 16


MAC Algorithm mac_length mac_key_length
-------- ----------- ---------- --------------
NULL N/A 0 0
MD5 HMAC-MD5 16 16
SHA HMAC-SHA1 20 20
SHA256 HMAC-SHA256 32 32

TLS本身分两层:

  • TLS Handshake Protoco 完成 连接状态 的 创建/恢复。由三个子协议组成:Handshake protocol、Change cipher spec protocol、Alter procotol,完成服务端和客户端的认证、在应用层协议传输数据之前完成加密算法、密钥的协商
    • 对端的身份认证可用使用 非对称算法,虽然是可选,但是绝大多数时候至少会有一端被要求
    • 共享密钥的协商是安全的,没有第三方可以获取
    • 协商是可靠的:没有攻击者能够在不被通信双方觉察到的情况下修改协商通信
  • TLS Record Protocol 根据 连接状态 传输数据。在可靠传输协议(TCP)之上,提供私密(数据被对称加密)、可靠(数据会使用MAC进行一致性校验)的连接

总体而言,相关协议的关系为:

|-----------------------------------------------------------|
| |
| |------------------------------------------------|------| |
| | |---------------------------|-------------- | | |
| | | handshake protocol + | application | | | |
| | | change spec protocol + | data | | | |
| | | alert protocol | protocol | | | |
| | |----↑ TLS Handshke Protocl |-------------| | | |
| | | | |
| | |------------------------------------------ | | |
| | | | | | |
| | | TLS Record Protocl | | | |
| | | | | HTTP | |
| | |-----------------------------------------| | ... | |
| | | | |
| |----------------↑ TLS protocol -----------------| | |
| |--------- (TLS protocol + HTTP = HTTPS) --------|------| |
| |
|---------------------------------------↑ Application Layer |
----------------------------------------------------------------------|
| | |
| TCP | ... |
| | |
----------------------------------------------------↑ Transport Layer |

TLS握手协议

下文大部分是使用tls1.0规范。

握手阶段能够协商出连接中使用的 一系列的算法、一个主密钥、客户端和服务端随机参数,进而完成认证、加密和数字签名。

TLS全握手流程

全握手的消息流如下:

  Client                                               Server

1 ClientHello -------->
ServerHello 2
Certificate* 3
ServerKeyExchange* 4
CertificateRequest* 5
<-------- ServerHelloDone 6
7 Certificate*
8 ClientKeyExchange
9 CertificateVerify*
10 [ChangeCipherSpec]
11 Finished -------->
[ChangeCipherSpec] 12
<-------- Finished 13


14 Application Data <-------> Application Data

Fig. 1 - Message flow for a full handshake

* 代表可选

(copy from RFC-2246)

进一步说明:

步骤1 [客户端 ClientHello]客户端主动发起握手,发送 ClientHello。数据格式伪代码如下:

struct {
uint32 gmt_unix_time;
opaque random_bytes[28];
} Random;

struct {
uint8 major, minor;
} ProtocolVersion;

uint8 CipherSuite[2];

struct {
// 客户端期望使用的协议版本,值为其支持的最高的版本
ProtocolVersion client_version;

// 客户端生成的随机数
Random random;

// 客户端希望当前连接的ID
// 如果没有就为空
// 如果有,说明希望会话恢复。
SessionID session_id;

// 客户端支持的加密套件列表。如果希望会话恢复,需要传递上一次会话一样的值
CipherSuite cipher_suites<2..2^16-1>;

// 客户端支持的压缩算法列表。如果希望会话恢复,需要传递上一次会话一样的值
CompressionMethod compression_methods<1..2^8-1>;
} ClientHello;

步骤2 [服务端 ServerHello]服务端收到后,会回复 ServerHello。数据格式伪代码如下:

struct {
// 并客户端建议低的服务端最高支持的版本
ProtocolVersion server_version;

// 服务端生成的随机数
Random random;

// 连接的会话ID
// 如果客户端传递了,而且会话缓存也有效,服务端返回相同的值,
// 不然就是不同的值,表示新的会话
// 服务端也可能返回空,表示不希望会话被缓存
SessionID session_id;

// 服务端从客户端支持的列表中挑选出来的 加密套件
CipherSuite cipher_suite;

// 服务端从客户端支持的列表中挑选出来的 压缩算法
CompressionMethod compression_method;
} ServerHello;

步骤3 [服务端 Certificate*]如果加密套件要求证书认证,服务端紧接着会发送证书。数据格伪代码如下:

opaque ASN.1Cert<1..2^24-1>;

struct {
ASN.1Cert certificate_list<0..2^24-1>;
} Certificate;

协商结果如果是 *_anno_* (anonymous) 就不会执行这步。因为中间人攻击,这类套件已经被废弃了。

证书相关内容 见 安全问题章节。

步骤4 [服务端 ServerKeyExchange*]如果证书包括的信息不足以进行预主密钥交换,服务端紧接着发送本子消息。 这个消息和选中的加密套件中的密钥协商算法有关,主要用来给客户端传输预主密钥。包括如下两种情况:

  • RSA的公钥,客户端生成预主密钥后,用公钥加密预主密钥后继续传输
  • Diffie-Hellman的参数,客户端可以完成密钥交换
struct {
// The modulus of the server's temporary RSA key.
opaque rsa_modulus<1..2^16-1>;
// The public exponent of the server's temporary RSA key.
opaque rsa_exponent<1..2^16-1>;
} ServerRSAParams;

struct {
// The prime modulus used for the Diffie-Hellman operation.
opaque dh_p<1..2^16-1>;
// The generator used for the Diffie-Hellman operation.
opaque dh_g<1..2^16-1>;
// The server's Diffie-Hellman public value (g^X mod p).
opaque dh_Ys<1..2^16-1>;
} ServerDHParams; /* Ephemeral DH parameters */

// enum { anonymous, rsa, dsa } SignatureAlgorithm;
select (SignatureAlgorithm)
{ case anonymous: struct { };
case rsa:
digitally-signed struct {
// MD5(ClientHello.random + ServerHello.random + ServerParams);
opaque md5_hash[16];
// SHA(ClientHello.random + ServerHello.random + ServerParams);
opaque sha_hash[20];
};
case dsa:
digitally-signed struct {
opaque sha_hash[20];
};
} Signature;

struct {
// enum { rsa, diffie_hellman } KeyExchangeAlgorithm;
select (KeyExchangeAlgorithm) {
case diffie_hellman:
ServerDHParams params;
Signature signed_params;
case rsa:
ServerRSAParams params;
Signature signed_params;
};
} ServerKeyExchange;

这里面有两个问题:

  • 预主密钥的生成有两种方式,一个是客户端直接生成。这个会有不满足向前保密(攻击者将历史密文存储,有朝一日拿到了服务端的私钥,主密钥就被破解了,所有的历史消息也被破解了);另一种就是用DH算法,算法相关内容见 “安全问题” 相关的讨论。
  • 预主密钥和主密钥的关系。主密钥就是在后续传输过程中进行对称加解密用到的密钥。那预主密钥如何生成主密钥呢?
// PRF:  pseudo-random function。伪随机函数。入参是 私钥、种子、身份标签,得到的是一个固定长度的字节数组
// pre_master_secret
// 如果是RSA,则使用客户端内容解密后得到预主密钥
// 如果是DH,则使用DH算法,其 协商Key(Z) 就是主密钥
master_secret = PRF(pre_master_secret, "master secret",
ClientHello.random + ServerHello.random)
[0..47];

步骤5 [服务端 CertificateRequest*]如果需要的话,服务端会马上给客户端发送证书请求的消息。消息格式如下:

struct {
// enum { rsa_sign(1), dss_sign(2), rsa_fixed_dh(3), dss_fixed_dh(4), (255) } ClientCertificateType;
// 按照服务端喜好排序的一系列的证书类型的请求
ClientCertificateType certificate_types<1..2^8-1>;

// opaque DistinguishedName<1..2^16-1>;
// 可接受的证书机构(CA)的名字
DistinguishedName certificate_authorities<3..2^16-1>;
} CertificateRequest;

步骤6 [服务端 ServerHelloDone]服务端一定会发送 Server hello done 消息,来表明服务端的消息发完了,等待客户端响应。其消息格式为空。

struct { } ServerHelloDone;

步骤7 [客户端 Certificate*] 如果服务端发送了请求证书的消息。数据结构和第5步一样。

步骤8 [客户端 ClientKeyExchange]客户端总是会发送 密钥交换消息。对应服务端选中的加密套件,一般有两种情况:

  • 客户端生成了预主密钥,将其使用服务端的公钥加密后得到的内容
  • 客户端生成 DH算法的客户端参数

其数据结构为:

struct {
// The latest (newest) version supported by the client. This is
// used to detect version roll-back attacks. Upon receiving the
// premaster secret, the server should check that this value
// matches the value transmitted by the client in the client
// hello message.
ProtocolVersion client_version;

// 46 securely-generated random bytes.
opaque random[46];
} PreMasterSecret;

struct {
public-key-encrypted PreMasterSecret pre_master_secret;
} EncryptedPreMasterSecret;

struct {
// enum { implicit, explicit } PublicValueEncoding;
select (PublicValueEncoding) {
case implicit: struct { };
// The client's Diffie-Hellman public value (Yc).
case explicit: opaque dh_Yc<1..2^16-1>;
} dh_public;
} ClientDiffieHellmanPublic;


struct {
select (KeyExchangeAlgorithm) {
case rsa: EncryptedPreMasterSecret;
case diffie_hellman: ClientDiffieHellmanPublic;
} exchange_keys;
} ClientKeyExchange;

步骤9 [客户端 CertificateVerify*]如果客户端的证书被请求了,且客户端证书具有签名能力(所有证书,除了包含静态的Diffie-Hellman参数的证书)。将前面所有的信息组装到一起,进行签名后发送。签名算法同 Server key exchange message。

步骤10 [客户端 ChangeCipherSpec] 客户端会主动发送这个消息。这个消息不是握手协议的,而是一个单独的子协议。表明客户端已经完成了所有的协商信息同步了(这时候双方应该都能计算出预主密钥、主密钥及其参数),后续所有的消息都需要使用TLS记录层协议加密保护了。消息格式伪代码:

struct {
enum { change_cipher_spec(1), (255) } type;
} ChangeCipherSpec;

整个过程还有 连接状态 的概念,其实也很好理解,握手完成后,连接状态从 待读状态/待写状态 切换为 可读状态/可写状态。

步骤11 [客户端 Finished]客户端总是会发送本消息。它会把迄今为止所有的消息都组装到一起,然后签名。数据格式伪代码如下:

struct {
// verify_data =
// PRF(master_secret, finished_label, MD5(handshake_messages) +
// SHA-1(handshake_messages)) [0..11];
// finished_label
// For Finished messages sent by the client, the string "client
// finished". For Finished messages sent by the server, the
// string "server finished".
// handshake_messages
// All of the data from all handshake messages up to but not
// including this message. This is only data visible at the
// handshake layer and does not include record layer headers.
opaque verify_data[12];
} Finished;

步骤12 [服务端 ChangeCipherSpec] 服务端也会发送本消息,含义和数据结构同客户端

步骤13 [服务端 Finished] 服务端也会发送本消息,含义和数据结构同客户端

TLS快速握手

快速握手要解决握手耗时和握手CPU开销的问题。

Client                                                Server

ClientHello -------->
ServerHello
[ChangeCipherSpec]
<-------- Finished
[ChangeCipherSpec]
Finished -------->
Application Data <-------> Application Data

Fig. 2 - Message flow for an abbreviated handshake

理解了全握手,就比较容易快速握手了。当ClientHello 中携带了的 SessionID 服务端任务可以使用,服务端会直接回复一个ServerHello,其中的SessionID 的内容就是客户端传输过来的值。后续所有的数据就开始用 连接状态 的约定开始保护传输。

其他协议

握手协议-报警协议

Alter protocol 是指连接的某一方给另外一方发送的报警信息,协议格式如下:

struct {
// enum { warning(1), fatal(2), (255) } AlertLevel;
AlertLevel level;
AlertDescription description;
} Alert;
  • 对于fatal级别的错误,连接应该被关闭,也不应该被复用
  • 报警协议被要求被缴满和压缩传输,但是又归类为握手协议的子协议感觉不太合理(The TLS Handshake Protocol consists of a suite of three sub-protocols: change cipher spec protcol, handshake protocol, alert protocol

应用数据协议

应用数据消息已经被分帧、压缩和加密,由记录层传输。(这个协议看起来并没有什么用,RFC也只有一个段落描述)

记录协议

记录层接收上层的任意长度的非空的内容,经过加密等处理后交给下层协议传输。其消息格式如下:

|--------------------|--------------------|
| 类型 | 版本 | 长度 | |
|--------------------| |
| 标 头 | 数 据 |
|-----------------------------------------|

相关的格式的伪代码为:

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

struct {
uint8 major, minor;
} ProtocolVersion;

struct {
ContentType type;
ProtocolVersion version;
uint16 length;
// The application data. This data is transparent and treated as an
// independent block to be dealt with by the higher level protocol
// specified by the type field.
opaque fragment[TLSPlaintext.length];
} TLSPlaintext;

TLSCompressed 格式和明文一模一样,TLSCiphertext 的数据格式和明文很类似:

struct {
ContentType type;
ProtocolVersion version;
uint16 length;
select (CipherSpec.cipher_type) {
case stream: GenericStreamCipher;
case block: GenericBlockCipher;
} fragment;
} TLSCiphertext;
  • 数据先会使用协商的压缩算法进行压缩。得到的 TLSCompressed 数据格式还是和压缩前的一样,但是长度和数据发生了改变。
  • 数据接着会用协商好的签名算法进行签名,追加到 TLSCompressed 后面。
  • 数据接着是加密,转换为 TLSCiphertext

加密和签名的相关的参数生成逻辑为:

首先生成 key_block:
key_block = PRF(SecurityParameters.master_secret,
"key expansion",
SecurityParameters.server_random +
SecurityParameters.client_random);
(master_secret 由 pre_master_secret 派生而来)

进一步生成各种数据发生期间需要的参数:
client_write_MAC_secret = key_block[SecurityParameters.hash_size]
server_write_MAC_secret = key_block[SecurityParameters.hash_size]

client_write_key = key_block[SecurityParameters.key_material_length]
server_write_key = key_block[SecurityParameters.key_material_length]
// 对于 可导出的加密算法,还需要进一步处理:
final_client_write_key = PRF(SecurityParameters.client_write_key,
"client write key",
SecurityParameters.client_random +
SecurityParameters.server_random);
final_server_write_key = PRF(SecurityParameters.server_write_key,
"server write key",
SecurityParameters.client_random +
SecurityParameters.server_random);

// IV 只在非可导出块(非流)算法中需要生成
iv_block = PRF("", "IV block", SecurityParameters.client_random +
SecurityParameters.server_random);
client_write_IV = iv_block[SecurityParameters.IV_size]
server_write_IV = iv_block[SecurityParameters.IV_size]

tls各版本间的差别

tls1.0 vs. tls1.1

tls1.0 rfc 发布在1999年, tls1.1 rfc 发布在 2006年。tls1.1 对 tls1.0做了比较小的安全提升,大概为:

  • 使用显式IV替代隐式IV,避免 CBC攻击
  • 使用 bad_record_mac 替代 decrytion_failed 报警,避免 CBC攻击
  • ...

其差别可以查阅 RFC tls1.1

TLS扩展

TLS 扩展在 2003年被提出(RFC3546)并加入到后续的一系列版本中。

单个扩展的数据格式比较简单:

struct {
ExtensionType extension_type;
opaque extension_data<0..2^16-1>;
} Extension;

扩展后的ClientHello的数据格式为:

struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
CipherSuite cipher_suites<2..2^16-1>;
CompressionMethod compression_methods<1..2^8-1>;
Extension client_hello_extension_list<0..2^16-1>;
} ClientHello;

ClientHello 加上 Extension后还能正常工作的原因是 tls1.0 在设计的时候考虑到了向前兼容性的问题,做了如下规定:In the interests of forward compatibility, it is permitted for a client hello message to include extra data after the compression methods.

扩展后的ServerHello的数据格式为:

struct {
ProtocolVersion server_version;
Random random;
SessionID session_id;
CipherSuite cipher_suite;
CompressionMethod compression_method;
Extension server_hello_extension_list<0..2^16-1>;
} ServerHello;

服务端就可能根据自己的情况去回复扩展或者忽略扩展内容。

扩展的类型有:

  • server_name(0):客户端期望连接的服务端的名字。就是常用的 Server Name Indication。
  • max_fragment_length(1):客户端期望协商得到一个更小的明文帧长度(默认是2^14)
  • client_certificate_url(2):
  • trusted_ca_keys(3)
  • truncated_hmac(4):
  • status_request(5):安全问题中提到的 "ocsp stapling"

其他RFC陆陆续续的添加了新的扩展,常见的有:

  • session_ticket:表明支持没有状态的会话恢复。取代SessionID的一种方案。SessionID要求服务端存储连接状态相关的信息,SessionTicket则是将会话状态加密后发给客户端。当会话恢复时直接解密即可。
  • next_protocol_negotiation:表明支持NPN。
  • application_layer_protocol_negotiation:表明支持的应用层协议。这个是用来询问服务端是否支持HTTP/2。如果支持服务端会在扩展中回复,不然就是只支持HTTP/1.1
  • renegotiation_info:表明可以支持安全的重协商。这个是为了解决重协商(在现有的tls连接上再建立一个更安全的连接)里面的一个安全漏洞,不展开描述。

tls1.1 vs. tls1.2

tls1.1 rfc 发布在 2006年, tls1.2 rfc 发布在 2008年。tls1.2 对 tls1.1 做了比较大的安全提升,大概为:

  • 移除和新加了一些列的加密套件
  • 支持了部分扩展
  • ...

其差别可以查阅 RFC tls1.2

PSK

在tls1.2中使用了 PSK Pre-Shared Key 加密套件的概念,用来基于 PSKs 来支持认证(其他的认证方式有基于PKI 和 Kerberos)。

比如加密套件包括:

CipherSuite                        Key Exchange  Cipher       Hash

TLS_PSK_WITH_RC4_128_SHA PSK RC4_128 SHA

pre-shared key 是对称密钥,事先在通信双方完成共享。套件分为三类:

  • PSK key exchange algorithm:用来对称密钥完成认证
  • DHE_PSK key exchange algorithm:使用含有pre-shared key 的 Dffie-Hellman 交换认证
  • RSA_PSK key exchange algorithm:结合服务端公钥认证 和 客户端pre-shared key认证

细节本文不展开,只说明PSK用来完成认证、和公钥证书类似。

tls1.2 vs. tls1.3

tls1.2 rfc 发布在 2008年,tls1.1 rfc 发布在 2018年。tls1.3 对 tls1.2 做了较大的改动,大概为:

  • 支持的对称加密算法列表移除了被认为是遗留问题的算法,留下来的都是“关联数据认证加密(Authenticated Encryption with Associated Data)”算法。加密套件的概念发生了改变,认证机制和密钥交换交换机制 与 记录保护算法分开和Hash分离。
  • 增加 0-RTT模式,以牺牲部分安全性为代价为一些应用数在连接建立阶段节省移除往返
  • Static RSA 和 Diffie-Hellman加密套件被删除,提供向前安全
  • ServerHello 之后的所有握手信息都加密传输。新引入的 EncryptedExtension 消息可以保证扩展以加密的方式传输
  • 密钥导出函数被重新设计。新的设计使得密码学家能够通过改进的密钥分离特性进行更容易的分析。基于HMAC的提取-扩展密钥导出函数(HKDF)被用作一个基础的原始组件。
  • Handshake状态机进行了重大重构,以便更具一致性和删除多余的消息如ChangeCipherSpec(除了中间件兼容性需要)。
  • 椭圆曲线算法已经属于基本的规范,且包含了新的签名算法,如EdDSA。TLS 1.3删除了点格式协商以便于每个曲线使用单点格式。
  • 其它的密码学改进包括改变RSA填充以使用RSA概率签名方案(RSASSA-PSS),删除压缩,数字签名算法DSA,和定制DHE组(Ephemeral Diffie-Hellman)。
  • 废弃了TLS1.2的版本协商机制,以便在扩展中添加版本列表。这增加了不支持版本协商的server的兼容性。
  • 之前版本中会话恢复(根据或不根据server端状态)和基于Pre- Shared Key 的密码族已经被一个单独的新PSK交换所取代。
  • 引用已更新至最新版本的RFC(例如,RFC 5280而不是RFC 3280)。
  • ...

其差别可以查阅 RFC tls1.3

tls1.3 和签名的tls版本差别比较大,单独的讲。


参考