首页 > 程序开发 > 综合编程 > 其他综合 >

AFSecurityPolicy 之 evaluateServerTrust

2017-06-15

AFSecurityPolicy 之 evaluateServerTrust,iOS9 引入了新特性 App Transport Security (ATS),要求 App 内访问的网络必须使用 HTTPs 协议。

App Transport Security(ATS)

iOS9 引入了新特性 App Transport Security (ATS),要求 App 内访问的网络必须使用 HTTPs 协议。

在 WWDC 2016 开发者大会上,苹果宣布了一个最后期限:到 2017年1月1日 App Store 中的所有应用都必须启用 App Transport Security 安全功能。

ATS 要求后台服务器必须支持最新的 TLS_V1.2 协议和 ECDH 加密算法。鉴于 HTTP 全面改造升级 HTTPs 的业务铺设尚需时日,苹果延迟了 App 强制接入 ATS 的 deadline。目前允许开发者设置 NSAllowsArbitraryLoads=YES 来暂时关闭 ATS,从而继续使用 HTTP 连接。

在 Info.plist 中添加 App Transport Security Settings (或 NSAppTransportSecurity), 类型为 Dictionary。 在 App Transport Security Settings 下添加 Allow Arbitrary Loads (或 NSAllowsArbitraryLoads),类型为 Boolean,值设为 YES。

ATS 对 HTTP 协议屏蔽引起的问题
Transport security has blocked a cleartext HTTP
IOS-关于App Transport Security相关说明及适配
关于iOS9中的App Transport Security相关说明及适配
Xcode7.2与iOS9之坑 iOS 9之适配ATS

使用更安全的 HTTPs 替代 HTTP 是大势所趋,作为程序开发者必须了解 HTTPs 原理机制和 ATS 适配开发要点。
本篇续接 TLS Handshake Flow(extracts from RFCs) 和 TLS握手协商流程解析,介绍实际访问 HTTPs 服务器的 TLS(SSL) 握手阶段需要客户端处理的 NSURLAuthenticationChallenge。

NSURLAuthenticationChallenge.NSURLProtectionSpace

iOS 中的 NSURLAuthenticationChallenge 是认证挑战类,也就是要求客户端进行挑战。
而服务器要求的挑战方式则由 NSURLAuthenticationChallenge 的属性 NSURLProtectionSpace *protectionSpace 来描述。
那么 NSURLProtectionSpace(保护空间)里面有哪些信息呢?可以肯定的是包括挑战的方式(401授权、客户端证书、服务端要求信任等),服务器的 URL 地址,端口号,协议等等。

以下为@interface NSURLProtectionSpace 向外暴露的主要属性:

@interface NSURLProtectionSpace : NSObject 

@property (nullable, readonly, copy) NSString *realm;

@property (readonly) BOOL receivesCredentialSecurely;

@property (readonly) BOOL isProxy;

@property (readonly, copy) NSString *host;

@property (readonly) NSInteger port;

@property (nullable, readonly, copy) NSString *proxyType;

@property (nullable, readonly, copy) NSString *protocol;

@property (readonly, copy) NSString *authenticationMethod;

@end

对于 SSL(TLS HandShake),authenticationMethod 对应有2种类型:

NSURLAuthenticationMethodClientCertificate:SSL Client certificate,表示服务器要求客户端提供证书(收到 CertificateRequest)。 NSURLAuthenticationMethodServerTrust:SecTrustRef validation required,表示需要评估是否信任服务器证书。

对于 NSURLAuthenticationMethodServerTrust,NSURLProtectionSpace 提供了一个 SecTrustRef 属性。

@interface NSURLProtectionSpace(NSServerTrustValidationSpace)

/*!
    @method serverTrust
    @abstract Returns a SecTrustRef which represents the state of the servers SSL transaction state
    @result A SecTrustRef from Security.framework.  (Nil if the authenticationMethod is not NSURLAuthenticationMethodServerTrust)
 */
