Trying to understand URLSession Authentication challenges - swift

I am attempting to download a PDF from a URL.
private func downloadSessionWithFileURL(_ url: URL){
var request = URLRequest(url: url)
request.addValue("gzip, deflate", forHTTPHeaderField: "Accept-Encoding")
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
session.downloadTask(with: request).resume()
}
This calls its delegate method
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.previousFailureCount > 0 {
completionHandler(Foundation.URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
if let serverTrust = challenge.protectionSpace.serverTrust {
completionHandler(Foundation.URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverTrust))
} else {
print("unknown state. error: \(String(describing: challenge.error))")
}
}
The URLAuthenticationChallenges protectionSpace is always serverTrust.
When the URL of the PDF is attempted to be accessed it redirects user to a login screen. I would have thought there would be another call to
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
requiring user to enter their credentials but there isn't. So the download task attempts to download the contents of the redirected URL which is a login screen.
My Questions are.
What triggers a URLAuthenticationChallenge for a username and password. is it a specific header value in the HTML?
Which URLAuthenticationChallenge protectionSpace should I be expecting for a username password request from a server.

There are two different delegate protocols: for the URLSession itself, and its tasks.
URLSessionDelegate has: public func urlSession(_:didReceive:completionHandler:)
URLSessionTaskDelegate has: public func urlSession(_:task:didReceive:completionHandler:)
The URLSessionDelegate is used for server trust issues (e.g. allowing SSL trust when running through Charles or other proxy). The URLSessionTaskDelegate is used for authentication of an individual task.
So to get your authentication challenge, add this to your class:
extension MyClass: URLSessionTaskDelegate {
public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodDefault ||
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
let credential = URLCredential(user: self.basicAuthUserName,
password: self.basicAuthPassword,
persistence: .forSession)
completionHandler(.useCredential, credential)
}
else {
completionHandler(.performDefaultHandling, nil)
}
}
}

Some basics of SSL:
How SSL works? When client establishes the connection with server (called SSL handshake):
Client connects to server and requests server identify itself.
Server sends certificate to client (include public key)
Client checks if that certificate is valid. If it is, client creates a symmetric key (session key), encrypts with public key, then sends back to server
Server receives encrypted symmetric key, decrypts by its private key, then sends acknowledge packet to client
Client receives ACK and starts the session
1.What triggers a URLAuthenticationChallenge for a username and password. is it a specific header value in the HTML?
If you an have https connection, these methods will be triggered. These are for security purpose to prevent the man in the middle attack. For e.g, I can set up charles proxy server, install the public certificate on simulator/device and can monitor all the request that the app is sending to the actual server and thus obtain the sensitive information(API Keys, token, request Headers, request body etc) which I need to hide from attackers.
Which URLAuthenticationChallenge protectionSpace should I be expecting
for a username password request from a server.
You can either Compare the server certificate with the local certificates that you have in your apps:
if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0) {
let serverCertificateData = SecCertificateCopyData(serverCertificate) as Data
let localCer = Bundle.main.path(forResource: "fileName", ofType: "cer")
if let localCer = localCer {
if localCer.isEqual(to: serverCertificate) { completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust:serverTrust))
return
}
}
}
or you can compare the public keys:
if let serverCertificate = SecTrustGetCertificateAtIndex(trust, 0), let serverCertificateKey = publicKey(for: serverCertificate) {
if pinnedKeys().contains(serverCertificateKey) {
completionHandler(.useCredential, URLCredential(trust: trust))
return
}
}
Comparing public keys is a better approach as when comparing certificates, you have to keep a copy of the local certificate in the app and when the certificates expires which will have to update the certificates in the app, which require an update in the app store.

Related

Correct Alamofire retry for JWT if status 401?

