AFNetWorking之AFSecurityPolicy

一.告知服务器意图的HTTP方法

①GET: 直接访问URI识别的资源,也就是说根据URI来获取资源。

②POST: 用来传输实体的主体。

③PUT: 用来传输文件。

④HEAD: 用来获取报文首部,和GET方法差不多,只是响应部分不会返回主体内容。

⑤DELETE: 删除文件,和PUT恰恰相反。按照请求的URI来删除指定位置的资源。

⑥OPTIONS: 询问支持的方法,用来查询针对请求URI指定的资源支持的方法。

⑦TRACE: 追踪路径,返回服务器端之前的请求通信环信息。

⑧CONNECT: 要求用隧道协议连接代理,要求在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信。SSL(Secure Sockets Layer)和TLS(Transport Layer Security)就是把通信内容加密后进行隧道传输的。

二.https连接建立流程



使用数字证书可以确保公钥不被冒充。
生成数字证书的流程的如下:
1、持有人将公钥以及身份信息发送给权威机构。
2、权威机构负责对持有人的身份进行验证,确保公钥和持有人的信息准确无误。
3、权威机构使用自己私钥对持有人公钥进行数字签名,生成数字证书。
4、为了确保证书不被篡改,权威机构对数字证书进行hash计算(指纹算法),生成摘要(指纹),使用自己的私钥对摘要进行数字签名,放到数字证书中。
5、对持有人收费。

三.AFSecurityPolicy

AFSecurityPolicy 用来评价通过X.509(数字证书的标准)的数字证书和公钥进行的安全网络连接是否值得信任。在应用内添加SSL证书能够有效的防止中间人的攻击和安全漏洞。强烈建议涉及用户敏感或隐私数据或金融信息的应用全部网络连接都采用使用SSL的HTTPS连接。
可以从Bundle中获取证书,获取指定的安全协议,评估服务器信任。
分为三个安全等级

#import <Foundation/Foundation.h>
#import <Security/Security.h>

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,//代表不使用固定证书(本地)验证服务器,直接从客户端系统中受信任的颁发机构CA列表中去验证
    AFSSLPinningModePublicKey,//代表会对服务器返回的证书中的PublicKey进行验证,通过则通过,否则不通过
    AFSSLPinningModeCertificate,//代表会对服务器返回的证书同本地证书全部进行校验,通过则通过,否则不通过
};

默认是AFSSLPinningModeNone,一般要防止中间人攻击,可以选用第三种AFSSLPinningModeCertificate

@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;

这个属性保存着所有的可用做校验的证书的集合。AFNetworking默认会搜索工程中所有.cer的证书文件。如果想制定某些证书,可使用certificatesInBundle在目标路径下加载证书,然后调用policyWithPinningMode:withPinnedCertificates创建一个本类对象。

注意: 只要在证书集合中任何一个校验通过,evaluateServerTrust:forDomain: 就会返回true,即通过校验。

@property (nonatomic, assign) BOOL allowInvalidCertificates;
//默认是NO。不允许过期或无效的证书
@property (nonatomic, assign) BOOL validatesDomainName;
//默认是YES。需要验证证书中的域名
+ (NSSet <NSData *> *)certificatesInBundle:(NSBundle *)bundle;

返回指定bundle中的证书。如果使用AFNetworking的证书验证 ,就必须实现此方法,并且使用policyWithPinningMode:withPinnedCertificates 方法来创建实例对象。

AFNetworking中挑战认证的方法

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler;
//这个方法在NSURLSessionDelegate中

函数说明: 服务器端在接受到用户的请求时,某些时候会需要验证客户段是否是正常用户(通常是在涉及到金钱交易和用户个人信息的时候),在决定是否返回数据。这种情况就是挑战认证,即双方进行公钥和私钥的验证。 (NSURLAuthenticationChallenge *challenge)。接收到挑战后,客户端要根据服务端传来的challenge来生成completionHandler所需的NSURLSessionAuthChallengeDisposition disposition和NSURLCredential *credential(disposition指定应对这个挑战的方法,而credential是客户端生成的挑战证书,注意只有challenge中认证方法为NSURLAuthenticationMethodServerTrust的时候,才需要生成挑战证书)。最后调用completionHandler回应服务器端的挑战

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    /**
     挑战认证的处理方式(枚举类型有三种)
     NSURLSessionAuthChallengeUseCredential  采用指定的证书(证书可能为nil)
     NSURLSessionAuthChallengePerformDefaultHandling  采用默认的处理方式 (似乎这个代理没有被实现,证书参数被忽视(即,没有证书))
     NSURLSessionAuthChallengeCancelAuthenticationChallenge 取消挑战认证
     NSURLSessionAuthChallengeRejectProtectionSpace 这一次的挑战被拒绝,下一次在进行尝试,忽略这一次的证书
     */
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
// sessionDidReceiveAuthenticationChallenge自定义的应对服务器端挑战认证的方法
    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
