在 iOS 中进行网络通讯时,为了安全,可能会产生认证质询(Authentication Challenge),例如: HTTP Basic Authentication
、 HTTPS Server Trust Authentication
。本文介绍的是使用 URLSession
发送网络请求时,应该如何处理这些认证质询,最后会对 iOS 最著名的两个网络框架 -- AFNetworking
和 Alamofire
中处理认证质询部分的代码进行阅读、分析。本文中使用的开发语言是 Swift。html
当发送一个 URLSessionTask
请求时,服务器可能会发出一个或者多个认证质询。URLSessionTask
会尝试处理,若是不能处理,则会调用 URLSessionDelegate
的方法来处理。git
URLSessionDelegate
处理认证质询的方法:github
// 这个方法用于处理 session 范围内的质询,如:HTTPS Server Trust Authentication。一旦成功地处理了此类质询,从该 URLSession 建立的全部任务保持有效。
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
}
// 这个方法用于处理任务特定的质询,例如:基于用户名/密码的质询。每一个任务均可能发出本身的质询。当须要处理 session 范围内的质询时,但上面那个方法又没有实现的话,也会调用此方法来代替
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
}
复制代码
若是须要 URLSessionDelegate
的方法来处理认证质询,但是又没有实现相应的方法,则服务器可能会拒绝该请求,而且返回一个值为 401(禁止)的 HTTP 状态代码。swift
URLSessionDelegate
处理认证质询的方法都会接受一个 challenge: URLAuthenticationChallenge
参数,它提供了在处理认证质询时所需的信息。api
class URLAuthenticationChallenge: NSObject {
// 须要认证的区域
var protectionSpace: URLProtectionSpace
// 表示最后一次认证失败的 URLResponse 实例
var failureResponse: URLResponse?
// 以前认证失败的次数
var previousFailureCount: Int
// 建议的凭据,有多是质询提供的默认凭据,也有多是上次认证失败时使用的凭据
var proposedCredential: URLCredential?
// 上次认证失败的 Error 实例
var error: Error?
// 质询的发送者
var sender: URLAuthenticationChallengeSender?
}
复制代码
其中,它的核心是 protectionSpace: URLProtectionSpace
属性。数组
须要认证的区域,定义了认证质询的一系列信息,这些信息肯定了应开发者应该如何响应质询,提供怎样的 URLCredential
,例如: host
、端口、质询的类型等。安全
class URLProtectionSpace : NSObject {
// 质询的类型
var authenticationMethod: String
// 进行客户端证书认证时,可接受的证书颁发机构
var distinguishedNames: [Data]?
var host: String
var port: Int
var `protocol`: String? var proxyType: String? var realm: String? var receivesCredentialSecurely: Bool // 表示服务器的SSL事务状态 var serverTrust: SecTrust? } 复制代码
其中,它的 authenticationMethod
属性代表了正在发出的质询的类型(例如: HTTP Basic Authentication
、 HTTPS Server Trust Authentication
)。使用此值来肯定是否能够处理该质询和怎么处理质询。服务器
authenticationMethod
属性的值为如下常量之一,这些就是认证质询的类型。网络
/* session 范围内的认证质询 */
// 客户端证书认证
let NSURLAuthenticationMethodClientCertificate: String
// 协商使用 Kerberos 仍是 NTLM 认证
let NSURLAuthenticationMethodNegotiate: String
// NTLM 认证
let NSURLAuthenticationMethodNTLM: String
// 服务器信任认证(证书验证)
let NSURLAuthenticationMethodServerTrust: String
/* 任务特定的认证质询 */
// 使用某种协议的默认认证方法
let NSURLAuthenticationMethodDefault: String
// HTML Form 认证,使用 URLSession 发送请求时不会发出此类型认证质询
let NSURLAuthenticationMethodHTMLForm: String
// HTTP Basic 认证
let NSURLAuthenticationMethodHTTPBasic: String
// HTTP Digest 认证
let NSURLAuthenticationMethodHTTPDigest: String
复制代码
URLSessionDelegate
处理认证质询的方法都接受一个 completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
闭包参数,最终须要调用此闭包来响应质询,不然 URLSessionTask
请求会一直处于等待状态。session
这个闭包接受两个参数,它们的类型分别为 URLSession.AuthChallengeDisposition
、 URLCredential?
,须要根据 challenge.protectionSpace.authenticationMethod
的值,肯定如何响应质询,而且提供对应的 URLCredential
实例。
它是一个枚举类型,表示有如下几种方式来响应质询:
public enum AuthChallengeDisposition : Int {
// 使用指定的凭据(credential)
case useCredential
// 默认的质询处理,若是有提供凭据也会被忽略,若是没有实现 URLSessionDelegate 处理质询的方法则会使用这种方式
case performDefaultHandling
// 取消认证质询,若是有提供凭据也会被忽略,会取消当前的 URLSessionTask 请求
case cancelAuthenticationChallenge
// 拒绝质询,而且进行下一个认证质询,若是有提供凭据也会被忽略;大多数状况不会使用这种方式,没法为某个质询提供凭据,则一般应返回 performDefaultHandling
case rejectProtectionSpace
}
复制代码
要成功响应质询,还须要提供对应的凭据。有三种初始化方式,分别用于不一样类型的质询类型。
// 使用给定的持久性设置、用户名和密码建立 URLCredential 实例。
public init(user: String, password: String, persistence: URLCredential.Persistence) {
}
// 用于客户端证书认证质询,当 challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate 时使用
// identity: 私钥和和证书的组合
// certArray: 大多数状况下传 nil
// persistence: 该参数会被忽略,传 .forSession 会比较合适
public init(identity: SecIdentity, certificates certArray: [Any]?, persistence: URLCredential.Persistence) {
}
// 用于服务器信任认证质询,当 challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust 时使用
// 从 challenge.protectionSpace.serverTrust 中获取 SecTrust 实例
// 使用该方法初始化 URLCredential 实例以前,须要对 SecTrust 实例进行评估
public init(trust: SecTrust) {
}
复制代码
用于代表 URLCredential
实例的持久化方式,只有基于用户名和密码建立的 URLCredential
实例才会被持久化到 keychain
里面
public enum Persistence : UInt {
case none
case forSession
// 会存储在 iOS 的 keychain 里面
case permanent
// 会存储在 iOS 的 keychain 里面,而且会经过 iCloud 同步到其余 iOS 设备
@available(iOS 6.0, *)
case synchronizable
}
复制代码
用于管理 URLCredential
的持久化。
HTTP Basic
、 HTTP Digest
、 NTLM
都是基于用户名/密码的认证,处理这种认证质询的方式以下:
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM:
let user = "user"
let password = "password"
let credential = URLCredential(user: user, password: password, persistence: .forSession)
completionHandler(.useCredential, credential)
default:
completionHandler(.performDefaultHandling, nil)
}
}
复制代码
当发送一个 HTTPS 请求时, URLSessionDelegate
将收到一个类型为 NSURLAuthenticationMethodServerTrust
的认证质询。其余类型的认证质询都是服务器对 App 进行认证,而这种类型则是 App 对服务器进行认证。
大多数状况下,对于这种类型的认证质询能够不实现 URLSessionDelegate
处理认证质询的方法, URLSessionTask
会使用默认的处理方式( performDefaultHandling
)进行处理。可是若是是如下的状况,则须要手动进行处理:
SSL Pinning
来防止中间人攻击。对服务器的信任认证作法大体以下:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// 判断认证质询的类型,判断是否存在服务器信任实例 serverTrust
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
// 不然使用默认处理
completionHandler(.performDefaultHandling, nil)
return
}
// 自定义方法,对服务器信任实例 serverTrust 进行评估
if evaluate(trust, forHost: challenge.protectionSpace.host) {
// 评估经过则建立 URLCredential 实例,告诉系统接受服务器的凭据
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// 不然取消此次认证,告诉系统拒绝服务器的凭据
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
复制代码
对服务器的信任认证中最核心的步骤是对服务器信任实例 serverTrust
进行评估,也就是以上代码中的 evaluate(trust, forHost: challenge.protectionSpace.host)
。
这须要涉及到苹果的 Security
框架,它是一个比较底层的框架,用于保护 App 管理的数据,并控制对 App 的访问,通常开发不多会接触到。这里面的类都不能跟普通的类同样直接进行操做,例如:没有 getter
、 setter
方法,而是使用相似 C 语言风格的函数进行操做,这些函数的名字都是以对应的类名开头,例如:对 SecTrust
实例进行评估的函数 SecTrustEvaluateWithError(_:_:)
。
对服务器信任实例 serverTrust
进行评估须要用到的是 Certificate, Key, and Trust Services 部分
class SecTrust 复制代码
用于评估信任的类,主要包含了:
注意:能够从 SecTrust
实例中获取证书和公钥,但前提是已经对它进行了评估而且评估经过。评估经过后,SecTrust
实例中会存在一条正确的证书链。
class SecPolicy 复制代码
评估策略,Security
框架提供了如下策略:
// 返回默认 X.509 策略的策略对象,只验证证书是否符合 X.509 标准
func SecPolicyCreateBasicX509() -> SecPolicy
// 返回用于评估 SSL 证书链的策略对象
// 第一个参数:server,若是传 true,则表明是在客户端上验证 SSL 服务器证书
// 第二个参数:hostname,若是传非 nil,则表明会验证 hostname
func SecPolicyCreateSSL(Bool, CFString?) -> SecPolicy
// 返回用于检查证书吊销的策略对象,一般不须要本身建立吊销策略,除非但愿重写默认系统行为,例如强制使用特定方法或彻底禁用吊销检查。
func SecPolicyCreateRevocation(CFOptionFlags) -> SecPolicy?
复制代码
class SecCertificate 复制代码
X.509 标准证书类
私钥和证书的组合
还须要了解一些跟评估相关的证书概念
数字证书是由数字证书认证机构(Certificate authority,即 CA)来负责签发和管理。首先,CA 组织结构中,最顶层的就是根 CA,根 CA 下能够受权给多个二级 CA,而二级 CA 又能够受权多个三级 CA,因此 CA 的组织结构是一个树结构。根 CA 颁发的自签名证书就是根证书,通常操做系统中都嵌入了一些默认受信任的根证书。
因为证书是从根 CA 开始不断向下级受权签发的,因此证书链就是由某个证书和它各个上级证书组成的链条。一条完整的证书链是由最底层的证书开始,最终以根 CA 证书结束。但在这里的证书链指的是从某个须要评估的证书开始,最终以锚点证书结束,例如:须要评估的证书 - 中间证书 - 中间证书 - 锚点证书。
锚点证书一般是操做系统中嵌入的固有受信任的根证书之一。但在这里指的是评估 SecTrust
实例时,用于评估的证书链里面最顶层的证书,它多是根证书,也多是证书链中的某一个。评估时会在 SecTrustSetAnchorCertificates(_:_:)
函数指定的证书数组中查找锚点证书,或者使用系统提供的默认集合。
从 challenge.protectionSpace.serverTrust
获得 SecTrust
实例,经过如下函数来评估它是否有效
// iOS 12 如下的系统使用这个函数
func SecTrustEvaluate(_ trust: SecTrust, _ result: UnsafeMutablePointer<SecTrustResultType>) -> OSStatus
// iOS 12 及以上的系统推荐使用这个函数
func SecTrustEvaluateWithError(_ trust: SecTrust, _ error: UnsafeMutablePointer<CFError?>?) -> Bool
复制代码
评估的步骤以下:
SecTrust
实例中存在一个须要评估的证书,评估函数会根据评估策略建立对应的证书链,而后从须要评估的证书开始,直到锚点证书,依次验证证书链中各个证书的签名,若是中途某个证书不经过验证,或者某个证书已经设置了非默认信任设置(信任或者不信任),则会提早结束,返回一个成功或者失败的结果。注意:
keychain
(或 iOS 中的应用程序 keychain
)中搜索中间证书,还有可能经过网络来下载中间证书。SecTrustSetAnchorCertificates(_:_:)
函数指定的证书数组中查找锚点证书,或者使用系统提供的默认集合。SecTrust
实例中,例如调用如下函数
SecTrustCreateWithCertificates(_:_:_:)
SecTrustSetPolicies(_:_:)
评估结果
if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
var error: CFError?
let evaluationSucceeded = SecTrustEvaluateWithError(serverTrust, &error)
if evaluationSucceeded {
// 评估经过
} else {
// 评估不经过,error 包含了错误信息
}
} else {
var result = SecTrustResultType.invalid
let status = SecTrustEvaluate(serverTrust, &result)
if status == errSecSuccess && (result == .unspecified || result == .proceed) {
// 评估经过
} else {
// 评估不经过,result 和 status 包含了错误信息
}
}
复制代码
使用某些抓包软件能够对网络请求进行抓包,就算是 HTTPS
的请求均可以抓包成功。可是同时也会发现某些 App 发送的网络请求会抓包失败。由于这些 App 内使用了一项叫 SSL Pinning
的技术。抓包软件对网络请求进行抓包主要是利用“中间人攻击”的技术,而 SSL Pinning
技术则是能够防止“中间人攻击”。
SSL Pinning
具体有两种作法:
了解怎么手动进行服务器信任评估后,就能够轻松实现 SSL Pinning
关于 SSL Pinning
的选择,苹果的文档上有提出
Create a Long-Term Server Authentication Strategy
If you determine that you need to evaluate server trust manually in some or all cases, plan for what your app will do if you need to change your server credentials. Keep the following guidelines in mind:
- Compare the server’s credentials against a public key, instead of storing a single certificate in your app bundle. This will allow you to reissue a certificate for the same key and update the server, rather than needing to update the app.
- Compare the issuing certificate authority’s (CA’s) keys, rather than using the leaf key. This way, you can deploy certificates containing new keys signed by the same CA.
- Use a set of keys or CAs, so you can rotate server credentials more gracefully.
简单来讲就是推荐使用 Public Key Pinning
,并且是把多个比较高级别的 CA 公钥集成在 App 里面,这样服务器就能够在部署的证书的时候有更多的选择,更加的灵活。
以上已经详细地介绍了 iOS Authentication Challenge 的处理,接下来结合 iOS 最著名的两个网络框架 AFNetworking
和 Alamofire
,了解实际场景中的应用,分析代码实现的细节。其中因为 Alamofire
是使用更先进的开发语言 -- Swift 实现的,处理会更加详细和先进,因此是分析的重点。
Alamofire
中的认证质询处理会更加具体详细、完善,同时使用了新的 API ,如下是 Alamofire
5.0.0-rc.3 版本中的代码。
主要涉及 SessionDelegate.swift
、ServerTrustManager.swift
两个文件
typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: AFError?)
// URLSessionDelegate 的方法
// 若是没有实现用于处理 session 范围内的认证质询的方法,会调用这个方法做为代替
// 因此为了不重复,实际上只须要实现这个方法就能够处理全部状况
open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
eventMonitor?.urlSession(session, task: task, didReceive: challenge)
let evaluation: ChallengeEvaluation
// 判断认证质询,主要分为两种状况
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodServerTrust:
// 服务器信任认证质询,也就是 HTTPS 证书认证
evaluation = attemptServerTrustAuthentication(with: challenge)
case NSURLAuthenticationMethodHTTPBasic, NSURLAuthenticationMethodHTTPDigest, NSURLAuthenticationMethodNTLM,
NSURLAuthenticationMethodNegotiate, NSURLAuthenticationMethodClientCertificate:
// 其余类型认证质询
evaluation = attemptCredentialAuthentication(for: challenge, belongingTo: task)
default:
evaluation = (.performDefaultHandling, nil, nil)
}
// 若是存在错误,则经过回调告诉外界
if let error = evaluation.error {
stateProvider?.request(for: task)?.didFailTask(task, earlyWithError: error)
}
// 响应质询
completionHandler(evaluation.disposition, evaluation.credential)
}
复制代码
// 处理服务器信任认证质询的方法
func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation {
let host = challenge.protectionSpace.host
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let trust = challenge.protectionSpace.serverTrust
else {
return (.performDefaultHandling, nil, nil)
}
do {
// evaluator 是一个用于评估 serverTrust 的实例,它遵循了 ServerTrustEvaluating 协议
guard let evaluator = try stateProvider?.serverTrustManager?.serverTrustEvaluator(forHost: host) else {
return (.performDefaultHandling, nil, nil)
}
// 最终是调用 evaluator 的 valuate(_:forHost:) 方法来进行评估
try evaluator.evaluate(trust, forHost: host)
// 若是没有抛出错误,则建立 URLCredential 实例,告诉系统接受服务器的凭据
return (.useCredential, URLCredential(trust: trust), nil)
} catch {
// 不然取消此次认证质询,同时返回一个 error
return (.cancelAuthenticationChallenge, nil, error.asAFError(or: .serverTrustEvaluationFailed(reason: .customEvaluationFailed(error: error))))
}
}
复制代码
Alamofire
内部提供了几个遵循了 ServerTrustEvaluating
协议的类,用于不一样的评估方式,方便开发者使用,分别是:
SecPolicyCreateSSL(_:_:)
策略进行评估,能够选择是否验证host
DefaultTrustEvaluator
基础上,增长用检查证书撤销的策略进行评估DefaultTrustEvaluator
基础上,增长 Certificate Pinning
检查DefaultTrustEvaluator
基础上,增长 Public Key Pinning
检查能够看出这几种处理方式都是在默认的基础上,增长一些额外的评估,如下只分析PinnedCertificatesTrustEvaluator
的作法,对其余类的作法有兴趣的读者能够自行阅读源码
// PinnedCertificatesTrustEvaluator 中的评估方法
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
guard !certificates.isEmpty else {
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
// 由于进行了 Certificate Pinning,因此首先须要设置锚点证书
if acceptSelfSignedCertificates {
try trust.af.setAnchorCertificates(certificates)
}
// 进行默认的评估
if performDefaultValidation {
try trust.af.performDefaultValidation(forHost: host)
}
// 验证 host
if validateHost {
try trust.af.performValidation(forHost: host)
}
// 若是代码能运行到这里,表明评估经过;在手动设置了锚点证书后再使用评估函数而且经过了评估,此时从 SecTrust 实例取出的是一条包含了须要的评估证书直到锚点证书的证书链,将它们转为 Data 集合
let serverCertificatesData = Set(trust.af.certificateData)
// 将集成在 App 里的证书转为 Data 集合
let pinnedCertificatesData = Set(certificates.af.data)
// 判断两个集合是否有交集,这是为了进行一步增强安全性
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
if !pinnedCertificatesInServerData {
// 不然抛出错误
throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host, trust: trust, pinnedCertificates: certificates, serverCertificates: trust.af.certificates))
}
}
复制代码
若是 Alamofire
内置的几种评估方式不能知足开发者,则能够自定义遵照 ServerTrustEvaluating
协议的类,自行处理。
// 处理其余类型认证质询的方法
func attemptCredentialAuthentication(for challenge: URLAuthenticationChallenge, belongingTo task: URLSessionTask) -> ChallengeEvaluation {
// 以前有过失败,则返回
guard challenge.previousFailureCount == 0 else {
return (.rejectProtectionSpace, nil, nil)
}
// 这里是直接取出外界事先准备好的 URLCredential 实例
guard let credential = stateProvider?.credential(for: task, in: challenge.protectionSpace) else {
// 若是没有,则使用系统默认的处理
return (.performDefaultHandling, nil, nil)
}
return (.useCredential, credential, nil)
}
复制代码
Alamofire
对其余类型的认证质询的处理比较简单,由于这些类型的处理不肯定性比较大,因此 Alamofire
直接把认证质询转移给外界的调用者进行处理
AFNetworking
中的认证质询处理相对于 Alamofire
会没有那么全面,代码也简单不少,可是足以处理大多数的状况。如下是 AFNetworking
3.2.1 版本中的代码。
主要涉及 AFURLSessionManager.m
、 AFSecurityPolicy.m
两个文件。
它里面实现了 URLSessionDelegate
两个用于处理认证质询的方法 ,两个方法里面的代码几乎是同样的,因此下面只选择其中一个来分析
// URLSessionDelegate 的方法
// 若是没有实现用于处理 session 范围内的认证质询的方法,会调用这个方法做为代替
// 因此为了不重复,实际上只须要实现这个方法就能够处理全部状况
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// taskDidReceiveAuthenticationChallenge 是一个外界传入的 block,这里的意思是若是外界有传入,则把认证质询转移给外界的调用者进行处理
if (self.taskDidReceiveAuthenticationChallenge) {
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
// 不然判断质询类型
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 对服务器信任认证质询进行处理,也就是 HTTPS 证书认证
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 经过
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
// 不然取消认证质询
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
// 若是是其余类型的认证质询,则使用系统默认的处理
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
// 响应质询
if (completionHandler) {
completionHandler(disposition, credential);
}
}
复制代码
经过 AFSecurityPolicy
类里面的如下方法处理服务器信任认证质询
// 处理服务器信任认证质询的方法
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
// 这里的意思是若是须要验证 DomainName,又容许自签名的证书,但是又没有使用 SSL Pinning 或者没有提供证书,这样就会矛盾,因此直接返回 NO
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
return NO;
}
NSMutableArray *policies = [NSMutableArray array];
// 根据是否须要验证 DomainName(host) 来设置评估策略
if (self.validatesDomainName) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
if (self.SSLPinningMode == AFSSLPinningModeNone) {
// 在没有使用 SSL Pinning 的状况下,若是 allowInvalidCertificates 为 YES,表示不对证书进行评估,能够直接经过,不然须要对证书进行评估
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
// 在使用 SSL Pinning 的状况下,若是证书评估不经过,并且不容许自签名证书,则直接返回 NO
return NO;
}
switch (self.SSLPinningMode) {
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 由于进行了 Certificate Pinning,因此首先须要设置锚点证书
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)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);
// 判断两个数组是否有交集,这是为了进行一步增强安全性
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
// 对 serverTrust 里面证书链的证书逐个进行评估,而且返回评估经过的证书对应的公钥数组
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
for (id trustChainPublicKey in publicKeys) {
for (id pinnedPublicKey in self.pinnedPublicKeys) {
// 判断两个数组是否有交集,这是为了进行一步增强安全性
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
default:
return NO;
}
return NO;
}
复制代码
Public Key Pinning
的评估方法
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
// 使用 X.509 策略
SecPolicyRef policy = SecPolicyCreateBasicX509();
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
for (CFIndex i = 0; i < certificateCount; i++) {
// 从 serverTrust 取出证书
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
SecCertificateRef someCertificates[] = {certificate};
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);
SecTrustRef trust;
// 建立新的 SecTrustRef 实例
__Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
SecTrustResultType result;
// 对新的 SecTrustRef 实例进行评估
__Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
// 若是评估经过,则取出公钥,加入到 trustChain 数组,最后返回
[trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];
_out:
if (trust) {
CFRelease(trust);
}
if (certificates) {
CFRelease(certificates);
}
continue;
}
CFRelease(policy);
return [NSArray arrayWithArray:trustChain];
}
复制代码
AFNetworking
和 Alamofire
对认证质询的处理跟本文前面部分所介绍的内容基本一致。从它们的源码来看,我我的以为虽然 AFNetworking
的代码比较简单,可是逻辑有点混乱,而且因为存在 allowInvalidCertificates
的判断,因此逻辑就更复杂了,而 Alamofire
的代码更加细致,处理的方式更全面,逻辑也很清晰。形成这样的结果多是由于 AFNetworking
过久没有更新,而 Alamofire
却一直在更新。因此若是有须要的话,我更推荐参考 Alamofire
的代码。
认证质询(Authentication Challenge)是进行安全的网络通讯中重要的一环。在开发中,咱们通常会使用 AFNetworking
或者 Alamofire
搭建 App 的网络层,大多数状况下直接使用它们自带的功能对认证质询进行处理已经足够。但若是存在特殊状况,仍是须要咱们对这方面进行深刻的了解,进行自定义的处理。本文尽可能全面地对认证质询相关的知识进行介绍,但因为涉及到苹果的 Security
框架,相关 API 的使用说明比较分散,也存在比较多的细节,因此我没有对它们所有进行介绍,有须要的读者能够仔细阅读苹果官方文档,参考 Alamofire
的代码进行使用。
Handling an Authentication Challenge