I am trying to make a retry for my Alamofire Interceptor because I work with JSON Web Token. Adapt works great. But the server updates the Access token every 10 minutes after user registration or authorization. After 10 mins Access token doesn't work anymore, and the server response is 401. So I need to Refresh the token when the status is 401. As I mentioned above, adapt works great. But I need help understanding how to deal with retry. Below is my Interceptor:
class RequestInterceptor: Alamofire.RequestInterceptor {
func adapt( _ urlRequest: URLRequest, for session: Session, completion: #escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
func retry( _ request: Request, for session: Session, dueTo error: Error, completion: #escaping (RetryResult) -> Void) {
guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
completion(.doNotRetryWithError(error))
return
}
}
}
My View Model:
func refreshTokenFunc() {
AF.request(TabBarModel.Request.refreshTokenUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, interceptor: RequestInterceptor()).response { response in
...
And usage (I work with SwiftUI):
.task {
tabBarViewModel.refreshTokenFunc()
}
I was trying with some examples from the Internet. But it doesn't work for me.
In you retry you need to call the completion handler on both sides of the guard, not just in the else side. completion(.retry) is common but you could also track a delay to make sure you don't overload the backend.
Additionally, you should be validating response and checking the error, not reaching directly into request.task.
AF.request(...).validate()... // Ensure the response code is within range.
// In retry
guard let error = error.asAFError, error.responseCode == 401 else { ... }

add public key pinning in alamofire manger class swift

here is my alamofire manager, how I can add public key pinning on it ? please help me, I couldn't know the way to do it in my code, if possible I need explanation step by step on how do that with AFManager that has all the requests
class AFManager : NSObject{
///without headers (post)
//used this to registration
class func requestPOSTURL(_ strURL : String, params : [String :
AnyObject]?, success:#escaping (JSON) -> Void, failure:#escaping (Error) -> Void){
URLCache.shared.removeAllCachedResponses()
Alamofire.request(strURL, method: .post, parameters: params, encoding: URLEncoding.httpBody).responseJSON { (responseObject) -> Void in
//print(responseObject)
if responseObject.result.isSuccess {
let resJson = JSON(responseObject.result.value!)
success(resJson)
}
if responseObject.result.isFailure {
let error : Error = responseObject.result.error!
failure(error)
}
}
}
///// response string (post)
//used this in login // used in change password
class func strRequestPOSTURL(_ strURL : String, params : [String : String]?, headers : [String : String]?, success:#escaping (JSON) -> Void, failure:#escaping (Error) -> Void){
URLCache.shared.removeAllCachedResponses()
Alamofire.request(strURL, method: .post, parameters: params, encoding: URLEncoding.httpBody, headers: headers).responseJSON { (response) in
//print(response)
if response.result.isSuccess {
let resJson = JSON(response.result.value!)
success(resJson)
}
if response.result.isFailure {
let error : Error = response.result.error!
failure(error)
}
}
}
}
I saw this sample but didn't know how to do it and where I should put the code see the link below :
https://infinum.co/the-capsized-eight/ssl-pinning-revisited
Security
Using a secure HTTPS connection when communicating with servers and web services is an important step in securing sensitive data. By default, Alamofire will evaluate the certificate chain provided by the server using Apple's built in validation provided by the Security framework. While this guarantees the certificate chain is valid, it does not prevent man-in-the-middle (MITM) attacks or other potential vulnerabilities. In order to mitigate MITM attacks, applications dealing with sensitive customer data or financial information should use certificate or public key pinning provided by the ServerTrustPolicy.
ServerTrustPolicy
The ServerTrustPolicy enumeration evaluates the server trust generally provided by an URLAuthenticationChallenge when connecting to a server over a secure HTTPS connection.
let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
)
There are many different cases of server trust evaluation giving you complete control over the validation process:
performDefaultEvaluation: Uses the default server trust evaluation
while allowing you to control whether to validate the host provided
by the challenge.
pinCertificates: Uses the pinned certificates to validate the server
trust. The server trust is considered valid if one of the pinned
certificates match one of the server certificates.
pinPublicKeys: Uses the pinned public keys to validate the server
trust. The server trust is considered valid if one of the pinned
public keys match one of the server certificate public keys.
disableEvaluation: Disables all evaluation which in turn will always
consider any server trust as valid.
customEvaluation: Uses the associated closure to evaluate the
validity of the server trust thus giving you complete control over
the validation process. Use with caution.
Server Trust Policy Manager
The ServerTrustPolicyManager is responsible for storing an internal mapping of server trust policies to a particular host. This allows Alamofire to evaluate each host against a different server trust policy.
let serverTrustPolicies: [String: ServerTrustPolicy] = [
"test.example.com": .pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
),
"insecure.expired-apis.com": .disableEvaluation
]
let sessionManager = SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)
Make sure to keep a reference to the new SessionManager instance, otherwise your requests will all get cancelled when your sessionManager is deallocated.
These server trust policies will result in the following behavior:
test.example.com will always use certificate pinning with certificate chain and host validation enabled thus requiring the following criteria to be met to allow the TLS handshake to succeed:
Certificate chain MUST be valid.
Certificate chain MUST include one of the pinned certificates.
Challenge host MUST match the host in the certificate chain's leaf certificate.
insecure.expired-apis.com will never evaluate the certificate chain and will always allow the TLS handshake to succeed.
All other hosts will use the default evaluation provided by Apple.
Subclassing Server Trust Policy Manager
If you find yourself needing more flexible server trust policy matching behavior (i.e. wildcarded domains), then subclass the ServerTrustPolicyManager and override the serverTrustPolicyForHost method with your own custom implementation.
class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
var policy: ServerTrustPolicy?
// Implement your custom domain matching behavior...
return policy
}
}
Validating the Host
The .performDefaultEvaluation, .pinCertificates and .pinPublicKeys server trust policies all take a validateHost parameter. Setting the value to true will cause the server trust evaluation to verify that hostname in the certificate matches the hostname of the challenge. If they do not match, evaluation will fail. A validateHost value of false will still evaluate the full certificate chain, but will not validate the hostname of the leaf certificate.
It is recommended that validateHost always be set to true in production environments.
Validating the Certificate Chain
Pinning certificates and public keys both have the option of validating the certificate chain using the validateCertificateChain parameter. By setting this value to true, the full certificate chain will be evaluated in addition to performing a byte equality check against the pinned certificates or public keys. A value of false will skip the certificate chain validation, but will still perform the byte equality check.
There are several cases where it may make sense to disable certificate chain validation. The most common use cases for disabling validation are self-signed and expired certificates. The evaluation would always fail in both of these cases, but the byte equality check will still ensure you are receiving the certificate you expect from the server.
It is recommended that validateCertificateChain always be set to true in production environments.
App Transport Security
With the addition of App Transport Security (ATS) in iOS 9, it is possible that using a custom ServerTrustPolicyManager with several ServerTrustPolicy objects will have no effect. If you continuously see CFNetwork SSLHandshake failed (-9806) errors, you have probably run into this problem. Apple's ATS system overrides the entire challenge system unless you configure the ATS settings in your app's plist to disable enough of it to allow your app to evaluate the server trust.
If you run into this problem (high probability with self-signed certificates), you can work around this issue by adding the following to your Info.plist.
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/>
<!-- Optional: Specify minimum TLS version -->
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
</dict>
Whether you need to set the NSExceptionRequiresForwardSecrecy to NO depends on whether your TLS connection is using an allowed cipher suite. In certain cases, it will need to be set to NO. The NSExceptionAllowsInsecureHTTPLoads MUST be set to YES in order to allow the SessionDelegate to receive challenge callbacks. Once the challenge callbacks are being called, the ServerTrustPolicyManager will take over the server trust evaluation. You may also need to specify the NSTemporaryExceptionMinimumTLSVersion if you're trying to connect to a host that only supports TLS versions less than 1.2.
It is recommended to always use valid certificates in production environments.
Using Self-Signed Certificates with Local Networking
If you are attempting to connect to a server running on your localhost, and you are using self-signed certificates, you will need to add the following to your Info.plist.
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
According to Apple documentation, setting NSAllowsLocalNetworking to YES allows loading of local resources without disabling ATS for the rest of your app.
Reference:-
https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md
For implementation details refer the tests.
https://github.com/Alamofire/Alamofire/blob/master/Tests/TLSEvaluationTests.swift#L290-L450
SSL pinning using TrustKit with Alamofire. Here I have included API Manager class. This will help you solve using Alamofire with TrustKit.
class ApiManager: SessionDelegate{
var sessionManager: SessionManager?
override init(){
super.init()
initReachibility()
sessionManager = SessionManager.init(configuration: URLSessionConfiguration.ephemeral, delegate: self)
}
override func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
// Call into TrustKit here to do pinning validation
if TrustKit.sharedInstance().pinningValidator.handle(challenge, completionHandler: completionHandler) == false {
// TrustKit did not handle this challenge: perhaps it was not for server trust
// or the domain was not pinned. Fall back to the default behavior
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
func makeRequestAlamofire(route:URL, method:HTTPMethod, autherized:Bool, parameter:Parameters,header:[String:String], callback: #escaping (APIResult<Data>) -> Void){
sessionManager?.request(route,method: method,parameters:parameter, encoding: JSONEncoding.default,headers:headers ).validate(statusCode: 200..<300)
.validate(contentType: ["application/json"]).responseData { response in
//Pin Validtion returner
guard response.error == nil else {
// Display Error Alert
print("Result Pinning validation failed for \(route.absoluteString)\n\n\(response.error.debugDescription)")
return
}
switch response.result {
case .success(let val):
print("Success")
case .failure(let error):
print("Faild")
}
}
}
}
For the full tutorial refer this link.
Alamofire has changed sll pinning code snipped with the new version(Alamofire 5.0).
You should use ServerTrustManager just like below,
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = timeoutIntervalForRequest
let trustManager = ServerTrustManager(evaluators: [
"dev.ehliyetcepte.com": PublicKeysTrustEvaluator(),
"uat.ehliyetcepte.com": DisabledEvaluator(),
"pilot.ehliyetcepte.com": DisabledEvaluator(),
"prod.ehliyetcepte.com": DisabledEvaluator()])
self.session = Session(startRequestsImmediately: true,
configuration: configuration,
delegate: self,
serverTrustManager: trustManager)
I would recommend using TrustKit. It is a dedicated library that works with everything base on NSURLSession, including Alamofire. Depending on your use case it may be as simple as adding a few values to Info.plist.
Certificate pinning, same as any security measure, is not something you should implement yourself, but you should use a proven library.
let serverTrustPolicies: [String: ServerTrustPolicy] = [
// or `pinPublicKeys`
"test.example.com": .pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
),
"insecure.expired-apis.com": .disableEvaluation
]
let sessionManager = SessionManager(
serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)
I found this solution
let session = Session(delegate:CustomSessionDelegate())
session.request....
class CustomSessionDelegate: SessionDelegate {
private static let publicKeyHash = "your_public_key"
let rsa2048Asn1Header:[UInt8] = [
0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
]
override func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil);
return
}
if let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) {
// Server public key
guard let serverPublicKey = SecCertificateCopyKey(serverCertificate) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
guard let serverPublicKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, nil) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let data:Data = serverPublicKeyData as Data
// Server Hash key
let serverHashKey = sha256(data: data)
// Local Hash Key
let publickKeyLocal = type(of: self).publicKeyHash
if (serverHashKey == publickKeyLocal) {
// Success! This is our server
print("Public key pinning is successfully completed")
completionHandler(.useCredential, URLCredential(trust:serverTrust))
return
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
}
}
private func sha256(data : Data) -> String {
var keyWithHeader = Data(rsa2048Asn1Header)
keyWithHeader.append(data)
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
keyWithHeader.withUnsafeBytes {
_ = CC_SHA256($0, CC_LONG(keyWithHeader.count), &hash)
}
return Data(hash).base64EncodedString()
}

multipart/form-data upload Task doesn't call Authentication Challenge for large files in iOS

I'm trying to upload a zip file from mobile to a remote Azure server using URLSession.dataTask(with: r as URLRequest). For that, I'm using NSURLAuthenticationMethodServerTrust & NSURLAuthenticationMethodClientCertificate authentication. The two works fine when the size of zip file is ~15KB.
But for bigger files, I only see NSURLAuthenticationMethodServerTrust and then the requested times out. I've already spent 3 days without any concrete direction on this.
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate{
if useFirstCert, let certURL = Bundle.main.url(forResource: "azure-client1-cert", withExtension: "p12"){
let cred = credential(from: certURL, password: "passcode")
completionHandler(URLSession.AuthChallengeDisposition.useCredential, cred)
}else {
completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
}else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust{
completionHandler(URLSession.AuthChallengeDisposition.rejectProtectionSpace, nil)
}else{
completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil);
}
}
Finally I got a response from Microsoft as following blogpost - https://blogs.msdn.microsoft.com/waws/2017/04/03/posting-a-large-file-can-fail-if-you-enable-client-certificates/
I’ve just had a successful certificate upload in a prototype app with a 124K file. All needs to be done is to Set the Expect: 100-continue header for the request.
let request = NSMutableURLRequest(url)
request.setValue("100-continue", forHTTPHeaderField: "Expect")
It's applicable for all protocols using IIS.

Swift NSURLSession HTTPS Self Sign - Request never reaches server

I'm having troubles performing a HTTPS post request to my django rest api. I have django-sslserver running to expose the api on port 8000. Things seem fine, when I make a request in a browser https://server-ip-addr:8000/api_view/ my browser complains "Hey! This guys certificate is self signed!" I say "yeah I know, that's me" and continue into the dangerous response.
Anyways I'm attempting to perform the same in Swift for an iOS application. I've found from this link here on implementing NSURLSession delegate protocols/functions NSURLSessionDelegate.URLSession() and NSURLSessionTaskDelegate.URLSession(). I've tweaked the example so that it atomically performs login attempts to my sslserver.
I've done so in the following code, a class implementing the two protocols above designed to pass username/password credentials to the server and wait for a response.
class SecureLogin: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate {
func attemptLogin(username: String, password: String,
callback: ((NSData!,NSURLResponse!,NSError!) -> Void)?) {
println("inside attempt login")
var request = NSMutableURLRequest(URL: NSURL(string: "https://147.222.164.91:8000/ldapauth/")!)
request.HTTPMethod = "POST"
var params = ["username":username, "password":password] as Dictionary<String, String>
var err: NSError?
request.HTTPBody = NSJSONSerialization.dataWithJSONObject(params, options: nil, error: &err)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
var configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
var session = NSURLSession(configuration: configuration,
delegate: self,
delegateQueue:NSOperationQueue.mainQueue())
var task = session.dataTaskWithRequest(request,callback)
task.resume()
}
func URLSession(session: NSURLSession,
didReceiveChallenge challenge: NSURLAuthenticationChallenge,
completionHandler: (NSURLSessionAuthChallengeDisposition,NSURLCredential!) -> Void) {
println("Challenge received")
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, NSURLCredential(forTrust: challenge.protectionSpace.serverTrust))
}
func URLSession(session: NSURLSession,
task: NSURLSessionTask,
willPerformHTTPRedirection response: NSHTTPURLResponse,
newRequest request: NSURLRequest,
completionHandler: (NSURLRequest!) -> Void) {
println("Redirection received")
var newRequest : NSURLRequest? = request
println(newRequest?.description)
completionHandler(newRequest)
}
}
So I go to attempt to perform the attemptLogin() function, providing the simple callback function to confirm a response
var gatekeeper = SecureLogin()
gatekeeper.attemptLogin(username, password: password, callback: {data, response, error -> Void in
println("inside gatekeeper")
}
println("beginning wait")
sleep(25)
I sleep the thread 25 seconds to keep the process alive long enough for the response to come in.
The output on the console looks like:
inside attempt login
beginning wait
Then the program dies, no response/"inside gatekeeper" message received, my django server terminal doesn't show any received requests either. I ran a sanity check: I've commented out the implementations of the delegate methods and the server receives the request, responds with:
inside attempt login
beginning wait
2015-01-27 11:29:37.192 LdapAuthSecure[12783:1475994] NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9812)
the expected error when there is no protocol for handling an authorization challenge.
Can anyone spot any problems with my implementation of the NSURLSession or its delegates? I find it very strange that the request doesn't even reach the server. Thank you for any help!
If this is iOS 9, and if you built your app against the iOS 9 (or later) SDK, you'll also have to tweak your Info.plist file to tell it to allow insecure loads. Otherwise, the URL loading system won't get as far as calling your authentication handler.
so I've never coded for iOS but I do know from memory that native apps will not popup a dialog nor accept self signed certificates by default.
If you own a domain (or if not, it's probably easier) you can get a free certificate from https://www.startssl.com/ or https://buy.wosign.com/free/
Or you can install the self signed certificate by emailing it to your phone and then opening it.
You can remove the self signed certificate from the profiles page in Settings.
I also found this answer for you: https://stackoverflow.com/a/22674004/4837003
But that looks like it will disable validation.

How do I accept a self-signed SSL certificate using iOS 7's NSURLSession and its family of delegate methods for development purposes?

I am developing an iPhone app. During development, I need to connect to a server that's using a self-signed SSL certificate. I'm pretty certain - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler is my opportunity to write some exception code to allow this. However, I can't find any resources that tell me how to do this. I can see the following error in the log:
NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9813)
In addition to this, when I NSLog(#"error = %#", error); from within the above delegate method I get:
Error Domain=NSURLErrorDomain Code=-1202 "The certificate for
this server is invalid. You might be connecting to a server that is
pretending to be api.mydevelopmenturl.example which could put your
confidential information at risk." UserInfo=0x10cbdbcf0
{NSUnderlyingError=0x112ec9730 "The certificate for this server is
invalid. You might be connecting to a server that is pretending to be
api.mydevelopmenturl.example which could put your confidential information
at risk.", NSErrorFailingURLStringKey=https://api.mydevelopmenturl.example/posts,
NSErrorFailingURLKey=https://api.mydevelopmenturl.example/posts,
NSLocalizedRecoverySuggestion=Would you like to connect to the
server anyway?, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x112e5a020>,
NSLocalizedDescription=The certificate for this server is invalid.
You might be connecting to a server that is pretending to be
api.mydevelopmenturl.example which could put your confidential
information at risk.}
Any ideas on how to resolve this issue? Please post code as I've read the conceptual docs and I don't understand them. Here's an example of one that's beyond me: https://developer.apple.com/library/content/technotes/tn2232/_index.html
This works for me:
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:Nil];
...
...
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler{
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]){
if([challenge.protectionSpace.host isEqualToString:#"mydomain.example"]){
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}
}
}
Apple has a Technical Note 2232 which is quite informative and explains in detail HTTPS server trust evaluation.
In this case error -1202 in the NSURLErrorDomain domain is NSURLErrorServerCertificateUntrusted, which means that server trust evaluation has failed. You might also receive a variety of other errors; Appendix A: Common Server Trust Evaluation Errors lists the most common ones.
From the Technical Note:
In most cases the best way to resolve a server trust evaluation
failure is to fix the server. This has two benefits: it offers the
best security and it reduces the amount of code you have to write. The
remainder of this technote describes how you can diagnose server trust
evaluation failures and, if it's not possible to fix the server, how
you can customize server trust evaluation to allow your connection to
proceed without completely undermining the user's security.
The particular bit that is germane to this question is the section on NSURLSession server trust evaluation:
NSURLSession allows you to customize HTTPS server trust evaluation by
implementing the -URLSession:didReceiveChallenge:completionHandler:
delegate method. To customize HTTPS server trust evaluation, look for
a challenge whose protection space has an authentication method of
NSURLAuthenticationMethodServerTrust. For those challenges, resolve
them as described below. For other challenges, the ones that you don't
care about, call the completion handler block with the
NSURLSessionAuthChallengePerformDefaultHandling disposition and a NULL
credential.
When dealing with the NSURLAuthenticationMethodServerTrust
authentication challenge, you can get the trust object from the
challenge's protection space by calling the -serverTrust method. After
using the trust object to do your own custom HTTPS server trust
evaluation, you must resolve the challenge in one of two ways:
If you want to deny the connection, call the completion handler block with
the NSURLSessionAuthChallengeCancelAuthenticationChallenge disposition
and a NULL credential.
If you want to allow the connection, create a credential from your
trust object (using +[NSURLCredential credentialForTrust:]) and
call the completion handler block with that credential and the
NSURLSessionAuthChallengeUseCredential disposition.
The upshot of all this is that if you implement the following delegate method, you can override server trust for a particular server:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler
{
if([challenge.protectionSpace.authenticationMethod
isEqualToString:NSURLAuthenticationMethodServerTrust])
{
if([challenge.protectionSpace.host
isEqualToString:#"domaintooverride.example"])
{
NSURLCredential *credential =
[NSURLCredential credentialForTrust:
challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
}
else
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
Note that you have to handle both the case of the host matching the one you want to override and all other cases. If you don't handle the "all other cases" part, the behavior result is undefined.
Find a trusted SSL certificate authority online that's offering a free 90 day trial for new certificates. Install the certificate on your server. You now have 90 days to develop your app to a point where you can make a decision as to whether or not it's worth it to pay money to "renew" the certificate. This is the best answer for me since my decision to use the self-signed certificate was financially motivated and 90 days gives me enough time develop my app to a point where I can decide if it's worth it to spend money on an SSL certificate or not. This approach avoids having to deal with the security implications of running a codebase that is tweaked to accept self-signed certificates. Sweet! Yay for bootstrapping!
Do yourself a huge favour and don't.
Start by reading the paper The most dangerous code in the world: validating SSL certificates in non-browser software, especially section 10, "Breaking or disabling certificate validation". It specifically calls out a Cocoa-related blog that specifically describes how to do what you ask.
But don't. Disabling SSL certificate checking is like introducing a ticking time bomb into your app. Sometime, someday, it will accidentally be left enabled, and a build will get into the wild. And on that day, your users will be put at serious risk.
Instead you should use a certificate, signed with an intermediate cert that you can install and trust on that specific device, which will allow the SSL validation to succeed without endangering any other device than your own (and only then, temporarily).
For Swift 3.0 / 4
If you would just like to allow any kind of self-signed certificates, you could use the following approach, to implement an URLSessionDelegate.
Apple provides additional information of how to use the URLSessionDelegate for all kinds of authentication methods: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/Articles/AuthenticationChallenges.html
At first implement the delegate method and assign an according delegate:
let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = urlSession.dataTask(with: urlRequest).resume()
Now implement the delegate's method
https://developer.apple.com/documentation/foundation/nsurlsessiondelegate/1409308-urlsession?language=objc
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.previousFailureCount == 0 else {
challenge.sender?.cancel(challenge)
// Inform the user that the user name and password are incorrect
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Within your authentication handler delegate method, you should check to see if the challenge protection space has an authentication type of NSURLAuthenticationMethodServerTrust
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust
// and if so, obtain the serverTrust information from that protection space.
&& challenge.protectionSpace.serverTrust != nil
&& challenge.protectionSpace.host == "yourdomain.com" {
let proposedCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(URLSession.AuthChallengeDisposition.useCredential, proposedCredential)
}
}
Still, you could adapt the acceptance of any self-signed cert for your provided domain to match to a very specific one. Make sure you added this certificate before to your build targets bundle. I named it here "cert.cer"
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.previousFailureCount == 0 else {
challenge.sender?.cancel(challenge)
// Inform the user that the user name and password are incorrect
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust
&& challenge.protectionSpace.serverTrust != nil
&& challenge.protectionSpace.host == "yourdomain.com" {
if let trust = challenge.protectionSpace.serverTrust,
let pem = Bundle.main.url(forResource:"cert", withExtension: "cer"),
let data = NSData(contentsOf: pem),
let cert = SecCertificateCreateWithData(nil, data) {
let certs = [cert]
SecTrustSetAnchorCertificates(trust, certs as CFArray)
var result=SecTrustResultType.invalid
if SecTrustEvaluate(trust,&result)==errSecSuccess {
if result==SecTrustResultType.proceed || result==SecTrustResultType.unspecified {
let proposedCredential = URLCredential(trust: trust)
completionHandler(.useCredential,proposedCredential)
return
}
}
}
}
completionHandler(.performDefaultHandling, nil)
}
Same as friherd's solution but in swift:
func URLSession(session: NSURLSession, task: NSURLSessionTask, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust{
let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust!)
completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential,credential);
}
}
just need add .cer to SecTrust and it pass on ATS
class NSURLSessionPinningDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust) {
if let trust = challenge.protectionSpace.serverTrust,
let pem = Bundle.main.path(forResource: "https", ofType: "cer"),
let data = NSData(contentsOfFile: pem),
let cert = SecCertificateCreateWithData(nil, data) {
let certs = [cert]
SecTrustSetAnchorCertificates(trust, certs as CFArray)
completionHandler(URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: trust))
return
}
}
// Pinning failed
completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil)
}
}
update xcode 9
var result:(message:String, data:Data?) = (message: "Fail", data: nil)
var request = URLRequest(url: url)
let sessionDelegate = SessionDelegate()
let session = URLSession(configuration: .default, delegate: sessionDelegate, delegateQueue: nil)
let task = session.dataTask(with: request){(data, response, error) in
}
task.resume()
the delegate task
class SessionDelegate:NSObject, URLSessionDelegate
{
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if(challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
{
print(challenge.protectionSpace.host)
if(challenge.protectionSpace.host == "111.11.11.11")
{
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential)
}
}
}
}
Here is the solution that worked for me.
You need to accept the connection in through the connection's delegate including both messages:
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
return [protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
Please note that with doing this, you're not checking the trustability of the certificate, so only the SSL encryption of the HTTPS connection is interesting, but the signing authority is not taking into consideration here, which can decrease security.
This Works fine for me to by pass self-signed :
Delegate : NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session **task**:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
completionHandler(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}
Perhaps a better way is to provide the user with the opportunity to accept the certificate confirming (visually) that the URL is accurate for the service being accessed. For example, if the host is entered into some app setting, test at the user's entry and let the user decide right there.
Consider that this "user confirm" tactic is used by Safari, thus condoned by Apple, it would make sense that it would be employed logically for other apps.
Suggest digging into NSErrorRecoveryAttempting (am doing no myself)
http://apple.co/22Au1GR
Get the host confirmed, then take the individual URL exclusion route mentioned herewithin. Depending upon the implementation it may also make sense to store the host as an exclusion for future reference.
This seems like something Apple would have implemented by nature in Cocoa but as of yet, I have not found an 'easy button'. Would have liked a "kLetUserDecide" flag on something in NSURL or NSURLSession instead of everyone having to implement the delegate method as well as the NSErrorRecoveryAttempting protocol.