From ddac85fbd38edd07f94e802fbbd55300fd7ccc93 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 18 Sep 2026 12:36:25 +0300 Subject: [PATCH 1/4] Verify the connector server certificate against the host over the legacy SSL connection RemoteFrameworkConnection wrapped the socket in an SSLSocket, which does not check the server certificate against the host it connects to, so anyone with a certificate the client trusts could sit in the middle and read the connector server key. Enable JSSE "HTTPS" endpoint identification for the handshake (CodeQL java/unsafe-cert-trust, alert #24). The check can be switched off with -Dorg.identityconnectors.framework.remote.hostnameVerification=false; the client then still compares the certificate names with the host and logs a warning naming the certificate's subject and subjectAltName entries, so the certificate can be fixed and the check re-enabled. --- .../remote/CertificateHostnameMatcher.java | 175 ++++++++++++ .../api/remote/RemoteFrameworkConnection.java | 84 +++++- .../RemoteConnectorInfoManagerSSLTests.java | 7 +- .../CertificateHostnameMatcherTests.java | 116 ++++++++ .../RemoteFrameworkConnectionSSLTests.java | 262 ++++++++++++++++++ .../src/test/resources/KeyStore-san.jks | Bin 0 -> 2165 bytes .../src/test/resources/wildcard-san.pem | 20 ++ .../api/RemoteFrameworkConnectionInfo.java | 15 +- 8 files changed, 671 insertions(+), 8 deletions(-) create mode 100644 OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcher.java create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcherTests.java create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnectionSSLTests.java create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/resources/KeyStore-san.jks create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/resources/wildcard-san.pem 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..e51053ae7 --- /dev/null +++ b/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcher.java @@ -0,0 +1,175 @@ +/* + * 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.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.security.auth.x500.X500Principal; + +/** + * Matches a host name or IP address against the names in an X.509 server + * certificate the way RFC 6125 / RFC 2818 (and JSSE's "HTTPS" endpoint + * identification) do: 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. + *

+ * This is a diagnostic aid for deployments that switched hostname + * verification off; 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("(\\d{1,3}\\.){3}\\d{1,3}"); + + 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) { + if (isIpLiteral(host)) { + for (String address : subjectAltNames(certificate, SAN_IP_ADDRESS)) { + if (sameAddress(host, address)) { + return true; + } + } + 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; + } + + /** + * 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(); + } + + 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; a {@code *} in the leftmost label of the + * certificate name stands for exactly one label of the host. + */ + private static boolean matchesDnsName(String host, String name) { + String lowerHost = host.toLowerCase(Locale.ENGLISH); + String lowerName = name.toLowerCase(Locale.ENGLISH); + if (!lowerName.startsWith("*.")) { + return lowerHost.equals(lowerName); + } + String suffix = lowerName.substring(1); + if (suffix.indexOf('.', 1) < 0) { + // "*.com": a wildcard must not cover a whole top-level domain + return false; + } + int firstDot = lowerHost.indexOf('.'); + return firstDot > 0 && lowerHost.substring(firstDot).equals(suffix); + } + + 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..3f723b4e2 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. */ + private 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,48 @@ 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".equalsIgnoreCase(System.getProperty(HOSTNAME_VERIFICATION_PROPERTY)); + } + + /** + * With verification switched off, still tell the administrator (once per + * server) when the certificate would not have passed, so the certificate + * 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; + } + String problem; + try { + Certificate[] chain = sslSocket.getSession().getPeerCertificates(); + if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) { + problem = "presented no X.509 certificate"; + } else if (CertificateHostnameMatcher.matches(connectionInfo.getHost(), + (X509Certificate) chain[0])) { + return; + } else { + problem = "presented a certificate that does not match the host: " + + CertificateHostnameMatcher.describe((X509Certificate) chain[0]); + } + } catch (SSLPeerUnverifiedException e) { + problem = "presented no verifiable certificate"; + } + if (REPORTED_MISMATCHES.add(server)) { + LOG.warn("TLS hostname verification is disabled ({0}=false) and connector server {1} {2}." + + " The connection is exposed to man-in-the-middle attacks;" + + " fix the server certificate and re-enable verification.", + HOSTNAME_VERIFICATION_PROPERTY, server, problem); + } + } + 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/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..40a1725c3 --- /dev/null +++ b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcherTests.java @@ -0,0 +1,116 @@ +/* + * 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: + *

+ */ +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 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..bd7f70fc9 --- /dev/null +++ b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/RemoteFrameworkConnectionSSLTests.java @@ -0,0 +1,262 @@ +/* + * 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.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetAddress; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +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.security.GuardedString; +import org.identityconnectors.framework.api.RemoteFrameworkConnectionInfo; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +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"))) { + withHostnameVerificationProperty("off", () -> { + try { + connect("127.0.0.1", server.getPort(), trustManagers(store)).close(); + fail("A value other than 'false' must not disable hostname verification"); + } catch (ConnectorException e) { + assertHandshakeFailure(e); + } + }); + } + } + + // ---- helpers --------------------------------------------------------- + + private interface Action { + void run() throws Exception; + } + + private static void withHostnameVerificationProperty(String value, Action action) + throws Exception { + String property = RemoteFrameworkConnection.HOSTNAME_VERIFICATION_PROPERTY; + String previous = System.getProperty(property); + System.setProperty(property, value); + try { + action.run(); + } finally { + if (previous == null) { + System.clearProperty(property); + } else { + System.setProperty(property, previous); + } + } + } + + 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 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 0000000000000000000000000000000000000000..04d6902c2a9625dd3726c4d9a56291279a7b09b6 GIT binary patch literal 2165 zcmb7FXHb)i63v$c5_&?wh@pj|p@&F^OVLmw9Rz7nR6sNoDT)Gu1PDDsK&eWNg^?zx zAYAZ*a6!ryq7>;O0)kZO0Uq9YGta+oXZFw8ncbap&g}00*#7|nfxw3e{F`v!+aCVD z7q9vq0tMB5-wQw>=sunb-NEy6aw~8GP(U7x1OO-%Iy(q;$AykHes0OwH)t+W5fLLr z9v>$Q{=+WXxDcx`FT!vIx+ z%1l2WH(wilc=qvt%p|pq!O(26PMChnZ^q)7f7cL(vD~YqnzD9@WM2R8b?%}yd9RVA za*FPA%7E{@>%vPBJ-Ta|CvK0ob&(|M3;%(59ac{5$I0~UdflfZU ztn5d{b$(JI<#uPjV`huw+%XL1Y7S<@*}cDeFAMPww(_n^tJBxOpeZk8a{Q(<4SmY` za|^f}Lt3q*RI1fsPT$0h_=`t(ov}FeIe-T@`#d0xMiY3$xNII zcRTQq8MyoWfyS*lWC&T2ckXPSuY@&M4S(ZQp4-DqMyNGKicYw|jN#p|lQQK1R`>q1 ziuIxn3KJcX+I6pcv9`O@%36=-hS|lKX!7nQqw`0Y{z?j_;p}4@qX$jhp%XQ?pGl!A zN7B#Csc5B&ER7KY;9IQx{S=`>x1x5MX{$unS#uKNm0p7TI`?(%S_PwuUt|6lCXls zMgoWV2}H7#t=azRH8(*xaU- z4E*hk?wAzT5i@a2|6tYdqvAb!y{4ey-irHBNo2cQNdoSw7C*&VavIq*=(7bcDdG}z zF?E`lkcs3WZ&Q=EqbbAAu4nI@(D&lQF!GBJB-FB=m3uOfrBAU-yNPyX77j=-bVSzp zgaSG3LU+fvoxp-)UK$|_@P{F}D?~5&t)$L$qO1b`otF?AZ0mx+RntmmchvOtju;6} z_a^G*8gSmH6&B#N_c>DyVJ%ElI|H3yON#m`IT;X3q_Jn&D64$fMuUxE9`_< zGYk#X<@A>???c}D1TeEF<9^ngd(jao$WitrTP6Qol@0Qn%w^{IV)!-$N|YkJq{ z&aH=$^=6EE54dC68V@zIP2FaC=UZKbPQ__}WUf zrL`y;BC#>}6b$laX3|<58Rt2Pij+*ZR7rAL+D- zWUyyYu&N*)@e78axP1ICU-a?z4+_RhqWK6Kcx_cJyf%TLs_k-AfPh#31>pY+{D1AJ zfYk5n0309)6%YX7sQ@>a3IL#76?CBXxcc4j2~h%KnX;-_kw^M*zCv!PzlJ8a&a?nu zs+bSYuB52wGW(?BLXxFFi};saiu(gb)VQfFCm^EtwlAQMb=0)o#^TY7{4}Zb`R)+p zj9pKpF^m*yI3=8NL2uRPJxZynM#srmxD91yR}wwRu)m_H4h)DHJa+0s^DeM}msz{5 zHB3$MYyPs*z?4l?<+3M4J4pkpWii}7BDr~mj1q8JH!}T`Fy_=NAFo_?lGRw2FtDOl zpRW{E%uy{g?*rO|CCO>QRt&jH*(^UNlJ7=!g80K2NBXWw zPbaD-rBeZyPkcEy3Wfk6px7F3i5EMRLX01J1S+&g2t1O zzsg9cEMA)WKeghZk%!+g4smFV-vjua@iA}DRkxYL?R)a;axmA9SdejTk>ws}_^ODBf?n^(f zL9+<<9$SH-!R6&F(fvl>4`fOABtk<+lqwq=2hFn46{~Pv``f2IB9tD(O)}z~eBO2R z#C_@?j-h)Jsditv7t4})lR8%dCUgkPuI`n4(&JlNMLAxj2b#)Fu_&qGYhp3pFF_vO z)Eny`i_LH220faXE{eoaPc{4U`hcI#N&+=-rI>X(NG;u}eJY~%V-%K;mOogC_@Fr; zPD0#_w`w6hZOWdr@t- Date: Tue, 29 Sep 2026 19:38:06 +0300 Subject: [PATCH 2/4] Address review: keep the maven plugin working, align the diagnostic with JSSE - openicf-maven-plugin: the trust-all trust manager of RemoteFrameworkConnectionInfoConverter is now an X509ExtendedTrustManager. JSSE does not wrap it, so it does not add the "HTTPS" hostname check, which gives no security when every certificate is trusted and would only reject connector servers whose certificate does not name the configured host. - CertificateHostnameMatcher follows JSSE's wildcard rules for private CAs (partial leftmost wildcards such as w*.example.com, wildcards over a single-label suffix such as *.local), compares IDN-normalized names and accepts only valid IPv4 literals, so 300.1.1.1 is no longer resolved. The WARN now says the certificate "may not match" the host. - Only the exact value "false" disables hostname verification, as documented. - useSSL javadoc: the subject CN is never used for an IP address. - Tests: the mismatch WARN, "FALSE" keeping verification on, the default trust store (javax.net.ssl.trustStore) path, multiple CNs, a certificate without names, private-CA wildcards, and the plugin connecting by IP to a CN-only certificate. --- .../remote/CertificateHostnameMatcher.java | 65 +++++++--- .../api/remote/RemoteFrameworkConnection.java | 6 +- .../CertificateHostnameMatcherTests.java | 43 ++++++- .../RemoteFrameworkConnectionSSLTests.java | 80 +++++++++++-- .../src/test/resources/multi-cn.pem | 20 ++++ .../src/test/resources/no-names.pem | 19 +++ .../test/resources/private-wildcard-san.pem | 20 ++++ .../api/RemoteFrameworkConnectionInfo.java | 14 ++- ...emoteFrameworkConnectionInfoConverter.java | 32 ++++- ...FrameworkConnectionInfoConverterTests.java | 111 ++++++++++++++++++ .../src/test/resources/KeyStore.jks | Bin 0 -> 2257 bytes 11 files changed, 370 insertions(+), 40 deletions(-) create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/resources/multi-cn.pem create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/resources/no-names.pem create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/resources/private-wildcard-san.pem create mode 100644 OpenICF-maven-plugin/src/test/java/org/forgerock/openicf/maven/RemoteFrameworkConnectionInfoConverterTests.java create mode 100644 OpenICF-maven-plugin/src/test/resources/KeyStore.jks 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 index e51053ae7..75b8da531 100644 --- 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 @@ -15,6 +15,7 @@ */ package org.identityconnectors.framework.impl.api.remote; +import java.net.IDN; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.cert.CertificateParsingException; @@ -32,19 +33,23 @@ /** * Matches a host name or IP address against the names in an X.509 server - * certificate the way RFC 6125 / RFC 2818 (and JSSE's "HTTPS" endpoint - * identification) do: IP addresses against subjectAltName iPAddress entries, + * 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. + * subject CN only when the certificate carries no dNSName at all. 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; the enforcing check is done by JSSE during the handshake. + * 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("(\\d{1,3}\\.){3}\\d{1,3}"); + 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() { } @@ -117,22 +122,52 @@ private static boolean sameAddress(String host, String address) { } /** - * Case-insensitive comparison; a {@code *} in the leftmost label of the - * certificate name stands for exactly one label of the host. + * 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 = host.toLowerCase(Locale.ENGLISH); - String lowerName = name.toLowerCase(Locale.ENGLISH); - if (!lowerName.startsWith("*.")) { + String lowerHost = normalize(host); + String lowerName = normalize(name); + int lastWildcard = lowerName.lastIndexOf('*'); + if (lastWildcard < 0) { return lowerHost.equals(lowerName); } - String suffix = lowerName.substring(1); - if (suffix.indexOf('.', 1) < 0) { - // "*.com": a wildcard must not cover a whole top-level domain + // "*", "*." 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; } - int firstDot = lowerHost.indexOf('.'); - return firstDot > 0 && lowerHost.substring(firstDot).equals(suffix); + 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) { 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 3f723b4e2..020726c2e 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 @@ -66,7 +66,7 @@ public class RemoteFrameworkConnection implements Closeable { "org.identityconnectors.framework.remote.hostnameVerification"; /** Servers already reported as mismatching while verification is off. */ - private static final Set REPORTED_MISMATCHES = ConcurrentHashMap.newKeySet(); + static final Set REPORTED_MISMATCHES = ConcurrentHashMap.newKeySet(); private Socket socket; private BinaryObjectSerializer encoder; @@ -152,7 +152,7 @@ private void init(RemoteFrameworkConnectionInfo connectionInfo) throws Exception * the property value can not silently weaken the connection. */ static boolean isHostnameVerificationEnabled() { - return !"false".equalsIgnoreCase(System.getProperty(HOSTNAME_VERIFICATION_PROPERTY)); + return !"false".equals(System.getProperty(HOSTNAME_VERIFICATION_PROPERTY)); } /** @@ -175,7 +175,7 @@ private static void reportCertificateMismatch(RemoteFrameworkConnectionInfo conn (X509Certificate) chain[0])) { return; } else { - problem = "presented a certificate that does not match the host: " + problem = "presented a certificate that may not match the host: " + CertificateHostnameMatcher.describe((X509Certificate) chain[0]); } } catch (SSLPeerUnverifiedException e) { 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 index 40a1725c3..77d23e2f7 100644 --- 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 @@ -33,8 +33,16 @@ *

  • {@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 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.
  • * + * 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 { @@ -86,6 +94,39 @@ public void matchesWildcardForExactlyOneLeftmostLabel() throws Exception { 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 describeListsSubjectAndSubjectAltNames() throws Exception { String description = CertificateHostnameMatcher.describe(keyStoreCertificate("KeyStore-san.jks")); 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 index bd7f70fc9..ddeb9194a 100644 --- 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 @@ -15,10 +15,12 @@ */ 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 static org.testng.Assert.fail; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; @@ -114,15 +116,60 @@ public void connectsWhenHostnameVerificationIsDisabled() throws Exception { public void keepsVerificationUnlessPropertyIsExactlyFalse() throws Exception { KeyStore store = loadKeyStore("KeyStore.jks"); try (TlsServer server = new TlsServer(store, InetAddress.getByName("127.0.0.1"))) { - withHostnameVerificationProperty("off", () -> { - try { - connect("127.0.0.1", server.getPort(), trustManagers(store)).close(); - fail("A value other than 'false' must not disable hostname verification"); + 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 reportsMismatchOnlyForNonMatchingCertificateWhenVerificationIsDisabled() + throws Exception { + KeyStore cnOnly = loadKeyStore("KeyStore.jks"); + try (TlsServer server = new TlsServer(cnOnly, InetAddress.getByName("127.0.0.1"))) { + withHostnameVerificationProperty("false", () -> + connect("127.0.0.1", server.getPort(), trustManagers(cnOnly)).close()); + assertTrue(RemoteFrameworkConnection.REPORTED_MISMATCHES + .contains("127.0.0.1:" + server.getPort())); + } + KeyStore san = loadKeyStore("KeyStore-san.jks"); + try (TlsServer server = new TlsServer(san, InetAddress.getByName("127.0.0.1"))) { + withHostnameVerificationProperty("false", () -> + connect("127.0.0.1", server.getPort(), trustManagers(san)).close()); + assertFalse(RemoteFrameworkConnection.REPORTED_MISMATCHES + .contains("127.0.0.1:" + server.getPort())); + } + } + + /** + * 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 --------------------------------------------------------- @@ -133,16 +180,25 @@ private interface Action { private static void withHostnameVerificationProperty(String value, Action action) throws Exception { - String property = RemoteFrameworkConnection.HOSTNAME_VERIFICATION_PROPERTY; - String previous = System.getProperty(property); - System.setProperty(property, value); + 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 { - if (previous == null) { - System.clearProperty(property); - } else { - System.setProperty(property, previous); + 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]); + } } } } 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/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 a0f365911..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 @@ -71,9 +71,10 @@ public RemoteFrameworkConnectionInfo(String host, int port, GuardedString key) { * The remote framework key * @param useSSL * Set to true if we are to connect via SSL. The server - * certificate is then verified against {@code host}: it must - * list it as a subjectAltName dNSName or iPAddress entry (or as - * CN when it has no subjectAltName). Setting the system property + * 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 @@ -109,9 +110,10 @@ public RemoteFrameworkConnectionInfo(String host, int port, GuardedString key, b * The remote framework key * @param useSSL * Set to true if we are to connect via SSL. The server - * certificate is then verified against {@code host}: it must - * list it as a subjectAltName dNSName or iPAddress entry (or as - * CN when it has no subjectAltName). Setting the system property + * 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 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..ecf79a5f7 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,10 +135,16 @@ 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 + * and add the "HTTPS" hostname check the framework enables for the + * connection: with every certificate trusted, that check adds no security + * and 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() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } @@ -146,6 +156,22 @@ public void checkClientTrusted(java.security.cert.X509Certificate[] certs, public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) { } + + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, + String authType, Socket socket) { + } + + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, + String authType, Socket socket) { + } + + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, + String authType, SSLEngine engine) { + } + + 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 0000000000000000000000000000000000000000..c317f2557f347b55ca72e9c74775baaef31b38de GIT binary patch literal 2257 zcmc(gc{J3G8pnUX88aAU-=;CLghrz=hOBR)ERkJh9b-v~8C!`gHP%qPDp`*3+Q!J9 zmokxyA#11!k*#cn$TEno`=0ln+d23CfB$&?_1YrONm1BVc1AkvB&*W`Ehr1#^ zV$ZyJKlaTCM^F#%6$db*IvMpHdOA7KE%^z}^U4aLx$6do6cEj8m@F=QIAcMwb>eq#2}akJ1$r= znk6#NtE9<=EYJZ@xoK!vx`b{7Yv*Z zx#Zzh6K?b_q;}GPKa;*aGU?e8wnFXLKbMoX*cTNW&WgXdYA)Sf@|HeR2PS)0>F~4a z6_IG-`Sl)ClQd$HSBa$SfXDsQOU`~mk=#Sq)ubryyV2>}GM31Udw63@%wyEq!U@dRp(leprYI)Z2>D`}M$)~gA%%Hj#v%IAb zThn#thlXm8JjoX4L%yh|M=K<)Y550+<67q~kIc$?BQ?uLi!w&*v4#fyjgcu)dZ2r| zyr1fFf=TOW7L0fmJ*rGX$vrJj$UFd(0*+C3f^6c;?x=F(3C7p=K-sOZRKi}5q7;i`~^Bp z|9}`nt9n9To!^(p*l=oI=)}A!xAM8!1tP3vLuW(n0)2LN@@aLOtDUr7*>npzeoQ3I zQV>kU4u-tjETRH$6S=;x~1={q>G7HZl7{ok;XXpI&fx5mN4gR=c9yNMk}-Q5nc zw5ac5KfNrmWkf_;@x$ZR#LA2!^4XhhTN!b9C4)QL_xN5yTTjd#3Za06)%?~ z&X|J?0`qmsn)CX|(~hi$q7V;ZeCO0}5kA|J@}oE0myM)xR0f|6tHx7!EDYr>cR&<5cl@byamOM{z!a z#(^OGE&newQ9;>XtvPrcP%6j|;HV%1LIpvfV@2t-q{&)k?CwAW<(A5Hx!F2H6m-Ckgk7&CVYB|=z>59vy6TN3i6Z#z#!e0~th n9au8e=!n;o*042fF5-L|i|r#Nn_(X6@w@n-W~`16TEF-w@Albo literal 0 HcmV?d00001 From d35f8e4938093814452e7f2338415d4b9993736c Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Tue, 29 Sep 2026 20:27:29 +0300 Subject: [PATCH 3/4] Address CodeQL: add @Override to the trust-all X509ExtendedTrustManager methods --- .../maven/RemoteFrameworkConnectionInfoConverter.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 ecf79a5f7..03bb3e9b3 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 @@ -145,30 +145,37 @@ public Object fromConfiguration(final ConverterLookup converterLookup, */ protected List getTrustManager() { 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) { } From 1100178092c17e71160ab13ee6b5e83eaafa194f Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Wed, 30 Sep 2026 12:50:25 +0300 Subject: [PATCH 4/4] Address review: flag hosts JSSE cannot verify, pin the mismatch WARN - CertificateHostnameMatcher: a host that is not a valid DNS name (underscore, empty label, leading/trailing hyphen) matches nothing, as JSSE rejects it before looking at the certificate; one trailing dot is ignored, as JSSE does. The WARN tells to configure such a server by a valid name or by IP (RemoteFrameworkConnection.describeMismatch). - Plugin trust manager javadoc: the disabledAlgorithms check is skipped too. - Tests: CapturingLogSpi (selected by surefire) pins exactly one WARN per server; the test clears its state and binds both servers at once. --- .../connector-framework-internal/pom.xml | 10 ++ .../remote/CertificateHostnameMatcher.java | 35 +++++- .../api/remote/RemoteFrameworkConnection.java | 56 ++++++---- .../common/logging/CapturingLogSpi.java | 75 +++++++++++++ .../CertificateHostnameMatcherTests.java | 32 +++++- .../RemoteFrameworkConnectionSSLTests.java | 100 ++++++++++++++++-- .../src/test/resources/invalid-host-san.pem | 20 ++++ ...emoteFrameworkConnectionInfoConverter.java | 11 +- 8 files changed, 302 insertions(+), 37 deletions(-) create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/logging/CapturingLogSpi.java create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/resources/invalid-host-san.pem diff --git a/OpenICF-java-framework/connector-framework-internal/pom.xml b/OpenICF-java-framework/connector-framework-internal/pom.xml index 8e17cdc97..7b3c88bd5 100644 --- a/OpenICF-java-framework/connector-framework-internal/pom.xml +++ b/OpenICF-java-framework/connector-framework-internal/pom.xml @@ -140,6 +140,16 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.identityconnectors.common.logging.CapturingLogSpi + + + maven-dependency-plugin 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 index 75b8da531..3653b93a1 100644 --- 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 @@ -29,6 +29,7 @@ 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; /** @@ -36,7 +37,9 @@ * 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. Wildcards + * 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. *

    @@ -59,6 +62,7 @@ private CertificateHostnameMatcher() { * 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)) { @@ -67,6 +71,9 @@ static boolean matches(String host, X509Certificate certificate) { } return false; } + if (!isVerifiableHost(host)) { + return false; + } List dnsNames = subjectAltNames(certificate, SAN_DNS_NAME); if (dnsNames.isEmpty()) { String commonName = commonName(certificate); @@ -80,6 +87,27 @@ static boolean matches(String host, X509Certificate certificate) { 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. @@ -104,6 +132,11 @@ static String describe(X509Certificate certificate) { 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(); } 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 020726c2e..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 @@ -157,8 +157,8 @@ static boolean isHostnameVerificationEnabled() { /** * With verification switched off, still tell the administrator (once per - * server) when the certificate would not have passed, so the certificate - * can be fixed and verification re-enabled. + * 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) { @@ -166,27 +166,45 @@ private static void reportCertificateMismatch(RemoteFrameworkConnectionInfo conn if (REPORTED_MISMATCHES.contains(server)) { return; } - String problem; + Certificate[] chain; try { - Certificate[] chain = sslSocket.getSession().getPeerCertificates(); - if (chain.length == 0 || !(chain[0] instanceof X509Certificate)) { - problem = "presented no X.509 certificate"; - } else if (CertificateHostnameMatcher.matches(connectionInfo.getHost(), - (X509Certificate) chain[0])) { - return; - } else { - problem = "presented a certificate that may not match the host: " - + CertificateHostnameMatcher.describe((X509Certificate) chain[0]); - } + chain = sslSocket.getSession().getPeerCertificates(); } catch (SSLPeerUnverifiedException e) { - problem = "presented no verifiable certificate"; + 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); } - if (REPORTED_MISMATCHES.add(server)) { - LOG.warn("TLS hostname verification is disabled ({0}=false) and connector server {1} {2}." - + " The connection is exposed to man-in-the-middle attacks;" - + " fix the server certificate and re-enable verification.", - HOSTNAME_VERIFICATION_PROPERTY, server, problem); + } + + /** + * 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 { 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/remote/CertificateHostnameMatcherTests.java b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/framework/impl/api/remote/CertificateHostnameMatcherTests.java index 77d23e2f7..5bcce6cce 100644 --- 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 @@ -38,7 +38,9 @@ * 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 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 @@ -127,6 +129,34 @@ public void treatsOutOfRangeOctetsAsHostName() throws Exception { 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")); 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 index ddeb9194a..259dfa9ac 100644 --- 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 @@ -15,8 +15,9 @@ */ package org.identityconnectors.framework.impl.api.remote; -import static org.testng.Assert.assertFalse; +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; @@ -24,9 +25,12 @@ 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; @@ -40,9 +44,12 @@ 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; /** @@ -130,24 +137,73 @@ public void keepsVerificationUnlessPropertyIsExactlyFalse() throws Exception { } @Test - public void reportsMismatchOnlyForNonMatchingCertificateWhenVerificationIsDisabled() + public void reportsMismatchOnceAndOnlyForNonMatchingCertificateWhenVerificationIsDisabled() throws Exception { + assertCapturingLog(); KeyStore cnOnly = loadKeyStore("KeyStore.jks"); - try (TlsServer server = new TlsServer(cnOnly, InetAddress.getByName("127.0.0.1"))) { - withHostnameVerificationProperty("false", () -> - connect("127.0.0.1", server.getPort(), trustManagers(cnOnly)).close()); - assertTrue(RemoteFrameworkConnection.REPORTED_MISMATCHES - .contains("127.0.0.1:" + server.getPort())); + 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"); - try (TlsServer server = new TlsServer(san, InetAddress.getByName("127.0.0.1"))) { + CapturingLogSpi.clear(); + try (TlsServer server = new TlsServer(san, address)) { + connect(host, server.getPort(), trustManagers(san)).close(); withHostnameVerificationProperty("false", () -> - connect("127.0.0.1", server.getPort(), trustManagers(san)).close()); - assertFalse(RemoteFrameworkConnection.REPORTED_MISMATCHES - .contains("127.0.0.1:" + server.getPort())); + 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 @@ -203,6 +259,23 @@ private static void withSystemProperties(String[][] properties, Action action) } } + 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)) { @@ -229,6 +302,11 @@ private static KeyStore loadKeyStore(String name) throws Exception { } } + 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()); 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-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 03bb3e9b3..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 @@ -137,11 +137,12 @@ public Object fromConfiguration(final ConverterLookup converterLookup, * Create a trust manager that trusts all certificates It is not using a * particular keyStore. *

    - * It is an {@link X509ExtendedTrustManager} so that JSSE does not wrap it - * and add the "HTTPS" hostname check the framework enables for the - * connection: with every certificate trusted, that check adds no security - * and would only reject connector servers whose certificate does not name - * the configured host. + * 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 X509ExtendedTrustManager() {