移动安全之CertificatePinning
背景:
项目组是为美国一商业银行开发移动支付类软件.提到银行类软件,大家第一反应都是安全性的要求特别高,的确在开发过程中,我们对安全方面也做了非常多的处理跟防护.这里先介绍下比较重要的一个——证书锁定 (CertificatePinning)
什么是CertificatePinning
在SSL/TLS通信中,客户端通过数字证书判断服务器是否可信,并采用证书的公钥与服务器进行加密通信.
然而在很多移动应用中,开发者不检查服务器证书的有效性,或选择接受所有的证书,这样就会导致中间人攻击.
关于这点,举个例子.在钓鱼WiFi网络中,攻击者可以通过设置DNS服务器使客户端与指定的服务器进行通信。攻击者在服务器上部署另一个证书,在会话建立阶段,客户端会收到这张证书。如果客户端忽略这个证书的异常,或者接受这个证书,就会成功建立会话、开始加密通信。因为攻击者拥有私钥,他可以解密得到客户端发来数据的明文。攻击者还可以模拟客户端,与真正的服务器联系,充当中间人做监听.(摘取自互联网)
解决问题的一种方法是从可信CA申请一个证书.但在移动软件开发中,不推荐这种方法.这种验证只判断了证书是否CA可信的,并没有验证服务器本身是否可信.攻击者可以盗用其他可信证书,或者盗取CA私钥为自己颁发虚假证书.
事实上,移动软件大多只和固定的服务器通信,因此可以在代码更精确地直接验证某张特定的证书,这种方法称为“证书锁定”(certificatepinning)
如何CertificatePinning
-
首先服务器端使用RSA算法生成一对公钥私钥对,服务器端持有私钥,线下将公钥传给客户端。App中将这个值硬编码到本地。
-
App端可以自己实现一个X509TrustManager接口,在其中的CheckServerTrusted()方法里通过证书链拿到PublicKey,
-
比较1和2中进行md5的值,如果匹配则服务器验证通过,否则立即终止与此服务器的通信
示例代码(Android)
public final class PubKeyManager implements X509TrustManager { private static String PUB_KEY = "30820122300d06092a864886f70d0101" + "0105000382010f003082010a0282010100b35ea8adaf4cb6db86068a836f3c85" + "5a545b1f0cc8afb19e38213bac4d55c3f2f19df6dee82ead67f70a990131b6bc" + "ac1a9116acc883862f00593199df19ce027c8eaaae8e3121f7f329219464e657" + "2cbf66e8e229eac2992dd795c4f23df0fe72b6ceef457eba0b9029619e0395b8" + "609851849dd6214589a2ceba4f7a7dcceb7ab2a6b60c27c69317bd7ab2135f50" + "c6317e5dbfb9d1e55936e4109b7b911450c746fe0d5d07165b6b23ada7700b00" + "33238c858ad179a82459c4718019c111b4ef7be53e5972e06ca68a112406da38" + "cf60d2f4fda4d1cd52f1da9fd6104d91a34455cd7b328b02525320a35253147b" + "e0b7a5bc860966dc84f10d723ce7eed5430203010001"; public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { if (chain == null) { throw new IllegalArgumentException("checkServerTrusted: X509Certificate array is null"); } if (!(chain.length > 0)) { throw new IllegalArgumentException("checkServerTrusted: X509Certificate is empty"); } if (!(null != authType && authType.equalsIgnoreCase("RSA"))) { throw new CertificateException("checkServerTrusted: AuthType is not RSA"); } // Perform customary SSL/TLS checks try { TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); tmf.init((KeyStore) null); for (TrustManager trustManager : tmf.getTrustManagers()) { ((X509TrustManager) trustManager).checkServerTrusted(chain, authType); } } catch (Exception e) { throw new CertificateException(e); } // Hack ahead: BigInteger and toString(). We know a DER encoded Public Key begins // with 0x30 (ASN.1 SEQUENCE and CONSTRUCTED), so there is no leading 0x00 to drop. RSAPublicKey pubkey = (RSAPublicKey) chain[0].getPublicKey(); String encoded = new BigInteger(1 /* positive */, pubkey.getEncoded()).toString(16); // Pin it! final boolean expected = PUB_KEY.equalsIgnoreCase(encoded); if (!expected) { throw new CertificateException("checkServerTrusted: Expected public key: " + PUB_KEY + ", got public key:" + encoded); } } } }
CertificatePinning的应用
在实际应用中,一款移动应用往往不止一个后台,尤其是在支付类产品中,经常需要去集成第三方支付网关。
但是第三方的服务什么时候会更改证书,这个就说不准了。初期,我们会把所有的这些第三方的服务提供的PublicKey都硬编码在本地,但是有次其中一个服务商自己改掉了,造成用户手上的产品直接不能使用了, 这个就给我们带来了不必要的麻烦。
解决方案:
-
自己的服务端使用RSA算法生成一对公钥私钥对,服务器端持有私钥,线下将公钥传给客户端。App中将这个值硬编码到本地。
-
自己的服务端提供API,获得当前所有服务器(包括第三方)公钥的SHASUM值.当然这个值必须通过1中存在本地PublicKey签名验证得到
-
再通过最基本CertificatePinning的办法(第二章提到的),直接从各服务端拿到各自公钥, 连同本地PublicKey一起计算SHASUM。
-
比较2和3中的值,如果相同则CertificatePinning通过,否则终止app.
这种方案将解决由第三方服务突然更换公钥,而导致App不能使用的情况。后期,我们还将优化这个方案,在最后一步如果不同的时候再次发送请求通知自己的服务器端,将错误数据发送让其检验是否是真更改还是存在不安全的环境。如果确实是第三方服务器上的证书更改了,服务器应该返回最新的公钥SHASUM值,如果没有更改,则返回原来的SHASUM值。
附录:概念解释
Concept | Description |
SSL | http://baike.baidu.com/link?url=el1VbbUH0_HGol9_Gx36L_oiwMwaJiQmKz94iAFyVJRJgkNLI_lS54MKVo7ERwil7mBcSWsAZ3pwZnNfyyxehK |
CA | http://baike.baidu.com/link?url=6uOhzRjx7XBPhqbrHtTeNwWjhwsjsAabU_Uwj3Tsh7zCDZPz6fknzW8yFP8YVfIXATkehdZSgr8ziPPHWVvAF_ |
RSA | http://baike.baidu.com/link?url=cvzT7VjNxiYJVeRVHAQkTRGb1Adq74aSkjYO5qPZAYeaLehtE2RVeg9UK4i3h9qMTuNCta9ND5MWv6iGkoZnNBMKbXm3hrClyXvyNf43u-BmI3EJ4HLwpGRq16tCY2Wo |
PublicKey /PrivateKey | http://baike.baidu.com/link?url=yJmN72KB_gczeCEj4ocDiPxVPoJKJrpqmdYIuBD9Lc4gt7oEJ34M6Bf5bj3Kmm68RHSi4Ovt79tleMe9GtM96a |
附录:参考文档
https://www.owasp.org/index.php/Certificate_and_Public_Key_Pinning