diff --git a/AFNetworking.podspec b/AFNetworking.podspec index 8c6affc..b7e836b 100644 --- a/AFNetworking.podspec +++ b/AFNetworking.podspec @@ -10,13 +10,16 @@ Pod::Spec.new do |s| s.requires_arc = true s.ios.deployment_target = '5.0' - s.ios.frameworks = 'MobileCoreServices', 'SystemConfiguration' + s.ios.frameworks = 'MobileCoreServices', 'SystemConfiguration', 'Security' s.osx.deployment_target = '10.7' - s.osx.frameworks = 'CoreServices', 'SystemConfiguration' + s.osx.frameworks = 'CoreServices', 'SystemConfiguration', 'Security' s.prefix_header_contents = <<-EOS #import + +#define _AFNETWORKING_PIN_SSL_CERTIFICATES_ + #if __IPHONE_OS_VERSION_MIN_REQUIRED #import #import diff --git a/AFNetworking/AFHTTPClient.h b/AFNetworking/AFHTTPClient.h index bbf575d..e875395 100644 --- a/AFNetworking/AFHTTPClient.h +++ b/AFNetworking/AFHTTPClient.h @@ -21,6 +21,7 @@ // THE SOFTWARE. #import +#import "AFURLConnectionOperation.h" #import @@ -137,6 +138,13 @@ typedef enum { @property (readonly, nonatomic, assign) AFNetworkReachabilityStatus networkReachabilityStatus; #endif +/** + Default SSL pinning mode for each `AFHTTPRequestOperation` which will be enqueued with `enqueueHTTPRequestOperation:`. + */ +#ifdef _AFNETWORKING_PIN_SSL_CERTIFICATES_ +@property (nonatomic, assign) AFURLConnectionOperationSSLPinningMode defaultSSLPinMode; +#endif + ///--------------------------------------------- /// @name Creating and Initializing HTTP Clients ///--------------------------------------------- @@ -440,7 +448,7 @@ typedef enum { ///---------------- /** - ### Network Reachability + ## Network Reachability The following constants are provided by `AFHTTPClient` as possible network reachability statuses. @@ -471,7 +479,7 @@ typedef enum { A key in the userInfo dictionary in a `AFNetworkingReachabilityDidChangeNotification` notification. The corresponding value is an `NSNumber` object representing the `AFNetworkReachabilityStatus` value for the current reachability status. - ### Parameter Encoding + ## Parameter Encoding The following constants are provided by `AFHTTPClient` as possible methods for serializing parameters into query string or message body values. diff --git a/AFNetworking/AFHTTPClient.m b/AFNetworking/AFHTTPClient.m index 731d903..ccaa613 100644 --- a/AFNetworking/AFHTTPClient.m +++ b/AFNetworking/AFHTTPClient.m @@ -540,6 +540,10 @@ static void AFNetworkReachabilityReleaseCallback(const void *info) { #pragma mark - - (void)enqueueHTTPRequestOperation:(AFHTTPRequestOperation *)operation { +#ifdef _AFNETWORKING_PIN_SSL_CERTIFICATES_ + operation.SSLPinningMode = self.defaultSSLPinMode; +#endif + [self.operationQueue addOperation:operation]; } diff --git a/AFNetworking/AFURLConnectionOperation.h b/AFNetworking/AFURLConnectionOperation.h index 233627d..1a54a5c 100644 --- a/AFNetworking/AFURLConnectionOperation.h +++ b/AFNetworking/AFURLConnectionOperation.h @@ -59,6 +59,14 @@ The built-in `completionBlock` provided by `NSOperation` allows for custom behavior to be executed after the request finishes. It is a common pattern for class constructors in subclasses to take callback block parameters, and execute them conditionally in the body of its `completionBlock`. Make sure to handle cancelled operations appropriately when setting a `completionBlock` (i.e. returning early before parsing response data). See the implementation of any of the `AFHTTPRequestOperation` subclasses for an example of this. Subclasses are strongly discouraged from overriding `setCompletionBlock:`, as `AFURLConnectionOperation`'s implementation includes a workaround to mitigate retain cycles, and what Apple rather ominously refers to as ["The Deallocation Problem"](http://developer.apple.com/library/ios/#technotes/tn2109/). + + ## SSL Pinning + + Relying on the CA trust model to validate SSL certificates exposes your app to security vulnerabilities, such as man-in-the-middle attacks. For applications that connect to known servers, SSL certificate pinning provides an increased level of security, by checking server certificate validity against those specified in the app bundle. + + SSL with certificate pinning is strongly recommended for any application that transmits sensitive information to an external webservice. + + When `_AFNETWORKING_PIN_SSL_CERTIFICATES_` is defined and the Security framework is linked, connections will be validated on all matching certificates with a `.cer` extension in the bundle root. ## NSCoding & NSCopying Conformance @@ -75,6 +83,15 @@ - A copy of an operation will not include the `outputStream` of the original. - Operation copies do not include `completionBlock`. `completionBlock` often strongly captures a reference to `self`, which would otherwise have the unintuitive side-effect of pointing to the _original_ operation when copied. */ + +#ifdef _AFNETWORKING_PIN_SSL_CERTIFICATES_ +typedef enum { + AFSSLPinningModeNone, + AFSSLPinningModePublicKey, + AFSSLPinningModeCertificate, +} AFURLConnectionOperationSSLPinningMode; +#endif + @interface AFURLConnectionOperation : NSOperation = __IPHONE_5_0) || \ (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_8) @@ -131,7 +148,6 @@ NSCoding, NSCopying> */ @property (readonly, nonatomic, assign) NSStringEncoding responseStringEncoding; - ///------------------------------- /// @name Managing URL Credentials ///------------------------------- @@ -150,6 +166,15 @@ NSCoding, NSCopying> */ @property (nonatomic, strong) NSURLCredential *credential; +/** + The pinning mode which will be used for SSL connections. `AFSSLPinningModePublicKey` by default. + + @discussion To enable SSL Pinning, `#define _AFNETWORKING_PIN_SSL_CERTIFICATES_` in `Prefix.pch`. Also, make sure that the Security framework is linked with the binary. See the "SSL Pinning" section in the `AFURLConnectionOperation`" header for more information. + */ +#ifdef _AFNETWORKING_PIN_SSL_CERTIFICATES_ +@property (nonatomic, assign) AFURLConnectionOperationSSLPinningMode SSLPinningMode; +#endif + ///------------------------ /// @name Accessing Streams ///------------------------ @@ -290,6 +315,21 @@ NSCoding, NSCopying> ///---------------- /** + ## Network Reachability + + The following constants are provided by `AFURLConnectionOperation` as possible SSL Pinning options. + + enum { + AFSSLPinningModePublicKey, + AFSSLPinningModeCertificate, + } + + `AFSSLPinningModePublicKey` + Pin SSL connections to certificate public key (SPKI). + + `AFSSLPinningModeCertificate` + Pin SSL connections to exact certificate. This may cause problems when your certificate expires and needs re-issuance. + ## User info dictionary keys These keys may exist in the user info dictionary, in addition to those defined for NSError. diff --git a/AFNetworking/AFURLConnectionOperation.m b/AFNetworking/AFURLConnectionOperation.m index f4196ce..51cfd6b 100644 --- a/AFNetworking/AFURLConnectionOperation.m +++ b/AFNetworking/AFURLConnectionOperation.m @@ -145,6 +145,9 @@ static inline BOOL AFStateTransitionIsValid(AFOperationState fromState, AFOperat @dynamic inputStream; @synthesize outputStream = _outputStream; @synthesize credential = _credential; +#ifdef _AFNETWORKING_PIN_SSL_CERTIFICATES_ +@synthesize SSLPinningMode = _SSLPinningMode; +#endif @synthesize shouldUseCredentialStorage = _shouldUseCredentialStorage; @synthesize userInfo = _userInfo; @synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier; @@ -167,7 +170,6 @@ static inline BOOL AFStateTransitionIsValid(AFOperationState fromState, AFOperat + (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t oncePredicate; - dispatch_once(&oncePredicate, ^{ _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil]; [_networkRequestThread start]; @@ -176,24 +178,61 @@ static inline BOOL AFStateTransitionIsValid(AFOperationState fromState, AFOperat return _networkRequestThread; } +#ifdef _AFNETWORKING_PIN_SSL_CERTIFICATES_ + (NSArray *)pinnedCertificates { static NSArray *_pinnedCertificates = nil; static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ NSBundle *bundle = [NSBundle bundleForClass:[self class]]; NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."]; - NSMutableArray *certificates = [NSMutableArray array]; + + NSMutableArray *certificates = [NSMutableArray arrayWithCapacity:[paths count]]; for (NSString *path in paths) { NSData *certificateData = [NSData dataWithContentsOfFile:path]; [certificates addObject:certificateData]; } + _pinnedCertificates = [[NSArray alloc] initWithArray:certificates]; }); return _pinnedCertificates; } ++ (NSArray *)pinnedPublicKeys { + static NSArray *_pinnedPublicKeys = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSArray *pinnedCertificates = [self pinnedCertificates]; + NSMutableArray *publicKeys = [NSMutableArray arrayWithCapacity:[pinnedCertificates count]]; + + for (NSData *data in pinnedCertificates) { + SecCertificateRef allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)data); + NSCParameterAssert(allowedCertificate); + + SecCertificateRef allowedCertificates[] = {allowedCertificate}; + CFArrayRef certificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL); + + SecPolicyRef policy = SecPolicyCreateBasicX509(); + SecTrustRef allowedTrust = NULL; + OSStatus status = SecTrustCreateWithCertificates(certificates, policy, &allowedTrust); + NSAssert(status == noErr, @"SecTrustCreateWithCertificates error: %ld", (long int)status); + + SecKeyRef allowedPublicKey = SecTrustCopyPublicKey(allowedTrust); + [publicKeys addObject:(__bridge_transfer id)allowedPublicKey]; + + CFRelease(allowedTrust); + CFRelease(policy); + CFRelease(certificates); + CFRelease(allowedCertificate); + } + + _pinnedPublicKeys = [[NSArray alloc] initWithArray:publicKeys]; + }); + + return _pinnedPublicKeys; +} +#endif + - (id)initWithRequest:(NSURLRequest *)urlRequest { self = [super init]; if (!self) { @@ -510,12 +549,57 @@ willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challe NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; } else { - [[challenge sender] cancelAuthenticationChallenge:challenge]; + switch (self.SSLPinningMode) { + case AFSSLPinningModePublicKey: { + id publicKey = (__bridge_transfer id)SecTrustCopyPublicKey(serverTrust); + + if ([[self.class pinnedPublicKeys] containsObject:publicKey]) { + NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; + [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; + } else { + [[challenge sender] cancelAuthenticationChallenge:challenge]; + } + + break; + } + case AFSSLPinningModeCertificate: { + SecCertificateRef serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0); + NSData *serverCertificateData = (__bridge_transfer NSData *)SecCertificateCopyData(serverCertificate); + + if ([[[self class] pinnedCertificates] containsObject:serverCertificateData]) { + NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; + [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; + } else { + [[challenge sender] cancelAuthenticationChallenge:challenge]; + } + + break; + } + case AFSSLPinningModeNone: { +#ifdef _AFNETWORKING_ALLOW_INVALID_SSL_CERTIFICATES_ + NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; + [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; +#else + SecTrustResultType result = 0; + OSStatus status = SecTrustEvaluate(serverTrust, &result); + NSAssert(status == noErr, @"SecTrustEvaluate error: %ld", (long int)status); + + if (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed) { + NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; + [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge]; + } else { + [[challenge sender] cancelAuthenticationChallenge:challenge]; + } +#endif + break; + } + } } } } #endif + - (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace { diff --git a/Example/AFNetworking Mac Example.xcodeproj/project.pbxproj b/Example/AFNetworking Mac Example.xcodeproj/project.pbxproj index 7b6a013..52132e4 100644 --- a/Example/AFNetworking Mac Example.xcodeproj/project.pbxproj +++ b/Example/AFNetworking Mac Example.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ F82EB080159A172000B10B56 /* AFPropertyListRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = F82EB077159A172000B10B56 /* AFPropertyListRequestOperation.m */; }; F82EB081159A172000B10B56 /* AFURLConnectionOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = F82EB079159A172000B10B56 /* AFURLConnectionOperation.m */; }; F82EB082159A172000B10B56 /* AFXMLRequestOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = F82EB07B159A172000B10B56 /* AFXMLRequestOperation.m */; }; + F88812F216C533E9003C8B8C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F88812F116C533E9003C8B8C /* Security.framework */; }; F8A847CF161F55A500940F39 /* CoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8A847CE161F55A500940F39 /* CoreServices.framework */; }; F8A847D2161F55AC00940F39 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8A847D1161F55AC00940F39 /* SystemConfiguration.framework */; }; /* End PBXBuildFile section */ @@ -57,6 +58,7 @@ F82EB07A159A172000B10B56 /* AFXMLRequestOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AFXMLRequestOperation.h; path = ../AFNetworking/AFXMLRequestOperation.h; sourceTree = ""; }; F82EB07B159A172000B10B56 /* AFXMLRequestOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AFXMLRequestOperation.m; path = ../AFNetworking/AFXMLRequestOperation.m; sourceTree = ""; }; F877018B159A1CE700B45C0D /* AFNetworking Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "AFNetworking Example.entitlements"; sourceTree = ""; }; + F88812F116C533E9003C8B8C /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; F8A847CE161F55A500940F39 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; F8A847D1161F55AC00940F39 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -66,6 +68,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F88812F216C533E9003C8B8C /* Security.framework in Frameworks */, F8A847D2161F55AC00940F39 /* SystemConfiguration.framework in Frameworks */, F8A847CF161F55A500940F39 /* CoreServices.framework in Frameworks */, F8129C001591061B009BFE23 /* Cocoa.framework in Frameworks */, @@ -78,6 +81,7 @@ F8129BF01591061B009BFE23 = { isa = PBXGroup; children = ( + F88812F116C533E9003C8B8C /* Security.framework */, F8A847D1161F55AC00940F39 /* SystemConfiguration.framework */, F8A847CE161F55A500940F39 /* CoreServices.framework */, F877018B159A1CE700B45C0D /* AFNetworking Example.entitlements */, diff --git a/Example/AFNetworking iOS Example.xcodeproj/project.pbxproj b/Example/AFNetworking iOS Example.xcodeproj/project.pbxproj index 37fcb17..d89b181 100644 --- a/Example/AFNetworking iOS Example.xcodeproj/project.pbxproj +++ b/Example/AFNetworking iOS Example.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ F8129C7415910C37009BFE23 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F8129C7215910C37009BFE23 /* AppDelegate.m */; }; F818101615E6A0C600EF93C2 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50ABD6EC159FC2CE001BE42C /* MobileCoreServices.framework */; }; + F88812F016C533D6003C8B8C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8E469E013957DF100DB05C8 /* Security.framework */; }; F8A847C1161F51A300940F39 /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F8A847C0161F51A300940F39 /* Default-568h@2x.png */; }; F8A847C3161F523E00940F39 /* Default.png in Resources */ = {isa = PBXBuildFile; fileRef = F8A847C2161F523E00940F39 /* Default.png */; }; F8A847C5161F524200940F39 /* Default@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F8A847C4161F524200940F39 /* Default@2x.png */; }; @@ -95,6 +96,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F88812F016C533D6003C8B8C /* Security.framework in Frameworks */, F8E469651395739D00DB05C8 /* UIKit.framework in Frameworks */, F8E469671395739D00DB05C8 /* Foundation.framework in Frameworks */, F8E469691395739D00DB05C8 /* CoreGraphics.framework in Frameworks */, diff --git a/Example/Prefix.pch b/Example/Prefix.pch index 18b984d..df78b3c 100644 --- a/Example/Prefix.pch +++ b/Example/Prefix.pch @@ -1,5 +1,7 @@ #import +#define _AFNETWORKING_PIN_SSL_CERTIFICATES_ + #if __IPHONE_OS_VERSION_MIN_REQUIRED #ifndef __IPHONE_3_0 #warning "This project uses features only available in iPhone SDK 3.0 and later."