From e1c588a09aa6a9dadbade849568935dd0a653231 Mon Sep 17 00:00:00 2001
From: Jan Zickermann <jan.zickermann@dataport.de>
Date: Tue, 3 Sep 2024 17:10:31 +0200
Subject: [PATCH] OZG-6239 KOP-2608 Add TrustStore option

---
 .../xta/client/config/XtaClientConfig.java    |   4 ++
 .../core/XtaTLSClientParametersFactory.java   |   4 ++
 .../ozgcloud/xta/client/XtaClientITCase.java  |   3 --
 .../xta/client/XtaTestServerContainer.java    |  12 +----
 .../client/XtaTestServerSetupExtension.java   |  34 +++++++------
 .../XtaTLSClientParametersFactoryTest.java    |  48 ++++++++++++++++++
 .../store/john-smith-client-cert-keystore.p12 | Bin 0 -> 2851 bytes
 .../store/xta-test-server-truststore.jks      | Bin 0 -> 1136 bytes
 8 files changed, 76 insertions(+), 29 deletions(-)
 create mode 100644 src/test/resources/store/john-smith-client-cert-keystore.p12
 create mode 100644 src/test/resources/store/xta-test-server-truststore.jks

diff --git a/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java b/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
index 4f7241e..1693d82 100644
--- a/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
+++ b/src/main/java/de/ozgcloud/xta/client/config/XtaClientConfig.java
@@ -49,6 +49,10 @@ public class XtaClientConfig {
 	@Builder.Default
 	private final KeyStore clientCertKeystore = null;
 
+	@Valid
+	@Builder.Default
+	private final KeyStore trustStore = null;
+
 	@Builder.Default
 	private final boolean schemaValidation = true;
 	@Builder.Default
diff --git a/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java b/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java
index 911d35e..fee35b4 100644
--- a/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java
+++ b/src/main/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactory.java
@@ -45,6 +45,10 @@ public class XtaTLSClientParametersFactory {
 			if (clientCertKeystore != null) {
 				sslContextBuilder.loadKeyMaterial(loadStore(clientCertKeystore), clientCertKeystore.getPassword());
 			}
+			var trustStore = config.getTrustStore();
+			if (trustStore != null) {
+				sslContextBuilder.loadTrustMaterial(loadStore(trustStore), null);
+			}
 			return sslContextBuilder.build();
 		} catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | UnrecoverableKeyException | IOException |
 				CertificateException e) {
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java b/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
index 0ebe3dd..2169e0b 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaClientITCase.java
@@ -3,9 +3,7 @@ package de.ozgcloud.xta.client;
 import static de.ozgcloud.xta.client.XtaDevServerSetupExtension.*;
 import static org.assertj.core.api.Assertions.*;
 
-import org.junit.Ignore;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -13,7 +11,6 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 
 import lombok.SneakyThrows;
 
-@Ignore
 class XtaClientITCase {
 
 	@RegisterExtension
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java b/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java
index 6629771..da3b366 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaTestServerContainer.java
@@ -2,19 +2,15 @@ package de.ozgcloud.xta.client;
 
 import jakarta.validation.constraints.NotNull;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.testcontainers.containers.GenericContainer;
 import org.testcontainers.utility.DockerImageName;
 
 public class XtaTestServerContainer extends GenericContainer<XtaTestServerContainer> {
 
-	private static final Logger log = LoggerFactory.getLogger(XtaTestServerContainer.class);
 	private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("docker.ozg-sh.de/xta-test-server");
 	private static final String DEFAULT_TAG = "latest";
 	public static final int PORT = 8443;
 
-
 	public XtaTestServerContainer() {
 		this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
 	}
@@ -25,7 +21,7 @@ public class XtaTestServerContainer extends GenericContainer<XtaTestServerContai
 	}
 
 	public String getBaseUrl() {
-		return "https://localhost:%d/services/XTAService/".formatted(getMappedPort(PORT));
+		return "https://%s:%d/services/XTAService/".formatted(getHost(), getMappedPort(PORT));
 	}
 
 	public String getMsgBoxPortUrl() {
@@ -40,10 +36,4 @@ public class XtaTestServerContainer extends GenericContainer<XtaTestServerContai
 		return getBaseUrl() + "SendXtaPort";
 	}
 
-	public static void main(String[] args) {
-		try (XtaTestServerContainer container = new XtaTestServerContainer()) {
-			container.start();
-			System.out.println(container.getBaseUrl());
-		}
-	}
 }
diff --git a/src/test/java/de/ozgcloud/xta/client/XtaTestServerSetupExtension.java b/src/test/java/de/ozgcloud/xta/client/XtaTestServerSetupExtension.java
index 2ce8704..d7f3dd2 100644
--- a/src/test/java/de/ozgcloud/xta/client/XtaTestServerSetupExtension.java
+++ b/src/test/java/de/ozgcloud/xta/client/XtaTestServerSetupExtension.java
@@ -2,7 +2,6 @@ package de.ozgcloud.xta.client;
 
 import static de.ozgcloud.xta.client.XtaDevServerSetupExtension.*;
 
-import java.io.File;
 import java.util.List;
 import java.util.Objects;
 
@@ -12,8 +11,6 @@ import org.junit.jupiter.api.extension.BeforeEachCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
 import org.testcontainers.utility.DockerImageName;
 
-import com.google.common.io.Files;
-
 import de.ozgcloud.xta.client.config.XtaClientConfig;
 import de.ozgcloud.xta.client.core.WrappedXtaService;
 import de.ozgcloud.xta.client.model.Identifier;
@@ -36,13 +33,18 @@ public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllC
 	private static final DockerImageName XTA_TEST_SERVER_IMAGE = DockerImageName.parse("docker.ozg-sh.de/xta-test-server")
 			.withTag("1.4.1-SNAPSHOT");
 
+	private static final String JOHN_SMITH_KEYSTORE_PATH = "store/john-smith-client-cert-keystore.p12";
+	private static final String JOHN_SMITH_KEYSTORE_PASSWORD = "password";
+
+	private static final String XTA_TEST_SERVER_TRUSTSTORE_PATH = "store/xta-test-server-truststore.jks";
+	private static final String XTA_TEST_SERVER_TRUSTSTORE_PASSWORD = "password";
+
 	private XtaClient client;
 	private WrappedXtaService service;
 	private XtaClientConfig config;
 	private XtaClientFactory clientFactory;
 	private XtaTestServerContainer xtaServerContainer;
 
-
 	@Override
 	@SneakyThrows
 	public void beforeAll(ExtensionContext context) {
@@ -73,21 +75,25 @@ public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllC
 
 	@SneakyThrows
 	XtaClient setupClient() {
-		var clientCertKeystorePath = getEnvVar("KOP_SH_KIEL_DEV_PATH");
-		var clientCertKeystorePassword = getEnvVar("KOP_SH_KIEL_DEV_PASSWORD");
 
-		// TODO Trust store (xta-test-server certificate not trusted...)
 		var clientCertKeyStore = XtaClientConfig.KeyStore.builder()
-				.content(readBytesFromFile(clientCertKeystorePath))
+				.content(readBytesFromResource(JOHN_SMITH_KEYSTORE_PATH))
 				.type("PKCS12")
-				.password(clientCertKeystorePassword.toCharArray())
+				.password(JOHN_SMITH_KEYSTORE_PASSWORD.toCharArray())
+				.build();
+		var trustStore = XtaClientConfig.KeyStore.builder()
+				.content(readBytesFromResource(XTA_TEST_SERVER_TRUSTSTORE_PATH))
+				.type("JKS")
+				.password(XTA_TEST_SERVER_TRUSTSTORE_PASSWORD.toCharArray())
 				.build();
+
 		config = XtaClientConfig.builder()
 				.clientIdentifiers(List.of(CLIENT_IDENTIFIER3, CLIENT_IDENTIFIER2, CLIENT_IDENTIFIER1))
 				.managementServiceUrl(xtaServerContainer.getManagementPortUrl())
 				.sendServiceUrl(xtaServerContainer.getSendPortUrl())
 				.msgBoxServiceUrl(xtaServerContainer.getMsgBoxPortUrl())
 				.clientCertKeystore(clientCertKeyStore)
+				.trustStore(trustStore)
 				.logSoapRequests(true)
 				.logSoapResponses(true)
 				.build();
@@ -95,10 +101,6 @@ public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllC
 		return clientFactory.create();
 	}
 
-	private String getEnvVar(String name) {
-		return Objects.requireNonNull(System.getenv(name), "Environment variable " + name + " is required!");
-	}
-
 	@Override
 	@SneakyThrows
 	public void beforeEach(ExtensionContext context) {
@@ -157,8 +159,10 @@ public class XtaTestServerSetupExtension implements BeforeAllCallback, AfterAllC
 	}
 
 	@SneakyThrows
-	private static byte[] readBytesFromFile(String path) {
-		return Files.toByteArray(new File(path));
+	private static byte[] readBytesFromResource(String resourcePath) {
+		try (var inputStream = XtaTestServerSetupExtension.class.getClassLoader().getResourceAsStream(resourcePath)) {
+			return Objects.requireNonNull(inputStream, "Expect class-resource at: " + resourcePath).readAllBytes();
+		}
 	}
 
 }
diff --git a/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java b/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java
index 3e27432..294fcb0 100644
--- a/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java
+++ b/src/test/java/de/ozgcloud/xta/client/core/XtaTLSClientParametersFactoryTest.java
@@ -149,6 +149,54 @@ class XtaTLSClientParametersFactoryTest {
 				assertThat(sslContextResult).isEqualTo(sslContext);
 			}
 		}
+
+		@DisplayName("with trust store")
+		@Nested
+		class TestWithTrustStore {
+			@BeforeEach
+			@SneakyThrows
+			void mock() {
+				when(config.getTrustStore()).thenReturn(configKeystore);
+				doReturn(keystore).when(factory).loadStore(configKeystore);
+			}
+
+			@DisplayName("should call loadTrustMaterial")
+			@Test
+			@SneakyThrows
+			void shouldCallLoadTrustMaterial() {
+				factory.createXtaSslContext();
+
+				verify(sslContextBuilder).loadTrustMaterial(keystore, null);
+			}
+
+			@DisplayName("should return SSL context")
+			@Test
+			@SneakyThrows
+			void shouldSetProtocol() {
+				var sslContextResult = factory.createXtaSslContext();
+
+				assertThat(sslContextResult).isEqualTo(sslContext);
+			}
+		}
+
+		@DisplayName("without trust store")
+		@Nested
+		class TestWithoutTrustStore {
+			@BeforeEach
+			@SneakyThrows
+			void mock() {
+				when(config.getTrustStore()).thenReturn(null);
+			}
+
+			@DisplayName("should return SSL context")
+			@Test
+			@SneakyThrows
+			void shouldSetProtocol() {
+				var sslContextResult = factory.createXtaSslContext();
+
+				assertThat(sslContextResult).isEqualTo(sslContext);
+			}
+		}
 	}
 
 	@DisplayName("create SSL context builder")
diff --git a/src/test/resources/store/john-smith-client-cert-keystore.p12 b/src/test/resources/store/john-smith-client-cert-keystore.p12
new file mode 100644
index 0000000000000000000000000000000000000000..a727395694185315016bfccc2fd42e17749e4592
GIT binary patch
literal 2851
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
N_V?swCT7OA1pu3bDyIMd

literal 0
HcmV?d00001

diff --git a/src/test/resources/store/xta-test-server-truststore.jks b/src/test/resources/store/xta-test-server-truststore.jks
new file mode 100644
index 0000000000000000000000000000000000000000..0935b1e776a77b5caa7e8cfc4a6a02a9881d03b7
GIT binary patch
literal 1136
zcmezO_TO6u1_mYu1_nkjEmTpGs8EtxT#{O(P?VovqL7@(z`$5r$hb9<fi*(U)WDK~
zfkoAziAB+%iJ5Z&GZP~d69?1k0G55_t0o!nvT<s)d9;1!Wn|=LWiV)5ZpdxG$;KSY
z!Y0h*;%dlmzzgDV2(vk*7Ug8-!35ah0zw7?AQfD~oPPO5C5br-j_!ss22vm~Zed}!
z%)G?B<jlkzh2Z?0(vr;lykbLX14)o9v#?M^h@(OX#N|OCmpeNesvD@nUBt;KCXtd^
zl&zOknw*i5UzF#N2ev{lIX~AxPMp`s(9qD>z|g?dz`!6%oYxqcJA{1F#HfTE4UDV|
z%uS5^3<gb%Tue=jj113=OlHY>Nw;t{?wzXmJS<@M))dz?ZkN{Ik;(ZN-%Zlqt+l*P
z?1SFpd7oBCum9fjg@LK}et_t7)j}4|B5N*7Ue}Xyo}V_=Z`i$nC#U7pY!!xxWlnEY
z9$Gd}m2ypDY>3`|&wQ=;p7uRX%UO@M8!oT3N;EyjzroB@WNqTfJq|oc-wm0+Cb7Qy
z_O<8ljCQZpE;B3VCgm!#g(h7nk6&<vDLl;XR`-JTj8Fw_pMDL-goU%jO1JTsN{G#V
z<0R!%xLp6x9iNN~1)CN$pRfoN`+D}un$IFkUxOTKcDS=1)L6UeLi8TKlKs1PCkmaM
zt#M>y<2A{w$3chw9bB(rG0&Xkh}J<SW=00a#r_7q20U!cp|Zj(tOm@CjQ<T}K|DSd
zF%}V~*_r3pL>uH^sxnKBx1GGQ_PX$I18$HsKMM;p6Jwi!ARA{wn+Idt4<|-OkPrhq
za*%U_gPf6Jr`4N0XPxTw+Nj0d=eJi)Gf!l=o_ej?PrhH&epz(B<l94%65YpBiZ6Y=
z^JC2w%U-9?H^dlPijSXd$$Mk&zH7pjs@&KoZ5O+I&5!eJnl1Kmuf<sflX$InCfoML
z+C0|&c2YE=?2dW<!=o?Xh&N6XpI0X{={QrCPwo}&hmxybXI<DM?y6zC+d^}}ro4X*
zWs=j6_b%JJ@BMnI((Yx9M^DHzwq0Gm$IR>BoBsC`oA0i=ofg#KdwaL}<?qb1&ztFa
zt$dl$=6*}F>yE6|$u~CkI!w{~IUmjGwBKd7%kR(DwEL@mEaCmsp{#SN;*|e&sWT$V
syI&o@+tuv8{PG9mP!)OE%;|C3uc{s?JI~#pxb)6h&!-FrRJS+-0FB&{UjP6A

literal 0
HcmV?d00001

-- 
GitLab