自签名服务器根 CA 的 TLS 验证的 AFNetworking 问题

Posted

技术标签:

【中文标题】自签名服务器根 CA 的 TLS 验证的 AFNetworking 问题【英文标题】:AFNetworking problems with TLS Verification of a self signed server root CA 【发布时间】:2015-08-27 19:55:12 【问题描述】:

这个问题试图为我的特定用例找到解决方案,并记录我为遵循此过程的其他人所做的尝试。

我们有一个 RESTful 服务器和一个 ios 应用程序。我们有自己的证书颁发机构,服务器有根证书颁发机构和自签名证书。我们按照这个过程生成了以下文件:

http://datacenteroverlords.com/2012/03/01/creating-your-own-ssl-certificate-authority/

rootCA.pem 根CA.key 服务器.crt server.key

只有服务器证书存储在我们的服务器上,作为 SSL 过程的一部分,公钥与 API 调用一起发送以进行验证。

我已按照此流程使用 AFNetworking 来使用证书固定和公钥固定来验证我们的自签名证书:

http://initwithfunk.com/blog/2014/03/12/afnetworking-ssl-pinning-with-self-signed-certificates/

我们根据本指南将 .crt 文件转换为 .cer 文件(DER 格式):

https://support.ssl.com/Knowledgebase/Article/View/19/0/der-vs-crt-vs-cer-vs-pem-certificates-and-how-to-convert-them

并将 .cer 文件 (server.cer) 包含在 iOS 应用程序包中。这成功地允许我们的应用程序向我们的服务器发出 GET/POST 请求。但是,由于我们的服务器证书可能会过期或重新颁发,因此我们希望使用根 CA,就像 AFNetworking 上的此线程中的人们所做的那样:

https://github.com/AFNetworking/AFNetworking/issues/1944

目前我们已经更新到 AFNetworking 2.6.0,所以我们的网络库肯定应该包含所有更新,包括在这个讨论中的更新:

https://github.com/AFNetworking/AFNetworking/issues/2744

用于创建我们的安全策略的代码:

    var manager: AFHTTPRequestOperationManager = AFHTTPRequestOperationManager()
    manager.requestSerializer = AFJSONRequestSerializer() // force serializer to use JSON encoding

    let policy: AFSecurityPolicy = AFSecurityPolicy(pinningMode: AFSSLPinningMode.PublicKey)
    var data: [NSData] = [NSData]()
    for name: String in ["rootCA", "server"] 
        let path: String? = NSBundle.mainBundle().pathForResource(name, ofType: "cer")
        let keyData: NSData = NSData(contentsOfFile: path!)!
        data.append(keyData)
    
    policy.pinnedCertificates = data
    policy.allowInvalidCertificates = true 
    policy.validatesDomainName = false 
    manager.securityPolicy = policy

包含 server.cer 后,我们可以通过固定公钥来信任我们的服务器(也尝试过 AFSecurityPolicyPinningMode.Certificate);这很有效,因为包含了确切的证书。但是,因为我们可能会更改服务器拥有的 server.crt 文件,所以我们希望能够仅使用 rootCA.cer 来完成。

但是,只有 rootCA 包含在应用程序包中,这似乎不起作用。是不是 rootCA 没有足够的关于公钥的信息来验证用根 CA 签名的服务器证书? server.crt 文件的 CommonName 也可能会发生变化。

另外,由于我对 SSL 术语的流利程度非常原始,如果有人能澄清我是否提出了正确的问题,那就太好了。具体问题是:

    我是否正确生成了证书,以便服务器可以使用自签名的 server.crt 文件证明其身份? 是否可以仅将 rootCA.cer 文件包含到捆绑包中并能够验证叶证书 server.crt?它是否能够验证由同一个 rootCA 签名的另一个 server2.crt 文件?或者我们应该在 rootCA 和叶子之间包含一个中间证书? 公钥固定或证书固定是解决此问题的正确方法吗?我读过的每个论坛和博客文章都说是的,但即使使用最新的 AFNetworking 库,我们也没有任何运气。 服务器是否需要以某种方式同时发送 server.crt 和 roomCA.pem 签名?

【问题讨论】:

