本篇主要讲解Alamofire中安全验证代码

前言

作为开发人员,理解HTTPS的原理和应用算是一项基本技能。HTTPS目前来说是非常安全的,但仍然有大量的公司还在使用HTTP。其实HTTPS也并不是很贵啊。

在网上可以找到大把的介绍HTTTPS的文章,在阅读ServerTrustPolicy.swfit代码前,我们先简单的讲一下HTTPS请求的过程:

上边的图片已经标出了步骤,我们逐步的来分析:

  1. HTTPS请求以https开头,我们首先向服务器发送一条请求。

  2. 服务器需要一个证书,这个证书可以从某些机构获得,也可以自己通过工具生成,通过某些合法机构生成的证书客户端不需要进行验证,这样的请求不会触发Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)代理方法,自己生成的证书则需要客户端进行验证。证书中包含公钥和私钥:

    • 公钥是公开的,任何人都可以使用该公钥加密数据,只有知道了私钥才能解密数据
    • 私钥是要求高度保密的,只有知道了私钥才能解密用公钥加密的数据
    • 关于非对称加密的知识,大家可以在网上找到
  3. 服务器会把公钥发送给客户端

  4. 客户端此刻就拿到了公钥。注意,这里不是直接就拿公钥加密数据发送了,因为这仅仅能满足客户端给服务器发加密数据,那么服务器怎么给客户端发送加密数据呢?因此需要在客户端和服务器间建立一条通道,通道的密码只有客户端和服务器知道。只能让客户端自己生成一个密码,这个密码就是一个随机数,这个随机数绝对是安全的,因为目前只有客户端自己知道

  5. 客户端把这个随机数通过公钥加密后发送给服务器,就算被别人截获了加密后的数据,在没有私钥的情况下,是根本无法解密的

  6. 服务器用私钥把数据解密后,就获得了这个随机数

  7. 到这里客户端和服务器的安全连接就已经建立了,最主要的目的是交换随机数,然后服务器就用这个随机数把数据加密后发给客户端,使用的是对称加密技术。

  8. 客户端获得了服务器的加密数据,使用随机数解密,到此,客户端和服务器就能通过随机数发送数据了

HTTPS前边的几次握手是需要时间开销的,因此,不能每次连接都走一遍,这就是后边使用对称加密数据的原因。Alamofire中主要做的是对服务器的验证,关于自定义的安全验证应该也是模仿了上边的整个过程。相对于Apple来说,隐藏了发送随机数这一过程。

对于服务器的验证除了证书验证之外一定要加上域名验证,这样才能更安全。服务器若要验证客户端则会使用签名技术。如果伪装成客户端来获取服务器的数据最大的问题就是不知道某个请求的参数是什么,这样也就无法获取数据。

ServerTrustPolicyManager

ServerTrustPolicyManager是对ServerTrustPolicy的管理,我们可以暂时把ServerTrustPolicy当做是一个安全策略,就是指对一个服务器采取的策略。然而在真实的开发中,一个APP可能会用到很多不同的主机地址(host)。因此就产生了这样的需求,为不同的host绑定一个特定的安全策略。

因此ServerTrustPolicyManager需要一个字典来存放这些有key,value对应关系的数据。我们看下边的代码:

/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host.
open class ServerTrustPolicyManager {
/// The dictionary of policies mapped to a particular host.
open let policies: [String: ServerTrustPolicy] /// Initializes the `ServerTrustPolicyManager` instance with the given policies.
///
/// Since different servers and web services can have different leaf certificates, intermediate and even root
/// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This
/// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key
/// pinning for host3 and disabling evaluation for host4.
///
/// - parameter policies: A dictionary of all policies mapped to a particular host.
///
/// - returns: The new `ServerTrustPolicyManager` instance.
public init(policies: [String: ServerTrustPolicy]) {
self.policies = policies
} /// Returns the `ServerTrustPolicy` for the given host if applicable.
///
/// By default, this method will return the policy that perfectly matches the given host. Subclasses could override
/// this method and implement more complex mapping implementations such as wildcards.
///
/// - parameter host: The host to use when searching for a matching policy.
///
/// - returns: The server trust policy for the given host if found.
open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
return policies[host]
}
}

出于优秀代码的设计问题,在后续的使用中肯定会有根据host读取策略的要求,因此,在上边的类中设计了最后一个函数。

我们是这么使用的:

let serverTrustPolicies: [String: ServerTrustPolicy] = [
"test.example.com": .pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
),
"insecure.expired-apis.com": .disableEvaluation
] let sessionManager = SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)

在Alamofire中这个ServerTrustPolicyManager会在SessionDelegate的收到服务器要求验证的方法中会出现,这个会在后续的文章中给出说明。

把ServerTrustPolicyManager绑定到URLSession

ServerTrustPolicyManager作为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)
}
}
}

上边的代码用到了运行时,尤其是OBJC_ASSOCIATION_RETAIN_NONATOMIC这个选项,其中包含了强引用和若引用的问题,我想在这里简单的解释一下引用问题。

我们可以这么理解,不管是类还是对象,或者是对象的属性,我们都称之为一个object。我们把这个object比作一个铁盒子,当有其它的对象对他强引用的时候,就像给这个铁盒子绑了一个绳子,弱引用就像一条虚幻的激光一样连接这个盒子。当然,在oc中,很多对象默认的情况下就是strong的。

我们可以想象这个盒子是被绳子拉住了,才能漂浮在空中,如果没有绳子就会掉到无底深渊,然后销毁。这里最重要的概念就是,只要一个对象没有了强引用,那么就会立刻销毁。

我们举个例子:

MyViewController *myController = [[MyViewController alloc] init…];

上边的代码是再平常不过的一段代码,创建了一个MyViewController实例,然后使用myController指向了这个实例,因此这个实例就有了一个绳子,他就不会立刻销毁,如果我们把代码改成这样:

MyViewController * __weak myController = [[MyViewController alloc] init…];

把myController指向实例设置为弱引用,那么即使在下一行代码打印这个myController,也会是nil。因为实例并没有一个绳子让他能不不销毁。

所谓道理都是相通的,只要理解了这个概念就能明白引用循环的问题,需要注意的是作用域的问题,如果上边的myController在一个函数中,那么出了函数的作用域,也会销毁。

ServerTrustPolicy

接下来将是本篇文章最核心的内容,得益于swift语言的强大,ServerTrustPolicy被设计成enum枚举。既然本质上只是个枚举,那么我们先不关心枚举中的函数,先单独看看有哪些枚举子选项:

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)

千万别认为上边的某些选项是个函数,其实他们只是不同的类型加上关联值而已。我们先不对这些选项做不解释,因为在下边的方法中会根据这些选项做出不同的操作,到那时在说明这些选项的作用更好。

还有一点要明白,在swift中是像下边代码这样初始化枚举的:

ServerTrustPolicy.performDefaultEvaluation(validateHost: true)

我们用上帝视角来看作者的代码,接下来就应该看看那些带有static的函数了,因为这些函数都是静态函数,可以直接用ServerTrustPolicy调用,虽然归属于ServerTrustPolicy,但相对比较独立。

获取证书

 /// Returns all certificates within the given bundle with a `.cer` file extension.
///
/// - parameter bundle: The bundle to search for all `.cer` files.
///
/// - returns: All certificates within the given 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
}

在开发中,如果和服务器的安全连接需要对服务器进行验证,最好的办法就是在本地保存一些证书,拿到服务器传过来的证书,然后进行对比,如果有匹配的,就表示可以信任该服务器。从上边的函数中可以看出,Alamofire会在Bundle(默认为main)中查找带有[".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]后缀的证书。

注意,上边函数中的paths保存的是这些证书的路径,map把这些后缀转换成路径,我们以.cer为例。通过map后,原来的".cer"就变成了一个数组,也就是说通过map后,原来的数组变成了二维数组了,然后再通过joined()函数,把二维数组转换成一维数组。

然后要做的就是根据这些路径获取证书数据了,就不多做解释了。

获取公钥

这个比较好理解,就是在本地证书中取出公钥,至于证书是由什么组成的,大家可以网上自己查找相关内容,

 /// Returns all public keys within the given bundle with a `.cer` file extension.
///
/// - parameter bundle: The bundle to search for all `*.cer` files.
///
/// - returns: All public keys within the given 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
}

上边的函数很简单,但是他用到了另外一个函数publicKey(for: certificate)

通过SecCertificate获取SecKey

获取SecKey可以通过SecCertificate也可以通过SecTrust,下边的函数是第一种情况:

  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
}

上边的过程没什么好说的,基本上这是固定写法,值得注意的是上边默认是按照X509证书格式来解析的,因此在生成证书的时候最好使用这个格式。否则可能无法获取到publicKey。

