Alamofire server trust policy does not work for me - ios

I would like to override the security on my server because we are testing something in the future. I create a global Alamofire manager and set the server trust policy but it does not work. I change my domain to blah but I double check to make sure that it is the same.
class HTTPManager: Alamofire.Manager {
static let sharedManager: HTTPManager = {
let serverTrustPolicies: [String: ServerTrustPolicy] = [
"qa.blah.me": .DisableEvaluation
]
let serverTrustPolicyManger = ServerTrustPolicyManager(policies: serverTrustPolicies)
let configuration = Timberjack.defaultSessionConfiguration()
let manager = HTTPManager(configuration: configuration, serverTrustPolicyManager: serverTrustPolicyManger)
return manager
}()
}
This is how I use it.
HTTPManager.sharedManager.request(request).responseJSON(completionHandler: completionHandler)
This is the error message I get:
Error: The certificate for this server is invalid. You might be connecting to a server that is pretending to be “qa.blah.me” which could put your confidential information at risk.
Suggestion: Would you like to connect to the server anyway?
What am I doing wrong and how can I fix it?
Thanks!
PS: Alamofire 3.0

try to instantiate manager as Alamofire.manager instead HTTPManager

You're most likely being hit by ATS so the ServerTrustPolicy is probably not even getting called. I'd recommend reading through the entire Security section of the Alamofire README to make sure you have things configured correctly.
A quick way to tell if you're ServerTrustPolicy is being called is to drop a breakpoint in the serverTrustPolicyForHost() method and see if it gets called.
There's also no reason anymore to not cut valid certificates. You can create free certificates at Let's Encrypt.

Related

iOS TLS/SSL Pinning using NSRequiresCertificateTransparency key in Info.plist

