0

I'm stumped: my app needs to connect to a server which uses self-signed certificates for HTTPS and requires client-side authentication. Worse, I actually need the iOS media player to connect to that server, so I have followed Apple's instruction for this to the letter:

credential = [NSURLCredential credentialWithIdentity:identity certificates:certs persistence:NSURLCredentialPersistenceForSession];
NSURLProtectionSpace *space = [[NSURLProtectionSpace alloc] initWithHost:@"server.com" 
                                                                    port:0
                                                                protocol:NSURLProtectionSpaceHTTPS 
                                                                   realm:nil 
                                                    authenticationMethod:NSURLAuthenticationMethodClientCertificate];
[[NSURLCredentialStorage sharedCredentialStorage] setDefaultCredential:credential forProtectionSpace:space];

But it just won't work. So I tried to do a request to the server manually:

NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://server.com"]
                                       completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                           NSLog(@"Done : %@", error ? error : @"OK");
                                       }];

all I get is this error:

2016-06-13 08:22:37.767 TestiOSSSL[3172:870700] CFNetwork SSLHandshake failed (-9824 -> -9829)
2016-06-13 08:22:37.793 TestiOSSSL[3172:870700] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9829)
2016-06-13 08:22:37.815 TestiOSSSL[3172:870685] Done : Error Domain=NSURLErrorDomain Code=-1206 "The server “ server.com” requires a client certificate." UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x13de519b0>, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9829, NSUnderlyingError=0x13de4f280 {Error Domain=kCFErrorDomainCFNetwork Code=-1206 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=1, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x13de519b0>, _kCFNetworkCFStreamSSLErrorOriginalValue=-9829, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9829, kCFStreamPropertySSLPeerCertificates=<CFArray 0x13dda0a70 [0x1a0dc2150]>{type = immutable, count = 2, values = (
    0 : <cert(0x13dda4970) s: Server.com i: Localhost CA>
    1 : <cert(0x13dda50d0) s: Localhost CA i: Localhost CA>
)}}}, NSErrorPeerCertificateChainKey=<CFArray 0x13dda0a70 [0x1a0dc2150]>{type = immutable, count = 2, values = (
    0 : <cert(0x13dda4970) s: Server.com i: Localhost CA>
    1 : <cert(0x13dda50d0) s: Localhost CA i: Localhost CA>
)}, NSLocalizedDescription=The server “server.com” requires a client certificate., NSErrorFailingURLKey=https://server.com/, NSErrorFailingURLStringKey=https://server.com/, NSErrorClientCertificateStateKey=1}

Now if I set up my own NSURLSession and use the URLSession:didReceiveChallenge:completionHandler: callback:

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
theSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
NSURLSessionDataTask *task = [theSession dataTaskWithURL:[NSURL URLWithString:@"https://server.com"]
                                       completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                                           NSLog(@"Done : %@", error ? error : @"OK");
                                       }];

and then:

- (void)URLSession:(NSURLSession *)session 
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
                             NSURLCredential *credential))completionHandler
{
    NSLog(@"Asking for credential");
    NSURLCredential *conf = [session.configuration.URLCredentialStorage defaultCredentialForProtectionSpace:challenge.protectionSpace];
    completionHandler(NSURLSessionAuthChallengeUseCredential, conf);
}

Notice how I'm using [session.configuration.URLCredentialStorage defaultCredentialForProtectionSpace:challenge.protectionSpace], which is what I suppose the default implementation of the NSURLSession does when it gets an authentication challenge.

That works, for this particular connection! Which proves that the credential is OK and that it is properly registered as the default credential in the default NSURLCredentialStorage.

But any solution hinging around the didReceiveChallenge: callback is no good because I can't control which NSURLSession the media player is using.

I've tried the CustomHTTPProtocol hack and that doesn't work either.

Any suggestion? I've gone through all the similar posts on SO, I can't find a solution for this. This post is really close, but the accepted answer doesn't make sense to me and clearly contradicts Apple's documentation.

Community
  • 1
  • 1
Guy Moreillon
  • 993
  • 10
  • 28

1 Answers1

0

Although lots of functionality is shared between the default session and NSURLConnection, apparently that bit isn't. Have you tried calling that method on [NSURLSession sharedSession].configuration.URLCredentialStorage?

The other possibility is that the requests are happening in a separate task, in which case it may not be possible to do it in the way that you're trying, because it will involve a different shared session. If that's the case, you'll probably have to store the credential into the keychain yourself and trust that the other process will share the keychain and fetch the credential properly.

dgatwood
  • 10,129
  • 1
  • 28
  • 49
  • Thanks for the suggestion, yes, I did try calling it on the sharedSession, no joy. In any case, using the shared credential storage is never going to work in my case because the iOS MediaPlayer is not running in my process at all, it is completely isolated into a separate process and only shares the screen with my app. The only solution I could find that actually worked was to setup an HTTP proxy in my app that posed as the server to the media player and forwarded the requests it received to the actual server, doing the credential handling on its own connection. Clunky :-( – Guy Moreillon Jul 25 '16 at 09:50
  • Yeah, that's a problem for folks doing stuff with web views, too. If you haven't already, file a bug with Apple and ask for a way to provide credentials to the media player. Incidentally, video playback credentials have been a recurring problem for many years. There are still hacks in one of my websites to work around the fact that Safari's built-in video playback couldn't pass basic or digest auth credentials a few years ago. I filed a bug, and they eventually fixed it, but clearly that's not a usage model that they think about much. File a bug to remind them that authentication matters. – dgatwood Jul 25 '16 at 21:04