赞
踩
https通信一般使用非对称加密算法进行密钥传递,使用对称加密算法进行后续业务数据加密传输,一次完整的Https通信过程如下:
TCP是面向连接的协议,客户端与服务器进行通信之前,需要建立可靠的连接。因此TCP协议通过三次握手来建立客户端与服务器的可靠连接。
先了解一下涉及的关键词,其中比较重要的字段有:
Seq:序列号(Sequence number) 占32位,用来对TCP发起方发送的报文进行标记。
ack:确认序列号(Acknowlegment number) 占32位,ack=Seq+1(即确认序列号等于发送发的Seq+1)
注意:只有ACK标志位为1时,该字段才有效。
Flags:标志位(Flags)共6个,即URG、ACK、PSH、RST、SYN、FIN等。具体含义如下:
SYN:该标志位,表示请求建立一个新连接。
ACK:该标志位,表示确认序列号有效。
FIN:该标志位,表示释放一个连接。
RST:该标志位,表示重置连接
URG:该标志位,表示紧急指针(urgent pointer)有效
PSH:该标志位,表示接收方应该尽快将这个报文交给应用层
TCP三次握手示意图
SYN_SENT
状态,数据包包含:
LISTEN
监听阶段,返回给客户端一个TCP数据包,自己进入SYN_REVD状态,数据包包含:
ESTABLELISTED
建立阶段,数据包包含:
随后,服务器收到来自客户端的TCP报文之后,验证ack之后,明确了从服务器到客户端的数据传输是正常的。结束SYN-SENT阶段,进入ESTABLISHED阶段。至此TCP三次握手结束,客户端与服务器已经建立了可靠的连接,后续可进行数据通信。
注意:握手中涉及的Seq和ack值都是在初始值基础上进行计算的,一旦中途出现某一方发送的TCP报文丢失,变无法继续进行握手,以此确保了"三次握手"的顺利完成,保证了TCP报文传输的连贯性。
Https在经过TCP三次握手客户端与服务器建立了可靠通信连接之后,紧接着客户端与服务器要进行通信协议的协商,来确定后续的通信加密方式,具体流程如下:
加密算法
(图中Cipher Suites)和一段随机数字
Random1。其中随机数字Random1会在后面的对称加密中用到如果上一步通信协议协商,服务器支持客户端的通信协议,则服务端发送证书到客户端进行身份验证(CA证书认证)。在进行证书验证之前我们先了解一下Https证书认证工作流程
以及证书链
Https证书认证工作流程
公钥
及部分个人信息
提交给数字证书认证机构(CA)。CA自己的私钥
对服务器提供的公钥及信息进行签名生成数字证书
,并将此证书发给提交者。客户端使用内置CA根证书的公钥,对服务器证书内CA私钥签名的数据进行验签
,如果验签通过,说明该数字证书确实是CA颁发的,从而可以确认该证书中的公钥确实是合法服务器端提供的。证书链认证
如果服务器的证书不是CA根证书颁发,而是通过中级证书机构
签名颁发的证书,服务器在 SSL 握手期间不会仅向客户端发送它的证书,而是发送一个证书链,包括服务器 CA 以及到达可信的根 CA 所需要的任意中间证书,客户端或浏览器使用内置的CA根证书公钥对中间证书进行验证,如果根证书信任该中级证书,则该中级证书颁发的证书也是可信的,这就是证书链了。
例如,主要在本次的消息中的两个证书:中级证书颁发机构的证书
及其颁发的服务器证书
上一步客户端对服务器证书认证之后,客户端生成一个随机数Random3,然后使用服务器证书内的公钥,对Random3加密后发送给服务器(只有服务器使用对应的私钥才能解开)。这样https加密的密钥传递流程才算走完。(此时客户端和服务器都具备了随机数Random1+Random2+Random3)
由于非对称加密的运算成本较高,所以非对称加密算法一般只用来进行秘钥传递
,所以完成秘钥传递之后,客户端一般会使用之前与服务器协商的加密算法,将Random1+Random2+Random3作为对称加密算法的秘钥进行加密通信。至此整个Https协议通信结束(包含:建立连接、通信协议协商、证书认证、密钥传递、加密通信)
在Android4.2(Jelly Bean)开始,Android平台目前包含在每个版本中更新的100多个CA(证书授权机构)。CA具有一个证书个一个私钥,这点与服务器相似。为服务器颁发证书时,CA使用其私钥对证书进行签名。然后客户端可以使用CA公钥对服务器证书内签名数据进行验签,以此来确认服务器证书是否是客户端CA颁发的有效证书。
HTTPS通信所用到的证书由CA提供,需要在服务器中进行相应的设置才能生效。
HttpURLConnection
中已经支持了Https证书验证功能且默认为 SSLSocketFactory
,只要是通过知名CA机构签发的证书,那么,可以使用下面简单代码发起安全的请求,因为Android平台已经包含了该知名CA。
URL url = new URL("https://wikipedia.org");
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);
不过还有一些注意事项
javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found
出现这种情况原因有很多,其中包括:
针对上面情况讨论如何解决,同时保持与服务器的连接出于安全状态
问题描述
在这种情况下,由于您具有系统不信任的 CA,将发生 SSLHandshakeException
。原因可能是您有一个由 Android 尚不信任的新 CA 颁发的证书,或您的应用在没有 CA 的较旧版本上运行。CA 未知的原因通常是因为它不是公共 CA,而是政府、公司或教育机构等组织颁发的仅供自己使用的私有 CA。
解决方案
首先需要办法证书的未知CA提供公钥证书,让HttpsURLConnection
来信任您指定的 CA证书(而非系统默认的CA集)具体操作:
使用InputStrem
获取一个特定的CA证书
用该CA证书创建keyStore
用KeyStore
创建和初始化TrustManager
TrustManager
是系统用来验证来自服务器证书的工具,可以使用一个或多个CA证书从KeyStore
创建TrustManager
,而这些创建的TrustManager
将仅信任这些CA
通过TrustManager
来初始化SSLContext
SSLContext
它会提供一个SSLSocketFactory
,您可以用来替换来自HttpsURLConnection
的默认SSLSocketFactory
。这样一来,连接将使用您的 CA 验证证书。
替换HttpsURLConnection
中的默认的SSLSocketFactory为SSLContext
创建的SSLSocketFactory
/** *1.从本地路径加载创建证书 */ InputStream caInput = new BufferedInputStream(new FileInputStream("server.crt")); //约定证书公钥格式为x.509来实例化证书工厂类 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); //生成本地证书 Certificate ca; try { ca = cf.generateCertificate(caInput); } finally { caInput.close(); } /** *2.通过CA证书创建一个包含可信ca的密钥存储库keyStore */ String keyStoreType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(keyStoreType); keyStore.load(null, null); keyStore.setCertificateEntry("ca", ca); /** *3.通过keystore初始化TrustManagers */ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); tmf.init(keyStore); TrustManagers mTrustManagers = tmf.getTrustManagers() /** *4.通过TrustManager来初始化SSLContext */ SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, mTrustManagers, null); /** *5.替换HttpsURLConnection中默认的SSLSocketFactory,至此连接将使用您的 CA 来验证证书 */ URL url = new URL("https://certs.cac.washington.edu/CAtest/"); HttpsURLConnection urlConnection =(HttpsURLConnection)url.openConnection(); urlConnection.setSSLSocketFactory(sslContext.getSocketFactory()); ...
问题描述
如果使用自签名的服务器证书,也就是服务器充当自己的 CA。这与上面的未知CA情况相似,因此您可以使用前面介绍的方法。当然你也可以通过重写校验证书链TrustManager
中的方法checkServerTrusted()
来使用指定CA证书对服务器证书进行验证。
解决方案
首先需要服务端提供自签名的公钥证书,后续我们将使用此(windows(.cer)
/linux(.crt)
)证书对服务器证书进行校验。具体步骤如下:
创建一个X509TrustManager
接口实现类A,实现该接口内的checkServerTrusted()
方法
在checkServerTrusted()
方法中使用服务器提供的公钥的证书,来对服务器证书进行验证(使用证书内的公钥来验签服务器证书内通过私钥签名的数据)。
使用实现X509TrustManager
接口的A类来初始化SSLContext
对象
通过SSLContext对象提供的getSocketFactory()
方法返回的SSLSocketFactory
对象来覆盖HttpsURLConnection
类默认的的SSLSocketFactory
对象
创建实现X509TrustManager
接口checkServerTrusted()
方法的TrustManagerImpl类,来对服务器证书进行验证。
class TrustManagerImpl implements X509TrustManager { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { //TODO 用来验证客户端证书的方法,这里没有做双向验证因此忽略 } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String authType)throws CertificateException { //TODO 用于验证服务器证书的方法 try { /** *验证服务器证书链是否有效 */ if (x509Certificates == null) { System.err.println("X509Certificate array is null"); return; } if (x509Certificates.length <= 0) { System.err.println("X509Certificate is empty"); return; } if (null == authType || !authType.contains("RSA")) { System.err.println("authType is not RSA"); return; } /** *如果上述条件都通过,使用本地证书公钥验签服务器证书内私钥签名的数据(如果验签通过, *说明该服务器证书是合法自签名证书)。 */ //从本地路径加载证书流 InputStream certInput = new BufferedInputStream(context.getAssets().open("server.crt")); //约定证书公钥格式为x.509来实例化证书工厂类 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); //生成本地X509Certificate证书 X509Certificate localcertificate; try { localcertificate = (X509Certificate) certificateFactory.generateCertificate(certInput); } finally { certInput.close(); } //获取服务器证书链的根证书 X509Certificate certificate = x509Certificates[0]; //验证根证书有效期 certificate.checkValidity(); //使用本地证书公钥来验签服务器证书私钥签名的数据 certificate.verify(localCertificate.getPublicKey()); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } catch (SignatureException e) { e.printStackTrace(); } } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }
使用实现X509TrustManager
接口的TrustManagerImpl类来初始化SSLContext
对象
SSLContext sslContext = SSlContext.getInstance("TLS");
sslContext.init(null,new TrustManager[]{new TrustManagerImpl()},null);
通过SSLContext对象提供的getSocketFactory()
方法返回的SSLSocketFactory
对象来覆盖HttpsURLConnection
类默认的的SSLSocketFactory
对象
URL url = new URL("https://xxx.xxxx.xxxx");
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
....
至此,我们可通过自签名服务器证书及未知CA颁发证书实现与服务器https传输证书验证。
在验证SSL连接有两个关键环节,首先是上面验证证书是否来自值的信任的来源,还有就是主机名验证
常见错误如下:
java.io.IOException: Hostname 'example.com' was not verified
...
...
java.io.IOException: HTTPS hostname wrong: should be <xx.xxx.xxx.xxx>
出现上述问题一个原因是服务器配置错误。配置服务器所使用的证书不具有与您尝试连接的服务器的主题或主题备用名称字段。在握手期间,如果请求URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口实现程序来确定是否应该允许此连接(如果是主机名一致是不会调用该函数,即已明确主机名有效,并且不需要您的帮助),如果回调内实现不恰当,默认接受所有域名,则有安全风险。
因此我们在实现的HostnameVerifier
子类中,需要使用verify
函数效验服务器主机名的合法性,否则会导致恶意程序利用中间人攻击绕过主机名效验。
//反例
HostnameVerifier hnv=new HosernameVerifier(){
@Override
public boolean verify(String hostname,SSLSession session){
return ture;//对URL与主机名不一致的回调,不做处理,直接接受所有域名,存在安全隐患
}
}
//正列
HostnameVerifier hnv=new HosernameVerifier(){
@Override
public boolean verify(String hostname,SSLSession session){
//对请求URL与主机名不一致的回调,我们做合法性验证,目标主机名是否为我们指定的一致
if("youhostname".equals(hostname)){
return true;
}else{
HostnameVerifier hv=HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(hostname,session);
}
}
}
个人信息
和他的公钥信息
,同时还附有认证中心的签名信息
。根证书
根证书是由CA机构自己颁发,是颁发SSL证书的核心,是信任链的起始点
。根证书库是下载客户端浏览器时预先加载根证书的合集。因此根证书是十分重要的,因为它可确保浏览器自动信任已使用私钥签名的SSL证书。
中级(中间根)证书
中间根证书,是由CA机构的根证书颁发
。证书颁发机构(CA)不会直接从根目录颁发服务器证书(即SSL证书),因为这种行为是十分危险的,因为一旦发生错误颁发或者需要撤销root,则使用root签名的每个证书都会被撤销信任。
因此,为了规避上述风险,CA机构一般会引用中间根。CA机构使用其私钥对中间根进行签名,使浏览器信任中间根。然后CA机构使用中间根证书的私钥来签署用户申请的SSL证书。这种中间根的形式可以重复多次,即使用中间根签署另一个中间件,然后CA机构通过中间件签署SSL证书。
计算机中内置的是最顶级机构的根证书,不过不用担心,根证书的公匙在子级也是适用的
(通过中级证书颁发机构(中间根)签发的证书也可通过根证书公钥验签)。
首先考虑一次握手
TCP区别于UDP,TCP是面向连接的协议,因此在通信之前,需要确保客户端与服务器已经建立起了可靠的连接。而一次握手(即客户端向服务器发出连接请求,没有收到服务器的应答)无法确认是否已经建立连接,因此一次握手不符合TCP面向连接的设计思想。
再来看看两次握手
两次握手即客户端向服务器发出连接请求,服务器接收到并告诉客户端允许连接这是两次握手。那么TCP为什么不使用两次握手,两次握手会出现什么问题呢?
假如,客户端第一次发送一个TCP请求连接包SYNA+Seq,由于网络原因没有到达服务器。此时客户端会会将此次请求认为无效,进而重新发送一个请求连接包SYNB+Seq,此时服务收到该连接请求包SYNB+Seq,为该请求申请连接资源,并应答一个SYN+ACKB。与此同时客户端第一次发送的连接请求包SYNA+Seq延迟后到达了服务器,此时服务器认为是一个新的连接请求,所以服务器又为这个连接申请资源并返回Seq+ACKA。但是客户端会认为这个ACKA是无效的,并不会理会。但是服务器会一直为这个连接维持着资源,造成资源浪费。
三次握手,可解决连接资源浪费问题
我们再来看看三次握手时如何解决上述连接资源浪费问题的,当客户端第二次发送的连接请求SYNB+Seq包,到达服务器,服务器应答SYN+ACKB后,客户端紧接着回复一个ACK,告诉服务器,我收到你的允许连接应答了,咱门可以进行连接了。如果此时客户端第一次发送的请求连接SYNA+Seq包到达了服务器,服务器为该连接申请资源,并应答客户端SYN+ACKA,此时客户端认为该应答时无效的,不予理会。此时服务器为该连接申请的资源一直迟迟等不到客户端的反馈,服务器也认为该连接时无效的,便会释放相关连接资源。
但是这时会有一个问题,就是客户端在完成两次握手之后,便认为连接已经建立,而第三次握手可能由于网络原因在传输中丢失,服务器便会认为连接时无效的,这时候,如果客户端向服务器写数据,服务器将以RST
(重置连接
)包应答,这时客户端便可感知到服务器的错误。
因此TCP使用三次握手来建立可靠的连接,可避免服务器申请无效的连接资源。
挥手之前主动释放连接的客户端结束ESTABLISHED
阶段。随后开始四次挥手
客户端向服务器发送释放连接TCP包,此时,客户端进入FIN-WAIT-1(终止等待1)
阶段,即半关闭阶段。并且停止在客户端到服务器端方向上发送数据,但是客户端仍然能接收从服务器端传输过来的数据。数据包内容包含:
注意:这里不发送的是正常连接时传输的数据(非确认报文),而不是一切数据,所以客户端仍然能发送ACK确认报文。
服务器端接收到从客户端发出的TCP报文之后,确认了客户端想要释放连接,随后服务器端结束ESTABLISHED
阶段,进入CLOSE-WAIT(半关闭状态)
阶段并返回一段TCP报文,其中:
客户端收到从服务器端发出的TCP报文之后,确认了服务器收到了客户端发出的释放连接请求,随后客户端结束FIN-WAIT-1
阶段,进入FIN-WAIT-2
阶段。
服务器端自从发出ACK确认报文之后,经过CLOSED-WAIT
阶段,做好了释放服务器端到客户端方向上的连接准备,再次向客户端发出一段TCP报文,随后服务器端结束CLOSE-WAIT
阶段,进入LAST-ACK
阶段。并且停止在服务器端到客户端的方向上发送数据,但是服务器端仍然能够接收从客户端传输过来的数据其中:
客户端收到从服务器端发出的TCP报文,确认了服务器端已做好释放连接的准备,结束FIN-WAIT-2
阶段,进入TIME-WAIT
阶段,并向服务器端发送一段报文,其中:
接收到服务器准备好释放连接的信号
注意:注意此时TCP连接还没有释放,必须经过2MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
服务器端收到从客户端发出的TCP报文之后结束LAST-ACK
阶段,进入CLOSED
阶段。由此正式确认关闭服务器端到客户端方向上的连接。
客户端等待完2MSL之后,结束TIME-WAIT阶段,进入CLOSED阶段,由此完成“四次挥手”。
PS:为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
在Client发送出最后的ACK回复,但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭,它必须确认Server接收到了该ACK。Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是两倍的MSL(Maximum Segment Lifetime)。MSL指一个片段在网络中最大的存活时间,2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
参考文章:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。