I have found the answers I needed for now. My real need was to disallow loading sites that presented a revoked certificate, and OCSP Stapling is not the only way to do this and hence, in retrospect, the title of the question was focusing too much on OCSP.
Working with DTS I've found the following answers:
In order to prevent UIWebView/WKWebView loading a website, if it has a revoked certificate, there are two steps to carry out:
A. Enhanced HTTPS server trust evaluation
B. Determining if a certificate has been revoked
With regards A, options for UIWebView are not good. UIWebView does not provide any hooks for customising HTTPS server trust evaluation. It’s possible to do this using a custom NSURLProtocol but it's recommended to avoid that where possible.
The situation with WKWebView is much better. In WKWebView you can override HTTPS server trust evaluation via the `-webView:didReceiveAuthenticationChallenge:completionHandler:` navigation delegate method. This works in iOS 9 or later (iOS 8 does not pass the `NSURLAuthenticationMethodServerTrust` authentication challenge to that delegate).
With regards to B, you can explicitly check for certificate revocation via a revocation policy object (created with `SecPolicyCreateRevocation`). To force a check you have to set the `kSecRevocationRequirePositiveResponse` flag.
IMPORTANT NOTE: This flag did not work as expected prior to iOS 9.
My conclusion: Customising NSURLProtocol will make me able to support UIWebView, by getting a chance to do the certificate revocation check at right time during the TLS handshake, like I can do, out of the box, with WKWebView. But it requires a significant amount of work do to. And I will be facing the problem, that setting the `kSecRevocationRequirePositiveResponse` flag did not work as expected, prior to iOS 9. The could be solved by implementing my own certificate revocation check, which also seems to require a significant amount of work. Further more, for WKWebView, when using the out of the box delegate, it will only work on iOS 9 and forward, but I guess the WKWebView could also reap the benefits of the custom NSURLProtocol with custom certificate revocation check if I built it for UIWebView.
Here's sample code directly from a test sandbox project where certificate revocation check is working on iOS 9 using WKWebView:
import WebKit
func SecTrustGetPolicies(_ trust: SecTrust) -> [SecPolicy]? {
var policiesOpt: CFArray? = nil
guard SecTrustCopyPolicies(trust, &policiesOpt) == errSecSuccess else {
return nil
}
return (policiesOpt! as! [SecPolicy])
}
func SecTrustGetCertificates(_ trust: SecTrust) -> [SecCertificate]? {
var result = [SecCertificate]()
for i in 0..<SecTrustGetCertificateCount(trust) {
guard let cert = SecTrustGetCertificateAtIndex(trust, i) else {
return nil
}
result.append(cert)
}
return result
}
func SecTrustCopyWithExtraPolicy(_ trust: SecTrust, _ extraPolicy: SecPolicy) -> SecTrust? {
guard let originalPolicies = SecTrustGetPolicies(trust) else {
return nil
}
guard let originalCertificates = SecTrustGetCertificates(trust) else {
return nil
}
let policies = originalPolicies + [extraPolicy]
var trustOpt: SecTrust? = nil
guard SecTrustCreateWithCertificates(originalCertificates as NSArray, policies as NSArray, &trustOpt) == errSecSuccess else {
return nil
}
return trustOpt!
}
class WKWebViewController : UIViewController, WKNavigationDelegate {
var webView: WKWebView { return self.view as! WKWebView }
override func loadView() {
let config = WKWebViewConfiguration()
let webView = WKWebView(frame: CGRect.zero, configuration: config)
webView.navigationDelegate = self
self.view = webView
}
@IBAction func rootAction(_ sender: AnyObject) {
//HTML with links to good and bad HTTPS sites
self.webView.loadHTMLString(Sites.sitesHTML, baseURL: nil)
}
func isRevoked(trust originalTrust: SecTrust) -> Bool {
// Create a new trust object from the original trust object, adding the revocation trust policy.
guard let revocationPolicy = SecPolicyCreateRevocation(kSecRevocationUseAnyAvailableMethod | kSecRevocationRequirePositiveResponse) else {
return true
}
guard let trust = SecTrustCopyWithExtraPolicy(originalTrust, revocationPolicy) else {
return true
}
// Enable network operations.
var networkFetchAllowed: DarwinBoolean = false
guard SecTrustGetNetworkFetchAllowed(trust, &networkFetchAllowed) == errSecSuccess else {
return true
}
if !networkFetchAllowed.boolValue {
guard SecTrustSetNetworkFetchAllowed(trust, true) == errSecSuccess else {
return true
}
}
// Do the trust evaluation.
var trustResult = SecTrustResultType.invalid
guard SecTrustEvaluate(trust, &trustResult) == errSecSuccess else {
return true
}
guard [.proceed, .unspecified].contains(trustResult) else {
return true
}
return false
}
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
NSLog("did receive authentication challenge %@", challenge.protectionSpace.authenticationMethod)
switch challenge.protectionSpace.authenticationMethod {
case NSURLAuthenticationMethodServerTrust:
let trust = challenge.protectionSpace.serverTrust!
if self.isRevoked(trust: trust) {
completionHandler(.cancelAuthenticationChallenge, nil)
} else {
completionHandler(.performDefaultHandling, nil)
}
default:
completionHandler(.performDefaultHandling, nil)
}
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
NSLog("did fail provisional navigation %@", error as NSError)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
NSLog("did fail navigation %@", error as NSError)
}
}