Skip to content

Commit 0d1943e

Browse files
iigoninbennygoerzigKarstenSchnitterKai Sternad
committed
make test-suite runnable under FIPS
Signed-off-by: Igonin <iigonin@sternad.de> Co-authored-by: Benny Goerzig <benny.goerzig@sap.com> Co-authored-by: Karsten Schnitter <k.schnitter@sap.com> Co-authored-by: Kai Sternad <k.sternad@sternad.de>
1 parent 6dd888f commit 0d1943e

34 files changed

Lines changed: 585 additions & 120 deletions

src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Map;
1313

1414
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
15+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
1516
import org.junit.Assert;
1617
import org.junit.Rule;
1718
import org.junit.Test;
@@ -57,7 +58,11 @@ public void clusterShouldNotStart_nodesSanIpsAreInvalid() {
5758
cluster.before();
5859
Assert.fail("Cluster should not start, an exception expected");
5960
} catch (Exception e) {
60-
logsRule.assertThatContain("No subject alternative names matching IP address 127.0.0.1 found");
61+
if (CryptoServicesRegistrar.isInApprovedOnlyMode()) {
62+
logsRule.assertThatStackTraceContain("No subject alternative name found matching IP address 127.0.0.1");
63+
} else {
64+
logsRule.assertThatContain("No subject alternative names matching IP address 127.0.0.1 found");
65+
}
6166
}
6267
}
6368
}

src/integrationTest/java/org/opensearch/security/TlsTests.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
2121
import org.apache.hc.client5.http.impl.classic.HttpClients;
2222
import org.apache.hc.core5.http.NoHttpResponseException;
23+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
2324
import org.junit.ClassRule;
2425
import org.junit.Rule;
2526
import org.junit.Test;
@@ -51,7 +52,7 @@ public class TlsTests {
5152

5253
private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS);
5354

54-
public static final String SUPPORTED_CIPHER_SUIT = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256";
55+
public static final String SUPPORTED_CIPHER_SUIT = "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384";
5556
public static final String NOT_SUPPORTED_CIPHER_SUITE = "TLS_RSA_WITH_AES_128_CBC_SHA";
5657
public static final String AUTH_INFO_ENDPOINT = "/_opendistro/_security/authinfo?pretty";
5758

@@ -99,7 +100,11 @@ public void shouldSupportClientCipherSuite_negative() throws IOException {
99100
try (CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { NOT_SUPPORTED_CIPHER_SUITE })) {
100101
HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT);
101102

102-
assertThatThrownBy(() -> client.execute(httpGet), instanceOf(SSLHandshakeException.class));
103+
if (CryptoServicesRegistrar.isInApprovedOnlyMode()) {
104+
assertThatThrownBy(() -> client.execute(httpGet), instanceOf(IllegalStateException.class));
105+
} else {
106+
assertThatThrownBy(() -> client.execute(httpGet), instanceOf(SSLHandshakeException.class));
107+
}
103108
}
104109
}
105110
}

src/integrationTest/java/org/opensearch/security/hash/HashingTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
3838
public class HashingTests extends RandomizedTest {
3939

40-
private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);
40+
protected static TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS);
4141

4242
static final String PASSWORD = "top$ecret1234!";
4343

src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616

1717
import org.apache.http.HttpStatus;
1818
import org.awaitility.Awaitility;
19+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
1920
import org.junit.BeforeClass;
2021
import org.junit.Test;
2122

2223
import org.opensearch.security.support.ConfigConstants;
23-
import org.opensearch.test.framework.TestSecurityConfig;
2424
import org.opensearch.test.framework.cluster.ClusterManager;
2525
import org.opensearch.test.framework.cluster.LocalCluster;
2626
import org.opensearch.test.framework.cluster.TestRestClient;
@@ -44,9 +44,8 @@ public static void startCluster() {
4444
function = randomFrom(List.of("SHA224", "SHA256", "SHA384", "SHA512"));
4545
iterations = randomFrom(List.of(32000, 64000, 128000, 256000));
4646
length = randomFrom(List.of(128, 256, 512));
47-
48-
TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
49-
.hash(generatePBKDF2Hash("secret", function, iterations, length));
47+
var password = CryptoServicesRegistrar.isInApprovedOnlyMode() ? "dtekVF0vEAA9FNvm#KMkTwMN" : "secret";
48+
ADMIN_USER = ADMIN_USER.hash(generatePBKDF2Hash(password, function, iterations, length)).password("dtekVF0vEAA9FNvm#KMkTwMN");
5049
cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)
5150
.authc(AUTHC_HTTPBASIC_INTERNAL)
5251
.users(ADMIN_USER)
@@ -68,7 +67,7 @@ public static void startCluster() {
6867
.build();
6968
cluster.before();
7069

71-
try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) {
70+
try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), password)) {
7271
Awaitility.await()
7372
.alias("Load default configuration")
7473
.until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP"));

src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Map;
1616

1717
import org.apache.http.HttpStatus;
18+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
1819
import org.junit.ClassRule;
1920
import org.junit.Test;
2021

@@ -28,15 +29,20 @@
2829

2930
public class PBKDF2DefaultConfigHashingTests extends HashingTests {
3031

31-
private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
32-
.hash(
33-
generatePBKDF2Hash(
34-
"secret",
35-
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT,
36-
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT,
37-
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT
32+
private static final String password = CryptoServicesRegistrar.isInApprovedOnlyMode() ? "dtekVF0vEAA9FNvm#KMkTwMN" : "secret";
33+
34+
static {
35+
ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS)
36+
.hash(
37+
generatePBKDF2Hash(
38+
password,
39+
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT,
40+
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT,
41+
ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT
42+
)
3843
)
39-
);
44+
.password(password);
45+
}
4046

4147
@ClassRule
4248
public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE)

src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.List;
1313

1414
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
15+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
1516
import org.junit.ClassRule;
1617
import org.junit.Rule;
1718
import org.junit.Test;
@@ -98,7 +99,11 @@ public void shouldNotAuthenticateUserWithLdap() {
9899

99100
response.assertStatusCode(401);
100101
}
101-
logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException");
102+
if (CryptoServicesRegistrar.isInApprovedOnlyMode()) {
103+
logsRule.assertThatStackTraceContain("org.bouncycastle.tls.TlsFatalAlert");
104+
} else {
105+
logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException");
106+
}
102107
}
103108

