HTTPS and IP Addresses

Hi

I have an iOS 10 (only) app. The app needs to be able to support the use of self-signed certifcates for IP addresses i.e https://192.168.2.1 in a "test" mode. The IP addresses and certifcates are spawned virtual servers and they don't have access to a DNS or a certificate authority, importantly they are for testing only.


In the app info.plist I have the following entry:

<key>NSAppTransportSecurity</key> 
  <dict> 
        <key>NSAllowsArbitraryLoads</key> 
  <true/> 
  </dict>

If the app is in "test" mode, this is the code that gets executed:


class AllowSelfSignedCertificate: NSObject, URLSessionDelegate 
{ 
  private func urlSession(session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: (NSURLSession.AuthChallengeDisposition, URLCredential?) -> Void) 
  { 
    completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) 
  } 
} 

if mode == OperationMode.test 
{ 
  let session = URLSession(configuration: URLSessionConfiguration.ephemeral, delegate: AllowSelfSignedCertificate(), delegateQueue: nil) 

}

When I run the app in "test" mode I get the following error:
The certificate for this server is invalid. You might be connecting to a server that is pretending to be "192.168.2.1" which could put your confidential information
at risk
When I connect the app to the same network as the test server and include NSAllowsLocalNetworking, I get the same error. Following is the output of nsurl:

nscurl --ats-diagnostics https:/ 
Starting ATS Diagnostics 
Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https:/ 
A test will "PASS" if URLSession:task:didCompleteWithError: returns a nil error. 
Use '--verbose' to view the ATS dictionaries used and to display the error received in URLSession:task:didCompleteWithError:. 
================================================================================ 
Default ATS Secure Connection 
--- 
ATS Default Connection 
2016-12-28 14:52:50.763 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824) 
Result : FAIL 
--- 
================================================================================ 
Allowing Arbitrary Loads 
--- 
Allow All Loads 
2016-12-28 14:52:50.794 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
================================================================================ 
Configuring TLS exceptions for 192.168.2.1 
--- 
TLSv1.2 
2016-12-28 14:52:50.830 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824) 
Result : FAIL 
--- 
--- 
TLSv1.1 
2016-12-28 14:52:50.868 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824) 
Result : FAIL 
--- 
--- 
TLSv1.0 
2016-12-28 14:52:50.934 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824) 
Result : FAIL 
--- 
================================================================================ 
Configuring PFS exceptions for 192.168.2.1 
--- 
Disabling Perfect Forward Secrecy 
2016-12-28 14:52:50.948 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
================================================================================ 
Configuring PFS exceptions and allowing insecure HTTP for 192.168.2.1 
--- 
Disabling Perfect Forward Secrecy and Allowing Insecure HTTP 
2016-12-28 14:52:50.959 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
================================================================================ 
Configuring TLS exceptions with PFS disabled for 192.168.2.1 
--- 
TLSv1.2 with PFS disabled 
2016-12-28 14:52:50.971 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
--- 
TLSv1.1 with PFS disabled 
2016-12-28 14:52:50.980 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
--- 
TLSv1.0 with PFS disabled 
2016-12-28 14:52:50.990 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
================================================================================ 
Configuring TLS exceptions with PFS disabled and insecure HTTP allowed for 192.168.2.1 
--- 
TLSv1.2 with PFS disabled and insecure HTTP allowed 
2016-12-28 14:52:51.022 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
--- 
TLSv1.1 with PFS disabled and insecure HTTP allowed 
2016-12-28 14:52:51.041 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
--- 
--- 
TLSv1.0 with PFS disabled and insecure HTTP allowed 
2016-12-28 14:52:51.052 nscurl[36062:6500411] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813) 
Result : FAIL 
---

When I run openssl -s_client, this is the output (certificate information omitted)

