Browse Source

Fix pinned certificates and extract SSL-related things into SRSecurityOptions. (#406)

Nikita Lutsenko 9 years ago
parent
commit
5ce5f5d1d8

+ 32 - 0
SocketRocket.xcodeproj/project.pbxproj

@@ -36,6 +36,15 @@
 		8179958C1CE139700084DA37 /* SRDelegateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 817995851CE139700084DA37 /* SRDelegateController.m */; };
 		8179958D1CE139700084DA37 /* SRDelegateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 817995851CE139700084DA37 /* SRDelegateController.m */; };
 		817996801CE184F40084DA37 /* SRAutobahnUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 8179967F1CE184F40084DA37 /* SRAutobahnUtilities.m */; };
+		8186890E1D08D20D004F94C8 /* valid.cer in Resources */ = {isa = PBXBuildFile; fileRef = 8186890D1D08D20D004F94C8 /* valid.cer */; };
+		8186892F1D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8186892D1D08EF3C004F94C8 /* SRSecurityOptions.h */; };
+		818689301D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8186892D1D08EF3C004F94C8 /* SRSecurityOptions.h */; };
+		818689311D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8186892D1D08EF3C004F94C8 /* SRSecurityOptions.h */; };
+		818689321D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 8186892D1D08EF3C004F94C8 /* SRSecurityOptions.h */; };
+		818689331D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8186892E1D08EF3C004F94C8 /* SRSecurityOptions.m */; };
+		818689341D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8186892E1D08EF3C004F94C8 /* SRSecurityOptions.m */; };
+		818689351D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8186892E1D08EF3C004F94C8 /* SRSecurityOptions.m */; };
+		818689361D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 8186892E1D08EF3C004F94C8 /* SRSecurityOptions.m */; };
 		81B22EC51CE42D7E0073C636 /* SRError.h in Headers */ = {isa = PBXBuildFile; fileRef = 81B22EC31CE42D7E0073C636 /* SRError.h */; };
 		81B22EC61CE42D7E0073C636 /* SRError.h in Headers */ = {isa = PBXBuildFile; fileRef = 81B22EC31CE42D7E0073C636 /* SRError.h */; };
 		81B22EC71CE42D7E0073C636 /* SRError.h in Headers */ = {isa = PBXBuildFile; fileRef = 81B22EC31CE42D7E0073C636 /* SRError.h */; };
