Here’s another one on certificates. Let’s say you are trying to do client certificate-based authentication in your iOS app and you came up with something like this:
- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge { if ([challenge previousFailureCount] > 0) { NSLog(@"Incorrect auth challenge %@", challenge); [[challenge sender] cancelAuthenticationChallenge:challenge]; return; } // Checking the server certificate if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodClientCertificate && self.account.clientCertificate.privateRepresentation.length > 0) { /* Reading the certificate and creating the identity */ NSData *p12Data = self.account.clientCertificate.privateRepresentation; CFStringRef password = (__bridge CFStringRef)(self.account.clientCertificate.privatePassword); const void *keys[] = { kSecImportExportPassphrase }; const void *values[] = { password }; CFDictionaryRef optionsDictionary = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL); CFArrayRef p12Items; OSStatus result = SecPKCS12Import((__bridge CFDataRef)p12Data, optionsDictionary, &p12Items); if(result == noErr) { CFDictionaryRef identityDict = CFArrayGetValueAtIndex(p12Items, 0); SecIdentityRef identityApp =(SecIdentityRef)CFDictionaryGetValue(identityDict,kSecImportItemIdentity); SecCertificateRef certRef; SecIdentityCopyCertificate(identityApp, &certRef); SecCertificateRef certArray[1] = { certRef }; CFArrayRef myCerts = CFArrayCreate(NULL, (void *)certArray, 1, NULL); CFRelease(certRef); NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityApp certificates:(__bridge NSArray *)myCerts persistence:NSURLCredentialPersistencePermanent]; CFRelease(myCerts); [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; } else { // Certificate is invalid or password is invalid given the certificate NSLog(@"Invalid certificate or password"); return; } } else { // For normal authentication based on username and password. This could be NTLM or Default. if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodClientCertificate) { NSLog(@"Certificate might be required"); } NSURLCredential *credential = [NSURLCredential credentialWithUser:self.account.username password:self.account.password persistence:NSURLCredentialPersistenceNone]; [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; } }
All looks good, doesn’t it? And in most cases – it will even work fine. Unless your server handles the handshake, something goes wrong and you look at what happens on the wire:
Can you see what happens here? The client certificate is sent twice. That is not always an issue, but if your server is parsing the certificates, and expects them to be in order, and expects that the first one is the client certificate and what follows is the issuer… you are in trouble.
You may argue whether this is a client bug or the server issue… I would say the client should be fixed. Per RFC2246 and RFC5246 the certificate structure should contain sender’s cert once.
certificate_list
This is a sequence (chain) of X.509v3 certificates. The sender’s
certificate must come first in the list. Each following
certificate must directly certify the one preceding it. Because
certificate validation requires that root keys be distributed
independently, the self-signed certificate which specifies the
root certificate authority may optionally be omitted from the
chain, under the assumption that the remote end must already
possess it in order to validate it in any case.
The same message type and structure will be used for the client’s
response to a certificate request message. Note that a client may
send no certificates if it does not have an appropriate certificate
to send in response to the server’s authentication request.
So how do we fix it? Fortunately the fix is trivial. Change this line:
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityApp certificates:(__bridge NSArray *)myCerts persistence:NSURLCredentialPersistencePermanent];
to this:
NSURLCredential *credential = [NSURLCredential credentialWithIdentity:identityApp certificates:nil persistence:NSURLCredentialPersistencePermanent];
And everybody is happy again. The app sends a single certificate only, no change on the server required.
This was a lifesaver! After struggling with this issue for days, this simple and elegant solution solved the problem. I was using the standard methodology and couldn’t for the life of me figure out why it wasn’t working.
Many thanks Michał!
–Sal
It’s a very good article. It helps for me.
Respect to author
Interesting post. I am a bit confused on how this works, however, since the Apple docs say the certificates: parameter must be non-zero.
I have also seen reports of people saying this causes problems with their code (see one of the lower comments on this post: http://stackoverflow.com/questions/16371066/when-using-client-certificate-authentication-why-do-i-keep-getting-nsurlerrordo)
Are you sure this is safe to do?