diff --git a/build.gradle b/build.gradle index 0c12f07111..19a451429f 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,6 @@ import com.diffplug.gradle.spotless.JavaExtension -import org.opensearch.gradle.info.FipsBuildParams import org.opensearch.gradle.test.RestIntegTestTask import groovy.json.JsonBuilder @@ -235,6 +234,24 @@ tasks.register("listTasksAsJSON") { } } +def configureFipsJvmArgs(Test task) { + if ('true'.equalsIgnoreCase(System.getenv('OPENSEARCH_FIPS_MODE'))) { + def fipsSecurityFile = project.rootProject.file('src/test/resources/fips_java_test.security') + // BCFKS truststore is sourced from the core project: buildSrc/src/main/resources/opensearch-fips-truststore.bcfks + // It can also be regenerated by running distribution/src/bin/opensearch-fips-demo-installer + def fipsTrustStore = project.rootProject.file('src/test/resources/fips-jvm-truststore.bcfks') + task.jvmArgs += "-Djava.security.properties==${fipsSecurityFile}" + task.jvmArgs += "-Dorg.bouncycastle.fips.approved_only=true" + task.jvmArgs += "-Djavax.net.ssl.trustStore=${fipsTrustStore}" + task.jvmArgs += "-Djavax.net.ssl.trustStoreProvider=BCFIPS" + task.jvmArgs += "-Djavax.net.ssl.trustStoreType=BCFKS" + task.jvmArgs += "-Djavax.net.ssl.trustStorePassword=changeit" + } else { + def nonFipsSecurityFile = project.rootProject.file('src/test/resources/java_test.security') + task.jvmArgs += "-Djava.security.properties==${nonFipsSecurityFile}" + } +} + tasks.register('copyExtraTestResources', Copy) { dependsOn testClasses @@ -285,6 +302,7 @@ def setCommonTestConfig(Test task) { } task.dependsOn copyExtraTestResources task.finalizedBy jacocoTestReport + configureFipsJvmArgs(task) } test { @@ -547,6 +565,9 @@ subprojects { configurations.integrationTestImplementation.extendsFrom configurations.implementation configurations.integrationTestRuntimeOnly.extendsFrom configurations.runtimeOnly } + tasks.withType(Test).configureEach { t -> + configureFipsJvmArgs(t) + } } } } @@ -580,6 +601,7 @@ allprojects { integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}" integrationTestImplementation 'org.hamcrest:hamcrest:2.2' integrationTestImplementation "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}" + integrationTestImplementation "org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}" integrationTestImplementation "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}" integrationTestImplementation "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}" integrationTestImplementation('org.awaitility:awaitility:4.3.0') { @@ -660,6 +682,7 @@ tasks.register("integrationTest", Test) { "sun.util.resources.provider.*" ] } + configureFipsJvmArgs(it) } tasks.named("integrationTest") { @@ -684,16 +707,10 @@ dependencies { implementation libs.google.guava implementation 'org.greenrobot:eventbus-java:3.3.1' implementation 'commons-cli:commons-cli:1.11.0' - // When building with crypto.standard=FIPS-140-3 (set in gradle.properties), bcFips jars are provided by OpenSearch - if (FipsBuildParams.isInFipsMode()) { - compileOnly "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}" - compileOnly "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}" - compileOnly "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}" - } else { - implementation "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}" - implementation "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}" - implementation "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}" - } + compileOnly "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}" + compileOnly "org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}" + compileOnly "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}" + compileOnly "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}" implementation 'org.ldaptive:ldaptive:1.2.3' implementation 'com.nimbusds:nimbus-jose-jwt:10.9.1' implementation 'com.rfksystems:blake2b:2.0.0' @@ -799,6 +816,7 @@ dependencies { exclude(group: 'org.hamcrest', module: 'hamcrest') } testImplementation "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}" + testImplementation "org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}" testImplementation "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}" testImplementation "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}" // JUnit build requirement @@ -836,6 +854,18 @@ tasks.register('testsJar', Jar) { from(sourceSets.test.output) } +def configureSecurityAdminBcFips(AbstractArchiveTask task) { + def bcFipsJars = configurations.detachedConfiguration( + dependencies.create("org.bouncycastle:bc-fips:${versions.bouncycastle_jce}"), + dependencies.create("org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}"), + dependencies.create("org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}"), + dependencies.create("org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}") + ) + task.from(bcFipsJars) { + into 'deps/' + } +} + tasks.register("bundleSecurityAdminStandalone", Zip) { dependsOn(jar) archiveClassifier = 'securityadmin-standalone' @@ -852,6 +882,7 @@ tasks.register("bundleSecurityAdminStandalone", Zip) { into 'deps/securityconfig' } } +configureSecurityAdminBcFips(bundleSecurityAdminStandalone) tasks.register("bundleSecurityAdminStandaloneTarGz", Tar) { dependsOn(jar) @@ -871,6 +902,7 @@ tasks.register("bundleSecurityAdminStandaloneTarGz", Tar) { into 'deps/securityconfig' } } +configureSecurityAdminBcFips(bundleSecurityAdminStandaloneTarGz) buildRpm { arch = 'NOARCH' diff --git a/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java index ab12797e61..b95ed0d88f 100644 --- a/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java @@ -9,6 +9,7 @@ */ package org.opensearch.security; +import java.time.Duration; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -44,10 +45,15 @@ protected AbstractDefaultConfigurationTests(LocalCluster cluster) { this.cluster = cluster; } + private static final Duration AWAIT_TIMEOUT = Duration.ofSeconds(30); + @Test public void shouldLoadDefaultConfiguration() { try (TestRestClient client = cluster.getRestClient(NEW_USER)) { - Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + Awaitility.await() + .alias("Load default configuration") + .timeout(AWAIT_TIMEOUT) + .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); } try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { client.confirmCorrectCredentials(ADMIN_USER.getName()); @@ -103,7 +109,10 @@ private void prepareRolesTestCase() { public void securityUpgrade() throws Exception { try (var client = cluster.getRestClient(ADMIN_USER)) { // Setup: Make sure the config is ready before starting modifications - Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + Awaitility.await() + .alias("Load default configuration") + .timeout(AWAIT_TIMEOUT) + .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); // Setup: Collect default roles after cluster start final var expectedRoles = client.get("_plugins/_security/api/roles/"); final var expectedRoleNames = extractFieldNames(expectedRoles.getBodyAs(JsonNode.class)); @@ -143,7 +152,10 @@ public void securityUpgrade() throws Exception { public void securityRolesUpgrade() throws Exception { try (var client = cluster.getRestClient(ADMIN_USER)) { // Setup: Make sure the config is ready before starting modifications - Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + Awaitility.await() + .alias("Load default configuration") + .timeout(AWAIT_TIMEOUT) + .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); // Setup: Collect default roles after cluster start final var expectedRoles = client.get("_plugins/_security/api/roles/"); @@ -185,7 +197,10 @@ public void securityRolesUpgrade() throws Exception { public void securityRolesUpgradeSpecifyingRoles() throws Exception { try (var client = cluster.getRestClient(ADMIN_USER)) { // Setup: Make sure the config is ready before starting modifications - Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + Awaitility.await() + .alias("Load default configuration") + .timeout(AWAIT_TIMEOUT) + .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); // Setup: Collect default roles after cluster start final var expectedRoles = client.get("_plugins/_security/api/roles/"); diff --git a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java index 4b0ef62b49..4cc9bbc365 100644 --- a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java +++ b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java @@ -11,11 +11,13 @@ import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.framework.TestSecurityConfig; public class ConfigurationFiles { @@ -25,7 +27,6 @@ public static Path createConfigurationDirectory() { String[] configurationFiles = { CType.ACTIONGROUPS.configFileName(), CType.CONFIG.configFileName(), - CType.INTERNALUSERS.configFileName(), CType.NODESDN.configFileName(), CType.ROLES.configFileName(), CType.ROLESMAPPING.configFileName(), @@ -34,12 +35,36 @@ public static Path createConfigurationDirectory() { for (String fileName : configurationFiles) { copyResourceToFile(fileName, tempDirectory.resolve(fileName)); } + writeInternalUsersFile(tempDirectory.resolve(CType.INTERNALUSERS.configFileName())); return tempDirectory.toAbsolutePath(); } catch (IOException ex) { throw new RuntimeException("Cannot create directory with security plugin configuration.", ex); } } + // Generates internal_users.yml with a hash derived from DEFAULT_TEST_PASSWORD so the + // admin user can authenticate in both FIPS and non-FIPS mode without a hardcoded BCrypt hash. + private static void writeInternalUsersFile(Path destination) throws IOException { + String hash = TestSecurityConfig.hashPassword(TestSecurityConfig.DEFAULT_TEST_PASSWORD); + String content = """ + --- + _meta: + type: "internalusers" + config_version: 2 + new-user: + hash: "%s" + limited-user: + hash: "%s" + opendistro_security_roles: + - "user_limited-user__limited-role" + admin: + hash: "%s" + opendistro_security_roles: + - "user_admin__all_access" + """.formatted(hash, hash, hash); + Files.writeString(destination, content, StandardCharsets.UTF_8); + } + public static void copyResourceToFile(String resource, Path destination) { try (InputStream input = ConfigurationFiles.class.getClassLoader().getResourceAsStream(resource)) { Objects.requireNonNull(input, "Cannot find source resource " + resource); diff --git a/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java b/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java index c7e54f8791..6c850df63a 100644 --- a/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java +++ b/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java @@ -37,6 +37,7 @@ import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; import org.opensearch.http.HttpTransportSettings; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.AsyncActions; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.User; @@ -48,7 +49,6 @@ import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; -import reactor.netty.http.HttpProtocol; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -111,8 +111,8 @@ public void testUnauthenticatedManyMedium() { // Tweaks: final RequestBodySize size = RequestBodySize.Medium; final String requestPath = "/*/_search"; - final int parallelism = 20; - final int totalNumberOfRequests = 10_000; + final int parallelism = FipsMode.isEnabled() ? 8 : 20; + final int totalNumberOfRequests = FipsMode.isEnabled() ? 2_000 : 10_000; runResourceTest(size, requestPath, parallelism, totalNumberOfRequests); runResourceTestWithGenericClient(size, requestPath, parallelism, totalNumberOfRequests); @@ -123,8 +123,8 @@ public void testUnauthenticatedTonsSmall() { // Tweaks: final RequestBodySize size = RequestBodySize.Small; final String requestPath = "/*/_search"; - final int parallelism = 100; - final int totalNumberOfRequests = 15_000; + final int parallelism = FipsMode.isEnabled() ? 8 : 100; + final int totalNumberOfRequests = FipsMode.isEnabled() ? 2_000 : 15_000; runResourceTest(size, requestPath, parallelism, totalNumberOfRequests); runResourceTestWithGenericClient(size, requestPath, parallelism, totalNumberOfRequests); @@ -158,13 +158,7 @@ private void runResourceTestWithGenericClient( final int totalNumberOfRequests ) { final byte[] compressedRequestBody = createCompressedRequestBody(size); - try ( - final ReactorHttpClient client = cluster.getGenericClient( - HttpProtocol.HTTP3, - true, - Settings.builder().loadFromMap(NODE_SETTINGS).build() - ) - ) { + try (final ReactorHttpClient client = cluster.getGenericClient(Settings.builder().loadFromMap(NODE_SETTINGS).build())) { List> requestUris = new ArrayList<>(); for (int i = 0; i < totalNumberOfRequests; i++) { requestUris.add(Tuple.tuple(requestPath, compressedRequestBody)); diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java index dec52950ec..27f17b207f 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java @@ -139,17 +139,21 @@ public void shouldStillLoadSecurityConfigDuringBootstrapAndActiveConfigUpdateReq } }); - Awaitility.await().alias("Load default configuration").pollInterval(Duration.ofMillis(100)).until(() -> { - // After the configuration has been loaded, the rest clients should be able to connect successfully - cluster.triggerConfigurationReloadForCTypes( - internalNodeClient, - List.of(CType.ACTIONGROUPS, CType.CONFIG, CType.ROLES, CType.ROLESMAPPING, CType.TENANTS), - true - ); - try (final TestRestClient freshClient = cluster.getRestClient(USER_ADMIN)) { - return client.getAuthInfo().getStatusCode(); - } - }, equalTo(200)); + Awaitility.await() + .alias("Load default configuration") + .pollInterval(Duration.ofMillis(100)) + .timeout(Duration.ofSeconds(30)) + .until(() -> { + // After the configuration has been loaded, the rest clients should be able to connect successfully + cluster.triggerConfigurationReloadForCTypes( + internalNodeClient, + List.of(CType.ACTIONGROUPS, CType.CONFIG, CType.ROLES, CType.ROLESMAPPING, CType.TENANTS), + true + ); + try (final TestRestClient freshClient = cluster.getRestClient(USER_ADMIN)) { + return client.getAuthInfo().getStatusCode(); + } + }, equalTo(200)); } } } diff --git a/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java b/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java index 6b3768834f..4abeebf756 100644 --- a/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java +++ b/src/integrationTest/java/org/opensearch/security/TlsHostnameVerificationTests.java @@ -17,6 +17,7 @@ import org.junit.Rule; import org.junit.Test; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.certificate.TestCertificates; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -71,7 +72,11 @@ public void clusterShouldNotStart_nodesSanIpsAreInvalid() { ); clusterFuture.cancel(true); } catch (Exception e) { - logsRule.assertThatContain("No subject alternative names matching IP address 127.0.0.1 found"); + if (FipsMode.isEnabled()) { + logsRule.assertThatStackTraceContain("No subject alternative name found matching IP address 127.0.0.1"); + } else { + logsRule.assertThatContain("No subject alternative names matching IP address 127.0.0.1 found"); + } } } } diff --git a/src/integrationTest/java/org/opensearch/security/TlsTests.java b/src/integrationTest/java/org/opensearch/security/TlsTests.java index e2f5cd2e30..b4be3d2ce1 100644 --- a/src/integrationTest/java/org/opensearch/security/TlsTests.java +++ b/src/integrationTest/java/org/opensearch/security/TlsTests.java @@ -24,6 +24,7 @@ import org.junit.Test; import org.opensearch.security.auditlog.impl.AuditCategory; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.AuditCompliance; import org.opensearch.test.framework.AuditConfiguration; import org.opensearch.test.framework.AuditFilters; @@ -37,6 +38,7 @@ import static org.hamcrest.Matchers.instanceOf; import static org.opensearch.security.auditlog.AuditLog.Origin.REST; import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.audit.AuditMessagePredicate.auditPredicate; @@ -47,14 +49,21 @@ public class TlsTests { private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); - public static final String SUPPORTED_CIPHER_SUIT = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + public static final String SUPPORTED_CIPHER_SUIT = "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384"; public static final String NOT_SUPPORTED_CIPHER_SUITE = "TLS_RSA_WITH_AES_128_CBC_SHA"; public static final String AUTH_INFO_ENDPOINT = "/_opendistro/_security/authinfo?pretty"; @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .anonymousAuth(false) - .nodeSettings(Map.of(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of(SUPPORTED_CIPHER_SUIT))) + .nodeSettings( + Map.of( + SECURITY_SSL_HTTP_ENABLED_CIPHERS, + List.of(SUPPORTED_CIPHER_SUIT), + SECURITY_SSL_HTTP_ENABLED_PROTOCOLS, + List.of("TLSv1.2") + ) + ) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(USER_ADMIN) .audit( @@ -95,7 +104,11 @@ public void shouldSupportClientCipherSuite_negative() throws IOException { try (CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { NOT_SUPPORTED_CIPHER_SUITE })) { HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT); - assertThatThrownBy(() -> client.execute(httpGet), instanceOf(SSLHandshakeException.class)); + if (FipsMode.isEnabled()) { + assertThatThrownBy(() -> client.execute(httpGet), instanceOf(IllegalStateException.class)); + } else { + assertThatThrownBy(() -> client.execute(httpGet), instanceOf(SSLHandshakeException.class)); + } } } } diff --git a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java index 9293f5ecc1..b1d4c6e618 100644 --- a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java @@ -11,6 +11,7 @@ import java.util.concurrent.TimeUnit; +import org.awaitility.Awaitility; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -38,7 +39,8 @@ public class UserBruteForceAttacksPreventionTests { private static final User USER_5 = new User("simple-user-5").roles(ALL_ACCESS); public static final int ALLOWED_TRIES = 3; - public static final int TIME_WINDOW_SECONDS = 3; + public static final int TIME_WINDOW_SECONDS = 30; + public static final int LOCK_RELEASE_TIMEOUT_SECONDS = 9; private static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( new RateLimiting("internal_authentication_backend_limiting").type("username") .authenticationBackend("intern") @@ -74,9 +76,8 @@ public void shouldAuthenticateUserWhenBlockadeIsNotActive() { public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() { authenticateUserWithIncorrectPassword(USER_2, ALLOWED_TRIES); try (TestRestClient client = cluster.getRestClient(USER_2)) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); + Awaitility.await("user " + USER_2.getName() + " blocked after " + ALLOWED_TRIES + " failed attempts") + .until(() -> client.getAuthInfo().getStatusCode() == SC_UNAUTHORIZED); } // Rejecting REST request because of blocked user: logsRule.assertThatContain("Rejecting request because of blocked user: " + USER_2.getName()); @@ -86,9 +87,8 @@ public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() { public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGreaterThanLimit() { authenticateUserWithIncorrectPassword(USER_3, ALLOWED_TRIES * 2); try (TestRestClient client = cluster.getRestClient(USER_3)) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); + Awaitility.await("user " + USER_3.getName() + " blocked after " + (ALLOWED_TRIES * 2) + " failed attempts") + .until(() -> client.getAuthInfo().getStatusCode() == SC_UNAUTHORIZED); } logsRule.assertThatContain("Rejecting request because of blocked user: " + USER_3.getName()); } @@ -104,16 +104,14 @@ public void shouldNotBlockUserWhenNumberOfLoginAttemptIsBelowLimit() { } @Test - public void shouldReleaseLock() throws InterruptedException { + public void shouldReleaseLock() { authenticateUserWithIncorrectPassword(USER_5, ALLOWED_TRIES); try (TestRestClient client = cluster.getRestClient(USER_5)) { - HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(SC_UNAUTHORIZED); - TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); - - response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); + Awaitility.await("user " + USER_5.getName() + " initially blocked") + .until(() -> client.getAuthInfo().getStatusCode() == SC_UNAUTHORIZED); + Awaitility.await("block released for user " + USER_5.getName()) + .atMost(LOCK_RELEASE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> client.getAuthInfo().getStatusCode() == SC_OK); } logsRule.assertThatContain("Rejecting request because of blocked user: " + USER_5.getName()); } diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java index 8978926370..195ffe3622 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -25,6 +25,7 @@ import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -87,12 +88,22 @@ private static String randomAsciiAlphanumOfLength(final int length) { */ public static final TestSecurityConfig.User NEW_USER = new TestSecurityConfig.User("new-user"); - public static final String DEFAULT_PASSWORD = "secret"; + public static final String DEFAULT_PASSWORD = TestSecurityConfig.DEFAULT_TEST_PASSWORD; + + /** + * BC FIPS requires PBKDF2 passwords to be at least 112 bits; with ASCII chars that is 14 bytes. + */ + public static final int FIPS_MIN_PASSWORD_LENGTH = 14; public static final ToXContentObject EMPTY_BODY = (builder, params) -> builder.startObject().endObject(); public static final PasswordHasher passwordHasher = PasswordHasherFactory.createPasswordHasher( - Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT).build() + Settings.builder() + .put( + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, + FipsMode.isEnabled() ? ConfigConstants.PBKDF2 : ConfigConstants.BCRYPT + ) + .build() ); protected static LocalCluster.Builder clusterBuilder() { diff --git a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java index 8af5910d19..e316e98cc4 100644 --- a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java @@ -38,9 +38,9 @@ public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { private final static String HIDDEN_USERS = "hidden-user"; - public final static String TEST_USER_PASSWORD = randomAlphabetic(10); + public final static String TEST_USER_PASSWORD = randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH); - public final static String TEST_USER_NEW_PASSWORD = randomAlphabetic(10); + public final static String TEST_USER_NEW_PASSWORD = randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH); @ClassRule public static LocalCluster localCluster = clusterBuilder().users( @@ -69,11 +69,11 @@ public void accountInfo() throws Exception { assertThat(response.getBody(), account.get("tenants").isObject()); assertThat(response.getBody(), account.get("roles").isArray()); } - try (TestRestClient client = localCluster.getRestClient(NEW_USER.getName(), "a")) { + try (TestRestClient client = localCluster.getRestClient(NEW_USER.getName(), randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH))) { HttpResponse response = client.get(accountPath()); assertThat(response, isUnauthorized()); } - try (TestRestClient client = localCluster.getRestClient("a", "b")) { + try (TestRestClient client = localCluster.getRestClient("a", randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH))) { HttpResponse response = client.get(accountPath()); assertThat(response, isUnauthorized()); } @@ -90,18 +90,27 @@ public void changeAccountPassword() throws Exception { HttpResponse response = client.get(accountPath()); assertThat(response, isOk()); assertThat(response.getBooleanFromJsonBody("/is_reserved"), is(true)); - assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isForbidden()); + assertThat( + client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH))), + isForbidden() + ); } try (TestRestClient client = localCluster.getRestClient(HIDDEN_USERS, DEFAULT_PASSWORD)) { HttpResponse response = client.get(accountPath()); assertThat(response, isOk()); assertThat(response.getBooleanFromJsonBody("/is_hidden"), is(true)); - assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isNotFound()); + assertThat( + client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH))), + isNotFound() + ); } try (TestRestClient client = localCluster.getAdminCertRestClient()) { HttpResponse response = client.get(accountPath()); assertThat(response, isOk()); - assertThat(client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10))), isNotFound()); + assertThat( + client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH))), + isNotFound() + ); } } @@ -127,7 +136,7 @@ private void verifyWrongPayload(final TestRestClient client) throws Exception { } private void verifyPasswordCanBeChanged() throws Exception { - final var newPassword = randomAlphabetic(10); + final var newPassword = randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH); try (TestRestClient client = localCluster.getRestClient(TEST_USER, TEST_USER_PASSWORD)) { HttpResponse resp = client.putJson( accountPath(), @@ -144,8 +153,8 @@ private void verifyPasswordCanBeChanged() throws Exception { @Test public void testPutAccountRetainsAccountInformation() throws Exception { final var username = "test"; - final String password = randomAlphabetic(10); - final String newPassword = randomAlphabetic(10); + final String password = randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH); + final String newPassword = randomAlphabetic(FIPS_MIN_PASSWORD_LENGTH); try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { assertThat( client.putJson( diff --git a/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java b/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java index 643a6d0bee..235d99d1ac 100644 --- a/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java @@ -40,7 +40,7 @@ public class CreateResetPasswordTest { public static final String INVALID_PASSWORD_REGEX = "user 1 fair password"; - public static final String VALID_WEAK_PASSWORD = "Asdfghjkl1!"; + public static final String VALID_WEAK_PASSWORD = "Password1234!!"; public static final String VALID_SIMILAR_PASSWORD = "456Additional00001_1234!"; diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java index 76dd413454..bd7294d391 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRegExpPasswordRulesRestApiIntegrationTest.java @@ -79,8 +79,8 @@ public void canNotCreateUsersWithPassword() throws Exception { final var r = client.patch( internalUsers(), patch( - addOp("testuser1", internalUserWithPassword("$aA123456789")), - addOp("testuser2", internalUserWithPassword("testpassword2")) + addOp("testuser1", internalUserWithPassword("$aA1234567890ab")), + addOp("testuser2", internalUserWithPassword("testpassword23")) ) ); assertThat(r, isBadRequest("/reason", PASSWORD_VALIDATION_ERROR_MESSAGE)); @@ -93,12 +93,12 @@ public void canNotCreateUsersWithPassword() throws Exception { @Test public void canCreateUsersWithPassword() throws Exception { try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("$aA123456789")), isCreated()); - assertThat(client.putJson(internalUsers("ok2"), internalUserWithPassword("$Aa123456789")), isCreated()); - assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAA")), isCreated()); - assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAC")), isOk()); - assertThat(client.patch(internalUsers(), patch(addOp("ok3", internalUserWithPassword("$1aAAAAAAAAB")))), isOk()); - assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("Admin_123")), isOk()); + assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("$aA1234567890ab")), isCreated()); + assertThat(client.putJson(internalUsers("ok2"), internalUserWithPassword("$Aa1234567890ab")), isCreated()); + assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAAAAab")), isCreated()); + assertThat(client.putJson(internalUsers("ok3"), internalUserWithPassword("$1aAAAAAAAACab")), isOk()); + assertThat(client.patch(internalUsers(), patch(addOp("ok3", internalUserWithPassword("$1aAAAAAAAABab")))), isOk()); + assertThat(client.putJson(internalUsers("ok1"), internalUserWithPassword("Admin_1234567890")), isOk()); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java index 427d33803f..b612b72832 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersRestApiIntegrationTest.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableMap; import org.apache.http.HttpStatus; import org.junit.Assert; +import org.junit.Assume; import org.junit.ClassRule; import org.junit.Test; @@ -33,6 +34,7 @@ import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -129,6 +131,26 @@ public void availableForRESTAdminUser() throws Exception { super.availableForRESTAdminUser(localCluster); } + @Test + public void changingPasswordBelowFipsFloorIsRejected() throws Exception { + Assume.assumeTrue("FIPS password floor only applies under FIPS", FipsMode.isEnabled()); + try (TestRestClient client = localCluster.getAdminCertRestClient()) { + final var username = randomAsciiAlphanumOfLength(10); + + // 1. Create the user with a password that clears the 14-char FIPS floor. + assertThat( + client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH))), + isCreated() + ); + + // 2. Change the password to one below the floor -> clean 400, not a hang. + assertThat( + client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH - 1))), + isBadRequest() + ); + } + } + static ToXContentObject internalUserWithPassword(final String password) { return internalUser(null, null, null, password, null, null, null); } @@ -235,7 +257,7 @@ void verifyBadRequestOperations(TestRestClient client) throws Exception { assertThat( client.putJson( apiPath(predefinedUserName), - internalUser(randomAsciiAlphanumOfLength(10), configJsonArray(generateArrayValues(false)), null, null) + internalUser(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), configJsonArray(generateArrayValues(false)), null, null) ), isCreated() ); @@ -381,7 +403,7 @@ void verifyCrudOperationsForCombination( final var newUserJsonPut = internalUser( hidden, reserved, - randomAsciiAlphanumOfLength(10), + randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), randomConfigArray(false), attributes, securityRoles @@ -396,7 +418,7 @@ void verifyCrudOperationsForCombination( final var updatedUserJsonPut = internalUser( hidden, reserved, - randomAsciiAlphanumOfLength(10), + randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), randomConfigArray(false), attributes, securityRoles @@ -416,7 +438,7 @@ void verifyCrudOperationsForCombination( final var newUserJsonPatch = internalUser( hidden, reserved, - randomAsciiAlphanumOfLength(10), + randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), configJsonArray("a", "b"), (builder, params) -> builder.startObject().endObject(), configJsonArray() @@ -513,7 +535,9 @@ public void userApiWithDotsInName() throws Exception { assertThat( client.putJson( apiPath(dottedUserName), - (builder, params) -> builder.startObject().field("password", randomAsciiAlphanumOfLength(10)).endObject() + (builder, params) -> builder.startObject() + .field("password", randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)) + .endObject() ), isCreated() ); @@ -523,7 +547,7 @@ public void userApiWithDotsInName() throws Exception { client.putJson( apiPath(dottedUserName), (builder, params) -> builder.startObject() - .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) + .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH).toCharArray())) .endObject() ), isCreated() @@ -537,7 +561,7 @@ public void userApiWithDotsInName() throws Exception { addOp( dottedUserName, (ToXContentObject) (builder, params) -> builder.startObject() - .field("password", randomAsciiAlphanumOfLength(10)) + .field("password", randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)) .endObject() ) ) @@ -553,7 +577,7 @@ public void userApiWithDotsInName() throws Exception { addOp( dottedUserName, (ToXContentObject) (builder, params) -> builder.startObject() - .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) + .field("hash", passwordHasher.hash(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH).toCharArray())) .endObject() ) ) @@ -592,7 +616,7 @@ public void noPasswordChange() throws Exception { apiPath("user1"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") - .field("password", randomAsciiAlphanumOfLength(10)) + .field("password", randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)) .field("backend_roles", configJsonArray("admin", "role_a")) .endObject() ), @@ -603,7 +627,7 @@ public void noPasswordChange() throws Exception { apiPath("user2"), (builder, params) -> builder.startObject() .field("hash", "$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m") - .field("password", randomAsciiAlphanumOfLength(10)) + .field("password", randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)) .endObject() ), isCreated() @@ -622,7 +646,7 @@ public void noPasswordChange() throws Exception { client.putJson( apiPath("user2"), (builder, params) -> builder.startObject() - .field("password", randomAsciiAlphanumOfLength(10)) + .field("password", randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)) .field("backend_roles", configJsonArray("admin", "role_b")) .endObject() ), @@ -634,7 +658,7 @@ public void noPasswordChange() throws Exception { @Test public void securityRoles() throws Exception { final var userWithSecurityRoles = randomAsciiAlphanumOfLength(15); - final var userWithSecurityRolesPassword = randomAsciiAlphanumOfLength(10); + final var userWithSecurityRolesPassword = randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH); try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { assertThat( client.patch(apiPath(), patch(addOp(userWithSecurityRoles, internalUser(userWithSecurityRolesPassword, null, null, null)))), @@ -696,7 +720,7 @@ void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, fina assertThat( client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), - internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) + internalUser(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), null, null, configJsonArray(HIDDEN_ROLE)) ), isNotFound().withAttribute("/message", "Resource 'hidden-role' is not available.") ); @@ -707,7 +731,7 @@ void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, fina patch( addOp( randomAsciiAlphanumOfLength(10), - internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE)) + internalUser(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), null, null, configJsonArray(HIDDEN_ROLE)) ) ) ), @@ -723,7 +747,10 @@ void impossibleToSetHiddenRoleIsNotAllowed(final String predefinedUserName, fina void canAssignedHiddenRole(final TestRestClient client) throws Exception { final var userNamePut = randomAsciiAlphanumOfLength(4); assertThat( - client.putJson(apiPath(userNamePut), internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray(HIDDEN_ROLE))), + client.putJson( + apiPath(userNamePut), + internalUser(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), null, null, configJsonArray(HIDDEN_ROLE)) + ), isCreated() ); } @@ -732,7 +759,7 @@ void settingOfUnknownRoleIsNotAllowed(final String predefinedUserName, final Tes assertThat( client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), - internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) + internalUser(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), null, null, configJsonArray("unknown-role")) ), isNotFound().withAttribute("/message", "role 'unknown-role' not found.") ); @@ -742,7 +769,7 @@ void settingOfUnknownRoleIsNotAllowed(final String predefinedUserName, final Tes patch( addOp( randomAsciiAlphanumOfLength(4), - internalUser(randomAsciiAlphanumOfLength(10), null, null, configJsonArray("unknown-role")) + internalUser(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH), null, null, configJsonArray("unknown-role")) ) ) ), @@ -767,7 +794,9 @@ public void parallelPutRequests() throws Exception { executorService.submit( () -> client.putJson( apiPath(userName), - (builder, params) -> builder.startObject().field("password", randomAsciiAlphanumOfLength(10)).endObject() + (builder, params) -> builder.startObject() + .field("password", randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)) + .endObject() ) ) ); @@ -804,11 +833,17 @@ public void restrictedUsernameContents() throws Exception { URLEncoder.encode(randomAsciiAlphanumOfLength(4) + ":" + randomAsciiAlphanumOfLength(3), StandardCharsets.UTF_8) )) { assertThat( - client.putJson(apiPath(username), internalUserWithPassword(randomAsciiAlphanumOfLength(10))), + client.putJson( + apiPath(username), + internalUserWithPassword(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)) + ), isBadRequest("/message", restrictedTerm) ); assertThat( - client.patch(apiPath(), patch(addOp(username, internalUserWithPassword(randomAsciiAlphanumOfLength(10))))), + client.patch( + apiPath(), + patch(addOp(username, internalUserWithPassword(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH)))) + ), isBadRequest("/message", restrictedTerm) ); } @@ -857,12 +892,12 @@ public void serviceUsers() throws Exception { assertThat( client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), - serviceUserWithHash(true, passwordHasher.hash(randomAsciiAlphanumOfLength(10).toCharArray())) + serviceUserWithHash(true, passwordHasher.hash(randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH).toCharArray())) ), isBadRequest() ); // Add Service account with password & Hash -- should fail - final var password = randomAsciiAlphanumOfLength(10); + final var password = randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH); assertThat( client.putJson( apiPath(randomAsciiAlphanumOfLength(10)), diff --git a/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java index 06101426ad..33d2e04678 100644 --- a/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest.java @@ -32,7 +32,10 @@ public class InternalUsersScoreBasedPasswordRulesRestApiIntegrationTest extends AbstractApiIntegrationTest { @ClassRule - public static LocalCluster localCluster = clusterBuilder().nodeSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 9).build(); + public static LocalCluster localCluster = clusterBuilder().nodeSetting( + ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, + FIPS_MIN_PASSWORD_LENGTH + ).build(); String internalUsers(String... path) { final var fullPath = new StringJoiner("/").add(super.apiPath("internalusers")); @@ -53,14 +56,14 @@ ToXContentObject internalUserWithPassword(final String password) { @Test public void canNotCreateUsersWithPassword() throws Exception { try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - final var r1 = client.putJson(internalUsers("admin"), internalUserWithPassword("password89")); + final var r1 = client.putJson(internalUsers("admin"), internalUserWithPassword("password123456789")); assertThat(r1, isBadRequest()); assertThat( r1.getTextFromJsonBody("/reason"), org.hamcrest.Matchers.containsString(RequestContentValidator.ValidationError.WEAK_PASSWORD.message()) ); - final var r2 = client.putJson(internalUsers("admin"), internalUserWithPassword("A123456789")); + final var r2 = client.putJson(internalUsers("admin"), internalUserWithPassword("ABCDE123456789")); assertThat(r2, isBadRequest()); assertThat( r2.getTextFromJsonBody("/reason"), @@ -79,10 +82,10 @@ public void canNotCreateUsersWithPassword() throws Exception { @Test public void canCreateUserWithPassword() throws Exception { try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { - final var createdResp = client.putJson(internalUsers("str1234567"), internalUserWithPassword("s5tRx2r4bwex")); + final var createdResp = client.putJson(internalUsers("str1234567"), internalUserWithPassword("s5tRx2r4bwexYZ")); assertThat(createdResp, isCreated()); - final var patchResp = client.patch(internalUsers(), patch(addOp("str1234567", internalUserWithPassword("s5tRx2r4bwex")))); + final var patchResp = client.patch(internalUsers(), patch(addOp("str1234567", internalUserWithPassword("s5tRx2r4bwexYZ")))); assertThat(patchResp, isOk()); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java index 82fda7c18f..d53228589c 100644 --- a/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/RollbackVersionApiIntegrationTest.java @@ -11,6 +11,9 @@ package org.opensearch.security.api; +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -49,6 +52,14 @@ private String RollbackVersion(String versionId) { public void setupConfigVersionsIndex() throws Exception { try (TestRestClient client = localCluster.getRestClient(ADMIN_USER)) { assertThat(client.createUser(USER.getName(), USER), anyOf(isOk(), isCreated())); + // Version save triggered by user creation is async; wait until at least 2 versions + // exist so that rollback tests can find a previous version to roll back to. + Awaitility.await("at least 2 config versions persisted").atMost(15, TimeUnit.SECONDS).until(() -> { + var resp = client.get(ENDPOINT_PREFIX + "/versions"); + if (resp.getStatusCode() != 200) return false; + var versions = resp.bodyAsJsonNode().get("versions"); + return versions != null && versions.isArray() && versions.size() >= 2; + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java index 13274cb20c..9aa3bd0a8d 100644 --- a/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/ViewVersionApiIntegrationTest.java @@ -34,8 +34,10 @@ public class ViewVersionApiIntegrationTest extends AbstractApiIntegrationTest { + private static final String LIMITED_USER_PASSWORD = "limitedPass1234!"; + @Rule - public LocalCluster localCluster = clusterBuilder().users(new TestSecurityConfig.User("limitedUser").password("limitedPass")) + public LocalCluster localCluster = clusterBuilder().users(new TestSecurityConfig.User("limitedUser").password(LIMITED_USER_PASSWORD)) .nodeSetting(EXPERIMENTAL_SECURITY_CONFIGURATIONS_VERSIONS_ENABLED, true) .build(); @@ -108,7 +110,7 @@ public void testViewSpecificVersionNotFound() throws Exception { @Test public void testViewAllVersions_forbiddenWithoutAdminCert() throws Exception { - try (TestRestClient client = localCluster.getRestClient("limitedUser", "limitedPass")) { + try (TestRestClient client = localCluster.getRestClient("limitedUser", LIMITED_USER_PASSWORD)) { var response = client.get(viewVersionBase()); assertThat(response, anyOf(isUnauthorized(), isForbidden())); } diff --git a/src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java index 20cba1aa61..3da335a7fa 100644 --- a/src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java +++ b/src/integrationTest/java/org/opensearch/security/hash/Argon2DefaultConfigHashingTests.java @@ -15,10 +15,14 @@ import java.util.Map; import org.apache.http.HttpStatus; +import org.junit.Assume; import org.junit.ClassRule; import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.rules.RuleChain; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -31,7 +35,7 @@ public class Argon2DefaultConfigHashingTests extends HashingTests { private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS) .hash( generateArgon2Hash( - "secret", + TestSecurityConfig.DEFAULT_TEST_PASSWORD, ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY_DEFAULT, ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS_DEFAULT, ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_PARALLELISM_DEFAULT, @@ -41,8 +45,7 @@ public class Argon2DefaultConfigHashingTests extends HashingTests { ) ); - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + private static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) .anonymousAuth(false) @@ -56,6 +59,14 @@ public class Argon2DefaultConfigHashingTests extends HashingTests { ) .build(); + @ClassRule + public static final RuleChain rules = RuleChain.outerRule(new ExternalResource() { + @Override + protected void before() { + Assume.assumeFalse("Skipping Argon2 hashing tests: Argon2 is (yet) not FIPS-compliant", FipsMode.isEnabled()); + } + }).around(cluster); + @Test public void shouldAuthenticateWithCorrectPassword() { String hash = generateArgon2Hash( diff --git a/src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java index 344b4ad37d..5ee5608e69 100644 --- a/src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java +++ b/src/integrationTest/java/org/opensearch/security/hash/BCryptCustomConfigHashingTests.java @@ -19,12 +19,15 @@ import org.apache.http.HttpStatus; import org.awaitility.Awaitility; import org.junit.After; +import org.junit.Assume; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -53,10 +56,15 @@ public static Collection data() { return Arrays.asList(new Object[][] { { "A", 4 }, { "B", 6 }, { "Y", 10 }, { "A", 10 }, { "B", 4 }, { "Y", 6 } }); } + @BeforeClass + public static void skipInFipsMode() { + Assume.assumeFalse("Skipping BCrypt hashing tests: BCrypt is not FIPS-compliant", FipsMode.isEnabled()); + } + @Before public void startCluster() { TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS) - .hash(generateBCryptHash("secret", minor, rounds)); + .hash(generateBCryptHash(TestSecurityConfig.DEFAULT_TEST_PASSWORD, minor, rounds)); cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) @@ -76,7 +84,7 @@ public void startCluster() { .build(); cluster.before(); - try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), TestSecurityConfig.DEFAULT_TEST_PASSWORD)) { Awaitility.await() .alias("Load default configuration") .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); diff --git a/src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java index 86adc728ea..00c3b15ce4 100644 --- a/src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java +++ b/src/integrationTest/java/org/opensearch/security/hash/BCryptDefaultConfigHashingTests.java @@ -15,10 +15,14 @@ import java.util.Map; import org.apache.http.HttpStatus; +import org.junit.Assume; import org.junit.ClassRule; import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.junit.rules.RuleChain; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -30,8 +34,7 @@ public class BCryptDefaultConfigHashingTests extends HashingTests { private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + private static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) .anonymousAuth(false) @@ -40,6 +43,14 @@ public class BCryptDefaultConfigHashingTests extends HashingTests { ) .build(); + @ClassRule + public static final RuleChain rules = RuleChain.outerRule(new ExternalResource() { + @Override + protected void before() { + Assume.assumeFalse("Skipping BCrypt hashing tests: BCrypt is not FIPS-compliant", FipsMode.isEnabled()); + } + }).around(cluster); + @Test public void shouldAuthenticateWithCorrectPassword() { String hash = generateBCryptHash( diff --git a/src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java index e402db2293..51cec2d12a 100644 --- a/src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java +++ b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2CustomConfigHashingTests.java @@ -39,8 +39,6 @@ public class PBKDF2CustomConfigHashingTests extends HashingTests { private LocalCluster cluster; - private static final String PASSWORD = "top$ecret1234!"; - private final String function; private final int iterations; private final int length; @@ -65,9 +63,9 @@ public static Collection data() { @Before public void startCluster() { - TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS) - .hash(generatePBKDF2Hash("secret", function, iterations, length)); + .password(TestSecurityConfig.DEFAULT_TEST_PASSWORD) + .hash(generatePBKDF2Hash(TestSecurityConfig.DEFAULT_TEST_PASSWORD, function, iterations, length)); cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) @@ -89,7 +87,7 @@ public void startCluster() { .build(); cluster.before(); - try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), "secret")) { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), TestSecurityConfig.DEFAULT_TEST_PASSWORD)) { Awaitility.await() .alias("Load default configuration") .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); diff --git a/src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java index 2becee1f36..e1c551c53a 100644 --- a/src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java +++ b/src/integrationTest/java/org/opensearch/security/hash/PBKDF2DefaultConfigHashingTests.java @@ -31,12 +31,13 @@ public class PBKDF2DefaultConfigHashingTests extends HashingTests { private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS) .hash( generatePBKDF2Hash( - "secret", + TestSecurityConfig.DEFAULT_TEST_PASSWORD, ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT, ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT, ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT ) - ); + ) + .password(TestSecurityConfig.DEFAULT_TEST_PASSWORD); @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) diff --git a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java index 3bc6107ca6..c2cefde95c 100644 --- a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java @@ -33,10 +33,10 @@ import static org.hamcrest.Matchers.notNullValue; public class BasicAuthTests { - static final User TEST_USER = new User("test_user").password("s3cret"); + static final User TEST_USER = new User("test_user").password("s3cr3t-test-user-pw"); public static final String CUSTOM_ATTRIBUTE_NAME = "superhero"; - static final User SUPER_USER = new User("super-user").password("super-password").attr(CUSTOM_ATTRIBUTE_NAME, "true"); + static final User SUPER_USER = new User("super-user").password("super-secret-password").attr(CUSTOM_ATTRIBUTE_NAME, "true"); public static final String NOT_EXISTING_USER = "not-existing-user"; public static final String INVALID_PASSWORD = "secret-password"; diff --git a/src/integrationTest/java/org/opensearch/security/http/BasicWithAnonymousAuthTests.java b/src/integrationTest/java/org/opensearch/security/http/BasicWithAnonymousAuthTests.java index 4b5fd51477..c84194e4bd 100644 --- a/src/integrationTest/java/org/opensearch/security/http/BasicWithAnonymousAuthTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/BasicWithAnonymousAuthTests.java @@ -28,10 +28,10 @@ import static org.hamcrest.Matchers.notNullValue; public class BasicWithAnonymousAuthTests { - static final User TEST_USER = new User("test_user").password("s3cret"); + static final User TEST_USER = new User("test_user").password("s3cr3t-test-user-pw"); public static final String CUSTOM_ATTRIBUTE_NAME = "superhero"; - static final User SUPER_USER = new User("super-user").password("super-password").attr(CUSTOM_ATTRIBUTE_NAME, "true"); + static final User SUPER_USER = new User("super-user").password("super-secret-password").attr(CUSTOM_ATTRIBUTE_NAME, "true"); public static final String NOT_EXISTING_USER = "not-existing-user"; public static final String INVALID_PASSWORD = "secret-password"; diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index b3732685cb..85257f1858 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; import java.util.Map; @@ -72,11 +73,18 @@ public class OnBehalfOfJwtAuthenticationTest { "alternativeSigningKeyalternativeSigningKeyalternativeSigningKeyalternativeSigningKey".getBytes(StandardCharsets.UTF_8) ); - private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); + private static final String encryptionKey = fipsCompatibleEncryptionKey(); + + private static String fipsCompatibleEncryptionKey() { + final byte[] keyMaterial = new byte[32]; + new SecureRandom().nextBytes(keyMaterial); + return Base64.getEncoder().encodeToString(keyMaterial); + } + public static final String ADMIN_USER_NAME = "admin"; public static final String OBO_USER_NAME_WITH_PERM = "obo_user"; public static final String OBO_USER_NAME_NO_PERM = "obo_user_no_perm"; - public static final String DEFAULT_PASSWORD = "secret"; + public static final String DEFAULT_PASSWORD = TestSecurityConfig.DEFAULT_TEST_PASSWORD; public static final String NEW_PASSWORD = "testPassword123!!"; public static final String OBO_TOKEN_REASON = "{\"description\":\"Test generation\"}"; public static final String OBO_ENDPOINT_PREFIX = "_plugins/_security/api/obo/token"; diff --git a/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java b/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java index 1a930ae0bb..1b9bb5aa09 100644 --- a/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java @@ -27,8 +27,11 @@ import static org.hamcrest.Matchers.notNullValue; public class RolesMappingTests { - static final TestSecurityConfig.User USER_A = new TestSecurityConfig.User("userA").password("s3cret").backendRoles("mapsToRoleA"); - static final TestSecurityConfig.User USER_B = new TestSecurityConfig.User("userB").password("P@ssw0rd").backendRoles("mapsToRoleB"); + // passwords must be >=14 chars (112 bits) to satisfy FIPS PBKDF2 minimum + static final TestSecurityConfig.User USER_A = new TestSecurityConfig.User("userA").password("s3cr3t-user-a-pw") + .backendRoles("mapsToRoleA"); + static final TestSecurityConfig.User USER_B = new TestSecurityConfig.User("userB").password("P@ssw0rd-user-b-pw") + .backendRoles("mapsToRoleB"); private static final TestSecurityConfig.Role ROLE_A = new TestSecurityConfig.Role("roleA").clusterPermissions("cluster_all"); diff --git a/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java b/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java index 9a29151830..d4a37ab674 100644 --- a/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java @@ -16,6 +16,7 @@ import org.junit.Test; import org.junit.rules.RuleChain; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; @@ -94,7 +95,11 @@ public void shouldNotAuthenticateUserWithLdap() { response.assertStatusCode(401); } - logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException"); + if (FipsMode.isEnabled()) { + logsRule.assertThatStackTraceContain("org.bouncycastle.tls.TlsFatalAlert"); + } else { + logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException"); + } } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ApiTokenTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ApiTokenTest.java index d20dda198d..9fa593e730 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/ApiTokenTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/ApiTokenTest.java @@ -56,10 +56,7 @@ public class ApiTokenTest { private static final String OBO_DESCRIPTION = "{\"description\":\"Testing\", \"service\":\"self-issued\"}"; private static final String signingKey = Base64.getEncoder() .encodeToString("jwt signing key for an on behalf of token authentication backend for testing".getBytes(StandardCharsets.UTF_8)); - private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey".getBytes(StandardCharsets.UTF_8)); - public static final String ADMIN_USER_NAME = "admin"; - public static final String REGULAR_USER_NAME = "regular_user"; - public static final String DEFAULT_PASSWORD = "secret"; + private static final String encryptionKey = Base64.getEncoder().encodeToString("encryptionKey!!".getBytes(StandardCharsets.UTF_8)); public static final String NEW_PASSWORD = "testPassword123!!"; public static final String TEST_TOKEN_PAYLOAD = """ { @@ -99,7 +96,7 @@ public class ApiTokenTest { """; public static final String CURRENT_AND_NEW_PASSWORDS = "{ \"current_password\": \"" - + DEFAULT_PASSWORD + + TestSecurityConfig.DEFAULT_TEST_PASSWORD + "\", \"password\": \"" + NEW_PASSWORD + "\" }"; diff --git a/src/integrationTest/java/org/opensearch/security/ssl/util/SSLRequestHelperTests.java b/src/integrationTest/java/org/opensearch/security/ssl/util/SSLRequestHelperTests.java index 71c0642291..5f7c57455f 100644 --- a/src/integrationTest/java/org/opensearch/security/ssl/util/SSLRequestHelperTests.java +++ b/src/integrationTest/java/org/opensearch/security/ssl/util/SSLRequestHelperTests.java @@ -39,13 +39,13 @@ import org.junit.rules.TemporaryFolder; import org.bouncycastle.asn1.x509.CRLReason; import org.bouncycastle.cert.jcajce.JcaX509v2CRLBuilder; -import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.bouncycastle.operator.ContentSigner; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestRequest.Method; import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.support.PemKeyReader; import org.opensearch.test.framework.certificate.CertificateData; import org.opensearch.test.framework.certificate.TestCertificates; @@ -59,8 +59,8 @@ public class SSLRequestHelperTests { - private static final String STORE_NAME = CryptoServicesRegistrar.isInApprovedOnlyMode() ? "truststore.bcfks" : "truststore.jks"; - private static final String STORE_TYPE = CryptoServicesRegistrar.isInApprovedOnlyMode() ? "BCFKS" : "JKS"; + private static final String STORE_NAME = FipsMode.isEnabled() ? "truststore.bcfks" : "truststore.jks"; + private static final String STORE_TYPE = FipsMode.isEnabled() ? "BCFKS" : "JKS"; private static final char[] INTERNAL_STORE_PASSWORD = DEFAULT_STORE_PASSWORD.toCharArray(); /** Minimum TLS packet buffer size used when the engine reports a smaller value. */ diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index d24a20656a..ba6bc44ce9 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -31,8 +31,10 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -70,6 +72,7 @@ import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.cluster.OpenSearchClientProvider.UserCredentialsHolder; import org.opensearch.test.framework.data.TestIndex; import org.opensearch.transport.client.Client; @@ -95,10 +98,23 @@ */ public class TestSecurityConfig { + public static final String DEFAULT_TEST_PASSWORD; + + static { + byte[] bytes = new byte[16]; // satisfies BC FIPS 112-bit minimum + new SecureRandom().nextBytes(bytes); + DEFAULT_TEST_PASSWORD = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + } + private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); private static final PasswordHasher passwordHasher = PasswordHasherFactory.createPasswordHasher( - Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT).build() + Settings.builder() + .put( + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, + FipsMode.isEnabled() ? ConfigConstants.PBKDF2 : ConfigConstants.BCRYPT + ) + .build() ); private Config config = new Config(); @@ -500,7 +516,7 @@ public static final class User implements UserCredentialsHolder, ToXContentObjec public User(String name) { this.name = name; - this.password = "secret"; + this.password = DEFAULT_TEST_PASSWORD; } public User description(String description) { @@ -1314,7 +1330,7 @@ public SecurityDynamicConfiguration geActionGroupsConfiguration( } } - static String hashPassword(final String clearTextPassword) { + public static String hashPassword(final String clearTextPassword) { return passwordHasher.hash(clearTextPassword.toCharArray()); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/AlgorithmKit.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/AlgorithmKit.java index 60ae56410c..5d516443af 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/AlgorithmKit.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/AlgorithmKit.java @@ -77,7 +77,7 @@ private static void notEmptyAlgorithmName(String signatureAlgorithmName) { */ public static AlgorithmKit ecdsaSha256withEcdsa(Provider securityProvider, String ellipticCurve) { notEmptyAlgorithmName(ellipticCurve); - Supplier supplier = ecdsaKeyPairSupplier(requireNonNull(securityProvider, "Security provider is required"), ellipticCurve); + Supplier supplier = ecdsaKeyPairSupplier(securityProvider, ellipticCurve); return new AlgorithmKit(SIGNATURE_ALGORITHM_SHA_256_WITH_ECDSA, supplier); } @@ -119,7 +119,12 @@ public KeyPair generateKeyPair() { private static Supplier rsaKeyPairSupplier(Provider securityProvider, int keySize) { try { - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", securityProvider); + KeyPairGenerator generator; + if (securityProvider == null) { + generator = KeyPairGenerator.getInstance("RSA"); + } else { + generator = KeyPairGenerator.getInstance("RSA", securityProvider); + } log.info("Initialize key pair generator with keySize: {}", keySize); generator.initialize(keySize); return generator::generateKeyPair; @@ -132,7 +137,12 @@ private static Supplier rsaKeyPairSupplier(Provider securityProvider, i private static Supplier ecdsaKeyPairSupplier(Provider securityProvider, String ellipticCurve) { try { - KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", securityProvider); + KeyPairGenerator generator; + if (securityProvider == null) { + generator = KeyPairGenerator.getInstance("EC"); + } else { + generator = KeyPairGenerator.getInstance("EC", securityProvider); + } log.info("Initialize key pair generator with elliptic curve: {}", ellipticCurve); ECGenParameterSpec ecsp = new ECGenParameterSpec(ellipticCurve); generator.initialize(ecsp); diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuer.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuer.java index e55d9e2735..276a7e73df 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuer.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuer.java @@ -30,7 +30,6 @@ import java.security.KeyPair; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.Provider; import java.security.PublicKey; import java.util.Calendar; import java.util.Date; @@ -86,12 +85,10 @@ class CertificatesIssuer { private static final AtomicLong ID_COUNTER = new AtomicLong(System.currentTimeMillis()); - private final Provider securityProvider; private final AlgorithmKit algorithmKit; private final JcaX509ExtensionUtils extUtils; - CertificatesIssuer(Provider securityProvider, AlgorithmKit algorithmKit) { - this.securityProvider = securityProvider; + CertificatesIssuer(AlgorithmKit algorithmKit) { this.algorithmKit = algorithmKit; this.extUtils = getExtUtils(); } @@ -164,7 +161,7 @@ private X509CertificateHolder buildCertificateHolder( } private ContentSigner createContentSigner(PrivateKey privateKey) throws OperatorCreationException { - return new JcaContentSignerBuilder(algorithmKit.getSignatureAlgorithmName()).setProvider(securityProvider).build(privateKey); + return new JcaContentSignerBuilder(algorithmKit.getSignatureAlgorithmName()).build(privateKey); } private void addExtendedKeyUsageExtension(X509v3CertificateBuilder builder, CertificateMetadata certificateMetadata) diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuerFactory.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuerFactory.java index 8d7c43c69e..45ae8f6cb2 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuerFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/CertificatesIssuerFactory.java @@ -10,9 +10,6 @@ package org.opensearch.test.framework.certificate; import java.security.Provider; -import java.util.Optional; - -import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; import static org.opensearch.test.framework.certificate.AlgorithmKit.ecdsaSha256withEcdsa; import static org.opensearch.test.framework.certificate.AlgorithmKit.rsaSha256withRsa; @@ -30,8 +27,6 @@ private CertificatesIssuerFactory() { } - private static final Provider DEFAULT_SECURITY_PROVIDER = new BouncyCastleFipsProvider();; - /** * @see {@link #rsaBaseCertificateIssuer(Provider)} */ @@ -45,8 +40,7 @@ public static CertificatesIssuer rsaBaseCertificateIssuer() { * @return new instance of {@link CertificatesIssuer} */ public static CertificatesIssuer rsaBaseCertificateIssuer(Provider securityProvider) { - Provider provider = Optional.ofNullable(securityProvider).orElse(DEFAULT_SECURITY_PROVIDER); - return new CertificatesIssuer(provider, rsaSha256withRsa(provider, KEY_SIZE)); + return new CertificatesIssuer(rsaSha256withRsa(securityProvider, KEY_SIZE)); } /** @@ -62,7 +56,6 @@ public static CertificatesIssuer ecdsaBaseCertificatesIssuer() { * @return new instance of {@link CertificatesIssuer} */ public static CertificatesIssuer ecdsaBaseCertificatesIssuer(Provider securityProvider) { - Provider provider = Optional.ofNullable(securityProvider).orElse(DEFAULT_SECURITY_PROVIDER); - return new CertificatesIssuer(provider, ecdsaSha256withEcdsa(securityProvider, "P-384")); + return new CertificatesIssuer(ecdsaSha256withEcdsa(securityProvider, "P-384")); } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/PemConverter.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/PemConverter.java index 749ab232bc..4f1b84f7bc 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/PemConverter.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/PemConverter.java @@ -109,7 +109,7 @@ private static PemObject createPkcs8PrivateKeyPem(PrivateKey privateKey, String private static OutputEncryptor getPasswordEncryptor(String password) throws OperatorCreationException { if (!Strings.isNullOrEmpty(password)) { - JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES); + JceOpenSSLPKCS8EncryptorBuilder encryptorBuilder = new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.AES_256_CBC); encryptorBuilder.setRandom(secureRandom); encryptorBuilder.setPassword(password.toCharArray()); return encryptorBuilder.build(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java index 0fd75b08a1..0fff83ffc5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/CloseableHttpClientFactory.java @@ -24,6 +24,8 @@ import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.core5.http.io.SocketConfig; +import static org.opensearch.security.ssl.util.SSLConfigConstants.ALLOWED_SSL_PROTOCOLS; + class CloseableHttpClientFactory { private final SSLContext sslContext; @@ -52,7 +54,7 @@ public CloseableHttpClient getHTTPClient() { final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( this.sslContext, - null, + ALLOWED_SSL_PROTOCOLS, supportedCipherSuites, NoopHostnameVerifier.INSTANCE ); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java index 34a105ea39..fb7760fac4 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/MinimumSecuritySettingsSupplierFactory.java @@ -32,6 +32,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.test.framework.certificate.TestCertificates; public class MinimumSecuritySettingsSupplierFactory { @@ -83,6 +84,9 @@ private Settings.Builder minimumOpenSearchSettingsBuilder(int node, boolean sslO testCertificates.getNodeKey(node, PRIVATE_KEY_HTTP_PASSWORD).getAbsolutePath() ); builder.put("plugins.security.ssl.http.pemkey_password", PRIVATE_KEY_HTTP_PASSWORD); + if (FipsMode.isEnabled()) { + builder.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.PBKDF2); + } if (sslOnly == false) { builder.put(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, false); builder.putList("plugins.security.authcz.admin_dn", testCertificates.getAdminDNs()); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java index 37005a0272..15d3fb659c 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/OpenSearchClientProvider.java @@ -66,12 +66,11 @@ import org.opensearch.client.RestClientBuilder; import org.opensearch.client.RestHighLevelClient; import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.support.PemKeyReader; import org.opensearch.test.framework.certificate.CertificateData; import org.opensearch.test.framework.certificate.TestCertificates; -import reactor.netty.http.HttpProtocol; - import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_TYPE; import static org.opensearch.test.framework.cluster.TestRestClientConfiguration.getBasicAuthHeader; @@ -233,7 +232,7 @@ default TestRestClient getRestClient(CertificateData useCertificateData, Header. /** * Returns a generic HTTP/1.1/HTTP 2.0/HTTP 3.0 client. */ - default ReactorHttpClient getGenericClient(HttpProtocol protocol, boolean secure, Settings settings) { + default ReactorHttpClient getGenericClient(Settings settings) { return new ReactorHttpClient(true, true, settings, getHttpAddress()); } @@ -294,7 +293,12 @@ private SSLContext getSSLContext(CertificateData useCertificateData) { if (useCertificateData != null) { Certificate[] chainOfTrust = { useCertificateData.certificate() }; ks.setKeyEntry("admin-certificate", useCertificateData.getKey(), null, chainOfTrust); - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + KeyManagerFactory keyManagerFactory; + if (FipsMode.isEnabled()) { + keyManagerFactory = KeyManagerFactory.getInstance("PKIX", "BCJSSE"); + } else { + keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + } keyManagerFactory.init(ks, null); keyManagers = keyManagerFactory.getKeyManagers(); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/ReactorHttpClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/ReactorHttpClient.java index 3f91b1f36f..584168f2bf 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/ReactorHttpClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/ReactorHttpClient.java @@ -19,10 +19,15 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; import org.opensearch.http.netty4.http3.Http3Utils; +import org.opensearch.security.support.FipsMode; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -38,7 +43,14 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior; +import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.resolver.DefaultAddressResolverGroup; import reactor.core.publisher.Flux; @@ -50,13 +62,13 @@ import reactor.netty.http.HttpProtocol; import reactor.netty.http.client.HttpClient; -import static org.opensearch.http.HttpTransportSettings.SETTING_HTTP_HTTP3_ENABLED; import static org.opensearch.http.HttpTransportSettings.SETTING_HTTP_MAX_CONTENT_LENGTH; /** * Tiny helper to send http requests over netty. */ public class ReactorHttpClient implements Closeable { + private static final Logger LOG = LogManager.getLogger(ReactorHttpClient.class); private static final java.util.Random RAND = new java.util.Random(); private final boolean compression; @@ -66,9 +78,19 @@ public class ReactorHttpClient implements Closeable { private final InetSocketAddress remoteAddress; public ReactorHttpClient(boolean compression, boolean secure, Settings settings, InetSocketAddress remoteAddress) { + this(compression, secure, selectSupportedProtocol(secure), settings, remoteAddress); + } + + public ReactorHttpClient( + boolean compression, + boolean secure, + HttpProtocol protocol, + Settings settings, + InetSocketAddress remoteAddress + ) { this.compression = compression; this.secure = secure; - this.protocol = randomProtocol(secure, settings); + this.protocol = protocol; this.settings = settings; this.remoteAddress = remoteAddress; } @@ -127,6 +149,7 @@ private List sendRequests( ) ) ) + .doOnError(e -> LOG.warn("Request failed [protocol={}]: {}", protocol, e.getMessage(), e)) ) .toArray(Mono[]::new); @@ -150,22 +173,47 @@ private HttpClient createClient(final InetSocketAddress remoteAddress, final Eve if (secure) { if (protocol == HttpProtocol.HTTP11) { + Consumer http11Configure = s -> { + if (FipsMode.isEnabled()) { + s.sslProvider(SslProvider.JDK); + } + s.clientAuth(ClientAuth.NONE).trustManager(InsecureTrustManagerFactory.INSTANCE); + }; return client.protocol(protocol) .secure( - spec -> spec.sslContext( - Http11SslContextSpec.forClient() - .configure(s -> s.clientAuth(ClientAuth.NONE).trustManager(InsecureTrustManagerFactory.INSTANCE)) - ).handshakeTimeout(Duration.ofSeconds(30)) + spec -> spec.sslContext(Http11SslContextSpec.forClient().configure(http11Configure)) + .handshakeTimeout(Duration.ofSeconds(30)) ); } else if (protocol == HttpProtocol.H2) { - return client.protocol(new HttpProtocol[] { HttpProtocol.HTTP11, HttpProtocol.H2 }) + Consumer h2Configure = s -> { + if (FipsMode.isEnabled()) { + s.sslProvider(SslProvider.JDK) + .applicationProtocolConfig( + new ApplicationProtocolConfig( + Protocol.ALPN, + SelectorFailureBehavior.NO_ADVERTISE, + SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2 + ) + ); + } + s.clientAuth(ClientAuth.NONE).trustManager(InsecureTrustManagerFactory.INSTANCE); + }; + HttpProtocol[] h2Protocols = FipsMode.isEnabled() + ? new HttpProtocol[] { HttpProtocol.H2 } + : new HttpProtocol[] { HttpProtocol.HTTP11, HttpProtocol.H2 }; + return client.protocol(h2Protocols) .secure( - spec -> spec.sslContext( - Http2SslContextSpec.forClient() - .configure(s -> s.clientAuth(ClientAuth.NONE).trustManager(InsecureTrustManagerFactory.INSTANCE)) - ).handshakeTimeout(Duration.ofSeconds(30)) + spec -> spec.sslContext(Http2SslContextSpec.forClient().configure(h2Configure)) + .handshakeTimeout(Duration.ofSeconds(30)) ); } else { + if (FipsMode.isEnabled()) { + throw new IllegalStateException( + "HTTP/3 requires BoringSSL which is not built from the FIPS-validated branch in this distribution; " + + "substitute a FIPS-certified BoringSSL build to enable HTTP/3 in FIPS mode" + ); + } return client.protocol(protocol) .secure( spec -> spec.sslContext( @@ -181,6 +229,11 @@ private HttpClient createClient(final InetSocketAddress remoteAddress, final Eve ); } } else { + if (FipsMode.isEnabled()) { + throw new IllegalStateException( + "Plaintext connections are not permitted in FIPS mode; TLS is required to engage the FIPS security providers" + ); + } if (protocol == HttpProtocol.HTTP11) { return client.protocol(protocol); } else { @@ -198,15 +251,22 @@ public HttpProtocol protocol() { return protocol; } - private static HttpProtocol randomProtocol(boolean secure, Settings settings) { - HttpProtocol[] values = null; + private static HttpProtocol selectSupportedProtocol(boolean secure) { + if (FipsMode.isEnabled() && !secure) { + throw new IllegalStateException( + "Plaintext connections are not permitted in FIPS mode; TLS is required to engage the FIPS security providers" + ); + } + + HttpProtocol[] values; if (secure) { - if (Http3Utils.isHttp3Available() && SETTING_HTTP_HTTP3_ENABLED.get(settings).booleanValue() == true) { - values = new HttpProtocol[] { HttpProtocol.HTTP11, HttpProtocol.H2, HttpProtocol.HTTP3 }; - } else { - values = new HttpProtocol[] { HttpProtocol.HTTP11, HttpProtocol.H2 }; - } + // In FIPS mode, exclude HTTP/3: the bundled BoringSSL (used by QUIC) is not built from the + // FIPS-validated branch and bypasses JSSE/BC FIPS entirely. This is a build-level constraint — + // a FIPS-certified BoringSSL substituted at the OS level would re-enable HTTP/3 in FIPS mode. + values = Http3Utils.isHttp3Available() && !FipsMode.isEnabled() + ? new HttpProtocol[] { HttpProtocol.HTTP11, HttpProtocol.H2, HttpProtocol.HTTP3 } + : new HttpProtocol[] { HttpProtocol.HTTP11, HttpProtocol.H2 }; } else { values = new HttpProtocol[] { HttpProtocol.HTTP11, HttpProtocol.H2C }; } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 973721ebdf..6eb74e0ad1 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -46,6 +46,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; +import java.util.function.BooleanSupplier; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; @@ -60,6 +61,7 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.search.QueryCachingPolicy; import org.apache.lucene.search.Weight; +import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.bouncycastle.util.encoders.Hex; import org.opensearch.OpenSearchException; @@ -220,6 +222,7 @@ import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.state.SecurityMetadata; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.support.GuardedSearchOperationWrapper; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.ModuleInfo; @@ -362,21 +365,53 @@ private boolean sslCertificatesHotReloadEnabled(final Settings settings) { return settings.getAsBoolean(SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED, false); } - static void validateFipsMode(final String fipsModeEnvValue, final Settings settings) { - if ("true".equalsIgnoreCase(fipsModeEnvValue)) { + static void validateFipsMode(final Settings settings) { + validateFipsMode(settings, FipsMode.isEnabled(), CryptoServicesRegistrar::isInApprovedOnlyMode); + } + + static void validateFipsMode(final Settings settings, final boolean isFipsMode, final BooleanSupplier isInApprovedOnlyMode) { + if (isFipsMode) { + List violations = new ArrayList<>(); + + if (!isInApprovedOnlyMode.getAsBoolean()) { + violations.add( + "BCFIPS security provider is not running in FIPS approved-only mode. Ensure cluster is starting with '-Dorg.bouncycastle.fips.approved_only=true'" + ); + } + String hashingAlgorithm = settings.get( ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT ); if (!ConfigConstants.PBKDF2.equalsIgnoreCase(hashingAlgorithm)) { - throw new IllegalStateException( - "FIPS mode is enabled (OPENSEARCH_FIPS_MODE=true) but password hashing algorithm is set to '" + violations.add( + "Password hashing algorithm is set to '" + hashingAlgorithm + "'. Only PBKDF2 is allowed in FIPS mode. Set '" + ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM + "' to 'pbkdf2'. Note: changing the hashing algorithm requires all existing passwords to be rehashed." ); } + + // A configured minimum below that floor would let the API accept passwords the hasher then refuses at runtime. + final int minPasswordLength = settings.getAsInt(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, -1); + if (minPasswordLength >= 0 && minPasswordLength < PasswordValidator.FIPS_MIN_PASSWORD_LENGTH) { + violations.add( + "Password minimum length '%s' is set to %d, but FIPS mode requires at least %d characters because shorter passwords cannot be hashed with PBKDF2." + .formatted( + ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, + minPasswordLength, + PasswordValidator.FIPS_MIN_PASSWORD_LENGTH + ) + ); + } + + if (!violations.isEmpty()) { + throw new IllegalStateException( + "FIPS mode is enabled (OPENSEARCH_FIPS_MODE=true) but the following configuration issues were found:\n- " + + String.join("\n- ", violations) + ); + } } } @@ -512,7 +547,7 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) ); } - validateFipsMode(System.getenv("OPENSEARCH_FIPS_MODE"), settings); + validateFipsMode(settings); if (!client && !settings.getAsBoolean(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false)) { // check for demo certificates @@ -1258,7 +1293,7 @@ public Collection createComponents( threadPool.getThreadContext() ); this.roleMapper = roleMapper; - tokenManager = new SecurityTokenManager(cs, threadPool, userService, roleMapper); + tokenManager = new SecurityTokenManager(cs, threadPool, userService, roleMapper, this.configPath); apiTokenRepository = new ApiTokenRepository(localClient, clusterService); PrivilegesConfiguration privilegesConfiguration = new PrivilegesConfiguration( @@ -1452,6 +1487,9 @@ public Settings additionalSettings() { if (!SSLConfig.isSslOnlyMode()) { builder.put(NetworkModule.TRANSPORT_TYPE_KEY, "org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport"); builder.put(NetworkModule.HTTP_TYPE_KEY, "org.opensearch.security.http.SecurityHttpServerTransport"); + if (FipsMode.isEnabled() && !settings.hasValue(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH)) { + builder.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, PasswordValidator.FIPS_MIN_PASSWORD_LENGTH); + } } return builder.build(); } diff --git a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java index 52604f4675..b5493e8950 100644 --- a/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java +++ b/src/main/java/org/opensearch/security/action/apitokens/ApiTokenRepository.java @@ -30,6 +30,7 @@ import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchSecurityException; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.Randomness; import org.opensearch.core.action.ActionListener; import org.opensearch.index.IndexNotFoundException; import org.opensearch.security.configuration.TokenListener; @@ -41,7 +42,7 @@ public class ApiTokenRepository { public static final String TOKEN_PREFIX = "os_"; - private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final SecureRandom SECURE_RANDOM = Randomness.createSecure(); private final ApiTokenIndexHandler apiTokenIndexHandler; private final List tokenListener = new ArrayList<>(); diff --git a/src/main/java/org/opensearch/security/auditlog/sink/ExternalOpenSearchSink.java b/src/main/java/org/opensearch/security/auditlog/sink/ExternalOpenSearchSink.java index 744e2ab46e..f3546433c3 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/ExternalOpenSearchSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/ExternalOpenSearchSink.java @@ -155,7 +155,7 @@ public ExternalOpenSearchSink( ); } - effectiveKeyPassword = PemKeyReader.randomChars(12); + effectiveKeyPassword = SSLConfigConstants.DEFAULT_STORE_PASSWORD.toCharArray(); effectiveKeyAlias = "al"; effectiveTruststore = PemKeyReader.toTruststore(effectiveKeyAlias, trustCertificates); effectiveKeystore = PemKeyReader.toKeystore( diff --git a/src/main/java/org/opensearch/security/auth/http/jwt/keybyoidc/JwtVerifier.java b/src/main/java/org/opensearch/security/auth/http/jwt/keybyoidc/JwtVerifier.java index de2f47a3f3..c42e68c4e9 100644 --- a/src/main/java/org/opensearch/security/auth/http/jwt/keybyoidc/JwtVerifier.java +++ b/src/main/java/org/opensearch/security/auth/http/jwt/keybyoidc/JwtVerifier.java @@ -115,7 +115,7 @@ private JWSVerifier getInitializedSignatureVerifier(JWK key, SignedJWT jwt) thro } if (result == null) { - throw new BadCredentialsException("Cannot verify JWT"); + throw new BadCredentialsException("Cannot verify JWT - unsupported key type: " + key.getClass().getName()); } else { return result; } diff --git a/src/main/java/org/opensearch/security/auth/http/kerberos/HTTPSpnegoAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/kerberos/HTTPSpnegoAuthenticator.java index ec10c74a1f..0fb5c8fbb5 100644 --- a/src/main/java/org/opensearch/security/auth/http/kerberos/HTTPSpnegoAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/kerberos/HTTPSpnegoAuthenticator.java @@ -19,6 +19,7 @@ import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HashMap; @@ -27,6 +28,7 @@ import java.util.Optional; import java.util.Set; import javax.security.auth.Subject; +import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import com.google.common.base.Strings; @@ -78,12 +80,7 @@ public HTTPSpnegoAuthenticator(final Settings settings, final Path configPath) { try { if (settings.getAsBoolean("krb_debug", false)) { JaasKrbUtil.setDebug(true); - System.setProperty("sun.security.krb5.debug", "true"); - System.setProperty("java.security.debug", "gssloginconfig,logincontext,configparser,configfile"); - System.setProperty("sun.security.spnego.debug", "true"); - log.info("Kerberos debug is enabled on stdout"); - } else { - log.debug("Kerberos debug is NOT enabled"); + log.info("Kerberos debug is enabled"); } } catch (Throwable e) { log.error("Unable to enable krb_debug due to ", e); @@ -181,14 +178,16 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { log.warn("No 'Negotiate Authorization' header, send 401 and 'WWW-Authenticate Negotiate'"); return null; } else { - final byte[] decodedNegotiateHeader = Base64.getDecoder().decode(authorizationHeader.substring(10)); - + byte[] decodedNegotiateHeader = null; GSSContext gssContext = null; + LoginContext loginContext = null; byte[] outToken = null; try { + decodedNegotiateHeader = Base64.getDecoder().decode(authorizationHeader.substring(10)); - final Subject subject = JaasKrbUtil.loginUsingKeytab(acceptorPrincipal, acceptorKeyTabPath, false); + loginContext = JaasKrbUtil.loginUsingKeytabWithContext(acceptorPrincipal, acceptorKeyTabPath, false); + final Subject subject = loginContext.getSubject(); final GSSManager manager = GSSManager.getInstance(); final int credentialLifetime = GSSCredential.INDEFINITE_LIFETIME; @@ -225,6 +224,9 @@ public GSSCredential run() throws GSSException { } return null; } finally { + if (decodedNegotiateHeader != null) { + Arrays.fill(decodedNegotiateHeader, (byte) 0); + } if (gssContext != null) { try { gssContext.dispose(); @@ -232,6 +234,7 @@ public GSSCredential run() throws GSSException { // Ignore } } + JaasKrbUtil.logout(loginContext); } if (principal == null) { @@ -241,10 +244,7 @@ public GSSCredential run() throws GSSException { final String username = ((SimpleUserPrincipal) principal).getName(); if (username == null || username.length() == 0) { - log.error( - "Got empty or null user from kerberos. Normally this means that you acceptor principal {} does not match the server hostname", - acceptorPrincipal - ); + log.error("Got empty or null user from kerberos. The configured acceptor principal may not match the server hostname"); } return new AuthCredentials(username, (Object) outToken).markComplete(); diff --git a/src/main/java/org/opensearch/security/auth/http/kerberos/util/JaasKrbUtil.java b/src/main/java/org/opensearch/security/auth/http/kerberos/util/JaasKrbUtil.java index 182eafe999..35c68900d5 100644 --- a/src/main/java/org/opensearch/security/auth/http/kerberos/util/JaasKrbUtil.java +++ b/src/main/java/org/opensearch/security/auth/http/kerberos/util/JaasKrbUtil.java @@ -27,11 +27,16 @@ import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + /** * JAAS utilities for Kerberos login. */ public final class JaasKrbUtil { + private static final Logger log = LogManager.getLogger(JaasKrbUtil.class); + private static boolean debug = false; private JaasKrbUtil() {} @@ -40,8 +45,11 @@ public static void setDebug(final boolean debug) { JaasKrbUtil.debug = debug; } - public static Subject loginUsingKeytab(final Set principalAsStrings, final Path keytabPath, final boolean initiator) - throws LoginException { + public static LoginContext loginUsingKeytabWithContext( + final Set principalAsStrings, + final Path keytabPath, + final boolean initiator + ) throws LoginException { final Set principals = new HashSet(); for (String p : principalAsStrings) { @@ -54,7 +62,23 @@ public static Subject loginUsingKeytab(final Set principalAsStrings, fin final String confName = "KeytabConf"; final LoginContext loginContext = new LoginContext(confName, subject, null, conf); loginContext.login(); - return loginContext.getSubject(); + return loginContext; + } + + /** + * Safely logout and clean up a LoginContext. + * This method should be called in a 'finally' block to ensure credentials are cleared. + * + * @param loginContext The LoginContext to logout + */ + public static void logout(final LoginContext loginContext) { + if (loginContext != null) { + try { + loginContext.logout(); + } catch (LoginException e) { + log.debug("Logout failed, credentials may not have been fully cleared", e); + } + } } public static Configuration useKeytab(final String principal, final Path keytabPath, final boolean initiator) { diff --git a/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java index 6ccead820e..0d39ef030c 100644 --- a/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java @@ -110,8 +110,7 @@ public User authenticate(AuthenticationContext context) { if (!internalUsersModel.exists(credentials.getUsername())) { userExists = false; password = credentials.getPassword(); - hash = "$2y$12$NmKhjNssNgSIj8iXT7SYxeXvMA1E95a9tCt4cySY9FrQ4fB18xEc2"; // Ensure the same cryptographic complexity for users not - // found and invalid password + hash = passwordHasher.getDummyHash(); // Ensure the same cryptographic complexity for users not found and invalid password } else { userExists = true; password = credentials.getPassword(); diff --git a/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthenticationBackend.java index 17000c7eb2..8f78528cfa 100755 --- a/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthenticationBackend.java @@ -309,7 +309,7 @@ private static LdapEntry existsSearchingAllBases( ); if (isDebugEnabled) { - log.debug("Results for LDAP search for " + user + " in base " + entry.getKey() + ":\n" + result); + log.debug("Results for LDAP search for {} in base {}:\n{}", user, entry.getKey(), foundEntries); } if (foundEntries != null) { diff --git a/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthorizationBackend.java b/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthorizationBackend.java index 0dbf183fa4..64e207290d 100755 --- a/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthorizationBackend.java +++ b/src/main/java/org/opensearch/security/auth/ldap/backend/LDAPAuthorizationBackend.java @@ -13,6 +13,8 @@ import java.io.FileNotFoundException; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.security.KeyStore; @@ -36,6 +38,8 @@ import javax.naming.ldap.LdapName; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableList; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -48,6 +52,8 @@ import org.opensearch.security.auth.ldap.util.ConfigConstants; import org.opensearch.security.auth.ldap.util.LdapHelper; import org.opensearch.security.auth.ldap.util.Utils; +import org.opensearch.security.auth.ldap2.SNISettingTLSSocketFactory; +import org.opensearch.security.auth.ldap2.SocketFactoryClassLoader; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.PemKeyReader; import org.opensearch.security.support.WildcardMatcher; @@ -68,6 +74,7 @@ import org.ldaptive.SearchFilter; import org.ldaptive.SearchScope; import org.ldaptive.control.RequestControl; +import org.ldaptive.provider.ProviderConfig; import org.ldaptive.provider.ProviderConnection; import org.ldaptive.sasl.Mechanism; import org.ldaptive.sasl.SaslConfig; @@ -76,7 +83,6 @@ import org.ldaptive.ssl.CredentialConfig; import org.ldaptive.ssl.CredentialConfigFactory; import org.ldaptive.ssl.SslConfig; -import org.ldaptive.ssl.ThreadLocalTLSSocketFactory; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_TRANSPORT_KEYSTORE_PASSWORD; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_TRANSPORT_TRUSTSTORE_PASSWORD; @@ -86,7 +92,7 @@ public class LDAPAuthorizationBackend implements AuthorizationBackend { private static final AtomicInteger CONNECTION_COUNTER = new AtomicInteger(); private static final String COM_SUN_JNDI_LDAP_OBJECT_DISABLE_ENDPOINT_IDENTIFICATION = "com.sun.jndi.ldap.object.disableEndpointIdentification"; - private static final List DEFAULT_TLS_PROTOCOLS = Arrays.asList("TLSv1.2", "TLSv1.1"); + private static final List DEFAULT_TLS_PROTOCOLS = ImmutableList.copyOf(SSLConfigConstants.ALLOWED_SSL_PROTOCOLS); static final int ONE_PLACEHOLDER = 1; static final int TWO_PLACEHOLDER = 2; static final String DEFAULT_ROLEBASE = ""; @@ -129,7 +135,7 @@ public static void checkConnection(final ConnectionConfig connectionConfig, Stri ClassLoader originalClassloader = null; if (isJava9OrHigher) { originalClassloader = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(new Java9CL()); + Thread.currentThread().setContextClassLoader(new SocketFactoryClassLoader()); } checkConnection0(connectionConfig, bindDn, password, originalClassloader, isJava9OrHigher); @@ -144,7 +150,7 @@ public static Connection getConnection(final Settings settings, final Path confi ClassLoader originalClassloader = null; if (isJava9OrHigher) { originalClassloader = Thread.currentThread().getContextClassLoader(); - Thread.currentThread().setContextClassLoader(new Java9CL()); + Thread.currentThread().setContextClassLoader(new SocketFactoryClassLoader()); } return getConnection0(settings, configPath, originalClassloader, isJava9OrHigher); @@ -183,7 +189,7 @@ private static void checkConnection0( byte[] password, final ClassLoader cl, final boolean needRestore - ) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException, LdapException { + ) throws LdapException { Connection connection = null; @@ -201,9 +207,29 @@ private static void checkConnection0( config.setConnectionInitializer(new BindConnectionInitializer(bindDn, new Credential(password))); DefaultConnectionFactory connFactory = new DefaultConnectionFactory(config); - connection = connFactory.getConnection(); - connection.open(); + String sniHostname = null; + if (config.getLdapUrl() != null && config.getLdapUrl().startsWith("ldaps:")) { + configureSNISocketFactory(connFactory); + String ldapUrl = config.getLdapUrl(); + try { + sniHostname = new URI(ldapUrl).getHost(); + if (StringUtils.isBlank(sniHostname)) { + log.warn("Could not extract hostname from LDAP URL '{}'; proceeding without SNI configuration", ldapUrl); + } + } catch (URISyntaxException e) { + log.warn("Malformed LDAP URL '{}'; proceeding without SNI configuration: {}", ldapUrl, e.getMessage()); + } + } + + try ( + SNISettingTLSSocketFactory.SniContext ignored = StringUtils.isBlank(sniHostname) + ? null + : SNISettingTLSSocketFactory.configure(sniHostname) + ) { + connection = connFactory.getConnection(); + connection.open(); + } } finally { Utils.unbindAndCloseSilently(connection); connection = null; @@ -302,9 +328,20 @@ private static Connection getConnection0( } DefaultConnectionFactory connFactory = new DefaultConnectionFactory(config); - connection = connFactory.getConnection(); - connection.open(); + // Register custom socket factory for SNI hostname verification with BouncyCastle FIPS + // This addresses a known issue where JNDI LDAP doesn't pass hostname to SSLSocketFactory + // See: https://github.com/bcgit/bc-java/issues/460 + if (enableSSL) { + configureSNISocketFactory(connFactory); + try (var ignored = SNISettingTLSSocketFactory.configure(split[0])) { + connection = connFactory.getConnection(); + connection.open(); + } + } else { + connection = connFactory.getConnection(); + connection.open(); + } if (connection != null && connection.isOpen()) { break; @@ -462,6 +499,19 @@ private static void restoreClassLoader0(final ClassLoader cl) { } } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static boolean shouldVerifyHostname(SslConfig sslConf) { + return sslConf != null && !(sslConf.getHostnameVerifier() instanceof AllowAnyHostnameVerifier); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static void configureSNISocketFactory(DefaultConnectionFactory connFactory) { + Map props = new HashMap<>(); + props.put("java.naming.ldap.factory.socket", "org.opensearch.security.auth.ldap2.SNISettingTLSSocketFactory"); + final ProviderConfig providerConfig = connFactory.getProvider().getProviderConfig(); + providerConfig.setProperties(props); + } + private static void configureSSL(final ConnectionConfig config, final Settings settings, final Path configPath) throws Exception { final boolean isDebugEnabled = log.isDebugEnabled(); @@ -528,7 +578,21 @@ private static void configureSSL(final ConnectionConfig config, final Settings s ); } - cc = CredentialConfigFactory.createX509CredentialConfig(trustCertificates, authenticationCertificate, authenticationKey); + final KeyStore pemTruststore = PemKeyReader.toTruststore("al", trustCertificates); + final char[] pemInMemoryPassword = SSLConfigConstants.DEFAULT_STORE_PASSWORD.toCharArray(); + final KeyStore pemKeystore = PemKeyReader.toKeystore( + "al", + pemInMemoryPassword, + authenticationCertificate != null ? new X509Certificate[] { authenticationCertificate } : null, + authenticationKey + ); + cc = CredentialConfigFactory.createKeyStoreCredentialConfig( + pemTruststore, + null, + pemKeystore, + pemKeystore != null ? new String(pemInMemoryPassword) : null, + null + ); if (isDebugEnabled) { log.debug("Use PEM to secure communication with LDAP server (client auth is {})", authenticationKey != null); @@ -604,9 +668,6 @@ private static void configureSSL(final ConnectionConfig config, final Settings s // https://www.oracle.com/technetwork/java/javase/10-0-2-relnotes-4477557.html // https://www.oracle.com/technetwork/java/javase/11-0-1-relnotes-5032023.html } - - System.setProperty(COM_SUN_JNDI_LDAP_OBJECT_DISABLE_ENDPOINT_IDENTIFICATION, "true"); - } final List enabledCipherSuites = settings.getAsList(ConfigConstants.LDAPS_ENABLED_SSL_CIPHERS, Collections.emptyList()); @@ -1154,31 +1215,4 @@ private String getRoleFromEntry(final Connection ldapConnection, final LdapName return null; } - - @SuppressWarnings("rawtypes") - private final static Class clazz = ThreadLocalTLSSocketFactory.class; - - private final static class Java9CL extends ClassLoader { - - public Java9CL() { - super(); - } - - @SuppressWarnings("unused") - public Java9CL(ClassLoader parent) { - super(parent); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Class loadClass(String name) throws ClassNotFoundException { - - if (!name.equalsIgnoreCase("org.ldaptive.ssl.ThreadLocalTLSSocketFactory")) { - return super.loadClass(name); - } - - return clazz; - } - - } } diff --git a/src/main/java/org/opensearch/security/auth/ldap2/HostnameAwareConnectionFactory.java b/src/main/java/org/opensearch/security/auth/ldap2/HostnameAwareConnectionFactory.java new file mode 100644 index 0000000000..793ed3484d --- /dev/null +++ b/src/main/java/org/opensearch/security/auth/ldap2/HostnameAwareConnectionFactory.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import org.ldaptive.Connection; +import org.ldaptive.ConnectionConfig; +import org.ldaptive.DefaultConnectionFactory; +import org.ldaptive.LdapURL; + +/** + * {@link DefaultConnectionFactory} that extracts the hostname from the LDAP URL and returns a + * {@link SniAwareConnection}, so the SNI context is established when the connection is opened — + * the TLS socket is created at {@code open()}, not at {@code getConnection()}. + * + *

Extending {@link DefaultConnectionFactory} (rateher than merely implementing + * {@code ConnectionFactory}) lets the same class back a connection pool, which requires a + * concrete {@link DefaultConnectionFactory}, as well as serve non-pooled connections directly. + * The pool creates each physical connection via {@link #getConnection()} and then calls + * {@code open()} on it, so the SNI wrapping applies uniformly to pooled and non-pooled paths. + * + *

This is necessary because JNDI LDAP resolves hostnames to IP addresses before creating + * SSL sockets, making the hostname unavailable for SNI configuration. + */ +public class HostnameAwareConnectionFactory extends DefaultConnectionFactory { + + private final String ldapUrl; + + public HostnameAwareConnectionFactory(ConnectionConfig config, String ldapUrl) { + super(config); + this.ldapUrl = ldapUrl; + } + + @Override + public Connection getConnection() { + String hostname = new LdapURL(ldapUrl).getEntry().getHostname(); + return new SniAwareConnection(super.getConnection(), hostname); + } +} diff --git a/src/main/java/org/opensearch/security/auth/ldap2/LDAPConnectionFactoryFactory.java b/src/main/java/org/opensearch/security/auth/ldap2/LDAPConnectionFactoryFactory.java index 124fc92da9..5b7f711167 100644 --- a/src/main/java/org/opensearch/security/auth/ldap2/LDAPConnectionFactoryFactory.java +++ b/src/main/java/org/opensearch/security/auth/ldap2/LDAPConnectionFactoryFactory.java @@ -12,11 +12,14 @@ package org.opensearch.security.auth.ldap2; import java.nio.file.Path; +import java.security.GeneralSecurityException; import java.time.Duration; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.net.ssl.TrustManagerFactory; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -79,24 +82,37 @@ public LDAPConnectionFactoryFactory(Settings settings, Path configPath) throws S public ConnectionFactory createConnectionFactory(ConnectionPool connectionPool) { if (connectionPool != null) { return new PooledConnectionFactory(connectionPool); - } else { - return createBasicConnectionFactory(); } + return createHostnameAwareConnectionFactory(); } - @SuppressWarnings("unchecked") - public DefaultConnectionFactory createBasicConnectionFactory() { - DefaultConnectionFactory result = new DefaultConnectionFactory(getConnectionConfig()); + /** + * Creates a {@link HostnameAwareConnectionFactory} that returns SNI-aware connections, so the + * SNI context is set when the connection is opened (at {@code open()}, not + * {@code getConnection()}). Being a {@link DefaultConnectionFactory}, it serves both non-pooled + * connections directly and backs a connection pool. + */ + private DefaultConnectionFactory createHostnameAwareConnectionFactory() { + return configureFactory(new HostnameAwareConnectionFactory(getConnectionConfig(), getLdapUrlString())); + } - result.setProvider(new PrivilegedProvider((Provider) result.getProvider())); + public DefaultConnectionFactory createBasicConnectionFactory() { + return configureFactory(new DefaultConnectionFactory(getConnectionConfig())); + } - JndiProviderConfig jndiProviderConfig = (JndiProviderConfig) result.getProvider().getProviderConfig(); + /** + * Applies the shared provider (privileged, for the JNDI socket-factory classloader) and SSL + * wiring that every factory this class builds needs. + */ + @SuppressWarnings("unchecked") + private T configureFactory(T factory) { + factory.setProvider(new PrivilegedProvider((Provider) factory.getProvider())); if (this.sslConfig != null) { - configureSSLinConnectionFactory(result); + configureSSLinConnectionFactory(factory); } - return result; + return factory; } public ConnectionPool createConnectionPool() { @@ -120,10 +136,14 @@ public ConnectionPool createConnectionPool() { AbstractConnectionPool result; + // Use hostname-aware DefaultConnectionFactory for pool to ensure SNI is set for all connections + // including those created during pool initialization + DefaultConnectionFactory hostnameAwareFactory = createHostnameAwareConnectionFactory(); + if ("blocking".equals(this.settings.get(ConfigConstants.LDAP_POOL_TYPE))) { - result = new BlockingConnectionPool(poolConfig, createBasicConnectionFactory()); + result = new BlockingConnectionPool(poolConfig, hostnameAwareFactory); } else { - result = new SoftLimitConnectionPool(poolConfig, createBasicConnectionFactory()); + result = new SoftLimitConnectionPool(poolConfig, hostnameAwareFactory); } result.setValidator(getConnectionValidator()); @@ -301,7 +321,7 @@ private void configureSSL(ConnectionConfig config) { this.sslConfig.getEffectiveTruststoreAliasesArray(), this.sslConfig.getEffectiveKeystore(), this.sslConfig.getEffectiveKeyPasswordString(), - this.sslConfig.getEffectiveKeyAliasesArray() + null ); ldaptiveSslConfig.setCredentialConfig(cc); @@ -322,14 +342,26 @@ private void configureSSL(ConnectionConfig config) { } } - if (this.sslConfig.getSupportedCipherSuites() != null && this.sslConfig.getSupportedCipherSuites().length > 0) { - ldaptiveSslConfig.setEnabledCipherSuites(this.sslConfig.getSupportedCipherSuites()); + final String[] enabledCipherSuites = this.sslConfig.getSupportedCipherSuites(); + if (enabledCipherSuites != null && enabledCipherSuites.length > 0) { + ldaptiveSslConfig.setEnabledCipherSuites(enabledCipherSuites); + log.debug("enabled ssl cipher suites for ldaps {}", Arrays.toString(enabledCipherSuites)); } - ldaptiveSslConfig.setEnabledProtocols(this.sslConfig.getSupportedProtocols()); + final String[] enabledProtocols = this.sslConfig.getSupportedProtocols(); + log.debug("enabled ssl/tls protocols for ldaps {}", Arrays.toString(enabledProtocols)); + ldaptiveSslConfig.setEnabledProtocols(enabledProtocols); if (this.sslConfig.isTrustAllEnabled()) { ldaptiveSslConfig.setTrustManagers(new AllowAnyTrustManager()); + } else { + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); + tmf.init(this.sslConfig.getEffectiveTruststore()); + ldaptiveSslConfig.setTrustManagers(tmf.getTrustManagers()[0]); + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Failed to initialize PKIX TrustManager for LDAPS", e); + } } config.setSslConfig(ldaptiveSslConfig); @@ -352,7 +384,15 @@ private void configureSSLinConnectionFactory(DefaultConnectionFactory connection props.put("jndi.starttls.allowAnyHostname", "true"); } - connectionFactory.getProvider().getProviderConfig().setProperties(props); + // Register the custom socket factory with JNDI for SSL/TLS connections + // SNISettingTLSSocketFactory uses BouncyCastle's CustomSSLSocketFactory to properly + // set SNI hostname and endpoint identification for hostname verification. + // This addresses a known issue where JNDI LDAP doesn't pass hostname to SSLSocketFactory. + // See: https://github.com/bcgit/bc-java/issues/460 + props.put("java.naming.ldap.factory.socket", "org.opensearch.security.auth.ldap2.SNISettingTLSSocketFactory"); + JndiProviderConfig providerConfig = (JndiProviderConfig) connectionFactory.getProvider().getProviderConfig(); + providerConfig.setProperties(props); + providerConfig.setClassLoader(new SocketFactoryClassLoader(SNISettingTLSSocketFactory.class.getClassLoader())); } } diff --git a/src/main/java/org/opensearch/security/auth/ldap2/SNISettingTLSSocketFactory.java b/src/main/java/org/opensearch/security/auth/ldap2/SNISettingTLSSocketFactory.java new file mode 100644 index 0000000000..a8a0155194 --- /dev/null +++ b/src/main/java/org/opensearch/security/auth/ldap2/SNISettingTLSSocketFactory.java @@ -0,0 +1,179 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.util.Collections; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.util.IPAddress; + +import org.ldaptive.ssl.ThreadLocalTLSSocketFactory; + +/** + * Custom socket factory for LDAP connections that ensures SNI hostname is properly set + * for BouncyCastle JSSE provider hostname verification. + * + *

This addresses a known issue where JNDI LDAP's socket creation doesn't pass hostname + * information to the SSLSocketFactory, causing BouncyCastle's hostname verification to fail. + * @see https://github.com/bcgit/bc-java/issues/460 + * + *

The solution wraps the delegate SSLSocketFactory and intercepts all socket creation + * methods to set SNI parameters after socket creation but before the socket is returned to + * the caller. The hostname is provided via ThreadLocal by the connection factory before + * establishing the connection. + */ +public class SNISettingTLSSocketFactory extends SSLSocketFactory { + + private static final Logger log = LogManager.getLogger(SNISettingTLSSocketFactory.class); + + private static final ThreadLocal hostnameThreadLocal = new ThreadLocal<>(); + + private final SSLSocketFactory delegate; + + /** + * Required by JNDI to get the socket factory instance. + * This method is called by JNDI when java.naming.ldap.factory.socket is set. + */ + public static SSLSocketFactory getDefault() { + log.debug("SNISettingTLSSocketFactory.getDefault() called by JNDI"); + // Get the configured SSL socket factory from ldaptive's ThreadLocal + SSLSocketFactory delegate = (SSLSocketFactory) ThreadLocalTLSSocketFactory.getDefault(); + log.debug("Wrapping delegate factory: {}", delegate.getClass().getName()); + return new SNISettingTLSSocketFactory(delegate); + } + + /** + * A no-throw {@link AutoCloseable} returned by {@link #configure} for use in try-with-resources. + */ + @FunctionalInterface + public interface SniContext extends AutoCloseable { + @Override + void close(); + } + + /** + * Sets the SNI hostname for the current thread, returning an {@link SniContext} that clears it + * on close. Intended for use in try-with-resources: + * + *

{@code
+     * try (var ignored = SNISettingTLSSocketFactory.configure(hostname)) {
+     *     connection.open();
+     * }
+     * }
+ */ + public static SniContext configure(String hostname) { + hostnameThreadLocal.set(hostname); + log.debug("Configured SNI context: hostname={}", hostname); + return SNISettingTLSSocketFactory::clearContext; + } + + static String getHostname() { + return hostnameThreadLocal.get(); + } + + static void clearContext() { + hostnameThreadLocal.remove(); + } + + SNISettingTLSSocketFactory(SSLSocketFactory delegate) { + this.delegate = delegate; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { + log.debug("createSocket(Socket, host={}, port={}) called", host, port); + Socket result = delegate.createSocket(socket, host, port, autoClose); + return configureSocket(result); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + log.debug("createSocket(host={}, port={}) called", host, port); + Socket result = delegate.createSocket(host, port); + return configureSocket(result); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + log.debug("createSocket(host={}, port={}, localHost={}, localPort={}) called", host, port, localHost, localPort); + Socket result = delegate.createSocket(host, port, localHost, localPort); + return configureSocket(result); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + log.debug("createSocket(host={}, port={}) called", host, port); + Socket result = delegate.createSocket(host, port); + return configureSocket(result); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + log.debug("createSocket(host={}, port={}, localAddress={}, localPort={}) called", address, port, localAddress, localPort); + Socket result = delegate.createSocket(address, port, localAddress, localPort); + return configureSocket(result); + } + + /** + * Sets the SNI {@code server_name} on a newly created socket from the hostname carried on the + * ThreadLocal. Hostname verification is handled by JNDI's endpoint identification (and + * ldaptive's verifier) — not here. + * + * @param socket the created socket + * @return the configured socket + */ + protected Socket configureSocket(Socket socket) { + if (!(socket instanceof SSLSocket sslSocket)) { + log.debug("Socket is not an SSLSocket, skipping SNI configuration"); + return socket; + } + + String hostname = getHostname(); + + log.debug("Configuring SNI for socket, hostname: {}", hostname); + + if (hostname == null) { + log.warn("No hostname available for SNI configuration on socket: {}", socket.getClass().getName()); + return socket; + } + + SSLParameters params = sslSocket.getSSLParameters(); + if (!IPAddress.isValid(hostname)) { + log.debug("Configuring SNI for hostname: {} on socket: {}", hostname, socket.getClass().getName()); + params.setServerNames(Collections.singletonList(new SNIHostName(hostname))); + } + + sslSocket.setSSLParameters(params); + log.debug("Successfully configured socket for: {}", hostname); + + return socket; + } + +} diff --git a/src/main/java/org/opensearch/security/auth/ldap2/SniAwareConnection.java b/src/main/java/org/opensearch/security/auth/ldap2/SniAwareConnection.java new file mode 100644 index 0000000000..880d2fac9b --- /dev/null +++ b/src/main/java/org/opensearch/security/auth/ldap2/SniAwareConnection.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import org.ldaptive.BindRequest; +import org.ldaptive.Connection; +import org.ldaptive.ConnectionConfig; +import org.ldaptive.LdapException; +import org.ldaptive.Response; +import org.ldaptive.control.RequestControl; +import org.ldaptive.provider.ProviderConnection; + +/** + * {@link Connection} decorator that establishes the {@link SNISettingTLSSocketFactory} SNI + * context for the duration of every socket-creating call ({@code open} / {@code reopen}). + * + *

ldaptive's {@code getConnection()} returns an unopened connection; the TLS socket + * is created later, at {@code open()}. Setting the SNI ThreadLocal only around + * {@code getConnection()} therefore clears it before the socket exists, so the socket factory + * sees no hostname (no SNI) and no verify flag (hostname verification skipped). Wrapping + * {@code open()}/{@code reopen()} keeps the hostname and verify flag available exactly when the + * socket is created — for non-pooled and pooled connections alike, since a pool opens + * connections on the thread that initialises/borrows them. + * + *

This whole mechanism (this decorator + the SNI ThreadLocal in + * {@link SNISettingTLSSocketFactory} + {@link HostnameAwareConnectionFactory}) is a workaround for + * the JNDI LDAP provider resolving hostnames to IPs before socket creation (bc-java#460). ldaptive + * 2.x's native (Netty) transport opens sockets with the real hostname, giving SNI and hostname + * verification natively — migrating off the JNDI provider would remove this class, + * {@link SNISettingTLSSocketFactory}, and {@link HostnameAwareConnectionFactory}. + */ +class SniAwareConnection implements Connection { + + private final Connection delegate; + private final String hostname; + + SniAwareConnection(Connection delegate, String hostname) { + this.delegate = delegate; + this.hostname = hostname; + } + + /** The SNI hostname this connection establishes at {@code open()}. Package-private for tests. */ + String hostname() { + return hostname; + } + + @Override + public Response open() throws LdapException { + try (var ignored = SNISettingTLSSocketFactory.configure(hostname)) { + return delegate.open(); + } + } + + @Override + public Response open(BindRequest request) throws LdapException { + try (var ignored = SNISettingTLSSocketFactory.configure(hostname)) { + return delegate.open(request); + } + } + + @Override + public Response reopen() throws LdapException { + try (var ignored = SNISettingTLSSocketFactory.configure(hostname)) { + return delegate.reopen(); + } + } + + @Override + public Response reopen(BindRequest request) throws LdapException { + try (var ignored = SNISettingTLSSocketFactory.configure(hostname)) { + return delegate.reopen(request); + } + } + + @Override + public ConnectionConfig getConnectionConfig() { + return delegate.getConnectionConfig(); + } + + @Override + public boolean isOpen() { + return delegate.isOpen(); + } + + @Override + public ProviderConnection getProviderConnection() { + return delegate.getProviderConnection(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public void close(RequestControl[] controls) { + delegate.close(controls); + } +} diff --git a/src/main/java/org/opensearch/security/auth/ldap2/SocketFactoryClassLoader.java b/src/main/java/org/opensearch/security/auth/ldap2/SocketFactoryClassLoader.java new file mode 100644 index 0000000000..e2faf0d1f5 --- /dev/null +++ b/src/main/java/org/opensearch/security/auth/ldap2/SocketFactoryClassLoader.java @@ -0,0 +1,45 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import org.ldaptive.ssl.ThreadLocalTLSSocketFactory; + +/** + * Classloader that resolves the JNDI LDAP socket-factory classes by name. + * + *

On Java 9+ the module system means JNDI's {@code com.sun.jndi.ldap.Connection.getSocketFactory} + * loads the class named by {@code java.naming.ldap.factory.socket} through the {@code java.naming} + * module loader, which cannot see application/plugin classes on the classpath. Installing this as the + * thread-context classloader lets JNDI resolve {@link SNISettingTLSSocketFactory} (and ldaptive's + * {@link ThreadLocalTLSSocketFactory}) by name; everything else delegates to the parent loader. + */ +public final class SocketFactoryClassLoader extends ClassLoader { + + public SocketFactoryClassLoader() { + super(); + } + + public SocketFactoryClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (SNISettingTLSSocketFactory.class.getName().equals(name)) { + return SNISettingTLSSocketFactory.class; + } + if (ThreadLocalTLSSocketFactory.class.getName().equalsIgnoreCase(name)) { + return ThreadLocalTLSSocketFactory.class; + } + return super.loadClass(name); + } +} diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 4cd2ddab2a..aafb626ed8 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -12,60 +12,124 @@ package org.opensearch.security.authtoken.jwt; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; import javax.crypto.Cipher; import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.bouncycastle.crypto.KDFCalculator; +import org.bouncycastle.crypto.fips.FipsKDF; + +import org.opensearch.common.Randomness; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.FipsMode; +import org.opensearch.security.util.KeyUtils; + public class EncryptionDecryptionUtil { - private final Cipher encryptCipher; - private final Cipher decryptCipher; + private static final byte[] HKDF_INFO = "opensearch-obo-jwt-encryption".getBytes(StandardCharsets.UTF_8); + private static final int GCM_NONCE_LENGTH = 12; // 96 bits, recommended for AES-GCM + private static final int GCM_TAG_LENGTH = 128; // bits + private static final String AES_GCM_NO_PADDING = "AES/GCM/NoPadding"; + + // HKDF derives an AES-256 key (32 bytes). + private static final int AES_KEY_LENGTH_BYTES = 32; - public EncryptionDecryptionUtil(final String secret) { - this.encryptCipher = createCipherFromSecret(secret, CipherMode.ENCRYPT); - this.decryptCipher = createCipherFromSecret(secret, CipherMode.DECRYPT); + // A KDF cannot create entropy, so the derived key is only as strong as its input keying material. + // We therefore require at least as much IKM as the derived key it backs (AES-256). + private static final int MINIMUM_IKM_BYTES = AES_KEY_LENGTH_BYTES; + + private final SecretKey aesKey; + private final SecureRandom secureRandom = Randomness.createSecure(); + + /** + * Resolves the encryption key from {@code } in the given settings, supporting either a keystore + * (e.g. BCFKS, keeping the key out of cluster state) or a Base64-encoded plaintext value. Both issuance + * and verification call this so they derive the same AES key from the same key material. + * + * @return a util instance, or {@code null} if no key is configured + */ + public static EncryptionDecryptionUtil fromSettings(final Settings settings, final String prefix, final Path configPath) { + final SecretKey keystoreKey = KeyUtils.loadKeyFromKeystore(settings, prefix, configPath); + if (keystoreKey != null) { + return new EncryptionDecryptionUtil(keystoreKey.getEncoded()); + } + final String configured = settings.get(prefix); + return configured != null ? new EncryptionDecryptionUtil(configured) : null; } - public String encrypt(final String data) { - byte[] encryptedBytes = processWithCipher(data.getBytes(StandardCharsets.UTF_8), encryptCipher); - return Base64.getEncoder().encodeToString(encryptedBytes); + public EncryptionDecryptionUtil(final String encodedSecret) { + this(decodeBase64(encodedSecret)); } - public String decrypt(final String encryptedString) { - byte[] decodedBytes = Base64.getDecoder().decode(encryptedString); - return new String(processWithCipher(decodedBytes, decryptCipher), StandardCharsets.UTF_8); + public EncryptionDecryptionUtil(final byte[] secretBytes) { + this.aesKey = deriveKey(secretBytes); } - private static Cipher createCipherFromSecret(final String secret, final CipherMode mode) { + private static byte[] decodeBase64(final String secret) { try { - final byte[] decodedKey = Base64.getDecoder().decode(secret); - final Cipher cipher = Cipher.getInstance("AES"); - final SecretKey originalKey = new SecretKeySpec(Arrays.copyOf(decodedKey, 16), "AES"); - cipher.init(mode.opmode, originalKey); - return cipher; - } catch (final Exception e) { - throw new RuntimeException("Error creating cipher from secret in mode " + mode.name(), e); + return Base64.getDecoder().decode(secret); + } catch (final IllegalArgumentException e) { + throw new RuntimeException("encryption_key is not a valid Base64-encoded value", e); } } - private static byte[] processWithCipher(final byte[] data, final Cipher cipher) { + public String encrypt(final String data) { + byte[] plaintext = data.getBytes(StandardCharsets.UTF_8); try { - return cipher.doFinal(data); + byte[] nonce = new byte[GCM_NONCE_LENGTH]; + secureRandom.nextBytes(nonce); + Cipher cipher = Cipher.getInstance(AES_GCM_NO_PADDING); + cipher.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, nonce)); + byte[] ciphertext = cipher.doFinal(plaintext); + byte[] output = new byte[GCM_NONCE_LENGTH + ciphertext.length]; + System.arraycopy(nonce, 0, output, 0, GCM_NONCE_LENGTH); + System.arraycopy(ciphertext, 0, output, GCM_NONCE_LENGTH, ciphertext.length); + return Base64.getEncoder().encodeToString(output); } catch (final Exception e) { throw new RuntimeException("Error processing data with cipher", e); } } - private enum CipherMode { - ENCRYPT(Cipher.ENCRYPT_MODE), - DECRYPT(Cipher.DECRYPT_MODE); + public String decrypt(final String encryptedString) { + byte[] decodedBytes = Base64.getDecoder().decode(encryptedString); + try { + byte[] nonce = Arrays.copyOfRange(decodedBytes, 0, GCM_NONCE_LENGTH); + byte[] ciphertext = Arrays.copyOfRange(decodedBytes, GCM_NONCE_LENGTH, decodedBytes.length); + Cipher cipher = Cipher.getInstance(AES_GCM_NO_PADDING); + cipher.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(GCM_TAG_LENGTH, nonce)); + return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8); + } catch (final Exception e) { + throw new RuntimeException("Error processing data with cipher", e); + } + } - private final int opmode; + private static SecretKey deriveKey(final byte[] secretBytes) { + if (FipsMode.isEnabled() && secretBytes.length < MINIMUM_IKM_BYTES) { + throw new IllegalArgumentException( + "Configured encryption_key decodes to %d bytes of key material, but FIPS mode requires at least %d bytes.".formatted( + secretBytes.length, + MINIMUM_IKM_BYTES + ) + ); + } - private CipherMode(final int opmode) { - this.opmode = opmode; + try { + FipsKDF.AgreementKDFParameters hkdfParams = FipsKDF.HKDF.withPRF(FipsKDF.AgreementKDFPRF.SHA256_HMAC) + .using(secretBytes) + .withIV(HKDF_INFO); // BC FIPS maps withIV → HKDF info (expand-phase context binding per RFC 5869) + KDFCalculator kdf = new FipsKDF.AgreementOperatorFactory().createKDFCalculator(hkdfParams); + byte[] derivedKey = new byte[AES_KEY_LENGTH_BYTES]; // AES-256 + kdf.generateBytes(derivedKey); + return new SecretKeySpec(derivedKey, "AES"); + } catch (final Exception e) { + throw new RuntimeException("Error deriving key from secret", e); + } finally { + Arrays.fill(secretBytes, (byte) 0); } } } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index f1e493a7e3..dcdd9b905e 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -11,9 +11,11 @@ package org.opensearch.security.authtoken.jwt; +import java.nio.file.Path; import java.text.ParseException; import java.util.Base64; import java.util.Date; +import javax.crypto.SecretKey; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,6 +25,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.secure_sm.AccessController; import org.opensearch.security.authtoken.jwt.claims.JwtClaimsBuilder; +import org.opensearch.security.util.KeyUtils; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; @@ -36,13 +39,14 @@ import com.nimbusds.jwt.SignedJWT; public class JwtVendor { + public static final String SIGNING_KEY_PROPERTY_KEY = "signing_key"; private static final Logger logger = LogManager.getLogger(JwtVendor.class); private final JWK signingKey; private final JWSSigner signer; - public JwtVendor(Settings settings) { - final Tuple tuple = createJwkFromSettings(settings); + public JwtVendor(Settings settings, Path configPath) { + final Tuple tuple = createJwkFromSettings(settings, configPath); signingKey = tuple.v1(); signer = tuple.v2(); } @@ -53,10 +57,14 @@ public JwtVendor(Settings settings) { * PublicKeyUse: SIGN * Encryption Algorithm: HS512 * */ - static Tuple createJwkFromSettings(final Settings settings) { + static Tuple createJwkFromSettings(final Settings settings, final Path configPath) { final OctetSequenceKey key; - if (settings.get("signing_key") != null) { - final String signingKey = settings.get("signing_key"); + + final SecretKey keystoreKey = KeyUtils.loadKeyFromKeystore(settings, SIGNING_KEY_PROPERTY_KEY, configPath); + if (keystoreKey != null) { + key = new OctetSequenceKey.Builder(keystoreKey.getEncoded()).algorithm(JWSAlgorithm.HS512).keyUse(KeyUse.SIGNATURE).build(); + } else if (settings.get(SIGNING_KEY_PROPERTY_KEY) != null) { + final String signingKey = settings.get(SIGNING_KEY_PROPERTY_KEY); key = new OctetSequenceKey.Builder(Base64.getDecoder().decode(signingKey)).algorithm(JWSAlgorithm.HS512) .keyUse(KeyUse.SIGNATURE) .build(); diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java b/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java index 2b9b5c7abe..0bc74805d2 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/claims/OBOJwtClaimsBuilder.java @@ -18,9 +18,9 @@ public class OBOJwtClaimsBuilder extends JwtClaimsBuilder { private final EncryptionDecryptionUtil encryptionDecryptionUtil; - public OBOJwtClaimsBuilder(String encryptionKey) { + public OBOJwtClaimsBuilder(EncryptionDecryptionUtil encryptionDecryptionUtil) { super(); - this.encryptionDecryptionUtil = encryptionKey != null ? new EncryptionDecryptionUtil(encryptionKey) : null; + this.encryptionDecryptionUtil = encryptionDecryptionUtil; } public OBOJwtClaimsBuilder addRoles(List roles) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java index ebb3a4e88b..ecd3eacf6e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java @@ -37,6 +37,17 @@ public class PasswordValidator { private static final int MAX_LENGTH = 100; + /** + * Minimum password length required under FIPS mode, {@value} characters. + * + *

BC FIPS derives PBKDF2 keys from the password and rejects key material below 112 bits — fewer than + * 14 ASCII characters — throwing a {@link org.bouncycastle.crypto.fips.FipsUnapprovedOperationError} at + * hashing time. To avoid that, under FIPS the node raises the configurable + * {@value org.opensearch.security.support.ConfigConstants#SECURITY_RESTAPI_PASSWORD_MIN_LENGTH} to at least + * this value (see {@link org.opensearch.security.OpenSearchSecurityPlugin#additionalSettings}). + */ + public static final int FIPS_MIN_PASSWORD_LENGTH = 14; + /** * Checks a username similarity and a password * names and passwords like: diff --git a/src/main/java/org/opensearch/security/hasher/AbstractPasswordHasher.java b/src/main/java/org/opensearch/security/hasher/AbstractPasswordHasher.java index 151f657de1..5a33f84152 100644 --- a/src/main/java/org/opensearch/security/hasher/AbstractPasswordHasher.java +++ b/src/main/java/org/opensearch/security/hasher/AbstractPasswordHasher.java @@ -30,6 +30,20 @@ abstract class AbstractPasswordHasher implements PasswordHasher { */ HashingFunction hashingFunction; + private volatile String dummyHash; + + @Override + public String getDummyHash() { + if (dummyHash == null) { + synchronized (this) { + if (dummyHash == null) { + dummyHash = hash(new char[] { 'd', 'u', 'm', 'm', 'y', 'p', 'a', 's', 's', 'w', 'o', 'r', 'd', 'h', 'a', 's', 'h' }); + } + } + } + return dummyHash; + } + /** * {@inheritDoc} */ diff --git a/src/main/java/org/opensearch/security/hasher/PBKDF2PasswordHasher.java b/src/main/java/org/opensearch/security/hasher/PBKDF2PasswordHasher.java index d28d2a919a..0b311872ac 100644 --- a/src/main/java/org/opensearch/security/hasher/PBKDF2PasswordHasher.java +++ b/src/main/java/org/opensearch/security/hasher/PBKDF2PasswordHasher.java @@ -19,6 +19,8 @@ class PBKDF2PasswordHasher extends AbstractPasswordHasher { + static final String DEFAULT_ADMIN_PASSWORD = "admin@OpenSearch"; + private static final int DEFAULT_SALT_LENGTH = 128; PBKDF2PasswordHasher(String function, int iterations, int length) { @@ -40,14 +42,22 @@ public String hash(char[] password) { public boolean check(char[] password, String hash) { checkPasswordNotNullOrEmpty(password); checkHashNotNullOrEmpty(hash); + CharBuffer passwordBuffer = CharBuffer.wrap(password); try { return Password.check(passwordBuffer, hash).with(getPBKDF2FunctionFromHash(hash)); + } catch (Throwable e) { + return false; } finally { cleanup(passwordBuffer); } } + @Override + public String defaultAdminPassword() { + return DEFAULT_ADMIN_PASSWORD; + } + private HashingFunction getPBKDF2FunctionFromHash(String hash) { return CompressedPBKDF2Function.getInstanceFromHash(hash); } diff --git a/src/main/java/org/opensearch/security/hasher/PasswordHasher.java b/src/main/java/org/opensearch/security/hasher/PasswordHasher.java index b115feac58..b7276e4303 100644 --- a/src/main/java/org/opensearch/security/hasher/PasswordHasher.java +++ b/src/main/java/org/opensearch/security/hasher/PasswordHasher.java @@ -33,4 +33,24 @@ public interface PasswordHasher { * @return true if the password matches the hashed password, false otherwise */ boolean check(char[] password, String hashedPassword); + + /** + * Returns a dummy hash used for constant-time comparison when a user is not found, + * preventing timing-based user enumeration attacks. The hash is in the same format + * and uses the same parameters as hashes produced by this hasher. + * + * @return a valid dummy hash string for this hasher's algorithm + */ + String getDummyHash(); + + /** + * Returns the default admin password used to detect whether the shipped + * internal_users.yml has been left unmodified. Each implementation declares + * a password appropriate for its algorithm's security requirements. + * + * @return the default admin password for this hasher + */ + default String defaultAdminPassword() { + return "admin"; + } } diff --git a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java index b8a39f7d6b..71ad6ee654 100644 --- a/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/OnBehalfOfAuthenticator.java @@ -11,7 +11,9 @@ package org.opensearch.security.http; +import java.nio.file.Path; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.List; import java.util.Map.Entry; @@ -19,6 +21,7 @@ import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; +import javax.crypto.SecretKey; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.LogManager; @@ -38,42 +41,92 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.JwtParserBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.WeakKeyException; public class OnBehalfOfAuthenticator implements HTTPAuthenticator { private static final int MINIMUM_SIGNING_KEY_BIT_LENGTH = 512; + private static final String SIGNING_KEY = "signing_key"; + private static final String ENCRYPTION_KEY = "encryption_key"; protected final Logger log = LogManager.getLogger(this.getClass()); private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER_PREFIX = "bearer "; - private final JwtParser jwtParser; - private final String encryptionKey; + private final Settings settings; private final Boolean enabled; private final String clusterName; + private final Path configPath; + private volatile boolean initialized = false; + private volatile JwtParser jwtParser; + private volatile EncryptionDecryptionUtil encryptionUtil; + + public OnBehalfOfAuthenticator(Settings settings, String clusterName, Path configPath) { + this.enabled = settings.getAsBoolean("enabled", Boolean.TRUE); + this.settings = settings; + this.clusterName = clusterName; + this.configPath = configPath; + } - private final EncryptionDecryptionUtil encryptionUtil; + /** + * Builds the JWT parser and encryption helper on first use. Initialization is attempted exactly once. + * + * @return {@code true} if OBO authentication is usable, {@code false} if it is misconfigured + */ + private synchronized boolean ensureInitialized() { + if (!initialized) { + initialized = true; + try { + jwtParser = AccessController.doPrivileged(this::buildJwtParser); + encryptionUtil = EncryptionDecryptionUtil.fromSettings(settings, ENCRYPTION_KEY, configPath); + } catch (final RuntimeException e) { + log.error("On-behalf-of authentication is misconfigured; OBO tokens will be rejected: {}", e.toString(), e); + } + } + return jwtParser != null; + } - public OnBehalfOfAuthenticator(Settings settings, String clusterName) { - enabled = settings.getAsBoolean("enabled", Boolean.TRUE); - encryptionKey = settings.get("encryption_key"); - jwtParser = AccessController.doPrivileged(() -> { - JwtParserBuilder builder = initParserBuilder(settings.get("signing_key")); - return builder.build(); - }); - this.clusterName = clusterName; - this.encryptionUtil = encryptionKey != null ? new EncryptionDecryptionUtil(encryptionKey) : null; + /** + * Builds the HMAC verification parser. The signing key may be supplied either via a keystore (e.g. BCFKS, + * keeping the key out of cluster state) or as a Base64-encoded {@code signing_key} setting. The keystore + * path mirrors how {@link org.opensearch.security.authtoken.jwt.JwtVendor} signs OBO tokens, so issuance + * and verification share the same key material. + */ + private JwtParser buildJwtParser() { + final SecretKey keystoreKey = KeyUtils.loadKeyFromKeystore(settings, SIGNING_KEY, configPath); + if (keystoreKey != null) { + final byte[] keyBytes = keystoreKey.getEncoded(); + validateSigningKeyBitLength(keyBytes.length * Byte.SIZE); + return Jwts.parser().verifyWith(Keys.hmacShaKeyFor(keyBytes)).build(); + } + final String signingKey = settings.get(SIGNING_KEY); + validateSigningKey(signingKey); + return KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log).build(); } - private JwtParserBuilder initParserBuilder(final String signingKey) { + /** + * Validates a Base64-encoded {@code signing_key}, throwing {@link OpenSearchSecurityException} with a + * descriptive message when it is missing, not valid Base64, or below the + * {@value #MINIMUM_SIGNING_KEY_BIT_LENGTH}-bit minimum. Package-private static so it can be unit-tested + * directly without standing up an authenticator. + */ + static void validateSigningKey(final String signingKey) { if (signingKey == null) { throw new OpenSearchSecurityException("Unable to find on behalf of authenticator signing_key"); } + final int signingKeyLengthBits; + try { + signingKeyLengthBits = Base64.getDecoder().decode(signingKey).length * Byte.SIZE; + } catch (final IllegalArgumentException e) { + throw new OpenSearchSecurityException("Signing key is not a valid Base64-encoded value: " + e.getMessage()); + } + validateSigningKeyBitLength(signingKeyLengthBits); + } - final int signingKeyLengthBits = signingKey.length() * 8; + static void validateSigningKeyBitLength(final int signingKeyLengthBits) { if (signingKeyLengthBits < MINIMUM_SIGNING_KEY_BIT_LENGTH) { throw new OpenSearchSecurityException( "Signing key size was " @@ -83,9 +136,6 @@ private JwtParserBuilder initParserBuilder(final String signingKey) { + " bits." ); } - JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); - - return jwtParserBuilder; } private List extractSecurityRolesFromClaims(Claims claims) { @@ -148,7 +198,7 @@ private AuthCredentials extractCredentials0(final SecurityRequest request) { } String jwtToken = extractJwtFromHeader(request); - if (jwtToken == null) { + if (jwtToken == null || !ensureInitialized()) { return null; } diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index 88b2c7d890..05f9fcd7a8 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -11,6 +11,7 @@ package org.opensearch.security.identity; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Date; import java.util.Set; @@ -29,6 +30,7 @@ import org.opensearch.identity.tokens.AuthToken; import org.opensearch.identity.tokens.OnBehalfOfClaims; import org.opensearch.identity.tokens.TokenManager; +import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; @@ -52,6 +54,7 @@ public class SecurityTokenManager implements TokenManager { private final ThreadPool threadPool; private final UserService userService; private final RoleMapper roleMapper; + private final Path configPath; private Settings oboSettings = null; private final LongSupplier timeProvider = System::currentTimeMillis; @@ -61,12 +64,14 @@ public SecurityTokenManager( final ClusterService cs, final ThreadPool threadPool, final UserService userService, - RoleMapper roleMapper + final RoleMapper roleMapper, + final Path configPath ) { this.cs = cs; this.threadPool = threadPool; this.userService = userService; this.roleMapper = roleMapper; + this.configPath = configPath; } @Subscribe @@ -81,7 +86,7 @@ public void onDynamicConfigModelChanged(final DynamicConfigModel dcm) { /** For testing */ JwtVendor createJwtVendor(final Settings settings) { try { - return new JwtVendor(settings); + return new JwtVendor(settings, configPath); } catch (final Exception ex) { logger.error("Unable to create the JwtVendor instance", ex); return null; @@ -129,7 +134,9 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final throw new IllegalArgumentException("Roles cannot be null"); } - final OBOJwtClaimsBuilder claimsBuilder = new OBOJwtClaimsBuilder(oboSettings.get("encryption_key")); + final OBOJwtClaimsBuilder claimsBuilder = new OBOJwtClaimsBuilder( + EncryptionDecryptionUtil.fromSettings(oboSettings, "encryption_key", configPath) + ); // Add obo claims claimsBuilder.issuer(cs.getClusterName().value()); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 686ffecead..e144d3a02c 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -67,6 +67,7 @@ import org.opensearch.security.securityconf.impl.v7.ConfigV7.Authz; import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthzDomain; import org.opensearch.security.support.ReflectionHelper; +import org.opensearch.security.util.KeyUtils; public class DynamicConfigModelV7 extends DynamicConfigModel { @@ -393,10 +394,12 @@ private void buildAAA() { * order: -1 - prioritize the OBO authentication when it gets enabled */ Settings oboSettings = getDynamicOnBehalfOfSettings(); - if (oboSettings.get("signing_key") != null) { + final boolean signingKeyConfigured = oboSettings.get("signing_key") != null + || oboSettings.get("signing_key" + KeyUtils.KEYSTORE_ALIAS) != null; + if (signingKeyConfigured) { final AuthDomain _ad = new AuthDomain( new NoOpAuthenticationBackend(Settings.EMPTY, null), - new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings(), this.cih.getClusterName()), + new OnBehalfOfAuthenticator(getDynamicOnBehalfOfSettings(), this.cih.getClusterName(), this.configPath), false, -1 ); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index 1d6edec335..a35a31081c 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -460,6 +460,18 @@ public static class OnBehalfOfSettings { @JsonProperty("encryption_key") private String encryptionKey; + private final Map additionalSettings = new HashMap<>(); + + @JsonAnySetter + public void setAdditionalSetting(final String name, final Object value) { + additionalSettings.put(name, value); + } + + @JsonAnyGetter + public Map getAdditionalSettings() { + return additionalSettings; + } + @JsonIgnore public String configAsJson() { return DefaultObjectMapper.writeValueAsString(this, false); @@ -491,7 +503,12 @@ public void setEncryptionKey(String encryptionKey) { @Override public String toString() { - return "OnBehalfOfSettings [ enabled=" + enabled + ", signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; + return String.format( + "OnBehalfOfSettings [ enabled=%s, signing_key=%s, encryption_key=%s]", + enabled, + signingKey != null ? "****" : "", + encryptionKey != null ? "****" : "" + ); } } diff --git a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java index 867a40f3b4..8818c84599 100644 --- a/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java +++ b/src/main/java/org/opensearch/security/ssl/DefaultSecurityKeyStore.java @@ -88,11 +88,10 @@ import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_TRANSPORT_SERVER_KEYSTORE_KEYPASSWORD; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_TRANSPORT_SERVER_PEMKEY_PASSWORD; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_TRANSPORT_TRUSTSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_TYPE; public class DefaultSecurityKeyStore implements SecurityKeyStore { - private static final String DEFAULT_STORE_TYPE = "JKS"; - private void printJCEWarnings() { try { final int aesMaxKeyLength = Cipher.getMaxAllowedKeyLength("AES"); diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index 5a4780f875..393353e4d6 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -18,7 +18,6 @@ package org.opensearch.security.ssl; import java.nio.file.Path; -import java.security.Security; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -32,7 +31,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; import org.opensearch.OpenSearchException; import org.opensearch.Version; @@ -224,8 +222,6 @@ protected OpenSearchSecuritySSLPlugin(final Settings settings, final Path config log.error("SSL not activated for http and/or transport."); } - tryAddSecurityProvider(); - this.sslSettingsManager = new SslSettingsManager(new Environment(settings, configPath)); } @@ -742,14 +738,4 @@ protected Settings migrateSettings(Settings settings) { public ThreadPool getThreadPool() { return this.threadPool; } - - private void tryAddSecurityProvider() { - AccessController.doPrivileged(() -> { - if (Security.getProvider("BCFIPS") == null) { - Security.addProvider(new BouncyCastleFipsProvider()); - log.debug("Bouncy Castle FIPS Provider added"); - } - return null; - }); - } } diff --git a/src/main/java/org/opensearch/security/ssl/SslConfiguration.java b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java index ad5f835f53..bb58a6dbfa 100644 --- a/src/main/java/org/opensearch/security/ssl/SslConfiguration.java +++ b/src/main/java/org/opensearch/security/ssl/SslConfiguration.java @@ -12,6 +12,8 @@ package org.opensearch.security.ssl; import java.nio.file.Path; +import java.security.Provider; +import java.security.Security; import java.util.List; import java.util.Objects; import java.util.Set; @@ -90,7 +92,7 @@ SslContext buildServerSslContext(final boolean validateCertificates) { return AccessController.doPrivilegedChecked(() -> { KeyManagerFactory kmFactory = keyStoreConfiguration.createKeyManagerFactory(validateCertificates); Set issuerDns = keyStoreConfiguration.getIssuerDns(); - return SslContextBuilder.forServer(kmFactory) + final SslContextBuilder builder = SslContextBuilder.forServer(kmFactory) .sslProvider(sslParameters.provider()) .clientAuth(sslParameters.clientAuth()) .protocols(sslParameters.allowedProtocols().toArray(new String[0])) @@ -115,20 +117,38 @@ SslContext buildServerSslContext(final boolean validateCertificates) { ApplicationProtocolNames.HTTP_1_1 ) ) - .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates, issuerDns)) - .build(); + .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates, issuerDns)); + routePkcs11ThroughSunJsse(builder); + return builder.build(); }); } catch (SSLException e) { throw new OpenSearchException("Failed to build server SSL context", e); } } + /** + * A PKCS#11-resident private key is non-exportable, so the BouncyCastle FIPS JSSE provider cannot sign + * with it (it fails with "no encoding for key" during the TLS CertificateVerify). SunJSSE instead + * delegates the signature operation to the key's own provider (SunPKCS11), letting the token perform it. + * This only affects the TLS engine's handshake signing; the JDK {@link io.netty.handler.ssl.SslProvider} is unchanged. + */ + private void routePkcs11ThroughSunJsse(final SslContextBuilder builder) { + if (!keyStoreConfiguration.isPkcs11()) { + return; + } + final Provider sunJSSE = Security.getProvider("SunJSSE"); + if (sunJSSE == null) { + throw new OpenSearchException("SunJSSE provider not available; required for PKCS#11 key store support"); + } + builder.sslContextProvider(sunJSSE); + } + SslContext buildClientSslContext(final boolean validateCertificates) { try { return AccessController.doPrivilegedChecked(() -> { KeyManagerFactory kmFactory = keyStoreConfiguration.createKeyManagerFactory(validateCertificates); Set issuerDns = keyStoreConfiguration.getIssuerDns(); - return SslContextBuilder.forClient() + final SslContextBuilder builder = SslContextBuilder.forClient() .sslProvider(sslParameters.provider()) .protocols(sslParameters.allowedProtocols()) .ciphers(sslParameters.allowedCiphers()) @@ -138,8 +158,9 @@ SslContext buildClientSslContext(final boolean validateCertificates) { .sslProvider(sslParameters.provider()) .keyManager(kmFactory) .trustManager(trustStoreConfiguration.createTrustManagerFactory(validateCertificates, issuerDns)) - .endpointIdentificationAlgorithm(null) - .build(); + .endpointIdentificationAlgorithm(null); + routePkcs11ThroughSunJsse(builder); + return builder.build(); }); } catch (Exception e) { throw new OpenSearchException("Failed to build client SSL context", e); diff --git a/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java index 0af172e95f..3f05fbc84a 100644 --- a/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java +++ b/src/main/java/org/opensearch/security/ssl/SslSettingsManager.java @@ -36,6 +36,7 @@ import org.opensearch.security.ssl.config.SslCertificatesLoader; import org.opensearch.security.ssl.config.SslParameters; import org.opensearch.security.ssl.config.TrustStoreConfiguration; +import org.opensearch.security.support.PemKeyReader; import org.opensearch.watcher.FileChangesListener; import org.opensearch.watcher.FileWatcher; import org.opensearch.watcher.ResourceWatcherService; @@ -48,6 +49,7 @@ import static org.opensearch.security.ssl.util.SSLConfigConstants.EXTENDED_KEY_USAGE_ENABLED; import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_ALIAS; import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.KEYSTORE_TYPE; import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_CERT_FILEPATH; import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_KEY_FILEPATH; import static org.opensearch.security.ssl.util.SSLConfigConstants.PEM_TRUSTED_CAS_FILEPATH; @@ -74,6 +76,7 @@ import static org.opensearch.security.ssl.util.SSLConfigConstants.SSL_TRANSPORT_SERVER_EXTENDED_PREFIX; import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_ALIAS; import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_FILEPATH; +import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; import static org.opensearch.transport.AuxTransport.AUX_TRANSPORT_TYPES_SETTING; public class SslSettingsManager { @@ -346,7 +349,10 @@ private void validateKeyStoreSettings(CertType transportType, final Settings set final var clientAuth = ClientAuth.valueOf( transportSettings.get(CLIENT_AUTH_MODE, ClientAuth.OPTIONAL.name()).toUpperCase(Locale.ROOT) ); - if (!transportSettings.hasValue(KEYSTORE_FILEPATH)) { + // A PKCS#11 keystore/truststore loads its material from the token, so no filepath is required. + final boolean isPkcs11Keystore = PemKeyReader.PKCS11.equalsIgnoreCase(transportSettings.get(KEYSTORE_TYPE)); + final boolean isPkcs11Truststore = PemKeyReader.PKCS11.equalsIgnoreCase(transportSettings.get(TRUSTSTORE_TYPE)); + if (!isPkcs11Keystore && !transportSettings.hasValue(KEYSTORE_FILEPATH)) { throw new OpenSearchException( "Wrong " + transportType.id().toLowerCase(Locale.ROOT) @@ -355,7 +361,7 @@ private void validateKeyStoreSettings(CertType transportType, final Settings set + " must be set" ); } - if (clientAuth == ClientAuth.REQUIRE && !transportSettings.hasValue(TRUSTSTORE_FILEPATH)) { + if (clientAuth == ClientAuth.REQUIRE && !isPkcs11Truststore && !transportSettings.hasValue(TRUSTSTORE_FILEPATH)) { throw new OpenSearchException( "Wrong " + transportType.id().toLowerCase(Locale.ROOT) @@ -448,7 +454,12 @@ private void validateTransportSettings(final Settings transportSettings) { } private void verifyKeyAndTrustStoreSettings(final Settings settings) { - if (!settings.hasValue(KEYSTORE_FILEPATH) || !settings.hasValue(TRUSTSTORE_FILEPATH)) { + // PKCS#11 keystores/truststores have no filepath - their material comes from the token. + final boolean keyStorePresent = settings.hasValue(KEYSTORE_FILEPATH) + || PemKeyReader.PKCS11.equalsIgnoreCase(settings.get(KEYSTORE_TYPE)); + final boolean trustStorePresent = settings.hasValue(TRUSTSTORE_FILEPATH) + || PemKeyReader.PKCS11.equalsIgnoreCase(settings.get(TRUSTSTORE_TYPE)); + if (!keyStorePresent || !trustStorePresent) { throw new OpenSearchException( "Wrong Transport/Tran SSL configuration. One of Keystore and Truststore files or X.509 PEM certificates and " + "PKCS#8 keys groups should be set to configure Transport layer properly" @@ -461,7 +472,10 @@ private boolean hasExtendedKeyUsageEnabled(final Settings settings) { } private boolean hasKeyOrTrustStoreSettings(final Settings settings) { - return settings.hasValue(KEYSTORE_FILEPATH) || settings.hasValue(TRUSTSTORE_FILEPATH); + return settings.hasValue(KEYSTORE_FILEPATH) + || settings.hasValue(TRUSTSTORE_FILEPATH) + || PemKeyReader.PKCS11.equalsIgnoreCase(settings.get(KEYSTORE_TYPE)) + || PemKeyReader.PKCS11.equalsIgnoreCase(settings.get(TRUSTSTORE_TYPE)); } private boolean hasPemStoreSettings(final Settings settings) { diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java index aed7c9b4c6..ecbf78ebd2 100644 --- a/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreConfiguration.java @@ -29,6 +29,7 @@ import org.opensearch.OpenSearchException; import org.opensearch.common.collect.Tuple; +import org.opensearch.security.support.PemKeyReader; public interface KeyStoreConfiguration { @@ -36,6 +37,14 @@ public interface KeyStoreConfiguration { List loadCertificates(); + /** + * @return true when the key material is held in a PKCS#11 token. Such keys are non-exportable, so the + * TLS engine must delegate signing to the token's provider (SunPKCS11) rather than BouncyCastle FIPS. + */ + default boolean isPkcs11() { + return false; + } + default KeyManagerFactory createKeyManagerFactory(boolean validateCertificates) { final var keyStore = createKeyStore(); if (validateCertificates) { @@ -119,17 +128,24 @@ public List loadCertificates() { } final var list = listBuilder.build(); if (list.isEmpty()) { - throw new OpenSearchException("The file " + path + " does not contain any certificates"); + throw new OpenSearchException( + "The keystore " + (path != null ? path : "(PKCS#11 token)") + " does not contain any certificates" + ); } return listBuilder.build(); } catch (GeneralSecurityException e) { - throw new OpenSearchException("Couldn't load certificates from file " + path, e); + throw new OpenSearchException("Couldn't load certificates from " + (path != null ? path : "PKCS#11 token"), e); } } + @Override + public boolean isPkcs11() { + return PemKeyReader.PKCS11.equalsIgnoreCase(type); + } + @Override public List files() { - return List.of(path); + return path != null ? List.of(path) : List.of(); } @Override diff --git a/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java index 5f7e6c0fe8..2af2c0aadb 100644 --- a/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java +++ b/src/main/java/org/opensearch/security/ssl/config/KeyStoreUtils.java @@ -212,14 +212,18 @@ public static void validateKeyStoreCertificates(final KeyStore keyStore, Set loadConfiguration(final Environment environment) { final var settings = environment.settings(); final var sslConfigSettings = settings.getByPrefix(fullSslConfigSuffix); - if (settings.hasValue(sslConfigSuffix + KEYSTORE_FILEPATH)) { + final boolean isPkcs11Keystore = PemKeyReader.PKCS11.equalsIgnoreCase(environment.settings().get(sslConfigSuffix + KEYSTORE_TYPE)); + final boolean isPkcs11Truststore = PemKeyReader.PKCS11.equalsIgnoreCase( + environment.settings().get(sslConfigSuffix + TRUSTSTORE_TYPE) + ); + if (settings.hasValue(sslConfigSuffix + KEYSTORE_FILEPATH) || isPkcs11Keystore) { final var keyStorePassword = resolvePassword(sslConfigSuffix + KEYSTORE_PASSWORD, settings, DEFAULT_STORE_PASSWORD); return Tuple.tuple( - environment.settings().hasValue(sslConfigSuffix + TRUSTSTORE_FILEPATH) + environment.settings().hasValue(sslConfigSuffix + TRUSTSTORE_FILEPATH) || isPkcs11Truststore ? buildJdkTrustStoreConfiguration( sslConfigSettings, environment, @@ -126,9 +130,10 @@ private KeyStoreConfiguration.JdkKeyStoreConfiguration buildJdkKeyStoreConfigura final char[] keyStorePassword, final char[] keyPassword ) { - final Path path = resolvePath(environment.settings().get(sslConfigSuffix + KEYSTORE_FILEPATH), environment); final String explicitType = environment.settings().get(sslConfigSuffix + KEYSTORE_TYPE); - final String resolvedType = PemKeyReader.extractStoreType(path.toString(), explicitType); + final boolean isPkcs11 = PemKeyReader.PKCS11.equalsIgnoreCase(explicitType); + final Path path = isPkcs11 ? null : resolvePath(environment.settings().get(sslConfigSuffix + KEYSTORE_FILEPATH), environment); + final String resolvedType = isPkcs11 ? PemKeyReader.PKCS11 : PemKeyReader.extractStoreType(path.toString(), explicitType); return new KeyStoreConfiguration.JdkKeyStoreConfiguration( path, resolvedType, @@ -143,9 +148,10 @@ private TrustStoreConfiguration.JdkTrustStoreConfiguration buildJdkTrustStoreCon final Environment environment, final char[] trustStorePassword ) { - final Path path = resolvePath(environment.settings().get(sslConfigSuffix + TRUSTSTORE_FILEPATH), environment); final String explicitType = environment.settings().get(sslConfigSuffix + TRUSTSTORE_TYPE); - final String resolvedType = PemKeyReader.extractStoreType(path.toString(), explicitType); + final boolean isPkcs11 = PemKeyReader.PKCS11.equalsIgnoreCase(explicitType); + final Path path = isPkcs11 ? null : resolvePath(environment.settings().get(sslConfigSuffix + TRUSTSTORE_FILEPATH), environment); + final String resolvedType = isPkcs11 ? PemKeyReader.PKCS11 : PemKeyReader.extractStoreType(path.toString(), explicitType); return new TrustStoreConfiguration.JdkTrustStoreConfiguration( path, resolvedType, diff --git a/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java index 62b4a707a7..7c3d42f350 100644 --- a/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java +++ b/src/main/java/org/opensearch/security/ssl/config/TrustStoreConfiguration.java @@ -112,11 +112,13 @@ public List loadCertificates() { } final var list = listBuilder.build(); if (list.isEmpty()) { - throw new OpenSearchException("The file " + path + " does not contain any certificates"); + throw new OpenSearchException( + "The truststore " + (path != null ? path : "(PKCS#11 token)") + " does not contain any certificates" + ); } return listBuilder.build(); } catch (GeneralSecurityException e) { - throw new OpenSearchException("Couldn't load certificates from file " + path, e); + throw new OpenSearchException("Couldn't load certificates from " + (path != null ? path : "PKCS#11 token"), e); } } diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index 689c41238e..4ab4dc78a2 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -24,11 +24,10 @@ import java.util.Locale; import java.util.function.Function; -import org.bouncycastle.crypto.CryptoServicesRegistrar; - import org.opensearch.common.settings.Setting; import org.opensearch.common.settings.Settings; import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.support.FipsMode; import io.netty.handler.ssl.ClientAuth; @@ -38,7 +37,9 @@ public final class SSLConfigConstants { */ public static final String DEFAULT_STORE_PASSWORD = "changeit"; // #16 public static final String JDK_TLS_REJECT_CLIENT_INITIATED_RENEGOTIATION = "jdk.tls.rejectClientInitiatedRenegotiation"; - public static final String[] ALLOWED_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + public static final String[] ALLOWED_SSL_PROTOCOLS = FipsMode.isEnabled() + ? new String[] { "TLSv1.3", "TLSv1.2" } + : new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; /** * Shared settings prefixes/postfixes @@ -46,9 +47,9 @@ public final class SSLConfigConstants { public static final String ENABLED = "enabled"; public static final String CLIENT_AUTH_MODE = "clientauth_mode"; public static final String ENFORCE_CERT_RELOAD_DN_VERIFICATION = "enforce_cert_reload_dn_verification"; - public static final String DEFAULT_STORE_TYPE = CryptoServicesRegistrar.isInApprovedOnlyMode() - ? "BCFKS" - : KeyStore.getDefaultType().toUpperCase(Locale.ROOT); + public static final String DEFAULT_STORE_TYPE = FipsMode.isEnabled() + ? "BCFKS" // PKCS#11 must remain opt-in via explicit config + : KeyStore.getDefaultType().toUpperCase(Locale.ROOT); // JDK default (PKCS12); JKS rejects null key passwords public static final String SSL_PREFIX = "plugins.security.ssl."; public static final String KEYSTORE_TYPE = "keystore_type"; diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java b/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java index 2dd4bef9c4..a0b855b15c 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLRequestHelper.java @@ -35,6 +35,7 @@ import java.util.Date; import java.util.Enumeration; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.net.ssl.SSLEngine; @@ -43,7 +44,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.opensearch.OpenSearchException; import org.opensearch.common.settings.Settings; @@ -55,6 +55,7 @@ import org.opensearch.security.ssl.transport.PrincipalExtractor.Type; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_HTTP_TRUSTSTORE_PASSWORD; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_TYPE; public class SSLRequestHelper { @@ -245,12 +246,19 @@ static CertificateValidator buildValidatorFromTruststore( final Collection crls, final String truststore ) throws IOException, GeneralSecurityException { - final String defaultStoreType = CryptoServicesRegistrar.isInApprovedOnlyMode() ? "BCFKS" : "JKS"; + final String defaultStoreType = DEFAULT_STORE_TYPE; final String truststoreType = settings.get(SSLConfigConstants.SECURITY_SSL_HTTP_TRUSTSTORE_TYPE, defaultStoreType); - final String truststorePassword = SECURITY_SSL_HTTP_TRUSTSTORE_PASSWORD.getSetting(settings); + final char[] truststorePassword = Optional.ofNullable(SECURITY_SSL_HTTP_TRUSTSTORE_PASSWORD.getSetting(settings)) + .filter(p -> !p.isEmpty()) + .map(String::toCharArray) + .orElse(null); final KeyStore ts = KeyStore.getInstance(truststoreType); - try (FileInputStream fin = new FileInputStream(env.configDir().resolve(truststore).toAbsolutePath().toString())) { - ts.load(fin, (truststorePassword == null || truststorePassword.isEmpty()) ? null : truststorePassword.toCharArray()); + if ("PKCS11".equalsIgnoreCase(truststoreType)) { + ts.load(null, truststorePassword); + } else { + try (final var fin = new FileInputStream(env.configDir().resolve(truststore).toAbsolutePath().toString())) { + ts.load(fin, truststorePassword); + } } return new CertificateValidator(trustAnchorsFrom(ts), crls); } diff --git a/src/main/java/org/opensearch/security/support/FipsMode.java b/src/main/java/org/opensearch/security/support/FipsMode.java new file mode 100644 index 0000000000..169a5d4208 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/FipsMode.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.support; + +/** + * Single source of truth for FIPS mode detection. + * Set {@code OPENSEARCH_FIPS_MODE=true} in the environment to enable. + */ +public final class FipsMode { + + public static java.util.function.Supplier envSupplier = () -> System.getenv("OPENSEARCH_FIPS_MODE"); + + public static boolean isEnabled() { + return "true".equalsIgnoreCase(envSupplier.get()); + } + + private FipsMode() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/org/opensearch/security/support/PemKeyReader.java b/src/main/java/org/opensearch/security/support/PemKeyReader.java index e99ee5161c..0057c8e4a2 100644 --- a/src/main/java/org/opensearch/security/support/PemKeyReader.java +++ b/src/main/java/org/opensearch/security/support/PemKeyReader.java @@ -39,27 +39,19 @@ import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.Paths; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; +import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyException; -import java.security.KeyFactory; import java.security.KeyStore; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.PKCS8EncodedKeySpec; import java.util.Collection; import java.util.Locale; -import javax.crypto.Cipher; -import javax.crypto.EncryptedPrivateKeyInfo; -import javax.crypto.NoSuchPaddingException; +import java.util.Optional; +import java.util.stream.Stream; import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -67,9 +59,15 @@ import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.crypto.CryptoServicesRegistrar; -import org.bouncycastle.util.io.pem.PemObject; -import org.bouncycastle.util.io.pem.PemReader; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.openssl.PEMException; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; +import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; +import org.bouncycastle.pkcs.PKCSException; import org.opensearch.OpenSearchException; import org.opensearch.common.settings.Settings; @@ -82,89 +80,58 @@ public final class PemKeyReader { private static final Logger log = LogManager.getLogger(PemKeyReader.class); public static final String JKS = "JKS"; + public static final String JCEKS = "JCEKS"; public static final String PKCS12 = "PKCS12"; public static final String BCFKS = "BCFKS"; + public static final String PKCS11 = "PKCS11"; - private static byte[] readPrivateKey(File file) throws KeyException { - try (final InputStream in = new FileInputStream(file)) { - return readPrivateKey(in); - } catch (final IOException e) { - throw new KeyException("could not fine key file: " + file); - } - } - - private static byte[] readPrivateKey(final InputStream in) throws KeyException { - try (final PemReader pemReader = new PemReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { - final PemObject pemObject = pemReader.readPemObject(); - if (pemObject == null) { - throw new KeyException( - "could not find a PKCS #8 private key in input stream" - + " (see http://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)" - ); - } - return pemObject.getContent(); - } catch (final IOException ioe) { - throw new KeyException( - "could not find a PKCS #8 private key in input stream" - + " (see http://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)", - ioe - ); - } - } - - public static PrivateKey toPrivateKey(File keyFile, String keyPassword) throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidKeySpecException, InvalidAlgorithmParameterException, KeyException, IOException { + public static PrivateKey toPrivateKey(File keyFile, String keyPassword) throws KeyException, IOException { if (keyFile == null) { return null; } - return getPrivateKeyFromByteBuffer(PemKeyReader.readPrivateKey(keyFile), keyPassword); + try (InputStream in = new FileInputStream(keyFile)) { + return toPrivateKey(in, keyPassword); + } } - public static PrivateKey toPrivateKey(InputStream in, String keyPassword) throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidKeySpecException, InvalidAlgorithmParameterException, KeyException, IOException { + public static PrivateKey toPrivateKey(InputStream in, String keyPassword) throws KeyException, IOException { if (in == null) { return null; } - return getPrivateKeyFromByteBuffer(PemKeyReader.readPrivateKey(in), keyPassword); - } - - private static PrivateKey getPrivateKeyFromByteBuffer(byte[] encodedKey, String keyPassword) throws NoSuchAlgorithmException, - NoSuchPaddingException, InvalidKeySpecException, InvalidAlgorithmParameterException, KeyException, IOException { - - PKCS8EncodedKeySpec encodedKeySpec = generateKeySpec(keyPassword == null ? null : keyPassword.toCharArray(), encodedKey); - try { - return KeyFactory.getInstance("RSA").generatePrivate(encodedKeySpec); - } catch (InvalidKeySpecException ignore) { - try { - return KeyFactory.getInstance("DSA").generatePrivate(encodedKeySpec); - } catch (InvalidKeySpecException ignore2) { - try { - return KeyFactory.getInstance("EC").generatePrivate(encodedKeySpec); - } catch (InvalidKeySpecException e) { - throw new InvalidKeySpecException("Neither RSA, DSA nor EC worked", e); + try (PEMParser parser = new PEMParser(new InputStreamReader(in, StandardCharsets.UTF_8))) { + Object obj = parser.readObject(); + if (obj == null) { + throw new KeyException( + "could not find a PKCS #8 private key in input stream" + + " (see http://netty.io/wiki/sslcontextbuilder-and-private-key.html for more information)" + ); + } + PrivateKeyInfo pki; + switch (obj) { + case PKCS8EncryptedPrivateKeyInfo pkcs8EncryptedPrivateKeyInfo -> { + if (keyPassword == null) { + throw new KeyException("private key is encrypted but no password was provided"); + } + try { + pki = pkcs8EncryptedPrivateKeyInfo.decryptPrivateKeyInfo( + new JceOpenSSLPKCS8DecryptorProviderBuilder().build(keyPassword.toCharArray()) + ); + } catch (PKCSException | OperatorCreationException e) { + throw new KeyException("Failed to decrypt private key", e); + } } + case PrivateKeyInfo privateKeyInfo -> pki = privateKeyInfo; + case PEMKeyPair pemKeyPair -> pki = pemKeyPair.getPrivateKeyInfo(); + default -> throw new KeyException("Unexpected PEM object type: " + obj.getClass().getName()); + } + try { + return new JcaPEMKeyConverter().getPrivateKey(pki); + } catch (PEMException e) { + throw new KeyException("Failed to convert private key", e); } } } - private static PKCS8EncodedKeySpec generateKeySpec(char[] password, byte[] key) throws IOException, NoSuchAlgorithmException, - NoSuchPaddingException, InvalidKeySpecException, InvalidKeyException, InvalidAlgorithmParameterException { - - if (password == null) { - return new PKCS8EncodedKeySpec(key); - } - - EncryptedPrivateKeyInfo encryptedPrivateKeyInfo = new EncryptedPrivateKeyInfo(key); - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(encryptedPrivateKeyInfo.getAlgName()); - PBEKeySpec pbeKeySpec = new PBEKeySpec(password); - SecretKey pbeKey = keyFactory.generateSecret(pbeKeySpec); - - Cipher cipher = Cipher.getInstance(encryptedPrivateKeyInfo.getAlgName()); - cipher.init(Cipher.DECRYPT_MODE, pbeKey, encryptedPrivateKeyInfo.getAlgParameters()); - - return encryptedPrivateKeyInfo.getKeySpec(cipher); - } - public static X509Certificate loadCertificateFromFile(String file) throws Exception { if (file == null) { return null; @@ -186,16 +153,81 @@ public static X509Certificate loadCertificateFromStream(InputStream in) throws E } public static KeyStore loadKeyStore(final String storePath, final String keyStorePassword, final String type) throws Exception { - if (storePath == null) { + if (storePath == null && !PKCS11.equalsIgnoreCase(type)) { return null; } String storeType = extractStoreType(storePath, type); - - final KeyStore store = KeyStore.getInstance(storeType); - store.load(new FileInputStream(storePath), keyStorePassword == null ? null : keyStorePassword.toCharArray()); + final char[] password = keyStorePassword == null ? null : keyStorePassword.toCharArray(); + final KeyStore store; + if (PKCS11.equalsIgnoreCase(storeType)) { + try { + store = KeyStore.getInstance(storeType); + store.load(null, password); + } catch (Exception e) { + throw new OpenSearchException( + "Failed to initialize PKCS#11 keystore. Ensure a PKCS#11 provider is registered and configured " + + "(e.g. SunPKCS11, IBMPKCS11Impl, or your HSM vendor's provider).", + e + ); + } + } else { + store = KeyStore.getInstance(storeType); + try (final var in = new FileInputStream(storePath)) { + store.load(in, password); + } + } return store; } + /** + * Loads a {@link SecretKey} entry from a keystore by alias. + * Delegates to {@link #loadKeyStore}, so FIPS-mode type enforcement (BCFKS / PKCS11 only) applies automatically. + * + * @param keyPassword key-level password; falls back to {@code keyStorePassword} when {@code null}, + * consistent with {@code keytool} convention where both passwords default to the same value + * @throws IllegalArgumentException for any failure: keystore I/O error, missing alias, or wrong entry type + */ + public static SecretKey loadSecretKeyFromKeystore( + final String storePath, + final String keyStorePassword, + final String type, + final String alias, + final String keyPassword + ) { + final KeyStore store; + try { + store = loadKeyStore(storePath, keyStorePassword, type); + } catch (Exception e) { + throw new IllegalArgumentException( + "Failed to load secret key from keystore-type '%s' at path '%s'".formatted(type, storePath), + e + ); + } + if (store == null) { + throw new IllegalArgumentException("Failed to load secret key from keystore-type '%s' at path '%s'".formatted(type, storePath)); + } + try { + final char[] kp = keyPassword != null + ? keyPassword.toCharArray() + : (keyStorePassword != null ? keyStorePassword.toCharArray() : null); + final Key key = store.getKey(alias, kp); + if (key == null) { + throw new IllegalArgumentException("No key found at alias '" + alias + "' in keystore"); + } + if (!(key instanceof SecretKey secretKey)) { + throw new IllegalArgumentException( + "Entry at alias '" + alias + "' is not a SecretKey (found " + key.getClass().getName() + ")" + ); + } + return secretKey; + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException( + "Failed to load secret key from keystore-type '%s' at path '%s'".formatted(type, storePath), + e + ); + } + } + public static PrivateKey loadKeyFromFile(String password, String keyFile) throws Exception { if (keyFile == null) { @@ -345,25 +377,16 @@ public static KeyStore toKeystore( } - public static char[] randomChars(int len) { - final SecureRandom r = new SecureRandom(); - final char[] ret = new char[len]; - for (int i = 0; i < len; i++) { - ret[i] = (char) (r.nextInt(26) + 'a'); - } - return ret; - } - public static String extractStoreType(String storePath, String storeType) { - if (null == storeType) { - storeType = detectStoreType(storePath); - } - if (CryptoServicesRegistrar.isInApprovedOnlyMode() && !PemKeyReader.BCFKS.equalsIgnoreCase(storeType)) { + var finalStoreType = Optional.ofNullable(storeType).orElseGet(() -> detectStoreType(storePath)); + var isFipsSupportedStoreType = Stream.of(PKCS11, BCFKS).anyMatch(it -> it.equalsIgnoreCase(finalStoreType)); + + if (FipsMode.isEnabled() && !isFipsSupportedStoreType) { throw new IllegalArgumentException( - storeType.toUpperCase(Locale.ROOT) + " keystores / truststores are not supported in FIPS mode - use BCFKS." + finalStoreType.toUpperCase(Locale.ROOT) + " keystores / truststores are not supported in FIPS mode - use BCFKS or PKCS#11" ); } - return storeType; + return finalStoreType; } private static String detectStoreType(String path) { @@ -381,6 +404,14 @@ private static String detectStoreType(String path) { ) { return PemKeyReader.JKS; } + // JCEKS: 0xCECECECE + if ((magic[0] & 0xFF) == 0xCE // + && (magic[1] & 0xFF) == 0xCE // + && (magic[2] & 0xFF) == 0xCE // + && (magic[3] & 0xFF) == 0xCE // + ) { + return PemKeyReader.JCEKS; + } // ASN.1: distinguish BCFKS from PKCS12 by outer structure // PKCS12 (RFC 7292): outer SEQUENCE starts with INTEGER (version = 3) // BCFKS: outer SEQUENCE starts with SEQUENCE (encrypted content envelope) diff --git a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java index 066ff3d547..c90cd3b981 100644 --- a/src/main/java/org/opensearch/security/tools/SecurityAdmin.java +++ b/src/main/java/org/opensearch/security/tools/SecurityAdmin.java @@ -41,7 +41,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.security.KeyStore; +import java.security.Principal; import java.security.PrivateKey; +import java.security.Provider; +import java.security.Security; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; @@ -51,8 +54,12 @@ import java.util.Locale; import java.util.Map; import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedKeyManager; import com.google.common.base.Charsets; import com.google.common.base.Joiner; @@ -191,17 +198,27 @@ public static int execute(final String[] args) throws Exception { Options options = new Options(); options.addOption("nhnv", "disable-host-name-verification", false, "Disable hostname verification"); options.addOption( - Option.builder("ts").longOpt("truststore").hasArg().argName("file").desc("Path to truststore (JKS/PKCS12 format)").build() + Option.builder("ts") + .longOpt("truststore") + .hasArg() + .argName("file") + .desc("Path to truststore (JKS/PKCS12/BCFKS/PKCS11 format)") + .build() ); options.addOption( - Option.builder("ks").longOpt("keystore").hasArg().argName("file").desc("Path to keystore (JKS/PKCS12 format").build() + Option.builder("ks") + .longOpt("keystore") + .hasArg() + .argName("file") + .desc("Path to keystore (JKS/PKCS12/BCFKS/PKCS11 format") + .build() ); options.addOption( Option.builder("tst") .longOpt("truststore-type") .hasArg() .argName("type") - .desc("JKS or PKCS12, if not given we use the file extension to detect the type") + .desc("JKS, PKCS12, BCFKS, or PKCS11; if not given the type is auto-detected from the file") .build() ); options.addOption( @@ -209,7 +226,7 @@ public static int execute(final String[] args) throws Exception { .longOpt("keystore-type") .hasArg() .argName("type") - .desc("JKS or PKCS12, if not given we use the file extension to detect the type") + .desc("JKS, PKCS12, BCFKS, or PKCS11; if not given the type is auto-detected from the file") .build() ); options.addOption( @@ -507,6 +524,8 @@ public static int execute(final String[] args) throws Exception { if (kspass == null && promptForPassword) { kspass = promptForPassword("Keystore", "kspass", OPENDISTRO_SECURITY_KS_PASS); } + } else if ("PKCS11".equalsIgnoreCase(kst) && kspass == null && promptForPassword) { + kspass = promptForPassword("PKCS#11 token PIN", "kspass", OPENDISTRO_SECURITY_KS_PASS); } if (ts != null) { @@ -514,6 +533,8 @@ public static int execute(final String[] args) throws Exception { if (tspass == null && promptForPassword) { tspass = promptForPassword("Truststore", "tspass", OPENDISTRO_SECURITY_TS_PASS); } + } else if ("PKCS11".equalsIgnoreCase(tst) && tspass == null && promptForPassword) { + tspass = promptForPassword("PKCS#11 token PIN", "tspass", OPENDISTRO_SECURITY_TS_PASS); } if (key != null) { @@ -1223,12 +1244,34 @@ private static void validate(CommandLine line) throws ParseException { throw new ParseException("Specify at least -cd or -f together with vc"); } - if (!line.hasOption("vc") && !line.hasOption("ks") && !line.hasOption("cert") /*&& !line.hasOption("simple-auth")*/) { - throw new ParseException("Specify at least -ks or -cert"); + boolean pkcs11Keystore = "PKCS11".equalsIgnoreCase(line.getOptionValue("kst")); + if (!line.hasOption("vc") && !line.hasOption("ks") && !line.hasOption("cert") && !pkcs11Keystore) { + throw new ParseException("Specify at least -ks, -cert, or -kst PKCS11"); + } + if (pkcs11Keystore && line.hasOption("ks")) { + throw new ParseException( + "Do not specify -ks together with -kst PKCS11; PKCS11 keystores are token-based and have no file path" + ); + } + if (pkcs11Keystore && Security.getProviders("KeyStore.PKCS11") == null) { + throw new ParseException("No PKCS#11 provider is registered in the JVM; cannot use -kst PKCS11"); } - if (!line.hasOption("vc") && !line.hasOption("mo") && !line.hasOption("ts") && !line.hasOption("cacert")) { - throw new ParseException("Specify at least -ts or -cacert"); + boolean pkcs11Truststore = "PKCS11".equalsIgnoreCase(line.getOptionValue("tst")); + if (!line.hasOption("vc") && !line.hasOption("ts") && !line.hasOption("cacert") && !pkcs11Truststore) { + throw new ParseException("Specify at least -ts, -cacert, or -tst PKCS11"); + } + if (pkcs11Truststore && line.hasOption("ts")) { + throw new ParseException( + "Do not specify -ts together with -tst PKCS11; PKCS11 truststores are token-based and have no file path" + ); + } + if (pkcs11Truststore && Security.getProviders("KeyStore.PKCS11") == null) { + throw new ParseException("No PKCS#11 provider is registered in the JVM; cannot use -tst PKCS11"); + } + + if (line.hasOption("cert") != line.hasOption("key")) { + throw new ParseException("-cert and -key must both be specified together"); } // TODO add more validation rules @@ -1515,32 +1558,37 @@ private static SSLContext sslContext( String keypass ) throws Exception { + if ("PKCS11".equalsIgnoreCase(keyStoreType)) { + final char[] kspassChars = kspass == null ? null : kspass.toCharArray(); + return buildPkcs11SslContext( + loadStore(ks, keyStoreType, kspassChars), + kspassChars, + ksAlias, + cacert, + ts, + tspass, + trustStoreType + ); + } + final SSLContextBuilder sslContextBuilder = SSLContexts.custom(); if (ks != null) { - File keyStoreFile = Paths.get(ks).toFile(); - - KeyStore keyStore = KeyStore.getInstance(keyStoreType.toUpperCase()); - keyStore.load(new FileInputStream(keyStoreFile), kspass.toCharArray()); - sslContextBuilder.loadKeyMaterial(keyStore, kspass.toCharArray(), (aliases, socket) -> { + final char[] kspassChars = kspass == null ? null : kspass.toCharArray(); + final KeyStore keyStore = loadStore(ks, keyStoreType, kspassChars); + sslContextBuilder.loadKeyMaterial(keyStore, kspassChars, (aliases, socket) -> { if (aliases == null || aliases.isEmpty()) { return ksAlias; } - if (ksAlias == null || ksAlias.isEmpty()) { return aliases.keySet().iterator().next(); } - return ksAlias; }); } - if (ts != null) { - File trustStoreFile = Paths.get(ts).toFile(); - - KeyStore trustStore = KeyStore.getInstance(trustStoreType.toUpperCase()); - trustStore.load(new FileInputStream(trustStoreFile), tspass == null ? null : tspass.toCharArray()); - sslContextBuilder.loadTrustMaterial(trustStore, null); + if (ts != null || "PKCS11".equalsIgnoreCase(trustStoreType)) { + sslContextBuilder.loadTrustMaterial(loadStore(ts, trustStoreType, tspass == null ? null : tspass.toCharArray()), null); } if (cacert != null) { @@ -1585,6 +1633,100 @@ private static SSLContext sslContext( return sslContextBuilder.build(); } + private static SSLContext buildPkcs11SslContext( + KeyStore keyStore, + char[] pin, + String ksAlias, + String cacert, + String ts, + String tspass, + String trustStoreType + ) throws Exception { + Provider sunJSSE = Security.getProvider("SunJSSE"); + if (sunJSSE == null) { + throw new IllegalStateException("SunJSSE provider not available; required for PKCS11 key store support"); + } + SSLContext ctx = SSLContext.getInstance("TLS", sunJSSE); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, pin); + KeyManager[] kms = Arrays.stream(kmf.getKeyManagers()) + .map(km -> km instanceof X509ExtendedKeyManager ? new AliasSelectingKeyManager((X509ExtendedKeyManager) km, ksAlias) : km) + .toArray(KeyManager[]::new); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + if (cacert != null) { + try (FileInputStream in = new FileInputStream(Paths.get(cacert).toFile())) { + tmf.init(PemKeyReader.toTruststore("ca", PemKeyReader.loadCertificatesFromStream(in))); + } + } else if (ts != null || "PKCS11".equalsIgnoreCase(trustStoreType)) { + tmf.init(loadStore(ts, trustStoreType, tspass == null ? null : tspass.toCharArray())); + } else { + tmf.init((KeyStore) null); + } + + ctx.init(kms, tmf.getTrustManagers(), null); + return ctx; + } + + private static final class AliasSelectingKeyManager extends X509ExtendedKeyManager { + private final X509ExtendedKeyManager delegate; + private final String alias; + + AliasSelectingKeyManager(X509ExtendedKeyManager delegate, String alias) { + this.delegate = delegate; + this.alias = alias; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, java.net.Socket socket) { + return alias != null ? alias : delegate.chooseClientAlias(keyType, issuers, socket); + } + + @Override + public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) { + return alias != null ? alias : delegate.chooseEngineClientAlias(keyType, issuers, engine); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, java.net.Socket socket) { + return delegate.chooseServerAlias(keyType, issuers, socket); + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return delegate.getClientAliases(keyType, issuers); + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return delegate.getServerAliases(keyType, issuers); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return delegate.getCertificateChain(alias); + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return delegate.getPrivateKey(alias); + } + } + + private static KeyStore loadStore(String path, String type, char[] password) throws Exception { + KeyStore ks = KeyStore.getInstance(type.toUpperCase()); + + if ("PKCS11".equalsIgnoreCase(type)) { + ks.load(null, password); + } else { + try (final var fin = new FileInputStream(Paths.get(path).toFile())) { + ks.load(fin, password); + } + } + return ks; + } + private static String responseToString(Response response, boolean prettyJson) { ByteSource byteSource = new ByteSource() { @Override diff --git a/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java index 180426dcdc..d88acf3f81 100644 --- a/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java +++ b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java @@ -30,6 +30,7 @@ import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; @@ -57,8 +58,9 @@ public class SecuritySettingsConfigurer { public SecuritySettingsConfigurer(Installer installer) { this.installer = installer; + String hashingAlgorithm = FipsMode.isEnabled() ? ConfigConstants.PBKDF2 : ConfigConstants.BCRYPT; this.passwordHasher = PasswordHasherFactory.createPasswordHasher( - Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT).build() + Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, hashingAlgorithm).build() ); } @@ -248,8 +250,9 @@ void updateAdminPassword() throws IOException { */ private boolean isAdminPasswordSetToAdmin(String internalUsersFile) throws IOException { JsonNode internalUsers = yamlMapper().readTree(new FileInputStream(internalUsersFile)); - return internalUsers.has("admin") - && passwordHasher.check(DEFAULT_ADMIN_PASSWORD.toCharArray(), internalUsers.get("admin").get("hash").asString()); + if (!internalUsers.has("admin")) return false; + String hash = internalUsers.get("admin").get("hash").asString(); + return passwordHasher.check(passwordHasher.defaultAdminPassword().toCharArray(), hash); } /** diff --git a/src/main/java/org/opensearch/security/user/UserService.java b/src/main/java/org/opensearch/security/user/UserService.java index bc3bd9301c..7110bfebdc 100644 --- a/src/main/java/org/opensearch/security/user/UserService.java +++ b/src/main/java/org/opensearch/security/user/UserService.java @@ -13,14 +13,17 @@ import java.io.IOException; import java.net.URLDecoder; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Random; import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; @@ -47,6 +50,7 @@ import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.InternalUserV7; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.support.SecurityJsonNode; import org.opensearch.transport.client.Client; @@ -156,9 +160,13 @@ public SecurityDynamicConfiguration createOrUpdateAccount(ObjectNode contentA if (!attributeNode.get("service").isNull() && attributeNode.get("service").asString().equalsIgnoreCase("true")) { // If this is a // service account verifyServiceAccount(securityJsonNode, accountName); - String password = generatePassword(); - contentAsNode.put("hash", passwordHasher.hash(password.toCharArray())); - contentAsNode.put("service", "true"); + char[] password = generatePassword(); + try { + contentAsNode.put("hash", passwordHasher.hash(password)); + contentAsNode.put("service", "true"); + } finally { + Arrays.fill(password, '\0'); + } } else { contentAsNode.put("service", "false"); } @@ -234,13 +242,17 @@ private void verifyServiceAccount(SecurityJsonNode securityJsonNode, String acco private static final String ALL_CHARS = LOWERCASE + UPPERCASE + DIGITS; /** - * Generate an 8 - 16 character password with 1+ lowercase, 1+ uppercase, 1+ digit. + * Generate a random password with 1+ lowercase, 1+ uppercase, 1+ digit. Under FIPS the length is 20-27 + * characters (≥119 bits over the 62-char alphabet, exceeding the FIPS 112-bit requirement); otherwise 8-15. + * Uses {@link Randomness#createSecure()}, which resolves to the FIPS-approved SP 800-90A DRBG when FIPS is + * enabled. The returned {@code char[]} must be zeroed by the caller after use. * - * @return A password for a service account. + * @return A generated service-account password. */ - public static String generatePassword() { - Random random = Randomness.get(); - int length = random.nextInt(8) + 8; + public static char[] generatePassword() { + final SecureRandom random = Randomness.createSecure(); + final int base = FipsMode.isEnabled() ? 20 : 8; + int length = random.nextInt(8) + base; // FIPS: 20-27 chars (≥119 bits); otherwise: 8-15 chars char[] password = new char[length]; // Guarantee at least one of each required type @@ -261,7 +273,7 @@ public static String generatePassword() { password[j] = tmp; } - return new String(password); + return password; } /** @@ -297,22 +309,41 @@ public AuthToken generateAuthToken(String accountName) throws IOException { .orElseThrow(() -> new UserServiceException(AUTH_TOKEN_GENERATION_MESSAGE)); // Generate a new password for the account and store the hash of it - String plainTextPassword = generatePassword(); - contentAsNode.put("hash", passwordHasher.hash(plainTextPassword.toCharArray())); - contentAsNode.put("enabled", "true"); - contentAsNode.put("service", "true"); - - // Update the internal user associated with the auth token - internalUsersConfiguration.remove(accountName); - contentAsNode.remove("name"); - internalUsersConfiguration.putCObject( - accountName, - DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) - ); - saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); - - authToken = Base64.getUrlEncoder().encodeToString((accountName + ":" + plainTextPassword).getBytes(StandardCharsets.UTF_8)); - return new BasicAuthToken("Basic " + authToken); + char[] plainTextPassword = generatePassword(); + byte[] credentialBytes = null; + try { + contentAsNode.put("hash", passwordHasher.hash(plainTextPassword)); + contentAsNode.put("enabled", "true"); + contentAsNode.put("service", "true"); + + // Update the internal user associated with the auth token + internalUsersConfiguration.remove(accountName); + contentAsNode.remove("name"); + internalUsersConfiguration.putCObject( + accountName, + DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()) + ); + saveAndUpdateConfigs(getUserConfigName().toString(), client, CType.INTERNALUSERS, internalUsersConfiguration); + + byte[] accountBytes = accountName.getBytes(StandardCharsets.UTF_8); + ByteBuffer passwordByteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(plainTextPassword)); + byte[] passwordBytes = new byte[passwordByteBuffer.remaining()]; + passwordByteBuffer.get(passwordBytes); + Arrays.fill(passwordByteBuffer.array(), (byte) 0); + credentialBytes = new byte[accountBytes.length + 1 + passwordBytes.length]; + System.arraycopy(accountBytes, 0, credentialBytes, 0, accountBytes.length); + credentialBytes[accountBytes.length] = ':'; + System.arraycopy(passwordBytes, 0, credentialBytes, accountBytes.length + 1, passwordBytes.length); + Arrays.fill(passwordBytes, (byte) 0); + + authToken = Base64.getUrlEncoder().encodeToString(credentialBytes); + return new BasicAuthToken("Basic " + authToken); + } finally { + Arrays.fill(plainTextPassword, '\0'); + if (credentialBytes != null) { + Arrays.fill(credentialBytes, (byte) 0); + } + } } catch (JacksonException ex) { throw new UserServiceException(FAILED_ACCOUNT_RETRIEVAL_MESSAGE); diff --git a/src/main/java/org/opensearch/security/util/KeyUtils.java b/src/main/java/org/opensearch/security/util/KeyUtils.java index e69f51e5a1..ae6f716226 100644 --- a/src/main/java/org/opensearch/security/util/KeyUtils.java +++ b/src/main/java/org/opensearch/security/util/KeyUtils.java @@ -11,6 +11,7 @@ package org.opensearch.security.util; +import java.nio.file.Path; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -18,12 +19,15 @@ import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import java.util.Objects; +import javax.crypto.SecretKey; import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchSecurityException; +import org.opensearch.common.settings.Settings; import org.opensearch.core.common.Strings; import org.opensearch.secure_sm.AccessController; +import org.opensearch.security.support.PemKeyReader; import io.jsonwebtoken.JwtParserBuilder; import io.jsonwebtoken.Jwts; @@ -31,6 +35,51 @@ public class KeyUtils { + public static final String KEYSTORE_ALIAS = "_keystore_alias"; + public static final String KEYSTORE_PATH = "_keystore_path"; + public static final String KEYSTORE_TYPE = "_keystore_type"; + public static final String KEYSTORE_PASSWORD = "_keystore_password"; + public static final String KEYSTORE_KEY_PASSWORD = "_keystore_key_password"; + + /** + * Loads a {@link SecretKey} from a keystore configured via the given settings prefix. + *

+ * Expected settings: + *

    + *
  • {@code }{@value #KEYSTORE_ALIAS} — required; absence returns {@code null} (acts as an opt-in flag)
  • + *
  • {@code }{@value #KEYSTORE_PATH} — the keystore file; an absolute path is used as-is, a relative + * path is resolved against {@code configPath} (the node config directory), so the keystore can be configured + * relative to that directory like other security file settings
  • + *
  • {@code }{@value #KEYSTORE_TYPE} — optional; auto-detected from file content when absent
  • + *
  • {@code }{@value #KEYSTORE_PASSWORD} — the keystore password
  • + *
  • {@code }{@value #KEYSTORE_KEY_PASSWORD} — the key password; falls back to {@code KEYSTORE_PASSWORD} when absent
  • + *
+ * + * @param configPath the node config directory; resolved against for relative keystore paths + * @return the loaded {@link SecretKey}, or {@code null} if the alias setting is absent + * @throws IllegalArgumentException if the alias is present but the path is missing, the keystore + * cannot be loaded, or the entry at the alias is not a {@link SecretKey} + */ + public static SecretKey loadKeyFromKeystore(final Settings settings, final String prefix, final Path configPath) { + final String alias = settings.get(prefix + KEYSTORE_ALIAS); + if (alias == null) { + return null; + } + final String type = settings.get(prefix + KEYSTORE_TYPE); + final String ksPassword = settings.get(prefix + KEYSTORE_PASSWORD); + final String keyPassword = settings.get(prefix + KEYSTORE_KEY_PASSWORD); + final String pathStr = resolveKeystorePath(settings.get(prefix + KEYSTORE_PATH), configPath); + + return PemKeyReader.loadSecretKeyFromKeystore(pathStr, ksPassword, type, alias, keyPassword); + } + + private static String resolveKeystorePath(final String pathStr, final Path configPath) { + if (pathStr == null || pathStr.isEmpty()) { + return pathStr; + } + return configPath.resolve(pathStr).toAbsolutePath().toString(); + } + public static JwtParserBuilder createJwtParserBuilderFromSigningKey(final String signingKey, final Logger log) { JwtParserBuilder jwtParserBuilder = null; diff --git a/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfigurator.java b/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfigurator.java index 0ec161c64a..6706462e69 100644 --- a/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfigurator.java +++ b/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfigurator.java @@ -78,7 +78,7 @@ public class SettingsBasedSSLConfigurator { public static final String VERIFY_HOSTNAMES = "verify_hostnames"; public static final String TRUST_ALL = "trust_all"; - private static final List DEFAULT_TLS_PROTOCOLS = ImmutableList.of("TLSv1.2", "TLSv1.1"); + private static final List DEFAULT_TLS_PROTOCOLS = ImmutableList.copyOf(SSLConfigConstants.ALLOWED_SSL_PROTOCOLS); private SSLContextBuilder sslContextBuilder; private final Settings settings; @@ -302,7 +302,7 @@ private void initFromPem() throws SSLConfigException { } try { - effectiveKeyPassword = PemKeyReader.randomChars(12); + effectiveKeyPassword = SSLConfigConstants.DEFAULT_STORE_PASSWORD.toCharArray(); effectiveKeyAlias = "al"; effectiveTruststore = PemKeyReader.toTruststore(effectiveKeyAlias, trustCertificates); effectiveKeystore = PemKeyReader.toKeystore( @@ -517,14 +517,6 @@ public String[] getEffectiveTruststoreAliasesArray() { } } - public String[] getEffectiveKeyAliasesArray() { - if (this.effectiveKeyAlias == null) { - return null; - } else { - return new String[] { this.effectiveKeyAlias }; - } - } - @Override public String toString() { return "SSLConfig [sslContext=" diff --git a/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4.java b/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4.java index ea170878f4..546f0ba6b9 100644 --- a/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4.java +++ b/src/main/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4.java @@ -303,7 +303,7 @@ private void initFromPem() throws SSLConfigException { } try { - effectiveKeyPassword = PemKeyReader.randomChars(12); + effectiveKeyPassword = SSLConfigConstants.DEFAULT_STORE_PASSWORD.toCharArray(); effectiveKeyAlias = "al"; effectiveTruststore = PemKeyReader.toTruststore(effectiveKeyAlias, trustCertificates); effectiveKeystore = PemKeyReader.toKeystore( @@ -531,14 +531,6 @@ public String[] getEffectiveTruststoreAliasesArray() { } } - public String[] getEffectiveKeyAliasesArray() { - if (this.effectiveKeyAlias == null) { - return null; - } else { - return new String[] { this.effectiveKeyAlias }; - } - } - @Override public String toString() { return "SSLConfig [sslContext=" diff --git a/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java b/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java index 99782324dc..ff59cf8837 100644 --- a/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java +++ b/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java @@ -12,19 +12,24 @@ package org.opensearch.security; import java.io.File; +import java.nio.file.Files; import java.util.Arrays; +import java.util.Objects; import org.apache.hc.core5.http.Header; import org.apache.http.HttpStatus; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.opensearch.common.settings.Settings; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.cluster.ClusterConfiguration; import org.opensearch.security.test.helper.cluster.ClusterHelper; +import org.opensearch.security.test.helper.file.FipsHashAdapter; import org.opensearch.security.test.helper.rest.RestHelper; import static org.hamcrest.MatcherAssert.assertThat; @@ -32,9 +37,17 @@ public class AdvancedSecurityMigrationTests extends SingleClusterTest { + @Rule + public TemporaryFolder configDirectory = new TemporaryFolder(); + @Before - public void setupBefore() { - ClusterHelper.updateDefaultDirectory(new File(TEST_RESOURCE_RELATIVE_PATH + "security_passive").getAbsolutePath()); + public void setupBefore() throws Exception { + final File source = new File(TEST_RESOURCE_RELATIVE_PATH + "security_passive"); + for (final File yml : Objects.requireNonNull(source.listFiles((dir, name) -> name.endsWith(".yml")))) { + Files.copy(yml.toPath(), configDirectory.getRoot().toPath().resolve(yml.getName())); + } + FipsHashAdapter.adaptConfigFile(new File(configDirectory.getRoot(), "internal_users.yml")); + ClusterHelper.updateDefaultDirectory(configDirectory.getRoot().getAbsolutePath()); } @After diff --git a/src/test/java/org/opensearch/security/InitializationIntegrationTests.java b/src/test/java/org/opensearch/security/InitializationIntegrationTests.java index 76ccf94c92..a9434982d6 100644 --- a/src/test/java/org/opensearch/security/InitializationIntegrationTests.java +++ b/src/test/java/org/opensearch/security/InitializationIntegrationTests.java @@ -35,6 +35,7 @@ import org.apache.hc.core5.http2.HttpVersionPolicy; import org.apache.http.HttpStatus; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; @@ -52,6 +53,7 @@ import org.opensearch.security.action.configupdate.ConfigUpdateResponse; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.cluster.ClusterHelper; @@ -282,6 +284,7 @@ public void testConfigHotReload() throws Exception { @Test public void testDefaultConfig() throws Exception { + Assume.assumeFalse("Shipped default config is not FIPS-compatible", FipsMode.isEnabled()); final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true).build(); setup(Settings.EMPTY, null, settings, false); RestHelper rh = nonSslRestHelper(); diff --git a/src/test/java/org/opensearch/security/OpenSearchSecurityPluginFIPSValidationTest.java b/src/test/java/org/opensearch/security/OpenSearchSecurityPluginFIPSValidationTest.java index 8e8e93877b..a530eab695 100644 --- a/src/test/java/org/opensearch/security/OpenSearchSecurityPluginFIPSValidationTest.java +++ b/src/test/java/org/opensearch/security/OpenSearchSecurityPluginFIPSValidationTest.java @@ -29,7 +29,7 @@ public void testFipsModeWithDefaultAlgorithmThrows() { IllegalStateException ex = assertThrows( IllegalStateException.class, - () -> OpenSearchSecurityPlugin.validateFipsMode("true", settings) + () -> OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true) ); assertThat(ex.getMessage(), containsString("FIPS mode is enabled")); assertThat(ex.getMessage(), containsString("Only PBKDF2 is allowed in FIPS mode")); @@ -42,7 +42,7 @@ public void testFipsModeWithBcryptThrows() { IllegalStateException ex = assertThrows( IllegalStateException.class, - () -> OpenSearchSecurityPlugin.validateFipsMode("true", settings) + () -> OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true) ); assertThat(ex.getMessage(), containsString("bcrypt")); assertThat(ex.getMessage(), containsString("FIPS mode is enabled")); @@ -54,7 +54,7 @@ public void testFipsModeWithArgon2Throws() { IllegalStateException ex = assertThrows( IllegalStateException.class, - () -> OpenSearchSecurityPlugin.validateFipsMode("true", settings) + () -> OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true) ); assertThat(ex.getMessage(), containsString("argon2")); } @@ -64,7 +64,7 @@ public void testFipsModeWithPbkdf2Succeeds() { Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "pbkdf2").build(); // Should not throw - OpenSearchSecurityPlugin.validateFipsMode("true", settings); + OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true); } @Test @@ -72,7 +72,7 @@ public void testFipsModeWithPbkdf2UpperCaseSucceeds() { Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "PBKDF2").build(); // Should not throw - OpenSearchSecurityPlugin.validateFipsMode("true", settings); + OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true); } @Test @@ -80,14 +80,62 @@ public void testFipsModeDisabledAllowsAnyAlgorithm() { Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "bcrypt").build(); // Should not throw when FIPS mode is not enabled - OpenSearchSecurityPlugin.validateFipsMode("false", settings); + OpenSearchSecurityPlugin.validateFipsMode(settings, false, () -> false); } @Test - public void testFipsModeNullEnvAllowsAnyAlgorithm() { + public void testFipsModeWithPasswordMinLengthBelowFloorThrows() { + Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "pbkdf2") + .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 8) + .build(); + + IllegalStateException ex = assertThrows( + IllegalStateException.class, + () -> OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true) + ); + assertThat(ex.getMessage(), containsString("FIPS mode requires at least 14 characters")); + } + + @Test + public void testFipsModeWithPasswordMinLengthAtFloorSucceeds() { + Settings settings = Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "pbkdf2") + .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, 14) + .build(); + + // At the floor is acceptable + OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true); + } + + @Test + public void testFipsModeWithPasswordMinLengthUnsetSucceeds() { + // Unset (-1) is fine: additionalSettings() defaults it to the FIPS floor at boot. + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "pbkdf2").build(); + + OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> true); + } + + @Test + public void testFipsModeEnabledWithoutApprovedOnlyModeThrows() { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "pbkdf2").build(); + + IllegalStateException ex = assertThrows( + IllegalStateException.class, + () -> OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> false) + ); + assertThat(ex.getMessage(), containsString("BCFIPS security provider is not running in FIPS approved-only mode")); + } + + @Test + public void testFipsModeEnabledWithBothViolationsThrows() { Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, "bcrypt").build(); - // Should not throw when env var is null - OpenSearchSecurityPlugin.validateFipsMode(null, settings); + IllegalStateException ex = assertThrows( + IllegalStateException.class, + () -> OpenSearchSecurityPlugin.validateFipsMode(settings, true, () -> false) + ); + assertThat(ex.getMessage(), containsString("BCFIPS security provider is not running in FIPS approved-only mode")); + assertThat(ex.getMessage(), containsString("Only PBKDF2 is allowed in FIPS mode")); } } diff --git a/src/test/java/org/opensearch/security/UserServiceUnitTests.java b/src/test/java/org/opensearch/security/UserServiceUnitTests.java index b449629058..dcd1fb0d28 100644 --- a/src/test/java/org/opensearch/security/UserServiceUnitTests.java +++ b/src/test/java/org/opensearch/security/UserServiceUnitTests.java @@ -17,6 +17,7 @@ import java.util.Optional; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -29,6 +30,7 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.user.UserFilterType; import org.opensearch.security.user.UserService; import org.opensearch.transport.client.Client; @@ -41,6 +43,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) @@ -121,19 +124,42 @@ public void restrictedFromUsername() { @Test public void testGeneratedPasswordContents() { - String password = UserService.generatePassword(); + final int min = FipsMode.isEnabled() ? 20 : 8; + final int max = FipsMode.isEnabled() ? 27 : 15; + char[] password = UserService.generatePassword(); + assertThat(password.length >= min && password.length <= max, is(true)); + assertThat(new String(password).chars().anyMatch(Character::isLowerCase), is(true)); + assertThat(new String(password).chars().anyMatch(Character::isUpperCase), is(true)); + assertThat(new String(password).chars().anyMatch(Character::isDigit), is(true)); + + char[] password2 = UserService.generatePassword(); + assertNotEquals(new String(password), new String(password2)); + } - // Verify length is 8-16 - assertThat(password.length() >= 8 && password.length() <= 16, is(true)); + @Test + public void testGeneratedPasswordEntropyMeetsFipsRequirement() { + Assume.assumeTrue("Password entropy floor only applies under FIPS", FipsMode.isEnabled()); + // FIPS 140-2/3 requires minimum 112 bits of entropy for cryptographic keys + final double FIPS_MIN_ENTROPY_BITS = 112.0; + final int CHARSET_SIZE = 62; // a-z (26) + A-Z (26) + 0-9 (10) + final int MIN_PASSWORD_LENGTH = 20; + + double entropyPerChar = Math.log(CHARSET_SIZE) / Math.log(2); + double minEntropy = entropyPerChar * MIN_PASSWORD_LENGTH; + + assertTrue( + String.format("Password entropy %.2f bits must be >= %.2f bits (FIPS minimum)", minEntropy, FIPS_MIN_ENTROPY_BITS), + minEntropy >= FIPS_MIN_ENTROPY_BITS + ); - // Verify at least 1 lowercase, 1 uppercase, 1 digit - assertThat(password.chars().anyMatch(Character::isLowerCase), is(true)); - assertThat(password.chars().anyMatch(Character::isUpperCase), is(true)); - assertThat(password.chars().anyMatch(Character::isDigit), is(true)); + char[] password = UserService.generatePassword(); + assertTrue("Generated password must be >= " + MIN_PASSWORD_LENGTH + " chars", password.length >= MIN_PASSWORD_LENGTH); - // Verify two generated passwords are different - String password2 = UserService.generatePassword(); - assertNotEquals(password, password2); + double actualEntropy = entropyPerChar * password.length; + assertTrue( + String.format("Actual password entropy %.2f bits must be >= %.2f bits", actualEntropy, FIPS_MIN_ENTROPY_BITS), + actualEntropy >= FIPS_MIN_ENTROPY_BITS + ); } } diff --git a/src/test/java/org/opensearch/security/UtilTests.java b/src/test/java/org/opensearch/security/UtilTests.java index f357e59221..433a1055a9 100644 --- a/src/test/java/org/opensearch/security/UtilTests.java +++ b/src/test/java/org/opensearch/security/UtilTests.java @@ -34,6 +34,7 @@ import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.support.SecurityUtils; import org.opensearch.security.support.WildcardMatcher; @@ -41,6 +42,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; public class UtilTests { @@ -156,6 +158,7 @@ public void testEnvReplace() { @Test public void testEnvReplacePBKDF2() { + assumeFalse("BCFIPS requires password to be at least 14 chars long", FipsMode.isEnabled()); Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.PBKDF2).build(); final PasswordHasher passwordHasherPBKDF2 = PasswordHasherFactory.createPasswordHasher(settings); assertThat(SecurityUtils.replaceEnvVars("abv${env.MYENV}xyz", settings), is("abv${env.MYENV}xyz")); @@ -192,6 +195,52 @@ public void testEnvReplacePBKDF2() { assertTrue(checked); } + @Test + public void testEnvReplacePBKDF2BCFips() { + // BCFIPS approved-only mode requires passwords of at least 14 characters (>= 112 bits of entropy). + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.PBKDF2).build(); + final PasswordHasher passwordHasherPBKDF2 = PasswordHasherFactory.createPasswordHasher(settings); + assertThat(SecurityUtils.replaceEnvVars("abv${env.MYENV}xyz", settings), is("abv${env.MYENV}xyz")); + assertThat(SecurityUtils.replaceEnvVars("abv${envbc.MYENV}xyz", settings), is("abv${envbc.MYENV}xyz")); + assertThat(SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTtFipsPassword}xyz", settings), is("abvtTtFipsPasswordxyz")); + assertTrue( + passwordHasherPBKDF2.check( + "tTtFipsPassword".toCharArray(), + SecurityUtils.replaceEnvVars("${envbc.MYENV:-tTtFipsPassword}", settings) + ) + ); + assertThat( + SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTtFipsPassword}xyz${env.MYENV:-xxxFipsPassword}", settings), + is("abvtTtFipsPasswordxyzxxxFipsPassword") + ); + assertTrue( + SecurityUtils.replaceEnvVars("abv${env.MYENV:-tTtFipsPassword}xyz${envbc.MYENV:-xxxFipsPassword}", settings) + .startsWith("abvtTtFipsPasswordxyz$3$") + ); + assertThat(SecurityUtils.replaceEnvVars("abv${env.MYENV:tTtFipsPassword}xyz", settings), is("abv${env.MYENV:tTtFipsPassword}xyz")); + assertThat(SecurityUtils.replaceEnvVars("abv${env.MYENV-tTtFipsPassword}xyz", settings), is("abv${env.MYENV-tTtFipsPassword}xyz")); + + Map.Entry envEntry = System.getenv() + .entrySet() + .stream() + .filter(it -> it.getValue() != null) + .filter(it -> it.getValue().length() >= 14) + .findFirst() + .orElseThrow(() -> new AssertionError("No env var with value >= 14 chars found")); + + String k = envEntry.getKey(); + String val = envEntry.getValue(); + assertThat(SecurityUtils.replaceEnvVars("abv${env." + k + "}xyz", settings), is("abv" + val + "xyz")); + assertThat(SecurityUtils.replaceEnvVars("abv${" + k + "}xyz", settings), is("abv${" + k + "}xyz")); + assertThat(SecurityUtils.replaceEnvVars("abv${env." + k + ":-k182765gghFipsX}xyz", settings), is("abv" + val + "xyz")); + assertThat( + SecurityUtils.replaceEnvVars("abv${env." + k + "}xyzabv${env." + k + "}xyz", settings), + is("abv" + val + "xyzabv" + val + "xyz") + ); + assertThat(SecurityUtils.replaceEnvVars("abv${env." + k + ":-k182765gghFipsX}xyz", settings), is("abv" + val + "xyz")); + assertTrue(passwordHasherPBKDF2.check(val.toCharArray(), SecurityUtils.replaceEnvVars("${envbc." + k + "}", settings))); + } + @Test public void testNoEnvReplace() { Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DISABLE_ENVVAR_REPLACEMENT, true).build(); diff --git a/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java b/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java index 386f92b24e..1d1c6c25e3 100644 --- a/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java +++ b/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java @@ -16,6 +16,7 @@ import org.apache.http.HttpStatus; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; import org.opensearch.common.settings.Settings; @@ -24,6 +25,7 @@ import org.opensearch.security.auditlog.impl.AuditMessage; import org.opensearch.security.auditlog.integration.TestAuditlogImpl; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.helper.cluster.ClusterConfiguration; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; @@ -379,6 +381,7 @@ public void testPBKDF2HashRedaction() { @Test public void testArgon2HashRedaction() { + Assume.assumeFalse("Argon2 is not supported in FIPS mode", FipsMode.isEnabled()); final Settings settings = Settings.builder() .put("plugins.security.audit.type", TestAuditlogImpl.class.getName()) .put(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED, "opendistro_security_all_access") diff --git a/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java b/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java index 21cb969522..5980746836 100644 --- a/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java +++ b/src/test/java/org/opensearch/security/auth/InternalAuthBackendTests.java @@ -15,16 +15,20 @@ import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collection; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; +import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.securityconf.InternalUsersModel; import org.opensearch.security.support.ConfigConstants; @@ -39,21 +43,51 @@ import static org.mockito.Mockito.when; import static org.mockito.internal.verification.VerificationModeFactory.times; +@RunWith(Parameterized.class) public class InternalAuthBackendTests { - private InternalUsersModel internalUsersModel; + private final Settings settings; + private InternalUsersModel internalUsersModel; private InternalAuthenticationBackend internalAuthenticationBackend; + private PasswordHasher passwordHasher; + private String storedHash; + + public InternalAuthBackendTests(String algorithmName, Settings settings) { + this.settings = settings; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection hashingAlgorithms() { + return Arrays.asList( + new Object[][] { + { + "BCrypt", + Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT) + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS, 4) + .build() }, + { + "PBKDF2", + Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.PBKDF2) + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS, 1) + .build() }, + { + "Argon2", + Settings.builder() + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.ARGON2) + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_MEMORY, 8) + .put(ConfigConstants.SECURITY_PASSWORD_HASHING_ARGON2_ITERATIONS, 1) + .build() } } + ); + } @Before public void internalAuthBackendTestsSetup() { - internalAuthenticationBackend = spy( - new InternalAuthenticationBackend( - PasswordHasherFactory.createPasswordHasher( - Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.BCRYPT).build() - ) - ) - ); + passwordHasher = PasswordHasherFactory.createPasswordHasher(settings); + storedHash = passwordHasher.hash("$adminpassword!".toCharArray()); + internalAuthenticationBackend = spy(new InternalAuthenticationBackend(passwordHasher)); internalUsersModel = mock(InternalUsersModel.class); internalAuthenticationBackend.onInternalUsersModelChanged(internalUsersModel); } @@ -71,16 +105,14 @@ private char[] createArrayFromPasswordBytes(byte[] password) { public void testHashActionWithValidUserValidPassword() { // Make authentication info for valid username with valid password - final String validPassword = "admin"; + final String validPassword = "$adminpassword!"; final byte[] validPasswordBytes = validPassword.getBytes(); - final AuthCredentials validUsernameAuth = new AuthCredentials("admin", validPasswordBytes); - - final String hash = "$2y$12$NmKhjNssNgSIj8iXT7SYxeXvMA1E95a9tCt4cySY9FrQ4fB18xEc2"; + final AuthCredentials validUsernameAuth = new AuthCredentials("$adminpassword!", validPasswordBytes); char[] array = createArrayFromPasswordBytes(validPasswordBytes); - when(internalUsersModel.getHash(validUsernameAuth.getUsername())).thenReturn(hash); + when(internalUsersModel.getHash(validUsernameAuth.getUsername())).thenReturn(storedHash); when(internalUsersModel.exists(validUsernameAuth.getUsername())).thenReturn(true); when(internalUsersModel.getAttributes(validUsernameAuth.getUsername())).thenReturn(ImmutableMap.of()); when(internalUsersModel.getSecurityRoles(validUsernameAuth.getUsername())).thenReturn(ImmutableSet.of()); @@ -91,7 +123,7 @@ public void testHashActionWithValidUserValidPassword() { // Act internalAuthenticationBackend.authenticate(new AuthenticationContext(validUsernameAuth)); - verify(internalAuthenticationBackend, times(1)).passwordMatchesHash(hash, array); + verify(internalAuthenticationBackend, times(1)).passwordMatchesHash(storedHash, array); verify(internalUsersModel, times(1)).getBackendRoles(validUsernameAuth.getUsername()); } @@ -101,32 +133,30 @@ public void testHashActionWithValidUserInvalidPassword() { // Make authentication info for valid with bad password final String gibberishPassword = "ajdhflkasdjfaklsdf"; final byte[] gibberishPasswordBytes = gibberishPassword.getBytes(); - final AuthCredentials validUsernameAuth = new AuthCredentials("admin", gibberishPasswordBytes); - - final String hash = "$2y$12$NmKhjNssNgSIj8iXT7SYxeXvMA1E95a9tCt4cySY9FrQ4fB18xEc2"; + final AuthCredentials validUsernameAuth = new AuthCredentials("$adminpassword!", gibberishPasswordBytes); char[] array = createArrayFromPasswordBytes(gibberishPasswordBytes); - when(internalUsersModel.getHash("admin")).thenReturn(hash); - when(internalUsersModel.exists("admin")).thenReturn(true); + when(internalUsersModel.getHash("$adminpassword!")).thenReturn(storedHash); + when(internalUsersModel.exists("$adminpassword!")).thenReturn(true); OpenSearchSecurityException ex = Assert.assertThrows( OpenSearchSecurityException.class, () -> internalAuthenticationBackend.authenticate(new AuthenticationContext(validUsernameAuth)) ); assert (ex.getMessage().contains("password does not match")); - verify(internalAuthenticationBackend, times(1)).passwordMatchesHash(hash, array); + verify(internalAuthenticationBackend, times(1)).passwordMatchesHash(storedHash, array); } @Test public void testHashActionWithInvalidUserValidPassword() { // Make authentication info for valid and invalid usernames both with bad passwords - final String validPassword = "admin"; + final String validPassword = "$adminpassword!"; final byte[] validPasswordBytes = validPassword.getBytes(); final AuthCredentials invalidUsernameAuth = new AuthCredentials("ertyuiykgjjfguyifdghc", validPasswordBytes); - final String hash = "$2y$12$NmKhjNssNgSIj8iXT7SYxeXvMA1E95a9tCt4cySY9FrQ4fB18xEc2"; + final String hash = passwordHasher.getDummyHash(); char[] array = createArrayFromPasswordBytes(validPasswordBytes); @@ -149,7 +179,7 @@ public void testHashActionWithInvalidUserInvalidPassword() { final byte[] gibberishPasswordBytes = gibberishPassword.getBytes(); final AuthCredentials invalidUsernameAuth = new AuthCredentials("ertyuiykgjjfguyifdghc", gibberishPasswordBytes); - final String hash = "$2y$12$NmKhjNssNgSIj8iXT7SYxeXvMA1E95a9tCt4cySY9FrQ4fB18xEc2"; + final String hash = passwordHasher.getDummyHash(); char[] array = createArrayFromPasswordBytes(gibberishPasswordBytes); diff --git a/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java b/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java index f79bcae38a..1e1d334b1f 100644 --- a/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java @@ -35,6 +35,7 @@ import org.hamcrest.Matchers; import org.junit.After; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -53,6 +54,7 @@ import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.filter.SecurityResponse; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; @@ -107,6 +109,11 @@ public class HTTPSamlAuthenticatorTest { private static X509Certificate spSigningCertificate; private static PrivateKey spSigningPrivateKey; + @BeforeClass + public static void skipInFipsMode() { + Assume.assumeFalse("Skipping SAML tests: SAML frameworks are not FIPS-compliant", FipsMode.isEnabled()); + } + @Before public void setUp() throws Exception { mockSamlIdpServer = new MockSamlIdpServer(); diff --git a/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTest.java b/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTest.java index 0b7d5eec42..e9f8cd5e12 100755 --- a/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTest.java +++ b/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTest.java @@ -31,6 +31,7 @@ import org.opensearch.security.auth.ldap.util.ConfigConstants; import org.opensearch.security.auth.ldap.util.LdapHelper; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.user.User; @@ -45,6 +46,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeFalse; public class LdapBackendTest { @@ -174,7 +176,6 @@ public void testLdapAuthenticationSSL() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -194,7 +195,6 @@ public void testLdapAuthenticationSSLPEMFile() throws Exception { ConfigConstants.LDAPS_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").toFile().getName() ) - .put("verify_hostnames", false) .put("path.home", ".") .put("path.conf", FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").getParent()) .build(); @@ -216,6 +216,7 @@ public void testLdapAuthenticationSSLPEMText() throws Exception { @Test public void testLdapAuthenticationSSLSSLv3() throws Exception { + assumeFalse("SSLv3 is not FIPS-approved", FipsMode.isEnabled()); final Settings settings = Settings.builder() .putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapsPort) @@ -244,7 +245,6 @@ public void testLdapAuthenticationSSLUnknowCipher() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_ciphers", "AAA") .put("path.home", ".") .build(); @@ -266,7 +266,6 @@ public void testLdapAuthenticationSpecialCipherProtocol() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_protocols", "TLSv1.2") .putList("enabled_ssl_ciphers", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA") .put("path.home", ".") @@ -286,7 +285,6 @@ public void testLdapAuthenticationSSLNoKeystore() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -721,7 +719,6 @@ public void testLdapAuthenticationStartTLS() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_START_TLS, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); diff --git a/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTestNewStyleConfig.java b/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTestNewStyleConfig.java index 7fc2c59f98..73d0b697fc 100644 --- a/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTestNewStyleConfig.java +++ b/src/test/java/org/opensearch/security/auth/ldap/LdapBackendTestNewStyleConfig.java @@ -31,6 +31,7 @@ import org.opensearch.security.auth.ldap.util.ConfigConstants; import org.opensearch.security.auth.ldap.util.LdapHelper; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.user.User; @@ -42,6 +43,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import static org.junit.Assume.assumeFalse; public class LdapBackendTestNewStyleConfig { @@ -173,7 +175,6 @@ public void testLdapAuthenticationSSL() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -193,7 +194,6 @@ public void testLdapAuthenticationSSLPEMFile() throws Exception { ConfigConstants.LDAPS_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").toFile().getName() ) - .put("verify_hostnames", false) .put("path.home", ".") .put("path.conf", FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").getParent()) .build(); @@ -216,13 +216,13 @@ public void testLdapAuthenticationSSLPEMText() throws Exception { @Test public void testLdapAuthenticationSSLSSLv3() throws Exception { + assumeFalse("SSLv3 is not FIPS-approved", FipsMode.isEnabled()); final Settings settings = Settings.builder() .putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapsPort) .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_protocols", "SSLv3") .put("path.home", ".") .build(); @@ -244,7 +244,6 @@ public void testLdapAuthenticationSSLUnknownCipher() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_ciphers", "AAA") .put("path.home", ".") .build(); @@ -266,7 +265,6 @@ public void testLdapAuthenticationSpecialCipherProtocol() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_protocols", "TLSv1.2") .putList("enabled_ssl_ciphers", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA") .put("path.home", ".") @@ -286,7 +284,6 @@ public void testLdapAuthenticationSSLNoKeystore() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -570,7 +567,6 @@ public void testLdapAuthenticationStartTLS() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_START_TLS, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); diff --git a/src/test/java/org/opensearch/security/auth/ldap/srv/LdapServer.java b/src/test/java/org/opensearch/security/auth/ldap/srv/LdapServer.java index adeec6b06f..10d9aaccaa 100644 --- a/src/test/java/org/opensearch/security/auth/ldap/srv/LdapServer.java +++ b/src/test/java/org/opensearch/security/auth/ldap/srv/LdapServer.java @@ -18,6 +18,7 @@ import java.io.StringReader; import java.net.BindException; import java.nio.charset.StandardCharsets; +import java.security.KeyStore; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.TimeUnit; @@ -25,6 +26,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Handler; import java.util.logging.LogRecord; +import javax.net.ssl.TrustManagerFactory; import com.google.common.io.CharStreams; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -45,7 +47,6 @@ import com.unboundid.ldif.LDIFReader; import com.unboundid.util.ssl.KeyStoreKeyManager; import com.unboundid.util.ssl.SSLUtil; -import com.unboundid.util.ssl.TrustStoreTrustManager; final class LdapServer { private final static Logger LOG = LogManager.getLogger(LdapServer.class); @@ -115,11 +116,15 @@ private Collection getInMemoryListenerConfigs() throws E Collection listenerConfigs = new ArrayList(); String serverKeyStorePath = FileHelper.resolveStore("ldap/node-0-keystore").path().toFile().getAbsolutePath(); + + KeyStore trustStore = FileHelper.getKeystoreFromClassPath("ldap/truststore", "changeit"); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(trustStore); + final SSLUtil serverSSLUtil = new SSLUtil( new KeyStoreKeyManager(serverKeyStorePath, "changeit".toCharArray()), - new TrustStoreTrustManager(serverKeyStorePath) + tmf.getTrustManagers()[0] ); - // final SSLUtil clientSSLUtil = new SSLUtil(new TrustStoreTrustManager(serverKeyStorePath)); ldapPort = SocketUtils.findAvailableTcpPort(); ldapsPort = SocketUtils.findAvailableTcpPort(); diff --git a/src/test/java/org/opensearch/security/auth/ldap2/HostnameAwareConnectionFactoryTest.java b/src/test/java/org/opensearch/security/auth/ldap2/HostnameAwareConnectionFactoryTest.java new file mode 100644 index 0000000000..de6f0667a1 --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/ldap2/HostnameAwareConnectionFactoryTest.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import org.junit.After; +import org.junit.Test; + +import org.ldaptive.Connection; +import org.ldaptive.ConnectionConfig; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class HostnameAwareConnectionFactoryTest { + + @After + public void clearThreadLocal() { + SNISettingTLSSocketFactory.clearContext(); + } + + @Test + public void getConnection_wrapsWithHostnameFromUrl_withoutSettingContextAtBuild() { + HostnameAwareConnectionFactory factory = new HostnameAwareConnectionFactory( + new ConnectionConfig("ldaps://example.com:636"), + "ldaps://example.com:636" + ); + + Connection connection = factory.getConnection(); + + // The connection is wrapped with the hostname parsed from the LDAP URL; the wrapper then + // establishes it as the SNI context at open() — see SniAwareConnectionTest. + assertTrue(connection instanceof SniAwareConnection); + assertEquals("example.com", ((SniAwareConnection) connection).hostname()); + // Building the connection must NOT set the context — the socket isn't created yet. + assertNull(SNISettingTLSSocketFactory.getHostname()); + } + + @Test(expected = IllegalArgumentException.class) + public void getConnection_throwsOnUnparseableUrl() { + new HostnameAwareConnectionFactory(new ConnectionConfig("ldaps://example.com:636"), "not-a-valid-url").getConnection(); + } +} diff --git a/src/test/java/org/opensearch/security/auth/ldap2/LDAPConnectionFactoryFactoryClassLoaderTest.java b/src/test/java/org/opensearch/security/auth/ldap2/LDAPConnectionFactoryFactoryClassLoaderTest.java new file mode 100644 index 0000000000..944c97e3b4 --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/ldap2/LDAPConnectionFactoryFactoryClassLoaderTest.java @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auth.ldap.util.ConfigConstants; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.test.helper.file.FileHelper; + +import org.ldaptive.DefaultConnectionFactory; +import org.ldaptive.provider.Provider; +import org.ldaptive.provider.jndi.JndiProviderConfig; +import org.ldaptive.ssl.ThreadLocalTLSSocketFactory; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; +import static org.junit.Assert.assertThrows; + +public class LDAPConnectionFactoryFactoryClassLoaderTest { + + private static final String SNI_FACTORY = "org.opensearch.security.auth.ldap2.SNISettingTLSSocketFactory"; + + /** Builds a real factory and returns the classloader ldap2 sets on the JNDI provider config. */ + private static ClassLoader factoryProvidedClassLoader() throws Exception { + Settings settings = Settings.builder() + .putList(ConfigConstants.LDAP_HOSTS, "localhost:636") + .put(ConfigConstants.LDAPS_ENABLE_SSL, true) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) + .put("path.home", ".") + .build(); + + DefaultConnectionFactory connectionFactory = new LDAPConnectionFactoryFactory(settings, null).createBasicConnectionFactory(); + + @SuppressWarnings("unchecked") + Provider provider = (Provider) connectionFactory.getProvider(); + return provider.getProviderConfig().getClassLoader(); + } + + @Test + public void providerConfig_carriesClassLoaderThatResolvesSniSocketFactory() throws Exception { + ClassLoader factoryClassLoader = factoryProvidedClassLoader(); + assertThat(factoryClassLoader.loadClass(SNI_FACTORY), is(sameInstance(SNISettingTLSSocketFactory.class))); + } + + @Test + public void providerConfig_classLoaderResolvesThreadLocalTlsSocketFactory() throws Exception { + ClassLoader factoryClassLoader = factoryProvidedClassLoader(); + assertThat( + factoryClassLoader.loadClass(ThreadLocalTLSSocketFactory.class.getName()), + is(sameInstance(ThreadLocalTLSSocketFactory.class)) + ); + } + + @Test + public void providerConfig_classLoaderDelegatesUnknownClassesToParent() throws Exception { + ClassLoader factoryClassLoader = factoryProvidedClassLoader(); + assertThrows(ClassNotFoundException.class, () -> factoryClassLoader.loadClass("com.example.DoesNotExist")); + } + + @Test + public void noArgConstructor_resolvesSniSocketFactory() throws Exception { + ClassLoader loader = new SocketFactoryClassLoader(); + assertThat(loader.loadClass(SNI_FACTORY), is(sameInstance(SNISettingTLSSocketFactory.class))); + } +} diff --git a/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestNewStyleConfig2.java b/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestNewStyleConfig2.java index 1127fd80e8..f67d99bf6b 100644 --- a/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestNewStyleConfig2.java +++ b/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestNewStyleConfig2.java @@ -38,7 +38,7 @@ import org.opensearch.security.auth.ldap.util.ConfigConstants; import org.opensearch.security.auth.ldap.util.LdapHelper; import org.opensearch.security.ssl.util.SSLConfigConstants; -import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.user.User; @@ -51,6 +51,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import static org.junit.Assume.assumeFalse; @RunWith(Parameterized.class) public class LdapBackendTestNewStyleConfig2 { @@ -195,7 +196,6 @@ public void testLdapAuthenticationSSL() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -214,7 +214,6 @@ public void testLdapAuthenticationSSLPEMFile() throws Exception { ConfigConstants.LDAPS_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").toFile().getName() ) - .put("verify_hostnames", false) .put("path.home", ".") .put("path.conf", FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").getParent()) .build(); @@ -237,6 +236,7 @@ public void testLdapAuthenticationSSLPEMText() throws Exception { @Test public void testLdapAuthenticationSSLSSLv3() throws Exception { + assumeFalse("SSLv3 is not FIPS-approved", FipsMode.isEnabled()); final Settings settings = createBaseSettings().putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapsPort) .put("users.u1.search", "(uid={0})") @@ -252,7 +252,8 @@ public void testLdapAuthenticationSSLSSLv3() throws Exception { Assert.fail("Expected Exception"); } catch (Exception e) { assertThat(e.getCause().getClass(), is(org.ldaptive.provider.ConnectionException.class)); - Assert.assertTrue(ExceptionUtils.getStackTrace(e).contains("No appropriate protocol")); + var message = FipsMode.isEnabled() ? "'protocols' cannot be null, or contain unsupported protocols" : "No appropriate protocol"; + Assert.assertTrue(ExceptionUtils.getStackTrace(e).contains(message)); } } @@ -264,7 +265,6 @@ public void testLdapAuthenticationSSLUnknownCipher() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_ciphers", "AAA") .put("path.home", ".") .build(); @@ -274,10 +274,8 @@ public void testLdapAuthenticationSSLUnknownCipher() throws Exception { Assert.fail("Expected Exception"); } catch (Exception e) { assertThat(e.getCause().getClass(), is(org.ldaptive.provider.ConnectionException.class)); - Assert.assertTrue( - ExceptionUtils.getStackTrace(e), - WildcardMatcher.from("*unsupported*ciphersuite*aaa*").test(ExceptionUtils.getStackTrace(e).toLowerCase()) - ); + var message = FipsMode.isEnabled() ? "No usable cipher suites enabled" : "Unsupported CipherSuite: AAA"; + Assert.assertTrue(ExceptionUtils.getStackTrace(e).contains(message)); } } @@ -289,7 +287,6 @@ public void testLdapAuthenticationSpecialCipherProtocol() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_protocols", "TLSv1.2") .putList("enabled_ssl_ciphers", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA") .put("path.home", ".") @@ -308,7 +305,6 @@ public void testLdapAuthenticationSSLNoKeystore() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -612,7 +608,6 @@ public void testLdapAuthenticationStartTLS() throws Exception { .put("users.u1.search", "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_START_TLS, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); diff --git a/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestOldStyleConfig2.java b/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestOldStyleConfig2.java index f25c20b19a..cf264ff558 100755 --- a/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestOldStyleConfig2.java +++ b/src/test/java/org/opensearch/security/auth/ldap2/LdapBackendTestOldStyleConfig2.java @@ -37,6 +37,7 @@ import org.opensearch.security.auth.ldap.util.ConfigConstants; import org.opensearch.security.auth.ldap.util.LdapHelper; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; @@ -50,6 +51,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; +import static org.junit.Assume.assumeFalse; @RunWith(Parameterized.class) public class LdapBackendTestOldStyleConfig2 { @@ -219,7 +221,6 @@ public void testLdapAuthenticationSSL() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -236,7 +237,6 @@ public void testLdapAuthenticationSSLPooled() throws Exception { .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(ConfigConstants.LDAP_POOL_ENABLED, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -255,7 +255,6 @@ public void testLdapAuthenticationSSLPEMFile() throws Exception { ConfigConstants.LDAPS_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").toFile().getName() ) - .put("verify_hostnames", false) .put("path.home", ".") .put("path.conf", FileHelper.getAbsoluteFilePathFromClassPath("ldap/root-ca.pem").getParent()) .build(); @@ -278,6 +277,7 @@ public void testLdapAuthenticationSSLPEMText() throws Exception { @Test public void testLdapAuthenticationSSLSSLv3() throws Exception { + assumeFalse("SSLv3 is not FIPS-approved", FipsMode.isEnabled()); final Settings settings = createBaseSettings().putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapsPort) .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") @@ -293,7 +293,8 @@ public void testLdapAuthenticationSSLSSLv3() throws Exception { Assert.fail("Expected Exception"); } catch (Exception e) { assertThat(e.getCause().getClass(), is(org.ldaptive.provider.ConnectionException.class)); - Assert.assertTrue(ExceptionUtils.getStackTrace(e).contains("No appropriate protocol")); + var message = FipsMode.isEnabled() ? "'protocols' cannot be null, or contain unsupported protocols" : "No appropriate protocol"; + Assert.assertTrue(ExceptionUtils.getStackTrace(e).contains(message)); } } @@ -305,7 +306,6 @@ public void testLdapAuthenticationSSLUnknowCipher() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_ciphers", "AAA") .put("path.home", ".") .build(); @@ -314,8 +314,9 @@ public void testLdapAuthenticationSSLUnknowCipher() throws Exception { new LDAPAuthenticationBackend2(settings, null).authenticate(ctx("jacksonm", "secret")); Assert.fail("Expected Exception"); } catch (Exception e) { - assertThat(e.getCause().getClass().toString(), org.ldaptive.provider.ConnectionException.class, is(e.getCause().getClass())); - Assert.assertTrue(ExceptionUtils.getStackTrace(e), EXCEPTION_MATCHER.test(ExceptionUtils.getStackTrace(e).toLowerCase())); + assertThat(e.getCause().getClass(), is(org.ldaptive.provider.ConnectionException.class)); + var message = FipsMode.isEnabled() ? "No usable cipher suites enabled" : "Unsupported CipherSuite: AAA"; + Assert.assertTrue(ExceptionUtils.getStackTrace(e).contains(message)); } } @@ -327,7 +328,6 @@ public void testLdapAuthenticationSpecialCipherProtocol() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .putList("enabled_ssl_protocols", "TLSv1.2") .putList("enabled_ssl_ciphers", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA") .put("path.home", ".") @@ -346,7 +346,6 @@ public void testLdapAuthenticationSSLNoKeystore() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_SSL, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); @@ -646,7 +645,6 @@ public void testLdapAuthenticationStartTLS() throws Exception { .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") .put(ConfigConstants.LDAPS_ENABLE_START_TLS, true) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) - .put("verify_hostnames", false) .put("path.home", ".") .build(); diff --git a/src/test/java/org/opensearch/security/auth/ldap2/LdapMtlsSniAuthenticationTest.java b/src/test/java/org/opensearch/security/auth/ldap2/LdapMtlsSniAuthenticationTest.java new file mode 100644 index 0000000000..5c861cd5e5 --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/ldap2/LdapMtlsSniAuthenticationTest.java @@ -0,0 +1,77 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auth.AuthenticationContext; +import org.opensearch.security.auth.ldap.srv.EmbeddedLDAPServer; +import org.opensearch.security.auth.ldap.util.ConfigConstants; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.user.User; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Proves that SNI hostname is correctly propagated via ThreadLocal when connecting over LDAPS. + * + * The LDAP client connects to "localhost" with explicit hostname verification enabled. + * BouncyCastle requires the hostname from ThreadLocal (set by HostnameAwareConnectionFactory + * before JNDI resolves it to an IP) to verify the server certificate's SAN against "localhost". + * Successful authentication proves SNI was correctly set — without it, BouncyCastle cannot + * determine the expected hostname and hostname verification would fail. + */ +public class LdapMtlsSniAuthenticationTest { + + private static EmbeddedLDAPServer ldapServer; + private static int ldapsPort; + + @BeforeClass + public static void startLdapServer() throws Exception { + ldapServer = new EmbeddedLDAPServer(); + ldapServer.applyLdif("base.ldif"); + ldapsPort = ldapServer.getLdapsPort(); + } + + @AfterClass + public static void stopLdapServer() throws Exception { + if (ldapServer != null) { + ldapServer.stop(); + } + } + + @Test + public void authenticate_succeeds_provingSnIAndHostnameVerification() throws Exception { + Settings settings = Settings.builder() + .putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapsPort) + .put("users.u1.search", "(uid={0})") + .put(ConfigConstants.LDAPS_ENABLE_SSL, true) + .put(ConfigConstants.LDAPS_VERIFY_HOSTNAMES, true) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ldap/truststore").path()) + .put("path.home", ".") + .build(); + + User user = new LDAPAuthenticationBackend2(settings, null).authenticate( + new AuthenticationContext(new AuthCredentials("jacksonm", "secret".getBytes())) + ); + + assertThat(user, is(notNullValue())); + assertThat(user.getName(), is("cn=Michael Jackson,ou=people,o=TEST")); + } +} diff --git a/src/test/java/org/opensearch/security/auth/ldap2/SNISettingTLSSocketFactoryTest.java b/src/test/java/org/opensearch/security/auth/ldap2/SNISettingTLSSocketFactoryTest.java new file mode 100644 index 0000000000..47b2723936 --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/ldap2/SNISettingTLSSocketFactoryTest.java @@ -0,0 +1,204 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import java.net.InetAddress; +import java.net.Socket; +import java.util.List; +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SNIServerName; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +public class SNISettingTLSSocketFactoryTest { + + private final SNISettingTLSSocketFactory factory = new SNISettingTLSSocketFactory(null); + + @After + public void clearThreadLocal() { + SNISettingTLSSocketFactory.clearContext(); + } + + // --- configureSocket --- + + @Test + public void configureSocket_setsSni() throws Exception { + SSLSocket socket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory.configure("example.com"); + + factory.configureSocket(socket); + + List serverNames = socket.getSSLParameters().getServerNames(); + assertEquals(1, serverNames.size()); + assertEquals("example.com", ((SNIHostName) serverNames.get(0)).getAsciiName()); + // Endpoint identification (hostname verification) is JNDI's job, not the factory's. + assertNull(socket.getSSLParameters().getEndpointIdentificationAlgorithm()); + } + + @Test + public void configureSocket_skipsSniForIpAddress() throws Exception { + SSLSocket socket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory.configure("192.168.1.1"); + + factory.configureSocket(socket); + + assertNull(socket.getSSLParameters().getServerNames()); + } + + @Test + public void configureSocket_skipsWhenNoHostname() throws Exception { + SSLSocket socket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + + factory.configureSocket(socket); + + assertNull(socket.getSSLParameters().getServerNames()); + assertNull(socket.getSSLParameters().getEndpointIdentificationAlgorithm()); + } + + @Test(expected = IllegalArgumentException.class) + public void configureSocket_throwsOnInvalidHostname() throws Exception { + SSLSocket socket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory.configure("invalid..hostname"); + + factory.configureSocket(socket); + } + + @Test + public void configureSocket_passesThroughNonSslSocket() { + Socket socket = new Socket(); + SNISettingTLSSocketFactory.configure("example.com"); + + Socket result = factory.configureSocket(socket); + + assertSame(socket, result); + } + + // --- cipher suite delegation --- + + @Test + public void getDefaultCipherSuites_delegatesToDelegate() throws Exception { + SSLSocketFactory real = SSLContext.getDefault().getSocketFactory(); + SNISettingTLSSocketFactory f = new SNISettingTLSSocketFactory(real); + + assertEquals(real.getDefaultCipherSuites(), f.getDefaultCipherSuites()); + } + + @Test + public void getSupportedCipherSuites_delegatesToDelegate() throws Exception { + SSLSocketFactory real = SSLContext.getDefault().getSocketFactory(); + SNISettingTLSSocketFactory f = new SNISettingTLSSocketFactory(real); + + assertEquals(real.getSupportedCipherSuites(), f.getSupportedCipherSuites()); + } + + // --- createSocket --- + + @Test + public void createSocket_wrappingSocket_configuresSni() throws Exception { + SSLSocket sslSocket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory f = new SNISettingTLSSocketFactory(stubDelegate(sslSocket)); + SNISettingTLSSocketFactory.configure("example.com"); + + Socket result = f.createSocket(new Socket(), "example.com", 636, true); + + assertSame(sslSocket, result); + assertEquals("example.com", ((SNIHostName) ((SSLSocket) result).getSSLParameters().getServerNames().get(0)).getAsciiName()); + } + + @Test + public void createSocket_stringHost_configuresSni() throws Exception { + SSLSocket sslSocket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory f = new SNISettingTLSSocketFactory(stubDelegate(sslSocket)); + SNISettingTLSSocketFactory.configure("example.com"); + + Socket result = f.createSocket("example.com", 636); + + assertSame(sslSocket, result); + assertEquals("example.com", ((SNIHostName) ((SSLSocket) result).getSSLParameters().getServerNames().get(0)).getAsciiName()); + } + + @Test + public void createSocket_stringHostWithLocalAddress_configuresSni() throws Exception { + SSLSocket sslSocket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory f = new SNISettingTLSSocketFactory(stubDelegate(sslSocket)); + SNISettingTLSSocketFactory.configure("example.com"); + + Socket result = f.createSocket("example.com", 636, InetAddress.getLoopbackAddress(), 0); + + assertSame(sslSocket, result); + assertEquals("example.com", ((SNIHostName) ((SSLSocket) result).getSSLParameters().getServerNames().get(0)).getAsciiName()); + } + + @Test + public void createSocket_inetAddress_configuresSni() throws Exception { + SSLSocket sslSocket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory f = new SNISettingTLSSocketFactory(stubDelegate(sslSocket)); + SNISettingTLSSocketFactory.configure("example.com"); + + Socket result = f.createSocket(InetAddress.getLoopbackAddress(), 636); + + assertSame(sslSocket, result); + assertEquals("example.com", ((SNIHostName) ((SSLSocket) result).getSSLParameters().getServerNames().get(0)).getAsciiName()); + } + + @Test + public void createSocket_inetAddressWithLocalAddress_configuresSni() throws Exception { + SSLSocket sslSocket = (SSLSocket) SSLContext.getDefault().getSocketFactory().createSocket(); + SNISettingTLSSocketFactory f = new SNISettingTLSSocketFactory(stubDelegate(sslSocket)); + SNISettingTLSSocketFactory.configure("example.com"); + + Socket result = f.createSocket(InetAddress.getLoopbackAddress(), 636, InetAddress.getLoopbackAddress(), 0); + + assertSame(sslSocket, result); + assertEquals("example.com", ((SNIHostName) ((SSLSocket) result).getSSLParameters().getServerNames().get(0)).getAsciiName()); + } + + private static SSLSocketFactory stubDelegate(Socket socket) { + return new SSLSocketFactory() { + public String[] getDefaultCipherSuites() { + return new String[0]; + } + + public String[] getSupportedCipherSuites() { + return new String[0]; + } + + public Socket createSocket(Socket s, String host, int port, boolean autoClose) { + return socket; + } + + public Socket createSocket(String host, int port) { + return socket; + } + + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) { + return socket; + } + + public Socket createSocket(InetAddress host, int port) { + return socket; + } + + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) { + return socket; + } + }; + } +} diff --git a/src/test/java/org/opensearch/security/auth/ldap2/SniAwareConnectionTest.java b/src/test/java/org/opensearch/security/auth/ldap2/SniAwareConnectionTest.java new file mode 100644 index 0000000000..d5bd3fb3ac --- /dev/null +++ b/src/test/java/org/opensearch/security/auth/ldap2/SniAwareConnectionTest.java @@ -0,0 +1,260 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.auth.ldap2; + +import org.junit.After; +import org.junit.Test; + +import org.ldaptive.BindRequest; +import org.ldaptive.Connection; +import org.ldaptive.ConnectionConfig; +import org.ldaptive.LdapException; +import org.ldaptive.Response; +import org.ldaptive.ResultCode; +import org.ldaptive.control.RequestControl; +import org.ldaptive.provider.ProviderConnection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class SniAwareConnectionTest { + + private static final String HOST = "example.com"; + + @After + public void clearThreadLocal() { + SNISettingTLSSocketFactory.clearContext(); + } + + // --- socket-creating calls: SNI context is live during the delegate call and cleared afterwards --- + + @Test + public void open_setsSniDuringCall_returnsDelegateResponse_clearsAfter() throws LdapException { + RecordingConnection delegate = new RecordingConnection(); + SniAwareConnection connection = new SniAwareConnection(delegate, HOST); + + // Before open(), no context is set — the socket isn't created yet. + assertNull(SNISettingTLSSocketFactory.getHostname()); + + Response response = connection.open(); + + assertEquals(HOST, delegate.hostnameAtOpen); // hostname was live during the delegate's open() + assertSame(delegate.response, response); // delegate's response is passed straight through + assertNull(SNISettingTLSSocketFactory.getHostname()); // and cleared afterwards (no ThreadLocal leak) + } + + @Test + public void openWithBindRequest_setsSniDuringCall_forwardsRequest_clearsAfter() throws LdapException { + RecordingConnection delegate = new RecordingConnection(); + SniAwareConnection connection = new SniAwareConnection(delegate, HOST); + BindRequest request = new BindRequest(); + + Response response = connection.open(request); + + assertEquals(HOST, delegate.hostnameAtOpen); + assertSame(request, delegate.bindRequest); + assertSame(delegate.response, response); + assertNull(SNISettingTLSSocketFactory.getHostname()); + } + + @Test + public void reopen_setsSniDuringCall_returnsDelegateResponse_clearsAfter() throws LdapException { + RecordingConnection delegate = new RecordingConnection(); + SniAwareConnection connection = new SniAwareConnection(delegate, HOST); + + Response response = connection.reopen(); + + assertEquals(HOST, delegate.hostnameAtOpen); + assertSame(delegate.response, response); + assertNull(SNISettingTLSSocketFactory.getHostname()); + } + + @Test + public void reopenWithBindRequest_setsSniDuringCall_forwardsRequest_clearsAfter() throws LdapException { + RecordingConnection delegate = new RecordingConnection(); + SniAwareConnection connection = new SniAwareConnection(delegate, HOST); + BindRequest request = new BindRequest(); + + Response response = connection.reopen(request); + + assertEquals(HOST, delegate.hostnameAtOpen); + assertSame(request, delegate.bindRequest); + assertSame(delegate.response, response); + assertNull(SNISettingTLSSocketFactory.getHostname()); + } + + // --- socket-creating calls clear the SNI context even when the delegate fails --- + + @Test + public void open_clearsSniContext_whenDelegateThrows() { + assertSniContextClearedOnFailure(SniAwareConnection::open); + } + + @Test + public void openWithBindRequest_clearsSniContext_whenDelegateThrows() { + assertSniContextClearedOnFailure(connection -> connection.open(new BindRequest())); + } + + @Test + public void reopen_clearsSniContext_whenDelegateThrows() { + assertSniContextClearedOnFailure(SniAwareConnection::reopen); + } + + @Test + public void reopenWithBindRequest_clearsSniContext_whenDelegateThrows() { + assertSniContextClearedOnFailure(connection -> connection.reopen(new BindRequest())); + } + + /** + * Invokes a socket-creating call whose delegate throws, and asserts the failure propagates while + * the SNI context was live during the call and is still cleared afterwards (try-with-resources). + */ + private void assertSniContextClearedOnFailure(SocketCall call) { + RecordingConnection delegate = new RecordingConnection(); + delegate.failure = new LdapException("simulated open failure"); + SniAwareConnection connection = new SniAwareConnection(delegate, HOST); + + LdapException thrown = assertThrows(LdapException.class, () -> call.invoke(connection)); + + assertSame(delegate.failure, thrown); // the delegate's exception propagates unchanged + assertEquals(HOST, delegate.hostnameAtOpen); // context was live when the delegate ran + assertNull(SNISettingTLSSocketFactory.getHostname()); // and still cleared despite the failure + } + + @FunctionalInterface + private interface SocketCall { + void invoke(SniAwareConnection connection) throws LdapException; + } + + // --- pass-through methods: no SNI context, plain delegation --- + + @Test + public void hostname_returnsConfiguredHostname() { + assertEquals(HOST, new SniAwareConnection(new RecordingConnection(), HOST).hostname()); + } + + @Test + public void getConnectionConfig_delegates() { + RecordingConnection delegate = new RecordingConnection(); + assertSame(delegate.connectionConfig, new SniAwareConnection(delegate, HOST).getConnectionConfig()); + } + + @Test + public void isOpen_delegates() { + RecordingConnection delegate = new RecordingConnection(); + SniAwareConnection connection = new SniAwareConnection(delegate, HOST); + + delegate.open = true; + assertTrue(connection.isOpen()); + + delegate.open = false; + assertFalse(connection.isOpen()); + } + + @Test + public void getProviderConnection_delegates() { + RecordingConnection delegate = new RecordingConnection(); + SniAwareConnection connection = new SniAwareConnection(delegate, HOST); + + assertNull(connection.getProviderConnection()); + assertTrue(delegate.getProviderConnectionCalled); + } + + @Test + public void close_delegates() { + RecordingConnection delegate = new RecordingConnection(); + new SniAwareConnection(delegate, HOST).close(); + assertEquals(1, delegate.closeCalls); + } + + @Test + public void closeWithControls_delegates() { + RecordingConnection delegate = new RecordingConnection(); + RequestControl[] controls = new RequestControl[0]; + + new SniAwareConnection(delegate, HOST).close(controls); + + assertSame(controls, delegate.closeControls); + } + + /** + * Recording ldaptive {@link Connection} double: captures the SNI hostname observed during + * open()/reopen() and records the arguments/invocations of the pass-through methods. + */ + private static final class RecordingConnection implements Connection { + final Response response = new Response<>(null, ResultCode.SUCCESS); + final ConnectionConfig connectionConfig = new ConnectionConfig("ldaps://example.com:636"); + String hostnameAtOpen; + BindRequest bindRequest; + boolean open; + boolean getProviderConnectionCalled; + int closeCalls; + RequestControl[] closeControls; + LdapException failure; + + @Override + public Response open() throws LdapException { + hostnameAtOpen = SNISettingTLSSocketFactory.getHostname(); + if (failure != null) { + throw failure; + } + return response; + } + + @Override + public Response open(BindRequest request) throws LdapException { + bindRequest = request; + return open(); + } + + @Override + public Response reopen() throws LdapException { + return open(); + } + + @Override + public Response reopen(BindRequest request) throws LdapException { + bindRequest = request; + return open(); + } + + @Override + public ConnectionConfig getConnectionConfig() { + return connectionConfig; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public ProviderConnection getProviderConnection() { + getProviderConnectionCalled = true; + return null; + } + + @Override + public void close() { + closeCalls++; + } + + @Override + public void close(RequestControl[] controls) { + closeControls = controls; + } + } +} diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java index d9f663d9fe..9435b5321b 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtilsTest.java @@ -11,19 +11,30 @@ package org.opensearch.security.authtoken.jwt; +import java.nio.charset.StandardCharsets; import java.util.Base64; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; +import org.opensearch.security.support.FipsMode; + import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; public class EncryptionDecryptionUtilsTest { + // encryption_key is consumed Base64-decoded; a 32-char alphanumeric string decodes to only 24 bytes, + // under the 32-byte AES-256 floor FIPS requires. Base64-encode the 32 bytes so it decodes back to 32. + final static String key = RandomStringUtils.secure().nextAlphanumeric(32); + final static String encodedKey = Base64.getEncoder().encodeToString(key.getBytes(StandardCharsets.UTF_8)); + @Test public void testEncryptDecrypt() { - String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String secret = encodedKey; String data = "Hello, OpenSearch!"; EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); @@ -36,8 +47,8 @@ public void testEncryptDecrypt() { @Test public void testDecryptingWithWrongKey() { - String secret1 = Base64.getEncoder().encodeToString("correctKey12345".getBytes()); - String secret2 = Base64.getEncoder().encodeToString("wrongKey1234567".getBytes()); + String secret1 = Base64.getEncoder().encodeToString("correctKey123456correctKey123456".getBytes()); + String secret2 = Base64.getEncoder().encodeToString("wrongKey12345678wrongKey12345678".getBytes()); String data = "Hello, OpenSearch!"; EncryptionDecryptionUtil util1 = new EncryptionDecryptionUtil(secret1); @@ -51,7 +62,7 @@ public void testDecryptingWithWrongKey() { @Test public void testDecryptingCorruptedData() { - String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String secret = encodedKey; String corruptedEncryptedString = "corruptedData"; EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); @@ -62,7 +73,7 @@ public void testDecryptingCorruptedData() { @Test public void testEncryptDecryptEmptyString() { - String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String secret = encodedKey; String data = ""; EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); @@ -74,19 +85,48 @@ public void testEncryptDecryptEmptyString() { @Test(expected = NullPointerException.class) public void testEncryptNullValue() { - String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + String secret = encodedKey; String data = null; EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); util.encrypt(data); } + @Test + public void testFipsModeRejectsWeakEncryptionKey() { + Assume.assumeTrue(FipsMode.isEnabled()); + // 16-byte (128-bit) key material — below the 256-bit minimum required to back the derived AES-256 key + String weakSecret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); + + IllegalArgumentException ex = Assert.assertThrows(IllegalArgumentException.class, () -> new EncryptionDecryptionUtil(weakSecret)); + assertThat(ex.getMessage(), containsString("decodes to 16 bytes of key material, but FIPS mode requires at least 32 bytes")); + } + + @Test + public void testFipsModeAcceptsStrongEncryptionKey() { + FipsMode.envSupplier = () -> "true"; + // 32-byte (256-bit) key material satisfies the FIPS minimum + String strongSecret = Base64.getEncoder().encodeToString("mySecretKey12345mySecretKey12345".getBytes()); + String data = "Hello, OpenSearch!"; + + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(strongSecret); + + assertThat(util.decrypt(util.encrypt(data)), is(data)); + } + + @Test + public void testByteArrayConstructorWipesInput() { + final byte[] ikm = new byte[32]; + java.util.Arrays.fill(ikm, (byte) 9); + new EncryptionDecryptionUtil(ikm); + assertThat("IKM should be zeroed after key derivation", ikm, is(new byte[32])); + } + @Test(expected = NullPointerException.class) public void testDecryptNullValue() { - String secret = Base64.getEncoder().encodeToString("mySecretKey12345".getBytes()); String data = null; - EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(secret); + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(encodedKey); util.decrypt(data); } } diff --git a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java index 0cd6a364d8..d0a4c8ae71 100644 --- a/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java +++ b/src/test/java/org/opensearch/security/authtoken/jwt/JwtVendorTest.java @@ -11,11 +11,16 @@ package org.opensearch.security.authtoken.jwt; +import java.io.File; +import java.io.FileOutputStream; import java.nio.charset.StandardCharsets; +import java.security.KeyStore; import java.util.Base64; import java.util.Date; import java.util.List; import java.util.function.LongSupplier; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; import org.apache.commons.lang3.RandomStringUtils; import org.apache.logging.log4j.Level; @@ -23,13 +28,16 @@ import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.Logger; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.opensearch.OpenSearchException; import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.Settings; import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.util.KeyUtils; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.jwk.JWK; @@ -42,6 +50,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.IsNull.notNullValue; +import static org.opensearch.security.authtoken.jwt.JwtVendor.SIGNING_KEY_PROPERTY_KEY; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; @@ -50,6 +59,9 @@ import static org.mockito.Mockito.when; public class JwtVendorTest { + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + private Appender mockAppender; private ArgumentCaptor logEventCaptor; @@ -57,11 +69,17 @@ public class JwtVendorTest { "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; final static String signingKeyB64Encoded = Base64.getEncoder().encodeToString(signingKey.getBytes(StandardCharsets.UTF_8)); + // encryption_key is consumed Base64-decoded; a 32-char alphanumeric string decodes to only 24 bytes, + // under the 32-byte AES-256 floor FIPS requires. Base64-encode the 32 bytes so it decodes back to 32. + final static String claimsEncryptionKey = RandomStringUtils.secure().nextAlphanumeric(32); + final static String claimsEncryptionKeyB64Encoded = Base64.getEncoder() + .encodeToString(claimsEncryptionKey.getBytes(StandardCharsets.UTF_8)); + @Test public void testCreateJwkFromSettings() { - final Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); + final Settings settings = Settings.builder().put(SIGNING_KEY_PROPERTY_KEY, signingKeyB64Encoded).build(); - final Tuple jwk = JwtVendor.createJwkFromSettings(settings); + final Tuple jwk = JwtVendor.createJwkFromSettings(settings, tempDir.getRoot().toPath()); assertThat(jwk.v1().getAlgorithm().getName(), is("HS512")); assertThat(jwk.v1().getKeyUse().toString(), is("sig")); assertTrue(jwk.v1().toOctetSequenceKey().getKeyValue().decodeToString().startsWith(signingKey)); @@ -69,15 +87,21 @@ public void testCreateJwkFromSettings() { @Test public void testCreateJwkFromSettingsWithWeakKey() { - Settings settings = Settings.builder().put("signing_key", "abcd1234").build(); - Throwable exception = assertThrows(OpenSearchException.class, () -> JwtVendor.createJwkFromSettings(settings)); + Settings settings = Settings.builder().put(SIGNING_KEY_PROPERTY_KEY, "abcd1234").build(); + Throwable exception = assertThrows( + OpenSearchException.class, + () -> JwtVendor.createJwkFromSettings(settings, tempDir.getRoot().toPath()) + ); assertThat(exception.getMessage(), containsString("The secret length must be at least 256 bits")); } @Test public void testCreateJwkFromSettingsWithoutSigningKey() { Settings settings = Settings.builder().put("jwt", "").build(); - Throwable exception = assertThrows(RuntimeException.class, () -> JwtVendor.createJwkFromSettings(settings)); + Throwable exception = assertThrows( + RuntimeException.class, + () -> JwtVendor.createJwkFromSettings(settings, tempDir.getRoot().toPath()) + ); assertThat( exception.getMessage(), equalTo("Settings for signing key is missing. Please specify at least the option signing_key with a shared secret.") @@ -94,10 +118,10 @@ public void testCreateJwtWithRolesNoEncryptionKey() throws Exception { int expirySeconds = 300; LongSupplier currentTime = () -> 1696413600000L; // No encryption_key — roles should be written as plain 'dr' claim - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).build(); + Settings settings = Settings.builder().put(SIGNING_KEY_PROPERTY_KEY, signingKeyB64Encoded).build(); Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); - JwtVendor OBOJwtVendor = new JwtVendor(settings); + JwtVendor OBOJwtVendor = new JwtVendor(settings, tempDir.getRoot().toPath()); final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( new OBOJwtClaimsBuilder(null).addRoles(roles) .issuer(issuer) @@ -126,14 +150,16 @@ public void testCreateJwtWithRoles() throws Exception { int expirySeconds = 300; // 2023 oct 4, 10:00:00 AM GMT LongSupplier currentTime = () -> 1696413600000L; - String claimsEncryptionKey = "1234567890123456"; - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + Settings settings = Settings.builder() + .put(SIGNING_KEY_PROPERTY_KEY, signingKeyB64Encoded) + .put("encryption_key", claimsEncryptionKeyB64Encoded) + .build(); Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); - JwtVendor OBOJwtVendor = new JwtVendor(settings); + JwtVendor OBOJwtVendor = new JwtVendor(settings, tempDir.getRoot().toPath()); final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( - new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) + new OBOJwtClaimsBuilder(new EncryptionDecryptionUtil(claimsEncryptionKeyB64Encoded)).addRoles(roles) .addBackendRoles(false, backendRoles) .issuer(issuer) .subject(subject) @@ -154,7 +180,7 @@ public void testCreateJwtWithRoles() throws Exception { assertThat(((Date) signedJWT.getJWTClaimsSet().getClaims().get("iat")).getTime(), is(1696413600000L)); // 2023 oct 4, 10:05:00 AM GMT assertThat(((Date) signedJWT.getJWTClaimsSet().getClaims().get("exp")).getTime(), is(1696413900000L)); - EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); + EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKeyB64Encoded); assertThat( encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("encrypted_roles").toString()), equalTo(expectedRoles) @@ -174,17 +200,16 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { int expirySeconds = 300; LongSupplier currentTime = () -> (long) 100; - String claimsEncryptionKey = "1234567890123456"; Settings settings = Settings.builder() - .put("signing_key", signingKeyB64Encoded) - .put("encryption_key", claimsEncryptionKey) + .put(SIGNING_KEY_PROPERTY_KEY, signingKeyB64Encoded) + .put("encryption_key", claimsEncryptionKeyB64Encoded) .put(ConfigConstants.EXTENSIONS_BWC_PLUGIN_MODE, true) .build(); - final JwtVendor OBOJwtVendor = new JwtVendor(settings); + final JwtVendor OBOJwtVendor = new JwtVendor(settings, tempDir.getRoot().toPath()); Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); final ExpiringBearerAuthToken authToken = OBOJwtVendor.createJwt( - new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) + new OBOJwtClaimsBuilder(new EncryptionDecryptionUtil(claimsEncryptionKeyB64Encoded)).addRoles(roles) .addBackendRoles(true, backendRoles) .issuer(issuer) .subject(subject) @@ -206,7 +231,7 @@ public void testCreateJwtWithBackendRolesIncluded() throws Exception { assertThat(signedJWT.getJWTClaimsSet().getClaims().get("backend_roles"), is(notNullValue())); assertThat(signedJWT.getJWTClaimsSet().getClaims().get("backend_roles").toString(), equalTo(expectedBackendRoles)); - EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKey); + EncryptionDecryptionUtil encryptionUtil = new EncryptionDecryptionUtil(claimsEncryptionKeyB64Encoded); assertThat( encryptionUtil.decrypt(signedJWT.getJWTClaimsSet().getClaims().get("encrypted_roles").toString()), equalTo(expectedRoles) @@ -225,8 +250,10 @@ public void testCreateJwtLogsCorrectly() throws Exception { // Mock settings and other required dependencies LongSupplier currentTime = () -> (long) 100; - String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); - Settings settings = Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + Settings settings = Settings.builder() + .put(SIGNING_KEY_PROPERTY_KEY, signingKeyB64Encoded) + .put("encryption_key", claimsEncryptionKeyB64Encoded) + .build(); final String issuer = "cluster_0"; final String subject = "admin"; @@ -235,10 +262,10 @@ public void testCreateJwtLogsCorrectly() throws Exception { final List backendRoles = List.of("Sales", "Support"); int expirySeconds = 300; - final JwtVendor OBOJwtVendor = new JwtVendor(settings); + final JwtVendor OBOJwtVendor = new JwtVendor(settings, tempDir.getRoot().toPath()); Date expiryTime = new Date(currentTime.getAsLong() + expirySeconds * 1000); OBOJwtVendor.createJwt( - new OBOJwtClaimsBuilder(claimsEncryptionKey).addRoles(roles) + new OBOJwtClaimsBuilder(new EncryptionDecryptionUtil(claimsEncryptionKeyB64Encoded)).addRoles(roles) .addBackendRoles(true, backendRoles) .issuer(issuer) .subject(subject) @@ -260,4 +287,52 @@ public void testCreateJwtLogsCorrectly() throws Exception { assertTrue(parts.length >= 3); } + @Test + public void testCreateJwkFromKeystoreSettings() throws Exception { + final byte[] keyBytes = Base64.getDecoder().decode(signingKeyB64Encoded); + final SecretKey hmacKey = new SecretKeySpec(keyBytes, "HmacSHA512"); + + final KeyStore ks = KeyStore.getInstance("BCFKS"); + ks.load(null, null); + ks.setKeyEntry("jwt-signing", hmacKey, "keypass".toCharArray(), null); + + final File tempKs = tempDir.newFile("test-jwt-ks.bcfks"); + try (var out = new FileOutputStream(tempKs)) { + ks.store(out, "kspass".toCharArray()); + } + + final Settings settings = Settings.builder() + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_PATH, tempKs.getAbsolutePath()) + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_TYPE, "BCFKS") + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_PASSWORD, "kspass") + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_ALIAS, "jwt-signing") + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_KEY_PASSWORD, "keypass") + .build(); + + final Tuple jwk = JwtVendor.createJwkFromSettings(settings, tempDir.getRoot().toPath()); + assertThat(jwk.v1().getAlgorithm().getName(), is("HS512")); + assertThat(jwk.v1().getKeyUse().toString(), is("sig")); + assertThat(jwk.v1().toOctetSequenceKey().getKeyValue().decode(), equalTo(keyBytes)); + } + + @Test + public void testCreateJwkFromKeystoreSettingsNonexistentAlias() throws Exception { + final KeyStore ks = KeyStore.getInstance("BCFKS"); + ks.load(null, null); + + final File tempKs = tempDir.newFile("test-jwt-ks-empty.bcfks"); + try (var out = new FileOutputStream(tempKs)) { + ks.store(out, "kspass".toCharArray()); + } + + final Settings settings = Settings.builder() + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_PATH, tempKs.getAbsolutePath()) + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_TYPE, "BCFKS") + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_PASSWORD, "kspass") + .put(SIGNING_KEY_PROPERTY_KEY + KeyUtils.KEYSTORE_ALIAS, "nonexistent") + .build(); + + assertThrows(IllegalArgumentException.class, () -> JwtVendor.createJwkFromSettings(settings, tempDir.getRoot().toPath())); + } + } diff --git a/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java b/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java index 4d247f2c79..3ba42387ca 100644 --- a/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java +++ b/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java @@ -40,6 +40,7 @@ import org.opensearch.security.test.helper.cluster.ClusterConfiguration; import org.opensearch.security.test.helper.cluster.ClusterHelper; import org.opensearch.security.test.helper.cluster.ClusterInfo; +import org.opensearch.security.test.helper.file.FipsHashAdapter; import org.opensearch.security.test.helper.rest.RestHelper; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; import org.opensearch.transport.client.Client; @@ -120,7 +121,10 @@ public void testNonSSLReindex() throws Exception { + cl2Info.httpPort + "\"," + "\"username\": \"nagilum\"," - + "\"password\": \"nagilum\"" + // Pad under FIPS so the remote credential matches cl2's adapted (PBKDF2) fixture hash. + + "\"password\": \"" + + FipsHashAdapter.adaptPassword("nagilum") + + "\"" + "}," + "\"index\": \"twitter\"," + "\"size\": 10" diff --git a/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java index 0cfcb2a105..0dd756a090 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java @@ -345,10 +345,10 @@ public void testHasPermissionCheckParam_AccessNotAllowedCase() throws Exception rh.keystore = "restapi/kirk-keystore"; rh.sendAdminCertificate = true; - String createUserBody = "{" + "\"password\": \"test-pass\"," + "\"backend_roles\": []" + "}"; + String createUserBody = "{" + "\"password\": \"test-pass-1234x\"," + "\"backend_roles\": []" + "}"; response = rh.executePutRequest("_plugins/_security/api/internalusers/test_user", createUserBody, adminCredsHeader); - Header testUserHeader = encodeBasicHeader("test_user", "test-pass"); + Header testUserHeader = encodeBasicHeader("test_user", "test-pass-1234x"); rh.sendAdminCertificate = false; // test_user has no permissions to GET /_cluster/health response accessAllowed:false diff --git a/src/test/java/org/opensearch/security/hasher/AbstractPasswordHasherTests.java b/src/test/java/org/opensearch/security/hasher/AbstractPasswordHasherTests.java index 5e420c9c78..d05d449b23 100644 --- a/src/test/java/org/opensearch/security/hasher/AbstractPasswordHasherTests.java +++ b/src/test/java/org/opensearch/security/hasher/AbstractPasswordHasherTests.java @@ -17,6 +17,7 @@ import org.junit.Test; import org.opensearch.OpenSearchSecurityException; +import org.opensearch.security.support.FipsMode; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -27,7 +28,7 @@ public abstract class AbstractPasswordHasherTests extends LuceneTestCase { PasswordHasher passwordHasher; - final String password = "testPassword"; + final String password = FipsMode.isEnabled() ? "notarealpassword" : "testPassword"; final String wrongPassword = "wrongTestPassword"; @Test @@ -76,18 +77,18 @@ public void shouldHandleNullHashWhenChecking() { @Test public void shouldCleanupPasswordCharArray() { - char[] password = new char[] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }; - passwordHasher.hash(password); - assertThat("\0\0\0\0\0\0\0\0", is(new String(password))); + char[] passwordAsChar = password.toCharArray(); + passwordHasher.hash(passwordAsChar); + assertThat("\0".repeat(password.length()), is(new String(passwordAsChar))); } @Test public void shouldCleanupPasswordCharBuffer() { - char[] password = new char[] { 'p', 'a', 's', 's', 'w', 'o', 'r', 'd' }; - CharBuffer passwordBuffer = CharBuffer.wrap(password); - passwordHasher.hash(password); - assertThat("\0\0\0\0\0\0\0\0", is(new String(password))); - assertThat("\0\0\0\0\0\0\0\0", is(passwordBuffer.toString())); + char[] passwordAsChar = password.toCharArray(); + CharBuffer passwordBuffer = CharBuffer.wrap(passwordAsChar); + passwordHasher.hash(passwordAsChar); + assertThat("\0".repeat(password.length()), is(new String(passwordAsChar))); + assertThat("\0".repeat(password.length()), is(passwordBuffer.toString())); } } diff --git a/src/test/java/org/opensearch/security/hasher/BCryptPasswordHasherTests.java b/src/test/java/org/opensearch/security/hasher/BCryptPasswordHasherTests.java index ee950f1058..41c709aad4 100644 --- a/src/test/java/org/opensearch/security/hasher/BCryptPasswordHasherTests.java +++ b/src/test/java/org/opensearch/security/hasher/BCryptPasswordHasherTests.java @@ -36,8 +36,8 @@ public void setup() { @Test public void shouldBeBackwardsCompatible() { String legacyHash = "$2y$12$gdh2ecVBQmwpmcAeyReicuNtXyR6GMWSfXHxtcBBqFeFz2VQ8kDZe"; - assertThat(passwordHasher.check(password.toCharArray(), legacyHash), is(true)); - assertThat(passwordHasher.check(wrongPassword.toCharArray(), legacyHash), is(false)); + assertThat(passwordHasher.check("testPassword".toCharArray(), legacyHash), is(true)); + assertThat(passwordHasher.check("wrongTestPassword".toCharArray(), legacyHash), is(false)); } @Test diff --git a/src/test/java/org/opensearch/security/hasher/PBKDF2PasswordHasherTests.java b/src/test/java/org/opensearch/security/hasher/PBKDF2PasswordHasherTests.java index 6d54173a10..9f3bab1e8e 100644 --- a/src/test/java/org/opensearch/security/hasher/PBKDF2PasswordHasherTests.java +++ b/src/test/java/org/opensearch/security/hasher/PBKDF2PasswordHasherTests.java @@ -13,11 +13,15 @@ import org.junit.Before; import org.junit.Test; +import org.bouncycastle.crypto.fips.FipsUnapprovedOperationError; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeTrue; public class PBKDF2PasswordHasherTests extends AbstractPasswordHasherTests { @@ -57,4 +61,11 @@ public void shouldGenerateValidHashesFromParameters() { assertThat(hasher.check(password.toCharArray(), hash), is(true)); assertThat(hasher.check(wrongPassword.toCharArray(), hash), is(false)); } + + @Test + public void shouldThrowExceptionForWeekPassword() { + assumeTrue("BCFIPS provider is required", FipsMode.isEnabled()); + var hasher = new PBKDF2PasswordHasher("SHA512", 10000, 512); + assertThrows(FipsUnapprovedOperationError.class, () -> hasher.hash("test".toCharArray())); + } } diff --git a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java index ce49940923..8d4f56307b 100644 --- a/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/http/OnBehalfOfAuthenticatorTest.java @@ -23,7 +23,10 @@ import java.util.Optional; import java.util.Set; import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.apache.commons.lang3.RandomStringUtils; import org.apache.hc.core5.http.HttpHeaders; import org.apache.logging.log4j.Level; @@ -31,18 +34,24 @@ import org.apache.logging.log4j.core.Appender; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.Logger; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.security.filter.SecurityResponse; +import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; +import org.opensearch.security.util.BCFipsEntropyDaemonFilter; import org.opensearch.security.util.FakeRestRequest; +import org.opensearch.security.util.KeyUtils; +import org.opensearch.test.BouncyCastleThreadFilter; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.mockito.ArgumentCaptor; @@ -54,106 +63,127 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +@RunWith(RandomizedRunner.class) +@ThreadLeakFilters(filters = { BouncyCastleThreadFilter.class, BCFipsEntropyDaemonFilter.class }) public class OnBehalfOfAuthenticatorTest { final static String clusterName = "cluster_0"; final static String enableOBO = "true"; final static String disableOBO = "false"; - final static String claimsEncryptionKey = RandomStringUtils.randomAlphanumeric(16); + // encryption_key is consumed Base64-decoded; a 32-char alphanumeric string decodes to only 24 bytes, + // under the 32-byte AES-256 floor FIPS requires. Base64-encode the 32 bytes so it decodes back to 32. + final static String claimsEncryptionKey = RandomStringUtils.secure().nextAlphanumeric(32); + final static String claimsEncryptionKeyB64Encoded = Base64.getEncoder() + .encodeToString(claimsEncryptionKey.getBytes(StandardCharsets.UTF_8)); final static String signingKey = "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; final static String signingKeyB64Encoded = Base64.getEncoder().encodeToString(signingKey.getBytes(StandardCharsets.UTF_8)); final static SecretKey secretKey = Keys.hmacShaKeyFor(signingKeyB64Encoded.getBytes(StandardCharsets.UTF_8)); - private static final String SECURITY_PREFIX = "/_plugins/_security/"; - private static final String ON_BEHALF_OF_SUFFIX = "api/obo/token"; - private static final String ACCOUNT_SUFFIX = "api/account"; + private static final String KEYSTORE_STORE_PASSWORD = "kspass"; + private static final String KEYSTORE_KEY_PASSWORD = "keypass"; + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); @Test - public void testReRequestAuthenticationReturnsEmptyOptional() { - OnBehalfOfAuthenticator authenticator = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + public void testReRequestAuthenticationReturnsEmptyOptional() throws Exception { + OnBehalfOfAuthenticator authenticator = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Optional result = authenticator.reRequestAuthentication(null, null); assertFalse(result.isPresent()); } @Test - public void testGetTypeReturnsExpectedType() { - OnBehalfOfAuthenticator authenticator = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + public void testGetTypeReturnsExpectedType() throws Exception { + OnBehalfOfAuthenticator authenticator = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); String type = authenticator.getType(); assertThat(type, is("onbehalfof_jwt")); } @Test - public void testNoKey() { - Exception exception = assertThrows( - RuntimeException.class, - () -> extractCredentialsFromJwtHeader( - null, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy"), - false - ) + public void testNoSigningKey() throws Exception { + OpenSearchSecurityException ex = assertThrows( + OpenSearchSecurityException.class, + () -> OnBehalfOfAuthenticator.validateSigningKey(null) ); - assertThat(exception.getMessage(), equalTo("Unable to find on behalf of authenticator signing_key")); + assertThat(ex.getMessage(), equalTo("Unable to find on behalf of authenticator signing_key")); } @Test - public void testEmptyKey() { - Exception exception = assertThrows( - RuntimeException.class, - () -> extractCredentialsFromJwtHeader( - "", - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy"), - false - ) + public void testEmptySigningKey() throws Exception { + OpenSearchSecurityException ex = assertThrows( + OpenSearchSecurityException.class, + () -> OnBehalfOfAuthenticator.validateSigningKey("") ); assertThat( - exception.getMessage(), + ex.getMessage(), equalTo("Signing key size was 0 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") ); } @Test - public void testBadKey() { - Exception exception = assertThrows( - RuntimeException.class, - () -> extractCredentialsFromJwtHeader( - Base64.getEncoder().encodeToString(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }), - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy"), - false - ) + public void testSigningKeyOf80BitsRejected() throws Exception { + // 10 raw bytes -> Base64-decoded length is 80 bits, below the 512-bit minimum + String key = Base64.getEncoder().encodeToString(new byte[] { 1, 3, 3, 4, 3, 6, 7, 8, 3, 10 }); + OpenSearchSecurityException ex = assertThrows( + OpenSearchSecurityException.class, + () -> OnBehalfOfAuthenticator.validateSigningKey(key) ); assertThat( - exception.getMessage(), - equalTo("Signing key size was 128 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") + ex.getMessage(), + equalTo("Signing key size was 80 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") ); } @Test - public void testWeakKeyExceptionHandling() throws Exception { - Settings settings = Settings.builder().put("signing_key", "testKey").put("encryption_key", claimsEncryptionKey).build(); - try { - OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, "testCluster"); - fail("Expected WeakKeyException"); - } catch (OpenSearchSecurityException e) { - assertThat( - e.getMessage(), - equalTo("Signing key size was 56 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") - ); - } + public void testSigningKeyOf384BitsRejected() throws Exception { + // 64 Base64 characters decode to only 48 bytes = 384 bits, below the 512-bit minimum + final String key = Base64.getEncoder().encodeToString(new byte[48]); + assertThat(key.length(), equalTo(64)); + OpenSearchSecurityException ex = assertThrows( + OpenSearchSecurityException.class, + () -> OnBehalfOfAuthenticator.validateSigningKey(key) + ); + assertThat( + ex.getMessage(), + equalTo("Signing key size was 384 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") + ); + } + + @Test + public void testSigningKeyOf40BitsRejected() throws Exception { + // "testKey" Base64-decodes to 5 bytes = 40 bits + OpenSearchSecurityException ex = assertThrows( + OpenSearchSecurityException.class, + () -> OnBehalfOfAuthenticator.validateSigningKey("testKey") + ); + assertThat( + ex.getMessage(), + equalTo("Signing key size was 40 bits, which is not secure enough. Please use a signing_key with a size >= 512 bits.") + ); + } + + @Test + public void testMisconfiguredKeyDeclinesWithoutThrowing() throws Exception { + Settings settings = Settings.builder() + .put("enabled", enableOBO) + .put("signing_key", "testKey") // misconfigured signing key + .put("encryption_key", claimsEncryptionKeyB64Encoded) + .build(); + OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, clusterName, tempDir.getRoot().toPath()); + Map headers = Map.of("Authorization", "Bearer a.b.c"); + AuthCredentials credentials = auth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), null); + assertNull(credentials); } @Test public void testTokenMissing() throws Exception { - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = new HashMap(); AuthCredentials credentials = jwtAuth.extractCredentials( @@ -169,7 +199,7 @@ public void testInvalid() throws Exception { String jwsToken = "123invalidtoken.."; - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -183,13 +213,15 @@ public void testInvalid() throws Exception { @Test public void testDisabled() throws Exception { String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("ext_0") + .and() + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(disableOBOSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(disableOBOSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -201,7 +233,7 @@ public void testDisabled() throws Exception { } @Test - public void testInvalidTokenException() { + public void testInvalidTokenException() throws Exception { Appender mockAppender = mock(Appender.class); ArgumentCaptor logEventCaptor = ArgumentCaptor.forClass(LogEvent.class); when(mockAppender.getName()).thenReturn("MockAppender"); @@ -214,7 +246,7 @@ public void testInvalidTokenException() { String invalidToken = "invalidToken"; Settings settings = defaultSettings(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(settings, clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(settings, clusterName, tempDir.getRoot().toPath()); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Bearer " + invalidToken); @@ -236,13 +268,15 @@ public void testInvalidTokenException() { @Test public void testNonSpecifyOBOSetting() throws Exception { String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("ext_0") + .and() + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(nonSpecifyOBOSetting(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(nonSpecifyOBOSetting(), clusterName, tempDir.getRoot().toPath()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -261,13 +295,15 @@ public void testBearer() throws Exception { expectedAttributes.put("attr.jwt.aud", "[\"ext_0\"]"); String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("ext_0") + .and() + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -287,12 +323,14 @@ public void testBearer() throws Exception { public void testBearerWrongPosition() throws Exception { String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(secretKey, SignatureAlgorithm.HS512) + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("ext_0") + .and() + .signWith(secretKey, Jwts.SIG.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = new HashMap(); headers.put("Authorization", jwsToken + "Bearer " + " 123"); @@ -308,12 +346,14 @@ public void testBearerWrongPosition() throws Exception { @Test public void testBasicAuthHeader() throws Exception { String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(secretKey, SignatureAlgorithm.HS512) + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("ext_0") + .and() + .signWith(secretKey, Jwts.SIG.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, "Basic " + jwsToken); @@ -337,7 +377,7 @@ public void testMissingBearerScheme() throws Exception { String craftedToken = "beaRerSomeActualToken"; // This token matches the BEARER pattern but doesn't contain the BEARER_PREFIX - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, craftedToken); AuthCredentials credentials = jwtAuth.extractCredentials( @@ -356,15 +396,17 @@ public void testMissingBearerScheme() throws Exception { } @Test - public void testMissingBearerPrefixInAuthHeader() { + public void testMissingBearerPrefixInAuthHeader() throws Exception { String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(secretKey, SignatureAlgorithm.HS512) + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("ext_0") + .and() + .signWith(secretKey, Jwts.SIG.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = Collections.singletonMap(HttpHeaders.AUTHORIZATION, jwsToken); @@ -377,12 +419,12 @@ public void testMissingBearerPrefixInAuthHeader() { } @Test - public void testPlainTextedRolesFromDrClaim() { + public void testPlainTextedRolesFromDrClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").claim("roles", "role1,role2").setAudience("svc1"), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Leonard McCoy").claim("roles", "role1,role2").audience().add("svc1").and(), true ); @@ -393,13 +435,165 @@ public void testPlainTextedRolesFromDrClaim() { } @Test - public void testBackendRolesExtraction() { + public void testSigningKeyFromKeystoreVerifiesToken() throws Exception { + final byte[] keyBytes = Base64.getDecoder().decode(signingKeyB64Encoded); + final FileHelper.TypedStore typedStore = FileHelper.storeSecretKey( + tempDir, + "obo-signing", + new SecretKeySpec(keyBytes, "HmacSHA512"), + KEYSTORE_STORE_PASSWORD, + KEYSTORE_KEY_PASSWORD + ); + + final Settings settings = putKeystoreSettings( + Settings.builder().put("enabled", enableOBO).put("encryption_key", claimsEncryptionKeyB64Encoded), + typedStore, + "signing_key", + "obo-signing" + ).build(); + + final String jwsToken = Jwts.builder() + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("svc1") + .and() + .claim("roles", "role1,role2") + .signWith(Keys.hmacShaKeyFor(keyBytes), Jwts.SIG.HS512) + .compact(); + + final OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, clusterName, tempDir.getRoot().toPath()); + final Map headers = Map.of("Authorization", "Bearer " + jwsToken); + final AuthCredentials credentials = auth.extractCredentials( + new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), + null + ); + + assertNotNull(credentials); + assertThat(credentials.getUsername(), is("Leonard McCoy")); + assertThat(credentials.getSecurityRoles().size(), is(2)); + } + + @Test + public void testEncryptionKeyFromKeystoreDecryptsRoles() throws Exception { + final byte[] aesKeyBytes = new byte[32]; + Arrays.fill(aesKeyBytes, (byte) 7); + final FileHelper.TypedStore typedStore = FileHelper.storeSecretKey( + tempDir, + "obo-enc", + new SecretKeySpec(aesKeyBytes, "AES"), + KEYSTORE_STORE_PASSWORD, + KEYSTORE_KEY_PASSWORD + ); + + final Settings settings = putKeystoreSettings( + Settings.builder().put("enabled", enableOBO).put("signing_key", signingKeyB64Encoded), + typedStore, + "encryption_key", + "obo-enc" + ).build(); + + // Simulate issuance: encrypt the roles with the same keystore-derived key + final EncryptionDecryptionUtil issuerUtil = EncryptionDecryptionUtil.fromSettings( + settings, + "encryption_key", + tempDir.getRoot().toPath() + ); + final String encryptedRoles = issuerUtil.encrypt("role1,role2"); + + final String jwsToken = Jwts.builder() + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("svc1") + .and() + .claim("encrypted_roles", encryptedRoles) + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) + .compact(); + + final OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, clusterName, tempDir.getRoot().toPath()); + final Map headers = Map.of("Authorization", "Bearer " + jwsToken); + final AuthCredentials credentials = auth.extractCredentials( + new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), + null + ); + + assertNotNull(credentials); + assertThat(credentials.getSecurityRoles().size(), is(2)); + assertTrue(credentials.getSecurityRoles().contains("role1")); + assertTrue(credentials.getSecurityRoles().contains("role2")); + } + + @Test + public void testSigningAndEncryptionKeysBothFromKeystore() throws Exception { + final byte[] signingKeyBytes = Base64.getDecoder().decode(signingKeyB64Encoded); + final FileHelper.TypedStore signingStore = FileHelper.storeSecretKey( + tempDir, + "obo-signing", + new SecretKeySpec(signingKeyBytes, "HmacSHA512"), + KEYSTORE_STORE_PASSWORD, + KEYSTORE_KEY_PASSWORD + ); + + final byte[] aesKeyBytes = new byte[32]; + Arrays.fill(aesKeyBytes, (byte) 7); + final FileHelper.TypedStore encryptionStore = FileHelper.storeSecretKey( + tempDir, + "obo-enc", + new SecretKeySpec(aesKeyBytes, "AES"), + KEYSTORE_STORE_PASSWORD, + KEYSTORE_KEY_PASSWORD + ); + + Settings.Builder builder = putKeystoreSettings( + Settings.builder().put("enabled", enableOBO), + signingStore, + "signing_key", + "obo-signing" + ); + builder = putKeystoreSettings(builder, encryptionStore, "encryption_key", "obo-enc"); + final Settings settings = builder.build(); + + // Simulate issuance: encrypt the roles with the same keystore-derived AES key the verifier will resolve. + final EncryptionDecryptionUtil issuerUtil = EncryptionDecryptionUtil.fromSettings( + settings, + "encryption_key", + tempDir.getRoot().toPath() + ); + final String encryptedRoles = issuerUtil.encrypt("role1,role2"); + + final String jwsToken = Jwts.builder() + .issuer(clusterName) + .subject("Leonard McCoy") + .audience() + .add("svc1") + .and() + .claim("encrypted_roles", encryptedRoles) + .signWith(Keys.hmacShaKeyFor(signingKeyBytes), Jwts.SIG.HS512) + .compact(); + + final OnBehalfOfAuthenticator auth = new OnBehalfOfAuthenticator(settings, clusterName, tempDir.getRoot().toPath()); + final Map headers = Map.of("Authorization", "Bearer " + jwsToken); + final AuthCredentials credentials = auth.extractCredentials( + new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), + null + ); + + assertNotNull(credentials); + assertThat(credentials.getUsername(), is("Leonard McCoy")); + assertThat(credentials.getSecurityRoles().size(), is(2)); + assertTrue(credentials.getSecurityRoles().contains("role1")); + assertTrue(credentials.getSecurityRoles().contains("role2")); + } + + @Test + public void testBackendRolesExtraction() throws Exception { String rolesString = "role1, role2 ,role3,role4 , role5"; final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Test User").setAudience("audience_0").claim("backend_roles", rolesString), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Test User").audience().add("audience_0").and().claim("backend_roles", rolesString), true ); @@ -412,21 +606,24 @@ public void testBackendRolesExtraction() { } @Test - public void testRolesDecryptionFromErClaimWithNoEncryptionKeyReturnsEmpty() { - EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(claimsEncryptionKey); + public void testRolesDecryptionFromErClaimWithNoEncryptionKeyReturnsEmpty() throws Exception { + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(claimsEncryptionKeyB64Encoded); String encryptedRole = util.encrypt("admin,developer"); // No encryption_key in settings final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator( Settings.builder().put("enabled", enableOBO).put("signing_key", signingKeyB64Encoded).build(), - clusterName + clusterName, + tempDir.getRoot().toPath() ); final String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Test User") - .setAudience("audience_0") + .issuer(clusterName) + .subject("Test User") + .audience() + .add("audience_0") + .and() .claim("encrypted_roles", encryptedRole) - .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) .compact(); final AuthCredentials credentials = jwtAuth.extractCredentials( new FakeRestRequest(Map.of("Authorization", "Bearer " + jwsToken), new HashMap<>()).asSecurityRequest(), @@ -438,18 +635,21 @@ public void testRolesDecryptionFromErClaimWithNoEncryptionKeyReturnsEmpty() { } @Test - public void testPlainTextRolesFromDrClaimWithNoEncryptionKey() { + public void testPlainTextRolesFromDrClaimWithNoEncryptionKey() throws Exception { // No encryption_key in settings — dr claim should still be readable final OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator( Settings.builder().put("enabled", enableOBO).put("signing_key", signingKeyB64Encoded).build(), - clusterName + clusterName, + tempDir.getRoot().toPath() ); final String jwsToken = Jwts.builder() - .setIssuer(clusterName) - .setSubject("Test User") - .setAudience("audience_0") + .issuer(clusterName) + .subject("Test User") + .audience() + .add("audience_0") + .and() .claim("roles", "role1,role2") - .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) .compact(); final AuthCredentials credentials = jwtAuth.extractCredentials( new FakeRestRequest(Map.of("Authorization", "Bearer " + jwsToken), new HashMap<>()).asSecurityRequest(), @@ -462,14 +662,20 @@ public void testPlainTextRolesFromDrClaimWithNoEncryptionKey() { } @Test - public void testRolesDecryptionFromErClaim() { - EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(claimsEncryptionKey); + public void testRolesDecryptionFromErClaim() throws Exception { + EncryptionDecryptionUtil util = new EncryptionDecryptionUtil(claimsEncryptionKeyB64Encoded); String encryptedRole = util.encrypt("admin,developer"); final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Test User").setAudience("audience_0").claim("encrypted_roles", encryptedRole), + claimsEncryptionKeyB64Encoded, + Jwts.builder() + .issuer(clusterName) + .subject("Test User") + .audience() + .add("audience_0") + .and() + .claim("encrypted_roles", encryptedRole), true ); @@ -483,8 +689,8 @@ public void testNullClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").claim("roles", null).setAudience("svc1"), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Leonard McCoy").claim("roles", null).audience().add("svc1").and(), false ); @@ -498,8 +704,8 @@ public void testNonStringClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").claim("roles", 123L).setAudience("svc1"), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Leonard McCoy").claim("roles", 123L).audience().add("svc1").and(), true ); @@ -514,8 +720,8 @@ public void testRolesMissing() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Leonard McCoy").setAudience("svc1"), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Leonard McCoy").audience().add("svc1").and(), false ); @@ -530,8 +736,8 @@ public void testWrongSubjectKey() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).claim("roles", "role1,role2").claim("asub", "Dr. Who").setAudience("svc1"), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).claim("roles", "role1,role2").claim("asub", "Dr. Who").audience().add("svc1").and(), false ); @@ -543,8 +749,8 @@ public void testMissingAudienceClaim() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Test User").claim("roles", "role1,role2"), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Test User").claim("roles", "role1,role2"), false ); @@ -556,8 +762,8 @@ public void testExp() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Expired").setExpiration(new Date(100)), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Expired").expiration(new Date(100)), false ); @@ -569,8 +775,8 @@ public void testNbf() throws Exception { final AuthCredentials credentials = extractCredentialsFromJwtHeader( signingKeyB64Encoded, - claimsEncryptionKey, - Jwts.builder().setIssuer(clusterName).setSubject("Expired").setNotBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), + claimsEncryptionKeyB64Encoded, + Jwts.builder().issuer(clusterName).subject("Expired").notBefore(new Date(System.currentTimeMillis() + (1000 * 36000))), false ); @@ -591,7 +797,12 @@ public void testRolesArray() throws Exception { + "}" ); - final AuthCredentials credentials = extractCredentialsFromJwtHeader(signingKeyB64Encoded, claimsEncryptionKey, builder, true); + final AuthCredentials credentials = extractCredentialsFromJwtHeader( + signingKeyB64Encoded, + claimsEncryptionKeyB64Encoded, + builder, + true + ); assertNotNull(credentials); assertThat(credentials.getUsername(), is("Cluster_0")); @@ -605,13 +816,15 @@ public void testRolesArray() throws Exception { public void testDifferentIssuer() throws Exception { String jwsToken = Jwts.builder() - .setIssuer("Wrong Cluster Identifier") - .setSubject("Leonard McCoy") - .setAudience("ext_0") - .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), SignatureAlgorithm.HS512) + .issuer("Wrong Cluster Identifier") + .subject("Leonard McCoy") + .audience() + .add("ext_0") + .and() + .signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) .compact(); - OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName); + OnBehalfOfAuthenticator jwtAuth = new OnBehalfOfAuthenticator(defaultSettings(), clusterName, tempDir.getRoot().toPath()); Map headers = new HashMap(); headers.put("Authorization", "Bearer " + jwsToken); @@ -623,6 +836,19 @@ public void testDifferentIssuer() throws Exception { assertNull(credentials); } + private static Settings.Builder putKeystoreSettings( + Settings.Builder builder, + FileHelper.TypedStore typedStore, + String prefix, + String alias + ) { + return builder.put(prefix + KeyUtils.KEYSTORE_PATH, typedStore.path()) + .put(prefix + KeyUtils.KEYSTORE_TYPE, typedStore.type()) + .put(prefix + KeyUtils.KEYSTORE_PASSWORD, KEYSTORE_STORE_PASSWORD) + .put(prefix + KeyUtils.KEYSTORE_ALIAS, alias) + .put(prefix + KeyUtils.KEYSTORE_KEY_PASSWORD, KEYSTORE_KEY_PASSWORD); + } + /** extracts a default user credential from a request header */ private AuthCredentials extractCredentialsFromJwtHeader( final String signingKeyB64Encoded, @@ -636,13 +862,12 @@ private AuthCredentials extractCredentialsFromJwtHeader( .put("signing_key", signingKeyB64Encoded) .put("encryption_key", encryptionKey) .build(), - clusterName + clusterName, + tempDir.getRoot().toPath() ); - final String jwsToken = jwtBuilder.signWith( - Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), - SignatureAlgorithm.HS512 - ).compact(); + final String jwsToken = jwtBuilder.signWith(Keys.hmacShaKeyFor(Base64.getDecoder().decode(signingKeyB64Encoded)), Jwts.SIG.HS512) + .compact(); final Map headers = Map.of("Authorization", "Bearer " + jwsToken); return jwtAuth.extractCredentials(new FakeRestRequest(headers, new HashMap<>()).asSecurityRequest(), null); } @@ -651,7 +876,7 @@ private Settings defaultSettings() { return Settings.builder() .put("enabled", enableOBO) .put("signing_key", signingKeyB64Encoded) - .put("encryption_key", claimsEncryptionKey) + .put("encryption_key", claimsEncryptionKeyB64Encoded) .build(); } @@ -659,15 +884,15 @@ private Settings disableOBOSettings() { return Settings.builder() .put("enabled", disableOBO) .put("signing_key", signingKeyB64Encoded) - .put("encryption_key", claimsEncryptionKey) + .put("encryption_key", claimsEncryptionKeyB64Encoded) .build(); } private Settings noSigningKeyOBOSettings() { - return Settings.builder().put("enabled", disableOBO).put("encryption_key", claimsEncryptionKey).build(); + return Settings.builder().put("enabled", disableOBO).put("encryption_key", claimsEncryptionKeyB64Encoded).build(); } private Settings nonSpecifyOBOSetting() { - return Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKey).build(); + return Settings.builder().put("signing_key", signingKeyB64Encoded).put("encryption_key", claimsEncryptionKeyB64Encoded).build(); } } diff --git a/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java b/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java index 59051e9a02..5a97260e73 100644 --- a/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java +++ b/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java @@ -19,9 +19,12 @@ import org.opensearch.security.test.DynamicSecurityConfig; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.test.helper.file.FipsHashAdapter; public class HttpClientTest extends SingleClusterTest { + private static final String ADMIN_PASSWORD = FipsHashAdapter.adaptPassword("admin"); + @Override protected String getResourceFolder() { return "auditlog"; @@ -41,7 +44,7 @@ public void testPlainConnection() throws Exception { try ( final HttpClient httpClient = HttpClient.builder(clusterInfo.httpHost + ":" + clusterInfo.httpPort) - .setBasicCredentials("admin", "admin") + .setBasicCredentials("admin", ADMIN_PASSWORD) .build() ) { Assert.assertTrue(httpClient.index("{\"a\":5}", "index", "type", false)); @@ -49,7 +52,7 @@ public void testPlainConnection() throws Exception { Assert.assertTrue(httpClient.index("{\"a\":5}", "index", "type", true)); } - try (final HttpClient httpClient = HttpClient.builder("unknownhost:6654").setBasicCredentials("admin", "admin").build()) { + try (final HttpClient httpClient = HttpClient.builder("unknownhost:6654").setBasicCredentials("admin", ADMIN_PASSWORD).build()) { Assert.assertFalse(httpClient.index("{\"a\":5}", "index", "type", false)); Assert.assertFalse(httpClient.index("{\"a\":5}", "index", "type", true)); Assert.assertFalse(httpClient.index("{\"a\":5}", "index", "type", true)); @@ -58,7 +61,7 @@ public void testPlainConnection() throws Exception { try ( final HttpClient httpClient = HttpClient.builder("unknownhost:6654", clusterInfo.httpHost + ":" + clusterInfo.httpPort) .enableSsl(FileHelper.getKeystoreFromClassPath("auditlog/truststore", "changeit"), false) - .setBasicCredentials("admin", "admin") + .setBasicCredentials("admin", ADMIN_PASSWORD) .build() ) { Assert.assertFalse(httpClient.index("{\"a\":5}", "index", "type", false)); @@ -68,7 +71,7 @@ public void testPlainConnection() throws Exception { try ( final HttpClient httpClient = HttpClient.builder("unknownhost:6654", clusterInfo.httpHost + ":" + clusterInfo.httpPort) - .setBasicCredentials("admin", "admin") + .setBasicCredentials("admin", ADMIN_PASSWORD) .build() ) { Assert.assertTrue(httpClient.index("{\"a\":5}", "index", "type", false)); @@ -96,7 +99,7 @@ public void testSslConnection() throws Exception { try ( final HttpClient httpClient = HttpClient.builder(clusterInfo.httpHost + ":" + clusterInfo.httpPort) .enableSsl(FileHelper.getKeystoreFromClassPath("auditlog/truststore", "changeit"), false) - .setBasicCredentials("admin", "admin") + .setBasicCredentials("admin", ADMIN_PASSWORD) .build() ) { Assert.assertTrue(httpClient.index("{\"a\":5}", "index", "type", false)); @@ -106,7 +109,7 @@ public void testSslConnection() throws Exception { try ( final HttpClient httpClient = HttpClient.builder(clusterInfo.httpHost + ":" + clusterInfo.httpPort) - .setBasicCredentials("admin", "admin") + .setBasicCredentials("admin", ADMIN_PASSWORD) .build() ) { Assert.assertFalse(httpClient.index("{\"a\":5}", "index", "type", false)); diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index f1145b706b..25924e6822 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -13,6 +13,7 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; import java.util.Base64; import org.junit.After; @@ -69,7 +70,7 @@ public class SecurityTokenManagerTest { @Before public void setup() { - tokenManager = spy(new SecurityTokenManager(cs, threadPool, userService, (user, caller) -> user.getSecurityRoles())); + tokenManager = spy(new SecurityTokenManager(cs, threadPool, userService, (user, caller) -> user.getSecurityRoles(), null)); } @After @@ -106,7 +107,7 @@ private DynamicConfigModel createMockJwtVendorInTokenManager(boolean includeEncr final Settings settings = Settings.builder() .put("enabled", true) .put("signing_key", signingKeyB64Encoded) - .put("encryption_key", (includeEncryptionKey ? "1234567890" : null)) + .put("encryption_key", (includeEncryptionKey ? fipsCompatibleEncryptionKey() : null)) .build(); final DynamicConfigModel dcm = mock(DynamicConfigModel.class); when(dcm.getDynamicOnBehalfOfSettings()).thenReturn(settings); @@ -115,6 +116,16 @@ private DynamicConfigModel createMockJwtVendorInTokenManager(boolean includeEncr return dcm; } + /** + * Returns a base64-encoded key with 32 bytes of material, so the AES-256 key derived by + * {@code EncryptionDecryptionUtil} clears the BC FIPS IKM floor (>= 112 bits; 32 bytes for AES-256). + */ + private static String fipsCompatibleEncryptionKey() { + final byte[] keyMaterial = new byte[32]; + new SecureRandom().nextBytes(keyMaterial); + return Base64.getEncoder().encodeToString(keyMaterial); + } + @Test public void issueServiceAccountToken_error() throws Exception { final String expectedAccountName = "abc-123"; diff --git a/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java b/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java index f0c498e15b..4dacdc6ccc 100644 --- a/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java +++ b/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java @@ -26,6 +26,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.StringContains.containsString; @RunWith(Parameterized.class) public class ConfigV7Test { @@ -133,4 +135,39 @@ public void testOnBehalfOfSettings() { Assert.assertNull(oboSettings.getSigningKey()); Assert.assertNull(oboSettings.getEncryptionKey()); } + + @Test + public void testOnBehalfOfSettingsCarriesKeystoreSubSettings() throws Exception { + final String json = "{\"enabled\":true," + + "\"signing_key_keystore_path\":\"/etc/obo/ks.bcfks\"," + + "\"signing_key_keystore_alias\":\"obo-signing\"," + + "\"signing_key_keystore_type\":\"BCFKS\"," + + "\"encryption_key_keystore_alias\":\"obo-enc\"}"; + + final ConfigV7.OnBehalfOfSettings obo = DefaultObjectMapper.readValue(json, ConfigV7.OnBehalfOfSettings.class); + final String rendered = obo.configAsJson(); + + assertThat(rendered, containsString("signing_key_keystore_path")); + assertThat(rendered, containsString("/etc/obo/ks.bcfks")); + assertThat(rendered, containsString("signing_key_keystore_alias")); + assertThat(rendered, containsString("obo-signing")); + assertThat(rendered, containsString("encryption_key_keystore_alias")); + } + + @Test + public void testOnBehalfOfSettingsToStringRedactsKeys() { + ConfigV7.OnBehalfOfSettings oboSettings = new ConfigV7.OnBehalfOfSettings(); + oboSettings.setSigningKey("c3VwZXItc2VjcmV0LXNpZ25pbmcta2V5"); + oboSettings.setEncryptionKey("c3VwZXItc2VjcmV0LWVuY3J5cHRpb24ta2V5"); + + final String rendered = oboSettings.toString(); + assertThat(rendered, not(containsString("c3VwZXItc2VjcmV0LXNpZ25pbmcta2V5"))); + assertThat(rendered, not(containsString("c3VwZXItc2VjcmV0LWVuY3J5cHRpb24ta2V5"))); + assertThat(rendered, containsString("signing_key=****")); + assertThat(rendered, containsString("encryption_key=****")); + + final String emptyRendered = new ConfigV7.OnBehalfOfSettings().toString(); + assertThat(emptyRendered, containsString("signing_key=")); + assertThat(emptyRendered, containsString("encryption_key=")); + } } diff --git a/src/test/java/org/opensearch/security/ssl/CertificateValidatorTest.java b/src/test/java/org/opensearch/security/ssl/CertificateValidatorTest.java index 3740eb91d1..d52863fe11 100644 --- a/src/test/java/org/opensearch/security/ssl/CertificateValidatorTest.java +++ b/src/test/java/org/opensearch/security/ssl/CertificateValidatorTest.java @@ -35,11 +35,11 @@ import org.apache.logging.log4j.Logger; import org.junit.Assert; import org.junit.Test; -import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.opensearch.security.ssl.util.CertificateValidator; import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.ssl.util.SSLRequestHelper; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.helper.file.FileHelper; import static org.hamcrest.MatcherAssert.assertThat; @@ -87,9 +87,7 @@ public void testStaticCRL() throws Exception { validator.validate(certsToValidate.toArray(new X509Certificate[0])); Assert.fail(); } catch (GeneralSecurityException e) { - String expectedMessage = CryptoServicesRegistrar.isInApprovedOnlyMode() - ? "Certificate revocation after 2018-05-05" - : "Certificate has been revoked"; + String expectedMessage = FipsMode.isEnabled() ? "Certificate revocation after 2018-05-05" : "Certificate has been revoked"; Assert.assertNotNull(ExceptionUtils.findMsg(e, expectedMessage)); } } @@ -129,9 +127,7 @@ public void testStaticCRLOk() throws Exception { try { validator.validate(certsToValidate.toArray(new X509Certificate[0])); } catch (GeneralSecurityException e) { - String expectedMessage = CryptoServicesRegistrar.isInApprovedOnlyMode() - ? "No CRLs found for issuer" - : "Certificate has been revoked"; + String expectedMessage = FipsMode.isEnabled() ? "No CRLs found for issuer" : "Certificate has been revoked"; Assert.assertNotNull(ExceptionUtils.findMsg(e, expectedMessage)); } } @@ -165,9 +161,7 @@ public void testNoValidationPossible() throws Exception { Assert.fail(); } catch (GeneralSecurityException e) { assertThat(e, instanceOf(CertPathValidatorException.class)); - String expectedMessage = CryptoServicesRegistrar.isInApprovedOnlyMode() - ? "No CRLs found for issuer" - : "Certificate has been revoked"; + String expectedMessage = FipsMode.isEnabled() ? "Certificate revocation after" : "Certificate has been revoked"; Assert.assertNotNull(ExceptionUtils.findMsg(e, expectedMessage)); } } @@ -203,9 +197,7 @@ public void testCRLDP() throws Exception { validator.validate(certsToValidate.toArray(new X509Certificate[0])); Assert.fail(); } catch (GeneralSecurityException e) { - String expectedMessage = CryptoServicesRegistrar.isInApprovedOnlyMode() - ? "No CRLs found for issuer" - : "Certificate has been revoked"; + String expectedMessage = FipsMode.isEnabled() ? "Certificate revocation after" : "Certificate has been revoked"; Assert.assertNotNull(ExceptionUtils.findMsg(e, expectedMessage)); } } diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesRule.java b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java index 65786ba0be..bb4010f14f 100644 --- a/src/test/java/org/opensearch/security/ssl/CertificatesRule.java +++ b/src/test/java/org/opensearch/security/ssl/CertificatesRule.java @@ -13,9 +13,11 @@ import java.io.IOException; import java.math.BigInteger; +import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; @@ -45,15 +47,18 @@ import org.bouncycastle.cert.X509v3CertificateBuilder; import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; -import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; import org.opensearch.common.collect.Tuple; +import org.opensearch.security.test.helper.file.FileHelper; -public class CertificatesRule extends ExternalResource { +import static org.opensearch.security.dlic.rest.validation.PasswordValidator.FIPS_MIN_PASSWORD_LENGTH; +import static org.opensearch.security.ssl.CertificatesUtils.privateKeyToPemObject; +import static org.opensearch.security.ssl.CertificatesUtils.writePemContent; +import static org.opensearch.security.ssl.util.SSLConfigConstants.DEFAULT_STORE_TYPE; - private final static BouncyCastleFipsProvider BOUNCY_CASTLE_FIPS_PROVIDER = new BouncyCastleFipsProvider(); +public class CertificatesRule extends ExternalResource { private final TemporaryFolder temporaryFolder = new TemporaryFolder(); @@ -61,7 +66,7 @@ public class CertificatesRule extends ExternalResource { private Path configRootFolder; - private final String privateKeyPassword = RandomStringUtils.randomAlphabetic(10); + private final String privateKeyPassword = RandomStringUtils.secure().nextAlphanumeric(FIPS_MIN_PASSWORD_LENGTH); private X509CertificateHolder caCertificateHolder; @@ -128,7 +133,7 @@ public PrivateKey accessCertificatePrivateKey() { } public KeyPair generateKeyPair() throws NoSuchAlgorithmException { - KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", BOUNCY_CASTLE_FIPS_PROVIDER); + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(4096); return generator.generateKeyPair(); } @@ -179,7 +184,7 @@ public X509CertificateHolder generateCaCertificate( endDate ).addExtension(Extension.basicConstraints, true, new BasicConstraints(true)) .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyCertSign | KeyUsage.cRLSign)) - .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_FIPS_PROVIDER).build(parentKeyPair.getPrivate())); + .build(new JcaContentSignerBuilder("SHA256withRSA").build(parentKeyPair.getPrivate())); } public Tuple generateAccessCertificate(final KeyPair parentKeyPair) throws NoSuchAlgorithmException, @@ -282,8 +287,39 @@ public Tuple generateAccessCertificate( final Instant startDate, final Instant endDate, final List sans + ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { + return generateCertificate(subject, issuer, parentKeyPair, serialNumber, startDate, endDate, sans, false); + } + + public Tuple generateNodeCertificate(final KeyPair parentKeyPair, final String subject) + throws NoSuchAlgorithmException, IOException, OperatorCreationException { + final var startAndEndDate = generateStartAndEndDate(); + return generateCertificate( + subject, + DEFAULT_SUBJECT_NAME, + parentKeyPair, + generateSerialNumber(), + startAndEndDate.v1(), + startAndEndDate.v2(), + defaultSubjectAlternativeNames(), + true + ); + } + + private Tuple generateCertificate( + final String subject, + final String issuer, + final KeyPair parentKeyPair, + final BigInteger serialNumber, + final Instant startDate, + final Instant endDate, + final List sans, + final boolean includeServerAuth ) throws NoSuchAlgorithmException, IOException, OperatorCreationException { final var keyPair = generateKeyPair(); + final KeyPurposeId[] ekuOids = includeServerAuth + ? new KeyPurposeId[] { KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth } + : new KeyPurposeId[] { KeyPurposeId.id_kp_clientAuth }; final var certificate = createCertificateBuilder( subject, issuer, @@ -298,9 +334,9 @@ public Tuple generateAccessCertificate( true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.nonRepudiation | KeyUsage.keyEncipherment) ) - .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)) + .addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(ekuOids)) .addExtension(Extension.subjectAlternativeName, false, new DERSequence(sans.toArray(sans.toArray(new ASN1Encodable[0])))) - .build(new JcaContentSignerBuilder("SHA256withRSA").setProvider(BOUNCY_CASTLE_FIPS_PROVIDER).build(parentKeyPair.getPrivate())); + .build(new JcaContentSignerBuilder("SHA256withRSA").build(parentKeyPair.getPrivate())); return Tuple.tuple(keyPair.getPrivate(), certificate); } @@ -349,4 +385,28 @@ public BigInteger generateSerialNumber() { return BigInteger.valueOf(Instant.now().plusMillis(100).getEpochSecond()); } + public record PemFiles(Path cert, Path key, Path trustedCasPem, FileHelper.TypedStore trustStore, String trustStorePassword) { + } + + public PemFiles generatePemFiles(final String subject, final String password) throws Exception { + final var keyPair = generateKeyPair(); + final X509CertificateHolder caCert = generateCaCertificate(keyPair); + final var nodeKeyAndCert = generateNodeCertificate(keyPair, subject); + final var cert = configRootFolder.resolve("test-node.crt.pem"); + final var key = configRootFolder.resolve("test-node.key.pem"); + final var trustedCasPem = configRootFolder.resolve("test-ca.pem"); + writePemContent(cert, nodeKeyAndCert.v2(), caCert); + writePemContent(key, privateKeyToPemObject(nodeKeyAndCert.v1(), password)); + writePemContent(trustedCasPem, caCert); + final String tsExt = FileHelper.TYPE_TO_EXTENSION_MAP.get(DEFAULT_STORE_TYPE).getFirst(); + final var tsFile = new FileHelper.TypedStore(configRootFolder.resolve("test-ca-truststore" + tsExt), DEFAULT_STORE_TYPE); + final var ts = KeyStore.getInstance(tsFile.type()); + ts.load(null, null); + ts.setCertificateEntry("ca", toX509Certificate(caCert)); + try (var out = Files.newOutputStream(tsFile.path())) { + ts.store(out, password.toCharArray()); + } + return new PemFiles(cert, key, trustedCasPem, tsFile, password); + } + } diff --git a/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java index e6ad9991ee..0de829819d 100644 --- a/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java +++ b/src/test/java/org/opensearch/security/ssl/CertificatesUtils.java @@ -26,8 +26,8 @@ public class CertificatesUtils { public static void writePemContent(final Path path, final Object... content) throws IOException { - for (final Object c : content) { - try (JcaPEMWriter writer = new JcaPEMWriter(Files.newBufferedWriter(path))) { + try (JcaPEMWriter writer = new JcaPEMWriter(Files.newBufferedWriter(path))) { + for (final Object c : content) { writer.writeObject(c); } } @@ -36,7 +36,7 @@ public static void writePemContent(final Path path, final Object... content) thr public static PemObject privateKeyToPemObject(final PrivateKey privateKey, final String password) throws Exception { return new PKCS8Generator( PrivateKeyInfo.getInstance(privateKey.getEncoded()), - new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.PBE_SHA1_3DES).setRandom(new SecureRandom()) + new JceOpenSSLPKCS8EncryptorBuilder(PKCS8Generator.AES_256_CBC).setRandom(new SecureRandom()) .setPassword(password.toCharArray()) .build() ).generate(); diff --git a/src/test/java/org/opensearch/security/ssl/SSLTest.java b/src/test/java/org/opensearch/security/ssl/SSLTest.java index bab075e196..88c074ee83 100644 --- a/src/test/java/org/opensearch/security/ssl/SSLTest.java +++ b/src/test/java/org/opensearch/security/ssl/SSLTest.java @@ -32,6 +32,7 @@ import com.google.common.collect.Lists; import org.apache.hc.core5.http.NoHttpResponseException; import org.junit.Assert; +import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -54,6 +55,7 @@ import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.AbstractSecurityUnitTest; import org.opensearch.security.test.SingleClusterTest; import org.opensearch.security.test.helper.file.FileHelper; @@ -64,6 +66,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.opensearch.common.network.NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY; +import static org.opensearch.security.dlic.rest.validation.PasswordValidator.FIPS_MIN_PASSWORD_LENGTH; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_HTTP_KEYSTORE_KEYPASSWORD; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_HTTP_PEMKEY_PASSWORD; import static org.opensearch.security.ssl.SecureSSLSettings.SSLSetting.SECURITY_SSL_TRANSPORT_KEYSTORE_KEYPASSWORD; @@ -74,6 +77,9 @@ @SuppressWarnings({ "resource", "unchecked" }) public class SSLTest extends SingleClusterTest { + @ClassRule + public static final CertificatesRule certificatesRule = new CertificatesRule(false); + @Rule public final ExpectedException thrown = ExpectedException.none(); @@ -185,14 +191,19 @@ public void testCipherAndProtocols() throws Exception { Security.setProperty("jdk.tls.disabledAlgorithms", ""); + var typedKeyStore = FileHelper.resolveStore("ssl/node-0-keystore"); + var typedTrustStore = FileHelper.resolveStore("ssl/truststore"); + Settings settings = Settings.builder() .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, false) .put(ConfigConstants.SECURITY_SSL_ONLY, true) .put(SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_ALIAS, "node-0") .put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true) .put(SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE, "REQUIRE") - .put(SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, FileHelper.resolveStore("ssl/node-0-keystore").path()) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ssl/truststore").path()) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, typedKeyStore.path()) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_TYPE, typedKeyStore.type()) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_TRUSTSTORE_FILEPATH, typedTrustStore.path()) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_TRUSTSTORE_TYPE, typedTrustStore.type()) // WEAK and insecure cipher, do NOT use this, its here for unittesting only!!! .put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_CIPHERS, "SSL_RSA_EXPORT_WITH_RC4_40_MD5") // WEAK and insecure protocol, do NOT use this, its here for unittesting only!!! @@ -213,8 +224,8 @@ public void testCipherAndProtocols() throws Exception { settings = Settings.builder() .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, true) .put(ConfigConstants.SECURITY_SSL_ONLY, true) - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, FileHelper.resolveStore("ssl/node-0-keystore").path()) - .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, FileHelper.resolveStore("ssl/truststore").path()) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, typedKeyStore.path()) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, typedTrustStore.path()) // WEAK and insecure cipher, do NOT use this, its here for unittesting only!!! .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_CIPHERS, "SSL_RSA_EXPORT_WITH_RC4_40_MD5") // WEAK and insecure protocol, do NOT use this, its here for unittesting only!!! @@ -413,35 +424,27 @@ public void testHttpsAndNodeSSLPKCS1Pem() throws Exception { @Test public void testHttpsAndNodeSSLPemEnc() throws Exception { + final String keyPassword = randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH); + final CertificatesRule.PemFiles pem = certificatesRule.generatePemFiles( + "CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE", + keyPassword + ); final MockSecureSettings mockSecureSettings = new MockSecureSettings(); - mockSecureSettings.setString(SECURITY_SSL_HTTP_PEMKEY_PASSWORD.propertyName, "changeit"); - mockSecureSettings.setString(SECURITY_SSL_TRANSPORT_PEMKEY_PASSWORD.propertyName, "changeit"); + mockSecureSettings.setString(SECURITY_SSL_HTTP_PEMKEY_PASSWORD.propertyName, keyPassword); + mockSecureSettings.setString(SECURITY_SSL_TRANSPORT_PEMKEY_PASSWORD.propertyName, keyPassword); final Settings settings = Settings.builder() .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, true) .put(ConfigConstants.SECURITY_SSL_ONLY, true) - .put( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.crt.pem") - ) - .put( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.key") - ) - .put( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") - ) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, pem.cert()) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, pem.key()) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, pem.trustedCasPem()) .put(TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, false) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true) .put(SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE, "REQUIRE") - .put( - SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.crt.pem") - ) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.key")) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH, pem.cert()) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH, pem.key()) .put( SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") @@ -454,45 +457,39 @@ public void testHttpsAndNodeSSLPemEnc() throws Exception { RestHelper rh = restHelper(); rh.enableHTTPClientSSL = true; rh.trustHTTPServerCertificate = true; + rh.customTrustStoreFile = pem.trustStore(); + rh.customTrustStorePassword = pem.trustStorePassword(); rh.sendAdminCertificate = true; String res = rh.executeSimpleRequest("_opendistro/_security/sslinfo?pretty"); Assert.assertTrue(res.contains("TLS")); Assert.assertTrue(rh.executeSimpleRequest("_nodes/settings?pretty").contains(clusterInfo.clustername)); - // Assert.assertTrue(!executeSimpleRequest("_opendistro/_security/sslinfo?pretty").contains("null")); Assert.assertTrue(res.contains("CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE")); } @Test public void testSSLPemEncWithInsecureSettings() throws Exception { + final String keyPassword = randomAsciiAlphanumOfLength(FIPS_MIN_PASSWORD_LENGTH); + final CertificatesRule.PemFiles pem = certificatesRule.generatePemFiles( + "CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE", + keyPassword + ); + final Settings settings = Settings.builder() .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED, true) .put(ConfigConstants.SECURITY_SSL_ONLY, true) - .put( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.crt.pem") - ) - .put( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.key") - ) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMCERT_FILEPATH, pem.cert()) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMKEY_FILEPATH, pem.key()) // legacy insecure passwords - .put(SECURITY_SSL_TRANSPORT_PEMKEY_PASSWORD.insecurePropertyName, "changeit") - .put(SECURITY_SSL_HTTP_PEMKEY_PASSWORD.insecurePropertyName, "changeit") - .put( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") - ) + .put(SECURITY_SSL_TRANSPORT_PEMKEY_PASSWORD.insecurePropertyName, keyPassword) + .put(SECURITY_SSL_HTTP_PEMKEY_PASSWORD.insecurePropertyName, keyPassword) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PEMTRUSTEDCAS_FILEPATH, pem.trustedCasPem()) .put(TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, false) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true) .put(SSLConfigConstants.SECURITY_SSL_HTTP_CLIENTAUTH_MODE, "REQUIRE") - .put( - SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH, - FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.crt.pem") - ) - .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/pem/node-4.key")) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMCERT_FILEPATH, pem.cert()) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_PEMKEY_FILEPATH, pem.key()) .put( SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") @@ -504,6 +501,8 @@ public void testSSLPemEncWithInsecureSettings() throws Exception { RestHelper rh = restHelper(); rh.enableHTTPClientSSL = true; rh.trustHTTPServerCertificate = true; + rh.customTrustStoreFile = pem.trustStore(); + rh.customTrustStorePassword = pem.trustStorePassword(); rh.sendAdminCertificate = true; Assert.assertTrue( @@ -627,6 +626,8 @@ public void testHttpsEnforceFail() throws Exception { @Test public void testHttpsV3Fail() throws Exception { + assumeFalse("SSLv3 is not FIPS-approved", FipsMode.isEnabled()); + thrown.expect(SSLHandshakeException.class); final Settings settings = Settings.builder() @@ -824,6 +825,7 @@ public void testCRLPem() throws Exception { FileHelper.getAbsoluteFilePathFromClassPath("ssl/chain-ca.pem") ) .put(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_VALIDATE, true) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_FILE, FileHelper.getAbsoluteFilePathFromClassPath("ssl/crl/revoked.crl")) .put(SSLConfigConstants.SECURITY_SSL_HTTP_CRL_VALIDATION_DATE, CertificateValidatorTest.CRL_DATE.getTime()) .build(); diff --git a/src/test/java/org/opensearch/security/ssl/SslSettingsManagerReloadListenerTest.java b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerReloadListenerTest.java index 8f02f16613..54745545b5 100644 --- a/src/test/java/org/opensearch/security/ssl/SslSettingsManagerReloadListenerTest.java +++ b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerReloadListenerTest.java @@ -23,6 +23,7 @@ import java.util.concurrent.TimeUnit; import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.awaitility.Awaitility; import org.junit.After; import org.junit.Before; @@ -36,6 +37,9 @@ import org.opensearch.env.Environment; import org.opensearch.env.TestEnvironment; import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.util.BCFipsEntropyDaemonFilter; +import org.opensearch.test.BouncyCastleThreadFilter; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; import org.opensearch.watcher.ResourceWatcherService; @@ -54,6 +58,7 @@ import static org.opensearch.security.ssl.util.SSLConfigConstants.TRUSTSTORE_TYPE; import static org.opensearch.transport.AuxTransport.AUX_TRANSPORT_TYPES_SETTING; +@ThreadLeakFilters(filters = { BouncyCastleThreadFilter.class, BCFipsEntropyDaemonFilter.class }) public class SslSettingsManagerReloadListenerTest extends RandomizedTest { @ClassRule @@ -84,8 +89,8 @@ public void setUp() throws Exception { ); } - static Path path(final String fileName) { - return certificatesRule.configRootFolder().resolve(fileName); + static Path path(final String filename) { + return certificatesRule.configRootFolder().resolve(filename); } @After @@ -144,6 +149,16 @@ private void reloadSslContextOnJdkStoreFilesChangedForTransportType(CertType cer final String keyStorePathSetting = settingPrefix + KEYSTORE_FILEPATH; final String keyStoreTypeSetting = settingPrefix + KEYSTORE_TYPE; final String certTypeFilePrefix = certType.id().toLowerCase(Locale.ROOT); + FileHelper.TypedStore typedTrustStore = FileHelper.resolveStore( + certificatesRule.configRootFolder(), + certTypeFilePrefix + "_truststore", + ".jks" + ); + FileHelper.TypedStore typedKeyStore = FileHelper.resolveStore( + certificatesRule.configRootFolder(), + certTypeFilePrefix + "_keystore", + ".p12" + ); final var keyStorePassword = randomAsciiAlphanumOfLength(10); final var secureSettings = new MockSecureSettings(); secureSettings.setString(settingPrefix + "truststore_password_secure", keyStorePassword); @@ -156,18 +171,18 @@ private void reloadSslContextOnJdkStoreFilesChangedForTransportType(CertType cer // If certType is TRANSPORT the following line will re-enable it. .put(SECURITY_SSL_TRANSPORT_ENABLED, false) .put(enabledSetting, true) - .put(trustStorePathSetting, path(certTypeFilePrefix + "_truststore.jks")) - .put(trustStoreTypeSetting, "jks") - .put(keyStorePathSetting, path(certTypeFilePrefix + "_keystore.p12")) - .put(keyStoreTypeSetting, "pkcs12") + .put(trustStorePathSetting, typedTrustStore.path()) + .put(trustStoreTypeSetting, typedTrustStore.type()) + .put(keyStorePathSetting, typedKeyStore.path()) + .put(keyStoreTypeSetting, typedKeyStore.type()) .setSecureSettings(secureSettings) .build(), (filePrefix, caCertificate, accessKeyAndCertificate) -> { - final var trustStore = KeyStore.getInstance("jks"); + final var trustStore = KeyStore.getInstance(typedTrustStore.type()); trustStore.load(null, null); trustStore.setCertificateEntry("ca", certificatesRule.toX509Certificate(caCertificate)); - writeStore(trustStore, path(String.format("%s_truststore.jks", filePrefix)), keyStorePassword); - final var keyStore = KeyStore.getInstance("pkcs12"); + writeStore(trustStore, typedTrustStore.path(), keyStorePassword); + final var keyStore = KeyStore.getInstance(typedKeyStore.type()); keyStore.load(null, null); keyStore.setKeyEntry( "pk", @@ -175,7 +190,7 @@ private void reloadSslContextOnJdkStoreFilesChangedForTransportType(CertType cer certificatesRule.privateKeyPassword().toCharArray(), new X509Certificate[] { certificatesRule.toX509Certificate(accessKeyAndCertificate.v2()) } ); - writeStore(keyStore, path(String.format("%s_keystore.p12", filePrefix)), keyStorePassword); + writeStore(keyStore, typedKeyStore.path(), keyStorePassword); } ); } diff --git a/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java index 8e4131e173..f5bf5ff259 100644 --- a/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java +++ b/src/test/java/org/opensearch/security/ssl/SslSettingsManagerTest.java @@ -16,6 +16,7 @@ import java.util.Locale; import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -26,6 +27,8 @@ import org.opensearch.env.Environment; import org.opensearch.env.TestEnvironment; import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.util.BCFipsEntropyDaemonFilter; +import org.opensearch.test.BouncyCastleThreadFilter; import io.netty.handler.ssl.ClientAuth; import io.netty.handler.ssl.SslContext; @@ -66,6 +69,7 @@ import static org.opensearch.transport.AuxTransport.AUX_TRANSPORT_TYPES_SETTING; import static org.junit.Assert.assertThrows; +@ThreadLeakFilters(filters = { BouncyCastleThreadFilter.class, BCFipsEntropyDaemonFilter.class }) public class SslSettingsManagerTest extends RandomizedTest { @ClassRule diff --git a/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java index 9a00d1ca59..74efa001d7 100644 --- a/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java +++ b/src/test/java/org/opensearch/security/ssl/config/JdkSslCertificatesLoaderTest.java @@ -25,6 +25,7 @@ import org.opensearch.common.collect.Tuple; import org.opensearch.common.settings.MockSecureSettings; import org.opensearch.env.TestEnvironment; +import org.opensearch.security.support.FipsMode; import static java.util.Objects.isNull; import static org.hamcrest.CoreMatchers.is; @@ -270,7 +271,9 @@ private void testJdkBasedSslConfiguration(final String sslConfigPrefix, final bo } String randomKeyStoreType() { - return randomFrom(new String[] { "jks", "pkcs12", null }); + return FipsMode.isEnabled() + ? randomFrom(new String[] { "bcfks", null }) + : randomFrom(new String[] { "bcfks", "jks", "pkcs12", null }); } String randomKeyStorePassword(final boolean useSecurePassword) { @@ -282,7 +285,7 @@ Path createTrustStore(final String type, final String password, Map SanParser.parse(x509)); assertThat(ex.getCause(), instanceOf(UnknownHostException.class)); @@ -71,7 +71,7 @@ public void badIpSan_fipsMode_throwsRuntimeException() throws Exception { @Test public void badIpSan_nonFipsMode_returnsEmpty() throws Exception { - Assume.assumeFalse(CryptoServicesRegistrar.isInApprovedOnlyMode()); + Assume.assumeFalse(FipsMode.isEnabled()); assertThat(SanParser.parse(buildCertWithBadIpSan()), is("")); } @@ -80,7 +80,7 @@ public void badIpSan_nonFipsMode_returnsEmpty() throws Exception { /** Builds a self-signed cert with a 3-byte iPAddress SAN (invalid: must be 4 or 16 bytes). */ private static X509Certificate buildCertWithBadIpSan() throws Exception { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(1024); + kpg.initialize(2048); KeyPair kp = kpg.generateKeyPair(); X500Name dn = new X500Name("CN=test"); Date now = new Date(); diff --git a/src/test/java/org/opensearch/security/ssl/util/SSLConfigConstantsTest.java b/src/test/java/org/opensearch/security/ssl/util/SSLConfigConstantsTest.java index 9a8da523d8..f6e5fda9bb 100644 --- a/src/test/java/org/opensearch/security/ssl/util/SSLConfigConstantsTest.java +++ b/src/test/java/org/opensearch/security/ssl/util/SSLConfigConstantsTest.java @@ -16,6 +16,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.security.ssl.config.CertType; +import org.opensearch.security.support.FipsMode; import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS; import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS; @@ -26,13 +27,19 @@ public class SSLConfigConstantsTest { @Test public void testDefaultTLSProtocols() { final var tlsDefaultProtocols = SSLConfigConstants.getSecureSSLProtocols(Settings.EMPTY, CertType.TRANSPORT); - assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1" }, tlsDefaultProtocols); + final var expected = FipsMode.isEnabled() + ? new String[] { "TLSv1.3", "TLSv1.2" } + : new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + assertArrayEquals(expected, tlsDefaultProtocols); } @Test public void testDefaultSSLProtocols() { final var sslDefaultProtocols = SSLConfigConstants.getSecureSSLProtocols(Settings.EMPTY, CertType.HTTP); - assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1" }, sslDefaultProtocols); + final var expected = FipsMode.isEnabled() + ? new String[] { "TLSv1.3", "TLSv1.2" } + : new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; + assertArrayEquals(expected, sslDefaultProtocols); } @Test diff --git a/src/test/java/org/opensearch/security/support/FipsModeTest.java b/src/test/java/org/opensearch/security/support/FipsModeTest.java new file mode 100644 index 0000000000..06f5ebe6ad --- /dev/null +++ b/src/test/java/org/opensearch/security/support/FipsModeTest.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ +package org.opensearch.security.support; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.assertThrows; + +public class FipsModeTest { + + private java.util.function.Supplier originalSupplier; + + @Before + public void saveSupplier() { + originalSupplier = FipsMode.envSupplier; + } + + @After + public void restoreSupplier() { + FipsMode.envSupplier = originalSupplier; + } + + @Test + public void isEnabled_isTrueOnlyForCaseInsensitiveTrueValue() { + for (String enabled : new String[] { "true", "TRUE", "True" }) { + FipsMode.envSupplier = () -> enabled; + assertThat("expected enabled for: " + enabled, FipsMode.isEnabled(), equalTo(true)); + } + for (String disabled : new String[] { "false", null, "", "yes" }) { + FipsMode.envSupplier = () -> disabled; + assertThat("expected disabled for: " + disabled, FipsMode.isEnabled(), equalTo(false)); + } + } + + @Test + public void constructor_isNotInstantiable() throws Exception { + Constructor constructor = FipsMode.class.getDeclaredConstructor(); + constructor.setAccessible(true); + Exception ex = assertThrows(InvocationTargetException.class, constructor::newInstance); + assertThat(ex.getCause(), instanceOf(UnsupportedOperationException.class)); + } +} diff --git a/src/test/java/org/opensearch/security/support/PemKeyReaderDetectStoreTypeTest.java b/src/test/java/org/opensearch/security/support/PemKeyReaderDetectStoreTypeTest.java index 133d8b400c..9196ba5ca3 100644 --- a/src/test/java/org/opensearch/security/support/PemKeyReaderDetectStoreTypeTest.java +++ b/src/test/java/org/opensearch/security/support/PemKeyReaderDetectStoreTypeTest.java @@ -15,14 +15,11 @@ import java.io.FileOutputStream; import java.io.UncheckedIOException; import java.security.KeyStore; -import java.security.Security; import org.apache.lucene.tests.util.LuceneTestCase; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.bouncycastle.crypto.CryptoServicesRegistrar; -import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -33,25 +30,19 @@ public class PemKeyReaderDetectStoreTypeTest extends LuceneTestCase { - static { - if (Security.getProvider("BCFIPS") == null) { - Security.addProvider(new BouncyCastleFipsProvider()); - } - } - @Rule public TemporaryFolder tempDir = new TemporaryFolder(); @Test public void detectsJks() throws Exception { - assumeFalse("JKS truststores are not supported in FIPS mode", CryptoServicesRegistrar.isInApprovedOnlyMode()); + assumeFalse("JKS truststores are not supported in FIPS mode", FipsMode.isEnabled()); File file = storeFile("JKS"); assertThat(PemKeyReader.extractStoreType(file.getAbsolutePath(), null), equalTo(PemKeyReader.JKS)); } @Test public void detectsPkcs12() throws Exception { - assumeFalse("PKCS12 truststores are not supported in FIPS mode", CryptoServicesRegistrar.isInApprovedOnlyMode()); + assumeFalse("PKCS12 truststores are not supported in FIPS mode", FipsMode.isEnabled()); File file = storeFile("PKCS12"); assertThat(PemKeyReader.extractStoreType(file.getAbsolutePath(), null), equalTo(PemKeyReader.PKCS12)); } @@ -65,7 +56,7 @@ public void detectsBcfks() throws Exception { @Test public void explicitTypeSkipsDetection() throws Exception { // file content is irrelevant when type is explicitly provided - assumeFalse("PKCS12 truststores are not supported in FIPS mode", CryptoServicesRegistrar.isInApprovedOnlyMode()); + assumeFalse("PKCS12 truststores are not supported in FIPS mode", FipsMode.isEnabled()); File file = tempDir.newFile("irrelevant.bin"); try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(new byte[] { 0x00, 0x01, 0x02, 0x03 }); diff --git a/src/test/java/org/opensearch/security/support/PemKeyReaderLoadKeyStoreTest.java b/src/test/java/org/opensearch/security/support/PemKeyReaderLoadKeyStoreTest.java new file mode 100644 index 0000000000..ac44f4f0e3 --- /dev/null +++ b/src/test/java/org/opensearch/security/support/PemKeyReaderLoadKeyStoreTest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.support; + +import java.io.File; +import java.io.FileOutputStream; +import java.security.KeyStore; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.opensearch.OpenSearchException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +public class PemKeyReaderLoadKeyStoreTest { + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + + @Test + public void returnsNullWhenPathIsNullAndTypeIsNotPkcs11() throws Exception { + assertNull(PemKeyReader.loadKeyStore(null, null, PemKeyReader.BCFKS)); + } + + @Test + public void loadsBcfksStore() throws Exception { + File file = storeFile(); + KeyStore ks = PemKeyReader.loadKeyStore(file.getAbsolutePath(), null, PemKeyReader.BCFKS); + assertThat(ks, notNullValue()); + assertThat(ks.getType(), equalTo(PemKeyReader.BCFKS)); + } + + @Test + public void pkcs11ThrowsDescriptiveExceptionWhenProviderUnconfigured() { + // In a standard test environment there is no PKCS#11 token configured, + // so getInstance or load will fail — verify our message wraps it. + OpenSearchException ex = assertThrows(OpenSearchException.class, () -> PemKeyReader.loadKeyStore(null, null, PemKeyReader.PKCS11)); + assertThat(ex.getMessage(), containsString("Failed to initialize PKCS#11 keystore")); + assertThat(ex.getCause(), notNullValue()); + assertThat(ex.getCause().getMessage(), containsString("PKCS11 not found")); + } + + private File storeFile() throws Exception { + File file = tempDir.newFile("store.bcfks"); + KeyStore ks = KeyStore.getInstance("BCFKS"); + ks.load(null, null); + try (FileOutputStream fos = new FileOutputStream(file)) { + ks.store(fos, new char[0]); + } + return file; + } +} diff --git a/src/test/java/org/opensearch/security/support/PemKeyReaderLoadSecretKeyTest.java b/src/test/java/org/opensearch/security/support/PemKeyReaderLoadSecretKeyTest.java new file mode 100644 index 0000000000..707f9209cf --- /dev/null +++ b/src/test/java/org/opensearch/security/support/PemKeyReaderLoadSecretKeyTest.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.support; + +import java.nio.charset.StandardCharsets; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.util.BCFipsEntropyDaemonFilter; +import org.opensearch.test.BouncyCastleThreadFilter; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThrows; + +@RunWith(RandomizedRunner.class) +@ThreadLeakFilters(filters = { BouncyCastleThreadFilter.class, BCFipsEntropyDaemonFilter.class }) +public class PemKeyReaderLoadSecretKeyTest { + + private static final SecretKey SECRET_KEY = new SecretKeySpec( + "unit-test-hmac-key-256-bits!!!!!".getBytes(StandardCharsets.US_ASCII), + "HmacSHA256" + ); + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + + @Test + public void loadsSecretKeyByAlias() throws Exception { + FileHelper.TypedStore typedStore = FileHelper.storeSecretKey(tempDir, "test-key", SECRET_KEY, "kspass", "keypass"); + SecretKey loaded = PemKeyReader.loadSecretKeyFromKeystore( + typedStore.path().toString(), + "kspass", + typedStore.type(), + "test-key", + "keypass" + ); + assertThat(loaded, notNullValue()); + assertThat(loaded.getEncoded(), equalTo(SECRET_KEY.getEncoded())); + } + + @Test + public void keyPasswordFallsBackToKeystorePassword() throws Exception { + FileHelper.TypedStore typedStore = FileHelper.storeSecretKey(tempDir, "test-key", SECRET_KEY, "kspass", "kspass"); + SecretKey loaded = PemKeyReader.loadSecretKeyFromKeystore( + typedStore.path().toString(), + "kspass", + typedStore.type(), + "test-key", + null + ); + assertThat(loaded.getEncoded(), equalTo(SECRET_KEY.getEncoded())); + } + + @Test + public void throwsWhenPathIsNull() { + var storeType = FileHelper.randomKeyStoreType(); + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> PemKeyReader.loadSecretKeyFromKeystore(null, "kspass", storeType, "test-key", "keypass") + ); + assertThat(ex.getMessage(), containsString("Failed to load secret key from keystore-type ")); + assertThat(ex.getMessage(), containsString(storeType)); + assertThat(ex.getMessage(), containsString("at path 'null'")); + } + + @Test + public void throwsForNonexistentAlias() throws Exception { + FileHelper.TypedStore typedStore = FileHelper.storeSecretKey(tempDir, "test-key", SECRET_KEY, "kspass", "keypass"); + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> PemKeyReader.loadSecretKeyFromKeystore( + typedStore.path().toString(), + "kspass", + typedStore.type(), + "no-such-alias", + "keypass" + ) + ); + assertThat(ex.getMessage(), containsString("No key found at alias")); + assertThat(ex.getMessage(), containsString("no-such-alias")); + } + + @Test + public void throwsForWrongKeystorePassword() throws Exception { + FileHelper.TypedStore typedStore = FileHelper.storeSecretKey(tempDir, "test-key", SECRET_KEY, "kspass", "keypass"); + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> PemKeyReader.loadSecretKeyFromKeystore( + typedStore.path().toString(), + "wrongkspass", + typedStore.type(), + "test-key", + "keypass" + ) + ); + assertThat(ex.getMessage(), containsString("Failed to load secret key from keystore-type ")); + assertThat(ex.getMessage(), containsString(typedStore.type())); + assertThat(ex.getMessage(), containsString(typedStore.path().toString())); + } + + @Test + public void throwsForWrongKeyPassword() throws Exception { + FileHelper.TypedStore typedStore = FileHelper.storeSecretKey(tempDir, "test-key", SECRET_KEY, "kspass", "keypass"); + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> PemKeyReader.loadSecretKeyFromKeystore( + typedStore.path().toString(), + "kspass", + typedStore.type(), + "test-key", + "wrongkeypass" + ) + ); + assertThat(ex.getMessage(), containsString("Failed to load secret key from keystore-type ")); + assertThat(ex.getMessage(), containsString(typedStore.type())); + assertThat(ex.getMessage(), containsString(typedStore.path().toString())); + } + + @Test + public void autoDetectsStoreTypeFromFileContentWhenTypeIsNull() throws Exception { + FileHelper.TypedStore typedStore = FileHelper.storeSecretKey(tempDir, "test-key", SECRET_KEY, "kspass", "keypass"); + SecretKey loaded = PemKeyReader.loadSecretKeyFromKeystore(typedStore.path().toString(), "kspass", null, "test-key", "keypass"); + assertThat(loaded, notNullValue()); + assertThat(loaded.getEncoded(), equalTo(SECRET_KEY.getEncoded())); + } + + @Test + public void throwsWhenAliasHoldsNonSecretKey() { + String ksPath = getClass().getClassLoader().getResource("kirk-keystore.bcfks").getPath(); + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> PemKeyReader.loadSecretKeyFromKeystore(ksPath, "changeit", "BCFKS", "kirk", "changeit") + ); + assertThat(ex.getMessage(), containsString("kirk")); + assertThat(ex.getMessage(), containsString("is not a SecretKey")); + } +} diff --git a/src/test/java/org/opensearch/security/test/AbstractSecurityUnitTest.java b/src/test/java/org/opensearch/security/test/AbstractSecurityUnitTest.java index 86dbf15493..5a4f8555dc 100644 --- a/src/test/java/org/opensearch/security/test/AbstractSecurityUnitTest.java +++ b/src/test/java/org/opensearch/security/test/AbstractSecurityUnitTest.java @@ -83,10 +83,12 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.ssl.util.SSLConfigConstants; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.test.helper.cluster.ClusterHelper; import org.opensearch.security.test.helper.cluster.ClusterInfo; import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.test.helper.file.FipsHashAdapter; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; import org.opensearch.security.test.helper.rules.SecurityTestWatcher; import org.opensearch.threadpool.ThreadPool; @@ -143,10 +145,10 @@ public abstract class AbstractSecurityUnitTest extends RandomizedTest { public final TestWatcher testWatcher = new SecurityTestWatcher(); public static Header encodeBasicHeader(final String username, final String password) { + final String adaptedPassword = FipsHashAdapter.adaptPassword(Objects.requireNonNull(password)); return new BasicHeader( "Authorization", - "Basic " - + Base64.getEncoder().encodeToString((username + ":" + Objects.requireNonNull(password)).getBytes(StandardCharsets.UTF_8)) + "Basic " + Base64.getEncoder().encodeToString((username + ":" + adaptedPassword).getBytes(StandardCharsets.UTF_8)) ); } @@ -167,7 +169,7 @@ protected RestHighLevelClient getRestClient( var typedKeyStore = FileHelper.resolveStore(prefix + keyStoreName); File keyStoreFile = typedKeyStore.path().toFile(); KeyStore keyStore = KeyStore.getInstance(typedKeyStore.type()); - keyStore.load(new FileInputStream(keyStoreFile), null); + keyStore.load(new FileInputStream(keyStoreFile), "changeit".toCharArray()); sslContextBuilder.loadKeyMaterial(keyStore, "changeit".toCharArray()); var typedTrustStore = FileHelper.resolveStore(prefix + trustStoreName); @@ -179,11 +181,13 @@ protected RestHighLevelClient getRestClient( SSLContext sslContext = sslContextBuilder.build(); HttpHost httpHost = new HttpHost("https", info.httpHost, info.httpPort); - + String[] tlsVersions = FipsMode.isEnabled() + ? new String[] { "TLSv1.2", "TLSv1.3" } + : new String[] { "TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3" }; RestClientBuilder restClientBuilder = RestClient.builder(httpHost).setHttpClientConfigCallback(builder -> { TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() .setSslContext(sslContext) - .setTlsVersions(new String[] { "TLSv1", "TLSv1.1", "TLSv1.2", "SSLv3" }) + .setTlsVersions(tlsVersions) .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 .setTlsDetailsFactory(new Factory() { @@ -311,6 +315,10 @@ protected Settings.Builder minimumSecuritySettingsBuilder(int node, boolean sslO builder.put(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, false); } builder.put("cluster.routing.allocation.disk.threshold_enabled", false); + + if (FipsMode.isEnabled()) { + builder.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.PBKDF2); + } builder.put(other); return builder; } diff --git a/src/test/java/org/opensearch/security/test/helper/cluster/ClusterHelper.java b/src/test/java/org/opensearch/security/test/helper/cluster/ClusterHelper.java index 6ada07addd..c11272ca22 100644 --- a/src/test/java/org/opensearch/security/test/helper/cluster/ClusterHelper.java +++ b/src/test/java/org/opensearch/security/test/helper/cluster/ClusterHelper.java @@ -63,6 +63,9 @@ import org.opensearch.http.HttpInfo; import org.opensearch.node.Node; import org.opensearch.node.PluginAwareNode; +import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.AbstractSecurityUnitTest; import org.opensearch.security.test.NodeSettingsSupplier; import org.opensearch.security.test.SingleClusterTest; @@ -206,6 +209,7 @@ public final synchronized ClusterInfo startCluster( tcpPortsAllIt.next(), httpPortsIt.next() ); + applyFipsPasswordHashing(nodeSettingsBuilder, setting); final Settings settingsForNode; if (nodeSettingsSupplier != null) { final Settings suppliedSettings = nodeSettingsSupplier.get(nodeNum); @@ -244,6 +248,7 @@ public void run() { tcpPortsAllIt.next(), httpPortsIt.next() ); + applyFipsPasswordHashing(nodeSettingsBuilder, setting); final Settings settingsForNode; if (nodeSettingsSupplier != null) { final Settings suppliedSettings = nodeSettingsSupplier.get(nodeNum); @@ -466,7 +471,7 @@ private Settings.Builder getMinimumNonSecurityNodeSettingsBuilder( int httpPort ) { - return AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), isClusterManagerNode, isDataNode) + Settings.Builder builder = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), isClusterManagerNode, isDataNode) .put("node.name", "node_" + clustername + "_num" + nodenum) .put("cluster.name", clustername) .put("path.data", "./target/data/" + clustername + "/data") @@ -483,6 +488,14 @@ private Settings.Builder getMinimumNonSecurityNodeSettingsBuilder( .put("http.cors.enabled", true) .put("path.home", "./target") .put("bootstrap.serial_filter", true); + + return builder; + } + + private static void applyFipsPasswordHashing(final Settings.Builder builder, final NodeSettings setting) { + if (FipsMode.isEnabled() && setting.getPlugins().contains(OpenSearchSecurityPlugin.class)) { + builder.put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.PBKDF2); + } } private enum ClusterState { diff --git a/src/test/java/org/opensearch/security/test/helper/file/FileHelper.java b/src/test/java/org/opensearch/security/test/helper/file/FileHelper.java index b19042dee4..9af226afe4 100644 --- a/src/test/java/org/opensearch/security/test/helper/file/FileHelper.java +++ b/src/test/java/org/opensearch/security/test/helper/file/FileHelper.java @@ -29,6 +29,7 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; @@ -43,11 +44,13 @@ import java.nio.file.Paths; import java.security.KeyStore; import java.util.List; +import java.util.Locale; import java.util.Map; +import javax.crypto.SecretKey; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.junit.rules.TemporaryFolder; import org.opensearch.common.io.Streams; import org.opensearch.common.xcontent.XContentFactory; @@ -56,8 +59,10 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.support.FipsMode; import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static com.carrotsearch.randomizedtesting.RandomizedTest.randomFrom; public class FileHelper { @@ -89,6 +94,37 @@ public static String inferStoreType(String filePath) { .orElseThrow(() -> new IllegalArgumentException("Unknown keystore type for file path: " + filePath)); } + /** Writes a single {@link SecretKey} entry into a new keystore file of the given type and returns it. */ + public static TypedStore storeSecretKey(TemporaryFolder tempDir, String alias, SecretKey key, String storePassword, String keyPassword) + throws Exception { + final String storeType = randomKeyStoreType(); + final File file = tempDir.newFile(alias + "." + storeType.toLowerCase(Locale.ROOT)); + final KeyStore ks = KeyStore.getInstance(storeType); + ks.load(null, null); + ks.setKeyEntry(alias, key, keyPassword.toCharArray(), null); + try (FileOutputStream fos = new FileOutputStream(file)) { + ks.store(fos, storePassword.toCharArray()); + } + return new TypedStore(file.toPath(), storeType); + } + + /** + * Returns a randomly chosen keystore type that supports {@link SecretKey} entries. + *
    + *
  • JKS is excluded: its {@code engineSetKeyEntry} enforces {@code instanceof PrivateKey} (the + * asymmetric-key interface), so {@code SecretKey} entries are rejected with "Cannot store + * non-PrivateKeys".
  • + *
  • JCEKS was introduced specifically to extend JKS with {@code SecretKey} support.
  • + *
  • BCFKS (BC FIPS) also supports {@code SecretKey}.
  • + *
+ * Requires the calling test to run under {@code @RunWith(RandomizedRunner.class)}. + */ + public static String randomKeyStoreType() { + return FipsMode.isEnabled() // + ? randomFrom(new String[] { "bcfks" }) // + : randomFrom(new String[] { "bcfks", "jceks", "pkcs12" }); + } + public record TypedStore(Path path, String type) { } @@ -107,7 +143,7 @@ public static KeyStore getKeystoreFromClassPath(final String baseName, String pa *

* The format is chosen based on the runtime environment: *

    - *
  • FIPS approved-only mode ({@link CryptoServicesRegistrar#isInApprovedOnlyMode()}) → + *
  • FIPS mode ({@link FipsMode#isEnabled()}) → * {@code .bcfks} / {@code "BCFKS"}
  • *
  • Non-FIPS → {@code .jks} / {@code "JKS"} if a JKS variant exists on the classpath, * otherwise {@code .p12} / {@code "PKCS12"}
  • @@ -118,7 +154,7 @@ public static KeyStore getKeystoreFromClassPath(final String baseName, String pa * @throws IllegalStateException if no matching file is found on the classpath */ public static TypedStore resolveStore(final String baseName) { - if (CryptoServicesRegistrar.isInApprovedOnlyMode()) { + if (FipsMode.isEnabled()) { return new TypedStore(getAbsoluteFilePathFromClassPath(baseName + ".bcfks"), "BCFKS"); } if (classpathResourceExists(baseName + ".jks")) { @@ -127,6 +163,14 @@ public static TypedStore resolveStore(final String baseName) { return new TypedStore(getAbsoluteFilePathFromClassPath(baseName + ".p12"), "PKCS12"); } + public static TypedStore resolveStore(final Path dir, final String baseName, final String nonFipsExtension) { + if (FipsMode.isEnabled()) { + return new TypedStore(dir.resolve(baseName + ".bcfks"), "BCFKS"); + } + Path path = dir.resolve(baseName + nonFipsExtension); + return new TypedStore(path, inferStoreType(path)); + } + public static boolean classpathResourceExists(final String name) { return FileHelper.class.getClassLoader().getResource(name) != null; } @@ -158,7 +202,11 @@ public static BytesReference readYamlContent(final String file) { XContentParser parser = null; try { parser = XContentType.YAML.xContent() - .createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, new StringReader(loadFile(file))); + .createParser( + NamedXContentRegistry.EMPTY, + THROW_UNSUPPORTED_OPERATION, + new StringReader(FipsHashAdapter.adaptConfig(loadFile(file))) + ); parser.nextToken(); final XContentBuilder builder = XContentFactory.jsonBuilder(); builder.copyCurrentStructure(parser); @@ -181,7 +229,11 @@ public static BytesReference readYamlContentFromString(final String yaml) { XContentParser parser = null; try { parser = XContentType.YAML.xContent() - .createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, new StringReader(yaml)); + .createParser( + NamedXContentRegistry.EMPTY, + THROW_UNSUPPORTED_OPERATION, + new StringReader(FipsHashAdapter.adaptConfig(yaml)) + ); parser.nextToken(); final XContentBuilder builder = XContentFactory.jsonBuilder(); builder.copyCurrentStructure(parser); diff --git a/src/test/java/org/opensearch/security/test/helper/file/FipsHashAdapter.java b/src/test/java/org/opensearch/security/test/helper/file/FipsHashAdapter.java new file mode 100644 index 0000000000..8868f22b54 --- /dev/null +++ b/src/test/java/org/opensearch/security/test/helper/file/FipsHashAdapter.java @@ -0,0 +1,149 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.test.helper.file; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.hasher.PasswordHasher; +import org.opensearch.security.hasher.PasswordHasherFactory; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; + +/** + * Test-only helper that rewrites the static bcrypt fixtures and their short demo passwords into + * FIPS-legal PBKDF2 form, applying the same padding to both the stored hash and the sent password. + * No-op outside FIPS. + */ +public final class FipsHashAdapter { + + private FipsHashAdapter() {} + + /** Suffix appended to fixture passwords under FIPS so they clear the 14-char (112-bit) PBKDF2 floor. */ + static final String FIPS_PASSWORD_PADDING = "_fips_pw_padding"; + + /** Fixture bcrypt hash -> the plaintext it encodes, covering every user any test logs in as. */ + private static final Map BCRYPT_HASH_TO_PLAINTEXT = Map.ofEntries( + // top-level internal_users.yml and the various /internal_users.yml fixtures + Map.entry("$2a$04$idGSEpNOhFbyiRL6toGPT.orh7ENOEU8kAqwkRFaXWRdA6wVgyqUu", "user_b"), + Map.entry("$2a$04$jQcEXpODnTFoGDuA7DPdSevA84CuH/7MOYkb80M3XZIrH76YMWS9G", "user_c"), + Map.entry("$2a$04$NDy7mGbRNrmPMh9nSnIB.OTMFkcioEd69A04ReSGkJDd7QHxnCcVC", "user_a"), + Map.entry("$2a$12$4AcgAt3xwOWadA5s5blL6ev39OXDNhmOesEoo33eZtrq2N0YrU3H.", "kibanaserver"), + Map.entry("$2a$12$61vXe3cXy32p0cjsW0Y/SeZa7kEVSWuQK0jg98D9d5zOGXfo5NgyC", "crusherw"), + Map.entry("$2a$12$6.4Y6L//xeKQ7t8YEG0s6OH4F4q9gMw0J8E0GjmUMNZeyIWu1IRWS", "user_role01_role02_role03"), + Map.entry("$2a$12$A41IxPXV1/Dx46C6i1ufGubv.p3qYX7xVcY46q33sylYbIqQVwTMu", "worf"), + Map.entry("$2a$12$bP0CO5d5nhmaTOj7mGteHugXQQ8jlSV0dxcl5//moZ1xnI.pVPXfe", "abc:abc"), + Map.entry("$2a$12$GI9JXffO3WUjTsU7Yy3E4.LBxC2ILo66Zg/rr79BpikSL2IIRezQa", "spock"), + Map.entry("$2a$12$Ioo1uXmH.Nq/lS5dUVBEsePSmZ5pSIpVO/xKHaquU/Jvq97I7nAgG", "sarek"), + Map.entry("$2a$12$JU2QjYVTlI24Q/enEOpf2uTLCPGchN.eXWCsrBiieUcRoeh53NB0y", "restoreuser"), + Map.entry("$2a$12$LZvbDVnegkTbEFTu9hHnWO4HIrdB9rGaKcEOID5n0VV4j58cnvyZ.", "writer"), + Map.entry("$2a$12$n5nubfWATfQjSYHiWtUyeOxMIxFInUHOAx8VMmGmxFNPGpaBmeB.m", "nagilum"), + Map.entry("$2a$12$P.QbiwOsnxgz7kLBT10F7u6GhY7//Keyz7Xwf7lNzskRxpo9.zxFS", "theindexadmin"), + Map.entry("$2a$12$wkY2BsRneCU5za1OPYlzsehQit6gu2vprVv/4jHiSEEBv2ThunaTS", "picard"), + Map.entry("$2a$12$XrBfLQh2T8wIzpxE5vzhUOPjjGfONcD8UEjd5IT5KveG8ULZaj04.", "user_role01"), + Map.entry("$2a$12$xZOcnwYPYQ3zIadnlQIJ0eNhX1ngwMkTN.oMwkKxoGvDVPn4/6XtO", "kirk"), + Map.entry("$2a$12$9Zr4IgoJRqK6xJq4xjoa6OZAnY4QOQ6xIhcCxeYoQtB/HriMkeJSC", "dlsnoinvest"), + Map.entry("$2a$12$VcCDgh2NDk07JGN0rjGbM.Ad41qVR/YFJcgHp0UGns5JDymv..TOG", "admin"), + Map.entry("$2a$12$1HqHxm3QTfzwkse7vwzhFOV4gDv787cZ8BwmCwNEyJhn0CZoo8VVu", "test"), + // FLS/DLS caching + indexing fixtures (dlsfls/, cache/) + Map.entry("$2a$12$7QIoVBGdO41qSCNoecU3L.yyXb9vGrCvEtVlpnC4oWLt/q0AsAN52", "hr_employee"), + Map.entry("$2a$12$JJSXNfTowz7Uu5ttXfeYpeYE0arACvcwlPBStB1F.MI7f0U9Z4DGC", "kibanaro"), + Map.entry("$2a$12$YCBrpxYyFusK609FurY5Ee3BlmuzWw0qHwpwqEyNhM2.XnQY3Bxpe", "password"), + Map.entry("$2y$12$SP9z.rBgEHTlueKkiqSK/OxqB2PLJN/eRoNJ8WOPoHWIpirvbFAAy", "password"), + Map.entry("$2a$12$30rb6oabnodiSdysdWJnhO.4sVRkyNudPC1woYCJFhXja3rkyXbam", "dlsflsuser"), + Map.entry("$2a$12$Kv.4sU5r1zy2ZqnSDm99Ae6ImCMKtjJq4enT.9d3c55cA0O2LGNH6", "finance_employee"), + Map.entry("$2a$12$c26Pnq6yiZcgi8PxNEyp5O3wIn1G1eJfCvJFifEKosQJeojUZf/D6", "finance_trainee"), + Map.entry("$2a$12$LRNG7ETwMcO68VNh14B3AuKPkvOaC0k26.QnSrv9AvbmT1JRNMJum", "hr.employee"), + Map.entry("$2a$12$s6rC7o345lvXp.JpTrA91O6xYAGVCCxKdVclsNkWJTaquW4GK9E9u", "hr_trainee"), + Map.entry("$2a$12$gxsE8oEicXy3mNBkGEO5K.P3J/CDq3GHXDYeQTmVI/v3AA84vqIXm", "no_roles"), + Map.entry("$2a$12$6sre4JH7O4Rgh7ubWmeyWus6UIIA13MqW8eR8KD5Qbxn06CDbJG/G", "snapshotrestore"), + // rest-api admin fixtures (restapi/) + Map.entry("$2y$12$ft8tXtxb.dyO/5MrDXHLc.e1o3dktEQJMvR2e.sgVDyD/gR7G9dLS", "admin_all_access"), + Map.entry("$2y$12$W5AdCO/j08KiDu7EF/1Zf.nkcQM/7s.TtAdN2pRpbDM31xXcIIJUq", "rest_api_admin_allowlist"), + Map.entry("$2y$12$xFUIepz0vILRMzMkZMGY1Ow1P1eJo8TJ2oGiaFXaenGrOMsmDnKZS", "rest_api_admin_nodesdn"), + Map.entry("$2y$12$X5ZamIheHYc2bihGTbK66Oe1.1vJ19akH0OFGF7TvI2BhbbED.KcO", "rest_api_admin_user"), + Map.entry("$2y$12$aHkyhk95XbrMCByYYVAlrek1thXpTDuVKJW01vdLYPh6kyR36j7x6", "user_rest_api_access"), + Map.entry("$2y$12$xgJfGiHpYOkRpF9W9dXYZOpJJ4bHz3VTwdv7ZZYTwlvx7NbH62qUi", "user_tenant_parameters_substitution"), + Map.entry("$2y$12$capXg1HNP49Vxeb6ijzRnu5BLMUE0ZePq1l3MhF8tjnuxg614uaY6", "rest_api_admin_config_update"), + Map.entry("$2y$12$pUn1a6jdIeR.stkvEqNe5uK3rOY7Dj3uQfE8Cvd2bjNjTQ2HbsBMK", "rest_api_admin_internalusers"), + Map.entry("$2y$12$BR.CBsElNLj8v2dzpHJ7bOKVLwWKWjKDhlEvBIvAe9b6/m0xWy2Bq", "rest_api_admin_roles"), + Map.entry("$2y$12$irI4k0eKE8z9OXEd1jO4eeQfPV8WRMfttzutAhEeRBWy5XNXOlpr.", "rest_api_admin_ssl_info"), + Map.entry("$2y$12$DxNdaBBMvTq5wO5XlnwlTeGSaC7yNoFoJt2N5TVtraopxPnGjMol2", "rest_api_admin_ssl_reloadcerts"), + Map.entry("$2y$12$q05T7m7DFtkLLj.MVJ6jjuZkAywG4ZwaNi9fiYn6XCJelN2TUXCy2", "rest_api_admin_tenants"), + // index-pattern / protected-indices / system-indices fixtures + Map.entry("$2y$12$93KcWlQxeify28LSx8EjYOnHv1AQ6vJZXSRUVTnfSN7AxTfvCBfu.", "indexAccessNoRoleUser"), + Map.entry("$2y$12$LavzCpUFiFwXD22rc0n.SOVExdnzDcn6lHY48XKPl6KKdvAHm0awm", "protectedIndexUser"), + Map.entry("$2y$12$15ZXoaH/sB.0nESo6VABt.V02HkpA2lQ5QvIFcVqNelUoAdXv1g3O", "negated_regex_user"), + Map.entry("$2y$12$eaSv29maDe1Y0FQXMHi1legXq8Ec/YbWujMq5Mg2RhYZEc9Pzw19y", "negative_lookahead_user"), + Map.entry("$2y$12$d1ONiqarfTF9xOuqKeNukeze1bVBXC1FyXJSpuC3B6/Ekbfu3ULHm", "normal_user"), + Map.entry("$2y$12$V3.ACgUpHP9TlSbV3CNekOGB1NVov1C6Rq3QtXWCvACKVeQnkBCgG", "normal_user_without_system_index") + ); + + /** Distinct plaintext passwords the fixtures use; only these are padded on the credential side. */ + private static final Set KNOWN_PLAINTEXTS = Set.copyOf(BCRYPT_HASH_TO_PLAINTEXT.values()); + + private static volatile Map bcryptToPbkdf2; + + /** Pads a known fixture password to FIPS-legal length under FIPS; leaves everything else untouched. */ + public static String adaptPassword(final String password) { + if (password == null || !FipsMode.isEnabled() || !KNOWN_PLAINTEXTS.contains(password)) { + return password; + } + return password + FIPS_PASSWORD_PADDING; + } + + /** Rewrites known bcrypt fixture hashes to PBKDF2 of the padded plaintext under FIPS; no-op otherwise. */ + public static String adaptConfig(final String content) { + if (content == null || content.isEmpty() || !FipsMode.isEnabled()) { + return content; + } + String adapted = content; + for (final Map.Entry replacement : pbkdf2Replacements().entrySet()) { + if (adapted.contains(replacement.getKey())) { + adapted = adapted.replace(replacement.getKey(), replacement.getValue()); + } + } + return adapted; + } + + /** Rewrites a config file's hashes to PBKDF2 in place under FIPS, for init paths that read it from disk. */ + public static void adaptConfigFile(final File file) throws IOException { + Files.writeString(file.toPath(), adaptConfig(Files.readString(file.toPath(), StandardCharsets.UTF_8)), StandardCharsets.UTF_8); + } + + /** Lazily builds the {@code bcrypt -> PBKDF2} table using the FIPS node's default PBKDF2 params. */ + private static Map pbkdf2Replacements() { + Map local = bcryptToPbkdf2; + if (local == null) { + synchronized (FipsHashAdapter.class) { + local = bcryptToPbkdf2; + if (local == null) { + final PasswordHasher hasher = PasswordHasherFactory.createPasswordHasher( + Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, ConfigConstants.PBKDF2).build() + ); + final Map table = new HashMap<>(); + for (final Map.Entry entry : BCRYPT_HASH_TO_PLAINTEXT.entrySet()) { + table.put(entry.getKey(), hasher.hash(adaptPassword(entry.getValue()).toCharArray())); + } + bcryptToPbkdf2 = local = Map.copyOf(table); + } + } + } + return local; + } +} diff --git a/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java b/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java index 9127c5406d..5ab90321eb 100644 --- a/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java +++ b/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java @@ -85,6 +85,7 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.test.helper.cluster.ClusterInfo; import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.security.test.helper.file.FipsHashAdapter; import tools.jackson.databind.JsonNode; @@ -100,6 +101,8 @@ public class RestHelper { public boolean enableHTTPClientSSLv3Only = false; public boolean sendAdminCertificate = false; public boolean trustHTTPServerCertificate = true; + public FileHelper.TypedStore customTrustStoreFile = null; + public String customTrustStorePassword = null; public boolean sendHTTPClientCredentials = false; public String keystore = "node-0-keystore"; public final String prefix; @@ -319,7 +322,10 @@ protected final CloseableHttpAsyncClient getHTTPClient() throws Exception { final HttpAsyncClientBuilder hcb = HttpAsyncClients.custom(); if (sendHTTPClientCredentials) { - UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("sarek", "sarek".toCharArray()); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( + "sarek", + FipsHashAdapter.adaptPassword("sarek").toCharArray() + ); BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(new AuthScope(null, -1), credentials); hcb.setDefaultCredentialsProvider(credentialsProvider); @@ -339,8 +345,15 @@ protected final CloseableHttpAsyncClient getHTTPClient() throws Exception { String keystoreDir = ksParent != null ? ksParent + "/" : ""; var resolvedTrustStore = FileHelper.resolveStore(keystoreDir + "truststore"); - final KeyStore myTrustStore = KeyStore.getInstance(resolvedTrustStore.type()); - myTrustStore.load(new FileInputStream(resolvedTrustStore.path().toFile()), "changeit".toCharArray()); + final KeyStore myTrustStore; + if (customTrustStoreFile != null) { + myTrustStore = KeyStore.getInstance(customTrustStoreFile.type()); + char[] tsPass = customTrustStorePassword != null ? customTrustStorePassword.toCharArray() : null; + myTrustStore.load(new FileInputStream(customTrustStoreFile.path().toFile()), tsPass); + } else { + myTrustStore = KeyStore.getInstance(resolvedTrustStore.type()); + myTrustStore.load(new FileInputStream(resolvedTrustStore.path().toFile()), "changeit".toCharArray()); + } final KeyStore keyStore = KeyStore.getInstance(resolvedKeyStore.type()); keyStore.load(new FileInputStream(resolvedKeyStore.path().toFile()), "changeit".toCharArray()); diff --git a/src/test/java/org/opensearch/security/tools/HasherTests.java b/src/test/java/org/opensearch/security/tools/HasherTests.java index be9227eb8b..ed2d7ed557 100644 --- a/src/test/java/org/opensearch/security/tools/HasherTests.java +++ b/src/test/java/org/opensearch/security/tools/HasherTests.java @@ -32,6 +32,7 @@ public class HasherTests { private final ByteArrayOutputStream out = new ByteArrayOutputStream(); private final PrintStream originalOut = System.out; private final InputStream originalIn = System.in; + private final String password = "notarealpassword"; @Before public void setOutputStreams() { @@ -46,58 +47,58 @@ public void restoreStreams() { @Test public void testWithDefaultArguments() { - Hasher.main(new String[] { "-p", "password" }); + Hasher.main(new String[] { "-p", password }); assertTrue("should return a valid BCrypt hash with the default BCrypt configuration", out.toString().startsWith("$2y$12")); } // BCRYPT @Test public void testWithBCryptRoundsArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "BCrypt", "-r", "5" }); + Hasher.main(new String[] { "-p", password, "-a", "BCrypt", "-r", "5" }); assertTrue("should return a valid BCrypt hash with the correct value for \"rounds\"", out.toString().startsWith("$2y$05")); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "BCrypt", "-r", "5" }); + Hasher.main(new String[] { "-p", password, "-a", "BCrypt", "-r", "5" }); assertTrue("should return a valid BCrypt hash with the correct value for \"rounds\"", out.toString().startsWith("$2y$05")); } @Test public void testWithBCryptMinorArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "BCrypt", "-min", "A" }); + Hasher.main(new String[] { "-p", password, "-a", "BCrypt", "-min", "A" }); assertTrue("should return a valid BCrypt hash with the correct value for \"minor\"", out.toString().startsWith("$2a$12")); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "BCrypt", "-min", "Y" }); + Hasher.main(new String[] { "-p", password, "-a", "BCrypt", "-min", "Y" }); assertTrue("should return a valid BCrypt hash with the correct value for \"minor\"", out.toString().startsWith("$2y$12")); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "BCrypt", "-min", "B" }); + Hasher.main(new String[] { "-p", password, "-a", "BCrypt", "-min", "B" }); assertTrue("should return a valid BCrypt hash with the correct value for \"minor\"", out.toString().startsWith("$2b$12")); out.reset(); } @Test public void testWithBCryptAllArguments() { - Hasher.main(new String[] { "-p", "password", "-a", "BCrypt", "-min", "A", "-r", "5" }); + Hasher.main(new String[] { "-p", password, "-a", "BCrypt", "-min", "A", "-r", "5" }); assertTrue("should return a valid BCrypt hash with the correct configuration", out.toString().startsWith("$2a$05")); } @Test public void testWithPBKDF2Prefix() { - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2" }); assertTrue("should return a valid PBKDF2 hash with the default configuration", out.toString().startsWith("$3$25")); } @Test public void testWithArgon2Prefix() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2" }); assertTrue("should return a valid Argon2 hash with the default configuration", out.toString().startsWith("$argon2")); } // PBKDF2 @Test public void testWithPBKDF2DefaultArguments() { - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2" }); CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 600000); @@ -106,14 +107,14 @@ public void testWithPBKDF2DefaultArguments() { @Test public void testWithPBKDF2FunctionArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2", "-f", "SHA512" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2", "-f", "SHA512" }); CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA512"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 600000); assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 256); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2", "-f", "SHA384" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2", "-f", "SHA384" }); pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA384"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 600000); @@ -122,14 +123,14 @@ public void testWithPBKDF2FunctionArgument() { @Test public void testWithPBKDF2IterationsArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2", "-i", "100000" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2", "-i", "100000" }); CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 100000); assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 256); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2", "-i", "200000" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2", "-i", "200000" }); pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 200000); @@ -138,14 +139,14 @@ public void testWithPBKDF2IterationsArgument() { @Test public void testWithPBKDF2LengthArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2", "-l", "400" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2", "-l", "400" }); CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 600000); assertEquals("should return a valid PBKDF2 hash with the default value for \"length\"", pbkdf2Function.getLength(), 400); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2", "-l", "300" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2", "-l", "300" }); pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA256"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 600000); @@ -154,7 +155,7 @@ public void testWithPBKDF2LengthArgument() { @Test public void testWithPBKDF2AllArguments() { - Hasher.main(new String[] { "-p", "password", "-a", "PBKDF2", "-l", "250", "-i", "150000", "-f", "SHA384" }); + Hasher.main(new String[] { "-p", password, "-a", "PBKDF2", "-l", "250", "-i", "150000", "-f", "SHA384" }); CompressedPBKDF2Function pbkdf2Function = CompressedPBKDF2Function.getInstanceFromHash(out.toString()); assertEquals("should return a valid PBKDF2 hash with the correct value for \"function\"", pbkdf2Function.getAlgorithm(), "SHA384"); assertEquals("should return a valid PBKDF2 hash with the default value for \"iterations\"", pbkdf2Function.getIterations(), 150000); @@ -164,7 +165,7 @@ public void testWithPBKDF2AllArguments() { // ARGON2 @Test public void testWithArgon2DefaultArguments() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2" }); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals( "should return a valid Argon2 hash with the default value for \"memory\"", @@ -200,7 +201,7 @@ public void testWithArgon2DefaultArguments() { @Test public void testWithArgon2MemoryArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-m", "47104" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-m", "47104" }); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the correct value for \"memory\"", argon2Function.getMemory(), 47104); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -210,7 +211,7 @@ public void testWithArgon2MemoryArgument() { assertEquals("should return a valid Argon2 hash with the default value for \"version\"", argon2Function.getVersion(), 19); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-m", "19456" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-m", "19456" }); argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the correct value for \"memory\"", argon2Function.getMemory(), 19456); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -222,7 +223,7 @@ public void testWithArgon2MemoryArgument() { @Test public void testWithArgon2IterationsArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-i", "1" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-i", "1" }); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the correct value for \"iterations\"", argon2Function.getIterations(), 1); @@ -232,7 +233,7 @@ public void testWithArgon2IterationsArgument() { assertEquals("should return a valid Argon2 hash with the default value for \"version\"", argon2Function.getVersion(), 19); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-i", "5" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-i", "5" }); argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the correct value for \"iterations\"", argon2Function.getIterations(), 5); @@ -244,7 +245,7 @@ public void testWithArgon2IterationsArgument() { @Test public void testWithArgon2ParallelismArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-par", "2" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-par", "2" }); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -254,7 +255,7 @@ public void testWithArgon2ParallelismArgument() { assertEquals("should return a valid Argon2 hash with the default value for \"version\"", argon2Function.getVersion(), 19); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-par", "1" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-par", "1" }); argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -266,7 +267,7 @@ public void testWithArgon2ParallelismArgument() { @Test public void testWithArgon2LengthArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-l", "64" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-l", "64" }); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -276,7 +277,7 @@ public void testWithArgon2LengthArgument() { assertEquals("should return a valid Argon2 hash with the default value for \"version\"", argon2Function.getVersion(), 19); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-l", "12" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-l", "12" }); argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -288,7 +289,7 @@ public void testWithArgon2LengthArgument() { @Test public void testWithArgon2TypeArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-t", "argon2i" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-t", "argon2i" }); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -298,7 +299,7 @@ public void testWithArgon2TypeArgument() { assertEquals("should return a valid Argon2 hash with the default value for \"version\"", argon2Function.getVersion(), 19); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-t", "argon2d" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-t", "argon2d" }); argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -310,7 +311,7 @@ public void testWithArgon2TypeArgument() { @Test public void testWithArgon2VersionArgument() { - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-v", "16" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-v", "16" }); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -320,7 +321,7 @@ public void testWithArgon2VersionArgument() { assertEquals("should return a valid Argon2 hash with the correct value for \"version\"", argon2Function.getVersion(), 16); out.reset(); - Hasher.main(new String[] { "-p", "password", "-a", "Argon2", "-v", "19" }); + Hasher.main(new String[] { "-p", password, "-a", "Argon2", "-v", "19" }); argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the default value for \"memory\"", argon2Function.getMemory(), 65536); assertEquals("should return a valid Argon2 hash with the default value for \"iterations\"", argon2Function.getIterations(), 3); @@ -333,23 +334,7 @@ public void testWithArgon2VersionArgument() { @Test public void testWithArgon2AllArguments() { Hasher.main( - new String[] { - "-p", - "password", - "-a", - "Argon2", - "-m", - "47104", - "-i", - "1", - "-par", - "2", - "-l", - "64", - "-t", - "argon2d", - "-v", - "19" } + new String[] { "-p", password, "-a", "Argon2", "-m", "47104", "-i", "1", "-par", "2", "-l", "64", "-t", "argon2d", "-v", "19" } ); Argon2Function argon2Function = Argon2Function.getInstanceFromHash(out.toString().trim()); assertEquals("should return a valid Argon2 hash with the correct value for \"memory\"", argon2Function.getMemory(), 47104); diff --git a/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java b/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java index 18b857cf84..a5ce377039 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java @@ -34,6 +34,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createDirectory; @@ -137,7 +138,7 @@ private static void checkCertificateValidity(String certPath) throws Exception { x509Certificate.checkValidity(); verifyExpiryAtLeastAYearFromNow(expiry); - assertThat(x509Certificate.getSigAlgName(), is(equalTo("SHA256withRSA"))); + assertThat(x509Certificate.getSigAlgName(), is(equalToIgnoringCase("SHA256withRSA"))); } } } diff --git a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java index 203056cb87..07c9338683 100644 --- a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java +++ b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java @@ -29,13 +29,16 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.RandomizedRunner; -import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.hasher.PasswordHasher; +import org.opensearch.security.hasher.PasswordHasherFactory; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.tools.Hasher; import static org.hamcrest.MatcherAssert.assertThat; @@ -44,15 +47,16 @@ import static org.hamcrest.Matchers.is; import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_INVALID_REGEX; import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT; -import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_ADMIN_PASSWORD; import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_PASSWORD_MIN_LENGTH; import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.REST_ENABLED_ROLES; import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.isKeyPresentInYMLFile; import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createDirectory; import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createFile; import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.deleteDirectoryRecursive; +import static com.carrotsearch.randomizedtesting.RandomizedTest.randomAsciiAlphanumOfLength; import static org.junit.Assert.assertThrows; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; @RunWith(RandomizedRunner.class) public class SecuritySettingsConfigurerTests { @@ -118,7 +122,8 @@ public void testUpdateAdminPasswordWithCustomPassword() throws NoSuchFieldExcept } @Test - public void testUpdateAdminPassword_noPasswordSupplied() throws IOException { + public void testUpdateAdminPassword_noPasswordSupplied() throws NoSuchFieldException, IllegalAccessException { + unsetEnvVariables(); // Ensure ADMIN_PASSWORD is empty so that no custom password is supplied SecuritySettingsConfigurer.ADMIN_PASSWORD = ""; installer.setExitHandler(status -> { throw new TestExitException(status); }); @@ -164,6 +169,7 @@ public void testUpdateAdminPasswordWithShortPassword() throws NoSuchFieldExcepti @Test public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() throws NoSuchFieldException, IllegalAccessException, IOException { + assumeFalse("Weak passwords cannot be hashed with FIPS-approved KDFs (112-bit minimum)", FipsMode.isEnabled()); setEnv(adminPasswordKey, "weakpassword"); installer.environment = ExecutionEnvironment.TEST; // In test environment, password validation is skipped. @@ -183,7 +189,7 @@ public void testUpdateAdminPasswordWithCustomInternalUsersYML() throws IOExcepti " type: \"internalusers\"", " config_version: 2", "admin:", - " hash: " + Hasher.hash(RandomStringUtils.randomAlphanumeric(16).toCharArray()), + " hash: " + Hasher.hash(randomAsciiAlphanumOfLength(16).toCharArray(), fixtureHasherSettings()), " backend_roles:", " - \"admin\"" ); @@ -406,15 +412,21 @@ private void verifyStdOutContainsString(String s) { assertThat(outContent.toString(), containsString(s)); } + private static Settings fixtureHasherSettings() { + String algorithm = FipsMode.isEnabled() ? ConfigConstants.PBKDF2 : ConfigConstants.BCRYPT; + return Settings.builder().put(ConfigConstants.SECURITY_PASSWORD_HASHING_ALGORITHM, algorithm).build(); + } + private void setUpInternalUsersYML() throws IOException { String internalUsersFile = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; Path internalUsersFilePath = Paths.get(internalUsersFile); + PasswordHasher fixtureHasher = PasswordHasherFactory.createPasswordHasher(fixtureHasherSettings()); List defaultContent = Arrays.asList( "_meta:", " type: \"internalusers\"", " config_version: 2", "admin:", - " hash: " + Hasher.hash(DEFAULT_ADMIN_PASSWORD.toCharArray()), + " hash: " + fixtureHasher.hash(fixtureHasher.defaultAdminPassword().toCharArray()), " reserved: true", " backend_roles:", " - \"admin\"", diff --git a/src/test/java/org/opensearch/security/util/BCFipsEntropyDaemonFilter.java b/src/test/java/org/opensearch/security/util/BCFipsEntropyDaemonFilter.java new file mode 100644 index 0000000000..5fb39b0160 --- /dev/null +++ b/src/test/java/org/opensearch/security/util/BCFipsEntropyDaemonFilter.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.util; + +import com.carrotsearch.randomizedtesting.ThreadFilter; + +/** + * Thread-leak filter for the "BC FIPS Entropy Daemon", which the framework's {@code BouncyCastleThreadFilter} + * does not yet cover. Shared by tests that touch BC FIPS keystores/crypto under {@code RandomizedRunner}. + */ +public class BCFipsEntropyDaemonFilter implements ThreadFilter { + @Override + public boolean reject(Thread t) { + return "BC FIPS Entropy Daemon".equals(t.getName()); + } +} diff --git a/src/test/java/org/opensearch/security/util/KeyUtilsLoadKeyFromKeystoreTest.java b/src/test/java/org/opensearch/security/util/KeyUtilsLoadKeyFromKeystoreTest.java new file mode 100644 index 0000000000..5a853e4f98 --- /dev/null +++ b/src/test/java/org/opensearch/security/util/KeyUtilsLoadKeyFromKeystoreTest.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.util; + +import java.io.File; +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.opensearch.common.settings.Settings; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.opensearch.security.util.KeyUtils.KEYSTORE_ALIAS; +import static org.opensearch.security.util.KeyUtils.KEYSTORE_KEY_PASSWORD; +import static org.opensearch.security.util.KeyUtils.KEYSTORE_PASSWORD; +import static org.opensearch.security.util.KeyUtils.KEYSTORE_PATH; +import static org.opensearch.security.util.KeyUtils.KEYSTORE_TYPE; + +public class KeyUtilsLoadKeyFromKeystoreTest { + + private static final String PREFIX = "signing_key"; + private static final SecretKey SECRET_KEY = new SecretKeySpec( + "unit-test-hmac-key-256-bits!!!!!".getBytes(StandardCharsets.US_ASCII), + "HmacSHA256" + ); + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + + @Test + public void returnsNullWhenAliasSettingAbsent() { + Settings settings = Settings.builder().build(); + assertThat(KeyUtils.loadKeyFromKeystore(settings, PREFIX, tempDir.getRoot().toPath()), nullValue()); + } + + @Test + public void loadsKeyWhenAllSettingsPresent() throws Exception { + File ks = keystoreWithKey("bcfks"); + Settings settings = Settings.builder() + .put(PREFIX + KEYSTORE_PATH, ks.getAbsolutePath()) + .put(PREFIX + KEYSTORE_TYPE, "bcfks") + .put(PREFIX + KEYSTORE_PASSWORD, "kspass") + .put(PREFIX + KEYSTORE_ALIAS, "test-key") + .put(PREFIX + KEYSTORE_KEY_PASSWORD, "keypass") + .build(); + SecretKey loaded = KeyUtils.loadKeyFromKeystore(settings, PREFIX, tempDir.getRoot().toPath()); + assertThat(loaded, notNullValue()); + assertThat(loaded.getEncoded(), equalTo(SECRET_KEY.getEncoded())); + } + + private File keystoreWithKey(String storeType) throws Exception { + File file = tempDir.newFile("test." + storeType.toLowerCase()); + KeyStore ks = KeyStore.getInstance(storeType); + ks.load(null, null); + ks.setKeyEntry("test-key", SECRET_KEY, "keypass".toCharArray(), null); + try (FileOutputStream fos = new FileOutputStream(file)) { + ks.store(fos, "kspass".toCharArray()); + } + return file; + } +} diff --git a/src/test/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4Test.java b/src/test/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4Test.java index 3183828e77..2a3518c46b 100644 --- a/src/test/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4Test.java +++ b/src/test/java/org/opensearch/security/util/SettingsBasedSSLConfiguratorV4Test.java @@ -61,10 +61,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.bouncycastle.tls.TlsFatalAlert; import org.opensearch.common.settings.MockSecureSettings; import org.opensearch.common.settings.Settings; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.FipsMode; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.test.helper.network.SocketUtils; import org.opensearch.security.util.SettingsBasedSSLConfiguratorV4.SSLConfig; @@ -134,7 +136,8 @@ public void testPemWrongTrust() throws Exception { CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslConfig.toSSLConnectionSocketFactory()).build() ) { - thrown.expect(SSLHandshakeException.class); + Class exceptionClass = FipsMode.isEnabled() ? TlsFatalAlert.class : SSLHandshakeException.class; + thrown.expect(exceptionClass); try (CloseableHttpResponse response = httpClient.execute(new HttpGet(testServer.getUri()))) { Assert.fail("Connection should have failed due to wrong trust"); @@ -159,7 +162,7 @@ public void testPemClientAuth() throws Exception { .put("prefix.enable_ssl_client_auth", "true") .put("prefix.pemcert_filepath", "kirk.pem") .put("prefix.pemkey_filepath", "kirk.key") - .put("prefix.pemkey_password", "secret") + .put("prefix.pemkey_password", "notarealpassword") .build(); Path configPath = rootCaPemPath.getParent(); @@ -194,7 +197,7 @@ public void testPemClientAuthFailure() throws Exception { .put("prefix.enable_ssl_client_auth", "true") .put("prefix.pemcert_filepath", "wrong-kirk.pem") .put("prefix.pemkey_filepath", "wrong-kirk.key") - .put("prefix.pemkey_password", "G0CVtComen4a") + .put("prefix.pemkey_password", "notarealpassword") .build(); Path configPath = rootCaPemPath.getParent(); @@ -357,7 +360,8 @@ public void testJksWrongTrust() throws Exception { CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslConfig.toSSLConnectionSocketFactory()).build() ) { - thrown.expect(SSLHandshakeException.class); + Class exceptionClass = FipsMode.isEnabled() ? TlsFatalAlert.class : SSLHandshakeException.class; + thrown.expect(exceptionClass); try (CloseableHttpResponse response = httpClient.execute(new HttpGet(testServer.getUri()))) { Assert.fail("Connection should have failed due to wrong trust"); diff --git a/src/test/resources/auditlog/endpoints/configuration_wrong_endpoint_names.yml b/src/test/resources/auditlog/endpoints/configuration_wrong_endpoint_names.yml index dee8c95641..9187fb7008 100644 --- a/src/test/resources/auditlog/endpoints/configuration_wrong_endpoint_names.yml +++ b/src/test/resources/auditlog/endpoints/configuration_wrong_endpoint_names.yml @@ -11,7 +11,6 @@ plugins.security: username: auditloguser password: auditlogpassword enable_ssl: false - verify_hostnames: false enable_ssl_client_auth: false endpoint3: type: debug diff --git a/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml b/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml index 027ee6869e..0bd01d9122 100644 --- a/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml +++ b/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml @@ -7,7 +7,6 @@ plugins.security: username: auditloguser password: auditlogpassword enable_ssl: false - verify_hostnames: false enable_ssl_client_auth: false endpoints: endpoint1: @@ -36,7 +35,6 @@ plugins.security: username: auditloguser password: auditlogpassword enable_ssl: false - verify_hostnames: false enable_ssl_client_auth: false endpoint3: type: debug diff --git a/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_names.yml b/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_names.yml index 2b96265492..b7e36a082c 100644 --- a/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_names.yml +++ b/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_names.yml @@ -12,7 +12,6 @@ plugins.security: username: auditloguser password: auditlogpassword enable_ssl: false - verify_hostnames: false enable_ssl_client_auth: false endpoint3: type: debug diff --git a/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_types.yml b/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_types.yml index c59adc4ee1..1d3f6447db 100644 --- a/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_types.yml +++ b/src/test/resources/auditlog/endpoints/routing/configuration_wrong_endpoint_types.yml @@ -12,7 +12,6 @@ plugins.security: username: auditloguser password: auditlogpassword enable_ssl: false - verify_hostnames: false enable_ssl_client_auth: false endpoint3: type: debug diff --git a/src/test/resources/fips-jvm-truststore.bcfks b/src/test/resources/fips-jvm-truststore.bcfks new file mode 100644 index 0000000000..22f4b6c455 Binary files /dev/null and b/src/test/resources/fips-jvm-truststore.bcfks differ diff --git a/src/test/resources/fips_java_test.security b/src/test/resources/fips_java_test.security new file mode 100644 index 0000000000..77bbea2fa9 --- /dev/null +++ b/src/test/resources/fips_java_test.security @@ -0,0 +1,58 @@ +# Security properties for FIPS test runs. +# Used with == (complete override): only the providers listed here are available. +# SunJCE is intentionally absent — this removes DES, MD5-based PBE, and other +# non-FIPS algorithms without requiring programmatic Security.removeProvider() calls. + +security.provider.1=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider C:HYBRID;ENABLE{All}; +security.provider.2=org.bouncycastle.jsse.provider.BouncyCastleJsseProvider fips:BCFIPS +security.provider.3=SunPKCS11 +security.provider.4=SUN +security.provider.5=SunJGSS +security.provider.6=JdkLDAP + +login.configuration.provider=sun.security.provider.ConfigFile + +policy.expandProperties=true +policy.allowSystemProperty=true + +keystore.type=BCFKS +keystore.type.compat=true + +ssl.KeyManagerFactory.algorithm=PKIX +ssl.TrustManagerFactory.algorithm=PKIX + +jdk.certpath.disabledAlgorithms=MD2, MD5, SHA1 jdkCA & usage TLSServer, \ + RSA keySize < 1024, DSA keySize < 1024, EC keySize < 224, \ + SHA1 usage SignedJAR & denyAfter 2019-01-01 + +jdk.security.legacyAlgorithms=SHA1, \ + RSA keySize < 2048, DSA keySize < 2048, \ + DES, DESede, MD5, RC2, ARCFOUR + +jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, \ + DSA keySize < 1024, SHA1 denyAfter 2019-01-01 + +jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, DTLSv1.0, RC4, DES, \ + MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \ + ECDH, TLS_RSA_* + +jdk.tls.legacyAlgorithms=NULL, anon, RC4, DES, 3DES_EDE_CBC + +jdk.tls.keyLimits=AES/GCM/NoPadding KeyUpdate 2^37, \ + ChaCha20-Poly1305 KeyUpdate 2^37 + +crypto.policy=unlimited + +jdk.security.caDistrustPolicies=SYMANTEC_TLS,ENTRUST_TLS,CAMERFIRMA_TLS + +jdk.tls.alpnCharset=ISO_8859_1 + +# Revocation via BCTLS TrustManager (covers all TLS including LDAPS) +com.sun.net.ssl.checkRevocation=true + +# BC FIPS CertPath revocation mechanisms +ocsp.enable=true +org.bouncycastle.x509.enableCRLDP=true + +# OCSP stapling — request stapled response from server +jdk.tls.client.enableStatusRequestExtension=true diff --git a/src/test/resources/java_test.security b/src/test/resources/java_test.security new file mode 100644 index 0000000000..82cf1ed27c --- /dev/null +++ b/src/test/resources/java_test.security @@ -0,0 +1,55 @@ +# Security properties for non-FIPS test runs. +# Used with == (complete override) so all desired providers must be listed explicitly. +# Standard JDK providers are preserved in their default order; BCFIPS is appended last +# so it is available for tests that rely on BC-specific features without displacing JDK providers. + +security.provider.1=SUN +security.provider.2=SunRsaSign +security.provider.3=SunEC +security.provider.4=SunJSSE +security.provider.5=SunJCE +security.provider.6=SunJGSS +security.provider.7=SunSASL +security.provider.8=XMLDSig +security.provider.9=SunPCSC +security.provider.10=JdkLDAP +security.provider.11=JdkSASL +security.provider.12=SunPKCS11 +security.provider.13=org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider C:HYBRID;ENABLE{All}; + +login.configuration.provider=sun.security.provider.ConfigFile + +policy.expandProperties=true +policy.allowSystemProperty=true + +keystore.type=pkcs12 +keystore.type.compat=true + +ssl.KeyManagerFactory.algorithm=SunX509 +ssl.TrustManagerFactory.algorithm=PKIX + +jdk.certpath.disabledAlgorithms=MD2, MD5, SHA1 jdkCA & usage TLSServer, \ + RSA keySize < 1024, DSA keySize < 1024, EC keySize < 224, \ + SHA1 usage SignedJAR & denyAfter 2019-01-01 + +jdk.security.legacyAlgorithms=SHA1, \ + RSA keySize < 2048, DSA keySize < 2048, \ + DES, DESede, MD5, RC2, ARCFOUR + +jdk.jar.disabledAlgorithms=MD2, MD5, RSA keySize < 1024, \ + DSA keySize < 1024, SHA1 denyAfter 2019-01-01 + +jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, DTLSv1.0, RC4, DES, \ + MD5withRSA, DH keySize < 1024, EC keySize < 224, 3DES_EDE_CBC, anon, NULL, \ + ECDH, TLS_RSA_* + +jdk.tls.legacyAlgorithms=NULL, anon, RC4, DES, 3DES_EDE_CBC + +jdk.tls.keyLimits=AES/GCM/NoPadding KeyUpdate 2^37, \ + ChaCha20-Poly1305 KeyUpdate 2^37 + +crypto.policy=unlimited + +jdk.security.caDistrustPolicies=SYMANTEC_TLS,ENTRUST_TLS,CAMERFIRMA_TLS + +jdk.tls.alpnCharset=ISO_8859_1 diff --git a/src/test/resources/ldap/config.yml b/src/test/resources/ldap/config.yml index b7a099a36c..e327d85536 100644 --- a/src/test/resources/ldap/config.yml +++ b/src/test/resources/ldap/config.yml @@ -40,7 +40,6 @@ config: hosts: "localhost:${ldapsPort}" usersearch: "(uid={0})" enable_ssl: true - verify_hostnames: false description: "Migrated from v6" authz: {} do_not_fail_on_forbidden: false diff --git a/src/test/resources/ldap/config_ldap2.yml b/src/test/resources/ldap/config_ldap2.yml index cea994dd02..5ef6da0a4a 100644 --- a/src/test/resources/ldap/config_ldap2.yml +++ b/src/test/resources/ldap/config_ldap2.yml @@ -40,7 +40,6 @@ config: hosts: "localhost:${ldapsPort}" usersearch: "(uid={0})" enable_ssl: true - verify_hostnames: false description: "Migrated from v6" authz: {} do_not_fail_on_forbidden: false diff --git a/src/test/resources/ldap/test1.yml b/src/test/resources/ldap/test1.yml index e0ad96ceea..c6d926b68f 100644 --- a/src/test/resources/ldap/test1.yml +++ b/src/test/resources/ldap/test1.yml @@ -110,4 +110,3 @@ pemtrustedcas_content: | pTIIfrQcZ1vrDg0lYzVgQ1iT -----END CERTIFICATE----- usersearch: "(uid={0})" -verify_hostnames: false diff --git a/src/test/resources/ssl/pem/node-4.crt.pem b/src/test/resources/ssl/pem/node-4.crt.pem deleted file mode 100644 index 3bf727f94f..0000000000 --- a/src/test/resources/ssl/pem/node-4.crt.pem +++ /dev/null @@ -1,50 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEcTCCA1mgAwIBAgIBBjANBgkqhkiG9w0BAQsFADCBlTETMBEGCgmSJomT8ixk -ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1w -bGUgQ29tIEluYy4xJDAiBgNVBAsMG0V4YW1wbGUgQ29tIEluYy4gU2lnbmluZyBD -QTEkMCIGA1UEAwwbRXhhbXBsZSBDb20gSW5jLiBTaWduaW5nIENBMB4XDTE4MDUw -NTE0MzcxNFoXDTI4MDUwMjE0MzcxNFowVjELMAkGA1UEBhMCREUxDTALBgNVBAcM -BFRlc3QxDTALBgNVBAoMBFRlc3QxDDAKBgNVBAsMA1NTTDEbMBkGA1UEAwwSbm9k -ZS00LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -1EjVHY9yVLJjWN7J5OU2pkZyrttRO674kUtIn1Z8lCNTD31DIfOnJ6iLlUDZ0HhC -9VumGOu4dc9HCSoxr4ck1GiDG0YrVdbZ6Hx4E3P+BY/RCxuQutGQeKi4BwgBm/bF -5yg9ro0+ia2808RB9pWuvLClypPjhSJSMfG+UUJ8+23jRYdxYRboWPScacbuV634 -1AdOdlr7k7Rauw7nagehRi1k/DUEEOrSHbWqxyziiisJvU97Ey9FZhVya8st8srM -wgNst+Lr16458V1pLRm097PoBYQMU9/MgMVXIz0Rmm1oqRMCPW5CPg6x5SXiKoCf -9i1rwmt5abB58tYwxwJB8wIDAQABo4IBCDCCAQQwDgYDVR0PAQH/BAQDAgWgMAkG -A1UdEwQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW -BBSuaYlnZZNkm6DUGh5K4zLBBYvQMTAfBgNVHSMEGDAWgBSUd+KTMQs/tufL5Y5q -RKTE4wTB4DBiBgNVHR8EWzBZMFegVaBThlFodHRwczovL3Jhdy5naXRodWJ1c2Vy -Y29udGVudC5jb20vZmxvcmFndW5uY29tL3VuaXR0ZXN0LWFzc2V0cy9tYXN0ZXIv -cmV2b2tlZC5jcmwwJAYDVR0RBB0wG4ISbm9kZS00LmV4YW1wbGUuY29tiAUqAwQF -BTANBgkqhkiG9w0BAQsFAAOCAQEAlCP1gK4DkG5VwPAY68BeifMSmSFai/JOOVSA -URk7UiQ+nNkwo7Wuvf60aXqN+K440D3fvW3Bzk7+EnnKQFNXm4jief3ndM4Hvra0 -WKh979iWfrBY2LUqWhpW9zsIEx6PQGJIBlAfNiaQSW+gU05DQ8sxcfOgPPou94XT -xrioFBg42JDMDLhiNhyLcMgMEwmdyfNw+N28HLrsoEJ223H9DWwfse1j/IgOKA8h -5/PWNWUE1OFUflWov9wmlMVH4RUDovJGjvzFqZ7e+mH5gALp3CNtIcwl8u5jT3Y7 -206hdGScJoTWorKv5ujyhrGY/2Vp7AzY3pHHuP74HZL0HHvp4w== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEBzCCAu+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADCBjzETMBEGCgmSJomT8ixk -ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1w -bGUgQ29tIEluYy4xITAfBgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEh -MB8GA1UEAwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENBMB4XDTE4MDUwNTE0Mzcw -OFoXDTI4MDUwNDE0MzcwOFowgZUxEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJ -kiaJk/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSQw -IgYDVQQLDBtFeGFtcGxlIENvbSBJbmMuIFNpZ25pbmcgQ0ExJDAiBgNVBAMMG0V4 -YW1wbGUgQ29tIEluYy4gU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBANWsMh2EWiqH2eZmaiHreWG4NlhLZGcUbwzRIZT0HmeeBolQygGq -cJE1MpzCMYdezjTRaws/FVA2dkrtcox2xGT6YG7sKqr+4VlIt3Pd0Sah/5dEdRJv -RsN2mj8V8xNUZdduD6NnrIGW/wAoF4isDNJ3QlGFhPM0f0Of5TVFIyholgrevNLT -7D5rdUupIW192zQbOOuOxOmeXkunl8u35wq/VI/ZyJ4/mutCLR5sqd6/kOSDKQTU -gQ+xIrs7LiuF1xZbCtRT3/PWnnD/GJulUsuJ0xOeEHkQaJuwRwYzqFkyVrEea2Wf -U6XmSRZK9L0q5jy8TpCgzULlxb92POZd9ssCAwEAAaNmMGQwDgYDVR0PAQH/BAQD -AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFJR34pMxCz+258vljmpE -pMTjBMHgMB8GA1UdIwQYMBaAFFvHubBG7v/2QuZe5M8S+FdMajAVMA0GCSqGSIb3 -DQEBCwUAA4IBAQAY22ahhmYBdYUpPwQEyEUexyTWal29sbV+R44qVKM6FDEEd/8Q -cFe5cnguDqmLBwHDLey4eSsAHI5tBUtslPJMqobWbwzswxdZ9WCOaLBWlvZdK4XU -hkrq919wENMT6DVagNdpNRmDA47G4eRha4oD1ZO2YCFM0H8rEWDRSlaAHsGHLR59 -cJ5AgPqAVrEMfP6WzxXW2ThY6HD1LsE69T20/CfR8/k826BkcYVHKR/MQ/YZOWXb -ccfb7D5o/oMop0E4+huCdF7ZDOt7/f5+BAfJZ08GCMLy5GSxU9gf8WiT/yNBYETS -DAj+BAKhzlzvsaC3E2lAeyUepIMN0B8YHjqV ------END CERTIFICATE----- diff --git a/src/test/resources/ssl/pem/node-4.key b/src/test/resources/ssl/pem/node-4.key deleted file mode 100644 index 965c02e4c4..0000000000 --- a/src/test/resources/ssl/pem/node-4.key +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN ENCRYPTED PRIVATE KEY----- -MIIE6TAbBgkqhkiG9w0BBQMwDgQIAUztzPE48tECAggABIIEyMq0nF+vkpgA9vs1 -LrkGVinMLcl9kcnZ6sclulgUpar+agZ7yce3rtdFYrTFTRe9cLfOI/+nm3scsHaH -kf0j6ZVAAyyXupivEV9jnFw6QjMql14EEEpzSjdUJ0e+yO8IIXO5q8dx7tFpiUUg -WlSTaVqLS82OhuwD1VrDykR5XlmHp6COOhEJT7mVdvmNZFCwQ8VUg9k2hjAWB7vw -PhJPNBnhI4Tr5Gb0/qFvpPcPkV5muxSkSJ2VA+YgHHgvZKpC/8E46uq7t2m45owM -iCPOAVSy5nLigKcttP9Q8o7LY+l8iJdwkzTotu1gFjYfVp6+n1vHIKDf3kQz457v -wCpSTa9p7PEreAo+SNUsigG23SsNIWP9/uUpPiy3G4X2kuNlxWdmqY8KSmblttGq -6vmdIqpNwGOGDy6stfU9K7tgVdoEvRXfaNS1PwyF4V9ZVF9y6HEqmRcdpdjKIufW -H49nF7AcNPGsEGCeX36ILnAGborvWstCOMl0WFkBg45sbLiT2Q9gAfSo37+Rfh11 -O+k6V8p5X30o6ftav3OR7j/dViKp/5j5dyAMjB/4tcDMkywHwm8Evyik8aWomQer -1u6KPpE9hYn2YOY2cZouiC3d8KNLZJMdkcOlqYRs23N9QNNeFW3CvGGb0203+uWb -185gjGHVX/lT8INlquPJyAt/3VT4EWTaSvIUS4pI75kDq/gzr56ak4/GgDEB4pK+ -yH7npm3U1p/jyoriJ3/1jMXkRp7HN42XSL2/m/Kzk5fX9JaNNO4ANHpuxMqV6MU/ -krWMa2bbC2shaVxZFOlx7LOUL7eHh7Vkk3T6/G28s57d9pW8qHBlwgNrYdegB8aC -wF8SNmo75c4xHkpShx+FmGG/Q7h0VYzzr2j9XQTs7GspbEJo9ugTSVoAEVLI2avL -sE6GS8AzKGTDKPNlzAru2+nzqQ4aG0RxYMpg9a+cPMQeoos7knjkkT1Twc1gcWQv -r9O3vsY87pLKMSsstDtkZRONqxwPqbfDIMyV2JOF/x1EZ3+V1NMQErwGgl/tqlW7 -+PnW9JrIVFp+AEAzY666cta/RSJ2MNa7OrodIrgH5TtEwEy3qDH1YtVXUXG9A05W -AGjLMFCQKEtrTrN4EotqXEXLmY54JF9WvT7hvtjYYhxkS+QtquujJb6En+Pte+Lm -HN89dZToAY62LxhR4P1morg0jUwrAMssFa0YnMO3wN6ADQ8RSVT0cyOtGDM77DrI -ps/K1tY+lQi9OV1pflP+Q6SukTWs+6dm8MpRcySPiKI+OZLVwy0MYub8UlCaO7PK -QM+VcXVX3OyIKUe6BUiEE7KVnFh3V/I0KfNbYKInEM3gWPV06BWNSUo8TEXrxXWz -xNRGG9r8RKf99cMzotox7G2YUSBJdI3SAqUse5ubdOW7TRsSMtdzMpSr62hambZ8 -BYLjtjYZm050vu3PHeYO6htCMj52m+w6IFrPGlmmKqwMsgnSC31Ns5zmMQP5Oeu9 -spLrr+oGsURF3DRVdAXGBY1OlvWCDLsCSp0kOF2Vpj4y7v9zPEEku8w6eGMbRKcc -+Cg86Acrc1hOkDtzWtfpbDkkhXPkcYU8ic8c7gN3japCoH+RFLqoNhJow1vx6TW1 -9vCq0aTqdksLCNZ1Bw== ------END ENCRYPTED PRIVATE KEY----- diff --git a/src/test/resources/ssl/pem/node-4.p12 b/src/test/resources/ssl/pem/node-4.p12 deleted file mode 100644 index 892b6297bf..0000000000 Binary files a/src/test/resources/ssl/pem/node-4.p12 and /dev/null differ diff --git a/src/test/resources/sslConfigurator/pem/kirk.key b/src/test/resources/sslConfigurator/pem/kirk.key index fc358dacd9..8407fa0bad 100644 --- a/src/test/resources/sslConfigurator/pem/kirk.key +++ b/src/test/resources/sslConfigurator/pem/kirk.key @@ -1,29 +1,30 @@ -----BEGIN ENCRYPTED PRIVATE KEY----- -MIIE9jAoBgoqhkiG9w0BDAEDMBoEFJXPhgGfl3qpK2ps9gqNUx35uHCaAgIIAASC -BMjmI3bvVJNwKmLDaj2z4MRqn4h99ktK8mAd3rFH65QRHP0VrbTJ7ymLTsWCEwXr -QUjR87tDsgtsEFoazXv0HATPVLkAwQzHwDkps1WSaphh2MG/05QSpdMYqP8yxKVg -HdSOabKlwh8IQIA3QDCefCYwM+jRx1hw1B1hMXabUtqN1EENdNp6bZ76qxoiPyFm -zq3yruaBS0CjexYbdF1wOjIAtoTkD2v/B+kiVUlz+k12nK9Wk3uf4OHL26gMI/o5 -J2tRJ5xCGHfOQaz/VCp8QV3qnpjUp/sBMNRL6O64flmbamwN5/8y1D1xP900ZSWS -LjrfvQAaSh52O8orcaFXSoPNRYyOsLMZ4/L7ysJP6RPLGI/MwQE/XF5p/JNcFM0X -n1DR6UJGWl7KfJy7LT2EEM3ztiH87OvSsnrYeoBTJUE5MSmhxeiWHoPus8OsxA8v -DNHKAMMiiaxL2Wmt+et4zpZJM7wRyRNVGqHKgCYudpCB2Del8RKm4zjF1i60EVc3 -Nm9ngw3veZRhiNUrIwNqJ2dx/ZUzPQ13wUAJ9H+GKSl5SrL4JXxs1yQYClbL5TBU -luPUhzlgSHVMzl9UCevI6j6AbGCi1DkppUelR5LN7lTgiBcCMc8XoFGzhriepobX -tZeUM+HJtjLGcq1yGLApM775JIl8LgrpkpuACMPs6dFSqwp5612hFtbaOqFmQn2P -SC6Kk4LcV7UahCehXtLr/S5QMoJvE7HfW92/+7Ln4tc1KCBE0+7KDq5PtjQgFybo -UoGUvXtva0m0Ff6gt1fdyoK2/y+V91fwMc/sCrlfNIA8bz9Lk98mrppOs4vfbKlq -9BFoZwuAebO/nuXOqn4U4gKxDabDkcxuMqgxquqtpePEH583FDNKxBvVVuZpDAbb -KTEOxXEDZvvMJeD0P99C1L6XPhpj0olCLgiC51P8/2aoLQC9YLm5I0ne+j/La7qN -i89+0FvnaoS75VtIlIj+kbrYOzWrnGIWHsB4k1kiSfwjdOBgbb4fc3tq9xpO9Vya -VWecagNlNvIz6Oqi8HXX9HT73kZ0GL95DB+FZsKKy03Bvys3tPEnl/R5mJlZslfr -0rsknLRM39Kty2W577IhRY/OypLIUppfa/R1x+yE/zN3cAAiMFyQi9BQpcEF/66x -zjGoYanv6unKojMQJ1KOgxWRgdhZfRcUomZJdgoU6/+ZzvftHQ2/KmaGOHiVdk6E -ARnYUTwTBHoIpS8d7SfZYAjj0I51ICHWpNU2ecZ7bCJEFjRdwbOHqQOV25rpAjM9 -JdUwfi/yuw+LwjmRFFe4wqn/Je15/DM+3fNIYYwSZ2tYMjeDJFvFDcqX7m/j0DLf -G1Q65pz3hiOedkxKobHvmEygbqaAXX7gxUXKQf7NuooBfIgGiiMv0RMYZxqxYAwy -MVG2C1+SjlhdQgkUgFfXikku4A+3b2I+UEaJ/Jot03WCzIpJ7KIFJo5Q/56dhK7e -lRWBjhGupivqlvgWdUYGwsfd0OVpSaUChOKnO2mGPZmnyoig2F2VaE9yX8KopWkk -k+4D/wGJWWkW0NJN/7bGVkq5nXORzCMvN3r/UcovhEbDKAiMfIZz4eGw76xpqCmR -puulRd0958X0/eOUE8jLSHCJsdGmwfOoJ0U= +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQWxBz0oLGTSOaOffj +7I3onAICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEELPsqDmIRVTB6yd0 +isp4TJwEggTQFiVR7Nk8Mgx7oYpT7LgG352oampixw3AQ9H9fYs06Y189iSO0W23 +Lo2aTpBwE5PLDTN7PZQ/d0iwLL93QA5DEsRIC0qxZsuOoS4krL7ApAA4DiTNHq9A +sRe7YctV1nW6JIt2+nEQdqKt8HDXQWNOKdULKmPhEFVdBlElRJZiwYpG0uqxvwpK +gA6VQekJaxsFh4E5Nq1OXcVLGmEj663UIsxunaf6QjH1B63dUX/Z91WA8MLXJdOs +ZbxZDSRfShMwkm9oyStcMb49x+budsdqDOcei9oPkA5Eo+jj78ZQpaNDNYqWyKEE +sfws6M9wjLuqNwrjWvEH6kA6KzPVVsPHp0W+6zfTRbvn/EwU/o5uberiBqG/ZWvc +FYIRCzwKYqsoTN8n/KamZRCSueIzHwI9MUj3sYkJE9a2Tgb4GxqeDkEdC34FJBil +AtbvmmNRLKCm16YonRk5EiqrbA9FkVCrYuBmjpy0ckXFPtG8q7a5AF0u7js5i8xk +zy8IbtkOnj5mybibRvxY3eMAsyCmaVkUAxEukGogmSeGcqUJWnQC8EeLaarGfLtv +tKtueRpI7vznlW3XxPd3CFj0L+SCvPkXap6FKEqv3qTVvDf467WAdjE+pjHnJt87 +W4vX9xDPEOJBwfNzLzVK3zP3KRWdZyd63tJJ5neYiGQ16Omjo4RdWTVQal1DpJVf +C+tjlJ8SDCN+GodDjS47gWMiD29Euz6XH+GqhTbIe4CQiCbytt7UWfWcLu7qaTLF +F0DU2AyBFj9Qj1hP8YDwFxavLcxj53c8/hqFs3Ag+fjOxhmOW1E4jvda01wAd+vY +UjJux9YaaGDdV6jh2iDJVk5kIgE6idkbmsof0U7LS/0HD9CH4Mw9PPTMmKHSQvKX +hxr1pprONvyCmQscx5dsx3zvTuMBs37sqPRmaezdfBoPkmQqjE7g8df8N579ftAN +r+0moITX3uLfk0V+w6RrKptJbJiEdKWy/syC3NZtbTsmoJgrwL3iHOj7ZZgHRWhn +vmPUVrIqsnvxnYaWXdWklF1ZfwvBiam++KMSqbe/KRpdIyqK9mzHZpWzh23f/bg5 +HpG/edYv3b1GF07De/Nd5si1fP5v5C56xY4WulR0z9gb3pTVbDUoCBwgB89kiLL7 +68ZKpSuvU4HjvG+hb7I+BcO6KWyVhrdoS7i1nkkNW2eyQSEKdl5LimGHvSibwuWZ ++4d8OBdy2SBTj4EIEm5cp4rb0ENV8e3v8kn5ew6eAHOf0WARZrQZ2ZKgtrI8CH4J +Rbyyscqsn/GD9+iSqeWWoc3uaTy7e3eM3SXMzYkpJZUWlxlr0gt7nJAC1kDfhNaL +zuquj2ru1NiogQB9guDU1IRoQcV2Ql1prhuT44pis3AtyikvYOrRe6fbcxUzcbU1 +dll5M9SNOQs/O1tcitSILCyikW+XWn07o97irGS0bEkdjBxjd97h2AKQVeqWgdL4 +WKZS8ay0xEBiuIhWovbxWKk4aEc/d10wtgiH5L5/lQHhZPZfojg+EJYlnrz5BVMx +wn9KFsxQFiZS4n3keImh8WgWvl7uuuoT+s74r6TI2OJoswRFSA+9XaEQKwKO2rTS +B55WbRUTBt9iWI2cfiI2RtHv3+rc83F3GiMZUz1th82fcwgVOVbRGP0= -----END ENCRYPTED PRIVATE KEY----- diff --git a/src/test/resources/sslConfigurator/pem/wrong-kirk.key b/src/test/resources/sslConfigurator/pem/wrong-kirk.key index dffd1e348a..635539eda5 100644 --- a/src/test/resources/sslConfigurator/pem/wrong-kirk.key +++ b/src/test/resources/sslConfigurator/pem/wrong-kirk.key @@ -1,29 +1,30 @@ -----BEGIN ENCRYPTED PRIVATE KEY----- -MIIE9jAoBgoqhkiG9w0BDAEDMBoEFB6EqCstahSd0lALqXY+Qe32U/FtAgIIAASC -BMjcdNjt9qQnTvwlnI0OudBSnY1vOnMVXlZGKAzDkkOeVciDGiyq3N/dch7ICKRU -/IrJa4z81YbZGalGDjqSEwhR0PNAmmmKRRi7pXnQ2VPDKNPQ7At8FQ4pPwPc2W4w -6NE54P9IaU+EBphG7Yln2s0h75p7tGuX8Lr6VBiNIgaOhAzEIupvOgJoSQvAv6QS -0Oe+n4a/oR99eOizKijN+YrxWvP+ejRexV9ABsHbEMtm4rU031lOt9wQybJmdsqO -Jh9UoGRPYeYTToum/jgfhXu0QkY5hwK1IOQjinERarjHbvsMRiyS3UlY5hz/F0Gy -7qpep6kNNJ6zHb+2Hup9QIdt0Fl2nN+kTqmvEBXdZ/ldfCxjqU97vq92M0uP5GH/ -KV3XN8neYwr/i0W4o5mJPiHho8/azBHdkefaEOPzLGfwwoj0W+1E0FZUj8DAU2r/ -QQNQ4E+iTVDvBQefIHcJM80voeOk0ZhWDBXNo2lblSjat3L5PHjThav0sHPpyreN -3+4caP0SxinBLn3PzKxbuMv7fnPjKntCBuF7WsL8gx5Y8tTpNvtu/PYo/LowgX89 -SZN2kS7F/zV7PaNpDXMhe/h7Ribv1zpAliy6DYRW8GE2kYSz06zaabGfHrTwowuO -tFla4z4xh8N0bBiK7pUN085uOV5UsNogv2pWOYh0mo7MAzXatW+zdwsFx79R9gBT -sMeImSRqYuyBvczfbRyebJAU6er3+IXITj+Ii/Z0LHCZ8p+ZXy9iVwb2+bfjr21r -t/9nGYVC4XQvzO+HQ4N8teFsUhCvG/3tgAMOeOBKwsIs+5V8ae52PwnrW0IzTtmj -S2OLc7IkYLlKtPZRYPft5+XOXvNUWUPjV3Ky9miN3eCi4kM3082sZk1wCphd3gjg -fYtUX7qQQmaq90Aep9b6AH+u5lP1a5Oh9leLayh+w7gA/hqKBA5b9j3PmPVkZM4J -NHe9owsuReenFHID0kS1h+X6fbEYqjNNv6xDHxe8FpWDfm5kXpozl4N5J8/7syDa -ntRO5SzVSCOO8zCIsKATE9C7ZP82mukyiRPfQFLKaeQf4BGJ65pFTGb+2Msc4ZXw -1P5yFGIOVhLYbzhF9tlSTucuOPf9E5IFiIJJjgA/d2McU90+wYXMP2GCUNH6d2H/ -/M/dB8g3dj393wMrQwGEyQ5Dwch8iCkmTVl5i7GgNVdAY3KXA6m16nHH5NXjtFa9 -0YFIV+RT6fMCGBpoYdAEA8Pvu7DLah3aX8kWNXlg95Z09LriyBzkcm1lcTZWgP1k -uwFF1F2/y0zSY+Xbl+A8PqkdJIL8ox1qsERYU2fEaDAK5xKPhVjnDU/s+KkXmb0w -OBcVt2HNwAHDRyIzslG9JCQdThr/ygaK1+vtFAktLcnHApBouHo/OV74ufQqJq6m -XHdTIs5gwfhFLuVRtGLKYcqUaiF1L2QkMBDwWW3cbXaut7RgfjIFatQe5SdcG8ap -7Bnx25MCqxNVghMVV9NRE88MWhEgrK0bHxD3yRiIcVEB/HOxbb8W68LaQLw0Q7sl -ijIjz7z/im/N9owgkiFvHI4qg3ubbLaIE4MoazU+as8knKcROuLTIsDY91UPywaa -/dD4xRqAsSmqEbD26z34vtstE5mBnqL4ha4= +MIIFNTBfBgkqhkiG9w0BBQ0wUjAxBgkqhkiG9w0BBQwwJAQQ3XxOuHit4uX4mBwy +I2UndQICCAAwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEPU7SbMOhz0Yb9nv +r5h7AVMEggTQsGcvsn5tv0LMqGRt1sZTjy+kx6IkYErBbsfgDCz+Ei5RkAKjAoDE +3tYCJVIuj2kcK6Niwncu9llc/mFeD290vu6CzllnISRTK9azIbZvt1YJGmze3Bfy +e03H2YwybaDWJQN+J1hE5FxsAEHDMCpMrSQHvy/yM5NMfrMsISX9/m5vhaPxxKzI +3HMxIEfdIwwLyq5/dsOevuz6sl6umtUg0F9lNr2v4pH678qSxftyaGd9ceDpTb17 +8ULb+wOWb5iZ/nDRLZpuFOxNeE+cpBDPQEHDRPuqm+vIQ0JjI63YrgbbfSQflTKb +gjs84iLCtW3wQtgaJTZkRpwTxAAYTYQEplUKIj3ORBmEtesBn0zcd/xvf3bjg+1f +LTgIkYvfl+7BC2MXK9N01v+c+b+EnqE3h2ZLuYb8RzxBM1AO3b0XESxVL8ug5API +3rfdLNBamGpB8jUYTpqoZMs5cFvFC7FDOCKoZm+Cvu0riInZs1JaLgwj4oMpWArL +Dwv+uBo78F7PDPrkXDTisKIgoHPDbLU9dETW0BesjCCDe2EZ/BlUgYAwsN1Vu4Te +rkXRBD2l1KwBQ1sPYh26DxFeRIMhd4xVO/vnccCGQ6sxIxIUjilYACNlxk5231bn +yrgO3w9Cfra/vxTnlZPOzuB03c896F5ZydJOihaOzzGtB60v1LMgcd0zm/O3HeVf +1QYwhDo4ajiUTuLiFTciwi2PmJlk9aH5k58/Iou9EgqHNdQIp2wMC5XznYM9v0OL +UWwM9wIXjQL3vHVFUF53HsytEfJDH4Jb6C5z33TnPYJsfkgeav7zuYNDTllGDF21 +YXA3LmA6rHPZYLmWHYjla8PXtoOdrdBCnWoRo+5Q9jmHJSxSRssZl3IQgcoccEyC +QSR/HHxmA8dTMsais8MeQDBGwAQXA68N8lL1AWDZ5bGgU99DD3i1yzPYbOgDGs5r +UJwJlG2d01deV1HQ2vKE+iu9PZpel28aiczmdsdFaqrTAeNorUsz/yt6nJpbg3qy +rM3IpHOW7HerA9kMwDkaXfLFE9a3zxcsI0Z3xloDhIR1R8Ih8549cwTBULt8Ttj3 +qK0GhsuUYxBINjhOPYiym/WmO4DWmjMJt0do8f/T4VOaSyZg4Vo1di4ou6/JPMZI +7Rjtk73kOVqBC7kdISyinKFhWeGv+BJlw22pOCW2LSyNOJNKN/H3kahNLOTDGlp8 +gn8eNUY8xLm70RYQRn6b2DXRTfuzb7wdAMIoGvlJaOTsCjRPy8srEM7F91ufvtiI +1vyv8f+/tV2NELMN9z57x0EbM1K9W+Pfx4BnJ4hYYxB71nBGxua9+iqZqzHZxLQS +2ON4tb1GJhd1MIxyufrv12nyvqrfP6QWpjZ+mTo4huUJZC8iAcSekN9dSdVhWZ6z +a3wA/CybeGZCGBuNGINtTpbJ7K0DFls/I4j2Dw4VMg20IPHf47ASQ/ADisP2x2zz +sGXqbiTNAWs9VpKHaYKi/wrrIkZBbOMbQJ+SEG7UNIGDtsThNKwNRaGb75mjqsL1 +lZUJEHzR+2llqtVdxOAijuMfjkRdf9fEBKSxRBG8Xr3CRWDltaqovUQa2rrHB99O +sDu71fZA4bdoPbM7Hr9Pkix6E4/wNFEXWXxg4sa85GDv7rc4gUql+nI= -----END ENCRYPTED PRIVATE KEY----- diff --git a/tools/securityadmin.bat b/tools/securityadmin.bat index a44562b6d9..e10782b320 100644 --- a/tools/securityadmin.bat +++ b/tools/securityadmin.bat @@ -1,14 +1,11 @@ -@echo off -set DIR=%~dp0 - -if defined OPENSEARCH_JAVA_HOME ( - set BIN_PATH="%OPENSEARCH_JAVA_HOME%\bin\java.exe" -) else if defined JAVA_HOME ( - set BIN_PATH="%JAVA_HOME%\bin\java.exe" -) else ( - echo Unable to find java runtime - echo OPENSEARCH_JAVA_HOME or JAVA_HOME must be defined - exit /b 1 -) - -%BIN_PATH% -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "%DIR%\..\*;%DIR%\..\..\..\lib\*;%DIR%\..\deps\*" org.opensearch.security.tools.SecurityAdmin %* 2> nul \ No newline at end of file +@echo off + +set OPENSEARCH_MAIN_CLASS=org.opensearch.security.tools.SecurityAdmin +set OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=plugins/opensearch-security + +rem Forward JAVA_OPTS into OPENSEARCH_JAVA_OPTS for backward compatibility +if defined JAVA_OPTS ( + set OPENSEARCH_JAVA_OPTS=%JAVA_OPTS% %OPENSEARCH_JAVA_OPTS% +) + +"%~dp0..\..\..\bin\opensearch-cli.bat" %* diff --git a/tools/securityadmin.sh b/tools/securityadmin.sh index 91fb8271c5..f170a928f0 100755 --- a/tools/securityadmin.sh +++ b/tools/securityadmin.sh @@ -1,29 +1,14 @@ -#!/bin/bash +#!/usr/bin/env bash -SCRIPT_PATH="${BASH_SOURCE[0]}" -if ! [ -x "$(command -v realpath)" ]; then - if [ -L "$SCRIPT_PATH" ]; then +set -e -o pipefail - [ -x "$(command -v readlink)" ] || { echo "Not able to resolve symlink. Install realpath or readlink.";exit 1; } +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" - # try readlink (-f not needed because we know its a symlink) - DIR="$( cd "$( dirname $(readlink "$SCRIPT_PATH") )" && pwd -P)" - else - DIR="$( cd "$( dirname "$SCRIPT_PATH" )" && pwd -P)" - fi -else - DIR="$( cd "$( dirname "$(realpath "$SCRIPT_PATH")" )" && pwd -P)" -fi +# Forward JAVA_OPTS into OPENSEARCH_JAVA_OPTS for backward compatibility +OPENSEARCH_JAVA_OPTS="${JAVA_OPTS:+${JAVA_OPTS} }${OPENSEARCH_JAVA_OPTS}" -BIN_PATH="java" - -# now set the path to java: first OPENSEARCH_JAVA_HOME, then JAVA_HOME -if [ ! -z "$OPENSEARCH_JAVA_HOME" ]; then - BIN_PATH="$OPENSEARCH_JAVA_HOME/bin/java" -elif [ ! -z "$JAVA_HOME" ]; then - BIN_PATH="$JAVA_HOME/bin/java" -else - echo "WARNING: nor OPENSEARCH_JAVA_HOME nor JAVA_HOME is set, will use $(which $BIN_PATH)" -fi - -"$BIN_PATH" $JAVA_OPTS -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "$DIR/../*:$DIR/../../../lib/*:$DIR/../deps/*" org.opensearch.security.tools.SecurityAdmin "$@" 2>/dev/null +OPENSEARCH_MAIN_CLASS=org.opensearch.security.tools.SecurityAdmin \ + OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=plugins/opensearch-security \ + OPENSEARCH_JAVA_OPTS="$OPENSEARCH_JAVA_OPTS" \ + "${SCRIPT_DIR}/../../../bin/opensearch-cli" \ + "$@"