openssl s_client -connect 192.168.2.1:443 
CONNECTED(00000003) 
verify error:num=18:self signed certificate 
verify return:1 
--- 
No client certificate CA names sent 
--- 
SSL handshake has read 979 bytes and written 456 bytes 
--- 
New, TLSv1/SSLv3, Cipher is AES128-SHA 
Server public key is 2048 bit 
Secure Renegotiation IS supported 
Compression: NONE 
Expansion: NONE 
SSL-Session: 
    Protocol  : TLSv1 
    Cipher    : AES128-SHA 
    Session-ID: BC8C61C53D3BBBC9CEAEA468BCC54FA4C494C43192274A48E4195A80FDB36EDC 
    Session-ID-ctx: 
    Master-Key: 511F32CC76D8D7C457003B1042D9E678E159DC653D1F8AE844E6EA9A280727D0BA41E6329FFECFE628C1B8CC41EF71A0 
    Key-Arg   : None 
    Start Time: 1482901084 
    Timeout   : 300 (sec) 
    Verify return code: 18 (self signed certificate) 
--- 
read:errno=0


What ATS configuration should I adopt in the app to enable http://<ip_address> in a test operation mode?


Thanks for your help

Accepted Reply

Is it your opinion, that I should remove the NSAppTransportSecurity stanza from info.plist and to manually get the self-signed certifcate onto the test device to prevent having to handle URLSessionDelegate?

Yes. Writing code to override HTTPS server trust evaluation is extra work and it carries with it significant security riks. If you can avoid doing so, you should.

Share and Enjoy

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

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

Replies

The app needs to be able to support the use of self-signed certifcates for IP addresses i.e https://192.168.2.1 in a "test" mode.

First up, I recommend that you avoid adding a test mode to your app. IMO, there’s a better way to do this:

  1. Create your own custom CA

  2. Use that CA to create a digital identity for your server

  3. Install that CA’s root certificate on your client devices

This has a bunch of advantages:

  • It’s less code to write; you don’t have to override server trust evaluation on the client, disable ATS, and so on.

  • Because there’s no test code in your client, there’s no chance of you accidentally shipping an app to customers with that test code enabled.

    WARNING Don’t discount this risk. There’s at least one ‘household name’ developer who has had to ship an emergency update to their app because they accidentally shipped it with server trust evaluation disabled. Don’t be that guy!

  • Similarly, there’s no chance of any App Review hiccoughs because there are no ATS exceptions that you have to remember to remove in the production build.

  • Your testing is more realistic because it uses exactly the same code paths as your production app.

If necessary you can create a test CA using facilities that are built in macOS; see Technote 2326 Creating Certificates for TLS Testing for the details.

Also, keep in mind that most server platforms support Bonjour in some form, which allows you to use

.local
names to talk to the server. And while a real CA won’t issue a certificate for a
.local
name, your test CA can!

Alternatively, a CA can issue a certificate for an IP address via the Subject Alternate Name extension.

If you ignore the above advice and continue with your current strategy, my first suggestion is that you do your testing over HTTP (rather than HTTPS). Given that your test servers aren’t even close to your real servers, there doesn’t seem to be any drawbacks to use HTTP for this test mode.

Note HTTP requests to an IP address will work without any ATS exceptions on iOS 10. If you also want to test iOS 9, you’ll need to disable ATS entirely by setting

NSAllowsArbitraryLoads
.

If you want to stick with HTTPS, you’ll have to take two steps to get things working:

  • Disable ATS — Again, it’s reasonable to do this using

    NSAllowsArbitraryLoads
    given that this is a dedicated test setup.
  • Disable default HTTPS server trust evaluation — You can do this using the techniques described in Technote 2232 HTTPS Server Trust Evaluation.

Share and Enjoy

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

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

Appreciate you taking the time to respond.


What I would like to understand is for iOS 10 the code and configuration I posted above worked, with iOS 10.2 (14C92) the app now displays TLS errors and the session delegate on NSURLSession does not get invoked. Are you able to explain what may have changed.


