iOS Authentication Challenge

概述

在 iOS 中进行网络通讯时,为了安全,可能会产生认证质询(Authentication Challenge),例如: HTTP Basic AuthenticationHTTPS Server Trust Authentication 。本文介绍的是使用 URLSession 发送网络请求时,应该如何处理这些认证质询,最后会对 iOS 最著名的两个网络框架 -- AFNetworkingAlamofire 中处理认证质询部分的代码进行阅读、分析。本文中使用的开发语言是 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

肯定质询类型

URLAuthenticationChallenge

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 属性。数组

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 AuthenticationHTTPS Server Trust Authentication )。使用此值来肯定是否能够处理该质询和怎么处理质询。服务器

NSURLAuthenticationMethod 常量

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.AuthChallengeDispositionURLCredential? ,须要根据 challenge.protectionSpace.authenticationMethod 的值,肯定如何响应质询,而且提供对应的 URLCredential 实例。

URLSession.AuthChallengeDisposition

它是一个枚举类型,表示有如下几种方式来响应质询:

public enum AuthChallengeDisposition : Int {

    // 使用指定的凭据(credential)
    case useCredential 

    // 默认的质询处理,若是有提供凭据也会被忽略,若是没有实现 URLSessionDelegate 处理质询的方法则会使用这种方式
    case performDefaultHandling 
	
    // 取消认证质询,若是有提供凭据也会被忽略,会取消当前的 URLSessionTask 请求
    case cancelAuthenticationChallenge 

    // 拒绝质询,而且进行下一个认证质询,若是有提供凭据也会被忽略;大多数状况不会使用这种方式,没法为某个质询提供凭据,则一般应返回 performDefaultHandling
    case rejectProtectionSpace
}
复制代码

URLCredential

要成功响应质询,还须要提供对应的凭据。有三种初始化方式,分别用于不一样类型的质询类型。

// 使用给定的持久性设置、用户名和密码建立 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.Persistence

用于代表 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
}
复制代码

URLCredentialStorage

用于管理 URLCredential 的持久化。

基于用户名/密码的认证

HTTP BasicHTTP DigestNTLM 都是基于用户名/密码的认证,处理这种认证质询的方式以下:

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 Server Trust Authentication

当发送一个 HTTPS 请求时, URLSessionDelegate 将收到一个类型为 NSURLAuthenticationMethodServerTrust 的认证质询。其余类型的认证质询都是服务器对 App 进行认证,而这种类型则是 App 对服务器进行认证。

大多数状况下,对于这种类型的认证质询能够不实现 URLSessionDelegate 处理认证质询的方法, URLSessionTask 会使用默认的处理方式( performDefaultHandling )进行处理。可是若是是如下的状况,则须要手动进行处理:

  • 与使用自签名证书的服务器进行 HTTPS 链接。
  • 进行更严格的服务器信任评估来增强安全性,如:经过使用 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)
    }
}
复制代码

Trust Services

对服务器的信任认证中最核心的步骤是对服务器信任实例 serverTrust 进行评估,也就是以上代码中的 evaluate(trust, forHost: challenge.protectionSpace.host)

这须要涉及到苹果的 Security 框架,它是一个比较底层的框架,用于保护 App 管理的数据,并控制对 App 的访问,通常开发不多会接触到。这里面的类都不能跟普通的类同样直接进行操做,例如:没有 gettersetter 方法,而是使用相似 C 语言风格的函数进行操做,这些函数的名字都是以对应的类名开头,例如:对 SecTrust 实例进行评估的函数 SecTrustEvaluateWithError(_:_:)

对服务器信任实例 serverTrust 进行评估须要用到的是 Certificate, Key, and Trust Services 部分

SecTrust

class SecTrust 复制代码

用于评估信任的类,主要包含了:

  • 一个须要评估的证书,可能还有它所在的证书链中的中间证书和根证书(锚点证书)
  • 一个或者多个评估策略

注意:能够从 SecTrust 实例中获取证书和公钥,但前提是已经对它进行了评估而且评估经过。评估经过后,SecTrust 实例中会存在一条正确的证书链。

SecPolicy

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?
复制代码

SecCertificate

class SecCertificate 复制代码

X.509 标准证书类

SecIdentity

私钥和证书的组合

证书概念

还须要了解一些跟评估相关的证书概念

根证书

数字证书是由数字证书认证机构(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 包含了错误信息
    }
}
复制代码

