Certificate revocation check with SecPolicyCreateRevocation/SecTrustEvaluateWithError does not work

When trying to check if a certificate has been revoked with SecPolicyCreateRevocation (Flags: kSecRevocationUseAnyAvailableMethod | kSecRevocationRequirePositiveResponse) and SecTrustEvaluateWithError I always get the result error code errSecIncompleteCertRevocationCheck, regardless if the certificate was revoked or not.

Reproduction: Execute the program from the attached Xcode project (See Feedback FB21224106).

Error output:

Error: Error Domain=NSOSStatusErrorDomain Code=-67635 ""revoked.badssl.com","E8","ISRG Root X1" certificates do not meet pinning requirements" UserInfo={NSLocalizedDescription="revoked.badssl.com","E8","ISRG Root X1" certificates do not meet pinning requirements, NSUnderlyingError=0x6000018d48a0 {Error Domain=NSOSStatusErrorDomain Code=-67635 "Certificate 0 “revoked.badssl.com” has errors: Failed to check revocation;" UserInfo={NSLocalizedDescription=Certificate 0 “revoked.badssl.com” has errors: Failed to check revocation;}}}

To me it looks like that the revocation check just fails („Failed to check revocation;“), no further information is provided by the returned error.

In the example the certificate chain of https://revoked.badssl.com (default code) and https://badssl.com is verified (to switch see comments in the code). I have a proxy configured in the system, I assume that the revocation check will use it.

On the same machine, the browsers (Safari and Google Chrome) can successfully detect if the certificate was revoked (revoked.badssl.com) or not (badssl.com) without further changes in the system/proxy settings. Note: The example leaks some memory, it’s just a test program.

Am I missing something?

Feedback: FB21224106

Answered by DTS Engineer in 868667022
This gels with my understanding of how things should … work

Actually, that understanding was wrong. It seems that things only end up on Apple’s ‘naughty’ list if they’re revoked for the right (well, wrong) reasons, specifically, for compromised keys [1].

So, to test this stuff you need to check a certificate that the system really thinks is revoked. A good example is this:

https://global-root-ca-revoked.chain-demos.digicert.com/

Using that I crafted a small test program that gets a server’s certificate chain and checks it for revocation. The code is at the end of this post. When I run this on macOS 15.7.1, I see this:

will run task, url: https://example.com
challenge NSURLAuthenticationMethodServerTrust
will check chain
did check chain, success: true
did run task, status: 200, bytes: 513

will run task, url: https://revoked.badssl.com
challenge NSURLAuthenticationMethodServerTrust
will check chain
did check chain, success: true
did run task, status: 200, bytes: 575

will run task, url: https://global-root-ca-revoked.chain-demos.digicert.com/
challenge NSURLAuthenticationMethodServerTrust
will check chain
did check chain, success: false
  details: Error Domain=NSOSStatusErrorDomain Code=-67820 …
did not run task, error: NSURLErrorDomain / -1202

This replicates what I see in Safari on the same Mac.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] There are lots of subtleties here:

  • Apple’s CRL aggregation policy can vary based on the specific CA.
  • For Let’s Encrypt, because we know it doesn’t support OCSP, we don’t do the secondary OCSP check.
  • A CRL has flags to indicate why a specific certificate was revoked. See keyCompromise and friends in RFC 5280. Apple’s CRL aggregation policy chooses whether or not to include a certificate based on those flags.

To quote Arthur Dent “Don’t ask me how it works or I’ll start to whimper.”

import Foundation

func check(chain: [SecCertificate]) {
    do {
        print("will check chain")
        let policy = SecPolicyCreateBasicX509()
        let trust = try secCall { SecTrustCreateWithCertificates(chain as NSArray, policy, $0) }
        var errorQ: CFError? = nil
        let success = SecTrustEvaluateWithError(trust, &errorQ)
        print("did check chain, success: \(success)")
        if !success {
            print("  details: \(errorQ!)")
        }
    } catch {
        print("did not check chain, error: \(error)")
    }
}

