/* |
File: ServerTrustChallengeHandler.m |
|
Contains: Handles HTTPS server trust challenges. |
|
Written by: DTS |
|
Copyright: Copyright (c) 2011 Apple Inc. All Rights Reserved. |
|
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. |
("Apple") in consideration of your agreement to the following |
terms, and your use, installation, modification or |
redistribution of this Apple software constitutes acceptance of |
these terms. If you do not agree with these terms, please do |
not use, install, modify or redistribute this Apple software. |
|
In consideration of your agreement to abide by the following |
terms, and subject to these terms, Apple grants you a personal, |
non-exclusive license, under Apple's copyrights in this |
original Apple software (the "Apple Software"), to use, |
reproduce, modify and redistribute the Apple Software, with or |
without modifications, in source and/or binary forms; provided |
that if you redistribute the Apple Software in its entirety and |
without modifications, you must retain this notice and the |
following text and disclaimers in all such redistributions of |
the Apple Software. Neither the name, trademarks, service marks |
or logos of Apple Inc. may be used to endorse or promote |
products derived from the Apple Software without specific prior |
written permission from Apple. Except as expressly stated in |
this notice, no other rights or licenses, express or implied, |
are granted by Apple herein, including but not limited to any |
patent rights that may be infringed by your derivative works or |
by other works in which the Apple Software may be incorporated. |
|
The Apple Software is provided by Apple on an "AS IS" basis. |
APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING |
WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, |
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING |
THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN |
COMBINATION WITH YOUR PRODUCTS. |
|
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, |
INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED |
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY |
OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION |
OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY |
OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR |
OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF |
SUCH DAMAGE. |
|
*/ |
|
#import "ServerTrustChallengeHandler.h" |
|
#import "Credentials.h" |
|
#import "DebugOptions.h" |
|
@interface ServerTrustChallengeHandler () |
|
@property (nonatomic, retain, readwrite) UIAlertView * alertView; |
|
@end |
|
@implementation ServerTrustChallengeHandler |
|
+ (void)registerHandlers |
// Called by the handler registry within ChallengeHandler to request that the |
// concrete subclass register itself. |
{ |
// We observe the serverValidation debug option and, when it changes, either register |
// or deregister ourselves based on the value of the option. We pass in |
// NSKeyValueObservingOptionInitial so that our observer is called immediately, and this |
// then sets up our initial state. |
[[DebugOptions sharedDebugOptions] addObserver:self forKeyPath:@"serverValidation" options:NSKeyValueObservingOptionInitial context:NULL]; |
} |
|
+ (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context |
// A KVO callback called when the serverValidation debug option changes. If the user |
// has requested default validation, we pull ourselves out of the registry, otherwise we |
// make sure we're in there. |
{ |
if ( (object == [DebugOptions sharedDebugOptions]) && [keyPath isEqual:@"serverValidation"] ) { |
|
// The following code relies on two properties of challenge handling registration: |
// |
// o It's OK to deregister a handler that's not registered. |
// o It's OK to register a handler that's already registered. |
// |
// Without these two properties this code would have to keep track of whether it's |
// registered or not, which would be less fun. |
|
if ([DebugOptions sharedDebugOptions].serverValidation == kDebugOptionsServerValidationDefault) { |
[ChallengeHandler deregisterHandlerClass:[self class] forAuthenticationMethod:NSURLAuthenticationMethodServerTrust]; |
} else { |
[ChallengeHandler registerHandlerClass:[self class] forAuthenticationMethod:NSURLAuthenticationMethodServerTrust]; |
} |
} else { |
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; |
} |
} |
|
- (void)dealloc |
{ |
assert(self.alertView == nil); |
[super dealloc]; |
} |
|
#pragma mark * Core code |
|
@synthesize alertView = _alertView; |
|
static SecCertificateRef SecTrustGetLeafCertificate(SecTrustRef trust) |
// Returns the leaf certificate from a SecTrust object (that is always the |
// certificate at index 0). |
{ |
SecCertificateRef result; |
|
assert(trust != NULL); |
|
if (SecTrustGetCertificateCount(trust) > 0) { |
result = SecTrustGetCertificateAtIndex(trust, 0); |
assert(result != NULL); |
} else { |
result = NULL; |
} |
return result; |
} |
|
static NSMutableDictionary * sSiteToCertificateMap; // keys are host names as NSString |
// values are SecCertificateRef |
|
+ (void)resetTrustedCertificates |
{ |
// We don't just release the entire array because the _serverTrustResolvedWithSuccess |
// code assumes that, if execution gets that far, sSiteToCertificateMap is not nil. |
if (sSiteToCertificateMap != nil) { |
[sSiteToCertificateMap removeAllObjects]; |
} |
} |
|
- (void)_serverTrustResolvedWithSuccess:(BOOL)success rememberSuccess:(BOOL)rememberSuccess |
// Some common code that's called in a variety of places to finally resolve the |
// challenge. Also, if rememberSuccess is set, we add an entry for this challenge |
// into sSiteToCertificateMap so that future challenges can be automatically resolved. |
{ |
NSURLCredential * credential; |
|
// ! success && rememberSuccess is a weird combination, but we allow is so |
// that our clients don't have to jump through too many hoops. |
|
// On succes, create a credential with which to resolve the challenge. |
|
credential = nil; |
if (success) { |
NSURLProtectionSpace * protectionSpace; |
SecTrustRef trust; |
NSString * host; |
SecCertificateRef serverCert; |
|
protectionSpace = [self.challenge protectionSpace]; |
assert(protectionSpace != nil); |
|
trust = [protectionSpace serverTrust]; |
assert(trust != NULL); |
|
credential = [NSURLCredential credentialForTrust:trust]; |
assert(credential != nil); |
|
// If we've been asked to remember the response, do so now. |
|
if (rememberSuccess) { |
assert(sSiteToCertificateMap != nil); |
|
host = [[self.challenge protectionSpace] host]; |
assert(host != nil); |
if ( [sSiteToCertificateMap objectForKey:host] == nil ) { |
|
serverCert = SecTrustGetLeafCertificate(trust); |
if (serverCert != NULL) { |
[sSiteToCertificateMap setObject:(id)serverCert forKey:host]; |
} |
} |
} |
} |
|
// Pass the final credential to the base class's stop code (which in turn |
// tells us to tear down our UI) and then tell our delegate. |
|
[self stopWithCredential:credential]; |
[self.delegate challengeHandlerDidFinish:self]; |
} |
|
- (void)_evaluateAskPerUntrustedSiteTrust |
// Implements the kDebugOptionsServerValidationAskPerUntrustedSite server trust |
// validation option. |
{ |
OSStatus err; |
NSURLProtectionSpace * protectionSpace; |
SecTrustRef trust; |
BOOL trusted; |
SecTrustResultType trustResult; |
SecCertificateRef previousCert; |
|
protectionSpace = [self.challenge protectionSpace]; |
assert(protectionSpace != nil); |
|
trust = [protectionSpace serverTrust]; |
assert(trust != NULL); |
|
// Evaluate the trust the standard way. |
|
err = SecTrustEvaluate(trust, &trustResult); |
trusted = (err == noErr) && ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)); |
|
// If the standard policy says that it's trusted, allow it right now. |
// Otherwise do our custom magic. |
|
if (trusted) { |
[self _serverTrustResolvedWithSuccess:YES rememberSuccess:NO]; |
} else { |
if (sSiteToCertificateMap == nil) { |
sSiteToCertificateMap = [[NSMutableDictionary alloc] init]; |
assert(sSiteToCertificateMap != nil); |
} |
|
// Check to see if we've previously seen this server. |
|
previousCert = (SecCertificateRef) [sSiteToCertificateMap objectForKey:[protectionSpace host]]; |
assert( (previousCert == NULL) || (CFGetTypeID(previousCert) == SecCertificateGetTypeID()) ); |
|
if (previousCert == NULL) { |
// We've not seen this server before. Ask the user. |
|
assert(self.alertView == nil); |
self.alertView = [[[UIAlertView alloc] initWithTitle:@"ACCEPT WEBSITE CERTIFICATE" |
message:@"THE CERTIFICATE FOR THIS WEBSITE IS INVALID. TAP ACCEPT TO CONNECT TO THIS WEBSITE ANYWAY." |
delegate:self |
cancelButtonTitle:@"Accept" |
otherButtonTitles:@"Cancel", |
nil |
] autorelease]; |
assert(self.alertView != nil); |
|
[self.alertView show]; |
|
// continues in -alertView:clickedButtonAtIndex: |
} else { |
BOOL success; |
SecCertificateRef serverCert; |
|
// We've seen this server before. Check to see whether the |
// certificate from the connection matches the certificate |
// we saw last time. If so, allow the connection. If not, |
// deny the connection. |
|
success = NO; |
serverCert = SecTrustGetLeafCertificate(trust); |
if (serverCert != NULL) { |
CFDataRef previousCertData; |
CFDataRef serverCertData; |
|
previousCertData = SecCertificateCopyData(previousCert); |
serverCertData = SecCertificateCopyData(serverCert ); |
|
assert(previousCertData != NULL); |
assert(serverCertData != NULL); |
|
success = CFEqual(previousCertData, serverCertData); |
|
CFRelease(previousCertData); |
CFRelease(serverCertData); |
} |
|
if (success) { |
[self _serverTrustResolvedWithSuccess:YES rememberSuccess:NO]; |
} else { |
[self _serverTrustResolvedWithSuccess:NO rememberSuccess:NO]; |
} |
} |
} |
} |
|
- (void)_evaluateImportedCertificatesTrust |
// Implements the kDebugOptionsServerValidationTrustImportedCertificates server |
// trust validation option. |
{ |
OSStatus err; |
NSURLProtectionSpace * protectionSpace; |
SecTrustRef trust; |
SecTrustResultType trustResult; |
BOOL trusted; |
|
protectionSpace = [self.challenge protectionSpace]; |
assert(protectionSpace != nil); |
|
trust = [protectionSpace serverTrust]; |
assert(trust != NULL); |
|
// Evaluate the trust the standard way. |
|
err = SecTrustEvaluate(trust, &trustResult); |
trusted = (err == noErr) && ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)); |
|
// If that fails, apply our certificates as anchors and see if that helps. |
// |
// It's perfectly acceptable to apply all of our certificates to the SecTrust |
// object, and let the SecTrust object sort out the mess. Of course, this assumes |
// that the user trusts all certificates equally in all situations, which is implicit |
// in our user interface; you could provide a more sophisticated user interface |
// to allow the user to trust certain certificates for certain sites and so on). |
|
if ( ! trusted ) { |
err = SecTrustSetAnchorCertificates(trust, (CFArrayRef) [Credentials sharedCredentials].certificates); |
if (err == noErr) { |
err = SecTrustEvaluate(trust, &trustResult); |
} |
trusted = (err == noErr) && ((trustResult == kSecTrustResultProceed) || (trustResult == kSecTrustResultUnspecified)); |
} |
|
if (trusted) { |
[self _serverTrustResolvedWithSuccess:YES rememberSuccess:NO]; |
} else { |
[self _serverTrustResolvedWithSuccess:NO rememberSuccess:NO]; |
} |
} |
|
- (void)_handleServerTrustChallenge |
// Handles a server trust challenge according to the serverValidation debug option. |
// This is called out of -didStart, and thus can present UI. However, it may |
// or not present UI depending on the specific server trust and debug options. |
{ |
switch ( [DebugOptions sharedDebugOptions].serverValidation ) { |
default: |
// fall through |
case kDebugOptionsServerValidationDefault: { |
// We should never have got here because we deregister ourselves when |
// the user selects the default case. |
assert(NO); |
[self _serverTrustResolvedWithSuccess:NO rememberSuccess:NO]; |
} break; |
case kDebugOptionsServerValidationAskPerUntrustedSite: { |
[self _evaluateAskPerUntrustedSiteTrust]; |
} break; |
case kDebugOptionsServerValidationTrustImportedCertificates: { |
[self _evaluateImportedCertificatesTrust]; |
} break; |
case kDebugOptionsServerValidationDisabled: { |
// Just say yes. |
[self _serverTrustResolvedWithSuccess:YES rememberSuccess:NO]; |
} break; |
} |
} |
|
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex |
// An alert view delegate callback that's called when the alert is dismissed. |
// We use the tapped button index to decide how to resolve the challenge. |
{ |
#pragma unused(alertView) |
assert(alertView == self.alertView); |
[self _serverTrustResolvedWithSuccess:(buttonIndex == 0) rememberSuccess:YES]; |
} |
|
#pragma mark * Override points |
|
- (void)didStart |
// Called by our base class to tell us to create our UI. |
{ |
[super didStart]; |
[self _handleServerTrustChallenge]; |
} |
|
- (void)willFinish |
// Called by our base class to tell us to tear down our UI. |
{ |
[super willFinish]; |
|
// If an alert is still up, tear it down immediately. |
|
if (self.alertView != nil) { |
self.alertView.delegate = nil; |
[self.alertView dismissWithClickedButtonIndex:self.alertView.cancelButtonIndex animated:NO]; |
self.alertView = nil; |
} |
} |
|
@end |