How to check if a Certificate is installed and trusted on iOS - ios

I've an app which prompts the user to download and install a Configuration Profile. The profile contains a Root CA embedded inside it. I want to check if the Configuration Profile is installed on the device, after it got downloaded.
After going through the Apple Developer Forums, I realised that one way to do this is to check if the Certificate embedded in the profile is installed and trusted by the user. If it is, it would implicitly mean (with exceptions) that the Configuration profile was installed by the user.
I went through this link where the OP had similar requirement but apparently it is not able to detect if the certificate is already installed.
Does anybody have experience doing this?

You cant use SecTrustEvaluateAsyncWithError to recognise whether the certificate is installed(trusted) on not e.g.:
// Load cert
guard let filePath = Bundle.main.path(forResource: "your_cert", ofType: "crt"),
let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)),
let certificate = SecCertificateCreateWithData(nil, data as CFData)
else {
return
}
// Check
var secTrust: SecTrust?
if SecTrustCreateWithCertificates(certificate, SecPolicyCreateBasicX509(), &secTrust) == errSecSuccess, let trust = secTrust {
SecTrustEvaluateAsyncWithError(trust, .main) { trust, result, error in
print("Cert is", result ? "installed" : "not installed")
}
}

Related

Connecting to the Secure Content in IOS

I am trying to connect to the portal object with the authenticated user which is cached and used throughout the app session, to provide the app with a view of a portal that is centered around a single user.
When the app is restarted, the credential must be reinstated, or the user must repeat the authentication process.
But every time when I connect it asks for username and password, I actually want to embed that into the code.
Any workarounds?
Below is my code.
self.portal = AGSPortal(url: URL(string: "https://www.arcgis.com")!, loginRequired: false)
self.portal.credential = AGSCredential(user: "theUser", password: "thePassword")
self.portal.load() {[weak self] (error) in
if let error = error {
print(error)
return
}
if self?.portal.loadStatus == AGSLoadStatus.loaded {
let fullName = self?.portal.user?.fullName
print(fullName!)
}
}
You can use AGSCredentialCache's enableAutoSyncToKeychainWithIdentifier:accessGroup:acrossDevices:accessible: to store credentials in the Keychain and when you re-launch the app, it won't prompt again. Please call this function at the start of the application using AGSAuthenticationManager.shared().credentialCache.
Regards,
Nimesh

How to verify X.509 certificate was signed by another certificate?