@property (nullable, readonly) SecTrustRef serverTrust NS_AVAILABLE(10_6, 3_0);

@end

NSURLCredential

客户端若要接受挑战,则需提供挑战的凭证(用户和密码,或者提供客户端证书,或者信任服务器证书,或者代理)。
iOS 提供了一个 NSURLCredential 的类来表示挑战凭证。

针对 HTTP 401 错误 - 未授权: (Unauthorized),需要通过用户账号密码建立凭证:

// Initialize a NSURLCredential with a user and password
+[NSURLCredential(NSInternetPassword) credentialWithUser:password:persistence:]

针对 TLS HandShake 中服务器下发的 Certificate,macOS/iOS 的 Security.framework 的 SecPolicy.h/SecTrust.h 中定义了证书校验信任评估的接口。

// Evaluates a trust reference synchronously.
OSStatus SecTrustEvaluate(SecTrustRef trust, SecTrustResultType * __nullable result);

调用 SecTrustEvaluate 对证书校验通过后,客户端需要基于对服务器的信任来建立凭证。

// Create a new NSURLCredential which specifies that a handshake has been trusted.
+[NSURLCredential(NSServerTrust) credentialForTrust:];

AFSecurityPolicy

当基于 NSURLConnection 或 NSURLSession 访问 HTTPs 网站时,NSURLConnection 或 NSURLSession 并没有验证证书是否合法,无法避免中间人攻击。
要做到真正的安全通信,需要客户端校验确认是否信任服务器证书(链)并提供凭证。否则会返回以下错误:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9843)

AFNetworking 的 AFSecurityPolicy 接口封装了证书验证的过程。

收到 NSURLAuthenticationChallenge

在收到 Certificate 后,相关回调 NSURLConnectionDelegate/NSURLSessionDelegate 中调用 AFSecurityPolicy 对 NSURLAuthenticationChallenge 进行处理。

AFNetworking 2.x 的 AFURLConnectionOperation/NSURLConnection
回调 NSURLConnectionDelegate
-[AFURLConnectionOperation connection:willSendRequestForAuthenticationChallenge:];

AFNetworking 3.x 的 AFURLSessionManager 的回调 NSURLSessionDelegate
-[AFURLSessionManager URLSession:didReceiveChallenge:completionHandler:]
NSURLSessionTaskDelegate
-[AFURLSessionManager URLSession:task:didReceiveChallenge:completionHandler:]

接受 NSURLAuthenticationChallenge

在 willSendRequestForAuthenticationChallenge 或 didReceiveChallenge 中均需要处理如何接受挑战(NSURLAuthenticationChallenge)。

    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        // 校验评估是否信任证书(链)
        if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
            // 创建 NSURLCredential 对象
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            // NSURLConnectionDelegate/willSendRequestForAuthenticationChallenge:为 challenge 的发送方提供 credential
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
            // NSURLSessionDelegate/didReceiveChallenge:回调 completionHandler
            // completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        } else {
            // NSURLConnectionDelegate/willSendRequestForAuthenticationChallenge
            [[challenge sender] cancelAuthenticationChallenge:challenge];
            // NSURLSessionDelegate/didReceiveChallenge:回调 completionHandler
            // completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }
    }

evaluateServerTrust forDomain

-[AFSecurityPolicy evaluateServerTrust:forDomain:] 主要调用了 SecPolicyCreateSSL、SecTrustSetPolicies 和 SecTrustEvaluate 这3个函数。

-[AFSecurityPolicy evaluateServerTrust:forDomain:]
{
    // 1. 为服务器 domain 创建 SecPolicyRef
    [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];

    // 2. 为 SecTrustRef 设置 SecPolicyRef[]
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    // 3.调用 SecTrustEvaluate 校验证书(链)——SecTrustRef
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    }

}

