写点什么

验证 HTTPS 请求的证书(五)

  • 2019-12-10
  • 本文字数:6568 字

    阅读完需:约 22 分钟

验证 HTTPS 请求的证书(五)

关注仓库,及时获得更新:iOS-Source-Code-Analyze


自 iOS9 发布之后,由于新特性 App Transport Security 的引入,在默认行为下是不能发送 HTTP 请求的。很多网站都在转用 HTTPS,而 AFNetworking 中的 AFSecurityPolicy 就是为了阻止中间人攻击,以及其它漏洞的工具。


AFSecurityPolicy 主要作用就是验证 HTTPS 请求的证书是否有效,如果 app 中有一些敏感信息或者涉及交易信息,一定要使用 HTTPS 来保证交易或者用户信息的安全。

AFSSLPinningMode

使用 AFSecurityPolicy 时,总共有三种验证服务器是否被信任的方式:


Objective-C


typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {    AFSSLPinningModeNone,    AFSSLPinningModePublicKey,    AFSSLPinningModeCertificate,};
复制代码


  • AFSSLPinningModeNone 是默认的认证方式,只会在系统的信任的证书列表中对服务端返回的证书进行验证

  • AFSSLPinningModeCertificate 需要客户端预先保存服务端的证书

  • AFSSLPinningModeCertificate 也需要预先保存服务端发送的证书,但是这里只会验证证书中的公钥是否正确

初始化以及设置

在使用 AFSecurityPolicy 验证服务端是否受到信任之前,要对其进行初始化,使用初始化方法时,主要目的是设置验证服务器是否受信任的方式


Objective-C


+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {    return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];}
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates { AFSecurityPolicy *securityPolicy = [[self alloc] init]; securityPolicy.SSLPinningMode = pinningMode;
[securityPolicy setPinnedCertificates:pinnedCertificates];
return securityPolicy;}
复制代码


这里没有什么地方值得解释的。不过在调用 pinnedCertificate 的 setter 方法时,会从全部的证书中取出公钥保存到 pinnedPublicKeys 属性中。


Objective-C


- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {    _pinnedCertificates = pinnedCertificates;
if (self.pinnedCertificates) { NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]]; for (NSData *certificate in self.pinnedCertificates) { id publicKey = AFPublicKeyForCertificate(certificate); if (!publicKey) { continue; } [mutablePinnedPublicKeys addObject:publicKey]; } self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys]; } else { self.pinnedPublicKeys = nil; }}
复制代码


在这里调用了 AFPublicKeyForCertificate 对证书进行操作,返回一个公钥。

操作 SecTrustRef

serverTrust 的操作的函数基本上都是 C 的 API,都定义在 Security 模块中,先来分析一下在上一节中 AFPublicKeyForCertificate 的实现


Objective-C


static id AFPublicKeyForCertificate(NSData *certificate) {    id allowedPublicKey = nil;    SecCertificateRef allowedCertificate;    SecCertificateRef allowedCertificates[1];    CFArrayRef tempCertificates = nil;    SecPolicyRef policy = nil;    SecTrustRef allowedTrust = nil;    SecTrustResultType result;
allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate); __Require_Quiet(allowedCertificate != NULL, _out);
allowedCertificates[0] = allowedCertificate; tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
policy = SecPolicyCreateBasicX509(); __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out); __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
_out: if (allowedTrust) { CFRelease(allowedTrust); }
if (policy) { CFRelease(policy); }
if (tempCertificates) { CFRelease(tempCertificates); }
if (allowedCertificate) { CFRelease(allowedCertificate); }
return allowedPublicKey;}
复制代码


  1. 初始化一坨临时变量


   id allowedPublicKey = nil;    SecCertificateRef allowedCertificate;    SecCertificateRef allowedCertificates[1];    CFArrayRef tempCertificates = nil;    SecPolicyRef policy = nil;    SecTrustRef allowedTrust = nil;    SecTrustResultType result;
复制代码


  1. 使用 SecCertificateCreateWithData 通过 DER 表示的数据生成一个 SecCertificateRef,然后判断返回值是否为 NULL


   allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);    __Require_Quiet(allowedCertificate != NULL, _out);
复制代码


* 这里使用了一个非常神奇的宏 `__Require_Quiet`,它会判断 `allowedCertificate != NULL` 是否成立,如果 `allowedCertificate` 为空就会跳到 `_out` 标签处继续执行
复制代码


       #ifndef __Require_Quiet             #define __Require_Quiet(assertion, exceptionLabel)                            \               do                                                                          \               {                                                                           \                   if ( __builtin_expect(!(assertion), 0) )                                \                   {                                                                       \                       goto exceptionLabel;                                                \                   }                                                                       \               } while ( 0 )         #endif
复制代码


  1. 通过上面的 allowedCertificate 创建一个 CFArray


   allowedCertificates[0] = allowedCertificate;    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);
复制代码