I want to secure my app against man-in-the-middle (mitm) attacks using SSL Pinning.
By default it is possible to use a proxy like Charles or mitmproxy to intercept traffic, and decrypt it using a self-signed certificate.
After extensive research, I found several options:
Adding NSPinnedDomains > MY_DOMAIN > NSPinnedLeafIdentities to Info.plist
Apple Documentation: Identity Pinning
Guardsquare: Leveraging Info.plist Based Certificate Pinning
Pros: Simple
Cons: App becomes unusable once Certificate/Private Key is renewed (typically after a few months)
Adding NSPinnedDomains > MY_DOMAIN > NSPinnedCAIdentities to Info.plist
Apple Documentation: same as above
Pros: Simple. No failure on Leaf Certificate renewal because Root CAs are pinned instead (expiration dates decades out)
Cons: Seems redundant as most root CAs are already included in the OS
Checking certificates in code URLSessionDelegate > SecTrustEvaluateWithError (or Alamofire wrapper)
Ray Wenderlich: Preventing Man-in-the-Middle Attacks in iOS with SSL Pinning
Apple Documentation: Handling an Authentication Challenge
Medium article: Everything you need to know about SSL Pinning
Medium article: Securing iOS Applications with SSL Pinning
Pros: More flexibility. Potentially more secure. Recommended by Apple (see Apple-link above).
Cons: A more laborious version of (1) or (2). Same Cons as (1) and (2) regarding leaf expirations / root CA redundancies. More complicated.
Adding NSExceptionDomains > MY_DOMAIN > NSRequiresCertificateTransparency to Info.plist
Apple documentation: Section Info.plist keys 'Certificate Transparency'
Pros: Very simple. No redundant CA integration.
Cons: Documentation is unclear whether this should be used for ssl pinning
After evaluation I came to the following conclusion:
Not suitable for a production app because of certificate expiration
Probably best balance between simplicity, security and sustainability – but I don't like the duplication of adding root CAs the system already knows
Too complicated, too risky, any implementation error may lock the app
My preferred way. Simple. Works in my tests but – unclear documentation.
I am tempted to use option (4) but I am not sure if this is really meant for ssl pinning.
In the documentation it says:
Certificate Transparency (CT) is a protocol that ATS can use to identify mistakenly or maliciously issued X.509 certificates. Set the value for the NSRequiresCertificateTransparency key to YES to require that for a given domain, server certificates are supported by valid, signed CT timestamps from at least two CT logs trusted by Apple. For more information about Certificate Transparency, see RFC6962.
and in the linked RFC6962:
This document describes an experimental protocol for publicly logging the existence of Transport Layer Security (TLS) certificates [...]
The terms "experimental protocol" and "publicly logging" raise flags for me and although flipping the feature on in the Info.plist seems to solve SSL pinning I am not sure if I should use it.
I am by no means a security expert and I need a dead simple solution that gives me decent protection while protecting me from choking my own app through possible expired / changed certificates.
My question:
Should I use NSRequiresCertificateTransparency for ssl pinning and preventing mitm-attacks on my app?
And if not:
What should I use instead?
PS:
This same question was essentially already asked in this thread:
https://developer.apple.com/forums/thread/675791
However the answer was vague about NSRequiresCertificateTransparency (4. in my list above):
Right, Certificate Transparency is great tool for verifying that a provided leaf does contain a set of SCTs (Signed Certificate Timestamp) either embedded in the certificate (RFC 6962), through a TLS extension (which can be seen in a Packet Trace), or by checking the OCSP logs of the certificate. When you make a trust decision in your app, I would recommend taking a look at is property via the SecPolicyRef object.
Additional side note:
My expectation from Apple as a security-aware company would have been, that pinning to root CAs was enabled by default, and that I would have to add exceptions manually, e.g. allow proxying with Charles on debug builds.
I hear Android does it that way.
I'm using SecTrustEvaluateWithError to evaluate the certificate. In case if certificate expired or any another case where evaluate return an error, I getting new one from the server. Certificate is stored and received witch keychain. One of the problem I faced with this solution was updating existing certificate at keychain because in apple documentation way to do it, is by using kSecValueRef but that one returns error whenever you try to update it. Instead cert is saved with kSecValueData.
So solution nr 3 (kind of) is used here, but in my case there is a socket connection instead.
First I connect to the socket with settings using CocoaAsyncSocket library
GCDAsyncSocketManuallyEvaluateTrust: NSNumber(value: true),
kCFStreamSSLPeerName as String: NSString("name")
next I use delegate to receive trust object
public func socket(_ sock: GCDAsyncSocket, didReceive trust: SecTrust, completionHandler: #escaping (Bool) -> Void)
next evaluate with existing one (from keychain), or update cert and repeat evaluation
if let cert = CertificateManager.shared.getServerCertificate() {
SecTrustSetAnchorCertificates(trust, [cert] as NSArray)
SecTrustSetAnchorCertificatesOnly(trust, true)
var error: CFError?
let evaluationSucceeded = SecTrustEvaluateWithError(trust, &error)
guard evaluationSucceeded else {
CertificateManager.shared.updateCertificate()
return
}
completionHandler(evaluationSucceeded)
} else {
CertificateManager.shared.updateCertificate()
}
Method for getting certificate is just a regular URLSession dataTask on domain that has certificate with URLSessionDelegate I get a URLAuthenticationChallenge from that object you can retrieve certificate and save it to keychain.
There is info from apple documentation how to store certificate
Best if you read thru it, but how I mentioned above I faced problems with that solution with updating existing one so there there are methods that I use for saving and retrieving certificate
add as data:
public func saveServerCertificate(_ certificate: SecCertificate, completion: #escaping () -> Void) throws {
let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: attribute]
let status = SecItemCopyMatching(query as CFDictionary, nil)
switch status {
case errSecItemNotFound:
let certData = SecCertificateCopyData(certificate) as Data
let saveQuery: [String: Any] = [kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: attribute,
kSecValueData as String: certData]
let addStatus = SecItemAdd(saveQuery as CFDictionary, nil)
guard addStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
completion()
case errSecSuccess:
let certData = SecCertificateCopyData(certificate) as Data
let attributes: [String: Any] = [kSecValueData as String: certData]
let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
guard updateStatus == errSecSuccess else { throw KeychainError.unhandledError(status: status) }
completion()
default:
throw KeychainError.unhandledError(status: status)
}
}
get and create certificate with data:
public func getServerCertificate(completion: #escaping (SecCertificate) -> Void) throws {
let query: [String: Any] = [kSecClass as String: kSecClassCertificate,
kSecAttrLabel as String: attribute,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
kSecReturnData as String: true]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
switch status {
case errSecItemNotFound:
throw KeychainError.noCertificate
case errSecSuccess:
guard let existingItem = item as? [String : Any],
let certData = existingItem[kSecValueData as String] as? Data
else {
throw KeychainError.unexpectedCertificateData
}
if let certificate = SecCertificateCreateWithData(nil, certData as CFData) {
completion(certificate)
} else {
throw KeychainError.unexpectedCertificateData
}
default:
throw KeychainError.unhandledError(status: status)
}
}
Additional resource for you is OWASP. It is good to follow their recommendations for all of your platforms.
https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning
As for your points:
The public key does not always change when the certificates change (leaf certs are usualy rotated yearly?). This is one of the advantages of pinning against the public key.
'Cons: Seems redundant as most root CAs are already included in the OS'
As you said here, pinning against the root cert is pointless, as the certificate is likely already trusted by the OS.
However, from the doc you linked to: 'A pinned CA public key must appear in a certificate chain either in an intermediate or root certificate. Pinned keys are always associated with a domain name, and the app will refuse to connect to that domain unless the pinning requirement is met.'
You would be pinning against the intermediate certificate here. For peace of mind you can do a test to print out the public key of your root certs + intermediate certs and prove they don't match.
'Too complicated, too risky, any implementation error may lock the app.' Apple provides an implementation in their tech note, and you can test all the codepaths yourself manually. Also, as owasp recommends, you can look into trust kit. I seems it has an implementation of this, and again here you have the option to just pin against the intermediate certificate (NOT ROOT) vs leaf node certs. Intermediate certificates typically last for 5-10 years.
I would hold off on this as personally I am not sure, I think this might just be an additional check you might want to use in addition to cert pinning. There also does not seem to be mention of this in the owasp document.
Personally, if I was writing a new app, I would go with option 1. Since android N, the OS provides a similar approach, which means you can also stay in sync with your android counterparts and they will also only have to update when you do and vice versa. https://developer.android.com/training/articles/security-config.html#CertificatePinning
I am not a security expert, but I am giving my thoughts based on my experience working for large coorperations that have their applications penetration tested. If you are really working on an app that requires high security, you really should have a penetration tester test your application. If you are in a big company, you may have a cyber team that can help.
No, you can’t do ssl certificate pining by using NSRequiresCertificateTransparency, it’s uses for client side TLS. If you want to implement pinning ,you can use server certificate pining to prevent MITM attacks.
Certificate pining
The difference is bellow
1) Client-side certificate transparency
For iOS apps, turning on client-side certificate transparency check is rather simple – you do nothing! Certificate transparency is enforced by default on devices running iOS 12.1.1 and higher. For devices running earlier versions of the iOS, you will need to set the NSRequiresCertificateTransparency option to YES in your Info.plist file.
2) Server-side certificate transparency
Certificate transparency has two aspects:
Pin the certificate: You can download the server’s certificate and bundle it into your app. At runtime, the app compares the server’s certificate to the one you’ve embedded.
Pin the public key: You can retrieve the certificate’s public key and include it in your code as a string. At runtime, the app compares the certificate’s public key to the one hard-coded in your code.
An SSL certificate with an SCT is definitely required. Make sure your server certificate is one with a valid SCT. Almost every CA these days issue certificates with SCTs