The story: I call a request where I am getting a JWS token which I parse with the JOSESwift library. In the response I have a x5u parameter, which is a URL pointing to a certificate, which was used to sign the payload. I download the certificate and using JOSESwift I verify that. With some simplification I am doing the following:
let serverCert = // Getting the downloaded certificate as a SecCertificate object
var publicKey: SecKey!
publicKey = SecCertificateCopyKey(serverCert)
let rsaVerifier = RSAVerifier(algorithm: .ES256, publicKey: publicKey)
if let headerVerifier = Verifier(verifyingAlgorithm: .RS256, publicKey: rsaVerifier.publicKey) {
do {
_ = try jws.validate(using: headerVerifier)
print("Verifying success")
} catch {
print("Verifying failed with error:", error)
}
}
Question1: This is working nice so far. Now I want to verify that the downloaded certificate was indeed signed by a specific certificate I store locally in my application. And that's where I am stuck, that I am unable to find out how it could be done.
Question2: A requirement for the functionality is that the locally stored certificate can be self-signed, but also it can be some other certificate from a chain. So basically I should trust the locally stored certificate regardless it is self-signed or not. Is it doable, or when verifying we should always know about the root certificate ?
[rootCert] <--signed by-- [localCert] <--signed by-- [receivedCert] // We have only the local and receivedCert
My thoughts/ What I tried: My first thought was to use a SecTrust object, with setting the locally stored certificate as the trust anchor, and use the SecTrustEvaluateWithError(_:_:) to check if it's is a correct chain(what I conclude eventually will check also if the received certificate was signed by the one I store locally). I have seen working this in SSLPinning implementations, but somehow it is returning me false, even though I double checked that the certificates are correct. Here is what I am doing:
let serverCert = // Getting my server cert as a SecCertificate object
let localCert = // Getting my server cert as a SecCertificate object
let policy = SecPolicyCreateBasicX509()
var optionalTrust: SecTrust?
let status = SecTrustCreateWithCertificates([serverCert] as AnyObject,
policy,
&optionalTrust)
guard status == errSecSuccess else { return }
let trust = optionalTrust! // Safe to force unwrap now
SecTrustSetAnchorCertificates(trust, [localCert] as CFArray)
var error: CFError?
print("Veryfing result: ", SecTrustEvaluateWithError(trust, &error))
print(error)
Is it a good approach to verify that the received certification was signed by the local one ? If yes than what I am doing wrong that I am getting false ? If the approach is bad, what else can I try here ?
I found a similar question, but it is in Java(but maybe can give a hint what I am trying to accomplish): Verify x509 signature Java. However it also mentions the root certificate, so it again rises me the Question2 if it's doable without the root/self-signed certificate.
Update
As suggested in the comments, I am printing the error I am getting from SecTrustEvaluateWithError:
Error Domain=NSOSStatusErrorDomain Code=-25318 "“AuthInfoKey:18”
certificate is not trusted"
UserInfo={NSLocalizedDescription=“AuthInfoKey:18” certificate is not
trusted, NSUnderlyingError=0x280efb9c0 {Error
Domain=NSOSStatusErrorDomain Code=-25318 "Certificate 0
“AuthInfoKey:18” has errors: Unable to build chain to root (possible
missing intermediate);" UserInfo={NSLocalizedDescription=Certificate 0
“AuthInfoKey:18” has errors: Unable to build chain to root (possible
missing intermediate);}}}
The issues seems reasonable as I do not have access to the root certificate as mentioned above, I want to verify an intermediate certificate.
So again my question is: it possible to check the serverCert(an intermediate certificate) was signed by my local certificate(an other intermediate certificate) with SecTrustEvaluateWithError ? For me it seems it is working only if one has also the root certificate.

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(...)

Issue Saving Certificate to iOS Keychain -25300 (not found) if deleting, but -25299 (duplicate item) if adding

I've hit an interesting issue with the Apple keychain and am wondering what I am doing wrong.
func saveCert(accessGroup: String? = nil, certData: Data, label: String? = nil) -> Error? {
var query = createKeychainAddQueryDict()
if let accessGroup = accessGroup {
query[String(kSecAttrAccessGroup)] = accessGroup
}
query[String(kSecValueData)] = certData
query[String(kSecClass)] = kSecClassCertificate
if let label = label {
query[String(kSecAttrLabel)] = label
}
var status = SecItemDelete(query as CFDictionary)
if status != noErr {
print("Error deleting cer from keychain. Error: \(status)")
}
let resultCode = SecItemAdd(query as CFDictionary, nil)
return getErrorFromKeychainCode(code: resultCode)
}
I'm saving a self signed certificate, but I've verified the serial number is different for each item I'm trying to store.
I get a -25300 error (cannot find item) when I try to delete the cert out, but I get a -25299 error (duplicate item already exists) when I try to save into the keychain.
I'm stumped as to why or how, loading or deleting the key out of that location are both failing, and saving is declaring the position is taken.
Any insight? I've experimented with hardcoding a number of random labels that I've never used before, and they too get the duplicate entry error.
I found two solutions:
Request values for a particular key later. Use async delayed. From time to time the Keychain doesn't provide the result with -25300. The keychain is an SQLite database too. It seems the database is currently busy. So, request the data later.
You have already written something into this key but, you used another protection level. To avoid this, use keys with protection level inside its name. E.g., someKey into someKey-afterFirstUnlock
I tried lots of things, and none of them worked. I eventually found out that the kSecCertificate class uses the issuer and serial number attributes to compute its uniqueId.
Because I'm using a self-signed certificate the SecCertificateCreateWithData operation fails with result nil. I believe this is causing all my certificates to evaluate to the same empty - empty id. I tried storing this same data in a kSecGenericPassword, and set a distinct account attribute, and the issue has gone away.

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