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

19

这是一个旨在为我的特定用例寻找解决方案并记录我尝试做的事情以供其他遵循此过程的人参考的问题。

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

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

rootCA.pem rootCA.key server.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完成此操作。

然而,仅使用app bundle中的rootCA似乎行不通。是因为根CA没有足够的关于公钥的信息来验证使用根CA签名的服务器证书吗?服务器.crt文件可能还具有变化的CommonName。

此外,由于我的SSL术语流畅程度非常基础,如果有人能澄清我是否提出了正确的问题,那将是很好的。具体问题如下:

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

1
你正在使用Xcode7/iOS9或者是Xcode6/iOS8进行应用程序开发吗?如果是前者,请注意现在连接服务器的规则变得更加严格了,如果SSL加密套件被认为不够强,则连接会在系统级别被中断。这被称为ATS(App Transport Security),你可以在https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/上找到更多相关信息。 - jcayzac
@jcayzac,目前正试图使其在Xcode 6 / iOS 8上运行。我们尚未对iOS 9做过太多的工作。是否建议直接切换到通过iOS 9实施事物?即使我们的用户群尚未更新,它会起作用吗? - mitrenegade
我的建议是永远不要使用AFNetworking...但这只是我的建议。 - TheCodingArt
有更好的替代品吗?看起来AFNetworking是一个被广泛接受且维护得很好的库。可能比我不费力气写出来的任何东西都要好。 - mitrenegade
3个回答

9
在使用各种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的知识。

  1. AFNetworking实现的setSessionDidReceiveAuthenticationChallengeBlock方法具有以下签名:

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

credential参数是一个需要分配值的引用/入出变量。在Swift中,它看起来像这样:AutoreleasingUnsafeMutablePointer。为了在C中将某些内容分配给它,您需要执行以下操作:

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

在Swift中,它看起来像这样:(来自将NSArray转换为RLMArray与RKValueTransFormer失败将outputValue转换为AutoreleasingUnsafeMutablePointer<AnyObject?>
credential.memory = NSURLCredential(trust: trust)

  1. SecPolicyCreateSSL、SecCertificateCreateWithData 和 SecTrustGetCertificateAtIndex 返回 Unmanaged! 对象,你需要使用 takeRetainedValue() 或 takeUnretainedValue() 进行转换/桥接。 (参见 http://nshipster.com/unmanaged/)。我们在使用 takeRetainedValue() 并多次调用该方法时遇到了内存问题/崩溃(在 SecDestroy 上崩溃)。现在我们切换到使用 takeUnretainedValue(),因为在验证之后你不需要证书或 SSL 策略,所以构建似乎是稳定的。

  2. 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生成和签名的叶子证书。应用程序包仅包含根CA,因此可以在服务器证书过期后循环使用,而现有应用程序不会失败。
如果我进一步深入了解AFNetworking并找出一个一到三行的解决方案(通过切换它们提供的所有小标志),我也会发布更新。
如果AlamoFire也开始支持SSL,请在此处发布解决方案。

3

如果您正在使用CocoPods,则应继承AFSecurityPolicy类,并根据mitrenegade的答案实现安全检查https://dev59.com/B1wY5IYBdhLWcg3wwqA5#32469609

这是我的代码。

在发布请求时初始化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

1

我曾遇到同样的问题,并通过在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)以及从证书中获取公钥的帮助方法:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接