From cb49fc28b0e2fc2594e9857900feb181731fe678 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 17:34:25 +0300 Subject: [PATCH 01/26] IGNITE-29209 Add fresh SSL contexts with certificate metadata --- .../ignite/ssl/AbstractSslContextFactory.java | 141 ++++++++- .../ignite/ssl/DelegatingSSLContextSpi.java | 5 +- .../apache/ignite/ssl/SSLContextWrapper.java | 51 ++- .../apache/ignite/ssl/SslContextFactory.java | 7 +- .../ignite/ssl/SslContextMetadataTest.java | 299 ++++++++++++++++++ .../ignite/testsuites/SecurityTestSuite.java | 2 + 6 files changed, 493 insertions(+), 12 deletions(-) create mode 100644 modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index 77dedf84eba78..2cf140ba2e2b7 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -18,7 +18,12 @@ package org.apache.ignite.ssl; import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Enumeration; import java.util.concurrent.atomic.AtomicReference; import javax.cache.configuration.Factory; import javax.net.ssl.KeyManager; @@ -28,11 +33,11 @@ import javax.net.ssl.TrustManager; import org.apache.ignite.IgniteException; import org.apache.ignite.internal.util.typedef.internal.A; +import org.jetbrains.annotations.Nullable; /** - * Represents abstract implementation of SSL Context Factory that caches the result of the first successful - * attempt to create an {@link SSLContext} and always returns it as a result of further invocations of the - * {@link AbstractSslContextFactory#create()}} method. + * Base SSL context factory that caches the result of the first successful {@link #create()} call. + * Use {@link #createNew()} to build another context without changing the cached one. */ public abstract class AbstractSslContextFactory implements Factory { /** */ @@ -118,26 +123,29 @@ public void setProtocols(String... protocols) { private SSLContext createSslContext() throws SSLException { checkParameters(); - KeyManager[] keyMgrs = createKeyManagers(); + KeyMaterial material = createKeyMaterial(); TrustManager[] trustMgrs = createTrustManagers(); try { SSLContext ctx = SSLContext.getInstance(proto); + SSLParameters sslParameters = null; + if (cipherSuites != null || protocols != null) { - SSLParameters sslParameters = new SSLParameters(); + sslParameters = new SSLParameters(); if (cipherSuites != null) sslParameters.setCipherSuites(cipherSuites); if (protocols != null) sslParameters.setProtocols(protocols); - - ctx = new SSLContextWrapper(ctx, sslParameters); } - ctx.init(keyMgrs, trustMgrs, null); + if (sslParameters != null || material.chain != null || material.privateKeyMissing) + ctx = new SSLContextWrapper(ctx, sslParameters, material.chain, material.privateKeyMissing); + + ctx.init(material.keyMgrs, trustMgrs, null); return ctx; } @@ -172,6 +180,18 @@ protected void checkNullParameter(Object param, String name) throws SSLException */ protected abstract KeyManager[] createKeyManagers() throws SSLException; + /** + * Creates key managers and their certificate chain from the same load of the key material. The default calls + * {@link #createKeyManagers()} and leaves the chain unknown. Subclasses that can provide certificate metadata should + * override this method and supply the chain without loading the material again. + * + * @return Key managers and their chain, if known. + * @throws SSLException If the key material could not be loaded. + */ + protected KeyMaterial createKeyMaterial() throws SSLException { + return new KeyMaterial(createKeyManagers(), null); + } + /** * @return Created Trust Managers. * @throws SSLException If Trust Managers could not be created. @@ -196,4 +216,109 @@ protected void checkNullParameter(Object param, String name) throws SSLException return ctx; } + + /** + * Builds a new SSL context from the current factory settings without reading or replacing the context cached by + * {@link #create()}. + *

+ * A subclass that builds its context in its own {@link #create()} must override this method too and build the context from + * the same sources. This implementation calls {@link #checkParameters()}, {@link #createKeyMaterial()} and + * {@link #createTrustManagers()}; it does not call {@link #create()}. + *

+ * A subclass that only changes the key source can override {@link #createKeyMaterial()} instead. When building a context + * through another mechanism, attach its loaded certificate chain, if known, using + * {@link SSLContextWrapper#SSLContextWrapper(SSLContext, SSLParameters, X509Certificate[])}. + * + * @return New initialized SSL context. + * @throws IgniteException If the context could not be created. + */ + public SSLContext createNew() { + try { + return createSslContext(); + } + catch (SSLException e) { + throw new IgniteException(e); + } + } + + /** Key managers and the certificate chain loaded with them. */ + protected static final class KeyMaterial { + /** */ + private final KeyManager[] keyMgrs; + + /** */ + private final X509Certificate[] chain; + + /** Whether the loaded store is known to contain no private key. */ + private final boolean privateKeyMissing; + + /** + * @param keyMgrs Key managers, or {@code null} to use the provider's defaults. + * @param chain Chain of the single private key, own certificate first, or {@code null} if unknown. + */ + public KeyMaterial(@Nullable KeyManager[] keyMgrs, @Nullable X509Certificate[] chain) { + this(keyMgrs, chain, false); + } + + /** + * @param keyMgrs Key managers. + * @param chain Certificate chain, if known. + * @param privateKeyMissing Whether the loaded store contains no private key. + */ + private KeyMaterial(@Nullable KeyManager[] keyMgrs, @Nullable X509Certificate[] chain, boolean privateKeyMissing) { + this.keyMgrs = keyMgrs == null ? null : keyMgrs.clone(); + this.chain = chain == null || chain.length == 0 ? null : chain.clone(); + this.privateKeyMissing = privateKeyMissing; + } + + /** @return Key managers, or {@code null} to use the provider's defaults. */ + public @Nullable KeyManager[] keyManagers() { + return keyMgrs == null ? null : keyMgrs.clone(); + } + + /** + * Takes the chain from the store used to create the key managers. A store with one private key and an X.509 chain + * supplies that chain. More than one private key, or a chain containing a non-X.509 certificate, leaves the chain + * unknown. A store without a private key records that fact separately from an unknown chain. + * + * @param keyMgrs Key managers initialized from the store. + * @param store Already loaded key store. + * @return Key managers and the chain of the single private key, if known. + * @throws KeyStoreException If the store cannot be read. + */ + public static KeyMaterial fromKeyStore(KeyManager[] keyMgrs, KeyStore store) throws KeyStoreException { + String keyAlias = null; + + for (Enumeration aliases = store.aliases(); aliases.hasMoreElements(); ) { + String alias = aliases.nextElement(); + + if (!store.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) + continue; + + if (keyAlias != null) + return new KeyMaterial(keyMgrs, null); + + keyAlias = alias; + } + + if (keyAlias == null) + return new KeyMaterial(keyMgrs, null, true); + + Certificate[] certs = store.getCertificateChain(keyAlias); + + if (certs == null || certs.length == 0) + return new KeyMaterial(keyMgrs, null); + + X509Certificate[] chain = new X509Certificate[certs.length]; + + for (int i = 0; i < certs.length; i++) { + if (!(certs[i] instanceof X509Certificate)) + return new KeyMaterial(keyMgrs, null); + + chain[i] = (X509Certificate)certs[i]; + } + + return new KeyMaterial(keyMgrs, chain); + } + } } diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java index 360e92239c6bb..170cd5280561a 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java @@ -51,12 +51,13 @@ class DelegatingSSLContextSpi extends SSLContextSpi { /** {@inheritDoc} */ @Override protected SSLSocketFactory engineGetSocketFactory() { - return new SSLSocketFactoryWrapper(delegate.getSocketFactory(), parameters); + return parameters == null ? delegate.getSocketFactory() : new SSLSocketFactoryWrapper(delegate.getSocketFactory(), parameters); } /** {@inheritDoc} */ @Override protected SSLServerSocketFactory engineGetServerSocketFactory() { - return new SSLServerSocketFactoryWrapper(delegate.getServerSocketFactory(), parameters); + return parameters == null ? delegate.getServerSocketFactory() + : new SSLServerSocketFactoryWrapper(delegate.getServerSocketFactory(), parameters); } /** {@inheritDoc} */ diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java index 67bc834d0886c..22b44b85db234 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java @@ -17,20 +17,69 @@ package org.apache.ignite.ssl; +import java.security.cert.X509Certificate; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; +import org.jetbrains.annotations.Nullable; /** - * Wrapper for {@link SSLContext} that extend source context with custom SSL parameters. + * Wrapper for {@link SSLContext} with custom SSL parameters and the certificate chain loaded with the context. */ public class SSLContextWrapper extends SSLContext { + /** Chain loaded with the delegate, or {@code null} if unknown. */ + private final X509Certificate[] chain; + + /** Whether the loaded key material is known to contain no private key. */ + private final boolean privateKeyMissing; + /** * @param delegate Wrapped SSL context. * @param sslParameters Extended SSL parameters. */ public SSLContextWrapper(SSLContext delegate, SSLParameters sslParameters) { + this(delegate, sslParameters, delegate instanceof SSLContextWrapper + ? ((SSLContextWrapper)delegate).getCertificateChain() : null, + delegate instanceof SSLContextWrapper && ((SSLContextWrapper)delegate).isPrivateKeyMissing()); + } + + /** + * Wraps a context with its loaded certificate chain. The chain describes the key material, not the outcome of a TLS + * handshake. The delegate must not be reinitialized with different key material after wrapping. + * + * @param delegate Wrapped SSL context. + * @param sslParameters Extended SSL parameters, or {@code null} to keep the delegate's parameters. + * @param chain Chain of the single private key, own certificate first, or {@code null} if unknown. + */ + public SSLContextWrapper(SSLContext delegate, @Nullable SSLParameters sslParameters, @Nullable X509Certificate[] chain) { + this(delegate, sslParameters, chain, false); + } + + /** + * Wraps a context with its loaded certificate chain and records whether that key material has no private key. + * + * @param delegate Wrapped SSL context. + * @param sslParameters Extended SSL parameters, or {@code null} to keep the delegate's parameters. + * @param chain Chain of the single private key, own certificate first, or {@code null} if unknown. + * @param privateKeyMissing {@code true} only if the loaded material is known to contain no private key. Do not pass + * {@code true} just because the chain is unknown: the material may contain several keys or use custom key managers. + */ + public SSLContextWrapper(SSLContext delegate, @Nullable SSLParameters sslParameters, @Nullable X509Certificate[] chain, + boolean privateKeyMissing) { super(new DelegatingSSLContextSpi(delegate, sslParameters), delegate.getProvider(), delegate.getProtocol()); + + this.chain = chain == null || chain.length == 0 ? null : chain.clone(); + this.privateKeyMissing = privateKeyMissing; + } + + /** @return Chain loaded with the context, own certificate first, or {@code null} if unknown. */ + public @Nullable X509Certificate[] getCertificateChain() { + return chain == null ? null : chain.clone(); + } + + /** @return {@code true} only if the loaded material is known to contain no private key. */ + public boolean isPrivateKeyMissing() { + return privateKeyMissing; } } diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java index af7d140b01f2f..e8525771f6250 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java @@ -260,6 +260,11 @@ public static TrustManager getDisabledTrustManager() { /** {@inheritDoc} */ @Override protected final KeyManager[] createKeyManagers() throws SSLException { + return createKeyMaterial().keyManagers(); + } + + /** {@inheritDoc} */ + @Override protected final KeyMaterial createKeyMaterial() throws SSLException { try { KeyManagerFactory keyMgrFactory = KeyManagerFactory.getInstance(keyAlgorithm); @@ -267,7 +272,7 @@ public static TrustManager getDisabledTrustManager() { keyMgrFactory.init(keyStore, keyStorePwd); - return keyMgrFactory.getKeyManagers(); + return KeyMaterial.fromKeyStore(keyMgrFactory.getKeyManagers(), keyStore); } catch (NoSuchAlgorithmException e) { throw new SSLException("Unsupported key algorithm: " + keyAlgorithm, e); diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java new file mode 100644 index 0000000000000..cc26636f98ae4 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -0,0 +1,299 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.ssl; + +import java.io.InputStream; +import java.net.ServerSocket; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; +import org.apache.ignite.testframework.GridTestUtils; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; + +/** Tests that certificate metadata belongs to the key material used to create a context. */ +public class SslContextMetadataTest extends GridCommonAbstractTest { + /** Store and key password of the test fixtures. */ + private static final char[] PASSWORD = "123456".toCharArray(); + + /** {@code createNew()} loads the store once, preserves the cached context and attaches the new chain to the new context. */ + @Test + public void testMetadataUsesOneLoadAndStaysWithContext() throws Exception { + KeyStore firstStore = loadStore("node01"); + StoreFactory factory = new StoreFactory(firstStore); + + SSLContext first = factory.create(); + + assertArrayEquals(chain(firstStore, "node01"), chain(first)); + assertEquals(1, factory.loads); + + factory.store = loadStore("node02"); + + SSLContext next = factory.createNew(); + + assertNotSame(first, next); + assertSame(first, factory.create()); + assertEquals(2, factory.loads); + assertArrayEquals(chain(firstStore, "node01"), chain(first)); + assertArrayEquals(chain(factory.store, "node02"), chain(next)); + } + + /** Both factory methods retain certificate metadata when they also apply TLS parameters. */ + @Test + public void testMetadataWithTlsParameters() throws Exception { + KeyStore store = loadStore("node01"); + StoreFactory factory = new StoreFactory(store); + String[] protocols = {"TLSv1.2"}; + String[] ciphers = {"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}; + + factory.setProtocols(protocols); + factory.setCipherSuites(ciphers); + + for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { + assertArrayEquals(chain(store, "node01"), chain(ctx)); + + SSLEngine engine = ctx.createSSLEngine(); + + assertArrayEquals(protocols, engine.getEnabledProtocols()); + assertArrayEquals(ciphers, engine.getEnabledCipherSuites()); + } + } + + /** More than one private key leaves the chain unknown without preventing context creation. */ + @Test + public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { + assertNull(chain(new StoreFactory(loadStore("node0102")).create())); + } + + /** Trusted certificates and secret keys do not count as private keys. */ + @Test + public void testOnlyPrivateKeyEntriesCount() throws Exception { + KeyStore keys = loadStore("node01"); + KeyStore store = KeyStore.getInstance("JCEKS"); + + store.load(null, PASSWORD); + store.setCertificateEntry("trusted", keys.getCertificate("node01")); + store.setEntry("secret", new KeyStore.SecretKeyEntry(new SecretKeySpec(new byte[16], "AES")), + new KeyStore.PasswordProtection(PASSWORD)); + + StoreFactory factory = new StoreFactory(store); + + assertNull(chain(factory.create())); + + store.setKeyEntry("key", keys.getKey("node01", PASSWORD), PASSWORD, keys.getCertificateChain("node01")); + + assertArrayEquals(chain(keys, "node01"), chain(factory.createNew())); + } + + /** Trust-only contexts can still be built for clients, and another parameters wrapper preserves the missing-key marker. */ + @Test + public void testTrustOnlyContextCreation() throws Exception { + StoreFactory factory = new StoreFactory(loadStore("trustboth")); + + for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { + assertNull(chain(ctx)); + assertTrue(((SSLContextWrapper)ctx).isPrivateKeyMissing()); + + SSLParameters params = new SSLParameters(); + + params.setProtocols(new String[] {"TLSv1.2"}); + + SSLContextWrapper outer = new SSLContextWrapper(ctx, params); + + assertTrue(outer.isPrivateKeyMissing()); + assertArrayEquals(new String[] {"TLSv1.2"}, outer.createSSLEngine().getEnabledProtocols()); + } + } + + /** A subclass that does not override createKeyMaterial() still supplies validation and managers on every build. */ + @Test + public void testLegacyOverridesAreUsedWithoutMetadata() { + LegacyFactory factory = new LegacyFactory(); + + SSLContext first = factory.create(); + + assertSame(first, factory.create()); + assertNotSame(first, factory.createNew()); + assertNull(chain(first)); + assertEquals(2, factory.checks); + assertEquals(2, factory.keys); + assertEquals(2, factory.trust); + } + + /** Neither the source arrays nor arrays returned to callers can replace the recorded key managers and chain. */ + @Test + public void testKeyMaterialDefensiveCopies() throws Exception { + KeyManager mgr = new KeyManager() { }; + KeyManager[] mgrs = {mgr}; + X509Certificate[] expected = chain(loadStore("node01"), "node01"); + X509Certificate[] input = expected.clone(); + AbstractSslContextFactory.KeyMaterial material = new AbstractSslContextFactory.KeyMaterial(mgrs, input); + + mgrs[0] = null; + input[0] = null; + material.keyManagers()[0] = null; + + assertSame(mgr, material.keyManagers()[0]); + + LegacyFactory factory = new LegacyFactory() { + /** {@inheritDoc} */ + @Override protected KeyMaterial createKeyMaterial() { + return material; + } + }; + + assertArrayEquals(expected, chain(factory.create())); + } + + /** Neither callers nor another parameters wrapper can replace a context's recorded chain. */ + @Test + public void testWrapperPreservesAndCopiesChain() throws Exception { + X509Certificate[] expected = chain(loadStore("node01"), "node01"); + X509Certificate[] input = expected.clone(); + SSLContext delegate = SSLContext.getInstance("TLS"); + + delegate.init(null, null, null); + + SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, input); + + input[0] = null; + wrapper.getCertificateChain()[0] = null; + + assertArrayEquals(expected, wrapper.getCertificateChain()); + + SSLParameters params = new SSLParameters(); + + params.setProtocols(new String[] {"TLSv1.2"}); + + SSLContextWrapper outer = new SSLContextWrapper(wrapper, params); + + assertArrayEquals(expected, outer.getCertificateChain()); + assertArrayEquals(new String[] {"TLSv1.2"}, outer.createSSLEngine().getEnabledProtocols()); + } + + /** Adding metadata without TLS parameters preserves the default engine settings and socket factory classes. */ + @Test + public void testMetadataWrapperPreservesContextBehavior() throws Exception { + SSLContext delegate = SSLContext.getInstance("TLS"); + + delegate.init(null, null, null); + + SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, chain(loadStore("node01"), "node01")); + SSLEngine engine = wrapper.createSSLEngine(); + + assertArrayEquals(delegate.createSSLEngine().getEnabledProtocols(), engine.getEnabledProtocols()); + assertArrayEquals(delegate.createSSLEngine().getEnabledCipherSuites(), engine.getEnabledCipherSuites()); + assertEquals(delegate.getSocketFactory().getClass(), wrapper.getSocketFactory().getClass()); + assertEquals(delegate.getServerSocketFactory().getClass(), wrapper.getServerSocketFactory().getClass()); + + try (ServerSocket sock = wrapper.getServerSocketFactory().createServerSocket()) { + assertFalse(sock.isBound()); + } + } + + /** @return Loaded test key store. */ + private static KeyStore loadStore(String name) throws Exception { + KeyStore store = KeyStore.getInstance("JKS"); + + try (InputStream in = Files.newInputStream(Path.of(GridTestUtils.keyStorePath(name)))) { + store.load(in, PASSWORD); + } + + return store; + } + + /** @return Chain stored for the alias. */ + private static X509Certificate[] chain(KeyStore store, String alias) throws Exception { + Certificate[] chain = store.getCertificateChain(alias); + + return Arrays.copyOf(chain, chain.length, X509Certificate[].class); + } + + /** @return Chain attached to the context, if known. */ + private static X509Certificate[] chain(SSLContext ctx) { + return ctx instanceof SSLContextWrapper ? ((SSLContextWrapper)ctx).getCertificateChain() : null; + } + + /** A file factory whose existing store-loading hook supplies the test material. */ + private static class StoreFactory extends SslContextFactory { + /** */ + private KeyStore store; + + /** */ + private int loads; + + /** */ + StoreFactory(KeyStore store) { + this.store = store; + + setKeyStoreType(store.getType()); + setKeyStoreFilePath("test-store"); + setKeyStorePassword(PASSWORD); + setTrustManagers(getDisabledTrustManager()); + } + + /** {@inheritDoc} */ + @Override protected KeyStore loadKeyStore(String type, String path, char[] pwd) { + loads++; + + return store; + } + } + + /** A subclass that implements the manager hooks without overriding createKeyMaterial(). */ + private static class LegacyFactory extends AbstractSslContextFactory { + /** */ + private int checks; + + /** */ + private int keys; + + /** */ + private int trust; + + /** {@inheritDoc} */ + @Override protected void checkParameters() { + checks++; + } + + /** {@inheritDoc} */ + @Override protected KeyManager[] createKeyManagers() { + keys++; + + return null; + } + + /** {@inheritDoc} */ + @Override protected TrustManager[] createTrustManagers() { + trust++; + + return null; + } + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java index da768a22290fb..3df66ce6f6b2f 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java @@ -79,6 +79,7 @@ import org.apache.ignite.internal.thread.context.OperationContextAttributesTest; import org.apache.ignite.plugin.security.SecurityBasicPermissionSetSerializationTest; import org.apache.ignite.ssl.MultipleSSLContextsTest; +import org.apache.ignite.ssl.SslContextMetadataTest; import org.apache.ignite.tools.junit.JUnitTeamcityReporter; import org.junit.BeforeClass; import org.junit.runner.RunWith; @@ -141,6 +142,7 @@ IgniteSecurityProcessorTest.class, MultipleSSLContextsTest.class, + SslContextMetadataTest.class, MaintenanceModeNodeSecurityTest.class, ServiceAuthorizationTest.class, ServiceStaticConfigTest.class, From ae4204169a2ce22833d1856624423ef74c96d665 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 20:12:15 +0300 Subject: [PATCH 02/26] IGNITE-29209 Preserve peer addresses in metadata wrappers --- .../java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java | 2 +- .../java/org/apache/ignite/ssl/SslContextMetadataTest.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java index 170cd5280561a..060ef9f83d6e6 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java @@ -72,7 +72,7 @@ class DelegatingSSLContextSpi extends SSLContextSpi { /** {@inheritDoc} */ @Override protected SSLEngine engineCreateSSLEngine(String s, int i) { - final SSLEngine engine = delegate.createSSLEngine(); + final SSLEngine engine = delegate.createSSLEngine(s, i); if (parameters != null) engine.setSSLParameters(parameters); diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index cc26636f98ae4..27c12f0349208 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -197,7 +197,7 @@ public void testWrapperPreservesAndCopiesChain() throws Exception { assertArrayEquals(new String[] {"TLSv1.2"}, outer.createSSLEngine().getEnabledProtocols()); } - /** Adding metadata without TLS parameters preserves the default engine settings and socket factory classes. */ + /** Adding metadata without TLS parameters preserves the peer address, default settings and socket factory classes. */ @Test public void testMetadataWrapperPreservesContextBehavior() throws Exception { SSLContext delegate = SSLContext.getInstance("TLS"); @@ -205,8 +205,10 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { delegate.init(null, null, null); SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, chain(loadStore("node01"), "node01")); - SSLEngine engine = wrapper.createSSLEngine(); + SSLEngine engine = wrapper.createSSLEngine("peer.example", 10800); + assertEquals("peer.example", engine.getPeerHost()); + assertEquals(10800, engine.getPeerPort()); assertArrayEquals(delegate.createSSLEngine().getEnabledProtocols(), engine.getEnabledProtocols()); assertArrayEquals(delegate.createSSLEngine().getEnabledCipherSuites(), engine.getEnabledCipherSuites()); assertEquals(delegate.getSocketFactory().getClass(), wrapper.getSocketFactory().getClass()); From 0802865a3e198fd6e98ef188ed29665dcf1cd77a Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 21:20:01 +0300 Subject: [PATCH 03/26] IGNITE-29209 Name SSLEngine peer parameters host and port --- .../java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java index 060ef9f83d6e6..193aa6c6a2013 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java @@ -71,8 +71,8 @@ class DelegatingSSLContextSpi extends SSLContextSpi { } /** {@inheritDoc} */ - @Override protected SSLEngine engineCreateSSLEngine(String s, int i) { - final SSLEngine engine = delegate.createSSLEngine(s, i); + @Override protected SSLEngine engineCreateSSLEngine(String host, int port) { + final SSLEngine engine = delegate.createSSLEngine(host, port); if (parameters != null) engine.setSSLParameters(parameters); From cf8143f403bcdc19e2d7451adfd6cdc8eb30e1e4 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 21:53:44 +0300 Subject: [PATCH 04/26] IGNITE-29209 Allow factories to implement only createKeyMaterial --- .../ignite/ssl/AbstractSslContextFactory.java | 17 ++++-- .../apache/ignite/ssl/SslContextFactory.java | 7 ++- .../ignite/ssl/SslContextMetadataTest.java | 60 ++++++++++++++++++- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index 2cf140ba2e2b7..07599a5301ad7 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -175,15 +175,20 @@ protected void checkNullParameter(Object param, String name) throws SSLException protected abstract void checkParameters() throws SSLException; /** - * @return Created Key Managers. - * @throws SSLException If Key Managers could not be created. + * Creates key managers without certificate metadata. + * + * @return Created key managers. + * @throws SSLException If this method is not overridden or key managers cannot be created. + * @deprecated Override {@link #createKeyMaterial()} to return key managers with their certificate chain. */ - protected abstract KeyManager[] createKeyManagers() throws SSLException; + @Deprecated + protected KeyManager[] createKeyManagers() throws SSLException { + throw new SSLException("Subclass must override createKeyMaterial() or createKeyManagers()"); + } /** - * Creates key managers and their certificate chain from the same load of the key material. The default calls - * {@link #createKeyManagers()} and leaves the chain unknown. Subclasses that can provide certificate metadata should - * override this method and supply the chain without loading the material again. + * Creates key managers and their certificate chain from the same load of the key material. New subclasses should + * override this method. The default calls the legacy {@link #createKeyManagers()} hook and leaves the chain unknown. * * @return Key managers and their chain, if known. * @throws SSLException If the key material could not be loaded. diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java index e8525771f6250..05940885e1224 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java @@ -258,7 +258,12 @@ public static TrustManager getDisabledTrustManager() { return new DisabledX509TrustManager(); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + * + * @deprecated Use {@link #createKeyMaterial()} to retain certificate metadata. + */ + @Deprecated @Override protected final KeyManager[] createKeyManagers() throws SSLException { return createKeyMaterial().keyManagers(); } diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 27c12f0349208..01395a1625314 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -29,8 +29,10 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManager; +import org.apache.ignite.IgniteException; import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.junit.Test; @@ -146,7 +148,44 @@ public void testLegacyOverridesAreUsedWithoutMetadata() { assertEquals(2, factory.trust); } - /** Neither the source arrays nor arrays returned to callers can replace the recorded key managers and chain. */ + /** A subclass can still obtain key managers from the file factory's legacy hook. */ + @Test + public void testLegacyKeyManagerAdapter() throws Exception { + StoreFactory factory = new StoreFactory(loadStore("node01")); + KeyManager[] mgrs = factory.legacyKeyManagers(); + + assertNotNull(mgrs); + assertTrue(mgrs.length > 0); + assertEquals(1, factory.loads); + } + + /** A factory that implements neither key-material hook reports the missing implementation. */ + @Test + public void testMissingKeyMaterialImplementation() { + AbstractSslContextFactory factory = new AbstractSslContextFactory() { + /** {@inheritDoc} */ + @Override protected void checkParameters() { + // No parameters are required by this factory. + } + + /** {@inheritDoc} */ + @Override protected TrustManager[] createTrustManagers() { + return null; + } + }; + + String msg = "Subclass must override createKeyMaterial() or createKeyManagers()"; + + Throwable err = GridTestUtils.assertThrows(log, factory::create, IgniteException.class, msg); + + assertTrue(err.getCause() instanceof SSLException); + + err = GridTestUtils.assertThrows(log, factory::createNew, IgniteException.class, msg); + + assertTrue(err.getCause() instanceof SSLException); + } + + /** A subclass can supply material directly, and callers cannot replace its recorded managers or chain through arrays. */ @Test public void testKeyMaterialDefensiveCopies() throws Exception { KeyManager mgr = new KeyManager() { }; @@ -161,14 +200,25 @@ public void testKeyMaterialDefensiveCopies() throws Exception { assertSame(mgr, material.keyManagers()[0]); - LegacyFactory factory = new LegacyFactory() { + AbstractSslContextFactory factory = new AbstractSslContextFactory() { + /** {@inheritDoc} */ + @Override protected void checkParameters() { + // No parameters are required by this factory. + } + /** {@inheritDoc} */ @Override protected KeyMaterial createKeyMaterial() { return material; } + + /** {@inheritDoc} */ + @Override protected TrustManager[] createTrustManagers() { + return null; + } }; assertArrayEquals(expected, chain(factory.create())); + assertArrayEquals(expected, chain(factory.createNew())); } /** Neither callers nor another parameters wrapper can replace a context's recorded chain. */ @@ -266,6 +316,12 @@ private static class StoreFactory extends SslContextFactory { return store; } + + /** @return Key managers supplied by the inherited legacy hook. */ + @SuppressWarnings("deprecation") + private KeyManager[] legacyKeyManagers() throws SSLException { + return super.createKeyManagers(); + } } /** A subclass that implements the manager hooks without overriding createKeyMaterial(). */ From 42d5fe4cbecbdb58a4c8cdcb4a6a509ddc6f968d Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 22:13:03 +0300 Subject: [PATCH 05/26] IGNITE-29209 Verify missing-key metadata and clarify wrapper constructors --- .../apache/ignite/ssl/SSLContextWrapper.java | 6 +++++ .../ignite/ssl/SslContextMetadataTest.java | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java index 22b44b85db234..af2110151203b 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java @@ -33,6 +33,9 @@ public class SSLContextWrapper extends SSLContext { private final boolean privateKeyMissing; /** + * Wraps a context with custom SSL parameters. If the delegate is an {@code SSLContextWrapper}, copies its + * certificate chain and missing-private-key flag. + * * @param delegate Wrapped SSL context. * @param sslParameters Extended SSL parameters. */ @@ -45,6 +48,8 @@ public SSLContextWrapper(SSLContext delegate, SSLParameters sslParameters) { /** * Wraps a context with its loaded certificate chain. The chain describes the key material, not the outcome of a TLS * handshake. The delegate must not be reinitialized with different key material after wrapping. + * This constructor uses the supplied chain and sets the missing-private-key flag to {@code false}. + * It does not copy metadata from the delegate. * * @param delegate Wrapped SSL context. * @param sslParameters Extended SSL parameters, or {@code null} to keep the delegate's parameters. @@ -56,6 +61,7 @@ public SSLContextWrapper(SSLContext delegate, @Nullable SSLParameters sslParamet /** * Wraps a context with its loaded certificate chain and records whether that key material has no private key. + * Both metadata values come from the arguments, even if the delegate is an {@code SSLContextWrapper}. * * @param delegate Wrapped SSL context. * @param sslParameters Extended SSL parameters, or {@code null} to keep the delegate's parameters. diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 01395a1625314..bb9c816b10228 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -53,6 +53,7 @@ public void testMetadataUsesOneLoadAndStaysWithContext() throws Exception { SSLContext first = factory.create(); assertArrayEquals(chain(firstStore, "node01"), chain(first)); + assertFalse(privateKeyMissing(first)); assertEquals(1, factory.loads); factory.store = loadStore("node02"); @@ -64,6 +65,7 @@ public void testMetadataUsesOneLoadAndStaysWithContext() throws Exception { assertEquals(2, factory.loads); assertArrayEquals(chain(firstStore, "node01"), chain(first)); assertArrayEquals(chain(factory.store, "node02"), chain(next)); + assertFalse(privateKeyMissing(next)); } /** Both factory methods retain certificate metadata when they also apply TLS parameters. */ @@ -87,10 +89,15 @@ public void testMetadataWithTlsParameters() throws Exception { } } - /** More than one private key leaves the chain unknown without preventing context creation. */ + /** Multiple private keys leave the chain unknown but are not reported as missing. */ @Test public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { - assertNull(chain(new StoreFactory(loadStore("node0102")).create())); + StoreFactory factory = new StoreFactory(loadStore("node0102")); + + for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { + assertNull(chain(ctx)); + assertFalse(privateKeyMissing(ctx)); + } } /** Trusted certificates and secret keys do not count as private keys. */ @@ -106,11 +113,17 @@ public void testOnlyPrivateKeyEntriesCount() throws Exception { StoreFactory factory = new StoreFactory(store); - assertNull(chain(factory.create())); + SSLContext withoutKey = factory.create(); + + assertNull(chain(withoutKey)); + assertTrue(privateKeyMissing(withoutKey)); store.setKeyEntry("key", keys.getKey("node01", PASSWORD), PASSWORD, keys.getCertificateChain("node01")); - assertArrayEquals(chain(keys, "node01"), chain(factory.createNew())); + SSLContext withKey = factory.createNew(); + + assertArrayEquals(chain(keys, "node01"), chain(withKey)); + assertFalse(privateKeyMissing(withKey)); } /** Trust-only contexts can still be built for clients, and another parameters wrapper preserves the missing-key marker. */ @@ -292,6 +305,11 @@ private static X509Certificate[] chain(SSLContext ctx) { return ctx instanceof SSLContextWrapper ? ((SSLContextWrapper)ctx).getCertificateChain() : null; } + /** @return Whether the context reports that its key material has no private key. */ + private static boolean privateKeyMissing(SSLContext ctx) { + return ctx instanceof SSLContextWrapper && ((SSLContextWrapper)ctx).isPrivateKeyMissing(); + } + /** A file factory whose existing store-loading hook supplies the test material. */ private static class StoreFactory extends SslContextFactory { /** */ From fdb2a29f3bb41abc22809f96bac0924ffebff62b Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 22:50:15 +0300 Subject: [PATCH 06/26] IGNITE-29209 Keep legacy key-manager loading outside KeyMaterial --- .../ignite/ssl/AbstractSslContextFactory.java | 19 +++++++++++-------- .../ignite/ssl/SslContextMetadataTest.java | 4 +++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index 07599a5301ad7..237aaeb2bf4d4 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -124,6 +124,9 @@ private SSLContext createSslContext() throws SSLException { checkParameters(); KeyMaterial material = createKeyMaterial(); + KeyManager[] keyMgrs = material == null ? createKeyManagers() : material.keyMgrs; + X509Certificate[] chain = material == null ? null : material.chain; + boolean privateKeyMissing = material != null && material.privateKeyMissing; TrustManager[] trustMgrs = createTrustManagers(); @@ -142,10 +145,10 @@ private SSLContext createSslContext() throws SSLException { sslParameters.setProtocols(protocols); } - if (sslParameters != null || material.chain != null || material.privateKeyMissing) - ctx = new SSLContextWrapper(ctx, sslParameters, material.chain, material.privateKeyMissing); + if (sslParameters != null || chain != null || privateKeyMissing) + ctx = new SSLContextWrapper(ctx, sslParameters, chain, privateKeyMissing); - ctx.init(material.keyMgrs, trustMgrs, null); + ctx.init(keyMgrs, trustMgrs, null); return ctx; } @@ -187,14 +190,14 @@ protected KeyManager[] createKeyManagers() throws SSLException { } /** - * Creates key managers and their certificate chain from the same load of the key material. New subclasses should - * override this method. The default calls the legacy {@link #createKeyManagers()} hook and leaves the chain unknown. + * Creates key managers and their certificate metadata from the same load. New subclasses must return the loaded material. + * The default returns {@code null} so existing subclasses can still supply keys through {@link #createKeyManagers()}. * - * @return Key managers and their chain, if known. + * @return Loaded key material, or {@code null} to use the legacy key-manager hook. * @throws SSLException If the key material could not be loaded. */ - protected KeyMaterial createKeyMaterial() throws SSLException { - return new KeyMaterial(createKeyManagers(), null); + protected @Nullable KeyMaterial createKeyMaterial() throws SSLException { + return null; } /** diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index bb9c816b10228..ae9466abe009f 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -148,9 +148,11 @@ public void testTrustOnlyContextCreation() throws Exception { /** A subclass that does not override createKeyMaterial() still supplies validation and managers on every build. */ @Test - public void testLegacyOverridesAreUsedWithoutMetadata() { + public void testLegacyOverridesAreUsedWithoutMetadata() throws Exception { LegacyFactory factory = new LegacyFactory(); + assertNull(factory.createKeyMaterial()); + SSLContext first = factory.create(); assertSame(first, factory.create()); From 78053e973eb03f077e402eee775d6baca4d51650 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 23:39:22 +0300 Subject: [PATCH 07/26] IGNITE-29209 Keep legacy adaptation in createKeyMaterial --- .../ignite/ssl/AbstractSslContextFactory.java | 19 ++++++++----------- .../ignite/ssl/SslContextMetadataTest.java | 4 +--- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index 237aaeb2bf4d4..d269abbd90722 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -124,9 +124,6 @@ private SSLContext createSslContext() throws SSLException { checkParameters(); KeyMaterial material = createKeyMaterial(); - KeyManager[] keyMgrs = material == null ? createKeyManagers() : material.keyMgrs; - X509Certificate[] chain = material == null ? null : material.chain; - boolean privateKeyMissing = material != null && material.privateKeyMissing; TrustManager[] trustMgrs = createTrustManagers(); @@ -145,10 +142,10 @@ private SSLContext createSslContext() throws SSLException { sslParameters.setProtocols(protocols); } - if (sslParameters != null || chain != null || privateKeyMissing) - ctx = new SSLContextWrapper(ctx, sslParameters, chain, privateKeyMissing); + if (sslParameters != null || material.chain != null || material.privateKeyMissing) + ctx = new SSLContextWrapper(ctx, sslParameters, material.chain, material.privateKeyMissing); - ctx.init(keyMgrs, trustMgrs, null); + ctx.init(material.keyMgrs, trustMgrs, null); return ctx; } @@ -190,14 +187,14 @@ protected KeyManager[] createKeyManagers() throws SSLException { } /** - * Creates key managers and their certificate metadata from the same load. New subclasses must return the loaded material. - * The default returns {@code null} so existing subclasses can still supply keys through {@link #createKeyManagers()}. + * Creates key managers and their certificate metadata from the same load. New subclasses should override this method. + * The default adapts {@link #createKeyManagers()} for existing subclasses and leaves the certificate chain unknown. * - * @return Loaded key material, or {@code null} to use the legacy key-manager hook. + * @return Loaded key material. * @throws SSLException If the key material could not be loaded. */ - protected @Nullable KeyMaterial createKeyMaterial() throws SSLException { - return null; + protected KeyMaterial createKeyMaterial() throws SSLException { + return new KeyMaterial(createKeyManagers(), null); } /** diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index ae9466abe009f..bb9c816b10228 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -148,11 +148,9 @@ public void testTrustOnlyContextCreation() throws Exception { /** A subclass that does not override createKeyMaterial() still supplies validation and managers on every build. */ @Test - public void testLegacyOverridesAreUsedWithoutMetadata() throws Exception { + public void testLegacyOverridesAreUsedWithoutMetadata() { LegacyFactory factory = new LegacyFactory(); - assertNull(factory.createKeyMaterial()); - SSLContext first = factory.create(); assertSame(first, factory.create()); From bda9a61a0095af49ae985df09ddef730b79689bf Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Mon, 26 Oct 2026 23:50:54 +0300 Subject: [PATCH 08/26] IGNITE-29209 Format SSL context metadata delegation. --- .../main/java/org/apache/ignite/ssl/SSLContextWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java index af2110151203b..af182ac8435cc 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java @@ -40,8 +40,8 @@ public class SSLContextWrapper extends SSLContext { * @param sslParameters Extended SSL parameters. */ public SSLContextWrapper(SSLContext delegate, SSLParameters sslParameters) { - this(delegate, sslParameters, delegate instanceof SSLContextWrapper - ? ((SSLContextWrapper)delegate).getCertificateChain() : null, + this(delegate, sslParameters, + delegate instanceof SSLContextWrapper ? ((SSLContextWrapper)delegate).getCertificateChain() : null, delegate instanceof SSLContextWrapper && ((SSLContextWrapper)delegate).isPrivateKeyMissing()); } From 65c8098c9703b69cbcb8c04d4ac5167c71b74a3d Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 00:50:25 +0300 Subject: [PATCH 09/26] IGNITE-29209 Test SSL metadata and legacy factory guarantees. --- .../ignite/ssl/SslContextMetadataTest.java | 72 +++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index bb9c816b10228..fdeb374866928 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -18,7 +18,6 @@ package org.apache.ignite.ssl; import java.io.InputStream; -import java.net.ServerSocket; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyStore; @@ -31,6 +30,8 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import org.apache.ignite.IgniteException; import org.apache.ignite.testframework.GridTestUtils; @@ -146,16 +147,28 @@ public void testTrustOnlyContextCreation() throws Exception { } } - /** A subclass that does not override createKeyMaterial() still supplies validation and managers on every build. */ + /** Legacy hooks run for each new context, but not when create() returns its cached context. */ @Test - public void testLegacyOverridesAreUsedWithoutMetadata() { + public void testLegacyHooksRunOncePerBuiltContext() { LegacyFactory factory = new LegacyFactory(); SSLContext first = factory.create(); - assertSame(first, factory.create()); - assertNotSame(first, factory.createNew()); assertNull(chain(first)); + assertEquals(1, factory.checks); + assertEquals(1, factory.keys); + assertEquals(1, factory.trust); + + assertSame(first, factory.create()); + + assertEquals(1, factory.checks); + assertEquals(1, factory.keys); + assertEquals(1, factory.trust); + + SSLContext next = factory.createNew(); + + assertNotSame(first, next); + assertNull(chain(next)); assertEquals(2, factory.checks); assertEquals(2, factory.keys); assertEquals(2, factory.trust); @@ -260,7 +273,32 @@ public void testWrapperPreservesAndCopiesChain() throws Exception { assertArrayEquals(new String[] {"TLSv1.2"}, outer.createSSLEngine().getEnabledProtocols()); } - /** Adding metadata without TLS parameters preserves the peer address, default settings and socket factory classes. */ + /** Explicit metadata takes precedence over the wrapped context's metadata. */ + @Test + public void testExplicitWrapperMetadataOverridesDelegateMetadata() throws Exception { + X509Certificate[] expectedChain = chain(loadStore("node01"), "node01"); + SSLContext delegate = SSLContext.getInstance("TLS"); + + delegate.init(null, null, null); + + SSLContextWrapper keyless = new SSLContextWrapper(delegate, null, null, true); + SSLContextWrapper withChain = new SSLContextWrapper(keyless, null, expectedChain); + + assertArrayEquals(expectedChain, withChain.getCertificateChain()); + assertFalse(withChain.isPrivateKeyMissing()); + + SSLContextWrapper unknownChain = new SSLContextWrapper(withChain, null, null); + + assertNull(unknownChain.getCertificateChain()); + assertFalse(unknownChain.isPrivateKeyMissing()); + + SSLContextWrapper explicitlyKeyless = new SSLContextWrapper(withChain, null, null, true); + + assertNull(explicitlyKeyless.getCertificateChain()); + assertTrue(explicitlyKeyless.isPrivateKeyMissing()); + } + + /** Adding metadata without TLS parameters preserves peer addresses, TLS defaults and unconnected socket creation. */ @Test public void testMetadataWrapperPreservesContextBehavior() throws Exception { SSLContext delegate = SSLContext.getInstance("TLS"); @@ -269,16 +307,26 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, chain(loadStore("node01"), "node01")); SSLEngine engine = wrapper.createSSLEngine("peer.example", 10800); + SSLEngine delegateEngine = delegate.createSSLEngine(); assertEquals("peer.example", engine.getPeerHost()); assertEquals(10800, engine.getPeerPort()); - assertArrayEquals(delegate.createSSLEngine().getEnabledProtocols(), engine.getEnabledProtocols()); - assertArrayEquals(delegate.createSSLEngine().getEnabledCipherSuites(), engine.getEnabledCipherSuites()); - assertEquals(delegate.getSocketFactory().getClass(), wrapper.getSocketFactory().getClass()); - assertEquals(delegate.getServerSocketFactory().getClass(), wrapper.getServerSocketFactory().getClass()); + assertArrayEquals(delegateEngine.getEnabledProtocols(), engine.getEnabledProtocols()); + assertArrayEquals(delegateEngine.getEnabledCipherSuites(), engine.getEnabledCipherSuites()); + + try (SSLSocket expected = (SSLSocket)delegate.getSocketFactory().createSocket(); + SSLSocket actual = (SSLSocket)wrapper.getSocketFactory().createSocket()) { + assertFalse(actual.isConnected()); + assertFalse(actual.isBound()); + assertArrayEquals(expected.getEnabledProtocols(), actual.getEnabledProtocols()); + assertArrayEquals(expected.getEnabledCipherSuites(), actual.getEnabledCipherSuites()); + } - try (ServerSocket sock = wrapper.getServerSocketFactory().createServerSocket()) { - assertFalse(sock.isBound()); + try (SSLServerSocket expected = (SSLServerSocket)delegate.getServerSocketFactory().createServerSocket(); + SSLServerSocket actual = (SSLServerSocket)wrapper.getServerSocketFactory().createServerSocket()) { + assertFalse(actual.isBound()); + assertArrayEquals(expected.getEnabledProtocols(), actual.getEnabledProtocols()); + assertArrayEquals(expected.getEnabledCipherSuites(), actual.getEnabledCipherSuites()); } } From f72363123da33b5db0aa11093466ad1b8f0e3a60 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 01:06:24 +0300 Subject: [PATCH 10/26] IGNITE-29209 Generate explicit key material for SSL metadata tests. --- modules/core/pom.xml | 7 + .../ignite/ssl/SslContextMetadataTest.java | 82 +++--- .../TestCertificateAuthority.java | 247 ++++++++++++++++++ 3 files changed, 304 insertions(+), 32 deletions(-) create mode 100644 modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java diff --git a/modules/core/pom.xml b/modules/core/pom.xml index bbdf058dea191..47fd6cc45de8b 100644 --- a/modules/core/pom.xml +++ b/modules/core/pom.xml @@ -158,6 +158,13 @@ test + + org.bouncycastle + bcpkix-jdk18on + 1.86 + test + + org.eclipse.jetty.ee11 jetty-ee11-servlets diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index fdeb374866928..9ca68bbabb3d9 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -17,13 +17,11 @@ package org.apache.ignite.ssl; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.KeyStore; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.concurrent.TimeUnit; import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; @@ -33,8 +31,10 @@ import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; +import javax.security.auth.x500.X500Principal; import org.apache.ignite.IgniteException; import org.apache.ignite.testframework.GridTestUtils; +import org.apache.ignite.testframework.TestCertificateAuthority; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.junit.Test; @@ -42,46 +42,56 @@ /** Tests that certificate metadata belongs to the key material used to create a context. */ public class SslContextMetadataTest extends GridCommonAbstractTest { - /** Store and key password of the test fixtures. */ - private static final char[] PASSWORD = "123456".toCharArray(); + /** Store and key password of the generated test material. */ + private static final char[] PASSWORD = TestCertificateAuthority.PWD; + + /** Authority that signs the test certificates. */ + private TestCertificateAuthority ca; + + /** {@inheritDoc} */ + @Override protected void beforeTest() throws Exception { + ca = new TestCertificateAuthority("metadata-ca"); + } /** {@code createNew()} loads the store once, preserves the cached context and attaches the new chain to the new context. */ @Test public void testMetadataUsesOneLoadAndStaysWithContext() throws Exception { - KeyStore firstStore = loadStore("node01"); + KeyStore firstStore = issueStore("first"); StoreFactory factory = new StoreFactory(firstStore); SSLContext first = factory.create(); - assertArrayEquals(chain(firstStore, "node01"), chain(first)); + assertArrayEquals(chain(firstStore, "first"), chain(first)); + assertEquals(new X500Principal("CN=first"), chain(first)[0].getSubjectX500Principal()); assertFalse(privateKeyMissing(first)); assertEquals(1, factory.loads); - factory.store = loadStore("node02"); + factory.store = issueStore("next"); SSLContext next = factory.createNew(); assertNotSame(first, next); assertSame(first, factory.create()); assertEquals(2, factory.loads); - assertArrayEquals(chain(firstStore, "node01"), chain(first)); - assertArrayEquals(chain(factory.store, "node02"), chain(next)); + assertArrayEquals(chain(firstStore, "first"), chain(first)); + assertArrayEquals(chain(factory.store, "next"), chain(next)); + assertEquals(new X500Principal("CN=next"), chain(next)[0].getSubjectX500Principal()); assertFalse(privateKeyMissing(next)); } /** Both factory methods retain certificate metadata when they also apply TLS parameters. */ @Test public void testMetadataWithTlsParameters() throws Exception { - KeyStore store = loadStore("node01"); + KeyStore store = issueStore("node"); StoreFactory factory = new StoreFactory(store); String[] protocols = {"TLSv1.2"}; - String[] ciphers = {"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}; + String[] ciphers = {"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}; factory.setProtocols(protocols); factory.setCipherSuites(ciphers); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertArrayEquals(chain(store, "node01"), chain(ctx)); + assertArrayEquals(chain(store, "node"), chain(ctx)); SSLEngine engine = ctx.createSSLEngine(); @@ -93,7 +103,12 @@ public void testMetadataWithTlsParameters() throws Exception { /** Multiple private keys leave the chain unknown but are not reported as missing. */ @Test public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { - StoreFactory factory = new StoreFactory(loadStore("node0102")); + KeyStore store = issueStore("first"); + KeyStore other = issueStore("second"); + + store.setKeyEntry("second", other.getKey("second", PASSWORD), PASSWORD, other.getCertificateChain("second")); + + StoreFactory factory = new StoreFactory(store); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { assertNull(chain(ctx)); @@ -104,11 +119,11 @@ public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { /** Trusted certificates and secret keys do not count as private keys. */ @Test public void testOnlyPrivateKeyEntriesCount() throws Exception { - KeyStore keys = loadStore("node01"); + KeyStore keys = issueStore("node"); KeyStore store = KeyStore.getInstance("JCEKS"); store.load(null, PASSWORD); - store.setCertificateEntry("trusted", keys.getCertificate("node01")); + store.setCertificateEntry("trusted", keys.getCertificate("node")); store.setEntry("secret", new KeyStore.SecretKeyEntry(new SecretKeySpec(new byte[16], "AES")), new KeyStore.PasswordProtection(PASSWORD)); @@ -119,18 +134,23 @@ public void testOnlyPrivateKeyEntriesCount() throws Exception { assertNull(chain(withoutKey)); assertTrue(privateKeyMissing(withoutKey)); - store.setKeyEntry("key", keys.getKey("node01", PASSWORD), PASSWORD, keys.getCertificateChain("node01")); + store.setKeyEntry("key", keys.getKey("node", PASSWORD), PASSWORD, keys.getCertificateChain("node")); SSLContext withKey = factory.createNew(); - assertArrayEquals(chain(keys, "node01"), chain(withKey)); + assertArrayEquals(chain(keys, "node"), chain(withKey)); assertFalse(privateKeyMissing(withKey)); } /** Trust-only contexts can still be built for clients, and another parameters wrapper preserves the missing-key marker. */ @Test public void testTrustOnlyContextCreation() throws Exception { - StoreFactory factory = new StoreFactory(loadStore("trustboth")); + KeyStore store = KeyStore.getInstance("PKCS12"); + + store.load(null, PASSWORD); + store.setCertificateEntry("trusted", issueStore("trusted").getCertificate("trusted")); + + StoreFactory factory = new StoreFactory(store); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { assertNull(chain(ctx)); @@ -155,6 +175,7 @@ public void testLegacyHooksRunOncePerBuiltContext() { SSLContext first = factory.create(); assertNull(chain(first)); + assertFalse(privateKeyMissing(first)); assertEquals(1, factory.checks); assertEquals(1, factory.keys); assertEquals(1, factory.trust); @@ -169,6 +190,7 @@ public void testLegacyHooksRunOncePerBuiltContext() { assertNotSame(first, next); assertNull(chain(next)); + assertFalse(privateKeyMissing(next)); assertEquals(2, factory.checks); assertEquals(2, factory.keys); assertEquals(2, factory.trust); @@ -177,7 +199,7 @@ public void testLegacyHooksRunOncePerBuiltContext() { /** A subclass can still obtain key managers from the file factory's legacy hook. */ @Test public void testLegacyKeyManagerAdapter() throws Exception { - StoreFactory factory = new StoreFactory(loadStore("node01")); + StoreFactory factory = new StoreFactory(issueStore("node")); KeyManager[] mgrs = factory.legacyKeyManagers(); assertNotNull(mgrs); @@ -216,7 +238,7 @@ public void testMissingKeyMaterialImplementation() { public void testKeyMaterialDefensiveCopies() throws Exception { KeyManager mgr = new KeyManager() { }; KeyManager[] mgrs = {mgr}; - X509Certificate[] expected = chain(loadStore("node01"), "node01"); + X509Certificate[] expected = chain(issueStore("node"), "node"); X509Certificate[] input = expected.clone(); AbstractSslContextFactory.KeyMaterial material = new AbstractSslContextFactory.KeyMaterial(mgrs, input); @@ -250,7 +272,7 @@ public void testKeyMaterialDefensiveCopies() throws Exception { /** Neither callers nor another parameters wrapper can replace a context's recorded chain. */ @Test public void testWrapperPreservesAndCopiesChain() throws Exception { - X509Certificate[] expected = chain(loadStore("node01"), "node01"); + X509Certificate[] expected = chain(issueStore("node"), "node"); X509Certificate[] input = expected.clone(); SSLContext delegate = SSLContext.getInstance("TLS"); @@ -276,7 +298,7 @@ public void testWrapperPreservesAndCopiesChain() throws Exception { /** Explicit metadata takes precedence over the wrapped context's metadata. */ @Test public void testExplicitWrapperMetadataOverridesDelegateMetadata() throws Exception { - X509Certificate[] expectedChain = chain(loadStore("node01"), "node01"); + X509Certificate[] expectedChain = chain(issueStore("node"), "node"); SSLContext delegate = SSLContext.getInstance("TLS"); delegate.init(null, null, null); @@ -305,7 +327,7 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { delegate.init(null, null, null); - SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, chain(loadStore("node01"), "node01")); + SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, chain(issueStore("node"), "node")); SSLEngine engine = wrapper.createSSLEngine("peer.example", 10800); SSLEngine delegateEngine = delegate.createSSLEngine(); @@ -330,15 +352,11 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { } } - /** @return Loaded test key store. */ - private static KeyStore loadStore(String name) throws Exception { - KeyStore store = KeyStore.getInstance("JKS"); - - try (InputStream in = Files.newInputStream(Path.of(GridTestUtils.keyStorePath(name)))) { - store.load(in, PASSWORD); - } + /** @return In-memory store with one private key and its certificate chain. */ + private KeyStore issueStore(String name) throws Exception { + long now = System.currentTimeMillis(); - return store; + return ca.issue(name, now - TimeUnit.HOURS.toMillis(1), now + TimeUnit.DAYS.toMillis(1)); } /** @return Chain stored for the alias. */ diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java new file mode 100644 index 0000000000000..15bf792574824 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.testframework; + +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.security.spec.ECGenParameterSpec; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; +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.security.auth.x500.X500Principal; +import org.apache.ignite.ssl.SSLContextWrapper; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.ExtendedKeyUsage; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +/** + * Certificate authority for tests that need explicit keys, trust relationships and certificate validity periods. + */ +public class TestCertificateAuthority { + /** Password of every store made here. */ + public static final char[] PWD = "123456".toCharArray(); + + /** */ + private static final SecureRandom RND = new SecureRandom(); + + /** */ + private final X500Principal name; + + /** */ + private final KeyPair keys; + + /** */ + private final X509Certificate cert; + + /** @param cn Common name of the authority. */ + public TestCertificateAuthority(String cn) throws Exception { + this(cn, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1), System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365)); + } + + /** + * @param cn Common name of the authority. + * @param notBefore Time the authority's certificate becomes valid; rounded down to a second. + * @param notAfter Time the authority's certificate expires; rounded down to a second. + */ + public TestCertificateAuthority(String cn, long notBefore, long notAfter) throws Exception { + name = new X500Principal("CN=" + cn); + keys = keyPair(); + + cert = sign(name, keys, notBefore, notAfter, true); + } + + /** + * @param cn Common name of the certificate. + * @param notBefore Time it becomes valid; rounded down to a second. + * @param notAfter Time it expires; rounded down to a second. + * @return Key store with the private key and a chain containing the issued certificate and this authority's certificate. + */ + public KeyStore issue(String cn, long notBefore, long notAfter) throws Exception { + return issue(cn, new X500Principal("CN=" + cn), keyPair(), notBefore, notAfter); + } + + /** + * @param alias Alias for the private key and certificate chain. + * @param subject Subject of the certificate. + * @param pair Key pair of the certificate owner. + * @param notBefore Time it becomes valid; rounded down to a second. + * @param notAfter Time it expires; rounded down to a second. + * @return Key store with the private key and a chain containing the issued certificate and this authority's certificate. + */ + public KeyStore issue(String alias, X500Principal subject, KeyPair pair, long notBefore, long notAfter) throws Exception { + X509Certificate leaf = sign(subject, pair, notBefore, notAfter, false); + + KeyStore store = emptyStore(); + + store.setKeyEntry(alias, pair.getPrivate(), PWD, new X509Certificate[] {leaf, cert}); + + return store; + } + + /** @return Trust store with this authority alone. */ + public KeyStore trustStore() throws Exception { + return trustStore(this); + } + + /** + * @param authorities Authorities to trust. + * @return Trust store with them. + */ + public static KeyStore trustStore(TestCertificateAuthority... authorities) throws Exception { + KeyStore store = emptyStore(); + + for (TestCertificateAuthority a : authorities) + store.setCertificateEntry(a.name.getName(), a.cert); + + return store; + } + + /** + * @param keyStore Store with the certificate to present. + * @param trustStore Store with the authorities to trust. + * @return Context using the supplied key and trust stores, with certificate metadata attached. + */ + public static SSLContext context(KeyStore keyStore, KeyStore trustStore) throws Exception { + SSLContext ctx = SSLContext.getInstance("TLS"); + + ctx.init(keyManagers(keyStore), trustManagers(trustStore), null); + + return new SSLContextWrapper(ctx, null, certificateChain(keyStore)); + } + + /** + * @param keyStore Store with at most one private key. + * @return Its certificate chain, or {@code null} if the store has no private key. + */ + public static X509Certificate[] certificateChain(KeyStore keyStore) throws Exception { + X509Certificate[] chain = null; + + for (String alias : Collections.list(keyStore.aliases())) { + if (!keyStore.isKeyEntry(alias)) + continue; + + if (chain != null) + throw new IllegalArgumentException("The test key store must contain at most one private key"); + + Certificate[] certs = keyStore.getCertificateChain(alias); + + chain = Arrays.copyOf(certs, certs.length, X509Certificate[].class); + } + + return chain; + } + + /** + * @param keyStore Store with the certificate to present. + * @return Key managers that present it. + */ + public static KeyManager[] keyManagers(KeyStore keyStore) throws Exception { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + + kmf.init(keyStore, PWD); + + return kmf.getKeyManagers(); + } + + /** + * @param trustStore Store with the authorities to trust. + * @return Trust managers that trust them. + */ + public static TrustManager[] trustManagers(KeyStore trustStore) throws Exception { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + + tmf.init(trustStore); + + return tmf.getTrustManagers(); + } + + /** + * @param store Store. + * @param path File to write or replace. + */ + public static void save(KeyStore store, Path path) throws Exception { + try (OutputStream out = Files.newOutputStream(path)) { + store.store(out, PWD); + } + } + + /** + * @param subj Subject. + * @param subjKeys Key pair of the subject. + * @param notBefore Time the certificate becomes valid. + * @param notAfter Time it expires. + * @param ca Whether the certificate is the authority's own. + * @return Certificate signed by this authority. + */ + private X509Certificate sign(X500Principal subj, KeyPair subjKeys, long notBefore, long notAfter, boolean ca) throws Exception { + JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( + name, + new BigInteger(Long.SIZE, RND).add(BigInteger.ONE), + new Date(notBefore), + new Date(notAfter), + subj, + subjKeys.getPublic()); + + if (ca) + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + else + builder.addExtension(Extension.extendedKeyUsage, false, + new ExtendedKeyUsage(new KeyPurposeId[] {KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth})); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256withECDSA").build(keys.getPrivate()); + + return new JcaX509CertificateConverter().getCertificate(builder.build(signer)); + } + + /** */ + private static KeyPair keyPair() throws Exception { + KeyPairGenerator gen = KeyPairGenerator.getInstance("EC"); + + gen.initialize(new ECGenParameterSpec("secp256r1")); + + return gen.generateKeyPair(); + } + + /** */ + private static KeyStore emptyStore() throws Exception { + KeyStore store = KeyStore.getInstance("PKCS12"); + + store.load(null, PWD); + + return store; + } +} From 9716fd9fb24217feee1afea3f2046cf457343d46 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 01:20:45 +0300 Subject: [PATCH 11/26] IGNITE-29209 Use JKS for generated certificate stores. --- .../apache/ignite/testframework/TestCertificateAuthority.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java index 15bf792574824..693006fc8f558 100644 --- a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java +++ b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java @@ -238,7 +238,7 @@ private static KeyPair keyPair() throws Exception { /** */ private static KeyStore emptyStore() throws Exception { - KeyStore store = KeyStore.getInstance("PKCS12"); + KeyStore store = KeyStore.getInstance("JKS"); store.load(null, PWD); From 893a520f8808f3e23d4cced42bd5d51519142311 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 02:04:23 +0300 Subject: [PATCH 12/26] IGNITE-29209 Make metadata test premises and expectations explicit. --- .../ignite/ssl/SslContextMetadataTest.java | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 9ca68bbabb3d9..57c672b62b852 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -31,7 +31,6 @@ import javax.net.ssl.SSLServerSocket; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; -import javax.security.auth.x500.X500Principal; import org.apache.ignite.IgniteException; import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.TestCertificateAuthority; @@ -53,36 +52,38 @@ public class SslContextMetadataTest extends GridCommonAbstractTest { ca = new TestCertificateAuthority("metadata-ca"); } - /** {@code createNew()} loads the store once, preserves the cached context and attaches the new chain to the new context. */ + /** A fresh context loads the replacement store once and leaves the cached context's certificate metadata unchanged. */ @Test - public void testMetadataUsesOneLoadAndStaysWithContext() throws Exception { - KeyStore firstStore = issueStore("first"); - StoreFactory factory = new StoreFactory(firstStore); + public void testFreshContextUsesReplacementStoreAndPreservesCachedContext() throws Exception { + KeyStore originalKeys = keyStoreWithPrivateKey("original"); + X509Certificate[] originalChain = storedChain(originalKeys, "original"); + StoreFactory factory = new StoreFactory(originalKeys); - SSLContext first = factory.create(); + SSLContext cached = factory.create(); - assertArrayEquals(chain(firstStore, "first"), chain(first)); - assertEquals(new X500Principal("CN=first"), chain(first)[0].getSubjectX500Principal()); - assertFalse(privateKeyMissing(first)); assertEquals(1, factory.loads); + assertArrayEquals(originalChain, reportedChain(cached)); - factory.store = issueStore("next"); + KeyStore replacementKeys = keyStoreWithPrivateKey("replacement"); + X509Certificate[] replacementChain = storedChain(replacementKeys, "replacement"); - SSLContext next = factory.createNew(); + factory.store = replacementKeys; - assertNotSame(first, next); - assertSame(first, factory.create()); + SSLContext fresh = factory.createNew(); + + assertNotSame(cached, fresh); assertEquals(2, factory.loads); - assertArrayEquals(chain(firstStore, "first"), chain(first)); - assertArrayEquals(chain(factory.store, "next"), chain(next)); - assertEquals(new X500Principal("CN=next"), chain(next)[0].getSubjectX500Principal()); - assertFalse(privateKeyMissing(next)); + assertArrayEquals(replacementChain, reportedChain(fresh)); + + assertSame(cached, factory.create()); + assertArrayEquals(originalChain, reportedChain(cached)); + assertEquals("Returning the cached context must not load the store again", 2, factory.loads); } /** Both factory methods retain certificate metadata when they also apply TLS parameters. */ @Test public void testMetadataWithTlsParameters() throws Exception { - KeyStore store = issueStore("node"); + KeyStore store = keyStoreWithPrivateKey("node"); StoreFactory factory = new StoreFactory(store); String[] protocols = {"TLSv1.2"}; String[] ciphers = {"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}; @@ -91,7 +92,8 @@ public void testMetadataWithTlsParameters() throws Exception { factory.setCipherSuites(ciphers); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertArrayEquals(chain(store, "node"), chain(ctx)); + assertArrayEquals(storedChain(store, "node"), reportedChain(ctx)); + assertFalse("The loaded store contains a private key", reportsMissingPrivateKey(ctx)); SSLEngine engine = ctx.createSSLEngine(); @@ -103,23 +105,23 @@ public void testMetadataWithTlsParameters() throws Exception { /** Multiple private keys leave the chain unknown but are not reported as missing. */ @Test public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { - KeyStore store = issueStore("first"); - KeyStore other = issueStore("second"); + KeyStore store = keyStoreWithPrivateKey("first"); + KeyStore other = keyStoreWithPrivateKey("second"); store.setKeyEntry("second", other.getKey("second", PASSWORD), PASSWORD, other.getCertificateChain("second")); StoreFactory factory = new StoreFactory(store); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertNull(chain(ctx)); - assertFalse(privateKeyMissing(ctx)); + assertNull(reportedChain(ctx)); + assertFalse("Two private keys are present even though the served chain is unknown", reportsMissingPrivateKey(ctx)); } } /** Trusted certificates and secret keys do not count as private keys. */ @Test public void testOnlyPrivateKeyEntriesCount() throws Exception { - KeyStore keys = issueStore("node"); + KeyStore keys = keyStoreWithPrivateKey("node"); KeyStore store = KeyStore.getInstance("JCEKS"); store.load(null, PASSWORD); @@ -131,15 +133,15 @@ public void testOnlyPrivateKeyEntriesCount() throws Exception { SSLContext withoutKey = factory.create(); - assertNull(chain(withoutKey)); - assertTrue(privateKeyMissing(withoutKey)); + assertNull(reportedChain(withoutKey)); + assertTrue("Neither the trusted certificate nor the secret key is a private key", reportsMissingPrivateKey(withoutKey)); store.setKeyEntry("key", keys.getKey("node", PASSWORD), PASSWORD, keys.getCertificateChain("node")); SSLContext withKey = factory.createNew(); - assertArrayEquals(chain(keys, "node"), chain(withKey)); - assertFalse(privateKeyMissing(withKey)); + assertArrayEquals(storedChain(keys, "node"), reportedChain(withKey)); + assertFalse("The store now contains a private-key entry", reportsMissingPrivateKey(withKey)); } /** Trust-only contexts can still be built for clients, and another parameters wrapper preserves the missing-key marker. */ @@ -148,12 +150,12 @@ public void testTrustOnlyContextCreation() throws Exception { KeyStore store = KeyStore.getInstance("PKCS12"); store.load(null, PASSWORD); - store.setCertificateEntry("trusted", issueStore("trusted").getCertificate("trusted")); + store.setCertificateEntry("trusted", keyStoreWithPrivateKey("trusted").getCertificate("trusted")); StoreFactory factory = new StoreFactory(store); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertNull(chain(ctx)); + assertNull(reportedChain(ctx)); assertTrue(((SSLContextWrapper)ctx).isPrivateKeyMissing()); SSLParameters params = new SSLParameters(); @@ -174,8 +176,8 @@ public void testLegacyHooksRunOncePerBuiltContext() { SSLContext first = factory.create(); - assertNull(chain(first)); - assertFalse(privateKeyMissing(first)); + assertNull(reportedChain(first)); + assertFalse("Legacy hooks do not report whether a private key is available", reportsMissingPrivateKey(first)); assertEquals(1, factory.checks); assertEquals(1, factory.keys); assertEquals(1, factory.trust); @@ -189,8 +191,8 @@ public void testLegacyHooksRunOncePerBuiltContext() { SSLContext next = factory.createNew(); assertNotSame(first, next); - assertNull(chain(next)); - assertFalse(privateKeyMissing(next)); + assertNull(reportedChain(next)); + assertFalse("Legacy hooks do not report whether a private key is available", reportsMissingPrivateKey(next)); assertEquals(2, factory.checks); assertEquals(2, factory.keys); assertEquals(2, factory.trust); @@ -199,7 +201,7 @@ public void testLegacyHooksRunOncePerBuiltContext() { /** A subclass can still obtain key managers from the file factory's legacy hook. */ @Test public void testLegacyKeyManagerAdapter() throws Exception { - StoreFactory factory = new StoreFactory(issueStore("node")); + StoreFactory factory = new StoreFactory(keyStoreWithPrivateKey("node")); KeyManager[] mgrs = factory.legacyKeyManagers(); assertNotNull(mgrs); @@ -238,7 +240,7 @@ public void testMissingKeyMaterialImplementation() { public void testKeyMaterialDefensiveCopies() throws Exception { KeyManager mgr = new KeyManager() { }; KeyManager[] mgrs = {mgr}; - X509Certificate[] expected = chain(issueStore("node"), "node"); + X509Certificate[] expected = storedChain(keyStoreWithPrivateKey("node"), "node"); X509Certificate[] input = expected.clone(); AbstractSslContextFactory.KeyMaterial material = new AbstractSslContextFactory.KeyMaterial(mgrs, input); @@ -265,14 +267,14 @@ public void testKeyMaterialDefensiveCopies() throws Exception { } }; - assertArrayEquals(expected, chain(factory.create())); - assertArrayEquals(expected, chain(factory.createNew())); + assertArrayEquals(expected, reportedChain(factory.create())); + assertArrayEquals(expected, reportedChain(factory.createNew())); } /** Neither callers nor another parameters wrapper can replace a context's recorded chain. */ @Test public void testWrapperPreservesAndCopiesChain() throws Exception { - X509Certificate[] expected = chain(issueStore("node"), "node"); + X509Certificate[] expected = storedChain(keyStoreWithPrivateKey("node"), "node"); X509Certificate[] input = expected.clone(); SSLContext delegate = SSLContext.getInstance("TLS"); @@ -298,7 +300,7 @@ public void testWrapperPreservesAndCopiesChain() throws Exception { /** Explicit metadata takes precedence over the wrapped context's metadata. */ @Test public void testExplicitWrapperMetadataOverridesDelegateMetadata() throws Exception { - X509Certificate[] expectedChain = chain(issueStore("node"), "node"); + X509Certificate[] expectedChain = storedChain(keyStoreWithPrivateKey("node"), "node"); SSLContext delegate = SSLContext.getInstance("TLS"); delegate.init(null, null, null); @@ -327,7 +329,7 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { delegate.init(null, null, null); - SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, chain(issueStore("node"), "node")); + SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, storedChain(keyStoreWithPrivateKey("node"), "node")); SSLEngine engine = wrapper.createSSLEngine("peer.example", 10800); SSLEngine delegateEngine = delegate.createSSLEngine(); @@ -353,26 +355,26 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { } /** @return In-memory store with one private key and its certificate chain. */ - private KeyStore issueStore(String name) throws Exception { + private KeyStore keyStoreWithPrivateKey(String name) throws Exception { long now = System.currentTimeMillis(); return ca.issue(name, now - TimeUnit.HOURS.toMillis(1), now + TimeUnit.DAYS.toMillis(1)); } /** @return Chain stored for the alias. */ - private static X509Certificate[] chain(KeyStore store, String alias) throws Exception { + private static X509Certificate[] storedChain(KeyStore store, String alias) throws Exception { Certificate[] chain = store.getCertificateChain(alias); return Arrays.copyOf(chain, chain.length, X509Certificate[].class); } /** @return Chain attached to the context, if known. */ - private static X509Certificate[] chain(SSLContext ctx) { + private static X509Certificate[] reportedChain(SSLContext ctx) { return ctx instanceof SSLContextWrapper ? ((SSLContextWrapper)ctx).getCertificateChain() : null; } - /** @return Whether the context reports that its key material has no private key. */ - private static boolean privateKeyMissing(SSLContext ctx) { + /** @return Whether the context reports that no private key was loaded. Unknown metadata does not set this marker. */ + private static boolean reportsMissingPrivateKey(SSLContext ctx) { return ctx instanceof SSLContextWrapper && ((SSLContextWrapper)ctx).isPrivateKeyMissing(); } From 856866a1f8bdd6c83782f79fd0c00cac59b1862a Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 02:27:28 +0300 Subject: [PATCH 13/26] IGNITE-29209 Make certificate test inputs explicit and reusable --- .../ignite/ssl/SslContextMetadataTest.java | 95 ++++++++----------- .../TestCertificateAuthority.java | 39 ++++++-- 2 files changed, 72 insertions(+), 62 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 57c672b62b852..56d28044cdf38 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -18,11 +18,8 @@ package org.apache.ignite.ssl; import java.security.KeyStore; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.concurrent.TimeUnit; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.KeyGenerator; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; @@ -37,6 +34,7 @@ import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.junit.Test; +import static org.apache.ignite.testframework.TestCertificateAuthority.certificateChain; import static org.junit.Assert.assertArrayEquals; /** Tests that certificate metadata belongs to the key material used to create a context. */ @@ -55,8 +53,8 @@ public class SslContextMetadataTest extends GridCommonAbstractTest { /** A fresh context loads the replacement store once and leaves the cached context's certificate metadata unchanged. */ @Test public void testFreshContextUsesReplacementStoreAndPreservesCachedContext() throws Exception { - KeyStore originalKeys = keyStoreWithPrivateKey("original"); - X509Certificate[] originalChain = storedChain(originalKeys, "original"); + KeyStore originalKeys = ca.issue(); + X509Certificate[] originalChain = certificateChain(originalKeys); StoreFactory factory = new StoreFactory(originalKeys); SSLContext cached = factory.create(); @@ -64,8 +62,8 @@ public void testFreshContextUsesReplacementStoreAndPreservesCachedContext() thro assertEquals(1, factory.loads); assertArrayEquals(originalChain, reportedChain(cached)); - KeyStore replacementKeys = keyStoreWithPrivateKey("replacement"); - X509Certificate[] replacementChain = storedChain(replacementKeys, "replacement"); + KeyStore replacementKeys = ca.issue(); + X509Certificate[] replacementChain = certificateChain(replacementKeys); factory.store = replacementKeys; @@ -83,7 +81,7 @@ public void testFreshContextUsesReplacementStoreAndPreservesCachedContext() thro /** Both factory methods retain certificate metadata when they also apply TLS parameters. */ @Test public void testMetadataWithTlsParameters() throws Exception { - KeyStore store = keyStoreWithPrivateKey("node"); + KeyStore store = ca.issue(); StoreFactory factory = new StoreFactory(store); String[] protocols = {"TLSv1.2"}; String[] ciphers = {"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}; @@ -92,7 +90,7 @@ public void testMetadataWithTlsParameters() throws Exception { factory.setCipherSuites(ciphers); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertArrayEquals(storedChain(store, "node"), reportedChain(ctx)); + assertArrayEquals(certificateChain(store), reportedChain(ctx)); assertFalse("The loaded store contains a private key", reportsMissingPrivateKey(ctx)); SSLEngine engine = ctx.createSSLEngine(); @@ -105,10 +103,11 @@ public void testMetadataWithTlsParameters() throws Exception { /** Multiple private keys leave the chain unknown but are not reported as missing. */ @Test public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { - KeyStore store = keyStoreWithPrivateKey("first"); - KeyStore other = keyStoreWithPrivateKey("second"); + String secondKey = "second-key"; + KeyStore store = ca.issue(); + KeyStore other = ca.issue(secondKey); - store.setKeyEntry("second", other.getKey("second", PASSWORD), PASSWORD, other.getCertificateChain("second")); + store.setKeyEntry(secondKey, other.getKey(secondKey, PASSWORD), PASSWORD, other.getCertificateChain(secondKey)); StoreFactory factory = new StoreFactory(store); @@ -121,12 +120,15 @@ public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { /** Trusted certificates and secret keys do not count as private keys. */ @Test public void testOnlyPrivateKeyEntriesCount() throws Exception { - KeyStore keys = keyStoreWithPrivateKey("node"); + String keyAlias = "private-key"; + KeyStore keys = ca.issue(keyAlias); + + // JCEKS can hold both private keys and secret keys. KeyStore store = KeyStore.getInstance("JCEKS"); store.load(null, PASSWORD); - store.setCertificateEntry("trusted", keys.getCertificate("node")); - store.setEntry("secret", new KeyStore.SecretKeyEntry(new SecretKeySpec(new byte[16], "AES")), + store.setCertificateEntry("trusted", keys.getCertificate(keyAlias)); + store.setEntry("secret", new KeyStore.SecretKeyEntry(KeyGenerator.getInstance("AES").generateKey()), new KeyStore.PasswordProtection(PASSWORD)); StoreFactory factory = new StoreFactory(store); @@ -136,36 +138,31 @@ public void testOnlyPrivateKeyEntriesCount() throws Exception { assertNull(reportedChain(withoutKey)); assertTrue("Neither the trusted certificate nor the secret key is a private key", reportsMissingPrivateKey(withoutKey)); - store.setKeyEntry("key", keys.getKey("node", PASSWORD), PASSWORD, keys.getCertificateChain("node")); + store.setKeyEntry(keyAlias, keys.getKey(keyAlias, PASSWORD), PASSWORD, keys.getCertificateChain(keyAlias)); SSLContext withKey = factory.createNew(); - assertArrayEquals(storedChain(keys, "node"), reportedChain(withKey)); + assertArrayEquals(certificateChain(keys), reportedChain(withKey)); assertFalse("The store now contains a private-key entry", reportsMissingPrivateKey(withKey)); } /** Trust-only contexts can still be built for clients, and another parameters wrapper preserves the missing-key marker. */ @Test public void testTrustOnlyContextCreation() throws Exception { - KeyStore store = KeyStore.getInstance("PKCS12"); - - store.load(null, PASSWORD); - store.setCertificateEntry("trusted", keyStoreWithPrivateKey("trusted").getCertificate("trusted")); + StoreFactory factory = new StoreFactory(ca.trustStore()); + String[] protocols = {"TLSv1.2"}; + SSLParameters params = new SSLParameters(); - StoreFactory factory = new StoreFactory(store); + params.setProtocols(protocols); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { assertNull(reportedChain(ctx)); assertTrue(((SSLContextWrapper)ctx).isPrivateKeyMissing()); - SSLParameters params = new SSLParameters(); - - params.setProtocols(new String[] {"TLSv1.2"}); - SSLContextWrapper outer = new SSLContextWrapper(ctx, params); assertTrue(outer.isPrivateKeyMissing()); - assertArrayEquals(new String[] {"TLSv1.2"}, outer.createSSLEngine().getEnabledProtocols()); + assertArrayEquals(protocols, outer.createSSLEngine().getEnabledProtocols()); } } @@ -201,7 +198,7 @@ public void testLegacyHooksRunOncePerBuiltContext() { /** A subclass can still obtain key managers from the file factory's legacy hook. */ @Test public void testLegacyKeyManagerAdapter() throws Exception { - StoreFactory factory = new StoreFactory(keyStoreWithPrivateKey("node")); + StoreFactory factory = new StoreFactory(ca.issue()); KeyManager[] mgrs = factory.legacyKeyManagers(); assertNotNull(mgrs); @@ -238,9 +235,10 @@ public void testMissingKeyMaterialImplementation() { /** A subclass can supply material directly, and callers cannot replace its recorded managers or chain through arrays. */ @Test public void testKeyMaterialDefensiveCopies() throws Exception { - KeyManager mgr = new KeyManager() { }; - KeyManager[] mgrs = {mgr}; - X509Certificate[] expected = storedChain(keyStoreWithPrivateKey("node"), "node"); + KeyStore keys = ca.issue(); + KeyManager[] mgrs = TestCertificateAuthority.keyManagers(keys); + KeyManager mgr = mgrs[0]; + X509Certificate[] expected = certificateChain(keys); X509Certificate[] input = expected.clone(); AbstractSslContextFactory.KeyMaterial material = new AbstractSslContextFactory.KeyMaterial(mgrs, input); @@ -274,7 +272,7 @@ public void testKeyMaterialDefensiveCopies() throws Exception { /** Neither callers nor another parameters wrapper can replace a context's recorded chain. */ @Test public void testWrapperPreservesAndCopiesChain() throws Exception { - X509Certificate[] expected = storedChain(keyStoreWithPrivateKey("node"), "node"); + X509Certificate[] expected = certificateChain(ca.issue()); X509Certificate[] input = expected.clone(); SSLContext delegate = SSLContext.getInstance("TLS"); @@ -287,20 +285,21 @@ public void testWrapperPreservesAndCopiesChain() throws Exception { assertArrayEquals(expected, wrapper.getCertificateChain()); + String[] protocols = {"TLSv1.2"}; SSLParameters params = new SSLParameters(); - params.setProtocols(new String[] {"TLSv1.2"}); + params.setProtocols(protocols); SSLContextWrapper outer = new SSLContextWrapper(wrapper, params); assertArrayEquals(expected, outer.getCertificateChain()); - assertArrayEquals(new String[] {"TLSv1.2"}, outer.createSSLEngine().getEnabledProtocols()); + assertArrayEquals(protocols, outer.createSSLEngine().getEnabledProtocols()); } /** Explicit metadata takes precedence over the wrapped context's metadata. */ @Test public void testExplicitWrapperMetadataOverridesDelegateMetadata() throws Exception { - X509Certificate[] expectedChain = storedChain(keyStoreWithPrivateKey("node"), "node"); + X509Certificate[] expectedChain = certificateChain(ca.issue()); SSLContext delegate = SSLContext.getInstance("TLS"); delegate.init(null, null, null); @@ -329,12 +328,14 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { delegate.init(null, null, null); - SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, storedChain(keyStoreWithPrivateKey("node"), "node")); - SSLEngine engine = wrapper.createSSLEngine("peer.example", 10800); + SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, certificateChain(ca.issue())); + String host = "peer.example"; + int port = 10800; + SSLEngine engine = wrapper.createSSLEngine(host, port); SSLEngine delegateEngine = delegate.createSSLEngine(); - assertEquals("peer.example", engine.getPeerHost()); - assertEquals(10800, engine.getPeerPort()); + assertEquals(host, engine.getPeerHost()); + assertEquals(port, engine.getPeerPort()); assertArrayEquals(delegateEngine.getEnabledProtocols(), engine.getEnabledProtocols()); assertArrayEquals(delegateEngine.getEnabledCipherSuites(), engine.getEnabledCipherSuites()); @@ -354,20 +355,6 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { } } - /** @return In-memory store with one private key and its certificate chain. */ - private KeyStore keyStoreWithPrivateKey(String name) throws Exception { - long now = System.currentTimeMillis(); - - return ca.issue(name, now - TimeUnit.HOURS.toMillis(1), now + TimeUnit.DAYS.toMillis(1)); - } - - /** @return Chain stored for the alias. */ - private static X509Certificate[] storedChain(KeyStore store, String alias) throws Exception { - Certificate[] chain = store.getCertificateChain(alias); - - return Arrays.copyOf(chain, chain.length, X509Certificate[].class); - } - /** @return Chain attached to the context, if known. */ private static X509Certificate[] reportedChain(SSLContext ctx) { return ctx instanceof SSLContextWrapper ? ((SSLContextWrapper)ctx).getCertificateChain() : null; diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java index 693006fc8f558..4bc0d2754ae3b 100644 --- a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java +++ b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java @@ -24,7 +24,6 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; -import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.security.spec.ECGenParameterSpec; @@ -32,6 +31,7 @@ import java.util.Collections; import java.util.Date; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; @@ -49,14 +49,16 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; /** - * Certificate authority for tests that need explicit keys, trust relationships and certificate validity periods. + * Issues JKS stores protected by {@link #PWD}, each with an EC P-256 private key and its certificate chain. + * Certificates use SHA256withECDSA signatures. Issued leaf certificates allow both server and client authentication, + * so the same material works for node-to-node TLS. Tests choose the issuer and validity dates when those are part of the scenario. */ public class TestCertificateAuthority { /** Password of every store made here. */ public static final char[] PWD = "123456".toCharArray(); - /** */ - private static final SecureRandom RND = new SecureRandom(); + /** Certificate serial numbers must be positive and unique within this authority. */ + private final AtomicLong serial = new AtomicLong(); /** */ private final X500Principal name; @@ -67,7 +69,11 @@ public class TestCertificateAuthority { /** */ private final X509Certificate cert; - /** @param cn Common name of the authority. */ + /** + * Creates an authority valid from yesterday until a year from now. + * + * @param cn Common name of the authority. + */ public TestCertificateAuthority(String cn) throws Exception { this(cn, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1), System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365)); } @@ -84,8 +90,25 @@ public TestCertificateAuthority(String cn, long notBefore, long notAfter) throws cert = sign(name, keys, notBefore, notAfter, true); } + /** @return Store with one private key and a currently valid certificate whose name is irrelevant to the test. */ + public KeyStore issue() throws Exception { + return issue("test-certificate"); + } + + /** + * Issues a certificate valid from an hour ago until a day from now, for tests that do not check expiry. + * + * @param cn Common name, also used as the key alias. + * @return Store with the private key and certificate chain. + */ + public KeyStore issue(String cn) throws Exception { + long now = System.currentTimeMillis(); + + return issue(cn, now - TimeUnit.HOURS.toMillis(1), now + TimeUnit.DAYS.toMillis(1)); + } + /** - * @param cn Common name of the certificate. + * @param cn Common name of the certificate, also used as the key alias. * @param notBefore Time it becomes valid; rounded down to a second. * @param notAfter Time it expires; rounded down to a second. * @return Key store with the private key and a chain containing the issued certificate and this authority's certificate. @@ -151,7 +174,7 @@ public static X509Certificate[] certificateChain(KeyStore keyStore) throws Excep X509Certificate[] chain = null; for (String alias : Collections.list(keyStore.aliases())) { - if (!keyStore.isKeyEntry(alias)) + if (!keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) continue; if (chain != null) @@ -210,7 +233,7 @@ public static void save(KeyStore store, Path path) throws Exception { private X509Certificate sign(X500Principal subj, KeyPair subjKeys, long notBefore, long notAfter, boolean ca) throws Exception { JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder( name, - new BigInteger(Long.SIZE, RND).add(BigInteger.ONE), + BigInteger.valueOf(serial.incrementAndGet()), new Date(notBefore), new Date(notAfter), subj, From fbf5c510da4facf05dfa75a63221ee63e2e0d0f5 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 02:33:13 +0300 Subject: [PATCH 14/26] IGNITE-29209 Let expiry tests specify dates without an unrelated subject --- .../testframework/TestCertificateAuthority.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java index 4bc0d2754ae3b..85aed26b504d3 100644 --- a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java +++ b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java @@ -57,6 +57,9 @@ public class TestCertificateAuthority { /** Password of every store made here. */ public static final char[] PWD = "123456".toCharArray(); + /** Name used when certificate identity is not part of the test. */ + private static final String DEFAULT_NAME = "test-certificate"; + /** Certificate serial numbers must be positive and unique within this authority. */ private final AtomicLong serial = new AtomicLong(); @@ -92,7 +95,7 @@ public TestCertificateAuthority(String cn, long notBefore, long notAfter) throws /** @return Store with one private key and a currently valid certificate whose name is irrelevant to the test. */ public KeyStore issue() throws Exception { - return issue("test-certificate"); + return issue(DEFAULT_NAME); } /** @@ -107,6 +110,15 @@ public KeyStore issue(String cn) throws Exception { return issue(cn, now - TimeUnit.HOURS.toMillis(1), now + TimeUnit.DAYS.toMillis(1)); } + /** + * @param notBefore Time the certificate becomes valid; rounded down to a second. + * @param notAfter Time it expires; rounded down to a second. + * @return Store with one private key and a certificate with the requested validity dates. + */ + public KeyStore issue(long notBefore, long notAfter) throws Exception { + return issue(DEFAULT_NAME, notBefore, notAfter); + } + /** * @param cn Common name of the certificate, also used as the key alias. * @param notBefore Time it becomes valid; rounded down to a second. From cbef7aa5fe4aadf586a8808674cf2a64a9512ad4 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 02:52:24 +0300 Subject: [PATCH 15/26] IGNITE-29209 Expose key-store contents and hook calls in tests --- .../ignite/ssl/SslContextMetadataTest.java | 82 ++++++++++--------- .../TestCertificateAuthority.java | 19 +++-- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 56d28044cdf38..408df05840862 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -36,6 +36,7 @@ import static org.apache.ignite.testframework.TestCertificateAuthority.certificateChain; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotEquals; /** Tests that certificate metadata belongs to the key material used to create a context. */ public class SslContextMetadataTest extends GridCommonAbstractTest { @@ -53,36 +54,38 @@ public class SslContextMetadataTest extends GridCommonAbstractTest { /** A fresh context loads the replacement store once and leaves the cached context's certificate metadata unchanged. */ @Test public void testFreshContextUsesReplacementStoreAndPreservesCachedContext() throws Exception { - KeyStore originalKeys = ca.issue(); + KeyStore originalKeys = ca.keyStoreWithPrivateKey(); X509Certificate[] originalChain = certificateChain(originalKeys); - StoreFactory factory = new StoreFactory(originalKeys); + InMemoryStoreFactory factory = new InMemoryStoreFactory(originalKeys); SSLContext cached = factory.create(); - assertEquals(1, factory.loads); + assertEquals(1, factory.loadKeyStoreCalls); assertArrayEquals(originalChain, reportedChain(cached)); - KeyStore replacementKeys = ca.issue(); + KeyStore replacementKeys = ca.keyStoreWithPrivateKey(); X509Certificate[] replacementChain = certificateChain(replacementKeys); + assertNotEquals(originalChain[0], replacementChain[0]); + factory.store = replacementKeys; SSLContext fresh = factory.createNew(); assertNotSame(cached, fresh); - assertEquals(2, factory.loads); + assertEquals(2, factory.loadKeyStoreCalls); assertArrayEquals(replacementChain, reportedChain(fresh)); assertSame(cached, factory.create()); assertArrayEquals(originalChain, reportedChain(cached)); - assertEquals("Returning the cached context must not load the store again", 2, factory.loads); + assertEquals("Returning the cached context must not load the store again", 2, factory.loadKeyStoreCalls); } /** Both factory methods retain certificate metadata when they also apply TLS parameters. */ @Test public void testMetadataWithTlsParameters() throws Exception { - KeyStore store = ca.issue(); - StoreFactory factory = new StoreFactory(store); + KeyStore store = ca.keyStoreWithPrivateKey(); + InMemoryStoreFactory factory = new InMemoryStoreFactory(store); String[] protocols = {"TLSv1.2"}; String[] ciphers = {"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}; @@ -103,13 +106,14 @@ public void testMetadataWithTlsParameters() throws Exception { /** Multiple private keys leave the chain unknown but are not reported as missing. */ @Test public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { + String firstKey = "first-key"; String secondKey = "second-key"; - KeyStore store = ca.issue(); - KeyStore other = ca.issue(secondKey); + KeyStore store = ca.keyStoreWithPrivateKey(firstKey); + KeyStore other = ca.keyStoreWithPrivateKey(secondKey); store.setKeyEntry(secondKey, other.getKey(secondKey, PASSWORD), PASSWORD, other.getCertificateChain(secondKey)); - StoreFactory factory = new StoreFactory(store); + InMemoryStoreFactory factory = new InMemoryStoreFactory(store); for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { assertNull(reportedChain(ctx)); @@ -121,7 +125,7 @@ public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { @Test public void testOnlyPrivateKeyEntriesCount() throws Exception { String keyAlias = "private-key"; - KeyStore keys = ca.issue(keyAlias); + KeyStore keys = ca.keyStoreWithPrivateKey(keyAlias); // JCEKS can hold both private keys and secret keys. KeyStore store = KeyStore.getInstance("JCEKS"); @@ -131,7 +135,7 @@ public void testOnlyPrivateKeyEntriesCount() throws Exception { store.setEntry("secret", new KeyStore.SecretKeyEntry(KeyGenerator.getInstance("AES").generateKey()), new KeyStore.PasswordProtection(PASSWORD)); - StoreFactory factory = new StoreFactory(store); + InMemoryStoreFactory factory = new InMemoryStoreFactory(store); SSLContext withoutKey = factory.create(); @@ -149,7 +153,7 @@ public void testOnlyPrivateKeyEntriesCount() throws Exception { /** Trust-only contexts can still be built for clients, and another parameters wrapper preserves the missing-key marker. */ @Test public void testTrustOnlyContextCreation() throws Exception { - StoreFactory factory = new StoreFactory(ca.trustStore()); + InMemoryStoreFactory factory = new InMemoryStoreFactory(ca.trustStore()); String[] protocols = {"TLSv1.2"}; SSLParameters params = new SSLParameters(); @@ -175,35 +179,35 @@ public void testLegacyHooksRunOncePerBuiltContext() { assertNull(reportedChain(first)); assertFalse("Legacy hooks do not report whether a private key is available", reportsMissingPrivateKey(first)); - assertEquals(1, factory.checks); - assertEquals(1, factory.keys); - assertEquals(1, factory.trust); + assertEquals(1, factory.parameterChecks); + assertEquals(1, factory.keyManagerCalls); + assertEquals(1, factory.trustManagerCalls); assertSame(first, factory.create()); - assertEquals(1, factory.checks); - assertEquals(1, factory.keys); - assertEquals(1, factory.trust); + assertEquals(1, factory.parameterChecks); + assertEquals(1, factory.keyManagerCalls); + assertEquals(1, factory.trustManagerCalls); SSLContext next = factory.createNew(); assertNotSame(first, next); assertNull(reportedChain(next)); assertFalse("Legacy hooks do not report whether a private key is available", reportsMissingPrivateKey(next)); - assertEquals(2, factory.checks); - assertEquals(2, factory.keys); - assertEquals(2, factory.trust); + assertEquals(2, factory.parameterChecks); + assertEquals(2, factory.keyManagerCalls); + assertEquals(2, factory.trustManagerCalls); } /** A subclass can still obtain key managers from the file factory's legacy hook. */ @Test public void testLegacyKeyManagerAdapter() throws Exception { - StoreFactory factory = new StoreFactory(ca.issue()); + InMemoryStoreFactory factory = new InMemoryStoreFactory(ca.keyStoreWithPrivateKey()); KeyManager[] mgrs = factory.legacyKeyManagers(); assertNotNull(mgrs); assertTrue(mgrs.length > 0); - assertEquals(1, factory.loads); + assertEquals(1, factory.loadKeyStoreCalls); } /** A factory that implements neither key-material hook reports the missing implementation. */ @@ -235,7 +239,7 @@ public void testMissingKeyMaterialImplementation() { /** A subclass can supply material directly, and callers cannot replace its recorded managers or chain through arrays. */ @Test public void testKeyMaterialDefensiveCopies() throws Exception { - KeyStore keys = ca.issue(); + KeyStore keys = ca.keyStoreWithPrivateKey(); KeyManager[] mgrs = TestCertificateAuthority.keyManagers(keys); KeyManager mgr = mgrs[0]; X509Certificate[] expected = certificateChain(keys); @@ -272,7 +276,7 @@ public void testKeyMaterialDefensiveCopies() throws Exception { /** Neither callers nor another parameters wrapper can replace a context's recorded chain. */ @Test public void testWrapperPreservesAndCopiesChain() throws Exception { - X509Certificate[] expected = certificateChain(ca.issue()); + X509Certificate[] expected = certificateChain(ca.keyStoreWithPrivateKey()); X509Certificate[] input = expected.clone(); SSLContext delegate = SSLContext.getInstance("TLS"); @@ -299,7 +303,7 @@ public void testWrapperPreservesAndCopiesChain() throws Exception { /** Explicit metadata takes precedence over the wrapped context's metadata. */ @Test public void testExplicitWrapperMetadataOverridesDelegateMetadata() throws Exception { - X509Certificate[] expectedChain = certificateChain(ca.issue()); + X509Certificate[] expectedChain = certificateChain(ca.keyStoreWithPrivateKey()); SSLContext delegate = SSLContext.getInstance("TLS"); delegate.init(null, null, null); @@ -328,7 +332,7 @@ public void testMetadataWrapperPreservesContextBehavior() throws Exception { delegate.init(null, null, null); - SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, certificateChain(ca.issue())); + SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, certificateChain(ca.keyStoreWithPrivateKey())); String host = "peer.example"; int port = 10800; SSLEngine engine = wrapper.createSSLEngine(host, port); @@ -366,15 +370,15 @@ private static boolean reportsMissingPrivateKey(SSLContext ctx) { } /** A file factory whose existing store-loading hook supplies the test material. */ - private static class StoreFactory extends SslContextFactory { + private static class InMemoryStoreFactory extends SslContextFactory { /** */ private KeyStore store; /** */ - private int loads; + private int loadKeyStoreCalls; /** */ - StoreFactory(KeyStore store) { + InMemoryStoreFactory(KeyStore store) { this.store = store; setKeyStoreType(store.getType()); @@ -385,7 +389,7 @@ private static class StoreFactory extends SslContextFactory { /** {@inheritDoc} */ @Override protected KeyStore loadKeyStore(String type, String path, char[] pwd) { - loads++; + loadKeyStoreCalls++; return store; } @@ -400,29 +404,29 @@ private KeyManager[] legacyKeyManagers() throws SSLException { /** A subclass that implements the manager hooks without overriding createKeyMaterial(). */ private static class LegacyFactory extends AbstractSslContextFactory { /** */ - private int checks; + private int parameterChecks; /** */ - private int keys; + private int keyManagerCalls; /** */ - private int trust; + private int trustManagerCalls; /** {@inheritDoc} */ @Override protected void checkParameters() { - checks++; + parameterChecks++; } /** {@inheritDoc} */ @Override protected KeyManager[] createKeyManagers() { - keys++; + keyManagerCalls++; return null; } /** {@inheritDoc} */ @Override protected TrustManager[] createTrustManagers() { - trust++; + trustManagerCalls++; return null; } diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java index 85aed26b504d3..29bcfd74aad48 100644 --- a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java +++ b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java @@ -94,8 +94,8 @@ public TestCertificateAuthority(String cn, long notBefore, long notAfter) throws } /** @return Store with one private key and a currently valid certificate whose name is irrelevant to the test. */ - public KeyStore issue() throws Exception { - return issue(DEFAULT_NAME); + public KeyStore keyStoreWithPrivateKey() throws Exception { + return keyStoreWithPrivateKey(DEFAULT_NAME); } /** @@ -104,10 +104,10 @@ public KeyStore issue() throws Exception { * @param cn Common name, also used as the key alias. * @return Store with the private key and certificate chain. */ - public KeyStore issue(String cn) throws Exception { + public KeyStore keyStoreWithPrivateKey(String cn) throws Exception { long now = System.currentTimeMillis(); - return issue(cn, now - TimeUnit.HOURS.toMillis(1), now + TimeUnit.DAYS.toMillis(1)); + return keyStoreWithPrivateKey(cn, now - TimeUnit.HOURS.toMillis(1), now + TimeUnit.DAYS.toMillis(1)); } /** @@ -115,8 +115,8 @@ public KeyStore issue(String cn) throws Exception { * @param notAfter Time it expires; rounded down to a second. * @return Store with one private key and a certificate with the requested validity dates. */ - public KeyStore issue(long notBefore, long notAfter) throws Exception { - return issue(DEFAULT_NAME, notBefore, notAfter); + public KeyStore keyStoreWithPrivateKey(long notBefore, long notAfter) throws Exception { + return keyStoreWithPrivateKey(DEFAULT_NAME, notBefore, notAfter); } /** @@ -125,8 +125,8 @@ public KeyStore issue(long notBefore, long notAfter) throws Exception { * @param notAfter Time it expires; rounded down to a second. * @return Key store with the private key and a chain containing the issued certificate and this authority's certificate. */ - public KeyStore issue(String cn, long notBefore, long notAfter) throws Exception { - return issue(cn, new X500Principal("CN=" + cn), keyPair(), notBefore, notAfter); + public KeyStore keyStoreWithPrivateKey(String cn, long notBefore, long notAfter) throws Exception { + return keyStoreWithPrivateKey(cn, new X500Principal("CN=" + cn), keyPair(), notBefore, notAfter); } /** @@ -137,7 +137,8 @@ public KeyStore issue(String cn, long notBefore, long notAfter) throws Exception * @param notAfter Time it expires; rounded down to a second. * @return Key store with the private key and a chain containing the issued certificate and this authority's certificate. */ - public KeyStore issue(String alias, X500Principal subject, KeyPair pair, long notBefore, long notAfter) throws Exception { + public KeyStore keyStoreWithPrivateKey(String alias, X500Principal subject, KeyPair pair, long notBefore, long notAfter) + throws Exception { X509Certificate leaf = sign(subject, pair, notBefore, notAfter, false); KeyStore store = emptyStore(); From 2c9b01f235a7bec5598b6d56e2bc077c2d8c9cb6 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 15:28:46 +0300 Subject: [PATCH 16/26] IGNITE-29209 Build fresh SSL bundles from loaded key material --- .../ignite/ssl/AbstractSslContextFactory.java | 186 +++------- .../ignite/ssl/DelegatingSSLContextSpi.java | 5 +- .../org/apache/ignite/ssl/KeyMaterial.java | 127 +++++++ .../apache/ignite/ssl/SSLContextWrapper.java | 57 +-- .../java/org/apache/ignite/ssl/SslBundle.java | 74 ++++ .../apache/ignite/ssl/SslContextFactory.java | 34 +- .../ignite/ssl/SslContextMetadataTest.java | 351 +++++++++--------- .../TestCertificateAuthority.java | 21 +- 8 files changed, 459 insertions(+), 396 deletions(-) create mode 100644 modules/commons/src/main/java/org/apache/ignite/ssl/KeyMaterial.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/ssl/SslBundle.java diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index d269abbd90722..dbcac5803ed30 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -18,12 +18,10 @@ package org.apache.ignite.ssl; import java.security.KeyManagementException; -import java.security.KeyStore; -import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.Enumeration; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import javax.cache.configuration.Factory; import javax.net.ssl.KeyManager; @@ -37,7 +35,7 @@ /** * Base SSL context factory that caches the result of the first successful {@link #create()} call. - * Use {@link #createNew()} to build another context without changing the cached one. + * Use {@link #createSslBundle()} to load another context and its certificate chains without changing the cached context. */ public abstract class AbstractSslContextFactory implements Factory { /** */ @@ -115,16 +113,43 @@ public void setProtocols(String... protocols) { } /** - * Creates SSL context based on factory settings. + * Builds a fresh context with the legacy key-manager hook. Subclasses that load a key store can override this method + * and call {@link #createSslBundle(KeyMaterial)} to include its certificate chains. * - * @return Initialized SSL context. - * @throws SSLException If SSL context could not be created. + * @return Fresh context and the certificate metadata available to this factory. + * @throws IgniteException If the context could not be created. */ - private SSLContext createSslContext() throws SSLException { - checkParameters(); + public SslBundle createSslBundle() { + try { + checkParameters(); + + return createSslBundle(createKeyManagers(), null); + } + catch (SSLException e) { + throw new IgniteException(e); + } + } - KeyMaterial material = createKeyMaterial(); + /** + * Builds a fresh context from one loaded store. The caller must check its parameters before loading the material. + * This method does not call {@link #createKeyManagers()} or change the context cached by {@link #create()}. + * + * @param material Loaded key store, key password and manager algorithm. + * @return Context and certificate chains from that material. + * @throws SSLException If key managers, certificate metadata or the context cannot be created. + */ + protected SslBundle createSslBundle(KeyMaterial material) throws SSLException { + return createSslBundle(material.keyManagers(), material.certificateChains()); + } + /** + * @param keyMgrs Key managers, or {@code null} to use the provider's defaults. + * @param chains Certificate chains by private-key alias, or {@code null} if unknown. + * @return Initialized context and its certificate metadata. + * @throws SSLException If trust managers or the context cannot be created. + */ + private SslBundle createSslBundle(@Nullable KeyManager[] keyMgrs, + @Nullable Map> chains) throws SSLException { TrustManager[] trustMgrs = createTrustManagers(); try { @@ -142,12 +167,12 @@ private SSLContext createSslContext() throws SSLException { sslParameters.setProtocols(protocols); } - if (sslParameters != null || material.chain != null || material.privateKeyMissing) - ctx = new SSLContextWrapper(ctx, sslParameters, material.chain, material.privateKeyMissing); + if (sslParameters != null) + ctx = new SSLContextWrapper(ctx, sslParameters); - ctx.init(material.keyMgrs, trustMgrs, null); + ctx.init(keyMgrs, trustMgrs, null); - return ctx; + return new SslBundle(ctx, chains); } catch (NoSuchAlgorithmException e) { throw new SSLException("Unsupported SSL protocol: " + proto, e); @@ -179,22 +204,11 @@ protected void checkNullParameter(Object param, String name) throws SSLException * * @return Created key managers. * @throws SSLException If this method is not overridden or key managers cannot be created. - * @deprecated Override {@link #createKeyMaterial()} to return key managers with their certificate chain. + * @deprecated Override {@link #createSslBundle()} to return a context with its certificate chains. */ @Deprecated protected KeyManager[] createKeyManagers() throws SSLException { - throw new SSLException("Subclass must override createKeyMaterial() or createKeyManagers()"); - } - - /** - * Creates key managers and their certificate metadata from the same load. New subclasses should override this method. - * The default adapts {@link #createKeyManagers()} for existing subclasses and leaves the certificate chain unknown. - * - * @return Loaded key material. - * @throws SSLException If the key material could not be loaded. - */ - protected KeyMaterial createKeyMaterial() throws SSLException { - return new KeyMaterial(createKeyManagers(), null); + throw new SSLException("Subclass must override createSslBundle() or createKeyManagers()"); } /** @@ -208,122 +222,12 @@ protected KeyMaterial createKeyMaterial() throws SSLException { SSLContext ctx = sslCtx.get(); if (ctx == null) { - try { - ctx = createSslContext(); + ctx = createSslBundle().context(); - if (!sslCtx.compareAndSet(null, ctx)) - ctx = sslCtx.get(); - } - catch (SSLException e) { - throw new IgniteException(e); - } + if (!sslCtx.compareAndSet(null, ctx)) + ctx = sslCtx.get(); } return ctx; } - - /** - * Builds a new SSL context from the current factory settings without reading or replacing the context cached by - * {@link #create()}. - *

- * A subclass that builds its context in its own {@link #create()} must override this method too and build the context from - * the same sources. This implementation calls {@link #checkParameters()}, {@link #createKeyMaterial()} and - * {@link #createTrustManagers()}; it does not call {@link #create()}. - *

- * A subclass that only changes the key source can override {@link #createKeyMaterial()} instead. When building a context - * through another mechanism, attach its loaded certificate chain, if known, using - * {@link SSLContextWrapper#SSLContextWrapper(SSLContext, SSLParameters, X509Certificate[])}. - * - * @return New initialized SSL context. - * @throws IgniteException If the context could not be created. - */ - public SSLContext createNew() { - try { - return createSslContext(); - } - catch (SSLException e) { - throw new IgniteException(e); - } - } - - /** Key managers and the certificate chain loaded with them. */ - protected static final class KeyMaterial { - /** */ - private final KeyManager[] keyMgrs; - - /** */ - private final X509Certificate[] chain; - - /** Whether the loaded store is known to contain no private key. */ - private final boolean privateKeyMissing; - - /** - * @param keyMgrs Key managers, or {@code null} to use the provider's defaults. - * @param chain Chain of the single private key, own certificate first, or {@code null} if unknown. - */ - public KeyMaterial(@Nullable KeyManager[] keyMgrs, @Nullable X509Certificate[] chain) { - this(keyMgrs, chain, false); - } - - /** - * @param keyMgrs Key managers. - * @param chain Certificate chain, if known. - * @param privateKeyMissing Whether the loaded store contains no private key. - */ - private KeyMaterial(@Nullable KeyManager[] keyMgrs, @Nullable X509Certificate[] chain, boolean privateKeyMissing) { - this.keyMgrs = keyMgrs == null ? null : keyMgrs.clone(); - this.chain = chain == null || chain.length == 0 ? null : chain.clone(); - this.privateKeyMissing = privateKeyMissing; - } - - /** @return Key managers, or {@code null} to use the provider's defaults. */ - public @Nullable KeyManager[] keyManagers() { - return keyMgrs == null ? null : keyMgrs.clone(); - } - - /** - * Takes the chain from the store used to create the key managers. A store with one private key and an X.509 chain - * supplies that chain. More than one private key, or a chain containing a non-X.509 certificate, leaves the chain - * unknown. A store without a private key records that fact separately from an unknown chain. - * - * @param keyMgrs Key managers initialized from the store. - * @param store Already loaded key store. - * @return Key managers and the chain of the single private key, if known. - * @throws KeyStoreException If the store cannot be read. - */ - public static KeyMaterial fromKeyStore(KeyManager[] keyMgrs, KeyStore store) throws KeyStoreException { - String keyAlias = null; - - for (Enumeration aliases = store.aliases(); aliases.hasMoreElements(); ) { - String alias = aliases.nextElement(); - - if (!store.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) - continue; - - if (keyAlias != null) - return new KeyMaterial(keyMgrs, null); - - keyAlias = alias; - } - - if (keyAlias == null) - return new KeyMaterial(keyMgrs, null, true); - - Certificate[] certs = store.getCertificateChain(keyAlias); - - if (certs == null || certs.length == 0) - return new KeyMaterial(keyMgrs, null); - - X509Certificate[] chain = new X509Certificate[certs.length]; - - for (int i = 0; i < certs.length; i++) { - if (!(certs[i] instanceof X509Certificate)) - return new KeyMaterial(keyMgrs, null); - - chain[i] = (X509Certificate)certs[i]; - } - - return new KeyMaterial(keyMgrs, chain); - } - } } diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java index 193aa6c6a2013..767f51d6b01c1 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/DelegatingSSLContextSpi.java @@ -51,13 +51,12 @@ class DelegatingSSLContextSpi extends SSLContextSpi { /** {@inheritDoc} */ @Override protected SSLSocketFactory engineGetSocketFactory() { - return parameters == null ? delegate.getSocketFactory() : new SSLSocketFactoryWrapper(delegate.getSocketFactory(), parameters); + return new SSLSocketFactoryWrapper(delegate.getSocketFactory(), parameters); } /** {@inheritDoc} */ @Override protected SSLServerSocketFactory engineGetServerSocketFactory() { - return parameters == null ? delegate.getServerSocketFactory() - : new SSLServerSocketFactoryWrapper(delegate.getServerSocketFactory(), parameters); + return new SSLServerSocketFactoryWrapper(delegate.getServerSocketFactory(), parameters); } /** {@inheritDoc} */ diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/KeyMaterial.java b/modules/commons/src/main/java/org/apache/ignite/ssl/KeyMaterial.java new file mode 100644 index 0000000000000..a07796e8fe2de --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/KeyMaterial.java @@ -0,0 +1,127 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.ssl; + +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import org.apache.ignite.internal.util.typedef.internal.A; +import org.jetbrains.annotations.Nullable; + +/** + * A loaded key store and the settings needed to create its key managers. Both managers and certificate metadata are read from + * this store without loading it again. The caller must not modify the store while building a context and its metadata. + */ +public final class KeyMaterial { + /** */ + private final KeyStore store; + + /** */ + private final char[] keyPwd; + + /** */ + private final String algorithm; + + /** + * @param store Loaded key store. + * @param keyPwd Password for its private keys, or {@code null} if the store requires no password. + * @param algorithm Key manager algorithm. + */ + public KeyMaterial(KeyStore store, @Nullable char[] keyPwd, String algorithm) { + A.notNull(store, "store", algorithm, "algorithm"); + + this.store = store; + this.keyPwd = keyPwd == null ? null : keyPwd.clone(); + this.algorithm = algorithm; + } + + /** + * @return Key managers initialized from the loaded store. + * @throws SSLException If the algorithm is unavailable or the keys cannot be read. + */ + public KeyManager[] keyManagers() throws SSLException { + try { + KeyManagerFactory factory = KeyManagerFactory.getInstance(algorithm); + + factory.init(store, keyPwd); + + return factory.getKeyManagers(); + } + catch (NoSuchAlgorithmException e) { + throw new SSLException("Unsupported key algorithm: " + algorithm, e); + } + catch (GeneralSecurityException e) { + throw new SSLException("Failed to initialize Key Manager (security exception occurred)", e); + } + } + + /** + * @return Immutable X.509 chains by private-key alias, own certificate first. An empty map means there are no private keys. + * An empty list retains an alias whose chain is absent or contains a non-X.509 certificate. + * @throws SSLException If the loaded store cannot be read. + */ + public Map> certificateChains() throws SSLException { + try { + Map> chains = new TreeMap<>(); + + for (Enumeration aliases = store.aliases(); aliases.hasMoreElements(); ) { + String alias = aliases.nextElement(); + + if (store.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) + chains.put(alias, certificateChain(store.getCertificateChain(alias))); + } + + return Collections.unmodifiableMap(chains); + } + catch (KeyStoreException e) { + throw new SSLException("Failed to read certificate chains from key store", e); + } + } + + /** + * @param certs Chain from the store, if present. + * @return Immutable X.509 chain, or an empty list if it is absent or contains another certificate type. + */ + private static List certificateChain(@Nullable Certificate[] certs) { + if (certs == null) + return Collections.emptyList(); + + List chain = new ArrayList<>(certs.length); + + for (Certificate cert : certs) { + if (!(cert instanceof X509Certificate)) + return Collections.emptyList(); + + chain.add((X509Certificate)cert); + } + + return Collections.unmodifiableList(chain); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java index af182ac8435cc..fb92fdd08d320 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java @@ -17,75 +17,20 @@ package org.apache.ignite.ssl; -import java.security.cert.X509Certificate; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; -import org.jetbrains.annotations.Nullable; /** - * Wrapper for {@link SSLContext} with custom SSL parameters and the certificate chain loaded with the context. + * Wrapper for {@link SSLContext} that extends the source context with custom SSL parameters. */ public class SSLContextWrapper extends SSLContext { - /** Chain loaded with the delegate, or {@code null} if unknown. */ - private final X509Certificate[] chain; - - /** Whether the loaded key material is known to contain no private key. */ - private final boolean privateKeyMissing; - /** - * Wraps a context with custom SSL parameters. If the delegate is an {@code SSLContextWrapper}, copies its - * certificate chain and missing-private-key flag. - * * @param delegate Wrapped SSL context. * @param sslParameters Extended SSL parameters. */ public SSLContextWrapper(SSLContext delegate, SSLParameters sslParameters) { - this(delegate, sslParameters, - delegate instanceof SSLContextWrapper ? ((SSLContextWrapper)delegate).getCertificateChain() : null, - delegate instanceof SSLContextWrapper && ((SSLContextWrapper)delegate).isPrivateKeyMissing()); - } - - /** - * Wraps a context with its loaded certificate chain. The chain describes the key material, not the outcome of a TLS - * handshake. The delegate must not be reinitialized with different key material after wrapping. - * This constructor uses the supplied chain and sets the missing-private-key flag to {@code false}. - * It does not copy metadata from the delegate. - * - * @param delegate Wrapped SSL context. - * @param sslParameters Extended SSL parameters, or {@code null} to keep the delegate's parameters. - * @param chain Chain of the single private key, own certificate first, or {@code null} if unknown. - */ - public SSLContextWrapper(SSLContext delegate, @Nullable SSLParameters sslParameters, @Nullable X509Certificate[] chain) { - this(delegate, sslParameters, chain, false); - } - - /** - * Wraps a context with its loaded certificate chain and records whether that key material has no private key. - * Both metadata values come from the arguments, even if the delegate is an {@code SSLContextWrapper}. - * - * @param delegate Wrapped SSL context. - * @param sslParameters Extended SSL parameters, or {@code null} to keep the delegate's parameters. - * @param chain Chain of the single private key, own certificate first, or {@code null} if unknown. - * @param privateKeyMissing {@code true} only if the loaded material is known to contain no private key. Do not pass - * {@code true} just because the chain is unknown: the material may contain several keys or use custom key managers. - */ - public SSLContextWrapper(SSLContext delegate, @Nullable SSLParameters sslParameters, @Nullable X509Certificate[] chain, - boolean privateKeyMissing) { super(new DelegatingSSLContextSpi(delegate, sslParameters), delegate.getProvider(), delegate.getProtocol()); - - this.chain = chain == null || chain.length == 0 ? null : chain.clone(); - this.privateKeyMissing = privateKeyMissing; - } - - /** @return Chain loaded with the context, own certificate first, or {@code null} if unknown. */ - public @Nullable X509Certificate[] getCertificateChain() { - return chain == null ? null : chain.clone(); - } - - /** @return {@code true} only if the loaded material is known to contain no private key. */ - public boolean isPrivateKeyMissing() { - return privateKeyMissing; } } diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslBundle.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslBundle.java new file mode 100644 index 0000000000000..e328dfeb176cc --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslBundle.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.ssl; + +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import javax.net.ssl.SSLContext; +import org.apache.ignite.internal.util.typedef.internal.A; +import org.jetbrains.annotations.Nullable; + +/** + * An initialized SSL context and a snapshot of the certificate chains loaded with it. The chains describe its key material, + * not the certificate selected for a particular connection. Do not reinitialize the context after creating the bundle. + */ +public final class SslBundle { + /** */ + private final SSLContext ctx; + + /** */ + private final Map> chains; + + /** + * @param ctx Initialized SSL context. + * @param chains Chains by private-key alias, own certificate first. {@code null} means the aliases are unknown; + * an empty map means there are no private keys. An empty list means that alias has a private key but its X.509 chain + * is unknown. Trusted certificate entries and secret keys must not be included. + */ + public SslBundle(SSLContext ctx, @Nullable Map> chains) { + A.notNull(ctx, "ctx"); + + this.ctx = ctx; + + if (chains == null) + this.chains = null; + else { + Map> copy = new TreeMap<>(); + + chains.forEach((alias, chain) -> copy.put(alias, List.copyOf(chain))); + + this.chains = Collections.unmodifiableMap(copy); + } + } + + /** @return Initialized context. */ + public SSLContext context() { + return ctx; + } + + /** + * @return Immutable chains ordered by alias, or {@code null} if the aliases are unknown. An empty map means no private keys; + * an empty list means that alias has a private key with an unknown X.509 chain. + */ + public @Nullable Map> certificateChains() { + return chains; + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java index 05940885e1224..1ca71f50468be 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java @@ -28,12 +28,12 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import org.apache.ignite.IgniteException; import org.apache.ignite.internal.util.typedef.internal.A; /** @@ -261,33 +261,35 @@ public static TrustManager getDisabledTrustManager() { /** * {@inheritDoc} * - * @deprecated Use {@link #createKeyMaterial()} to retain certificate metadata. + * @deprecated Use {@link #createSslBundle()} to obtain a context with its certificate chains. */ @Deprecated @Override protected final KeyManager[] createKeyManagers() throws SSLException { - return createKeyMaterial().keyManagers(); + return loadKeyMaterial().keyManagers(); } /** {@inheritDoc} */ - @Override protected final KeyMaterial createKeyMaterial() throws SSLException { + @Override public SslBundle createSslBundle() { try { - KeyManagerFactory keyMgrFactory = KeyManagerFactory.getInstance(keyAlgorithm); + checkParameters(); - KeyStore keyStore = loadKeyStore(keyStoreType, keyStoreFilePath, keyStorePwd); - - keyMgrFactory.init(keyStore, keyStorePwd); - - return KeyMaterial.fromKeyStore(keyMgrFactory.getKeyManagers(), keyStore); - } - catch (NoSuchAlgorithmException e) { - throw new SSLException("Unsupported key algorithm: " + keyAlgorithm, e); + return createSslBundle(loadKeyMaterial()); } - catch (GeneralSecurityException e) { - throw new SSLException("Failed to initialize Key Manager (security exception occurred) [type=" + - keyStoreType + ", keyStorePath=" + keyStoreFilePath + ']', e); + catch (SSLException e) { + throw new IgniteException(e); } } + /** + * Loads the key store once for both key managers and certificate metadata. + * + * @return Loaded store with its key password and manager algorithm. + * @throws SSLException If the store cannot be loaded. + */ + protected KeyMaterial loadKeyMaterial() throws SSLException { + return new KeyMaterial(loadKeyStore(keyStoreType, keyStoreFilePath, keyStorePwd), keyStorePwd, keyAlgorithm); + } + /** {@inheritDoc} */ @Override protected final TrustManager[] createTrustManagers() throws SSLException { if (trustMgrs != null) diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 408df05840862..96e3842c8dc9e 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -18,15 +18,21 @@ package org.apache.ignite.ssl; import java.security.KeyStore; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; 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 java.util.concurrent.Callable; import javax.crypto.KeyGenerator; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import org.apache.ignite.IgniteException; import org.apache.ignite.testframework.GridTestUtils; @@ -38,7 +44,7 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotEquals; -/** Tests that certificate metadata belongs to the key material used to create a context. */ +/** Tests that a bundle's certificate metadata belongs to the key material used to create its context. */ public class SslContextMetadataTest extends GridCommonAbstractTest { /** Store and key password of the generated test material. */ private static final char[] PASSWORD = TestCertificateAuthority.PWD; @@ -51,40 +57,46 @@ public class SslContextMetadataTest extends GridCommonAbstractTest { ca = new TestCertificateAuthority("metadata-ca"); } - /** A fresh context loads the replacement store once and leaves the cached context's certificate metadata unchanged. */ + /** Each bundle loads its store once; fresh bundles do not replace the context cached by the legacy create method. */ @Test - public void testFreshContextUsesReplacementStoreAndPreservesCachedContext() throws Exception { - KeyStore originalKeys = ca.keyStoreWithPrivateKey(); - X509Certificate[] originalChain = certificateChain(originalKeys); + public void testFreshBundlePreservesCachedContext() throws Exception { + KeyStore originalKeys = ca.keyStoreWithPrivateKey("original"); + List originalChain = Arrays.asList(certificateChain(originalKeys)); InMemoryStoreFactory factory = new InMemoryStoreFactory(originalKeys); + SslBundle original = factory.createSslBundle(); + + assertEquals(Map.of("original", originalChain), original.certificateChains()); + assertEquals(1, factory.loadKeyStoreCalls); + assertEquals(1, factory.parameterChecks); SSLContext cached = factory.create(); - assertEquals(1, factory.loadKeyStoreCalls); - assertArrayEquals(originalChain, reportedChain(cached)); + assertSame(cached, factory.create()); + assertEquals(2, factory.loadKeyStoreCalls); + assertEquals(2, factory.parameterChecks); - KeyStore replacementKeys = ca.keyStoreWithPrivateKey(); - X509Certificate[] replacementChain = certificateChain(replacementKeys); + KeyStore replacementKeys = ca.keyStoreWithPrivateKey("replacement"); + List replacementChain = Arrays.asList(certificateChain(replacementKeys)); - assertNotEquals(originalChain[0], replacementChain[0]); + assertNotEquals(originalChain.get(0), replacementChain.get(0)); factory.store = replacementKeys; - SSLContext fresh = factory.createNew(); - - assertNotSame(cached, fresh); - assertEquals(2, factory.loadKeyStoreCalls); - assertArrayEquals(replacementChain, reportedChain(fresh)); + SslBundle fresh = factory.createSslBundle(); + assertNotSame(cached, fresh.context()); + assertNotSame(original.context(), fresh.context()); + assertEquals(Map.of("replacement", replacementChain), fresh.certificateChains()); + assertEquals(Map.of("original", originalChain), original.certificateChains()); assertSame(cached, factory.create()); - assertArrayEquals(originalChain, reportedChain(cached)); - assertEquals("Returning the cached context must not load the store again", 2, factory.loadKeyStoreCalls); + assertEquals(3, factory.loadKeyStoreCalls); + assertEquals(3, factory.parameterChecks); } - /** Both factory methods retain certificate metadata when they also apply TLS parameters. */ + /** TLS parameters apply to the context while its metadata remains available separately. */ @Test public void testMetadataWithTlsParameters() throws Exception { - KeyStore store = ca.keyStoreWithPrivateKey(); + KeyStore store = ca.keyStoreWithPrivateKey("node"); InMemoryStoreFactory factory = new InMemoryStoreFactory(store); String[] protocols = {"TLSv1.2"}; String[] ciphers = {"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}; @@ -92,33 +104,36 @@ public void testMetadataWithTlsParameters() throws Exception { factory.setProtocols(protocols); factory.setCipherSuites(ciphers); - for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertArrayEquals(certificateChain(store), reportedChain(ctx)); - assertFalse("The loaded store contains a private key", reportsMissingPrivateKey(ctx)); + SslBundle bundle = factory.createSslBundle(); - SSLEngine engine = ctx.createSSLEngine(); + assertEquals(Map.of("node", Arrays.asList(certificateChain(store))), bundle.certificateChains()); + + for (SSLContext ctx : new SSLContext[] {bundle.context(), factory.create()}) { + SSLEngine engine = ctx.createSSLEngine("peer.example", 10800); assertArrayEquals(protocols, engine.getEnabledProtocols()); assertArrayEquals(ciphers, engine.getEnabledCipherSuites()); + assertEquals("peer.example", engine.getPeerHost()); + assertEquals(10800, engine.getPeerPort()); } } - /** Multiple private keys leave the chain unknown but are not reported as missing. */ + /** Every private-key alias retains its own chain. */ @Test - public void testMultiplePrivateKeysHaveUnknownChain() throws Exception { + public void testMultiplePrivateKeysRetainAllChains() throws Exception { String firstKey = "first-key"; String secondKey = "second-key"; KeyStore store = ca.keyStoreWithPrivateKey(firstKey); KeyStore other = ca.keyStoreWithPrivateKey(secondKey); + List firstChain = Arrays.asList(certificateChain(store)); + List secondChain = Arrays.asList(certificateChain(other)); store.setKeyEntry(secondKey, other.getKey(secondKey, PASSWORD), PASSWORD, other.getCertificateChain(secondKey)); - InMemoryStoreFactory factory = new InMemoryStoreFactory(store); + SslBundle bundle = new InMemoryStoreFactory(store).createSslBundle(); - for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertNull(reportedChain(ctx)); - assertFalse("Two private keys are present even though the served chain is unknown", reportsMissingPrivateKey(ctx)); - } + assertEquals(Map.of(firstKey, firstChain, secondKey, secondChain), bundle.certificateChains()); + assertEquals(List.of(firstKey, secondKey), new ArrayList<>(bundle.certificateChains().keySet())); } /** Trusted certificates and secret keys do not count as private keys. */ @@ -136,69 +151,97 @@ public void testOnlyPrivateKeyEntriesCount() throws Exception { new KeyStore.PasswordProtection(PASSWORD)); InMemoryStoreFactory factory = new InMemoryStoreFactory(store); + SslBundle withoutKey = factory.createSslBundle(); - SSLContext withoutKey = factory.create(); - - assertNull(reportedChain(withoutKey)); - assertTrue("Neither the trusted certificate nor the secret key is a private key", reportsMissingPrivateKey(withoutKey)); + assertEquals(Map.of(), withoutKey.certificateChains()); + assertNotNull(withoutKey.context().getSocketFactory()); store.setKeyEntry(keyAlias, keys.getKey(keyAlias, PASSWORD), PASSWORD, keys.getCertificateChain(keyAlias)); - SSLContext withKey = factory.createNew(); + SslBundle withKey = factory.createSslBundle(); - assertArrayEquals(certificateChain(keys), reportedChain(withKey)); - assertFalse("The store now contains a private-key entry", reportsMissingPrivateKey(withKey)); + assertEquals(Map.of(keyAlias, Arrays.asList(certificateChain(keys))), withKey.certificateChains()); + assertEquals("The earlier bundle remains keyless", Map.of(), withoutKey.certificateChains()); } - /** Trust-only contexts can still be built for clients, and another parameters wrapper preserves the missing-key marker. */ + /** A private key with a non-X.509 chain is retained as an alias with unknown certificate metadata. */ @Test - public void testTrustOnlyContextCreation() throws Exception { - InMemoryStoreFactory factory = new InMemoryStoreFactory(ca.trustStore()); - String[] protocols = {"TLSv1.2"}; - SSLParameters params = new SSLParameters(); + public void testNonX509ChainRetainsPrivateKeyAlias() throws Exception { + String alias = "private-key"; + KeyStore store = ca.keyStoreWithPrivateKey(alias); + X509Certificate cert = (X509Certificate)store.getCertificate(alias); + Certificate otherType = new Certificate("test-certificate") { + /** {@inheritDoc} */ + @Override public byte[] getEncoded() throws CertificateEncodingException { + return cert.getEncoded(); + } + + /** {@inheritDoc} */ + @Override public void verify(PublicKey key) { + throw new UnsupportedOperationException(); + } + + /** {@inheritDoc} */ + @Override public void verify(PublicKey key, String provider) { + throw new UnsupportedOperationException(); + } - params.setProtocols(protocols); + /** {@inheritDoc} */ + @Override public PublicKey getPublicKey() { + return cert.getPublicKey(); + } - for (SSLContext ctx : new SSLContext[] {factory.create(), factory.createNew()}) { - assertNull(reportedChain(ctx)); - assertTrue(((SSLContextWrapper)ctx).isPrivateKeyMissing()); + /** {@inheritDoc} */ + @Override public String toString() { + return "Test certificate with a non-X.509 type"; + } + }; - SSLContextWrapper outer = new SSLContextWrapper(ctx, params); + store.setKeyEntry(alias, store.getKey(alias, PASSWORD), PASSWORD, new Certificate[] {otherType}); - assertTrue(outer.isPrivateKeyMissing()); - assertArrayEquals(protocols, outer.createSSLEngine().getEnabledProtocols()); - } + KeyMaterial material = new KeyMaterial(store, PASSWORD, SslContextFactory.DFLT_KEY_ALGORITHM); + + assertEquals(Map.of(alias, List.of()), material.certificateChains()); } - /** Legacy hooks run for each new context, but not when create() returns its cached context. */ + /** Legacy hooks run once per new context and leave metadata unknown rather than reporting no private keys. */ @Test public void testLegacyHooksRunOncePerBuiltContext() { LegacyFactory factory = new LegacyFactory(); - SSLContext first = factory.create(); - assertNull(reportedChain(first)); - assertFalse("Legacy hooks do not report whether a private key is available", reportsMissingPrivateKey(first)); - assertEquals(1, factory.parameterChecks); - assertEquals(1, factory.keyManagerCalls); - assertEquals(1, factory.trustManagerCalls); - assertSame(first, factory.create()); - assertEquals(1, factory.parameterChecks); assertEquals(1, factory.keyManagerCalls); assertEquals(1, factory.trustManagerCalls); - SSLContext next = factory.createNew(); + SslBundle next = factory.createSslBundle(); - assertNotSame(first, next); - assertNull(reportedChain(next)); - assertFalse("Legacy hooks do not report whether a private key is available", reportsMissingPrivateKey(next)); + assertNotSame(first, next.context()); + assertNull(next.certificateChains()); + assertSame(first, factory.create()); assertEquals(2, factory.parameterChecks); assertEquals(2, factory.keyManagerCalls); assertEquals(2, factory.trustManagerCalls); } + /** Existing subclasses can still populate the protected cache before the first create call. */ + @Test + public void testLegacyProtectedCache() throws Exception { + LegacyFactory factory = new LegacyFactory(); + SSLContext prepared = TestCertificateAuthority.context(ca.keyStoreWithPrivateKey(), ca.trustStore()); + + factory.sslCtx.set(prepared); + + assertSame(prepared, factory.create()); + assertEquals(0, factory.parameterChecks); + assertEquals(0, factory.keyManagerCalls); + assertEquals(0, factory.trustManagerCalls); + + assertNotSame(prepared, factory.createSslBundle().context()); + assertSame(prepared, factory.create()); + } + /** A subclass can still obtain key managers from the file factory's legacy hook. */ @Test public void testLegacyKeyManagerAdapter() throws Exception { @@ -210,7 +253,21 @@ public void testLegacyKeyManagerAdapter() throws Exception { assertEquals(1, factory.loadKeyStoreCalls); } - /** A factory that implements neither key-material hook reports the missing implementation. */ + /** Invalid parameters are reported before loading the store. */ + @Test + public void testInvalidParametersDoNotLoadStore() throws Exception { + InMemoryStoreFactory factory = new InMemoryStoreFactory(ca.keyStoreWithPrivateKey()); + + factory.setTrustManagers((TrustManager[])null); + + GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, + IgniteException.class, "trustManagers must be provided"); + + assertEquals(1, factory.parameterChecks); + assertEquals(0, factory.loadKeyStoreCalls); + } + + /** A factory implementing neither a bundle builder nor the legacy key-manager hook reports the missing implementation. */ @Test public void testMissingKeyMaterialImplementation() { AbstractSslContextFactory factory = new AbstractSslContextFactory() { @@ -225,42 +282,42 @@ public void testMissingKeyMaterialImplementation() { } }; - String msg = "Subclass must override createKeyMaterial() or createKeyManagers()"; + String msg = "Subclass must override createSslBundle() or createKeyManagers()"; Throwable err = GridTestUtils.assertThrows(log, factory::create, IgniteException.class, msg); assertTrue(err.getCause() instanceof SSLException); - err = GridTestUtils.assertThrows(log, factory::createNew, IgniteException.class, msg); + err = GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, IgniteException.class, msg); assertTrue(err.getCause() instanceof SSLException); } - /** A subclass can supply material directly, and callers cannot replace its recorded managers or chain through arrays. */ + /** A custom store source does not need the legacy key-manager hook and keeps its own copy of the key password. */ @Test - public void testKeyMaterialDefensiveCopies() throws Exception { - KeyStore keys = ca.keyStoreWithPrivateKey(); - KeyManager[] mgrs = TestCertificateAuthority.keyManagers(keys); - KeyManager mgr = mgrs[0]; - X509Certificate[] expected = certificateChain(keys); - X509Certificate[] input = expected.clone(); - AbstractSslContextFactory.KeyMaterial material = new AbstractSslContextFactory.KeyMaterial(mgrs, input); - - mgrs[0] = null; - input[0] = null; - material.keyManagers()[0] = null; + public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { + KeyStore store = ca.keyStoreWithPrivateKey("node"); + char[] pwd = PASSWORD.clone(); + KeyMaterial material = new KeyMaterial(store, pwd, SslContextFactory.DFLT_KEY_ALGORITHM); - assertSame(mgr, material.keyManagers()[0]); + Arrays.fill(pwd, '?'); AbstractSslContextFactory factory = new AbstractSslContextFactory() { /** {@inheritDoc} */ - @Override protected void checkParameters() { - // No parameters are required by this factory. + @Override public SslBundle createSslBundle() { + try { + checkParameters(); + + return createSslBundle(material); + } + catch (SSLException e) { + throw new IgniteException(e); + } } /** {@inheritDoc} */ - @Override protected KeyMaterial createKeyMaterial() { - return material; + @Override protected void checkParameters() { + // The test supplies already loaded material. } /** {@inheritDoc} */ @@ -269,104 +326,34 @@ public void testKeyMaterialDefensiveCopies() throws Exception { } }; - assertArrayEquals(expected, reportedChain(factory.create())); - assertArrayEquals(expected, reportedChain(factory.createNew())); + assertEquals(Map.of("node", Arrays.asList(certificateChain(store))), factory.createSslBundle().certificateChains()); } - /** Neither callers nor another parameters wrapper can replace a context's recorded chain. */ + /** Mutating the source map, its lists or the store cannot change an existing bundle. */ @Test - public void testWrapperPreservesAndCopiesChain() throws Exception { - X509Certificate[] expected = certificateChain(ca.keyStoreWithPrivateKey()); - X509Certificate[] input = expected.clone(); - SSLContext delegate = SSLContext.getInstance("TLS"); + public void testImmutableCertificateMetadata() throws Exception { + KeyStore store = ca.keyStoreWithPrivateKey("node"); + SslBundle loaded = new InMemoryStoreFactory(store).createSslBundle(); + List expected = Arrays.asList(certificateChain(store)); + List inputChain = new ArrayList<>(expected); + Map> input = new HashMap<>(); - delegate.init(null, null, null); + input.put("node", inputChain); - SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, input); + SslBundle bundle = new SslBundle(loaded.context(), input); - input[0] = null; - wrapper.getCertificateChain()[0] = null; + inputChain.clear(); + input.clear(); + store.deleteEntry("node"); - assertArrayEquals(expected, wrapper.getCertificateChain()); + assertEquals(Map.of("node", expected), loaded.certificateChains()); + assertEquals(Map.of("node", expected), bundle.certificateChains()); + assertSame(loaded.context(), bundle.context()); - String[] protocols = {"TLSv1.2"}; - SSLParameters params = new SSLParameters(); - - params.setProtocols(protocols); - - SSLContextWrapper outer = new SSLContextWrapper(wrapper, params); - - assertArrayEquals(expected, outer.getCertificateChain()); - assertArrayEquals(protocols, outer.createSSLEngine().getEnabledProtocols()); - } - - /** Explicit metadata takes precedence over the wrapped context's metadata. */ - @Test - public void testExplicitWrapperMetadataOverridesDelegateMetadata() throws Exception { - X509Certificate[] expectedChain = certificateChain(ca.keyStoreWithPrivateKey()); - SSLContext delegate = SSLContext.getInstance("TLS"); - - delegate.init(null, null, null); - - SSLContextWrapper keyless = new SSLContextWrapper(delegate, null, null, true); - SSLContextWrapper withChain = new SSLContextWrapper(keyless, null, expectedChain); - - assertArrayEquals(expectedChain, withChain.getCertificateChain()); - assertFalse(withChain.isPrivateKeyMissing()); - - SSLContextWrapper unknownChain = new SSLContextWrapper(withChain, null, null); - - assertNull(unknownChain.getCertificateChain()); - assertFalse(unknownChain.isPrivateKeyMissing()); - - SSLContextWrapper explicitlyKeyless = new SSLContextWrapper(withChain, null, null, true); - - assertNull(explicitlyKeyless.getCertificateChain()); - assertTrue(explicitlyKeyless.isPrivateKeyMissing()); - } - - /** Adding metadata without TLS parameters preserves peer addresses, TLS defaults and unconnected socket creation. */ - @Test - public void testMetadataWrapperPreservesContextBehavior() throws Exception { - SSLContext delegate = SSLContext.getInstance("TLS"); - - delegate.init(null, null, null); - - SSLContextWrapper wrapper = new SSLContextWrapper(delegate, null, certificateChain(ca.keyStoreWithPrivateKey())); - String host = "peer.example"; - int port = 10800; - SSLEngine engine = wrapper.createSSLEngine(host, port); - SSLEngine delegateEngine = delegate.createSSLEngine(); - - assertEquals(host, engine.getPeerHost()); - assertEquals(port, engine.getPeerPort()); - assertArrayEquals(delegateEngine.getEnabledProtocols(), engine.getEnabledProtocols()); - assertArrayEquals(delegateEngine.getEnabledCipherSuites(), engine.getEnabledCipherSuites()); - - try (SSLSocket expected = (SSLSocket)delegate.getSocketFactory().createSocket(); - SSLSocket actual = (SSLSocket)wrapper.getSocketFactory().createSocket()) { - assertFalse(actual.isConnected()); - assertFalse(actual.isBound()); - assertArrayEquals(expected.getEnabledProtocols(), actual.getEnabledProtocols()); - assertArrayEquals(expected.getEnabledCipherSuites(), actual.getEnabledCipherSuites()); - } - - try (SSLServerSocket expected = (SSLServerSocket)delegate.getServerSocketFactory().createServerSocket(); - SSLServerSocket actual = (SSLServerSocket)wrapper.getServerSocketFactory().createServerSocket()) { - assertFalse(actual.isBound()); - assertArrayEquals(expected.getEnabledProtocols(), actual.getEnabledProtocols()); - assertArrayEquals(expected.getEnabledCipherSuites(), actual.getEnabledCipherSuites()); - } - } - - /** @return Chain attached to the context, if known. */ - private static X509Certificate[] reportedChain(SSLContext ctx) { - return ctx instanceof SSLContextWrapper ? ((SSLContextWrapper)ctx).getCertificateChain() : null; - } - - /** @return Whether the context reports that no private key was loaded. Unknown metadata does not set this marker. */ - private static boolean reportsMissingPrivateKey(SSLContext ctx) { - return ctx instanceof SSLContextWrapper && ((SSLContextWrapper)ctx).isPrivateKeyMissing(); + GridTestUtils.assertThrows(log, () -> bundle.certificateChains().put("other", List.of()), + UnsupportedOperationException.class, null); + GridTestUtils.assertThrows(log, () -> bundle.certificateChains().get("node").set(0, expected.get(1)), + UnsupportedOperationException.class, null); } /** A file factory whose existing store-loading hook supplies the test material. */ @@ -377,6 +364,9 @@ private static class InMemoryStoreFactory extends SslContextFactory { /** */ private int loadKeyStoreCalls; + /** */ + private int parameterChecks; + /** */ InMemoryStoreFactory(KeyStore store) { this.store = store; @@ -387,6 +377,13 @@ private static class InMemoryStoreFactory extends SslContextFactory { setTrustManagers(getDisabledTrustManager()); } + /** {@inheritDoc} */ + @Override protected void checkParameters() throws SSLException { + parameterChecks++; + + super.checkParameters(); + } + /** {@inheritDoc} */ @Override protected KeyStore loadKeyStore(String type, String path, char[] pwd) { loadKeyStoreCalls++; @@ -401,7 +398,7 @@ private KeyManager[] legacyKeyManagers() throws SSLException { } } - /** A subclass that implements the manager hooks without overriding createKeyMaterial(). */ + /** A subclass that implements only the legacy manager hooks. */ private static class LegacyFactory extends AbstractSslContextFactory { /** */ private int parameterChecks; diff --git a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java index 29bcfd74aad48..c7d9b71788eda 100644 --- a/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java +++ b/modules/core/src/test/java/org/apache/ignite/testframework/TestCertificateAuthority.java @@ -38,7 +38,8 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.security.auth.x500.X500Principal; -import org.apache.ignite.ssl.SSLContextWrapper; +import org.apache.ignite.ssl.KeyMaterial; +import org.apache.ignite.ssl.SslBundle; import org.bouncycastle.asn1.x509.BasicConstraints; import org.bouncycastle.asn1.x509.ExtendedKeyUsage; import org.bouncycastle.asn1.x509.Extension; @@ -169,14 +170,28 @@ public static KeyStore trustStore(TestCertificateAuthority... authorities) throw /** * @param keyStore Store with the certificate to present. * @param trustStore Store with the authorities to trust. - * @return Context using the supplied key and trust stores, with certificate metadata attached. + * @return Context using the supplied key and trust stores. */ public static SSLContext context(KeyStore keyStore, KeyStore trustStore) throws Exception { SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(keyManagers(keyStore), trustManagers(trustStore), null); - return new SSLContextWrapper(ctx, null, certificateChain(keyStore)); + return ctx; + } + + /** + * @param keyStore Store with the certificates to present. + * @param trustStore Store with the authorities to trust. + * @return Context and certificate chains from the supplied stores. + */ + public static SslBundle bundle(KeyStore keyStore, KeyStore trustStore) throws Exception { + KeyMaterial material = new KeyMaterial(keyStore, PWD, KeyManagerFactory.getDefaultAlgorithm()); + SSLContext ctx = SSLContext.getInstance("TLS"); + + ctx.init(material.keyManagers(), trustManagers(trustStore), null); + + return new SslBundle(ctx, material.certificateChains()); } /** From be95316277d8426d59cc46cd31117933bf755e8d Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 15:35:43 +0300 Subject: [PATCH 17/26] IGNITE-29209 Clarify fresh bundle creation contract --- .../org/apache/ignite/ssl/AbstractSslContextFactory.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index dbcac5803ed30..dccf4e9ec68d7 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -113,8 +113,9 @@ public void setProtocols(String... protocols) { } /** - * Builds a fresh context with the legacy key-manager hook. Subclasses that load a key store can override this method - * and call {@link #createSslBundle(KeyMaterial)} to include its certificate chains. + * Builds a fresh context and its certificate metadata without calling {@link #create()} or changing its cached context. + * The default implementation calls the legacy key-manager hook and leaves certificate metadata unknown. Subclasses that + * load a key store can override this method and call {@link #createSslBundle(KeyMaterial)} to include its certificate chains. * * @return Fresh context and the certificate metadata available to this factory. * @throws IgniteException If the context could not be created. From 7ab76356a919e030dc97c45f3b1457e34df33950 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 15:45:19 +0300 Subject: [PATCH 18/26] IGNITE-29209 Make metadata test aliases explicit --- .../ignite/ssl/SslContextMetadataTest.java | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 96e3842c8dc9e..8a3796c889eda 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -60,12 +60,13 @@ public class SslContextMetadataTest extends GridCommonAbstractTest { /** Each bundle loads its store once; fresh bundles do not replace the context cached by the legacy create method. */ @Test public void testFreshBundlePreservesCachedContext() throws Exception { - KeyStore originalKeys = ca.keyStoreWithPrivateKey("original"); + String originalAlias = "original"; + KeyStore originalKeys = ca.keyStoreWithPrivateKey(originalAlias); List originalChain = Arrays.asList(certificateChain(originalKeys)); InMemoryStoreFactory factory = new InMemoryStoreFactory(originalKeys); SslBundle original = factory.createSslBundle(); - assertEquals(Map.of("original", originalChain), original.certificateChains()); + assertEquals(Map.of(originalAlias, originalChain), original.certificateChains()); assertEquals(1, factory.loadKeyStoreCalls); assertEquals(1, factory.parameterChecks); @@ -75,7 +76,8 @@ public void testFreshBundlePreservesCachedContext() throws Exception { assertEquals(2, factory.loadKeyStoreCalls); assertEquals(2, factory.parameterChecks); - KeyStore replacementKeys = ca.keyStoreWithPrivateKey("replacement"); + String replacementAlias = "replacement"; + KeyStore replacementKeys = ca.keyStoreWithPrivateKey(replacementAlias); List replacementChain = Arrays.asList(certificateChain(replacementKeys)); assertNotEquals(originalChain.get(0), replacementChain.get(0)); @@ -86,8 +88,8 @@ public void testFreshBundlePreservesCachedContext() throws Exception { assertNotSame(cached, fresh.context()); assertNotSame(original.context(), fresh.context()); - assertEquals(Map.of("replacement", replacementChain), fresh.certificateChains()); - assertEquals(Map.of("original", originalChain), original.certificateChains()); + assertEquals(Map.of(replacementAlias, replacementChain), fresh.certificateChains()); + assertEquals(Map.of(originalAlias, originalChain), original.certificateChains()); assertSame(cached, factory.create()); assertEquals(3, factory.loadKeyStoreCalls); assertEquals(3, factory.parameterChecks); @@ -96,7 +98,8 @@ public void testFreshBundlePreservesCachedContext() throws Exception { /** TLS parameters apply to the context while its metadata remains available separately. */ @Test public void testMetadataWithTlsParameters() throws Exception { - KeyStore store = ca.keyStoreWithPrivateKey("node"); + String alias = "node"; + KeyStore store = ca.keyStoreWithPrivateKey(alias); InMemoryStoreFactory factory = new InMemoryStoreFactory(store); String[] protocols = {"TLSv1.2"}; String[] ciphers = {"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"}; @@ -106,15 +109,13 @@ public void testMetadataWithTlsParameters() throws Exception { SslBundle bundle = factory.createSslBundle(); - assertEquals(Map.of("node", Arrays.asList(certificateChain(store))), bundle.certificateChains()); + assertEquals(Map.of(alias, Arrays.asList(certificateChain(store))), bundle.certificateChains()); for (SSLContext ctx : new SSLContext[] {bundle.context(), factory.create()}) { - SSLEngine engine = ctx.createSSLEngine("peer.example", 10800); + SSLEngine engine = ctx.createSSLEngine(); assertArrayEquals(protocols, engine.getEnabledProtocols()); assertArrayEquals(ciphers, engine.getEnabledCipherSuites()); - assertEquals("peer.example", engine.getPeerHost()); - assertEquals(10800, engine.getPeerPort()); } } @@ -296,7 +297,8 @@ public void testMissingKeyMaterialImplementation() { /** A custom store source does not need the legacy key-manager hook and keeps its own copy of the key password. */ @Test public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { - KeyStore store = ca.keyStoreWithPrivateKey("node"); + String alias = "node"; + KeyStore store = ca.keyStoreWithPrivateKey(alias); char[] pwd = PASSWORD.clone(); KeyMaterial material = new KeyMaterial(store, pwd, SslContextFactory.DFLT_KEY_ALGORITHM); @@ -326,33 +328,34 @@ public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { } }; - assertEquals(Map.of("node", Arrays.asList(certificateChain(store))), factory.createSslBundle().certificateChains()); + assertEquals(Map.of(alias, Arrays.asList(certificateChain(store))), factory.createSslBundle().certificateChains()); } /** Mutating the source map, its lists or the store cannot change an existing bundle. */ @Test public void testImmutableCertificateMetadata() throws Exception { - KeyStore store = ca.keyStoreWithPrivateKey("node"); + String alias = "node"; + KeyStore store = ca.keyStoreWithPrivateKey(alias); SslBundle loaded = new InMemoryStoreFactory(store).createSslBundle(); List expected = Arrays.asList(certificateChain(store)); List inputChain = new ArrayList<>(expected); Map> input = new HashMap<>(); - input.put("node", inputChain); + input.put(alias, inputChain); SslBundle bundle = new SslBundle(loaded.context(), input); inputChain.clear(); input.clear(); - store.deleteEntry("node"); + store.deleteEntry(alias); - assertEquals(Map.of("node", expected), loaded.certificateChains()); - assertEquals(Map.of("node", expected), bundle.certificateChains()); + assertEquals(Map.of(alias, expected), loaded.certificateChains()); + assertEquals(Map.of(alias, expected), bundle.certificateChains()); assertSame(loaded.context(), bundle.context()); GridTestUtils.assertThrows(log, () -> bundle.certificateChains().put("other", List.of()), UnsupportedOperationException.class, null); - GridTestUtils.assertThrows(log, () -> bundle.certificateChains().get("node").set(0, expected.get(1)), + GridTestUtils.assertThrows(log, () -> bundle.certificateChains().get(alias).set(0, expected.get(1)), UnsupportedOperationException.class, null); } From 72a76a4c1db30c8b67b0ef1f2b7a4bda26d1e3af Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 16:10:03 +0300 Subject: [PATCH 19/26] IGNITE-29209 Keep key material loading internal to the file factory --- .../src/main/java/org/apache/ignite/ssl/SslContextFactory.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java index 1ca71f50468be..1f182b2c1c0dd 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java @@ -286,7 +286,7 @@ public static TrustManager getDisabledTrustManager() { * @return Loaded store with its key password and manager algorithm. * @throws SSLException If the store cannot be loaded. */ - protected KeyMaterial loadKeyMaterial() throws SSLException { + private KeyMaterial loadKeyMaterial() throws SSLException { return new KeyMaterial(loadKeyStore(keyStoreType, keyStoreFilePath, keyStorePwd), keyStorePwd, keyAlgorithm); } From a360d09a188ebdddf4bc68d771bcfa2f16086d77 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 16:28:29 +0300 Subject: [PATCH 20/26] IGNITE-29209 Leave unrelated wrapper wording unchanged --- .../src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java index fb92fdd08d320..67bc834d0886c 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SSLContextWrapper.java @@ -21,7 +21,7 @@ import javax.net.ssl.SSLParameters; /** - * Wrapper for {@link SSLContext} that extends the source context with custom SSL parameters. + * Wrapper for {@link SSLContext} that extend source context with custom SSL parameters. */ public class SSLContextWrapper extends SSLContext { /** From b672056dcbafe97d58e7de3e394bc52c5af201ab Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 16:54:40 +0300 Subject: [PATCH 21/26] IGNITE-29209 Isolate legacy context creation from SSL bundles --- .../ignite/ssl/AbstractSslContextFactory.java | 56 +++++++++++-------- .../ignite/ssl/SslContextMetadataTest.java | 44 ++++++++++----- 2 files changed, 64 insertions(+), 36 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index dccf4e9ec68d7..172634dfb7c75 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -19,9 +19,6 @@ import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import javax.cache.configuration.Factory; import javax.net.ssl.KeyManager; @@ -53,7 +50,12 @@ public abstract class AbstractSslContextFactory implements Factory { /** Enabled protocols. */ protected String[] protocols; - /** Cached instance of an {@link SSLContext}. */ + /** + * Cached instance used by the legacy {@link #create()} method. + * + * @deprecated Hold the result of {@link #createSslBundle()} outside the factory. + */ + @Deprecated protected final AtomicReference sslCtx = new AtomicReference<>(); /** @@ -114,43 +116,34 @@ public void setProtocols(String... protocols) { /** * Builds a fresh context and its certificate metadata without calling {@link #create()} or changing its cached context. - * The default implementation calls the legacy key-manager hook and leaves certificate metadata unknown. Subclasses that - * load a key store can override this method and call {@link #createSslBundle(KeyMaterial)} to include its certificate chains. + * Subclasses must override this method. Those that load a key store can call {@link #createSslBundle(KeyMaterial)} to + * include its certificate chains. * * @return Fresh context and the certificate metadata available to this factory. + * @throws UnsupportedOperationException If the subclass does not implement bundle creation. * @throws IgniteException If the context could not be created. */ public SslBundle createSslBundle() { - try { - checkParameters(); - - return createSslBundle(createKeyManagers(), null); - } - catch (SSLException e) { - throw new IgniteException(e); - } + throw new UnsupportedOperationException("Subclass must override createSslBundle()"); } /** * Builds a fresh context from one loaded store. The caller must check its parameters before loading the material. - * This method does not call {@link #createKeyManagers()} or change the context cached by {@link #create()}. * * @param material Loaded key store, key password and manager algorithm. * @return Context and certificate chains from that material. * @throws SSLException If key managers, certificate metadata or the context cannot be created. */ protected SslBundle createSslBundle(KeyMaterial material) throws SSLException { - return createSslBundle(material.keyManagers(), material.certificateChains()); + return new SslBundle(createSslContext(material.keyManagers()), material.certificateChains()); } /** * @param keyMgrs Key managers, or {@code null} to use the provider's defaults. - * @param chains Certificate chains by private-key alias, or {@code null} if unknown. - * @return Initialized context and its certificate metadata. + * @return Initialized context. * @throws SSLException If trust managers or the context cannot be created. */ - private SslBundle createSslBundle(@Nullable KeyManager[] keyMgrs, - @Nullable Map> chains) throws SSLException { + private SSLContext createSslContext(@Nullable KeyManager[] keyMgrs) throws SSLException { TrustManager[] trustMgrs = createTrustManagers(); try { @@ -173,7 +166,7 @@ private SslBundle createSslBundle(@Nullable KeyManager[] keyMgrs, ctx.init(keyMgrs, trustMgrs, null); - return new SslBundle(ctx, chains); + return ctx; } catch (NoSuchAlgorithmException e) { throw new SSLException("Unsupported SSL protocol: " + proto, e); @@ -218,12 +211,29 @@ protected KeyManager[] createKeyManagers() throws SSLException { */ protected abstract TrustManager[] createTrustManagers() throws SSLException; - /** {@inheritDoc} */ + /** + * Returns the cached context, building it through the subclass's bundle method or its legacy key-manager hook. + * + * @return Cached context. + * @deprecated Use {@link #createSslBundle()} and keep the context in the component that uses it. + */ + @Deprecated @Override public SSLContext create() { SSLContext ctx = sslCtx.get(); if (ctx == null) { - ctx = createSslBundle().context(); + try { + if (getClass().getMethod("createSslBundle").getDeclaringClass() == AbstractSslContextFactory.class) { + checkParameters(); + + ctx = createSslContext(createKeyManagers()); + } + else + ctx = createSslBundle().context(); + } + catch (NoSuchMethodException | SSLException e) { + throw new IgniteException(e); + } if (!sslCtx.compareAndSet(null, ctx)) ctx = sslCtx.get(); diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 8a3796c889eda..f9734a47dded7 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -205,7 +205,7 @@ public void testNonX509ChainRetainsPrivateKeyAlias() throws Exception { assertEquals(Map.of(alias, List.of()), material.certificateChains()); } - /** Legacy hooks run once per new context and leave metadata unknown rather than reporting no private keys. */ + /** Legacy creation stays cached; requesting a bundle does not invoke the old hooks. */ @Test public void testLegacyHooksRunOncePerBuiltContext() { LegacyFactory factory = new LegacyFactory(); @@ -216,14 +216,13 @@ public void testLegacyHooksRunOncePerBuiltContext() { assertEquals(1, factory.keyManagerCalls); assertEquals(1, factory.trustManagerCalls); - SslBundle next = factory.createSslBundle(); + GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, + UnsupportedOperationException.class, "Subclass must override createSslBundle()"); - assertNotSame(first, next.context()); - assertNull(next.certificateChains()); assertSame(first, factory.create()); - assertEquals(2, factory.parameterChecks); - assertEquals(2, factory.keyManagerCalls); - assertEquals(2, factory.trustManagerCalls); + assertEquals(1, factory.parameterChecks); + assertEquals(1, factory.keyManagerCalls); + assertEquals(1, factory.trustManagerCalls); } /** Existing subclasses can still populate the protected cache before the first create call. */ @@ -238,9 +237,6 @@ public void testLegacyProtectedCache() throws Exception { assertEquals(0, factory.parameterChecks); assertEquals(0, factory.keyManagerCalls); assertEquals(0, factory.trustManagerCalls); - - assertNotSame(prepared, factory.createSslBundle().context()); - assertSame(prepared, factory.create()); } /** A subclass can still obtain key managers from the file factory's legacy hook. */ @@ -289,9 +285,8 @@ public void testMissingKeyMaterialImplementation() { assertTrue(err.getCause() instanceof SSLException); - err = GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, IgniteException.class, msg); - - assertTrue(err.getCause() instanceof SSLException); + GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, + UnsupportedOperationException.class, "Subclass must override createSslBundle()"); } /** A custom store source does not need the legacy key-manager hook and keeps its own copy of the key password. */ @@ -329,6 +324,29 @@ public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { }; assertEquals(Map.of(alias, Arrays.asList(certificateChain(store))), factory.createSslBundle().certificateChains()); + + SSLContext cached = factory.create(); + + assertSame(cached, factory.create()); + assertNotSame(cached, factory.createSslBundle().context()); + } + + /** Failure in a new factory must not fall back to legacy key managers. */ + @Test + public void testBundleFailureDoesNotUseLegacyManagers() { + String failure = "Certificate source failed"; + LegacyFactory factory = new LegacyFactory() { + /** {@inheritDoc} */ + @Override public SslBundle createSslBundle() { + throw new UnsupportedOperationException(failure); + } + }; + + GridTestUtils.assertThrows(log, factory::create, UnsupportedOperationException.class, failure); + + assertEquals(0, factory.parameterChecks); + assertEquals(0, factory.keyManagerCalls); + assertEquals(0, factory.trustManagerCalls); } /** Mutating the source map, its lists or the store cannot change an existing bundle. */ From 65a208e51413a3445152b0bbd00a54e131476ce9 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 17:40:47 +0300 Subject: [PATCH 22/26] IGNITE-29209 Keep legacy SSL creation independent of bundles --- .../ignite/ssl/AbstractSslContextFactory.java | 15 ++++----- .../ignite/ssl/SslContextMetadataTest.java | 31 ++++++++++++------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index 172634dfb7c75..9d62832321573 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -202,7 +202,7 @@ protected void checkNullParameter(Object param, String name) throws SSLException */ @Deprecated protected KeyManager[] createKeyManagers() throws SSLException { - throw new SSLException("Subclass must override createSslBundle() or createKeyManagers()"); + throw new SSLException("Subclass must override createKeyManagers() to use create()"); } /** @@ -212,7 +212,8 @@ protected KeyManager[] createKeyManagers() throws SSLException { protected abstract TrustManager[] createTrustManagers() throws SSLException; /** - * Returns the cached context, building it through the subclass's bundle method or its legacy key-manager hook. + * Returns the cached context, building it through the legacy key-manager hook. + * Overriding {@link #createSslBundle()} does not change this method's behavior. * * @return Cached context. * @deprecated Use {@link #createSslBundle()} and keep the context in the component that uses it. @@ -223,15 +224,11 @@ protected KeyManager[] createKeyManagers() throws SSLException { if (ctx == null) { try { - if (getClass().getMethod("createSslBundle").getDeclaringClass() == AbstractSslContextFactory.class) { - checkParameters(); + checkParameters(); - ctx = createSslContext(createKeyManagers()); - } - else - ctx = createSslBundle().context(); + ctx = createSslContext(createKeyManagers()); } - catch (NoSuchMethodException | SSLException e) { + catch (SSLException e) { throw new IgniteException(e); } diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index f9734a47dded7..5e69735810d90 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -279,7 +279,7 @@ public void testMissingKeyMaterialImplementation() { } }; - String msg = "Subclass must override createSslBundle() or createKeyManagers()"; + String msg = "Subclass must override createKeyManagers() to use create()"; Throwable err = GridTestUtils.assertThrows(log, factory::create, IgniteException.class, msg); @@ -289,7 +289,7 @@ public void testMissingKeyMaterialImplementation() { UnsupportedOperationException.class, "Subclass must override createSslBundle()"); } - /** A custom store source does not need the legacy key-manager hook and keeps its own copy of the key password. */ + /** Bundle creation needs no legacy key-manager hook; create() still does. Loaded material copies its key password. */ @Test public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { String alias = "node"; @@ -323,17 +323,20 @@ public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { } }; - assertEquals(Map.of(alias, Arrays.asList(certificateChain(store))), factory.createSslBundle().certificateChains()); + SslBundle bundle = factory.createSslBundle(); - SSLContext cached = factory.create(); + assertEquals(Map.of(alias, Arrays.asList(certificateChain(store))), bundle.certificateChains()); - assertSame(cached, factory.create()); - assertNotSame(cached, factory.createSslBundle().context()); + Throwable err = GridTestUtils.assertThrows(log, factory::create, IgniteException.class, + "Subclass must override createKeyManagers() to use create()"); + + assertTrue(err.getCause() instanceof SSLException); + assertNotSame(bundle.context(), factory.createSslBundle().context()); } - /** Failure in a new factory must not fall back to legacy key managers. */ + /** A bundle override does not change the legacy creation path or its cached context. */ @Test - public void testBundleFailureDoesNotUseLegacyManagers() { + public void testLegacyCreationIsIndependentOfBundleCreation() { String failure = "Certificate source failed"; LegacyFactory factory = new LegacyFactory() { /** {@inheritDoc} */ @@ -342,11 +345,15 @@ public void testBundleFailureDoesNotUseLegacyManagers() { } }; - GridTestUtils.assertThrows(log, factory::create, UnsupportedOperationException.class, failure); + SSLContext cached = factory.create(); - assertEquals(0, factory.parameterChecks); - assertEquals(0, factory.keyManagerCalls); - assertEquals(0, factory.trustManagerCalls); + GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, + UnsupportedOperationException.class, failure); + + assertSame(cached, factory.create()); + assertEquals(1, factory.parameterChecks); + assertEquals(1, factory.keyManagerCalls); + assertEquals(1, factory.trustManagerCalls); } /** Mutating the source map, its lists or the store cannot change an existing bundle. */ From fecbd9e1e6533d56c45f353443af3c1d048c884b Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 18:08:05 +0300 Subject: [PATCH 23/26] IGNITE-29209 Preserve legacy key manager initialization --- .../apache/ignite/ssl/SslContextFactory.java | 18 ++++++++++++++++- .../ignite/ssl/SslContextMetadataTest.java | 20 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java index d4547c22b16ef..856be26d1e4ca 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java @@ -28,6 +28,7 @@ import java.security.cert.X509Certificate; import java.util.Arrays; import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; @@ -268,7 +269,22 @@ public static TrustManager getDisabledTrustManager() { */ @Deprecated @Override protected final KeyManager[] createKeyManagers() throws SSLException { - return loadKeyMaterial().keyManagers(); + try { + KeyManagerFactory keyMgrFactory = KeyManagerFactory.getInstance(keyAlgorithm); + + KeyStore keyStore = loadKeyStore(keyStoreType, keyStoreFilePath, keyStorePwd); + + keyMgrFactory.init(keyStore, keyStorePwd); + + return keyMgrFactory.getKeyManagers(); + } + catch (NoSuchAlgorithmException e) { + throw new SSLException("Unsupported key algorithm: " + keyAlgorithm, e); + } + catch (GeneralSecurityException e) { + throw new SSLException("Failed to initialize Key Manager (security exception occurred) [type=" + + keyStoreType + ", keyStorePath=" + keyStoreFilePath + ']', e); + } } /** {@inheritDoc} */ diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 5e69735810d90..6279d1af514d3 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -250,6 +250,26 @@ public void testLegacyKeyManagerAdapter() throws Exception { assertEquals(1, factory.loadKeyStoreCalls); } + /** Legacy store loaders can still pass null to JSSE to initialize key managers without a store. */ + @Test + public void testLegacyNullKeyStore() { + SslContextFactory factory = new SslContextFactory() { + /** {@inheritDoc} */ + @Override protected KeyStore loadKeyStore(String type, String path, char[] pwd) { + return null; + } + }; + + factory.setKeyStoreFilePath("unused-by-custom-loader"); + factory.setKeyStorePassword(PASSWORD); + factory.setTrustManagers(SslContextFactory.getDisabledTrustManager()); + + SSLContext ctx = factory.create(); + + assertNotNull(ctx.createSSLEngine()); + assertSame(ctx, factory.create()); + } + /** Invalid parameters are reported before loading the store. */ @Test public void testInvalidParametersDoNotLoadStore() throws Exception { From 166027dccbcf6bea8f96595c49a1280f2951520c Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 19:19:50 +0300 Subject: [PATCH 24/26] IGNITE-29209 Build fresh SSL bundles without legacy factory state --- .../ignite/ssl/AbstractSslContextFactory.java | 84 ++----- .../apache/ignite/ssl/SslContextFactory.java | 51 +--- .../ignite/ssl/SslContextMetadataTest.java | 235 ++++-------------- 3 files changed, 77 insertions(+), 293 deletions(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java index 9d62832321573..a7c65a65cccc0 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/AbstractSslContextFactory.java @@ -19,7 +19,6 @@ import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; -import java.util.concurrent.atomic.AtomicReference; import javax.cache.configuration.Factory; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; @@ -31,8 +30,8 @@ import org.jetbrains.annotations.Nullable; /** - * Base SSL context factory that caches the result of the first successful {@link #create()} call. - * Use {@link #createSslBundle()} to load another context and its certificate chains without changing the cached context. + * Builds a fresh SSL context and its certificate metadata on each call. The component using the context is responsible for + * retaining it between connections. */ public abstract class AbstractSslContextFactory implements Factory { /** */ @@ -50,14 +49,6 @@ public abstract class AbstractSslContextFactory implements Factory { /** Enabled protocols. */ protected String[] protocols; - /** - * Cached instance used by the legacy {@link #create()} method. - * - * @deprecated Hold the result of {@link #createSslBundle()} outside the factory. - */ - @Deprecated - protected final AtomicReference sslCtx = new AtomicReference<>(); - /** * Gets protocol for secure transport. * @@ -115,27 +106,23 @@ public void setProtocols(String... protocols) { } /** - * Builds a fresh context and its certificate metadata without calling {@link #create()} or changing its cached context. - * Subclasses must override this method. Those that load a key store can call {@link #createSslBundle(KeyMaterial)} to - * include its certificate chains. + * Checks the factory parameters, loads its key material once, and builds a fresh context with the certificate chains from + * that material. The factory does not retain the result. * * @return Fresh context and the certificate metadata available to this factory. - * @throws UnsupportedOperationException If the subclass does not implement bundle creation. * @throws IgniteException If the context could not be created. */ public SslBundle createSslBundle() { - throw new UnsupportedOperationException("Subclass must override createSslBundle()"); - } + try { + checkParameters(); - /** - * Builds a fresh context from one loaded store. The caller must check its parameters before loading the material. - * - * @param material Loaded key store, key password and manager algorithm. - * @return Context and certificate chains from that material. - * @throws SSLException If key managers, certificate metadata or the context cannot be created. - */ - protected SslBundle createSslBundle(KeyMaterial material) throws SSLException { - return new SslBundle(createSslContext(material.keyManagers()), material.certificateChains()); + KeyMaterial material = loadKeyMaterial(); + + return new SslBundle(createSslContext(material.keyManagers()), material.certificateChains()); + } + catch (SSLException e) { + throw new IgniteException(e); + } } /** @@ -149,20 +136,17 @@ private SSLContext createSslContext(@Nullable KeyManager[] keyMgrs) throws SSLEx try { SSLContext ctx = SSLContext.getInstance(proto); - SSLParameters sslParameters = null; - if (cipherSuites != null || protocols != null) { - sslParameters = new SSLParameters(); + SSLParameters sslParameters = new SSLParameters(); if (cipherSuites != null) sslParameters.setCipherSuites(cipherSuites); if (protocols != null) sslParameters.setProtocols(protocols); - } - if (sslParameters != null) ctx = new SSLContextWrapper(ctx, sslParameters); + } ctx.init(keyMgrs, trustMgrs, null); @@ -194,16 +178,12 @@ protected void checkNullParameter(Object param, String name) throws SSLException protected abstract void checkParameters() throws SSLException; /** - * Creates key managers without certificate metadata. + * Loads the key store and the settings used to read its keys. Called once per context, after the parameters are checked. * - * @return Created key managers. - * @throws SSLException If this method is not overridden or key managers cannot be created. - * @deprecated Override {@link #createSslBundle()} to return a context with its certificate chains. + * @return Loaded key material. + * @throws SSLException If the key material cannot be loaded. */ - @Deprecated - protected KeyManager[] createKeyManagers() throws SSLException { - throw new SSLException("Subclass must override createKeyManagers() to use create()"); - } + protected abstract KeyMaterial loadKeyMaterial() throws SSLException; /** * @return Created Trust Managers. @@ -212,30 +192,12 @@ protected KeyManager[] createKeyManagers() throws SSLException { protected abstract TrustManager[] createTrustManagers() throws SSLException; /** - * Returns the cached context, building it through the legacy key-manager hook. - * Overriding {@link #createSslBundle()} does not change this method's behavior. + * Builds a fresh context through {@link #createSslBundle()}. * - * @return Cached context. - * @deprecated Use {@link #createSslBundle()} and keep the context in the component that uses it. + * @return Fresh initialized context. + * @throws IgniteException If the context could not be created. */ - @Deprecated @Override public SSLContext create() { - SSLContext ctx = sslCtx.get(); - - if (ctx == null) { - try { - checkParameters(); - - ctx = createSslContext(createKeyManagers()); - } - catch (SSLException e) { - throw new IgniteException(e); - } - - if (!sslCtx.compareAndSet(null, ctx)) - ctx = sslCtx.get(); - } - - return ctx; + return createSslBundle().context(); } } diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java index 856be26d1e4ca..58da511294165 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java @@ -27,22 +27,19 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; -import org.apache.ignite.IgniteException; import org.apache.ignite.internal.systemview.SensitiveProperty; import org.apache.ignite.internal.util.typedef.internal.A; /** * SSL context factory that provides SSL context configuration with specified key and trust stores. * - * This factory caches the result of the first successful attempt to create an {@link SSLContext} and always returns it - * as a result of further invocations of the {@link SslContextFactory#create()}} method. + * Each call loads the stores again and builds a fresh context. The component using it should retain the context between + * connections. *

* In some cases it is useful to disable certificate validation of client side (e.g. when connecting * to a server with self-signed certificate). This can be achieved by setting a disabled trust manager @@ -262,50 +259,8 @@ public static TrustManager getDisabledTrustManager() { return new DisabledX509TrustManager(); } - /** - * {@inheritDoc} - * - * @deprecated Use {@link #createSslBundle()} to obtain a context with its certificate chains. - */ - @Deprecated - @Override protected final KeyManager[] createKeyManagers() throws SSLException { - try { - KeyManagerFactory keyMgrFactory = KeyManagerFactory.getInstance(keyAlgorithm); - - KeyStore keyStore = loadKeyStore(keyStoreType, keyStoreFilePath, keyStorePwd); - - keyMgrFactory.init(keyStore, keyStorePwd); - - return keyMgrFactory.getKeyManagers(); - } - catch (NoSuchAlgorithmException e) { - throw new SSLException("Unsupported key algorithm: " + keyAlgorithm, e); - } - catch (GeneralSecurityException e) { - throw new SSLException("Failed to initialize Key Manager (security exception occurred) [type=" + - keyStoreType + ", keyStorePath=" + keyStoreFilePath + ']', e); - } - } - /** {@inheritDoc} */ - @Override public SslBundle createSslBundle() { - try { - checkParameters(); - - return createSslBundle(loadKeyMaterial()); - } - catch (SSLException e) { - throw new IgniteException(e); - } - } - - /** - * Loads the key store once for both key managers and certificate metadata. - * - * @return Loaded store with its key password and manager algorithm. - * @throws SSLException If the store cannot be loaded. - */ - private KeyMaterial loadKeyMaterial() throws SSLException { + @Override protected KeyMaterial loadKeyMaterial() throws SSLException { return new KeyMaterial(loadKeyStore(keyStoreType, keyStoreFilePath, keyStorePwd), keyStorePwd, keyAlgorithm); } diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java index 6279d1af514d3..290268eae507c 100644 --- a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextMetadataTest.java @@ -29,11 +29,11 @@ import java.util.Map; import java.util.concurrent.Callable; import javax.crypto.KeyGenerator; -import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; import org.apache.ignite.IgniteException; import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.TestCertificateAuthority; @@ -57,9 +57,9 @@ public class SslContextMetadataTest extends GridCommonAbstractTest { ca = new TestCertificateAuthority("metadata-ca"); } - /** Each bundle loads its store once; fresh bundles do not replace the context cached by the legacy create method. */ + /** Each create or bundle request loads fresh material once and leaves earlier bundles unchanged. */ @Test - public void testFreshBundlePreservesCachedContext() throws Exception { + public void testFreshContextsLoadMaterialOnce() throws Exception { String originalAlias = "original"; KeyStore originalKeys = ca.keyStoreWithPrivateKey(originalAlias); List originalChain = Arrays.asList(certificateChain(originalKeys)); @@ -70,9 +70,9 @@ public void testFreshBundlePreservesCachedContext() throws Exception { assertEquals(1, factory.loadKeyStoreCalls); assertEquals(1, factory.parameterChecks); - SSLContext cached = factory.create(); + SSLContext created = factory.create(); - assertSame(cached, factory.create()); + assertNotSame(original.context(), created); assertEquals(2, factory.loadKeyStoreCalls); assertEquals(2, factory.parameterChecks); @@ -86,13 +86,17 @@ public void testFreshBundlePreservesCachedContext() throws Exception { SslBundle fresh = factory.createSslBundle(); - assertNotSame(cached, fresh.context()); + assertNotSame(created, fresh.context()); assertNotSame(original.context(), fresh.context()); assertEquals(Map.of(replacementAlias, replacementChain), fresh.certificateChains()); assertEquals(Map.of(originalAlias, originalChain), original.certificateChains()); - assertSame(cached, factory.create()); assertEquals(3, factory.loadKeyStoreCalls); assertEquals(3, factory.parameterChecks); + + assertNotSame(fresh.context(), factory.create()); + assertEquals(4, factory.loadKeyStoreCalls); + assertEquals(4, factory.parameterChecks); + assertEquals(4, factory.bundleCalls); } /** TLS parameters apply to the context while its metadata remains available separately. */ @@ -205,71 +209,6 @@ public void testNonX509ChainRetainsPrivateKeyAlias() throws Exception { assertEquals(Map.of(alias, List.of()), material.certificateChains()); } - /** Legacy creation stays cached; requesting a bundle does not invoke the old hooks. */ - @Test - public void testLegacyHooksRunOncePerBuiltContext() { - LegacyFactory factory = new LegacyFactory(); - SSLContext first = factory.create(); - - assertSame(first, factory.create()); - assertEquals(1, factory.parameterChecks); - assertEquals(1, factory.keyManagerCalls); - assertEquals(1, factory.trustManagerCalls); - - GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, - UnsupportedOperationException.class, "Subclass must override createSslBundle()"); - - assertSame(first, factory.create()); - assertEquals(1, factory.parameterChecks); - assertEquals(1, factory.keyManagerCalls); - assertEquals(1, factory.trustManagerCalls); - } - - /** Existing subclasses can still populate the protected cache before the first create call. */ - @Test - public void testLegacyProtectedCache() throws Exception { - LegacyFactory factory = new LegacyFactory(); - SSLContext prepared = TestCertificateAuthority.context(ca.keyStoreWithPrivateKey(), ca.trustStore()); - - factory.sslCtx.set(prepared); - - assertSame(prepared, factory.create()); - assertEquals(0, factory.parameterChecks); - assertEquals(0, factory.keyManagerCalls); - assertEquals(0, factory.trustManagerCalls); - } - - /** A subclass can still obtain key managers from the file factory's legacy hook. */ - @Test - public void testLegacyKeyManagerAdapter() throws Exception { - InMemoryStoreFactory factory = new InMemoryStoreFactory(ca.keyStoreWithPrivateKey()); - KeyManager[] mgrs = factory.legacyKeyManagers(); - - assertNotNull(mgrs); - assertTrue(mgrs.length > 0); - assertEquals(1, factory.loadKeyStoreCalls); - } - - /** Legacy store loaders can still pass null to JSSE to initialize key managers without a store. */ - @Test - public void testLegacyNullKeyStore() { - SslContextFactory factory = new SslContextFactory() { - /** {@inheritDoc} */ - @Override protected KeyStore loadKeyStore(String type, String path, char[] pwd) { - return null; - } - }; - - factory.setKeyStoreFilePath("unused-by-custom-loader"); - factory.setKeyStorePassword(PASSWORD); - factory.setTrustManagers(SslContextFactory.getDisabledTrustManager()); - - SSLContext ctx = factory.create(); - - assertNotNull(ctx.createSSLEngine()); - assertSame(ctx, factory.create()); - } - /** Invalid parameters are reported before loading the store. */ @Test public void testInvalidParametersDoNotLoadStore() throws Exception { @@ -284,34 +223,34 @@ public void testInvalidParametersDoNotLoadStore() throws Exception { assertEquals(0, factory.loadKeyStoreCalls); } - /** A factory implementing neither a bundle builder nor the legacy key-manager hook reports the missing implementation. */ + /** A failed load is reported instead of reusing an earlier context, and the next request can recover. */ @Test - public void testMissingKeyMaterialImplementation() { - AbstractSslContextFactory factory = new AbstractSslContextFactory() { - /** {@inheritDoc} */ - @Override protected void checkParameters() { - // No parameters are required by this factory. - } - - /** {@inheritDoc} */ - @Override protected TrustManager[] createTrustManagers() { - return null; - } - }; + public void testFailedLoadCanBeRetried() throws Exception { + InMemoryStoreFactory factory = new InMemoryStoreFactory(ca.keyStoreWithPrivateKey()); + SSLContext original = factory.create(); + String failure = "Key material is unavailable"; - String msg = "Subclass must override createKeyManagers() to use create()"; + factory.loadFailure = failure; - Throwable err = GridTestUtils.assertThrows(log, factory::create, IgniteException.class, msg); + Throwable err = GridTestUtils.assertThrows(log, factory::create, IgniteException.class, failure); assertTrue(err.getCause() instanceof SSLException); - GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, - UnsupportedOperationException.class, "Subclass must override createSslBundle()"); + GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, IgniteException.class, failure); + + factory.loadFailure = null; + + SSLContext recovered = factory.create(); + + assertNotSame(original, recovered); + assertNotNull(recovered.createSSLEngine()); + assertEquals(4, factory.loadKeyStoreCalls); + assertEquals(4, factory.parameterChecks); } - /** Bundle creation needs no legacy key-manager hook; create() still does. Loaded material copies its key password. */ + /** Mutating the caller's password does not change how loaded material opens its private keys. */ @Test - public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { + public void testLoadedMaterialCopiesKeyPassword() throws Exception { String alias = "node"; KeyStore store = ca.keyStoreWithPrivateKey(alias); char[] pwd = PASSWORD.clone(); @@ -319,61 +258,10 @@ public void testLoadedMaterialBypassesLegacyKeyManagerHook() throws Exception { Arrays.fill(pwd, '?'); - AbstractSslContextFactory factory = new AbstractSslContextFactory() { - /** {@inheritDoc} */ - @Override public SslBundle createSslBundle() { - try { - checkParameters(); - - return createSslBundle(material); - } - catch (SSLException e) { - throw new IgniteException(e); - } - } - - /** {@inheritDoc} */ - @Override protected void checkParameters() { - // The test supplies already loaded material. - } + X509KeyManager mgr = (X509KeyManager)material.keyManagers()[0]; - /** {@inheritDoc} */ - @Override protected TrustManager[] createTrustManagers() { - return null; - } - }; - - SslBundle bundle = factory.createSslBundle(); - - assertEquals(Map.of(alias, Arrays.asList(certificateChain(store))), bundle.certificateChains()); - - Throwable err = GridTestUtils.assertThrows(log, factory::create, IgniteException.class, - "Subclass must override createKeyManagers() to use create()"); - - assertTrue(err.getCause() instanceof SSLException); - assertNotSame(bundle.context(), factory.createSslBundle().context()); - } - - /** A bundle override does not change the legacy creation path or its cached context. */ - @Test - public void testLegacyCreationIsIndependentOfBundleCreation() { - String failure = "Certificate source failed"; - LegacyFactory factory = new LegacyFactory() { - /** {@inheritDoc} */ - @Override public SslBundle createSslBundle() { - throw new UnsupportedOperationException(failure); - } - }; - - SSLContext cached = factory.create(); - - GridTestUtils.assertThrows(log, (Callable)factory::createSslBundle, - UnsupportedOperationException.class, failure); - - assertSame(cached, factory.create()); - assertEquals(1, factory.parameterChecks); - assertEquals(1, factory.keyManagerCalls); - assertEquals(1, factory.trustManagerCalls); + assertNotNull(mgr.getPrivateKey(alias)); + assertArrayEquals(certificateChain(store), mgr.getCertificateChain(alias)); } /** Mutating the source map, its lists or the store cannot change an existing bundle. */ @@ -404,7 +292,7 @@ public void testImmutableCertificateMetadata() throws Exception { UnsupportedOperationException.class, null); } - /** A file factory whose existing store-loading hook supplies the test material. */ + /** A file factory whose store-loading hook supplies the test material. */ private static class InMemoryStoreFactory extends SslContextFactory { /** */ private KeyStore store; @@ -415,6 +303,12 @@ private static class InMemoryStoreFactory extends SslContextFactory { /** */ private int parameterChecks; + /** */ + private int bundleCalls; + + /** Failure to report while the store is unavailable. */ + private String loadFailure; + /** */ InMemoryStoreFactory(KeyStore store) { this.store = store; @@ -426,54 +320,27 @@ private static class InMemoryStoreFactory extends SslContextFactory { } /** {@inheritDoc} */ - @Override protected void checkParameters() throws SSLException { - parameterChecks++; - - super.checkParameters(); - } - - /** {@inheritDoc} */ - @Override protected KeyStore loadKeyStore(String type, String path, char[] pwd) { - loadKeyStoreCalls++; - - return store; - } + @Override public SslBundle createSslBundle() { + bundleCalls++; - /** @return Key managers supplied by the inherited legacy hook. */ - @SuppressWarnings("deprecation") - private KeyManager[] legacyKeyManagers() throws SSLException { - return super.createKeyManagers(); + return super.createSslBundle(); } - } - - /** A subclass that implements only the legacy manager hooks. */ - private static class LegacyFactory extends AbstractSslContextFactory { - /** */ - private int parameterChecks; - - /** */ - private int keyManagerCalls; - - /** */ - private int trustManagerCalls; /** {@inheritDoc} */ - @Override protected void checkParameters() { + @Override protected void checkParameters() throws SSLException { parameterChecks++; - } - /** {@inheritDoc} */ - @Override protected KeyManager[] createKeyManagers() { - keyManagerCalls++; - - return null; + super.checkParameters(); } /** {@inheritDoc} */ - @Override protected TrustManager[] createTrustManagers() { - trustManagerCalls++; + @Override protected KeyStore loadKeyStore(String type, String path, char[] pwd) throws SSLException { + loadKeyStoreCalls++; + + if (loadFailure != null) + throw new SSLException(loadFailure); - return null; + return store; } } } From f1449e9593a0dd478109bbddd0f884c615a0d8c8 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 19:19:56 +0300 Subject: [PATCH 25/26] IGNITE-29209 Reuse the communication SSL context across connections --- .../tcp/internal/ClusterStateProvider.java | 8 -- .../tcp/internal/GridNioServerWrapper.java | 10 +- .../ignite/ssl/SslContextReuseTest.java | 114 ++++++++++++++++++ .../ignite/testsuites/SecurityTestSuite.java | 2 + 4 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 modules/core/src/test/java/org/apache/ignite/ssl/SslContextReuseTest.java diff --git a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/ClusterStateProvider.java b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/ClusterStateProvider.java index d4383c096cefb..017ae837389d8 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/ClusterStateProvider.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/ClusterStateProvider.java @@ -19,7 +19,6 @@ import java.util.UUID; import java.util.function.Supplier; -import javax.net.ssl.SSLEngine; import org.apache.ignite.Ignite; import org.apache.ignite.IgniteLogger; import org.apache.ignite.cluster.ClusterNode; @@ -108,13 +107,6 @@ public boolean isSslEnabled() { return ignite.configuration().getSslContextFactory() != null; } - /** - * @return {@link SSLEngine} for ssl connections. - */ - public SSLEngine createSSLEngine() { - return ignite.configuration().getSslContextFactory().create().createSSLEngine(); - } - /** * @return {@code true} if {@link TcpCommunicationSpi} stopped. */ diff --git a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java index 9368f9f8a83db..7ec7ea0b0a0d8 100644 --- a/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java +++ b/modules/core/src/main/java/org/apache/ignite/spi/communication/tcp/internal/GridNioServerWrapper.java @@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.function.Supplier; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import org.apache.ignite.IgniteCheckedException; @@ -217,6 +218,9 @@ public class GridNioServerWrapper { private final int msgQueueWarningSize = IgniteSystemProperties.getInteger( IgniteSystemProperties.IGNITE_TCP_COMM_MSG_QUEUE_WARN_SIZE, 0); + /** Context shared by incoming and outgoing connections until the SPI stops. */ + private SSLContext sslCtx; + /** NIO server. */ private GridNioServer nioSrv; @@ -476,7 +480,7 @@ public GridNioSession createNioSession(ClusterNode node, int connIdx) throws Ign if (stateProvider.isSslEnabled()) { meta.put(SSL_META.ordinal(), sslMeta = new GridSslMeta()); - SSLEngine sslEngine = stateProvider.createSSLEngine(); + SSLEngine sslEngine = sslCtx.createSSLEngine(); sslEngine.setUseClientMode(true); @@ -803,6 +807,8 @@ public GridNioServer resetNioServer() throws IgniteCheckedException { if (cfg.boundTcpPort() >= 0) throw new IgniteCheckedException("Tcp NIO server was already created on port " + cfg.boundTcpPort()); + sslCtx = stateProvider.isSslEnabled() ? igniteCfg.getSslContextFactory().create() : null; + IgniteCheckedException lastEx = null; // If configured TCP port is busy, find first available in range. @@ -919,7 +925,7 @@ private IgniteMessageFactory get() { if (stateProvider.isSslEnabled()) { GridNioSslFilter sslFilter = U.sslFilter( - igniteCfg.getSslContextFactory().create(), + sslCtx, true, ByteOrder.LITTLE_ENDIAN, log, diff --git a/modules/core/src/test/java/org/apache/ignite/ssl/SslContextReuseTest.java b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextReuseTest.java new file mode 100644 index 0000000000000..93fb82c0110cb --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/ssl/SslContextReuseTest.java @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.ssl; + +import java.security.KeyStore; +import java.util.concurrent.atomic.AtomicInteger; +import javax.cache.configuration.Factory; +import javax.net.ssl.SSLContext; +import org.apache.ignite.IgniteException; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.internal.IgniteEx; +import org.apache.ignite.internal.util.typedef.internal.U; +import org.apache.ignite.spi.communication.tcp.internal.ConnectionClientPool; +import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi; +import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder; +import org.apache.ignite.testframework.TestCertificateAuthority; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.junit.Test; + +/** Tests that communication connections reuse the context loaded at startup. */ +public class SslContextReuseTest extends GridCommonAbstractTest { + /** Discovery addresses shared only by the nodes in this test. */ + private final TcpDiscoveryVmIpFinder ipFinder = new TcpDiscoveryVmIpFinder(true); + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); + + ((TcpDiscoverySpi)cfg.getDiscoverySpi()).setIpFinder(ipFinder); + + return cfg; + } + + /** {@inheritDoc} */ + @Override protected void afterTest() throws Exception { + stopAllGrids(); + + super.afterTest(); + } + + /** Opening and reopening a communication connection must not invoke the SSL factory again. */ + @Test + public void testCommunicationReusesContextOnReconnect() throws Exception { + TestCertificateAuthority ca = new TestCertificateAuthority("communication-ca"); + CountingFactory firstFactory = new CountingFactory(ca); + CountingFactory secondFactory = new CountingFactory(ca); + IgniteEx first = startGrid(0, cfg -> { + cfg.setSslContextFactory(firstFactory); + }); + IgniteEx second = startGrid(1, cfg -> { + cfg.setSslContextFactory(secondFactory); + }); + int firstStartupCalls = firstFactory.calls.get(); + int secondStartupCalls = secondFactory.calls.get(); + + assertEquals((Integer)1, first.compute(first.cluster().forNodeId(second.localNode().id())).call(() -> 1)); + + ConnectionClientPool pool = U.field(first.configuration().getCommunicationSpi(), "clientPool"); + + pool.forceCloseConnection(second.localNode().id()); + + assertEquals((Integer)1, first.compute(first.cluster().forNodeId(second.localNode().id())).call(() -> 1)); + assertEquals(firstStartupCalls, firstFactory.calls.get()); + assertEquals(secondStartupCalls, secondFactory.calls.get()); + } + + /** A factory that builds a fresh context on every call from mutually trusted test material. */ + private static class CountingFactory implements Factory { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + private final KeyStore keys; + + /** */ + private final KeyStore trust; + + /** */ + private final AtomicInteger calls = new AtomicInteger(); + + /** @param ca Authority shared by both nodes. */ + CountingFactory(TestCertificateAuthority ca) throws Exception { + keys = ca.keyStoreWithPrivateKey(); + trust = ca.trustStore(); + } + + /** {@inheritDoc} */ + @Override public SSLContext create() { + calls.incrementAndGet(); + + try { + return TestCertificateAuthority.context(keys, trust); + } + catch (Exception e) { + throw new IgniteException(e); + } + } + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java index 13e9204fd95f8..eb9e1015b0d44 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java @@ -81,6 +81,7 @@ import org.apache.ignite.ssl.MultipleSSLContextsTest; import org.apache.ignite.ssl.SSLContextWrapperTest; import org.apache.ignite.ssl.SslContextMetadataTest; +import org.apache.ignite.ssl.SslContextReuseTest; import org.apache.ignite.tools.junit.JUnitTeamcityReporter; import org.junit.BeforeClass; import org.junit.runner.RunWith; @@ -145,6 +146,7 @@ MultipleSSLContextsTest.class, SSLContextWrapperTest.class, SslContextMetadataTest.class, + SslContextReuseTest.class, MaintenanceModeNodeSecurityTest.class, ServiceAuthorizationTest.class, ServiceStaticConfigTest.class, From e11f5b28475a81160707e0b7c234743b888f16f3 Mon Sep 17 00:00:00 2001 From: Anton Vinogradov Date: Tue, 27 Oct 2026 19:20:35 +0300 Subject: [PATCH 26/26] IGNITE-29209 Remove unused SSL context import --- .../src/main/java/org/apache/ignite/ssl/SslContextFactory.java | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java index 58da511294165..5a412b1272546 100644 --- a/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java +++ b/modules/commons/src/main/java/org/apache/ignite/ssl/SslContextFactory.java @@ -27,7 +27,6 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory;