iOS支持https自定义证书验证

事件缘起

任性的苹果要求6月1日起所有上架app都需要支持ipv6。尼玛赶紧检查下代码是否能够满足要求,检查是否不兼容IPv6点击这里。结果是一个大大的懵逼啊。项目使用的AFNetworking根本就没升级还停留在2.0阶段,顿时心中策马奔腾啊。果断升级之,本以为很easy,分分钟搞定的事情,结果。。。

问题

AFNetworking 2.0 ->AFNetworking 3.0发生了很大变化,废弃了很多的类的同时,基本构架也发生了很大的变化。
AFNetworking 2.0使用NSURLConnection的基础API。
AFNetworking 3.0现已完全基于NSURLSession的API,这降低了维护的负担,同时支持苹果增强关于NSURLSession提供的任何额外功能。(以后苹果所有基于网络的功能,都会使用NSURLSession来扩展。在NSURLConnection类中也说明 DEPRECATED: The NSURLConnection class should no longer be used. NSURLSession is the replacement for NSURLConnection)为什么要使用NSURLConnection这里一篇文章从性能方面进行了分析

AFNetworking 3.0相对于2.0具体来说

  • 弃用类
AFURLConnectionOperation
AFHTTPRequestOperation
AFHTTPRequestOperationManager
  • 修改的类
    下面的类包含基于NSURLConnection的API的内部实现。他们已经被使用NSURLSession重构:
UIImageView+AFNetworking
UIWebView+AFNetworking
UIButton+AFNetworking

3.0和2.0类对比


iOS支持https自定义证书验证_第1张图片
AFNetworking2.0

iOS支持https自定义证书验证_第2张图片
AFNetworking3.0

检查我们原来的代码实现,发现我们恰恰就使用了废弃的类AFHTTPRequestOperation。我们原来的网络库竟然一直不升级我也是醉了。对此我只能说:让我来。
使用最新的AFURLSessionManager来发送网络请求就是了。

    NSURL *URL = [NSURL URLWithString:MyURL];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
    [request setHTTPMethod:@"POST"];
    [request setTimeoutInterval:10];
    [request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
    
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.HTTPMaximumConnectionsPerHost = 1;
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
    
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {

        if (error) {
            NSLog(@"没有网络--- error---%@", error);
        } else {
            NSLog(@"网络是通的");
            NSDictionary* dict = responseObject;
        }
    }];
    
    [dataTask resume];

发现打印结果:没有网络 NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)

检查Info.plist中退回到http配置

NSAppTransportSecurity

    NSAllowsArbitraryLoads
    

也增加了。why?
结果:原因是苹果的 官方资料 说首先必须要基于TLS 1.2版本协议。然后证书的加密的算法还需要达到SHA256或者更高位的RSA密钥或ECC密钥,如果不符合,请求将被中断并返回nil。原来我们需要验证证书啊。

解决办法

在AFNetworking中,只要通过下面的代码,你就可以使用自签证书来访问HTTPS

    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
    //是否允许不信任的证书(证书无效、证书时间过期)通过验证 ,默认为NO
    securityPolicy.allowInvalidCertificates = YES;

这么做有个问题,就是你无法验证证书是否是你的服务器后端的证书,给中间人攻击留下了漏洞,可以通过重定向路由来分析伪造你的服务器端打开了大门。

AFNetworking是允许内嵌证书的,通过内嵌证书,AFNetworking就通过比对服务器端证书、内嵌的证书、站点域名是否一致来验证连接的服务器是否正确。由于CA证书验证是通过站点域名进行验证的,如果你的服务器后端有绑定的域名,这是最方便的。将你的服务器端证书,如果是pem格式的,用下面的命令转成cer格式

openssl x509 - in <你的服务器证书>.pem -outform der -out test.cer

然后将生成的test.cer文件,如果有自建ca,再加上ca的cer格式证书,引入到app的bundle里,然后在网络请求的地方做如下设置

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModeCertificate];
或者
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES; 

如果app的bundle没有这个证书,会报错:
In order to validate a domain name for self signed certificates, you MUST use pinning.
这个pinning,指的是证书锁定,意思就是只有client包含的证书和服务器的证书一致时,才能通过验证。
查看AFNetworking的源代码,有如下核心方法
- evaluateServerTrust:forDomain:详情参考这里

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    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
        //  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).
        //           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];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case 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;
        }
        case 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;
        }
    }
    
    return NO;
}

关于签名证书:AFnetworking默认会去程序中寻找所有cer文件,并找符合要求的。当然我们也可以指定的cer文件名称:

    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.HTTPMaximumConnectionsPerHost = 1;
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
    
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode: AFSSLPinningModeCertificate];
    NSString *certificatePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"cer"];
    NSData *certificateData = [NSData dataWithContentsOfFile:certificatePath];
    
    NSSet *certificateSet  = [[NSSet alloc] initWithObjects:certificateData, nil];
    [securityPolicy setPinnedCertificates:certificateSet];
    securityPolicy.allowInvalidCertificates = YES;
    securityPolicy.validatesDomainName = NO;
    manager.securityPolicy = securityPolicy;

至此AFSecurityPolicy就只会比对服务器证书和内嵌证书是否一致,不会再验证证书是否和站点域名一致了。
  这么做为什么是安全的?了解HTTPS的人都知道,整个验证体系中,最核心的实际上是服务器的私钥。私钥永远,永远也不会离开服务器,或者以任何形式向外传输。私钥和公钥是配对的,如果事先在客户端预留了公钥,只要服务器端的公钥和预留的公钥一致,实际上就已经可以排除中间人攻击了。

最后

最后完整验证证书代码

    NSURL *URL = [NSURL URLWithString:URL_FRONT];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
    [request setHTTPMethod:@"POST"];
    [request setTimeoutInterval:10];//设置超时
    [request setHTTPBody:[jsonStr dataUsingEncoding:NSUTF8StringEncoding]];
    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;// 设置缓存策略
    
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.HTTPMaximumConnectionsPerHost = 1;
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:config];
    
    // 安全验证
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode: AFSSLPinningModeCertificate];
    NSString *certificatePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"cer"];
    NSData *certificateData = [NSData dataWithContentsOfFile:certificatePath];
    
    NSSet *certificateSet  = [[NSSet alloc] initWithObjects:certificateData, nil];
    [securityPolicy setPinnedCertificates:certificateSet];
    securityPolicy.allowInvalidCertificates = YES;
    securityPolicy.validatesDomainName = NO;
    manager.securityPolicy = securityPolicy;
    
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {

        if (error) {
            NSLog(@"没有网络--- error---%@", error);

        } else {
            NSLog(@"网络是通的");
            NSLog(@"%@ %@", response, responseObject);
        }
        
    }];
    
    [dataTask resume];

如有错误,欢迎指正!

参考:Use AFN request https #3524
https

你可能感兴趣的:(iOS支持https自定义证书验证)