Apple ships a patched version of OpenSSL with macOS. If no precautions are taken, their changes rob you of the power to choose your trusted CAs, and break the semantics of a callback that can be used for custom checks and verifications in client software.

Abstract

If OpenSSL’s certificate verification fails while connecting to a server, Apple’s code will intercept that error and attempt to verify the certificate chain itself with system trust settings from the keyring, potentially throwing away your verification results. Therefore:

You can’t limit your trust to certain CAs using SSL_CTX_load_verify_locations . This apparently isn’t news but doesn’t appear to be widely known.

Contrary to documentation, returning 0 from SSL_CTX_set_verify ’s callback does not make the TLS handshake fail. That makes the callback unsuitable for extra verification purposes (such as hostname verification). MITRE has assigned CVE-2014-2234 for this issue.

Apple was not interested in my bug report because they deprecated their OpenSSL years ago. Hence this summary together with work-arounds.

The Verify Callback

OpenSSL’s SSL_CTX_set_verify allows setting a callback function that is called for each certificate in the chain. It is invoked with the result of OpenSSL’s own verification of each certificate ( 1 for success, 0 for failure) and an x509_ctx object that can be used to get the certificate and – if applicable – verification error in question.

As for the return code of this callback, Apple’s own documentation says:

The return value of verify_callback controls the strategy of the further verification process. If verify_callback returns 0, the verification process is immediately stopped with “verification failed” state. If SSL_VERIFY_PEER is set, a verification failure alert is sent to the peer and the TLS/SSL handshake is terminated.

So technically the callback is useful for two things:

Gaining more information about a failure and reacting to it. Additional checks such as hostname verification or a more strict certificate validation. Returning 0 should abort any handshake.

But that’s not the case.

Instead Apple’s OpenSSL aborts the chain verification (i.e. the callback doesn’t get called any further on any remaining certificates in the chain) but does not fail the handshake if it decides that the certificate chain of the peer is trustworthy.

If you rely on the correct behavior and perform important checks within the callback, this behavior exposes you to man-in-the-middle attacks.

You can observe it with the following C code:

#include <errno.h> #include <netdb.h> #include <resolv.h> #include <stdio.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> #include <openssl/err.h> #include <openssl/ssl.h> int verify(int ok, X509_STORE_CTX *store) { // Always abort verification with an error. return 0; } int main(int argc, char *argv[]) { // Initialize OpenSSL SSL_library_init(); SSL_load_error_strings(); // Create a context SSL_CTX *ctx = SSL_CTX_new(TLSv1_client_method()); if (ctx == NULL) { ERR_print_errors_fp(stderr); abort(); } // Load trusted CAs from default paths. SSL_CTX_set_default_verify_paths(ctx); // Set verify function SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify); // Resolve struct addrinfo hints, *ai; memset(&hints, 0, sizeof hints); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; int ai_error = getaddrinfo("www.apple.com", "https", &hints, &ai); if(ai_error != 0) { fprintf(stderr, "getaddrinfo: %s

", gai_strerror(ai_error)); abort(); } // Connect int sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if(connect(sock, ai->ai_addr, ai->ai_addrlen) != 0) { close(sock); perror("connect"); abort(); } // Wrap connection with TLS SSL *ssl = SSL_new(ctx); if (ssl == NULL) { ERR_print_errors_fp(stderr); abort(); } SSL_set_fd(ssl, sock); if (SSL_connect(ssl) == -1) { ERR_print_errors_fp(stderr); } else { // Should NOT be reached with the verify function from above! printf("Connected with cipher %s

", SSL_get_cipher(ssl)); SSL_shutdown(ssl); } SSL_free(ssl); close(sock); SSL_CTX_free(ctx); return 0; }

Compile it using

$ cc ssl_client.c -lssl -lcrypto -o ssl_client

and run without arguments.

This program succeeds only when linked against Apple’s patched OpenSSL. Any other fails with an error message like:

SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed:s3_clnt.c:1166:

I have double-checked this back to a vintage 0.9.8e-fips-rhel5 on CentOS 5. It is definitely an Apple-only problem.

Background

The reason for this unexpected behavior is that Apple is trying to be helpful. Certificate validation and especially trust databases are a hassle and OpenSSL’s handling of them is rather user-hostile. So Apple patched a Trust Evaluation Agent (TEA) into their OpenSSL. It gives failed verifications a second chance using the system keyring as trust store.

To follow what happens, it is also necessary to understand that the TLS context that gets passed around carries an error code that is distinct from the return code of the verification callback mentioned before. This error code directly affects TEA’s behavior.

Now, if a client attempts a TLS handshake with a server:

TEA first calls OpenSSL’s original verification function. If OpenSSL’s verification fails (i.e. because OpenSSL can’t verify the certificate chain, or because the callback returns something else than 1 ), TEA checks the current context’s error code for one of the following constants: X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY ,

, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT ,

