iOS 9 and tvOS both brought with them a new addition called App Transport Security. In short, it means that, in an effort to encourage wider use of https, vanilla http connections will be prevented from connecting unless an exception exists in the app’s Info.plist.
Both Portfolio and Studio Pro provide a companion Mac-based loading app. It provides a RESTful service on the local network to facilitate quickly loading media from a computer. This has operated solely over http, which is probably sufficient for use on a trusted, private network, but with an upcoming update I am making it possible to directly connect to a known address rather than only what is visible via the Bonjour broadcasts. In this situation it makes more sense to err on the side of security and switch it over to use https instead.
The Problem
Since the loader is effectively a web server, it needs both the public and private keys to provide an SSL connection. While I plan on including a default pair of these so it works immediately (and thus obtainable by anyone – I do not feel security with this is important enough to require the extra step in forcing users to use their own keys), I also want to make it possible for users who do need or want the security to be able to achieve it.
The secondary goal I have is to not require an iOS-accepted CA to sign the SSL certificate someone chooses to use, making it possible to use a self-signed SSL certificate. This means that I need to do one of two things to ensure security: make the apps enforce certificate pinning or CA certificate pinning. The former restricts the app to connecting only to a single, specific SSL certificate while the latter lets to connect to any loader with a certificate signed by the CA. I chose the latter option to allow for maximum flexibility.
A Solution
Portfolio and Studio Pro both use AFNetworking (Alamofire is the Swift equivalent) for interacting with the loader. Both of these provide a fairly easy way to evaluate the SecPolicyRef
object provided by the system when checking for server trust.
Providing CA certificate pinning with AFNetworking means creating a custom subclass of AFSecurityPolicy
and overriding - (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain
{
SecCertificateRef caCertificate = /* implementation-specific */;
/* We need to build our own SecTrustRef since
the one provided by the system expects the
host name to match */
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustCertificates = [NSMutableArray array];
for (CFIndex index = 0; index < certificateCount; index++)
{
SecCertificateRef c = SecTrustGetCertificateAtIndex(serverTrust, index);
[trustCertificates addObject:(__bridge id) c];
}
/* SecPolicyCreateBasicX509 is the key to
skipping requiring a host name match */
SecPolicyRef policy = SecPolicyCreateBasicX509();
SecTrustRef testTrust = nil;
OSStatus testTrustResult = SecTrustCreateWithCertificates(trustCertificates, policy, &testTrust);
if (testTrustResult != errSecSuccess || testTrust == nil)
{
CFRelease(policy);
return NO;
}
/* Allow only certificates signed by our CA */
SecTrustSetAnchorCertificates(testTrust, @[(__bridge id) caCertificate]);
SecTrustSetAnchorCertificatesOnly(testTrust, true);
SecTrustResultType trustResult = kSecTrustResultInvalid;
SecTrustEvaluate(testTrust, &trustResult);
CFRelease(policy);
CFRelease(testTrust);
BOOL allow = (trustResult == kSecTrustResultUnspecified || trustResult == kSecTrustResultProceed);
return allow
}
Some error checking omitted for brevity
And since I needed it for an upcoming tvOS app, the equivalent in Alamofire is to override ServerTrustPolicyManager
:
import Alamofire
public class MyServerTrustPolicyManager: ServerTrustPolicyManager
{
/* The default version of this function requires
the host name to match, but we don't care about
that as long as the signing CA is what we expect */
override public func serverTrustPolicyForHost(host: String) -> ServerTrustPolicy?
{
return ServerTrustPolicy.CustomEvaluation({ (serverTrust, host) -> Bool in
return self.evaluateSSL(serverTrust)
})
}
public func evaluateSSL(trust: SecTrustRef!) -> Bool
{
let caCertificate = /* implementation-specific */
/* We need to build our own SecTrustRef since
the one provided by the system expects the
host name to match */
let certificateCount = SecTrustGetCertificateCount(trust)
var trustCertificates = [SecCertificateRef]()
for index in 0..<certificateCount
{
guard let c = SecTrustGetCertificateAtIndex(trust, index) else
{
return false
}
trustCertificates.append(c)
}
/* SecPolicyCreateBasicX509 is the key to
skipping requiring a host name match */
let policy = SecPolicyCreateBasicX509()
var testTrust: SecTrustRef? = nil
let testTrustResult = SecTrustCreateWithCertificates(trustCertificates, policy, &testTrust)
if (testTrustResult != errSecSuccess || testTrust == nil)
{
return false
}
/* Allow only certificates signed by our CA */
SecTrustSetAnchorCertificates(testTrust!, [caCertificate])
SecTrustSetAnchorCertificatesOnly(testTrust!, true)
var trustResult: SecTrustResultType = SecTrustResultType(kSecTrustResultInvalid)
SecTrustEvaluate(testTrust!, &trustResult)
let allow = (trustResult == SecTrustResultType(kSecTrustResultUnspecified) || trustResult == SecTrustResultType(kSecTrustResultProceed))
return allow
}
}
Some error checking omitted for brevity