最核心的方法evaluate

从函数设计的角度考虑,evaluate应该接受两个参数,一个是服务器的证书,一个是host。返回一个布尔类型。

evaluate函数是枚举中的一个函数,因此它必然依赖枚举的子选项。这就说明只有初始化枚举才能使用这个函数。

举一个现实生活中的一个小例子。有一个管理员,他手下管理这3个员工,分别是厨师,前台,行政,现在有一个任务需要想办法弄明白这3个人会不会喊麦,有两种方法可以得出结果,一种是管理员一个一个的去问,也就是得出结果的方法掌握在管理员手中,只有通过管理员才能知道答案。有一个老板想知道厨师会不会喊麦。他必须要去问管理员才行。这就造成了逻辑上的问题。另一种方法,让每一个人当场喊一个,任何人在任何场合都能得出结果。

最近重新看了代码大全这本书,对子程序的设计有了全新的认识。重点还在于抽象类型是什么?这个就不多说了,有兴趣的朋友可以去看看那本书。

这个函数很长,但总体的思想是根据不同的策略做出不同的操作。我们先把该函数弄上来:

 /// Evaluates whether the server trust is valid for the given host.
///
/// - parameter serverTrust: The server trust to evaluate.
/// - parameter host: The host of the challenge protection space.
///
/// - returns: Whether the server trust is valid.
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
}

不管选用那种策略,要完成验证都需要3步:

  1. SecPolicyCreateSSL 创建策略,是否验证host
  2. SecTrustSetPolicies 为待验证的对象设置策略
  3. trustIsValid 进行验证

到了这里就有必要介绍一下几种策略的用法了:

  • performDefaultEvaluation 默认的策略,只有合法证书才能通过验证
  • performRevokedEvaluation 对注销证书做的一种额外设置,关于注销证书验证超过了本篇文章的范围,有兴趣的朋友可以查看官方文档。
  • pinCertificates 验证指定的证书,这里边有一个参数:是否验证证书链,关于证书链的相关内容可以看这篇文章iOS 中对 HTTPS 证书链的验证.验证证书链算是比较严格的验证了。这里边设置锚点等等,这里就不做解释了。如果不验证证书链的话,只要对比指定的证书有没有和服务器信任的证书匹配项,只要有一个能匹配上,就验证通过
  • pinPublicKeys 这个更上边的那个差不多,就不做介绍了
  • disableEvaluation 该选项下,验证一直都是通过的,也就是说无条件信任
  • customEvaluation 自定义验证,需要返回一个布尔类型的结果

上边的这些验证选项中,我们可能根据自己的需求进行验证,其中最安全的是证书链加host双重验证。而且在上边的evaluate函数中用到了4个辅助函数,我们来看看:

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
}

func certificateData(for trust: SecTrust) -> [Data]

该函数把服务器的SecTrust处理成证书二进制数组

 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)
}

func certificateData(for certificates: [SecCertificate]) -> [Data]

private func certificateData(for certificates: [SecCertificate]) -> [Data] {
return certificates.map { SecCertificateCopyData($0) as Data }
}

func publicKeys(for trust: 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
}

总结

其实在开发中,可以不必关心这些实现细节,要想弄明白这些策略的详情,还需要做很多的功课才行。

由于知识水平有限,如有错误,还望指出

链接

Alamofire源码解读系列(一)之概述和使用 简书-----博客园

Alamofire源码解读系列(二)之错误处理(AFError) 简书-----博客园

Alamofire源码解读系列(三)之通知处理(Notification) 简书-----博客园

Alamofire源码解读系列(四)之参数编码(ParameterEncoding) 简书-----博客园

Alamofire源码解读系列(五)之结果封装(Result) 简书-----博客园

Alamofire源码解读系列(六)之Task代理(TaskDelegate) 简书-----博客园

Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 简书-----博客园