Reading the Technote, it appears the simpliest approach is opening Safari in iOS and browing to the local server i.e https://192.168.0.2 and accepting the certifcate. A dialog appears with the options "Continue", "Details" and "Cancel". If I click "Details", "Trust" appears in the top right. My understanding is that if I tap "Trust", the certifciate is now stored on the device but a profile is not created.

- OR -


Should I email the self-signed certificate (.cer) file so that it's accessible on the device i.e. live.com or gmail.com. Open the attachment and follow the process to "Install" the certificate, thus creating a profile. With a profile created my app can perform the desired network operation. I should be able to remove App Transport Security stanza from the info.plist altogether. And no longer need to perform server trust validation for self signed certificates (refer to my sample code in original post). Is this correct approach?


Thanks again

What I would like to understand is for iOS 10 the code and configuration I posted above worked, with iOS 10.2 (14C92) the app now displays TLS errors and the session delegate on NSURLSession does not get invoked.

Which configuration? The one with just

NSAllowsArbitraryLoads
set? That should completely disable ATS, which means your session delegate callbacks should be called. If they’re not, something else is happening.

Here’s how I tested this:

  1. I created a self-signed TLS server identity and added it to my keychain.

    Note If you don’t have such an identity handy, you can create one using the Certificate Assistant feature in Keychain Access.

  2. I ran TLSTool to create a dummy server that uses that identity. I’ve pasted in an example of this below.

  3. I created a dummy app that tries to connect to that server. I’ve pasted the source code in below.

  4. In that app, I set

    NSAllowsArbitraryLoads
    .

When I tap the test button the app prints this:

… task session challenge NSURLAuthenticationMethodServerTrust
…
… error Error Domain=NSURLErrorDomain Code=-1202 …

As you can see, the session delegate is called before the request fails.

Please try this for yourself and let me know what you see.

Share and Enjoy

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

let myEmail = "eskimo" + "1" + "@apple.com"
$ TLSTool s_server -cert biff.local

Note In this example,

biff.local
is the name of the TLS server identity from step 1.

import UIKit

class ViewController: UIViewController, URLSessionDelegate {

var session: URLSession! = nil

@IBAction func testAction(_ sender: Any) {
    if self.session == nil {
        let config = URLSessionConfiguration.default

        // Don't cache, which avoids complications during testing. 

        config.requestCachePolicy = .reloadIgnoringLocalCacheData

        self.session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
    }

    let request = URLRequest(url: URL(string: "https://192.168.1.189:4433")!)
    self.session!.dataTask(with: request) { (data, response, error) in
        if let error = error {
            NSLog("error %@", error as NSError)
        } else {
            NSLog("success")
        }
    }.resume()
}

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
    NSLog("task session challenge %@", challenge.protectionSpace.authenticationMethod)
    completionHandler(.performDefaultHandling, nil)
}

}

