Skip to content

Commit

Permalink
Pinning failure better error (AFNetworking#3425)
Browse files Browse the repository at this point in the history
* Error code on pinning failure must be meaningful

`NSURLErrorServerCertificateUntrusted` instead of `NSURLErrorCancelled`

* Remove unneeded __block qualifier

* Add warnings about implementing an authentication challenge block

* Simpler session-level authentication request handling

Handle session-level authentication requests exclusively through user-provided `sessionDidReceiveAuthenticationChallenge` block. When `sessionDidReceiveAuthenticationChallenge` is not set, go through the `URLSession:task:didReceiveChallenge:completionHandler:` delegate method instead.

* Precise error when the server trust is invalid

Fixes #3165

* Better server trust error with userInfo dictionary

* Simplify code flow

* Better setSessionDidReceiveAuthenticationChallengeBlock: documentation

* Do not completely bypass the security policy

Let the possibility to the `taskDidReceiveAuthenticationChallenge` block to fallback to security policy evaluation by returning `NSURLSessionAuthChallengePerformDefaultHandling`.

* Better expectation descriptions for invalid server trust tests

* Rename ServerTrustErrorKey into AuthenticationChallengeErrorKey

* New authentication challenge hander block

Deprecate `setTaskDidReceiveAuthenticationChallengeBlock:` in favor of the new `setAuthenticationChallengeHandler:` method.

The new challenge handler allows
* The user to handle the completion handler themself (fixes #2520)
* The security policy to be evaluated
* Aborting the connection with a proper error (see also #3165)

* Fix nullability annotation

* Throw if the authentication challenge handler returns an invalid value

* Document -[AFURLSessionManager setAuthenticationChallengeHandler:]
  • Loading branch information
0xced authored Mar 27, 2020
1 parent a1f208b commit c4d8f62
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 36 deletions.
27 changes: 26 additions & 1 deletion AFNetworking/AFURLSessionManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,11 @@ NS_ASSUME_NONNULL_BEGIN
Sets a block to be executed when a connection level authentication challenge has occurred, as handled by the `NSURLSessionDelegate` method `URLSession:didReceiveChallenge:completionHandler:`.
@param block A block object to be executed when a connection level authentication challenge has occurred. The block returns the disposition of the authentication challenge, and takes three arguments: the session, the authentication challenge, and a pointer to the credential that should be used to resolve the challenge.
@warning Implementing a session authentication challenge handler yourself totally bypasses AFNetworking's security policy defined in `AFSecurityPolicy`. Make sure you fully understand the implications before implementing a custom session authentication challenge handler. If you do not want to bypass AFNetworking's security policy, use `setTaskDidReceiveAuthenticationChallengeBlock:` instead.
@see -securityPolicy
@see -setTaskDidReceiveAuthenticationChallengeBlock:
*/
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block;

Expand All @@ -370,7 +375,27 @@ NS_ASSUME_NONNULL_BEGIN
@param block A block object to be executed when a session task has received a request specific authentication challenge. The block returns the disposition of the authentication challenge, and takes four arguments: the session, the task, the authentication challenge, and a pointer to the credential that should be used to resolve the challenge.
*/
- (void)setTaskDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block;
- (void)setTaskDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block DEPRECATED_MSG_ATTRIBUTE("Use setAuthenticationChallengeHandler: instead.");

/**
Sets a block to be executed when a session task has received a request specific authentication challenge, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didReceiveChallenge:completionHandler:`.
@param authenticationChallengeHandler A block object to be executed when a session task has received a request specific authentication challenge.
When implementing an authentication challenge handler, you should check the authentication method first (`challenge.protectionSpace.authenticationMethod `) to decide if you want to handle the authentication challenge yourself or if you want AFNetworking to handle it. If you want AFNetworking to handle the authentication challenge, just return `@(NSURLSessionAuthChallengePerformDefaultHandling)`. For example, you certainly want AFNetworking to handle certificate validation (i.e. authentication method == `NSURLAuthenticationMethodServerTrust`) as defined by the security policy. If you want to handle the challenge yourself, you have four options:
1. Return `nil` from the authentication challenge handler. You **MUST** call the completion handler with a disposition and credentials yourself. Use this if you need to present a user interface to let the user enter their credentials.
2. Return an `NSError` object from the authentication challenge handler. You **MUST NOT** call the completion handler when returning an `NSError `. The returned error will be reported in the completion handler of the task. Use this if you need to abort an authentication challenge with a specific error.
3. Return an `NSURLCredential` object from the authentication challenge handler. You **MUST NOT** call the completion handler when returning an `NSURLCredential`. The returned credentials will be used to fulfil the challenge. Use this when you can get credentials without presenting a user interface.
4. Return an `NSNumber` object wrapping an `NSURLSessionAuthChallengeDisposition`. Supported values are `@(NSURLSessionAuthChallengePerformDefaultHandling)`, `@(NSURLSessionAuthChallengeCancelAuthenticationChallenge)` and `@(NSURLSessionAuthChallengeRejectProtectionSpace)`. You **MUST NOT** call the completion handler when returning an `NSNumber`.
If you return anything else from the authentication challenge handler, an exception will be thrown.
For more information about how URL sessions handle the different types of authentication challenges, see [NSURLSession](https://developer.apple.com/reference/foundation/nsurlsession?language=objc) and [URL Session Programming Guide](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/URLLoadingSystem/URLLoadingSystem.html).
@see -securityPolicy
*/
- (void)setAuthenticationChallengeHandler:(id (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition , NSURLCredential * _Nullable)))authenticationChallengeHandler;

/**
Sets a block to be executed periodically to track upload progress, as handled by the `NSURLSessionTaskDelegate` method `URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:`.
Expand Down
88 changes: 57 additions & 31 deletions AFNetworking/AFURLSessionManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ static dispatch_group_t url_session_manager_completion_group() {

typedef NSURLRequest * (^AFURLSessionTaskWillPerformHTTPRedirectionBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request);
typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionTaskDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential);
typedef id (^AFURLSessionTaskAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential));
typedef void (^AFURLSessionDidFinishEventsForBackgroundURLSessionBlock)(NSURLSession *session);

typedef NSInputStream * (^AFURLSessionTaskNeedNewBodyStreamBlock)(NSURLSession *session, NSURLSessionTask *task);
Expand Down Expand Up @@ -167,12 +168,30 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
}
}

static const void * const AuthenticationChallengeErrorKey = &AuthenticationChallengeErrorKey;

static NSError * ServerTrustError(SecTrustRef serverTrust, NSURL *url)
{
NSBundle *CFNetworkBundle = [NSBundle bundleWithIdentifier:@"com.apple.CFNetwork"];
NSString *defaultValue = @"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “%@” which could put your confidential information at risk.";
NSString *descriptionFormat = NSLocalizedStringWithDefaultValue(@"Err-1202.w", nil, CFNetworkBundle, defaultValue, @"") ?: defaultValue;
NSString *localizedDescription = [descriptionFormat componentsSeparatedByString:@"%@"].count <= 2 ? [NSString localizedStringWithFormat:descriptionFormat, url.host] : descriptionFormat;
NSDictionary *userInfo = @{
NSURLErrorFailingURLErrorKey: url,
NSURLErrorFailingURLStringErrorKey: url.absoluteString,
NSURLErrorFailingURLPeerTrustErrorKey: (__bridge id)serverTrust,
NSLocalizedDescriptionKey: localizedDescription
};
return [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateUntrusted userInfo:userInfo];
}

#pragma mark - NSURLSessionTaskDelegate

- (void)URLSession:(__unused NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
error = objc_getAssociatedObject(task, AuthenticationChallengeErrorKey) ?: error;
__strong AFURLSessionManager *manager = self.manager;

__block id responseObject = nil;
Expand Down Expand Up @@ -453,6 +472,7 @@ @interface AFURLSessionManager ()
@property (readwrite, nonatomic, copy) AFURLSessionDidFinishEventsForBackgroundURLSessionBlock didFinishEventsForBackgroundURLSession AF_API_UNAVAILABLE(macos);
@property (readwrite, nonatomic, copy) AFURLSessionTaskWillPerformHTTPRedirectionBlock taskWillPerformHTTPRedirection;
@property (readwrite, nonatomic, copy) AFURLSessionTaskDidReceiveAuthenticationChallengeBlock taskDidReceiveAuthenticationChallenge;
@property (readwrite, nonatomic, copy) AFURLSessionTaskAuthenticationChallengeBlock authenticationChallengeHandler;
@property (readwrite, nonatomic, copy) AFURLSessionTaskNeedNewBodyStreamBlock taskNeedNewBodyStream;
@property (readwrite, nonatomic, copy) AFURLSessionTaskDidSendBodyDataBlock taskDidSendBodyData;
@property (readwrite, nonatomic, copy) AFURLSessionTaskDidCompleteBlock taskDidComplete;
Expand Down Expand Up @@ -913,7 +933,9 @@ - (NSString *)description {
}

- (BOOL)respondsToSelector:(SEL)selector {
if (selector == @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)) {
if (selector == @selector(URLSession:didReceiveChallenge:completionHandler:)) {
return self.sessionDidReceiveAuthenticationChallenge != nil;
} else if (selector == @selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)) {
return self.taskWillPerformHTTPRedirection != nil;
} else if (selector == @selector(URLSession:dataTask:didReceiveResponse:completionHandler:)) {
return self.dataTaskDidReceiveResponse != nil;
Expand Down Expand Up @@ -945,27 +967,10 @@ - (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
NSAssert(self.sessionDidReceiveAuthenticationChallenge != nil, @"`respondsToSelector:` implementation forces `URLSession:didReceiveChallenge:completionHandler:` to be called only if `self.sessionDidReceiveAuthenticationChallenge` is not nil");

if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
NSURLCredential *credential = nil;
NSURLSessionAuthChallengeDisposition disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);

if (completionHandler) {
completionHandler(disposition, credential);
Expand Down Expand Up @@ -996,21 +1001,42 @@ - (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
BOOL evaluateServerTrust = NO;
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
NSURLCredential *credential = nil;

if (self.taskDidReceiveAuthenticationChallenge) {
if (self.authenticationChallengeHandler) {
NSAssert(self.taskDidReceiveAuthenticationChallenge == nil, @"Do not call both `setAuthenticationChallengeHandler:` and `setTaskDidReceiveAuthenticationChallengeBlock:`");
id result = self.authenticationChallengeHandler(session, task, challenge, completionHandler);
if (result == nil) {
return;
} else if ([result isKindOfClass:NSError.class]) {
objc_setAssociatedObject(task, AuthenticationChallengeErrorKey, result, OBJC_ASSOCIATION_RETAIN);
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
} else if ([result isKindOfClass:NSURLCredential.class]) {
credential = result;
disposition = NSURLSessionAuthChallengeUseCredential;
} else if ([result isKindOfClass:NSNumber.class]) {
disposition = [result integerValue];
NSAssert(disposition == NSURLSessionAuthChallengePerformDefaultHandling || disposition == NSURLSessionAuthChallengeCancelAuthenticationChallenge || disposition == NSURLSessionAuthChallengeRejectProtectionSpace, @"");
evaluateServerTrust = disposition == NSURLSessionAuthChallengePerformDefaultHandling && [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
} else {
@throw [NSException exceptionWithName:@"Invalid Return Value" reason:@"The return value from the authentication challenge handler must be nil, an NSError, an NSURLCredential or an NSNumber." userInfo:nil];
}
} else if (self.taskDidReceiveAuthenticationChallenge) {
NSLog(@"WARNING: -[AFURLSessionManager setTaskDidReceiveAuthenticationChallengeBlock:] is deprecated, use -[AFURLSessionManager setAuthenticationChallengeHandler:] instead.");
disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential);
} else {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
evaluateServerTrust = YES;
}

if (evaluateServerTrust) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
objc_setAssociatedObject(task, AuthenticationChallengeErrorKey, ServerTrustError(challenge.protectionSpace.serverTrust, task.currentRequest.URL), OBJC_ASSOCIATION_RETAIN);
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}

Expand Down
36 changes: 32 additions & 4 deletions Tests/Tests/AFHTTPSessionManagerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ - (void)testInvalidPublicKeyPinningSecurityPolicyWithoutBaseURL {
# pragma mark - Server Trust

- (void)testInvalidServerTrustProducesCorrectErrorForCertificatePinning {
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Request should fail"];
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Request should fail with untrusted certificate error"];
NSURL *googleCertificateURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"google.com" withExtension:@"cer"];
NSData *googleCertificateData = [NSData dataWithContentsOfURL:googleCertificateURL];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://apple.com/"]];
Expand All @@ -795,15 +795,16 @@ - (void)testInvalidServerTrustProducesCorrectErrorForCertificatePinning {
}
failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
XCTAssertEqualObjects(error.domain, NSURLErrorDomain);
XCTAssertEqual(error.code, NSURLErrorCancelled);
XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted);
XCTAssertEqualObjects(error.localizedDescription, @"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “apple.com” which could put your confidential information at risk.");
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
[manager invalidateSessionCancelingTasks:YES resetSession:NO];
}

- (void)testInvalidServerTrustProducesCorrectErrorForPublicKeyPinning {
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Request should fail"];
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Request should fail with untrusted certificate error"];
NSURL *googleCertificateURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"google.com" withExtension:@"cer"];
NSData *googleCertificateData = [NSData dataWithContentsOfURL:googleCertificateURL];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://apple.com/"]];
Expand All @@ -820,11 +821,38 @@ - (void)testInvalidServerTrustProducesCorrectErrorForPublicKeyPinning {
}
failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
XCTAssertEqualObjects(error.domain, NSURLErrorDomain);
XCTAssertEqual(error.code, NSURLErrorCancelled);
XCTAssertEqual(error.code, NSURLErrorServerCertificateUntrusted);
XCTAssertEqualObjects(error.localizedDescription, @"The certificate for this server is invalid. You might be connecting to a server that is pretending to be “apple.com” which could put your confidential information at risk.");
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeout];
[manager invalidateSessionCancelingTasks:YES resetSession:NO];
}

# pragma mark - Custom Authentication Challenge Handler

- (void)testAuthenticationChallengeHandlerCredentialResult {
__weak XCTestExpectation *expectation = [self expectationWithDescription:@"Request succeed with provided credentials"];
self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
[self.manager setAuthenticationChallengeHandler:^id _Nonnull(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSURLAuthenticationChallenge * _Nonnull challenge, void (^ _Nonnull completionHandler)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable)) {
if ([challenge.protectionSpace.realm isEqualToString:@"Fake Realm"]) {
return [NSURLCredential credentialWithUser:@"user" password:@"passwd" persistence:NSURLCredentialPersistenceNone];
}
return @(NSURLSessionAuthChallengePerformDefaultHandling);
}];
[self.manager
GET:@"basic-auth/user/passwd"
parameters:nil
progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
[expectation fulfill];
}
failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
XCTFail(@"Request should succeed");
[expectation fulfill];
}];
[self waitForExpectationsWithCommonTimeoutUsingHandler:nil];
[self.manager invalidateSessionCancelingTasks:YES];
}

@end

0 comments on commit c4d8f62

Please sign in to comment.