Alamofire 5 AF.upload() fails to send an image to a server with an invalid certificate

I am sending an image, using the following call:
AF.upload(...)
to a "Development server" that has an invalid certificate, and consequently, I get the following error:
NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be ...
I have already solved this problem for "AF.request(...)" calls; that is, I can perform "AF.request(...)" calls to a server with an invalid certificate using the following code:
#if DEBUGDEV
//To enable connections with wrong certificate
private let session: Session = {
let evaluators: [String: ServerTrustEvaluating] = [
"api.my.server.dev.api.group.com": DisabledEvaluator()
]
let manager = ServerTrustManager(allHostsMustBeEvaluated: false,
evaluators: evaluators)
let configuration = URLSessionConfiguration.af.default
return Session(configuration: configuration,
serverTrustManager: manager,
eventMonitors: [ AlamofireLogger() ])
}()
#else
private let session: Session = Session.default
#endif
And then calling:
let request = self.session.request(urlConvertible)
In addition I have modified the Info.plist file to contain "NSAppTransportSecurity -> NSExceptionDomains -> "api.my.server.dev.api.group.com" -> "NSThirdPartyExceptionAllowsInsecureHTTPLoads = false" and other entries that can be found in other StackOverFlow" posts.
However; the previous code works for "AF.request(...)", but not for "AF.upload(...)", so I would need a way to make "AF.upload(..)" work properly for servers with and invalid certificate.
Is it a way to "insert" the "Session" inside the "AF.upload(...)" call ? or
Is another way so that "AF.upload(...)" can connect to servers with an invalid certificate?
After reviewing and updating my question, I have realised that the answer is easy; that is, in the same way I do not call
AF.request(...)
but
self.session.request(...)
I can do the same with upload; that is, to call:
self.session.upload(...)
instead of calling:
AF.upload(...)

SFAuthenticationSession completion handler not called

I am trying to implement an approach to exchange cookies from Safari and App. I am using SFAuthenticationSession since the cookies sharing was disabled. I read through the topic and it seems this is the best solution to achieve this. There are not too many blogs or repos to use as an example.
I have implemented the changes redirect in the server side as following.
First I store the cookie as https://example.com/?cookie=12345. Then from the app I start an Authentication Session pointing to https://example.com/getcookie which redirects to customapp://dummy/cookies?cookie=12345
Once stated this. The swift implementation is the following (thanks to this):
let callbackUrl = "customapp://dummy/cookies"
let authURL = "https://example.com/getcookie"
self.authSession = SFAuthenticationSession(url: URL(string: authURL)!, callbackURLScheme: callbackUrl, completionHandler: { (callBack:URL?, error:Error? ) in
guard error == nil, let successURL = callBack else {
return
}
let cookie = self.parseQuery(url: (successURL.absoluteString), param: "cookie")
print(cookie!)
})
self.authSession?.start()
You may notice I am not interested on signing in but getting a cookie stored previously.
Can anyone please advice? My problem is that although the site is redirecting, the completion handler is not called, so I can't parse the callback url.
UPDATE
I found out I was missing the protocol in the Info.plist. After adding the custom protocol to it, the handler was called. Nevertheless, the handler was only called the second time I engaged the Authentication Session.
Any clue?

CredStore Perform Query error