final class SessionDelegate: NSObject, URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        print("challenge \(challenge.protectionSpace.authenticationMethod)")
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            let trust = challenge.protectionSpace.serverTrust!
            let chain = SecTrustCopyCertificateChain(trust) as! [SecCertificate]
            check(chain: chain)
        }
        return (.performDefaultHandling, nil)
    }
}

func run(url: URL) async throws {
    do {
        print("will run task, url: \(url)")
        let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
        let delegate = SessionDelegate()
        let (data, response) = try await URLSession.shared.data(for: request, delegate: delegate)
        let httpResponse = response as! HTTPURLResponse
        print("did run task, status: \(httpResponse.statusCode), bytes: \(data.count)")
    } catch let error as NSError {
        print("did not run task, error: \(error.domain) / \(error.code)")
    }
    print()
}

func main() async throws {
    try await run(url: .init(string: "https://example.com")!)
    try await run(url: .init(string: "https://revoked.badssl.com")!)
    try await run(url: .init(string: "https://global-root-ca-revoked.chain-demos.digicert.com/")!)
}

try await main()
Am I missing something?

Other than that certificate revocation checking is a tangled mess? No )-:

I’ll likely have more to say about this in a bit — I’m just checking on some details internally — but I wanted to reply now and ask about your high-level goal here. Why are you doing explicit revocation checking? What behaviour in your product will change based on the result?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hello!

Thank you for your reply!

The high-level goal here is to implement certificate verification for the use of libcurl built with openSSL (CURLOPT_SSL_CTX_FUNCTION/SSL_CTX_set_cert_verify_callback) as we currently use that HTTP stack for all platforms of our software (macOS, Windows, Ubuntu) and it would be quite an effort to change that to use native HTTP APIs from the operating system, and for security reasons revocation check should also be done here like the browsers do.

Since libcurl version 8.15 the curl-built-in support for "Secure Transport" was removed, as it is deprecated and does not support TLS 1.3, therefore we want to use openSSL as the SSL backend instead, which lacks keychain access.

See:

https://github.com/curl/curl/pull/16677

https://curl.se/mail/lib-2025-08/0048.html

I've just made a quick and dirty sample for reproduction purposes ;-)

Excellent...

revocation check should also be done here like the browsers do.

OK. I’m going to factor that into my research. Thanks.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

So… there’s a lot to unpack here.

The reason why kSecRevocationRequirePositiveResponse is causing you grief is that:

  • The server’s certificate was issued by Let’s Encrypt.
  • Let’s Encrypt has stopped supporting OCSP. You can learn about that directly from them in “Ending OCSP Support in 2025” [1].
  • Apple client’s don’t support CRLs directly.

Apple clients learn about revocation via a separate path. My understanding is that we don’t document this path in detail. We did talk about it in WWDC 2017 Session 701 Your Apps and Evolving Network Security Standards [2], and I encourage you to watch that (start around 17:40). There have also been third-party folks who’ve dug into this in more detail [3] [4]. Of course, from Apple’s perspective all of these are implementation details, and we encourage folks to rely on APIs instead.

Now, the issue here is that the APIs aren’t doing what you want. Before I go there, however, I want to clarify this bit:

On the same machine, the browsers (Safari …) can successfully detect if the certificate was revoked (revoked.badssl.com)

How are you actually testing that? I tried it here in my office and I didn’t see a failure. Specifically:

  1. Using a ‘fresh’ macOS 26.1, I launched Safari.
  2. I entered badssl.com into the address bar and pressed Return. That page loaded, as expected.
  3. I clicked on the “revoked” link, leading revoked.badssl.com. That page loaded, with Safari showing the big red webpage that was returned by the server.

This gels with my understanding of how things should [5] work (as explained in the WWDC presentation, the client does a final check, which doesn’t work because Let’s Encrypt doesn’t support OCSP) but it doesn’t match what you’re reporting. I’d like to resolve that discrepancy being going further.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] https://letsencrypt.org/2024/12/05/ending-ocsp