104109
}

src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
2424
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
2525
import org.apache.hc.core5.http.io.SocketConfig;
26+
import org.bouncycastle.crypto.CryptoServicesRegistrar;
2627

2728
class CloseableHttpClientFactory {
2829

@@ -50,9 +51,12 @@ public CloseableHttpClient getHTTPClient() {
5051

5152
final HttpClientBuilder hcb = HttpClients.custom();
5253

54+
// TLSv1.3 dropped support for AES-CBC
55+
String[] protocols = supportedCipherSuites != null && CryptoServicesRegistrar.isInApprovedOnlyMode() ? new String[] { "TLSv1.2" } : null;
56+
5357
final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
5458
this.sslContext,
55-
null,
59+
protocols,
5660
supportedCipherSuites,
5761
NoopHostnameVerifier.INSTANCE
5862
);

src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthorizationBackend.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.apache.logging.log4j.LogManager;
4040
import org.apache.logging.log4j.Logger;
4141

42+
import org.ldaptive.ssl.DefaultHostnameVerifier;
4243
import org.opensearch.OpenSearchSecurityException;
4344
import org.opensearch.common.settings.Settings;
4445
import org.opensearch.core.common.Strings;
@@ -201,6 +202,20 @@ private static void checkConnection0(
201202
config.setConnectionInitializer(new BindConnectionInitializer(bindDn, new Credential(password)));
202203

203204
DefaultConnectionFactory connFactory = new DefaultConnectionFactory(config);
205+
206+
// Register custom socket factory for SNI hostname verification with BouncyCastle FIPS if SSL is enabled
207+
if (config.getLdapUrl() != null && config.getLdapUrl().startsWith("ldaps:")) {
208+
// Extract hostname from LDAP URL and set in ThreadLocal for SNI
209+
String ldapUrl = config.getLdapUrl();
210+
int protocolEnd = ldapUrl.indexOf("://");
211+
if (protocolEnd > 0) {
212+
String hostPort = ldapUrl.substring(protocolEnd + 3);
213+
String hostname = hostPort.contains(":") ? hostPort.substring(0, hostPort.indexOf(":")) : hostPort;
214+
org.opensearch.security.auth.ldap2.SNISettingTLSSocketFactory.setHostname(hostname);
215+
}
216+
configureCustomSocketFactory(connFactory);
217+
}
218+
204219
connection = connFactory.getConnection();
205220

206221
connection.open();
@@ -257,6 +272,12 @@ private static Connection getConnection0(
257272
log.trace("Connect to {}", config.getLdapUrl());
258273
}
259274

275+
// Set hostname in ThreadLocal for SNI before SSL configuration
276+
// This is needed for BouncyCastle FIPS hostname verification
277+
if (enableSSL) {
278+
org.opensearch.security.auth.ldap2.SNISettingTLSSocketFactory.setHostname(split[0]);
279+
}
280+
260281
configureSSL(config, settings, configPath);
261282

262283
final String bindDn = settings.get(ConfigConstants.LDAP_BIND_DN, null);
@@ -302,6 +323,14 @@ private static Connection getConnection0(
302323
}
303324

304325
DefaultConnectionFactory connFactory = new DefaultConnectionFactory(config);
326+
327+
// Register custom socket factory for SNI hostname verification with BouncyCastle FIPS
328+
// This addresses a known issue where JNDI LDAP doesn't pass hostname to SSLSocketFactory
329+
// See: https://github.com/bcgit/bc-java/issues/460
330+
if (enableSSL) {
331+
configureCustomSocketFactory(connFactory);
332+
}
333+
305334
connection = connFactory.getConnection();
306335

307336
connection.open();
@@ -462,6 +491,14 @@ private static void restoreClassLoader0(final ClassLoader cl) {
462491
}
463492
}
464493

494+
@SuppressWarnings({"unchecked", "rawtypes"})
495+
private static void configureCustomSocketFactory(DefaultConnectionFactory connFactory) {
496+
Map<String, Object> props = new HashMap<>();
497+
props.put("java.naming.ldap.factory.socket", "org.opensearch.security.auth.ldap2.SNISettingTLSSocketFactory");
498+
final org.ldaptive.provider.ProviderConfig providerConfig = connFactory.getProvider().getProviderConfig();
499+
providerConfig.setProperties(props);
500+
}
501+
465502
private static void configureSSL(final ConnectionConfig config, final Settings settings, final Path configPath) throws Exception {
466503

467504
final boolean isDebugEnabled = log.isDebugEnabled();
@@ -607,6 +644,8 @@ private static void configureSSL(final ConnectionConfig config, final Settings s
607644

608645
System.setProperty(COM_SUN_JNDI_LDAP_OBJECT_DISABLE_ENDPOINT_IDENTIFICATION, "true");
609646

647+
} else {
648+
sslConfig.setHostnameVerifier(new DefaultHostnameVerifier());
610649
}
611650

612651
final List<String> enabledCipherSuites = settings.getAsList(ConfigConstants.LDAPS_ENABLED_SSL_CIPHERS, Collections.emptyList());
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
12+
package org.opensearch.security.auth.ldap2;
13+
14+
import java.net.URI;
15+
import java.util.regex.Matcher;
16+
import java.util.regex.Pattern;
17+
18+
import org.apache.logging.log4j.LogManager;
19+
import org.apache.logging.log4j.Logger;
20+
import org.ldaptive.Connection;
21+
import org.ldaptive.ConnectionFactory;
22+
23+
/**
24+
* Wrapper around ConnectionFactory that extracts the hostname from the LDAP URL
25+
* and stores it in ThreadLocal for use by SNISettingTLSSocketFactory.
26+
*
27+
* <p>This is necessary because JNDI LDAP resolves hostnames to IP addresses before
28+
* creating SSL sockets, making the hostname unavailable for SNI configuration.
29+
*/
30+
public class HostnameAwareConnectionFactory implements ConnectionFactory {
31+
32+
private static final Logger log = LogManager.getLogger(HostnameAwareConnectionFactory.class);
33+
private static final Pattern LDAP_URL_PATTERN = Pattern.compile("ldaps?://([^:/]+)(?::(\\d+))?");
34+
35+
private final ConnectionFactory delegate;
36+
private final String ldapUrl;
37+
38+
public HostnameAwareConnectionFactory(ConnectionFactory delegate, String ldapUrl) {
39+
this.delegate = delegate;
40+
this.ldapUrl = ldapUrl;
41+
}
42+
43+
@Override
44+
public Connection getConnection() throws org.ldaptive.LdapException {
45+
// Extract hostname from LDAP URL and set in ThreadLocal before getting connection
46+
String hostname = extractHostname(ldapUrl);
47+
if (hostname != null) {
48+
log.debug("Setting hostname for LDAP connection: {}", hostname);
49+
SNISettingTLSSocketFactory.setHostname(hostname);
50+
} else {
51+
log.debug("Could not extract hostname from LDAP URL: {}", ldapUrl);
52+
}
53+
54+
// ThreadLocal is cleared in SNISettingTLSSocketFactory.configureSocket() after socket creation
55+
return delegate.getConnection();
56+
}
57+
58+
/**
59+
* Extracts the hostname from an LDAP URL.
60+
* Handles space-separated multiple URLs by extracting the first one.
61+
*
62+
* @param url the LDAP URL (e.g., "ldaps://localhost:636" or "ldaps://server1:636 ldaps://server2:636")
63+
* @return the hostname, or null if extraction fails
64+
*/
65+
static String extractHostname(String url) {
66+
if (url == null || url.isEmpty()) {
67+
return null;
68+
}
69+
70+
// Handle space-separated multiple URLs - extract the first one
71+
String firstUrl = url.split("\\s+")[0];
72+
73+
try {
74+
// Try parsing as URI first
75+
URI uri = new URI(firstUrl);
76+
if (uri.getHost() != null) {
77+
return uri.getHost();
78+
}
79+
} catch (Exception e) {
80+
// Fall through to regex pattern
81+
}
82+
83+
// Fallback to regex pattern
84+
Matcher matcher = LDAP_URL_PATTERN.matcher(firstUrl);
85+
if (matcher.find()) {
86+
return matcher.group(1);
87+
}
88+
89+
return null;
90+
}
91+
}

0 commit comments

Comments
 (0)