NSStream fails to properly close SSL Session

The short version:

Closing NSStream doesn't completely reset SSL connection as subsequent connection attempts have SSL SessionID from the previously closed session. Session ID only resets after process restart or after ~10 minutes.

The long version:

Our Cocoa app using is NSStream to connect over TCP with TLS 1.2 to the server powered by libevent (libevent.org). The initial connection works fine, both sets of certtificates are validated, events are being delivered normally. When the client decides to close the connection (see "closeWithReason" method below) , it basically just closes both NSInputStream and NSOutputStream objects. What happens next is that any subsequent connection attempt with the same settings/certificates results in SSL Handshake Error -9806 ("connection closed via error"). The issue resolves itself when either process is restarted (client or server), it also fixes itself in about 10 minute time. After that (restart or time period) the connection is established as usual.

Digging with Wireshark revealed that the client tries to establish the connection after breaking it previously, it sends "Client Hello" message but the server doesn't reply with "Server Hello". As it turns out the subsequent connections have SSL Session ID filled with the id from the session that was closed before, therefore server doesn't treat it as a new connection and it all goes weird from there. As it was mentioned above, this Session ID is reset in about 10 minutes time.


What we've tried:

  • Using kCFStreamPropertyShouldCloseNativeSocket flag
  • Closing the socket on the client (in addition to closing the streams).
  • Experimenting with reseting different properties of SSL Session when we closing the connection.


Notes:
The issue is reproducable when the server is using libevent library but it all works fine when server is using OpenSSL. It seems that they treat having SSL Session ID for otherwise new connection differently.


Here is the code snippet for the connection logic though there is nothing out of ordniary there:

@property (strong) NSInputStream  *inputStream;
@property (strong) NSOutputStream *outputStream;

- (void)connectToServer
{
    CFReadStreamRef  readStream;
    CFWriteStreamRef writeStream;
    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef) self.ip, (uint) self.port, &readStream, &writeStream);
    self.inputStream  = (NSInputStream *) CFBridgingRelease(readStream);
    self.outputStream = (NSOutputStream *) CFBridgingRelease(writeStream);
    self.identity = //Valid client certificate
    NSDictionary *settings = @{ (NSString *) kCFStreamSSLLevel                    : @"kCFStreamSocketSecurityLevelTLSv1_2",
                                (NSString *) kCFStreamSSLCertificates              : self.identity,
                                (NSString *) kCFStreamSSLValidatesCertificateChain : (id) kCFBooleanFalse,
                                (NSString *) kCFStreamSSLPeerName                  : (NSNull *) kCFNull,
    };
    [self.inputStream setProperty:settings forKey:(NSString *) kCFStreamPropertySSLSettings];
    [self.inputStream setProperty:(id) kCFBooleanTrue forKey:(NSString *) kCFStreamPropertyShouldCloseNativeSocket];

    [self.outputStream setProperty:settings forKey:(NSString *) kCFStreamPropertySSLSettings];
    [self.outputStream setProperty:(id) kCFBooleanTrue forKey:(NSString *) kCFStreamPropertyShouldCloseNativeSocket];


    [self.inputStream setDelegate:self];
    [self.outputStream setDelegate:self];

    [self.inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [self.outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [self.inputStream open];
    [self.outputStream open];
}

- (void)closeWithReason:(NSString *)reason
{
    //...

    [self.inputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    [self.outputStream removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

    [self.inputStream close];
    [self.outputStream close];

    self.inputStream = nil;
    self.outputStream = nil;
}

Closing NSStream doesn't completely reset SSL connection as subsequent connection attempts have SSL SessionID from the previously closed session. Session ID only resets after process restart or after ~10 minutes.

Indeed. This is expected behaviour as per QA1727 TLS Session Cache.

It's weird that this is causing you problems: TLS session resume is a documented part of the TLS standard and AFAIK iOS implements it correctly. Certainly, if the iOS side of this were truly broken we'd know about it; it's used by millions of Safari users every day.

Honestly, I suspect that this is a problem with your server. If the server is not prepared to resume a session it's supposed to ignore the client's proposed session ID and do the full handshake.

If you can't fix the server then you can probably work around this on the client side, but IMO it'd be better to attempt a server-side fix first.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
NSStream fails to properly close SSL Session
 
 
Q