一次iOS客户端PKI及HTTPS双向认证的踩坑记录
背景
需求背景:
- 最近公司的产品涉及App 与硬件端的交互,App与云端的交互,硬件端与云端的数据交换, 公司需要保障每一端的身份得到信任,于是引入PKI体系,从外部寻找了对应的PKI供应商负责协助搭建与提供PKI相关服务;
- 当硬件端变成server角色时,需要对请求的客户端client进行身份认证。 如果仅仅只认证server的身份,我们只用单项认证就可以了。 但是如果要对client也进行身份认证,那么就需要用到双向认证的相关理论和技术。
- 要保障各个端的通信安全,需要保障在各个端通信时数据是被加密的,并且采用可信的证书签名数据进而交换;
基础理论
做的过程中,涉及到一些技术的名词,先做一些解释和理解, 包括PKI,密码学与加解密 (公私钥,证书链,CA), 证书格式, HTTPS, 双向认证以及单项认证, 抓包分析;
PKI
A public key infrastructure (PKI) is a set of roles, policies, hardware, software and procedures needed to create, manage, distribute, use, store and revoke digital certificates and manage public-key encryption.
PKI的解释可以参见 百度百科PKI ,或者 维基百科PKI 。 它称作公钥基础设施。本质上它是一种方式方法,用来管理与实现数据的加密交换,证书管理等,是一套加解密安全保障机制;
证书
首先谈一下证书,CA这些。 因为这些内容和密码学密切相关,可以先初步对现代密码学的一些基础理论和技术进行了解与熟悉。
加解密与签名:
- 在现在的数据安全中,涉及数据的加解密,一般有对称加解密,非对称加解密;
- 当我们对数据进行散列得到摘要,在用私钥进行加密,就得到数据的签名;
至于具体的加解密过程和签名算法之类,可以自行网上搜索了解;
那么,证书是什么? 它从何而来?它又有什么作用呢?
证书生成:
其实这里提到的是CA证书(Certificate Authority Certificate),CA是Certificate Authority的缩写,也叫“证书授权中心”。CA证书其实本质是一段明文数据和加密数据的组合。 CA证书可采用openssl生成;
openssl生成证书的过程可参考:
通过openssl生成私钥: openssl genrsa -out server.key 1024
根据私钥生成证书申请文件csr : openssl req -new -key server.key -out server.csr
使用私钥对证书申请进行签名从而生成证书: openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650
这样就生成了有效期为:10年的证书文件
认识证书
x509 证书一般会用到三类文件,key,csr,crt。Key是私用密钥,openssl格式,通常是rsa算法。 csr是证书请求文件,用于申请证书。在制作csr文件的时候,必须使用自己的私钥来签署申请,还可以设定一个密钥。crt是CA认证后的证书文件(windows下面的csr,其实是crt),签署人用自己的key给你签署的凭证。
数字证书的内容(CA证书内容)
X.509是常见通用的证书格式。所有的证书都符合为Public Key Infrastructure (PKI) 制定的 ITU-T X509 国际标准。
X.509 是比较流行的 SSL 数字证书标准,包含(但不限于)以下的字段:
字段 | 值说明 |
---|---|
对象名称(Subject Name) | 用于识别该数字证书的信息 |
共有名称(Common Name) | 对于客户证书,通常是相应的域名 |
证书颁发者(Issuer Name) | 发布并签署该证书的实体的信息 |
签名算法(Signature Algorithm) | 签名所使用的算法 |
序列号(Serial Number) | 数字证书机构(Certificate Authority, CA)给证书的唯一整数,一个数字证书一个序列号 |
生效期(Not Valid Before ) | 无 |
失效期(Not Valid After) | 无 |
公钥(Public Key) | 可公开的密钥 |
签名(Signature) | 通过签名算法计算证书内容后得到的数据,用于验证证书是否被篡改 |
指纹(fingerPrint) | 证书的ID |
那么 ,CA证书一般是什么样的格式存在呢?
PKCS#7 常用的后缀是: .P7B .P7C .SPC ;
PKCS#12 常用的后缀有: .P12 .PFX (iOS都会知道开发者证书导入的时候用到了 .P12文件,)
X.509 DER 编码(ASCII)的后缀是: .DER .CER .CRT ,der,cer文件一般是二进制格式的,只放证书,不含私钥 (在iOS中一般是这种.der, .cer格式);
.cer/.crt是用于存放证书,它是2进制形式存放的,不含私钥(crt文件可能是二进制的,也可能是文本格式的,应该以文本格式居多,功能同der/cer);
X.509 PAM 编码(Base64)的后缀是: .PEM .CER .CRT (.pem 跟crt/cer的区别是它以 Ascii来 表示,pem文件一般是文本格式的,可以放证书或者私钥,或者两者都有,pem如果只含私钥的话,一般用.key扩展名,而且可以有密码保护)
pfx/p12用于存放个人证书/私钥,他通常包含保护密码,2进制方式(pfx,p12文件是二进制格式,同时含私钥和证书,通常有保护密码)
p10是证书请求、p7r是CA对证书请求的回复,只用于导入、p7b以树状展示证书链(certificate chain),同时也支持单个证书,不含私钥。
怎么判断是文本格式还是二进制?
用记事本打开,如果是规则的数字字母,如
—–BEGIN CERTIFICATE—–
MIIE9jCCA96gAwIBAgIQVXD9d9wgivhJM//a3VIcDjANBgkqhkiG9w0BAQUFADBy
—–END CERTIFICATE—–
就是文本的,上面的BEGIN CERTIFICATE,说明这是一个证书,如果是—–BEGIN RSA PRIVATE KEY—–,说明这是一个私钥
自签证书的生成:
使用openssl来生成一些列的自签名证书
(1)创建根证书私钥:
openssl genrsa -out root.key 1024
(2)创建根证书请求文件:
openssl req -new -out root.csr -key root.key
(3)创建根证书
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650
HTTPS
为什么要用HTTPS? 解决了什么问题?
我们知道http的缺点:
1. 数据明文
2. 不验证通信方的身份
3. 数据报文容易被篡改
4. 容易遭受MITM攻击
16188020047381.jpgHTTPS 是运行在 TLS/SSL 之上的 HTTP,与普通的 HTTP 相比,在数据传输的安全性上有很大的提升。
为了提高安全性,我们常用的做法是使用对称加密的手段加密数据。可是只使用对称加密的话,双方通信的开始总会以明文的方式传输密钥。那么从一开始这个密钥就泄露了,谈不上什么安全。所以 TLS/SSL 在握手的阶段,结合非对称加密的手段,保证只有通信双方才知道对称加密的密钥。
So, 为什么要用双向认证? 双向认证解决了什么问题?
双向认证,顾名思义,客户端和服务器端都需要验证对方的身份,在建立https连接的过程中,握手的流程比单向认证多了几步。单向认证的过程,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。
双向通信流程,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。
能保证通信的双方是指定的端,在很多P2P(端对端)的通信中,也有很多实际的应用。
双向认证
什么是单向认证?
16188021507084.jpg- 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务器端;
- 服务器端将本机的公钥证书(server.crt)发送给客户端;
- 客户端读取公钥证书(server.crt),取出了服务端公钥;
- 客户端生成一个随机数(密钥R),用刚才得到的服务器公钥去加密这个随机数形成密文,发送给服务端;
- 服务端用自己的私钥(server.key)去解密这个密文,得到了密钥R
- 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
什么是双向认证?
16188021983690.jpg- 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端;
- 服务器端将本机的公钥证书(server.crt)发送给客户端;
- 客户端读取公钥证书(server.crt),取出了服务端公钥;
- 客户端将客户端公钥证书(client.crt)发送给服务器端;
- 服务器端解密客户端公钥证书,拿到客户端公钥;
- 客户端发送自己支持的加密方案给服务器端;
- 服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后发送给客户端;
- 客户端使用自己的私钥解密加密方案,生成一个随机数R,使用服务器公钥加密后传给服务器端;
- 服务端用自己的私钥去解密这个密文,得到了密钥R
- 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。
可以理解为,在SSL握手阶段,客户端和服务端通过非对称加密,以及证书链校验,协商并确保双方得到一个会话秘钥,连接成功后,采用这个会话秘钥对数据进行加密传输,就可以保障数据的安全性;
证书的校验是如何执行的?
首先,客户端收到服务端发来的证书链数据,类似下图:
16188022588233.jpg
服务端证书一般由中间证书签发,而中间证书由根证书进行签发,根证书由CA机构生成,上一级的证书是下一级证书的签发者,由此形成的证书树形结构,就是证书链。证书链校验过程如下:
1.取上级证书的公钥,对下级证书的签名进行解密得出下级证书的摘要digest1 ;
2.对下级证书进行信息摘要digest2;
3.判断digest1是否等于digest2,相等则说明下级证书校验通过;
- 依次对各个相邻级别证书实施1~3步骤,直到根证书(或者可信任锚点[trusted anchor]);
备注:因为下级证书是上级证书CA进行签发颁布的,上级CA会用自己的私钥,对签发的下级证书的相关信息进行加密,得到下级证书的签名;
所以上级证书的公钥能够解密下级证书的签名,也能证明下级证书的上级CA 是正确的。同时根证书或者锚点证书是内置在系统中作为可信证书的
另外: 查看证书的数据显示,还有一个叫做指纹的,而指纹是证书的唯一值,通常用于在证书库中查找特定证书。
指纹不是证书的一部分。相反,它是通过获取整个证书(包括签名)的加密哈希来计算的。
不同的加密实现可能使用不同的散列算法来计算指纹,从而为同一证书提供不同的指纹。
(例如,Windows Crypto API计算证书的SHA-1哈希以计算指纹,而OpenSSL可以生成SHA-256或SHA-1哈希。)因此,您需要确保使用数据库指纹的客户端使用相同的API或一致的哈希算法。
iOS中的双向认证:
目前我们iOS的项目是OC语言的项目,项目中的网络请求用了著名的第三方开源库AFNetwoking, 我们知道AFNetworking也是基于苹果原生网络类NSURLSession封装得到。
目前获取数据的接口API后端环境 ,已开启了双向认证,服务端双向认证的配置由IT人员完成。于是需要在AFN请求接口的时候完成HTTPS双向认证的处理。
还有一部分也需要注意,App当中会涉及利用WKWebView加载一些H5页面,而前端的网页部署是同一套环境的时候,访问也需要完成HTTPS的认证,同时网页当中也会通过我们的API接口访问一些数据,这些也是需要完成HTTPS双向认证。 所以,iOS当中会有至少两部分的HTTPS认证处理。
实际处理如下:
- 第一部分,项目工程ATS的配置
- 第二部分,针对接口网络请求: NSURLSession,NSURLConnection 等的认证处理;
- 第三部分,针对WKWebView加载H5页面的处理;
iOS的网络请求中,有URL Loading System,是整个网络请求上层的基础:
16188024041573.jpg在认证过程中,我们最主要接触到和处理的是: NSURLSession 、NSURLCredential 、NSURLProtocol
接口网络请求的双向认证处理:
- 当在iOS中发起一个网络请求,如果是HTTPS的域名,NSURLSession会触发回调方法,-URLSession:didReceiveChallenge:completionHandler: 回调中会收到一个 challenge,也就是质询,需要你提供认证信息才能完成连接。通过challenge.protectionSpace.authenticationMethod 取得保护空间protectionSpace要求我们认证的方式;
- 如果这个值是 NSURLAuthenticationMethodServerTrust 的话,代表需要对服务端证书的认证挑战进行处置,如果这个值是 NSURLAuthenticationMethodClientCertificate 的话,代表服务端要求客户端提供证书接受认证挑战;
查看AFN的源码知道,AFHTTPSessionManager实例有个方法
- setSessionDidReceiveAuthenticationChallengeBlock:
就是用来实现 -URLSession:didReceiveChallenge:completionHandler: 的代理方法的, 所以,对于接口的HTTPS双向认证,我们都可以放在这个代理方法中去实现, 具体实现后面再说。
WKWebview加载网页H5的双向认证处理:
UIWebView UIWebView does not provide any way for an app to customize its HTTPS server trust evaluations (r. 10131336) . You can work around this limitation using an NSURLProtocol subclass, as illustrated by Sample Code 'CustomHTTPProtocol'.
从官方 HTTPS Server Trust Evaluation 的介绍来看,对于webView加载自签名的HTTPS网站,不能直接采用NSURLSession的方式处理。根据 iOS使用NSURLProtocol来Hook拦截WKWebview请求 的介绍, 因为webView的内核通信是IPC(进程间通信),和APP是不同的进程,不能对web的请求直接进行拦截处理。
WKWebView 在独立于 App Process 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在WKWebView 上直接使用 NSURLProtocol 无法拦截请求。
第一种处理方式: 我们可以使用私有类 WKBrowsingContextController 通过 registerSchemeForCustomProtocol 方法向 WebProcessPool 注册全局自定义 scheme 来达到我们的目的
同时,在让WKWebview支持NSURLProtocol 的时候,也需要注意一些官方对于WKWebView的审核规则,避免出现私有API的调用,具体实现方式见后面。
因为以上第一种处理方式,经验证,避免不了Web当中存在的POST请求body数据被清空,需要额外处理,同时,处理的方式也比较复杂。 但发现WKWebView已经提供了代理方法,用来处理自定义证书信任策略等,于是采用 第二种处理方式 ,我们在
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential))completionHandler
当中去实现H5的双向认证逻辑
实践
编码
根据整个业务流程,我们把编码实现过程大致分为以下几步:
- App启动时候,通过PKI供应商的HTTPS通道,进行证书的申请和校验,并将证书保存在App中,并且完成ATS的设置;
- 从证书当中导出双向认证需要的自签名的根证书,以及该移动设备关联的客户端证书数据;
- 在App当中接口请求的时候,通过根证书,以及客户端证书数据完成双向认证;
- 在App当中完成加载H5页面时,
拦截并重构请求,完成网页及网页请求接口的双向认证,通过代理实现双向认证; - 优化App应用证书的启动过程,以及证书管理的安全等;
证书的申请,因为采用第三方的SDK实现的,编码工作较为简单,简单调用SDK的API实现即可,本质上是一个文件下载的过程,下载下来的证书数据格式为 .pfx / .p12,保存在App的沙河目录下,有口令可以对P12文件数据进行导出导入,而ATS设置中,需要对于公司HTTPS域名进行例外处理,而ATS不开启会触发额外的审核,上架时候需要说明ATS设置的缘由
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionDomains</key>
<dict>
<key>xxxx.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
从证书当中导出所需数据
NSData *PKCS12Data = [NSData dataWithContentsOfFile:localFilePath];
self.pkcs12FileData = PKCS12Data;
if (PKCS12Data) {
SecTrustRef trust = NULL;
if ([self extractServerTrustData:&trust fromPKCS12Data:PKCS12Data]) {
CFIndex certCount;
certCount = SecTrustGetCertificateCount(trust);
if (certCount >= 1) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(trust, certCount - 1);
NSData *data = (__bridge_transfer NSData *)SecCertificateCopyData(certificate);
self.serverRootCerData = data;
}
}
}
// extractServerTrustData当中实现数据的获取
NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:authCode
forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data, (__bridge CFDictionaryRef)optionsDictionary, &items);
if (securityError == 0) {
CFDictionaryRef trustDataDict = CFArrayGetValueAtIndex(items, 0);
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue(trustDataDict, kSecImportItemTrust);
*outTrust = (SecTrustRef)tempTrust;
*outIdentity = (SecIdentityRef)CFDictionaryGetValue(trustDataDict, kSecImportItemIdentity);//客户端证书凭证数据
*certArr = (CFArrayRef)CFDictionaryGetValue(certDataDict, kSecImportItemCertChain);
}
接口进行双向认证
// 如果是NSURLSession请求,代理中进行处理, AFN请求中,调用 [manager setSessionDidReceiveAuthenticationChallengeBlock:]
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *customCredential = nil;
// 对服务端证书进行认证
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
OSStatus err;
SecTrustRef trust = [[challenge protectionSpace] serverTrust];
if (trust == NULL) {
return;
}
// 设置锚点证书
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
NSMutableArray *pinnedCertificates = [NSMutableArray array];
// 证书的数据是之前获取的根证书数据
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverRootCerData)];
err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates);
if (err == noErr) {
err = SecTrustSetAnchorCertificatesOnly(trust, false);
}
CFErrorRef error = NULL;
if (@available(iOS 12.0, *)) {
__unused bool r = SecTrustEvaluateWithError(trust, &error);
if (error == noErr) {
customCredential = [NSURLCredential credentialForTrust:trust];
}
} else {
SecTrustResultType trustResult = kSecTrustResultInvalid;
err = SecTrustEvaluate(trust, &trustResult); //kSecTrustResultRecoverableTrustFailure
if (err == noErr) {
if ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)) {
customCredential = [NSURLCredential credentialForTrust:trust];
}
}
}
} else {
// 服务端接收客户端证书认证
SecIdentityRef identity = NULL;
CFArrayRef certArray = NULL;
if ([self extractIdentity:&identity fromPKCS12Data:pkcs12FileData certArray:&certArray]) {
// identity 系统使用的证书数据身份,客户端证书对应的数据
// certificates 建议传nil,除非服务端需要传递 intermediate certifate,一般服务端内置了中间证书
// NSURLCredentialPersistenceForSession 对于网络双向认证,只用填写这个值就可以
if (identity) {
customCredential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistenceForSession];
}
disposition = NSURLSessionAuthChallengeUseCredential;
}
}
if (completionHandler) {
completionHandler(disposition, customCredential);
}
}
App WKWebView加载 H5
首先需要通过NSURLProtocol 对webView请求拦截重构,实现双向认证的处理
// 避免私有API审核被拒
Class cls = [[[WKWebView new] valueForKey:@"browsingContextController"] class];
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if (cls && [cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// 只需要匹配https的scheme, 这里不拦截http协议的请求
[cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
}
// 注册网络请求协议代理类到URL Loading System
[NSURLProtocol registerClass:LLURLProtocol.class];
在 LLURLProtocol的类中,我们把webView的请求进行了HOOK,然后通过 NSURLSession 构造了新的请求。LLURLProtocol是继承自NSURLProtocol, NSURLProtocol需要实现几个协议方法,可以 理解 NSURLProtocol:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request;
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request;
- (void)startLoading;
- (void)stopLoading;
于是我做了如下的实现:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
if (![request.URL.scheme isEqualToString:@"https"]) {
return NO;
}
if ([NSURLProtocol propertyForKey:HTTPHandledIdentifier inRequest:request]) {
return NO;
}
// 对于指定的host,允许hook,则返回YES,否则NO
return result;
}
// 插入防止重复请求的标志
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReqeust = [request mutableCopy];
[NSURLProtocol setProperty:@YES
forKey:HTTPHandledIdentifier
inRequest:mutableReqeust];
return mutableReqeust;
}
// 启动请求
- (void)startLoading {
self.startDate = [NSDate date];
self.data = [NSMutableData data];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
self.dataTask = [self.session dataTaskWithRequest:self.request];
[self.dataTask resume];
}
// 请求结束
- (void)stopLoading {
[self.dataTask cancel];
self.dataTask = nil;
}
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *_Nullable))completionHandler {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *customCredential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
}
// 里面的实现方式和第3步的是一样的。
}
至此,我们便完成对于WebView加载自签名的HTTPS的H5加载了。
以上第一种实现方式有bug,采用第二种webView代理的方式直接实现:
// 处理webView双向认证的信任逻辑
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *_Nullable credential))completionHandler {
// 只对API的域名进行双向认证处理,其他web页不处理
NSString *authHost = challenge.protectionSpace.host;
if (![authHost containsString:@"xxx.com"]) {
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *customCredential = nil;
if (completionHandler) {
completionHandler(disposition, customCredential);
}
return;
}
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengeUseCredential;
NSURLCredential *customCredential = nil;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 服务端认证
OSStatus err;
SecTrustRef trust = [[challenge protectionSpace] serverTrust];
// 设置锚点证书
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);
NSMutableArray *pinnedCertificates = [NSMutableArray array];
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverRootCerData)];
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)serverIntermediateCerData)];
err = SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)pinnedCertificates);
if (err == noErr) {
err = SecTrustSetAnchorCertificatesOnly(trust, false);
}
if (err != noErr) {
NSLog(@"\nset anchor certificates failed!, pinnedCertificates: %@ \n", pinnedCertificates);
}
CFErrorRef error = NULL;
if (@available(iOS 12.0, *)) {
__unused bool r = SecTrustEvaluateWithError(trust, &error);
if (error == noErr) {
customCredential = [NSURLCredential credentialForTrust:trust];
} else {
NSLog(@"\nverify server certificates failed!, pinnedCertificates: %@ \n", pinnedCertificates);
}
} else {
SecTrustResultType trustResult = kSecTrustResultInvalid;
err = SecTrustEvaluate(trust, &trustResult); //kSecTrustResultRecoverableTrustFailure
if (err == noErr) {
if ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)) {
customCredential = [NSURLCredential credentialForTrust:trust];
}
}
}
} else {
// 客户端发送证书认证
SecIdentityRef identity = NULL;
CFArrayRef certArray = NULL;
// 从PKCS12文件数据导出identity
if ([self extractIdentity:&identity fromPKCS12Data:pkcs12FileData certArray:&certArray]) {
// identity 系统使用的证书数据身份,客户端证书对应的数据
// certificates 建议传nil,除非服务端需要传递 intermediate certifate,一般服务端内置了中间证书
// NSURLCredentialPersistenceForSession 对于网络双向认证,只用填写这个值就可以
if (identity) {
customCredential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray *)certArray persistence:NSURLCredentialPersistenceForSession];
}
disposition = NSURLSessionAuthChallengeUseCredential;
}
}
if (completionHandler) {
completionHandler(disposition, customCredential);
}
}
调试
当需要对网络数据包进行分析,需要和IT人员一起查找并确定网关,或者服务端代理,配置等问题时,需要有好的工具才可以让我们事半功倍。 而在mac上,Apple官方也给出了建议的参考工具,如 Taking Advantage of Third-Party Network Debugging Tools 所说,包括其他很多IT人员也会用,选择 WireShark 这款应用。
Wireshark is a free and open source packet analyzer that supports macOS.
至于该如何使用,我们可以参照网络的使用说明,简单看一下怎么抓包使用:
打开wireShark应用,然后我们连接的网络是有线USB 还是无线Wi-Fi,选择后就可以进入抓包界面。
![16188029309741.jpg](https://img.haomeiwen.com/i831317/1391867bbc3c8876.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
一般情况下,我们需要过滤我们关心的端口,或者协议,支持筛选:
687E4D5A-E8EA-4734-8DE2-E08746A8F77E.png
抓包完成停止后,就可以 在File - > Export specified Packets ,当中导出当前的抓包数据, 好了,接下来把抓包数据给IT人员一起分析,查找问题吧!
总结
-
PKI和HTTPS双向认证, 第一步首先得将需要的服务端证书,以及客户端证书数据打包到App当中,至于证书的获取形式是预先打包,还是通过可靠的下载渠道进行下载,就跟业务的规划有关了;
-
要完成HTTPS双向认证,需要分别对于接口的请求,NSURLSession的代理进行证书数据的校验,
也需要对于WKWebView加载的H5做HOOK,然后转到代理去完成证书的认证; -
调试中利用好一些工具比如WireShark,从抓包数据可以直接看到SSL握手过程,以及证书数据的传输等,哪个环节出现了问题,更有效地排查解决掉问题。
-
还有个问题需要继续测试并观察:iOS - NSProtocol 拦截 WKWebView POST 请求 body 会被清空的问题解决
可以弃用拦截WKWebView的方式,在WKWebview的代理当中完成授权认证,可以避免这种问题出现
最后:文中的很多内容都是自己摸索和不断理解得到的,如有理解偏差的,欢迎指正交流!
参考
iOS 中对 HTTPS 证书链的验证
HTTPS双向认证研究
证书的信任链校验:certificate trust chain
iOS使用NSURLProtocol来Hook拦截WKWebview请求并回放的一种姿势
Overriding TLS Chain Validation Correctly
Creating Certificates for TLS Testing
HTTPS Server Trust Evaluation
Taking Advantage of Third-Party Network Debugging Tools
iOS 12、macOS 10.14、watchOS 5 和 tvOS 12 中可用的受信任根证书列表