iOS URLSession Authentication Challenge及SSL Pinning

APP即使开启HTTPS请求,也无法阻止中间人攻击。更安全的做法是,启用SSL Pinning。本文主要介绍SSL Pinning、Authentication Challenge相关的内容。

中间人攻击 & SSL Pinning

使用Charles、Stream等工具能抓包HTTPS请求,是「中间人攻击」的典型应用。抓包工具作为中间人,截获客户端发送给服务端的请求,伪装成客户端与服务端通信;同时将服务端返回的内容转发给客户端。基于这个原理,需要客户端信任中间人(抓包工具)的证书,否则抓包工具显示的请求内容也是加密的。

SSL Pinning(证书绑定)技术主要用来防止中间人攻击,原理就是在客户端内置证书或公钥,对服务端返回的证书有效期、所属域名、公钥、证书内容等信息进行校验,以验证服务端是否合法,校验不通过则阻断请求。开启了SSL Pinning之后,客户端不再接收操作系统内置的证书,使用代理抓包时会造成请求失败,保证了客户端和服务端通信的安全。但SSL Pinning也有缺点:由于签发的证书都有有效期,当证书过期时,客户端只能进行升级。

在NSURLSession中开启SSL Pinning

在NSURLSession中可以很便捷的使用SSL Pinning。示例:

let configuration = URLSessionConfiguration.default
self.session = URLSession.init(configuration: configuration, delegate: self, delegateQueue: nil)

let urlRequest = URLRequest.init(url: URL.init(string: "https://easeapi.com")!)
self.task = self.session.dataTask(with: urlRequest) { (data, response, error) in
    if error == nil {
    }
}
//self.task = self.session.dataTask(with: urlRequest)
self.task?.resume()

配置URLSession的delegate,并实现以下两个「Authentication Challenge」(身份验证挑战)方法:

//URLSessionDelegate:session级别身份验证挑战
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//处理session类型的挑战。一次成功处理后,该会话所有请求都有效
}

//URLSessionTaskDelegate:task级别身份验证挑战
func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//处理非session类型的挑战。且当session挑战方法没有实现是,也会调用此方法。
}

Authentication Challenge

不仅仅是在HTTPS请求时,需要授权访问的通信,服务端一般都会发起身份认证挑战,术语称为「Authentication Challenge」。比如请求FTP服务器时会被要求输入用户名密码,常见的还有HTTP Basic Authentication、HTTPS Server Trust Authentication等。URLSession支持以下认证类型:

NSURLAuthenticationMethodDefault
NSURLAuthenticationMethodHTTPBasic
NSURLAuthenticationMethodHTTPDigest
NSURLAuthenticationMethodHTMLForm
NSURLAuthenticationMethodNTLM
NSURLAuthenticationMethodNegotiate
NSURLAuthenticationMethodClientCertificate
NSURLAuthenticationMethodServerTrust

除了NSURLAuthenticationMethodServerTrust类型的认证挑战是客户端对服务器进行认证,其它类型的都是服务端对客户端进行认证。在HTTPS请求时,客户端会收到NSURLAuthenticationMethodServerTrust类型的身份认证挑战。

上述session和task级别挑战方法有什么区别?

就是字面的意思。session挑战仅作用于支持会话的验证中,也就是在一个会话中,验证通过之后,后续不再要求重新响应挑战。根据文档说明,session级别挑战仅支持NSURLAuthenticationMethodNTLM、NSURLAuthenticationMethodNegotiate、NSURLAuthenticationMethodClientCertificate、NSURLAuthenticationMethodServerTrust四种,其他认证类型全部走task级别身份验证方法。

且,当session级别验证方法没有实现时,也会走task级别身份验证方法。

另,笔者实践时发现,需要显式声明支持URLSessionTaskDelegate协议,否则即使没有实现session级别身份验证挑战方法,task级别身份验证挑战也不会执行。

处理认证挑战方法

URLAuthenticationChallenge有几个重要的属性:

open class URLAuthenticationChallenge : NSObject, NSSecureCoding {
    @NSCopying open var protectionSpace: URLProtectionSpace { get }
    @NSCopying open var proposedCredential: URLCredential? { get }
    open var previousFailureCount: Int { get }
    @NSCopying open var failureResponse: URLResponse? { get }
    open var sender: URLAuthenticationChallengeSender? { get }
}

