diff --git a/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcher.java b/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcher.java
new file mode 100644
index 000000000..3653b93a1
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcher.java
@@ -0,0 +1,243 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.identityconnectors.framework.impl.api.remote;
+
+import java.net.IDN;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.net.ssl.SNIHostName;
+import javax.security.auth.x500.X500Principal;
+
+/**
+ * Matches a host name or IP address against the names in an X.509 server
+ * certificate, approximating RFC 2818 and JSSE's "HTTPS" endpoint
+ * identification: IP addresses against subjectAltName iPAddress entries,
+ * host names against subjectAltName dNSName entries, falling back to the
+ * subject CN only when the certificate carries no dNSName at all. As in JSSE,
+ * one trailing dot of the host is ignored and a host name that is not a valid
+ * DNS name matches nothing. Wildcards
+ * follow JSSE's rules for certificates from a private CA; the public suffix
+ * check JSSE adds for public CAs is not replicated.
+ *
+ * This is a diagnostic aid for deployments that switched hostname
+ * verification off, so a mismatch reported here is advisory; the enforcing
+ * check is done by JSSE during the handshake.
+ */
+final class CertificateHostnameMatcher {
+
+ private static final int SAN_DNS_NAME = 2;
+ private static final int SAN_IP_ADDRESS = 7;
+ private static final Pattern IPV4_LITERAL =
+ Pattern.compile("((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)");
+
+ private CertificateHostnameMatcher() {
+ }
+
+ /**
+ * Returns whether {@code host} (a DNS name or an IP literal) is one of the
+ * names the certificate was issued for.
+ */
+ static boolean matches(String host, X509Certificate certificate) {
+ host = stripTrailingDot(host);
+ if (isIpLiteral(host)) {
+ for (String address : subjectAltNames(certificate, SAN_IP_ADDRESS)) {
+ if (sameAddress(host, address)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (!isVerifiableHost(host)) {
+ return false;
+ }
+ List dnsNames = subjectAltNames(certificate, SAN_DNS_NAME);
+ if (dnsNames.isEmpty()) {
+ String commonName = commonName(certificate);
+ return commonName != null && matchesDnsName(host, commonName);
+ }
+ for (String dnsName : dnsNames) {
+ if (matchesDnsName(host, dnsName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether JSSE can verify a certificate for {@code host} at all:
+ * an IP literal, or a valid DNS name once one trailing dot is removed.
+ * JSSE rejects any other host name (an underscore, an empty label, a
+ * leading or trailing hyphen) before it looks at the certificate, so no
+ * certificate can pass hostname verification for it.
+ */
+ static boolean isVerifiableHost(String host) {
+ host = stripTrailingDot(host);
+ if (isIpLiteral(host)) {
+ return true;
+ }
+ try {
+ // the check sun.security.util.HostnameChecker.matchDNS runs first
+ new SNIHostName(host);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Describes the subject and subjectAltName entries of the certificate for
+ * log messages, e.g.
+ * {@code subject 'CN=localhost', subjectAltName [dns:localhost, ip:127.0.0.1]}.
+ */
+ static String describe(X509Certificate certificate) {
+ StringBuilder description = new StringBuilder("subject '")
+ .append(certificate.getSubjectX500Principal().getName(X500Principal.RFC2253))
+ .append('\'');
+ List names = new ArrayList();
+ for (String dnsName : subjectAltNames(certificate, SAN_DNS_NAME)) {
+ names.add("dns:" + dnsName);
+ }
+ for (String address : subjectAltNames(certificate, SAN_IP_ADDRESS)) {
+ names.add("ip:" + address);
+ }
+ if (names.isEmpty()) {
+ description.append(", no subjectAltName");
+ } else {
+ description.append(", subjectAltName ").append(names);
+ }
+ return description.toString();
+ }
+
+ /** JSSE drops the dot of a fully qualified name before checking it. */
+ private static String stripTrailingDot(String host) {
+ return host.endsWith(".") ? host.substring(0, host.length() - 1) : host;
+ }
+
+ private static boolean isIpLiteral(String host) {
+ return host.indexOf(':') >= 0 || IPV4_LITERAL.matcher(host).matches();
+ }
+
+ private static boolean sameAddress(String host, String address) {
+ String literal = host;
+ if (literal.startsWith("[") && literal.endsWith("]")) {
+ literal = literal.substring(1, literal.length() - 1);
+ }
+ try {
+ // both operands are literals, so no name resolution happens here
+ return InetAddress.getByName(literal).equals(InetAddress.getByName(address));
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Case-insensitive comparison of IDN-normalized names; a {@code *} in the
+ * leftmost label of the certificate name (e.g. {@code *.example.com} or
+ * {@code w*.example.com}) matches within exactly one label of the host.
+ */
+ private static boolean matchesDnsName(String host, String name) {
+ String lowerHost = normalize(host);
+ String lowerName = normalize(name);
+ int lastWildcard = lowerName.lastIndexOf('*');
+ if (lastWildcard < 0) {
+ return lowerHost.equals(lowerName);
+ }
+ // "*", "*." and "*com" are not wildcard names; a '*' outside the
+ // leftmost label is compared literally, as JSSE does
+ if (lowerName.equals("*.") || lowerName.indexOf('.', lastWildcard) < 0) {
+ return false;
+ }
+ String[] hostLabels = lowerHost.split("\\.", -1);
+ String[] nameLabels = lowerName.split("\\.", -1);
+ if (hostLabels.length != nameLabels.length) {
+ return false;
+ }
+ for (int i = 1; i < nameLabels.length; i++) {
+ if (!hostLabels[i].equals(nameLabels[i])) {
+ return false;
+ }
+ }
+ return hostLabels[0].matches(wildcardLabel(nameLabels[0]));
+ }
+
+ private static String wildcardLabel(String label) {
+ StringBuilder regex = new StringBuilder();
+ int start = 0;
+ for (int star = label.indexOf('*'); star >= 0; star = label.indexOf('*', start)) {
+ regex.append(Pattern.quote(label.substring(start, star))).append("[^.]*");
+ start = star + 1;
+ }
+ return regex.append(Pattern.quote(label.substring(start))).toString();
+ }
+
+ private static String normalize(String name) {
+ try {
+ name = IDN.toUnicode(IDN.toASCII(name));
+ } catch (IllegalArgumentException e) {
+ // not a valid IDN: compare as is
+ }
+ return name.toLowerCase(Locale.ENGLISH);
+ }
+
+ private static List subjectAltNames(X509Certificate certificate, int type) {
+ List names = new ArrayList();
+ Collection> entries;
+ try {
+ entries = certificate.getSubjectAlternativeNames();
+ } catch (CertificateParsingException e) {
+ return names;
+ }
+ if (entries == null) {
+ return names;
+ }
+ for (List> entry : entries) {
+ if (entry.size() >= 2 && Integer.valueOf(type).equals(entry.get(0))
+ && entry.get(1) instanceof String) {
+ names.add((String) entry.get(1));
+ }
+ }
+ return names;
+ }
+
+ /** The most specific CN of the subject, or {@code null}. */
+ private static String commonName(X509Certificate certificate) {
+ String subject = certificate.getSubjectX500Principal().getName(X500Principal.RFC2253);
+ try {
+ List rdns = new LdapName(subject).getRdns();
+ // RFC 2253 lists the most specific RDN first, LdapName stores it last
+ for (int i = rdns.size() - 1; i >= 0; i--) {
+ Rdn rdn = rdns.get(i);
+ if ("CN".equalsIgnoreCase(rdn.getType()) && rdn.getValue() instanceof String) {
+ return (String) rdn.getValue();
+ }
+ }
+ } catch (InvalidNameException e) {
+ // fall through: no usable CN
+ }
+ return null;
+ }
+}
diff --git a/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnection.java b/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnection.java
index e19c570de..21bf686de 100644
--- a/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnection.java
+++ b/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnection.java
@@ -19,6 +19,7 @@
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
+ * Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.impl.api.remote;
@@ -28,9 +29,15 @@
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
@@ -46,6 +53,21 @@
public class RemoteFrameworkConnection implements Closeable {
private static final Log LOG = Log.getLog(RemoteFrameworkConnection.class);
+
+ /**
+ * System property controlling whether the connector server certificate is
+ * checked against the configured host when {@code useSSL} is on
+ * (RFC 2818 / RFC 6125 "HTTPS" endpoint identification). Verification is
+ * on unless the property is set to exactly {@code false}; disabling it
+ * leaves the connection open to man-in-the-middle attacks by anyone
+ * holding a certificate the client trusts.
+ */
+ public static final String HOSTNAME_VERIFICATION_PROPERTY =
+ "org.identityconnectors.framework.remote.hostnameVerification";
+
+ /** Servers already reported as mismatching while verification is off. */
+ static final Set REPORTED_MISMATCHES = ConcurrentHashMap.newKeySet();
+
private Socket socket;
private BinaryObjectSerializer encoder;
private BinaryObjectDeserializer decoder;
@@ -97,10 +119,22 @@ private void init(RemoteFrameworkConnectionInfo connectionInfo) throws Exception
factory = context.getSocketFactory();
}
- socket =
- factory.createSocket(socket, connectionInfo.getHost(), connectionInfo
- .getPort(), true);
- ((SSLSocket) socket).startHandshake();
+ SSLSocket sslSocket =
+ (SSLSocket) factory.createSocket(socket, connectionInfo.getHost(),
+ connectionInfo.getPort(), true);
+ // SSLSocket does not check the server certificate against the
+ // host on its own: have JSSE do it during the handshake.
+ boolean verifyHostname = isHostnameVerificationEnabled();
+ if (verifyHostname) {
+ SSLParameters parameters = sslSocket.getSSLParameters();
+ parameters.setEndpointIdentificationAlgorithm("HTTPS");
+ sslSocket.setSSLParameters(parameters);
+ }
+ sslSocket.startHandshake();
+ if (!verifyHostname) {
+ reportCertificateMismatch(connectionInfo, sslSocket);
+ }
+ socket = sslSocket;
}
} catch (Exception e) {
try {
@@ -113,6 +147,66 @@ private void init(RemoteFrameworkConnectionInfo connectionInfo) throws Exception
init(socket);
}
+ /**
+ * Only the literal {@code false} disables verification, so that a typo in
+ * the property value can not silently weaken the connection.
+ */
+ static boolean isHostnameVerificationEnabled() {
+ return !"false".equals(System.getProperty(HOSTNAME_VERIFICATION_PROPERTY));
+ }
+
+ /**
+ * With verification switched off, still tell the administrator (once per
+ * server) when the connection would not have passed, so the certificate
+ * or the configured host can be fixed and verification re-enabled.
+ */
+ private static void reportCertificateMismatch(RemoteFrameworkConnectionInfo connectionInfo,
+ SSLSocket sslSocket) {
+ String server = connectionInfo.getHost() + ":" + connectionInfo.getPort();
+ if (REPORTED_MISMATCHES.contains(server)) {
+ return;
+ }
+ Certificate[] chain;
+ try {
+ chain = sslSocket.getSession().getPeerCertificates();
+ } catch (SSLPeerUnverifiedException e) {
+ chain = null;
+ }
+ String mismatch = describeMismatch(connectionInfo.getHost(), chain);
+ if (mismatch != null && REPORTED_MISMATCHES.add(server)) {
+ LOG.warn("TLS hostname verification is disabled ({0}=false) and connector server {1} {2}",
+ HOSTNAME_VERIFICATION_PROPERTY, server, mismatch);
+ }
+ }
+
+ /**
+ * Says why a connection to {@code host} that presented {@code chain}
+ * ({@code null} when no certificate could be verified) would fail
+ * hostname verification and what to do about it, or returns {@code null}
+ * when it would pass.
+ */
+ static String describeMismatch(String host, Certificate[] chain) {
+ String problem;
+ String remedy = "fix the server certificate and re-enable verification";
+ if (!CertificateHostnameMatcher.isVerifiableHost(host)) {
+ problem = "is configured by a name that is not a valid DNS host name,"
+ + " so no certificate can pass hostname verification for it";
+ remedy = "configure the server by a valid host name or by its IP address"
+ + " and re-enable verification";
+ } else if (chain == null) {
+ problem = "presented no verifiable certificate";
+ } else if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) {
+ problem = "presented no X.509 certificate";
+ } else if (CertificateHostnameMatcher.matches(host, (X509Certificate) chain[0])) {
+ return null;
+ } else {
+ problem = "presented a certificate that may not match the host: "
+ + CertificateHostnameMatcher.describe((X509Certificate) chain[0]);
+ }
+ return problem + ". The connection is exposed to man-in-the-middle attacks; "
+ + remedy + ".";
+ }
+
private void init(Socket socket) throws Exception {
this.socket = socket;
InputStream inputStream = this.socket.getInputStream();
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/logging/CapturingLogSpi.java b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/logging/CapturingLogSpi.java
new file mode 100644
index 000000000..b83a261a6
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/logging/CapturingLogSpi.java
@@ -0,0 +1,75 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.identityconnectors.common.logging;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Logging SPI of this module's tests, selected by the surefire configuration
+ * through {@link Log#LOGSPI_PROP}: it logs to standard out like the default
+ * {@link StdOutLogger} and also records every message, so that a test can
+ * check what was logged.
+ */
+public class CapturingLogSpi extends StdOutLogger {
+
+ private static final List ENTRIES = new CopyOnWriteArrayList();
+
+ private static final class Entry {
+ final Class> clazz;
+ final Log.Level level;
+ final String message;
+
+ Entry(Class> clazz, Log.Level level, String message) {
+ this.clazz = clazz;
+ this.level = level;
+ this.message = message;
+ }
+ }
+
+ /**
+ * Returns whether this SPI is the one the framework logs through.
+ */
+ public static boolean isActive() {
+ return Log.getSpiClass() == CapturingLogSpi.class;
+ }
+
+ /** Forgets every message recorded so far. */
+ public static void clear() {
+ ENTRIES.clear();
+ }
+
+ /**
+ * Returns the messages logged so far for {@code clazz} at {@code level}.
+ */
+ public static List messages(Class> clazz, Log.Level level) {
+ List messages = new ArrayList();
+ for (Entry entry : ENTRIES) {
+ if (entry.clazz == clazz && entry.level == level) {
+ messages.add(entry.message);
+ }
+ }
+ return messages;
+ }
+
+ @Override
+ public void log(Class> clazz, String methodName, Log.Level level, String message,
+ Throwable ex) {
+ ENTRIES.add(new Entry(clazz, level, message));
+ super.log(clazz, methodName, level, message, ex);
+ }
+}
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/RemoteConnectorInfoManagerSSLTests.java b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/RemoteConnectorInfoManagerSSLTests.java
index 94eae13cb..f73aa9bf5 100644
--- a/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/RemoteConnectorInfoManagerSSLTests.java
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/RemoteConnectorInfoManagerSSLTests.java
@@ -19,6 +19,7 @@
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
+ * Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.impl.api;
@@ -201,10 +202,12 @@ protected ConnectorInfoManager getConnectorInfoManager() throws Exception {
final int PORT = 8761;
+ // The client verifies the server certificate against the host it
+ // connects to, so the certificate must carry 127.0.0.1 as subjectAltName.
TrustManager clientTrustManager =
- new MyTrustManager("KeyStore.jks");
+ new MyTrustManager("KeyStore-san.jks");
KeyManager serverKeyManager =
- new MyKeyManager("KeyStore.jks");
+ new MyKeyManager("KeyStore-san.jks");
synchronized (RemoteConnectorInfoManagerSSLTests.class) {
if (null == _server) {
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcherTests.java b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcherTests.java
new file mode 100644
index 000000000..5bcce6cce
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcherTests.java
@@ -0,0 +1,187 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.identityconnectors.framework.impl.api.remote;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import org.testng.annotations.Test;
+
+/**
+ * Test certificates:
+ *
+ * - {@code KeyStore.jks}: {@code CN=localhost}, no subjectAltName;
+ * - {@code KeyStore-san.jks}: {@code CN=localhost},
+ * {@code SAN=dns:localhost,ip:127.0.0.1,ip:::1};
+ * - {@code wildcard-san.pem}: {@code CN=cn.example.org},
+ * {@code SAN=dns:*.example.com,dns:example.net};
+ * - {@code multi-cn.pem}: {@code CN=right.example,OU=x,CN=wrong.example},
+ * no subjectAltName;
+ * - {@code private-wildcard-san.pem}: {@code O=OpenICF test},
+ * {@code SAN=dns:*.com,dns:w*.example.org,dns:300.1.1.1};
+ * - {@code no-names.pem}: {@code O=OpenICF test}, no CN, no subjectAltName;
+ * - {@code invalid-host-san.pem}: {@code O=OpenICF test},
+ * {@code SAN=dns:my_host.example,dns:-bad-.example.com}.
+ *
+ * The PEM files are self-signed, generated with e.g.
+ * {@code openssl req -x509 -newkey rsa:2048 -nodes -keyout /dev/null -days 3650
+ * -subj "/O=OpenICF test" -addext "subjectAltName=DNS:*.com,DNS:w*.example.org"}.
+ */
+public class CertificateHostnameMatcherTests {
+
+ @Test
+ public void matchesCommonNameWhenCertificateHasNoSubjectAltName() throws Exception {
+ X509Certificate cert = keyStoreCertificate("KeyStore.jks");
+ assertTrue(CertificateHostnameMatcher.matches("localhost", cert));
+ assertTrue(CertificateHostnameMatcher.matches("LOCALHOST", cert));
+ assertFalse(CertificateHostnameMatcher.matches("example.com", cert));
+ }
+
+ @Test
+ public void neverMatchesIpAddressAgainstCommonName() throws Exception {
+ X509Certificate cert = keyStoreCertificate("KeyStore.jks");
+ assertFalse(CertificateHostnameMatcher.matches("127.0.0.1", cert));
+ }
+
+ @Test
+ public void matchesDnsSubjectAltName() throws Exception {
+ X509Certificate cert = keyStoreCertificate("KeyStore-san.jks");
+ assertTrue(CertificateHostnameMatcher.matches("localhost", cert));
+ assertFalse(CertificateHostnameMatcher.matches("localhost.localdomain", cert));
+ }
+
+ @Test
+ public void matchesIpSubjectAltName() throws Exception {
+ X509Certificate cert = keyStoreCertificate("KeyStore-san.jks");
+ assertTrue(CertificateHostnameMatcher.matches("127.0.0.1", cert));
+ assertTrue(CertificateHostnameMatcher.matches("::1", cert));
+ assertTrue(CertificateHostnameMatcher.matches("0:0:0:0:0:0:0:1", cert));
+ assertFalse(CertificateHostnameMatcher.matches("127.0.0.2", cert));
+ }
+
+ @Test
+ public void ignoresCommonNameWhenSubjectAltNameHasDnsNames() throws Exception {
+ X509Certificate cert = pemCertificate("wildcard-san.pem");
+ assertFalse(CertificateHostnameMatcher.matches("cn.example.org", cert));
+ assertTrue(CertificateHostnameMatcher.matches("example.net", cert));
+ assertTrue(CertificateHostnameMatcher.matches("EXAMPLE.NET", cert));
+ }
+
+ @Test
+ public void matchesWildcardForExactlyOneLeftmostLabel() throws Exception {
+ X509Certificate cert = pemCertificate("wildcard-san.pem");
+ assertTrue(CertificateHostnameMatcher.matches("www.example.com", cert));
+ assertTrue(CertificateHostnameMatcher.matches("WWW.Example.COM", cert));
+ assertFalse(CertificateHostnameMatcher.matches("example.com", cert));
+ assertFalse(CertificateHostnameMatcher.matches("a.b.example.com", cert));
+ assertFalse(CertificateHostnameMatcher.matches("wwwexample.com", cert));
+ }
+
+ @Test
+ public void acceptsWildcardFormsJsseAcceptsFromPrivateCa() throws Exception {
+ X509Certificate cert = pemCertificate("private-wildcard-san.pem");
+ assertTrue(CertificateHostnameMatcher.matches("example.com", cert));
+ assertFalse(CertificateHostnameMatcher.matches("com", cert));
+ assertFalse(CertificateHostnameMatcher.matches("www.example.com", cert));
+ assertTrue(CertificateHostnameMatcher.matches("www.example.org", cert));
+ assertTrue(CertificateHostnameMatcher.matches("w.example.org", cert));
+ assertFalse(CertificateHostnameMatcher.matches("x.example.org", cert));
+ }
+
+ @Test
+ public void usesMostSpecificCommonName() throws Exception {
+ X509Certificate cert = pemCertificate("multi-cn.pem");
+ assertTrue(CertificateHostnameMatcher.matches("right.example", cert));
+ assertFalse(CertificateHostnameMatcher.matches("wrong.example", cert));
+ }
+
+ @Test
+ public void matchesNothingWithoutCommonNameOrSubjectAltName() throws Exception {
+ X509Certificate cert = pemCertificate("no-names.pem");
+ assertFalse(CertificateHostnameMatcher.matches("localhost", cert));
+ assertFalse(CertificateHostnameMatcher.matches("127.0.0.1", cert));
+ }
+
+ @Test
+ public void treatsOutOfRangeOctetsAsHostName() throws Exception {
+ // 300.1.1.1 is not an IPv4 literal: it is matched against dNSName
+ // entries instead of being resolved and compared as an address
+ X509Certificate cert = pemCertificate("private-wildcard-san.pem");
+ assertTrue(CertificateHostnameMatcher.matches("300.1.1.1", cert));
+ }
+
+ @Test
+ public void verifiesOnlyIpLiteralsAndValidDnsNames() {
+ for (String host : new String[] { "localhost", "connector-server", "icf.local",
+ "WWW.Example.COM", "www.example.com.", "127.0.0.1", "::1", "[::1]", "300.1.1.1" }) {
+ assertTrue(CertificateHostnameMatcher.isVerifiableHost(host), host);
+ }
+ for (String host : new String[] { "my_host.example", "my_host.example.",
+ "a..example.com", "-bad-.example.com", "localhost.." }) {
+ assertFalse(CertificateHostnameMatcher.isVerifiableHost(host), host);
+ }
+ }
+
+ @Test
+ public void ignoresTrailingDotOfHost() throws Exception {
+ assertTrue(CertificateHostnameMatcher.matches("localhost.", keyStoreCertificate("KeyStore.jks")));
+ assertTrue(CertificateHostnameMatcher.matches("localhost.", keyStoreCertificate("KeyStore-san.jks")));
+ assertTrue(CertificateHostnameMatcher.matches("www.example.com.", pemCertificate("wildcard-san.pem")));
+ }
+
+ @Test
+ public void neverMatchesHostThatIsNotAValidDnsName() throws Exception {
+ // JSSE rejects these host names before it looks at the certificate,
+ // even when the certificate carries exactly that name
+ X509Certificate cert = pemCertificate("invalid-host-san.pem");
+ assertFalse(CertificateHostnameMatcher.matches("my_host.example", cert));
+ assertFalse(CertificateHostnameMatcher.matches("-bad-.example.com", cert));
+ }
+
+ @Test
+ public void describeListsSubjectAndSubjectAltNames() throws Exception {
+ String description = CertificateHostnameMatcher.describe(keyStoreCertificate("KeyStore-san.jks"));
+ assertTrue(description.contains("CN=localhost"), description);
+ assertTrue(description.contains("dns:localhost"), description);
+ assertTrue(description.contains("ip:127.0.0.1"), description);
+
+ description = CertificateHostnameMatcher.describe(keyStoreCertificate("KeyStore.jks"));
+ assertTrue(description.contains("CN=localhost"), description);
+ assertFalse(description.contains("dns:"), description);
+ }
+
+ private static X509Certificate keyStoreCertificate(String name) throws Exception {
+ try (InputStream in = CertificateHostnameMatcherTests.class.getResourceAsStream("/" + name)) {
+ assertNotNull(in, "missing test resource " + name);
+ KeyStore store = KeyStore.getInstance("JKS");
+ store.load(in, "changeit".toCharArray());
+ return (X509Certificate) store.getCertificate(store.aliases().nextElement());
+ }
+ }
+
+ private static X509Certificate pemCertificate(String name) throws Exception {
+ try (InputStream in = CertificateHostnameMatcherTests.class.getResourceAsStream("/" + name)) {
+ assertNotNull(in, "missing test resource " + name);
+ return (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(in);
+ }
+ }
+}
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnectionSSLTests.java b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnectionSSLTests.java
new file mode 100644
index 000000000..259dfa9ac
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnectionSSLTests.java
@@ -0,0 +1,396 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.identityconnectors.framework.impl.api.remote;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.identityconnectors.common.CollectionUtil;
+import org.identityconnectors.common.logging.CapturingLogSpi;
+import org.identityconnectors.common.logging.Log;
+import org.identityconnectors.common.security.GuardedString;
+import org.identityconnectors.framework.api.RemoteFrameworkConnectionInfo;
+import org.identityconnectors.framework.common.exceptions.ConnectorException;
+import org.testng.SkipException;
+import org.testng.annotations.Test;
+
+/**
+ * Verifies that the legacy connector server client checks the server
+ * certificate against the configured host (TLS hostname verification).
+ *
+ * {@code KeyStore.jks} holds a certificate with {@code CN=localhost} and no
+ * subjectAltName; {@code KeyStore-san.jks} holds one with
+ * {@code SAN=dns:localhost,ip:127.0.0.1,ip:::1}.
+ */
+public class RemoteFrameworkConnectionSSLTests {
+
+ private static final char[] PASSWORD = "changeit".toCharArray();
+ private static final int TIMEOUT = 10000;
+
+ @Test
+ public void rejectsServerCertificateWithoutMatchingName() throws Exception {
+ KeyStore store = loadKeyStore("KeyStore.jks");
+ try (TlsServer server = new TlsServer(store, InetAddress.getByName("127.0.0.1"))) {
+ try {
+ connect("127.0.0.1", server.getPort(), trustManagers(store)).close();
+ fail("Expected the handshake to fail: certificate has no name matching 127.0.0.1");
+ } catch (ConnectorException e) {
+ assertHandshakeFailure(e);
+ }
+ }
+ }
+
+ @Test
+ public void verifiesHostnameEvenWithPlainX509TrustManager() throws Exception {
+ KeyStore store = loadKeyStore("KeyStore.jks");
+ try (TlsServer server = new TlsServer(store, InetAddress.getByName("127.0.0.1"))) {
+ try {
+ connect("127.0.0.1", server.getPort(),
+ CollectionUtil. newList(new PinningTrustManager(store)))
+ .close();
+ fail("Expected the handshake to fail: certificate has no name matching 127.0.0.1");
+ } catch (ConnectorException e) {
+ assertHandshakeFailure(e);
+ }
+ }
+ }
+
+ @Test
+ public void acceptsServerCertificateWithMatchingSubjectAltName() throws Exception {
+ KeyStore store = loadKeyStore("KeyStore-san.jks");
+ try (TlsServer server = new TlsServer(store, InetAddress.getByName("127.0.0.1"))) {
+ connect("127.0.0.1", server.getPort(), trustManagers(store)).close();
+ }
+ }
+
+ @Test
+ public void fallsBackToCommonNameWhenCertificateHasNoSubjectAltName() throws Exception {
+ KeyStore store = loadKeyStore("KeyStore.jks");
+ InetAddress localhost = InetAddress.getByName("localhost");
+ try (TlsServer server = new TlsServer(store, localhost)) {
+ connect("localhost", server.getPort(), trustManagers(store)).close();
+ }
+ }
+
+ @Test
+ public void connectsWhenHostnameVerificationIsDisabled() throws Exception {
+ KeyStore store = loadKeyStore("KeyStore.jks");
+ try (TlsServer server = new TlsServer(store, InetAddress.getByName("127.0.0.1"))) {
+ withHostnameVerificationProperty("false", () ->
+ connect("127.0.0.1", server.getPort(), trustManagers(store)).close());
+ }
+ }
+
+ @Test
+ public void keepsVerificationUnlessPropertyIsExactlyFalse() throws Exception {
+ KeyStore store = loadKeyStore("KeyStore.jks");
+ try (TlsServer server = new TlsServer(store, InetAddress.getByName("127.0.0.1"))) {
+ for (String value : new String[] { "off", "FALSE" }) {
+ withHostnameVerificationProperty(value, () -> {
+ try {
+ connect("127.0.0.1", server.getPort(), trustManagers(store)).close();
+ fail("'" + value + "' must not disable hostname verification");
+ } catch (ConnectorException e) {
+ assertHandshakeFailure(e);
+ }
+ });
+ }
+ }
+ }
+
+ @Test
+ public void reportsMismatchOnceAndOnlyForNonMatchingCertificateWhenVerificationIsDisabled()
+ throws Exception {
+ assertCapturingLog();
+ KeyStore cnOnly = loadKeyStore("KeyStore.jks");
+ KeyStore san = loadKeyStore("KeyStore-san.jks");
+ InetAddress loopback = InetAddress.getByName("127.0.0.1");
+ RemoteFrameworkConnection.REPORTED_MISMATCHES.clear();
+ CapturingLogSpi.clear();
+ // both servers are bound at once, so their ports differ
+ try (TlsServer cnOnlyServer = new TlsServer(cnOnly, loopback);
+ TlsServer sanServer = new TlsServer(san, loopback)) {
+ withHostnameVerificationProperty("false", () -> {
+ for (int i = 0; i < 2; i++) {
+ connect("127.0.0.1", cnOnlyServer.getPort(), trustManagers(cnOnly)).close();
+ connect("127.0.0.1", sanServer.getPort(), trustManagers(san)).close();
+ }
+ });
+ List warnings = mismatchWarnings("127.0.0.1:" + cnOnlyServer.getPort());
+ assertEquals(warnings.size(), 1, "one warning per server: " + warnings);
+ assertTrue(warnings.get(0).contains("may not match the host"), warnings.get(0));
+ assertEquals(mismatchWarnings("127.0.0.1:" + sanServer.getPort()).size(), 0);
+ }
+ }
+
+ /**
+ * JSSE drops the trailing dot of a fully qualified host name before
+ * checking it, so {@code localhost.} matches {@code dns:localhost}, and
+ * with verification off no warning is logged for it.
+ */
+ @Test
+ public void acceptsFullyQualifiedHostWithTrailingDot() throws Exception {
+ assertCapturingLog();
+ String host = "localhost.";
+ InetAddress address;
+ try {
+ address = InetAddress.getByName(host);
+ } catch (UnknownHostException e) {
+ throw new SkipException(host + " does not resolve here");
+ }
+ KeyStore san = loadKeyStore("KeyStore-san.jks");
+ CapturingLogSpi.clear();
+ try (TlsServer server = new TlsServer(san, address)) {
+ connect(host, server.getPort(), trustManagers(san)).close();
+ withHostnameVerificationProperty("false", () ->
+ connect(host, server.getPort(), trustManagers(san)).close());
+ assertEquals(mismatchWarnings(host + ":" + server.getPort()).size(), 0);
+ }
+ }
+
+ @Test
+ public void describesWhyVerificationWouldFail() throws Exception {
+ Certificate[] cnOnly = { certificate("KeyStore.jks") };
+ Certificate[] san = { certificate("KeyStore-san.jks") };
+ assertNull(RemoteFrameworkConnection.describeMismatch("localhost", san));
+ assertNull(RemoteFrameworkConnection.describeMismatch("127.0.0.1", san));
+ String cnOnlyByIp = RemoteFrameworkConnection.describeMismatch("127.0.0.1", cnOnly);
+ assertTrue(cnOnlyByIp.contains("may not match the host: subject 'CN=localhost"), cnOnlyByIp);
+ assertTrue(cnOnlyByIp.contains("no subjectAltName"), cnOnlyByIp);
+ assertTrue(cnOnlyByIp.contains("fix the server certificate"), cnOnlyByIp);
+ assertTrue(RemoteFrameworkConnection.describeMismatch("localhost", null)
+ .contains("presented no verifiable certificate"));
+ // JSSE rejects the name itself, so no certificate can fix it
+ String invalidHost = RemoteFrameworkConnection.describeMismatch("my_host.example", san);
+ assertTrue(invalidHost.contains("not a valid DNS host name"), invalidHost);
+ assertTrue(invalidHost.contains("by its IP address"), invalidHost);
+ }
+
+ /**
+ * Without trust managers the connection trusts the JVM default trust
+ * store ({@code javax.net.ssl.trustStore}), the typical deployment; a
+ * PKIX failure instead of the hostname failure would mean the store was
+ * not used.
+ */
+ @Test
+ public void verifiesHostnameWithDefaultTrustStore() throws Exception {
+ KeyStore store = loadKeyStore("KeyStore.jks");
+ String trustStore = new File(RemoteFrameworkConnectionSSLTests.class
+ .getResource("/KeyStore.jks").toURI()).getPath();
+ withSystemProperties(new String[][] {
+ { "javax.net.ssl.trustStore", trustStore },
+ { "javax.net.ssl.trustStorePassword", new String(PASSWORD) },
+ { "javax.net.ssl.trustStoreType", "JKS" } }, () -> {
+ try (TlsServer server = new TlsServer(store, InetAddress.getByName("127.0.0.1"))) {
+ connect("127.0.0.1", server.getPort(), null).close();
+ fail("Expected the handshake to fail: certificate has no name matching 127.0.0.1");
+ } catch (ConnectorException e) {
+ assertHandshakeFailure(e);
+ }
+ });
+ }
+
+ // ---- helpers ---------------------------------------------------------
+
+ private interface Action {
+ void run() throws Exception;
+ }
+
+ private static void withHostnameVerificationProperty(String value, Action action)
+ throws Exception {
+ withSystemProperties(new String[][] {
+ { RemoteFrameworkConnection.HOSTNAME_VERIFICATION_PROPERTY, value } }, action);
+ }
+
+ private static void withSystemProperties(String[][] properties, Action action)
+ throws Exception {
+ String[] previous = new String[properties.length];
+ for (int i = 0; i < properties.length; i++) {
+ previous[i] = System.setProperty(properties[i][0], properties[i][1]);
+ }
+ try {
+ action.run();
+ } finally {
+ for (int i = 0; i < properties.length; i++) {
+ if (previous[i] == null) {
+ System.clearProperty(properties[i][0]);
+ } else {
+ System.setProperty(properties[i][0], previous[i]);
+ }
+ }
+ }
+ }
+
+ private static void assertCapturingLog() {
+ assertTrue(CapturingLogSpi.isActive(), "the surefire configuration must select "
+ + CapturingLogSpi.class.getName() + " through " + Log.LOGSPI_PROP);
+ }
+
+ /** WARN messages about the given server logged since the last clear. */
+ private static List mismatchWarnings(String server) {
+ List warnings = new ArrayList();
+ for (String message : CapturingLogSpi.messages(RemoteFrameworkConnection.class,
+ Log.Level.WARN)) {
+ if (message.contains("connector server " + server + " ")) {
+ warnings.add(message);
+ }
+ }
+ return warnings;
+ }
+
+ private static void assertHandshakeFailure(ConnectorException e) {
+ Throwable cause = e;
+ while (cause != null && !(cause instanceof SSLHandshakeException)) {
+ cause = cause.getCause();
+ }
+ assertNotNull(cause, "Expected an SSLHandshakeException in the cause chain of: " + e);
+ String message = String.valueOf(cause.getMessage());
+ assertTrue(message.contains("subject alternative") || message.contains("No name matching"),
+ "Expected a hostname verification failure, got: " + message);
+ }
+
+ private static RemoteFrameworkConnection connect(String host, int port,
+ List trustManagers) {
+ return new RemoteFrameworkConnection(new RemoteFrameworkConnectionInfo(host, port,
+ new GuardedString(PASSWORD), true, trustManagers, TIMEOUT));
+ }
+
+ private static KeyStore loadKeyStore(String name) throws Exception {
+ try (InputStream in = RemoteFrameworkConnectionSSLTests.class.getResourceAsStream("/" + name)) {
+ assertNotNull(in, "missing test resource " + name);
+ KeyStore store = KeyStore.getInstance("JKS");
+ store.load(in, PASSWORD);
+ return store;
+ }
+ }
+
+ private static Certificate certificate(String keyStore) throws Exception {
+ KeyStore store = loadKeyStore(keyStore);
+ return store.getCertificate(store.aliases().nextElement());
+ }
+
+ private static List trustManagers(KeyStore store) throws Exception {
+ TrustManagerFactory factory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ factory.init(store);
+ return Arrays.asList(factory.getTrustManagers());
+ }
+
+ /**
+ * A legacy-style trust manager (not an {@code X509ExtendedTrustManager})
+ * that trusts exactly the certificates found in a key store, mirroring
+ * what integrators commonly plug into {@link RemoteFrameworkConnectionInfo}.
+ */
+ private static final class PinningTrustManager implements X509TrustManager {
+ private final KeyStore store;
+
+ PinningTrustManager(KeyStore store) {
+ this.store = store;
+ }
+
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ checkTrusted(chain);
+ }
+
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ checkTrusted(chain);
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ private void checkTrusted(X509Certificate[] chain) throws CertificateException {
+ try {
+ if (store.getCertificateAlias(chain[0]) == null) {
+ throw new CertificateException("untrusted certificate");
+ }
+ } catch (CertificateException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new CertificateException(e);
+ }
+ }
+ }
+
+ /**
+ * Minimal TLS server: completes the handshake for every accepted
+ * connection and then waits for the client to hang up.
+ */
+ private static final class TlsServer implements AutoCloseable {
+ private final SSLServerSocket serverSocket;
+
+ TlsServer(KeyStore store, InetAddress bindAddress) throws Exception {
+ KeyManagerFactory factory =
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ factory.init(store, PASSWORD);
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(factory.getKeyManagers(), null, null);
+ serverSocket = (SSLServerSocket) context.getServerSocketFactory()
+ .createServerSocket(0, 1, bindAddress);
+ Thread acceptor = new Thread(this::serve, "RemoteFrameworkConnectionSSLTests-server");
+ acceptor.setDaemon(true);
+ acceptor.start();
+ }
+
+ int getPort() {
+ return serverSocket.getLocalPort();
+ }
+
+ private void serve() {
+ while (!serverSocket.isClosed()) {
+ try (SSLSocket socket = (SSLSocket) serverSocket.accept()) {
+ socket.setSoTimeout(TIMEOUT);
+ socket.startHandshake();
+ socket.getInputStream().read();
+ } catch (IOException e) {
+ // handshake rejected by the client or server closed: next connection
+ }
+ }
+ }
+
+ public void close() throws IOException {
+ serverSocket.close();
+ }
+ }
+}
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/resources/KeyStore-san.jks b/OpenICF-java-framework/connector-framework-internal/src/test/resources/KeyStore-san.jks
new file mode 100644
index 000000000..04d6902c2
Binary files /dev/null and b/OpenICF-java-framework/connector-framework-internal/src/test/resources/KeyStore-san.jks differ
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/resources/invalid-host-san.pem b/OpenICF-java-framework/connector-framework-internal/src/test/resources/invalid-host-san.pem
new file mode 100644
index 000000000..a9b5ab015
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/resources/invalid-host-san.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDQDCCAiigAwIBAgIUSJ5Q3UpbICey7bdRFNkF0sPDxeAwDQYJKoZIhvcNAQEL
+BQAwFzEVMBMGA1UECgwMT3BlbklDRiB0ZXN0MB4XDTI2MDkzMDA5MjIzMFoXDTM2
+MDkyNzA5MjIzMFowFzEVMBMGA1UECgwMT3BlbklDRiB0ZXN0MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAulLfyH7eA6VleuccfjFtv3uSqaCAEh2Vw0ZJ
+pAtVyWsV682FwV52sueiJviIl2W31AQrM1NRIScb9+31Zg1At+bia5FwtnxpFNC/
+PxCb6FLrB4rpcMuPV+nEE8Kw93Le9JU3+p0zkx0Lb3wcKuHIOjKd+9EYtiS4tq9+
+z7IzfOpOdjCPJD+yGhPUKm4oOMd79vzTH4g/cUfPBAgYEzlPBDCKH9sh+U0uwtkl
+luJsUcHn71JIehL+f0JB+MfvwoeHBIqVE7imQC7x9Db2i46ywGQQYxDUWvN0h/1f
+jrGlumc76Dmcxwo25IaEDHgEGrXKUy2hhX4WRAl8kj55cB6UnQIDAQABo4GDMIGA
+MB0GA1UdDgQWBBS7EGoeqbFNAgTaVcNcUT6hNY+u6DAfBgNVHSMEGDAWgBS7EGoe
+qbFNAgTaVcNcUT6hNY+u6DAPBgNVHRMBAf8EBTADAQH/MC0GA1UdEQQmMCSCD215
+X2hvc3QuZXhhbXBsZYIRLWJhZC0uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQAD
+ggEBAKALqNcrve91PZHNekTwQRaEPuzdQXbkV1mIwa5bgjOXIrb+RWmexU3Swvmc
+DuN3n1yfNwlD7RkmIGqWg+juW2stqki/g0lEubOOyPouK1kg45C3Dd0JAly1Zkvy
+STL6Wqc2MuXAtdPWP6Y4miJaScwKXVOk283LWs1W293ZCKjl90Jz2OgTvGf/sjQb
+L20V9s1o6tAtk/icLboGaCEEaWwDlDuF7mRuZgbpZSGWvCBeR4qTN/L3Co0dLoVJ
+NzMbFWuQL2PMivTzYFb3FNONN1u5f2Fury8kz55Qh6PaaTYRzN9vNJLkN/eW2B6D
+Rkhs3CLMc5flsx5i0RIhCKZJgN8=
+-----END CERTIFICATE-----
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/resources/multi-cn.pem b/OpenICF-java-framework/connector-framework-internal/src/test/resources/multi-cn.pem
new file mode 100644
index 000000000..2a077bcf6
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/resources/multi-cn.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDWTCCAkGgAwIBAgIUGCZSTwgAsTuWuXKko/e2R+1cLjowDQYJKoZIhvcNAQEL
+BQAwPDEWMBQGA1UEAwwNd3JvbmcuZXhhbXBsZTEKMAgGA1UECwwBeDEWMBQGA1UE
+AwwNcmlnaHQuZXhhbXBsZTAeFw0yNjA5MjkxNjE5MTZaFw0zNjA5MjYxNjE5MTZa
+MDwxFjAUBgNVBAMMDXdyb25nLmV4YW1wbGUxCjAIBgNVBAsMAXgxFjAUBgNVBAMM
+DXJpZ2h0LmV4YW1wbGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCy
+vswae14YvCsk/jXStk63u0MHCUPYV6xA4NNei65SSei4Bptpbpo2EhKzfOtpIarD
+dWQcxa304xuitNZKMN1TrToeoq+6oSIeiDAy+GHTrS7B42/rF+3e9NSjZpbFIHBY
+OdPpLfNo1qKOTJPfe4A8pgzeO3/aj2JY9dwnDWx6spCeZvEMDaJiHAYasY/AeO/7
+b9hylV5kqEk+kbM4D9S8PoA4xnbBKZdHDDD7KgFWJOlyLGb+SF7s2m6JiM9jGgky
+kqqOREN8Nop9ZG75jreY6gEUyDy2kVN51pldntoBgdaGDpOuw2BefuCCOV+mklGY
+FHlS1E3Fc1iBN4+kcT71AgMBAAGjUzBRMB0GA1UdDgQWBBR3paXcd+WVQDM+jWYx
+xiCb60OAUjAfBgNVHSMEGDAWgBR3paXcd+WVQDM+jWYxxiCb60OAUjAPBgNVHRMB
+Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQB4k/rCTudn80UlEixe/DCEKPZD
+AFiGXvYPiMfMTmVSFuXUmHCd/prgMbCVyYAz4Nq0nWNY96TYOVpw1kHW8gDbHimm
+o4UDVWZnADyIAdnqx2ncKy5cx9/Xr8rauHy0IF7YcRUzk6GAbWdZY7njpjifi0Np
+RegM1VKgwxzdmYglcaaeK4kZLb6hJtHtEfxvt4XLASHowwPVVABFl+bYtNxe7VCq
+VAE84BYRrNkpH5jDMV3QqE2/CEsKvkpQY6tuSqVzJ4YNftF+G8CANvuAHsw41D10
+n66ayDkRUrf089pW7YYCHYZ1HhJqjEULG7PysXAopXzViZ3TQpFAL6UEPVXs
+-----END CERTIFICATE-----
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/resources/no-names.pem b/OpenICF-java-framework/connector-framework-internal/src/test/resources/no-names.pem
new file mode 100644
index 000000000..1e46254ea
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/resources/no-names.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDDzCCAfegAwIBAgIUJ1YUsmEh5B4YVwSLKc1CEUgMhnwwDQYJKoZIhvcNAQEL
+BQAwFzEVMBMGA1UECgwMT3BlbklDRiB0ZXN0MB4XDTI2MDkyOTE2MTkyMFoXDTM2
+MDkyNjE2MTkyMFowFzEVMBMGA1UECgwMT3BlbklDRiB0ZXN0MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEAobDNjy63J8dVWy04APSKjSXOAW6mfc3fJ7N5
+/aNLTpnstq41Ng4YaSDvHPikF0cBy2XUwhF8jf4s/WpSOhy1JkNzaiiGKNZM58LA
+WkAFc3vWM5uWmw25JgSp5W6jLaGD/qHXWQe8XidrAikqVqfK4UChYuwQklaW85mA
+RXpuZKr+5DubCVwY9yzcSEqbDyzQKvPoW38fHIoT8qXt5hhvxR8xn01gEg131q4g
+5ssu/D60ZM2Q41Mk5yVvMvlHiaY6+zjO+ibgYMScRev+bbGc6+r2VeGS58GlqRNK
+VX4Aq5pslOYDMkbGnp7u9vpoK91X2tkeL6J0xjnzQAjnLpAGxQIDAQABo1MwUTAd
+BgNVHQ4EFgQUy0jmqf1IA+r6XncaA8wUQZoNDlswHwYDVR0jBBgwFoAUy0jmqf1I
+A+r6XncaA8wUQZoNDlswDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC
+AQEARISPRJtJ4V80aGeT+A2bt3r2vletd+XyQ/ajaXQM9OpWiFgOcf8eJFHM2y0G
+XguYWsZWVfm6vmE0G+KYukAdR8OcwhILhUgXMQUWs+ffipdzkoqsX1Fpz0ySsL+E
+DlHyX9UbTbIbaIUblhgFkIq2V/FDE+tb2gKQ/EECUQAPx3lUrcddTKIYbqmvEM7v
+1bu5pHiBRkncgyhhrA4wfVrYS7/xMJhiPWFoCYklE57/vL+dfwc2enEifUMWRqnj
+lwYf3JhJHTeLCHavGno5OR6tx5QAdRQLQA0IDhvU1TSJM2Z2/GzyZpeTQ5SN+g7P
+iXLPAW7NwPOuzhLf1KjLKx9DAw==
+-----END CERTIFICATE-----
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/resources/private-wildcard-san.pem b/OpenICF-java-framework/connector-framework-internal/src/test/resources/private-wildcard-san.pem
new file mode 100644
index 000000000..552b7923c
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/resources/private-wildcard-san.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDPTCCAiWgAwIBAgIUGnBzUa31EE9tgsO77U7NLd2HAyUwDQYJKoZIhvcNAQEL
+BQAwFzEVMBMGA1UECgwMT3BlbklDRiB0ZXN0MB4XDTI2MDkyOTE2MTk1OFoXDTM2
+MDkyNjE2MTk1OVowFzEVMBMGA1UECgwMT3BlbklDRiB0ZXN0MIIBIjANBgkqhkiG
+9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6CrkeqUW98xE6yHU+thzoyy7vV11bikHwy1f
+9PCaE6eFe+QXu7NeLF65KD18jv+dYKKfwl4yW0bMuFP0ujPXFSwsgJVD4HT0AGG+
+kw9ygUW+N12hL80lWCVBuLlgELZoZfIpyen9KD/iG+dTnLlEs3Ii/M6ap8Stbrdf
+AKeOqyzwwnkh2orj4Yf37t2kjOjqct5Tf2w69iwRiOwl2gON3HiNnQ7Gl22HQSQL
+FTlwR0I/yVPoujD7UA88DSBueEhthg/FtF3okeHSM7wePdzCcD5w7tWi1Huujref
+/N2vc9NAMc3pqB5Y91qYrvvx9DGHyJ3Obd1IHYIJfNZqpSpKvQIDAQABo4GAMH4w
+HQYDVR0OBBYEFHBj83oYEuOLX7eRbQtqQFhNV5zMMB8GA1UdIwQYMBaAFHBj83oY
+EuOLX7eRbQtqQFhNV5zMMA8GA1UdEwEB/wQFMAMBAf8wKwYDVR0RBCQwIoIFKi5j
+b22CDncqLmV4YW1wbGUub3JnggkzMDAuMS4xLjEwDQYJKoZIhvcNAQELBQADggEB
+AB6xBIZBJdB8RRHki57OhmRYzTq+DUxf40Tw1B51QLEso6Oa/GL/xS9T0YHJDr4j
+Q9U6MSRt1J51jcaDyctdPDd96ud8FNtPK3h5HuSMqESNeSNr8u10sVGVovuQT3UU
+TlJr9Cu7pEIX2RlBsuEn6H+WXPB0cTYtfUEJ9HAliH/KhxxFphA9jYN5KhgHK2U7
+9zrUPlpfZDCtUZpCgN75ucnAnxUZmcrf5YsYOuiljl12nVnmSY6aaWp8ezMVZm+o
+EwAyXGavCDXpRm390scCFs/5hKbmvXIK62vxWQDUopBco4BwHfrKREy1HeAEYgf6
++kaXe5gLjpSJSyomM7908KE=
+-----END CERTIFICATE-----
diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/resources/wildcard-san.pem b/OpenICF-java-framework/connector-framework-internal/src/test/resources/wildcard-san.pem
new file mode 100644
index 000000000..094c5648b
--- /dev/null
+++ b/OpenICF-java-framework/connector-framework-internal/src/test/resources/wildcard-san.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDLTCCAhWgAwIBAgIJAJiu6UyYAXyyMA0GCSqGSIb3DQEBCwUAMDAxFTATBgNV
+BAoTDE9wZW5JQ0YgdGVzdDEXMBUGA1UEAxMOY24uZXhhbXBsZS5vcmcwIBcNMjYw
+OTE4MDkyNzEyWhgPMjA1NjA5MTAwOTI3MTJaMDAxFTATBgNVBAoTDE9wZW5JQ0Yg
+dGVzdDEXMBUGA1UEAxMOY24uZXhhbXBsZS5vcmcwggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDZMsfEKEiuvkf6d3tOmn6ndoMbAF/DGb3bnsvZ9yfgMPqq
+chKHjZigLJwcSCYH84fMTbOJfOVIB+lynIiDcewSyYUzduUr8FYh0Nu2nnQqOFjc
+o9UFZGdGWI5ou9XdSRRlfTzb/JXUiOmHPwQFSHoOWtJ5bQEyXcq4VaQKmHRqlrVN
+UeoiEsoTxh9Hp/x0P/u1vlNOdP1YJioeF5qvSodpmwiuxOXNQsUe2mNcdl5c8pia
+omAm7eznbbst72XF6P7zTt3MtegA8mcf4aCrTaGm8pNmcz/jxGufFqHqlEzxVarb
+dkehZULH2pYMl7YS8NFmL9o0uYyTcI9RrplvR2jfAgMBAAGjSDBGMB0GA1UdDgQW
+BBSMT3EPXPhvdQ0zQkyfu6D5S1Oj9DAlBgNVHREEHjAcgg0qLmV4YW1wbGUuY29t
+ggtleGFtcGxlLm5ldDANBgkqhkiG9w0BAQsFAAOCAQEAZrRnTuCgh/bXQv8dv64t
+fEU5xw9Mts39xi/+8/gC96njrK/ydeZGHQC7bSSgv2c7An7kXw5iESk3vLkg2wnX
+SGGeYyjTDU6kQNo9VI2qSFdIPmgrzwu1Zk+M33PAZMk3X5XEtUaWEi1xBCQo9+K3
+01g9tRWElBpRgaI2WSUu31l1IBkIi7FymJi5KJdaVM0eCTRpFrf8wbuL+7H+bipY
+N+/P5myrYBH1BDBWBco44GuEZe2O8vPngWf+MnLVVJkFHUO5MZueLL5e8Ju//DuB
+7WsdR1yk4pk9VeHUQuWJxyug8JhusWQFzr0Z9UDeEVCe83p2myECCwokPVpmZ0bW
+MA==
+-----END CERTIFICATE-----
diff --git a/OpenICF-java-framework/connector-framework/src/main/java/org/identityconnectors/framework/api/RemoteFrameworkConnectionInfo.java b/OpenICF-java-framework/connector-framework/src/main/java/org/identityconnectors/framework/api/RemoteFrameworkConnectionInfo.java
index 21750764e..a73b08534 100644
--- a/OpenICF-java-framework/connector-framework/src/main/java/org/identityconnectors/framework/api/RemoteFrameworkConnectionInfo.java
+++ b/OpenICF-java-framework/connector-framework/src/main/java/org/identityconnectors/framework/api/RemoteFrameworkConnectionInfo.java
@@ -20,6 +20,7 @@
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
* Portions Copyrighted 2015 ForgeRock AS.
+ * Portions Copyrighted 2026 3A Systems, LLC
*/
package org.identityconnectors.framework.api;
@@ -69,7 +70,13 @@ public RemoteFrameworkConnectionInfo(String host, int port, GuardedString key) {
* @param key
* The remote framework key
* @param useSSL
- * Set to true if we are to connect via SSL.
+ * Set to true if we are to connect via SSL. The server
+ * certificate is then verified against {@code host}: an IP
+ * address must be a subjectAltName iPAddress entry, a host name
+ * a subjectAltName dNSName entry (or the subject CN when the
+ * certificate has no dNSName entry). Setting the system property
+ * {@code org.identityconnectors.framework.remote.hostnameVerification}
+ * to {@code false} disables this check (not recommended).
* @param trustManagers
* List of {@link TrustManager}'s to use for establising the SSL
* connection. May be null or empty, in which case the default
@@ -102,7 +109,13 @@ public RemoteFrameworkConnectionInfo(String host, int port, GuardedString key, b
* @param key
* The remote framework key
* @param useSSL
- * Set to true if we are to connect via SSL.
+ * Set to true if we are to connect via SSL. The server
+ * certificate is then verified against {@code host}: an IP
+ * address must be a subjectAltName iPAddress entry, a host name
+ * a subjectAltName dNSName entry (or the subject CN when the
+ * certificate has no dNSName entry). Setting the system property
+ * {@code org.identityconnectors.framework.remote.hostnameVerification}
+ * to {@code false} disables this check (not recommended).
* @param trustManagers
* List of {@link TrustManager}'s to use for establising the SSL
* connection. May be null or empty, in which case the default
diff --git a/OpenICF-maven-plugin/src/main/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverter.java b/OpenICF-maven-plugin/src/main/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverter.java
index 26edf428b..f5ab88a5a 100644
--- a/OpenICF-maven-plugin/src/main/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverter.java
+++ b/OpenICF-maven-plugin/src/main/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverter.java
@@ -20,15 +20,19 @@
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
+ *
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.forgerock.openicf.maven;
+import java.net.Socket;
import java.util.Arrays;
import java.util.List;
+import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
+import javax.net.ssl.X509ExtendedTrustManager;
import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
import org.codehaus.plexus.component.configurator.ConfigurationListener;
@@ -131,21 +135,51 @@ public Object fromConfiguration(final ConverterLookup converterLookup,
/**
* Create a trust manager that trusts all certificates It is not using a
- * particular keyStore
+ * particular keyStore.
+ *
+ * It is an {@link X509ExtendedTrustManager} so that JSSE does not wrap it:
+ * neither the "HTTPS" hostname check the framework enables for the
+ * connection nor the {@code jdk.certpath.disabledAlgorithms} check on the
+ * server's certificate chain applies. With every certificate trusted,
+ * neither check adds security; the hostname check would only reject
+ * connector servers whose certificate does not name the configured host.
*/
protected List getTrustManager() {
- return Arrays.asList((TrustManager) new X509TrustManager() {
+ return Arrays.asList((TrustManager) new X509ExtendedTrustManager() {
+ @Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
+ @Override
public void checkClientTrusted(java.security.cert.X509Certificate[] certs,
String authType) {
}
+ @Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
String authType) {
}
+
+ @Override
+ public void checkClientTrusted(java.security.cert.X509Certificate[] certs,
+ String authType, Socket socket) {
+ }
+
+ @Override
+ public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
+ String authType, Socket socket) {
+ }
+
+ @Override
+ public void checkClientTrusted(java.security.cert.X509Certificate[] certs,
+ String authType, SSLEngine engine) {
+ }
+
+ @Override
+ public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
+ String authType, SSLEngine engine) {
+ }
});
}
}
diff --git a/OpenICF-maven-plugin/src/test/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverterTests.java b/OpenICF-maven-plugin/src/test/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverterTests.java
new file mode 100644
index 000000000..823e388af
--- /dev/null
+++ b/OpenICF-maven-plugin/src/test/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverterTests.java
@@ -0,0 +1,111 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.forgerock.openicf.maven;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.security.KeyStore;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLSocket;
+
+import org.identityconnectors.common.security.GuardedString;
+import org.identityconnectors.framework.api.RemoteFrameworkConnectionInfo;
+import org.identityconnectors.framework.impl.api.remote.RemoteFrameworkConnection;
+import org.testng.annotations.Test;
+
+/**
+ * The plugin trusts every connector server certificate, so the framework's
+ * hostname verification must not reject a server whose certificate does not
+ * name the configured host. {@code KeyStore.jks} holds a certificate with
+ * {@code CN=localhost} and no subjectAltName, which JSSE never matches against
+ * {@code 127.0.0.1}.
+ */
+public class RemoteFrameworkConnectionInfoConverterTests {
+
+ private static final char[] PASSWORD = "changeit".toCharArray();
+ private static final int TIMEOUT = 10000;
+
+ @Test
+ public void trustAllConnectionSkipsHostnameVerification() throws Exception {
+ assertNull(System.getProperty(RemoteFrameworkConnection.HOSTNAME_VERIFICATION_PROPERTY),
+ "hostname verification must be on for this test");
+ try (TlsServer server = new TlsServer(loadKeyStore("KeyStore.jks"),
+ InetAddress.getByName("127.0.0.1"))) {
+ new RemoteFrameworkConnection(new RemoteFrameworkConnectionInfo("127.0.0.1",
+ server.getPort(), new GuardedString(PASSWORD), true,
+ new RemoteFrameworkConnectionInfoConverter().getTrustManager(), TIMEOUT))
+ .close();
+ }
+ }
+
+ private static KeyStore loadKeyStore(String name) throws Exception {
+ try (InputStream in =
+ RemoteFrameworkConnectionInfoConverterTests.class.getResourceAsStream("/" + name)) {
+ assertNotNull(in, "missing test resource " + name);
+ KeyStore store = KeyStore.getInstance("JKS");
+ store.load(in, PASSWORD);
+ return store;
+ }
+ }
+
+ /**
+ * Minimal TLS server: completes the handshake for every accepted
+ * connection and then waits for the client to hang up.
+ */
+ private static final class TlsServer implements AutoCloseable {
+ private final SSLServerSocket serverSocket;
+
+ TlsServer(KeyStore store, InetAddress bindAddress) throws Exception {
+ KeyManagerFactory factory =
+ KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ factory.init(store, PASSWORD);
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(factory.getKeyManagers(), null, null);
+ serverSocket = (SSLServerSocket) context.getServerSocketFactory()
+ .createServerSocket(0, 1, bindAddress);
+ Thread acceptor = new Thread(this::serve, "RemoteFrameworkConnectionInfoConverterTests-server");
+ acceptor.setDaemon(true);
+ acceptor.start();
+ }
+
+ int getPort() {
+ return serverSocket.getLocalPort();
+ }
+
+ private void serve() {
+ while (!serverSocket.isClosed()) {
+ try (SSLSocket socket = (SSLSocket) serverSocket.accept()) {
+ socket.setSoTimeout(TIMEOUT);
+ socket.startHandshake();
+ socket.getInputStream().read();
+ } catch (IOException e) {
+ // handshake rejected by the client or server closed: next connection
+ }
+ }
+ }
+
+ public void close() throws IOException {
+ serverSocket.close();
+ }
+ }
+}
diff --git a/OpenICF-maven-plugin/src/test/resources/KeyStore.jks b/OpenICF-maven-plugin/src/test/resources/KeyStore.jks
new file mode 100644
index 000000000..c317f2557
Binary files /dev/null and b/OpenICF-maven-plugin/src/test/resources/KeyStore.jks differ