For testing purposes, we may want to have local testing servers using self-signed SSL certificate for HTTPS connection. Now, suppose we have a local server with self-signed certificate, establishing an actual HTTPS connection would require us to trust our self-signed certificate. It is easy on a browser: few clicks, and you will be on your way. But how about a Swift application?
Caution: Get an actual, trusted, signed certicate for production apps!
App Transport Security (ATS) is a technology that requires an app to either support best practice HTTPS security or statically declare its security limitations via a property in its
Info.plist
.
Normally, a simple HTTP/HTTPS POST request in Swift 3 will look something like this:
func request(){
let postString = "test"
var request = URLRequest(url: URL(string: "https://google.com")!)
request.httpMethod = "POST"
request.httpBody = postString.data(using: String.Encoding.utf8)
let task = URLSession.shared.dataTask(with: request){(data: Data?, response: URLResponse?, error: Error?) in
if let error = error{
print("error: ")
print(error)
return
}
if let data = data{
print("data: ")
print(data)
}
if let response = response{
print("response: ")
print(response)
}
}
task.resume()
}
Notice that the URL string states explicity to use HTTPS. Here, Google is used as an example as it has a trusted certificate:
And this is the response we get:
<NSHTTPURLResponse: 0x60c000031cc0> { URL: https://google.com/ } { Status Code: 405, Headers {
"Content-Length" = (
1589
);
"Content-Type" = (
"text/html; charset=UTF-8"
);
Date = (
"Wed, 01 Aug 2018 09:40:51 GMT"
);
Server = (
gws
);
allow = (
"GET, HEAD"
);
"alt-svc" = (
"quic=\":443\"; ma=2592000; v=\"44,43,39,35\""
);
"x-frame-options" = (
SAMEORIGIN
);
"x-xss-protection" = (
"1; mode=block"
);
} }
All is good.
In this case, ATS took care of our secure connection.
However, it will be a different story if we attempt an HTTPS connection to a server with self-signed certificate:
...(TL;DR)
Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “172.24.1.1” which could put your confidential information at risk."
...(TL;DR)
Well, here is how we get Swift to trust us to be who we claim we are.
In order for our Swift app to trust our self-signed certificate, we would first need to exclude our domain (and its subdomains) from ATS. Open Info.plist
as source code and insert the following:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>172:24:1:1</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
Which would look like this if Info.plist
is opened as property list:
However, this is not enough. We would also need to handle authentication challenges in our code.
In order to handle a URLAuthenticationChallenge, we need to implement
func urlSession(URLSession, didReceive: URLAuthenticationChallenge, completionHandler: (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
in a URLSessionDelegate.
For example, suppose we trigger our HTTP request in ViewController
. We will extend it to be a URLSessionDelegate
and implement the method, which white-lists our domain:
extension ViewController: URLSessionDelegate{
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.host == "172.24.1.1" {
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
} else {
completionHandler(.performDefaultHandling, nil)
}
}
}
Now we would need to use our delegate, and so we will not use URLSession.shared.dataTask(with: request)
. What we need is a dataTask
of a URLSession
that uses our delegate:
func request(){
let postString = "test"
var request = URLRequest(url: URL(string: "https://172.24.1.1:8443")!)
request.httpMethod = "POST"
request.httpBody = postString.data(using: String.Encoding.utf8)
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if let error = error{
print("error: ")
print(error)
return
}
if let data = data{
print("data: ")
print(data)
}
if let response = response{
print("response: ")
print(response)
}
}
task.resume()
}
Now, try to make the POST request:
<NSHTTPURLResponse: 0x60c0000379a0> { URL: https://172.24.1.1:8443/ } { Status Code: 412, Headers {
"Content-Language" = (
en
);
"Content-Length" = (
980
);
"Content-Type" = (
"text/html;charset=utf-8"
);
Date = (
"Wed, 01 Aug 2018 11:01:55 GMT"
);
Server = (
"Apache-Coyote/1.1"
);
} }
Yay!
Well explained. Thank you