NSURLCredentialStorage setDefaultCredential: doesn't work for [NSURLSession sharedSession]

812 views Asked by At

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.

1

There are 1 answers

4
dgatwood On

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.