* 下面的 `SecTrustCreateWithCertificates` 只会接收数组作为参数。
复制代码


  1. 创建一个默认的符合 X509 标准的 SecPolicyRef,通过默认的 SecPolicyRef 和证书创建一个 SecTrustRef 用于信任评估,对该对象进行信任评估,确认生成的 SecTrustRef 是值得信任的。


   policy = SecPolicyCreateBasicX509();    __Require_noErr_Quiet(SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust), _out);    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
复制代码


* 这里使用的 `__Require_noErr_Quiet` 和上面的宏差不多,只是会根据返回值判断是否存在错误。
复制代码


  1. 获取公钥


   allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
复制代码


* 这里的 `__bridge_transfer` 会将结果桥接成 `NSObject` 对象,然后将 `SecTrustCopyPublicKey` 返回的指针释放。
复制代码


  1. 释放各种 C 语言指针


   if (allowedTrust) {        CFRelease(allowedTrust);    }
if (policy) { CFRelease(policy); }
if (tempCertificates) { CFRelease(tempCertificates); }
if (allowedCertificate) { CFRelease(allowedCertificate); }
复制代码


每一个 SecTrustRef 的对象都是包含多个 SecCertificateRefSecPolicyRef。其中 SecCertificateRef 可以使用 DER 进行表示,并且其中存储着公钥信息。


对它的操作还有 AFCertificateTrustChainForServerTrustAFPublicKeyTrustChainForServerTrust 但是它们几乎调用了相同的 API。


Objective-C


static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
for (CFIndex i = 0; i < certificateCount; i++) { SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i); [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)]; }
return [NSArray arrayWithArray:trustChain];}
复制代码


  • SecTrustGetCertificateAtIndex 获取 SecTrustRef 中的证书

  • SecCertificateCopyData 从证书中或者 DER 表示的数据

验证服务端是否受信

验证服务端是否守信是通过 - [AFSecurityPolicy evaluateServerTrust:forDomain:] 方法进行的。


Objective-C


- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust                  forDomain:(NSString *)domain{
#1: 不能隐式地信任自己签发的证书
#2: 设置 policy
#3: 验证证书是否有效
#4: 根据 SSLPinningMode 对服务端进行验证
return NO;}
复制代码


  1. 不能隐式地信任自己签发的证书


   if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");        return NO;    }
复制代码


> Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).> Instead, add your own (self-signed) CA certificate to the list of trusted anchors.* 所以如果没有提供证书或者不验证证书,并且还设置 `allowInvalidCertificates` 为**真**,满足上面的所有条件,说明这次的验证是不安全的,会直接返回 `NO`
复制代码


  1. 设置 policy


   NSMutableArray *policies = [NSMutableArray array];    if (self.validatesDomainName) {        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];    } else {        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];    }
复制代码


* 如果要验证域名的话,就以域名为参数创建一个 `SecPolicyRef`,否则会创建一个符合 X509 标准的默认 `SecPolicyRef` 对象
复制代码


  1. 验证证书的有效性


   if (self.SSLPinningMode == AFSSLPinningModeNone) {        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {        return NO;    }
复制代码


* 如果**只根据信任列表中的证书**进行验证,即 `self.SSLPinningMode == AFSSLPinningModeNone`。如果允许无效的证书的就会直接返回 `YES`。不允许就会对服务端信任进行验证。* 如果服务器信任无效,并且不允许无效证书,就会返回 `NO`
复制代码


  1. 根据 SSLPinningMode 对服务器信任进行验证


   switch (self.SSLPinningMode) {        case AFSSLPinningModeNone:        default:            return NO;        case AFSSLPinningModeCertificate: {            ...        }        case AFSSLPinningModePublicKey: {            ...        }    }
复制代码


* `AFSSLPinningModeNone` 直接返回 `NO`* `AFSSLPinningModeCertificate`
复制代码


       NSMutableArray *pinnedCertificates = [NSMutableArray array];         for (NSData *certificateData in self.pinnedCertificates) {             [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];         }         SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
if (!AFServerTrustIsValid(serverTrust)) { return NO; }
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA) NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) { if ([self.pinnedCertificates containsObject:trustChainCertificate]) { return YES; } }
return NO;
复制代码


    1.  从 `self.pinnedCertificates` 中获取 DER 表示的数据    2.  使用 `SecTrustSetAnchorCertificates` 为服务器信任设置证书    3.  判断服务器信任的有效性    4.  使用 `AFCertificateTrustChainForServerTrust` 获取服务器信任中的全部 DER 表示的证书    5.  如果 `pinnedCertificates` 中有相同的证书,就会返回 `YES`
* `AFSSLPinningModePublicKey`
复制代码


       NSUInteger trustedPublicKeyCount = 0;         NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
for (id trustChainPublicKey in publicKeys) { for (id pinnedPublicKey in self.pinnedPublicKeys) { if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) { trustedPublicKeyCount += 1; } } } return trustedPublicKeyCount > 0;
复制代码


    * 这部分的实现和上面的差不多,区别有两点
1. 会从服务器信任中获取公钥 2. `pinnedPublicKeys` 中的公钥与服务器信任中的公钥相同的数量大于 0,就会返回真
复制代码

与 AFURLSessionManager 协作

在代理协议 - URLSession:didReceiveChallenge:completionHandler: 或者 - URLSession:task:didReceiveChallenge:completionHandler: 代理方法被调用时会运行这段代码


Objective-C


if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {    if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {        disposition = NSURLSessionAuthChallengeUseCredential;        credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];    } else {        disposition = NSURLSessionAuthChallengeRejectProtectionSpace;    }} else {    disposition = NSURLSessionAuthChallengePerformDefaultHandling;}
复制代码


NSURLAuthenticationChallenge 表示一个认证的挑战,提供了关于这次认证的全部信息。它有一个非常重要的属性 protectionSpace,这里保存了需要认证的保护空间, 每一个 NSURLProtectionSpace 对象都保存了主机地址,端口和认证方法等重要信息。


在上面的方法中,如果保护空间中的认证方法为 NSURLAuthenticationMethodServerTrust,那么就会使用在上一小节中提到的方法 - [AFSecurityPolicy evaluateServerTrust:forDomain:] 对保护空间中的 serverTrust 以及域名 host 进行认证


根据认证的结果,会在 completionHandler 中传入不同的 dispositioncredential 参数。

小结

  • AFSecurityPolicy 同样也作为一个即插即用的模块,在 AFNetworking 中作为验证 HTTPS 证书是否有效的模块存在,在 iOS 对 HTTPS 日渐重视的今天,在我看来,使用 HTTPS 会成为今后 API 开发的标配。

相关文章

关于其他 AFNetworking 源代码分析的其他文章:



本文转载自 Draveness 技术博客。


原文链接:https://draveness.me/afnetworking5


2019-12-10 17:471457

评论

发布
暂无评论
发现更多内容

React-Hooks怎样封装防抖和节流-面试真题

beifeng1996

React

已献出膝盖!GitHub上的宝藏级SpringBoot核心文档,讲得太清晰了

Geek_0c76c3

Java 数据库 开源 程序员 开发

测试工程师六大能力模型

霍格沃兹测试开发学社

有一说一!你背的MySQL八股文可能存在错误的结论

Java永远的神

MySQL 数据库 程序人生 Java 面试 架构师

【LeetCode】链表组件Java题解

Albert

算法 LeetCode 10月月更

STM32L051测试 (四、Flash和EEPROM的读写)

矜辰所致

EEPROM Flash STM32L051 10月月更

Vue.$nextTick的原理是什么-vue面试进阶

bb_xiaxia1998

Vue

实例分析Scheduled Thread Pool Executor与Timer的区别

华为云开发者联盟

线程 开发 华为云 企业号十月PK榜

MobTech短信验证常见问题

MobTech袤博科技

sdk 短信验证

前端几个常见考察点整理

beifeng1996

React

测试过程中遇到的那些奇葩bug

霍格沃兹测试开发学社

如何从 0 开始学 Python 自动化测试开发(一)

霍格沃兹测试开发学社

视频中为什么需要这么多的颜色空间?

百度Geek说

计算机视觉 音视频 企业号十月 PK 榜

对最近火热的“DevOps已死”的回应

Jianmu

DevOps 敏捷开发 软件危机 自动化运维

Github星标百万!终于有人将Spring技术精髓收录成册

Geek_0c76c3

Java 数据库 开源 程序员 开发

Google 测试总监聊如何经营成功的测试职业生涯

霍格沃兹测试开发学社

Vue是怎样监听数组的变化的?

bb_xiaxia1998

Vue

C#使用Spire.PDF依赖,PDF转Word,Excel,Html代码分析

IC00

C# 学习 程序员 上位机 10月月更

Serverless架构演进与实践

得物技术

云计算 架构 DevOps 云原生 10月月更

告诉你个广告业务存储神器

华为云开发者联盟

数据库 广告 华为云 企业号十月PK榜

给想转Go或者Go进阶同学的一些建议

王中阳Go

php Go web java 编程 #go 10月月更

阿里新一代微服务,内部大佬手抄的笔记+脑图不容错过,全是精华

Geek_0c76c3

Java 数据库 开源 程序员 开发

厉害!年薪近百万架构师保姆式讲解的这份Spring源码笔记,绝绝子

Geek_0c76c3

Java 数据库 开源 程序员 开发

Springboot使用 prometheus监控

琦彦

监控 Prometheus springboot 10月月更

字节算法大神手写的算法笔记,曾连续多次霸榜 GitHub Trending 首页

Geek_0c76c3

Java 数据库 开源 程序员 开发

防窜货管理如何更智能? VoneTracer新功能给你答案!

旺链科技

区块链 溯源 产业区块链

DAM的神奇力量,看亚马逊云科技如何助力特赞以内容驱动增长?

Lily

扬帆出海,亚马逊云科技如何助力神策数据从0到1快速布局独立站出单?

Lily

数据结构学习,链式队列

IC00

学习 数据结构 算法 学习笔记 10月月更

vue面试常见考察点总结

bb_xiaxia1998

Vue

MySQL持续霸榜,《高性能MySQL》第4版追新巨献!

博文视点Broadview

验证 HTTPS 请求的证书(五)_文化 & 方法_Draveness_InfoQ精选文章