iOS应该了解的HTTPS(三)—— 如何使用HTTPS证书验证

Author Avatar
xiaoLit Created: Feb 12, 2019 Updated: May 14, 2020

一、NSURLSession 如何使用TLS证书验证

NSURLSession的代理方法

URLSession:didReceiveChallenge:completionHandler:

completionHandler这个block需要相关认证信息(NSURLSessionAuthChallengeDispositionNSURLCredential)才能完成连接。

这个方法能拿到一个NSURLAuthenticationChallenge类型的challenge,这个challenge里面包含着绝大部分网络请求的参数与信息,非常强大,可以详见备注。然后我们就可以根据challenge在这个代理方法中提供一些自定义的信息。

1. 默认的实现

如果我们不自定义内容称为NSURLSession的代理,不做任何处理的话那么系统为我们做了些什么呢?

系统的默认实现是验证challenge中返回的信任链,结果是有效的话则根据 serverTrust 创建 credential 用于同服务端确立 SSL 连接。否则会得到 The certificate for this server is invalid... 这样的错误而无法访问。

比如在访问 https://www.baidu.com 的时候咧,我们不实现这个方法也能访问成功的。系统对百度服务器返回来的证书链,从叶节点证书往根证书层层验证(有效期、签名等等),遇到根证书时,发现作为可信锚点的它存在与可信证书列表中,那么验证就通过,允许与服务端建立连接。

总结来说:就是《iOS应该了解的HTTPS》开篇提到的,如果你只是想iOS端普通的使用HTTPS的接口那就不必做任何操作,iOS默认都帮我们做好了。

2. 深入简出介入“验证数字证书有效性”步骤

NSURLSession的代理方法

URLSession:didReceiveChallenge:completionHandler:

回调中会收到一个NSURLAuthenticationChallenge类型的challenge。它其中的一个属性NSURLProtectionSpace这是权限认证的核心,它通常被称为保护空间,表示需要认证的服务器或者域,它定义了一系列的约束去告诉我们需要向服务器提供什么样的认证,通过判断authenticationMethod指定授权方式。

如果这个值是 NSURLAuthenticationMethodServerTrust 的时候,这个网络连接才会返回证书链做HTTPS的握手校验流程,也就是我们可以从protectionSpace中拿到serverTrust,我们就可以插手 TLS 握手中“验证数字证书有效性”这个一步骤了。

当取得保护空间要求我们认证的方式为NSURLAuthenticationMethodServerTrust时才会有serverTrust:

if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {  

    //拿到系统的 SecTrustRef 它包含验证策略(SecPolicyRef)以及一系列受信任的锚点证书
    SecTrustRef trust = challenge.protectionSpace.serverTrust;   
    ...
} 

拿到了trust证书链表后,我们需要把我们自己的证书放进去。也就是设置锚点证书。就是HTTPS的证书X.509HTTPS中的验证中提到的那个可自定义的锚点证书。

(1) 首先加载证书

//证书路径
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"cerName" ofType:@".cer"];
NSData * cerData = [NSData dataWithContentsOfFile:cerPath];

SecCertificateRef certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)(cerData));

//此处数组可以放多张证书
NSArray *trustedCertificates = @[CFBridgingRelease(certificate)];

(2) 设置锚点证书

SecTrustSetAnchorCertificates(trust, (__bridge CFArrayRef)self.trustedCertificates);

对于HTTPS的证书X.509HTTPS中的验证中提到的那个锚点证书通常来说指的是系统根证书,但是可以自定义说的就是这里。

注:SecTrustSetAnchorCertificates(serverTrust对象, 本地证书数组)将本地证书数组设置成需要参与验证的锚点证书。最后还是通过SecTrustEvaluate()方法进行校验,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书.

只调用 SecTrustSetAnchorCertificates () 这个函数的话,那么就只有作为参数被传入的证书作为锚点证书,连系统本身信任的 CA 证书不能作为锚点验证证书链。要想恢复系统中 CA 证书作为锚点的功能,还要再调用下面这个函数:

//true 代表仅被传入的证书作为锚点,false 允许系统 CA 证书也作为锚点。
SecTrustSetAnchorCertificatesOnly(trust, true);

注:这里设置为true,就可以防止答案就在于用户让系统信任了不应该信任的证书。用户设置系统信任的证书,会作为锚点证书(Anchor Certificate)来验证其他证书,当返回的服务器证书是锚点证书或者是基于该证书签发的证书(可以是多个层级)都会被信任。

这就是基于信任链校验方式的最大弱点。我们不能完全相信系统的校验。这也是TLS证书验证的重要理由之一。

(3) 验证证书

证书校验函数,在函数的内部递归地从叶节点证书到根证书验证。需要验证证书本身的合法性(验证签名完整性,验证证书有效期等);验证证书颁发者的合法性(查找颁发者的证书并检查其合法性,这个过程是递归的)。而递归的终止条件是证书验证过程中遇到了锚点证书。

