TLS server trust caching with two NSURLSessions

I've read the Technical QA regarding TLS caching and I'm attempting to workaround the issue of TLS server trust caching using two NSURLSessions but I'm not having success.


My test project setup is fairly simple:


Two separate NSURLSessions using ephemeralSessionConfiguration:

self.session1 = NSURLSession(configuration: NSURLSessionConfiguration.ephemeralSessionConfiguration(), delegate: self, delegateQueue: self.delegateQueue)
self.session2 = NSURLSession(configuration: NSURLSessionConfiguration.ephemeralSessionConfiguration(), delegate: self, delegateQueue: self.delegateQueue)


Two NSURLSessionDataTasks (one for each session) to the same URL (https://www.apple.com). The second one starts after a 5 second delay.

        let url = NSURL(string: "https://www.apple.com")
    
        let task1 = session1.dataTaskWithURL(url)
        task1.resume()
    
        let task2 = self.session2.dataTaskWithURL(url)
        let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(5 * Double(NSEC_PER_SEC)))
        dispatch_after(delayTime, dispatch_get_main_queue()) {
            task2.resume()
        }


And the session:didReceiveChallenge: delegate method implementation

    func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential!) -> Void) {
        let sessionName = session == self.session1 ? "session1" : "session2"
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            NSLog("didReceiveChallenge for \(sessionName) at \(challenge.protectionSpace.host) \(challenge.protectionSpace.port)")
            let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust)
            completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential)
        } else {
            NSLog("didReceiveChallenge \(challenge.protectionSpace.authenticationMethod)")
            completionHandler(NSURLSessionAuthChallengeDisposition.CancelAuthenticationChallenge, nil)
        }
    }


When I run the application both tasks complete successfully, however session:didReceiveChallenge: is only invoked once (for task1, session1). Based on the note in the QA1727, I was expecting it to be invoked twice (one time per session).


My questions:

1. Am I doing something wrong or am I misunderstanding the note in QA1727? I've tried different NSURLSessionConfiguration types but I see the same behaviour.


2. If this is the expected behaviour, is there any way to view the (cached) serverTrust object that is being used during the connection?

Accepted Reply

The platform is iOS and I'm targeting iOS 8.0+.

I recommend you testing with iOS 9 beta; this feature got broken at some point (I think with iOS 8) and wasn’t fixed until iOS 9 )-:

Often session:didReceiveChallenge: is invoked twice, sometimes just once, and sometimes not at all!

Make sure to disable caching on your requests, otherwise you might be getting answers from the cache.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

What platform? What version of that platform?

If you’re working on iOS, please retry your test on the latest iOS 9 beta.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

The platform is iOS and I'm targeting iOS 8.0+. I was testing with iPhone 6 running 8.4 and also the 8.4 simulator.


I just tested with Xcode 7 beta 6 (7A192o) on an iPod touch running iOS 9 beta 5 (13a4325c) and it is giving me even stranger results... Often session:didReceiveChallenge: is invoked twice, sometimes just once, and sometimes not at all!


Here's some logs of separate executions of my test app. "task completed" is logged in the session:didCompleteWithError: delegate.


// Challenge for each session (expected result)
2015-09-04 10:13:10.183 AuthenticationChallengeBug[1207:358570] didReceiveChallenge for session1 at www.apple.com 443
2015-09-04 10:13:10.370 AuthenticationChallengeBug[1207:358570] task completed
2015-09-04 10:13:15.443 AuthenticationChallengeBug[1207:358550] didReceiveChallenge for session2 at www.apple.com 443
2015-09-04 10:13:15.649 AuthenticationChallengeBug[1207:358560] task completed


// Challenge for session1 but not session2
2015-09-04 10:13:48.548 AuthenticationChallengeBug[1218:359195] didReceiveChallenge for session1 at www.apple.com 443
2015-09-04 10:13:48.686 AuthenticationChallengeBug[1218:359195] task completed
2015-09-04 10:13:53.867 AuthenticationChallengeBug[1218:359192] task completed


// No challenges
2015-09-04 10:14:07.534 AuthenticationChallengeBug[1223:359480] task completed
2015-09-04 10:14:12.986 AuthenticationChallengeBug[1223:359478] task completed


