深入研究SSL

来源:互联网 发布:网络打黄油是什么意思 编辑:程序博客网 时间:2024/06/15 11:28

SSL 缩写 Secure Socket Layer ,是几十年前网景公司制定的保证服务器和客户端安全通信的一种协议,大量使用在http的安全通信中,这里的安全通信有两层含义:

  • 通信双方身份的认证
  • 通信数据的保密

简单说就是首先要对通信两端的身份进行认证确保是真实的,接下来就是确保通信双方交换的数据进行加密确保只有真实的对手方才能看到,任何其他的人即便拿到数据也无法获得有效信息。这里要注意, SSL 并不包含或实现身份认证的方法和数据加密方法, SSL 只是制定了一套可靠的 C-S 之间的协商办法,来确定通信双方如何身份认证和加密。现在 SSL 里头基本使用 PKI 数字证书方式认证,加密的算法很多,对称非对称,这些网上资料很多自己 google 之。

SSL 是如何工作的呢?基本上 SSL 的工作分为两个阶段:握手阶段和数据传输阶段,若通信期间检测到不安全因素,比如握手时候发现另一端无法支持选择的协议或加密算法,或者发现数据被篡改,这时通信一方会发送警告消息,不安全因素影响比较大两端之间的通信就会终止,必须重新协商建立连接。

SSL 协议结构如下:

 

通过 SSL 分成三个子协议, HandShake( 握手) ChangeCipherSpec( 更改密钥规格 ), Alert( 告警 ) 。

   

SSL 的告警协议是用来为通信对方发送一条告警消息,告警分为两个层次: fatal 和 warning, 如果是 fatal 级别的如 MAC 计算出错或协商密钥算法失败则马上断开连接,要建立连接的话需要重新握手; warning 类型的消息一般只会记录日志,并不会断开连接。

   

SSL 更换密钥规格 (change cipher spec) 协议独立于握手协议,单独属于一类,也是其中最简单的一个。协议由单个消息组成 , 该消息只包含一个值为 1 的单个字节。该消息由客户端和服务器端各自发出用来通知对方,从这个消息以后要开始使用之前协商好的密钥套件了,这个消息一般是在握手到发出 Finish 消息之前发出。

   

SSL 握手协议主要负责如下工作:

―― 算法协商:首次通信时,双方通过握手协议协商密钥加密算法,数据加密算法和文摘算法。 
―― 身份验证:在密钥协商完成后,客户端与服务器端通过证书互相验证对方的身份。 
―― 确定密钥:最后使用协商好的密钥交换算法产生一个只有双方知道的秘密信息,客户端和服务器各自根据这个秘密信息计算出加密密钥,在接下来的记录协议中用来对应用数据进行加密;

   

如果说握手协议是 C/S 双方的协商的话记录协议就是利用协商结果对上层应用提供两种服务:

―― 机密性:使用协商好的通信密钥对业务数据加解密;

―― 数据完整性:利用协商好的 MAC 算法计算消息 HASH ,防止消息被篡改;

协议规定每个记录层的协议数据包长度不能超过 2^14(16K) ,因此记录协议接收到层应用业务数据若超长会将业务数据分块,压缩 ( 可选 ) ,计算 MAC ,加密,按上记录层协议头发出去。

 

疑问:

1.SSL协议中为什么change cipher spec消息要单独属于一类而不归类到握手消息中?

2.若将change cipher spec消息去掉,服务器端和客户端可以直接根据收到或发送Finished消息来让通信双方切换到使用协商好的密钥通信的状态中去好像也没什么影响,为什么一定要整出这么个change cipher spec协议来?

SSL握手过程即完成身份认证和建立加密通道的过程,分为四种。

――Full Handshake : 全流程握手,C/S双方从无到有建立SSL连接;

 

――Resum session Handshake : C/S双方曾经建立过连接,但中途断了,SSL会话信息还有保留,只需要执行部分握手流程就可建立SSL连接;

 

――Server Re-negotiation Handshake : 已经建立了SSL连接,但server端为了某些原因(比如安全性)要求重新对密钥进行协商,也只需要执行部分握手流程;

 

――Client Re-negotiation Handshake : 已经建立SSL连接,但client端为了某些原因要求重新协商,只需执行部分握手流程。


Full Handshake 
这里主要介绍全流程握手,握手步骤如下图所示,其他的握手过程都属于全流程的子集。
SSL握手协议总共有10中消息类型,类型名和枚举值如下:

