iOS10 - Self-signed SSL Certificates with NSOutputStream

Hello,

I'm working on an app which requires two distinct connections back to a server (due to requirements of the server itself). I originally used NSURLConnection for both connections, but found that they were being bundled together into a single stream. To get around that, the second connection was changed to use NSOutputStream and NSInputStream with CFStreamCreatePairWithSocketToHost. This solution worked for our needs with iOS9.


With iOS10, however, the stream(s) created by CFStreamCreatePairWithSocketToHost fail to authenticate with self-signed SSL certificates, which the app needs to be able to work with. While the NSURLConnection's stream works fine, the other fails with "CFNetwork SSLHandshake failed (-9807)". It works fine for servers with "real" certificates, but we can't guarantee that all servers will have those.


"Allow Arbitrary Loads" is set to YES in info.plist; I believe that setting is what allows the NSURLConnection to work. CFStreamCreatePairWithSocketToHost seems to ignore it.


So there are two questions:

  1. Is it possible to configure 2 NSURLConnections to remain independent of each other, so that the server will receive them as different streams? This would allow me to remove CFStreamCreatePairWithSocketToHost entirely.
  2. If (1) is not possible, can the NSOutputStream from CFStreamCreatePairWithSocketToHost be configured to trust self-signed SSL certificates?

Replies

To be clear, App Transport Security (ATS) has no bearing on this because:

  • ATS provides enhanced security; ATS settings will not, by themselves, disable the default RFC 2818 server trust evaluation done by our TLS clients

  • ATS only affects high-level HTTP[S] APIs, that is, NSURLSession and NSURLConnection and things layered on top of those

As to the change you’re seeing in iOS 10, that’s a bit of a mystery. By default both NSURLConnection and CFSocketStream should do standard RFC 2818 server trust evaluation, and they should fail or work the same on both iOS 9 and iOS 10. I suspect there’s other, trickier issues in play here (perhaps related to the fact that NSURLConnection will automatically back off to lower TLS versions if TLS 1.2 fails) but it’s hard to say without looking at the traffic on the wire.

Apropos that, I recommend that you do look at the traffic on the wire, just to get an idea as to what’s really going wrong here. For example, if you are working because of NSURLConnection’s automatic retry mechanism, that’s a concern because those retries slow everything down. You can do this using the tools described in QA1176 Getting a Packet Trace.

Regardless, coming back to your central questions, you wrote:

1. Is it possible to configure 2 NSURLConnections to remain independent of each other, so that the server will receive them as different streams?

No. However, you can do this with NSURLSession, by running the two requests in separate sessions. This is what I recommend you do because:

  • as you noted, it would allow you to remove your low-level code entirely

  • NSURLConnection has been deprecated for a while now, so moving to NSURLSession is the right option regardless of this issue

2. If (1) is not possible, can the NSOutputStream from CFStreamCreatePairWithSocketToHost be configured to trust self-signed SSL certificates?

Yes. You can disable the default TLS server trust evaluation using the technique described in the CFSocketStream section of Technote 2232 HTTPS Server Trust Evaluation.

However, you shouldn’t need to do that given my answer to your first question.

Finally, I wanted to ask you about this:

It works fine for servers with "real" certificates, but we can't guarantee that all servers will have those.

Why not? What’s the sticking point for using ‘real’ certificates in this context?

The reason I ask is that self-signed certificates, in addition to being not secure, are going to cause you all ongoing grief. It would be better if you avoided this whole issue by requiring ‘real’ certificates.

Share and Enjoy

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

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

Thanks for the quick reply. I'm working to switch everything over to NSURLSession, since that seems to be the correct approach. However, my delegate for NSURLSession:didReceiveChallenge is apparently never called. Connections using SSL fail with -9813, but connections over HTTP succeed, though I can't figure out how they're authenticating.


There is still an NSURLConnection that is called (and authenticates correctly) earlier in the app's life. Could that somehow be allowing the username/password authentication to persist, but cause NSURLSession's attempt to validate the certificate to fail?


For reference, the delgate's signature is:

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(nonnull NSURLAuthenticationChallenge *)challenge completionHandler:(nonnull void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler


As for not being able to guarantee CA-signed certificates: Users can create their own servers with our "main" software. That software doesn't require SSL certificates to be CA-signed, and I'm not able to enforce that requirement from the app-end of things.

Connections using SSL fail with -9813 …

That’s

errSSLNoRootCert
, whose meaning is pretty obvious.

For reference, the delgate's signature is: - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(nonnull NSURLAuthenticationChallenge *)challenge completionHandler:(nonnull void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler

That’s likely to be the problem. NSURLSession has two authentication delegate handling callbacks:

  • The one shown above is used for authentication challenges that are tied to a specific task; typically these are classic HTTP authentication challenges (Basic and Digest).

  • There’s a similar one, without the

    task
    parameter, that’s used for authentication challenges that are tied to a connection. The TLS authentication challenges are delivered there (along with other oddities, like NTLM).

As for not being able to guarantee CA-signed certificates: Users can create their own servers with our "main" software. That software doesn't require SSL certificates to be CA-signed, and I'm not able to enforce that requirement from the app-end of things.

And presumably the server can be named anything, so your client is going to need

NSAllowsArbitraryLoads
.

Share and Enjoy

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

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

I actually have both callbacks in place, the other being:

- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(nonnull NSURLAuthenticationChallenge *)challenge completionHandler:(nonnull void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler;


Neither is called.


NSAllowsArbitraryLoads has been set to YES since the beginning, which I believe is how my older calls were working in the first place. I'm going to work on converting the initial call from NSURLConnection to NSURLSession to see if that helps at all, but if you have any other input I would appreciate it. Thanks!

So this is all set, it was an error on my part. I wasn't pointing the task to the delegate correctly. My callbacks work correctly with


_mySessionConf = [NSURLSessionConfiguration defaultSessionConfiguration];

_mySession = [NSURLSession sessionWithConfiguration:_mySessionConf delegate:self delegateQueue:nil];

_myTask = [_mySession dataTaskWithURL:url];

[_myTask resume];


Thanks for all the help!