一、SSL协议的设计思想上一篇文章通过三个例子说明了HTTP协议存在的三个安全性问题:
或许有人会想在WEB应用层面解决这个问题,但是这样做有几个缺点:
其实不光HTTP协议存在这个问题,几乎大部分应用层的协议都会存在这些问题,因为应用层的数据包是直接递送给传输控制层明文传送的。 上个世纪90年代中期,网景公司为了解决HTTP协议明文传送的安全性问题,设计了SSL(Secure Sockets Layer 安全套接层)协议。
SSL协议的思想是基于传输控制层协议(例如TCP)建立一个安全的网络连接层。 这样一来,传输层负责提供可靠的网络连接,应用层负责处理业务,中间的数据安全由一个单独的层面来负责,大家各司其职,分工明确,安全套接层替代传输层为应用层直接提供安全且可靠的数据传输。这样,所有应用层的协议都可以配合SSL协议实现安全的数据传输,相对于基于应用的安全解决方案,这个方案更加通用可靠,也使得应用开发者可以专心处理业务逻辑而无需为应用的数据安全作过多的思考。
安全套接层
二、SSL协议的组成SSL协议由以下四个子协议组成:
SSL协议的组成如下图所示:
SSL协议组成
三、SSL会话(Session)和连接(Connection)
会话是一个虚拟概念,在很多地方我们都会碰到这个术语。那在SSL协议中,会话代表什么意思呢?
也就是说,一次会话,可能会包含多次连接、多次消息往返。一次会话内的所有连接,可以共享一些信息。比如,压缩方法、加密算法、哈希算法等。此外,SSL会话还必须保存一个称之为主秘密(master secret)的信息,客户端和服务器可以根据这个信息导出本次连接所需要的密钥及其他一些重要参数。 以下是SSL会话(Session)的属性:
以下则是SSL连接(Connection)所必备的一些属性:
注:上面提到的消息认证码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;
相反,解压后的记录可能会被重新组装成更大的数据块再往上层传送。 注意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;
压缩必须是无损压缩,并且压缩后的体积增长不能超过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;
如上密文结构所示,密码类型不同,得到的加密数据的结构也不同。密码类型有两种:序列密码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));
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的计算方法同上;
注意:如果分组密码采用的是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 ClientHelloClientHello消息的结构如下: struct { ProtocolVersion client_version; Random random; SessionID session_id; CipherSuite cipher_suites<2..2^16-1>; CompressionMethod compression_methods<1..2^8-1>; } ClientHello;
7.2 ServerHellostruct { ProtocolVersion server_version; Random random; SessionID session_id; CipherSuite cipher_suite; CompressionMethod compression_method; } ServerHello;
7.3 Server Certificate
如果服务器要求被认证(通常情况都是这样),在ServerHello消息之后,服务器会立即发送它的证书给客户端。证书的类型必须与选择的密码套件的密钥交换算法相匹配。通常都是X.509.V3证书,当密钥交换算法为FORTEZZA算法时,则是经过修改的X.509证书。
客户端接收到服务器的证书后,首先验证证书的签名,如果没有问题,则可进一步认证服务器的信息并可放心使用证书上附带的服务器公钥。 服务器的公钥可能有两种用法。最直接的用法就是将服务器公钥用于密钥交换。但是,在某些情况下,服务器的公钥并不直接用于密钥交换。服务器会根据选择的密钥交换算法发送额外的密钥交换参数给客户端。为了防止中间人攻击,服务器会使用自己的私钥给这些密钥交换参数进行签名,此时客户端可利用服务器证书中的公钥验证密钥交换参数消息的签名。 Certificate消息的结构如下: struct { ASN.1Cert certificate_list<1..2^24-1>; } Certificate;
7.4 Server Key Exchange一般情况下,服务器是不用发送密钥交换信息的,直接使用服务器证书中附带的公钥进行密钥交换即可。但是,在以下几种情况下,服务器需要发送ServerKeyExchange消息用于密钥交换:
注:根据美国当前的出口法,在从美国出口的软件中,模数大于512位的RSA公钥不能用来进行密钥交换。
根据选择的密钥交换算法的不同,ServerKeyExchange消息要发送的参数是不同的。SSL使用的密钥交换算法有三种:RSA、Diffie-Hellman和FORTEZZA KEA。 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消息时,就需要认证服务器发过来的证书。 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;
到此,应该说客户端和服务器已经完成了密钥的交换。因为利用预备主秘密(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中多余的数据会被丢弃。 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 VerifyCertificateVerify消息的作用是证明客户端拥有与刚刚所发送证书对应的私钥。它是一个签名消息,服务器接收到此消息之后,只要使用客户端证书中提供的公钥验证签名即可。 7.10 Finished结束消息总是在ChangeCipherSpec消息之后立即发送。结束消息是用来确认密钥交换和认证过程的成功结束的。结束消息是第一个使用刚刚协商好的加密算法、密钥和秘密进行加密保护和完整性保护的消息。通信双方在发送结束消息之后就可以开始传送应用层数据了。结束消息的接收方要验证结束消息的正确性。结束消息的结构如下: enum { client(0x434C4E54), server(0x53525652) } Sender; struct { opaque md5_hash[16]; opaque sha_hash[20]; } Finished;
其中,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? 另外,当客户端和服务器重用会话而非新建会话时,握手消息的时序如下图所示:
重用session握手时序图
八、总结
|