I want to perform a certificate or public key pining in my iOS app. I am not using Alamofire only the standard requests Apple provide. How I can make this, I read how to do it with alomofire but I am not using it. I want the standard way apple giving us but if it’s possible someone to give me more examples of what is happening and how. What the is the best way, certificate or public key and how to implement it.
You can use pinning on iOS, is not so terrible but you must search on Appel docs:
(and I Opened a TSI with ADC for details...)
basically You have to implement delegate methods:
func urlSession(_: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
{
print("got Challenge")
let serverTrust = challenge.protectionSpace.serverTrust
let areEqual = CertificateHelper.certIsValid(CERT, serverTrust: serverTrust!)
// Set SSL policies for domain name check
let policies = NSMutableArray()
policies.add(SecPolicyCreateSSL(true, (challenge.protectionSpace.host as CFString?)))
SecTrustSetPolicies(serverTrust!, policies);
// Evaluate server certificate
var result = SecTrustResultType.invalid
SecTrustEvaluate(serverTrust!, &result)
let isServerTrusted: Bool = (
result == SecTrustResultType.unspecified // unspecified means implicitely go on (see help with alt click on SecTrustEvaluate)
|| result == SecTrustResultType.proceed
)
if isServerTrusted && areEqual {
let credential = URLCredential(trust: serverTrust!)
let certificate = credential.certificates // debug.
// ok
completionHandler(.useCredential, credential)
} else {
// fail:
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
Related
I have just shifted to Alamofire 5.
Earlier I used URLSession and Certificate Pinner and to handle auth challenge I used delegate method of URLSessionDelegate with hash values
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,
completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
print("being challanged! for \(challenge.protectionSpace.host)")
guard let trust = challenge.protectionSpace.serverTrust else {
print("invalid trust!")
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(trust: trust)
let pinner = setupCertificatePinner(host: challenge.protectionSpace.host)
if (!pinner.validateCertificateTrustChain(trust)) {
print("failed: invalid certificate chain!")
challenge.sender?.cancel(challenge)
}
if (pinner.validateTrustPublicKeys(trust)) {
completionHandler(.useCredential, credential)
} else {
didPinningFailed = true
print("couldn't validate trust for \(challenge.protectionSpace.host)")
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
Having moved to Alamofire 5, there is no method sessionDidReceiveChallenge which was available in earlier version.
I tried:
private let session: Session = {
let manager = ServerTrustManager(allHostsMustBeEvaluated: true, evaluators:
["devDomain.com": DisabledTrustEvaluator(),
"prodDomain.com": PublicKeysTrustEvaluator()])
let configuration = URLSessionConfiguration.af.default
return Session(configuration: configuration, serverTrustManager: manager)
}()
But I get error:
Error Domain=Alamofire.AFError Code=11 "Server trust evaluation failed due to reason: No public keys were found or provided for evaluation."
Update:
I'd still prefer a way to parse it using 256 fingerprint only, as we get domains and its hashes in first api call.
First you need a ServerTrustEvaluating that handle the certificate pinning a simple implement would be something similar to
public final class CertificatePinnerTrustEvaluator: ServerTrustEvaluating {
public init() {}
func setupCertificatePinner(host: String) -> CertificatePinner {
//get the CertificatePinner
}
public func evaluate(_ trust: SecTrust, forHost host: String) throws {
let pinner = setupCertificatePinner(host: host)
if (!pinner.validateCertificateTrustChain(trust)) {
print("failed: invalid certificate chain!")
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
if (!pinner.validateTrustPublicKeys(trust)) {
print ("couldn't validate trust for \(host)")
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
}
}
To be able to use the same evaluator I would suggest to subclass ServerTrustManager to return the same evaluator I did it like this:
class CertificatePinnerServerTrustManager: ServerTrustManager {
let evaluator = CertificatePinnerTrustEvaluator()
init() {
super.init(allHostsMustBeEvaluated: true, evaluators: [:])
}
open override func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
return evaluator
}
}
after that you should be ready to go by creating the session and passing the manager to it
private let session: Session = {
let trustManager = CertificatePinnerServerTrustManager()
return Session(serverTrustManager: trustManager)
}()
My reference was the method urlSession(_:task:didReceive:completionHandler:) in Alamofire source in SessionDelegate.swift at line 86 (Alamofire V5.2.1)
If you want to pin with public keys you need to provide the certificates from which to parse those public keys in the bundle of your app, or otherwise provide them to PublicKeysTrustEvaluator.
How can I embed a custom CA SSL certificate in an application that connects to a local server?
This certificate should work like any CA certificate installed in the system, but embedded in this application instead of requiring the user to install it system-wide, so it won't show a security warning and only works with our local servers through the application
The need for this is to comply with ATS without needing the user to do any further configuration like downloading and installing the CA Certificate manually
I assume you're working with a self-signed certificate, in which case, what you're looking for is SSL pinning.
Depending on whether you're using a network lib or not, this might work differently, but what you want to do is store a public cert, and then handle the authentication challenge manually:
https://developer.apple.com/documentation/foundation/url_loading_system/handling_an_authentication_challenge
For example:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let trust = challenge.protectionSpace.serverTrust, SecTrustGetCertificateCount(trust) > 0 else {
// This case will probably get handled by ATS, but still...
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Or, 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
}
}
completionHandler(.cancelAuthenticationChallenge, nil)
}
fileprivate func pinnedKeys() -> [SecKey] {
var publicKeys: [SecKey] = []
if let pinnedCertificateURL = Bundle.main.url(forResource: "infinumco", withExtension: "crt") {
do {
let pinnedCertificateData = try Data(contentsOf: pinnedCertificateURL) as CFData
if let pinnedCertificate = SecCertificateCreateWithData(nil, pinnedCertificateData), let key = publicKey(for: pinnedCertificate) {
publicKeys.append(key)
}
} catch (_) {
// Handle error
}
}
return publicKeys
}
I have a couple of other examples how to handle this scenario here:
https://github.com/Adis/swift-ssl-pin-examples
I am uploading png Images->Base64->jsonData in URLRequest.httpBody size around 6MB. I am using a static NSURLSession with default Configuration and uploading using dataTask on urlsession. Sometimes its successfully uploaded to server sometimes its not and getting below error and nothing is printing at server side. I am not making parallel calls. We are using SSL pinning and handling authentication challenges proper so no SSL authentication error.
iOS device 11.3 and XCode 10 we are using.
Task <58BF437E-7388-4AE4-B676-2485A57CB0CD>.<10> finished with error - code: -999
private lazy var configuration: URLSessionConfiguration = {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = TimeInterval(120)
configuration.timeoutIntervalForResource = TimeInterval(120)
return configuration
}()
private lazy var urlSession = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
func invokeService(methodName : String, collectionName: String, queryDictionary: [String:AnyObject]! = nil, httpMethod: String! = nil) {
// Set up the URL request
let baseUrl: String = WebServiceConstants.hostUrl.qaUrl + "/\(collectionName)" + "/\(methodName)"
// let baseUrl: String = WebServiceConstants.hostUrl.demoUrl + "/\(collectionName)" + "/\(methodName)"
// let baseUrl: String = WebServiceConstants.hostUrl.prod_Url + "/\(collectionName)" + "/\(methodName)"
guard let url = URL(string: baseUrl) else {
return
}
var urlRequest = URLRequest(url: url)
// set up the session
// let configuration = URLSessionConfiguration.default
// configuration.timeoutIntervalForRequest = TimeInterval(120)
// configuration.timeoutIntervalForResource = TimeInterval(120)
//
// let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
urlRequest.httpMethod = httpMethod
do {
urlRequest.httpBody = try JSONSerialization.data(withJSONObject: queryDictionary, options: []) // pass dictionary to nsdata object and set it as request body
}
catch _ {
}
urlRequest.setValue(WebServiceConstants.HTTPStrings.contentTypeJSON, forHTTPHeaderField: WebServiceConstants.HTTPStrings.contentTypeHeader)
// print(queryDictionary)
if AppController.sharedInstance.isAlreadyLogin() && (KeychainWrapper.standard.string(forKey: Constants.UserDefaultKeys.authorizationHeaderValue) != nil) {
let authorizationHeaderValue = WebServiceConstants.HTTPStrings.authorizationHeaderValue + KeychainWrapper.standard.string(forKey: Constants.UserDefaultKeys.authorizationHeaderValue)!
urlRequest.setValue(authorizationHeaderValue, forHTTPHeaderField: WebServiceConstants.HTTPStrings.authorizationHeader)
}
let _ = urlSession.dataTask(with: urlRequest, completionHandler: { [unowned self]
(data, response, error) in
//print(response)
if error != nil {
if error?._code == NSURLErrorTimedOut {
// print(error?.localizedDescription)
let userInfo = [
NSLocalizedDescriptionKey: BWLocalizer.sharedInstance.localizedStringForKey(key:"App_Timeout_Message")
]
let errorTemp = NSError(domain:"", code:-1001, userInfo:userInfo)
self.delegate?.didFailWithError(errorObject: errorTemp)
} else if error?._code == NSURLErrorNotConnectedToInternet {
let userInfo = [
NSLocalizedDescriptionKey: BWLocalizer.sharedInstance.localizedStringForKey(key:"Internet_Not_Available")
]
let errorTemp = NSError(domain:"", code:-1001, userInfo:userInfo)
self.delegate?.didFailWithError(errorObject: errorTemp)
}
else if error?._code == NSURLErrorCancelled {
// canceled
print("Request is cancelled") // Control reaches here on Finished with Error code = -999
self.delegate?.didFailWithError(errorObject: error!)
}
else {
self.delegate?.didFailWithError(errorObject: error!)
}
} else {
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
self.delegate?.didReceiveResponse(responseObject: json as AnyObject)
//Implement your logic
print(json)
}
} catch {
self.delegate?.didFailWithError(errorObject: error)
}
}
}).resume()
}
*Added SSL Certificate pinning code *
extension WebserviceHandler : URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let trust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(trust: trust)
let pinner = setupCertificatePinner() // adding CertificateHash
if (!pinner.validateCertificateTrustChain(trust)) {
challenge.sender?.cancel(challenge)
}
if (pinner.validateTrustPublicKeys(trust)) {
completionHandler(.useCredential, credential)
}
else {
completionHandler(.cancelAuthenticationChallenge, nil)
let popUp = UIAlertController(title: "", message: BWLocalizer.sharedInstance.localizedStringForKey(key:"Certificate_Pining_Fail_Message"), preferredStyle: UIAlertController.Style.alert)
popUp.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {alertAction in popUp.dismiss(animated: true, completion: nil)}))
popUp.addAction(UIAlertAction(title: BWLocalizer.sharedInstance.localizedStringForKey(key:"Action_Go"), style: UIAlertAction.Style.default, handler: { action in
let urlToOpenAppStorepage = URL(string: WebServiceConstants.hostUrl.appstore_url)
let objApp = UIApplication.shared
objApp.openURL(urlToOpenAppStorepage!)
}))
UIApplication.shared.keyWindow?.rootViewController?.present(popUp, animated: true, completion: nil)
}
}
}
func setupCertificatePinner() -> CertificatePinner {
// let baseUrl: String = WebServiceConstants.hostUrl.dev_URL
let baseUrl: String = WebServiceConstants.hostUrl.qaUrl
// let baseUrl: String = WebServiceConstants.hostUrl.demoUrl
// let baseUrl: String = WebServiceConstants.hostUrl.prod_Url
let pinner = CertificatePinner(baseUrl)
/*
You will see something like this:
being challanged! for www.google.co.nz
hash order is usually most specific to least, so the first one is your domain, the last is the root CA
Production
hash: 8U/k3RvTcMVSafJeS9NGpY4KDFdTLwpQ/GUc+lmPH/s=
hash: 4vkhpuZeIkPQ+6k0lXGi7ywkVNV55LhVgU0GaWWMOdk=
hash: jZzMXbxSnIsuAiEBqDZulZ/wCrrpW9bRLMZ6QYxs0Gk=
hash: uthKDtpuYHgn+wRsokVptSysyqBzmr4RP86mOC703bg=
you might need to change the has below to be the second one in the list for the code to pass
QA
hash: LX6ZGwP3Uii+KCZxDxDWlDWijvNI6K/t2906cUzKYM4=
hash: 4vkhpuZeIkPQ+6k0lXGi7ywkVNV55LhVgU0GaWWMOdk=
hash: jZzMXbxSnIsuAiEBqDZulZ/wCrrpW9bRLMZ6QYxs0Gk=
*/
pinner.debugMode = true
pinner.addCertificateHash(WebServiceConstants.HTTPStrings.hashKey)
return pinner
}
Using pinning library : https://github.com/nicwise/certificatepinner
I see several bugs in that code.
The very first line in your authentication handler is going to cause failures if you're behind an authenticated proxy.
It will also fail if the server wants any sort of HTTP auth password or OAuth credential.
It will also fail in a number of other situations.
You should never cancel an authentication request unless the request is actually bad in some way. Canceling the authentication request also prevents the operating system from handling it transparently for you if you can. The only situation where you should cancel an authentication request is if you check a cert or whatever and it is actually invalid. Otherwise, you should generally trust the OS to do the right thing by default when you request external handling.
So use default handling unless the authentication method is server trust.
The code does not check the authentication method at all. You should not be doing any checks unless the authentication method is server trust, for all of the reasons listed above. If it is anything else, use default handling. Always.
The next if statement has two problems:
It provides a new state for the authentication request without returning. This means you can call the completion handler afterwards, which could cause crashes and other misbehavior.
It is calling methods on the challenge sender that are intended to affect behavior. That's how you used to do it with NSURLConnection, but you should never call any methods on the challenge sender (other than possibly to see if it is an object of your own creation, if you are using custom NSURLProtocol classes) with NSURLSession, because it can cause all sorts of problems, up to and including crashes. (See the giant warning in the documentation for NSURLAuthenticationChallenge's -sender method, or, for that matter, the two paragraphs before that warning.)
I'm not entirely sure I trust the pinning code, either. It looks like it passes if any key within the chain of trust is a trusted key, whereas typically pinning requires that the last (leaf) key in the chain of trust be a trusted key.
The security advice in that pinning code is also dubious. You probably shouldn't be pinning to a certificate, but rather to the key inside the certificate. Pinning the leaf cert's key is entirely appropriate, and is really the only appropriate choice, because it is typically the only one whose key is actually under your direct control. If you reissue the cert, that's no big deal, because you should be reissuing with the same key as before, unless your key has been compromised in some way.
Alternatively, you can add a trust layer if you want, but this either requires running your own custom root (which would require changing the validateCertificateTrustChain method to add your custom root cert while validating the chain of trust) or convincing a CA to sell you a cert that can sign other certs, which costs $$$$. Neither of these options seems very practical.
Those issues make me a little bit concerned about the library as a whole, but I don't have time to audit it. You should probably ask around and see if anybody has done a thorough audit of the library in question, as it is notoriously easy to make mistakes when writing code that works with TLS keys, and I'd hate to see you run into security problems later.
After you fix all of the bugs listed above, if you're still having problems, come back and ask another question with updated code. Also, please also ask the code pinning project in question to fix their code snippets, as they seem to contain the same bugs. :-) Thanks.
I am using swift 3 and hitting a web service for the first time. My web service runs over HTTPS and I want to test with encryption in place.
Here's my code so far:
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: webService.getLoginUrl())!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error == nil {
do {
if let json =
try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]{
//Implement your logic
print(json)
}
} catch {
print("error in JSONSerialization")
}
} else {
print(error!.localizedDescription)
}
})
task.resume()
When I run this against my test server, which is self-signed, I get:
The certificate for this server is invalid. You might be connecting to a server that is pretending to be “10.0.0.51” which could put your confidential information at risk.
So what I'd like to do is accept all certificates when testing, but not in production.
I've found a couple of sites like:
http://www.byteblocks.com/Post/Use-self-signed-SSL-certificate-in-iOS-application
https://github.com/socketio/socket.io-client-swift/issues/326
But these appear to predate swift 3.
How do I solve this problem?
After much research, I learned about how delegates work with URLSession objects in swift 3. Too many pieces to post a link, but in the end, this was the most helpful: https://gist.github.com/stinger/420107a71a02995c312036eb7919e9f9
So, to fix the problem, I inherited my class from URLSessionDelegate and then added the following function:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
//accept all certs when testing, perform default handling otherwise
if webService.isTesting() {
print("Accepting cert as always")
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
else {
print("Using default handling")
completionHandler(.performDefaultHandling, URLCredential(trust: challenge.protectionSpace.serverTrust!))
}
}
The isTesting() call determines if I'm using the test server, and then we accept all certificates if we're in testing mode.
I am trying to get rest data to iOS app, and I use:
var rest_url = "http://192.168.0.1:8000/rest/users/"
let url: NSURL = NSURL(string: rest_url)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
if(error != nil) {
println(error.localizedDescription)
}
println(data)
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSDictionary!
But I think I can't access my server like this. Does anyone know how I can access my server from the iOS simulator?
Do you have App Transport Security Settings in your Info.plist file?
If no then for debugging purpose you can set them like
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Such settings allow issuing requests to any server.
But don't do so for a release version. It is insecure.
Make sure that your IP of your Mac is 192.168.0.1. So your url could be
var rest_url = "http://YOUR MAC IP:8000/rest/users/"
If you have a server running on the machine where you iOS simulator is running, then you need to choose 'http://127.0.0.1' as the URL.
In your case it will be :
var rest_url = "http://127.0.0.1:8000/rest/users/"
For people finding this thread because they can't connect to localhost due to an invalid certificate: in your URLSessionDelegate you should respond to the URLAuthenticationChallenge with the following delegate method:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
func defaultAction() { completionHandler(.performDefaultHandling, nil) }
// Due to localhost using an invalid certificate, we need to manually accept it and move on
guard challenge.protectionSpace.host.hasPrefix("localhost") else { return defaultAction() }
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else { return defaultAction() }
guard let trust = challenge.protectionSpace.serverTrust else { return defaultAction() }
completionHandler(.useCredential, URLCredential(trust: trust))
}
Maybe you can replace 192.168.0.1 with localhost, when debugging with iOS simulator (that is, real devices should use your server's IP).
I also cannot access my test server using IP address on simulator. But when I am using localhost or 120.0.0.1, the simulator can work well with my test server.