Alamofire 安全认证
HTTP
网络传输有着被窃听篡改的风险, 在日常开发中大多采用HTTPS
来传输重要的数据,而HTTPS
涉及一系列安全认证,本篇探索Alamofire
如何处理安全认证
HTTP 与 HTTPS
在开始探索Alamofire
之前,了解下 HTTP
与 HTTPS
的一些知识.
HTTP 与 HTTPS 的网络参考模型
图片来自<<HTTP权威指南>>-
HTTP
与HTTPS
在网络模型上的区别在于HTTPS
多了一个安全层(SSL or TLS)
. 由于只是在HTTP
底层 多添加了一个安全层,所以使用HTTPS
不需要做过多的更改 -
HTTPS
数据在 传递给TCP
之前要经过SSL or TLS
加密,在这一层有一系列机制来保证通信的安全可靠
1️⃣证书校验:确定对方身份是否是我们的服务器
2️⃣数据加密(RSA,对称加密):保证数据不能被破译
3️⃣数据校验:保证数据没有被篡改
验证过程中涉及两个重要的概念:
- 数字签名:用来验证报文未被篡改或伪造的校验和(也是要被加密的)
- 数字证书:由一个可信任组织验证和签发的识别信息, 数字证书并没有单一的标准,现在使用的大多数证书都以一种标准格式
X.509 v3
. 证书的主要内容如图所示:
图片来自<<HTTP权威指南>>
HTTPS 握手过程
图片来自<<HTTP权威指南>>- 与
HTTP
相比HTTPS
多了步骤2 SSL安全握手
和步骤5 SSL关闭通知
,步骤2 SSL握手
涉及一系列校验以及加密解密过程 ,这也是HTTPS
性能弱于HTTP
的原因 .
证书字段的说明
图片来自<<HTTP权威指南>>SSL安全握手
SSL 安全握手
对应于HTTPS 握手过程的步骤2
.这里是SSL安全握手
的细节
-
步骤1:客户端向服务器请求证书,证书中包含
公钥
和证书的签名
. -
步骤3:客户端收到
服务器证书
时会对服务器证书的签名颁发机构
进行检查.如果这个机构是个很有权威的公共签名机构
(手机中可能存在许多有权威的签名颁发机构的证书
),这时会直接用系统的方法验证签名
,这个过程不需要我们来处理.,如果服务器的签名颁发机构
不是很有权威的(自签证书),则需要客户端验证证书. -
步骤3:证书验证成功后,会随机生成一串数字作为数据
对称加密的密匙
,并且把对称加密密匙
放到报文中,用服务器证书中的公钥
对报文进行加密,传递给服务器,服务器用自己的私钥解密拿到对称加密密钥
,告知客户端.此时客户端与服务器都有了对称加密的密匙
, 之后的数据传输双方都用对称加密密匙
进行加解密.
验证证书有效性
以下是验证证书有效性的流程:
- 日期检测:检查证书的起始日期和结束日期,以确保证书有效
-
签名颁发者可信度检测: 验证证书的
颁发机构
,因为任何人都可以生成证书,例如服务器生成的自签证书
,还有权威机构颁发的证书
.自签证书
需要客户端来验证机构是否合法. -
签名检测:一旦判定签名是可信的,客户端就要对
签名
使用签名颁发机构
的公开秘钥加密,并将其与校验码
进行比较,已查看证书是否被篡改. -
站点身份检测: 为防止服务器复制他人证书,或者拦截他人的流量,需要
验证证书中的域名与它们所对话的服务器的域名是否匹配
,如果不匹配,则连接失败.
Alamofire 如何操作认证过程?
上面的知识了解了,来看看 Alamofire 是如何实现认证过程的操作的.
Alamofire
基于 URLSession
,所以先来到 URLSessionDelegate
的代理方法
extension SessionDelegate: URLSessionDelegate {
//收到需要认证的请求
open func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
guard sessionDidReceiveChallengeWithCompletion == nil else {
sessionDidReceiveChallengeWithCompletion?(session, challenge, completionHandler)
return
}
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
var credential: URLCredential?
if let sessionDidReceiveChallenge = sessionDidReceiveChallenge {
(disposition, credential) = sessionDidReceiveChallenge(session, challenge)
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
let host = challenge.protectionSpace.host
if
//对于特定的host(主机地址),获取服务器信任策略
let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
//从SSL中获取服务器的事务状态
let serverTrust = challenge.protectionSpace.serverTrust
{
//服务器信任策略对象验证服务器的事务状态
if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
disposition = .useCredential
//通过验证则创建URL凭证
credential = URLCredential(trust: serverTrust)
} else {
//验证失败就取消
disposition = .cancelAuthenticationChallenge
}
}
}
//通知系统以完成
completionHandler(disposition, credential)
}
}
- 在不设置我们自己的处理闭包的情况下(
sessionDidReceiveChallenge
)Alamofire
为我们抽象了验证的总体流程:
Step 1:获取策略对象
Step 2:获取远端SSL信息,这个信息中有服务器的证书
Step 3:用创建的的策略对象
执行验证方法
Step 4:根据验证结果的成功失败创建相应的URLCredential
Step 5:通知系统调用completionHandler
来看看 session
的 serverTrustPolicyManager
属性:
extension URLSession {
private struct AssociatedKeys {
static var managerKey = "URLSession.ServerTrustPolicyManager"
}
var serverTrustPolicyManager: ServerTrustPolicyManager? {
get {
return objc_getAssociatedObject(self, &AssociatedKeys.managerKey) as? ServerTrustPolicyManager
}
set (manager) {
objc_setAssociatedObject(self, &AssociatedKeys.managerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
- 是一个关联属性
在看ServerTrustPolicyManager
:
open class ServerTrustPolicyManager {
public let policies: [String: ServerTrustPolicy]
public init(policies: [String: ServerTrustPolicy]) {
self.policies = policies
}
open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
return policies[host]
}
}
-
ServerTrustPolicyManager
只是保存一个字典,字典中存储了host
以及与host对应的 ServerTrustPolicy
.
来到 ServerTrustPolicy
,ServerTrustPolicy
代码比较长,就不放在一起了,分几个部分来说明.
public enum ServerTrustPolicy {
//使用默认的服务器信任评估,同时允许控制是否质询主机host
case performDefaultEvaluation(validateHost: Bool)
//执行废除评估,指定是否验证主机和一些选项
case performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags)
//使用固定的证书评估
//如果其中一个证书与服务器证书匹配,则信任服务器
case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool)
//使用固定的公钥评估
//如果固定的公式与服务器证书公钥之一匹配,则认为服务器有效
case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool)
//禁用评估
case disableEvaluation
//自定义评估,小心使用
case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool)
}
ServerTrustPolicy
是一个枚举类型,还提供了一些方法:
//找到某个bundle下的所有证书
public static func certificates(in bundle: Bundle = Bundle.main) -> [SecCertificate] {
var certificates: [SecCertificate] = []
let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in
bundle.paths(forResourcesOfType: fileExtension, inDirectory: nil)
}.joined())
for path in paths {
if
let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
let certificate = SecCertificateCreateWithData(nil, certificateData)
{
certificates.append(certificate)
}
}
return certificates
}
核心的认证方法:
public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool {
var serverTrustIsValid = false
switch self {
case let .performDefaultEvaluation(validateHost):
let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
SecTrustSetPolicies(serverTrust, policy)
serverTrustIsValid = trustIsValid(serverTrust)
case let .performRevokedEvaluation(validateHost, revocationFlags):
let defaultPolicy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
let revokedPolicy = SecPolicyCreateRevocation(revocationFlags)
SecTrustSetPolicies(serverTrust, [defaultPolicy, revokedPolicy] as CFTypeRef)
serverTrustIsValid = trustIsValid(serverTrust)
case let .pinCertificates(pinnedCertificates, validateCertificateChain, validateHost):
if validateCertificateChain {
let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
SecTrustSetPolicies(serverTrust, policy)
SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray)
SecTrustSetAnchorCertificatesOnly(serverTrust, true)
serverTrustIsValid = trustIsValid(serverTrust)
} else {
let serverCertificatesDataArray = certificateData(for: serverTrust)
let pinnedCertificatesDataArray = certificateData(for: pinnedCertificates)
outerLoop: for serverCertificateData in serverCertificatesDataArray {
for pinnedCertificateData in pinnedCertificatesDataArray {
if serverCertificateData == pinnedCertificateData {
serverTrustIsValid = true
break outerLoop
}
}
}
}
case let .pinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost):
var certificateChainEvaluationPassed = true
if validateCertificateChain {
let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil)
SecTrustSetPolicies(serverTrust, policy)
certificateChainEvaluationPassed = trustIsValid(serverTrust)
}
if certificateChainEvaluationPassed {
outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] {
for pinnedPublicKey in pinnedPublicKeys as [AnyObject] {
if serverPublicKey.isEqual(pinnedPublicKey) {
serverTrustIsValid = true
break outerLoop
}
}
}
}
case .disableEvaluation:
serverTrustIsValid = true
case let .customEvaluation(closure):
serverTrustIsValid = closure(serverTrust, host)
}
return serverTrustIsValid
}
-
evaluate
枚举自身的各种情况, 在各种case
下都是处理一些参数 之后调用trustIsValid
私有方法.拿到结果处理之后的逻辑
- 值得注意的是
case .disableEvaluation
都会返回true
,没有经过校验过程,直接信任服务器
trustIsValid
方法:
private func trustIsValid(_ trust: SecTrust) -> Bool {
var isValid = false
var result = SecTrustResultType.invalid
//Security 框架下的方法,执行真正的认证
let status = SecTrustEvaluate(trust, &result)
if status == errSecSuccess {
let unspecified = SecTrustResultType.unspecified
let proceed = SecTrustResultType.proceed
isValid = result == unspecified || result == proceed
}
return isValid
}
还有几个获取公钥
的方法:
//从bundle 中获取所有证书中的公钥
public static func publicKeys(in bundle: Bundle = Bundle.main) -> [SecKey] {
var publicKeys: [SecKey] = []
for certificate in certificates(in: bundle) {
if let publicKey = publicKey(for: certificate) {
publicKeys.append(publicKey)
}
}
return publicKeys
}
//私有:从服务器证书中获取公钥
private static func publicKeys(for trust: SecTrust) -> [SecKey] {
var publicKeys: [SecKey] = []
for index in 0..<SecTrustGetCertificateCount(trust) {
if
let certificate = SecTrustGetCertificateAtIndex(trust, index),
let publicKey = publicKey(for: certificate)
{
publicKeys.append(publicKey)
}
}
return publicKeys
}
//私有:从本地证书中获取公钥
private static func publicKey(for certificate: SecCertificate) -> SecKey? {
var publicKey: SecKey?
let policy = SecPolicyCreateBasicX509()
var trust: SecTrust?
let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust)
if let trust = trust, trustCreationStatus == errSecSuccess {
publicKey = SecTrustCopyPublicKey(trust)
}
return publicKey
}
Alamofire 自定义认证策略
经过了前面的源码解析以及知识了解,下面的代码就好理解了:
let serverTrustPlolicies:[String: ServerTrustPolicy] = [
"host0": .pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true),
"host1": .disableEvaluation,
"host2": .pinPublicKeys(publicKeys: ServerTrustPolicy.publicKeys(),
validateCertificateChain: true,
validateHost: true)
]
let serverTrustPolicyManager =
ServerTrustPolicyManager(policies: serverTrustPlolicies)
let sessionManger = SessionManager(serverTrustPolicyManager: )
-
有前面的分析我们知道, 认证的策略是保存在
ServerTrustPolicyManager
中的,所以我们 创建ServerTrustPolicyManager
,并指定我们自己的策略字典
,在构造SessionManager
时指定ServerTrustPolicyManager
,就可以自定义认证策略. -
上面的
策略字典
指定host0
采用固定证书的认证方式,host1
直接信任,host2
采用固定公钥的认证方式.
Alamofire 安全认证,你了解了吗?