iOS - HTTPS自制证书验证

证书类型

  • 从权威认证机构购买的证书
    • 优点:服务端如果使用的是这类证书的话,那么客户端一般不需要做什么,直接使用HTTPS请求就行了,苹果内置了那些受信任的根证书
    • 缺点:需要花钱
  • 自制证书
    • 优点:无需花钱
    • 缺点:这类证书是不受信任的,因此需要我们在代码中将自制证书设置为可信任证书

自己实现自制证书验证

  • 这里以NSURLSession请求为例演示下如何验证自制证书
  • 实现NSURLSessionDelegate的代理方法-URLSession:didReceiveChallenge:completionHandler:
  • 只要访问的是HTTPS请求就会调用此代理方法
  • 此代理方法的作用:对自制证书的验证
  • 代理方法的各个参数介绍
    • challenge:挑战、质问 (包含了受保护的空间)
    • completionHandler:处理自制证书,该block的两个参数的含义:disposition表示证书处理方式,credential表示需要处理的证书
    • disposition类型介绍
    NSURLSessionAuthChallengeUseCredential = 0,                  使用该证书,安装该证书
    NSURLSessionAuthChallengePerformDefaultHandling = 1,         默认方式,证书被忽略
    NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2,  取消请求,证书被忽略
    NSURLSessionAuthChallengeRejectProtectionSpace = 3,          拒绝本次请求
    
    • 证书认证结果SecTrustResultType类型介绍
    -------以下认证结果表示可信证书---------
    // 证书通过验证,是由于用户有操作设置了证书被信任,如:在弹出的是否信任的alert框中选择always trust
    // 一般是自制证书,非权威机构颁发的,如:Charles的代理证书
    kSecTrustResultProceed = 1,
    // 证书通过验证,用户没有设置这些证书是否被信任
    // 一般验证权威机构颁发的CA证书会返回这个结果
    kSecTrustResultUnspecified = 4,
    ------------------------------------
    
    -------以下认证结果表示不可信证书-------
    // 无效证书
    kSecTrustResultInvalid = 0,
    // 待确认证书              
    kSecTrustResultConfirm = 2,   
    // 被拒证书                   
    kSecTrustResultDeny = 3,      
    // 不可信证书      
    kSecTrustResultRecoverableTrustFailure = 5,       
    // 严重不可信证书
    kSecTrustResultFatalTrustFailure = 6, 
    // 其他错误       
    kSecTrustResultOtherError = 7
    
    • 受保护空间参数介绍
    // 认证方式
    challenge.protectionSpace.authenticationMethod
    
    // 返回的服务端证书
    // 如果authenticationMethod不是NSURLAuthenticationMethodServerTrust,那么serverTrust为nil
    challenge.protectionSpace.serverTrust
    
  • 自制证书认证步骤
    • 首先判断认证方式,是否为NSURLAuthenticationMethodServerTrust
    • 获取服务端返回的自制证书challenge.protectionSpace.serverTrust
    • 从app的资源文件中获取内置的本地证书(可以是多个),并设置为服务端证书的根证书,设置后会屏蔽掉系统的证书列表,当然可以通过SecTrustSetAnchorCertificatesOnly方法(第二个参数设置为NO)来使系统的证书列表继续起作用
    • 在设置的证书列表中验证服务端自制证书是否可信
    • 完成验证后通过回调告知系统如何处理自制证书
  • 代码实现:这里使用的do-while编程思想不错,避免了太多的if-else
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    // 证书的处理方式
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
    // 被处理的证书
    NSURLCredential *credential = nil;

    do
    {
        // 1、校验认证方式是否为NSURLAuthenticationMethodServerTrust
        if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
            break; /* failed */

        // 2、获取服务端返回的证书
        SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;

        // 3、从app的资源文件中获取内置的本地证书(可以是多个,这里只演示只有一个内置证书)
        // custom是你证书的名称,记得替换
        NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"custom" ofType:@"cer"];
        NSData *caCert = [NSData dataWithContentsOfFile:cerPath];
        if(nil != caCert) {
            // 创建证书
            SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
//            NSCAssert(caRef != nil, @"caRef is nil");
            if(nil == caRef)
                break; /* failed */

            // 添加自制证书,这里表明了可以添加多张自制证书
            NSArray *caArray = @[(__bridge id)(caRef)];
//            NSCAssert(caArray != nil, @"caArray is nil");
            if(nil == caArray)
                break; /* failed */

            // 将内置的本地证书列表设置为服务端证书的根证书
            // 设置可信任证书列表,设置后就只会在设置的证书列表中进行验证,屏蔽掉系统的证书列表
            OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
            // 要使系统的证书列表继续起作用可以调用此方法,第二个参数设置成NO即可
            SecTrustSetAnchorCertificatesOnly(serverTrust, NO);
//            NSCAssert(errSecSuccess == status, @"SecTrustSetAnchorCertificates failed");
            if(errSecSuccess != status)// errSecSuccess表示没有错误
                break; /* failed */
        }

        // 4、使用本地导入的证书列表验证服务器的证书是否可信
        SecTrustResultType result = -1;
        OSStatus status = SecTrustEvaluate(serverTrust, &result);
        if(errSecSuccess != status)
            break; /* failed */
        // kSecTrustResultUnspecified and kSecTrustResultProceed are success
        BOOL allowConnect = (result == kSecTrustResultUnspecified)// 证书通过验证,是由于用户没有设置这些证书是否被信任
                            || (result == kSecTrustResultProceed);// 证书通过验证,用户有操作设置了证书被信任
        if (!allowConnect)
            break; /* failed */

        // 5、设置证书的处理方式
        credential = [NSURLCredential credentialForTrust:serverTrust];
        disposition = NSURLSessionAuthChallengeUseCredential;
    } while(0);

    // 6、告知系统如何处理自制证书
    if(completionHandler) completionHandler(disposition, credential);
}

