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
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
keyCompromiseand 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()