Java代码  

  1. hello_request(0), client_hello(1), server_hello(2),certificate(11),   
  2. server_key_exchange (12), certificate_request(13),server_done(14),   
  3. certificate_verify(15), client_key_exchange(16),finished(20)  
  4.      

   所有的握手消息有统一的结构:

   

   

C代码  

  1. struct {  
  2.         HandshakeType msg_type; /* 握手消息类型,如上所述的枚举值 */  
  3.         uint24 length;  /* 握手消息体的长度用24位表示,即最大长度不超过16M*/  
  4.         HandShakeMsg payload /*握手消息的有效荷载,上面列的10类型之一*/  
  5. } Handshake;  
  6.    
  • Client Hello 阶段

客户端首先发 Client Hello消息到服务器端,服务器端收到hello消息后再发Server hello消息回应客户端。


Client Hello和Server Hello消息结构如下:

   

   

C代码  

  1. struct {  
  2. ProtocolVersion client_version;  
  3. Random random;  
  4. SessionID session_id;  
  5. CipherSuite cipher_suites<0..216-1>;  
  6. CompressionMethod compression_methods<0..28-1>;  
  7. } ClientHello;  
  8.   
  9. struct {  
  10. ProtocolVersion server_version;  
  11. Random random;  
  12. SessionID session_id;  
  13. CipherSuite cipher_suite;  
  14. CompressionMethod compression_method;  
  15. } ServerHello;   


选择SSL通信,当底层连接建立好 后会触发或调用SSL的初始化和握手,握手由Clinet端发出Client Hello消息开始。

Client Hello 消息

ProtocolVersion: 消息中协议版本是两个byte长度分别表示主次版本,如若在JAVA中初始化SSLContext时候选择了SSLv3则主版本号是3,次版本号为0,若 选择了TLSv1(TLSv1相当SSLv3的升级版)则主版本号是3,次版本号是1。
Random:随机数结构,由两部分组成,

   

C代码  

  1. struct {  
  2.    uint32 gmt_unix_time;  
  3.    opaque random_bytes[28];  
  4. } Random;   


一个4字节的系统当前时间,一个28位长的随机数,在后面计算所有消息的摘要或计算主密钥时候会用到。(疑惑1  在java的JSSE实现版本中貌似只有一个4字节的系统当前时间,没有28位的随机数,奇怪了,怎么和非JSSE 版本实现正确握手的啊…)
Session ID : SSL会话ID标识一次会话用,可以重用。会话ID都是由服务器分配因此在全流程握手中client hello消息中的session id是空,用字节0表示。

CiphersuitList : 密钥套件列表,列表中包含了Client端支持的所有密钥套件。一个密钥套件定义了一个密钥规格,其中描述如下 内容:密钥交换算法,是否出口,对称加密算法,支持的最高对称密钥位数,MAC算法(或摘要算法)。一个ciphersuit用2个字节表示,下面列举 JSSE支持的几个套件:
SSL_RSA_WITH_RC4_128_MD5 = 0x0004 /* 非对称加密算法或密钥交换算法为RSA,采用高强度128位对称加密算法RC4,摘要或MAC算法为MD5,不支持出口 */


SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA = 0x0014 /* 非对称加密算法或密钥交换算法支持RSA和DH,采用40位对称加密算法DES,摘要或MAC算法为SHA,可以出口 */ 
关于出口,美帝国主义要求要用他们的加密算法比如JDK中的JSSE,对称加密密钥长度不能超过40,非对称密钥长度不超过512,在国内只能使用这种低强度的玩意,赤裸裸的技术封锁啊。
这个加密套件列表长度不超过128K。


CompressionMethods: 客户端支持的压缩算法列表,填0表示空,JSSE会有默认算法。
Client Hello承载着这些信息被发送到Server端。

  • Server Hello阶段

Server Hello 消息

 

ProtocolVersion 
服务端服务器拿出消息中的版本号,再看看自己支持的版本列表,选个两者都支持的最高版本号定为这次协商出来的SSL协议使用的版本。比如C端发过来ssl 3.0,而S端发现自己只支持ssl2.0,server就会选择SSL2.0作为这次协商版本,反之若Server支持 SSL2.0,SSL3.0,TLS1.0,选两者都支持的最高版本SSL3.0。


Random : 产生的方式和Client Hello中相同。
Session ID: 服务端检测到传过来的session ID 是空或者检索session 列表没有发现传过来的session id就会新建一个, JSSE中Session ID是取系统时间的前32位(系统时间是long类型64位,只取其中32位)。


