Shipping an App with App Transport Security
In iOS 9 and OS X 10.11, Apple introduced App Transport Security (ATS), a low-level set of restrictions on apps’ network connections. One of the most visible of these restrictions is the requirement that apps no longer make connections over plain HTTP; instead, the OS enforces the use of HTTPS unless explicitly told otherwise.
There’s already been lots of great discussion about how ATS works – see, for example, Neglected Potential’s Working with Apple’s App Transport Security. Apple has also provided a descriptive tech note on the feature, clearly documenting the expectations of ATS and the exceptions that remain available to developers. And the community has noted in several articles that turning ATS off entirely is generally a Bad Idea.
This article is aimed at a different purpose: to look at the different speed bumps that can show up while building an app alongside ATS, and to explain how to get around them. There are lots of great little tricks that have only cropped up in OS X release notes or on Stack Overflow, or that can only be discovered by building a sample app. Let’s start by digging into where ATS applies.
What App Transport Security Does
ATS is implemented below the Foundation-level networking classes. Since the most common approaches to performing HTTP(S) communication in apps – NSURLSession and the soft-deprecated NSURLConnection – both live in Foundation, ATS will apply to both of them. This means, for example, that it’s not possible to circumvent ATS by continuing to use NSURLConnection. However, lower-level networking APIs in the CFNetwork framework are not subject to ATS’s restrictions.
Correction: the previous paragraph used to claim that CFNetwork-level APIs like
CFStreamCreatePairWithSocketToHost()
couldn’t circumvent ATS. This is
incorrect; CFNetwork provides a way to make secure connections without being
subject to ATS’s restrictions, though without the conveniences of the
higher-level frameworks. Thanks to @al45tair and
@justkwin for pointing out my error on Twitter. See this
hastily-assembled sample project for an example of how
to make this sort of connection.
ATS also represents an extra layer of security for network connections. It
doesn’t replace any existing security or authentication checks that your app
might be responding to. This means, for example, that using ATS doesn’t remove
the need for methods like NSURLSessionDelegate’s
-URLSession:didReceiveChallenge:completionHandler:
– any challenge/response
communications or certificate validation will still need to be done separately
from ATS.
For certain configurations, this means that successfully establishing a HTTPS connection requires some ATS work and some code in an NSURLSession’s delegate. Let’s take a look at such a setup now.
Self-Signed Certificates
Many HTTP servers offer HTTPS, but haven’t necessarily obtained the server certificate from a certificate authority (CA). Instead, these servers might use a self-signed certificate – one that’s been “issued” by that server to itself. Apps that want to connect to that server would then need to trust the server’s word, so to speak, rather than rely on a chain of trust back to a known CA.
The reasons for using a self-signed certificate vary widely, and in lots of circumstances, the correct path forward is to fix the server’s HTTPS configuration using a certificate from a CA. However, developers can’t always guarantee that’s possible – OS X Server, for example, uses a self-signed certificate for its services by default, and customers may be connecting your app to their own self-signed server.
In such a situation, we’d need to first disable ATS – either for one specific server, or for all connections. Let’s assume that users can configure the app to connect to multiple servers, and provide a general ATS exception in Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
However, that’s not quite enough to get a connection going to a server with a self-signed certificate. We’ll also need to inform the networking code in the app that it’s OK to have an invalid certificate. Assuming we’re using NSURLSession, we can do this with a delegate method:
func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
completionHandler(.UseCredential, NSURLCredential(trust: challenge.protectionSpace.serverTrust!))
}
Similar behavior is available for other networking technologies, as well:
NSURLConnectionDelegate provides a method
-connection:willSendRequestForAuthenticationChallenge:
, and Core Foundation
exposes several kCFStreamSSL*
properties that can be set on CFStreams for SSL
connections.
Balancing Security with Usability
While the ATS exception and NSURLSessionDelegate method implementation described above will let our app connect to any self-signed HTTPS server, it also represents a fairly significant drop in the security level for our app. The ATS exception effectively disables all the extra checks that iOS 9 and OS X 10.11 use by default for secure connections, leaving our app no better than it would have been under an older OS. Always think carefully before adding ATS exceptions.
Thankfully, we can bring back some of that security. In the hypothetical case above, we said our users might connect our app to any old self-signed server. However, apps often connect back to the developer’s servers (for content, updates, or feedback). We can take advantage of ATS for those connections by defining an additional exception for our domain and all its subdomains:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
With this more extensive ATS dictionary in our app’s Info.plist, we turn the
“allows insecure HTTP loads” flag back off for the example.com
domain and all
its subdomains. When our app makes any connection to a host in that domain,
then, we’ll be receiving the full protection of ATS.
This exception mechanism can handle even more complex scenarios. For example,
consider what might happen if most servers in example.com
were correctly
secured with HTTPS, but one server – say, insecure.example.com
– wasn’t yet
fully ATS-compatible. We can handle this by defining another more specific
exception for just that host:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>insecure.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
While it’s not yet documented, ATS exceptions can in fact be nested in this way.
Any exceptions specified without the NSIncludesSubdomains
key will be
evaluated before exceptions with the key, meaning that domain-specific
exceptions can override “wildcard” any-subdomain exceptions. (As with all
undocumented behavior, this is subject to change in future OS releases.)
Debugging ATS Failures
In the course of building an app with ATS, it’s possible – even likely – that one particular connection fails to pass the ATS checks, and that you’re not sure why. Thankfully, there are several different debugging utilities available for diagnosing ATS problems.
First, let’s look at what an ATS error looks like. Most ATS failures will
present as CFErrors with a code in the -9800 series. These are defined in the
Security/SecureTransport.h
header, available on both iOS and OS X. For
example, our self-signed server from before would present the following error
without an ATS exception:
2015-08-23 06:33:53.058 SelfSignedServerATSTest[3787:682937] CFNetwork SSLHandshake failed (-9824)
2015-08-23 06:33:53.060 SelfSignedServerATSTest[3787:682937] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9824)
And we’d get a similar error (but with a different code) after adding the ATS exception, but before implementing the NSURLSessionDelegate method to allow self-signed certificates:
2015-08-23 06:34:42.700 SelfSignedServerATSTest[3792:683731] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)
Using CFNetwork Diagnostics
In both of these error cases, we can set the environment variable
CFNETWORK_DIAGNOSTICS
to 1
in order to get more information on the console
about the failure. After turning this variable on in our app’s scheme, you’ll
notice a new log line with a path to a diagnostic file; this file, in turn, is
filled with information about all the actions the CFNetwork layer is taking on
behalf of your app’s networking code. From this file, we can pull more detailed
logs like:
Aug 23 06:36:12 SelfSignedServerATSTest[3798] <Notice>: CFNetwork Diagnostics [1:11] 06:36:12.272 {
Did Fail
Loader: <CFURLRequest 0x7feffae31ab0 [0x1039dc7b0]> {url = https://localhost, cs = 0x0}
Error: Error Domain=kCFErrorDomainCFNetwork Code=-1202 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x7feffad2be40>,
0 : <cert(0x7feffae357d0) s: markkula i: markkula>
)}}
init to origin load: 0.00655901s
total time: 0.026952s
total bytes: 0
} [1:11]
While this error doesn’t include a description, we see that the underlying error
code is -1202 rather than -9824. This particular code comes from the
CFNetwork/CFNetworkErrors.h
header, and its name clearly describes the
problem: it’s the error kCFURLErrorServerCertificateUntrusted
. From there, we
could work to fix the server’s HTTPS configuration, or add the necessary ATS
exception.
If the log still doesn’t contain quite enough useful information for your
purposes, the value for CFNETWORK_DIAGNOSTICS
goes all the way up to 3
,
producing increasingly verbose output at each level. Just be careful – at its
most verbose, the diagnostic output could contain sensitive information about
the connections being established.
Using nscurl
While CFNETWORK_DIAGNOSTICS
can be quite handy, its output can also be
somewhat cryptic, and it’s a bit of a hassle to pull the log file off an iOS
device. If you have access to an OS X machine running 10.11 or later, the
command-line utility nscurl
provides some basic ATS debugging capabilities.
Simply open a Terminal and run:
nscurl --ats-diagnostics https://example.com
The tool will run through several different combinations of ATS exceptions,
trying a secure connection to the given host under each ATS configuration and
reporting the result. The ATS configuration changes made by nscurl
include:
- Turning on the “allow arbitrary loads” flag
- Dropping the minimum required TLS version to 1.1, then 1.0
- Removing the Perfect Forward Secrecy (PFS) requirement
Including the --verbose
flag alongside --ats-diagnostics
causes nscurl
to
also output the exact ATS configuration dictionary being used for each
connection, as well as the error returned in its internal NSURLSessionDelegate
implementation. For a self-signed certificate on localhost, we might see output
like:
Allow All Loads
ATS Dictionary:
{
NSAllowsArbitraryLoads = true;
}
2015-08-23 06:54:12.625 nscurl[3838:696431] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)
Result : FAIL
Error : Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “localhost” which could put your confidential information at risk." UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=<SecTrust 0x7f8050c7f2b0 [0x7fff78cfe890]>, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9813, NSErrorPeerCertificateChainKey=(
"<SecCertificate 0x7f8050c7def0 [0x7fff78cfe890]>"
), NSUnderlyingError=0x7f8050c95e10 {Error Domain=kCFErrorDomainCFNetwork Code=-1202 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, kCFStreamPropertySSLPeerTrust=<SecTrust 0x7f8050c7f2b0 [0x7fff78cfe890]>, _kCFNetworkCFStreamSSLErrorOriginalValue=-9813, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9813, kCFStreamPropertySSLPeerCertificates=(
"<SecCertificate 0x7f8050c7def0 [0x7fff78cfe890]>"
)}}, NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be “localhost” which could put your confidential information at risk., NSErrorFailingURLKey=https://localhost/, NSErrorFailingURLStringKey=https://localhost/, NSErrorClientCertificateStateKey=0}
This tells us that even with arbitrary loads enabled, nscurl
doesn’t trust the
certificate being used by localhost – we’d need the NSURLSessionDelegate method
implementation above to work around this issue. More importantly, it tells us
the underlying -1202 error code right up front, rather than requiring that we
dig through a CFNetwork diagnostic log to figure out what’s going on.
Wrapping Up
All told, ATS represents a significant step forward in iOS and OS X network security. It helps encourage developers (and, indirectly, users) to transmit data securely across the Internet, regardless of what framework an app is using to perform its network operations. At the same time, ATS provides a flexible system for opting out when some parts of an app aren’t quite ready yet.
Networking is a pretty ubiquitous need in modern apps, and Apple has risen to meet the challenges of developing those apps with a robust set of options for working with and debugging ATS. Between runtime NSErrors, low-level framework logging, and external utilities, plenty of information is available to developers struggling with connections prevented by ATS.