1

We are trying to make an app which will communicate with our multiple servers over HTTPS using NSURLSession, in that we are using our own Root CA which is bundled in our app and ATS fully enabled (NSAllowsArbitraryLoads set to False).

On call of NSURLSession Authentication delegate i.e “URLAuthenticationChallenge” we are setting anchor cert through “SecTrustSetAnchorCertificates” which validates a certificate by verifying its signature plus the signatures of the certificates in its certificate chain, up to the anchor certificate. Also used SecTrustSetAnchorCertificatesOnly to exclude other anchors.

After successful execution of SecTrustEvaluate, received an error in SessionDataTask completionHandler which is mentioned below:

[4704:1445043] ATS failed system trust
[4704:1445043] System Trust failed for [1:0x1c41678c0]
[4704:1445043] TIC SSL Trust Error [1:0x1c41678c0]: 3:0
[4704:1445043] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)
(Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." 

Note:

When Root CA Certificate is installed and trusted on the device then HTTPS communication with ATS enabled works without any error. But I don't want user to manually install and trust the Root CA on the device.

Code Snippet:

func getRequest(){  
  let requestUrl = “https://server.test.com/hi.php” //url modified
        let configuration = URLSessionConfiguration.default  
        var request = try! URLRequest(url: requestUrl, method: .get)  

        let session = URLSession(configuration: configuration,  
                                 delegate: self,  
                                 delegateQueue: OperationQueue.main)  
        let task = session.dataTask(with: request, completionHandler: { (data, response, error) in  
            print("Data = \(data)")  
            print("Response = \(response)")  
            print("Error = \(error)")  
        })  
          task.resume()  
    }

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping(URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void){

if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {

    let trust = challenge.protectionSpace.serverTrust
    let rootCa = “root"
    if let rootCaPath = NSBundle.mainBundle().pathForResource(rootCa, ofType: "der") {
      if let rootCaData = NSData(contentsOfFile: rootCaPath) {
        let rootCert = SecCertificateCreateWithData(nil, rootCaData).takeRetainedValue()
        SecTrustSetAnchorCertificates(trust, [rootCert])
        SecTrustSetAnchorCertificatesOnly(trust, true) 
    }

    var trustResult: SecTrustResultType = 0
    SecTrustEvaluate(trust, &trustResult)
    if (Int(trustResult) == kSecTrustResultUnspecified ||
      Int(trustResult) == kSecTrustResultProceed) {
        // Trust certificate.
    } else {
      NSLog("Invalid server certificate.")
    }  
  } else {
    challenge.sender.cancelAuthenticationChallenge(challenge)
  }
}
Julian Fondren
  • 5,459
  • 17
  • 29
GKabra
  • 11
  • 2

1 Answers1

1

I think the actual crypto code looks right, at least at a glance, but the NSURLSession side of things isn't doing the right thing.

Several issues:

  • It is always a mistake to call anything on challenge.sender with NSURLSession. That's the way you do it for NSURLConnection. Instead, you must specify a behavior by calling the provided callback method with an appropriate disposition (and, in some cases, a credential).

  • It is a mistake to cancel challenges that you don't want to explicitly handle. If you cancel a challenge, you're saying that it is better for the connection to fail than to continue. Typically, you should use default handling instead so that those other types of challenges don't cause connection failures when they occur. So in the else case at the end, you should be doing something like:

    completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil)
    

    If you don't do this, you will break things like proxy authentication, and it will probably break in other situations as well.

  • You should cancel the request when you get an invalid cert. Where you have:

    NSLog("Invalid server certificate.")
    

    add something like:

    completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
    
  • You should accept the credential when you successfully validate it. Where you have // Trust certificate

    add something like:

    let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust)
    completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential);
    
dgatwood
  • 10,129
  • 1
  • 28
  • 49