浅谈SSL握手、证书、证书校验

HTTPS基于SSL/TLS协议,SSL最初由NetScape在1994年设计,经过了三个版本的更新,在1999年由IETF标准化成为TLS,其在TCP的基础上实现了一层安全的协议,包括握手、证书下发、秘钥协商等。它是互联网安全的基础,也是解决http请求被重放、篡改、传输泄露的基本手段,本文先讨论一下ssl/tls握手,我们需要先介绍一下两个算法:

(1)DH算法

DH算法是W.Diffie和M.Hellman提出的一种算法。这个算法的功能是:通信双方中的任何一个使用自己的秘钥和另一方的公钥,得到一个对称秘钥,这一对称秘钥在其他任何实体中都无法获得。从这个描述很明显可以看出,这个和RSA比较像,这个算法几乎就是专门为秘钥协商而产生的,即可以允许通信的双方在一个完全公开的不可信的信道上交换信息以生成一致的、可共享的秘钥。通信的A方生成一堆秘钥(公钥、私钥),A方根据现有的秘钥协商生成一个秘钥算法。

(2)ECC算法

ECC算法(Elliptic curve cryptography)是基于椭圆曲线数学的一种公钥密码的方法。椭圆曲线在密码学中的使用是在1985年由Neal Koblitz和VictorMiller分别独立提出的。ECC实际上代表着一类非对称加密算法,它的许多形式可能有稍微的不同,但所有的都依赖于解决椭圆曲线离散对数问题的困难性上。

ECC算法安全性的基础是在椭圆曲线点集上计算离散对数的困难性。这一安全性基础的改版导致复杂的实现和处理过程,但却使得在保存安全性不变的情况下,所使用的秘钥长度大为减少。ECC算法的主要优势正是在某些情况下,它比其他的方法(如RSA)使用更小的秘钥来提供相当的或更高等级的安全性保障。ECC的另一个优势是可用定义群直接的双线性映射,基于Weil对或是Tate对;双线性映射已经在密码学中发现了大量的应用,例如基于身份的加密。不过一个缺点是加密和解密操作的实现比其他算法花费的时间长。

通常DH和ECC算法会组合使用,组成ECDH算法。

下面我们通过抓包的方式来看一下HTTPS连接建立的过程(TLS协议有多种秘钥协商方式,我们以上面提到的ECDH秘钥协商为例,):

ssl握手阶段,客户端向服务端发送Client Hello,主要向服务端提供自己支持的加密套件、协议版本、随机数Random1(用于后续的ECDH秘钥协商)等信息,如图当前版本为TLS1.2,支持的17种TLS加密套件(ClipherSuite)如图所示:

(1)服务端响应后,向客户端发送一个Server Hello响应,首先会从客户端传入的加密套件列表中选择一个加密算法,确定一个服务端的随机数Random2(用来做ECDH秘钥协商)

这一步,服务器会将自己的证书下发给客户端,其中Certificates即是证书(注意,除了网站的CA证书外,还一并把该CA证书的中级证书下发,所以是两个证书,组成一个证书链)

执行ECDH秘钥交换所需要的一些数据(秘钥协商的方式有多种,改进后的协商方式比早期版本中的RSA方式的安全性大大提升,如中使用的是DH协商方式):

图 TLS握手-服务端秘钥交换

图 Diffe-Hellman协商方式

然后服务端发送Server Hello Done表示服务端握手响应完成。

图 TLS握手-服务端响应完成

(1)客户端收到了服务端下发的证书,对证书的颁发机构、有效期等进行校验(注意,证书是验证服务端身份合法与否的唯一方式):

图 TLS握手-客户端秘钥协商

客户端通过服务器提供的公钥,然后生成一个随机数Random3,使用该公钥对称加密后(PreMaster Key,就是7-17中的Pubkey)再发送给服务器,服务器得到这个key,通过自己的私钥解密获得Random3,这时客户端和服务器双方已经具有了Random1+Random2+Random3,两边根据约定好的算法再次生成一个秘钥我们称之为PrivateKey,这个PrivateKey就是未来双方动态协商好的对数据传输进行加密的key,使用的是效率较高的对称加密算法。

(1)然后客户端发送Change Clipher Spec,就是告诉服务器现在改变加密算法,使用动态协商出来的秘钥加密,客户端和服务器分别给对方发送一次用PrivateKey加密后的消息以验证是否可以正常加解密,如果全部成功,则握手成功。

(2)后面就使用该秘钥正常的进行传输加密,可以看到图7-18所示,报文已经是经过加密的了:

图 TLS传输报文加密

通过以上过程的简单分析,可以看到客户端和服务器在没有任何共享任何秘密的情况下,在一个完全不安全的通信信道上协商出一个安全的通信秘钥,这是保证最终链路通信安全的本质。

TLS证书

在TLS握手但是这个过程也存在不安全的因素,典型的就是中间人攻击问题,如下图:

图 中间人攻击

