Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TIMOB-19154]: iOS9 Deprecate NSURLConnection and replace with NSURLSession API #46

Merged
merged 11 commits into from
Feb 22, 2018
203 changes: 203 additions & 0 deletions iphone/appcelerator.https/SecurityManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,23 @@ - (ClientCertificate *)clientCertificateForHost:(NSString *)host {
// Return FALSE unless the NSURLAuthenticationChallenge is for TLS trust
// validation (aka NSURLAuthenticationMethodServerTrust) and this security
// manager was configured to handle the current url.

-(BOOL)willHandleChallenge:(NSURLAuthenticationChallenge *)challenge forSession:(NSURLSession *)session {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space

BOOL result = NO;
if ([challenge.protectionSpace.authenticationMethod isEqualToString: NSURLAuthenticationMethodServerTrust] ||
[challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate])
{
NSURL *currentURL = [NSURL URLWithString:challenge.protectionSpace.host];
if (currentURL.scheme == nil) {
currentURL = [NSURL URLWithString:[NSString stringWithFormat:@"https://%@",challenge.protectionSpace.host]];
}
result = [self willHandleURL:currentURL];
}

DebugLog(@"%s returns %@, challenge = %@, session = %@ URL = %@", __PRETTY_FUNCTION__, NSStringFromBOOL(result), challenge, session, challenge.protectionSpace.host);
return result;
}

-(BOOL)willHandleChallenge:(NSURLAuthenticationChallenge *)challenge forConnection:(NSURLConnection *)connection {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove the old NSURLConnection references and bump the module version to the next major.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not remove NSURLConnection in this release. If someone is using this module(this version) with SDK < 7.1.0, in that case it will create problem. Reason is, before 7.1.0 APSHTTPClient will use NSURLConnection apis from this module. Rather we should plan to remove NSURLConnection from this module with SDK 8.0.0. Till then most of developer will have moved to SDK 7.1.0+ . I'll create a ticket for same.
And this version should be released with/before, SDK 7.1.0. With SDK 7.1.0 and above , only this version of this module will work.

Copy link
Contributor

@hansemannn hansemannn Jan 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good reason. I thought it's not compatible anymore, but thats the other way around right? If devs update to 7.1.0, they also need to update to the latest https module. Talking about that, please bump the version to the next feature version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes with 7.1.0, latest https is mandatory. Updated version.

BOOL result = NO;
if ([challenge.protectionSpace.authenticationMethod isEqualToString: NSURLAuthenticationMethodServerTrust] ||
Expand All @@ -232,6 +249,192 @@ -(BOOL)willHandleChallenge:(NSURLAuthenticationChallenge *)challenge forConnecti

#pragma mark NSURLConnectionDelegate methods

-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space

{
DebugLog(@"%s session = %@, challenge = %@", __PRETTY_FUNCTION__, session, challenge);

// Normalize the server's host name to lower case.
NSString *host = [task.currentRequest.URL.host lowercaseString];

DebugLog(@"%s Normalized host name = %@", __PRETTY_FUNCTION__, host);

// Get the PinnedURL for this server.
ClientCertificate *pinnedClientCertificate = [self clientCertificateForHost:host];
NSString *authenticationMethod = [[challenge protectionSpace] authenticationMethod];

// Handle Two-phase mutual client-authentification
if ([authenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate] && pinnedClientCertificate != nil) {
NSData *p12Data = [NSData dataWithContentsOfURL:pinnedClientCertificate.url];

if (!p12Data) {
NSString *reason = [NSString stringWithFormat:@"Certificate data could not be extracted for host = %@.", task.currentRequest.URL.host];
NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];

@throw exception;
}

CFDataRef inPKCS12Data = (__bridge CFDataRef)p12Data;
SecIdentityRef identity;

OSStatus result = [self extractIdentity:&identity from:inPKCS12Data with:pinnedClientCertificate.password];

if (result != noErr) {
[challenge.sender cancelAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}

SecCertificateRef certificate = NULL;
SecIdentityCopyCertificate (identity, &certificate);

const void *certificates[] = { certificate };
CFArrayRef certificatesArray = CFArrayCreate(kCFAllocatorDefault, certificates, 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*)certificatesArray
persistence:NSURLCredentialPersistencePermanent];

[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
return;
}

if (![authenticationMethod isEqualToString: NSURLAuthenticationMethodServerTrust]) {
[challenge.sender cancelAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}

// It is a logic error (i.e. a bug in Titanium) if this method is
// called with a URL the security manager was not configured to
// handle.
if (![self willHandleURL:task.currentRequest.URL]) {
NSString *reason = [NSString stringWithFormat:@"LOGIC ERROR: Titanium bug called this SecurityManager with an unknown host \"%@\". Please report this issue to us at https://jira.appcelerator.org/browse/TIMOB", task.currentRequest.URL.host];
NSDictionary *userInfo = @{ @"session" : session };
NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:userInfo];

@throw exception;
}


SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
if(serverTrust == nil) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space

DebugLog(@"%s FAIL: challenge.protectionSpace.serverTrust is nil", __PRETTY_FUNCTION__);
[challenge.sender cancelAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}

// SecTrustEvaluate performs customary X509
// checks. Unusual conditions will cause the function to
// return *non-success*. Unusual conditions include an
// expired certifcate or self signed certifcate.
OSStatus status = SecTrustEvaluate(serverTrust, NULL);
if(status != errSecSuccess) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space

DebugLog(@"%s FAIL: standard TLS validation failed. SecTrustEvaluate returned %@", __PRETTY_FUNCTION__, @(status));
[challenge.sender cancelAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}

DebugLog(@"%s SecTrustEvaluate returned %@", __PRETTY_FUNCTION__, @(status));

// Get the PinnedURL for this server.
PublicKey *pinnedPublicKey = [self publicKeyForHost:host];

// It is a logic error (a bug in this SecurityManager class) if this
// security manager does not have a PinnedURL for this server.
if (pinnedPublicKey == nil) {
NSString *reason = [NSString stringWithFormat:@"LOGIC ERROR: appcelerator.https module bug: SecurityManager could not find a PublicKey for host \"%@\". Please report this issue to us at https://jira.appcelerator.org/browse/MOD-1706", task.currentRequest.URL.host];
NSDictionary *userInfo = @{ @"session" : session };
NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:userInfo];

@throw exception;
}

DebugLog(@"%s host %@ pinned to publicKey %@", __PRETTY_FUNCTION__, host, pinnedPublicKey);

CFIndex count = SecTrustGetCertificateCount(serverTrust);
CFIndex i = 0;

DebugLog(@"Number of certificates: %ld", count);

for (i = 0; i < count; i++) {
SecCertificateRef item = SecTrustGetCertificateAtIndex(serverTrust, i);
NSString *desc = (NSString *)CFBridgingRelease(CFCopyDescription(item));

DebugLog(@"%ld: %@", i, desc);
}

// Obtain the server's X509 certificate and public key.
SecCertificateRef serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, pinnedPublicKey.trustChainIndex);
if(serverCertificate == nil) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space

DebugLog(@"%s FAIL: Could not find the server's X509 certificate in serverTrust", __PRETTY_FUNCTION__);
[challenge.sender cancelAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
return;
}

// Create a friendlier Objective-C wrapper around this server's X509
// certificate.
X509Certificate *x509Certificate = [X509Certificate x509CertificateWithSecCertificate:serverCertificate andTrustChainIndex:pinnedPublicKey.trustChainIndex];
if (x509Certificate == nil) {
// CFBridgingRelease transfer's ownership of the CFStringRef
// returned by CFCopyDescription to ARC.
NSString *serverCertificateDescription = (NSString *)CFBridgingRelease(CFCopyDescription(serverCertificate));
NSString *reason = [NSString stringWithFormat:@"LOGIC ERROR: appcelerator.https module bug: SecurityManager could not create an X509Certificate for host \"%@\" using the SecCertificateRef \"%@\". Please report this issue to us at https://jira.appcelerator.org/browse/MOD-1706", task.currentRequest.URL.host, serverCertificateDescription];
NSDictionary *userInfo = @{ @"x509Certificate" : [NSNull null] };
NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:userInfo];

@throw exception;
}

DebugLog(@"%s server's X509 certificate = %@", __PRETTY_FUNCTION__, x509Certificate);
// Get the public key from this server's X509 certificate.
PublicKey *serverPublicKey = x509Certificate.publicKey;
if (serverPublicKey == nil) {
NSString *reason = [NSString stringWithFormat:@"LOGIC ERROR: appcelerator.https module bug: SecurityManager could not find the server's public key for host \"%@\" in the X509 certificate \"%@\". Please report this issue to us at https://jira.appcelerator.org/browse/MOD-1706", task.currentRequest.URL.host, x509Certificate];
NSDictionary *userInfo = @{ @"x509Certificate" : x509Certificate };
NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:userInfo];

@throw exception;
}

DebugLog(@"%s server's public key = %@", __PRETTY_FUNCTION__, serverPublicKey);

// Compare the public keys. If they match, then the server is
// authenticated.
BOOL publicKeysAreEqual = [pinnedPublicKey isEqualToPublicKey:serverPublicKey];
if(!publicKeysAreEqual) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Space

DebugLog(@"[WARN] Potential \"Man-in-the-Middle\" attack detected since host %@ does not hold the private key corresponding to the public key %@.", host, pinnedPublicKey);

NSDictionary *userDict = @{@"pinnedPublicKey": pinnedPublicKey, @"serverPublicKey": serverPublicKey };

NSException *exception = [NSException exceptionWithName:NSInvalidArgumentException
reason:@"Certificate could not be verified with provided public key"
userInfo:userDict];
@throw exception;
}

DebugLog(@"%s publicKeysAreEqual = %@", __PRETTY_FUNCTION__, NSStringFromBOOL(publicKeysAreEqual));
// Return success since the server holds the private key
// corresponding to the public key held bu this security manager.
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
[challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
}

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
DebugLog(@"%s connection = %@, challenge = %@", __PRETTY_FUNCTION__, connection, challenge);
Expand Down