SecTrust on macOS: Certificate Policies, Evaluation, and Failure Analysis
Evaluate macOS certificate trust with SecTrust policies, asynchronous checks, meaningful diagnostics, and narrowly scoped custom anchors.
SecTrust evaluates whether a certificate chain is trusted for a particular use. It is not a universal “certificate is good” Boolean independent of context. The evaluation combines the supplied certificates, trust anchors, validity dates, policy, and system trust configuration. A certificate valid for TLS server authentication is not automatically suitable for code signing or client authentication. On macOS, applications that implement custom certificate handling must be particularly careful not to weaken the system’s policy merely to make one connection succeed.
Use Security.framework’s trust APIs when you have a concrete reason to evaluate certificates directly, such as verifying a signed artifact or implementing a network authentication delegate. For ordinary HTTPS, prefer URLSession or Network.framework’s normal TLS integration so the platform handles certificate validation as part of the transport. A parallel custom trust decision can disagree with the real connection and create a false sense of security.
Construct a trust object for a stated purpose
Start with the certificate being evaluated and an explicit policy. The SSL policy is appropriate to server identity validation; basic X.509 validation answers a different question. If evaluating a certificate chain provided by a peer, supply available intermediates when possible and let the system locate missing intermediates according to the trust configuration. Do not add a certificate as an anchor simply because an intermediate is missing; that changes who can vouch for the chain.
An evaluation should record the intended purpose in the code path. Make the host name or other policy input part of the trust policy when validating a TLS server, and do not reuse a permissive trust object for unrelated operations. For a signed document, use the policy appropriate to the signature format and verify the signing time and revocation requirements the product actually promises.
The basic Swift shape is to create a SecTrust from certificates and a policy, then evaluate it away from the UI thread:
import Security
import Foundation
func evaluateServerTrust(_ certificate: SecCertificate) throws -> Bool {
let hostname = CFStringCreateWithCString(
nil, "api.example.com", CFStringBuiltInEncodings.UTF8.rawValue
)!
let policy = SecPolicyCreateSSL(true, hostname)
var trust: SecTrust?
let status = SecTrustCreateWithCertificates(certificate, policy, &trust)
guard status == errSecSuccess, let trust else {
throw TrustError.creationFailed(status)
}
return SecTrustEvaluateWithError(trust, nil)
}
enum TrustError: Error {
case creationFailed(OSStatus)
}
For a production TLS client, connect this policy reasoning to the actual transport’s authentication challenge rather than evaluating an unrelated copy and then accepting the connection anyway. If the evaluation can fetch intermediates or perform revocation work, it may involve network activity. Do not call a potentially blocking synchronous evaluation on AppKit’s main run loop.
Prefer asynchronous evaluation and preserve the error
SecTrustEvaluateWithError returns a pass/fail result and can provide a CFError explaining a failure. SecTrustEvaluateAsyncWithError lets the system evaluate on a dispatch queue and call back with the result. Preserve the error domain and code in diagnostics, but do not expose raw certificate subjects or internal service names in user-facing telemetry without a privacy reason.
A failure is not one generic “bad certificate” state. The chain may be incomplete, expired, not yet valid, revoked, anchored in an untrusted issuer, or incompatible with the requested policy. Some issues are recoverable in a specific workflow; revoked or identity-mismatched certificates should not be overridden. Distinguish trust evaluation from connection failure caused by DNS, network path, TLS protocol negotiation, or server availability.
When writing code that handles trust errors, avoid converting every failure into a prompt offering “trust anyway.” That teaches users to approve unknown certificate chains and can transform a secure connection into an attacker-controlled one. If a managed enterprise uses a private certificate authority, deploy that trust anchor through an approved configuration mechanism and test the correct trust domain instead of shipping a blanket bypass.
Custom anchors are a scoped trust change
SecTrustSetAnchorCertificates modifies the anchor set for the specific trust object. Apple’s API notes that setting a list of custom anchors can cause only those anchors to be considered; use the corresponding anchor-only setting deliberately if the built-in system anchors must remain enabled. Do not confuse adding an anchor for one verification with installing a system-wide root certificate. The former is a local evaluation configuration; the latter changes trust for other applications and users.
If you bundle a private anchor, protect its provenance and update path. A malicious or compromised root can authorize arbitrary names within the applicable policy. Pinning a leaf certificate is brittle across routine certificate renewal; pinning a public key or constrained CA may be more durable but still requires rotation and emergency recovery design. Never embed a private key in the application. Document how an anchor change is distributed, revoked, and audited.
For historical signature verification, changing the evaluation date may be appropriate when the product is verifying whether a signature was valid at signing time. It is not an acceptable way to make a currently expired TLS endpoint appear valid. Keep time semantics explicit and obtain the signing timestamp from a trusted source appropriate to the signature format.
Keep custom verification out of transport gaps
If a client uses URLSession’s authentication challenge delegate, the delegate must make a decision for the exact protection space and authentication method it receives. Do not accept a server-trust challenge because a previous request to a different host passed. Compare the challenge host to the expected endpoint, use the supplied trust object and platform policy, and reject unsupported challenge types. Avoid asynchronous callback races that complete the challenge more than once or never complete it.
Similarly, when using Network.framework TLS options, make trust configuration part of the connection parameters before starting the connection. Check state updates and failure codes; a trust callback is not a substitute for application-level authentication after the secure channel is established. TLS proves properties about the peer identity and encrypted channel under the chosen policy. The application still needs to authenticate the account, request, and authorization context.
Do not implement certificate parsing with ad hoc string comparisons against the subject common name. Hostname verification, SAN handling, intermediate chain construction, constraints, key usage, policy OIDs, and time checks are all structured certificate rules. Security.framework exists to perform those evaluations consistently. If a specific organizational policy requires an additional certificate extension or issuer constraint, layer that check on top of a successful system trust evaluation and fail closed when the required field is absent.
Investigate a failure without weakening it
When trust fails, capture the target host or artifact identity, requested policy, leaf certificate fingerprint, evaluation error code, and time. Use SecTrustGetTrustResult only after evaluation when the workflow needs the finer result category; the Boolean API and its error should remain the main decision. Compare the certificate chain presented by the peer with the chain seen by a known-good client, and verify the device clock and managed trust configuration before changing application behavior.
For TLS, test the same host from a clean account and on a known network, then compare the server’s configured chain and certificate dates. A network security appliance that substitutes certificates may reveal a managed proxy or inspection policy, not an application bug. Escalate with the certificate fingerprint and policy details rather than asking users to disable certificate checks.
For signed artifacts, verify the file’s signature independently and establish whether the certificate was valid at signing time. If a private anchor is expected, confirm its exact fingerprint and deployment source. Never fix a failed signature by accepting any self-signed certificate or by disabling Gatekeeper. Trust failures are valuable evidence that the artifact or endpoint does not match the configured security contract.
Test success, failure, and rotation paths
Build a test matrix with a valid chain, expired leaf, expired intermediate, missing intermediate, hostname mismatch, untrusted root, revoked fixture where available, and a private anchor. Include system clock changes only in an isolated test environment. Verify that the UI remains responsive while trust is evaluated, that cancellation is respected, and that errors are surfaced without credentials or document data.
Exercise certificate renewal and key rotation before production. Verify the trust policy survives a new leaf certificate under the intended issuer, and prove that a removed or compromised anchor no longer validates. Test network interruption during intermediate retrieval and any documented revocation behavior. Record supported macOS versions because trust-store contents and API availability can change over time.
The result should be a small trust decision with clear provenance. Select a policy matching the use, rely on the system’s chain evaluator, treat recoverable exceptions as explicit and temporary, and keep user data out of diagnostics. SecTrust is powerful because it centralizes complicated certificate rules; using it safely means resisting the temptation to turn a failed check into an unconditional success.
Related:
- Code Signing, Notarization, and Gatekeeper on macOS
- Secure Enclave Keys on macOS: Keychain Access Control Without Exportable Private Material
Sources: