Monday, May 3, 2010

NSStream: TCP and SSL

In Chapter 9 of More iPhone 3 Development, we showed how to use Bonjour and NSInputStream / NSOutputStream to do network communications. In that case, it was for making a simple networkable game, but the technique is essentially the same for any kind of low-level network communications.

On Mac OS X, NSStream, the superclass of both NSInputStream and NSOutputStream, has a class convenience method called getStreamsToHostNamed:port:inputStream:outputStream which creates a stream pair to a specified remote host. For some reason unbeknownst to me¹, Apple chose not to include this convenience method in the iPhone SDK. The underlying functionality is still there in the iPhone SDK, they just removed the method that make it easy to establish the streams. Apple rectified that situation shortly thereafter by issuing Technote TN QA1652, which gives a category on NSStream that restores the missing functionality.

Using this method really couldn't be easier. In our TicTacToe application, we could have established an OnlineSession object to another game specified by IP address and port rather than through Bounjour, like so:

    NSInputStream *is;
NSOutputStream *os;
[NSStream getStreamsToHostNamed:address
port:port
inputStream:&is
outputStream:&os
]
;
OnlineSession *session = [OnlineSession initWithInputStream:is outputStream:os];

Of course, there would still need to be an instance of TicTacToe running on the remote client listening for connections. With internet play, you're generally going to need some way of finding the address and port of the machine to connect to, but if a game is running and isn't firewalled, that will connect you to it. In fact, you can use this same technique for pretty much any kind of low-level network communications.

One thing that's not obvious, however, is how you can encrypt your network communications using SSL. It's pretty easy, but there is a significant gotcha that's worth mentioning. Let's look at OnlineSession's initWithInputStream:outputStream: and then add SSL support for it. Here is the original:

- (id)initWithInputStream:(NSInputStream *)theInStream outputStream:(NSOutputStream *)theOutStream
{
if (self = [super init]) {

inStream = [theInStream retain];
outStream = [theOutStream retain];

[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

inStream.delegate = self;
outStream.delegate = self;

if ([inStream streamStatus] == NSStreamStatusNotOpen)
[inStream open];

if ([outStream streamStatus] == NSStreamStatusNotOpen)
[outStream open];

packetQueue = [[NSMutableArray alloc] init];

}

return self;
}

That version creates a plaintext, unencrypted connection to the remote server. The way that we enable SSL encryption is to simply use setProperty:forKey: on both streams, setting the key NSStreamSocketSecurityLevelKey to a value that specifies the version of SSL to use. If you want to tell NSStream to use the highest version supported in common with the remote connection, specify NSStreamSocketSecurityLevelKey. That's what you'll usually want. Here's a new version of our init method that lets us specify whether to use SSL or not:
- (id)initWithInputStream:(NSInputStream *)theInStream outputStream:(NSOutputStream *)theOutStream useSSL:(BOOL)useSSL 
{
if (self = [super init]) {

inStream = [theInStream retain];
outStream = [theOutStream retain];

[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

inStream.delegate = self;
outStream.delegate = self;

if ([inStream streamStatus] == NSStreamStatusNotOpen)
[inStream open];

if ([outStream streamStatus] == NSStreamStatusNotOpen)
[outStream open];

packetQueue = [[NSMutableArray alloc] init];

if (useSSL)
{
[inStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey
]
;
[outStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey
]
;

}

}

return self;
}

And, we're done! Well, at least insofar as the NSStream documentation would have you believe. This code will work if everything is perfect. However, by default, SSL support in NSStream is a little paranoid. It won't, for example, use a self-signed certificate or an expired certificate to establish a secure connection. NSStream does a number of validity checks when establishing the secure connection, and if they don't all pass, the streams appear to be valid, but no data gets sent or received. This is somewhat frustrating, and it could be there's a way to find out when the secure connection failed, but I haven't been able to find it in the documentation, or using Google. There is an error domain declared for these errors (NSStreamSocketSSLErrorDomain), but in my experimentation, no errors gets generated, the streams even accept bytes for transfer, but nothing happens².

When it comes to security, a little paranoia is good, but there are many situations where all you want is the encryption provided by SSL, and you don't really care about whether the root certificate is valid (the root certificate identifies the certificate authority that issued the certificate). If you're doing an e-Commerce transaction, then you almost certainly want to make sure the certificate is valid and was issued by a valid authority. But if all you're trying to do is provide a little privacy from prying eyes, a self-signed certificate is often perfectly acceptable.

Unfortunately, NSStream doesn't provide a way to say "don't check the root certificate". Fortunately, CFStream does, and remember: CFStream and NSStream are toll-free bridged.

The way we can turn off these validity checks is by creating an instance of NSDictionary. In this dictionary, we'll specify Boolean values for a number of system-defined key values like kCFStreamSSLAllowsAnyRoot, kCFStreamSSLAllowsExpiredCertificates, and kCFStreamSSLValidatesCertificateChain, specifying either YES or NO as appropriate to your situation. SSL also does verification on the name of the remote peer. We can turn that off by overriding the peer name using the key kCFStreamSSLPeerName and setting it to null (well, technically, kCFNull).

Once we have this dictionary, we can feed it to our stream pair by casting them to their CF counterpart using CFReadStreamSetProperty and CFWriteStreamSetProperty. If we wanted to turn off all the validity checks and allow any certificate signed by any root certificate, we would do this:

- (id)initWithInputStream:(NSInputStream *)theInStream outputStream:(NSOutputStream *)theOutStream useSSL:(BOOL)useSSL 
{
if (self = [super init]) {

inStream = [theInStream retain];
outStream = [theOutStream retain];

[inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

inStream.delegate = self;
outStream.delegate = self;

if ([inStream streamStatus] == NSStreamStatusNotOpen)
[inStream open];

if ([outStream streamStatus] == NSStreamStatusNotOpen)
[outStream open];

packetQueue = [[NSMutableArray alloc] init];

if (useSSL)
{
[inStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey
]
;
[outStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
forKey:NSStreamSocketSecurityLevelKey
]
;

NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsExpiredCertificates,
[NSNumber numberWithBool:YES], kCFStreamSSLAllowsAnyRoot,
[NSNumber numberWithBool:NO], kCFStreamSSLValidatesCertificateChain,
kCFNull,kCFStreamSSLPeerName,
nil
]
;

CFReadStreamSetProperty((CFReadStreamRef)inStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);
CFWriteStreamSetProperty((CFWriteStreamRef)outStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);

}

}

return self;
}

If you're attempting to use SSL, but your network connection doesn't seem to do anything, try turning off the validity checks one at a time to see if that changes your result, or use the code above to turn them all off, which should at least tell you if the problem you're having is a failure to establish a secure connection. Be careful shipping applications with these checks turned



1 The reason is now knownst to me. The original method used NSHost which isn't available on the iPhone - thanks Chris!
2 If anyone knows how to determine when this is happening, please let me know and I'll post the information. I'm sure there has to be a way to detect the failure to establish an encrypted connection so you can "fallback" to a less secure option if you want to, but I haven't found it yet



8 comments:

Matt said...
This comment has been removed by the author.
Jeff LaMarche said...

Matt:

That's basically what the category does, it takes roughly the same logic you've got but wraps it up in a class method where it belongs, that's all.

Felix said...

I found a memory leak in your code. Don't forget to release the settings dictionary you created.

ensogukmevsim said...

its fine That's basically what the category does, it takes roughly the same logic you've got but wraps it up in a class method where it belongs, that's all.
http://www.iphoneservisi.net

Shu Ning Bian said...

Thank you so much for this post! Exactly what I was looking for, exactly right :D

You might want to mention the need to

#import

And also add CFNetworking to dependencies.

davbeck said...

NSOutputStream will return -1 when there is an error writing to the stream.

KR said...

If I'm correct, NSStream only handles server certificates. What if I want to use a client certificate? How do we tell the stream to use a particular identity?

h4ns said...

What youre saying is completely true. I know that everybody must say the same thing, but I just think that you put it in a way that everyone can understand. I also love the images you put in here. They fit so well with what youre trying to say. Im sure youll reach so many people with what youve got to say.

Arsenal vs Huddersfield Town live streaming
Arsenal vs Huddersfield Town live streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Wolverhampton Wanderers vs Stoke City Live Streaming
Notts County vs Manchester City Live Streaming
Notts County vs Manchester City Live Streaming
Bologna vs AS Roma Live Streaming
Bologna vs AS Roma Live Streaming
Juventus vs Udinese Live Streaming
Juventus vs Udinese Live Streaming
Napoli vs Sampdoria Live Streaming
Napoli vs Sampdoria Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
Fulham vs Tottenham Hotspur Live Streaming
AS Monaco vs Marseille Live Streaming
AS Monaco vs Marseille Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Alajuelense vs Perez Zeledon Live Streaming
Technology News | News Today | Live Streaming TV Channels