由于在一个不安全的信道上传输,假如存在一个中间人的角色(网络链路中的路由器、防火墙、waf等设备及系统),将客户端的握手行为完全截获,中间人先和客户端握手,再伪装成客户端和和服务器做握手(注意不是纯粹的代理),然后将服务器返回的证书截获后,中间人再伪造一个证书返回给客户端,那么客户端就完全被欺骗了,如果客户端没有能力鉴别证书的真伪,那客户端会继续进行秘钥协商,在这个过程中会产生两个被协商出来的秘钥,正常的话,客户端和服务器间应该只有一个秘钥,二者实现点对点的加解密,但是一旦被中间人攻击后,其实客户端和中间人之间存在一个协商的秘钥KEY-1,而中间人和服务器间存在另一个协商的秘钥KEY,如图7-18。因此KEY-1的存在,导致中间人完全知晓客户端和服务器的通信,所以中间人攻击的最终目的其实是要和客户端协商出一个点对点的对称加密秘钥,只有知道了这个秘钥,才能对客户端发送的数据进行解密,而客户端如果在判断服务器证书的环境上存在纰漏,那就会完全被中间人欺骗。所以证书在这里的作用就至关重要了,证书存在的目的就是判断服务器的身份是否合法。

证书用于在电子支付、通信等环节用于对参与方的身份信息合法性的校验,依赖于PKI(公钥基础设施)体系的一些基础设施进行构建,是一组由硬件、软件、参与者、管理政策与流程组成的基础架构,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。证书的基本作用就是标识身份,传递信任。公钥存储在数字证书中,标准的数字证书一般由可信数字证书认证机构(CA,根证书颁发机构)签发,此证书将用户的身份跟公钥链接在一起。CA必须保证其签发的每个证书的用户身份是唯一的。

证书的信任原理就如同以往我们听到的介绍信一样,假设B信任A,C信任B,那么C信任A,就是这种信任传递关系构建了证书的信任体系,在这个信任链条中,总有一个信任的源头A,这个A是被大家无条件信任的,现实中,A就是根证书机构,它是受到大家公认的,只要是A,大家就无条件信任它。

链接关系(证书链)通过注册和发布过程创建,取决于担保级别,链接关系可能由CA的各种软件或在人为监督下完成。PKI的确定链接关系的这一角色称为注册管理中心(RA,也称中级证书颁发机构)。RA确保公钥和个人身份链接,可以防抵赖。如果没有RA,CA的根证书遭到破坏或者泄露,由此CA颁发的其他证书就全部失去了安全性,一般根证书机构不会直接颁发证书,都是由授权的中级证书机构颁发,所以现在主流的商业数字证书机构CA一般都是提供三级证书,Root 证书签发中级RA证书,由RA证书签发用户使用的证书。

比如百度主站的证书,baidu.com是一个站点证书(终端用户证书),由中级证书GlobalSign Organization Validation CA认证颁发,而该中级证书由根证书BlobalSign Root CA颁发,这三个证书组成了一个证书链,如图,

当浏览器访问站点时,在SSL握手阶段服务器会返回给客户端除了根证书之外的证书链,浏览器沿着证书链校验,如果通过了根证书的校验,则该证书是可信的。根证书由权威CA机构颁发,只要是根证书,操作系统、应用程序都会无条件的信任它,这些证书一般都会预先随着操作系统或者应用程序预先安装到系统里面,如Linux、windows、jre等都有自己的根证书库,如windows系统中预装的根证书:

每个证书都有一个指纹签名,是一个采用某种hash算法生成的唯一的字符串,用来唯一的标识一个证书,如GlobalSign Root CA通过SHA1算法进行的签名,如图所示:

在jdk中也预先安装了一些根证书,不过主要记录的就是根证书的指纹,可以通过jdk自带的keytool查看(默认密码是“changeit”):

keytool -list -keystore$JAVA_HOME/lib/security/cacerts

在jdk中植入了104个根证书,篇幅所限,我们列出了部分证书的信息,黑体的就是GlobalSign的根证书,可以看到指纹和windows中是一致的:

所以,一般权威机构以及授权的中级证书颁发机构签发的证书,都可以通过证书链校验追溯到这些根证书中完成证书的校验,当然有一些小众的CA甚至一些自签名的证书无法追溯到这些根证书,浏览器就会提示证书错误,如12306网站使用了一个自签名的证书,这个证书的根证书颁发机构没有在权威CA列表里所以不被浏览器信任从而提升风险,唯一的解决办法就是手段将其加入到信任列表,但是这存在安全风险。

我们可以在做这样一个测试(如图7-19所示),我们使用抓包工具Fiddler将一个自签名的根证书(DO_NOT_TRUST_FiddlerRoot)加入到本机根证书信任列表里,如图7-24所示:

百度使用了https并且启用了http的强制跳转,浏览器栏有个绿色的图标,显示地址是安全的:

我们查看一下证书:

发现证书已经被替换成了DO_NOT_TRUST_FiddlerRoot,Fiddler起到了一个中间人的作用,它截获TLS握手的请求,然后将自己自签名的一个临时证书下发给客户端,然后自己再伪装成浏览器客户端去和服务器握手,并做后续的秘钥协商。由于该证书被手工加入操作系统的信任列,因此浏览器无条件信任,并没有任何风险提示,

在这里出于测试的目的,我们手工信任该证书,但是如果用户在自己的电脑上按照了来历不明的软件,或者是某些别有用心的程序,可能会暗地里添加根证书,如果手机被ROOT或者越狱,同样会存在此类风险。当然,CA证书在https中只是其广泛的用途之一,而遭受中间人攻击只是其面临的风险之一,证书的校验变得尤为重要。

证书校验

前面的篇幅,我们讨论了证书存在的一些风险,当然证书存在很多风险,这只是其中之一,我们在实际中最有可能遇到也是影响最大的,我们聚焦到手机移动应用上,看一下移动应用上的风险。

在绝大部分移动应用中,存在两种方式的访问:

以REST风格的HTTP接口访问,如用户在APP上的一些登录请求、发短信验证码请求等,基于HTTP标准的METHOD,如GET/POST等

webView页面访问,如嵌入在app native中的H5页面

安卓本质上是linux的jvm上跑的java应用,使用的是java语言,我们以安卓系统为例,当安卓的应用程序发起https访问时,对证书的校验方法是依赖于TrustManager来实现的,如果需要对证书进行自定义校验,需要实现自定义的TrustManager:

在发起HTTPS访问时,指定该自定义的TrustManager即可:

有了证书的校验方式,对于证书的处理,有以下几种方式:

将服务器端证书打包到APP内部,在发起https请求时,在SSL握手阶段对证书进行强校验,这种校验是最严格的,程序对比一下本地保存的证书和从服务器拿到的证书是否相同,实现方法就是从一个输入流中读取证书的文件(扩展名一般是crt),用该流创建一个KeyStore,然后再创建一个TrustManager,总之最终结果就是用本地的证书创建自定义的TrustManager,在SSL握手阶段使用该TrustManager进行证书验证::

依赖的是TrustManager类,在SSL握手拿到证书的时候会回调该类的checkServerTrusted方法,该方法如果没有任何实现,则不会对服务器证书做任何校验。需要注意的是,SSL连接的握手校验有两个关键环节,除了验证证书是否值得信任外,还要验证主机名是否一致,比如你请求网站A,但是却返回了网站B的证书,如果验证不通过会报以下异常:

使用HostnameVerifier对象来校验:

一般如果采用自签名的证书,或者是一些比较小众或者未知的的CA颁发的证书,都需要采用这种方式,这基本上可以杜绝被中间人的可能,但是也存在问题,就是当服务器端的证书到期续签或者因为一些原因需要更换(如改为其他的CA签发)的时候,客户端必须得升级,否则客户端根本无法访问网络,因为在SSL握手阶段就被阻断了。所以如果不是自签名证书或者是已知的CA颁发的话,使用证书链校验即可。

证书链校验

我们知道了证书链的概念,所以,通过服务器返回的站点的证书路径,我们通过终端证书-中级证书-根证书的方式来逐级的校验,如果在受信任的证书列表中找到了一个根证书能对证书链上的证书进行签名,说明这个证书是受信任的。这种实现起来比较简单,因为在java的JDK里面都已经帮我们实现了必要的校验逻辑,而且对客户端是透明的,采用这种方式,使用默认的TrustManager即可:

SSLContext context=SSLContext.getInstance("TLS");

//TrustManager传null会使用系统默认的”SunX509”TrustManager

context.init(null,null,null);

URL url=new URL("https://www.baidu.com");

HttpsURLConnection connection= (HttpsURLConnection) url.openConnection();

connection.setSSLSocketFactory(context.getSocketFactory());

InputStream is=connection.getInputStream();

当SSLContext的init方法第二个参数传null时,TrustManager会使用默认的SunX509实现,代码如下:

TrustManagerFactory mgr = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())

mgr.init((KeyStore)null);

TrustManager[] var2= var4.getTrustMan·agers();

当然也可以在该TrustManager上扩展功能,但是一般不建议怎么做,除非极其特殊的场景。以上的验证方法在Android系统上也基本类似。在发起REST接口访问时,即可以根据场景使用不同的验证方法。那么当native中嵌入WebView时该如何保证访问的安全性呢?以下是WebViewClient的部分方法:

在WebView加载页面时,会进行证书的校验,当校验出现错误时会回调该方法,SslError对象中会将证书传递过来,可以在这里进行自定义的验证,自定义的验证逻辑可以对传入的证书做任意的自定义校验,同时决定是proceed还是cancel通过以上的分析,可以看到,请求的安全保障依赖于客户端和服务器双方的很多契约,同时客户端对证书的校验是整个保障的基础。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180925G0TR3V00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券