10 月 23 - 25 日,QCon 上海站即将召开,现在购票,享9折优惠 了解详情
写点什么

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

评论

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

干货分享丨从MPG 线程模型,探讨Go语言的并发程序

华为云开发者联盟

并发 channel goroutines MPG 线程 Go 语言

如何通过 Serverless 提高 Java 微服务治理效率?

阿里巴巴云原生

Java Serverless 容器 微服务 云原生

AI不仅可以把李焕英带回2021,还能告诉你贾玲更像爸爸还是妈妈

京东科技开发者

人工智能 语音识别 语音合成

京东云新一代自研云服务器 4 月上线;COLING 2020丨面向机器阅读理解的双向认知思维网络

京东科技开发者

人工智能 开发者 云服务器

低代码开发平台解决方案之“金融服务行业”篇

优秀

低代码

PT100热电阻温度阻值对应表

不脱发的程序猿

数据分析 28天写作 PT100 3月日更 温度传感器

当跨国企业女职业经理人遇上创业女 CEO,两者会擦出什么样的火花?

科技新消息

如何凝聚党员力量?智慧组工系统构架组织部管理平台解决方案

源中瑞-龙先生

解决方案 党员 智慧组工

2021最新发布拼多多/字节/360/网易/面经总结

比伯

Java 编程 程序员 架构 面试

小鼎量化交易系统开发|小鼎炒币机器人软件APP开发

系统开发

BFAI量化交易系统开发|BFAI炒币机器人APP软件开发

系统开发

《谷歌是如何运营的》-读书笔记

曦语

读书笔记

如果延迟退休势在必行,区块链如何助力“养老助老”?

旺链科技

产业区块链

【LeetCode】删除字符串中的所有相邻重复项Java题解

Albert

算法 LeetCode 28天写作 3月日更

LinqToExcel.Extend 源码分析 第二波

happlyfox

28天写作 3月日更

深度分析前端构建工具:Vite2 v.s Snowpack3 v.s. Webpack5

智联大前端

vite webpack 构建工具

区块链电子合同签署平台,区块链电子存证

13530558032

书单|互联网企业面试案头书之程序员技术篇

博文视点Broadview

这操作真香!一线互联网企业高级Android工程师面试题大全,面试真题解析

欢喜学安卓

android 程序员 面试 移动开发

AI辅助宫颈癌筛查技术全球居首,守护者的力量来源是?

华为云开发者联盟

AI 华为云 目标检测 宫颈癌

盘点 HashMap 的实现原理及面试题

老王说编程

Java hashmap HashMap底层原理

从产品经理到产品架构师

博文视点Broadview

寻找被遗忘的勇气(九)

Changing Lin

3月日更

为什么我们开发 San 项目时要用 CLI?

百度Geek说

service SLI san command

区块链+版权-助力电子微版权保护

13530558032

JVM笔记 -- JVM的生命周期介绍

秦怀杂货店

JVM 生命周期

USB2.0 扩展器(一拖四)原理图、PCB,可打样使用

不脱发的程序猿

28天写作 电路设计 USB电路 USB转TTL 3月日更

能源管理可视化破冰而出,数字孪生打破传统运维僵局

一只数据鲸鱼

物联网 数据可视化 3D可视化 能源管理 智慧电厂

中国人工智能,赏花更要寻根

脑极体

Java面试热门技术框架:Spring Security Oauth2.0认证授权

Java架构追梦

Java spring 面试 金三银四跳槽

面试看这个就够了!最新BAT大厂面试者整理的Android面试题目模板,先收藏了

欢喜学安卓

android 程序员 面试 移动开发

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