challenge.protectionSpace.serverTrust 为证书,challenge.protectionSpace.host(或调用 [[connection currentRequest] valueForHTTPHeaderField:@”host”] 获取)为服务器域名。
以上展示的是 AFSSLPinningModeNone 模式完全信任服务器(domain)证书的基本处理流程。

SSL Pinning

除了去系统信任 CA 机构列表验证的 AFSSLPinningModeNone 模式,AFNetworking 还提供了 SSL Pinning 方式的验证。

SSL Pinning 方式(AFSSLPinningModeCertificate,AFSSLPinningModePublicKey)把服务端下发的证书预先打包到 APP 的 bundle 中,然后通过比较服务端下发的证书和本地证书是否相同来校验证书。
使用该方式的原因是CA机构颁发的证书比较昂贵,一些企业或者个人不申请CA颁发的证书,而是自己手动创建证书。用 SSL Pinning 的方式只要比较证书内容一样,无需验证证书的权威性。

AFSSLPinningModeNone

这个模式表示不做 SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书(链)。
若证书是信任机构签发的就会通过;若是自己服务器生成的证书,这里是不会通过的。

AFSSLPinningModeCertificate

这个模式表示用证书绑定方式验证证书,需要客户端保存服务端的证书拷贝。
适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地。但CS架构的APP应用一般事先知道要进行通信的服务端(例如 QQ 文件后台服务器:*.ftn.qq.com),可以直接在客户端保存这些固定服务端的证书用于校验。

验证分两步:第一步验证证书的域名/有效期等信息;第二步是对比服务端返回的证书跟客户端返回的是否一致。
从代码上看,和去系统信任机构列表里验证一样调用 SecTrustEvaluate,只是这里的列表换成了客户端预先保存的证书列表(链)。

AFSSLPinningModePublicKey

这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。
只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。

AFSSLPinningModeCertificate

AFCertificateTrustChainForServerTrust 中调用 SecTrustGetCertificateCount(serverTrust) 取得 SecTrustRef 证书链的长度;通过 SecTrustGetCertificateAtIndex 获取到证书链(trustChain)上的每一个证书(SecCertificateRef)。
然后对 trustChain(serverCertificates) 进行验证,具体参考源码注释。

AFSSLPinningModePublicKey

AFPublicKeyTrustChainForServerTrust 中获取证书链(trustChain)上的每一个证书(SecCertificateRef)后, 调用 SecTrustCreateWithCertificates 获取每个证书(SecCertificateRef)对应的 SecTrustRef,进而调用 SecTrustEvaluate 校验。
校验通过后,调用 SecTrustCopyPublicKey 获取每个证书 SecTrustRef 对应的公钥(SecKeyRef)。
然后对 trustChain(publicKeys)进行校验,具体参考源码注释。

源代码

以下摘自最新 AFNetworking 3.x 源码,稍作注释:

-[AFSecurityPolicy evaluateServerTrust:forDomain:]
{

    // 4.从 SecTrustRef 获取 trustChain,进行 SSL Pinning 验证。

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone: // 预期上面已经处理,不应走到这里
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }

            // The SecTrustEvaluate function looks for an anchor certificate in the array of certificates specified by the SecTrustSetAnchorCertificates function
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
            // 指定在本地证书列表(pinnedCertificates)中验证服务端返回证书的有效性(从当前证书一直逐级追溯到根证书都验证通过)
            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }

            // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            // 反向遍历服务器下发的证书链(serverCertificates),一般如果本地预存证书包含服务器返回的证书(或证书链的根证书),则校验通过。
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }

            return NO;
        }
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            // 获取服务器下发的证书链(serverCertificates)对应的公钥
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            // 遍历对比服务器下发证书公钥和本地预存证书公钥(pinnedPublicKeys)
            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }

            // AFPublicKeyTrustChainForServerTrust 并没有严格要求证书链都通过 SecTrustEvaluate,这里匹配上一个即算验证通过。
            return trustedPublicKeyCount > 0;
        }
    }

}
相关文章
最新文章
热点推荐