7

iOS URLSession Authentication Challenge及SSL Pinning

 3 years ago
source link: https://easeapi.com/blog/blog/137-ssl-pinning.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

137-ssl-pinning.jpg

中间人攻击 & 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符号化


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK