写点什么

验证 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:471521

评论

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

2023-02-11:给你两个整数 m 和 n 。构造一个 m x n 的网格,其中每个单元格最开始是白色, 请你用 红、绿、蓝 三种颜色为每个单元格涂色。所有单元格都需要被涂色, 涂色方案需要满足:

福大大架构师每日一题

算法 rust 福大大

架构训练营 - 模块四作业

Sam

架构实战营

软件测试/测试开发 | Web测试方法与技术实战演练

测试人

软件测试 自动化测试 测试开发 Web自动化测试

开源ChatGPT要来了;软件2.0智能革命;GLM、Diffusion模型大加速

OneFlow

人工智能 深度学习

运维训练营第十三课作业

好吃不贵

springboot自动配置原理

喝水不抬头

软件测试/测试开发 | Web测试方法与技术之CSS讲解

测试人

软件测试 自动化测试 测试开发 Web自动化测试 web测试

Python 发展趋势:与 Rust 深度融合、更易于编写 Web 应用

Python猫

Python

New Bing 内测,革搜索的命

冯骐

openai GPT ChatGPT New Bing 对话模型

使用开源实时监控系统 HertzBeat 5分钟搞定对 Mysql 数据库监控告警

TanCloud探云

Java 数据库 GitHub 开源 数据库监控

Dromara HertzBeat 开源社区新晋两位 Committer

TanCloud探云

Java GitHub 开源 后端 开源社区

ChatGPT十问十答 | 图解ChatGPT

涛哥 数字产品和业务架构

人工智能 ChatGPT

设计模式-值类型与引用类型、深拷贝与浅拷贝、原型模式详解

C++后台开发

数据结构 设计模式 后端开发 Linux服务器开发 C++开发

状态机设计举例

timerring

FPGA

ThreadLocal源码分析及避坑指南

喝水不抬头

怎样做新人培训

Joseph295

四点原因,Zoom裁员15%,视频会议甜蜜期结束

B Impact

【SpringBoot】SpringBoot常用注解

No8g攻城狮

Spring Boot 2 #面试

软件测试/测试开发 | Web自动化之Selenium安装

测试人

软件测试 自动化测试 测试开发 Web自动化测试 selenium

2023最新Python阅读书籍推荐

kcodez

Python

软件测试/测试开发 | Selenium 测试用例编写

测试人

软件测试 自动化测试 测试开发 Web自动化测试 selenium

C++ 智能指针(一) std::auto_ptr

orbitgw

c++ C++ STL

支撑MVP,架构师需要做什么

agnostic

MVP

极客时间架构训练营模块七-王者荣耀商城异地多活架构设计

张Dave

大家心心念念的RocketMQ5.x入门手册来喽

中间件兴趣圈

RocketMQ rocketmq5

软件测试/测试开发 | SeleniumIDE用例录制

测试人

软件测试 自动化测试 测试开发 Web自动化测试 selenium

免费的苹果手机投屏到电脑mac软件AIrserver7

茶色酒

AIrserver7

湖仓一体电商项目(二十):业务实现之编写写入DM层业务代码

Lansonli

湖仓一体电商项目

NodeJS 实战系列:DevOps 尚未解决的问题

光毅

DevOps nodejs

如何做总架构师

agnostic

总架构

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