//        NSURLAuthenticationMethodServerTrust 挑战认证方法(NSString)
//        也就是说服务器端需要客户端返回一个根据认证挑战的保护空间提供的信任(即challenge.protectionSpace.serverTrust)产生的挑战证书。
//        而这个证书就需要使用credentialForTrust:来创建一个NSURLCredential对象
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            // 基于客户端的安全策略来决定是否信任该服务器,不信任的话,也就没必要响应挑战(具体的判断方法后面会有详细解释)
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
//              创建挑战证书(挑战方式为UseCredential和PerformDefaultHandling都需要新建挑战证书)
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
//                确定挑战的方式
                if (credential) {
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
//                取消挑战
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
//              完成挑战
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

验证证书是否正确的方法

//该方法在AFSecurityPolicy中
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /**
        self.allowInvalidCertificates 是否允许使用自建证书
     self.validatesDomainName   验证domain是否有效
     pinnedCertificates 就是在客户端保存服务器端颁发的证书
     AFSSLPinningModeNone 表示你不使用SSL pinning 只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书
     这种情况表示,你即想验证自己的CA证书,但是并没有服务器设定的证书([self.pinnedCertificates count] == 0)或者 你只是像浏览器一下返回官方颁布的认证(self.SSLPinningMode == AFSSLPinningModeNone)这和你的需求是不符的,所以不能通过验证
     */
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        // https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
//        根据文档,你应该只相信自己提供的证书(自建证书,服务器端创建或者构造的CA证书,非官方颁布)
        //  According to the docs, you should only trust your provided certs for evaluation.
//        固定的证书添加信任,没有固定的证书,就不能通过验证
        //  Pinned certificates are added to the trust. Without pinned certificates,
        //  there is nothing to evaluate against.
        //
        //  From Apple Docs:
//        不要使用系统颁发的证书
        //          "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
//        添加自己CA证书作为锚点证书
        //           Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
        return NO;
    }
//          证书的策略
    NSMutableArray *policies = [NSMutableArray array];
//    验证domain 使用SecPolicyCreateSSL函数创建验证策略,其中第一个参数为true表示验证整个SSL证书链,第二个参数传入domain,用于判断整个证书链上需要验证的节点表示的那个domain是否和此处传入domain一致
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
//        如果不需要验证domain,使用默认的BasicX509验证策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
//        为serverTrust添加验证策略,即告诉服务端如何验证策略
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
//           如果SSLPinningMode为 AFSSLPinningModeNone,表示你不使用SSL pinning,但是我允许自建证书,那么返回YES,或者使用AFServerTrustIsValid函数看看serverTrust是否可信任,如果信任,也返回YES
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
        //        不允许自建证书,但是使用AFServerTrustIsValid验证返回的NO,所以不通过验证
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }
//      SSLPinningMode证书的验证方式
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone: //上面已经针对这个情况(即:自建证书)进行了判断,能通过验证的都已经返回,补充判断
        default:
            return NO;
            // 这个模式表示用证书绑定(SSL Pinning)方式验证证书,需要客户端保存有服务端的证书拷贝
            // 注意客户端保存的证书存放在self.pinnedCertificates中
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
//              这里使用SecCertificateCreateWithData函数对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
// 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }
//          服务器端的证书链,注意此处返回的证书链顺序是从叶节点到根节点
//            获得链式验证后,固定证书应该包含在过去的位置(例如它的根CA)
            // 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);
//            从服务器端证书链的根节点往下遍历,看看是否有与客户端的绑定证书一致的,有的话,就说明服务器端是可信的。因为遍历顺序正好相反,所以使用reverseObjectEnumerator
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }

            return NO;
        }
//            AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,客户端要有服务端的证书拷贝,只验证证书里的公钥,不验证证书的有效期等信息
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
//            取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
//            依次遍历公钥,如果和客户端绑定证书的公钥一致,那么就给trustedPublicKeyCount加一
            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
//            如果>0说明通过过验证
            return trustedPublicKeyCount > 0;
        }
    }

    return NO;
}

这里面涉及到了AFNetworking封装的一些,验证证书的方法,下面拿出一些比较重要的方法进行介绍

方法一:

static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;//证书是否有效,默认为NO
    SecTrustResultType result;
    /**
         __Require(<#assertion#>, <#exceptionLabel#>)
     errorCode 代表SecTrustEvaluate函数的返回值
     此函数的含义为,errorCode不为0则开始下一步执行,此处的_out为,退出判断函数
     #ifndef __Require_noErr_Quiet
     #define __Require_noErr_Quiet(errorCode, exceptionLabel)                      \
     do                                                                          \
     {                                                                           \
     if ( __builtin_expect(0 != (errorCode), 0) )                            \
     {                                                                       \
              goto exceptionLabel;                                                \
     }                                                                       \
     } while ( 0 )
     #endif
     */
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
//  如果通过SecTrustEvaluate验证,则根据result再次进行验证
//    SecTrustResultType result枚举值
//    kSecTrustResultUnspecified 评估成功,此证书也被默认信任
//    kSecTrustResultProceed 评估成功,用户认定信任此证书
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out:
    return isValid;
}

方法二:

//取出serverTrust中证书链上每个证书的公钥,并返回对应的该组公钥

static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
//    获取到serverTrust中所有的证书
    SecPolicyRef policy = SecPolicyCreateBasicX509();
//    获取证书的数量
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
//    遍历证书,取出公钥
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);

        SecCertificateRef someCertificates[] = {certificate};
        CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);

        SecTrustRef trust;
//        生成trust对象
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
//        验证trust对象
        SecTrustResultType result;
        __Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
//        如果trust对象通过验证(格式复合X509证书格式)获取trust的公钥,并添加到trustChain中
        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

    _out://验证失败
//        释放相应对象
        if (trust) {
            CFRelease(trust);
        }

        if (certificates) {
            CFRelease(certificates);
        }

        continue;
    }
    CFRelease(policy);
//    返回公钥数组
    return [NSArray arrayWithArray:trustChain];
}

参考:https://www.cnblogs.com/hthf/p/4986507.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容