37

I'm trying to connect an iOS app to a Windows C# sever using TLS over TCP/IP.

The TLS connection is using untrusted certificates created from an untrusted CA root certificate using the makecert utility.

To test these certificates I created a simple C# client and using those certificates it was able to connect and communicate with the server.

I'm not skilled at iOS development, but I did manage to find some code that connects me to the server, as follows:

-(bool)CreateAndConnect:(NSString *) remoteHost withPort:(NSInteger) serverPort
{
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)(remoteHost),
                                       serverPort, &readStream, &writeStream);

    CFReadStreamSetProperty(readStream, kCFStreamPropertySocketSecurityLevel,
                            kCFStreamSocketSecurityLevelNegotiatedSSL);

    NSInputStream *inputStream = (__bridge_transfer NSInputStream *)readStream;
    NSOutputStream *outputStream = (__bridge_transfer NSOutputStream *)writeStream;

    [inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey];

    // load certificate from servers exported p12 file
    NSArray *certificates = [[NSArray alloc] init];
    [self loadClientCertificates:certificates];

    NSDictionary *sslSettings = [NSDictionary dictionaryWithObjectsAndKeys:
                                 (id)kCFBooleanFalse, (id)kCFStreamSSLValidatesCertificateChain,
                                 certificates,(id)kCFStreamSSLCertificates,
                                 nil];

    [inputStream setProperty:sslSettings forKey:(__bridge NSString *)kCFStreamPropertySSLSettings];

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

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

    CFReadStreamOpen(readStream);
    CFWriteStreamOpen(writeStream);

    return true;
}

The code also seems to do some form of TLS negotiation, as the C# server rejects the connection if the p12 certificates are not provided as part of the NSStream settings.

So it appears like the first stage of the TLS negotiation is working.

To validate the server certificate I have this function, which gets called by the NSStream delegate on the NSStreamEventHasSpaceAvailable event:

// return YES if certificate verification is successful, otherwise NO
-(BOOL) VerifyCertificate:(NSStream *)stream
{
    NSData *trustedCertData = nil;
    BOOL result             = NO;
    SecTrustRef trustRef    = NULL;
    NSString *root_certificate_name      = @"reference_cert";
    NSString *root_certificate_extension = @"der";

    /* Load reference cetificate */
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];
    trustedCertData = [NSData dataWithContentsOfFile:[bundle pathForResource: root_certificate_name ofType: root_certificate_extension]];

    /* get trust object */
    /* !!!!! error is here as trustRef is NULL !!!! */
    trustRef = (__bridge SecTrustRef)[stream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust];

    /* loacate the reference certificate */
    NSInteger numCerts = SecTrustGetCertificateCount(trustRef);
    for (NSInteger i = 0; i < numCerts; i++) {
        SecCertificateRef secCertRef = SecTrustGetCertificateAtIndex(trustRef, i);
        NSData *certData = CFBridgingRelease(SecCertificateCopyData(secCertRef));
        if ([trustedCertData isEqualToData: certData]) {
            result = YES;
            break;
        }
    }
    return result;
}

Now the problem is, no matter what I try, the trustRef object is always null.

From this Apple developer link: https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html

There is this quote that suggest this should not be the case:

By the time your stream delegate’s event handler gets called to indicate that there is space available on the socket, the operating system has already constructed a TLS channel, obtained a certificate chain from the other end of the connection, and created a trust object to evaluate it.

Any hints on how to fix this?

How can I get access to that trustRef object for the NSStream?

Edit:

Thanks for the reply 100phole.

In trying to get this to work, I thought this might have something to do with the issue and in one of my many attempts I moved all of those socket related items into a class:

Something like this:

@interface Socket
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    NSInputStream *inputStream;
    NSOutputStream *outputStream;
@end

But that came up with the same results :(

I only reverted back to the version shown above because, based on my Google searching, that appears to be a fairly code common pattern.

For example, even this code from the Apple Developer site uses a very similar style:

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Streams/Articles/NetworkStreams.html#//apple_ref/doc/uid/20002277-BCIDFCDI

As I mentioned earlier, I'm no expert in Objective-C (far from it), so I might be wrong, but from what I have seen, moving those items into a class and having them persist did not seem to make any difference.

idz
  • 12,825
  • 1
  • 29
  • 40
jussij
  • 10,370
  • 1
  • 33
  • 49
  • 2
    Your streams appear to be local variables in a method, which means they will be destroyed when the method returns. – l00phole Oct 16 '15 at 09:29
  • Looked at your code only long enough to ask... why not use the builtin NSURL session and connection classes? The connection setup and auth handshakes are all handled for you (with hooks to interject when necessary). – Jody Hagins Nov 08 '15 at 23:27
  • You see the note in the web page you reference saying `/* Store a reference to the input and output streams so that they don't go away.... */` – Wain Nov 09 '15 at 17:12
  • 1
    What version of iOS are you testing on? – Nicolas S Nov 10 '15 at 18:22
  • @Nicolas S the iPad is running iOS version 9.0. – jussij Nov 13 '15 at 08:21
  • @Jody Hagins the reason the code does not use NSURL is because the iPad talks to the server using plain old TCP/IP over a socket. From what I understand NSURL uses the http:// protocol – jussij Nov 13 '15 at 08:22
  • I'm going to guess that your `Socket` class was instantiated inside `CreateAndConnect:withPort` instead of being an instance variable of that class. When `CreateAndConnect:withPort` returns, unless you have saved the streams someplace (like instance variables of `self`), they will be destroyed. Also, if you are just trying to get it to work, why not try this library: https://github.com/robbiehanson/CocoaAsyncSocket since it already seems to support what you want to do. – Jody Hagins Nov 13 '15 at 14:15
  • 2
    If you're connecting at a low level, this might not be in effect, but iOS 9 uses Application Transport Security, which you might want to disable or white list your server due to TLS. I would try to disable it altogether just to make sure it is not bothering you. http://stackoverflow.com/questions/32892121/xcode-7-1-beta-2-disable-ats/32894812#32894812 – Nicolas S Nov 13 '15 at 15:04
  • The p12 file contains the private keys and should probably stay on the server. You only need the public key and in any case, TLS should give these to you through key exchange. Can you get this working on another platform (Android, Windows)? – Roy Falk Dec 31 '15 at 13:41

2 Answers2

0

Since there seems to be some interest in this question I decided to update the question with a answer with details on how this problem was eventually solved.

First some background. I inherited this code from a previous developer and my role was to get the broken code to work.

I spent a lot of time writing and re-writing the connection code using the details from the Apple iOS developer web page, but nothing seemed to work.

I finally decided to take a closer look at this function, code I had inherited and incorrectly assumed was working:

[self loadClientCertificates:certificates];

At first glance the code looked OK. The function did nothing more than load certificates from file. But on closer inspection, while the code was loading the certificates correctly, it was not returning those certificates to the caller!!!

After fixing that code so that it correctly returned the certificates the connection code worked fine and the SecTrustRef was no longer NULL.

In summary:

1) The Apple documentation, while lacking good examples does appear to be accurate.

2) The reason the SecTrustRef was NULL was because no valid certificate could be found for the connection negotiations phase and that was because no certificates where being made available to the connection API due to the earlier mentioned coding error.

3) If you are seeing a similar error, my suggestion would be to check and double check your code, because as would be expected, the iOS side of the equation works as documented.

jussij
  • 10,370
  • 1
  • 33
  • 49
0

I recently created an Obj-C package to handle TLS taking into account the latest restrictions imposed by Apple. Getting the certificates right is a very important step.

https://github.com/eamonwhiter73/IOSObjCWebSockets

ewizard
  • 2,801
  • 4
  • 52
  • 110