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

mit*_*ade 18 ssl ios afnetworking

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

我们有一个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

并在iOS应用包中包含.cer文件(server.cer).这成功地允许我们的应用程序向我们的服务器发出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
Run Code Online (Sandbox Code Playgroud)

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

但是,只有应用程序包中包含rootCA,这似乎不起作用.rootCA是否没有足够的有关公钥的信息来验证与根CA签名的服务器证书?server.crt文件也可能具有更改的CommonName.

此外,由于我对SSL术语的流畅性非常粗糙,如果有人能够澄清我是否在问正确的问题,那就太棒了.具体问题是:

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

mit*_*ade 8

借助一堆不同的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
    }
Run Code Online (Sandbox Code Playgroud)

实现自定义验证

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
}
Run Code Online (Sandbox Code Playgroud)

我在学习这段代码时学到了很多关于swift的知识.

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

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

凭证参数是需要分配的引用/输入变量.在swift中它看起来像这样:AutoreleasingUnsafeMutablePointer.为了在C中为它指定一些东西,你可以这样做:

*credential = [[NSURLCredential alloc] initWithTrust...];
Run Code Online (Sandbox Code Playgroud)

在swift中,它看起来像这样:(从使用RKValueTransFormer将NSArray转换为RLMArray失败,将outputValue转换为AutoreleasingUnsafeMutablePointer <AnyObject?>)

credential.memory = NSURLCredential(trust: trust)
Run Code Online (Sandbox Code Playgroud)
  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,也可以在这里发布解决方案.