From f772fa852840a3a7cc3ecbc5d67c293cf68497d0 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:04:41 -0400 Subject: [PATCH 1/5] wallet: propagate TLS verification to native and RPC wallets --- external/monero-cpp | 2 +- src/main/cpp/monero_jni_bridge.cpp | 27 +++--- src/main/cpp/monero_jni_bridge.h | 6 +- .../common/MoneroConnectionManager.java | 19 +++-- .../monero/common/MoneroRpcConnection.java | 5 +- .../java/monero/wallet/MoneroWalletFull.java | 20 ++--- .../java/monero/wallet/MoneroWalletRpc.java | 17 +++- .../java/TestMoneroConnectionManager.java | 24 ++++++ src/test/java/TestNativeLibrary.java | 76 ++++++++++++++++- src/test/java/TestSerialization.java | 84 ++++++++++++++++++- 10 files changed, 240 insertions(+), 40 deletions(-) diff --git a/external/monero-cpp b/external/monero-cpp index 5ac0d137b..50be66211 160000 --- a/external/monero-cpp +++ b/external/monero-cpp @@ -1 +1 @@ -Subproject commit 5ac0d137b6c6052eb5fdd633849180b17cf8fbe3 +Subproject commit 50be6621116f8ab9ee857b42aa2ca6ab51581fed diff --git a/src/main/cpp/monero_jni_bridge.cpp b/src/main/cpp/monero_jni_bridge.cpp index ddcd6f71b..5057e4288 100644 --- a/src/main/cpp/monero_jni_bridge.cpp +++ b/src/main/cpp/monero_jni_bridge.cpp @@ -95,7 +95,7 @@ void rethrow_java_exception_as_cpp_exception(JNIEnv* env, jthrowable jexception) throw runtime_error(msg); } -void set_daemon_connection(JNIEnv *env, monero_wallet* wallet, jstring juri, jstring jusername, jstring jpassword, jstring jproxy_uri, jint jis_trusted) { +void set_daemon_connection(JNIEnv *env, monero_wallet* wallet, jstring juri, jstring jusername, jstring jpassword, jstring jproxy_uri, jint jis_trusted, jboolean jssl_verify) { // collect and release string params const char* _uri = juri ? env->GetStringUTFChars(juri, NULL) : nullptr; @@ -116,7 +116,9 @@ void set_daemon_connection(JNIEnv *env, monero_wallet* wallet, jstring juri, jst // set daemon connection try { - wallet->set_daemon_connection(uri, username, password, proxy_uri, is_trusted); + auto connection = std::make_shared(uri, username, password, proxy_uri); + connection->m_ssl_verify = jssl_verify; + wallet->set_daemon_connection(connection, is_trusted); } catch (...) { rethrow_cpp_exception_as_java_exception(env); } @@ -507,8 +509,8 @@ JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletDataJni(JN } } -JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_createWalletJni(JNIEnv *env, jclass clazz, jstring jconfig) { - MTRACE("Java_monero_wallet_MoneroWalletFull_createWalletJni"); +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_createWalletWithSslJni(JNIEnv *env, jclass clazz, jstring jconfig) { + MTRACE("Java_monero_wallet_MoneroWalletFull_createWalletWithSslJni"); // get config as json string const char* _config = jconfig ? env->GetStringUTFChars(jconfig, NULL) : nullptr; @@ -556,11 +558,11 @@ JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_isViewOnlyJni(JNI return wallet->is_view_only(); } -JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletFull_setDaemonConnectionJni(JNIEnv *env, jobject instance, jstring juri, jstring jusername, jstring jpassword, jstring jproxy_uri, jint jis_trusted) { - MTRACE("Java_monero_wallet_MoneroWalletFull_setDaemonConnectionJni"); +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletFull_setDaemonConnectionWithSslJni(JNIEnv *env, jobject instance, jstring juri, jstring jusername, jstring jpassword, jstring jproxy_uri, jint jis_trusted, jboolean jssl_verify) { + MTRACE("Java_monero_wallet_MoneroWalletFull_setDaemonConnectionWithSslJni"); monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); try { - set_daemon_connection(env, wallet, juri, jusername, jpassword, jproxy_uri, jis_trusted); + set_daemon_connection(env, wallet, juri, jusername, jpassword, jproxy_uri, jis_trusted, jssl_verify); } catch (...) { rethrow_cpp_exception_as_java_exception(env); } @@ -577,8 +579,8 @@ JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_isDaemonTrustedJn } } -JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getDaemonConnectionJni(JNIEnv *env, jobject instance) { - MTRACE("Java_monero_wallet_MoneroWalletFull_getDaemonConnectionJni()"); +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getDaemonConnectionWithSslJni(JNIEnv *env, jobject instance) { + MTRACE("Java_monero_wallet_MoneroWalletFull_getDaemonConnectionWithSslJni()"); // get wallet monero_wallet* wallet = get_handle(env, instance, JNI_WALLET_HANDLE); @@ -588,12 +590,13 @@ JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getDaemonConn std::shared_ptr daemon_connection = wallet->get_daemon_connection(); if (daemon_connection == nullptr) return 0; - // return string[uri, username, password] - jobjectArray vals = env->NewObjectArray(3, env->FindClass("java/lang/String"), nullptr); + // return string[uri, username, password, proxy_uri, ssl_verify] + jobjectArray vals = env->NewObjectArray(5, env->FindClass("java/lang/String"), nullptr); if (daemon_connection->m_uri != boost::none && !daemon_connection->m_uri.get().empty()) env->SetObjectArrayElement(vals, 0, env->NewStringUTF(daemon_connection->m_uri.get().c_str())); if (daemon_connection->m_username != boost::none && !daemon_connection->m_username.get().empty()) env->SetObjectArrayElement(vals, 1, env->NewStringUTF(daemon_connection->m_username.get().c_str())); if (daemon_connection->m_password != boost::none && !daemon_connection->m_password.get().empty()) env->SetObjectArrayElement(vals, 2, env->NewStringUTF(daemon_connection->m_password.get().c_str())); - if (daemon_connection->m_proxy_uri != boost::none && !daemon_connection->m_proxy_uri.get().empty()) env->SetObjectArrayElement(vals, 2, env->NewStringUTF(daemon_connection->m_proxy_uri.get().c_str())); + if (daemon_connection->m_proxy_uri != boost::none && !daemon_connection->m_proxy_uri.get().empty()) env->SetObjectArrayElement(vals, 3, env->NewStringUTF(daemon_connection->m_proxy_uri.get().c_str())); + env->SetObjectArrayElement(vals, 4, env->NewStringUTF(daemon_connection->m_ssl_verify ? "true" : "false")); return vals; } catch (...) { rethrow_cpp_exception_as_java_exception(env); diff --git a/src/main/cpp/monero_jni_bridge.h b/src/main/cpp/monero_jni_bridge.h index cf2ac4b81..5dab86caa 100644 --- a/src/main/cpp/monero_jni_bridge.h +++ b/src/main/cpp/monero_jni_bridge.h @@ -65,7 +65,7 @@ JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletJni(JNIEnv JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_openWalletDataJni(JNIEnv *, jclass, jstring, jint, jbyteArray, jbyteArray, jboolean); -JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_createWalletJni(JNIEnv *, jclass, jstring); +JNIEXPORT jlong JNICALL Java_monero_wallet_MoneroWalletFull_createWalletWithSslJni(JNIEnv *, jclass, jstring); JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getSeedLanguagesJni(JNIEnv *, jclass); @@ -73,9 +73,9 @@ JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getSeedLangua JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_isViewOnlyJni(JNIEnv *, jobject); -JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletFull_setDaemonConnectionJni(JNIEnv *, jobject, jstring, jstring, jstring, jstring, jint); +JNIEXPORT void JNICALL Java_monero_wallet_MoneroWalletFull_setDaemonConnectionWithSslJni(JNIEnv *, jobject, jstring, jstring, jstring, jstring, jint, jboolean); -JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getDaemonConnectionJni(JNIEnv *, jobject); +JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getDaemonConnectionWithSslJni(JNIEnv *, jobject); JNIEXPORT jboolean JNICALL Java_monero_wallet_MoneroWalletFull_isConnectedToDaemonJni(JNIEnv *, jobject); diff --git a/src/main/java/monero/common/MoneroConnectionManager.java b/src/main/java/monero/common/MoneroConnectionManager.java index 6680a0b23..582464ad8 100644 --- a/src/main/java/monero/common/MoneroConnectionManager.java +++ b/src/main/java/monero/common/MoneroConnectionManager.java @@ -78,7 +78,7 @@ public class MoneroConnectionManager { private boolean autoSwitch = DEFAULT_AUTO_SWITCH; private long timeoutMs = DEFAULT_TIMEOUT; private TaskLooper poller; - private Map> responseTimes = new HashMap>(); + private Map> responseTimes = new HashMap>(); // keyed by uri since connection hashes can change /** * Specify behavior when polling. @@ -175,7 +175,7 @@ public MoneroConnectionManager removeConnection(String uri) { MoneroRpcConnection connection = getConnectionByUri(uri); if (connection == null) throw new MoneroError("No connection exists with URI: " + uri); connections.remove(connection); - responseTimes.remove(connection); + responseTimes.remove(connection.getUri()); if (connection == currentConnection) { currentConnection = null; onConnectionChanged(currentConnection); @@ -637,13 +637,16 @@ private boolean checkConnections(Collection connections, Co private MoneroRpcConnection processResponses(Collection responses) { // add new connections + Map responsesByUri = new HashMap(); for (MoneroRpcConnection connection : responses) { - if (!responseTimes.containsKey(connection)) responseTimes.put(connection, new ArrayList()); + responsesByUri.put(connection.getUri(), connection); + if (!responseTimes.containsKey(connection.getUri())) responseTimes.put(connection.getUri(), new ArrayList()); } // insert response times or null - for (Entry> responseTime : responseTimes.entrySet()) { - responseTime.getValue().add(0, responses.contains(responseTime.getKey()) ? responseTime.getKey().getResponseTime() : null); + for (Entry> responseTime : responseTimes.entrySet()) { + MoneroRpcConnection response = responsesByUri.get(responseTime.getKey()); + responseTime.getValue().add(0, response == null ? null : response.getResponseTime()); // remove old response times if (responseTime.getValue().size() > MIN_BETTER_RESPONSES) responseTime.getValue().remove(responseTime.getValue().size() - 1); @@ -690,15 +693,15 @@ private MoneroRpcConnection getBestConnectionFromPrioritizedResponses(Collection if (priorityComparator.compare(bestResponse.getPriority(), bestConnection.getPriority()) != 0) return bestResponse; // keep best connection if not enough data - if (!responseTimes.containsKey(bestConnection)) return bestConnection; + if (!responseTimes.containsKey(bestConnection.getUri()) || responseTimes.get(bestConnection.getUri()).size() < MIN_BETTER_RESPONSES) return bestConnection; // check if a connection is consistently better for (MoneroRpcConnection connection : responses) { if (connection == bestConnection) continue; - if (!responseTimes.containsKey(connection) || responseTimes.get(connection).size() < MIN_BETTER_RESPONSES) continue; + if (!responseTimes.containsKey(connection.getUri()) || responseTimes.get(connection.getUri()).size() < MIN_BETTER_RESPONSES) continue; boolean better = true; for (int i = 0; i < MIN_BETTER_RESPONSES; i++) { - if (responseTimes.get(connection).get(i) == null || responseTimes.get(bestConnection).get(i) == null || responseTimes.get(connection).get(i) > responseTimes.get(bestConnection).get(i)) { + if (responseTimes.get(connection.getUri()).get(i) == null || responseTimes.get(bestConnection.getUri()).get(i) == null || responseTimes.get(connection.getUri()).get(i) > responseTimes.get(bestConnection.getUri()).get(i)) { better = false; break; } diff --git a/src/main/java/monero/common/MoneroRpcConnection.java b/src/main/java/monero/common/MoneroRpcConnection.java index 6c612b12f..3e59b8eee 100644 --- a/src/main/java/monero/common/MoneroRpcConnection.java +++ b/src/main/java/monero/common/MoneroRpcConnection.java @@ -52,6 +52,8 @@ /** * Maintains a connection and sends requests to a Monero RPC API. + * Equality compares URI, credentials, proxy, ZMQ URI, and TLS verification. + * Do not mutate these settings while using a connection as a hash key. * * TODO: refactor MoneroRpcConnection extends MoneroConnection? */ @@ -631,7 +633,7 @@ public int hashCode() { result = prime * result + ((uri == null) ? 0 : uri.hashCode()); result = prime * result + ((username == null) ? 0 : username.hashCode()); result = prime * result + ((zmqUri == null) ? 0 : zmqUri.hashCode()); - result = prime * result + ((proxyUri == null) ? 0 : proxyUri.hashCode()); + result = prime * result + Boolean.hashCode(sslVerify); return result; } @@ -641,6 +643,7 @@ public boolean equals(Object obj) { if (obj == null) return false; if (getClass() != obj.getClass()) return false; MoneroRpcConnection other = (MoneroRpcConnection) obj; + if (sslVerify != other.sslVerify) return false; if (password == null) { if (other.password != null) return false; } else if (!password.equals(other.password)) return false; diff --git a/src/main/java/monero/wallet/MoneroWalletFull.java b/src/main/java/monero/wallet/MoneroWalletFull.java index 5a2df885a..2b37b8a42 100644 --- a/src/main/java/monero/wallet/MoneroWalletFull.java +++ b/src/main/java/monero/wallet/MoneroWalletFull.java @@ -319,7 +319,7 @@ public static MoneroWalletFull createWallet(MoneroWalletConfig config) { private static MoneroWalletFull createWalletFromSeed(MoneroWalletConfig config) { if (config.getRestoreHeight() == null) config.setRestoreHeight(0l); - long jniWalletHandle = createWalletJni(serializeWalletConfig(config)); + long jniWalletHandle = createWalletWithSslJni(serializeWalletConfig(config)); MoneroWalletFull wallet = new MoneroWalletFull(jniWalletHandle, config.getPassword()); return wallet; } @@ -328,7 +328,7 @@ private static MoneroWalletFull createWalletFromKeys(MoneroWalletConfig config) if (config.getRestoreHeight() == null) config.setRestoreHeight(0l); if (config.getLanguage() == null) config.setLanguage(DEFAULT_LANGUAGE); try { - long jniWalletHandle = createWalletJni(serializeWalletConfig(config)); + long jniWalletHandle = createWalletWithSslJni(serializeWalletConfig(config)); MoneroWalletFull wallet = new MoneroWalletFull(jniWalletHandle, config.getPassword()); return wallet; } catch (Exception e) { @@ -338,7 +338,7 @@ private static MoneroWalletFull createWalletFromKeys(MoneroWalletConfig config) private static MoneroWalletFull createWalletRandom(MoneroWalletConfig config) { if (config.getLanguage() == null) config.setLanguage(DEFAULT_LANGUAGE); - long jniWalletHandle = createWalletJni(serializeWalletConfig(config)); + long jniWalletHandle = createWalletWithSslJni(serializeWalletConfig(config)); return new MoneroWalletFull(jniWalletHandle, config.getPassword()); } @@ -535,10 +535,10 @@ public void setDaemonConnection(MoneroRpcConnection daemonConnection, Boolean is beginCall(); try { int isTrustedJni = isTrusted == null ? -1 : (isTrusted ? 1 : 0); // negative if unset - if (daemonConnection == null) setDaemonConnectionJni("", "", "", "", isTrustedJni); + if (daemonConnection == null) setDaemonConnectionWithSslJni("", "", "", "", isTrustedJni, true); else { try { - setDaemonConnectionJni(daemonConnection.getUri() == null ? "" : daemonConnection.getUri().toString(), daemonConnection.getUsername(), daemonConnection.getPassword(), daemonConnection.getProxyUri(), isTrustedJni); + setDaemonConnectionWithSslJni(daemonConnection.getUri() == null ? "" : daemonConnection.getUri().toString(), daemonConnection.getUsername(), daemonConnection.getPassword(), daemonConnection.getProxyUri(), isTrustedJni, daemonConnection.getSslVerify()); } catch (Exception e) { throw new MoneroError(e.getMessage()); } @@ -571,8 +571,8 @@ public MoneroRpcConnection getDaemonConnection() { beginCall(); try { try { - String[] vals = getDaemonConnectionJni(); - return vals == null ? null : new MoneroRpcConnection(vals[0], vals[1], vals[2]); + String[] vals = getDaemonConnectionWithSslJni(); + return vals == null ? null : new MoneroRpcConnection(vals[0], vals[1], vals[2], null, vals[3]).setSslVerify(!"false".equals(vals[4])); } catch (Exception e) { throw new MoneroError(e.getMessage()); } @@ -1859,7 +1859,7 @@ public void close(boolean save) { private native static long openWalletDataJni(String password, int networkType, byte[] keysData, byte[] cacheData, boolean regtest); - private native static long createWalletJni(String walletConfigJson); + private native static long createWalletWithSslJni(String walletConfigJson); private native long getHeightJni(); @@ -1875,9 +1875,9 @@ public void close(boolean save) { private native boolean isViewOnlyJni(); - private native void setDaemonConnectionJni(String uri, String username, String password, String proxyUri, int isTrusted); + private native void setDaemonConnectionWithSslJni(String uri, String username, String password, String proxyUri, int isTrusted, boolean sslVerify); - private native String[] getDaemonConnectionJni(); // returns [uri, username, password] + private native String[] getDaemonConnectionWithSslJni(); // returns [uri, username, password, proxyUri, sslVerify] private native boolean isConnectedToDaemonJni(); diff --git a/src/main/java/monero/wallet/MoneroWalletRpc.java b/src/main/java/monero/wallet/MoneroWalletRpc.java index 0876528f5..ca2d71f07 100644 --- a/src/main/java/monero/wallet/MoneroWalletRpc.java +++ b/src/main/java/monero/wallet/MoneroWalletRpc.java @@ -510,14 +510,23 @@ public void setDaemonConnection(MoneroRpcConnection connection, Boolean isTruste setDaemonConnection(connection, isTrusted, null); } + /** + * Explicit SSL options take precedence over the connection's verification setting. + * The cached connection records the requested allow-any-cert setting, not custom SSL options. + * Wallet RPC enforces a CA file or fingerprints; otherwise SSL autodetect can accept unverified certificates. + */ public void setDaemonConnection(MoneroRpcConnection connection, Boolean isTrusted, SslOptions sslOptions) { - if (sslOptions == null) sslOptions = new SslOptions(); + if (sslOptions == null) { + sslOptions = new SslOptions(); + if (connection != null) sslOptions.setAllowAnyCert(!connection.getSslVerify()); + } Map params = new HashMap(); params.put("address", connection == null ? "placeholder" : connection.getUri()); params.put("username", connection == null ? "" : connection.getUsername()); params.put("password", connection == null ? "" : connection.getPassword()); params.put("trusted", isTrusted); - params.put("ssl_support", "autodetect"); + boolean hasCertificates = (sslOptions.getCertificateAuthorityFile() != null && !sslOptions.getCertificateAuthorityFile().isEmpty()) || (sslOptions.getAllowedFingerprints() != null && !sslOptions.getAllowedFingerprints().isEmpty()); + params.put("ssl_support", hasCertificates && !Boolean.TRUE.equals(sslOptions.getAllowAnyCert()) ? "enabled" : "autodetect"); // wallet rpc only enforces certificates if enabled params.put("ssl_private_key_path", sslOptions.getPrivateKeyPath()); params.put("ssl_certificate_path", sslOptions.getCertificatePath()); params.put("ssl_ca_file", sslOptions.getCertificateAuthorityFile()); @@ -535,8 +544,10 @@ else if (!NetworkUtils.isSameProxyUri(startupProxyUri, connection.getProxyUri()) } if (!params.containsKey("proxy")) params.put("proxy", ""); + MoneroRpcConnection daemonConnection = connection == null || connection.getUri() == null || connection.getUri().isEmpty() ? null : new MoneroRpcConnection(connection); + if (daemonConnection != null) daemonConnection.setSslVerify(!Boolean.TRUE.equals(params.get("ssl_allow_any_cert"))); rpc.sendJsonRequest("set_daemon", params); - this.daemonConnection = connection == null || connection.getUri() == null || connection.getUri().isEmpty() ? null : new MoneroRpcConnection(connection); + this.daemonConnection = daemonConnection; } @Override diff --git a/src/test/java/TestMoneroConnectionManager.java b/src/test/java/TestMoneroConnectionManager.java index 7e14ef573..04dadd320 100644 --- a/src/test/java/TestMoneroConnectionManager.java +++ b/src/test/java/TestMoneroConnectionManager.java @@ -23,6 +23,30 @@ public class TestMoneroConnectionManager { private static final int SYNC_PADDING = 1000; + + @Test + public void testResponseTimesAfterSettingChange() { + class TestConnection extends MoneroRpcConnection { + TestConnection(String uri, long responseTime) { + super(uri); + this.isOnline = true; + this.isAuthenticated = true; + this.responseTime = responseTime; + } + @Override + public boolean checkConnection(long timeoutMs) { + return false; + } + } + MoneroRpcConnection slower = new TestConnection("http://localhost:18081", 2); + MoneroRpcConnection faster = new TestConnection("http://localhost:18082", 1); + MoneroConnectionManager connectionManager = new MoneroConnectionManager().addConnection(faster).setConnection(slower); + connectionManager.checkConnections(); + connectionManager.checkConnections(); + slower.setSslVerify(false); // changes the connection's hash + connectionManager.checkConnections(); + assertTrue(faster == connectionManager.getConnection()); + } @Test public void testConnectionManager() throws InterruptedException, IOException { diff --git a/src/test/java/TestNativeLibrary.java b/src/test/java/TestNativeLibrary.java index 8fc9005b8..b9f1dc598 100644 --- a/src/test/java/TestNativeLibrary.java +++ b/src/test/java/TestNativeLibrary.java @@ -1,6 +1,17 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import monero.common.MoneroError; +import monero.common.MoneroRpcConnection; import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletFull; import monero.wallet.model.MoneroWalletConfig; @@ -27,8 +38,71 @@ public void testKnownKeyDerivation() { } } - public static void main(String[] args) { + @Test + public void testDaemonSslVerifyRoundTrip() { + MoneroRpcConnection connection = new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI, "user", "password").setProxyUri("127.0.0.1:19050").setSslVerify(false); + MoneroWallet wallet = MoneroWalletFull.createWallet(new MoneroWalletConfig().setNetworkType(TestUtils.NETWORK_TYPE).setSeed(TestUtils.SEED).setServer(connection).setRestoreHeight(0l)); + try { + assertFalse(wallet.getDaemonConnection().getSslVerify()); + assertEquals(connection, wallet.getDaemonConnection()); + wallet.setDaemonConnection(wallet.getDaemonConnection()); + assertEquals(connection, wallet.getDaemonConnection()); + assertFalse(wallet.getDaemonConnection().getSslVerify()); + assertEquals("user", wallet.getDaemonConnection().getUsername()); + assertEquals("password", wallet.getDaemonConnection().getPassword()); + wallet.setDaemonConnection(connection.setSslVerify(true)); + assertTrue(wallet.getDaemonConnection().getSslVerify()); + wallet.setDaemonConnection(connection.setSslVerify(false)); + assertFalse(wallet.getDaemonConnection().getSslVerify()); + assertThrows(MoneroError.class, () -> wallet.setDaemonConnection(new MoneroRpcConnection("https://127.0.0.1:65536"))); + assertEquals(connection, wallet.getDaemonConnection()); + wallet.setDaemonConnection(new MoneroRpcConnection(connection).setProxyUri("127.0.0.1:19051")); + assertEquals("127.0.0.1:19051", wallet.getDaemonConnection().getProxyUri()); + wallet.setDaemonConnection((MoneroRpcConnection) null); + assertNull(wallet.getDaemonConnection()); + wallet.setDaemonConnection(new MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI)); + assertTrue(wallet.getDaemonConnection().getSslVerify()); + assertNull(wallet.getDaemonConnection().getProxyUri()); + } finally { + wallet.close(false); + } + } + + @Test + public void testConcurrentDaemonConnectionSnapshots() throws Exception { + MoneroRpcConnection first = new MoneroRpcConnection("http://127.0.0.1:18081", "first", "password1").setProxyUri("127.0.0.1:19050").setSslVerify(false); + MoneroRpcConnection second = new MoneroRpcConnection("http://127.0.0.1:18082", "second", "password2").setProxyUri("127.0.0.1:19051"); + MoneroWallet wallet = MoneroWalletFull.createWallet(new MoneroWalletConfig().setNetworkType(TestUtils.NETWORK_TYPE)); + ExecutorService executor = Executors.newFixedThreadPool(2); + CountDownLatch start = new CountDownLatch(1); + try { + wallet.setDaemonConnection(first); + Future[] updates = new Future[2]; + MoneroRpcConnection[] connections = {first, second}; + for (int i = 0; i < connections.length; i++) { + MoneroRpcConnection connection = connections[i]; + updates[i] = executor.submit(() -> { + start.await(); + for (int j = 0; j < 200; j++) { + wallet.setDaemonConnection(connection); + MoneroRpcConnection snapshot = wallet.getDaemonConnection(); + assertTrue(first.equals(snapshot) || second.equals(snapshot)); + } + return null; + }); + } + start.countDown(); + for (Future update : updates) update.get(30, TimeUnit.SECONDS); + } finally { + executor.shutdownNow(); + wallet.close(false); + } + } + + public static void main(String[] args) throws Exception { new TestNativeLibrary().testKnownKeyDerivation(); + new TestNativeLibrary().testDaemonSslVerifyRoundTrip(); + new TestNativeLibrary().testConcurrentDaemonConnectionSnapshots(); System.out.println("Native library verified"); } } diff --git a/src/test/java/TestSerialization.java b/src/test/java/TestSerialization.java index 74dc33f0d..3834cea97 100644 --- a/src/test/java/TestSerialization.java +++ b/src/test/java/TestSerialization.java @@ -1,15 +1,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.core.type.TypeReference; import common.utils.JsonUtils; import java.math.BigInteger; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import monero.common.MoneroRpcConnection; +import monero.common.SslOptions; +import monero.wallet.MoneroWalletRpc; import org.junit.jupiter.api.Test; /** @@ -76,4 +85,77 @@ public void testListSerializationWithCustomTypes() { map1.remove("null"); // nulls should be removed during serialization assertEquals(map1, map2); } -} \ No newline at end of file + + @Test + public void testConnectionSslEquality() { + MoneroRpcConnection connection = new MoneroRpcConnection("https://localhost:18081", "user", "password").setProxyUri("127.0.0.1:9050"); + MoneroRpcConnection copy = new MoneroRpcConnection(connection); + assertEquals(connection, copy); + assertEquals(connection.hashCode(), copy.hashCode()); + copy.setSslVerify(false); + assertNotEquals(connection, copy); + assertNotEquals(copy, connection); + connection.setSslVerify(false); + assertEquals(connection, copy); + assertEquals(connection.hashCode(), copy.hashCode()); + } + + @Test + public void testWalletRpcSslOptions() { + Map params = new HashMap(); + MoneroWalletRpc wallet = new MoneroWalletRpc(new MoneroRpcConnection("http://localhost:18082") { + @Override + public Map sendJsonRequest(String method, Object requestParams) { + assertEquals("set_daemon", method); + params.clear(); + params.putAll(JsonUtils.toMap(requestParams)); + return Collections.emptyMap(); + } + }); + MoneroRpcConnection connection = new MoneroRpcConnection("https://localhost:18081"); + wallet.setDaemonConnection(connection); + assertEquals(false, params.get("ssl_allow_any_cert")); + wallet.setDaemonConnection(connection.setSslVerify(false)); + assertEquals(true, params.get("ssl_allow_any_cert")); + assertEquals("autodetect", params.get("ssl_support")); + wallet.setDaemonConnection(connection.setSslVerify(true)); + assertEquals(false, params.get("ssl_allow_any_cert")); + + // explicit options must not be weakened or mutated by the connection's setting + connection.setSslVerify(false); + SslOptions options = new SslOptions(); + wallet.setDaemonConnection(connection, false, options); + assertNull(params.get("ssl_allow_any_cert")); + assertEquals("autodetect", params.get("ssl_support")); + assertTrue(wallet.getDaemonConnection().getSslVerify()); + assertNotSame(connection, wallet.getDaemonConnection()); + assertFalse(connection.getSslVerify()); + options.setCertificateAuthorityFile("ca.pem"); + options.setAllowedFingerprints(Arrays.asList("fingerprint")); + wallet.setDaemonConnection(connection, false, options); + assertNull(params.get("ssl_allow_any_cert")); + assertEquals("ca.pem", params.get("ssl_ca_file")); + assertEquals(options.getAllowedFingerprints(), params.get("ssl_allowed_fingerprints")); + assertEquals("enabled", params.get("ssl_support")); // a ca file or fingerprints must be enforced + assertNull(options.getAllowAnyCert()); + options.setAllowAnyCert(false); + wallet.setDaemonConnection(connection, false, options); + assertEquals(false, params.get("ssl_allow_any_cert")); + assertTrue(wallet.getDaemonConnection().getSslVerify()); + assertFalse(connection.getSslVerify()); + wallet.setDaemonConnection(wallet.getDaemonConnection()); + assertEquals(false, params.get("ssl_allow_any_cert")); + options.setAllowAnyCert(true); + wallet.setDaemonConnection(connection.setSslVerify(true), false, options); + assertEquals(true, params.get("ssl_allow_any_cert")); + assertEquals("autodetect", params.get("ssl_support")); + assertFalse(wallet.getDaemonConnection().getSslVerify()); + assertTrue(connection.getSslVerify()); + wallet.setDaemonConnection(wallet.getDaemonConnection()); + assertEquals(true, params.get("ssl_allow_any_cert")); + wallet.setDaemonConnection((MoneroRpcConnection) null); + assertEquals("placeholder", params.get("address")); + assertNull(params.get("ssl_allow_any_cert")); + assertNull(wallet.getDaemonConnection()); + } +} From 5949fdcf16a3d1d60ab393ca797362f1df662ebd Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:04:29 -0400 Subject: [PATCH 2/5] build: bump version to 0.8.59 --- README.md | 4 ++-- pom.xml | 2 +- src/main/java/monero/common/MoneroUtils.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8bb84fe0f..0f6b7e089 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ walletFull.close(true); #### For Gradle, add to build.gradle: -`compile 'io.github.woodser:monero-java:0.8.58'` +`compile 'io.github.woodser:monero-java:0.8.59'` #### For Maven, add to pom.xml: @@ -105,7 +105,7 @@ walletFull.close(true); io.github.woodser monero-java - 0.8.58 + 0.8.59 ``` diff --git a/pom.xml b/pom.xml index 9aaa90781..5d4af3902 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.github.woodser monero-java - 0.8.58 + 0.8.59 Monero Java Library A Java library for using Monero https://github.com/woodser/monero-java diff --git a/src/main/java/monero/common/MoneroUtils.java b/src/main/java/monero/common/MoneroUtils.java index 50decffbd..33f458dae 100644 --- a/src/main/java/monero/common/MoneroUtils.java +++ b/src/main/java/monero/common/MoneroUtils.java @@ -42,7 +42,7 @@ public class MoneroUtils { * @return the version of this monero-java library */ public static String getVersion() { - return "0.8.58"; + return "0.8.59"; } /** From d80e71b2eb3e0563041d0409398e9c91d103746f Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:38:49 -0400 Subject: [PATCH 3/5] ci: run TLS regression tests --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2243440a0..8d6e173e8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -169,7 +169,7 @@ jobs: echo "static closure symbols are exported"; exit 1 fi mkdir -p test_wallets - mvn -B test -Dmaven.test.skip=false -Dtest=TestMoneroUtils,TestNativeLibrary -DargLine=-Djava.library.path=build + mvn -B test -Dmaven.test.skip=false -Dtest=TestMoneroUtils,TestNativeLibrary,TestSerialization,TestMoneroConnectionManager#testResponseTimesAfterSettingChange -DargLine=-Djava.library.path=build - name: Upload monero-java library uses: actions/upload-artifact@v7 @@ -288,7 +288,7 @@ jobs: MAVEN_OPTS: -Djava.library.path=build run: | mkdir -p test_wallets - mvn -B test -Dmaven.test.skip=false -Dtest=TestMoneroUtils,TestNativeLibrary -DargLine=-Djava.library.path=build + mvn -B test -Dmaven.test.skip=false -Dtest=TestMoneroUtils,TestNativeLibrary,TestSerialization,TestMoneroConnectionManager#testResponseTimesAfterSettingChange -DargLine=-Djava.library.path=build - name: Upload monero-java library uses: actions/upload-artifact@v7 @@ -398,7 +398,7 @@ jobs: echo "unexpected exports above"; exit 1 fi mkdir -p test_wallets - mvn -B test -Dmaven.test.skip=false -Dtest=TestMoneroUtils,TestNativeLibrary -DargLine=-Djava.library.path=build + mvn -B test -Dmaven.test.skip=false -Dtest=TestMoneroUtils,TestNativeLibrary,TestSerialization,TestMoneroConnectionManager#testResponseTimesAfterSettingChange -DargLine=-Djava.library.path=build - name: Upload monero-java library uses: actions/upload-artifact@v7 From 1c08a49f784dec16d08b4009d0fcc633ff4c86bb Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:57:38 -0400 Subject: [PATCH 4/5] build: update monero-cpp submodule --- external/monero-cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/monero-cpp b/external/monero-cpp index 50be66211..574ad3a4c 160000 --- a/external/monero-cpp +++ b/external/monero-cpp @@ -1 +1 @@ -Subproject commit 50be6621116f8ab9ee857b42aa2ca6ab51581fed +Subproject commit 574ad3a4c82870d9550d003a2483e002739ec55a From feec38c94cbf9ad950073be422a2f8e78a5855f8 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:59:21 -0400 Subject: [PATCH 5/5] docs: update api docs --- docs/javadocs/index-files/index-18.html | 4 +++- docs/javadocs/monero/common/MoneroRpcConnection.html | 2 ++ .../monero/common/class-use/MoneroRpcConnection.html | 4 +++- docs/javadocs/monero/common/class-use/SslOptions.html | 4 +++- docs/javadocs/monero/wallet/MoneroWalletRpc.html | 7 ++++++- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/docs/javadocs/index-files/index-18.html b/docs/javadocs/index-files/index-18.html index 2ae9f8b4d..ac5a90a91 100644 --- a/docs/javadocs/index-files/index-18.html +++ b/docs/javadocs/index-files/index-18.html @@ -359,7 +359,9 @@

S

setDaemonConnection(MoneroRpcConnection, Boolean) - Method in class monero.wallet.MoneroWalletRpc
 
setDaemonConnection(MoneroRpcConnection, Boolean, SslOptions) - Method in class monero.wallet.MoneroWalletRpc
-
 
+
+
Explicit SSL options take precedence over the connection's verification setting.
+
setDatabaseSize(Long) - Method in class monero.daemon.model.MoneroDaemonInfo
 
setDepth(Long) - Method in class monero.daemon.model.MoneroBlock
diff --git a/docs/javadocs/monero/common/MoneroRpcConnection.html b/docs/javadocs/monero/common/MoneroRpcConnection.html index 108d3db7e..4b3a68129 100644 --- a/docs/javadocs/monero/common/MoneroRpcConnection.html +++ b/docs/javadocs/monero/common/MoneroRpcConnection.html @@ -94,6 +94,8 @@

Class MoneroRpcConnectionpublic class MoneroRpcConnection extends Object
Maintains a connection and sends requests to a Monero RPC API. + Equality compares URI, credentials, proxy, ZMQ URI, and TLS verification. + Do not mutate these settings while using a connection as a hash key. TODO: refactor MoneroRpcConnection extends MoneroConnection?
diff --git a/docs/javadocs/monero/common/class-use/MoneroRpcConnection.html b/docs/javadocs/monero/common/class-use/MoneroRpcConnection.html index c5283d68f..b1f71fd98 100644 --- a/docs/javadocs/monero/common/class-use/MoneroRpcConnection.html +++ b/docs/javadocs/monero/common/class-use/MoneroRpcConnection.html @@ -320,7 +320,9 @@

Uses of MoneroWalletRpc.setDaemonConnection(MoneroRpcConnection connection, Boolean isTrusted, SslOptions sslOptions) -
 
+
+
Explicit SSL options take precedence over the connection's verification setting.
+
Constructors in monero.wallet with parameters of type MoneroRpcConnection
diff --git a/docs/javadocs/monero/common/class-use/SslOptions.html b/docs/javadocs/monero/common/class-use/SslOptions.html index 4409a65bb..e0d9e102e 100644 --- a/docs/javadocs/monero/common/class-use/SslOptions.html +++ b/docs/javadocs/monero/common/class-use/SslOptions.html @@ -72,7 +72,9 @@

Uses of SslOptio -
 
+
+
Explicit SSL options take precedence over the connection's verification setting.
+

diff --git a/docs/javadocs/monero/wallet/MoneroWalletRpc.html b/docs/javadocs/monero/wallet/MoneroWalletRpc.html index b8f82db03..00089bcda 100644 --- a/docs/javadocs/monero/wallet/MoneroWalletRpc.html +++ b/docs/javadocs/monero/wallet/MoneroWalletRpc.html @@ -943,7 +943,9 @@

Method Summary

setDaemonConnection(MoneroRpcConnection connection, Boolean isTrusted, SslOptions sslOptions)
-
 
+
+
Explicit SSL options take precedence over the connection's verification setting.
+
void
setSubaddressLabel(int accountIdx, int subaddressIdx, @@ -1425,6 +1427,9 @@

setDaemonConnection

public void setDaemonConnection(MoneroRpcConnection connection, Boolean isTrusted, SslOptions sslOptions)
+
Explicit SSL options take precedence over the connection's verification setting. + The cached connection records the requested allow-any-cert setting, not custom SSL options. + Wallet RPC enforces a CA file or fingerprints; otherwise SSL autodetect can accept unverified certificates.