引言
在网络请求、通讯过程中,最重要的就是安全了,稍有不慎,被别人截取、攻击,都有可能对自己或者公司带来不可估量的损失,所以,网络安全是尤为重大的。
这篇文章,我们就来讲讲,Alamofire
作为一个如此重要的三方库,它的安全策略是怎么设计和使用的。
HTTPS
在说到Alamofire
的安全策略之前,我们先来了解一下HTTPS
,毕竟Alamofire
也需要通过HTTPS
进行网络请求通讯的。
几种协议的介绍与关系
HTTP
:HTTP
协议传输的数据都是未加密的(明文),因此使用HTTP
协议传输隐私信息非常不安全。HTTPS
:为了保证隐私数据能加密传输,采用SSL/TLS
协议用于对HTTP
协议传输的数据进行加密,也就是HTTPS
。SSL
:SSL(Secure Sockets Layer)
协议是由网景公司设计,后被IETF
定义在RFC 6101
中。TLS
:TLS
可以说是SSL
的改进版,实际上我们现在的HTTPS
都是用的TLS
协议。
特点
HTTPS
在传输数据之前需要客户端(浏览器)与服务端(网站)之间进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。TLS/SSL
中使用了非对称加密,对称加密以及HASH
算法。其中非对称加密算法用于在握手过程中加密生成的密码,对称加密算法用于对真正传输的数据进行加密,而HASH
算法用于验证数据的完整性。TLS
握手过程中如果有任何错误,都会使加密连接断开,从而阻止了隐私信息的传输。
请求过程
我们先来看一下这张图(图片来自网络):
看着这张图,接下来我们来简单分析一下:
- 客户端的
HTTPS
请求首先向服务器发送一条请求,注意,HTTPS
请求均是以https
开头。- 这时候,服务器端就需要一个证书,这个证书既可以是自己通过某些工具生成,也可以是从某些机构获取。如果是通过某些合法机构生成的证书,是不需要进行验证的,同时,这些请求不会触发
@objc(URLSession:task:didReceiveChallenge:completionHandler:)
代理方法。如果是自己生成的证书,需要在客户端进行验证,且证书中应该包含公钥、私钥。(公钥:公开的,任何人都可以使用该公钥加密数据,只有知道了私钥才能解密数据。私钥:要求高度保密的,只有知道了私钥才能解密用公钥加密的数据。)- 服务器端把公钥发送给客户端
- 此时,客户端拿到公钥,这里要注意,拿到公钥后,并不会直接用于加密数据发送,仅仅是客户端给服务器端发送加密数据,还需要服务器端给客户端发送加密数据,因此,我们需要在客户端与服务器端建立一个安全的通讯通道,开启这条通道的密码只有客户端和服务器端知道。然后,客户端会自己生成一个随机数密码,因为这个随机数密码目前只有客户端知道,所以,这个随机数密码是绝对安全的。
- 再来,客户端用这个随机数密码再通过公钥加密后发送给服务器端,如果被中间人攻击截获了,没有私钥的情况下,他也是无法解密的。
- 服务器端收到客户端发送的加密数据后,使用私钥把数据解密后,就获取到了这个随机数。
- 此时此刻,客户端与服务器端的安全通道就已经连接好了,主要目的就是交换随机数,便于服务器使用这个随机数把数据加密后发送到客户端,此间,使用的是对称加密技术(备注:关于对称加密、非对称加密的详细知识网上或者书籍有很多,内容太多,这里就不详细解释了,也解释不完的😅)。
- 最后,客户端拿到了服务器端的加密数据后,再使用随机数解密,这样,客户端与服务器端就能通过随机数加密发送数据,进行安全的通讯了。
总结
HTTPS
每次握手其实都是需要时间开销的,所以,不能每次连接都这样走一次,因此,我们需要使用对称加密数据的方式。
在Alamofire
中,主要的工作是对服务器的验证,其自定义的安全策略验证,我猜,也是模仿的上边的这个过程。
另外,在对服务器的验证下,还应该加上域名验证,这样才能更加的安全
OK,前戏都已经说完了,接下来,进入主题。
ServerTrustPolicy
在查看
ServerTrustPolicy.swift
文件的时候,我们发现,最核心的2个类ServerTrustPolicyManager
和ServerTrustPolicy
。因此,接下来,我们就分别来说一说。
ServerTrustPolicy
简述
在Alamofire
中,ServerTrustPolicy
是一个枚举类型:
public enum ServerTrustPolicy {
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)
}
注意: 这些选项并不是函数,只是不同的类型加上了关联值而已。
函数说明
获取证书
首先,看下获取证书的函数方法:
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
}
- 如果在和服务器的安全连接中,需要对服务器进行验证,一个好的方法就是在本地工程保存一些证书,得到服务器传过来的证书后进行对比,如果有匹配,则表示可以信任该服务器。其中包括带有这些后缀的证书:
".cer"
,".CER"
,".crt"
,".CRT"
,".der"
,".DER"
。- 函数中,
paths
保存的是这些证书的路径,再通过map
函数转换为路径,最后,根据这些路径获取证书数据。
获取公钥
获取公钥的函数方法:
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
}
在本地证书中取出公钥,其中又调用了另外一个函数方法
publicKey(for: certificate)
,注意到,获取SecKey
可以通过SecCertificate
方式,也可以通过SecTrust
方式。
通过SecTrust获取SecKey
先看一下函数方法:
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
}
很简单的,没有什么好说的,都是固定的写法。
通过SecCertificate获取SecKey
先看一下函数方法:
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
}
- 一样的,固定写法,只是要特别注意一下
SecPolicyCreateBasicX509()
,默认是按照X509
证书格式来解析的,所以,在生成证书的时候,最好用这个格式来,不然有可能无法获得publicKey
。- 有关
X509
证书格式的详细说明看这里百度百科。
核心方法evaluate
我们先把函数看一下:
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
}
- 这个函数很长,一看
switch
语句,就知道,它的总体思想就是需要根据不同的策略做出不同操作。evaluate
函数需要接收2个参数,一个是服务器的证书,还有一个是host
,返回值是一个bool
类型。- 因为
evaluate
函数被定义在枚举中,因此,它肯定是依赖枚举的子选项,只有初始化枚举后,才能调用这个函数。
验证步骤说明
从上面的函数可以看到,不论我们使用哪一种策略,要完成验证,都需要以下步骤:
SecPolicyCreateSSL
:创建策略,是否验证hostSecTrustSetPolicies
:为待验证的对象设置策略trustIsValid
:进行验证
辅助函数
private func trustIsValid(_ trust: SecTrust) -> Bool
private func trustIsValid(_ trust: SecTrust) -> Bool {
var isValid = false
var result = SecTrustResultType.invalid
let status = SecTrustEvaluate(trust, &result)
if status == errSecSuccess {
let unspecified = SecTrustResultType.unspecified
let proceed = SecTrustResultType.proceed
isValid = result == unspecified || result == proceed
}
return isValid
}
该函数用于判断是否验证成功。
private func certificateData(for trust: SecTrust) -> [Data]
private func certificateData(for trust: SecTrust) -> [Data] {
var certificates: [SecCertificate] = []
for index in 0..<SecTrustGetCertificateCount(trust) {
if let certificate = SecTrustGetCertificateAtIndex(trust, index) {
certificates.append(certificate)
}
}
return certificateData(for: certificates)
}
该函数把服务器的
SecTrust
处理成证书二进制数组。
private func certificateData(for certificates: [SecCertificate]) -> [Data]
private func certificateData(for certificates: [SecCertificate]) -> [Data] {
return certificates.map { SecCertificateCopyData($0) as Data }
}
该函数把服务器的
SecCertificate
处理成证书二进制数组。
策略用法
在下边的验证选项中,我们可以根据自己的需求进行验证,最安全的是证书链加host
双重验证:
performDefaultEvaluation
:默认的策略,只有合法证书才能通过验证。performRevokedEvaluation
:对注销证书做的一种额外设置pinCertificates
:验证指定的证书,这里边有一个参数:是否验证证书链,关于证书链的相关内容可以去查一查其他更为详细的资料,验证证书链算是比较严格的验证了。如果不验证证书链的话,只要对比指定的证书有没有和服务器信任的证书匹配项,只要有一个能匹配上,就验证通过pinPublicKeys
:这个和上边的那个差不多disableEvaluation
:该选项下,验证一直都是通过的,也就是说无条件信任customEvaluation
:自定义验证,需要返回一个布尔类型的结果
ServerTrustPolicyManager
简述
ServerTrustPolicyManager
这个类是对ServerTrustPolicy
的管理类,因为在实际项目开发中,项目中可能会使用不同的主机地址host
,因此,我们需要为不同的host
绑定一个特定安全策略。
我们先来看一下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
使用了一个字典属性,用来存放有key
、value
对应关系的数据。- 由于需要根据
host
来读取策略,因此,该类增加了serverTrustPolicy
方法。
URLSession扩展
先看一下扩展代码:
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
作为URLSession
的一个属性,是通过运行时的手段来实现。
总结
这篇文章,也只是简单的解析了一下Alamofire
中,它的安全策略设计方法,当然,在实际项目开发中,大可以不必要关心这些实现细节,但是作为一个敬业的、喜欢iOS
开发的开发者来说,还是很有必要知晓其中的设计方法、使用方法,很多细节的东西,还需要做很多的功课才行。
常规打广告系列:
简书:Alamofire(八)-- 安全策略ServerTrustPolicy
掘金:Alamofire(八)-- 安全策略ServerTrustPolicy
小专栏:Alamofire(八)-- 安全策略ServerTrustPolicy