Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCORE-2196 Use the Windows trusted root certificates for TLS #7882

Merged
merged 7 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# NEXT RELEASE

### Enhancements
* <New feature description> (PR [#????](https://github.com/realm/realm-core/pull/????))
* None.
* On Windows devices Device Sync will additionally look up SSL certificates in the Windows Trusted Root Certification Authorities certificate store when establishing a connection. (PR [#7882](https://github.com/realm/realm-core/pull/7882))

### Fixed
* When a public name is defined on a property, calling `realm::Results::sort()` or `realm::Results::distinct()` with the internal name could throw an error like `Cannot sort on key path 'NAME': property 'PersonObject.NAME' does not exist`. ([realm/realm-js#6779](https://github.com/realm/realm-js/issues/6779), since v12.12.0)
Expand Down
10 changes: 4 additions & 6 deletions src/realm/sync/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,11 @@ elseif(REALM_HAVE_OPENSSL)
target_link_libraries(Sync PUBLIC OpenSSL::SSL)
endif()

if(WIN32 AND NOT WINDOWS_STORE)
target_link_libraries(Sync INTERFACE Version.lib)
if(CMAKE_VERSION VERSION_LESS "3.21")
# This is needed for OpenSSL, but CMake's FindOpenSSL didn't declare it
# on the OpenSSL::Crypto target until CMake 3.21.0.
target_link_libraries(Sync INTERFACE Crypt32.lib)
if(WIN32)
if(NOT WINDOWS_STORE)
target_link_libraries(Sync INTERFACE Version.lib)
endif()
target_link_libraries(Sync INTERFACE Crypt32.lib)
endif()

install(TARGETS Sync EXPORT realm
Expand Down
125 changes: 123 additions & 2 deletions src/realm/sync/network/network_ssl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
#include <realm/sync/network/network_ssl.hpp>

#if REALM_HAVE_OPENSSL
#include <openssl/conf.h>
#include <openssl/x509v3.h>
#ifdef _WIN32
using osslX509_NAME = X509_NAME; // alias this before including wincrypt.h because it gets clobbered
#include <Windows.h>
#include <wincrypt.h>
#else
#include <pthread.h>
#endif
#include <openssl/conf.h>
#include <openssl/x509v3.h>
#elif REALM_HAVE_SECURE_TRANSPORT
#include <fstream>
#include <vector>
Expand Down Expand Up @@ -64,6 +66,122 @@ void populate_cert_store_with_included_certs(X509_STORE* store, std::error_code&

#endif // REALM_INCLUDE_CERTS

#if REALM_HAVE_OPENSSL && _WIN32

/// Allow OpenSSL to look up certificates in the Windows Trusted Root Certification Authority list by implementing the
/// X509_LOOKUP interface.
class CapiLookup {
public:
CapiLookup()
{
// Try to open the store in all of these locations sequentially. Many of them might not exist, and the
// CERT_STORE_OPEN_EXISTING_FLAG flag
// will cause CertOpenStore to return null in which case we just move on to the next.
// The order is important - we go from the most likely to the least likely to optimize lookup.
static std::initializer_list<DWORD> store_locations{CERT_SYSTEM_STORE_CURRENT_USER,
CERT_SYSTEM_STORE_LOCAL_MACHINE,
CERT_SYSTEM_STORE_CURRENT_SERVICE,
CERT_SYSTEM_STORE_SERVICES,
CERT_SYSTEM_STORE_USERS,
CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY,
CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY,
CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE};

for (DWORD location : store_locations) {
constexpr DWORD flags =
CERT_STORE_READONLY_FLAG | CERT_STORE_SHARE_CONTEXT_FLAG | CERT_STORE_OPEN_EXISTING_FLAG;
HCERTSTORE store = CertOpenStore(CERT_STORE_PROV_SYSTEM_W, 0, NULL, flags | location, L"ROOT");
if (store)
m_stores.push_back(store);
}
}

~CapiLookup()
{
for (auto store : m_stores) {
CertCloseStore(store, 0);
}
}

int get_by_subject(X509_LOOKUP* ctx, X509_LOOKUP_TYPE type, const osslX509_NAME* name, X509_OBJECT* ret)
{
if (type != X509_LU_X509)
return 0;

// Convert the OpenSSL X509_NAME structure into its ASN.1 representation and construct a CAPI search parameter
CERT_NAME_BLOB capi_name = {0};
capi_name.cbData = i2d_X509_NAME(name, &capi_name.pbData);
int result = 0;

for (auto store : m_stores) {
PCCERT_CONTEXT cert =
CertFindCertificateInStore(store, X509_ASN_ENCODING, 0, CERT_FIND_SUBJECT_NAME, &capi_name, NULL);
if (!cert)
continue;

// Convert the ASN.1 representation of the CAPI certificate into an OpenSSL certificate and add it to the
// OpenSSL store
const unsigned char* encoded_cert_data = cert->pbCertEncoded;
X509* ossl_cert = d2i_X509(NULL, &encoded_cert_data, cert->cbCertEncoded);
result = X509_STORE_add_cert(X509_LOOKUP_get_store(ctx), ossl_cert);
X509_free(ossl_cert);
break;
}

OPENSSL_free(capi_name.pbData);

// if we previously added a certificate to the store we need to look it up again from the store and return
// that
if (result)
result = get_cached_object(ctx, type, name, ret);
return result;
}

private:
int get_cached_object(X509_LOOKUP* ctx, X509_LOOKUP_TYPE type, const osslX509_NAME* name, X509_OBJECT* ret)
{
REALM_ASSERT_RELEASE(type == X509_LU_X509);

// Loop through the objects already in the store to find the one we just added in get_by_subject.
// retrieve_by_subject returns a cert with refcount 1 but set1_X509 increases it.
// That's why we need to free it after before returning, otherwise it will leak.

STACK_OF(X509_OBJECT)* objects = X509_STORE_get0_objects(X509_LOOKUP_get_store(ctx));
X509_OBJECT* tmp = X509_OBJECT_retrieve_by_subject(objects, type, name);
if (!tmp)
return 0;

X509* cert = X509_OBJECT_get0_X509(tmp);
int result = X509_OBJECT_set1_X509(ret, cert);
X509_free(cert);

return result;
}

std::vector<HCERTSTORE> m_stores;
};

void add_windows_certificate_store_lookup(X509_STORE* store)
{
X509_LOOKUP_METHOD* capi_lookup = X509_LOOKUP_meth_new("capi");

X509_LOOKUP_meth_set_new_item(capi_lookup, [](X509_LOOKUP* ctx) {
auto* data = new CapiLookup();
return X509_LOOKUP_set_method_data(ctx, data);
});
X509_LOOKUP_meth_set_free(capi_lookup, [](X509_LOOKUP* ctx) {
auto* data = reinterpret_cast<CapiLookup*>(X509_LOOKUP_get_method_data(ctx));
delete data;
});
X509_LOOKUP_meth_set_get_by_subject(capi_lookup, [](auto ctx, auto type, auto name, auto ret) {
auto* data = reinterpret_cast<CapiLookup*>(X509_LOOKUP_get_method_data(ctx));
return data->get_by_subject(ctx, type, name, ret);
});

X509_STORE_add_lookup(store, capi_lookup);
}

#endif // REALM_HAVE_OPENSSL && _WIN32

#if REALM_HAVE_OPENSSL && (OPENSSL_VERSION_NUMBER < 0x10100000L || defined(LIBRESSL_VERSION_NUMBER))

Expand Down Expand Up @@ -330,6 +448,9 @@ void Context::ssl_use_default_verify(std::error_code& ec)
ec = std::error_code(int(ERR_get_error()), openssl_error_category);
return;
}
#endif
#ifdef _WIN32
add_windows_certificate_store_lookup(SSL_CTX_get_cert_store(m_ssl_ctx));
#endif
ec = std::error_code();
}
Expand Down
2 changes: 2 additions & 0 deletions src/realm/sync/network/network_ssl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ class Context {
/// default certificates for server verification. For OpenSSL,
/// use_default_verify() corresponds to
/// SSL_CTX_set_default_verify_paths(SSL_CTX*);
///
/// On Windows this also adds a lookup to the system Trusted Root Certification Authorities list.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to know if it failed to find certs for whatever reason so that you could fall back to m_ssl_context.use_included_certificate_roots();?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use_ methods are not mutually-exclusive. You can call as many as you like while you’re setting up your context.
Besides, looking up certificates happens during the SSL handshake, way after you’ve set up the context.

void use_default_verify();

/// The verify file is a PEM file containing trust certificates that the
Expand Down
73 changes: 73 additions & 0 deletions test/test_util_network_ssl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
#include "test.hpp"
#include "util/semaphore.hpp"

#ifdef _WIN32
#include <wincrypt.h>
#endif

using namespace realm;
using namespace realm::sync;
using namespace realm::test_util;
Expand Down Expand Up @@ -1216,4 +1220,73 @@ TEST(Util_Network_SSL_Certificate_Failure)
thread_2.join();
}

#ifdef _WIN32

// Adding a trusted root certificate causes a blocking popup to appear so only run this test
// if in an interactive session with a debugger attached, otherwise CI machines will block.
// TODO: maybe use the CI environment variable for the condition?
TEST_IF(Util_Network_SSL_Certificate_From_Windows_Cert_Store, IsDebuggerPresent())
{
std::string ca_dir = get_test_resource_path();

BIO* file = BIO_new_file((ca_dir + "crt.pem").c_str(), "rb");
X509* cert = PEM_read_bio_X509(file, NULL, 0, NULL);
BIO_free(file);

BIO* mem = BIO_new(BIO_s_mem());
int size = i2d_X509_bio(mem, cert);
BUF_MEM* buffer;
BIO_get_mem_ptr(mem, &buffer);

PCCERT_CONTEXT cert_context;
HCERTSTORE store = CertOpenSystemStoreA(NULL, "ROOT");
CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, reinterpret_cast<const BYTE*>(buffer->data),
buffer->length, CERT_STORE_ADD_USE_EXISTING, &cert_context);
BIO_free(mem);

network::Service service_1, service_2;
network::Socket socket_1{service_1}, socket_2{service_2};
network::ssl::Context ssl_context_1;
network::ssl::Context ssl_context_2;

ssl_context_1.use_certificate_chain_file(ca_dir + "dns-chain.crt.pem");
ssl_context_1.use_private_key_file(ca_dir + "dns-checked-server.key.pem");
ssl_context_2
.use_default_verify(); // this will import the Windows certificates in the context's certificate store

network::ssl::Stream ssl_stream_1{socket_1, ssl_context_1, network::ssl::Stream::server};
network::ssl::Stream ssl_stream_2{socket_2, ssl_context_2, network::ssl::Stream::client};
ssl_stream_1.set_logger(test_context.logger.get());
ssl_stream_2.set_logger(test_context.logger.get());

ssl_stream_2.set_verify_mode(network::ssl::VerifyMode::peer);

// We expect success because the certificate is signed for www.example.com
// in both Common Name and SAN.
ssl_stream_2.set_host_name("www.example.com");

connect_sockets(socket_1, socket_2);

auto connector = [&] {
std::error_code ec;
ssl_stream_2.handshake(ec);
CHECK_EQUAL(std::error_code(), ec);
};
auto acceptor = [&] {
std::error_code ec;
ssl_stream_1.handshake(ec);
CHECK_EQUAL(std::error_code(), ec);
};

std::thread thread_1(std::move(connector));
std::thread thread_2(std::move(acceptor));
thread_1.join();
thread_2.join();

CertDeleteCertificateFromStore(cert_context);
CertCloseStore(store, 0);
}

#endif // _WIN32

#endif // !REALM_MOBILE
Loading