I am running into an issue while doing API calls to my apps backend, every connection now prompts with
CredStore - performQuery - Error copying matching creds. Error=-25300, query={
atyp = http;
class = inet;
"m_Limit" = "m_LimitAll";
ptcl = http;
"r_Attributes" = 1;
srvr = "myappsurl.com";
sync = syna;
}
I am a little lost as I am not sure what is causing this, or what CredStore even does.
What purpose does CredStore serve in iOS?
This error occurs when trying to retrieve an URLCredential from URLCredentialStorage for an unknown URLProtectionSpace.
e.g.
let protectionSpace = URLProtectionSpace.init(host: host,
port: port,
protocol: "http",
realm: nil,
authenticationMethod: nil)
var credential: URLCredential? = URLCredentialStorage.shared.defaultCredential(for: protectionSpace)
produces
CredStore - performQuery - Error copying matching creds. Error=-25300, query={
class = inet;
"m_Limit" = "m_LimitAll";
ptcl = http;
"r_Attributes" = 1;
srvr = host;
sync = syna;
}
Give it a credential for the protection space:
let userCredential = URLCredential(user: user,
password: password,
persistence: .permanent)
URLCredentialStorage.shared.setDefaultCredential(userCredential, for: protectionSpace)
and the error goes away next time you try to retrieve the credential.
I am a little lost as I am not sure what is causing this, or what
CredStore even does. What purpose does CredStore serve in iOS?
Credential storage on iOS allows users to securely store certificate-based or password-based credentials on the device either temporarily or permanently to the keychain.
I suspect that you have some sort of authentication on your backend server and that server is requesting an authentication challenge to your app (for which no credential exists).
It can probably be safely ignored as returning nil from the URLCredentialStorage is a valid response
I'm not sure why do we get this error when perform requests with Alamofire, but if you do API requests with some token in HTTP headers, you maybe don't need credentials store at all. So we can disable it for our request:
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = ourHeaders
// disable default credential store
configuration.urlCredentialStorage = nil
let manager = Alamofire.SessionManager(configuration: configuration)
...
No errors after such change.
This same issue happens to me and I found that if your API URL does not contain a "/" at the end of URL then iOS does not send "Authorization" value to the server. Due to which you will see a message like posted in question in the console.
So Simply add "/" at the end of URL
https://example.com/api/devices/
In my case, I was not initialising Stripe SDK with API key.
STPPaymentConfiguration.shared().publishableKey = publishableKey
In case of any Stripe operation, we can print the error log, its easy to understand.
print(error.debugDescription)
This is transport error, let's add transport permission like this in plist file:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Be careful as that enables connection to any server from your app. Read more on App Transport Security before proceeding. See comment by #kezi
I edited the String that contains the URL to fix this issue:
var myUrl = "http://myurl.com"
myUrl = myUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)!
let url = URL(string: myUrl)
If you get this error, when using AVPlayer, just call .play() on main thread
The cause of me getting this error was due to me accidentally using two spaces between the "Bearer" and access token in my Authorization header.
Incorrect:
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
Correct:
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
Simple mistake, but it took a while to find it.
OK, I had this error, and fought with it for a long time (years) when interacting with my Ruby on Rails app.
I had default credentials set up as described in the accepted answer, but still got the error, and have been relying on a didReceiveChallenge response to supply the credentials - fortunately that worked as a work around.
But! I've just found the solution!
I was working on a hunch that that the protectedSpace fields did not match the Authorization challenge from the Ruby on Rails server - and I looked into the realm field, which seemed to be the only one that was being left undefined.
I started by printing out the server response headers, and although I was able to examine these, they did not include the WWW-Authorization field that would have included the realm field.
I thought this was maybe because my Rails app wasn't specifying the realm, so I started looking at the Rails side of things.
I found I could specify the realm in the call to,
authenticate_or_request_with_http_basic
...which I am using for HTTP Basic authentication.
I wasn't specifying a realm already, so added one,
authenticate_or_request_with_http_basic("My Rails App")
I then added the corresponding string to the protectionSpace,
NSURLProtectionSpace *protectionSpace =
[[NSURLProtectionSpace alloc] initWithHost:#"myrailsapp.com"
port:443
protocol:NSURLProtectionSpaceHTTPS
realm:#"My Rails App"
authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
Voila! That worked, and I no longer get the,
CredStore - performQuery - Error copying matching creds. Error=-25300
Even after specifying the realm in the Rails app, I still don't see it passed in the HTTP header, I don't know why, but at least it works.
The error may also be caused by a Content Security Policy (CSP) that may be too restrictive. In our case, we needed a CSP that is more or less completely open and allows everything. Keep in mind that opening the CSP can be a great security issue (depending on what exactly you're doing in the app).
I got this issue when I tried to open a http-page inside a web-view. But this page contained an popup which was opened first.
When backend team removed this popup everything became OK.
My issue was base64 encoding of an image that was being sent with a rest call
I had previously used
let strBase64 = imageData.base64EncodedString(options: .lineLength64Characters)
But 50% of the time I would get the error above.
I used the following instead which solved my problem...
let strBase64 = imageData.base64EncodedString()
Had the same issue with Twitter sign in. Turned out I used the the wrong API key.
I remove .cURLDescription
on
AF.request(url)
and that log is gone
When working with the Stripe IOS SDK, I found that I should have added the publishable key from stripe.
This is set in AppDelegate, as found in https://stripe.com/docs/development/quickstart, step 2.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
StripeAPI.defaultPublishableKey = "pk_test_....."
return true
}
let credentialData = "\(user):\(password)".data(using: String.Encoding.utf8)!
let base64Credentials = credentialData.base64EncodedString(options: [])
let headers = ["Authorization": "Basic \(base64Credentials)"]
Alamofire.request(url, method: .get, parameters: params,encoding: URLEncoding.default,headers: headers)
.responseJSON{
response in
guard let value = response.result.value else {return}
print(value)
}

Alamofire ServerTrustPolicy Certificate Pinning Not Blocking Charles Proxy Swift 3

I've searched far and wide and have not been able to find an answer for my question. To make our app more secure, we've been told to use "certificate pinning". We already make use of the Alamofire library for all our API calls, so it seems natural to use the ServerTrustPolicyManager included as a means to implement certificate pinning. I've included the proper certificates in my app bundle, and here is the code I use to configure my SessionManager for Alamofire:
let url = "https://www.mycompany.com"
var manager: SessionManager? {
let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
)
let serverTrustPolicies: [String: ServerTrustPolicy] = [
url: serverTrustPolicy
]
let config = URLSessionConfiguration.default
return SessionManager(configuration: config, serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies))
}
Now when I want to make an API request, I have this method:
func request(routerRequest request: URLRequestConvertible) -> DataRequest {
assert(url != "", "A base URL string must be set in order to make request")
print("URL: \(url) : \(request)")
return (manager ?? SessionManager.default).request(request)
}
Now the problem that I'm having is that this still works when I use something like a Charles Proxy ... all the requests are still going through. Shouldn't certificate pinning prevent something like a Charles Proxy from being able to work because the certificates won't match?
I've tested other apps that properly use certificate pinning, and they will block any kind of proxy (Charles or other) from making a connection. If I try running an app like Uber or my Wells Fargo banking app while I have Charles enabled, every request will get rejected and I'll see an error that says something like "Couldn't complete request, ssl certificate is invalid" (that's not verbatim).
I feel like there is a step that I'm missing after configuring my SessionManager. Most of the documentation that I've read and help I've come across seem to imply that after configuring the manager to enable certificate pinning, it should reject any request with an invalid certificate. Can anyone help me? What am I missing?
I appreciate any and all help. Thanks in advance!
I'm going to answer my own question, only because I want to possibly help anyone else with this same problem in the future. When I was configuring the serverTrustPolicies above, you create a dictionary of String : ServerTrustPolicy, my error lied in the String for the server name.
let serverTrustPolicies: [String: ServerTrustPolicy] = [
url: serverTrustPolicy
]
My url property was the baseURL we use for our API, which was https://www.mycompany.com/api - this was causing all my issues. Once I adjusted that to just be the domain www.mycompany.com, the pinning worked just as expected! Now when I run Charles Proxy with pinning enabled, I get all my requests rejected, and Charles puts out an error that says "No request was made, possibly the SSL Certificate was rejected."
Instead of having to do any serious String manipulation, I added this extension to use in the future - in case you have multiple baseURL's that you need to extract the domain out of:
extension String {
public func getDomain() -> String? {
guard let url = URL(string: self) else { return nil }
return url.host
}
}
Now you can do something like this:
let serverTrustPolicies: [String: ServerTrustPolicy] = [
url.getDomain() ?? url : serverTrustPolicy
]
Hope this helps someone else in the future! Goodluck

Resources