Validating server certificates signed by own CA in Swift
Feb 6, 2015
2 minutes read

In one of my recent apps I wanted to download data from an internal server, which had a certificate signed by an in-house certificate authority. To avoid hard-coding fingerprints in the application, I would rather validate the certificate to ensure that it was signed by any trusted CA, including my own.

To download files I have used NSUrlConnection asynchronously, thus my class needs to implement the protocol NSURLConnectionDataDelegate. The function which handles server certificate validation (together with for example HTTP Basic authentication) is connection:willSendRequestForAuthenticationChallenge.

Thus, all the code should go into this function:

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {
    ...
}

Allowing a single CA

We first add our DER-formatted certificate authority to the XCode project. We can then extract the path to it using:

let rootCa = "rootca"
let rootCaPath = NSBundle.mainBundle().pathForResource(rootCa, ofType: "der")

After this we read the data from the path into an NSData object.

let rootCaData = NSData(contentsOfFile: rootCaPath)

And finally we create a certificate out of it (in unmanaged memory) using SecCertificateCreateWithData

let rootCert = SecCertificateCreateWithData(nil, rootCaData).takeRetainedValue()

Finally we are ready to set the list of allowed certificates. If we also want to allow certificates signed by the regular CA list provided by iOS, we need the call to SecTrustSetAnchorCertificatesOnly, otherwise only our own CA would be trusted.

SecTrustSetAnchorCertificates(trust, [rootCert])
SecTrustSetAnchorCertificatesOnly(trust, 0) // also allow regular CAs.

The complete code, with if let for handling nil is below, which also includes the evaluation step using SecTrustEvaluate

func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {
  if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
    // First load our extra root-CAs to be trusted from the app bundle.
    let trust = challenge.protectionSpace.serverTrust
        
    let rootCa = "eitroot"
    if let rootCaPath = NSBundle.mainBundle().pathForResource(rootCa, ofType: "der") {
      if let rootCaData = NSData(contentsOfFile: rootCaPath) {
        let rootCert = SecCertificateCreateWithData(nil, rootCaData).takeRetainedValue()
        SecTrustSetAnchorCertificates(trust, [rootCert])
        SecTrustSetAnchorCertificatesOnly(trust, 0) // also allow regular CAs.
      }
    }
      
    var trustResult: SecTrustResultType = 0
    SecTrustEvaluate(trust, &trustResult)
        
    if (Int(trustResult) == kSecTrustResultUnspecified ||
      Int(trustResult) == kSecTrustResultProceed) {
        // Trust certificate.
        let credential = NSURLCredential(forTrust: challenge.protectionSpace.serverTrust)
        challenge.sender.useCredential(credential, forAuthenticationChallenge: challenge)
    } else {
      NSLog("Invalid server certificate.")
      challenge.sender.cancelAuthenticationChallenge(challenge)
    }  
  } else {
    NSLog("Got unexpected authentication method \(challenge.protectionSpace.authenticationMethod)");
    challenge.sender.cancelAuthenticationChallenge(challenge)
  }
}

Trusting multiple CA certificates

Since the SecTrustSetAnchorCertificates takes an array as input, this can be easily achieved with some Swift-functional-magic as below:

// ... [code as in single CA case] ...
// First load our extra root-CAs to be trusted from the app bundle.
let trust = challenge.protectionSpace.serverTrust
				
let rootCaFiles = ["zozsseca", "eitroot"]
let rootCaCerts = rootCaFiles.map() {
	(file: String) -> NSData? in
	let path = NSBundle.mainBundle().pathForResource(file, ofType: "der")
	return path != nil ? NSData(contentsOfFile: path!) : nil
}.filter({$0 != nil}).map() {
	SecCertificateCreateWithData(nil, $0!).takeRetainedValue()
}
				
// rootCaCerts now is an array of all (existent) root CAs. Tell iOS to trust them.
SecTrustSetAnchorCertificates(trust, rootCaCerts)
SecTrustSetAnchorCertificatesOnly(trust, 0) // Also allow default root-cert list.

var trustResult: SecTrustResultType = 0
SecTrustEvaluate(trust, &trustResult)
// ... [code as in single CA case] ...

If any of the root CA files could not be found, it will simply be ignored, and because of SecTrustSetAnchorCertificatesOnly the worst-case scenario of no readable custom root CAs, the default system list will be used.


Back to posts


comments powered by Disqus