Ciphersuit : server端收到密钥套件列表后,将密钥套件一个个拿出来,经过几道检查,选择第一个通过检查的套件。JSSE实现的SSL协议中的检查项目有如下几 项:检查服务端是否也支持这个套件,检查这个套件是不是被禁用(的确是支持某个套件,但出于某些原因被禁用了),检查套件是否符合出口限制,比如在国内加 密强度128或256位的对称密钥在检查时候就会被认为不合法pass掉;若server端启用了双向认证,某些不支持双向认证的套件就会被pass掉。 层层选拔,第一个通过的幸运儿光荣的成为密钥协商的成果,被放入Server Hello 消息中。
这样Server Hello消息组装好了,发出去告诉客户端协商的SSL版本和加密套件,并创建了一个会话,hello阶段结束。

服务发送证书(Server Certificates 可选)消息

在服务器发送完hello消息后接下来可以发送3个可选消息,服务器证书消息,服务器证书交换消息,客户端证书请求消息。服务器证书消息在全流程握手中一般是必须发的(疑惑2 :不知道除了session 重用外在什么情况下服务器端不需要发送证书过去?),但是在会话重用的消息中就不需要再发。


服务器证书中包含公钥,发给客户端用来验证签名或在密钥交换时候给消息加密。证书消息是紧跟着ServerHello消息发送,证书消息中就是一个证书列 表,证书应该转换成ASN.1 DER格式,不支持PKCS7格式。证书链中的证书挨个取下来放入列表中,按照次序服务器自己的证书放列表最前头,根CA证书放列表最后,证书链长度不超 过16M。

 

服务端密钥交换消息(Server Key Exchange 可选)

说到这个不得不介绍下SSL密钥交换方式。安全加密通信是发送方将信息加密,接收方将信息解密,加解密用的密钥分两类:对称和非对称加密(具体概念google之)。
非对称加密 的最大优点是可以将一部分密钥公开,叫公钥(public key),这样通信双方交换密钥很简单,A和B通信,A有一个密钥对Pri-A和Pub-A,B也有一个密钥对Pri-B和Pub-B,AB互相交换自己 公钥,A用Pub-B加密要发给B的消息,经过加密的消息就算被第三方窃取由于只有B有和Pub-B对应的私钥Pri-B,因此只有B才能解开消息,其他 没有对应私钥的无法获得有效信息,B如果要向A发送消息同理也用Pub-A加密消息。
 


照这个办法这个安全通信的加密问题是否圆满解决了呢,世上痛苦的事十之八九,答案当然是否定的,非对称加密有致命的缺点就是加解密效率太低,不能应用在实际通信中应用,而对称加密 的特性正好和非对称加密相反,加解密都用同一个密钥,效率高,但通过网络交换密钥又比较困难,万一被哪个不安分的偷了去那这个加密信道对他来说形同虚设了。


这时一个聪明的办法就是在SSL握手期间用非对称密钥加密对称密钥发送到对方,接收方有私钥将消息解密,获得对称密钥,等握手结束后再使用已经交换好的对称密钥来家解密,这样即解决了非对称加密低效的问题又解决了对称密钥难交换的问题,问题终于圆满解决了。

聪明的人不止一个,又有种优秀的密钥交换算法被整出来了,DH (Diffie Hellman)算法,一种专门用来交换密钥的算法,简单的讲就是在通信前, A和B双方约定2个大整数p和g,其中1<g<p,这两个整数可以公开,然后A,B各自产生一个随机数Xa和Xb,用DH算法将p,g,Xa 算出Ya,同样方法用p,g,Xb算出Yb,这个Xa和Xb各自要保管好,计算得到得Ya,Yb互相交换,这样A手头有Xa,Yb,B手头有Xb,Ya, 这个神奇的DH算法能用A,B手头的两个数各自算出一个相同的密码,使用这个密码能产生一个相同的对称密钥。而第三方只获得p,g,Ya,Yb是无法算出 Xa,Xb和密码的,经过计算这样A,B各自算出了共享的对称密钥再接下来的通信中对数据加解密。

再啰嗦一点有关对称密钥生成方面的细节, 对称密钥并不是直接生成的,是客户端产生一个预主密码(premaster),然后用密钥交换算法交给服务器端,两端根据这个预主密码计算出主密码,再用 主密码生成对称密钥。过程比较曲折。对于RSA的密钥交换方式,预主密码是一个48位的随机数,而DH算法的预主密码相当于上边用Xa,Yb或Xb,Ya 产生的那个共享密码。接下来的主密码生成方式和交换算法无关了,RSA和DH的都一样了,相当于密钥交换实际上是在交换那个Pre-master。
(疑惑3 : 为什么要搞出个预主密码和主密码来啊,客户端直接生成一个可用的对称密钥发给服务器端不就得了吗,折腾的,这点搞不懂,大牛指点一下啊 )