// Challenge for session2, but not session1
2015-09-04 10:19:26.149 AuthenticationChallengeBug[1295:363839] task completed
2015-09-04 10:19:31.772 AuthenticationChallengeBug[1295:363840] didReceiveChallenge for session2 at www.apple.com 443
2015-09-04 10:19:32.088 AuthenticationChallengeBug[1295:363840] task completed


I haven't noticed any pattern to the behaviour.

The platform is iOS and I'm targeting iOS 8.0+.

I recommend you testing with iOS 9 beta; this feature got broken at some point (I think with iOS 8) and wasn’t fixed until iOS 9 )-:

Often session:didReceiveChallenge: is invoked twice, sometimes just once, and sometimes not at all!

Make sure to disable caching on your requests, otherwise you might be getting answers from the cache.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thank you Quinn. It is working as expected on iOS 9 when I change my NSURLSessionConfiguration to nil out URLCredentialStorage like so:


let config = NSURLSessionConfiguration.ephemeralSessionConfiguration()
config.URLCredentialStorage = nil


As for iOS 8... that's really unfortunate 😟


A few follow-up questions:


1. Can you provide any details on the behaviour I can expect to see on iOS 8 when using NSURLSession? My understanding is that the TLS session cache is still tied to the process, so the app would be guaranteed to see session:didReceiveChallenge: at least once for each key: {<ip>:<port>}<domain>. Is that right?


2. Is there any way (via another NSURLSession delegate method, or property on NSURLRequest or NSURLResponse) to access the NSURLAuthenticationChallenge object in the case of a TLS session cache hit? In my particular circumstance I would like to examine the challenge object (specifically the leaf certificate in a server trust challenge) and do not need to to change the credential that was previously provided.


3. Is the port workaround in the technical note still valid on iOS8+? "If your two connections represent very different operations, you might consider configuring your server to listen on two different ports."

My understanding is that the TLS session cache is still tied to the process, so the app would be guaranteed to see session:didReceiveChallenge: at least once for each key: {

<ip>:<port>}<domain>
. Is that right?

Yes.

Is there any way (via another NSURLSession delegate method, or property on NSURLRequest or NSURLResponse) to access the NSURLAuthenticationChallenge object in the case of a TLS session cache hit?

No, because that makes no logical sense. When you run an HTTPS request, it runs over a TLS connection which runs over a TCP connection. You only get the server trust authentication challenge if the system has to establish a new TLS connection. There are two common cases where this doesn’t happen:

  • If the request runs over an existing TCP connection, maintained as part of the system HTTP 1.1 persistent connection cache. In that case there’s no new TCP connection, and hence no new TLS connection, and hence no challenge.

  • If the system has to start a TCP connection and then, when TLS comes up over that connection, you hit the TLS session cache. In this situation there’s no new TLS connection, because you’re resuming an existing one, and hence no challenge.

In my particular circumstance I would like to examine the challenge object (specifically the leaf certificate in a server trust challenge) and do not need to to change the credential that was previously provided.

Really? Why? Most folks who do this are implementing some form of certificate pinning, and a key aspect of that is the requirement that the client not send the request if the certificate pinning check fails. And that means you do need to be actively involved in the challenge process, that is, if your check fails, you want to be able to fail the request before it goes out on the wire.

Is the port workaround in the technical note still valid on iOS8+?

Yes. The TLS session cache entries are not shared between two ports on the same host.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Thanks for the reply. It's very helpful.

Really? Why? Most folks who do this are implementing some form of certificate pinning, and a key aspect of that is the requirement that the client not send the request if the certificate pinning check fails.


It's probably not a very common reason, but if you're interested, read on 🙂.


Our requirement is to dynamically discover application servers (which is being done via SRV record lookups), make a "discovery" HTTPS request, verify that the server is trusted (via verification of a response payload signature), and then pin the server certificate for all future requests. The plan was to grab the certificate during the challenge but since we can't guarantee that the challenge will arrive on iOS 8 we will likely need the server to include the pinned certificates in the payload of the discovery request.