SSL Pinning

使用某些抓包软件能够对网络请求进行抓包,就算是 HTTPS 的请求均可以抓包成功。可是同时也会发现某些 App 发送的网络请求会抓包失败。由于这些 App 内使用了一项叫 SSL Pinning 的技术。抓包软件对网络请求进行抓包主要是利用“中间人攻击”的技术,而 SSL Pinning 技术则是能够防止“中间人攻击”。

SSL Pinning 具体有两种作法:

  • Certificate Pinning:证书固定。将指定的证书集成在 App 里面,在进行服务器信任评估前,使用该证书做为锚点证书,再进行服务器信任评估,这样就能够限制了只有在指定的证书链上的证书才能经过评估,并且还能够限制只能是某些域名的证书。缺点是集成在 App 里面的证书会过时,若是证书过时,只能经过强制更新 App 才能保证正常进行网络访问。
  • Public Key Pinning:公钥固定。将指定的公钥集成在 App 里面,在进行服务器信任评估后,还会提取服务器返回的证书内的公钥,而后跟指定的公钥进行匹配。优势是公钥不像证书同样会过时。缺点是操做公钥会相对麻烦,并且违反了密钥轮换策略。

了解怎么手动进行服务器信任评估后,就能够轻松实现 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 最著名的两个网络框架 AFNetworkingAlamofire ,了解实际场景中的应用,分析代码实现的细节。其中因为 Alamofire 是使用更先进的开发语言 -- Swift 实现的,处理会更加详细和先进,因此是分析的重点。

Alamofire

Alamofire 中的认证质询处理会更加具体详细、完善,同时使用了新的 API ,如下是 Alamofire 5.0.0-rc.3 版本中的代码。

主要涉及 SessionDelegate.swiftServerTrustManager.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 协议的类,用于不一样的评估方式,方便开发者使用,分别是:

  • DefaultTrustEvaluator:默认的评估方式,使用SecPolicyCreateSSL(_:_:)策略进行评估,能够选择是否验证host
  • RevocationTrustEvaluator:在 DefaultTrustEvaluator 基础上,增长用检查证书撤销的策略进行评估
  • PinnedCertificatesTrustEvaluator:在 DefaultTrustEvaluator 基础上,增长 Certificate Pinning 检查
  • PublicKeysTrustEvaluator:在 DefaultTrustEvaluator 基础上,增长 Public Key Pinning 检查
  • CompositeTrustEvaluator:组合评估,使用多种处理方式进行评估
  • DisabledEvaluator:只用于开发调试的类,使用它进行评估永远不会经过

能够看出这几种处理方式都是在默认的基础上,增长一些额外的评估,如下只分析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

AFNetworking 中的认证质询处理相对于 Alamofire 会没有那么全面,代码也简单不少,可是足以处理大多数的状况。如下是 AFNetworking 3.2.1 版本中的代码。

主要涉及 AFURLSessionManager.mAFSecurityPolicy.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];
}
复制代码

对比

AFNetworkingAlamofire 对认证质询的处理跟本文前面部分所介绍的内容基本一致。从它们的源码来看,我我的以为虽然 AFNetworking 的代码比较简单,可是逻辑有点混乱,而且因为存在 allowInvalidCertificates 的判断,因此逻辑就更复杂了,而 Alamofire 的代码更加细致,处理的方式更全面,逻辑也很清晰。形成这样的结果多是由于 AFNetworking 过久没有更新,而 Alamofire 却一直在更新。因此若是有须要的话,我更推荐参考 Alamofire 的代码。

结语

认证质询(Authentication Challenge)是进行安全的网络通讯中重要的一环。在开发中,咱们通常会使用 AFNetworking 或者 Alamofire 搭建 App 的网络层,大多数状况下直接使用它们自带的功能对认证质询进行处理已经足够。但若是存在特殊状况,仍是须要咱们对这方面进行深刻的了解,进行自定义的处理。本文尽可能全面地对认证质询相关的知识进行介绍,但因为涉及到苹果的 Security 框架,相关 API 的使用说明比较分散,也存在比较多的细节,因此我没有对它们所有进行介绍,有须要的读者能够仔细阅读苹果官方文档,参考 Alamofire 的代码进行使用。

参考

Handling an Authentication Challenge

Certificate, Key, and Trust Services

HTTPS Server Trust Evaluation

相关文章
相关标签/搜索