, and X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT . If one of them matches, TEA will attempt to fix the error by verifying the certificate chain itself. If it succeeds, the handshake succeeds too; no matter what happened within OpenSSL and your verification callback.

This is problematic for several reasons:

Unless you explicitly set your trusted root certificates, the initial verification will always fail with the infamous X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY error ( 20 ). As you can see in my example, even telling OpenSSL to use its default CA locations (line 36) doesn’t work because they don’t exist on OS X – they’re well hidden within the keyring. Therefore, unless you explicitly change the error code, TEA will always think it should try to fix the failed verification. So for instance if you discovered a hostname mismatch within the callback and thus return 0, TEA will still just try to verify the certificate chain and ignore your objection if the chain is trustworthy.

If you want to use one of the error codes above for your extra checks (e.g. for blacklisting certain CAs), Apple’s OpenSSL will ignore your failures. This can be a rather unexpected behavior since it works correctly on other platforms.

Any effort to use certificate pinning or limiting your trust to certain CAs is undermined by TEA re-validating the chain for you.

None of this is obvious or documented.

Solutions

My motivation is not to point fingers; I want to warn and offer solutions. So far I’ve come up with three:

Disable TEA

If TEA is disabled, it also can’t overrule your verification decision. You can achieve that by calling

X509_TEA_set_state(0);

as part of your application’s initialization.

However, as far as I can tell, that function is not public. At least I couldn’t find any definition or documentation outside of Apple’s source code of the patch.

Another approach is setting the OPENSSL_X509_TEA_DISABLE if you can’t or don’t want to change any code. You can try it with the example from above:

$ env OPENSSL_X509_TEA_DISABLE=1 ./ssl_client

The major drawback is that this approach makes Apple’s OpenSSL just another hopelessly outdated OpenSSL installation.

Explicitly Set An Error Code

This is probably the best approach if you have to use the verification callback with Apple’s OpenSSL. Always change the error within the context to something TEA doesn’t consider fixable. X509_V_ERR_APPLICATION_VERIFICATION would be an obvious choice:

int verify(int ok, X509_STORE_CTX *store) { X509_STORE_CTX_set_error(store, X509_V_ERR_APPLICATION_VERIFICATION); return 0; }

There are more to choose from though.

Compile Your Own OpenSSL

In their response to my bug report, Apple suggested to use a self-compiled OpenSSL (…until I’m ready to migrate to their SecureTransport). Generally that’s good advice because their OpenSSL is hopelessly out of date.

A practical way is homebrew which takes some pain out of compiling software and keeping it up to date:

$ brew install openssl

Similar alternatives like MacPorts work just as fine of course.

This approach has at least two drawbacks though:

The software Apple ships is compiled against their OpenSSL (Python, Ruby…). You also have to double-check that your self-compiled software really picks up your custom OpenSSL instead of the system one (for example --with-brewed-openssl within homebrew). Your OpenSSL has no access to the keyring and thus system trust store. Apple’s patches were there to help you with that after all. You’ll have to manage your own set of trusted CAs, probably with the help of Mozilla’s infamous cacert.pem . homebrew helps out a bit by cloning the system keyring into /usr/local/etc/openssl/osx_cert.pem on installation so SSL_CTX_set_default_verify_paths works out of the box. In any case, now you’ll have to cope with two trust stores: your OpensSSL’s one, and the system one. This makes everything a bit crude and adds moving parts.

Ultimately, this is the long-term way to go. Although OpenSSL is not the epitome of great software, it’s not going anywhere. Especially because numerous cross-platform software including but not limited to development platforms like Python, Ruby, or Node.js are all using OpenSSL for their TLS needs.

Impact

Together with Christian Heimes I’ve analyzed multiple high-profile open source projects with TLS support whether they are affected and require prior notice. None of them seemed to perform validations within the verification callback in an exploitable way. So I consider the publication safe.

That doesn’t mean that such software doesn’t exist though. According to the documentation putting verification code into the callback is perfectly legit (for example, Google uses this pattern within Chromium OS and so does PHP) so caution is in order. Most endangered is cross-platform software that assumes that OpenSSL code behaves identically across platforms.

I for one ran into this bug while testing a patch from a bug tracker that worked perfectly fine on Linux.

Q&A

Is this related to the MITM bug I’ve heard about on the news?

No.

The temporal proximity with the bug in SecureTransport that affects both macOS and iOS is coincidental.

I also don’t try to ride the attention wave provided by it: I filed a bug for this issue in Apple’s bug database a week before CVE-2014-1266/HT6147 was published.

Does this affect iOS?

No.

iOS doesn’t ship OpenSSL. This is a macOS–only issue.

Does this affect Apple’s software like Safari or Mail?

No.

Generally, Apple’s software – and most Mac desktop software for that matter – is using Apple’s SecureTransport and not OpenSSL.

Credits

I’d like to thank Laurens Van Houtven, Christian Heimes, Jean-Paul Calderone, Glyph, Alex Stapleton, Matthew Iversen, and Matthew Green for their assistance while researching and assessing this bug.