While adding some tests, I realized I've been overcomplicating this. Specifically, urlSession(session: didReceive challenge: completionHandler:) gets called for all certificate validation, even for certificates which are trusted (I hadn't tried with trusted certs before). Writing my own is not the right way to handle certificate validation for my application. If you don't set a custom delegate, the application will trust all the certificates Safari trusts.
If the certificate isn't trusted yet, you will get a series of errors like this:
Connection 1: default TLS Trust evaluation failed(-9807)
Connection 1: TLS Trust encountered error 3:-9807
Connection 1: encountered error(3:-9807)
Connection 1: unable to determine interface type without an established connection
Task <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1> HTTP load failed, 0/0 bytes (error code: -1202 [3:-9807])
Task <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1> finished with error [-1202] Error Domain=NSURLErrorDomain Code=-1202 "The certificate for this server is invalid. You might be connecting to a server that is pretending to be “<destination>” which could put your confidential information at risk." UserInfo={NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, \_kCFStreamErrorDomainKey=3, NSErrorPeerCertificateChainKey=(
"<cert(0x7f91cd0a5600) s: <cert name> i: <cert name>>"
), NSErrorClientCertificateStateKey=0, NSErrorFailingURLKey=https://<destination>/<path>, NSErrorFailingURLStringKey=https://<destination>/<path>, NSUnderlyingError=0x600003d7eb50 {Error Domain=kCFErrorDomainCFNetwork Code=-1202 "(null)" UserInfo={\_kCFStreamPropertySSLClientCertificateState=0, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x6000001e4510>, \_kCFNetworkCFStreamSSLErrorOriginalValue=-9807, \_kCFStreamErrorDomainKey=3, \_kCFStreamErrorCodeKey=-9807, kCFStreamPropertySSLPeerCertificates=(
"<cert(0x7f91cd0a5600) s: <cert name> i: <cert name>>"
)}}, _NSURLErrorRelatedURLSessionTaskErrorKey=(
"LocalDataTask <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1>"
), \_kCFStreamErrorCodeKey=-9807, \_NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <F4695366-AE0C-41D9-A3AA-CEA0682E6413>.<1>, NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x6000001e4510>, NSLocalizedDescription=The certificate for this server is invalid. You might be connecting to a server that is pretending to be “<destination>” which could put your confidential information at risk.} The error can then be handled separately. In my case, I'm initializing an object to represent the connection to this remote server, so if the connection doesn't work, I just throw the error back to the code trying to create this object. I use the error to create an NSAlert, which I show as a sheet with a button to cancel, and a button to trust the certificate. If the user picks the button to trust the certificate, the sheet completion handler then adds it to the keychain:
case .failure(let error as NSError):
switch (error.domain,error.code) {
case (NSURLErrorDomain,-1202):
let errorSheet = NSAlert(error: error)
errorSheet.addButton(withTitle: "Cancel Connection")
errorSheet.addButton(withTitle: "Trust this Certificate")
errorSheet.beginSheetModal(for: self.myWindow!)
{ (response:NSApplication.ModalResponse) -> Void in
switch response{
case .alertFirstButtonReturn:
break
case .alertSecondButtonReturn:
let serverCertificate = (error.userInfo["NSErrorPeerCertificateChainKey"] as! [SecCertificate])[0]
let serverCertItemDictionary = [
kSecClass:kSecClassCertificate,
kSecValueRef:serverCertificate,
kSecReturnRef:true,
kSecReturnAttributes:true
] as [CFString : Any]
let serverCertDictionaryCF = serverCertItemDictionary as CFDictionary
var secItemAddReturn:CFTypeRef?
let secItemAddError = SecItemAdd(serverCertDictionaryCF, &secItemAddReturn)
switch secItemAddError {
case noErr:
break
case errSecDuplicateItem:
print("This server's certificate is already in the Keychain.")
default:
let errorString = SecCopyErrorMessageString(secItemAddError,nil)
print("SecItemAdd error: \(String(describing: errorString))")
}
default:
break
}
}
default:
let errorSheet = NSAlert(error: error)
errorSheet.beginSheetModal(for: self.myWindow!)
}
Extending the switch statements to handle other errors is relatively easy.