Certificate Pinning是什么,有什么用?

  Certificate Pinning,或者有叫作SSL Pinning/TLS Pinning的,都是指的同一个东西,中文翻译成证书锁定,最大的作用就是用来抵御针对CA工击。在实际当中,它一般被用来阻止man-in-the-middle(中间人工击)。

  说起中间人工击,可能不是那么直观,但是这一类工具我们可能经常会用到,如Mac平台的Charles和Windows平台的Fiddler。如果一个应用使用了Certificate Pinning技术,那么你使用前边说的工具是无法直接来调试/监控应用的网络流量的。

  当应用通过HTTPS握手连接到Fidder/Charles时,应用会检查请求的response的证书,如果发现与预设的不一致,会拒绝后续的网络请求,从而增加应用与服务器的安全通信。

  关于破解/越狱系统可以突破上述的限制,则是另外一个话题,这里就不展开了。

  为什么要用,我可以不用吗?

  没有绝对的安全,用或者不用都是权衡各种利弊,最后的一个妥协的结果。

  认为不应该使用的理由是:

  一般来说,操作系统自己的trust store就可以信赖了

  如果使用,应用需要在证书过期前更新证书,重新发版

  …

  认为应该使用的,可能是

  万一操作系统被破解,怎么办?就像上边提到的一样

  反正我的应用需要经常迭代,没关系

  …

  公司的安全部门要求应用里边做Certificate Pinning (有些能自己掌控的就不要依赖被人的意味)

  接下里,我们假定经过了各种权衡之后,我们同意后者。那么要怎么做呢?

  好的,要怎么实现呢?

  在做之前,我们先了解一下我们可以根据什么来Pinning?一般来说,可以直接Pin证书,或者Pin证书的public key。

  这里以Android平台为例子,看看我们一般都是怎么做的。

  学院派实现 - Pin证书

Certificate Pinning是如何工作的?_第1张图片

  其实是固定的写法,基本流程就是(其实注释已经很清楚了):

  加载证书文件,并使用CertificateFactory生成一个X509Certificate的实例

  创建一个KeyStore实例,并把前边的X509Certificate实例加进去,并起一个别名

  注意,这里其实是可以加多个证书进去的,但是注意别名不要重复,因为底层实现是使用一个Map存储别名与证书的

  创建一个TrustManager,并且使用前边的KeyStore实例进行初始化

  创建一个SSLContext,并且使用前边的TrustManager实例进行初始化

  最后,使用SSLContext创建一个SSLSocketFactory实例,并且把它赋值给我们用于https的请求连接对象HttpsURLConnection

  特别简单的配置文件实现 - Pin证书/Public Key

  在Android 7.0之后,Android支持一种特别方便的实现,只需要在Manifest文件的android:networkSecurityConfig属性加上对应的配置文件即可。

  这种方式支持证书文件和hash的public key两种形式。

  配置文件长这个样子

Certificate Pinning是如何工作的?_第2张图片

  OkHttp的实现 - Pin Public Key

  Android领域很多和网络相关的Library都支持OkHttp作为底层的网络请求引擎。所以我们可以看看OkHttp里边是怎么实现的。

  OkHttp也可以通过sslSocketFactory()来实现第一种的Pinning,不过我们可以通过其专门提供的CertificatePinner更加方便的实现Pinning。

  看一下例子:

Certificate Pinning是如何工作的?_第3张图片

  题外话:publicobject.com其实是OkHttp的作者Jesse Wilson自己的网站。

  作者在Google的时候创建了OkHttp,后边去了Square,把它“发扬光大”,后边又被Google拿过去作为Android系统底层网络请求的底层实现了。

  那么如何拿到上面所需要的hash值呢?官方给的一个方法是,先填写一个错的hash值,然后根据随后的exception的stack trace message,得到对应的hash值。

  其实也可以通过openssl提供的命令直接从der或者pem格式的证书中计算出来,由于命令相对复杂一些,我写了一个简单的脚本封装了一下,支持两种格式。

  下载链接是:get sha256 of x509 certificate.sh

  Pinning是如何工作的?

  那么上述的那些实现底层是如何工作的,怎么样保证Pinning呢?网上其实有比较详细讲解其工作原理的,但是可能偏理论化。这里以OkHttp为例,看一下它是如何实现的。

  我分析的版本是4.0.1,其他版本可能略有不同,但是大体流程是一样的。

  这里我画了一个简单的调用时序图(有些地方省略了,主要看一下它的调用流程),它可以帮助我们明白OkHttp到底是在什么时候进行Pinning的。

  

Certificate Pinning是如何工作的?_第4张图片


  我们可以看到,实在运行到预设的ConnectInterceptor时,进行Pinning的。

  这里主要看一下RealConnection.check方法的实现

Certificate Pinning是如何工作的?_第5张图片

Certificate Pinning是如何工作的?_第6张图片

  关于这个图的绘制,我把它的源代码也分享出来,有兴趣的朋友可以试试 Mermaid Live Editor。

  这个方法也是整个调用连的核心,在这里OkHttp创建了Socket连接,然后在执行完SSLSocket的握手创建SSLSession,从其里边拿到一个Certificate数组(getPeerCertificates),然后以此计算每个证书的subjectPublicKeyInfo的sha256 hash值,与预设的值做比较,如果有一个符合的,则验证通过,否则,则会抛一个SSLPeerUnverifiedException异常告知开发者。

  如果要打破砂锅问到底,那么就需要去看JDK中关于SSLSocket的实现代码了。

  Summary

  稍微总结一下,我们依次

  了解了为什么要做Pinning,即它可以解决什么问题

  Pinning也不是完美的,它也有它的弊端,还要根据自己的实际情况来决定是否使用

  以Android平台为例,列举了三种常见的实现方法

  以OkHttp为例,稍微深入了解了它是如何实现的

  关于在其他平台,比如iOS, Flutter可以去网上搜索相关的解决方案。