Alamofire源码解读系列(八)之安全策略(ServerTrustPolicy)的更多相关文章

  1. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  2. Alamofire源码解读系列(十)之序列化(ResponseSerialization)

    本篇主要讲解Alamofire中如何把服务器返回的数据序列化 前言 和前边的文章不同, 在这一篇中,我想从程序的设计层次上解读ResponseSerialization这个文件.更直观的去探讨该功能是 ...

  3. Alamofire源码解读系列(十一)之多表单(MultipartFormData)

    本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...

  4. Alamofire源码解读系列(十二)之时间轴(Timeline)

    本篇带来Alamofire中关于Timeline的一些思路 前言 Timeline翻译后的意思是时间轴,可以表示一个事件从开始到结束的时间节点.时间轴的概念能够应用在很多地方,比如说微博的主页就是一个 ...

  5. Alamofire源码解读系列(十二)之请求(Request)

    本篇是Alamofire中的请求抽象层的讲解 前言 在Alamofire中,围绕着Request,设计了很多额外的特性,这也恰恰表明,Request是所有请求的基础部分和发起点.这无疑给我们一个Req ...

  6. Alamofire源码解读系列(二)之错误处理(AFError)

    本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...

  7. Alamofire源码解读系列(四)之参数编码(ParameterEncoding)

    本篇讲解参数编码的内容 前言 我们在开发中发的每一个请求都是通过URLRequest来进行封装的,可以通过一个URL生成URLRequest.那么如果我有一个参数字典,这个参数字典又是如何从客户端传递 ...

  8. Alamofire源码解读系列(三)之通知处理(Notification)

    本篇讲解swift中通知的用法 前言 通知作为传递事件和数据的载体,在使用中是不受限制的.由于忘记移除某个通知的监听,会造成很多潜在的问题,这些问题在测试中是很难被发现的.但这不是我们这篇文章探讨的主 ...

  9. Alamofire源码解读系列(五)之结果封装(Result)

    本篇讲解Result的封装 前言 有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解.在Alamofire中,使用Response来描述请求后的结果.我们都知道Alamof ...

随机推荐

  1. GIS制图人员的自我修养(1)--制图误区

    GIS制图人员的自我修养 by 李远祥 最近一直坚持写GIS制图的技术专题,并不是为了要介绍有什么好的技术和方法去制图,而是要告诉所有从事这一方向的人员一个铁铮铮的实现--要做好GIS制图,必须加强自 ...

  2. 阿里云oss总是提示SignatureDoesNotMatch错误怎么办

    网上的所有阿里云oss(C#)的例子几乎试遍了,为什么还是提示SignatureDoesNotMatch错误?什么原因?怎么办?下载一个阿里云提供的windows客户端发现,依然提示签名错误. 开始怀 ...

  3. canvas绘制圆形进度条(或显示当前已浏览网页百分比)

    使用canvas绘制圆形进度条,或者是网页加载进度条 或者是显示你浏览了本网页多少-- 由于个浏览器的计算差异,打开浏览器时 初始值有所不同,但是当拉倒网页底部时,均显示100%. 兼容性:测试浏览器 ...

  4. vs2010入门程序和出错问题解决方案

    本篇文章分两个部分: 第一,如何建立一个Helloword工程 1.打开Vs2010新建项目 2.选择Visual C++>>Win32>>Win32控制台应用程序,输入项目名 ...

  5. C/C++中慎用宏(#define)

    宏的定义在程序中是非常有用的,但是使用不当,就会给自身造成很大的困扰.通常这种困扰为:宏使用在计算方面. 本例子主要是在宏的计算方面,很多时候,大家都知道定义一个计算的宏,对于编译和编程是多么的有用. ...

  6. c#基础语句——循环语句(for、while、foreach)

    循环类型:for.while.foreach 循环四要素:初始条件-->循环条件-->循环体-->状态改变 1.for 格式: for(初始条件:循环条件:状态改变) {循环体(br ...

  7. 【经验】JavaScript

    1.function closeWin(){             window.open('','_self');       window.opener=null;  //    window. ...

  8. js基础---cookie存储

    一.Cookie是什么Cookie是一种客户端(浏览器)把用户信息以文件形式存储到本地硬盘的技术,说白了就是一种浏览器技术 二.Cookie的作用Cookie的作用很单一,就是存储客户数据.(存储数据 ...

  9. 定制Maven的ArcheType

    根据需要定制Maven的ArcheType的好处不言而喻了,我就不再啰嗦.定制一般通过从Maven的项目构建,比手动构建省去了配置文件的编写.资源文件的复制等繁琐的操作,下面我们就说下从Maven项目 ...

  10. 南京.NET技术行业落地分享交流会圆满成功

    3月11日,由南京.NET社区发起,纳龙科技赞助,并联合举办的,.NET技术线下交流活动,圆满成功. 这是南京.NET圈子第一次的小型聚会,为了办好此次活动,工作人员不敢怠慢.早早准备好了小奖品与水果 ...