[2] This is no longer available from Apple but the following URL may help you track down a copy.

https://developer.apple.com/videos/play/wwdc2017/701/

[3] https://www.ssl.com/blogs/how-do-browsers-handle-revoked-ssl-tls-certificates/#ftoc-heading-4

[4] https://github.com/crtsh/certwatch_db/issues/52

[5] Well, for some definition of the word should |-:

This gels with my understanding of how things should … work

Actually, that understanding was wrong. It seems that things only end up on Apple’s ‘naughty’ list if they’re revoked for the right (well, wrong) reasons, specifically, for compromised keys [1].

So, to test this stuff you need to check a certificate that the system really thinks is revoked. A good example is this:

https://global-root-ca-revoked.chain-demos.digicert.com/

Using that I crafted a small test program that gets a server’s certificate chain and checks it for revocation. The code is at the end of this post. When I run this on macOS 15.7.1, I see this:

will run task, url: https://example.com
challenge NSURLAuthenticationMethodServerTrust
will check chain
did check chain, success: true
did run task, status: 200, bytes: 513

will run task, url: https://revoked.badssl.com
challenge NSURLAuthenticationMethodServerTrust
will check chain
did check chain, success: true
did run task, status: 200, bytes: 575

will run task, url: https://global-root-ca-revoked.chain-demos.digicert.com/
challenge NSURLAuthenticationMethodServerTrust
will check chain
did check chain, success: false
  details: Error Domain=NSOSStatusErrorDomain Code=-67820 …
did not run task, error: NSURLErrorDomain / -1202

This replicates what I see in Safari on the same Mac.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] There are lots of subtleties here:

  • Apple’s CRL aggregation policy can vary based on the specific CA.
  • For Let’s Encrypt, because we know it doesn’t support OCSP, we don’t do the secondary OCSP check.
  • A CRL has flags to indicate why a specific certificate was revoked. See keyCompromise and friends in RFC 5280. Apple’s CRL aggregation policy chooses whether or not to include a certificate based on those flags.

To quote Arthur Dent “Don’t ask me how it works or I’ll start to whimper.”

import Foundation

func check(chain: [SecCertificate]) {
    do {
        print("will check chain")
        let policy = SecPolicyCreateBasicX509()
        let trust = try secCall { SecTrustCreateWithCertificates(chain as NSArray, policy, $0) }
        var errorQ: CFError? = nil
        let success = SecTrustEvaluateWithError(trust, &errorQ)
        print("did check chain, success: \(success)")
        if !success {
            print("  details: \(errorQ!)")
        }
    } catch {
        print("did not check chain, error: \(error)")
    }
}

final class SessionDelegate: NSObject, URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        print("challenge \(challenge.protectionSpace.authenticationMethod)")
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            let trust = challenge.protectionSpace.serverTrust!
            let chain = SecTrustCopyCertificateChain(trust) as! [SecCertificate]
            check(chain: chain)
        }
        return (.performDefaultHandling, nil)
    }
}

func run(url: URL) async throws {
    do {
        print("will run task, url: \(url)")
        let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
        let delegate = SessionDelegate()
        let (data, response) = try await URLSession.shared.data(for: request, delegate: delegate)
        let httpResponse = response as! HTTPURLResponse
        print("did run task, status: \(httpResponse.statusCode), bytes: \(data.count)")
    } catch let error as NSError {
        print("did not run task, error: \(error.domain) / \(error.code)")
    }
    print()
}

func main() async throws {
    try await run(url: .init(string: "https://example.com")!)
    try await run(url: .init(string: "https://revoked.badssl.com")!)
    try await run(url: .init(string: "https://global-root-ca-revoked.chain-demos.digicert.com/")!)
}

try await main()
Certificate revocation check with SecPolicyCreateRevocation/SecTrustEvaluateWithError does not work
 
 
Q