概要
APP即使开启HTTPS请求,也无法阻止中间人攻击。更安全的做法是,启用SSL Pinning
抓包工具作为中间人,截获客户端发送给服务端的请求,伪装成客户端与服务端通信;同时将服务端返回的内容转发给客户端。基于这个原理,需要客户端信任中间人(抓包工具)的证书,否则抓包工具显示的请求内容也是加密的。
SSL Pinning(证书绑定)技术主要用来防止中间人攻击,原理就是在客户端内置证书或公钥,对服务端返回的证书有效期、所属域名、公钥、证书内容等信息进行校验,以验证服务端是否合法,校验不通过则阻断请求。开启了SSL Pinning之后,客户端不再接收操作系统内置的证书,使用代理抓包时会造成请求失败,保证了客户端和服务端通信的安全。但SSL Pinning也有缺点:由于签发的证书都有有效期,当证书过期时,客户端只能进行升级。
1.开启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挑战方法没有实现是,也会调用此方法。
}
2. Authentication Challenge
常见的类型 HTTP Basic Authentication、HTTPS Server Trust Authentication等。URLSession支持以下认证类型:
// session 级别
NSURLAuthenticationMethodClientCertificate
NSURLAuthenticationMethodNegotiate
NSURLAuthenticationMethodNTLM
NSURLAuthenticationMethodServerTrust
// task 级别
NSURLAuthenticationMethodDefault
NSURLAuthenticationMethodHTTPBasic
NSURLAuthenticationMethodHTTPDigest
NSURLAuthenticationMethodHTMLForm
除了NSURLAuthenticationMethodServerTrust类型的认证挑战是客户端对服务器进行认证,其它类型的都是服务端对客户端进行认证。在HTTPS请求时,客户端会收到服务器发送的 NSURLAuthenticationMethodServerTrust类型的身份认证挑战。
3. 处理认证挑战方法
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就是认证类型,值是上述提到的八种认证类型之一。客户端需要根据不同的认证类型来处理认证。
4. 响应挑战
执行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。)
5. 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同步到其它设备。
}
6. SSL Pinning实践
对于NSURLAuthenticationMethodServerTrust类型的认证请求,需要对服务端返回的serverTrust进行校验。常见的SSL Pinning的有两种方式:
-
公钥锁定 提取证书中的公钥并内置到客户端中,通过与服务器返回的公钥对比来验证服务端合法性。在制作证书密钥时,可以保持公钥不变,变相避免证书有效期问题。
-
证书锁定 对比本地和服务端返回的证书内容,完全匹配才算校验通过。 证书锁定更加安全,但密钥过期的风险较大。针对移动APP,一般都选择公钥锁定的方式。两种方式都需要操作serverTrust,详细的验证过程可以参考Alamofire及AFNetworking源码。
- Handle Server Trust Authentication Challenges 官方链接
let protectionSpace = challenge.protectionSpace
guard protectionSpace.authenticationMethod ==
NSURLAuthenticationMethodServerTrust,
protectionSpace.host.contains("example.com") else {
completionHandler(.performDefaultHandling, nil)
return
}
guard let serverTrust = protectionSpace.serverTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
if checkValidity(of: serverTrust) {
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
} else {
// Show a UI here warning the user the server credentials are
// invalid, and cancel the load.
completionHandler(.cancelAuthenticationChallenge, nil)
}
8. 获取证书相关信息 链接-
- SecTrustGetCertificateCount (获取证书链中的证书数量)
- SecTrustGetCertificateAtIndex (根据下标获取证书链中的证书)
- SecKeyCopyExternalRepresentation SecKey (获取证书中的公钥外部表示)