OSStatus status = SecTrustEvaluate(trust, &result);

其中当result等于kSecTrustResultProceed表示serverTrust验证成功,且该验证得到了用户认可(例如在弹出的是否信任的alert框中选择always trust)。

kSecTrustResultUnspecified表示serverTrust验证成功,此证书也被暗中信任了,但是用户并没有显示地决定信任该证书。 两者取其一就可以认为对serverTrust验证成功。

判断验证结果

if (status == errSecSuccess && (result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)) {
    credential = [NSURLCredential credentialForTrust:trust];
    if (credential) {
        disposition = NSURLSessionAuthChallengeUseCredential;
    }
} else {
    /* 无效的话,取消 */
    disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}

if (completionHandler) {
    completionHandler(disposition, credential);
}

对于代理方法中的completionHandler所需要的参数:

(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler

NSURLSessionAuthChallengeDisposition:

NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理credential会被忽略
NSURLSessionAuthChallengeUseCredential:使用指定的证书
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消

NSURLCredential:
//直接创建
credential = [NSURLCredential credentialForTrust:trust];

一多半的功告成,因为AFN也只不过是多分装和处理了一下,调用API不同不过底层API都是相同的即上面我们搞的那些,看着很多其实理清思路了并不多,大家可以看一下HTTPSTool,精简了分析注释和代码。

二、AFN 如何使用TLS证书验证

原理我们已经理清楚了,直接上代码说一下不同点就可以了。

1. 首先加载证书,略有区别

//证书路径
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"cerName" ofType:@".cer"];

//创建证书set
NSString *cerPath = [HTTPSTool cerPath];
NSData *dataSou = [NSData dataWithContentsOfFile:cerPath];
NSSet *set = [NSSet setWithObjects:dataSou, nil];

2. 初始化证书策略

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
AFSSLPinningMode:验证策略

AFSSLPinningModeNone : 不必将证书跟你的 APP 一起打包,完全信任服务器的证书,也就是默认的策略
AFSSLPinningModeCertificate : 对比服务器证书跟你的证书是否完全匹配。
AFSSLPinningModePublicKey : 只对比服务器证书的 public key 跟你的证书的 public key 是否匹配。

AFSSLPinningModeCertificate 比较安全,也就是我们上面NSURLSession使用的验证方法。

因為我们的证书是跟 APP 一起打包的,这也就代表说如果我们的证书过期了或是变动了,我们就得出一版新的 APP 而且旧版 APP 的证书就失效了。我们也可以在每次 APP 启动时,就自动连到某个服务器下载最新的证书,不过此时這个下载就会是有风险的。

AFSSLPinningModePublicKey 则是只有比对证书里的 public key,所以即使服务器证书有所变动,只要 public key 不变,就能通过验证。

所以如果你能确保每个使用者总是使用最新版本的 APP(例如是公司企业內部专用的),那就可以考虑 AFSSLPinningModeCertificate,否则的话选择 AFSSLPinningModePublicKey 是更加良好的解决方案,末尾我会贴一下AFN是如何实现的。

//是否允许无效证书(也就是自建的证书)默认为NO  如果是需要验证自建证书 需要设置为YES
securityPolicy.allowInvalidCertificates = NO;
是否需要验证域名
securityPolicy.validatesDomainName = YES;

验证域名,默认为YES。如置为NO,建议自己添加对应域名的校验逻辑。

假如证书的域名与你请求的域名不一致,需把该项设置为NO。即服务器使用其他可信任机构颁发的证书,也可以建立连接。

主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的。当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。

3. 设置验证策略

拿到网络请求的AFHTTPSessionManager类型的manager

//策略加入证书
[securityPolicy setPinnedCertificates:set];

//网络请求加入策略
[manager setSecurityPolicy:securityPolicy];

就此完工。

三、总结

再次说明,如果你只是想最简单使用后台提供的HTTPS接口(CA机构购买的证书),不需要做任何处理,因为iOS系统会帮我们自动验证HTTPS(包括域名验证等),不管是用的原生NSURLSession构建的网络库,还是使用AFN都不需要我们额外费心思。

如果你是有安全方面的要求或者是自制证书才需要加入TLS证书验证。

几个需要注意的地方

1. 域名验证

CFArrayRef policiesRef;
SecTrustCopyPolicies(trust, &policiesRef);

NSLog(@"%@",policiesRef);
打印 policiesRef 后,你会发现默认的验证策略就包含了域名验证,即“服务器证书上的域名和请求域名是否匹配”。如果你的一个证书需要用来连接不同域名的主机,或者你直接用 IP 地址去连接,那么你可以重设验证策略以忽略域名验证:

NSMutableArray *policies = [NSMutableArray array];

BasicX509 不验证域名是否相同
SecPolicyRef policy = SecPolicyCreateBasicX509();
[policies addObject:(__bridge_transfer id)policy];

修改trust中的验证策略
SecTrustSetPolicies(trust, (__bridge CFArrayRef)policies);

2. 证书管理方案

选择证书链的哪一节点作为锚点证书打包到App中?

很多开发者会直接选择叶子证书。其实对于自建证书来说,选择哪一节点都是可行的。而对于由CA颁发的证书,则建议导入颁发该证书的CA机构证书或者是更上一级CA机构的证书,甚至可以是根证书。这是因为:

(1) 一般叶子证书的有效期都比较短,Google和Baidu官网证书的有效期也就几个月;而App由于是客户端,需要一定的向后兼容,稍疏于检查,今天发布,过两天证书就过期了。

(2) 越往证书链的末端,证书越有可能变动;比如叶子证书由特定域名(aaa.bbb.com)改为通配域名(*.bbb.com)等等。短期内的变动,重新部署后,有可能旧版本App更新不及时而出现无法访问的问题。
因此使用CA机构证书是比较合适的,至于哪一级CA机构证书,并没有完全的定论,你可以自己评估选择。

这里如果你打包的是比较上级的CA根证书,是解决了过期问题,那么你觉得这个根证书可能只给你一家公司签名吗?如果被有心之人签下来另一个证书,岂不是成了合法的中间人。那么这个TLS证书验证的意义又何在呢?

对于这个问题很多大神也给出过合理的思路:
(1) 下载更新证书,会涉及到很多处理细节,比如长时间没上线证书处于更替间隙等。

(2) 在即将证书更替时,将新的证书一同打包发版。

(3) AFNAFSSLPinningModePublicKey只是公钥对比并不是整个证书信息对比的思路也不错。续签证书时候用同样的CSR就可以保证公私钥不变。

(4) AFNAFSSLPinningModePublicKey公钥验证策略是如何实现的?
AFN源码


在代理回调中我们看到关键验证方法中关于公钥验证的处理

获取服务器所返回证书链的公钥

我们设置的锚点证书公钥

备注

NSURLAuthenticationChallenge

- (NSURLProtectionSpace *)protectionSpace; // 这个函数返回一个类NSURLProtectionSpace,类中描述服务器中希望的认证方式以及协议,主机端口号等信息。
- (NSURLCredential *)proposedCredential; // 建议使用的证书
- (NSInteger)previousFailureCount; // 用户密码输入失败的次数。
- (NSURLResponse *)failureResponse; // 授权失败的响应头的详细信息
- (NSError *)error; // 最后一次授权失败的错误信息
NSURLProtectionSpace

- (NSString *)realm; // 用于定义保护的区域,在服务端可以通过 realm 将不同的资源分成不同的域,域的名称即为 realm 的值,每个域可能会有自己的权限鉴别方案。
- (BOOL)receivesCredentialSecurely; // 这个空间内的证书是否能够安全的发送
- (BOOL)isProxy; // 代理授权
- (NSString *)host; // 服务端主机地址,如果是代理则代理服务器地址
- (NSInteger)port; // 服务端端口地址,如果是代理则代理服务器的端口
- (NSString *)proxyType; // 代理类型,只对代理授权,比如http代理,socket代理等。
- (NSString *)protocol; // 使用的协议,比如http,https, ftp等,
- (NSString *)authenticationMethod; // 指定授权方式,比如401,客户端认证,服务端信任,代理等。
- (NSArray *)distinguishedNames; // 可接受的颁发机关客户端证书身份验证
- (SecTrustRef)serverTrust; // 用于服务端信任,指定一个信任对象,可以用这个对象来建立一个凭证。
NSURLProtectionSpace

NSURLProtectionSpaceHTTP // http协议
NSURLProtectionSpaceHTTPS // https协议
NSURLProtectionSpaceFTP // ftp协议
NSURLProtectionSpaceHTTPProxy // http代理
NSURLProtectionSpaceHTTPSProxy // https代理
NSURLProtectionSpaceFTPProxy // ftp代理
NSURLProtectionSpaceSOCKSProxy // socks代理
NSURLAuthenticationMethodDefault // 协议的默认身份认证
NSURLAuthenticationMethodHTTPBasic // http的basic认证,等同于NSURLAuthenticationMethodDefault
NSURLAuthenticationMethodHTTPDigest // http的摘要认证
NSURLAuthenticationMethodHTMLForm // html的表单认证
适用于任何协议
NSURLAuthenticationMethodNTLM // NTLM认证
NSURLAuthenticationMethodNegotiate // Negotiate认证
NSURLAuthenticationMethodClientCertificate // ssl证书认证,适用于任何协议
NSURLAuthenticationMethodServerTrust // ServerTrust认证,适用于任何协议

Reference

Overriding TLS Chain Validation Correctly

iOS 中对 HTTPS 证书链的验证

如何正確設定 AFNetworking 的安全連線

iOS中HTTP/HTTPS授权访问一

iOS中HTTP/HTTPS授权访问二