diff --git a/common/main/cpp/native_c4listener.cc b/common/main/cpp/native_c4listener.cc index 44c593f7f..7972dc40f 100644 --- a/common/main/cpp/native_c4listener.cc +++ b/common/main/cpp/native_c4listener.cc @@ -564,14 +564,21 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4Listener_startTls( jboolean allowPush, jboolean allowPull, jboolean enableDeltaSync) { - C4TLSConfig tlsConfig; + C4TLSConfig tlsConfig = {}; tlsConfig.privateKeyRepresentation = kC4PrivateKeyFromKey; tlsConfig.key = (C4KeyPair *) keyPair; tlsConfig.certificate = getCert(env, cert); + + // Client Cert Authentication: tlsConfig.requireClientCerts = requireClientCerts; - tlsConfig.rootClientCerts = getCert(env, rootClientCerts); - tlsConfig.certAuthCallback = &certAuthCallback; - tlsConfig.tlsCallbackContext = reinterpret_cast(context); + if (requireClientCerts == true) { + if (rootClientCerts != NULL) { + tlsConfig.rootClientCerts = getCert(env, rootClientCerts); + } else { + tlsConfig.certAuthCallback = &certAuthCallback; + tlsConfig.tlsCallbackContext = reinterpret_cast(context); + } + } return reinterpret_cast(startListener( env, diff --git a/common/main/java/com/couchbase/lite/internal/replicator/AbstractCBLWebSocket.java b/common/main/java/com/couchbase/lite/internal/replicator/AbstractCBLWebSocket.java index 7013c12a7..2c2d3e844 100644 --- a/common/main/java/com/couchbase/lite/internal/replicator/AbstractCBLWebSocket.java +++ b/common/main/java/com/couchbase/lite/internal/replicator/AbstractCBLWebSocket.java @@ -35,6 +35,7 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; @@ -57,6 +58,7 @@ import com.couchbase.lite.internal.core.C4Replicator; import com.couchbase.lite.internal.core.C4Socket; import com.couchbase.lite.internal.core.C4WebSocketCloseCode; +import com.couchbase.lite.internal.core.NativeContext; import com.couchbase.lite.internal.fleece.FLEncoder; import com.couchbase.lite.internal.fleece.FLValue; import com.couchbase.lite.internal.support.Log; @@ -230,6 +232,13 @@ else if (scheme.equalsIgnoreCase(C4Replicator.C4_REPLICATOR_TLS_SCHEME_2)) { return null; } + //------------------------------------------------------------------------- + // Client Certificate Authentication Identities + //------------------------------------------------------------------------- + + @NonNull + public static final NativeContext CLIENT_CERT_AUTH_KEY_MANAGER = new NativeContext<>(); + //------------------------------------------------------------------------- // Member Variables //------------------------------------------------------------------------- @@ -310,7 +319,7 @@ private OkHttpClient setupOkHttpClient() throws GeneralSecurityException { final OkHttpClient.Builder builder = BASE_HTTP_CLIENT.newBuilder(); // authenticator - final Authenticator authenticator = setupAuthenticator(); + final Authenticator authenticator = setupBasicAuthenticator(); if (authenticator != null) { builder.authenticator(authenticator); } // setup SSLFactory and trusted certificate (pinned certificate) @@ -319,11 +328,14 @@ private OkHttpClient setupOkHttpClient() throws GeneralSecurityException { return builder.build(); } - private Authenticator setupAuthenticator() { + private Authenticator setupBasicAuthenticator() { if (options != null && options.containsKey(C4Replicator.REPLICATOR_OPTION_AUTHENTICATION)) { @SuppressWarnings("unchecked") final Map auth = (Map) options.get(C4Replicator.REPLICATOR_OPTION_AUTHENTICATION); - if (auth != null) { + if (auth == null) { return null; } + + final String authType = (String) auth.get(C4Replicator.REPLICATOR_AUTH_TYPE); + if (C4Replicator.AUTH_TYPE_BASIC.equals(authType)) { final String username = (String) auth.get(C4Replicator.REPLICATOR_AUTH_USER_NAME); final String password = (String) auth.get(C4Replicator.REPLICATOR_AUTH_PASSWORD); if (username != null && password != null) { @@ -337,7 +349,7 @@ private Authenticator setupAuthenticator() { final List challenges = response.challenges(); Log.v(TAG, "CBLWebSocket received challenges " + challenges); if (challenges != null) { - for (Challenge challenge: challenges) { + for (Challenge challenge : challenges) { if (challenge.scheme().equals("Basic")) { return response.request() .newBuilder() @@ -346,7 +358,6 @@ private Authenticator setupAuthenticator() { } } } - return null; }; } @@ -380,7 +391,7 @@ private Request newRequest() { @SuppressWarnings("unchecked") final Map extraHeaders = (Map) options.get(C4Replicator.REPLICATOR_OPTION_EXTRA_HEADERS); if (extraHeaders != null) { - for (Map.Entry entry: extraHeaders.entrySet()) { + for (Map.Entry entry : extraHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue().toString()); } } @@ -471,6 +482,14 @@ private void didClose(Throwable error) { return; } + if (error instanceof SSLHandshakeException) { + closed( + C4Constants.ErrorDomain.NETWORK, + C4Constants.NetworkError.TLS_HANDSHAKE_FAILED, + null); + return; + } + closed(C4Constants.ErrorDomain.WEB_SOCKET, 0, null); } @@ -490,28 +509,55 @@ private String checkScheme(String scheme) { } private void setupSSLSocketFactory(OkHttpClient.Builder builder) throws GeneralSecurityException { - byte[] pin = null; + byte[] pinnedServerCert = null; boolean acceptOnlySelfSignedServerCert = false; + KeyManager clientCertAuthKeyManager = null; if (options != null) { + // Pinned Certificate: if (options.containsKey(C4Replicator.REPLICATOR_OPTION_PINNED_SERVER_CERT)) { - pin = (byte[]) options.get(C4Replicator.REPLICATOR_OPTION_PINNED_SERVER_CERT); + pinnedServerCert = (byte[]) options.get(C4Replicator.REPLICATOR_OPTION_PINNED_SERVER_CERT); } + + // Accept only self-signed server cert mode: if (options.containsKey(C4Replicator.REPLICATOR_OPTION_SELF_SIGNED_SERVER_CERT)) { - acceptOnlySelfSignedServerCert = - (boolean) options.get(C4Replicator.REPLICATOR_OPTION_SELF_SIGNED_SERVER_CERT); + acceptOnlySelfSignedServerCert = (boolean) + options.get(C4Replicator.REPLICATOR_OPTION_SELF_SIGNED_SERVER_CERT); } + + // Client Certificate Authentication: + @SuppressWarnings("unchecked") final Map auth + = (Map) options.get(C4Replicator.REPLICATOR_OPTION_AUTHENTICATION); + if (auth != null) { + final String authType = (String) auth.get(C4Replicator.REPLICATOR_AUTH_TYPE); + if (C4Replicator.AUTH_TYPE_CLIENT_CERT.equals(authType)) { + final long token = (long) auth.get(C4Replicator.REPLICATOR_AUTH_CLIENT_CERT_KEY); + clientCertAuthKeyManager = CLIENT_CERT_AUTH_KEY_MANAGER.getObjFromContext(token); + if (clientCertAuthKeyManager == null) { + Log.w(TAG, "No key manager configured for client certificate authentication"); + } + } + } + } + + // KeyManager for client cert authentication: + KeyManager[] keyManagers = null; + if (clientCertAuthKeyManager != null) { + keyManagers = new KeyManager[] {clientCertAuthKeyManager}; } + // TrustManager for server cert verification: final X509TrustManager trustManager = new CBLTrustManager( - pin, acceptOnlySelfSignedServerCert, serverCertsListener); + pinnedServerCert, acceptOnlySelfSignedServerCert, serverCertsListener); - SSLContext.getInstance("TLS").init(null, new TrustManager[] {trustManager}, null); - final SSLSocketFactory sslSocketFactory = new TLSSocketFactory(null, new TrustManager[] {trustManager}, null); + // SSLSocketFactory: + final SSLSocketFactory sslSocketFactory = new TLSSocketFactory( + keyManagers, new TrustManager[] {trustManager}, null); builder.sslSocketFactory(sslSocketFactory, trustManager); - if (pin != null || acceptOnlySelfSignedServerCert) { - // As the certificate will need to be matched with the pinned certificate, accepts any - // host name specified in the certificate. + // HostnameVerifier: + if (pinnedServerCert != null || acceptOnlySelfSignedServerCert) { + // As the certificate will need to be matched with the pinned certificate, + // accepts any host name specified in the certificate. builder.hostnameVerifier((s, sslSession) -> true); } } diff --git a/common/test/java/com/couchbase/lite/BaseReplicatorTest.java b/common/test/java/com/couchbase/lite/BaseReplicatorTest.java index 21d1943a3..0f5e86af6 100644 --- a/common/test/java/com/couchbase/lite/BaseReplicatorTest.java +++ b/common/test/java/com/couchbase/lite/BaseReplicatorTest.java @@ -151,6 +151,21 @@ protected final Replicator run(URI url, boolean push, boolean pull, boolean cont return run(config); } + protected final Replicator run( + int expectedErrorCode, + String expectedErrorDomain, + URI url, + boolean push, + boolean pull, + boolean continuous, + Authenticator auth, + Certificate pinnedServerCert) + throws CouchbaseLiteException { + final ReplicatorConfiguration config = makeConfig(push, pull, continuous, new URLEndpoint(url), pinnedServerCert); + if (auth != null) { config.setAuthenticator(auth); } + return run(config, expectedErrorCode, expectedErrorDomain, false, false, null); + } + protected final Replicator run( ReplicatorConfiguration config, int expectedErrorCode, @@ -179,8 +194,7 @@ private Replicator run( boolean ignoreErrorAtStopped, boolean reset, Fn.Consumer onReady) - throws CouchbaseLiteException - { + throws CouchbaseLiteException { baseTestReplicator = r; TestReplicatorChangeListener listener