1. SSL简介
证书锁定(SSL/TLS Pinning)顾名思义,将服务器提供的SSL/TLS证书内置到移动端开发的APP客户端中,当客户端发起请求时,通过比对内置的证书和服务器端证书的内容,以确定这个连接的合法性。
2. 证书锁定原理
证书锁定(SSL/TLS Pinning)提供了两种锁定方式: Certificate Pinning 和 Public Key Pinning,文头和概述描述的实际上是Certificate Pinning(证书锁定)。
1.1 证书锁定
我们需要将APP代码内置仅接受指定域名的证书,而不接受操作系统或浏览器内置的CA根证书对应的任何证书,通过这种授权方式,保障了APP与服务端通信的唯一性和安全性,因此我们移动端APP与服务端(例如API网关)之间的通信是可以保证绝对安全。但是CA签发证书都存在有效期问题,所以缺点是在证书续期后需要将证书重新内置到APP中
1.2 公钥锁定
公钥锁定则是提取证书中的公钥并内置到移动端APP中,通过与服务器对比公钥值来验证连接的合法性,我们在制作证书密钥时,公钥在证书的续期前后都可以保持不变(即密钥对不变),所以可以避免证书有效期问题。
所以我们最终选择使用公钥锁定,为避免证书过期需要客户端更新的问题
1.3 客户端获取公钥
这里采用公钥锁定的方式,则需要获取证书公钥的摘要hash,该值由服务器端提供,提取证书的摘要hash并查看base64的格式,示例:bAExy9pPp0EnzjAlYn1bsSEGvqYi1shl1OOshfH3XDA=,这就是我们将要进行证书锁定的指纹(Hash)信息
1.4 客户端使用SSL锁定选择
1、新海外使用的网络框架是Alamofire,其实该SDK本身支持SSL证书锁定,但是只支持证书锁定,需要将证书放在项目,读取证书的内容进行锁定
2、常用的第三方SDKTrustKit
,提供了使用公钥锁定,只需设置域名和对应的Hash值,然后处理网络系统回调方法:
func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
// 处理证书校验
}
所以这里选择使用TrustKit来进行公钥锁定
1.5 项目支持SSL证书锁定
1、podFile引入TrustKit框架,github源码
2、初始化TrustKit,在AppDelegate应用程序启动的方法中添加以下代码:
let trustKitConfig: [String: Any] = [
kTSKSwizzleNetworkDelegates: false,
kTSKPinnedDomains: [
"oversea-sit.yolanda.hk": [
kTSKIncludeSubdomains: true,
kTSKEnforcePinning: true,
kTSKPublicKeyHashes: [
"rseBZG6ykune+DEw3uysPJokdBusGMtrTeJlHd7bHLA=",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
]
]
]]
TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
3、创建自定义代理类:
class CustomSessionDelegate: SessionDelegate {
override func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
print("testLog: didReceive challenge")
if TrustKit.sharedInstance().pinningValidator.handle(challenge, completionHandler: completionHandler) == false {
// TrustKit did not handle this challenge: perhaps it was not for server trust
// or the domain was not pinned. Fall back to the default behavior
completionHandler(.performDefaultHandling, nil)
}
}
}
4、修改网络的初始化方法,设置自定义代码类
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = 10
configuration.timeoutIntervalForResource = 10
self.session = Session(configuration: configuration, delegate: CustomSessionDelegate())
5、测试验证
使用Charles设置网络抓包,网络请求接口响应错误提示:
Charles上的表现截图:
修改前在Charles上的截图:
1.6 问题记录
1、按照正常接入流程,发现在项目中使用Alamofire接口请求,不会出发系统方法func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void),于是新建Demo工程,使用系统自带的URLSession发起网络请求,可以正常触发didReceiveChallenge 方法,然后引入AlamofireSDK,使用跟新海外发起网络请求一样的操作,也可以触发didReceiveChallenge 方法,所以猜测是否是项目中有什么特殊配置或者其他的三方库影响导致的,最终确实是因为新海外项目中引入了LLDebugTool这个库,导致无法触发系统的方法,猜测可能是对于网络请求的监控有覆盖设置等操作,屏蔽这个库,便能正常了
这个类是开发人员在Debug环境下才引入的,所以屏蔽这个库对项目没有任何影响
2、为什么有LLDebugTool就不能正常触发didReceiveChallenge方法?
查看LLDebugTool源码,对于网络监控,重写了系统NSURLProtocol类的方法,在每次发起接口请求时,就重新设置了URLSession的delegate为自己,所以关于系统的代理方法都执行到了这个类里面