Skip to content

Commit 732ba41

Browse files
Remote JMX authentication bug fix and tests for SSL.
1 parent 8fa7b17 commit 732ba41

File tree

3 files changed

+128
-37
lines changed

3 files changed

+128
-37
lines changed

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/c/util/FileUtils.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@
3535
import java.nio.file.Files;
3636
import java.nio.file.Path;
3737
import java.nio.file.Paths;
38-
import java.util.ArrayList;
3938
import java.util.Arrays;
4039
import java.util.List;
4140

42-
import jdk.graal.compiler.debug.DebugContext;
4341
import org.graalvm.nativeimage.ImageSingletons;
4442

4543
import com.oracle.svm.core.SubstrateOptions;
4644
import com.oracle.svm.core.SubstrateUtil;
4745
import com.oracle.svm.core.c.libc.TemporaryBuildDirectoryProvider;
4846

47+
import jdk.graal.compiler.debug.DebugContext;
48+
4949
public class FileUtils {
5050

5151
public static void drainInputStream(InputStream source, OutputStream sink) {
@@ -64,20 +64,7 @@ public static void drainInputStream(InputStream source, OutputStream sink) {
6464
}
6565

6666
public static List<String> readAllLines(InputStream source) {
67-
try {
68-
BufferedReader reader = new BufferedReader(new InputStreamReader(source));
69-
List<String> result = new ArrayList<>();
70-
while (true) {
71-
String line = reader.readLine();
72-
if (line == null) {
73-
break;
74-
}
75-
result.add(line);
76-
}
77-
return result;
78-
} catch (IOException ex) {
79-
throw shouldNotReachHere(ex);
80-
}
67+
return new BufferedReader(new InputStreamReader(source)).lines().toList();
8168
}
8269

8370
public static int executeCommand(String... args) throws IOException, InterruptedException {

substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/jdk/JmxServerFeature.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ private static void registerJMXAgentResources() {
8383
"jdk.internal.agent.resources.agent");
8484

8585
resourcesRegistry.addResourceBundles(ConfigurationCondition.alwaysTrue(),
86-
"sun.security.util.Resources"); // required for password auth
86+
"sun.security.util.resources.security"); // required for password auth
8787
}
8888

8989
private static void configureProxy(BeforeAnalysisAccess access) {

substratevm/src/com.oracle.svm.test/src/com/oracle/svm/test/jmx/JmxTest.java

Lines changed: 124 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
2-
* Copyright (c) 2022, 2022, Oracle and/or its affiliates. All rights reserved.
3-
* Copyright (c) 2022, 2022, Red Hat Inc. All rights reserved.
2+
* Copyright (c) 2022, 2025, Oracle and/or its affiliates. All rights reserved.
3+
* Copyright (c) 2022, 2025, Red Hat Inc. All rights reserved.
44
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
55
*
66
* This code is free software; you can redistribute it and/or modify it
@@ -25,6 +25,9 @@
2525
*/
2626
package com.oracle.svm.test.jmx;
2727

28+
import static org.junit.Assert.assertEquals;
29+
import static org.junit.Assert.assertFalse;
30+
import static org.junit.Assert.assertNotNull;
2831
import static org.junit.Assert.assertTrue;
2932
import static org.junit.Assume.assumeTrue;
3033

@@ -39,9 +42,14 @@
3942
import java.lang.management.OperatingSystemMXBean;
4043
import java.lang.management.RuntimeMXBean;
4144
import java.lang.management.ThreadMXBean;
45+
import java.nio.file.Files;
46+
import java.nio.file.Path;
47+
import java.nio.file.attribute.PosixFilePermission;
4248
import java.util.HashMap;
4349
import java.util.List;
4450
import java.util.Map;
51+
import java.util.Set;
52+
import java.util.concurrent.TimeUnit;
4553

4654
import javax.management.MBeanServer;
4755
import javax.management.MBeanServerConnection;
@@ -50,6 +58,7 @@
5058
import javax.management.remote.JMXConnector;
5159
import javax.management.remote.JMXConnectorFactory;
5260
import javax.management.remote.JMXServiceURL;
61+
import javax.rmi.ssl.SslRMIClientSocketFactory;
5362

5463
import org.graalvm.nativeimage.ImageInfo;
5564
import org.junit.Assert;
@@ -58,40 +67,135 @@
5867

5968
import com.oracle.svm.core.VMInspectionOptions;
6069
import com.oracle.svm.core.jdk.management.ManagementAgentStartupHook;
70+
import com.oracle.svm.hosted.c.util.FileUtils;
6171
import com.oracle.svm.test.AddExports;
6272

6373
import jdk.management.jfr.FlightRecorderMXBean;
6474

6575
@AddExports("jdk.management.agent/jdk.internal.agent")
6676
public class JmxTest {
6777
static final String PORT_PROPERTY = "com.sun.management.jmxremote.port";
78+
static final String RMI_PORT_PROPERTY = "com.sun.management.jmxremote.rmi.port";
6879
static final String AUTH_PROPERTY = "com.sun.management.jmxremote.authenticate";
80+
static final String CLIENT_AUTH_PROPERTY = "com.sun.management.jmxremote.ssl.need.client.auth";
81+
static final String ACCESS_PROPERTY = "com.sun.management.jmxremote.access.file";
82+
static final String PASSWORD_PROPERTY = "com.sun.management.jmxremote.password.file";
6983
static final String SSL_PROPERTY = "com.sun.management.jmxremote.ssl";
84+
static final String KEYSTORE_FILENAME = "clientkeystore";
85+
static final String KEYSTORE_PASSWORD = "clientpass";
86+
static final String KEYSTORE_PASSWORD_PROPERTY = "javax.net.ssl.keyStorePassword";
87+
static final String KEYSTORE_PROPERTY = "javax.net.ssl.keyStore";
88+
static final String TRUSTSTORE_FILENAME = "servertruststore";
89+
static final String TRUSTSTORE_PASSWORD = "servertrustpass";
90+
static final String TRUSTSTORE_PASSWORD_PROPERTY = "javax.net.ssl.trustStorePassword";
91+
static final String TRUSTSTORE_PROPERTY = "javax.net.ssl.trustStore";
92+
static final String REGISTRY_SSL_PROPERTY = "com.sun.management.jmxremote.registry.ssl";
93+
static final String SOCKET_FACTORY_PROPERTY = "com.sun.jndi.rmi.factory.socket";
7094
static final String TEST_PORT = "12345";
71-
static final String FALSE = "false";
95+
static final String TEST_ROLE = "myTestRole";
96+
static final String TEST_ROLE_PASSWORD = "MYTESTP@SSWORD";
97+
static final String TRUE = "true";
7298

7399
@BeforeClass
74-
public static void checkForJFR() {
100+
public static void setup() throws IOException {
75101
assumeTrue("skipping JMX tests", !ImageInfo.inImageCode() ||
76102
(VMInspectionOptions.hasJmxClientSupport() && VMInspectionOptions.hasJmxServerSupport()));
77103

78104
System.setProperty(PORT_PROPERTY, TEST_PORT);
79-
System.setProperty(AUTH_PROPERTY, FALSE);
80-
System.setProperty(SSL_PROPERTY, FALSE);
105+
System.setProperty(RMI_PORT_PROPERTY, TEST_PORT);
106+
System.setProperty(AUTH_PROPERTY, TRUE);
107+
System.setProperty(CLIENT_AUTH_PROPERTY, TRUE);
108+
System.setProperty(SSL_PROPERTY, TRUE);
109+
System.setProperty(REGISTRY_SSL_PROPERTY, TRUE);
110+
111+
// Prepare temp directory with files required for testing authentication.
112+
Path tempDirectory = Files.createTempDirectory("jmxtest");
113+
Path jmxRemoteAccess = tempDirectory.resolve("jmxremote.access");
114+
Path jmxRemotePassword = tempDirectory.resolve("jmxremote.password");
115+
Path clientKeyStore = tempDirectory.resolve(KEYSTORE_FILENAME);
116+
Path serverTrustStore = tempDirectory.resolve(TRUSTSTORE_FILENAME);
117+
118+
// Generate SSL keystore, client cert, and truststore for testing SSL connection.
119+
createClientKey(tempDirectory);
120+
createClientCert(tempDirectory);
121+
assertTrue("Failed to create " + KEYSTORE_FILENAME, Files.exists(clientKeyStore));
122+
System.setProperty(KEYSTORE_PROPERTY, clientKeyStore.toString());
123+
System.setProperty(KEYSTORE_PASSWORD_PROPERTY, KEYSTORE_PASSWORD);
124+
createServerTrustStore(tempDirectory);
125+
assertTrue("Failed to create " + TRUSTSTORE_FILENAME, Files.exists(serverTrustStore));
126+
System.setProperty(TRUSTSTORE_PROPERTY, serverTrustStore.toString());
127+
System.setProperty(TRUSTSTORE_PASSWORD_PROPERTY, TRUSTSTORE_PASSWORD);
128+
129+
// The following are dummy access and password files required for testing authentication.
130+
Files.writeString(jmxRemoteAccess, TEST_ROLE + " readwrite");
131+
System.setProperty(ACCESS_PROPERTY, jmxRemoteAccess.toString());
132+
Files.writeString(jmxRemotePassword, TEST_ROLE + " " + TEST_ROLE_PASSWORD);
133+
System.setProperty(PASSWORD_PROPERTY, jmxRemotePassword.toString());
134+
135+
// Password file must have restricted access.
136+
Files.setPosixFilePermissions(jmxRemotePassword, Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE));
137+
81138
try {
82139
// We need to rerun the startup hook with the correct properties set.
83140
ManagementAgentStartupHook startupHook = new ManagementAgentStartupHook();
84141
startupHook.execute(false);
85142
} catch (Exception e) {
86-
Assert.fail("Failed to start server Cause: " + e.getMessage());
143+
Assert.fail("Failed to start server. Cause: " + e.getMessage());
144+
}
145+
}
146+
147+
private static void createClientKey(Path tempDirectory) throws IOException {
148+
runCommand(tempDirectory, List.of("keytool", "-genkey",
149+
"-keystore", KEYSTORE_FILENAME,
150+
"-alias", "clientkey",
151+
"-storepass", KEYSTORE_PASSWORD,
152+
"-keypass", KEYSTORE_PASSWORD,
153+
"-dname", "CN=test, OU=test, O=test, L=test, ST=test, C=test, EMAILADDRESS=test",
154+
"-validity", "99999",
155+
"-keyalg", "rsa"));
156+
}
157+
158+
private static void createClientCert(Path tempDirectory) throws IOException {
159+
runCommand(tempDirectory, List.of("keytool", "-exportcert",
160+
"-keystore", KEYSTORE_FILENAME,
161+
"-alias", "clientkey",
162+
"-storepass", KEYSTORE_PASSWORD,
163+
"-file", "client.cer"));
164+
}
165+
166+
private static void createServerTrustStore(Path tempDirectory) throws IOException {
167+
runCommand(tempDirectory, List.of("keytool", "-importcert",
168+
"-noprompt",
169+
"-file", "client.cer",
170+
"-keystore", TRUSTSTORE_FILENAME,
171+
"-storepass", TRUSTSTORE_PASSWORD));
172+
}
173+
174+
private static void runCommand(Path tempDirectory, List<String> command) throws IOException {
175+
ProcessBuilder pb = new ProcessBuilder().command(command);
176+
pb.directory(tempDirectory.toFile());
177+
final Process process = pb.start();
178+
try {
179+
process.waitFor(5, TimeUnit.SECONDS);
180+
} catch (InterruptedException e) {
181+
throw new IOException("Keytool execution error");
182+
}
183+
if (process.exitValue() > 0) {
184+
final String processError = String.join(" \\ ", FileUtils.readAllLines(process.getErrorStream()));
185+
final String processOutput = String.join(" \\ ", FileUtils.readAllLines(process.getInputStream()));
186+
throw new IOException(
187+
"Keytool execution error: " + processError + ", output: " + processOutput + ", command: " + command);
87188
}
88189
}
89190

90191
private static MBeanServerConnection getLocalMBeanServerConnectionStatic() {
91192
try {
92193
JMXServiceURL jmxUrl = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://" + "localhost" + ":" + TEST_PORT + "/jmxrmi");
93194
Map<String, Object> env = new HashMap<>();
94-
195+
String[] credentials = {TEST_ROLE, TEST_ROLE_PASSWORD};
196+
env.put(JMXConnector.CREDENTIALS, credentials);
197+
// Include below if protecting registry with SSL
198+
env.put(SOCKET_FACTORY_PROPERTY, new SslRMIClientSocketFactory());
95199
JMXConnector connector = JMXConnectorFactory.connect(jmxUrl, env);
96200
return connector.getMBeanServerConnection();
97201
} catch (IOException e) {
@@ -104,8 +208,8 @@ private static MBeanServerConnection getLocalMBeanServerConnectionStatic() {
104208
public void testConnection() throws Exception {
105209
// This simply tests that we can establish a connection between client and server
106210
MBeanServerConnection mbsc = getLocalMBeanServerConnectionStatic();
107-
assertTrue("Connection should not be null", mbsc != null);
108-
assertTrue("Connection default domain should not be empty", !mbsc.getDefaultDomain().isEmpty());
211+
assertNotNull("Connection should not be null", mbsc);
212+
assertFalse("Connection default domain should not be empty", mbsc.getDefaultDomain().isEmpty());
109213
}
110214

111215
@Test
@@ -138,7 +242,7 @@ public void testRuntimeMXBeanProxy() {
138242
}
139243

140244
assertTrue("PID should be positive.", runtimeMXBean.getPid() > 0);
141-
assertTrue("Class Path should not be null: ", runtimeMXBean.getClassPath() != null);
245+
assertNotNull("Class Path should not be null: ", runtimeMXBean.getClassPath());
142246
assertTrue("Start time should be positive", runtimeMXBean.getStartTime() > 0);
143247
}
144248

@@ -149,7 +253,7 @@ public void testRuntimeMXBeanDirect() throws MalformedObjectNameException {
149253
ObjectName objectName = new ObjectName("java.lang:type=Runtime");
150254
try {
151255
assertTrue("Uptime should be positive. ", (long) mbsc.getAttribute(objectName, "Pid") > 0);
152-
assertTrue("Class Path should not be null: ", mbsc.getAttribute(objectName, "ClassPath") != null);
256+
assertNotNull("Class Path should not be null: ", mbsc.getAttribute(objectName, "ClassPath"));
153257
assertTrue("Start time should be positive", (long) mbsc.getAttribute(objectName, "StartTime") > 0);
154258
} catch (Exception e) {
155259
Assert.fail("Remote invocations failed : " + e.getMessage());
@@ -168,7 +272,7 @@ public void testClassLoadingMXBeanProxy() {
168272
Assert.fail("Failed to get ClassLoadingMXBean. : " + e.getMessage());
169273
}
170274
if (ImageInfo.inImageRuntimeCode()) {
171-
assertTrue("Loaded Class count should be 0 (hardcoded at 0): ", classLoadingMXBean.getLoadedClassCount() == 0);
275+
assertEquals("Loaded Class count should be 0 (hardcoded at 0): ", 0, classLoadingMXBean.getLoadedClassCount());
172276
} else {
173277
assertTrue("If in java mode, number of loaded classes should be positive: ", classLoadingMXBean.getLoadedClassCount() > 0);
174278
}
@@ -246,7 +350,7 @@ public void testGarbageCollectorMXBeanProxy() {
246350
Assert.fail("Failed to get GarbageCollectorMXBean. : " + e.getMessage());
247351
}
248352
for (GarbageCollectorMXBean gcBean : garbageCollectorMXBeans) {
249-
assertTrue("GC object name should not be null", gcBean.getObjectName() != null);
353+
assertNotNull("GC object name should not be null", gcBean.getObjectName());
250354
assertTrue("Number of GC should not be negative", gcBean.getCollectionCount() >= 0);
251355
}
252356
}
@@ -263,7 +367,7 @@ public void testOperatingSystemMXBeanProxy() {
263367
} catch (Exception e) {
264368
Assert.fail("Failed to get OperatingSystemMXBean. : " + e.getMessage());
265369
}
266-
assertTrue("OS version can't be null. ", operatingSystemMXBean.getVersion() != null);
370+
assertNotNull("OS version can't be null. ", operatingSystemMXBean.getVersion());
267371
}
268372

269373
@Test
@@ -272,7 +376,7 @@ public void testOperatingSystemMXBeanDirect() throws MalformedObjectNameExceptio
272376
MBeanServerConnection mbsc = getLocalMBeanServerConnectionStatic();
273377
ObjectName objectName = new ObjectName("java.lang:type=OperatingSystem");
274378
try {
275-
assertTrue("OS version can't be null. ", mbsc.getAttribute(objectName, "Version") != null);
379+
assertNotNull("OS version can't be null. ", mbsc.getAttribute(objectName, "Version"));
276380
} catch (Exception e) {
277381
Assert.fail("Remote invokations failed : " + e.getMessage());
278382
}
@@ -290,7 +394,7 @@ public void testMemoryManagerMXBeanProxy() {
290394
Assert.fail("Failed to get MemoryManagerMXBean. : " + e.getMessage());
291395
}
292396
for (MemoryManagerMXBean memoryManagerMXBean : memoryManagerMXBeans) {
293-
assertTrue("Memory pool names should not be null. ", memoryManagerMXBean.getMemoryPoolNames() != null);
397+
assertNotNull("Memory pool names should not be null. ", memoryManagerMXBean.getMemoryPoolNames());
294398
}
295399
}
296400

@@ -307,7 +411,7 @@ public void testMemoryPoolMXBeanProxy() {
307411
Assert.fail("Failed to get MemoryPoolMXBean. : " + e.getMessage());
308412
}
309413
for (MemoryPoolMXBean memoryPoolMXBean : memoryPoolMXBeans) {
310-
assertTrue("Memory Pool name should not be null ", memoryPoolMXBean.getName() != null);
414+
assertNotNull("Memory Pool name should not be null ", memoryPoolMXBean.getName());
311415
}
312416
}
313417

@@ -324,7 +428,7 @@ public void testFlightRecorderMXBeanProxy() {
324428
Assert.fail("Failed to get FlightRecorderMXBean. : " + e.getMessage());
325429
}
326430
flightRecorderMXBean.newRecording();
327-
assertTrue("Flight recordings should be available because we just created one.", !flightRecorderMXBean.getRecordings().isEmpty());
431+
assertFalse("Flight recordings should be available because we just created one.", flightRecorderMXBean.getRecordings().isEmpty());
328432
}
329433

330434
@Test
@@ -336,7 +440,7 @@ public void testFlightRecorderMXBeanDirect() throws MalformedObjectNameException
336440
mbsc.invoke(objectName, "startRecording", new Object[]{recording}, new String[]{"long"});
337441
mbsc.invoke(objectName, "stopRecording", new Object[]{recording}, new String[]{"long"});
338442
} catch (Exception e) {
339-
Assert.fail("Remote invokations failed : " + e.getMessage());
443+
Assert.fail("Remote invocations failed : " + e.getMessage());
340444
}
341445
}
342446
}

0 commit comments

Comments
 (0)