您是使用 Xcode7/iOS9 还是使用 Xcode6/iOS8 构建应用程序?如果是前者,请注意连接到服务器的规则现在更加严格,如果 SSL 密码套件被认为不够强大,连接会在系统级别被丢弃。它被称为 ATS(应用程序传输安全),您可以在 developer.apple.com/library/prerelease/ios/technotes/… 找到更多信息 @jcayzac,目前正在尝试使其适用于 xcode 6/ios8。我们还没有对 ios9 做太多事情。是否建议只切换到通过 ios9 实现事物?即使我们的用户群没有更新,它还能工作吗? 我的建议是永远不要使用 AFNetworking...但这只是我的建议 有更好的选择吗?似乎 AFNetworking 是一个被广泛接受的库,并且维护得很好。可能比我无需投入大量工作就能写出的任何东西都要好。 【参考方案1】:

在大量不同 SSL 资源的帮助下,我找到了启用使用自签名证书来验证启用了 SSL 的私人服务器的解决方案。我也对 SSL、现有的 iOS 解决方案以及导致它无法在我的系统中运行的每个问题的小问题有了更好的理解。我将尝试概述我的解决方案中包含的所有资源,以及哪些小事有所作为。

我们仍在使用 AFNetworking,目前它是 2.6.0,据说包含证书固定。这是我们问题的根源。我们无法验证我们的私有服务器的身份,该服务器正在发送由自签名 CA 根签名的叶证书。在我们的 iOS 应用程序中,我们捆绑了自签名根证书,然后由 AFNetworking 将其设置为受信任的锚点。但是,由于服务器是本地服务器(我们的产品中包含硬件),IP 地址是动态的,因此 AFNetworking 的证书验证失败,因为我们无法禁用 IP 检查。

为了找到答案的根源,我们使用 AFHTTPSessionManager 来实现自定义 sessionDidReceiveAuthenticationChallengeCallback。 (参见:https://gist.github.com/r00m/e450b8b391a4bf312966)在该回调中,我们使用不检查主机名的 SecPolicy 验证服务器证书;请参阅http://blog.roderickmann.org/2013/05/validating-a-self-signed-ssl-certificate-in-ios-and-os-x-against-a-changing-host-name/,这是 NSURLConnection 而不是 NSURLSession 的旧实现。

代码:

创建一个 AFHTTPSessionManager

    var manager: AFHTTPSessionManager = AFHTTPSessionManager()
    manager.requestSerializer = AFJSONRequestSerializer() // force serializer to use JSON encoding
    manager.setSessionDidReceiveAuthenticationChallengeBlock  (session, challenge, credential) -> NSURLSessionAuthChallengeDisposition in

        if self.shouldTrustProtectionSpace(challenge, credential: credential) 
            // shouldTrustProtectionSpace will evaluate the challenge using bundled certificates, and set a value into credential if it succeeds
            return NSURLSessionAuthChallengeDisposition.UseCredential
        
        return NSURLSessionAuthChallengeDisposition.PerformDefaultHandling
    

自定义验证的实现

class func shouldTrustProtectionSpace(challenge: NSURLAuthenticationChallenge, var credential: AutoreleasingUnsafeMutablePointer<NSURLCredential?>) -> Bool 
    // note: credential is a reference; any created credential should be sent back using credential.memory

    let protectionSpace: NSURLProtectionSpace = challenge.protectionSpace
    var trust: SecTrustRef = protectionSpace.serverTrust!

    // load the root CA bundled with the app
    let certPath: String? = NSBundle.mainBundle().pathForResource("rootCA", ofType: "cer")
    if certPath == nil 
        println("Certificate does not exist!")
        return false
    

    let certData: NSData = NSData(contentsOfFile: certPath!)!
    let cert: SecCertificateRef? = SecCertificateCreateWithData(kCFAllocatorDefault, certData).takeUnretainedValue()

    if cert == nil 
        println("Certificate data could not be loaded. DER format?")
        return false
    

    // create a policy that ignores hostname
    let domain: CFString? = nil
    let policy:SecPolicy = SecPolicyCreateSSL(1, domain).takeRetainedValue() 

    // takes all certificates from existing trust
    let numCerts = SecTrustGetCertificateCount(trust)
    var certs: [SecCertificateRef] = [SecCertificateRef]()
    for var i = 0; i < numCerts; i++ 
        let c: SecCertificateRef? = SecTrustGetCertificateAtIndex(trust, i).takeUnretainedValue()
        certs.append(c!)
    

    // and adds them to the new policy
    var newTrust: Unmanaged<SecTrust>? = nil
    var err: OSStatus = SecTrustCreateWithCertificates(certs, policy, &newTrust)
    if err != noErr 
        println("Could not create trust")
    
    trust = newTrust!.takeUnretainedValue() // replace old trust

    // set root cert
    let rootCerts: [AnyObject] = [cert!]
    err = SecTrustSetAnchorCertificates(trust, rootCerts)

    // evaluate the certificate and product a trustResult
    var trustResult: SecTrustResultType = SecTrustResultType()
    SecTrustEvaluate(trust, &trustResult)

    if Int(trustResult) == Int(kSecTrustResultProceed) || Int(trustResult) == Int(kSecTrustResultUnspecified) 
        // create the credential to be used
        credential.memory = NSURLCredential(trust: trust)
        return true
    
    return false

在阅读这段代码时,我学到了一些关于 swift 的知识。

    AFNetworking 的 setSessionDidReceiveAuthenticationChallengeBlock 实现有这个签名:

    (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __nullable __autoreleasing * __nullable credential))block;

