objective-csocketsssltcpnsstream

Objective-C TCP Server with SSL / TLS support


I have a simple TCP Server / Client setup. This connection actually works great.

Now I wanted to implement SSL / TLS encryption for the socket connections. I created a PKCS12 certificate using keychain access. In my server I have the following code inside the accept callback:

NSString *certificatePath = [[NSBundle mainBundle] pathForResource:@"TCPServerCertificate" ofType:@"p12"];
NSData *certificateData = [NSData dataWithContentsOfFile:certificatePath];

CFArrayRef keyRef;
OSStatus status = SecPKCS12Import((__bridge CFDataRef)certificateData, (__bridge CFDictionaryRef)@{(__bridge NSString *)kSecImportExportPassphrase: @"1234"}, &keyRef);

if (status != noErr) {
    NSLog(@"PKCS12 import error %i", status);
    return;
}

CFDictionaryRef identityDict = CFArrayGetValueAtIndex(keyRef, 0);
SecIdentityRef identityRef = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);

SecCertificateRef certificate;
status = SecIdentityCopyCertificate(identityRef, &certificate);

if (status != noErr) {
    NSLog(@"sec identity copy failed: %i", status);
    return;
}

NSArray *certificates = [NSArray arrayWithObjects:(__bridge id)identityRef, (__bridge id)certificate, nil];

NSDictionary *settings = @{(NSString *)kCFStreamPropertyShouldCloseNativeSocket:    [NSNumber numberWithBool:YES],
                           (NSString *)kCFStreamSSLValidatesCertificateChain:       [NSNumber numberWithBool:YES],
                           (NSString *)kCFStreamSSLAllowsExpiredCertificates:       [NSNumber numberWithBool:NO],
                           (NSString *)kCFStreamSSLAllowsExpiredRoots:              [NSNumber numberWithBool:NO],
                           (NSString *)kCFStreamSSLAllowsAnyRoot:                   [NSNumber numberWithBool:YES],
                           (NSString *)kCFStreamSSLCertificates:                    certificates,
                           (NSString *)kCFStreamSSLIsServer:                        [NSNumber numberWithBool:YES],
                           (NSString *)kCFStreamSSLLevel:                           (NSString *)kCFStreamSocketSecurityLevelTLSv1};

CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);
CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);

Then I create NSStream instances of the streams and handle them in another class.

When I run the server and connect a client, I'm getting the regular NSStreamEventOpenCompleted in my delegate. When I then try to write to the stream or event if I just close the connection, I'm getting the following errors:

2013-10-25 13:27:08.584 TCPServer[6435:303] CFNetwork SSLHandshake failed (-9800)
2013-10-25 13:27:08.584 TCPServer[6435:303] NSStreamEventOpenCompleted
2013-10-25 13:27:08.585 TCPServer[6435:303] NSStreamEventErrorOccurred

I wonder what I have to implement on the client side. Also I wonder why I'm getting the handshake failure when sending data or disconnecting from the client side. Whenever this error occurs, the client thinks it's still connected.

Are there any good TCP SSL/TLS tutorials or other material that covers both, the client and the server side?


Solution

  • There are some problems with this code: You can not set kCFStreamPropertyShouldCloseNativeSocket with kCFStreamPropertySSLSettings, and you should not mix server and client code.

    For server you should just set a certificate

    CFReadStreamSetProperty(read, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    CFWriteStreamSetProperty(write, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    //kCFStreamPropertySocketSecurityLevel
    //Note: If you set this key, you must do so before setting any other SSL options, such as kCFStreamPropertySSLSettings.
    CFReadStreamSetProperty(read, kCFStreamPropertySocketSecurityLevel, kCFStreamSocketSecurityLevelNegotiatedSSL);
    CFWriteStreamSetProperty(write, kCFStreamPropertySocketSecurityLevel, kCFStreamSocketSecurityLevelNegotiatedSSL);
    //Creating server dictionnary
    
    //kCFStreamSSLIsServer
    //If the value of this key is kCFBooleanTrue, the kCFStreamSSLCertificates key must contain a valid value
    
    //kCFStreamSSLCertificates
    //Security property key whose value is a CFArray of SecCertificateRefs except for the first element in the array, which is a SecIdentityRef.
    //For more information, see SSLSetCertificate() in Security/SecureTransport.h.
    
    NSDictionary *settings = @{(id)kCFStreamSSLCertificates:                    certificates,
                               (id)kCFStreamSSLIsServer:                        @YES};
    
    
    //Apply settings
    CFReadStreamSetProperty(read, kCFStreamPropertySSLSettings, (__bridge CFDictionaryRef)(settings));
    CFWriteStreamSetProperty(write, kCFStreamPropertySSLSettings, (__bridge CFDictionaryRef)(settings));
    

    For client if you want to override the validation chain (See Apple doc on overriding chain validation) you should do the following:

    CFReadStreamSetProperty(read, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    CFWriteStreamSetProperty(write, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    //kCFStreamPropertySocketSecurityLevel
    //Note: If you set this key, you must do so before setting any other SSL options, such as kCFStreamPropertySSLSettings.
    
    CFReadStreamSetProperty(read, kCFStreamPropertySocketSecurityLevel, kCFStreamSocketSecurityLevelNegotiatedSSL);
    CFWriteStreamSetProperty(write, kCFStreamPropertySocketSecurityLevel, kCFStreamSocketSecurityLevelNegotiatedSSL);
    
    //DO NOT USE kCFStreamPropertySSLContext as it overrides the following configuration
    //create dictionnary for kCFStreamPropertySSLSettings
    
    //keys for dictionnary we want to change:kCFStreamSSLAllowsExpiredCertificates;kCFStreamSSLAllowsExpiredRoots;kCFStreamSSLAllowsAnyRoot;
    //kCFStreamSSLValidatesCertificateChain => no need to worry about the root
    //kCFStreamSSLPeerName kCFNull prevents name verification
    
    
    settings = @{(id)kCFStreamSSLValidatesCertificateChain: @NO,//The delegate will verify this
                 (id)kCFStreamSSLPeerName: (id)kCFNull};//prevents name verification if server is not fixed (eg. IP)
    
    //Apply settings
    CFReadStreamSetProperty(read, kCFStreamPropertySSLSettings, (__bridge CFDictionaryRef)(settings));
    CFWriteStreamSetProperty(write, kCFStreamPropertySSLSettings, (__bridge CFDictionaryRef)(settings));