Add a certificate validation using Android Keystore for RN (#1761)

* add a certificate validation using Android Keystore for RN
This commit is contained in:
Nabil Hachicha 2018-04-25 10:21:52 +01:00 committed by GitHub
parent 032ee3d178
commit 1c95abbff1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 361 additions and 2 deletions

View File

@ -1,3 +1,19 @@
/*
* Copyright 2018 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.realm.react;
import android.content.res.AssetManager;

View File

@ -1,3 +1,19 @@
/*
* Copyright 2018 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.realm.react;
import com.facebook.react.ReactPackage;

View File

@ -0,0 +1,152 @@
/*
* Copyright 2018 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.realm.react.util;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.internal.tls.OkHostnameVerifier;
public class SSLHelper {
private final static String TAG = "REALM SSLHelper";
// Holds the certificate chain (per hostname). We need to keep the order of each certificate
// according to it's depth in the chain. The depth of the last
// certificate is 0. The depth of the first certificate is chain
// length - 1.
private static HashMap<String, List<String>> ROS_CERTIFICATES_CHAIN;
// The default Android Trust Manager which uses the default KeyStore to
// validate the certificate chain.
private static X509TrustManager TRUST_MANAGER;
// Help transform a String PEM representation of the certificate, into
// X509Certificate format.
private static CertificateFactory CERTIFICATE_FACTORY;
// From Sync implementation:
// A recommended way of using the callback function is to return true
// if preverify_ok = 1 and depth > 0,
// always check the host name if depth = 0,
// and use an independent verification step if preverify_ok = 0.
//
// Another possible way of using the callback is to collect all the
// ROS_CERTIFICATES_CHAIN until depth = 0, and present the entire chain for
// independent verification.
//
// In this implementation we use the second method, since it's more suitable for
// the underlying Java API we need to call to validate the certificate chain.
public synchronized static boolean certificateVerifier(String serverAddress, String pemData, int depth) {
try {
if (ROS_CERTIFICATES_CHAIN == null) {
ROS_CERTIFICATES_CHAIN = new HashMap<>();
TRUST_MANAGER = systemDefaultTrustManager();
CERTIFICATE_FACTORY = CertificateFactory.getInstance("X.509");
}
if (!ROS_CERTIFICATES_CHAIN.containsKey(serverAddress)) {
ROS_CERTIFICATES_CHAIN.put(serverAddress, new ArrayList<String>());
}
ROS_CERTIFICATES_CHAIN.get(serverAddress).add(pemData);
if (depth == 0) {
// transform all PEM ROS_CERTIFICATES_CHAIN into Java X509
// with respecting the order/depth provided from Sync.
List<String> pemChain = ROS_CERTIFICATES_CHAIN.get(serverAddress);
int n = pemChain.size();
X509Certificate[] chain = new X509Certificate[n];
for (String pem : pemChain) {
// The depth of the last certificate is 0.
// The depth of the first certificate is chain length - 1.
chain[--n] = buildCertificateFromPEM(pem);
}
// verify the entire chain
try {
TRUST_MANAGER.checkServerTrusted(chain, "RSA");
// verify the hostname
boolean isValid = OkHostnameVerifier.INSTANCE.verify(serverAddress, chain[0]);
if (isValid) {
return true;
} else {
Log.e(TAG, "Can not verify the hostname for the host: " + serverAddress);
return false;
}
} catch (CertificateException e) {
Log.e(TAG, "Can not validate SSL chain certificate for the host: " + serverAddress, e);
return false;
} finally {
// don't keep the certificate chain in memory
ROS_CERTIFICATES_CHAIN.remove(serverAddress);
}
} else {
// return true, since the verification will happen for the entire chain
// when receiving the depth == 0 (host certificate)
return true;
}
} catch (Exception e) {
Log.e(TAG, "Error during certificate validation for host: " + serverAddress, e);
return false;
}
}
// Credit OkHttp https://github.com/square/okhttp/blob/e5c84e1aef9572adb493197c1b6c4e882aca085b/okhttp/src/main/java/okhttp3/OkHttpClient.java#L270
private static X509TrustManager systemDefaultTrustManager() {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
private static X509Certificate buildCertificateFromPEM(String pem) throws IOException, CertificateException {
InputStream stream = null;
try {
stream = new ByteArrayInputStream(pem.getBytes("UTF-8"));
return (X509Certificate) CERTIFICATE_FACTORY.generateCertificate(stream);
} finally {
if (stream != null) {
stream.close();
}
}
}
}

View File

@ -45,6 +45,7 @@ LOCAL_SRC_FILES += src/rpc.cpp
LOCAL_SRC_FILES += src/jsc/jsc_init.cpp
LOCAL_SRC_FILES += src/jsc/jsc_value.cpp
LOCAL_SRC_FILES += src/android/io_realm_react_RealmReactModule.cpp
LOCAL_SRC_FILES += src/android/jni_utils.cpp
LOCAL_SRC_FILES += src/android/jsc_override.cpp
LOCAL_SRC_FILES += src/android/platform.cpp
LOCAL_SRC_FILES += src/object-store/src/impl/collection_change_builder.cpp

View File

@ -23,17 +23,49 @@
#include "io_realm_react_RealmReactModule.h"
#include "rpc.hpp"
#include "platform.hpp"
#include "jni_utils.hpp"
using namespace realm::rpc;
using namespace realm::jni_util;
static RPCServer *s_rpc_server;
extern bool realmContextInjected;
jclass ssl_helper_class;
namespace realm {
// set the AssetManager used to access bundled files within the APK
void set_asset_manager(AAssetManager* assetManager);
}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*)
{
JNIEnv* env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
else {
JniUtils::initialize(vm, JNI_VERSION_1_6);
}
// We do lookup the class in this Thread, since FindClass sometimes fails
// when issued from the sync client thread
ssl_helper_class = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("io/realm/react/util/SSLHelper")));
return JNI_VERSION_1_6;
}
JNIEXPORT void JNI_OnUnload(JavaVM* vm, void*)
{
JNIEnv* env;
if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK) {
return;
}
else {
env->DeleteLocalRef(ssl_helper_class);
JniUtils::release();
}
}
JNIEXPORT void JNICALL Java_io_realm_react_RealmReactModule_setDefaultRealmFileDirectory
(JNIEnv *env, jclass, jstring fileDir, jobject javaAssetManager)
{

50
src/android/jni_utils.cpp Normal file
View File

@ -0,0 +1,50 @@
/*
* Copyright 2018 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "jni_utils.hpp"
#include <memory>
using namespace realm::jni_util;
static std::unique_ptr<JniUtils> s_instance;
void JniUtils::initialize(JavaVM* vm, jint vm_version) noexcept
{
s_instance = std::unique_ptr<JniUtils>(new JniUtils(vm, vm_version));
}
void JniUtils::release()
{
s_instance.release();
}
JNIEnv* JniUtils::get_env(bool attach_if_needed)
{
JNIEnv* env;
if (s_instance->m_vm->GetEnv(reinterpret_cast<void**>(&env), s_instance->m_vm_version) != JNI_OK) {
if (attach_if_needed) {
jint ret = s_instance->m_vm->AttachCurrentThread(reinterpret_cast<void**>(&env), nullptr);
}
}
return env;
}
void JniUtils::detach_current_thread()
{
s_instance->m_vm->DetachCurrentThread();
}

59
src/android/jni_utils.hpp Normal file
View File

@ -0,0 +1,59 @@
/*
* Copyright 2018 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef REALM_JNI_UTIL_JNI_UTILS_HPP
#define REALM_JNI_UTIL_JNI_UTILS_HPP
#include <jni.h>
#include <vector>
namespace realm {
namespace jni_util {
// Util functions for JNI.
class JniUtils {
public:
~JniUtils()
{
}
// Call this only once in JNI_OnLoad.
static void initialize(JavaVM* vm, jint vm_version) noexcept;
// Call this in JNI_OnUnload.
static void release();
// When attach_if_needed is false, returns the JNIEnv if there is one attached to this thread. Assert if there is
// none. When attach_if_needed is true, try to attach and return a JNIEnv if necessary.
static JNIEnv* get_env(bool attach_if_needed = false);
// Detach the current thread from the JVM. Only required for C++ threads that where attached in the first place.
// Failing to do so is a resource leak.
static void detach_current_thread();
private:
JniUtils(JavaVM* vm, jint vm_version) noexcept
: m_vm(vm)
, m_vm_version(vm_version)
{
}
JavaVM* m_vm;
jint m_vm_version;
};
} // namespace realm
} // namespace jni_util
#endif // REALM_JNI_UTIL_JNI_UTILS_HPP

View File

@ -33,6 +33,14 @@
#include "realm/util/logger.hpp"
#include "realm/util/uri.hpp"
#if REALM_ANDROID
#include <jni.h>
#include "./android/io_realm_react_RealmReactModule.h"
#include "./android/jni_utils.hpp"
extern jclass ssl_helper_class;
#endif
namespace realm {
namespace js {
@ -303,7 +311,6 @@ public:
bool operator ()(const std::string& server_address, sync::Session::port_type server_port, const char* pem_data, size_t pem_size, int preverify_ok, int depth)
{
const std::string pem_certificate {pem_data, pem_size};
{
std::lock_guard<std::mutex> lock {*m_mutex};
m_ssl_certificate_callback_done = false;
@ -824,7 +831,33 @@ void SyncClass<T>::populate_sync_config(ContextType ctx, ObjectType realm_constr
SSLVerifyCallbackSyncThreadFunctor<T> ssl_verify_functor {ctx, Value::validated_to_function(ctx, ssl_verify_func)};
ssl_verify_callback = std::move(ssl_verify_functor);
}
#if REALM_ANDROID
// For React Native Android, if the user didn't define the ssl_verify_callback, we provide a default
// implementation for him, otherwise all SSL validation will fail, since the Sync client doesn't have
// access to the Android Keystore.
// This default implementation will perform a JNI call to invoke a Java method defined at the `SSLHelper`
// to perform the certificate verification.
else {
auto ssl_verify_functor =
[](const std::string server_address, realm::sync::Session::port_type server_port,
const char* pem_data, size_t pem_size, int preverify_ok, int depth) {
JNIEnv* env = realm::jni_util::JniUtils::get_env(true);
static jmethodID java_certificate_verifier = env->GetStaticMethodID(ssl_helper_class, "certificateVerifier", "(Ljava/lang/String;Ljava/lang/String;I)Z");
jstring jserver_address = env->NewStringUTF(server_address.c_str());
// deep copy the pem_data into a string so DeleteLocalRef delete the local reference not the original const char
std::string pem(pem_data, pem_size);
jstring jpem = env->NewStringUTF(pem.c_str());
bool isValid = env->CallStaticBooleanMethod(ssl_helper_class, java_certificate_verifier,
jserver_address,
jpem, depth) == JNI_TRUE;
env->DeleteLocalRef(jserver_address);
env->DeleteLocalRef(jpem);
return isValid;
};
ssl_verify_callback = std::move(ssl_verify_functor);
}
#endif
bool is_partial = false;
ValueType partial_value = Object::get_property(ctx, sync_config_object, "partial");
if (!Value::is_undefined(ctx, partial_value)) {