I'm trying to figure out why an iPhone running iOS 14 isn't using TLS 1.3 to connect to a compatible web server.
Relevant code is:
- (void) streamOpened:(NSStream *)stream {
NSDictionary *settings = @{
(__bridge NSString *)kCFStreamSSLValidatesCertificateChain: (__bridge NSNumber *)kCFBooleanFalse
};
CFReadStreamSetProperty((CFReadStreamRef)inputStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);
CFWriteStreamSetProperty((CFWriteStreamRef)outputStream, kCFStreamPropertySSLSettings, (CFTypeRef)settings);
}
The full source can be seen here: https://github.com/tls-inspector/tls-inspector/blob/2.0.0/CertificateKit/Getters/CKAppleCertificateChainGetter.m
I've tried specifying the SSL Level with kCFStreamSSLLevel
set to kCFStreamSocketSecurityLevelTLSv1_3
but that didn't do anything.
If I use OpenSSL to connect it uses TLS 1.3 and I can verify that with a packet capture, but using CFStream it sticks to 1.2.
The short answer is that you end up using a deprecated API that does not support TLS 1.3.
The long answer, which details a potential solution, is given below.
I tried to solve this using CFStream
but did not succeed.
It might be possible. The problem is that you end up, as you do, at a low level using SSLConnectionRef
and friends and at a higher level using NSInputStream
and NSOutputStream
and friends and at some point you run into this https://developer.apple.com/documentation/security/secure_transport?language=objc legacy API.
On that page it mentions that that API was replaced by the Network framework and really this is what I am suggesting you should use. I was hoping to also implement a solution quickly but it needs a bit more rework than what SO is for so I leave it at that.
However, herewith some suggestions, basically the ideas I was hoping to implement.
As before, there are two levels.
At the lower level you end up using the nw_
and family but I would say do not go that way. It might be required for some specialised needs and your app might be in that category, but still, just take note that the higher level is built on top of this.
At the higher level, and this is where I think your solution lies, you end up using NSURL
and friends. Your goto guy here is probably NSURLSession
but the implementation will dictate that. I tried to give an outline and you can look at my code to get more detail but I think you will be in a much better position to implement from here onwards.
In that I was hoping to connect your legacy stream code to NSURLSession but when that failed I stopped. That is perhaps a bit optimistic and I think it requires a more serious rework, but the various delegates (NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionStreamDelegate and so on) seem to be ready and waiting for your solution and I don't think it is a lot of work actually.
The most relevant snippet of code from my attempt is below. This switches on TLS 1.3 and I tried to implement around this.
// Some configuration
NSURLSessionConfiguration * config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
// Note this one!!!
config.TLSMaximumSupportedProtocol = kTLSProtocolMaxSupported;
NSURLSession * urlSession = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:NSOperationQueue.currentQueue];
From this point onwards I tried to reuse your stream code but to be honest I think the final solution may not even use streams any more, and in stead just rely on using the correct delegate. Forgive me for getting excited at this point, but I suspect you will be able to greatly simplify your code as a result.
I enjoyed your app - it is pretty polished and I look forward to you solving this problem. I also enjoyed toying with it but for now that is my contribution.
First attempt
I eyeballed your code - it needs some reworking for TLS 1.3. I tried to do it but I am now rewriting that class so I stopped. You can do it, but, I can not guarantee that it will work!
Anyhow, here are some thoughts.
First on the existing code. Just some - ahem - observations, nothing serious ...
Note that your streamOpened
will apply the settings no matter which stream was triggered. The delegate will message that twice I believe, once for the input and once for the output stream. Although here it seems it does not matter you should be careful as that could introduce some serious bugs in another situation.
Also, I think you need to configure the streams before they are opened but this did not make any difference. If you configure the streams before they are opened in your performTaskForURL
or afterwards in the streamOpened
it did not matter.
I played with the configuration a bit. You need not set one on the output stream, only on the input. And the only key that is required is the one you already set. I could not get any difference no matter what I did.
Second, the solution I - ahem - think will work here.
You need to configure the URL session. So what I did was the following
- (void) performTaskForURL:(NSURL *)url{
queryURL = url;
// Some configuration
NSURLSessionConfiguration * config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
// Note this one!!!
config.TLSMaximumSupportedProtocol = kTLSProtocolMaxSupported;
NSURLSession * urlSession = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:NSOperationQueue.currentQueue];
// Just code to test the idea, not production ready I know
NSURLSessionStreamTask * streamTask = [urlSession streamTaskWithHostName:url.host
port:443];
[streamTask captureStreams];
}
- (void)URLSession:(NSURLSession *)session
streamTask:(NSURLSessionStreamTask *)streamTask
didBecomeInputStream:(NSInputStream *)inputStreamUrl
outputStream:(NSOutputStream *)outputStreamUrl
{
// I was hoping to get away with this,
// just setting your streams equal to the
// URL stream task streams but it did not
// work ... problem is you need more of the
// stream task delegate methods I believe
inputStream = inputStreamUrl;
outputStream = outputStreamUrl;
// This is some left over code from your performTaskForURL message
// Here you can see how I toyed with the stream configuration
// Configure here before it is opened
// Pretty much your current streamOpened message
// Note only input needs be configured (fwiw)
[self configureStream:inputStream];
inputStream.delegate = self;
outputStream.delegate = self;
[outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[outputStream open];
[inputStream open];
// I was hoping this would just work but I think it needs more work
}
The idea here is to configure a URL session with kTLSProtocolMaxSupported
and then to create a stream task off that. Now this is where I want to stop and think you are much better at going further. I was hoping I can just get the streams and throw them back to your code, but it did not work and for now I'm not going further.
I could not test my idea as I need to hook up a few more things to the NSURLSessionStreamDelegate
that your class now also implements. I think you do the same in the class already, but for streams.
Now, again, I may be wrong, and wanted to test before I posted, but either see if this might work and then I think you need to e.g. implement delegate methods such as URLSession:task:didReceiveChallenge:completionHandler:
.
I think you already do that sort of thing in your code, but this is why I am stopping here, as I think you will be a much better judge of this idea.
I've toyed with it some more but no success - but I think you need to use NSURLSession here, maybe even in stead of sockets.