wallet: propagate TLS verification to native and RPC wallets
What changed, and why it matters
This commit fixes how Java wallet code passes TLS/SSL certificate verification settings down to the underlying native Monero wallet and to RPC wallets. Previously, the Java layer could request 'verify the certificate' or 'allow any certificate,' but that setting was not reliably forwarded across the JNI bridge or translated into the correct RPC parameters. The patch renames several native methods, adds an ssl_verify flag through the bridge, and makes the RPC wallet honor the connection's verification preference. It also fixes a related bug where changing a connection's SSL setting changed its hash code, which could break connection-manager tracking.
Treat as a security-hardening fix and include in release notes. Users relying on TLS verification against malicious or misconfigured daemons should upgrade. Review whether prior versions silently disabled verification or ignored user settings; if so, consider a CVE and advisory. Verify the updated monero-cpp submodule contains matching SSL verification support.
Security signals we found
TLS/SSL verification preference now propagated across JNI to native wallet
RPC wallet now translates connection sslVerify into ssl_allow_any_cert and ssl_support parameters
Connection equality/hashCode now includes sslVerify, preventing silent mismatches
Connection manager response-time map keyed by URI to survive hash changes
Tests assert that explicit SslOptions cannot be weakened by connection settings
Evidence from the diff
The diff propagates sslVerify from MoneroRpcConnection into monero-cpp via JNI and into monero-wallet-rpc via set_daemon parameters. Native method signatures are renamed to *WithSsl variants and now carry a jboolean jssl_verify. MoneroWalletRpc.setDaemonConnection now maps connection.getSslVerify() to sslOptions.setAllowAnyCert(!sslVerify), and chooses ‘enabled’ vs ‘autodetect’ for ssl_support based on whether real certificate constraints (CA file or fingerprints) are present and verification is required. MoneroRpcConnection equality/hashCode now includes sslVerify, and MoneroConnectionManager keys responseTimes by URI to remain stable when connection properties change. Tests cover round-trip, concurrent snapshots, equality, and RPC parameter mapping.
Changed components
monero-java JNI bridge (monero_jni_bridge.cpp/.h)MoneroWalletFull Java native bindingsMoneroWalletRpc daemon connection setupMoneroRpcConnection equality/hashCodeMoneroConnectionManager response-time trackingexternal/monero-cpp submoduleInspect captured patch +240 / −40
### external/monero-cpp
@@ -1 +1 @@
-Subproject commit 5ac0d137b6c6052eb5fdd633849180b17cf8fbe3
+Subproject commit 50be6621116f8ab9ee857b42aa2ca6ab51581fed
### 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<monero_rpc_connection>(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<monero_wallet>(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<monero_wallet>(env, instance, JNI_WALLET_HANDLE);
@@ -588,12 +590,13 @@ JNIEXPORT jobjectArray JNICALL Java_monero_wallet_MoneroWalletFull_getDaemonConn
std::shared_ptr<monero_rpc_connection> 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);
### src/main/cpp/monero_jni_bridge.h
@@ -65,17 +65,17 @@ 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);
// ----------------------------- INSTANCE METHODS -----------------------------
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);
### 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<MoneroRpcConnection, List<Long>> responseTimes = new HashMap<MoneroRpcConnection, List<Long>>();
+ private Map<String, List<Long>> responseTimes = new HashMap<String, List<Long>>(); // 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<MoneroRpcConnection> connections, Co
private MoneroRpcConnection processResponses(Collection<MoneroRpcConnection> responses) {
// add new connections
+ Map<String, MoneroRpcConnection> responsesByUri = new HashMap<String, MoneroRpcConnection>();
for (MoneroRpcConnection connection : responses) {
- if (!responseTimes.containsKey(connection)) responseTimes.put(connection, new ArrayList<Long>());
+ responsesByUri.put(connection.getUri(), connection);
+ if (!responseTimes.containsKey(connection.getUri())) responseTimes.put(connection.getUri(), new ArrayList<Long>());
}
// insert response times or null
- for (Entry<MoneroRpcConnection, List<Long>> responseTime : responseTimes.entrySet()) {
- responseTime.getValue().add(0, responses.contains(responseTime.getKey()) ? responseTime.getKey().getResponseTime() : null);
+ for (Entry<String, List<Long>> 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;
}
### 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;
### 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();
### 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<String, Object> params = new HashMap<String, Object>();
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
### 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 {
### 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");
}
}
### 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<String, Object> params = new HashMap<String, Object>();
+ MoneroWalletRpc wallet = new MoneroWalletRpc(new MoneroRpcConnection("http://localhost:18082") {
+ @Override
+ public Map<String, Object> 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());
+ }
+}Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.