凭据参数是需要分配的引用/输入变量。很快它看起来像这样:AutoreleasingUnsafeMutablePointer。为了在 C 中为它分配一些东西,你会做这样的事情:

*credential = [[NSURLCredential alloc] initWithTrust...];

在swift中,它看起来像这样:(来自converting NSArray to RLMArray with RKValueTransFormer fails converting outputValue to AutoreleasingUnsafeMutablePointer<AnyObject?>)

credential.memory = NSURLCredential(trust: trust)

    SecPolicyCreateSSL、SecCertificateCreateWithData 和 SecTrustGetCertificateAtIndex 返回非托管!对象,您必须使用 takeRetainedValue() 或 takeUnretainedValue() 从本质上转换/桥接它们。 (见http://nshipster.com/unmanaged/)。当我们使用 takeRetainedValue() 并多次调用该方法时,我们遇到了内存问题/崩溃(SecDestroy 发生了崩溃)。现在,在我们改用 takeUnretainedValue() 后,构建看起来很稳定,因为验证后您不需要证书或 ssl 策略。

    TLS 会话缓存。 https://developer.apple.com/library/ios/qa/qa1727/_index.html 这意味着当您成功验证挑战时,您将永远不会再次获得挑战。当你测试一个有效的证书,然后测试一个无效的证书,然后跳过所有的验证,你会从服务器得到一个成功的响应,这真的会让你头疼。解决方案是每次使用有效证书并通过验证挑战后,在 iOS 模拟器中进行 Product->Clean。否则你可能会花一些时间错误地认为你终于得到了根 CA 来验证。

因此,对于我在服务器上遇到的问题,这只是一个可行的解决方案。我想在这里发布所有内容,希望能帮助其他运行本地或开发服务器的人,该服务器具有自签名 CA 和需要启用 SSL 的 iOS 产品。当然,对于 iOS 9 中的 ATS,我预计很快就会再次深入研究 SSL。

此代码目前存在一些内存管理问题,将在不久的将来更新。此外,如果有人看到此实现并说“啊哈哈,这与为无效证书返回 TRUE 一样糟糕”,请告诉我!据我通过我们自己的测试可以看出,该应用程序拒绝未经我们的根 CA 签名的无效服务器证书,并接受由根 CA 生成和签名的叶证书。 app bundle 仅包含根 CA,因此服务器证书过期后可以循环使用,现有应用不会失败。

如果我进一步研究 AFNetworking 并找出解决所有这些问题的一到三行解决方案(通过切换他们提供的所有这些小标志),我也会发布更新。

如果 AlamoFire 开始支持 SSL,请随时在此处发布解决方案。

【讨论】:

【参考方案2】:

如果您使用的是 coco pod,则子类化 AFSecurityPolicy 类并根据 mitrenegade 的回答 https://***.com/a/32469609/4000434 实施安全检查

听到的是我的密码。

在发布请求时初始化 AFHttpRequestOperationManager,如下所示。

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
    manager.securityPolicy = [RootCAAFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    [manager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [manager POST:Domain_Name parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) 
        success(operation,responseObject);
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
     failure:^(AFHTTPRequestOperation *operation, NSError *error) 
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        NSLog(@"Error  %@",error);
        failure(operation,error);
    ];

RootCAAFSecurityPolicy 是 AFSecurityPolicy 类的子类。请参阅下面的 RootCAAFSecurityPolicy .h 和 .m 类 重写方法

-(BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain

RootCAAFSecurityPolicy.h 类

#import <AFNetworking/AFNetworking.h>

@interface RootCAAFSecurityPolicy : AFSecurityPolicy

@end

RootCAAFSecurityPolicy.m 类

将 RootCA 替换为您的证书文件名

#import "RootCAAFSecurityPolicy.h"

@implementation RootCAAFSecurityPolicy
-(BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain

    if(self.SSLPinningMode == AFSSLPinningModeCertificate)
    
        return [self shouldTrustServerTrust:serverTrust];
    
    else
    
        return [super evaluateServerTrust:serverTrust forDomain:domain];
    

- (BOOL)shouldTrustServerTrust:(SecTrustRef)serverTrust

    // load up the bundled root CA
    NSString *certPath = [[NSBundle mainBundle] pathForResource:@"RootCA" ofType:@"der"];

    NSAssert(certPath != nil, @"Specified certificate does not exist!");

    NSData *certData = [[NSData alloc] initWithContentsOfFile:certPath];
    CFDataRef certDataRef = (__bridge_retained CFDataRef)certData;
    SecCertificateRef cert = SecCertificateCreateWithData(NULL, certDataRef);

    NSAssert(cert != NULL, @"Failed to create certificate object. Is the certificate in DER format?");


    // establish a chain of trust anchored on our bundled certificate
    CFArrayRef certArrayRef = CFArrayCreate(NULL, (void *)&cert, 1, NULL);
    OSStatus anchorCertificateStatus = SecTrustSetAnchorCertificates(serverTrust, certArrayRef);

    NSAssert(anchorCertificateStatus == errSecSuccess, @"Failed to specify custom anchor certificate");


    // trust also built-in certificates besides the specified CA
    OSStatus trustBuiltinCertificatesStatus = SecTrustSetAnchorCertificatesOnly(serverTrust, false);

    NSAssert(trustBuiltinCertificatesStatus == errSecSuccess, @"Failed to reenable trusting built-in anchor certificates");


    // verify that trust
    SecTrustResultType trustResult;
    OSStatus evalStatus =  SecTrustEvaluate(serverTrust, &trustResult);

    NSAssert(evalStatus == errSecSuccess, @"Failed to evaluate certificate trust");


    // clean up
    CFRelease(certArrayRef);
    CFRelease(cert);
    CFRelease(certDataRef);


    // did our custom trust chain evaluate successfully
    return (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified);

@end

【讨论】:

【参考方案3】:

我遇到了同样的问题,我通过比较AFURLSessionManagerdidReceiveChallenge 方法中的链的公钥来解决它。

-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler 
        // Get remote certificate
        SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;

        NSMutableArray *policies = [NSMutableArray array];
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef) challenge.protectionSpace.host)];

        SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
        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;
                
            
        

        // The pinnning check
        if (trustedPublicKeyCount > 0) 
            NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
         else 
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);
        
    