protectionSpace代表了一个需要认证的服务器区域。重要的属性如下:

open class URLProtectionSpace : NSObject, NSSecureCoding, NSCopying {
    open var realm: String? { get }
    open var receivesCredentialSecurely: Bool { get }
    open var host: String { get }
    open var port: Int { get }
    open var proxyType: String? { get }
    open var `protocol`: String? { get }
    //认证类型
    open var authenticationMethod: String { get }
    //可接受的证书颁发机构的数组
    open var distinguishedNames: [Data]? { get }
    //authenticationMethod == NSURLAuthenticationMethodServerTrust时,表示服务端的SSL事务状态。
    open var serverTrust: SecTrust? { get }
}

URLProtectionSpace包含服务器HOST、端口、协议等信息,authenticationMethod就是认证类型,值是上述提到的八种认证类型之一。客户端需要根据不同的认证类型来处理认证。

响应挑战

执行completionHandler回调方法来响应挑战。包含两个参数:URLSession.AuthChallengeDisposition和URLCredential?。

URLSession.AuthChallengeDisposition

处理挑战的方式。

public enum AuthChallengeDisposition : Int {
    case useCredential = 0 /* Use the specified credential, which may be nil */
    case performDefaultHandling = 1 /* Default handling for the challenge - as if this delegate were not implemented; the credential parameter is ignored. */
    case cancelAuthenticationChallenge = 2 /* The entire request will be canceled; the credential parameter is ignored. */
    case rejectProtectionSpace = 3 /* This challenge is rejected and the next authentication protection space should be tried; the credential parameter is ignored. */
}
  • useCredential

使用指定的凭据。

  • performDefaultHandling

默认处理,和没有实现delegate方法效果一样。URLCredential参数会被忽略。

  • cancelAuthenticationChallenge

请求将被取消,URLCredential参数会被忽略。

  • rejectProtectionSpace

拒绝本次且继续下一次认证,URLCredential参数会被忽略。这个配置仅适用于非常特殊的情况,比如一台windows服务器可以同时使用NSURLAuthenticationMethodNegotiate和NSURLAuthenticationMethodNTLM认证,但如果客户端只能处理NSURLAuthenticationMethodNTLM认证,则客户端可以先拒绝NSURLAuthenticationMethodNegotiate认证,等待接下来的NSURLAuthenticationMethodNTLM认证。Apple建议,在大多数情况下不会用到这个方式:如果不能提供凭据,需要回退到performDefaultHandling。

URLCredential

URLCredential代表认证凭据对象,有下面三个初始化方法:

//用于处理NSURLAuthenticationMethodHTTPBasic/HTTPDigest/NTLM等基于用户名密码的认证
public init(user: String, password: String, persistence: URLCredential.Persistence)

//处理NSURLAuthenticationMethodClientCertificate类型
public init(identity: SecIdentity, certificates certArray: [Any]?, persistence: URLCredential.Persistence)

//处理NSURLAuthenticationMethodServerTrust类型。在HTTPS请求时,会收到此类认证。
public init(trust: SecTrust)

URLCredential.Persistence

定义凭据是否需要持久化存储。

public enum Persistence : UInt {
        case none = 0//不存储
        case forSession = 1//仅在当前session有效
        case permanent = 2//永久存储在keychain中
        @available(iOS 6.0, *)
        case synchronizable = 3//永久存储在keychain中,且会通过AppleID同步到其它设备。
}

SSL Pinning实践

对于NSURLAuthenticationMethodServerTrust类型的认证请求,需要对服务端返回的serverTrust进行校验。常见的SSL Pinning的有两种方式:

  • 公钥锁定

提取证书中的公钥并内置到客户端中,通过与服务器返回的公钥对比来验证服务端合法性。在制作证书密钥时,可以保持公钥不变,变相避免证书有效期问题。

  • 证书锁定

对比本地和服务端返回的证书内容,完全匹配才算校验通过。

证书锁定更加安全,但密钥过期的风险较大。针对移动APP,一般都选择公钥锁定的方式。两种方式都需要操作serverTrust,详细的验证过程可以参考Alamofire及AFNetworking源码。

其它文章

handling_an_authentication_challenge
iOS APP灰度发布方案
iOS Asset Catalog and Bundle
iOS 13 Scene Delegate and multiple windows
iOS Crash log符号化