一次iOS客户端PKI及HTTPS双向认证的踩坑记录

3,552 阅读20分钟

背景

需求背景:

  • 最近公司的产品涉及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 10242)创建根证书请求文件:
openssl req -new -out root.csr -key root.key3)创建根证书
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650

HTTPS

为什么要用HTTPS? 解决了什么问题?

我们知道http的缺点: 1. 数据明文 2. 不验证通信方的身份 3. 数据报文容易被篡改 4. 容易遭受MITM攻击

HTTPS 是运行在 TLS/SSL 之上的 HTTP,与普通的 HTTP 相比,在数据传输的安全性上有很大的提升。 为了提高安全性,我们常用的做法是使用对称加密的手段加密数据。可是只使用对称加密的话,双方通信的开始总会以明文的方式传输密钥。那么从一开始这个密钥就泄露了,谈不上什么安全。所以 TLS/SSL 在握手的阶段,结合非对称加密的手段,保证只有通信双方才知道对称加密的密钥。 

16188020047381.jpg

So, 为什么要用双向认证? 双向认证解决了什么问题?

双向认证,顾名思义,客户端和服务器端都需要验证对方的身份,在建立https连接的过程中,握手的流程比单向认证多了几步。单向认证的过程,客户端从服务器端下载服务器端公钥证书进行验证,然后建立安全通信通道。 双向通信流程,客户端除了需要从服务器端下载服务器的公钥证书进行验证外,还需要把客户端的公钥证书上传到服务器端给服务器端进行验证,等双方都认证通过了,才开始建立安全通信通道进行数据传输。 能保证通信的双方是指定的端,在很多P2P(端对端)的通信中,也有很多实际的应用。

双向认证

什么是单向认证?

16188021507084.jpg

  1. 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务器端;
  2. 服务器端将本机的公钥证书(server.crt)发送给客户端;
  3. 客户端读取公钥证书(server.crt),取出了服务端公钥;
  4. 客户端生成一个随机数(密钥R),用刚才得到的服务器公钥去加密这个随机数形成密文,发送给服务端;
  5. 服务端用自己的私钥(server.key)去解密这个密文,得到了密钥R
  6. 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。

什么是双向认证?

16188021983690.jpg

  1. 客户端发起建立HTTPS连接请求,将SSL协议版本的信息发送给服务端;
  2. 服务器端将本机的公钥证书(server.crt)发送给客户端;
  3. 客户端读取公钥证书(server.crt),取出了服务端公钥;
  4. 客户端将客户端公钥证书(client.crt)发送给服务器端;
  5. 服务器端解密客户端公钥证书,拿到客户端公钥;
  6. 客户端发送自己支持的加密方案给服务器端;
  7. 服务器端根据自己和客户端的能力,选择一个双方都能接受的加密方案,使用客户端的公钥加密后发送给客户端;
  8. 客户端使用自己的私钥解密加密方案,生成一个随机数R,使用服务器公钥加密后传给服务器端;
  9. 服务端用自己的私钥去解密这个密文,得到了密钥R
  10. 服务端和客户端在后续通讯过程中就使用这个密钥R进行通信了。

可以理解为,在SSL握手阶段,客户端和服务端通过非对称加密,以及证书链校验,协商并确保双方得到一个会话秘钥,连接成功后,采用这个会话秘钥对数据进行加密传输,就可以保障数据的安全性;

证书的校验是如何执行的?

首先,客户端收到服务端发来的证书链数据,类似下图: 16188022588233.jpg

服务端证书一般由中间证书签发,而中间证书由根证书进行签发,根证书由CA机构生成,上一级的证书是下一级证书的签发者,由此形成的证书树形结构,就是证书链。证书链校验过程如下:

1.取上级证书的公钥,对下级证书的签名进行解密得出下级证书的摘要digest1 ; 2.对下级证书进行信息摘要digest2; 3.判断digest1是否等于digest2,相等则说明下级证书校验通过; 4. 依次对各个相邻级别证书实施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认证处理。    实际处理如下:

  1. 第一部分,项目工程ATS的配置
  2. 第二部分,针对接口网络请求: NSURLSession,NSURLConnection 等的认证处理;
  3. 第三部分,针对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的双向认证逻辑

实践

编码

根据整个业务流程,我们把编码实现过程大致分为以下几步:

  1. App启动时候,通过PKI供应商的HTTPS通道,进行证书的申请和校验,并将证书保存在App中,并且完成ATS的设置;
  2. 从证书当中导出双向认证需要的自签名的根证书,以及该移动设备关联的客户端证书数据;
  3. 在App当中接口请求的时候,通过根证书,以及客户端证书数据完成双向认证;
  4. 在App当中完成加载H5页面时,拦截并重构请求,完成网页及网页请求接口的双向认证,通过代理实现双向认证;
  5. 优化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

  1. 首先需要通过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

一般情况下,我们需要过滤我们关心的端口,或者协议,支持筛选: 687E4D5A-E8EA-4734-8DE2-E08746A8F77E.png

抓包完成停止后,就可以 在File - > Export specified Packets ,当中导出当前的抓包数据,  好了,接下来把抓包数据给IT人员一起分析,查找问题吧!


总结

  1. PKI和HTTPS双向认证, 第一步首先得将需要的服务端证书,以及客户端证书数据打包到App当中,至于证书的获取形式是预先打包,还是通过可靠的下载渠道进行下载,就跟业务的规划有关了;

  2. 要完成HTTPS双向认证,需要分别对于接口的请求,NSURLSession的代理进行证书数据的校验,也需要对于WKWebView加载的H5做HOOK,然后转到代理去完成证书的认证

  3. 调试中利用好一些工具比如WireShark,从抓包数据可以直接看到SSL握手过程,以及证书数据的传输等,哪个环节出现了问题,更有效地排查解决掉问题。

  4. 还有个问题需要继续测试并观察:

    iOS - NSProtocol 拦截 WKWebView POST 请求 body 会被清空的问题解决

    【腾讯Bugly干货分享】WKWebView 那些坑

    可以弃用拦截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 中可用的受信任根证书列表