这里是pinnedPublicKeys的初始化:

    // Get local certificates
    NSArray *certNames = @[@"root_cert"];
    self.pinnedPublicKeys = [NSMutableSet new];

    for (NSString *certName in certNames) 
        NSString *path = [bundle pathForResource:certName ofType:@"der"];
        NSData *certificate = [NSData dataWithContentsOfFile:path];

        id publicKey = AFPublicKeyForCertificate(certificate);
        if (publicKey) 
            [self.pinnedPublicKeys addObject:publicKey];
        
    

这里是获取密钥信任链的辅助方法(AFPublicKeyTrustChainForServerTrust),比较公钥(AFSecKeyIsEqualToKey)和从证书中获取公钥的方法(AFPublicKeyTrustChainForServerTrust):

static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef 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;
        SecTrustCreateWithCertificates(certificates, policy, &trust);

        SecTrustResultType result;
        SecTrustEvaluate(trust, &result);

        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

        if (trust) 
            CFRelease(trust);
        

        if (certificates) 
            CFRelease(certificates);
        

        continue;
    
    CFRelease(policy);

    return [NSArray arrayWithArray:trustChain];


static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) 
    return [(__bridge id)key1 isEqual:(__bridge id)key2];


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);

    allowedCertificates[0] = allowedCertificate;
    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);

    policy = SecPolicyCreateBasicX509();
    SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust);
    SecTrustEvaluate(allowedTrust, &result);

    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

    if (allowedTrust) 
        CFRelease(allowedTrust);
    

    if (policy) 
        CFRelease(policy);
    

    if (tempCertificates) 
        CFRelease(tempCertificates);
    

    if (allowedCertificate) 
        CFRelease(allowedCertificate);
    

    return allowedPublicKey;

【讨论】:

以上是关于自签名服务器根 CA 的 TLS 验证的 AFNetworking 问题的主要内容,如果未能解决你的问题,请参考以下文章

验证设备的自签名证书

ETCD:TLS

K8S集群tls证书管理

Linux搭建CA服务

如何为Amazon EMR生成trustedCertificates.pem和certificateChain.pem文件?

TLS 客户端是不是需要在信任库中有中间 CA?