I am trying to connect to a server that requires a client certificate. So the normal flow of events that happens when browsing to this server is that the web browser (both Safari and Chrome) prompts the user to select a certificate and retry the operation.
So how can I accomplish this in a embedded WebView in a Cocoa project?
I have so far identified that the error is raised in the didFailProvisionalLoadWithError
method:
- (开发者_Python百科void)webView:(WebView *)sender didFailProvisionalLoadWithError:(NSError *)error forFrame:(WebFrame *)frame {
NSLog(@"webView:didFailProvisionalLoadWithError:forFrame:");
NSLog(@" error = %@", error);
}
The error is indeed error = Error Domain=NSURLErrorDomain Code=-1206 UserInfo=0x1006a8030 "The server “myserver.xxx” requires a client certificate.
Problem solved.
A (known) issue with the WebView component was the culprit. Opened up a DTS support ticket with Apple and got a workaround.
EDIT: Here's the workaround from DTS ( I have no idea if this is still valid, since it was 3 years ago):
Magnus OK, I've had a chance to look at this and I know what's going on. Before we start talking about WebView, I need to bring you up to speed on the delegate methods used by NSURLConnection, which is the underlying API used to actually load data off the 'net. NSURLConnection started out supporting a single authentication delegate callback, -connection:didReceiveAuthenticationChallenge:, to which it passed the various authentication challenges that were supported at that time (username/password-style challenges). In Mac OS X 10.6 (and iOS 3.0) NSURLConnection was enhanced to support two addition types of authentication challenges for TLS connections: o client identity challenges (NSURLAuthenticationMethodClientCertificate), giving the delegate the opportunity to select a client identity for a given TLS connection o server trust challenges (NSURLAuthenticationMethodServerTrust), giving the delegate the opportunity to override the server trust evaluation for a given TLS connection For compatibility reasons it was not possible to pass these challenges to the delegate under all circumstances, so NSURLConnection introduced a new delegate callback, -connection:canAuthenticateAgainstProtectionSpace:, that allows the delegate to opt in to these challenges. * * * Now, let's bring this back to your app. As I mentioned, WebView uses NSURLConnection and, for each connection, acts as the connection delegate. It intercepts authentication challenges and passes them to its resource load delegate. This works just fine for old school authentication challenges, because WebView gets the challenge without having to do anything special; but it fails for TLS connection authentication challenges, because the delegate has to opt in those challenges. What you really need is the WebView version of the 'canAuthenticateAgainstProtectionSpace' authentication challenge. Well, it turns out that this is actually implemented. In looking through the open source for WebView, I found that there's a private delegate callback, -webView:resource:canAuthenticateAgainstProtectionSpace:forDataSource:, that does exactly what you want. http://www.opensource.apple.com/source/WebKit/WebKit-7533.20.25/mac/WebView/WebResourceLoadDelegatePrivate.h If you implement that method you can opt in to the client identity authentication challenge and, based on that challenge, present a user interface that allows the user to select an identity. I prototyped this in your test app and it works a charm. Here's the code I used to get the client identity challenge: - (BOOL)webView:(WebView *)sender resource:(id)identifier canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace forDataSource:(WebDataSource *)dataSource { NSLog(@"%@", [protectionSpace authenticationMethod]); return [[protectionSpace authenticationMethod] isEqual:NSURLAuthenticationMethodClientCertificate]; } and here's the code I used to respond to it: - (void)webView:(WebView *)sender resource:(id)identifier didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge fromDataSource:(WebDataSource *)dataSource { NSLog(@"didReceiveAuthenticationChallenge"); NSString *authenticationMethod = [[challenge protectionSpace] authenticationMethod]; NSLog(@" authenticationMethod = %@", authenticationMethod);
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge]; }
Obviously in a real app you'd need to display some UI and then, once the user has selected a client identity, create a credential for it (+[NSURLCredential credentialWithIdentity:certificates:persistence:]) and then apply that credential to the chalenge (-useCredential:forAuthenticationChallenge:). * * * So where do you proceed from here? Regardless of what else you do, you should file a bug against WebView to get the -webView:resource:canAuthenticateAgainstProtectionSpace:forDataSource: delegate callback published in the public headers. It's an obvious, and most annoying, omission. http://developer.apple.com/bugreporter/ Once you've filed a bug, please send me the bug number so that I can associate it with this incident. Beyond that, the way forward is less clear. If you're creating a non-Mac App Store app, my recommendation would be that you just implement the 'canAuthenticateAgainstProtectionSpace' delegate callback as I've shown above and move on with your life. OTOH, if you're creating a Mac App Store app, where the use of private API, including delegate callbacks, is strictly prohibited, life gets a lot trickier. Let me know in that case and we can discuss your options. Share and Enjoy
Set a WebResourceLoadDelegate
and implement the authentication-challenge–related delegate methods. You will be prompted when an authentication challenge is received, at which time you can provide the certificate to use.
ETA: Here is how you can create an NSURLCredential
from a certificate stored in clientSide.p12
:
NSString *thePath = [[NSBundle mainBundle]
pathForResource:@"clientside" ofType:@"p12"];
NSData *PKCS12Data = [[NSData alloc] initWithContentsOfFile:thePath];
CFDataRef inPKCS12Data = (CFDataRef)PKCS12Data;
SecIdentityRef identity;
SecTrustRef trust;
extractIdentityAndTrust(inPKCS12Data, &identity, &trust);
SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate (identity, &certificate);
const void *certs[] = {certificate};
CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
NSURLCredential *credential = [NSURLCredential
credentialWithIdentity:identity
certificates:(NSArray*)certArray
persistence:NSURLCredentialPersistencePermanent];
This comes from another question. You might also find this question helpful. I found these by Googling for "nsurlcredential certificate".
精彩评论