@@ -159,6 +168,9 @@
 		817995851CE139700084DA37 /* SRDelegateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRDelegateController.m; sourceTree = "<group>"; };
 		8179967E1CE184F40084DA37 /* SRAutobahnUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRAutobahnUtilities.h; sourceTree = "<group>"; };
 		8179967F1CE184F40084DA37 /* SRAutobahnUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRAutobahnUtilities.m; sourceTree = "<group>"; };
+		8186890D1D08D20D004F94C8 /* valid.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = valid.cer; path = ../../../../Desktop/valid.cer; sourceTree = "<group>"; };
+		8186892D1D08EF3C004F94C8 /* SRSecurityOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRSecurityOptions.h; sourceTree = "<group>"; };
+		8186892E1D08EF3C004F94C8 /* SRSecurityOptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRSecurityOptions.m; sourceTree = "<group>"; };
 		81B22EC31CE42D7E0073C636 /* SRError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRError.h; sourceTree = "<group>"; };
 		81B22EC41CE42D7E0073C636 /* SRError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRError.m; sourceTree = "<group>"; };
 		81B22EE21CE43ECC0073C636 /* SRURLUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRURLUtilities.h; sourceTree = "<group>"; };
@@ -342,9 +354,19 @@
 			path = Delegate;
 			sourceTree = "<group>";
 		};
+		8186892C1D08EF3C004F94C8 /* Security */ = {
+			isa = PBXGroup;
+			children = (
+				8186892D1D08EF3C004F94C8 /* SRSecurityOptions.h */,
+				8186892E1D08EF3C004F94C8 /* SRSecurityOptions.m */,
+			);
+			path = Security;
+			sourceTree = "<group>";
+		};
 		81B31C0D1CDC404100D86D43 /* Internal */ = {
 			isa = PBXGroup;
 			children = (
+				8186892C1D08EF3C004F94C8 /* Security */,
 				4861E7721D022211002FAB1D /* Proxy */,
 				817995831CE139540084DA37 /* Delegate */,
 				81B31C0E1CDC404100D86D43 /* IOConsumer */,
@@ -394,6 +416,7 @@
 				F62417EB14D52F3C003CE997 /* Supporting Files */,
 				F62417F314D52F3C003CE997 /* TCAppDelegate.h */,
 				F62417F414D52F3C003CE997 /* TCAppDelegate.m */,
+				8186890D1D08D20D004F94C8 /* valid.cer */,
 				F62417F614D52F3C003CE997 /* MainStoryboard.storyboard */,
 				F62417F914D52F3C003CE997 /* TCViewController.h */,
 				F62417FA14D52F3C003CE997 /* TCViewController.m */,
@@ -503,6 +526,7 @@
 				81CD05FE1CEEC65D00497F47 /* NSRunLoop+SRWebSocket.h in Headers */,
 				81CD05D81CEEC47300497F47 /* NSURLRequest+SRWebSocket.h in Headers */,
 				81B31C1D1CDC404100D86D43 /* SRIOConsumerPool.h in Headers */,
+				818689301D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */,
 				2D42277F1BB4365C000C1A6C /* SRWebSocket.h in Headers */,
 				81B31C2E1CDC406B00D86D43 /* SRHash.h in Headers */,
 				811934BE1CDAF725003AB243 /* SocketRocket.h in Headers */,
@@ -521,6 +545,7 @@
 				81CD06001CEEC65D00497F47 /* NSRunLoop+SRWebSocket.h in Headers */,
 				81CD05DA1CEEC47300497F47 /* NSURLRequest+SRWebSocket.h in Headers */,
 				81B31C1F1CDC404100D86D43 /* SRIOConsumerPool.h in Headers */,
+				818689321D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */,
 				3345DC8A1C52ACD70083CCB8 /* SRWebSocket.h in Headers */,
 				81B31C301CDC406B00D86D43 /* SRHash.h in Headers */,
 				811934C01CDAF726003AB243 /* SocketRocket.h in Headers */,
@@ -539,6 +564,7 @@
 				81CD05FF1CEEC65D00497F47 /* NSRunLoop+SRWebSocket.h in Headers */,
 				81CD05D91CEEC47300497F47 /* NSURLRequest+SRWebSocket.h in Headers */,
 				81B31C1E1CDC404100D86D43 /* SRIOConsumerPool.h in Headers */,
+				818689311D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */,
 				F668C8AA153E92F90044DBAC /* SRWebSocket.h in Headers */,
 				81B31C2F1CDC406B00D86D43 /* SRHash.h in Headers */,
 				811934BC1CDAF725003AB243 /* SocketRocket.h in Headers */,
@@ -556,6 +582,7 @@
 				81B31C141CDC404100D86D43 /* SRIOConsumer.h in Headers */,
 				81CD05FD1CEEC65D00497F47 /* NSRunLoop+SRWebSocket.h in Headers */,
 				81CD05D71CEEC47300497F47 /* NSURLRequest+SRWebSocket.h in Headers */,
+				8186892F1D08EF3C004F94C8 /* SRSecurityOptions.h in Headers */,
 				81B31C1C1CDC404100D86D43 /* SRIOConsumerPool.h in Headers */,
 				F6A12CD1145119B700C1D980 /* SRWebSocket.h in Headers */,
 				81B31C2D1CDC406B00D86D43 /* SRHash.h in Headers */,
@@ -727,6 +754,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				F62417EF14D52F3C003CE997 /* InfoPlist.strings in Resources */,
+				8186890E1D08D20D004F94C8 /* valid.cer in Resources */,
 				F62417F814D52F3C003CE997 /* MainStoryboard.storyboard in Resources */,
 				F61A0DC81625F44D00365EBD /* Default-568h@2x.png in Resources */,
 			);
@@ -757,6 +785,7 @@
 				81CD05DC1CEEC47300497F47 /* NSURLRequest+SRWebSocket.m in Sources */,
 				81B22ECA1CE42D7E0073C636 /* SRError.m in Sources */,
 				81B31C191CDC404100D86D43 /* SRIOConsumer.m in Sources */,
+				818689341D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */,
 				81CD06021CEEC65D00497F47 /* NSRunLoop+SRWebSocket.m in Sources */,
 				2D4227851BB43734000C1A6C /* SRWebSocket.m in Sources */,
 				81B31C211CDC404100D86D43 /* SRIOConsumerPool.m in Sources */,
@@ -774,6 +803,7 @@
 				81CD05DE1CEEC47300497F47 /* NSURLRequest+SRWebSocket.m in Sources */,
 				81B22ECC1CE42D7E0073C636 /* SRError.m in Sources */,
 				81B31C1B1CDC404100D86D43 /* SRIOConsumer.m in Sources */,
+				818689361D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */,
 				81CD06041CEEC65D00497F47 /* NSRunLoop+SRWebSocket.m in Sources */,
 				3345DC841C52ACD70083CCB8 /* SRWebSocket.m in Sources */,
 				81B31C231CDC404100D86D43 /* SRIOConsumerPool.m in Sources */,
@@ -802,6 +832,7 @@
 				81CD05DD1CEEC47300497F47 /* NSURLRequest+SRWebSocket.m in Sources */,
 				81B22ECB1CE42D7E0073C636 /* SRError.m in Sources */,
 				81B31C1A1CDC404100D86D43 /* SRIOConsumer.m in Sources */,
+				818689351D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */,
 				81CD06031CEEC65D00497F47 /* NSRunLoop+SRWebSocket.m in Sources */,
 				F6396B86153E67EC00345B5E /* SRWebSocket.m in Sources */,
 				81B31C221CDC404100D86D43 /* SRIOConsumerPool.m in Sources */,
@@ -817,6 +848,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				4861E7761D022211002FAB1D /* SRProxyConnect.m in Sources */,
+				818689331D08EF3C004F94C8 /* SRSecurityOptions.m in Sources */,
 				81CD05DB1CEEC47300497F47 /* NSURLRequest+SRWebSocket.m in Sources */,
 				81B22EC91CE42D7E0073C636 /* SRError.m in Sources */,
 				81B31C181CDC404100D86D43 /* SRIOConsumer.m in Sources */,

+ 74 - 0
SocketRocket/Internal/Security/SRSecurityOptions.h

@@ -0,0 +1,74 @@
+//
+// Copyright (c) 2016-present, Facebook, Inc.
+// All rights reserved.
+//
+// This source code is licensed under the BSD-style license found in the
+// LICENSE file in the root directory of this source tree. An additional grant
+// of patent rights can be found in the PATENTS file in the same directory.
+//
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SRSecurityOptions: NSObject
+
+@property (nonatomic, strong, readonly) NSURLRequest *request;
+
+/**
+ Returns `YES` if request uses SSL, otherwise - `NO`.
+ */
+@property (nonatomic, assign, readonly) BOOL requestRequiresSSL;
+
+/**
+ Optional array of `SecCertificateRef` SSL certificates to use for validation.
+ */
+@property (nullable, nonatomic, strong, readonly) NSArray *pinnedCertificates;
+
+/**
+ Set to `NO` to disable SSL certificate chain validation.
+ This option is not taken into account when using pinned certificates.
+ Default: YES.
+ */
+@property (nonatomic, assign, readonly) BOOL validatesCertificateChain;
+
+/**
+ Initializes an instance of a controller into it with a given request and returns it.
+
+ @param request Request to initialize with.
+ */
+- (instancetype)initWithRequest:(NSURLRequest *)request
+             pinnedCertificates:(nullable NSArray *)pinnedCertificates
+         chainValidationEnabled:(BOOL)chainValidationEnabled NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
++ (instancetype)new NS_UNAVAILABLE;
+
+///--------------------------------------
+#pragma mark - Streams
+///--------------------------------------
+
+/**
+ Updates all the security options for the current configuration.
+
+ @param stream Stream to update the options in.
+ */
+- (void)updateSecurityOptionsInStream:(NSStream *)stream;
+
+///--------------------------------------
+#pragma mark - Pinned Certificates
+///--------------------------------------
+
+/**
+ Validates whether a given security trust contains pinned certificates.
+ If no certificates are pinned - returns `YES`.
+
+ @param trust Security trust to validate.
+
+ @return `YES` if certificates where found, otherwise - `NO`.
+ */
+- (BOOL)securityTrustContainsPinnedCertificates:(SecTrustRef)trust;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 88 - 0
SocketRocket/Internal/Security/SRSecurityOptions.m

@@ -0,0 +1,88 @@
+//
+// Copyright (c) 2016-present, Facebook, Inc.
+// All rights reserved.
+//
+// This source code is licensed under the BSD-style license found in the
+// LICENSE file in the root directory of this source tree. An additional grant
+// of patent rights can be found in the PATENTS file in the same directory.
+//
+
+#import "SRSecurityOptions.h"
+
+#import "SRURLUtilities.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation SRSecurityOptions
+
+///--------------------------------------
+#pragma mark - Init
+///--------------------------------------
+
+- (instancetype)initWithRequest:(NSURLRequest *)request
+             pinnedCertificates:(nullable NSArray *)pinnedCertificates
+         chainValidationEnabled:(BOOL)chainValidationEnabled
+{
+    self = [super init];
+    if (!self) return self;
+
+    _request = request;
+    _requestRequiresSSL = SRURLRequiresSSL(request.URL);
+    _pinnedCertificates = pinnedCertificates;
+    _validatesCertificateChain = chainValidationEnabled;
+
+    return self;
+}
+
+///--------------------------------------
+#pragma mark - Stream
+///---------------------------------------
+
+- (void)updateSecurityOptionsInStream:(NSStream *)stream
+{
+    // SSL not required, skip everything
+    if (!self.requestRequiresSSL) {
+        return;
+    }
+
+    // Enable highest level of security (`.LevelNegotiatedSSL`) for the stream.
+    [stream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];
+
+    // If we are not using pinned certs and if chain validation is enabled - enable it on a stream.
+    BOOL chainValidationEnabled = (_pinnedCertificates.count == 0 && self.validatesCertificateChain);
+    NSDictionary<NSString *, id> *sslOptions = @{ (__bridge NSString *)kCFStreamSSLValidatesCertificateChain : @(chainValidationEnabled) };
+    [stream setProperty:sslOptions forKey:(__bridge NSString *)kCFStreamPropertySSLSettings];
+}
+
+///--------------------------------------
+#pragma mark - Pinned Certificates
+///--------------------------------------
+
+- (BOOL)securityTrustContainsPinnedCertificates:(SecTrustRef)trust
+{
+    NSUInteger requiredCertCount = self.pinnedCertificates.count;
+    if (requiredCertCount == 0) {
+        return YES;
+    }
+
+    NSUInteger validatedCertCount = 0;
+    CFIndex serverCertCount = SecTrustGetCertificateCount(trust);
+    for (CFIndex i = 0; i < serverCertCount; i++) {
+        SecCertificateRef cert = SecTrustGetCertificateAtIndex(trust, i);
+        NSData *data = CFBridgingRelease(SecCertificateCopyData(cert));
+        for (id ref in self.pinnedCertificates) {
+            SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
+            // TODO: (nlutsenko) Add caching, so we don't copy the data for every pinned cert all the time.
+            NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
+            if ([trustedCertData isEqualToData:data]) {
+                validatedCertCount++;
+                break;
+            }
+        }
+    }
+    return (requiredCertCount == validatedCertCount);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 46 - 73
SocketRocket/SRWebSocket.m

@@ -36,6 +36,7 @@
 #import "NSURLRequest+SRWebSocket.h"
 #import "NSRunLoop+SRWebSocket.h"
 #import "SRProxyConnect.h"
+#import "SRSecurityOptions.h"
 
 #if !__has_feature(objc_arc)
 #error SocketRocket must be compiled with ARC enabled
@@ -121,7 +122,8 @@ NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode";
 
     NSString *_secKey;
 
-    BOOL _pinnedCertFound;
+    SRSecurityOptions *_securityOptions;
+    BOOL _streamSecurityValidated;
 
     uint8_t _currentReadMaskKey[4];
     size_t _currentReadMaskOffset;
@@ -129,7 +131,6 @@ NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode";
     BOOL _closeWhenFinishedWriting;
     BOOL _failed;
 
-    BOOL _secure;
     NSURLRequest *_urlRequest;
 
     BOOL _sentClose;
@@ -163,7 +164,9 @@ NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode";
 
     _requestedProtocols = [protocols copy];
 
-    _secure = SRURLRequiresSSL(_url);
+    _securityOptions = [[SRSecurityOptions alloc] initWithRequest:request
+                                               pinnedCertificates:request.SR_SSLPinnedCertificates
+                                           chainValidationEnabled:allowsUntrustedSSLCertificates];
 
     _readyState = SR_CONNECTING;
 
@@ -399,30 +402,14 @@ NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode";
     [self _readHTTPHeader];
 }
 
-- (void)_updateSecureStreamOptions;
+- (void)_updateSecureStreamOptions
 {
-    if (_secure) {
-        [_outputStream setProperty:(__bridge NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL
-                            forKey:(__bridge NSString *)kCFStreamPropertySocketSecurityLevel];
+    SRFastLog(@"Setting up security for streams.");
+    [_securityOptions updateSecurityOptionsInStream:_inputStream];
+    [_securityOptions updateSecurityOptionsInStream:_outputStream];
 
-        NSMutableDictionary<NSString *, NSNumber *> *sslOptions = [NSMutableDictionary dictionary];
-
-        // If we're using pinned certs, don't validate the certificate chain
-        if ([_urlRequest SR_SSLPinnedCertificates].count) {
-            sslOptions[(__bridge NSString *)kCFStreamSSLValidatesCertificateChain] = @NO;
-        }
-
-#if DEBUG
-        self.allowsUntrustedSSLCertificates = YES;
-#endif
-
-        if (self.allowsUntrustedSSLCertificates) {
-            sslOptions[(__bridge NSString *)kCFStreamSSLValidatesCertificateChain] = @NO;
-            SRFastLog(@"Allowing connection to any root cert");
-        }
-
-        [_outputStream setProperty:sslOptions forKey:(__bridge NSString *)kCFStreamPropertySSLSettings];
-    }
+    SRFastLog(@"Allows connection any root cert: %d", _securityOptions.validatesCertificateChain);
+    SRFastLog(@"Pinned cert count: %d", _securityOptions.pinnedCertificates.count);
 
     _inputStream.delegate = self;
     _outputStream.delegate = self;
@@ -475,9 +462,10 @@ NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode";
     }];
 }
 