通过AFN实现自制证书验证

  • AFSecurityPolicy的三种验证模式
    • AFSSLPinningModeNone只验证证书是否在信任列表中
    • AFSSLPinningModePublicKey先验证证书是否在信任列表中,然后仅验证服务端证书与客户端证书的公钥是否一致
    • AFSSLPinningModeCertificate先验证证书是否在信任列表中,然后对比服务端证书和客户端证书是否一致(不仅限于公钥是否一致)
  • 通过给AFHTTPSessionManager设置回调setSessionDidReceiveAuthenticationChallengeBlock来验证自制证书,然后将内置的本地证书加入到可信任的证书列表中,即可通过证书的校验
  • 代码实现
// 记得设置个AFHTTPSessionManager属性
@property (nonatomic, strong) AFHTTPSessionManager *sessionManager;

- (void)validateCerByAFN {
    _sessionManager = [AFHTTPSessionManager manager];

    // 创建安全策略
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    // 是否允许使用自制证书
    securityPolicy.allowInvalidCertificates = YES;
    // 是否需要验证域名,默认YES
    securityPolicy.validatesDomainName = YES;
    _sessionManager.securityPolicy = securityPolicy;

    // 响应数据序列化格式
    _sessionManager.responseSerializer = [AFJSONResponseSerializer serializer];
    // 设置超时
    _sessionManager.requestSerializer.timeoutInterval = 30.f;
    // 缓存策略
    _sessionManager.requestSerializer.cachePolicy = NSURLRequestReloadIgnoringCacheData;
    // 接受的返回数据类型
    _sessionManager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/html", @"text/json", @"text/plain", @"text/javascript", @"text/xml", @"image/*", nil];
    // 打开状态栏的等待菊花
//    [AFNetworkActivityIndicatorManager sharedManager].enabled = YES;

    __weak typeof(self) weakSelf = self;
    [_sessionManager setSessionDidReceiveAuthenticationChallengeBlock:^NSURLSessionAuthChallengeDisposition(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential *__autoreleasing *_credential)
    {
        // 1、校验认证方式是否为NSURLAuthenticationMethodServerTrust
        if (![challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            return NSURLSessionAuthChallengePerformDefaultHandling;
        }

        // 2、设置自制证书,可以导入多张自制证书(也就是个循环,这里就不演示了)
        // custom是你证书的名称,记得替换
        NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"custom" ofType:@"cer"];
        if(cerPath != nil) {
            NSData *caCert = [NSData dataWithContentsOfFile:cerPath];

            // 将内置的本地证书列表赋值给AFN,便于后期基于此证书列表进行验证
            NSSet *cerArray = [[NSSet alloc] initWithObjects:caCert, nil];
            weakSelf.sessionManager.securityPolicy.pinnedCertificates = cerArray;

            // 创建证书
            SecCertificateRef caRef = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)caCert);
            // 从这个数组可以看出是可以有多张本地自制证书
            NSArray *caArray = @[(__bridge id)(caRef)];
            // 获取服务端返回的证书
            SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
            // 将内置的本地证书列表设置为服务端证书的根证书
            // 设置可信任证书列表,设置后就只会在设置的证书列表中进行验证,屏蔽掉系统的证书列表
            OSStatus status = SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)caArray);
            if(status == errSecSuccess) {// 表名设置成功
                // 要使系统的证书列表继续起作用可以调用此方法,第二个参数设置成NO即可
                SecTrustSetAnchorCertificatesOnly(serverTrust, NO);
            }
        }

        // 证书验证,获取证书的处理方式
        NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        if ([weakSelf.sessionManager.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
            disposition = NSURLSessionAuthChallengeUseCredential;
        } else {
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }

        return disposition;
    }];
}

参考文章

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

推荐阅读更多精彩内容