This was a pain in the neck, but I finally figured out the answer.
So I was attempting to open a connection to a server from iOS which may have a self-signed certificate installed.
The specific steps I used to open the connection (from my test harness) was:
1. Set up the variables
NSString *host = @"127.0.0.1"; NSInteger port = 12345; CFReadStreamRef readStream; CFWriteStreamRef writeStream;
2. Set debugging diagnostics. Noted here so I can remember this trick.
setenv("CFNETWORK_DIAGNOSTICS","3",1);
3. Open the sockets.
CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (__bridge CFStringRef)host, (uint32_t)port, &readStream, &writeStream); NSInputStream *inStream = (__bridge_transfer NSInputStream *)readStream; NSOutputStream *outStream = (__bridge_transfer NSOutputStream *)writeStream;
4. Set up SSL. The part that tripped me up: you must set the properties for kCFStreamPropertySSLSettings after setting NSStreamSocketSecurityLevelKey. It appears the NSStreamSocketSecurityLevelKey setting overwrites the kCFStreampropertySSLSettings parameter.
// Note: inStream and outStream are linked by an underlying object, so // parameters only need to be set on one of the two streams. NSDictionary *d = @{ (NSString *)kCFStreamSSLValidatesCertificateChain: (id)kCFBooleanFalse }; [inStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey]; [inStream setProperty:d forKey:(id)kCFStreamPropertySSLSettings];
5. Open and initalize as needed. Here I’m just opening (because I don’t care if this blocks; this is test code). If using blocking API, use threads. Otherwise use the runloop and delegate APIs.
[inStream open]; [outStream open];
On the Java (server) side, the way I set up my server socket for listening to incoming connections was:
1. Set up the variables. Note that my Config class is an internal class that reads properties, and is beyond the scope of this exercise.
Properties p = Config.get(); String keystore = p.getProperty("keystorefile"); String password = p.getProperty("keystorepassword"); int port = 12345;
2. Load the keystore and key manager. This can be a signed or (in my case) self-signed certificate.
FileInputStream keyFile = new FileInputStream(keystore); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(keyFile, password.toCharArray()); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); keyManagerFactory.init(keyStore, password.toCharArray()); KeyManager keyManagers[] = keyManagerFactory.getKeyManagers();
3. Create an SSLContext. Note that you cannot use “Default” for getInstance below, because that returns an already initialized context, and we want to initialize it with our parameters above. Also note that iOS 9 prefers TLS 1.2.
SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); sslContext.init(keyManagers, null, new SecureRandom());
4. Open a ServerSocket class to listen for incoming connections. Note the constant 50 below is arbitrary.
SSLServerSocketFactory socketFactory = sslContext.getServerSocketFactory(); socket = socketFactory.createServerSocket(port, 50);
Note that the way I loaded the keystore is sort of the “hard way” to do this; my eventual goal is to have the Java startup code generate a self-signed certificate internally if a keystore is not provided, but I haven’t figured out how to do that yet. (There are plenty of pages out there that show how, but most of them rely on internal Java APIs, and I’m sort of allergic to using undocumented stuff.)