Validating server certificates signed by own CA in Swift
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 theSecTrustSetAnchorCertificates
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.