-- (void) _connectionDoneWithError:(NSError *)error readStream:(NSInputStream *)readStream writeStream:(NSOutputStream *)writeStream
+- (void)_connectionDoneWithError:(NSError *)error readStream:(NSInputStream *)readStream writeStream:(NSOutputStream *)writeStream
 {
-    _proxyConnect = nil;        // don't need it anymore
+    _proxyConnect = nil; // Job's done! This is not longer required.
+
     if (error != nil) {
         [self _failWithError:error];
     } else {
@@ -492,9 +480,13 @@ NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode";
             [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode];
         }
 
-        dispatch_async(_workQueue, ^{
-            [self didConnect];
-        });
+        // If we don't require SSL validation - consider that we connected.
+        // Otherwise `didConnect` is called when SSL validation finishes.
+        if (!_securityOptions.requestRequiresSSL) {
+            dispatch_async(_workQueue, ^{
+                [self didConnect];
+            });
+        }
     }
 }
 
@@ -1385,50 +1377,31 @@ static const size_t SRFrameHeaderOverhead = 32;
     [self _writeData:frame];
 }
 
-- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode;
+- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode
 {
-    __weak typeof(self) weakSelf = self;
-
-    if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
-
-        NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates];
-        if (sslCerts) {
-            SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
-            if (secTrust) {
-                NSInteger numCerts = SecTrustGetCertificateCount(secTrust);
-                for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) {
-                    SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i);
-                    NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert));
-
-                    for (id ref in sslCerts) {
-                        SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref;
-                        NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert));
-
-                        if ([trustedCertData isEqualToData:certData]) {
-                            _pinnedCertFound = YES;
-                            break;
-                        }
-                    }
-                }
-            }
+    __weak typeof(self) wself = self;
 
-            if (!_pinnedCertFound) {
-                dispatch_async(_workQueue, ^{
-                    NSError *error = SRErrorWithDomainCodeDescription(NSURLErrorDomain, NSURLErrorClientCertificateRejected,
-                                                                      @"Invalid server certificate.");
-                    [weakSelf _failWithError:error];
-                });
-                return;
-            } else if (aStream == _outputStream) {
-                dispatch_async(_workQueue, ^{
-                    [self didConnect];
-                });
-            }
+    if (_securityOptions.requestRequiresSSL && !_streamSecurityValidated &&
+        (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) {
+        SecTrustRef trust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];
+        if (trust) {
+            _streamSecurityValidated = [_securityOptions securityTrustContainsPinnedCertificates:trust];
+        }
+        if (!_streamSecurityValidated) {
+            dispatch_async(_workQueue, ^{
+                NSError *error = SRErrorWithDomainCodeDescription(NSURLErrorDomain,
+                                                                  NSURLErrorClientCertificateRejected,
+                                                                  @"Invalid server certificate.");
+                [wself _failWithError:error];
+            });
+            return;
         }
+        dispatch_async(_workQueue, ^{
+            [self didConnect];
+        });
     }
-
     dispatch_async(_workQueue, ^{
-        [weakSelf safeHandleEvent:eventCode stream:aStream];
+        [wself safeHandleEvent:eventCode stream:aStream];
     });
 }
 
@@ -1442,13 +1415,13 @@ static const size_t SRFrameHeaderOverhead = 32;
             }
             assert(_readBuffer);
 
-            // didConnect fires after certificate verification if we're using pinned certificates.
-            BOOL usingPinnedCerts = [[_urlRequest SR_SSLPinnedCertificates] count] > 0;
-            if ((!_secure || !usingPinnedCerts) && self.readyState == SR_CONNECTING && aStream == _inputStream) {
+            if (!_securityOptions.requestRequiresSSL && self.readyState == SR_CONNECTING && aStream == _inputStream) {
                 [self didConnect];
             }
+
             [self _pumpWriting];
             [self _pumpScanner];
+
             break;
         }
 
@@ -1599,7 +1572,7 @@ static inline int32_t validate_dispatch_data_partial_string(NSData *data) {
         lastOffset = offset;
         U8_NEXT(str, offset, size, codepoint);
     }
-    
+
     if (codepoint == -1) {
         // Check to see if the last byte is valid or whether it was just continuing
         if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) {