From 3b4521ef1fac2f77e0f7b9c3b3c3866e50ebfb3f Mon Sep 17 00:00:00 2001 From: Sebastian Ratz Date: Mon, 3 Nov 2025 17:10:31 +0000 Subject: [PATCH 1/4] Use a merged JVM+OS trust store as default SSLContext --- .../runtime/CollectionTrustManager.java | 82 +++++ .../internal/runtime/InternalPlatform.java | 10 + .../core/internal/runtime/KeyStoreUtil.java | 160 +++++++++ .../META-INF/MANIFEST.MF | 5 +- .../runtime/AllInternalRuntimeTests.java | 2 + .../runtime/CollectionTrustManagerTest.java | 128 +++++++ .../internal/runtime/KeyStoreUtilTest.java | 326 ++++++++++++++++++ .../core/tests/internal/runtime/keystore.p12 | Bin 0 -> 2472 bytes .../tests/internal/runtime/truststore.jks | Bin 0 -> 764 bytes 9 files changed, 712 insertions(+), 1 deletion(-) create mode 100644 runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java create mode 100644 runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/KeyStoreUtil.java create mode 100644 runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java create mode 100644 runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java create mode 100644 runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/keystore.p12 create mode 100644 runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/truststore.jks diff --git a/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java new file mode 100644 index 00000000000..2fb6ae08a8d --- /dev/null +++ b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SAP SE - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.internal.runtime; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import javax.net.ssl.X509TrustManager; + +public class CollectionTrustManager implements X509TrustManager { + + private final List trustManagers; + + public CollectionTrustManager(List trustManagers) { + this.trustManagers = trustManagers; + } + + public List getTrustManagers() { + return trustManagers; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + CertificateException ce = null; + for (X509TrustManager trustManager : this.getTrustManagers()) { + try { + trustManager.checkClientTrusted(chain, authType); + return; + } catch (CertificateException e) { + if (ce == null) { + ce = e; + } else { + ce.addSuppressed(e); + } + } + } + if (ce != null) { + throw ce; + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + CertificateException ce = null; + for (X509TrustManager trustManager : this.getTrustManagers()) { + try { + trustManager.checkServerTrusted(chain, authType); + return; + } catch (CertificateException e) { + if (ce == null) { + ce = e; + } else { + ce.addSuppressed(e); + } + } + } + if (ce != null) { + throw ce; + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return this.getTrustManagers().stream() // + .map(X509TrustManager::getAcceptedIssuers) // + .filter(Objects::nonNull).flatMap(Arrays::stream).toArray(X509Certificate[]::new); + } + +} \ No newline at end of file diff --git a/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/InternalPlatform.java b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/InternalPlatform.java index 990d7d3c9cf..7148f259a8d 100644 --- a/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/InternalPlatform.java +++ b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/InternalPlatform.java @@ -48,6 +48,7 @@ import org.eclipse.core.runtime.Plugin; import org.eclipse.core.runtime.RegistryFactory; import org.eclipse.core.runtime.ServiceCaller; +import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.content.IContentTypeManager; import org.eclipse.core.runtime.preferences.IPreferencesService; import org.eclipse.equinox.app.IApplicationContext; @@ -585,6 +586,14 @@ private void initializeAuthorizationHandler() { } } + private void initializeSSLContext() { + try { + new KeyStoreUtil(getOS()).setUpSslContext(); + } catch (Exception e) { + RuntimeLog.log(Status.error("Exception setting up SSLContext", e)); //$NON-NLS-1$ + } + } + /* * Finds and loads the options file */ @@ -701,6 +710,7 @@ public void start(BundleContext runtimeContext) { initialized = true; stopped = false; initializeAuthorizationHandler(); + initializeSSLContext(); startServices(); } diff --git a/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/KeyStoreUtil.java b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/KeyStoreUtil.java new file mode 100644 index 00000000000..fce10d269ca --- /dev/null +++ b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/KeyStoreUtil.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SAP SE - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.internal.runtime; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import org.eclipse.osgi.service.environment.Constants; + +public class KeyStoreUtil { + + @SuppressWarnings("nls") + private static final String PROP_LOAD_OS_TRUST_STORE_BY_DEFAULT = "eclipse.load.os.trust.store.by.default"; + + private final String os; + + private static final record KeyStoreAndPassword(KeyStore keyStore, char[] password) { + } + + public KeyStoreUtil(String os) { + this.os = os; + } + + @SuppressWarnings("nls") + public void setUpSslContext() throws GeneralSecurityException, IOException { + + if (Boolean.FALSE.toString().equals(System.getProperty(PROP_LOAD_OS_TRUST_STORE_BY_DEFAULT, null))) { + return; + } + + List keyStores = new ArrayList<>(); + keyStores.add(new KeyStoreAndPassword(null, null)); // null will loads JVM cacerts OR store indicated by + // "javax.net.ssl.trustStore" properties + if (System.getProperty("javax.net.ssl.trustStore", "").isEmpty()) { // NOPMD + if (Constants.OS_MACOSX.equals(os)) { + keyStores.add(createKeyStore("KeychainStore", "Apple")); + } else if (Constants.OS_WIN32.equals(os)) { + keyStores.add(createKeyStore("Windows-ROOT", null)); + } + } + List trustManagers = new ArrayList<>(); + for (KeyStoreAndPassword storeAndPassword : keyStores) { + trustManagers.add(createX509TrustManager(storeAndPassword.keyStore())); + } + TrustManager[] tm = { new CollectionTrustManager(trustManagers) }; + + KeyManager[] km = {}; + KeyStoreAndPassword keyStore = createKeyStoreFromSystemProperties(); + if (keyStore != null) { + km = new KeyManager[] { createX509KeyManager(keyStore.keyStore(), keyStore.password()) }; + } + + SSLContext sslContext = SSLContext.getInstance("TLS"); + initSSLContext(sslContext, tm, km, null); + SSLContext.setDefault(sslContext); + } + + private KeyStoreAndPassword createKeyStore(String type, String provider) + throws GeneralSecurityException, IOException { + KeyStore keyStore; + if (provider == null) { + keyStore = KeyStore.getInstance(type); + } else { + keyStore = KeyStore.getInstance(type, provider); + } + keyStore.load(null, null); + return new KeyStoreAndPassword(keyStore, null); + } + + protected X509TrustManager createX509TrustManager(KeyStore keyStore) throws GeneralSecurityException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keyStore); + return Arrays.stream(tmf.getTrustManagers()).filter(X509TrustManager.class::isInstance) // + .map(X509TrustManager.class::cast) // + .findFirst().orElse(null); + } + + protected X509KeyManager createX509KeyManager(KeyStore keyStore, char[] password) throws GeneralSecurityException { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, password); + return Arrays.stream(kmf.getKeyManagers()).filter(X509KeyManager.class::isInstance) // + .map(X509KeyManager.class::cast) // + .findFirst().orElse(null); + } + + protected void initSSLContext(SSLContext context, TrustManager[] trustManagers, KeyManager[] keyManagers, + SecureRandom random) throws KeyManagementException { + context.init(keyManagers, trustManagers, random); + } + + /** + * Coding based on + * sun.security.ssl.SSLContextImpl.DefaultManagersHolder.getKeyManagers() with + * minor adjustments (access properties directy without AccessController, return + * nothing if properties not set). + */ + @SuppressWarnings("nls") + private KeyStoreAndPassword createKeyStoreFromSystemProperties() throws GeneralSecurityException, IOException { // NOPMD + String p11KeyStore = "PKCS11"; + String none = "NONE"; + String keyStore = System.getProperty("javax.net.ssl.keyStore", ""); // NOPMD + String keyStoreType = System.getProperty("javax.net.ssl.keyStoreType", ""); // NOPMD + String keyStoreProvider = System.getProperty("javax.net.ssl.keyStoreProvider", ""); // NOPMD + String keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword", ""); // NOPMD + + if (keyStoreType.isEmpty()) { + if (keyStore.isEmpty()) { + return null; + } + keyStoreType = KeyStore.getDefaultType(); + } + if (p11KeyStore.equals(keyStoreType) && !none.equals(keyStore)) { + throw new IllegalArgumentException("if keyStoreType is " + p11KeyStore + ", then keyStore must be " + none); + } + char[] passwd = null; + if (!keyStorePassword.isEmpty()) { + passwd = keyStorePassword.toCharArray(); + } + KeyStore ks = null; + if (keyStoreProvider.isEmpty()) { + ks = KeyStore.getInstance(keyStoreType); + } else { + ks = KeyStore.getInstance(keyStoreType, keyStoreProvider); + } + if (!keyStore.isEmpty() && !none.equals(keyStore)) { + try (InputStream is = new FileInputStream(keyStore)) { + ks.load(is, passwd); + } + } else { + ks.load(null, passwd); + } + return new KeyStoreAndPassword(ks, p11KeyStore.equals(keyStoreType) ? null : passwd); + } + +} diff --git a/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF b/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF index 96503cbe97d..c37cf4d1da8 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF +++ b/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF @@ -16,10 +16,13 @@ Require-Bundle: org.junit, org.eclipse.core.tests.harness;bundle-version="3.11.0" Import-Package: org.assertj.core.api, org.junit.jupiter.api;version="[5.14.0,6.0.0)", + org.junit.jupiter.api.condition;version="[5.14.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.14.0,6.0.0)", org.junit.jupiter.api.function;version="[5.14.0,6.0.0)", org.junit.jupiter.api.io;version="[5.14.0,6.0.0)", - org.junit.platform.suite.api;version="[1.14.0,2.0.0)" + org.junit.platform.suite.api;version="[1.14.0,2.0.0)", + org.mockito, + org.mockito.stubbing Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-17 Plugin-Class: org.eclipse.core.tests.runtime.RuntimeTestsPlugin diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/AllInternalRuntimeTests.java b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/AllInternalRuntimeTests.java index 765014a6b7a..57bff2dcc33 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/AllInternalRuntimeTests.java +++ b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/AllInternalRuntimeTests.java @@ -18,6 +18,8 @@ @Suite @SelectClasses({ // + CollectionTrustManagerTest.class, // + KeyStoreUtilTest.class, // LogSerializationTest.class, // PlatformURLLocalTest.class, // PlatformURLSessionTest.class }) diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java new file mode 100644 index 00000000000..c86068f3d80 --- /dev/null +++ b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java @@ -0,0 +1,128 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SAP SE - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.tests.internal.runtime; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import javax.net.ssl.X509TrustManager; +import org.eclipse.core.internal.runtime.CollectionTrustManager; +import org.junit.Test; + + +public class CollectionTrustManagerTest { + + @Test + public void testAcceptedIssuers() throws Exception { + X509Certificate[] acceptedIssuers1 = { mock(X509Certificate.class), mock(X509Certificate.class) }; + X509Certificate[] acceptedIssuers2 = { mock(X509Certificate.class), mock(X509Certificate.class) }; + X509TrustManager mock1 = mock(X509TrustManager.class); + X509TrustManager mock2 = mock(X509TrustManager.class); + when(mock1.getAcceptedIssuers()).thenReturn(acceptedIssuers1); + when(mock2.getAcceptedIssuers()).thenReturn(acceptedIssuers2); + CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(mock1, mock2)); + + X509Certificate[] allAcceptedIssuers = collectionTrustManager.getAcceptedIssuers(); + + assertThat(allAcceptedIssuers, + arrayContaining(acceptedIssuers1[0], acceptedIssuers1[1], acceptedIssuers2[0], acceptedIssuers2[1])); + } + + @Test + public void testCheckClientTrusted() throws Exception { + X509Certificate[] chainTrustedBy1 = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedBy2 = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedByNone = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedByBoth = { mock(X509Certificate.class) }; + String authType = "testAuthType"; + + CertificateException exceptionFrom1 = new CertificateException("exception from 1"); + CertificateException exceptionFrom2 = new CertificateException("exception from 2"); + + X509TrustManager mock1 = mock(X509TrustManager.class); + doThrow(IllegalStateException.class).when(mock1).checkClientTrusted(any(), any()); + doNothing().when(mock1).checkClientTrusted(eq(chainTrustedBy1), eq(authType)); + doNothing().when(mock1).checkClientTrusted(eq(chainTrustedByBoth), eq(authType)); + doThrow(exceptionFrom1).when(mock1).checkClientTrusted(eq(chainTrustedBy2), eq(authType)); + doThrow(exceptionFrom1).when(mock1).checkClientTrusted(eq(chainTrustedByNone), eq(authType)); + + X509TrustManager mock2 = mock(X509TrustManager.class); + doThrow(IllegalStateException.class).when(mock2).checkClientTrusted(any(), any()); + doNothing().when(mock2).checkClientTrusted(eq(chainTrustedBy2), eq(authType)); + doNothing().when(mock2).checkClientTrusted(eq(chainTrustedByBoth), eq(authType)); + doThrow(exceptionFrom2).when(mock2).checkClientTrusted(eq(chainTrustedBy1), eq(authType)); + doThrow(exceptionFrom2).when(mock2).checkClientTrusted(eq(chainTrustedByNone), eq(authType)); + + CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(mock1, mock2)); + + collectionTrustManager.checkClientTrusted(chainTrustedBy1, authType); + collectionTrustManager.checkClientTrusted(chainTrustedBy2, authType); + collectionTrustManager.checkClientTrusted(chainTrustedByBoth, authType); + + CertificateException exception = assertThrows(CertificateException.class, () -> { + collectionTrustManager.checkClientTrusted(chainTrustedByNone, authType); + }); + assertThat(exception, sameInstance(exceptionFrom1)); // first in the list + assertThat(exception.getSuppressed(), arrayContaining(sameInstance(exceptionFrom2))); // second, suppressed + } + + @Test + public void testCheckServerTrusted() throws Exception { + X509Certificate[] chainTrustedBy1 = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedBy2 = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedByNone = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedByBoth = { mock(X509Certificate.class) }; + String authType = "testAuthType"; + + CertificateException exceptionFrom1 = new CertificateException("exception from 1"); + CertificateException exceptionFrom2 = new CertificateException("exception from 2"); + + X509TrustManager mock1 = mock(X509TrustManager.class); + doThrow(IllegalStateException.class).when(mock1).checkServerTrusted(any(), any()); + doNothing().when(mock1).checkServerTrusted(eq(chainTrustedBy1), eq(authType)); + doNothing().when(mock1).checkServerTrusted(eq(chainTrustedByBoth), eq(authType)); + doThrow(exceptionFrom1).when(mock1).checkServerTrusted(eq(chainTrustedBy2), eq(authType)); + doThrow(exceptionFrom1).when(mock1).checkServerTrusted(eq(chainTrustedByNone), eq(authType)); + + X509TrustManager mock2 = mock(X509TrustManager.class); + doThrow(IllegalStateException.class).when(mock2).checkServerTrusted(any(), any()); + doNothing().when(mock2).checkServerTrusted(eq(chainTrustedBy2), eq(authType)); + doNothing().when(mock2).checkServerTrusted(eq(chainTrustedByBoth), eq(authType)); + doThrow(exceptionFrom2).when(mock2).checkServerTrusted(eq(chainTrustedBy1), eq(authType)); + doThrow(exceptionFrom2).when(mock2).checkServerTrusted(eq(chainTrustedByNone), eq(authType)); + + CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(mock1, mock2)); + + collectionTrustManager.checkServerTrusted(chainTrustedBy1, authType); + collectionTrustManager.checkServerTrusted(chainTrustedBy2, authType); + collectionTrustManager.checkServerTrusted(chainTrustedByBoth, authType); + + CertificateException exception = assertThrows(CertificateException.class, () -> { + collectionTrustManager.checkServerTrusted(chainTrustedByNone, authType); + }); + assertThat(exception, sameInstance(exceptionFrom1)); // first in the list + assertThat(exception.getSuppressed(), arrayContaining(sameInstance(exceptionFrom2))); // second, suppressed + } +} diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java new file mode 100644 index 00000000000..7f7ab41de1a --- /dev/null +++ b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java @@ -0,0 +1,326 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SAP SE - initial API and implementation + *******************************************************************************/ +package org.eclipse.core.tests.internal.runtime; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.matchesRegex; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; +import org.eclipse.core.internal.runtime.CollectionTrustManager; +import org.eclipse.core.internal.runtime.KeyStoreUtil; +import org.eclipse.core.runtime.Platform; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; + +@SuppressWarnings("restriction") +public class KeyStoreUtilTest { + + private static final List SYSTEM_PROPERTIES_TO_BACKUP_AND_RESTORE = List.of( // + "eclipse.load.os.trust.store.by.default", // + "javax.net.ssl.trustStore", // + "javax.net.ssl.trustStorePassword", // + "javax.net.ssl.trustStoreProvider", // + "javax.net.ssl.trustStoreType", // + "javax.net.ssl.keyStore", // + "javax.net.ssl.keyStorePassword", // + "javax.net.ssl.keyStoreProvider", // + "javax.net.ssl.keyStoreType"); + + @TempDir + private Path tempDir; + + private Map systemPropertyBackups = new HashMap<>(); + private SSLContext previousSslContext; + + @BeforeEach + public void setup() throws Exception { + for (String property : SYSTEM_PROPERTIES_TO_BACKUP_AND_RESTORE) { + systemPropertyBackups.put(property, System.getProperty(property, null)); + } + previousSslContext = SSLContext.getDefault(); + } + + @AfterEach + public void teardown() { + systemPropertyBackups.forEach((property, backupValue) -> { + if (backupValue == null) { + System.clearProperty(property); + } else { + System.setProperty(property, backupValue); + } + }); + SSLContext.setDefault(previousSslContext); + } + + @Test + public void loadTrustManagers_Default() throws Exception { + + TestSpecificKeyStoreUtil keyStoreUtil = new TestSpecificKeyStoreUtil(); + + keyStoreUtil.setUpSslContext(); + + assertThat(SSLContext.getDefault(), is(keyStoreUtil.recordedSslContext)); + + assertThat(keyStoreUtil.recordedTrustManagers, arrayWithSize(1)); + assertThat(keyStoreUtil.recordedTrustManagers[0], instanceOf(CollectionTrustManager.class)); + assertThat(((CollectionTrustManager) keyStoreUtil.recordedTrustManagers[0]).getAcceptedIssuers(), + not(emptyArray())); + + CollectionTrustManager tm = (CollectionTrustManager) keyStoreUtil.recordedTrustManagers[0]; + + // jvm + assertThat(tm.getTrustManagers(), not(empty())); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores, not(empty())); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(0).manager(), is(tm.getTrustManagers().get(0))); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(0).store(), is(nullValue())); + assertThat( + Arrays.stream(tm.getTrustManagers().get(0).getAcceptedIssuers()) + .map(X509Certificate::getSubjectX500Principal).map(X500Principal::getName).toList(), + hasItem(matchesRegex("(?i).*digicert.*root.*"))); + + if (OS.WINDOWS.equals(OS.current())) { + assertThat(tm.getTrustManagers(), hasSize(2)); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores, hasSize(2)); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(1).manager(), + is(tm.getTrustManagers().get(1))); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(1).store().getType(), is("Windows-ROOT")); + assertThat( + Arrays.stream(tm.getTrustManagers().get(1).getAcceptedIssuers()) + .map(X509Certificate::getSubjectX500Principal).map(X500Principal::getName).toList(), + hasItem(matchesRegex("(?i).*digicert.*root.*"))); + } else if (OS.MAC.equals(OS.current())) { + assertThat(tm.getTrustManagers(), hasSize(2)); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores, hasSize(2)); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(1).manager(), + is(tm.getTrustManagers().get(1))); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(1).store().getType(), is("KeychainStore")); + // Apple KeychainStore only includes the 'System' certificates + // (enterprise/admin managed) + // but not the 'System Roots' ones (public CAs). + // There's nothing guaranteed / deterministic in the 'System' on CI machines + // that we could check for here... + } else { + assertThat(tm.getTrustManagers(), hasSize(1)); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores, hasSize(1)); + } + + // no private keys + assertThat(keyStoreUtil.recordedKeyManagers, emptyArray()); + } + + @Test + public void loadTrustManagers_TrustSystemPropertiesPointToCustomTrustStore() throws Exception { + + System.setProperty("javax.net.ssl.trustStore", copyResourceToTempDirAndGetPath("truststore.jks")); + System.setProperty("javax.net.ssl.trustStorePassword", "verysecret"); + + TestSpecificKeyStoreUtil keyStoreUtil = new TestSpecificKeyStoreUtil(); + + keyStoreUtil.setUpSslContext(); + + assertThat(SSLContext.getDefault(), is(keyStoreUtil.recordedSslContext)); + + assertThat(keyStoreUtil.recordedTrustManagers, arrayWithSize(1)); + assertThat(((CollectionTrustManager) keyStoreUtil.recordedTrustManagers[0]).getAcceptedIssuers(), + not(emptyArray())); + + CollectionTrustManager tm = (CollectionTrustManager) keyStoreUtil.recordedTrustManagers[0]; + + assertThat(tm.getTrustManagers(), hasSize(1)); // only the properties-based store + + assertThat( + Arrays.stream(tm.getTrustManagers().get(0).getAcceptedIssuers()) + .map(X509Certificate::getSubjectX500Principal).map(X500Principal::getName).toList(), + hasItem("CN=Test,C=DE")); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores, hasSize(1)); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(0).manager(), is(tm.getTrustManagers().get(0))); + // null caused KeyManagerFactory to load default system properties + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(0).store(), is(nullValue())); + + assertThat(keyStoreUtil.recordedKeyManagers, emptyArray()); + } + + @Test + @EnabledOnOs({ OS.WINDOWS, OS.MAC }) + public void loadTrustManagers_TrustSystemPropertiesPointToPlatformSpecificKeystore() throws Exception { + if (OS.WINDOWS.equals(OS.current())) { + System.setProperty("javax.net.ssl.trustStore", "NONE"); + System.setProperty("javax.net.ssl.trustStoreType", "Windows-ROOT"); + } else if (OS.MAC.equals(OS.current())) { + System.setProperty("javax.net.ssl.trustStore", "NONE"); + System.setProperty("javax.net.ssl.trustStoreType", "KeychainStore"); + System.setProperty("javax.net.ssl.trustStoreProvider", "Apple"); + } + + TestSpecificKeyStoreUtil keyStoreUtil = new TestSpecificKeyStoreUtil(); + + keyStoreUtil.setUpSslContext(); + + assertThat(SSLContext.getDefault(), is(keyStoreUtil.recordedSslContext)); + + assertThat(keyStoreUtil.recordedTrustManagers, arrayWithSize(1)); + + CollectionTrustManager tm = (CollectionTrustManager) keyStoreUtil.recordedTrustManagers[0]; + + assertThat(tm.getTrustManagers(), hasSize(1)); // only the properties-based store + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores, hasSize(1)); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(0).manager(), is(tm.getTrustManagers().get(0))); + assertThat(keyStoreUtil.createdTrustManagersAndKeyStores.get(0).store(), is(nullValue())); + + if (OS.WINDOWS.equals(OS.current())) { + assertThat(((CollectionTrustManager) keyStoreUtil.recordedTrustManagers[0]).getAcceptedIssuers(), + not(emptyArray())); + assertThat( + Arrays.stream(tm.getTrustManagers().get(0).getAcceptedIssuers()) + .map(X509Certificate::getSubjectX500Principal).map(X500Principal::getName).toList(), + hasItem(matchesRegex("(?i).*digicert.*root.*"))); + } else if (OS.MAC.equals(OS.current())) { + // Apple KeychainStore only includes the 'System' certificates + // (enterprise/admin managed) + // but not the 'System Roots' ones (public CAs). + // There's nothing guaranteed / deterministic in the 'System' on CI machines + // that we could check for here... + } + } + + @Test + public void loadKeyManagers_Default() throws Exception { + + TestSpecificKeyStoreUtil keyStoreUtil = new TestSpecificKeyStoreUtil(); + + keyStoreUtil.setUpSslContext(); + + assertThat(SSLContext.getDefault(), is(keyStoreUtil.recordedSslContext)); + + assertThat(keyStoreUtil.recordedKeyManagers, emptyArray()); + assertThat(keyStoreUtil.createdKeyManagersAndKeyStores, hasSize(0)); + } + + @Test + public void loadKeyManagers_KeySystemPropertiesPointToCustomKeyStore() throws Exception { + + System.setProperty("javax.net.ssl.keyStore", copyResourceToTempDirAndGetPath("keystore.p12")); + System.setProperty("javax.net.ssl.keyStorePassword", "verysecret"); + System.setProperty("javax.net.ssl.keyStoreType", "PKCS12"); + + TestSpecificKeyStoreUtil keyStoreUtil = new TestSpecificKeyStoreUtil(); + + keyStoreUtil.setUpSslContext(); + + assertThat(SSLContext.getDefault(), is(keyStoreUtil.recordedSslContext)); + + assertThat(keyStoreUtil.recordedKeyManagers, arrayWithSize(1)); + assertThat(keyStoreUtil.recordedKeyManagers[0], instanceOf(X509KeyManager.class)); + + X509KeyManager km = (X509KeyManager) keyStoreUtil.recordedKeyManagers[0]; + + assertThat(keyStoreUtil.createdKeyManagersAndKeyStores, hasSize(1)); + assertThat(keyStoreUtil.createdKeyManagersAndKeyStores.get(0).manager(), is(km)); + assertThat(keyStoreUtil.createdKeyManagersAndKeyStores.get(0).store().getType(), is("PKCS12")); + + assertThat(km.getPrivateKey("test.key"), not(nullValue())); + } + + @Test + public void optOutViaSystemProperty() throws Exception { + System.setProperty("eclipse.load.os.trust.store.by.default", "false"); + + TestSpecificKeyStoreUtil keyStoreUtil = new TestSpecificKeyStoreUtil(); + + keyStoreUtil.setUpSslContext(); + + assertThat(SSLContext.getDefault(), is(previousSslContext)); + } + + private String copyResourceToTempDirAndGetPath(String resourceName) throws IOException { + Path file = tempDir.resolve(resourceName); + Files.copy(getClass().getResourceAsStream(resourceName), file, StandardCopyOption.REPLACE_EXISTING); + return file.toAbsolutePath().toString(); + } + + private static final class TestSpecificKeyStoreUtil extends KeyStoreUtil { + + public TestSpecificKeyStoreUtil() { + super(Platform.getOS()); + } + + public static record X509TrustManagerAndKeyStore(X509TrustManager manager, KeyStore store) { + } + + public static record X509KeyManagerAndKeyStore(X509KeyManager manager, KeyStore store) { + } + + public TrustManager[] recordedTrustManagers; + public KeyManager[] recordedKeyManagers; + public SSLContext recordedSslContext; + public final List createdTrustManagersAndKeyStores = new ArrayList<>(); + public final List createdKeyManagersAndKeyStores = new ArrayList<>(); + + @Override + protected X509TrustManager createX509TrustManager(KeyStore keyStore) throws GeneralSecurityException { + X509TrustManager manager = super.createX509TrustManager(keyStore); + this.createdTrustManagersAndKeyStores.add(new X509TrustManagerAndKeyStore(manager, keyStore)); + return manager; + } + + @Override + protected X509KeyManager createX509KeyManager(KeyStore keyStore, char[] password) + throws GeneralSecurityException { + X509KeyManager manager = super.createX509KeyManager(keyStore, password); + this.createdKeyManagersAndKeyStores.add(new X509KeyManagerAndKeyStore(manager, keyStore)); + return manager; + } + + @Override + protected void initSSLContext(SSLContext context, TrustManager[] trustManagers, KeyManager[] keyManagers, + SecureRandom random) throws KeyManagementException { + this.recordedTrustManagers = trustManagers; + this.recordedKeyManagers = keyManagers; + this.recordedSslContext = context; + } + } + +} \ No newline at end of file diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/keystore.p12 b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b86cf756e4e22be98b3a2ac35f5828fbd4e09d14 GIT binary patch literal 2472 zcmY*Zc{~#gAKzxyrirw8ay4g!%r#cZoVllx<$N5ul6!J(naMmDk1J=65=FVsWjV@` z`^Y`YRfr6aJDwh&&-=bTpZAa7^*w%neo++Ia}baXMSB+S)Ll;C31A)tW(uaj z43A+w6otL=|0wo+Foiw$7|uGbG|0*Sy5Jl@P#%RHdkm_hTz)y&IZ>7wSFL)+h*mQuIe;nPfl}xO+KF##FXc^^EAPc<9eN!>j)d7o(SF}A|9i>KFJGjO zSGQfuNL=7b@Y$l&g+S!E$n;ud%Xw=_){b0BSGB7CWyYzVM6s|!c1Dx+mK$U*PsVZB zBk3#qlpep)2k)XdNL0oP{~LNwEDCtjcO6qpS_3j|D+7mlVsUCTc|W;R=wYbq(M-zc zWnzE+!1|++TW_m7_??f^&uA#c(H+j14XP1ue{;n}rk_cNwdtto5oze;GGm zrp_qBcpJ}Uuzv~`KLFzu<+L8B+ovnx^M-mST4asiY^$N;C!CFT6R&@JdX>@HX4~xO z6ar45DyviwfK4;{npu14oSsT5T`*n^Wzb=J!b-8|c05a@3>~Ce5>`?4=Wh|bxD|Z+ z3Uk1^>@jPzkFDZP{cAht@-wHuXCKKlRO|ol){Wc+%a*8lmONFUD_{4o@op1mw4c$`x1U2dxJx3kQWp z#-+ZJG?_bUM&C8Jx%OzpiX&88bx4e)(KId83H~z?_h^m?g|ry(^(05>1{Pa1Qy=XT z<`3(+2OhJW{q5vBCL+1g-JC+A(h)^V5RFKjl;MOk&{&;&{*UGP^r!!we$dy#MS&JTHE!?3Q6i88&W4C9d4 z;_n<*7C)GyU+w&4B}`7VZm3Kyh{wHrG_^RoLJ6Veof(iZPH$k?E8+sc+!;33ljjXj zk_+%#Eygpu+tl@IS<#D}x`PRS42d_K*2wJo8eds>Yr3!3Xm$(62p za8N7~?tB@znk=#7ip0LwDp)gYrU~Cmi$OuLUcQXnFgiwd9_j+|E0^RoaAc;cb4YTL^^sf)n@dXy6z-_XEkRfRNgllanR9gOG2;;54aVh?lk?vn0KLZ_kx zJ4>!I$2NV)n{wb{Qq|xH@iYspscUoyh-Mr-)mas#zk$Abc)F;R6OoF^Y)z9!&wQ*E zNIpI0V9sxA7ZZ%Eq+dAMJ70RQ^JX(W>k4i~tjpa|o7zjpaHV8q!jbA1@pm*d0iYpW zTa8c6wC|b;h?z{@cNI+)T#CI~mUxsY8ZJ1F6TPQRmU#H7+UkBoikLd@-+dB_{5K!k z$|7vHoE+sALQ}L?c1!gRwx5qp7LKV1>Mdng6Jm)xLxAf^sx{Kjg${3djBDtG&nfah zyk$FJc?eIJ^Uk{37V`$sQ-P0FNZz^Golh7 z&Vn!}lZc{?*xA7xQ-__j%I@}rv~>}Acb<5eF|jcXdDtno36h z>!RG*T{`H>cF5yyMOfht{|{RC^#bbyBqxxKp9U=G_;CcOdOJL6gfnQkrMybO(lSHh z!NsSpletbFL*FYjO7p!jSL%%jp^!aYk5Emj%;mXvDj#x<7g)e)>J#*`1a^~nf_ARg zfww$Req!tEB@z8^`aYryRZmzEPktw%gIcq$z?Xn6q1@iVQ8IC6{X&9#2M8x6l^Kg+ zBaE;mfAPM7|ElvhT4DEX_N%snZ)?V^lAARMBZ*6LG4}Nlj!(y!AwMnhW)}@Q@b-9y zxR*G(ruSx;R&-u%r5{gfh@Nqnm2cArwkc(ypCrV6>sN|`WYEaTaiq)pLARH>Ifs1h z%(&gZW2H+7c5DPi&f2u;t_DN*+$NWUphXn@XPe^>WnII$lrRd%#!Lm5xA1a+Sm$=q z4b(N12nq&R1-;%5hd5%2RqbTGa^OTIr3!4gLoOk`JZolNo*>OQ}z2Pz0*vh z#`$K+mUG%}Z&UZ&4d^xdv$mo|`Qw%R_W=>j+GpPi=T9-;`*pTh`s>s7>t?myxOK9} zErn~Jfc!N(ZuOMwdXtWnEt0C?oMLk2$*1#$rC_VMQLi;F?JelJ z%6;ZeX;W*Pj`R21!r>lwmni(WB%#pI)2FDp=6AIG)oF*GpL$R#oS@~2GX+r-TIx<@9QUgWzsETGVr>FUaMeSRjwY=l@x%?D&+mz1{Tb$Q4n0|LmvOXZ+Y1xEVOmm*gJd!tB zeVdE>GQ$&Jj+C?RvD5DAT3wgZHBH(m*u=|ZyXybd$+~JsKke+Fe5>z{605cDB-M>q zFQ5Cn^Up;tY5xPhZvwJ}Ecztp@a~$h`sS;B_78oZ?e4yJ!AFcW{P3R>6Sps!V9$IT E09HyoX8-^I literal 0 HcmV?d00001 From cf11e5d6b26660afde3143961b533cf7ae14dd77 Mon Sep 17 00:00:00 2001 From: Sebastian Ratz Date: Tue, 4 Nov 2025 09:26:09 +0000 Subject: [PATCH 2/4] fix PlatformURLSessionTest --- .../META-INF/MANIFEST.MF | 7 +++++-- .../internal/runtime/PlatformURLSessionTest.java | 11 ++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF b/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF index c37cf4d1da8..cb71c3f255f 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF +++ b/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF @@ -14,7 +14,9 @@ Require-Bundle: org.junit, org.eclipse.test.performance;resolution:=optional, org.eclipse.core.runtime;bundle-version="3.29.0", org.eclipse.core.tests.harness;bundle-version="3.11.0" -Import-Package: org.assertj.core.api, +Import-Package: net.bytebuddy, + net.bytebuddy.agent, + org.assertj.core.api, org.junit.jupiter.api;version="[5.14.0,6.0.0)", org.junit.jupiter.api.condition;version="[5.14.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.14.0,6.0.0)", @@ -22,7 +24,8 @@ Import-Package: org.assertj.core.api, org.junit.jupiter.api.io;version="[5.14.0,6.0.0)", org.junit.platform.suite.api;version="[1.14.0,2.0.0)", org.mockito, - org.mockito.stubbing + org.mockito.stubbing, + org.objenesis Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-17 Plugin-Class: org.eclipse.core.tests.runtime.RuntimeTestsPlugin diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java index 0e185d1dd71..375d22aead4 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java +++ b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java @@ -31,6 +31,8 @@ import java.net.URL; import java.net.URLConnection; import java.util.stream.Collectors; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.agent.ByteBuddyAgent; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.Platform; import org.eclipse.core.tests.harness.session.SessionTestExtension; @@ -42,6 +44,8 @@ import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; +import org.objenesis.Objenesis; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestInstance(Lifecycle.PER_CLASS) @@ -49,7 +53,12 @@ public class PlatformURLSessionTest { @RegisterExtension SessionTestExtension extension = SessionTestExtension.forPlugin(PI_RUNTIME_TESTS) - .withCustomization(SessionTestExtension.createCustomConfiguration().setCascaded().setReadOnly()).create(); + .withCustomization(SessionTestExtension.createCustomConfiguration().setCascaded().setReadOnly() + .addBundle(Mockito.class) + .addBundle(ByteBuddy.class) + .addBundle(ByteBuddyAgent.class) + .addBundle(Objenesis.class) + ).create(); private static final String CONFIG_URL = "platform:/config/" + PI_RUNTIME_TESTS + "/"; private static final String DATA_CHILD = "child"; From e4ca19088c598b00785f35d3ca05a83d940a004b Mon Sep 17 00:00:00 2001 From: Sebastian Ratz Date: Tue, 4 Nov 2025 09:26:59 +0000 Subject: [PATCH 3/4] add missing super call --- .../eclipse/core/internal/runtime/CollectionTrustManager.java | 2 +- .../eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java index 2fb6ae08a8d..e0e41f49f91 100644 --- a/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java +++ b/runtime/bundles/org.eclipse.core.runtime/src/org/eclipse/core/internal/runtime/CollectionTrustManager.java @@ -79,4 +79,4 @@ public X509Certificate[] getAcceptedIssuers() { .filter(Objects::nonNull).flatMap(Arrays::stream).toArray(X509Certificate[]::new); } -} \ No newline at end of file +} diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java index 7f7ab41de1a..2b2fe787e3a 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java +++ b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/KeyStoreUtilTest.java @@ -320,7 +320,8 @@ protected void initSSLContext(SSLContext context, TrustManager[] trustManagers, this.recordedTrustManagers = trustManagers; this.recordedKeyManagers = keyManagers; this.recordedSslContext = context; + super.initSSLContext(context, trustManagers, keyManagers, random); } } -} \ No newline at end of file +} From c72d4b3681e2b20b38e02c3c5c25e9721671e4fc Mon Sep 17 00:00:00 2001 From: Sebastian Ratz Date: Tue, 4 Nov 2025 10:28:46 +0100 Subject: [PATCH 4/4] CollectionTrustManagerTest: Mockito -> custom stub --- .../META-INF/MANIFEST.MF | 9 +- .../runtime/CollectionTrustManagerTest.java | 267 ++++++++++++++---- .../runtime/PlatformURLSessionTest.java | 11 +- 3 files changed, 210 insertions(+), 77 deletions(-) diff --git a/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF b/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF index cb71c3f255f..2487c0c862b 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF +++ b/runtime/tests/org.eclipse.core.tests.runtime/META-INF/MANIFEST.MF @@ -14,18 +14,13 @@ Require-Bundle: org.junit, org.eclipse.test.performance;resolution:=optional, org.eclipse.core.runtime;bundle-version="3.29.0", org.eclipse.core.tests.harness;bundle-version="3.11.0" -Import-Package: net.bytebuddy, - net.bytebuddy.agent, - org.assertj.core.api, +Import-Package: org.assertj.core.api, org.junit.jupiter.api;version="[5.14.0,6.0.0)", org.junit.jupiter.api.condition;version="[5.14.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.14.0,6.0.0)", org.junit.jupiter.api.function;version="[5.14.0,6.0.0)", org.junit.jupiter.api.io;version="[5.14.0,6.0.0)", - org.junit.platform.suite.api;version="[1.14.0,2.0.0)", - org.mockito, - org.mockito.stubbing, - org.objenesis + org.junit.platform.suite.api;version="[1.14.0,2.0.0)" Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-17 Plugin-Class: org.eclipse.core.tests.runtime.RuntimeTestsPlugin diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java index c86068f3d80..ac5c36feb96 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java +++ b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/CollectionTrustManagerTest.java @@ -17,32 +17,30 @@ import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.sameInstance; import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import java.math.BigInteger; +import java.security.Principal; +import java.security.PublicKey; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Set; import javax.net.ssl.X509TrustManager; import org.eclipse.core.internal.runtime.CollectionTrustManager; import org.junit.Test; - +@SuppressWarnings("restriction") public class CollectionTrustManagerTest { @Test public void testAcceptedIssuers() throws Exception { - X509Certificate[] acceptedIssuers1 = { mock(X509Certificate.class), mock(X509Certificate.class) }; - X509Certificate[] acceptedIssuers2 = { mock(X509Certificate.class), mock(X509Certificate.class) }; - X509TrustManager mock1 = mock(X509TrustManager.class); - X509TrustManager mock2 = mock(X509TrustManager.class); - when(mock1.getAcceptedIssuers()).thenReturn(acceptedIssuers1); - when(mock2.getAcceptedIssuers()).thenReturn(acceptedIssuers2); - CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(mock1, mock2)); + X509Certificate[] acceptedIssuers1 = { new StubX509Certificate(), new StubX509Certificate() }; + X509Certificate[] acceptedIssuers2 = { new StubX509Certificate(), new StubX509Certificate() }; + X509TrustManager manager1 = new StubX509TrustManager(List.of(acceptedIssuers1)); + X509TrustManager manager2 = new StubX509TrustManager(List.of(acceptedIssuers2)); + CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(manager1, manager2)); X509Certificate[] allAcceptedIssuers = collectionTrustManager.getAcceptedIssuers(); @@ -52,30 +50,16 @@ public void testAcceptedIssuers() throws Exception { @Test public void testCheckClientTrusted() throws Exception { - X509Certificate[] chainTrustedBy1 = { mock(X509Certificate.class) }; - X509Certificate[] chainTrustedBy2 = { mock(X509Certificate.class) }; - X509Certificate[] chainTrustedByNone = { mock(X509Certificate.class) }; - X509Certificate[] chainTrustedByBoth = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedBy1 = { new StubX509Certificate() }; + X509Certificate[] chainTrustedBy2 = { new StubX509Certificate() }; + X509Certificate[] chainTrustedByNone = { new StubX509Certificate() }; + X509Certificate[] chainTrustedByBoth = { new StubX509Certificate() }; String authType = "testAuthType"; - CertificateException exceptionFrom1 = new CertificateException("exception from 1"); - CertificateException exceptionFrom2 = new CertificateException("exception from 2"); - - X509TrustManager mock1 = mock(X509TrustManager.class); - doThrow(IllegalStateException.class).when(mock1).checkClientTrusted(any(), any()); - doNothing().when(mock1).checkClientTrusted(eq(chainTrustedBy1), eq(authType)); - doNothing().when(mock1).checkClientTrusted(eq(chainTrustedByBoth), eq(authType)); - doThrow(exceptionFrom1).when(mock1).checkClientTrusted(eq(chainTrustedBy2), eq(authType)); - doThrow(exceptionFrom1).when(mock1).checkClientTrusted(eq(chainTrustedByNone), eq(authType)); - - X509TrustManager mock2 = mock(X509TrustManager.class); - doThrow(IllegalStateException.class).when(mock2).checkClientTrusted(any(), any()); - doNothing().when(mock2).checkClientTrusted(eq(chainTrustedBy2), eq(authType)); - doNothing().when(mock2).checkClientTrusted(eq(chainTrustedByBoth), eq(authType)); - doThrow(exceptionFrom2).when(mock2).checkClientTrusted(eq(chainTrustedBy1), eq(authType)); - doThrow(exceptionFrom2).when(mock2).checkClientTrusted(eq(chainTrustedByNone), eq(authType)); + StubX509TrustManager manager1 = new StubX509TrustManager(List.of(chainTrustedBy1, chainTrustedByBoth)); + StubX509TrustManager manager2 = new StubX509TrustManager(List.of(chainTrustedBy2, chainTrustedByBoth)); - CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(mock1, mock2)); + CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(manager1, manager2)); collectionTrustManager.checkClientTrusted(chainTrustedBy1, authType); collectionTrustManager.checkClientTrusted(chainTrustedBy2, authType); @@ -84,36 +68,22 @@ public void testCheckClientTrusted() throws Exception { CertificateException exception = assertThrows(CertificateException.class, () -> { collectionTrustManager.checkClientTrusted(chainTrustedByNone, authType); }); - assertThat(exception, sameInstance(exceptionFrom1)); // first in the list - assertThat(exception.getSuppressed(), arrayContaining(sameInstance(exceptionFrom2))); // second, suppressed + assertThat(exception, sameInstance(manager1.exception)); // first in the list + assertThat(exception.getSuppressed(), arrayContaining(sameInstance(manager2.exception))); // second, suppressed } @Test public void testCheckServerTrusted() throws Exception { - X509Certificate[] chainTrustedBy1 = { mock(X509Certificate.class) }; - X509Certificate[] chainTrustedBy2 = { mock(X509Certificate.class) }; - X509Certificate[] chainTrustedByNone = { mock(X509Certificate.class) }; - X509Certificate[] chainTrustedByBoth = { mock(X509Certificate.class) }; + X509Certificate[] chainTrustedBy1 = { new StubX509Certificate() }; + X509Certificate[] chainTrustedBy2 = { new StubX509Certificate() }; + X509Certificate[] chainTrustedByNone = { new StubX509Certificate() }; + X509Certificate[] chainTrustedByBoth = { new StubX509Certificate() }; String authType = "testAuthType"; - CertificateException exceptionFrom1 = new CertificateException("exception from 1"); - CertificateException exceptionFrom2 = new CertificateException("exception from 2"); + StubX509TrustManager manager1 = new StubX509TrustManager(List.of(chainTrustedBy1, chainTrustedByBoth)); + StubX509TrustManager manager2 = new StubX509TrustManager(List.of(chainTrustedBy2, chainTrustedByBoth)); - X509TrustManager mock1 = mock(X509TrustManager.class); - doThrow(IllegalStateException.class).when(mock1).checkServerTrusted(any(), any()); - doNothing().when(mock1).checkServerTrusted(eq(chainTrustedBy1), eq(authType)); - doNothing().when(mock1).checkServerTrusted(eq(chainTrustedByBoth), eq(authType)); - doThrow(exceptionFrom1).when(mock1).checkServerTrusted(eq(chainTrustedBy2), eq(authType)); - doThrow(exceptionFrom1).when(mock1).checkServerTrusted(eq(chainTrustedByNone), eq(authType)); - - X509TrustManager mock2 = mock(X509TrustManager.class); - doThrow(IllegalStateException.class).when(mock2).checkServerTrusted(any(), any()); - doNothing().when(mock2).checkServerTrusted(eq(chainTrustedBy2), eq(authType)); - doNothing().when(mock2).checkServerTrusted(eq(chainTrustedByBoth), eq(authType)); - doThrow(exceptionFrom2).when(mock2).checkServerTrusted(eq(chainTrustedBy1), eq(authType)); - doThrow(exceptionFrom2).when(mock2).checkServerTrusted(eq(chainTrustedByNone), eq(authType)); - - CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(mock1, mock2)); + CollectionTrustManager collectionTrustManager = new CollectionTrustManager(Arrays.asList(manager1, manager2)); collectionTrustManager.checkServerTrusted(chainTrustedBy1, authType); collectionTrustManager.checkServerTrusted(chainTrustedBy2, authType); @@ -122,7 +92,184 @@ public void testCheckServerTrusted() throws Exception { CertificateException exception = assertThrows(CertificateException.class, () -> { collectionTrustManager.checkServerTrusted(chainTrustedByNone, authType); }); - assertThat(exception, sameInstance(exceptionFrom1)); // first in the list - assertThat(exception.getSuppressed(), arrayContaining(sameInstance(exceptionFrom2))); // second, suppressed + assertThat(exception, sameInstance(manager1.exception)); // first in the list + assertThat(exception.getSuppressed(), arrayContaining(sameInstance(manager2.exception))); // second, suppressed + } + + private static class StubX509TrustManager implements X509TrustManager { + + public final CertificateException exception = new CertificateException(); + private List trusted; + + public StubX509TrustManager(List trusted) { + this.trusted = trusted; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + for (X509Certificate[] trustedChain : trusted) { + if (Arrays.equals(chain, trustedChain)) { + return; + } + } + throw exception; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + for (X509Certificate[] trustedChain : trusted) { + if (Arrays.equals(chain, trustedChain)) { + return; + } + } + throw exception; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trusted.stream().flatMap(Arrays::stream).toArray(X509Certificate[]::new); + } + + } + + private static class StubX509Certificate extends X509Certificate { + + @Override + public boolean hasUnsupportedCriticalExtension() { + throw new IllegalStateException(); + } + + @Override + public Set getCriticalExtensionOIDs() { + throw new IllegalStateException(); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + throw new IllegalStateException(); + } + + @Override + public byte[] getExtensionValue(String oid) { + throw new IllegalStateException(); + } + + @Override + public void checkValidity() { + throw new IllegalStateException(); + } + + @Override + public void checkValidity(Date date) { + throw new IllegalStateException(); + } + + @Override + public int getVersion() { + throw new IllegalStateException(); + } + + @Override + public BigInteger getSerialNumber() { + throw new IllegalStateException(); + } + + @SuppressWarnings("deprecation") + @Override + public Principal getIssuerDN() { + throw new IllegalStateException(); + } + + @SuppressWarnings("deprecation") + @Override + public Principal getSubjectDN() { + throw new IllegalStateException(); + } + + @Override + public Date getNotBefore() { + throw new IllegalStateException(); + } + + @Override + public Date getNotAfter() { + throw new IllegalStateException(); + } + + @Override + public byte[] getTBSCertificate() { + throw new IllegalStateException(); + } + + @Override + public byte[] getSignature() { + throw new IllegalStateException(); + } + + @Override + public String getSigAlgName() { + throw new IllegalStateException(); + } + + @Override + public String getSigAlgOID() { + throw new IllegalStateException(); + } + + @Override + public byte[] getSigAlgParams() { + throw new IllegalStateException(); + } + + @Override + public boolean[] getIssuerUniqueID() { + throw new IllegalStateException(); + } + + @Override + public boolean[] getSubjectUniqueID() { + throw new IllegalStateException(); + } + + @Override + public boolean[] getKeyUsage() { + throw new IllegalStateException(); + } + + @Override + public int getBasicConstraints() { + throw new IllegalStateException(); + } + + @Override + public byte[] getEncoded() { + throw new IllegalStateException(); + } + + @Override + public void verify(PublicKey key) { + throw new IllegalStateException(); + } + + @Override + public void verify(PublicKey key, String sigProvider) { + throw new IllegalStateException(); + } + + @Override + public PublicKey getPublicKey() { + throw new IllegalStateException(); + } + + @Override + public String toString() { + return Integer.toHexString(System.identityHashCode(this)); + } + + @Override + public boolean equals(Object other) { + return other == this; + } + } } diff --git a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java index 375d22aead4..0e185d1dd71 100644 --- a/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java +++ b/runtime/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/internal/runtime/PlatformURLSessionTest.java @@ -31,8 +31,6 @@ import java.net.URL; import java.net.URLConnection; import java.util.stream.Collectors; -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.agent.ByteBuddyAgent; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.Platform; import org.eclipse.core.tests.harness.session.SessionTestExtension; @@ -44,8 +42,6 @@ import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.RegisterExtension; -import org.mockito.Mockito; -import org.objenesis.Objenesis; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @TestInstance(Lifecycle.PER_CLASS) @@ -53,12 +49,7 @@ public class PlatformURLSessionTest { @RegisterExtension SessionTestExtension extension = SessionTestExtension.forPlugin(PI_RUNTIME_TESTS) - .withCustomization(SessionTestExtension.createCustomConfiguration().setCascaded().setReadOnly() - .addBundle(Mockito.class) - .addBundle(ByteBuddy.class) - .addBundle(ByteBuddyAgent.class) - .addBundle(Objenesis.class) - ).create(); + .withCustomization(SessionTestExtension.createCustomConfiguration().setCascaded().setReadOnly()).create(); private static final String CONFIG_URL = "platform:/config/" + PI_RUNTIME_TESTS + "/"; private static final String DATA_CHILD = "child";