This is an automated email from the ASF dual-hosted git repository.
lmccay pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 29c606fe4 KNOX-3105 - Add Topology Level Config for Truststore to
RemoteAuthProvider (#1001)
29c606fe4 is described below
commit 29c606fe4e5a9e423cc48eb0fbebcf2b7b13eeeb
Author: lmccay <[email protected]>
AuthorDate: Tue Mar 4 09:51:20 2025 -0500
KNOX-3105 - Add Topology Level Config for Truststore to RemoteAuthProvider
(#1001)
* KNOX-3105 - Add Topology Level Config for Truststore to RemoteAuthProvider
---
.../knox/gateway/filter/RemoteAuthFilter.java | 76 +++++++---
.../knox/gateway/filter/RemoteAuthFilterTest.java | 165 +++++++++++++++++++--
.../security/impl/DefaultKeystoreService.java | 21 +++
.../gateway/services/security/KeystoreService.java | 22 +++
4 files changed, 247 insertions(+), 37 deletions(-)
diff --git
a/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
b/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
index 8932b3873..bbf5d17aa 100755
---
a/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
+++
b/gateway-provider-security-authc-remote/src/main/java/org/apache/knox/gateway/filter/RemoteAuthFilter.java
@@ -63,20 +63,24 @@ import java.util.concurrent.TimeUnit;
public class RemoteAuthFilter implements Filter {
- private static final String CONFIG_REMOTE_AUTH_URL = "remote.auth.url";
- private static final String CONFIG_INCLUDE_HEADERS =
"remote.auth.include.headers";
- private static final String CONFIG_CACHE_KEY_HEADER =
"remote.auth.cache.key";
- private static final String CONFIG_EXPIRE_AFTER = "remote.auth.expire.after";
- private static final String DEFAULT_CACHE_KEY_HEADER = "Authorization";
- private static final String CONFIG_USER_HEADER = "remote.auth.user.header";
- private static final String CONFIG_GROUP_HEADER = "remote.auth.group.header";
- private static final String DEFAULT_CONFIG_USER_HEADER = "X-Knox-Actor-ID";
- private static final String DEFAULT_CONFIG_GROUP_HEADER =
"X-Knox-Actor-Groups-*";
- private static final String WILDCARD = "*";
+ static final String REMOTE_AUTH = "remote.auth.";
+ static final String CONFIG_REMOTE_AUTH_URL = REMOTE_AUTH + "url";
+ static final String CONFIG_INCLUDE_HEADERS = REMOTE_AUTH + "include.headers";
+ static final String CONFIG_CACHE_KEY_HEADER = REMOTE_AUTH + "cache.key";
+ static final String CONFIG_EXPIRE_AFTER = REMOTE_AUTH + "expire.after";
+ static final String DEFAULT_CACHE_KEY_HEADER = "Authorization";
+ static final String CONFIG_USER_HEADER = REMOTE_AUTH + "user.header";
+ static final String CONFIG_GROUP_HEADER = REMOTE_AUTH + "group.header";
+ static final String DEFAULT_CONFIG_USER_HEADER = "X-Knox-Actor-ID";
+ static final String DEFAULT_CONFIG_GROUP_HEADER = "X-Knox-Actor-Groups-*";
+ static final String CONFIG_TRUSTSTORE_PATH = REMOTE_AUTH + "truststore.path";
+ static final String CONFIG_TRUSTSTORE_PASSWORD = REMOTE_AUTH +
"truststore.password";
+ static final String CONFIG_TRUSTSTORE_TYPE = REMOTE_AUTH + "truststore.type";
+ static final String DEFAULT_TRUSTSTORE_TYPE = "JKS";
+ static final String WILDCARD = "*";
static final String TRACE_ID = "trace_id";
static final String REQUEST_ID_HEADER_NAME = "X-Request-Id";
-
private String remoteAuthUrl;
private List<String> includeHeaders;
private String cacheKeyHeader;
@@ -94,6 +98,10 @@ public class RemoteAuthFilter implements Filter {
AuditConstants.DEFAULT_AUDITOR_NAME,
AuditConstants.KNOX_SERVICE_NAME, AuditConstants.KNOX_COMPONENT_NAME );
private final RemoteAuthMessages LOGGER = MessagesFactory.get(
RemoteAuthMessages.class );
+ private String truststorePath;
+ private String truststorePassword;
+ private String truststoreType;
+
@Override
public void init(FilterConfig filterConfig) throws ServletException {
remoteAuthUrl = filterConfig.getInitParameter(CONFIG_REMOTE_AUTH_URL);
@@ -123,6 +131,13 @@ public class RemoteAuthFilter implements Filter {
} else {
groupHeaders = Arrays.asList(groupHeaderParam.split("\\s*,\\s*"));
}
+
+ truststorePath = filterConfig.getInitParameter(CONFIG_TRUSTSTORE_PATH);
+ truststorePassword =
filterConfig.getInitParameter(CONFIG_TRUSTSTORE_PASSWORD);
+ truststoreType = filterConfig.getInitParameter(CONFIG_TRUSTSTORE_TYPE);
+ if (truststoreType == null || truststoreType.isEmpty()) {
+ truststoreType = DEFAULT_TRUSTSTORE_TYPE;
+ }
}
public SSLSocketFactory createSSLSocketFactory(KeyStore trustStore) throws
Exception {
@@ -202,14 +217,7 @@ public class RemoteAuthFilter implements Filter {
if (services != null) {
KeystoreService keystoreService =
services.getService(ServiceType.KEYSTORE_SERVICE);
if (keystoreService != null) {
- try {
- truststore = keystoreService.getTruststoreForHttpClient();
- if (truststore == null) {
- truststore = keystoreService.getKeystoreForGateway();
- }
- } catch (KeystoreServiceException e) {
- LOGGER.failedToLoadTruststore(e.getMessage(), e);
- }
+ truststore = getTrustStore(truststore, keystoreService);
}
}
HttpURLConnection connection;
@@ -217,11 +225,11 @@ public class RemoteAuthFilter implements Filter {
URL url = new URL(remoteAuthUrl);
connection = (HttpURLConnection) url.openConnection();
if (truststore != null) {
- try {
- ((HttpsURLConnection)
connection).setSSLSocketFactory(createSSLSocketFactory(truststore));
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
+ try {
+ ((HttpsURLConnection)
connection).setSSLSocketFactory(createSSLSocketFactory(truststore));
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
} else {
connection = httpURLConnection;
@@ -229,6 +237,26 @@ public class RemoteAuthFilter implements Filter {
return connection;
}
+ private KeyStore getTrustStore(KeyStore truststore, KeystoreService
keystoreService) throws IOException {
+ try {
+ // Try topology-specific truststore first if configured
+ if (truststorePath != null && !truststorePath.isEmpty()) {
+ truststore = keystoreService.loadTruststore(truststorePath,
truststoreType, truststorePassword);
+ }
+ // Fall back to gateway-level truststore
+ if (truststore == null) {
+ truststore = keystoreService.getTruststoreForHttpClient();
+ if (truststore == null) {
+ truststore = keystoreService.getKeystoreForGateway();
+ }
+ }
+ } catch (KeystoreServiceException e) {
+ LOGGER.failedToLoadTruststore(e.getMessage(), e);
+ throw new IOException("Failed to load truststore: ", e);
+ }
+ return truststore;
+ }
+
private void continueWithEstablishedSecurityContext(Subject subject, final
HttpServletRequest request, final HttpServletResponse response, final
FilterChain chain) throws IOException, ServletException {
try {
Subject.doAs(
diff --git
a/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
b/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
index e31fff451..f81af57e5 100644
---
a/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
+++
b/gateway-provider-security-authc-remote/src/test/java/org/apache/knox/gateway/filter/RemoteAuthFilterTest.java
@@ -19,6 +19,10 @@ package org.apache.knox.gateway.filter;
import org.apache.knox.gateway.security.GroupPrincipal;
import org.apache.knox.gateway.security.PrimaryPrincipal;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.KeystoreService;
+import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.test.mock.MockServletContext;
import org.easymock.EasyMock;
import org.junit.Before;
@@ -28,6 +32,7 @@ import org.apache.logging.log4j.ThreadContext;
import javax.security.auth.Subject;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
@@ -45,17 +50,19 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.security.KeyStore;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+@SuppressWarnings("PMD.JUnit4TestShouldUseBeforeAnnotation")
public class RemoteAuthFilterTest {
public static final String BEARER_INVALID_TOKEN = "Bearer invalid-token";
public static final String BEARER_VALID_TOKEN = "Bearer valid-token";
- public static final String URL_SUCCESS = "http://example.com/auth";
- private static final String URL_FAIL = "http://example.com/authfail";
+ public static final String URL_SUCCESS = "https://example.com/auth";
+ private static final String URL_FAIL = "https://example.com/authfail";
public static final String X_AUTHENTICATED_USER = "X-Authenticated-User";
public static final String X_AUTHENTICATED_GROUP = "X-Authenticated-Group";
public static final String X_AUTHENTICATED_GROUP_2 =
"X-Authenticated-Group-2";
@@ -65,22 +72,52 @@ public class RemoteAuthFilterTest {
private HttpServletRequest requestMock;
private HttpServletResponse responseMock;
private TestFilterChain chainMock;
+ private GatewayServices gatewayServicesMock;
+ private KeystoreService keystoreServiceMock;
+ private ServletContext servletContextMock;
+
@Before
- public void setUp() {
- FilterConfig filterConfigMock =
EasyMock.createNiceMock(FilterConfig.class);
+ public void createMocks() {
requestMock = EasyMock.createMock(HttpServletRequest.class);
responseMock = EasyMock.createMock(HttpServletResponse.class);
+ }
+
+ private void setUp(String trustStorePath, String trustStorePass, String
trustStoreType) {
+ // Reset existing mocks
+ EasyMock.reset(requestMock, responseMock);
+
+ FilterConfig filterConfigMock =
EasyMock.createNiceMock(FilterConfig.class);
chainMock = new TestFilterChain();
-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.url")).andReturn("http://example.com/auth").anyTimes();
-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.include.headers")).andReturn("Authorization").anyTimes();
-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.cache.key")).andReturn("Authorization").anyTimes();
-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.expire.after")).andReturn("5").anyTimes();
-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.user.header")).andReturn(X_AUTHENTICATED_USER).anyTimes();
-
EasyMock.expect(filterConfigMock.getInitParameter("remote.auth.group.header"))
+ // Create and configure Gateway Services mocks
+ gatewayServicesMock = EasyMock.createNiceMock(GatewayServices.class);
+ keystoreServiceMock = EasyMock.createNiceMock(KeystoreService.class);
+ servletContextMock = EasyMock.createNiceMock(ServletContext.class);
+
+ // Set up Gateway Services expectations
+
EasyMock.expect(gatewayServicesMock.getService(ServiceType.KEYSTORE_SERVICE))
+ .andReturn(keystoreServiceMock)
+ .anyTimes();
+
EasyMock.expect(servletContextMock.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE))
+ .andReturn(gatewayServicesMock)
+ .anyTimes();
+
+ // Basic config
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_REMOTE_AUTH_URL)).andReturn("https://example.com/auth").anyTimes();
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_INCLUDE_HEADERS)).andReturn("Authorization").anyTimes();
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.DEFAULT_CACHE_KEY_HEADER)).andReturn("Authorization").anyTimes();
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_EXPIRE_AFTER)).andReturn("5").anyTimes();
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_USER_HEADER)).andReturn(X_AUTHENTICATED_USER).anyTimes();
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_GROUP_HEADER))
.andReturn(X_AUTHENTICATED_GROUP + "," +
X_AUTHENTICATED_GROUP_2 + ",X-Custom-Group-*").anyTimes();
- EasyMock.replay(filterConfigMock);
+ // Trust store config
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_TRUSTSTORE_PATH)).andReturn(trustStorePath).anyTimes();
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_TRUSTSTORE_PASSWORD)).andReturn(trustStorePass).anyTimes();
+
EasyMock.expect(filterConfigMock.getInitParameter(RemoteAuthFilter.CONFIG_TRUSTSTORE_TYPE)).andReturn(trustStoreType).anyTimes();
+
+ // Only replay the mocks that won't need additional expectations
+ EasyMock.replay(filterConfigMock, gatewayServicesMock,
servletContextMock);
filter = new RemoteAuthFilter();
try {
@@ -90,6 +127,11 @@ public class RemoteAuthFilterTest {
}
}
+ // Default setup method for backward compatibility
+ private void setUp() {
+ setUp(null, null, null);
+ }
+
private void setupURLConnection(String url) {
try {
filter.httpURLConnection = new MockHttpURLConnection(new URL(url));
@@ -100,6 +142,8 @@ public class RemoteAuthFilterTest {
@Test
public void successfulAuthentication() throws Exception {
+ setUp();
+
EasyMock.expect(requestMock.getServletContext()).andReturn(new
MockServletContext()).anyTimes();
EasyMock.expect(requestMock.getHeader("Authorization")).andReturn(BEARER_VALID_TOKEN).anyTimes();
EasyMock.expect(responseMock.getStatus()).andReturn(200).anyTimes();
@@ -134,6 +178,8 @@ public class RemoteAuthFilterTest {
@Test
public void authenticationFailsWithInvalidToken() throws Exception {
+ setUp();
+
EasyMock.expect(requestMock.getServletContext()).andReturn(new
MockServletContext()).anyTimes();
EasyMock.expect(responseMock.getStatus()).andReturn(401).anyTimes();
EasyMock.expect(requestMock.getHeader("Authorization")).andReturn(BEARER_INVALID_TOKEN).anyTimes();
@@ -160,11 +206,12 @@ public class RemoteAuthFilterTest {
@Test
public void testCacheBehavior() throws Exception {
+ setUp();
+
String principalName = "lmccayiv";
String groupNames = "admin2,scientists";
Subject subject = new Subject();
subject.getPrincipals().add(new PrimaryPrincipal(principalName));
- // Add groups to the principal if available
Arrays.stream(groupNames.split(",")).forEach(groupName ->
subject.getPrincipals()
.add(new GroupPrincipal(groupName)));
filter.setCachedSubject(BEARER_VALID_TOKEN, subject);
@@ -202,9 +249,10 @@ public class RemoteAuthFilterTest {
@Test
public void testTraceIdPropagation() throws Exception {
+ setUp();
+
String expectedTraceId = "test-trace-123";
- // Set up mocks
EasyMock.expect(requestMock.getServletContext())
.andReturn(new MockServletContext())
.anyTimes();
@@ -250,6 +298,8 @@ public class RemoteAuthFilterTest {
@Test
public void successfulAuthenticationWithMultipleGroups() throws Exception {
+ setUp();
+
EasyMock.expect(requestMock.getServletContext()).andReturn(new
MockServletContext()).anyTimes();
EasyMock.expect(requestMock.getHeader("Authorization")).andReturn(BEARER_VALID_TOKEN).anyTimes();
EasyMock.expect(responseMock.getStatus()).andReturn(200).anyTimes();
@@ -287,6 +337,95 @@ public class RemoteAuthFilterTest {
}
}
+ @Test
+ public void testSuccessfulHttpsRequestWithTrustStore() throws Exception {
+ // Setup with valid trust store configuration
+ setUp("/path/to/truststore.jks", "trustpass", "JKS");
+
+ KeyStore testTruststore = KeyStore.getInstance("JKS");
+
EasyMock.expect(keystoreServiceMock.loadTruststore("/path/to/truststore.jks",
"JKS", "trustpass"))
+ .andReturn(testTruststore)
+ .anyTimes();
+
+ EasyMock.expect(requestMock.getServletContext())
+ .andReturn(servletContextMock)
+ .anyTimes();
+ EasyMock.expect(requestMock.getHeader("Authorization"))
+ .andReturn(BEARER_VALID_TOKEN)
+ .anyTimes();
+ EasyMock.expect(responseMock.getStatus())
+ .andReturn(200)
+ .anyTimes();
+
responseMock.sendError(EasyMock.eq(HttpServletResponse.SC_UNAUTHORIZED),
EasyMock.anyString());
+ EasyMock.expectLastCall().andThrow(new AssertionError("Authentication
should be successful, but was not.")).anyTimes();
+
+ EasyMock.replay(requestMock, responseMock, keystoreServiceMock);
+
+ setupURLConnection("https://example.com/auth");
+ filter.doFilter(requestMock, responseMock, chainMock);
+
+ assertTrue("Filter chain should have been called",
chainMock.doFilterCalled);
+ }
+
+ @Test
+ public void testHttpsRequestWithoutTrustStore() throws Exception {
+ // Setup without trust store configuration
+ setUp(null, null, null);
+
+ KeyStore defaultTruststore = KeyStore.getInstance("JKS");
+ EasyMock.expect(keystoreServiceMock.getTruststoreForHttpClient())
+ .andReturn(defaultTruststore)
+ .anyTimes();
+
+ EasyMock.expect(requestMock.getServletContext())
+ .andReturn(servletContextMock)
+ .anyTimes();
+ EasyMock.expect(requestMock.getHeader("Authorization"))
+ .andReturn(BEARER_VALID_TOKEN)
+ .anyTimes();
+ EasyMock.expect(responseMock.getStatus())
+ .andReturn(200)
+ .anyTimes();
+
responseMock.sendError(EasyMock.eq(HttpServletResponse.SC_UNAUTHORIZED),
EasyMock.anyString());
+ EasyMock.expectLastCall().andThrow(new AssertionError("Authentication
should be successful, but was not.")).anyTimes();
+
+ EasyMock.replay(requestMock, responseMock, keystoreServiceMock);
+
+ setupURLConnection("https://example.com/auth");
+ filter.doFilter(requestMock, responseMock, chainMock);
+
+ assertTrue("Filter chain should have been called with default trust
store", chainMock.doFilterCalled);
+ }
+
+ @Test
+ public void testHttpsRequestWithInvalidTrustStoreConfig() throws Exception
{
+ // Setup with invalid trust store configuration
+ setUp("/nonexistent/path/truststore.jks", "password", "JKS");
+
+
EasyMock.expect(keystoreServiceMock.loadTruststore("/nonexistent/path/truststore.jks",
"JKS", "password"))
+ .andThrow(new KeystoreServiceException("Failed to load
truststore"))
+ .anyTimes();
+
+ EasyMock.expect(requestMock.getServletContext())
+ .andReturn(servletContextMock)
+ .anyTimes();
+ EasyMock.expect(requestMock.getHeader("Authorization"))
+ .andReturn(BEARER_VALID_TOKEN)
+ .anyTimes();
+ EasyMock.expect(responseMock.getStatus())
+ .andReturn(500)
+ .anyTimes();
+ responseMock.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Error processing authentication request");
+ EasyMock.expectLastCall().once();
+
+ EasyMock.replay(requestMock, responseMock, keystoreServiceMock);
+
+ filter.doFilter(requestMock, responseMock, chainMock);
+
+ assertFalse("Filter chain should not have been called",
chainMock.doFilterCalled);
+ EasyMock.verify(responseMock);
+ }
+
public static class MockHttpURLConnection extends HttpURLConnection {
private final URL url;
private int responseCode;
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java
index 2ed637a47..e51f904ae 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/security/impl/DefaultKeystoreService.java
@@ -46,6 +46,7 @@ import java.net.InetAddress;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -562,6 +563,26 @@ public class DefaultKeystoreService implements
KeystoreService {
}
}
+ @Override
+ public synchronized KeyStore loadKeyStore(String path, String keystoreType,
String password)
+ throws KeystoreServiceException {
+ try {
+ return createKeyStore(FileSystems.getDefault().getPath(path),
keystoreType, password.toCharArray());
+ } catch (Exception e) {
+ throw new KeystoreServiceException(e);
+ }
+ }
+
+ @Override
+ public synchronized KeyStore loadTruststore(String path, String
keystoreType, String password)
+ throws KeystoreServiceException {
+ try {
+ return loadKeyStore(FileSystems.getDefault().getPath(path),
keystoreType, password.toCharArray());
+ } catch (Exception e) {
+ throw new KeystoreServiceException(e);
+ }
+ }
+
// Package private for unit test access
synchronized void writeKeyStoreToFile(final KeyStore keyStore, final Path
path, char[] password)
throws KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException {
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java
index b2cb717a9..dc8069a86 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/KeystoreService.java
@@ -79,4 +79,26 @@ public interface KeystoreService extends Service {
char[] getCredentialForCluster(String clusterName, String alias, KeyStore
ks) throws KeystoreServiceException;
String getKeystorePath();
+
+ /**
+ * Load a keystore from the specified path with the given password.
+ *
+ * @param path The path to the keystore file
+ * @param password The password for the keystore
+ * @return The loaded KeyStore instance
+ * @throws KeystoreServiceException if loading fails
+ */
+ KeyStore loadKeyStore(String path, String keystoreType, String password)
+ throws KeystoreServiceException;
+
+ /**
+ * Load a truststore from the specified path with the given password.
+ *
+ * @param path The path to the truststore file
+ * @param password The password for the truststore
+ * @return The loaded KeyStore instance
+ * @throws KeystoreServiceException if loading fails
+ */
+ KeyStore loadTruststore(String path, String keystoreType, String password)
+ throws KeystoreServiceException;
}