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;
    }];
}

参考文章

  • iOS使用自签名证书实现HTTPS请求
  • Https在ios客户端的objective-c实现
  • 正确使用AFNetworking的SSL保证网络安全

你可能感兴趣的:(iOS - HTTPS自制证书验证)