稍微介绍了下背景知识,回归正题,既然密钥交换算法有很多种那SSL握手期间用哪种呢,这个就是之前由选择的ciphersuit决定的,比如选择的是 SSL_RSA_WITH_RC4_128_MD5 = 0x0004,那就是RSA的密钥交换算法即用非对称加密对称将密钥传送到对方,若选择的是SSL_DHE_RSA_EXPORT….那就使用DH交换算 法。对不同的交换算法发过去的消息结构也会不一样,下面主要介绍RSA和DH两种方式的密钥交换。

RSA方式的密钥交换 
在这种方式下,实际上服务器端的这个消息不是必须发送的,可选的。这个消息中包含了一个RSA公钥,公钥用两个参数表示,称为模数和指数。公钥已经在发送服务证书时候包含在证书里头交给客户端了,为啥还要发公钥过去啊?这个有如下几个原因:
   Reason1 . 之前证书中的包含的是DSS(DSA) 公钥,此类公钥只能拿来签名,不能拿来加密,因此需要server端生成一对非对称密钥用来临时加密用,并将公钥塞在Server Key Exchange消息中发送过去。
   Reason2 . 山姆大叔搞技术封锁,设置加密算法的出口限制,他规定RSA加密算法密钥长度不能超过512,如果超过512只能拿来签名,不能拿来加密。因此那些证书里 头的RSA公钥长度超过512的不得不重新生成一对512长度的临时加密用的密钥,并将RSA公钥塞进消息体发出去。
  Reason3 . 。。。我只知道上面两个原因,不排除还有其他的
这样说起来的话如果哥身处美国,不受出口限制,或哥证书中的公钥长度本来就没超过512就完全没必要发这个消息啦~因此这个消息是可选的,不是必须发的。


DH方式密钥交换 
用DH算法产生整数p,g,和server端的Ys,将这三个参数塞进消息体。

为了防止消息被恶意篡改,Server Key exchange消息中还要包含一个对密钥参数的签名。

请求客户端证书消息(可选)

如果是SSL的双向认证的话,服务器端还会发出client cert request 消息,要求客户端发他自己的证书过来验证。使用JAVA JSSE,如果有如下设置说明启用了双向认证:

Java代码  

  1. SSLServerSocket.setNeedClientAuth(true)   

此消息包含两部分内容:一个是server端支持的证书类型(RSA, DSA, ECDSA等。。。),另一部分是server端所信任的所有证书发行机构的DN(Distinguished Name)列表,客户端会用这些信息来筛选证书,以后会讲到。

   

服务端Server Hello Done消息 
这个消息没有什么内容,表示服务器刚才说了那么多,要说的都说完了,等着客户端回应了。

 

Client 处理和回应 Server Hello 阶段

客户端收到服务器发过来的那些消息,要做的是验证服务器证书,发送自己的证书(如果双向认证),发送计算出的预主密码,发送证书验证消息。

   

   

收到 Server Hello

在 server hello 阶段连续的发了多个消息,最先发出的是 server hello , client 收到后将 SSL 会话 ID ,服务器端的一个随机数,协商出的 SSL 协议版本号以及密钥套件放到会话缓存中。

   

收到 Server Certificates

接下来收到的应该是 server 端的证书消息了,取出所有的消息,最头上的是 server 证书,最末端的 CA 根证书。在 SUN JDK JSSE 中实现的 SSL 是这样处理的:

从 SSLContex 中取出 trust manager ,如果用过 java SSL 编程的同学应该知道,可以自己实现 X509TrustManager 和 X509KeyManager 两个接口来定制对证书的验证, sun jdk 中就是调用这个 trust manager 的验证方法来验证证书(在解决浏览器检测到证书是自签名的时候跳出警告框的问题就是可以自己实现这个 trust manager 来让浏览器不跳出警告框)。

   

JDK 当然也有默认的证书验证实现,就是验证签名有效性,验证证书是否过期等。证书签名的有效性验证是在取证书链中某证书中的公钥验证前一个证书的签名,这样第一个证书就是用第二个证书的公钥来验证,那有人可能会问最后的根 CA 的证书谁来验证?根 CA 的证书是自签名的,就是自己给自己签名,没人管的了, JDK 中某个文件中有存储一堆可信任的证书发行机构的证书列表,如果你的根 CA 在那个列表中并对比后确实是那个根 CA 的证书那就 OK ,验证通过,这个可信任的机构也可以自己实现那个 trust manager 来添加设置。

   

收到 Server Key Exchange 消息

RSA 方式密钥交换消息则把消息中的加密用公钥放入会话缓存中,作为客户端这边握手阶段的写密钥而不是用服务器证书中的公钥。

DH 方式的消息就把消息中的 p,g,Ys 三个参数记录下来,有这些 client 端就可以计算出 pre-master 了,只要回头再把自己这边的 Yc 参数发过去, server 端就也能计算出相同的 pre-maseter 了。

   

收到 server Certtificats Request 消息

将消息中的证书类型列表和可信任证书发行机构列表保存下来,可在后面发送客户端证书时候拿来筛选证书用。

   

收到 Server Hello Done 消息

收到这个消息后 client 端开始向 server 发消息了

   

发送 Client Certificates 消息

如果是 server 端要求客户端认证就会发这个消息,否则不发。客户端可能会有多个证书,在 JSSE 里头多个客户端证书存储在 keystore 里头,选哪个发过去呢?这时候要用到 server 端之前发的 cert request 消息中的支持的证书类型列表和信任的根 CA 列表,满足这个两个条件的第一个证书链就会被选中作为客户端证书。

   

发送 Client Key Exchange 消息

若是 RSA 方式密钥交换,则产生一个 48 位随机数作为 pre-master 并用服务器公钥加密后发出去

若是 DH 方式的密钥交换,则根据 sever 的 g,p,Ys ,产生 Xa 和 Yc , Xa 和 Ys 能计算出 pre-master ,把产生的 Yc 放入消息中发给 server ,这样 server 用它的 Xb 和 Yc 也能计算出 pre-master 了。计算出预主密码后就顺便把主密码 (master secret) 给算出来了。算出主密码就把对称密钥产生出来了。

   

发送 Certificate verify 消息

这个消息是可选的,只有在客户端发送了自己证书到服务器端,这个消息才需要发送。发这个消息的目的是让服务器验证发消息的客户端和客户端证书的真实所有者。这个消息中要包含一个签名,签名里头内容就是从 client hello 开始到目前为止所有握手消息(不包括本消息)的摘要,主密码,若是 RSA 方式则要把这些内容分别用 MD5 和 SHA1 计算一遍,两种摘要算法算得的摘要拼接起来用客户端证书中公钥对应的私钥加密就获得了签名。到时候服务器端会用收到的证书中的公钥来验证签名。

   

发送 change cipher spec 消息

发送这个消息,然后把 session 的写密钥设置成计算得到得对称密钥,从此消息之后再发送消息就会用这个写密钥来加密消息。

   

发送 client Finished 消息

Client 端的 Finished 消息一般都是紧随 change cipher spec 消息发送出去,标志着本方的 SSL 协商成功结束。消息中包含两个个摘要,是分别用 MD5 和 SHA 算法计算当前收到所有握手消息和主密码的摘要,不包括本消息和 change cipher spec( 因为不属于握手消息 ) 。

   

该 消息是SSL握手中的最后要互传的消息,包含一个所有握手消息的摘要值,这是为了防止中间人将强度较大的CipherSuite在client hello消息中删除,使得server不得不选择强度较小的CipherSuite,然而这非client所愿。问题是这个摘要不可以被中间人更改吗? 想象一下这时共享对称密钥已经协商好了,ChangeCipherSpec已经经过,所以这些消息本身是加过密的。

?????????Server 处理和回应 cleint Finished 阶段

Server 收到 client 的证书链后验证证书,并验证 certificatie verify 中的签名,验证通过了也就确认了 client 的身份,如假包换。收到 client key exchange 消息从中拿出 Pre-master 计算出 master ,生成对称密钥。收到 client 的 change cert spec 后将会话的读密钥设置为刚产生的对称密钥。

 

处理完这些 server 会发送自己的 change cipher spec 消息并把会话的写密钥设置为生成的对称密钥,最后发送 server Finished 消息, client 端收到 server 端的 change cipher spec 消息将会话的读密钥设置为生成的对称密钥。到此握手过程圆满结束,接下来的应用消息将使用设置好的读写密钥对数据加解密。

0 0
原创粉丝点击