What iOS versions support OCSP stapling using UIWebView and WKWebView?

I would like to be able to reject websites to be shown in the UIWebView/WKWebView in my app if the website presents a revoked certificate. I have made a test that shows that out-of-the-box, all versions of iOS from 7.0 to 10.2, both UIWebView/WKWebView allows showing websites over HTTPS having a revoked certificate.


During a WWDC 2016 talk I heard that OCSP Stapling is required, which is just what I want (Certificate Transparency and OCSP Stapling: https://developer.apple.com/videos/play/wwdc2016/706/?time=564). But it's not clear if this is only for iOS 10 nor is it clear if it enabled by default.


In addition to my main question "What iOS versions support OCSP stapling using UIWebView and WKWebView?", I would also like to know how to implement or activate the OCSP support.



Allow me to present some pointers to information that partly answers my question, but not fully:


-"Enforcing Stricter Server Trust Evaluation" from Apples Technical Note TN2232. Specifically it mentions "SecPolicyCreateRevocation lets you create a security policy that specifically checks for certificate revocation (for example, via OCSP or a CRL)" from: https://developer.apple.com/library/content/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECSTRICTER.


-Notice the callback "URLSession:didReceiveChallenge:completionHandler:". The callback is relevant "When a session first establishes a connection to a remote server that uses SSL or TLS, to allow your app to verify the server’s certificate chain". Available from iOS 7.0+. See https://developer.apple.com/reference/foundation/nsurlsessiondelegate/1409308-urlsession?language=objc.


-"UIWebView does not provide any way for an app to customize its HTTPS server trust evaluations (r. 10131336) . You can work around this limitation using an NSURLProtocol subclass, as illustrated by Sample Code 'CustomHTTPProtocol'" from https://developer.apple.com/library/content/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECUIWEBVIEW.


-"SecTrustEvaluate may need to access the network in order to complete its job ... when determining whether a certificate has been revoked (via a OCSP or CRL)" from https://developer.apple.com/library/content/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECHINTSANDTIPS.


-Other posts in Apple Developer Forums like "Re: Evaluation of certificates revocation (CRL/OCSP)" and "Unable to use CRL method for revocation checking"

Accepted Reply

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)
    }
}

Replies

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)
    }
}