Correctly overriding TLS logic for IoT communication

Hi.

I'm developing an application that should be able to connect to IoT devices in the local network. I want to use https with a self-signed certificate. As far as I understand it's against AST, but I could request an exception. 

There are 2 things that I would need to change to make it work:
  1. Trust my self-signed Root CA - I'm planning to use it to issue certificate on each device with subjectAlternativeName = DNS:<deviceId>.device.mydomain.com that is not real DNS name.

  2. Alter logic to verify host - those devices don't have public DNS addresses and the application communicates with IoT devices using address in local network like 192.168.0.14. Because my application knows the exact id of the device (each IoT device is connected to the user account) if a server responds with a certificate issued by my Root CA I would check that <deviceId> in the certificate matches with the IoT device user is trying to connect to instead of checking hostname.

In this scenario each device has its own unique key pair and even if someone disassembles and extracts private key from one device, it would not give them ability to intercept request from another device.

Alternatively, aside from using plaintext http, I was thinking about giving my IoT devices certificates issued by trusted CA, like Let's Encrypt, and setting up DNS that would actually resolve <deviceId>.device.mydomain.com to its current address on local network, but there are 2 issues with that approach that I would like to avoid:
  1. Let's Encrypt has a quota of 50 certificates per week and I assume other Public CAs would have similar restrictions

  2. My backend server might not be aware of the actual local address if there is no connectivity between IoT device and backend server, whereas application can find it on the local network.

