NSURLAuthenticationMethodServerTrust validation for server certificate - ios

I am handling https authentication for server based on its name but i want to trust server based on a certificate which server gives me. how can i do this ,any help ??
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if ([challenge.protectionSpace.authenticationMethod
isEqualToString:NSURLAuthenticationMethodServerTrust])
{
// we only trust our own domain
if ([challenge.protectionSpace.host isEqualToString:#"www.serverpage.com"])
{
NSURLCredential *credential =
[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}
}
[challenge.sender continueWithoutCredentialForAuthenticationChallenge:challenge];
}
I searched web and found most of the answers are just accepting any server authentication without validating .

NSURLConnection has a builtin method for doing SSL with non trusted certs. See the SO answer here. Please make sure you do not use it in production code as it is vulnerable to MITM attacks.

You need an encapsulate a server certificate inside your app.
When your app starts, you need to extract a Public Key from an encapsulated server certificate.
When run in delegate's callback URLSession:didReceiveChallenge:challenge:completionHandler
you need to iterate through obtained certificates from supplied challenge.protectionSpace.serverTrust
and do comparation with Public Key you extracted before.
Best thing - also do the same with an Issuer certificate - include original of it in your app, and investigate it with obtained one from challenge.protectionSpace.serverTrust
next code snippet is demonstrating an extraction of the Public Key from a certificate:
SecKeyRef getPKeyFromCert(SecCertificateRef cert) {
SecTrustRef newtrust = NULL;
CFMutableArrayRef certs = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
CFMutableArrayRef newPolicies = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
CFArrayAppendValue(certs, cert);
if (SecTrustCreateWithCertificates(certs, newPolicies, &newtrust) == errSecSuccess) {
return SecTrustCopyPublicKey(newtrust);
}
return NULL;
}
next code snippet is demonstrating an iteration through the certificates, supplied via the protectionSpace:
SecTrustRef trustRef = challenge.protectionSpace.serverTrust;
CFIndex count = SecTrustGetCertificateCount(trustRef);
CFIndex i = 0;
for (i = 0; i < count; i++) {
SecCertificateRef certRef = SecTrustGetCertificateAtIndex(trustRef, i);
SecKeyRef publicKey = getPKeyFromCert(certRef);
// do the magic
}

Related

How to implement client certificates and server authentication for iOS

I have recently gone through an extremely arduous process to build something that should be very simple yet appears to be essentially un-findable in any one place. I’d like to try to put everything here to ask if I’m doing anything wrong and, if not, to help anyone who needs this information.
Background: The product/service for which I was trying to provide security is built around WCF services on a Windows server that are accessible only though custom client apps on a PC or an iPad. One server per customer, no browser access. Everything was already TLS secured with authentication and authorization using Windows standard mechanisms and certs from a commercial CA.
To further restrict access, client/server certificates were implemented for the Windows platform using self-signed certificates (commercial CAs are not required in cases where there is mutual authentication with no public/browser access – despite claims to the contrary - and they are harder to manage).
Getting all this to work for the iPad was a terribly documented nightmare with an amazing amount of disinformation or partially correct recommendations. In what follows, I have tried to link to the best sources but I apologize if I have inadvertently missed an attribution. Please comment if there is anything wrong/misleading about this post.
Thanks
The main steps are:
Create a system for generating certificates (easy but non-trivial if it’s a production system)
Transfer the certificates to an iPad (NOT embedded in the app store bundle!)
Save all the received credentials in the app keychain (where Apple says they belong)
Retrieve the saved credentials from the keychain for use in NSURLConnections
Actually authenticate server certificate and return client credentials
Step 1. Generate Certificates
Ref: http://developer-should-know.tumblr.com/post/127063737582/how-to-create-your-own-pki-with-openssl
You can use other methods but OpenSSL for Windows [http://slproweb.com/products.html] is pretty awesome, except that the standard interface is cmdline and the documentation is hard to follow.
What I wish someone had explained to me up front is obvious, yet not:
[a] The app installs to a root level directory and includes configuration files that are used by default for settings that are not specified at the command line
[b] the locations of the intermediate and output files should be specified in the configuration files
[c] certain files need to be created by hand before running the commands
[d] you should build a file/folder structure that suits what you are trying to do and then customize the cfg files accordingly.
In my case, this meant one RootCA for my company, an intermediate cert per customer (set to be good to only make client certificates), a server certificate per customer, and client certificates as required. (this is the minimum config, never use CA/client pairs, keep the root in a lockbox)
Here’s my file structure:
c:\sslcert
root
certs
YourCompany (duplicate this structure as required)
intermediate
server
client
crl (optional)
In top level sslcert folder
.rnd (empty file)
certindex.txt (empty file)
serial.txt (Text file seeded with the text “01”, hold the quotes)
In root folder
RootCA.cfg
In certs\template folder
IntermediateCA.cfg
Set working directory and launch OpenSSL
cd \sslcert
c:\OpenSSL-Win32\bin\openssl.exe
Create root key and certificate in one step
req -config ./root/RootCA.cfg -new -x509 -days 7300 -extensions v3_ca -keyout root/YourCompanyRootCAkey.pem -out root/YourCompanyRootCAcert.cer
NOTE for the beginner: the -extensions allows you to choose to apply one of several subsections in the same cfg file.
Check key and cert (optional)
x509 -noout -text -in root/YourCompanyRootCAcert.cer
Request a new intermediate certificate
req -config certs/YourCompany/IntermediateCA.cfg -new -keyout certs/YourCompany/intermediate/intermediateCAkey.pem -out certs/YourCompany/intermediate/intermediateCAreq.pem  
Sign intermediate certificate using root certificate found in root config
ca -config root/RootCA.cfg -extensions v3_intermediate_ca -days 3650 -notext -in certs/YourCompany/intermediate/intermediateCAreq.pem -out certs/YourCompany/intermediate/YourCompanyIntermediateCAcert.cer
Check key and cert (optional)
x509 -noout -text -in certs/YourCompany/intermediate/YourCompanyIntermediateCAcert.cer
Create the certificate chain file by concatenating the intermediate and root certificates (this is just a simple append from the command line - the new chain will be added to the final p12 package)
c:\sslcert> type c:\sslcert\certs\YourCompany\intermediate\YourCompanyIntermediateCAcert.cer c:\sslcert\root\YourCompanyRootCAcert.cer > c:\sslcert\certs\YourCompany\intermediate\YourCompanyCAchain.cer
Request a new client key and certificate
genrsa -aes256 -out certs/YourCompany/client/YourCompanyClientkey.pem 2048
req -config certs/YourCompany/IntermediateCA.cfg -key
certs/YourCompany/client/YourCompanyClientkey.pem -new -sha256 -out certs/YourCompany/client/YourCompanyClientreq.pem
Sign and test client certificate with intermediate authority
ca -config certs/YourCompany/IntermediateCA.cfg -extensions usr_cert -days 1095 -notext -md sha256 -in certs/YourCompany/client/YourCompanyClientreq.pem -out certs/YourCompany/client/YourCompanyClientcert.cer
x509 -noout -text -in certs/YourCompany/client/YourCompanyClientcert.cer
verify -CAfile certs/YourCompany/intermediate/YourCompanyCAchain.cer certs/YourCompany/client/YourCompanyClientcert.cer
Package client certificate
pkcs12 -export -in certs/YourCompany/client/YourCompanyClientcert.cer -name “YourCompany Smips Client” -inkey certs/YourCompany/client/YourCompanyClientkey.pem -certfile certs/YourCompany/intermediate/YourCompanyCAchain.cer -out certs/YourCompany/client/YourCompanyClientWithName.p12
Rename pkcs for import into iOS from email/iTunes
c:\sslcert> copy c:\sslcert\certs\YourCompany\client\YourCompanyClient.p12 c:\sslcert\certs\YourCompany\client\YourCompanyClient.yourext12
Request a new server key and certificate
genrsa -aes256 -out certs/YourCompany/server/YourCompanyServerkey.pem 2048
req -config certs/YourCompany/IntermediateCA.cfg -key certs/YourCompany/server/YourCompanyServerkey.pem -new -sha256 -out certs/YourCompany/server/YourCompanyServerreq.pem
Sign and test server certificate with intermediate authority
ca -config certs/YourCompany/IntermediateCA.cfg -extensions server_cert -days 1095 -notext -md sha256 -in certs/YourCompany/server/YourCompanyServerreq.pem -out certs/YourCompany/server/YourCompanyServercert.cer
x509 -noout -text -in certs/YourCompany/server/YourCompanyServercert.cer
verify -CAfile certs/YourCompany/intermediate/YourCompanyCAchain.cer certs/YourCompany/server/YourCompanyServercert.cer
Package server certificate
pkcs12 -export -in certs/YourCompany/server/YourCompanyServercert.cer -name “YourCompany Smips Server” -inkey certs/YourCompany/server/YourCompanyServerkey.pem -certfile certs/YourCompany/intermediate/YourCompanyCAchain.cer -out certs/YourCompany/server/YourCompanyServer.p12
Here are the cfg files:
Root
dir = .
[ ca ]
default_ca = CA_default
[ CA_default ]
serial = $dir/serial.txt
database = $dir/certindex.txt
new_certs_dir = $dir/certs
certs = $dir/certs
private_key = $dir/root/yourcompanyRootCAkey.pem
certificate = $dir/root/yourcompanyRootCAcert.cer
default_days = 7300
default_md = sha256
preserve = no
email_in_dn = no
nameopt = default_ca
certopt = default_ca
policy = policy_strict
[ policy_strict ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 4096 # Size of keys
default_keyfile = key.pem # name of generated keys
default_md = sha256 # message digest algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
[ req_distinguished_name ]
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
0.organizationName_default = yourcompany
organizationalUnitName_default = yourcompanyRoot Certification
emailAddress_default = info#yourcompany.com
localityName_default = Okeefenokee
stateOrProvinceName_default = Wisconsin
countryName_default = US
[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
[ crl_ext ]
authorityKeyIdentifier=keyid:always
Intermediate
dir = .
# [For non-command-line folks, everything is keyed to the working directory here (.) so if your working prompt says c:\sslcerts> then the cfg will look for serial.txt at c:\sslcerts\serial.txt and bomb if it doesn’t find things laid out accordingly. Thats why you set up a directory structure to match these entries]
[ ca ]
default_ca = CA_default
[ CA_default ]
serial = $dir/serial.txt
database = $dir/certindex.txt
crl_dir = $dir/certs/yourcompany/crl
new_certs_dir = $dir/certs
certs = $dir/certs
private_key = $dir/certs/yourcompany/intermediate/IntermediateCAkey.pem
certificate = $dir/certs/yourcompany/intermediate/yourcompanyIntermediateCAcert.cer
default_days = 3650
default_md = sha256
preserve = no
email_in_dn = no
nameopt = default_ca
certopt = default_ca
crlnumber = $dir/certs/yourcompany/crl/crlnumber
crl = $dir/certs/yourcompany/crl/crl.pem
crl_extensions = crl_ext
default_crl_days = 365
policy = policy_loose
[ policy_loose ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ req ]
default_bits = 4096 # Size of keys
default_keyfile = $dir/certs/yourcompany/intermediate/IntermediateCAkey.pem
default_md = sha256 # message digest
# the old default was md1 - change this]
algorithm
string_mask = nombstr # permitted characters
distinguished_name = req_distinguished_name
x509_extensions = v3_intermediate_ca
[ req_distinguished_name ]
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
emailAddress = Email Address
emailAddress_max = 40
localityName = Locality Name (city, district)
stateOrProvinceName = State or Province Name (full name)
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
commonName = Common Name (hostname, IP, or your name)
commonName_max = 64
0.organizationName_default = yourcompany
organizationalUnitName_default = yourcompany Intermediate Certification
emailAddress_default = info#yourcompany.com
localityName_default = Okeefenokee
stateOrProvinceName_default = Wisconsin [should be spelled out]
countryName_default = US
[ v3_intermediate_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
# Important - the pathlen parameter prevents this cert from being used to create new intermediate certs. The subsequent subsections for server and client certs allows you to specify their type and intended usage, as distinct from the intermediate cert, in the same cfg file
[ usr_cert ]
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection
[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
[ crl_ext ]
authorityKeyIdentifier = keyid:always
2. Transfer certificates to an iPad
Ref: how to register the app to open the pdf file in my app in ipad
Apple recommends registering a new file type handled by your app and transferring a p12 file renamed with the new custom extension to a device (manually or email) to install client certs. The p12 file should include the public cert chain as well as the client cert info as defined in step 1 above. When you try to open such a file, the device sends a start/wakeup to your app delegate which you need to handle (not in didload because it could be a wake).
This has changed a bit with v8 or 9 but I need to support 7 so this is for the deprecated handler. Same solution though and it starts with adding to the app plist file as shown in the screenshots below.
Note that you will need two new icons and file extension that is not likely to be claimed by another app
Next you need the delegate/handler which should be self-explanatory. Since this part has nothing to do with the normal control flow I am handling all the delegate processing in AppDelegate.m. (is this so wrong?) Set up the methods/variables as required and please ignore the paranoid extra check for file existence...
Ref: https://www.raywenderlich.com/6475/basic-security-in-ios-5-tutorial-part-1
- (BOOL) application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation {
if (url) {
self.p12Data = [NSData dataWithContentsOfFile:[url path]];
if (!p12Data) {
[self messageBox:#"Warning" : #"Failed to read data file, cancelling certificate import"];
}
else {
[self presentAlertViewForPassPhrase];
}
NSFileManager * fileManager = [NSFileManager defaultManager];
if ( [fileManager fileExistsAtPath:[url path]] ) {
[fileManager removeItemAtPath:[url path] error:NULL];
}
}
return YES;
}
- (void)presentAlertViewForPassPhrase {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Certificate Credentials"
message:#"Please enter the passphrase for your certificate"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"Done", nil];
[alert setAlertViewStyle:UIAlertViewStyleSecureTextInput];
[alert show];
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {
if (buttonIndex == 1) { // User selected "Done"
UITextField *ppField = [alertView textFieldAtIndex:0];
if ([ppField.text length] > 0) {
[self loadCertificates:ppField.text];
}
//Handle Else
}
else
{ // User selected "Cancel"
[self messageBox:#"Information" : #"Certificate import cancelled"];
}
}
3. Save received credentials in the app keychain
Now that you have the raw p12 data it should be simple to figure out what to do next...NOT. All the documentation seems to be for name/pwd storage and a scary number of posters suggest saving the server cert to the file system, which is ok but makes absolutely no sense when you have the keychain and Apple says that's what it's for. Last but not least how do you distinguish between stored certs and how do you update them?
Long story short, I decided to do a complete delete/resave after trying all kinds of things that do not work to check if it should be an update or an initial load - besides that's what I wanted to do in the first place since it's my app chain. All this is CF stuff and I'm not using ARC because I refuse to port anything I don't have to. As near as I can tell, as long as you allocate CF, cast to NS, and CFRelease after use there are no warnings.
These are key references:
Enumerate all Keychain items in my iOS application
[essential to help visualize what your keychain looks like]
How to delete all keychain items accessible to an app?
What makes a keychain item unique (in iOS)?
http://help.sap.com/saphelp_smp307sdk/helpdata/en/7c/03830b70061014a937d8267bb3f358/content.htm
[https://developer.apple.com/library/ios/samplecode/AdvancedURLConnections/Listings/Credentials_m.html, which says:
// IMPORTANT: SecCertificateRef's are not uniqued (that is, you can get two
// different SecCertificateRef values that described the same fundamental
// certificate in the keychain), nor can they be compared with CFEqual. So
// we match up certificates based on their data values.
The summary is that (duh) the easiest thing to do is to assign a label to the cert so you can look it up uniquely and realize that if you save an identity it will be split up into key and cert automatically, which may - not sure - have led to some difficulty with replacement.
The code (explanation follows):
- (void) loadCertificates:(NSString *)passPhrase {
BOOL lastError = false;
NSMutableDictionary * p12Options = [[NSMutableDictionary alloc] init];
[p12Options setObject:passPhrase forKey:(id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus err = SecPKCS12Import((CFDataRef)p12Data, (CFDictionaryRef)p12Options, &items);
if (err != noErr) {
[self messageBox:#"Error" : #"Unable to extract security information with the supplied credentials. Please retry"];
lastError = true;
}
if (!lastError && err == noErr && CFArrayGetCount(items) > 0) {
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
//Clean-up
NSArray *secItemClasses = [NSArray arrayWithObjects:
(id)kSecClassCertificate,
(id)kSecClassKey,
(id)kSecClassIdentity,
nil];
for (id secItemClass in secItemClasses) {
NSDictionary *spec = #{(id)kSecClass: secItemClass};
err = SecItemDelete((CFDictionaryRef)spec);
}
//Client Identity & Certificate
SecIdentityRef clientIdentity = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
NSDictionary *addIdentityQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kClientIdentityLabel, kSecAttrLabel,
(id)clientIdentity, kSecValueRef,
nil];
err = SecItemAdd((CFDictionaryRef)addIdentityQuery, NULL);
if (err == errSecDuplicateItem) {
NSLog(#"Duplicate identity");
}
if (err != noErr) {
[self messageBox:#"Warning" : #"Failed to save the new identity"];
lastError = true;
}
//Server Certificate
CFArrayRef chain = CFDictionaryGetValue(identityDict, kSecImportItemCertChain);
CFIndex N = CFArrayGetCount(chain);
BOOL brk = false;
for (CFIndex i=0; (i < N) && (brk == false); i++) {
SecCertificateRef cert = (SecCertificateRef)CFArrayGetValueAtIndex(chain, i);
CFStringRef summary = SecCertificateCopySubjectSummary(cert);
NSString* strSummary = [[NSString alloc] initWithString:(NSString *)summary];
if ([strSummary containsString:#"Root"] || (i == N)) {
NSDictionary *addCertQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kServerCertificateLabel, kSecAttrLabel,
(id)cert, kSecValueRef,
nil];
err = SecItemAdd((CFDictionaryRef)addCertQuery, NULL);
if (err == errSecDuplicateItem) {
NSLog(#"Duplicate root certificate");
}
if (err != noErr) {
[self messageBox:#"Warning" : #"Failed to save the new server certificate"];
lastError = true;
}
brk = true;
}
[strSummary release];
CFRelease(summary);
}
}
else {
[self messageBox:#"Error" : #"Unable to extract security information with the supplied credentials. Please retry"];
lastError = true;
}
[p12Options release];
CFRelease(items);
if (!lastError) [self messageBox:#"Information" : #"Certificate import succeeded"];
}
where kClientIdentityLabel and kServerCertificateLabel are arbitrary labels.
The kSec functions are too many/complex to explain here in detail. Suffice it to say that everything is cleared, then the extracted client identity is saved, followed by extraction of the root CA which is then saved separately. Why the loop? because I didn't know if it is technically correct to assume the root is at the end of the chain but it will be if I generate the p12 so the code is just there for now.
Note that errors from kSec are encoded so this site is indispensable: https://www.osstatus.com
4. Retrieve the saved credentials from the keychain
Once the credentials are in the keychain you can extract them thusly (the failure modes leave something to be desired):
- (void) reloadCredentials {
self.clientCredential = nil;
self.serverCertificateData = nil;
if (self.useClientCertificateIfPresent) {
NSDictionary* idQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kClientIdentityLabel, kSecAttrLabel,
(id)kSecClassIdentity, kSecClass,
kCFBooleanTrue, kSecReturnRef,
kSecMatchLimitAll, kSecMatchLimit,
nil];
CFArrayRef result = nil;
OSStatus err = SecItemCopyMatching((CFDictionaryRef)idQuery, (CFTypeRef*)&result);
if (err == errSecItemNotFound) {
[self messageBox:#"Warning" : #"Client credentials not found. Server connection may fail"];
}
else if (err == noErr && result != nil ) {
SecIdentityRef clientIdentity = (SecIdentityRef)CFArrayGetValueAtIndex(result, 0);
SecCertificateRef clientCertificate;
SecIdentityCopyCertificate(clientIdentity, &clientCertificate);
const void *certs[] = { clientCertificate };
CFArrayRef certsArray = CFArrayCreate(NULL, certs, 1, NULL);
self.clientCredential = [NSURLCredential credentialWithIdentity:clientIdentity certificates:(NSArray*)certsArray
persistence:NSURLCredentialPersistenceNone];
CFRelease(certsArray);
CFRelease(clientCertificate);
CFRelease(result);
}
else {
[self messageBox:#"Warning" : #"Client or Server credentials not found. Server connection may fail"];
}
NSDictionary* serverCertQuery = [NSDictionary dictionaryWithObjectsAndKeys:
kServerCertificateLabel, kSecAttrLabel,
(id)kSecClassCertificate, kSecClass,
kCFBooleanTrue, kSecReturnRef,
kSecMatchLimitAll, kSecMatchLimit,
nil];
CFArrayRef result1 = nil;
err = SecItemCopyMatching((CFDictionaryRef)serverCertQuery, (CFTypeRef*)&result1);
if (err == errSecItemNotFound) {
[self messageBox:#"Warning" : #"Server certificate not found. Server connection may fail"];
}
else if (err == noErr && result1 != nil ) {
SecCertificateRef certRef = (SecCertificateRef)CFArrayGetValueAtIndex(result1, 0);
CFDataRef certRefData = SecCertificateCopyData(certRef);
self.serverCertificateData = (NSData *)certRefData;
CFRelease(certRefData);
CFRelease(result1);
}
else {
[self messageBox:#"Warning" : #"Client or Server credentials not found. Server connection may fail"];
}
}
}
5. Authenticate server certificate and return client credentials
Hoo boy. This is an edit to explain how to actually use the retrieved certificates (it was supposed to be the easy obvious part...)
First, Apple's already dubious documentation is obsoleted by the new Application Transport Security framework (see for instance: http://useyourloaf.com/blog/app-transport-security/). I'm not going to get into it here but the idea is to force everyone to always use https and trusted certs by default. For my scenario, with certificate pinning and mutual authentication between dedicated clients and a private server you can safely turn this feature off by adding a dictionary to your plist as follows:
Next, in Step 4, you already had the client credential to respond to that challenge immediately when it hits but the server cert is floating around as NSData in DER format as created by SecCertificateCopyData and it is not clear what should happen when that challenge arrives.
Turns out that what you are supposed to do is implement the algorithm in section 6 of the "X.509 standard" (https://www.rfc-editor.org/rfc/rfc5280). Luckily, this is implemented behind the scenes by the iOS SecTrustEvaluate function but there is scaffolding to build and weird stuff to understand.
[Slight problem - ran out of space!! Added a new question, including the end of this step.]
https://stackoverflow.com/questions/35964632/correctly-use-a-pinned-self-signed-certificate-in-ios-9-2
[Continuing from other post]
So that's it. Sorry about the not quite production quality but I wanted to slap this together while it was still fresh in my mind. I will update the post if I find errors.
Hope this helps and here's a final link to a very good book that, amongst other things, will give you the creeps about trusting commercial CAs...
https://www.cs.auckland.ac.nz/~pgut001/pubs/book.pdf
[I just(!) realized I can add another answer since the continuation link was down-voted and closed and there are two requests for the additional information that wouldn't fit above. The answer below starts with the question in the deleted post]
...The part that I am still not clear about is why I had to create a new trust and policy to implement an anchor certificate and pinning.
If I simply added the anchor to the trust received from the server I could not successfully return the pointer to the NSURLCredential received from the server, it seemed to get modified and rejected by the sender(?).
The question is, is this really the appropriate processing or can it be condensed? This is getting a bit tiresome but I don't want to accept something just because it "works". My current solution is shown below.
In Step 4, you already had the client credential to respond to that type of challenge without manipulation but the server cert is floating around as NSData in DER format as created by SecCertificateCopyData and it is not clear what should happen when that challenge arrives.
Turns out that what you are supposed to do is implement the algorithm in section 6 of the "X.509 standard" (https://www.rfc-editor.org/rfc/rfc5280). Luckily, this is implemented behind the scenes by the iOS SecTrustEvaluate function but there is scaffolding to build and weird stuff to understand. First the code (following a hat tip to my original source):
SecTrustEvaluate always returns kSecTrustResultRecoverableTrustFailure with SecPolicyCreateSSL
- (void)_handleServerTrustChallenge {
OSStatus status;
BOOL trusted = false;
SecTrustResultType trustResult;
SecTrustRef serverTrust = self.challenge.protectionSpace.serverTrust;
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; //candidate credential for return to sender if valid
if (svDelegate.serverCertificateData) {
//locally stored information
SecCertificateRef storedCertificate = SecCertificateCreateWithData(NULL, (CFDataRef)svDelegate.serverCertificateData);
NSMutableArray *anchorCertArray = [NSMutableArray arrayWithCapacity:1];
[anchorCertArray addObject:(id)storedCertificate];
//incoming credentials from server
NSMutableArray *receivedCertChain = [NSMutableArray array];
for(int i = 0; i < SecTrustGetCertificateCount(serverTrust); i++)
[receivedCertChain addObject:(id) SecTrustGetCertificateAtIndex(serverTrust,i))];
//new custom policy object to use in creating new trust
//YES indicates extendedKeyUsage is set to serverAuth; effectively ignore server name check by specifying incoming name
SecPolicyRef newPolicyRef = SecPolicyCreateSSL(YES, (CFStringRef)self.challenge.protectionSpace.host);
//create and evaluate new trust with pinned certificate
SecTrustRef newTrustRef = NULL;
SecTrustCreateWithCertificates((CFArrayRef) receivedCertChain, newPolicyRef, &newTrustRef);
status = SecTrustSetAnchorCertificates(newTrustRef, (CFArrayRef) anchorCertArray);
if (status == noErr) status = SecTrustSetAnchorCertificatesOnly(newTrustRef, TRUE);
if (status == noErr) status = SecTrustEvaluate(newTrustRef, &trustResult);
//----- debug -------
//CFShow(newPolicyRef);
//NSLog(#"%#", receivedCertChain);
CFRelease(newTrustRef);
CFRelease(newPolicyRef);
CFRelease(storedCertificate);
}
else { //Server certificate not stored, rely on standard trusted Root CA authorities
status = SecTrustEvaluate(serverTrust, &trustResult);
}
trusted = (status == noErr) && (trustResult == kSecTrustResultUnspecified);
if (!trusted) credential = nil;
[self stopWithCredential:credential];
[self.delegate challengeHandlerDidFinish:self];
}
So first I check whether the server cert was loaded (else process via conventional trusted CA method).
Next you select the "trust object" to evaluate. I failed to do this without making a working copy of the trust object I received over the wire from the server b/c it somehow messed up the 'NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]' reference if I used it directly. It appears though from the truly horrible Apple docs that this is a kosher approach (I, for one, recommend skimming the x.509 rfc if you want to understand any of it).
https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
[https://developer.apple.com/library/ios/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECCUSTOMROOT][2]
The trust requires a 'policy', the incoming certificate chain to evaluate, and one or more 'anchor certificates' which basically defines the origin in an arbitrary coordinate system - everything is validated downstream of the zero point even if it is not a root cert.
So you load the incoming chain and the stored cert into arrays to be provided to the new trust and create a new policy with SecPolicyCreateSSL - this sets a flag to indicate that the certificate should be checked to have been issued for serverAuth and that the incoming server name should be ignored (to allow some infrastructure flexibility).
Next you create the new trust using the new policy and the certificate array to be authenticated. Then you set the anchor and ensure that the chain will only be evaluated against your anchor cert, not just anything in the iOS keychain.
When you evaluate the trust it may seem odd that you accept kSecTrustResultUnspecified and not proceed or something more positive-sounding. In fact, proceed means you are following overrides from the user interface so that is actually bad; unspecified just means there's nothing wrong per the specified policy.
Finally, you return the credential from the incoming trust object (not the new one) and everything should be golden...

Making HTTPS request in iOS 9 with self-signed certificate [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 5 years ago.
Improve this question
I want to make an HTTPS request to custom server with self-signed certificate. I'm using NSURLConnection class and processing authentication challenges, but always receive error message in a console:
NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)
then method "connection:didFailWithError:" gets called with the following error:
Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x150094100>, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9802, NSErrorPeerCertificateChainKey=<CFArray 0x1500ddd90 [0x19f6dab68]>{type = immutable, count = 1, values = (
0 : <cert(0x14e6fb370) s: (server certificate name) i: (custom CA name)>
)}, NSUnderlyingError=0x1504ae170 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://217.92.80.156:9090/(method name and parameters), NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFNetworkCFStreamSSLErrorOriginalValue=-9802, kCFStreamPropertySSLPeerCertificates=<CFArray 0x1500ddd90 [0x19f6dab68]>{type = immutable, count = 1, values = (
0 : <cert(0x14e6fb370) s: (server certificate name) i: (custom CA name)>
)}, _kCFStreamPropertySSLClientCertificateState=2, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x150094100>, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., _kCFStreamPropertySSLClientCertificates=<CFArray 0x14e5ee8e0 [0x19f6dab68]>{type = mutable-small, count = 2, values = (
0 : <SecIdentityRef: 0x15012cd40>
1 : <cert(0x15014aa70) s: (client certificate name) i: (custom CA name)>
)}, _kCFStreamErrorDomainKey=3, NSErrorFailingURLKey=https://217.92.80.156:9090/(method name and parameters), _kCFStreamErrorCodeKey=-9802}}, NSErrorClientCertificateChainKey=<CFArray 0x14e5ee8e0 [0x19f6dab68]>{type = mutable-small, count = 2, values = (
0 : <SecIdentityRef: 0x15012cd40>
1 : <cert(0x15014aa70) s: (client certificate name) i: (custom CA name)>
)}, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://217.92.80.156:9090/(method name and parameters), NSErrorFailingURLStringKey=https://217.92.80.156:9090/(method name and parameters), NSErrorClientCertificateStateKey=2}
App receives two authentication challenges (NSURLAuthenticationMethodClientCertificate and NSURLAuthenticationMethodServerTrust) and processes them in a following manner:
- (void) connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if(challenge.proposedCredential && !challenge.error)
{
[challenge.sender useCredential:challenge.proposedCredential forAuthenticationChallenge:challenge];
return;
}
NSString *strAuthenticationMethod = challenge.protectionSpace.authenticationMethod;
NSLog(#"authentication method: %#", strAuthenticationMethod);
NSURLCredential *credential = nil;
if([strAuthenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate])
{
// get identity and certificate from p.12
NSData *PKCS12Data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"client" ofType:#"p12"]];
NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:#"password" forKey:(__bridge id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import((__bridge CFDataRef)PKCS12Data,(__bridge CFDictionaryRef)optionsDictionary, &items);
SecIdentityRef identity = NULL;
SecCertificateRef certificate = NULL;
if(securityError == errSecSuccess)
{
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex(items, 0);
identity = (SecIdentityRef)CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity);
CFArrayRef array = (CFArrayRef)CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemCertChain);
certificate = (SecCertificateRef)CFArrayGetValueAtIndex(array, 0);
}
credential = [NSURLCredential credentialWithIdentity:identity certificates:[NSArray arrayWithObject:(__bridge id)(certificate)] persistence:NSURLCredentialPersistenceNone];
CFRelease(items);
}
else if([strAuthenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
int trustCertificateCount = (int)SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust);
NSMutableArray *trustCertificates = [[NSMutableArray alloc] initWithCapacity:trustCertificateCount];
for(int i = 0; i < trustCertificateCount; i ++)
{
SecCertificateRef trustCertificate = SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, i);
[trustCertificates addObject:(__bridge id) trustCertificate];
}
SecPolicyRef policyRef = NULL;
policyRef = SecPolicyCreateSSL(YES, (__bridge CFStringRef) challenge.protectionSpace.host);
SecTrustRef trustRef = NULL;
if(policyRef)
{
SecTrustCreateWithCertificates((__bridge CFArrayRef) trustCertificates, policyRef, &trustRef);
CFRelease(policyRef);
}
if(trustRef)
{
// SecTrustSetAnchorCertificates(trustRef, (__bridge CFArrayRef) [NSArray array]);
// SecTrustSetAnchorCertificatesOnly(trustRef, NO);
SecTrustResultType result;
OSStatus trustEvalStatus = SecTrustEvaluate(trustRef, &result);
if(trustEvalStatus == errSecSuccess)
{
// just temporary attempt to make it working.
// i hope, there is no such problem, when we have final working version of certificates.
if(result == kSecTrustResultRecoverableTrustFailure)
{
CFDataRef errDataRef = SecTrustCopyExceptions(trustRef);
SecTrustSetExceptions(trustRef, errDataRef);
SecTrustEvaluate(trustRef, &result);
}
if(result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)
credential = [NSURLCredential credentialForTrust:trustRef];
}
CFRelease(trustRef);
}
}
else
{
DDLogWarn(#"Unexpected authentication method. Cancelling authentication ...");
[challenge.sender cancelAuthenticationChallenge:challenge];
}
if(credential)
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
else
[challenge.sender cancelAuthenticationChallenge:challenge];
}
In CFNetwork diagnostic log i can see that Handshake procedure is about to be started. At least app sends "ClientHello" message, then server sends its "ServerHello" message and requires for authentication. And here app tries to send authentication response, but immediately receives error. (At the same time, in server logs i don't see any messages about handshake at all). Here is part of diagnostic log:
Sep 15 10:51:49 AppName[331] <Notice>: CFNetwork Diagnostics [3:49] 10:51:49.185 {
Authentication Challenge
Loader: <CFURLRequest 0x1501931c0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
Challenge: challenge space https://217.92.80.156:9090/, ServerTrustEvaluationRequested (Hash f9810ad8165b3620)
} [3:49]
Sep 15 10:51:49 AppName[331] <Notice>: CFNetwork Diagnostics [3:50] 10:51:49.189 {
Use Credential
Loader: <CFURLRequest 0x1501931c0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
Credential: Name: server, Persistence: session
} [3:50]
Sep 15 10:51:49 AppName[331] <Notice>: CFNetwork Diagnostics [3:51] 10:51:49.190 {
touchConnection
Loader: <CFURLRequest 0x1501931c0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
Timeout Interval: 60.000 seconds
} [3:51]
Sep 15 10:51:49 AppName[331] <Notice>: CFNetwork Diagnostics [3:52] 10:51:49.192 {
Response Error
Request: <CFURLRequest 0x14e5d02a0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
Error: Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFNetworkCFStreamSSLErrorOriginalValue=-9802, kCFStreamPropertySSLPeerCertificates=<CFArray 0x1500ddd90 [0x19f6dab68]>{type = immutable, count = 1, values = (
0 : <cert(0x14e6fb370) s: (server certificate name) i: (custom CA name)>
)}, _kCFStreamPropertySSLClientCertificateState=2, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x150094100>, _kCFStreamPropertySSLClientCertificates=<CFArray 0x14e5ee8e0 [0x19f6dab68]>{type = mutable-small, count = 2, values = (
0 : <SecIdentityRef: 0x15012cd40>
1 : <cert(0x15014aa70) s: (client certificate name) i: (custom CA name)>
)}, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9802}
} [3:52]
Our back-end instance can be installed on customer side, so i can't set any domain exception in Info.plist file. Also app may request server by IP address in IPv4 form, but not by domain name (as it is in my example).
What have i tried:
used NSURLSession instead of NSURLConnection, but without any success;
checked Apple's ATS requirements for server implementation here (back-end developer is sure that his implementation meets all of them);
played with setting anchor certificates for trust validation in accordance with various solved issues from stackoverflow and Apple's developer forums;
paid attention particularly to similar post and its related solution at developer forums;
I'm testing https request on iPad Air 2 with iOS 9 GM Seed (Build 13A340) and xCode 7 GM Seed (Build 7A218). Important note: this functionality works fine with iOS 8. Taking that into account i may assume, that problem is in our server, but our back-end developer assured me that there everything is fine.
Now i'm out of ideas. I would appreciate if anyone can give me a hint, or at least suggest some other diagnostic, which would reveal particular error, more specific than "fatal alert".
Thanks.
EDIT 1: SecTrustEvaluate always returns kSecTrustResultRecoverableTrustFailure, that is why i had to find some kind of workaround.
According to this: https://forums.developer.apple.com/message/36842#36842
The best approach to fix HTTP load failed (kCFStreamErrorDomainSSL, -9802) is to set an exception in the info.plist file as follows:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>test.testdomain.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
The important point is that this is not less secure than iOS8, just not as secure as full ATS supported by iOS9.
Have you used nscurl to diagnose the connection issue? If you have a Mac running OS X v10.11 you can run something like this:
/usr/bin/nscurl --ats-diagnostics https://www.yourdomain.com
Alternatively, if you don't have 10.11, you can download the sample code here: https://developer.apple.com/library/mac/samplecode/SC1236/ and build it with XCode and run it like this (changing the path as appropriate for your machine):
/Users/somebody/Library/Developer/Xcode/DerivedData/TLSTool-hjuytnjaqebcfradighsrffxxyzq/Build/Products/Debug/TLSTool s_client -connect www.yourdomain.com:443
(To find the full path for the above, after you've built, open the Products group in your Project Navigator, right click on TLSTool, and "Show in Finder".)
You already linked to Apple's technote on this subject, https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/ but you didn't say if you ran nscurl or not.
I just met the same problem with ur's.Now i fixes it.It is because the tls version and the certificate sign.As the apple's document say below
apple's document
So i do this thing
info.plist setting
and it works
This issue was solved some time ago. It turned out to be invalid self-signed certificate. It didn't meet all requirements from Apple. Unfortunately i don't know, what exactly it was.

SecTrustEvaluate fails with kSecTrustResultRecoverableTrustFailure for self signed CA with non-matching subject name

Here's my pretty standard NSURLConnection callback for authenticating using self signed certificate:
- (SecCertificateRef)certRefFromDerNamed:(NSString*)derFileName resultingDataRef:(CFDataRef*)dataRefPtr{
NSString *thePath = [[NSBundle mainBundle] pathForResource:derFileName ofType:#"der"];
NSData *certData = [[NSData alloc] initWithContentsOfFile:thePath];
CFDataRef certDataRef = (__bridge_retained CFDataRef)certData;
SecCertificateRef cert = SecCertificateCreateWithData(NULL, certDataRef);
*dataRefPtr = certDataRef;
return cert;
}
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
if (connection == self.connection) {
BOOL trusted = NO;
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecPolicyRef policyRef = SecPolicyCreateBasicX509();
SecCertificateRef cert1;
CFDataRef certData1;
cert1 = [self certRefFromDerNamed:#"some3rdpartycacert" resultingDataRef:&certData1];
SecCertificateRef certArray[1] = { cert1 };
CFArrayRef certArrayRef = CFArrayCreate(NULL, (void *)certArray, 1, NULL);
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
SecTrustSetAnchorCertificates(serverTrust, certArrayRef);
SecTrustResultType trustResult;
SecTrustEvaluate(serverTrust, &trustResult);
trusted = (trustResult == kSecTrustResultUnspecified);
CFRelease(certArrayRef);
CFRelease(policyRef);
CFRelease(cert1);
CFRelease(certData1);
}
if (trusted) {
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust] forAuthenticationChallenge:challenge];
} else {
[challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
}
}
}
And trustResult is always kSecTrustResultRecoverableTrustFailure.
The certificate itself is a little problematic. According to curl cert subject name on server does not match the url I'm connecting to. I've contacted that 3rd party company and they told me that I need to accept this url mismatch in my code. The problem is that I don't know how to do this on iOS. I can either bypass the certificate check completely (by simply assuming trusted=YES and calling useCredential) or fail completely. The first solution is obviously wrong from security point of view and prone to MITM attacks.
Here's the CURL output (I've used PEM version for the same cert here):
ukaszs-iMac:Preferences lukasz$ curl --verbose --cacert ~/Desktop/some3rdpartycacert.txt https://dev-service.some3rdparty.com:50101/
* About to connect() to dev-service.some3rdparty.com port 50101 (#0)
* Trying XXX.XXX.XXX.XXX...
* connected
* Connected to dev-service.some3rdparty.com (XXX.XXX.XXX.XXX) port 50101 (#0)
* successfully set certificate verify locations:
* CAfile: /Users/lukasz/Desktop/some3rdpartycacert.txt
CApath: none
* SSLv3, TLS handshake, Client hello (1):
* SSLv3, TLS handshake, Server hello (2):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Request CERT (13):
* SSLv3, TLS handshake, Server finished (14):
* SSLv3, TLS handshake, CERT (11):
* SSLv3, TLS handshake, Client key exchange (16):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSLv3, TLS change cipher, Client hello (1):
* SSLv3, TLS handshake, Finished (20):
* SSL connection using AES256-SHA
* Server certificate:
* subject: C=CA; ST=Ontario; O=Some 3rdParty Corporation; CN=otherpage.some3rdparty.com; emailAddress=noc#some3rdparty.com
* start date: 2013-10-30 16:52:14 GMT
* expire date: 2013-10-30 16:52:14 GMT
* SSL: certificate subject name 'otherpage.some3rdparty.com' does not match target host name 'dev-service.some3rdparty.com'
* Closing connection #0
* SSLv3, TLS alert, Client hello (1):
curl: (51) SSL: certificate subject name 'otherpage.some3rdparty.com' does not match target host name 'dev-service.some3rdparty.com'
So, how to ignore this particular error on iOS?
You need to create a special policy using the actual hostname, then create and evaluate serverTrust from that. Roughly:
SecPolicyRef policyRef = SecPolicyCreateSSL(true, CFSTR("otherpage.some3rdparty.com"));
OSStatus status;
SecTrustRef serverTrust;
status = SecTrustCreateWithCertificates(certificatesFromOriginalServerTrust, policyRef, & serverTrust);
// noErr == status?
status = SecTrustSetAnchorCertificates(serverTrust, certArrayRef);
// noErr == status?
SecTrustResultType trustResult;
status = SecTrustEvaluate(serverTrust, &trustResult);
// noErr == status?
if(kSecTrustResultProceed == trustResult || kSecTrustResultUnspecified == trustResult) {
// all good
}
p.s. you're not using the policy you created.
I just found a more complete explanation here.
For this specific problem, you need to read
"iOS 5 Programming Pushing the Limits: Developing Extraordinary Mobile Apps for Apple iPhone, iPad, and iPod Touch" Book. (by Rob Napier, Mugunth Kumar)
In the book, at bottom of page 219, it says:
"If you're okay with the name you were passed, you reevaluate the certificate as a simple X.509 certificate rather than as part of an SSL handshake (that is, you evaluate it while ignoring the hostname)"
Then it have a special RNSecTrustEvaluateAsX509 function.
So using that function actually you evaluate certificate with ignoring hostname part and setting that part to be "trusted".
See http://bit.ly/1g1RzdF (go to page 219)
Code from book can be found here:
https://github.com/iosptl/ios5ptl/blob/master/ch11/Connection/Connection/ConnectionViewController.m

iOS app SSL .p12 Authentication - bad certificate error (-9825)

Updates
Edit 2/6/14:
I created a local Apache Tomcat server to test SSL w/ certificate authentication. I was successful! Everything works as expected, using both of my approaches below. (MKNetworkKit, and custom code). While this does tell me my code is working, my original issue is still not solved. I updated the title of the question to more specifically reflect the issue. Does anyone know if SAP Portal needs special settings to accept certificates from an iOS app? Remember, I was able to successfully authenticate using Safari mobile after importing the CA and .p12 into the shared keychain, I was only unsuccessful in code (which I now know the code works, just not with the portal).
I am creating a very simple iOS7 Cordova 3.2 application with a custom plugin to get data from an SSL web service by providing only a .p12 certificate for authentication (no basic auth or other user credentials needed). I am performing all tests on a physical iPad (no simulator). The web service lives on a development SAP NetWeaver portal box using a self signed certificate. For now, I imported the server's CA into the iOS keychain to avoid certificate trust errors. For testing purposes, my .p12 certificate is bundled locally inside the app, in the root of the mainBundle.
When trying to connect to the web service I get the follow error in the console:
CFNetwork SSLHandshake failed (-9825)
NSURLConnection/CFURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9825)
Error Domain=NSURLErrorDomain Code=-1205 "The server “myhostremoved.com” did not accept the certificate." UserInfo=0x14e99000 {NSErrorFailingURLStringKey=https://myhostremoved.com/sslwebservice/, NSErrorFailingURLKey=https://myhostremoved.com/sslwebservice/, NSLocalizedDescription=The server “myhostremoved.com” did not accept the certificate., NSUnderlyingError=0x14d9b1d0 "The server “myhostremoved.com” did not accept the certificate.", NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x14d94720>}
According to Apple's documentation site, the -9825 error referes to a bad certificate.
There are many questions on SO related to what I am trying to do, but none specifically pertaining to the error I am seeing. I approached the development of the code in two different ways.
First I tried to use code already on SO, adapting it to my use case. See code below:
- (void)startConnection:(CDVInvokedUrlCommand*)command {
NSDictionary *options = [command.arguments objectAtIndex:0];
NSURL *serverURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#", [options objectForKey:#"host"]]];//hostname provided by Cordova plugin, but could just as easily be hardcoded here
NSMutableURLRequest *connectionRequest = [NSMutableURLRequest requestWithURL:serverURL];
NSURLConnection *connection = nil;
connection = [[NSURLConnection alloc] initWithRequest:connectionRequest delegate:self startImmediately:YES];
}
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
// gets a certificate from local resources
NSString *thePath = [[NSBundle mainBundle] pathForResource:#"mycert" ofType:#"p12"];
NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
CFDataRef inPKCS12Data = (__bridge CFDataRef)PKCS12Data;
SecIdentityRef identity;
// extract the ideneity from the certificate
[self extractIdentity :inPKCS12Data :&identity];
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate (identity, &certificate);
const void *certs[] = {certificate};
CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
// create a credential from the certificate and ideneity, then reply to the challenge with the credential
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
}
- (OSStatus)extractIdentity:(CFDataRef)inP12Data :(SecIdentityRef*)identity {
OSStatus securityError = errSecSuccess;
CFStringRef password = CFSTR("MyCertPassw0rd");
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
securityError = SecPKCS12Import(inP12Data, options, &items);
if (securityError == 0) {
CFDictionaryRef ident = CFArrayGetValueAtIndex(items,0);
const void *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue(ident, kSecImportItemIdentity);
*identity = (SecIdentityRef)tempIdentity;
}
if (options) {
CFRelease(options);
}
return securityError;
}
On my second approach, I tried to use the MKNetworkKit library, which abstracts away alot of the code needed to interface with the certificate. All you need to do is provide the path to the certificate and the password. Again, I get the same error as above. This code is below.
- (void)startConnection:(CDVInvokedUrlCommand*)command {
NSDictionary *options = [command.arguments objectAtIndex:0];
NSURL *serverURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#", [options objectForKey:#"host"]]];//hostname provided by Cordova plugin, but could just as easily be hardcoded here
MKNetworkEngine *engine = [[MKNetworkEngine alloc] initWithHostName:serverURL customHeaderFields:nil];
MKNetworkOperation *op = [engine operationWithPath:nil params:nil httpMethod:#"GET" ssl:YES];
NSString *thePath = [[NSBundle mainBundle] pathForResource:#"mycert" ofType:#"p12"];
[op setShouldContinueWithInvalidCertificate:YES];
op.clientCertificate = thePath;
op.clientCertificatePassword = #"MyCertPassw0rd";
[op addCompletionHandler:^(MKNetworkOperation *operation) {
NSLog(#"[operation responseData]-->>%#", [operation responseString]);
}errorHandler:^(MKNetworkOperation *errorOp, NSError* err) {
NSLog(#"MKNetwork request error : %#", [err localizedDescription]);
}];
[engine enqueueOperation:op];
}
I get the same error using both approaches. Any ideas?
Known information
I know that the app is finding .p12 certificate because when I fudge the path to the .p12 cert, I receive an error saying it cannot find the certificate which I don't normally see otherwise.
I know the password I am providing for the certificate file is correct because when I fudge the password, I receive an error regarding the password I don't normally see otherwise.
I don't think certificate trust is an issue because if I remove my server's CA from the iOS keychain, I get an error in the console specifically stating the server cannot be trusted. After adding the CA back, this error no longer occurs, but I get same error as above (-9825)
Others tests
When providing basic auth in code (bypassing certificate authentication), everything works as expected and I receive no errors.
I have also tried all the same steps above, but using a .pfx file instead of my .p12 certificate, same errors.
The SSL service works in mobile Safari. I imported the .p12 certificate into my keychain via the iPhone Configuration utility, and tested the web service via mobile safari. Everything works as expected, no errors occur (no trust errors either), and I receive the expected output.
I am also able to successfully test the web service on my desktop using the rest-client utility
TL;DR
I am trying to authenticate to an SSL web service using only a .p12 certificate in objective-c. I know the server and web service both work with the cert for auth, but I keep getting errors when I try to establish the connection in objective-c, so something must be wrong in my code. Please help!
I finally was able to resolve this problem. I found the answer here
http://oso.com.pl/?p=207&lang=en
It looks like the certificate was being sent twice, causing an error on the server.
Incase the above link dies, I found the same solution already on SO (but for a different question and error).
When using Client Certificate Authentication, why do I keep getting NSURLErrorDomain Code=-1206?
Ultimately, changing this:
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:myIdentity certificates:(__bridge NSArray*)certsArray persistence:NSURLCredentialPersistenceNone];
to this:
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:myIdentity certificates:nil persistence:NSURLCredentialPersistenceNone];
solved my issue. It would be nice if anyone could offer more insight into this issue.
In some cases handshake may fail because server also asks intermediate and root certificates.
To fix it do the following:
Authentication challenge part:
OSStatus status = extractIdentityAndTrust(inP12data, &myIdentity, &myTrust, password);
SecCertificateRef myCertificate;
SecIdentityCopyCertificate(myIdentity, &myCertificate);
CFIndex count = SecTrustGetCertificateCount(myTrust);
NSMutableArray* myCertificates = [NSMutableArray arrayWithCapacity:count];
if (count > 1) {
for (int i = 1; i < count; ++i) {
[myCertificates addObject:(__bridge id)SecTrustGetCertificateAtIndex(myTrust, i)];
}
}
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:myIdentity certificates:myCertificates persistence: NSURLCredentialPersistenceForSession];
Extracting method
OSStatus extractIdentityAndTrust(CFDataRef inPKCS12Data,
SecIdentityRef *outIdentity,
SecTrustRef *outTrust,
CFStringRef keyPassword) {
OSStatus securityError = errSecSuccess;
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { keyPassword };
CFDictionaryRef optionsDictionary = NULL;
/* Create a dictionary containing the passphrase if one
was specified. Otherwise, create an empty dictionary. */
optionsDictionary = CFDictionaryCreate(
NULL, keys,
values, (keyPassword ? 1 : 0),
NULL, NULL);
CFArrayRef items = NULL;
securityError = SecPKCS12Import(inPKCS12Data,
optionsDictionary,
&items);
if (securityError == 0) {
CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex (items, 0);
const void *tempIdentity = NULL;
tempIdentity = CFDictionaryGetValue (myIdentityAndTrust,
kSecImportItemIdentity);
CFRetain(tempIdentity);
*outIdentity = (SecIdentityRef)tempIdentity;
const void *tempTrust = NULL;
tempTrust = CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemTrust);
CFRetain(tempTrust);
*outTrust = (SecTrustRef)tempTrust;
}
if (optionsDictionary)
CFRelease(optionsDictionary);
if (items)
CFRelease(items);
return securityError; }
Please note that loop from 1 (not 0) is not a mistake. First cert is already added to "myIdentify", so only other certs should be passed as certificates, otherwise most likely you'll receive an error during handshake because of duplicating.

Detecting CA:TRUE in x509 Basic Contraints

What is the proper way to check the basic constraints of a certificate ? Below is the code I am currently using to mimic the icons show in the Keychain (the reason for below is that while we have a
SFChooseIdentityPanel * identityPanel = [SFChooseIdentityPanel sharedChooseIdentityPanel];
the equivalent for selecting a CA or a Host/Leaf cert does not exist. And that is useful when setting up/locking down SSL connections.
Unfortunately - I cannot find the OID strings in the header files to cleanly extract the CA:TRUE or false form a cert (or am using the API in the wrong way).
So the questions are
How do I cleanly check for CA:TRUE - while below works - I can imagine that it would get foiled by a malformed cert with text strings in the right places.
Secondly - I am using a heuristic of Issuer==Subject to detect self signed. Is there a cleaner way to do this ?
Finally - from trial and error - below seems to mimic apple her choices in the keychain - but the docs are rather hard to understand. Does kSecTrustResultProceed really mean that the user has set an override and kSecTrustResultUnspecified that in fact the trust is specified by the system basic trust ? While it 'work's - I cannot quite understand the exact interpretation of the docs.
Thanks a lot. Code below.
Dw.
#implementation NSImage (CertificateSelectionPanelExtensions)
+(NSImage *)iconForCertificate:(SecCertificateRef)certificateRef small:(BOOL)isSmall
{
BOOL isCA = FALSE, isInvalid = TRUE, isUserTrust = FALSE;
NSString * issuer = nil, * subject = nil;
const void *keys[] = { kSecOIDX509V1SubjectName, kSecOIDX509V1IssuerName, kSecOIDExtendedKeyUsage, kSecOIDBasicConstraints };
CFArrayRef keySelection = CFArrayCreate(NULL, keys , sizeof(keys)/sizeof(keys[0]), &kCFTypeArrayCallBacks);
CFDictionaryRef vals = SecCertificateCopyValues(certificateRef, keySelection, NULL);
CFArrayRef values;
CFDictionaryRef dict;
dict = CFDictionaryGetValue(vals, kSecOIDBasicConstraints );
values = dict ? CFDictionaryGetValue(dict, kSecPropertyKeyValue) : NULL;
if (values) {
for(int i = 0; i < CFArrayGetCount(values); i++) {
CFDictionaryRef subDict = CFArrayGetValueAtIndex(values, i);
// We cannot find OID defines for the CA - so rely on the lower libraries to give us a string
// of sorts. Not a good idea - as now this code can be foiled by a actual string.
//
NSString *k = [NSString stringWithFormat:#"%#", CFDictionaryGetValue(subDict, kSecPropertyKeyLabel)];
NSString *v = [NSString stringWithFormat:#"%#", CFDictionaryGetValue(subDict, kSecPropertyKeyValue)];
if ([#"Certificate Authority" isEqualToString:k] && [#"Yes" isEqualToString:v]) {
isCA = TRUE;
}
}
};
// Fall back on a simple self-sign check if there where no kSecOIDBasicConstraints.
// set on the cert. Note that it is a DN is equal check - in some cases
// doing a 509v3 Subject/Authority Key Identifier may be better ?? XXXX
//
if (!isCA && !values) {
dict = CFDictionaryGetValue(vals, kSecOIDX509V1SubjectName);
values = dict ? CFDictionaryGetValue(dict, kSecPropertyKeyValue) : NULL;
subject = [NSString stringWithFormat:#"%#", values];
dict = CFDictionaryGetValue(vals, kSecOIDX509V1IssuerName);
values = dict ? CFDictionaryGetValue(dict, kSecPropertyKeyValue) : NULL;
issuer = [NSString stringWithFormat:#"%#", values];
// Crap way of secondgessing CA ness.
if ([issuer isEqualToString:subject])
isCA = TRUE;
};
SecPolicyRef policy = SecPolicyCreateBasicX509(); // SecPolicyCreateSSL(YES,nil);
CFArrayRef chain = CFArrayCreate(NULL, (const void**)(&certificateRef), 1, NULL);
SecTrustRef trustRef;
SecTrustCreateWithCertificates(chain, policy, &trustRef);
SecTrustResultType result;
SecTrustEvaluate (trustRef, &result);
if(result == kSecTrustResultProceed) {
isUserTrust = TRUE;
isInvalid = FALSE;
} else
if (result == kSecTrustResultUnspecified)
isInvalid = FALSE;
CFRelease(trustRef);
CFRelease(chain);
// Images as per /System/Library/Frameworks/SecurityInterface.framework/Versions/A/Resources
// Cert <Small | Large> <Personal | Root> [_Invalid | _UserTrust ]
//
return [NSImage imageNamed:[NSString stringWithFormat:#"Cert%#%#%#",
isSmall ? #"Small" : #"Large",
isCA ? #"Root" : #"Personal",
isInvalid ? #"_Invalid" : (isUserTrust ? #"_UserTrust" : #"")]];
}
#end
The basicConstraints extension is defined in X.509 as follows:
basicConstraints EXTENSION ::= {
SYNTAX BasicConstraintsSyntax
IDENTIFIED BY id-ce-basicConstraints }
BasicConstraintsSyntax ::= SEQUENCE {
cA BOOLEAN DEFAULT FALSE,
pathLenConstraint INTEGER (0..MAX) OPTIONAL }
This, in turn, is encoded according to the Distinguished Encoding Rules (X.690). The individual parts of the BasicConstraintsSyntax sequence do not have their own OIDs.
I wouldn’t claim to be an expert, but I don’t think there is a problem with the test for the cA flag that you are doing. I don’t think the [NSString stringWithFormat:#"%#", ...] part is necessary, mind.
As for checking for self-signed certificates, it seems not unreasonable to test the subject and issuer names; obviously that really tells you whether the certificate claims to be self-signed, and to actually test that it is you’d need to check the signature yourself (whether that’s something you want to do, I don’t know). FYI, on the topic of the key identifiers, according to RFC3280, in the specific case of a self-signed certificate, the authority key identifier can be omitted, so a certificate without an authority key identifier might be an indication that the certificate was self-signed, but it’s entirely possible for someone to deliberately issue a malformed certificate without authority key identifier.
The final question appears to be cleared up by looking at the docs, which indicate that the values mean roughly what you say.
The other thing worth saying is that there is code out there that can help with this kind of thing; for instance, Jens Alfke’s MYCrypto library
Edit (8th Feb 2012)
According to the X.509 Style Guide, it’s possible for the subject name to be empty, in which case you would need to look at the subjectAltName extension instead. It’s very unlikely that this would break a test for self-signed certificates that relied on comparing the subject and issuer names — it seems unreasonable to issue a certificate to yourself providing a DN for the issuer name but then leaving the subject name empty. However, it’s worth bearing in mind.

Resources