Note In this example, 192.168.1.189 is the IP address of my Mac running `TLSTool.

Hi eskimo


I was able to reproduce your example, thanks 🙂


The big difference between the example code and the code running in my app is the assignment of URLSessionDelegate and I was able to reproduce that in the example as well.


class ViewController: UIViewController {
    var ignoreSslCerts: URLSessionDelegate? = nil

    @IBAction func onSwitchClick(_ sender: UISwitch)
    {
        ignoreSslCerts = sender.isOn ? AllowSelfSignedCertificate() : nil
    }

    @IBAction func onButtonClick(_ sender: UIButton)
    {
      let config = URLSessionConfiguration.default
      config.requestCachePolicy = .reloadIgnoringLocalCacheData

        let session  =  URLSession(configuration: config, delegate: ignoreSslCerts, delegateQueue: OperationQueue.main)

        var request = URLRequest(url: URL(string: "https://192.168.0.11:4433"
        request.timeoutInterval = 10
        session.dataTask(with: request)
        {
            (data, response, error) in
            if let error = error
            {
                NSLog("error %@", error as NSError)
            }
            else
            {
                NSLog("success")
            }
        }.resume()
    }
}

class AllowSelfSignedCertificate: NSObject, URLSessionDelegate
{
    private func urlSession(session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: (NSURLSession.AuthChallengeDisposition, URLCredential?) -> Void)
    {
        completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
    }
}


I added a UISwitch controll to toogle the assignement of AllowSelfSignedCertifcate to ignoreSslCerts when the "isOn" property is true.


On the onButtonClick event, regardless if ignoreSslCerts is an instance of AllowSelfSignedCertificate or nil, the delegate does not get invoked. In saying that, the difference is the urlSession signature.


Your example

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

My example

private func urlSession(session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: (NSURLSession.AuthChallengeDisposition, URLCredential?) -> Void)


Once I updated my urlSession the function gets invoked 🙂. I guess that was the change I was looking for.


Is it your opinion, that I should remove the NSAppTransportSecurity stanza from info.plist and to manually get the self-signed certifcate onto the test device to prevent having to handle URLSessionDelegate?


Many thanks

Is it your opinion, that I should remove the NSAppTransportSecurity stanza from info.plist and to manually get the self-signed certifcate onto the test device to prevent having to handle URLSessionDelegate?

Yes. Writing code to override HTTPS server trust evaluation is extra work and it carries with it significant security riks. If you can avoid doing so, you should.

Share and Enjoy

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

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

Hi eskimo,


I have seen many of your topics related to this issue of connecting to the server, but somehow I still don't manage to get it working.

I have a development server, which already has a self-signed certificate, which can be reached either through an URL or locally (when one is in local network) through the IP. I don't know if that is important, but the URL, however, uses a Subdomain and Port.


I have tried to set Allow Arbitrary Loads setting to App Transport Security Settings in my Xcode but it or added the Exception Domains, but nothing seems to work. I always get the same error in the console: NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)


Now, I really don't want to do any hard-coding to bypass these security evaluations, I don't think it's the right way and I am sure, that there should be another solution. One of which, should be this one with the making of certificates.


I already created a custom CA. Based on this documentation, one must then with this CA create digital identities (certificates) for client and the server.

But after this point, I am not exactly sure, what must I do with them and, what should be uploaded where?


You mentioned, that the root certificate of CA should be uploaded on each test device. What should I do with digital identities? I think this part of documentation is missing and I couldn't find it anywhere.


Many thanks in advance.


Best regards,

fbele

OK, let me explain how I set this up:

  1. I use Certificate Assistant to create a custom CA per Technote 2326 Creating Certificates for TLS Testing.

  2. I use that to issue a digital identity for my server,

    deep-thought.local
    . TN2326 has the details on doing this as well.
  3. I set up a Mac to act as my server. I set its local DNS name to

    deep-thought.local
    via the Sharing preferences panel.
  4. I set up my server software on that Mac.

  5. I install the digital identity from step 2 as the digital identity for my server. How I do this depends on the server I’m using. For example:

    When I’m using Apache, I convert the digital identity to a PEM certificate and private key and put those values into the files referenced by

    SSLCertificateFile
    and
    SSLCertificateKeyFile
    (typically
    server.crt
    and
    server.key
    , respectively).

    For more info on this, you should consult the Apache docs.

    When I’m using

    TLSTool
    , it can use the digital identity directly from my Mac’s keychain.
  6. I install the certificate authority’s root certificate on my device (or simulator) per QA1948 HTTPS and Test Servers.

    IMPORTANT On iOS 10.3 and later you have to explicitly enable the certificate authority in Settings > General > About > Certificate Trust Settings. At some point I will update QA1948 to cover that but that process is more complex than you might think (specifically, QA1948 covers all of our platforms and some of those platforms — yes, I’m looking at you, tvOS! — make this hard).

At this point I can write NSURLSession code that fetches

https://deep-thought.local
without any HTTPS server trust evaluation overrides.

Please try this out and post back if you hit any snags.

Share and Enjoy

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

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