一、SSL协议的设计思想
上一篇文章通过三个例子说明了HTTP协议存在的三个安全性问题:
- 通信内容可以被窃听
- 通信内容可以被篡改
- 通信对象可以被冒充
或许有人会想在WEB应用层面解决这个问题,但是这样做有几个缺点:
- 大大增加了WEB应用的实现难度,除了WEB应用的业务逻辑,还必须为WEB应用单独设计编写一套加密和验证的方案;
- WEB应用本身无法对整个HTTP数据包进行加密,只能对数据包里面的关键内容进行加密,其他信息仍然有被窃取的风险,所以基于WEB应用的安全解决方案并不可靠;
- 基于WEB应用的解决方案不具备通用性。
其实不光HTTP协议存在这个问题,几乎大部分应用层的协议都会存在这些问题,因为应用层的数据包是直接递送给传输控制层明文传送的。
上个世纪90年代中期,网景公司为了解决HTTP协议明文传送的安全性问题,设计了SSL(Secure Sockets Layer 安全套接层)协议。
SSL协议的思想是基于传输控制层协议(例如TCP)建立一个安全的网络连接层。
简单来说,就是应用层和传输层之间多了一个安全套接层,应用层的数据先递送给安全套接层,安全套接层对应用层的数据进行分段、压缩、添加消息认证码和加密之后,再往下递送给传输层进行传送。同样,传输层把接收到的数据先传给安全套接层,由安全套接层解密、验证消息完整性、解压并且组装之后再传给应用层。
这样一来,传输层负责提供可靠的网络连接,应用层负责处理业务,中间的数据安全由一个单独的层面来负责,大家各司其职,分工明确,安全套接层替代传输层为应用层直接提供安全且可靠的数据传输。这样,所有应用层的协议都可以配合SSL协议实现安全的数据传输,相对于基于应用的安全解决方案,这个方案更加通用可靠,也使得应用开发者可以专心处理业务逻辑而无需为应用的数据安全作过多的思考。
二、SSL协议的组成
SSL协议由以下四个子协议组成:
-
记录协议(Record Protocol)
记录协议工作在SSL的底层,主要职责是接收上层协议或下层协议的消息并进行一系列的处理,然后再将处理后的消息继续向下或向上传递。对于从上层协议接收的消息,记录层的处理步骤是:将消息分段、压缩、添加消息认证码以及加密;对于从下层协议接收的消息,记录层的处理步骤是:解密、验证消息完整性、解压以及重新组装消息。其实,记录层的角色就像一个邮递员,只负责按照记录协议的规则进行搬砖工作。另外,记录层的上层协议除了应用层协议之外,还有SSL的其他子协议。 -
握手协议(Handshake Protocol)
SSL的握手协议又是干嘛的呢?跟TCP的握手协议有什么区别?
SSL协议的主要功能是为应用层提供数据加密等安全服务,那在开始对应用层的数据进行传输之前,通信双方必须得知道该使用什么算法加密数据以及对应的加密密钥是什么吧?因此,在开始对应用层的数据进行传输之前,通信双方必须得有一个协商的过程,SSL的握手协议就是对这个过程该协商什么以及怎么协商的一个规定。
SSL的握手协议是建立在TCP握手协议之上的。TCP握手协议的作用是建立一个可靠的网络连接,在TCP连接建立之后,SSL层就可以利用建立的TCP连接传输数据,此时进入SSL的握手协商阶段。SSL的握手协议主要目的是为通信双方确立安全连接所需要的安全参数,通常也会在此阶段对通信双方身份的真实性进行验证。 -
警告协议(Alert Protocol)
无论是在握手阶段还是在对应用层数据的传输阶段,都有可能出现差错。警告协议规定了在SSL协议工作过程中可能出现的差错、错误的严重等级以及相应的处理方式。 -
密码规范改变协议(Change Cipher Protocol)
在SSL握手刚开始的时候,由于加密参数还没确定,消息都是明文传送的;当双方在协商好加密参数之后,通信双方在发送握手结束消息之前,需要发送一个密码规范改变消息(Change Cipher Message)来通知对方随后的消息都使用刚刚协商好的加密算法和加密密钥进行加密。
对于握手协议和警告协议,它们包含许多种消息类型,而对于密码规范改变协议,它只有包含一种消息,该消息只是一个简单的通知。也许你会觉得奇怪,为什么不将这个如此简单的通知消息作为握手消息的一个子类型,而需要单独为它设置一个消息类型呢?后面会对此作出解释。
SSL协议的组成如下图所示:
三、SSL会话(Session)和连接(Connection)
会话是一个虚拟概念,在很多地方我们都会碰到这个术语。那在SSL协议中,会话代表什么意思呢?
前面我们提到,在应用层的数据开始传输之前,通信双方需要通过握手协议来协商一些安全参数。但是,握手过程的开销比较大,如果每次建立连接都要完整的走一遍握手过程来重新协商一遍安全参数,那么客户端每一次建立连接都会很慢,而且还会占用过多的网络带宽资源。为了避免这种情况发生,客户端和服务器可以把协商好的结果缓存起来并赋予它唯一的标识,当客户端在一定期限内再次发起连接请求的时候,服务器和客户端就可以重用之前协商好的结果而不用走完整个握手流程。因此,会话可以理解为:
在特定的时间内,特定的客户端和特定的服务器之间的所有通信内容的总和
也就是说,一次会话,可能会包含多次连接、多次消息往返。一次会话内的所有连接,可以共享一些信息。比如,压缩方法、加密算法、哈希算法等。此外,SSL会话还必须保存一个称之为主秘密(master secret)的信息,客户端和服务器可以根据这个信息导出本次连接所需要的密钥及其他一些重要参数。
以下是SSL会话(Session)的属性:
-
会话标识(session identifier)
服务器端生成的随机字节序列。 -
对端证书(peer certificate)
对端的X509.v3证书,该状态可能为null。 - 压缩方法(compression method)
-
密码规范(cipher spec)
指定数据加密算法(例如null,DES等)和MAC算法(例如MD5或者SHA),密码规范还定义了一些加密属性,例如hash_size。 -
主秘密(master secret)
服务端和客户端共享的48字节秘密。根据它可以导出每个连接所需的安全参数。 -
是否可重用(is resumable)
标志位,用来表明当前会话是否还可以用来初始化新的连接。
以下则是SSL连接(Connection)所必备的一些属性:
-
服务器端和客户端随机数(server and client random)
服务器和客户端为每次连接生成的随机字节序列。要这个有什么用呢?这个两个随机数会和会话的主秘密(master secret)一起经过一定的运算生成连接所需的加密密钥和MAC秘密。这两个随机数保证了每次连接都会生成不同的加密密钥和MAC秘密,保证了连接的安全性。 -
服务端写MAC秘密(server write MAC secret)
服务端用于生成消息认证码(Message Authentication Code)的秘密。 -
客户端写MAC秘密(client write MAC secret)
客户端用于生成消息认证码(Message Authentication Code)的秘密。 -
服务端写密钥(server write key)
服务端加密数据用的密钥和客户端解密数据用的密钥。 -
客户端写密钥(client write key)
客户端加密数据用的密钥和服务端解密数据用的密钥。 -
初始向量(initialization vectors)
当使用CBC模式的分组密码算法时,对应每一个加密密钥都要维持一个初始化向量(IV)。也就是说,对应于服务端写密钥(server write key)要有一个IV,对应于客户端写密钥(client write key)也要有一个。初始向量的作用是避免多次对同一文本加密产生相同的密文,它保证了密文的随机性。初始向量的值由握手协议初始化,此后每一条记录的最后一个密文分组作为下一条记录的初始化向量。 -
序列号(sequence numbers)
通信双方会各自为发送出去和接收到的消息维护自己的序列号。
注:上面提到的消息认证码MAC的作用和计算方法后文会有介绍
四、记录层(Record Layer)
前面已经对记录层的职能作了描述,这里就不再重复了。接下来主要详细介绍一下记录层的三个工作阶段:分段和组装、记录压缩和解压、记录保护。
4.1 分段和组装
记录层把从上层接收到的数据块分成小于或者等于2^14
字节的SSLPlaintext
记录。SSLPlaintext
的数据结构如下:
struct {
uint8 major, minor;
} ProtocolVersion;
enum {
change_cipher_spec(20),
alert(21),
handshake(22),
application_data(23),
(255)
} ContentType;
struct {
ContentType type;
ProtocolVersion version;
uint16 length;
opaque fragment[SSLPlaintext.length];
} SSLPlaintext;
- type:消息类型,记录层传输的消息类型有4种:密码规范改变消息、警告消息、握手消息和应用数据消息;
- version: 协议版本,本文介绍的是SSL 3.0;
-
length:消息长度,即
SSLPlaintext.fragment
的长度,不能超过2^14
字节; - fragment:消息内容。
相反,解压后的记录可能会被重新组装成更大的数据块再往上层传送。
注意1:如果上层传递下来的消息体积比较小,那么多个同种类型的上层消息有可能会被合并成一个SSLPlaintext
记录。密码规范改变消息被设计成一个单独的消息类型而不是作为握手消息的一部分跟这个特性有关,后面会讲具体原因。
注意2:不同类型数据的传输可能不是严格按先后顺序的,有可能是交叉的。通常应用层数据的传输优先级要低于其他类型的。
4.2 记录压缩解压
所有记录都使用会话状态中定义的压缩算法进行压缩。初始的时候压缩算法的值被定义成CompressionMethod.null
,此时压缩是一个恒等操作,即压缩前后的数据是一样的。压缩操作将记录从SSLPlaintext
转换成SSLCompressed
。如下:
struct {
ContentType type; /* same as SSLPlaintext.type */
ProtocolVersion version;/* same as SSLPlaintext.version */
uint16 length;
opaque fragment[SSLCompressed.length];
} SSLCompressed;
-
length:
SSLCompressed.fragment
的长度,不能超过2^14 + 1024字节; -
fragment:
SSLPlaintext.fragment
的压缩形式。
压缩必须是无损压缩,并且压缩后的体积增长不能超过1024字节。如果解压函数遇到一个解压后体积会超过2^14字节的SSLCompressed.fragment
,它会发出一个错误等级为严重的decompression_failure
警告消息。
解压操作则是反过来,将记录从SSLCompressed
转换成SSLPlaintext
。
4.3 记录保护
所有记录都使用当前会话状态中的密码规范CipherSpec
中定义的加密算法和MAC算法实现数据保护。密码规范的初始值是SSL_NULL_WITH_NULL_NULL
,此时不提供任何安全保护。
一旦握手阶段结束,通信双方就有了共享的秘密(secrets)来加密记录并且计算信息内容的Message Authentication Code(MAC)。进行加密操作和MAC操作的方法在密码规范CipherSpec
中定义并且受密码类型CipherSpec.cipher_type
的影响。加密函数和MAC函数将记录从SSLCompressed
转换成SSLCiphertext
,解密函数则相反。
struct {
ContentType type; /* same as SSLCompressed.type */
ProtocolVersion version; /* same as SSLCompressed.version */
uint16 length;
select (CipherSpec.cipher_type) {
case stream: GenericStreamCipher;
case block: GenericBlockCipher;
} fragment;
} SSLCiphertext;
-
length
:SSLCiphertext.fragment
的长度,不能超过2^14+2048字节; -
fragment
:SSLCompressed.fragment
的加密形式,包括MAC。
如上密文结构所示,密码类型不同,得到的加密数据的结构也不同。密码类型有两种:序列密码stream cipher
和分组密码block cipher
。下面简单介绍一下这两种类型的密码。
4.3.1 Null 或者标准序列密码(standard stream cipher)
序列密码算法(包括BulkCipherAlgorithm.null
)把SSLCompressed.fragment
结构体转换成序列密码形式的SSLCiphertext.fragment
结构体,如下。
stream-ciphered struct {
opaque content[SSLCompressed.length];
opaque MAC[CipherSpec.hash_size];
} GenericStreamCipher;
MAC的计算方法如下:
hash(MAC_write_secret + pad_2 +
hash(MAC_write_secret + pad_1 + seq_num +
SSLCompressed.type + SSLCompressed.length +
SSLCompressed.fragment));
-
pad_1:如果
hash
函数是MD5
,pad_1
为字符0x36
重复48次,如果hash
函数是SHA
,pad_1
为字符0x36
重复40次; -
pad_2:如果
hash
函数是MD5
,pad_2
为字符0x5c
重复48次,如果hash
函数是SHA
,pad_2
为字符0x5c
重复40次; - seq_num:该消息的序列号;
- hash:密码套件中指定的哈希算法。
4.3.2 分组密码(block cipher)
对于分组密码算法(例如RC2或DES),加密函数和MAC函数会把SSLCompressed.fragment
结构体转换成分组密码形式的结构体SSLCiphertext.fragment
,如下。
block-ciphered struct {
opaque content[SSLCompressed.length];
opaque MAC[CipherSpec.hash_size];
uint8 padding[GenericBlockCipher.padding_length];
uint8 padding_length;
} GenericBlockCipher;
MAC的计算方法同上;
-
padding
:为了让被加密内容长度等于分组密码长度整数倍而填充的字符; -
padding_length
:填充字符的长度,不超过分组密码的长度,也可能是0。
注意:如果分组密码采用的是CBC模式,第一条传送纪录的初始化向量(IV)是由握手协议初始化的,而接下来的记录的初始化向量则是上一条记录的最后一个密文分组。
五、密码规范改变协议(Change Cipher Spec Protocol)
密码规范改变协议是用来通知密码策略变化的。该协议只包含一个消息,该消息由值为1的单字节组成,如下。
struct {
enum { change_cipher_spec(1), (255) } type;
} ChangeCipherSpec;
改变密码规范消息由客户端或者服务器发送,来通知接收方接下来的消息记录将会由刚刚协商好的密码规范和密钥进行加密保护。
六、警告协议(Alert Protocol)
警告类型是SSL记录层支持的消息类型之一。警告消息的内容包含警告消息的严重程度和关于警告消息的一个描述。如下所示。
enum { warning(1), fatal(2), (255) } AlertLevel;
enum {
close_notify(0),
unexpected_message(10),
bad_record_mac(20),
decompression_failure(30),
handshake_failure(40),
no_certificate(41),
bad_certificate(42),
unsupported_certificate(43),
certificate_revoked(44),
certificate_expired(45),
certificate_unknown(46),
illegal_parameter (47) (255)
} AlertDescription;
struct {
AlertLevel level;
AlertDescription description;
} Alert;
一个fatal
等级的警告消息将导致连接的立即中断。跟其他消息类型一样,警告消息同样会经过记录层进行压缩和加密。
6.1 关闭警告(Closure Alerts)
通信的双方,在关闭连接的写入端之前,要求发出一个close_notify
警告,对方也要响应一个close_notify
警告并且立即关闭连接,丢弃正在写入的内容。连接关闭的发起方并不需要等到对方的close_notify
响应才关闭读出端。如果一个连接中断了但是没有发出close_notify
警告,那么与该连接相关联的会话将不能重用。
6.2 错误警告(Error Alerts)
握手协议中的错误处理非常简单。当检测到错误的时候,错误的检测方发送一个消息个对方。当发出或者接收到一个fatal
级别的警告消息时,双方立即关闭连接。服务器和客户端要丢弃跟此连接相关联的会话ID(session identifiers)、密钥(keys)和秘密(secrets)。
七、握手协议(Handshake Protocol)
在开始介绍握手协议之前,首先要明确握手协议的目标,即在开始传输应用层数据之前协商出安全通信所需的安全参数。这些参数主要包括:采用的协议版本、压缩算法、加密算法、哈希算法以及密钥等。此外,还可能会对服务器和客户端的真实身份进行认证。
握手过程的消息时序图如下:
握手消息的数据结构如下:
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;
} body;
} Handshake;
7.1 ClientHello
ClientHello
消息的结构如下:
struct {
ProtocolVersion client_version;
Random random;
SessionID session_id;
CipherSuite cipher_suites<2..2^16-1>;
CompressionMethod compression_methods<1..2^8-1>;
} ClientHello;
- client_version:客户端希望使用的协议版本,一般填写客户端支持的最新版本;
- random:客户端生成的32字节的随机数,后面导出SSL连接所需的密钥和秘密时需要用到它。客户端随机数与后面介绍到的服务器随机数一起保证了SSL连接密钥的随机性。它的数据结构如下:
struct {
uint32 gmt_unix_time;
opaque random_bytes[28];
} Random;
- session_id:客户端希望重用的会话ID。如果客户端没有可用的会话或者客户端希望生成新的会话,那么这个值为空;
-
cipher_suites:客户端支持的密码套件列表,客户端希望优先使用的密码套件排在前面。如果
session_id
的值不为空,那么该向量至少要包含对应会话的密码套件。密码套件的内容主要包括:密钥交换算法、对称加密算法、哈希算法等。 -
compression_methods:客户端支持的压缩方法列表,客户端希望优先使用的压缩方法排在前面。如果
session_id
的值不为空,那么该向量至少要包含对应会话的压缩方法。
7.2 ServerHello
struct {
ProtocolVersion server_version;
Random random;
SessionID session_id;
CipherSuite cipher_suite;
CompressionMethod compression_method;
} ServerHello;
- server_version:服务器会从自己支持的协议的最高版本和客户端支持的协议的最高版本中选择一个较低版本的协议;
- random:服务端生成的32字节随机数,与客户端的随机数结构相同;
-
session_id:本次会话ID。如果
ClientHello.session_id
不为空,服务器会查询它的session
缓存。如果查到了匹配的会话信息并且服务器愿意使用指定的会话建立新的连接,服务器会返回一样的会话ID。否则,服务器会新建一个会话并且新建会话的ID。 - cipher_suite:服务端从客户端支持的密码套件列表里面选择的一个密码套件;
- compression_method:服务端从客户端支持的压缩方法列表里面选择的一个压缩方法。
7.3 Server Certificate
如果服务器要求被认证(通常情况都是这样),在ServerHello
消息之后,服务器会立即发送它的证书给客户端。证书的类型必须与选择的密码套件的密钥交换算法相匹配。通常都是X.509.V3证书,当密钥交换算法为FORTEZZA
算法时,则是经过修改的X.509证书。
证书的内容一般可以分为三部分:
- 服务器的身份信息,例如域名、公司信息等;
- 服务器的公钥;
- CA的签名。
客户端接收到服务器的证书后,首先验证证书的签名,如果没有问题,则可进一步认证服务器的信息并可放心使用证书上附带的服务器公钥。
服务器的公钥可能有两种用法。最直接的用法就是将服务器公钥用于密钥交换。但是,在某些情况下,服务器的公钥并不直接用于密钥交换。服务器会根据选择的密钥交换算法发送额外的密钥交换参数给客户端。为了防止中间人攻击,服务器会使用自己的私钥给这些密钥交换参数进行签名,此时客户端可利用服务器证书中的公钥验证密钥交换参数消息的签名。
Certificate
消息的结构如下:
struct {
ASN.1Cert certificate_list<1..2^24-1>;
} Certificate;
- certificate_list:证书链。证书链的概念请参考 SSL教程:什么是SSL证书链?。
7.4 Server Key Exchange
一般情况下,服务器是不用发送密钥交换信息的,直接使用服务器证书中附带的公钥进行密钥交换即可。但是,在以下几种情况下,服务器需要发送ServerKeyExchange
消息用于密钥交换:
- 服务器没有证书;
- 服务器有证书,但是证书中的公钥长度过长,不能用来进行密钥交换;
- 密钥交换算法为Diffie-Hellman算法且服务器证书中不包含Diffie-Hellman参数;
- 密钥交换算法为
FORTEZZA KEA
算法。
注:根据美国当前的出口法,在从美国出口的软件中,模数大于512位的RSA公钥不能用来进行密钥交换。
根据选择的密钥交换算法的不同,ServerKeyExchange
消息要发送的参数是不同的。SSL使用的密钥交换算法有三种:RSA
、Diffie-Hellman
和FORTEZZA KEA
。
ServerKeyExchange
消息的结构如下:
struct {
select (KeyExchangeAlgorithm) {
case diffie_hellman:
ServerDHParams params;
Signature signed_params;
case rsa:
ServerRSAParams params;
Signature signed_params;
case fortezza_kea:
ServerFortezzaParams params;
};
} ServerKeyExchange;
对于RSA算法:
struct {
opaque rsa_modulus<1..2^16-1>;
opaque rsa_exponent<1..2^16-1>;
} ServerRSAParams;
对于DH算法:
struct {
opaque dh_p<1..2^16-1>;
opaque dh_g<1..2^16-1>;
opaque dh_Ys<1..2^16-1>;
} ServerDHParams;
对于FORTEZZA KEA算法:
struct {
opaque r_s [128];
} ServerFortezzaParams;
7.5 Certificate Request
当服务器要对客户端的身份进行认证时,需要发送此消息。
7.6 ServerHello Done
ServerHelloDone
消息表示Server Hello阶段的结束。发出该消息后,服务器会等待客户端的响应。如果客户端需要对服务器的身份进行认证,那么在客户端收到ServerHelloDone
消息时,就需要认证服务器发过来的证书。
ServerHelloDone
的消息结构如下:
struct { } ServerHelloDone;
7.7 Client Certificate
当服务器要对客户端的身份进行认证时,需要发送此消息。
7.8 Client Key Exchange
客户端密钥交换消息的内容取决于选择了哪一种密钥交换算法,如下:
struct {
select (KeyExchangeAlgorithm) {
case rsa: EncryptedPreMasterSecret;
case diffie_hellman: ClientDiffieHellmanPublic;
case fortezza_kea: FortezzaKeys;
} exchange_keys;
} ClientKeyExchange;
-
RSA
当使用RSA密钥交换算法时,由客户端生成一个48字节的预备主秘密(premaster secret)并用服务器证书中提供的公钥或者ServerKeyExchange
消息中提供的临时RSA公钥加密后发送给服务器。 -
Diffie-Hellman
当使用Diffie-Hellman密钥交换算法时,客户端是需要把它的DH公开值传回给服务器。这时分两种情况。如果客户端证书中已经包含了DH公开值,则客户端密钥交换消息不用再传送DH公开值,而是传送一个空消息;如果客户端证书没有包含DH公开值,则客户端密钥交换消息需要传送DH公开值。这样,根据DH算法,客户端和服务器都有足够的参数来生成共同的预备主秘密(premaster secret)。 -
FORTEZZA KEA
当使用FORTEZZA KEA密钥交换算法时,客户端会根据FORTEZZA KEA算法首先计算出一个令牌加密密钥TEK。然后,由客户端生成客户端写密钥client_write_key
、服务端写密钥server_write_key
、客户端写密钥对应的初始向量IVclient_write_iv
、服务端写密钥对应的初始向量IVserver_write_iv
以及48字节的预备主秘密(premaster secret)并用TEK进行加密后传给服务器。当然了,除了这几个参数外,还要传送其他的参数来让服务器能够产生根客户端相同的TEK。
到此,应该说客户端和服务器已经完成了密钥的交换。因为利用预备主秘密(premaster secret)可以导出主秘密(master secret),利用主秘密(master secret)最后可以导出最后使用的各种secret、key和IV。
主秘密(master secret)的计算方式如下:
master_secret =
MD5(pre_master_secret + SHA(’A’ + pre_master_secret +
ClientHello.random + ServerHello.random)) +
MD5(pre_master_secret + SHA(’BB’ + pre_master_secret +
ClientHello.random + ServerHello.random)) +
MD5(pre_master_secret + SHA(’CCC’ + pre_master_secret +
ClientHello.random + ServerHello.random));
最终使用的密钥、MAC秘密和初始向量IV的生成方式如下:
key_block =
MD5(master_secret + SHA(‘A’ + master_secret +
ServerHello.random +
ClientHello.random)) +
MD5(master_secret + SHA(‘BB’ + master_secret +
ServerHello.random +
ClientHello.random)) +
MD5(master_secret + SHA(‘CCC’ + master_secret +
ServerHello.random +
ClientHello.random)) + [...];
依次类推,直到有足够的输出为止。然后,key_block
会被分割成所需的参数。
client_write_MAC_secret[CipherSpec.hash_size]
server_write_MAC_secret[CipherSpec.hash_size]
client_write_key[CipherSpec.key_material]
server_write_key[CipherSpec.key_material]
client_write_IV[CipherSpec.IV_size] /* non-export ciphers */
server_write_IV[CipherSpec.IV_size] /* non-export ciphers */
key_block
中多余的数据会被丢弃。
对于出口加密算法(CipherSpec.is_exportable
为true
),还需经过以下步骤才能得出最终的密钥和初始向量。
final_client_write_key = MD5(client_write_key +
ClientHello.random +
ServerHello.random);
final_server_write_key = MD5(server_write_key +
ServerHello.random +
ClientHello.random);
client_write_IV = MD5(ClientHello.random + ServerHello.random);
server_write_IV = MD5(ServerHello.random + ClientHello.random);
注:对于FORTEZZA KEA密钥交换算法,主秘密(master secret)只用来生成MAC秘密。
7.9 Certificate Verify
CertificateVerify
消息的作用是证明客户端拥有与刚刚所发送证书对应的私钥。它是一个签名消息,服务器接收到此消息之后,只要使用客户端证书中提供的公钥验证签名即可。
7.10 Finished
结束消息总是在ChangeCipherSpec
消息之后立即发送。结束消息是用来确认密钥交换和认证过程的成功结束的。结束消息是第一个使用刚刚协商好的加密算法、密钥和秘密进行加密保护和完整性保护的消息。通信双方在发送结束消息之后就可以开始传送应用层数据了。结束消息的接收方要验证结束消息的正确性。结束消息的结构如下:
enum {
client(0x434C4E54), server(0x53525652)
} Sender;
struct {
opaque md5_hash[16];
opaque sha_hash[20];
} Finished;
- md5_hash
MD5(master_secret + pad2 + MD5(handshake_messages + Sender + master_secret + pad1));
- sha_hash
SHA(master_secret + pad2 + SHA(handshake_messages + Sender + master_secret + pad1));
其中,handshake_messages
包括除结束消息外的所有握手消息。ChangeCipherSpec
消息不算在内,因为它不属于握手消息的一部分。那为什么不把ChangeCipherSpec
消息作为握手消息的一部分而要把它独立出去呢?因为记录层有可能会把几个类型相同的消息合并成一个消息记录传送呢,如果ChangeCipherSpec
作为握手消息的一部分,那么ChangeCipherSpec
消息很有可能会和其它握手消息合并在一起传送。然而,我们希望看到的结果是,通信的双方在收到ChangeCipherSpec
消息之后,立即将协商好的密码规范应用到ChangeCipherSpec
之后的消息。如果是将ChangeCipherSpec
作为握手消息的一部分的话,就有可能存在问题。因此,需要将它独立出去。详细的解析请参考 Why is change cipher spec an independent protocol content type and not part of Handshake Messages?
另外,当客户端和服务器重用会话而非新建会话时,握手消息的时序如下图所示:
在
ServerHello
消息之后,服务器直接发送ChangeCipherSpec
消息和Finished
消息;客户端紧接着也直接发送ChangeCipherSpec
消息和Finished
消息。握手过程结束。在重用会话的情况下,客户端和服务器不用走完整个握手流程,因为通信双方可以根据会话状态中保存的主秘密(master secret)、ClientHello.random以及ServerHello.random直接导出加密密钥、MAC秘密和初始向量IV。
八、总结
- SSL协议在传输控制层的基础上建立了安全的连接,它作为一种通用可靠的安全解决方案,可与多种应用层协议结合使用,实现应用数据的安全传输。我们常见的https即为http协议与SSL协议(或者TLS协议)的结合;
- SSL协议是一个分层协议,由记录协议(Record Protocol)、警告协议(Alert Protocol)、密码规范改变协议(Change Cipher Protocol)和握手协议(Handshake Protocol)组成,其中记录协议工作在最底层;
- SSL协议在进行应用数据传输之前,需要通过握手协议来协商安全通信所需的安全参数。
另外,此文对于SSL协议的解析是基于 RFC6101。个人水平有限,文中所述难免有误,欢迎批评指正。