This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 9c48f83e7e IGNITE-18791 Add SSL Client authentication to REST (#1690)
9c48f83e7e is described below

commit 9c48f83e7e22e097a7db10937db57a2a87202d59
Author: Aleksandr <apk...@gmail.com>
AuthorDate: Tue Feb 21 11:47:47 2023 +0400

    IGNITE-18791 Add SSL Client authentication to REST (#1690)
    
    TCP Client and JDBC support this feature, now REST does it too.
---
 modules/rest/build.gradle                          |   6 +
 .../apache/ignite/internal/rest/RestComponent.java |  96 +++++++++++++--
 .../configuration/RestSslConfigurationSchema.java  |  15 +--
 .../ignite/internal/rest/RestComponentTest.java    | 134 +++++++++++++++++++++
 .../ignite/internal/rest/TestController.java       |  33 +++++
 .../ignite/internal/rest/ItPortRangeTest.java      |   1 +
 .../ignite/internal/rest/ssl/ItRestSslTest.java    |  87 +++++++++++--
 .../apache/ignite/internal/rest/ssl/RestNode.java  |  19 ++-
 8 files changed, 356 insertions(+), 35 deletions(-)

diff --git a/modules/rest/build.gradle b/modules/rest/build.gradle
index 3e1dea30d4..c9b2951bd4 100644
--- a/modules/rest/build.gradle
+++ b/modules/rest/build.gradle
@@ -40,8 +40,14 @@ dependencies {
 
     annotationProcessor libs.auto.service
 
+    testAnnotationProcessor libs.micronaut.inject.annotation.processor
+
     testImplementation project(':ignite-configuration')
+    testImplementation(testFixtures(project(':ignite-core')))
+    testImplementation(testFixtures(project(':ignite-configuration')))
+    testImplementation libs.hamcrest.core
     testImplementation libs.slf4j.jdk14
+
 }
 
 compileJava {
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
index d5fc16c084..755a253f1d 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
@@ -21,9 +21,11 @@ import static 
io.micronaut.context.env.Environment.BARE_METAL;
 
 import io.micronaut.context.ApplicationContext;
 import io.micronaut.http.server.exceptions.ServerStartupException;
+import io.micronaut.http.ssl.ClientAuthentication;
 import io.micronaut.openapi.annotation.OpenAPIInclude;
 import io.micronaut.runtime.Micronaut;
 import io.micronaut.runtime.exceptions.ApplicationStartupException;
+import io.netty.handler.ssl.ClientAuth;
 import io.swagger.v3.oas.annotations.OpenAPIDefinition;
 import io.swagger.v3.oas.annotations.info.Contact;
 import io.swagger.v3.oas.annotations.info.Info;
@@ -31,6 +33,9 @@ import io.swagger.v3.oas.annotations.info.License;
 import java.net.BindException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.apache.ignite.internal.logger.IgniteLogger;
@@ -43,8 +48,11 @@ import 
org.apache.ignite.internal.rest.api.configuration.NodeConfigurationApi;
 import org.apache.ignite.internal.rest.api.metric.NodeMetricApi;
 import org.apache.ignite.internal.rest.api.node.NodeManagementApi;
 import org.apache.ignite.internal.rest.configuration.RestConfiguration;
+import org.apache.ignite.internal.rest.configuration.RestSslConfiguration;
 import org.apache.ignite.internal.rest.configuration.RestSslView;
 import org.apache.ignite.internal.rest.configuration.RestView;
+import org.apache.ignite.lang.ErrorGroups.Common;
+import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.lang.IgniteInternalException;
 import org.jetbrains.annotations.Nullable;
 
@@ -147,7 +155,7 @@ public class RestComponent implements IgniteComponent {
                 + " [HTTP ports=[" + desiredHttpPort + ", " + desiredHttpPort 
+ portRange + "]],"
                 + " [HTTPS ports=[" + desiredHttpsPort + ", " + 
desiredHttpsPort + httpsPortRange + "]]";
 
-        throw new RuntimeException(msg);
+        throw new IgniteException(Common.UNEXPECTED_ERR, msg);
     }
 
     /** Starts Micronaut application using the provided ports.
@@ -171,7 +179,7 @@ public class RestComponent implements IgniteComponent {
             if (bindException != null) {
                 return false;
             }
-            throw new RuntimeException(e);
+            throw new IgniteException(Common.UNEXPECTED_ERR, e);
         }
     }
 
@@ -212,14 +220,20 @@ public class RestComponent implements IgniteComponent {
     }
 
     private Map<String, Object> properties(int port, int sslPort) {
-        boolean dualProtocol = restConfiguration.dualProtocol().value();
-        boolean sslEnabled = restConfiguration.ssl().enabled().value();
-        String keyStoreType = 
restConfiguration.ssl().keyStore().type().value();
-        String keyStorePath = 
restConfiguration.ssl().keyStore().path().value();
-        String keyStorePassword = 
restConfiguration.ssl().keyStore().password().value();
+        RestSslConfiguration sslCfg = restConfiguration.ssl();
+        boolean sslEnabled = sslCfg.enabled().value();
 
         if (sslEnabled) {
-            return Map.of(
+            String keyStorePath = sslCfg.keyStore().path().value();
+            // todo: replace with configuration-level validation 
https://issues.apache.org/jira/browse/IGNITE-18850
+            validateKeyStorePath(keyStorePath);
+
+            String keyStoreType = sslCfg.keyStore().type().value();
+            String keyStorePassword = sslCfg.keyStore().password().value();
+
+            boolean dualProtocol = restConfiguration.dualProtocol().value();
+
+            Map<String, Object> micronautSslConfig = Map.of(
                     "micronaut.server.port", port, // Micronaut is not going 
to handle requests on that port, but it's required
                     "micronaut.server.dual-protocol", dualProtocol,
                     "micronaut.server.ssl.port", sslPort,
@@ -228,11 +242,77 @@ public class RestComponent implements IgniteComponent {
                     "micronaut.server.ssl.key-store.password", 
keyStorePassword,
                     "micronaut.server.ssl.key-store.type", keyStoreType
             );
+
+            ClientAuth clientAuth = 
ClientAuth.valueOf(sslCfg.clientAuth().value().toUpperCase());
+            if (ClientAuth.NONE == clientAuth) {
+                return micronautSslConfig;
+            }
+
+
+            String trustStorePath = sslCfg.trustStore().path().value();
+            // todo: replace with configuration-level validation 
https://issues.apache.org/jira/browse/IGNITE-18850
+            validateTrustStore(trustStorePath);
+
+            String trustStoreType = sslCfg.trustStore().type().value();
+            String trustStorePassword = sslCfg.trustStore().password().value();
+
+            Map<String, Object> micronautClientAuthConfig = Map.of(
+                    "micronaut.server.ssl.client-authentication", 
toMicronautClientAuth(clientAuth),
+                    "micronaut.server.ssl.trust-store.path", "file:" + 
trustStorePath,
+                    "micronaut.server.ssl.trust-store.password", 
trustStorePassword,
+                    "micronaut.server.ssl.trust-store.type", trustStoreType
+            );
+
+            HashMap<String, Object> result = new HashMap<>();
+            result.putAll(micronautSslConfig);
+            result.putAll(micronautClientAuthConfig);
+
+            return result;
         } else {
             return Map.of("micronaut.server.port", port);
         }
     }
 
+    private static void validateKeyStorePath(String keyStorePath) {
+        if (keyStorePath.trim().isEmpty()) {
+            throw new IgniteException(
+                    Common.SSL_CONFIGURATION_ERR,
+                    "Trust store path is not configured. Please check your 
rest.ssl.keyStore.path configuration."
+            );
+        }
+
+        if (!Files.exists(Path.of(keyStorePath))) {
+            throw new IgniteException(
+                    Common.SSL_CONFIGURATION_ERR,
+                    "Trust store file not found: " + keyStorePath + ". Please 
check your rest.ssl.keyStore.path configuration."
+            );
+        }
+    }
+
+    private static void validateTrustStore(String trustStorePath) {
+        if (trustStorePath.trim().isEmpty()) {
+            throw new IgniteException(
+                    Common.SSL_CONFIGURATION_ERR,
+                    "Key store path is not configured. Please check your 
rest.ssl.trustStore.path configuration."
+            );
+        }
+
+        if (!Files.exists(Path.of(trustStorePath))) {
+            throw new IgniteException(
+                    Common.SSL_CONFIGURATION_ERR,
+                    "Key store file not found: " + trustStorePath + ". Please 
check your rest.ssl.trustStore.path configuration."
+            );
+        }
+    }
+
+    private String toMicronautClientAuth(ClientAuth clientAuth) {
+        switch (clientAuth) {
+            case OPTIONAL: return 
ClientAuthentication.WANT.name().toLowerCase();
+            case REQUIRE:  return 
ClientAuthentication.NEED.name().toLowerCase();
+            default: throw new IllegalArgumentException("Can not convert " + 
clientAuth.name() + " to micronaut type");
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     public synchronized void stop() throws Exception {
diff --git 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
index 00da64cad5..dbd2908d41 100644
--- 
a/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
+++ 
b/modules/rest/src/main/java/org/apache/ignite/internal/rest/configuration/RestSslConfigurationSchema.java
@@ -18,19 +18,13 @@
 package org.apache.ignite.internal.rest.configuration;
 
 import org.apache.ignite.configuration.annotation.Config;
-import org.apache.ignite.configuration.annotation.ConfigValue;
 import org.apache.ignite.configuration.annotation.Value;
 import org.apache.ignite.configuration.validation.Range;
-import 
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationSchema;
-import 
org.apache.ignite.internal.network.configuration.KeyStoreConfigurationValidator;
+import 
org.apache.ignite.internal.network.configuration.AbstractSslConfigurationSchema;
 
 /** REST SSL configuration. */
 @Config
-public class RestSslConfigurationSchema {
-
-    /** Whether SSL is enabled. */
-    @Value(hasDefault = true)
-    public final boolean enabled = false;
+public class RestSslConfigurationSchema extends AbstractSslConfigurationSchema 
{
 
     /** SSL port. */
     @Range(min = 1024, max = 0xFFFF)
@@ -41,9 +35,4 @@ public class RestSslConfigurationSchema {
     @Range(min = 0)
     @Value(hasDefault = true)
     public final int portRange = 100;
-
-    /** SSL keystore. */
-    @KeyStoreConfigurationValidator
-    @ConfigValue
-    public KeyStoreConfigurationSchema keyStore;
 }
diff --git 
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
new file mode 100644
index 0000000000..a1eaa2d6fd
--- /dev/null
+++ 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/RestComponentTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.internal.rest;
+
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import io.netty.handler.ssl.util.SelfSignedCertificate;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.List;
+import 
org.apache.ignite.internal.configuration.testframework.ConfigurationExtension;
+import 
org.apache.ignite.internal.configuration.testframework.InjectConfiguration;
+import org.apache.ignite.internal.rest.configuration.RestConfiguration;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.apache.ignite.lang.ErrorGroups.Common;
+import org.apache.ignite.lang.IgniteException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(ConfigurationExtension.class)
+@ExtendWith(WorkDirectoryExtension.class)
+class RestComponentTest {
+
+    private String keyStorePath;
+
+    @WorkDirectory
+    private Path workDir;
+
+    private static void pingOk(RestComponent component) throws IOException, 
InterruptedException {
+        HttpClient client = HttpClient.newBuilder().build();
+        HttpResponse<String> response = client.send(
+                HttpRequest.newBuilder()
+                        .uri(URI.create("http://"; + component.host() + ":" + 
component.httpPort() + "/ping")).GET()
+                        .build(),
+                BodyHandlers.ofString()
+        );
+
+        assertThat(response.statusCode(), is(200));
+        assertThat(response.body(), is("pong"));
+    }
+
+    @BeforeEach
+    void setUp() {
+        keyStorePath = 
workDir.resolve("keystore.p12").toAbsolutePath().toString();
+    }
+
+    @Test
+    @DisplayName("REST component stars with default configuration")
+    void defaultConfiguration(@InjectConfiguration RestConfiguration 
restConfiguration) throws Exception {
+        // Given
+        RestComponent component = new RestComponent(List.of(), 
restConfiguration);
+
+        // When
+        component.start();
+
+        // Then
+        pingOk(component);
+    }
+
+    @Test
+    @DisplayName("REST component does not start with ssl.enabled=true and no 
keystore")
+    void sslConfiguration(@InjectConfiguration("mock.ssl.enabled: true") 
RestConfiguration restConfiguration) {
+        // Given
+        RestComponent component = new RestComponent(List.of(), 
restConfiguration);
+
+        // When
+        IgniteException thrown = assertThrows(IgniteException.class, 
component::start);
+
+        // Then
+        assertThat(thrown.code(), is(Common.SSL_CONFIGURATION_ERR));
+    }
+
+    @Test
+    @DisplayName("REST component does not start with ssl.clientAuth=require 
and no truststore")
+    void clientAuthConfiguration(@InjectConfiguration RestConfiguration 
restConfiguration) throws Exception {
+        // Given correct keystore
+        generateKeystore(new SelfSignedCertificate());
+        restConfiguration.ssl().enabled().update(true).get();
+        restConfiguration.ssl().keyStore().path().update(keyStorePath).get();
+        restConfiguration.ssl().keyStore().password().update("changeit").get();
+        // And clientAuth=require But no truststore
+        restConfiguration.ssl().clientAuth().update("require").get();
+
+        RestComponent component = new RestComponent(List.of(), 
restConfiguration);
+
+        // When
+        IgniteException thrown = assertThrows(IgniteException.class, 
component::start);
+
+        // Then
+        assertThat(thrown.code(), is(Common.SSL_CONFIGURATION_ERR));
+    }
+
+    private void generateKeystore(SelfSignedCertificate cert)
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, 
CertificateException {
+        KeyStore ks = KeyStore.getInstance("PKCS12");
+        ks.load(null, null);
+        ks.setKeyEntry("key", cert.key(), null, new 
Certificate[]{cert.cert()});
+        try (FileOutputStream fos = new FileOutputStream(keyStorePath)) {
+            ks.store(fos, "changeit".toCharArray());
+        }
+    }
+}
diff --git 
a/modules/rest/src/test/java/org/apache/ignite/internal/rest/TestController.java
 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/TestController.java
new file mode 100644
index 0000000000..214c22d401
--- /dev/null
+++ 
b/modules/rest/src/test/java/org/apache/ignite/internal/rest/TestController.java
@@ -0,0 +1,33 @@
+/*
+ * 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.internal.rest;
+
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+
+/**
+ * Test controller. Exposes a single ping endpoint.
+ */
+@Controller
+public class TestController {
+    @Get("ping")
+    public String ping() {
+        return "pong";
+    }
+
+}
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
index fb5f7078ad..ed5e48165f 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItPortRangeTest.java
@@ -99,6 +99,7 @@ public class ItPortRangeTest {
                         10300,
                         10400,
                         true,
+                        false,
                         true
                 ))
                 .collect(Collectors.toList());
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
index e2e608cf31..82e213b829 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/ItRestSslTest.java
@@ -34,8 +34,10 @@ import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
 import java.security.cert.CertificateException;
 import java.util.stream.Stream;
+import javax.net.ssl.KeyManagerFactory;
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManagerFactory;
 import org.apache.ignite.internal.testframework.WorkDirectory;
@@ -62,6 +64,12 @@ public class ItRestSslTest {
     /** Trust store password. */
     private static final String trustStorePassword = "changeit";
 
+    /** Key store path. */
+    private static final String keyStorePath = "ssl/keystore.p12";
+
+    /** Key store password. */
+    private static final String keyStorePassword = "changeit";
+
     /** Path to the working directory. */
     @WorkDirectory
     private static Path workDir;
@@ -72,12 +80,17 @@ public class ItRestSslTest {
     /** SSL HTTP client that is expected to be defined in subclasses. */
     private static HttpClient sslClient;
 
+    /** SSL HTTP client that is expected to be defined in subclasses. */
+    private static HttpClient sslClientWithClientAuth;
+
     private static RestNode httpNode;
 
     private static RestNode httpsNode;
 
     private static RestNode dualProtocolNode;
 
+    private static RestNode httpsWithClientAuthNode;
+
     @BeforeAll
     static void beforeAll(TestInfo testInfo) throws Exception {
 
@@ -88,11 +101,16 @@ public class ItRestSslTest {
                 .sslContext(sslContext())
                 .build();
 
-        httpNode = new RestNode(workDir, testNodeName(testInfo, 3344), 3344, 
10300, 10400, false, false);
-        httpsNode = new RestNode(workDir, testNodeName(testInfo, 3345), 3345, 
10301, 10401, true, false);
-        dualProtocolNode = new RestNode(workDir, testNodeName(testInfo, 3346), 
3346, 10302, 10402, true, true);
-        Stream.of(httpNode, httpsNode, dualProtocolNode)
-                .forEach(RestNode::start);
+        sslClientWithClientAuth = HttpClient.newBuilder()
+                .sslContext(sslContextWithClientAuth())
+                .build();
+
+        httpNode = new RestNode(workDir, testNodeName(testInfo, 3344), 3344, 
10300, 10400, false, false, false);
+        httpsNode = new RestNode(workDir, testNodeName(testInfo, 3345), 3345, 
10301, 10401, true, false, false);
+        dualProtocolNode = new RestNode(workDir, testNodeName(testInfo, 3346), 
3346, 10302, 10402, true, false, true);
+        httpsWithClientAuthNode = new RestNode(workDir, testNodeName(testInfo, 
3347), 3347, 10303, 10403, true, true, false);
+
+        Stream.of(httpNode, httpsNode, dualProtocolNode, 
httpsWithClientAuthNode).forEach(RestNode::start);
     }
 
     @Test
@@ -143,7 +161,6 @@ public class ItRestSslTest {
 
         // Then IOException
         assertThrows(IOException.class, () -> client.send(request, 
BodyHandlers.ofString()));
-
     }
 
     @Test
@@ -156,20 +173,64 @@ public class ItRestSslTest {
         assertThrows(IOException.class, () -> client.send(request, 
BodyHandlers.ofString()));
     }
 
+    @Test
+    void httpsWithClientAuthProtocol(TestInfo testInfo) throws IOException, 
InterruptedException {
+        // When GET /management/v1/configuration/node
+        URI uri = URI.create(httpsWithClientAuthNode.httpsAddress() + 
"/management/v1/configuration/node");
+        HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+        // Then response code is 200
+        HttpResponse<String> response = sslClientWithClientAuth.send(request, 
BodyHandlers.ofString());
+        assertEquals(200, response.statusCode());
+    }
+
+    @Test
+    void httpsWithClientAuthProtocolButClientWithoutAuth(TestInfo testInfo) 
throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/node
+        URI uri = URI.create(httpsWithClientAuthNode.httpsAddress() + 
"/management/v1/configuration/node");
+        HttpRequest request = HttpRequest.newBuilder(uri).build();
+
+        // Expect IOException for SSL client that does not configure client 
auth
+        assertThrows(IOException.class, () -> sslClient.send(request, 
BodyHandlers.ofString()));
+    }
+
     @AfterAll
     static void afterAll() {
-        Stream.of(httpNode, httpsNode, dualProtocolNode)
-                .forEach(RestNode::stop);
+        Stream.of(httpNode, httpsNode, dualProtocolNode, 
httpsWithClientAuthNode).forEach(RestNode::stop);
     }
 
-    private static SSLContext sslContext()
-            throws CertificateException, KeyStoreException, IOException, 
NoSuchAlgorithmException, KeyManagementException {
-        String path = 
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+    private static SSLContext sslContext() throws CertificateException, 
KeyStoreException, IOException,
+            NoSuchAlgorithmException, KeyManagementException {
+
+        String tsPath = 
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+
+        KeyStore trustStore = KeyStore.getInstance(new File(tsPath), 
trustStorePassword.toCharArray());
         TrustManagerFactory trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-        KeyStore keyStore = KeyStore.getInstance(new File(path), 
trustStorePassword.toCharArray());
-        trustManagerFactory.init(keyStore);
+        trustManagerFactory.init(trustStore);
+
         SSLContext sslContext = SSLContext.getInstance("TLS");
         sslContext.init(null, trustManagerFactory.getTrustManagers(), new 
SecureRandom());
+
+        return sslContext;
+    }
+
+    private static SSLContext sslContextWithClientAuth() throws 
CertificateException, KeyStoreException, IOException,
+            NoSuchAlgorithmException, KeyManagementException, 
UnrecoverableKeyException {
+
+        String tsPath = 
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+        String ksPath = 
ItRestSslTest.class.getClassLoader().getResource(keyStorePath).getPath();
+
+        KeyStore trustStore = KeyStore.getInstance(new File(tsPath), 
trustStorePassword.toCharArray());
+        TrustManagerFactory trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        trustManagerFactory.init(trustStore);
+
+        KeyStore keyStore = KeyStore.getInstance(new File(ksPath), 
keyStorePassword.toCharArray());
+        KeyManagerFactory keyManagerFactory = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
+
+        SSLContext sslContext = SSLContext.getInstance("TLS");
+        sslContext.init(keyManagerFactory.getKeyManagers(), 
trustManagerFactory.getTrustManagers(), new SecureRandom());
+
         return sslContext;
     }
 }
diff --git 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
index ca19ddb61f..ca3f9cb06f 100644
--- 
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
+++ 
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ssl/RestNode.java
@@ -29,12 +29,19 @@ public class RestNode {
     /** Key store password. */
     private static final String keyStorePassword = "changeit";
 
+    /** Trust store path. */
+    private static final String trustStorePath = "ssl/truststore.jks";
+
+    /** Trust store password. */
+    private static final String trustStorePassword = "changeit";
+
     private final Path workDir;
     private final String name;
     private final int networkPort;
     private final int httpPort;
     private final int httpsPort;
     private final boolean sslEnabled;
+    private final boolean sslClientAuthEnabled;
     private final boolean dualProtocol;
 
     /** Constructor. */
@@ -45,6 +52,7 @@ public class RestNode {
             int httpPort,
             int httpsPort,
             boolean sslEnabled,
+            boolean sslClientAuthEnabled,
             boolean dualProtocol
     ) {
         this.workDir = workDir;
@@ -53,6 +61,7 @@ public class RestNode {
         this.httpPort = httpPort;
         this.httpsPort = httpsPort;
         this.sslEnabled = sslEnabled;
+        this.sslClientAuthEnabled = sslClientAuthEnabled;
         this.dualProtocol = dualProtocol;
     }
 
@@ -75,6 +84,8 @@ public class RestNode {
 
     private String bootstrapCfg() {
         String keyStoreAbsolutPath = 
ItRestSslTest.class.getClassLoader().getResource(keyStorePath).getPath();
+        String trustStoreAbsolutPath = 
ItRestSslTest.class.getClassLoader().getResource(trustStorePath).getPath();
+
         return "{\n"
                 + "  network: {\n"
                 + "    port: " + networkPort + ",\n"
@@ -87,10 +98,16 @@ public class RestNode {
                 + "    dualProtocol: " + dualProtocol + ",\n"
                 + "    ssl: {\n"
                 + "      enabled: " + sslEnabled + ",\n"
+                + "      clientAuth: " + (sslClientAuthEnabled ? "require" : 
"none") + ",\n"
                 + "      port: " + httpsPort + ",\n"
                 + "      keyStore: {\n"
                 + "        path: " + keyStoreAbsolutPath + ",\n"
-                    + "    password: " + keyStorePassword + "\n"
+                + "        password: " + keyStorePassword + "\n"
+                + "      }, \n"
+                + "      trustStore: {\n"
+                + "        type: JKS, "
+                + "        path: " + trustStoreAbsolutPath + ",\n"
+                + "        password: " + trustStorePassword + "\n"
                 + "      }\n"
                 + "    }\n"
                 + "  }"

Reply via email to