I'm not an expert in iOS development and my project is written with react-native, but I managed to get some code working:
Code Block
#import <React/RCTBridgeModule.h>
#import <React/RCTHTTPRequestHandler.h>
#import <React/RCTLog.h>
#import "RCTActiveDevice.h"
@implementation RCTHTTPRequestHandler(iotTls)
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
NSString *deviceId = [RCTActiveDevice getCurrentDeviceId];
if (deviceId && [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
SecCertificateRef issuerRootCa = SecTrustGetCertificateAtIndex(serverTrust, SecTrustGetCertificateCount(serverTrust) - 1);
NSData *issuerRootCaData = CFBridgingRelease(SecCertificateCopyData(issuerRootCa));
NSString *deviceRootCaPath = [[NSBundle mainBundle] pathForResource:@"device_root_ca" ofType:@"crt"];
NSData *deviceRootCaData = [NSData dataWithContentsOfFile:deviceRootCaPath];
SecCertificateRef deviceRootCa = SecCertificateCreateWithData(NULL, (bridge CFDataRef) deviceRootCaData);
bool isDeviceCertificate = [deviceRootCaData isEqualToData:issuerRootCaData];
if (isDeviceCertificate) {
NSString *domain = [NSString stringWithFormat:@"%@.device.mydomain.com", deviceId];
SecPolicyRef policy = SecPolicyCreateSSL(false, (bridge CFStringRef) domain);
SecTrustSetPolicies(serverTrust, (bridge CFArrayRef) [NSArray arrayWithObjects: (bridge id _Nonnull) (policy), nil]);
SecTrustSetAnchorCertificates(serverTrust, (bridge CFArrayRef) [NSArray arrayWithObjects: (bridge id _Nonnull) (deviceRootCa), nil]);
bool isDeviceTrusted;
if (@available(iOS 12.0, *)) {
isDeviceTrusted = SecTrustEvaluateWithError(serverTrust, nil);
} else {
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
isDeviceTrusted = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
if (isDeviceTrusted) {
NSURLCredential *credential = [NSURLCredential credentialForTrust: serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
return;
}
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]);
}
@end


My question is this approach good and would I be able to get approval for my application to be included in the App Store?
A few things to be aware of; unless you are using MDM, there is not an automatic way to trust a self signed root certificate on an iOS device, without going through the process of installing it by hand and trusting it yourself. That would not lend itself well to an App Store workflow. What about getting a leaf certificate for your IoT device that currently exists in the iOS trust store today? If there was a way to obtain a leaf from the trust store that could be mapped to a local hostname instead of an address, that would be the first path I would take as that.



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

Thank you for reply, Matt.
Thing is I don't need all requests from an iOS device to trust this self-signed certificate, only requests coming from my application. That's why I've bundled my self-signed Root CA (device_root_ca.crt) with the application and overridden standard TLS checking mechanism (code example I've posted), so I don't need this self-signed Root CA to be trusted on system level. If user opens browser and tries to make request to the IoT device, those will obviously fail.

Regarding local certificates - do you mean requesting certificate for address like <deviceId>.local? I think no CA would actually issue such certificate.

Just as a remark, this IoT device is going to be a retail product (like smart socket) and I want users that bought it to be able to control it directly over local network using my application. I don't have control over user devices or their local network.

so I don't need this self-signed Root CA to be trusted on system level.

How are you making a TLS connection to your IoT device if your iOS device does not know about the Root Certificate that the leaf was issued from on your IoT device? I am assuming that the connection is failing in your application with a message that contains something similar to "Unknown Root CA."


Regarding:

Just as a remark, this IoT device is going to be a retail product (like smart socket)
and I want users that bought it to be able to control it directly over local network
using my application. I don't have control over user devices or their local network.

I know you mentioned HTTP, but how much traffic do you need to push back and forth and what are your options? Can you use TLS with TCP or wss:// instead? If so, you could use a pre-shared keys for TLS instead of public key certificates.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

How are you making a TLS connection to your IoT device if your iOS device does not know about the Root Certificate that the leaf was issued from on your IoT device?

I have bundled my Root Certificate (device_root_ca.crt) inside iOS Application and then overridden function didReceiveChallenge that is mentioned in Overriding TLS Chain Validation Correctly. I posted the code in my very first message. The code assumes 2 things:
  1. Application always knows specific IoT device user is working with - deviceId stored inside RCTActiveDevice.

  2. IoT device has leaf certificate with subjectAlternativeName = DNS:<deviceId>.device.mydomain.com issued by device_root_ca.crt.

Inside didReceiveChallenge the code checks if leaf certificate from a server was issued by this device_root_ca.crt, then it's guaranteed to be one of my IoT devices. I could end my validation here and trust the certificate, but let's say someone bought device with deviceId = 1 and extracted the private key from it. If I would trust *any* certificate, then, in very unlikely situation, this person could MITM another user's IoT device with deviceId = 2. To avoid that scenario I'm using knowledge that user is trying to connect to deviceId = 2 and in this code I'm specifically checking that leaf certificate has subjectAlternativeName = DNS:2.device.mydomain.com, so that if server presents private key for deviceId = 1 it will be rejected by user's application.

I'm using react-native that implements JavaScript Fetch API on iOS. I believe the main logic is inside RCTHTTPRequestHandler. That's the class I have customized with didReceiveChallenge. On JavaScript code looks like this:
Code Block
// passing `deviceId` to iOS code
/*RCT*/ActiveDevice.setCurrentDeviceId("2");
// request eventually ends up in `didReceiveChallenge` that checks if leaf certificate belongs to `deviceId = 2`
fetch("https://192.168.0.14/state").then(...);

It works pretty well - checks that deviceId in leaf certificate matches the one I have set, fails if I set wrong deviceId.

I know you mentioned HTTP, but how much traffic do you need to push back and forth

Bandwidth wise not more than tens of kilobytes per request - user can monitor state of the device, control some settings, connect it to different WiFi network. When application is open it checks the state every couple seconds.

what are your options? Can you use TLS with TCP or wss:// instead?

I believe wss:// (just like https://) verifies that hostname matches to leaf certificate, whereas my iOS application connects to an IoT device using local address, which I cannot know upfront when issuing certificate for that device.
I can use TLS with TCP. If I understand right I wouldn't have to deal with hostname check, but I would have to add code to trust my Root Certificate and then implement deviceId check somehow.
Or as you mentioned use pre-shared keys, but I'm not familiar with it. Is it using own encryption over TCP using same key shared between IoT device and iOS application? I could do that too - user at least once connects to my backend during IoT device setup, so this key can be retrieved and stored on iOS device.
Anyway it seems like a bit more complex while not being more secure, than what I currently have, isn't it?

I was also thinking about using dynamic DNS that would resolve 2.device.mydomain.com to 192.168.0.14 and leaf certificates from Public CA, but as I mentioned in my very first message this solution has its own problems.

Hi, did this approach work for you? Were you able to get it approved for the app store?

Between the time that this thread quiesced and now, I posted TLS For Accessory Developers. It explains a bunch of background here.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Correctly overriding TLS logic for IoT communication
 
 
Q