diff --git a/CHANGELOG.md b/CHANGELOG.md index ac8aad99ffe64..982006c4bf6f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Add pluggable gRPC interceptors with explicit ordering([#19005](https://github.com/opensearch-project/OpenSearch/pull/19005)) - Add metrics for the merged segment warmer feature ([#18929](https://github.com/opensearch-project/OpenSearch/pull/18929)) - Add pointer based lag metric in pull-based ingestion ([#19635](https://github.com/opensearch-project/OpenSearch/pull/19635)) +- Add build-tooling to run in FIPS environment ([#18921](https://github.com/opensearch-project/OpenSearch/pull/18921)) ### Changed - Faster `terms` query creation for `keyword` field with index and docValues enabled ([#19350](https://github.com/opensearch-project/OpenSearch/pull/19350)) diff --git a/build.gradle b/build.gradle index 76c13a0456ffc..2c8d62f3f2b0f 100644 --- a/build.gradle +++ b/build.gradle @@ -71,6 +71,11 @@ apply from: 'gradle/run.gradle' apply from: 'gradle/missing-javadoc.gradle' apply from: 'gradle/code-coverage.gradle' +// Apply FIPS configuration to all projects +allprojects { + apply from: "$rootDir/gradle/fips.gradle" +} + // common maven publishing configuration allprojects { group = 'org.opensearch' @@ -421,8 +426,12 @@ gradle.projectsEvaluated { dependsOn(project(':libs:agent-sm:agent').prepareAgent) jvmArgs += ["-javaagent:" + project(':libs:agent-sm:agent').jar.archiveFile.get()] } - if (BuildParams.inFipsJvm) { - task.jvmArgs += ["-Dorg.bouncycastle.fips.approved_only=true"] + if (BuildParams.isInFipsJvm()) { + def fipsSecurityFile = project.rootProject.file('distribution/src/config/fips_java.security') + task.jvmArgs += [ + "-Dorg.bouncycastle.fips.approved_only=true", + "-Djava.security.properties=${fipsSecurityFile}" + ] } } } @@ -693,14 +702,6 @@ allprojects { plugins.withId('lifecycle-base') { checkPart1.configure { dependsOn 'check' } } - - plugins.withId('opensearch.testclusters') { - testClusters.configureEach { - if (BuildParams.inFipsJvm) { - keystorePassword 'notarealpasswordphrase' - } - } - } } subprojects { diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f5f565fcc8fe5..51360ee684a20 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -185,7 +185,7 @@ if (project != rootProject) { disableTasks('forbiddenApisMain', 'forbiddenApisTest', 'forbiddenApisIntegTest', 'forbiddenApisTestFixtures') jarHell.enabled = false thirdPartyAudit.enabled = false - if (org.opensearch.gradle.info.BuildParams.inFipsJvm) { + if (org.opensearch.gradle.info.BuildParams.isInFipsJvm()) { // We don't support running gradle with a JVM that is in FIPS 140 mode, so we don't test it. // WaitForHttpResourceTests tests would fail as they use JKS/PKCS12 keystores test.enabled = false @@ -255,7 +255,7 @@ if (project != rootProject) { tasks.register("integTest", Test) { inputs.dir(file("src/testKit")).withPropertyName("testkit dir").withPathSensitivity(PathSensitivity.RELATIVE) systemProperty 'test.version_under_test', version - onlyIf { org.opensearch.gradle.info.BuildParams.inFipsJvm == false } + onlyIf { org.opensearch.gradle.info.BuildParams.isInFipsJvm() == false } maxParallelForks = System.getProperty('tests.jvms', org.opensearch.gradle.info.BuildParams.defaultParallel.toString()) as Integer testClassesDirs = sourceSets.integTest.output.classesDirs classpath = sourceSets.integTest.runtimeClasspath diff --git a/buildSrc/src/main/groovy/org/opensearch/gradle/test/ClusterFormationTasks.groovy b/buildSrc/src/main/groovy/org/opensearch/gradle/test/ClusterFormationTasks.groovy index 0c88c33921309..0aafc332bf3e8 100644 --- a/buildSrc/src/main/groovy/org/opensearch/gradle/test/ClusterFormationTasks.groovy +++ b/buildSrc/src/main/groovy/org/opensearch/gradle/test/ClusterFormationTasks.groovy @@ -761,7 +761,7 @@ class ClusterFormationTasks { start.doLast(opensearchRunner) start.doFirst { // If the node runs in a FIPS 140-2 JVM, the BCFKS default keystore will be password protected - if (BuildParams.inFipsJvm) { + if (BuildParams.isInFipsJvm()) { node.config.systemProperties.put('javax.net.ssl.trustStorePassword', 'password') node.config.systemProperties.put('javax.net.ssl.keyStorePassword', 'password') } diff --git a/buildSrc/src/main/groovy/org/opensearch/gradle/test/StandaloneRestTestPlugin.groovy b/buildSrc/src/main/groovy/org/opensearch/gradle/test/StandaloneRestTestPlugin.groovy index 0d327ac31dbf5..9f64e7bde81cd 100644 --- a/buildSrc/src/main/groovy/org/opensearch/gradle/test/StandaloneRestTestPlugin.groovy +++ b/buildSrc/src/main/groovy/org/opensearch/gradle/test/StandaloneRestTestPlugin.groovy @@ -31,6 +31,8 @@ package org.opensearch.gradle.test import groovy.transform.CompileStatic +import org.gradle.api.artifacts.VersionCatalog +import org.gradle.api.artifacts.VersionCatalogsExtension import org.opensearch.gradle.OpenSearchJavaPlugin import org.opensearch.gradle.ExportOpenSearchBuildResourcesTask import org.opensearch.gradle.RepositoriesSetupPlugin @@ -92,6 +94,10 @@ class StandaloneRestTestPlugin implements Plugin { // create a compileOnly configuration as others might expect it project.configurations.create("compileOnly") project.dependencies.add('testImplementation', project.project(':test:framework')) + if (BuildParams.isInFipsJvm()) { + VersionCatalog libs = project.extensions.getByType(VersionCatalogsExtension).named("libs") + project.dependencies.add('testFipsRuntimeOnly', libs.findBundle("bouncycastle").get()) + } EclipseModel eclipse = project.extensions.getByType(EclipseModel) eclipse.classpath.sourceSets = [testSourceSet] diff --git a/buildSrc/src/main/java/org/opensearch/gradle/test/rest/RestTestUtil.java b/buildSrc/src/main/java/org/opensearch/gradle/test/rest/RestTestUtil.java index 061c477fab086..c122ce88d46df 100644 --- a/buildSrc/src/main/java/org/opensearch/gradle/test/rest/RestTestUtil.java +++ b/buildSrc/src/main/java/org/opensearch/gradle/test/rest/RestTestUtil.java @@ -102,6 +102,18 @@ static void setupDependencies(Project project, SourceSet sourceSet) { ); } + if (BuildParams.isInFipsJvm()) { + project.getDependencies() + .add( + sourceSet.getImplementationConfigurationName(), + "org.bouncycastle:bc-fips:" + VersionProperties.getVersions().get("bouncycastle_jce") + ); + project.getDependencies() + .add( + sourceSet.getImplementationConfigurationName(), + "org.bouncycastle:bctls-fips:" + VersionProperties.getVersions().get("bouncycastle_tls") + ); + } } } diff --git a/buildSrc/src/main/resources/opensearch-fips-truststore.bcfks b/buildSrc/src/main/resources/opensearch-fips-truststore.bcfks new file mode 100644 index 0000000000000..22f4b6c455576 Binary files /dev/null and b/buildSrc/src/main/resources/opensearch-fips-truststore.bcfks differ diff --git a/client/test/src/main/java/org/opensearch/client/BouncyCastleThreadFilter.java b/client/test/src/main/java/org/opensearch/client/BouncyCastleThreadFilter.java new file mode 100644 index 0000000000000..17e5554d1fa90 --- /dev/null +++ b/client/test/src/main/java/org/opensearch/client/BouncyCastleThreadFilter.java @@ -0,0 +1,25 @@ +/* + * 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.client; + +import com.carrotsearch.randomizedtesting.ThreadFilter; + +/** + * ThreadFilter to exclude ThreadLeak checks for BC’s global background threads + * + *

clone from the original, which is located in ':test:framework'

+ */ +public class BouncyCastleThreadFilter implements ThreadFilter { + @Override + public boolean reject(Thread t) { + String n = t.getName(); + // Ignore BC’s global background threads + return "BC Disposal Daemon".equals(n) || "BC Cleanup Executor".equals(n); + } +} diff --git a/client/test/src/main/java/org/opensearch/client/RestClientTestCase.java b/client/test/src/main/java/org/opensearch/client/RestClientTestCase.java index 9d52ee4cb3da7..e08a46f086e35 100644 --- a/client/test/src/main/java/org/opensearch/client/RestClientTestCase.java +++ b/client/test/src/main/java/org/opensearch/client/RestClientTestCase.java @@ -38,6 +38,7 @@ import com.carrotsearch.randomizedtesting.annotations.SeedDecorators; import com.carrotsearch.randomizedtesting.annotations.TestMethodProviders; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakAction; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakGroup; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; @@ -65,6 +66,7 @@ @ThreadLeakAction({ ThreadLeakAction.Action.WARN, ThreadLeakAction.Action.INTERRUPT }) @ThreadLeakZombies(ThreadLeakZombies.Consequence.IGNORE_REMAINING_TESTS) @ThreadLeakLingering(linger = 5000) // 5 sec lingering +@ThreadLeakFilters(filters = BouncyCastleThreadFilter.class) @TimeoutSuite(millis = 2 * 60 * 60 * 1000) public abstract class RestClientTestCase extends RandomizedTest { diff --git a/distribution/build.gradle b/distribution/build.gradle index 5de6fa0611ea0..a16d436f5603c 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -312,7 +312,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { * Properties to expand when copying packaging files * *****************************************************************************/ configurations { - ['libs', 'libsPluginCli', 'libsKeystoreCli', 'bcFips'].each { + ['libs', 'libsPluginCli', 'libsKeystoreCli', 'libsFipsInstallerCli', 'bcFips'].each { create(it) { canBeConsumed = false canBeResolved = true @@ -333,6 +333,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { libsPluginCli project(':distribution:tools:plugin-cli') libsKeystoreCli project(path: ':distribution:tools:keystore-cli') + libsFipsInstallerCli project(path: ':distribution:tools:fips-demo-installer-cli') bcFips libs.bundles.bouncycastle } @@ -346,7 +347,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { copySpec { // delay by using closures, since they have not yet been configured, so no jar task exists yet from(configurations.libs) - if ( BuildParams.inFipsJvm ) { + if ( BuildParams.isInFipsJvm() ) { from(configurations.bcFips) } into('tools/plugin-cli') { @@ -355,6 +356,10 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { into('tools/keystore-cli') { from(configurations.libsKeystoreCli) } + // Add FIPS installer CLI + into('tools/fips-demo-installer-cli') { + from(configurations.libsFipsInstallerCli) + } } } diff --git a/distribution/docker/build.gradle b/distribution/docker/build.gradle index ecc2d2c5c5766..ebbd9cb8895ba 100644 --- a/distribution/docker/build.gradle +++ b/distribution/docker/build.gradle @@ -295,3 +295,11 @@ subprojects { Project subProject -> tasks.named("composeUp").configure { dependsOn preProcessFixture } + +dockerCompose { + useComposeFiles = ['docker-compose.yml'] + if (BuildParams.isInFipsJvm()) { + environment.put("KEYSTORE_PASSWORD", "notarealpasswordphrase") + environment.put("FIPS_GENERATE_TRUSTSTORE", "true") + } +} diff --git a/distribution/docker/docker-compose.yml b/distribution/docker/docker-compose.yml index 5ed2b159ffe2b..4a0ee9426b105 100644 --- a/distribution/docker/docker-compose.yml +++ b/distribution/docker/docker-compose.yml @@ -16,6 +16,8 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - node.store.allow_mmap=false + - "KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD}" + - "FIPS_GENERATE_TRUSTSTORE=${FIPS_GENERATE_TRUSTSTORE}" volumes: - ./build/repo:/tmp/opensearch-repo - ./build/logs/1:/usr/share/opensearch/logs @@ -40,6 +42,8 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - node.store.allow_mmap=false + - "KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD}" + - "FIPS_GENERATE_TRUSTSTORE=${FIPS_GENERATE_TRUSTSTORE}" volumes: - ./build/repo:/tmp/opensearch-repo - ./build/logs/2:/usr/share/opensearch/logs diff --git a/distribution/docker/src/docker/bin/docker-entrypoint.sh b/distribution/docker/src/docker/bin/docker-entrypoint.sh index 59603462ac903..099d788c90dc8 100644 --- a/distribution/docker/src/docker/bin/docker-entrypoint.sh +++ b/distribution/docker/src/docker/bin/docker-entrypoint.sh @@ -74,19 +74,24 @@ if [[ -f bin/opensearch-users ]]; then fi if ls "/usr/share/opensearch/lib" | grep -E -q "bc-fips.*\.jar"; then - # If BouncyCastle FIPS is detected - enforcing keystore password policy. + # If BouncyCastle FIPS is detected - configure FIPS trust store in test mode + if [[ "$FIPS_GENERATE_TRUSTSTORE" == "true" ]]; then + (run_as_other_user_if_needed opensearch-fips-demo-installer --non-interactive) + fi + + # If BouncyCastle FIPS is detected - enforce keystore password policy. if [[ -z "$KEYSTORE_PASSWORD" ]]; then echo "[ERROR] FIPS mode requires a keystore password. KEYSTORE_PASSWORD is not set." >&2 exit 1 fi if [[ ! -f /usr/share/opensearch/config/opensearch.keystore ]]; then - # Keystore not found - creating with password. + # Keystore not found - create with password. COMMANDS="$(printf "%s\n%s" "$KEYSTORE_PASSWORD" "$KEYSTORE_PASSWORD")" echo "$COMMANDS" | run_as_other_user_if_needed opensearch-keystore create -p else - # Keystore already exists - checking encryption. + # Keystore already exists - check encryption. if ! run_as_other_user_if_needed opensearch-keystore has-passwd --silent; then # Keystore is unencrypted - securing it for FIPS mode. COMMANDS="$(printf "%s\n%s" "$KEYSTORE_PASSWORD" "$KEYSTORE_PASSWORD")" diff --git a/distribution/src/bin/opensearch-fips-demo-installer b/distribution/src/bin/opensearch-fips-demo-installer new file mode 100755 index 0000000000000..c5f4d6363ec9e --- /dev/null +++ b/distribution/src/bin/opensearch-fips-demo-installer @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +OPENSEARCH_MAIN_CLASS=org.opensearch.tools.cli.fips.truststore.FipsTrustStoreCommand \ + OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/fips-demo-installer-cli \ + "`dirname "$0"`"/opensearch-cli \ + "$@" diff --git a/distribution/src/bin/opensearch-fips-demo-installer.bat b/distribution/src/bin/opensearch-fips-demo-installer.bat new file mode 100644 index 0000000000000..9e6119083b24b --- /dev/null +++ b/distribution/src/bin/opensearch-fips-demo-installer.bat @@ -0,0 +1,16 @@ +@echo off + +setlocal enabledelayedexpansion +setlocal enableextensions + +set OPENSEARCH_MAIN_CLASS=org.opensearch.tools.cli.fips.truststore.FipsTrustStoreCommand +set OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES=lib/tools/fips-demo-installer-cli + +call "%~dp0opensearch-cli.bat" ^ + %%* ^ + || goto exit + +endlocal +endlocal +:exit +exit /b %ERRORLEVEL% diff --git a/distribution/tools/fips-demo-installer-cli/build.gradle b/distribution/tools/fips-demo-installer-cli/build.gradle new file mode 100644 index 0000000000000..fe5ce9b798353 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/build.gradle @@ -0,0 +1,36 @@ +/* + * 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. + */ + +import de.thetaphi.forbiddenapis.gradle.CheckForbiddenApis + +apply plugin: 'opensearch.build' + +dependencies { + api project(":libs:opensearch-cli") + api project(":server") + api project(':distribution:tools:java-version-checker') + api "info.picocli:picocli:${versions.picocli}" + api "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}" + + testImplementation project(":test:framework") +} + +configurations.runtimeClasspath { + exclude group: 'org.apache.logging.log4j' +} + +tasks.withType(CheckForbiddenApis).configureEach { + replaceSignatureFiles 'jdk-signatures' +} + +tasks.named("dependencyLicenses").configure { + mapping from: /bc.*/, to: 'bouncycastle' +} + +tasks.named("missingJavadoc").configure { it.enabled = false } +tasks.named("loggerUsageCheck").configure { it.enabled = false } diff --git a/distribution/tools/fips-demo-installer-cli/licenses/bc-fips-2.1.1.jar.sha1 b/distribution/tools/fips-demo-installer-cli/licenses/bc-fips-2.1.1.jar.sha1 new file mode 100644 index 0000000000000..831a41da72aa5 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/licenses/bc-fips-2.1.1.jar.sha1 @@ -0,0 +1 @@ +34c72d0367d41672883283933ebec24843570bf5 \ No newline at end of file diff --git a/distribution/tools/fips-demo-installer-cli/licenses/bouncycastle-LICENSE.txt b/distribution/tools/fips-demo-installer-cli/licenses/bouncycastle-LICENSE.txt new file mode 100644 index 0000000000000..1bd35a7a35c21 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/licenses/bouncycastle-LICENSE.txt @@ -0,0 +1,17 @@ +Copyright (c) 2000-2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/distribution/tools/fips-demo-installer-cli/licenses/bouncycastle-NOTICE.txt b/distribution/tools/fips-demo-installer-cli/licenses/bouncycastle-NOTICE.txt new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/licenses/bouncycastle-NOTICE.txt @@ -0,0 +1 @@ + diff --git a/distribution/tools/fips-demo-installer-cli/licenses/picocli-4.7.7.jar.sha1 b/distribution/tools/fips-demo-installer-cli/licenses/picocli-4.7.7.jar.sha1 new file mode 100644 index 0000000000000..f2289f8892807 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/licenses/picocli-4.7.7.jar.sha1 @@ -0,0 +1 @@ +82bcae3dc45ddeb08b4954e2f596772a42219715 \ No newline at end of file diff --git a/distribution/tools/fips-demo-installer-cli/licenses/picocli-LICENSE.txt b/distribution/tools/fips-demo-installer-cli/licenses/picocli-LICENSE.txt new file mode 100644 index 0000000000000..8dada3edaf50d --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/licenses/picocli-LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/distribution/tools/fips-demo-installer-cli/licenses/picocli-NOTICE.txt b/distribution/tools/fips-demo-installer-cli/licenses/picocli-NOTICE.txt new file mode 100644 index 0000000000000..160c635812574 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/licenses/picocli-NOTICE.txt @@ -0,0 +1,351 @@ +This project includes one or more documentation files from OpenJDK, licensed under GPL v2 with Classpath Exception. + +These files are included in the source distributions, not in the binary distributions of this project. + +The GNU General Public License (GPL) + +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Everyone is permitted to copy and distribute verbatim copies of this license +document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share +and change it. By contrast, the GNU General Public License is intended to +guarantee your freedom to share and change free software--to make sure the +software is free for all its users. This General Public License applies to +most of the Free Software Foundation's software and to any other program whose +authors commit to using it. (Some other Free Software Foundation software is +covered by the GNU Library General Public License instead.) You can apply it to +your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for this service if you wish), +that you receive source code or can get it if you want it, that you can change +the software or use pieces of it in new free programs; and that you know you +can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny +you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. You must +make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients to +know that what they have is not the original, so that any problems introduced +by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will +individually obtain patent licenses, in effect making the program proprietary. +To prevent this, we have made it clear that any patent must be licensed for +everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms of +this General Public License. The "Program", below, refers to any such program +or work, and a "work based on the Program" means either the Program or any +derivative work under copyright law: that is to say, a work containing the +Program or a portion of it, either verbatim or with modifications and/or +translated into another language. (Hereinafter, translation is included +without limitation in the term "modification".) Each licensee is addressed as +"you". + +Activities other than copying, distribution and modification are not covered by +this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made by +running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as +you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may +at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus +forming a work based on the Program, and copy and distribute such modifications +or work under the terms of Section 1 above, provided that you also meet all of +these conditions: + + a) You must cause the modified files to carry prominent notices stating + that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or + in part contains or is derived from the Program or any part thereof, to be + licensed as a whole at no charge to all third parties under the terms of + this License. + + c) If the modified program normally reads commands interactively when run, + you must cause it, when started running for such interactive use in the + most ordinary way, to print or display an announcement including an + appropriate copyright notice and a notice that there is no warranty (or + else, saying that you provide a warranty) and that users may redistribute + the program under these conditions, and telling the user how to view a copy + of this License. (Exception: if the Program itself is interactive but does + not normally print such an announcement, your work based on the Program is + not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be reasonably +considered independent and separate works in themselves, then this License, and +its terms, do not apply to those sections when you distribute them as separate +works. But when you distribute the same sections as part of a whole which is a +work based on the Program, the distribution of the whole must be on the terms +of this License, whose permissions for other licensees extend to the entire +whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your +rights to work written entirely by you; rather, the intent is to exercise the +right to control the distribution of derivative or collective works based on +the Program. + +In addition, mere aggregation of another work not based on the Program with the +Program (or with a work based on the Program) on a volume of a storage or +distribution medium does not bring the other work under the scope of this +License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 and +2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source + code, which must be distributed under the terms of Sections 1 and 2 above + on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to + give any third party, for a charge no more than your cost of physically + performing source distribution, a complete machine-readable copy of the + corresponding source code, to be distributed under the terms of Sections 1 + and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to + distribute corresponding source code. (This alternative is allowed only + for noncommercial distribution and only if you received the program in + object code or executable form with such an offer, in accord with + Subsection b above.) + +The source code for a work means the preferred form of the work for making +modifications to it. For an executable work, complete source code means all +the source code for all modules it contains, plus any associated interface +definition files, plus the scripts used to control compilation and installation +of the executable. However, as a special exception, the source code +distributed need not include anything that is normally distributed (in either +source or binary form) with the major components (compiler, kernel, and so on) +of the operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the source +code from the same place counts as distribution of the source code, even though +third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, modify, +sublicense or distribute the Program is void, and will automatically terminate +your rights under this License. However, parties who have received copies, or +rights, from you under this License will not have their licenses terminated so +long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the Program +or its derivative works. These actions are prohibited by law if you do not +accept this License. Therefore, by modifying or distributing the Program (or +any work based on the Program), you indicate your acceptance of this License to +do so, and all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), +the recipient automatically receives a license from the original licensor to +copy, distribute or modify the Program subject to these terms and conditions. +You may not impose any further restrictions on the recipients' exercise of the +rights granted herein. You are not responsible for enforcing compliance by +third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), conditions +are imposed on you (whether by court order, agreement or otherwise) that +contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose that +choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original +copyright holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In +such case, this License incorporates the limitation as if written in the body +of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any later +version", you have the option of following the terms and conditions either of +that version or of any later version published by the Free Software Foundation. +If the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software Foundation, +write to the Free Software Foundation; we sometimes make exceptions for this. +Our decision will be guided by the two goals of preserving the free status of +all derivatives of our free software and of promoting the sharing and reuse of +software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR +THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE +PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, +YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL +ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE +PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR +INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA +BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER +OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible +use to the public, the best way to achieve this is to make it free software +which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively convey the exclusion +of warranty; and each file should have at least the "copyright" line and a +pointer to where the full notice is found. + + One line to give the program's name and a brief idea of what it does. + + Copyright (C) + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., 59 + Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it +starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes + with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free + software, and you are welcome to redistribute it under certain conditions; + type 'show c' for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may be +called something other than 'show w' and 'show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, +if any, to sign a "copyright disclaimer" for the program, if necessary. Here +is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + 'Gnomovision' (which makes passes at compilers) written by James Hacker. + + signature of Ty Coon, 1 April 1989 + + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General Public +License instead of this License. + + +"CLASSPATH" EXCEPTION TO THE GPL + +Certain source files distributed by Oracle America and/or its affiliates are +subject to the following clarification and special exception to the GPL, but +only where Oracle has expressly included in the particular source file's header +the words "Oracle designates this particular file as subject to the "Classpath" +exception as provided by Oracle in the LICENSE file that accompanied this code." + + Linking this library statically or dynamically with other modules is making + a combined work based on this library. Thus, the terms and conditions of + the GNU General Public License cover the whole combination. + + As a special exception, the copyright holders of this library give you + permission to link this library with independent modules to produce an + executable, regardless of the license terms of these independent modules, + and to copy and distribute the resulting executable under terms of your + choice, provided that you also meet, for each linked independent module, + the terms and conditions of the license of that module. An independent + module is a module which is not derived from or based on this library. If + you modify this library, you may extend this exception to your version of + the library, but you are not obligated to do so. If you do not wish to do + so, delete this exception statement from your version. diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/CommonOptions.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/CommonOptions.java new file mode 100644 index 0000000000000..3651b3425ea8e --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/CommonOptions.java @@ -0,0 +1,27 @@ +/* + * 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.tools.cli.fips.truststore; + +import picocli.CommandLine.Option; + +/** + * Common command-line options shared across FIPS trust store commands. + */ +public class CommonOptions { + + @Option(names = { "-n", "--non-interactive" }, description = "Run in non-interactive mode (use defaults, no prompts)") + boolean nonInteractive; + + @Option(names = { "-f", "--force" }, description = "Force installation even if FIPS demo configuration already exists") + boolean force; + + @Option(names = { "-p", "--password" }, description = "Password for the BCFKS trust store. " + + "In non-interactive mode, this overrides auto-generated password.", arity = "1") + String password; +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigurationProperties.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigurationProperties.java new file mode 100644 index 0000000000000..e6d7564c0236b --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigurationProperties.java @@ -0,0 +1,48 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.util.Locale; + +/** + * Encapsulates properties related to a TrustStore configuration. Is primarily used to manage and + * log details of TrustStore configurations within the system. + * + * @param trustStorePath path to the trust store file + * @param trustStoreType type of the trust store (e.g., BCFKS, PKCS11) + * @param trustStorePassword password for the trust store + * @param trustStoreProvider security provider name for the trust store + */ +record ConfigurationProperties(String trustStorePath, String trustStoreType, String trustStorePassword, String trustStoreProvider) { + + public static final String JAVAX_NET_SSL_TRUST_STORE = "javax.net.ssl.trustStore"; + public static final String JAVAX_NET_SSL_TRUST_STORE_TYPE = "javax.net.ssl.trustStoreType"; + public static final String JAVAX_NET_SSL_TRUST_STORE_PROVIDER = "javax.net.ssl.trustStoreProvider"; + public static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword"; + + @Override + public String toString() { + return String.format( + Locale.ROOT, + """ + %s: %s + %s: %s + %s: %s + %s: %s""", + JAVAX_NET_SSL_TRUST_STORE, + trustStorePath, + JAVAX_NET_SSL_TRUST_STORE_TYPE, + trustStoreType, + JAVAX_NET_SSL_TRUST_STORE_PROVIDER, + trustStoreProvider, + JAVAX_NET_SSL_TRUST_STORE_PASSWORD, + trustStorePassword.isEmpty() ? "[NOT SET]" : "[SET]" + ); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigurationService.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigurationService.java new file mode 100644 index 0000000000000..e0398969cf3cb --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigurationService.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. + */ + +package org.opensearch.tools.cli.fips.truststore; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Map; + +import picocli.CommandLine; + +import static org.opensearch.tools.cli.fips.truststore.ConfigurationProperties.JAVAX_NET_SSL_TRUST_STORE; +import static org.opensearch.tools.cli.fips.truststore.ConfigurationProperties.JAVAX_NET_SSL_TRUST_STORE_PASSWORD; +import static org.opensearch.tools.cli.fips.truststore.ConfigurationProperties.JAVAX_NET_SSL_TRUST_STORE_PROVIDER; +import static org.opensearch.tools.cli.fips.truststore.ConfigurationProperties.JAVAX_NET_SSL_TRUST_STORE_TYPE; + +/** + * Service for managing OpenSearch configuration files. + * Handles verification and writing of JVM options and security configurations. + */ +public class ConfigurationService { + + /** + * Verifies that the jvm.options file exists, is readable, and doesn't contain existing FIPS configuration. + * + * @param spec the command specification for output + * @param options common command-line options + * @param confPath path to the OpenSearch configuration directory + * @throws IllegalStateException if validation fails + */ + public static void verifyJvmOptionsFile(CommandLine.Model.CommandSpec spec, CommonOptions options, Path confPath) { + var jvmOptionsFile = confPath.resolve("jvm.options"); + if (options.force) { + var ansi = spec.commandLine().getColorScheme().ansi(); + spec.commandLine().getOut().println(ansi.string("@|yellow WARNING: Force mode enabled, skipping configuration checks.|@")); + return; + } + + if (!Files.exists(jvmOptionsFile)) { + throw new IllegalStateException("jvm.options file does not exist: " + jvmOptionsFile); + } + + if (!Files.isReadable(jvmOptionsFile)) { + throw new IllegalStateException("jvm.options file is not readable: " + jvmOptionsFile); + } + + validateJvmOptionsContent(jvmOptionsFile); + } + + /** + * Validates that the jvm.options file doesn't already contain FIPS trust store properties. + * + * @param jvmOptionsFile path to the jvm.options file + * @throws IllegalStateException if FIPS configuration already exists + */ + protected static void validateJvmOptionsContent(Path jvmOptionsFile) { + try { + var size = Files.size(jvmOptionsFile); + if (size == 0) { + throw new IllegalStateException("jvm.options file is empty: " + jvmOptionsFile); + } + + var content = Files.readString(jvmOptionsFile, StandardCharsets.UTF_8); + + String[] fipsProperties = { + "-D" + JAVAX_NET_SSL_TRUST_STORE, + "-D" + JAVAX_NET_SSL_TRUST_STORE_PASSWORD, + "-D" + JAVAX_NET_SSL_TRUST_STORE_TYPE, + "-D" + JAVAX_NET_SSL_TRUST_STORE_PROVIDER, }; + + for (String property : fipsProperties) { + if (content.contains(property)) { + throw new IllegalStateException( + "FIPS demo configuration already exists in jvm.options. " + + "Found: '" + + property + + "'. " + + "Please remove existing configuration before running this installer, or use the '--force option'" + ); + } + } + } catch (IOException e) { + throw new IllegalStateException("Failed to read jvm.options file: " + e.getMessage(), e); + } + } + + /** + * Writes FIPS trust store configuration to the jvm.options file. + * + * @param properties the trust store configuration properties to write + * @param confPath path to the OpenSearch configuration directory + * @throws RuntimeException if writing fails + */ + public void writeSecurityConfigToJvmOptionsFile(ConfigurationProperties properties, Path confPath) { + var jvmOptionsFile = confPath.resolve("jvm.options"); + var configHeader = """ + + ################################################################ + ## Start OpenSearch FIPS Demo Configuration + ## WARNING: revise all the lines below before you go into production + ################################################################ + + """; + var configFooter = """ + ################################################################ + """; + + try { + var configBuilder = new StringBuilder(configHeader); + + // Write each configuration as -Dkey=value format + var configMap = Map.of( + JAVAX_NET_SSL_TRUST_STORE, + properties.trustStorePath(), + JAVAX_NET_SSL_TRUST_STORE_PASSWORD, + properties.trustStorePassword(), + JAVAX_NET_SSL_TRUST_STORE_TYPE, + properties.trustStoreType(), + JAVAX_NET_SSL_TRUST_STORE_PROVIDER, + properties.trustStoreProvider() + ); + + for (Map.Entry entry : configMap.entrySet()) { + configBuilder.append("-D").append(entry.getKey()).append("=").append(entry.getValue()); + configBuilder.append(System.lineSeparator()); + } + + configBuilder.append(configFooter); + + Files.writeString( + jvmOptionsFile, + configBuilder.toString(), + StandardCharsets.UTF_8, + StandardOpenOption.APPEND, + StandardOpenOption.CREATE + ); + } catch (IOException e) { + throw new RuntimeException("Exception writing security configuration to jvm.options: " + e.getMessage(), e); + } + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigureSystemTrustStore.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigureSystemTrustStore.java new file mode 100644 index 0000000000000..3098fd4361a65 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ConfigureSystemTrustStore.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. + */ + +package org.opensearch.tools.cli.fips.truststore; + +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import static org.opensearch.tools.cli.fips.truststore.ConfigurationProperties.JAVAX_NET_SSL_TRUST_STORE_PASSWORD; + +/** + * The ConfigureSystemTrustStore class provides methods to interact with and configure the system + * trust store for PKCS#11 providers. + */ +public class ConfigureSystemTrustStore { + + private static final String PKCS_11 = "PKCS11"; + private static final String TRUST_STORE_PASSWORD = Security.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD); + private static final String SYSTEMSTORE_PASSWORD = Objects.requireNonNullElse(TRUST_STORE_PASSWORD, ""); + + /** + * Finds all available PKCS11 KeyStore provider services in the security environment. + * + * @return list of PKCS11 KeyStore provider services + */ + public static List findPKCS11ProviderService() { + return Arrays.stream(Security.getProviders()) + .filter(it -> it.getName().toUpperCase(Locale.ROOT).contains(PKCS_11)) + .map(it -> it.getService("KeyStore", PKCS_11)) + .filter(Objects::nonNull) + .toList(); + } + + /** + * Creates configuration properties for a PKCS11-based trust store. + * + * @param pkcs11ProviderService the PKCS11 provider service to configure + * @return configuration properties for the PKCS11 trust store + */ + public static ConfigurationProperties configurePKCS11TrustStore(Provider.Service pkcs11ProviderService) { + return new ConfigurationProperties("NONE", PKCS_11, SYSTEMSTORE_PASSWORD, pkcs11ProviderService.getProvider().getName()); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/CreateFipsTrustStore.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/CreateFipsTrustStore.java new file mode 100644 index 0000000000000..1aebb980af003 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/CreateFipsTrustStore.java @@ -0,0 +1,193 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Collectors; + +import picocli.CommandLine; + +/** + * Utility class for creating FIPS-compliant trust stores. + * Converts JVM default trust stores to BCFKS format for FIPS compliance. + */ +public class CreateFipsTrustStore { + + private static final String JAVAX_NET_SSL_TRUST_STORE_PASSWORD = "javax.net.ssl.trustStorePassword"; + private static final String TRUST_STORE_PASSWORD = Security.getProperty(JAVAX_NET_SSL_TRUST_STORE_PASSWORD); + private static final String JVM_DEFAULT_PASSWORD = Objects.requireNonNullElse(TRUST_STORE_PASSWORD, "changeit"); + private static final String BCFKS = "BCFKS"; + private static final String BCFIPS = "BCFIPS"; + private static final List KNOWN_JDK_TRUSTSTORE_TYPES = List.of("PKCS12", "JKS"); + + /** + * Loads the JVM's default trust store (cacerts) from the specified Java home directory. + * + * @param spec the command specification for output + * @param javaHome path to the Java home directory + * @return the loaded KeyStore containing system certificates + * @throws IllegalStateException if cacerts file cannot be found or loaded + */ + public static KeyStore loadJvmDefaultTrustStore(CommandLine.Model.CommandSpec spec, Path javaHome) { + var cacertsPath = javaHome.resolve("lib").resolve("security").resolve("cacerts"); + if (!Files.exists(cacertsPath) || !Files.isReadable(cacertsPath)) { + throw new IllegalStateException("System cacerts not found at: " + cacertsPath); + } + + spec.commandLine().getOut().println("Loading system truststore from: " + cacertsPath); + + KeyStore jvmKeyStore = null; + for (var type : KNOWN_JDK_TRUSTSTORE_TYPES) { + try { + jvmKeyStore = KeyStore.getInstance(type); + try (var is = Files.newInputStream(cacertsPath)) { + jvmKeyStore.load(is, JVM_DEFAULT_PASSWORD.toCharArray()); + } + int certCount = jvmKeyStore.size(); + spec.commandLine().getOut().println("Loaded " + certCount + " certificates from system truststore"); + spec.commandLine().getOut().println("Successfully loaded cacerts as " + type + " format"); + break; + } catch (Exception e) { + jvmKeyStore = null; + // continue + } + } + + if (jvmKeyStore == null) { + throw new IllegalStateException( + "Could not load system cacerts in any known format " + + KNOWN_JDK_TRUSTSTORE_TYPES.stream().collect(Collectors.joining(", ", "[", "]")) + ); + } + + return jvmKeyStore; + } + + /** + * Creates configuration properties for a BCFKS trust store. + * + * @param bcfksPath path to the BCFKS trust store file + * @param password password for the trust store + * @return configuration properties for the BCFKS trust store + */ + static ConfigurationProperties configureBCFKSTrustStore(Path bcfksPath, String password) { + return new ConfigurationProperties(bcfksPath.toAbsolutePath().toString(), BCFKS, password, BCFIPS); + } + + /** + * Converts a source KeyStore to BCFKS format for FIPS compliance. + * + * @param spec the command specification for output + * @param sourceKeyStore the source KeyStore to convert + * @param options common command-line options + * @param password password for the new BCFKS trust store + * @param confPath path to the OpenSearch configuration directory + * @return path to the created BCFKS trust store file + * @throws RuntimeException if conversion fails + */ + public static Path convertToBCFKS( + CommandLine.Model.CommandSpec spec, + KeyStore sourceKeyStore, + CommonOptions options, + String password, + Path confPath + ) { + Path trustStorePath = confPath.resolve("opensearch-fips-truststore.bcfks"); + + if (Files.exists(trustStorePath)) { + if (options.force) { + try { + Files.delete(trustStorePath); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } else { + throw new RuntimeException("Operation cancelled. Trust store file already exists."); + } + } + + spec.commandLine().getOut().println("Converting to BCFKS format: " + trustStorePath.toAbsolutePath()); + + int copiedCount = 0; + KeyStore bcfksKeyStore; + try { + bcfksKeyStore = KeyStore.getInstance(BCFKS, BCFIPS); + bcfksKeyStore.load(null, password.toCharArray()); + + copyCerts(spec, sourceKeyStore, bcfksKeyStore, copiedCount); + writeBCFKSKeyStoreToFile(bcfksKeyStore, trustStorePath, password); + + spec.commandLine() + .getOut() + .printf( + Locale.ROOT, + "Successfully converted %s/%s certificates to BCFKS format.%n", + bcfksKeyStore.size(), + sourceKeyStore.size() + ); + + if (sourceKeyStore.size() > bcfksKeyStore.size()) { + spec.commandLine() + .getOut() + .printf( + Locale.ROOT, + "%s certificates could not be converted to BCFKS format.%n", + sourceKeyStore.size() - bcfksKeyStore.size() + ); + } + } catch (GeneralSecurityException | IOException e) { + throw new SecurityException(e); + } + + return trustStorePath; + } + + private static void copyCerts(CommandLine.Model.CommandSpec spec, KeyStore source, KeyStore target, int copiedCount) + throws KeyStoreException { + var aliases = source.aliases(); + + while (aliases.hasMoreElements()) { + var alias = aliases.nextElement(); + + if (source.isCertificateEntry(alias)) { + var cert = source.getCertificate(alias); + if (cert != null) { + try { + target.setCertificateEntry(alias, cert); + copiedCount++; + } catch (Exception e) { + spec.commandLine().getOut().printf(Locale.ROOT, "Failed to copy certificate '%s': %s%n", alias, e.getMessage()); + // Continue with other certificates + } + } + } + } + } + + private static void writeBCFKSKeyStoreToFile(KeyStore bcfksKeyStore, Path tempBcfksFile, String password) { + try (var outputStream = Files.newOutputStream(tempBcfksFile)) { + bcfksKeyStore.store(outputStream, password.toCharArray()); + } catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) { + throw new IllegalStateException("Failed to write BCFKS keystore to [" + tempBcfksFile + "]: " + e.getMessage(), e); + } + } + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommand.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommand.java new file mode 100644 index 0000000000000..410f6c127b222 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommand.java @@ -0,0 +1,88 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.cli.SuppressForbidden; + +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.Callable; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Help.Ansi.Style; +import picocli.CommandLine.Help.ColorScheme; +import picocli.CommandLine.Unmatched; + +/** + * Main command-line interface for the OpenSearch FIPS Demo Configuration Installer. + */ +@Command(name = "opensearch-fips-demo-installer", description = "OpenSearch FIPS Demo Configuration Installer", subcommands = { + GeneratedTrustStoreCommand.class, + SystemTrustStoreCommand.class, + ShowProvidersCommand.class }, mixinStandardHelpOptions = true, usageHelpAutoWidth = true, headerHeading = "Usage:%n", synopsisHeading = "%n", descriptionHeading = "%nDescription:%n", parameterListHeading = "%nParameters:%n", optionListHeading = "%nOptions:%n", commandListHeading = "%nCommands:%n") +public class FipsTrustStoreCommand implements Callable { + + private final static ColorScheme COLOR_SCHEME = new ColorScheme.Builder().commands(Style.bold, Style.underline) + .options(Style.fg_yellow) + .parameters(Style.fg_yellow) + .optionParams(Style.italic) + .errors(Style.fg_red, Style.bold) + .stackTraces(Style.italic) + .applySystemProperties() + .build(); + + @CommandLine.Spec + protected CommandLine.Model.CommandSpec spec; + + @CommandLine.Mixin + protected CommonOptions common; + + @Unmatched + @SuppressWarnings("unused") + private List unmatchedArgs; + + /** + * Executes the main command logic, presenting an interactive trust store configuration menu. + * + * @return exit code (0 for success, 1 for failure) + */ + @Override + public final Integer call() { + // workaround for opensearch-env logic that scans all environment variables, + // converts them to e.g. `-Ediscovery.type=single-node` and passes through as additional JVM params. + if (unmatchedArgs != null && !unmatchedArgs.isEmpty()) { + spec.commandLine() + .getOut() + .println( + spec.commandLine() + .getColorScheme() + .ansi() + .string("@|yellow Warning: Ignoring unrecognized arguments: " + unmatchedArgs + "|@") + ); + } + + var confPath = Path.of(System.getProperty("opensearch.path.conf")); + ConfigurationService.verifyJvmOptionsFile(spec, common, confPath); + return new TrustStoreService(UserInteractionService.getInstance()).executeInteractiveSelection(spec, common, confPath); + } + + /** + * Main entry point for the FIPS demo installer CLI. + * + * @param args command-line arguments + */ + @SuppressForbidden(reason = "Allowed to exit explicitly from #main()") + public static void main(String[] args) { + int exitCode = new CommandLine(new FipsTrustStoreCommand()).setColorScheme(COLOR_SCHEME) + .setExecutionExceptionHandler(new FipsTrustStoreExceptionHandler()) + .execute(args); + System.exit(exitCode); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreExceptionHandler.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreExceptionHandler.java new file mode 100644 index 0000000000000..6239618200c14 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreExceptionHandler.java @@ -0,0 +1,29 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.util.Optional; + +import picocli.CommandLine; + +/** + * Catches exceptions during command execution and prints only the error + * message in a formatted way, making the CLI output cleaner and more user-friendly. + */ +public class FipsTrustStoreExceptionHandler implements CommandLine.IExecutionExceptionHandler { + + @Override + public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { + var errorMessage = Optional.ofNullable(ex.getMessage()).orElse("[null]"); + commandLine.getErr().println(commandLine.getColorScheme().errorText("Error: " + errorMessage)); + + // Return the configured error exit code + return commandLine.getCommandSpec().exitCodeOnExecutionException(); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/GeneratedTrustStoreCommand.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/GeneratedTrustStoreCommand.java new file mode 100644 index 0000000000000..a874b3f69bed6 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/GeneratedTrustStoreCommand.java @@ -0,0 +1,37 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import picocli.CommandLine; +import picocli.CommandLine.Command; + +/** + * Command for generating a new BCFKS trust store from the JVM default trust store. + * Provides a CLI interface for trust store generation operations. + */ +@Command(name = "generated", description = "Generate a new BCFKS trust store from JVM default trust store", mixinStandardHelpOptions = true) +public class GeneratedTrustStoreCommand implements Callable { + + @CommandLine.Spec + protected CommandLine.Model.CommandSpec spec; + + @CommandLine.Mixin + protected CommonOptions common; + + @Override + public final Integer call() throws Exception { + var confPath = Path.of(System.getProperty("opensearch.path.conf")); + ConfigurationService.verifyJvmOptionsFile(spec, common, confPath); + return new TrustStoreService(UserInteractionService.getInstance()).generateTrustStore(spec, common, confPath); + } + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ProviderSelectionService.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ProviderSelectionService.java new file mode 100644 index 0000000000000..4fa9be6b802b0 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ProviderSelectionService.java @@ -0,0 +1,116 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.security.Provider; +import java.util.List; +import java.util.Optional; + +import picocli.CommandLine; + +/** + * Service for selecting appropriate security providers. + * Handles interactive and non-interactive provider selection for PKCS11 configurations. + */ +public class ProviderSelectionService { + + private final UserInteractionService userInteraction; + + public ProviderSelectionService(UserInteractionService userInteraction) { + this.userInteraction = userInteraction; + } + + /** + * Selects a PKCS11 provider service based on configuration and user interaction. + * + * @param spec the command specification for output + * @param options common command-line options + * @param serviceProviderList list of available PKCS11 provider services + * @param preselectedPKCS11Provider optional pre-selected provider name + * @return the selected provider service + * @throws IllegalStateException if no providers are available + * @throws IllegalArgumentException if pre-selected provider is not found + * @throws RuntimeException if user cancels the operation + */ + public Provider.Service selectProvider( + CommandLine.Model.CommandSpec spec, + CommonOptions options, + List serviceProviderList, + String preselectedPKCS11Provider + ) { + if (serviceProviderList.isEmpty()) { + throw new IllegalStateException("No PKCS11 providers available. Please ensure a PKCS11 provider is installed and configured."); + } + + if (preselectedPKCS11Provider != null) { + Optional found = serviceProviderList.stream() + .filter(service -> service.getProvider().getName().equals(preselectedPKCS11Provider)) + .findFirst(); + + if (found.isPresent()) { + return found.get(); + } else { + var err = spec.commandLine().getErr(); + err.println( + spec.commandLine() + .getColorScheme() + .errorText("ERROR: Specified PKCS11 provider '" + preselectedPKCS11Provider + "' not found.") + ); + err.println(spec.commandLine().getColorScheme().errorText("Available providers:")); + serviceProviderList.forEach( + service -> err.println(spec.commandLine().getColorScheme().errorText(" - " + service.getProvider().getName())) + ); + throw new IllegalArgumentException("Provider not found: " + preselectedPKCS11Provider); + } + } + + // Non-interactive mode: always use first available provider + if (options.nonInteractive) { + var service = serviceProviderList.get(0); + spec.commandLine().getOut().println("Using PKCS11 provider: " + service.getProvider().getName()); + return service; + } + + // Interactive mode: single provider requires confirmation + if (serviceProviderList.size() == 1) { + var service = serviceProviderList.get(0); + var providerName = service.getProvider().getName(); + if (userInteraction.confirmAction(spec, options, "Use PKCS11 provider '" + providerName + "'?")) { + return service; + } else { + return null; + } + } + + // Interactive mode: multiple providers - let user choose + return selectProviderInteractively(spec, serviceProviderList); + } + + /** + * Prompts the user to select a provider from multiple available options. + * + * @param spec the command specification for output + * @param serviceProviderList list of available PKCS11 provider services + * @return the user-selected provider service, or null if operation is cancelled + */ + protected Provider.Service selectProviderInteractively(CommandLine.Model.CommandSpec spec, List serviceProviderList) { + var out = spec.commandLine().getOut(); + out.println("Multiple PKCS11 providers found:"); + for (int i = 0; i < serviceProviderList.size(); i++) { + var service = serviceProviderList.get(i); + out.println(" " + (i + 1) + ". " + service.getProvider().getName() + " (Algorithm: " + service.getAlgorithm() + ")"); + } + out.println("Select PKCS11 provider"); + + var choice = userInteraction.promptForChoice(spec, serviceProviderList.size(), 1); + + return serviceProviderList.get(choice - 1); + } + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/SecurityProviderService.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/SecurityProviderService.java new file mode 100644 index 0000000000000..32ab071ddfaba --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/SecurityProviderService.java @@ -0,0 +1,54 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.Security; +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +import picocli.CommandLine; + +/** + * Service for displaying information about available security providers. + * Provides utilities to inspect and report on the security environment. + */ +public class SecurityProviderService { + + /** + * Prints current security provider configuration to the console. + * + * @param spec the command specification for output + */ + public static void printCurrentConfiguration(CommandLine.Model.CommandSpec spec) { + var detailLog = new StringWriter(); + var writer = new PrintWriter(detailLog); + var counter = new AtomicInteger(); + + writer.println("Available Security Providers:"); + Arrays.stream(Security.getProviders()) + .peek( + provider -> writer.printf( + Locale.ROOT, + " %d. %s (version %s)\n", + counter.incrementAndGet(), + provider.getName(), + provider.getVersionStr() + ) + ) + .flatMap(provider -> provider.getServices().stream().filter(service -> "KeyStore".equals(service.getType()))) + .forEach(service -> writer.printf(Locale.ROOT, " └─ KeyStore.%s\n", service.getAlgorithm())); + + writer.flush(); + spec.commandLine().getOut().println(detailLog); + } + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ShowProvidersCommand.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ShowProvidersCommand.java new file mode 100644 index 0000000000000..a5c4994a39244 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/ShowProvidersCommand.java @@ -0,0 +1,31 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.util.concurrent.Callable; + +import picocli.CommandLine; +import picocli.CommandLine.Command; + +/** + * Command for displaying available security providers. + * Shows current security configuration and exits. + */ +@Command(name = "show-providers", description = "Show available security providers and exit", mixinStandardHelpOptions = true) +public class ShowProvidersCommand implements Callable { + + @CommandLine.Spec + protected CommandLine.Model.CommandSpec spec; + + @Override + public final Integer call() { + SecurityProviderService.printCurrentConfiguration(spec); + return 0; + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/SystemTrustStoreCommand.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/SystemTrustStoreCommand.java new file mode 100644 index 0000000000000..968afa7acee0b --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/SystemTrustStoreCommand.java @@ -0,0 +1,46 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +/** + * Command for configuring system PKCS11 trust store. + * Enables the use of existing system trust store infrastructure. + */ +@Command(name = "system", description = "Use existing system PKCS11 trust store", mixinStandardHelpOptions = true) +public class SystemTrustStoreCommand implements Callable { + + @CommandLine.Spec + protected CommandLine.Model.CommandSpec spec; + + @CommandLine.Mixin + protected CommonOptions common; + + @Option(names = { "--pkcs11-provider" }, description = "Specify PKCS11 provider name directly") + private String preselectedPKCS11Provider; + + @Override + public final Integer call() { + var confPath = Path.of(System.getProperty("opensearch.path.conf")); + ConfigurationService.verifyJvmOptionsFile(spec, common, confPath); + return new TrustStoreService(UserInteractionService.getInstance()).useSystemTrustStore( + spec, + common, + preselectedPKCS11Provider, + confPath + ); + } + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/TrustStoreService.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/TrustStoreService.java new file mode 100644 index 0000000000000..c8d39da102a9b --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/TrustStoreService.java @@ -0,0 +1,143 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.function.Function; + +import picocli.CommandLine.Model.CommandSpec; + +/** + * Service for managing trust store operations. + * Handles generation and configuration of FIPS-compliant trust stores. + */ +public class TrustStoreService { + + private final UserInteractionService userInteraction; + + public TrustStoreService(UserInteractionService userInteraction) { + this.userInteraction = userInteraction; + } + + /** + * Generates a new BCFKS trust store from the JVM default trust store. + * + * @param spec the command specification for output + * @param options common command-line options + * @param confPath path to the OpenSearch configuration directory + * @return exit code (0 for success, 1 for failure) + */ + public Integer generateTrustStore(CommandSpec spec, CommonOptions options, Path confPath) { + if (!userInteraction.confirmAction(spec, options, "Generate new BCFKS trust store from system defaults?")) { + spec.commandLine().getOut().println("Operation cancelled."); + return 0; + } + + var password = Optional.ofNullable(options.password).orElseGet(() -> { + if (options.nonInteractive) { + spec.commandLine().getOut().println("Generated secure password for trust store (non-interactive mode)"); + return userInteraction.generateSecurePassword(); + } + return userInteraction.promptForPasswordWithConfirmation(spec, options, "Enter trust store password"); + }); + if (password.isBlank()) { + spec.commandLine() + .getOut() + .println(spec.commandLine().getColorScheme().ansi().string("@|yellow WARNING: Using empty password|@")); + } + + spec.commandLine().getOut().println("Generating BCFKS trust store..."); + var properties = Function.identity() + .andThen(path -> CreateFipsTrustStore.loadJvmDefaultTrustStore(spec, path)) + .andThen(trustStore -> CreateFipsTrustStore.convertToBCFKS(spec, trustStore, options, password, confPath)) + .andThen(bcfksPath -> CreateFipsTrustStore.configureBCFKSTrustStore(bcfksPath, password)) + .apply(Path.of(System.getProperty("java.home"))); + + new ConfigurationService().writeSecurityConfigToJvmOptionsFile(properties, confPath); + finishInstallation(spec, properties); + + return 0; + } + + /** + * Configures OpenSearch to use the system PKCS11 trust store. + * + * @param spec the command specification for output + * @param options common command-line options + * @param preselectedPKCS11Provider optional pre-selected PKCS11 provider name + * @param confPath path to the OpenSearch configuration directory + * @return exit code (0 for success, 1 for failure) + */ + public Integer useSystemTrustStore(CommandSpec spec, CommonOptions options, String preselectedPKCS11Provider, Path confPath) { + if (!userInteraction.confirmAction(spec, options, "Use system PKCS11 trust store?")) { + spec.commandLine().getOut().println("Operation cancelled."); + return 0; + } + + spec.commandLine().getOut().println("Configuring system PKCS11 trust store..."); + + var serviceProviderList = ConfigureSystemTrustStore.findPKCS11ProviderService(); + if (serviceProviderList.isEmpty()) { + throw new IllegalStateException( + "No PKCS11 provider found. Please check 'java.security' configuration file for installed providers." + ); + } + + var selectedService = new ProviderSelectionService(userInteraction).selectProvider( + spec, + options, + serviceProviderList, + preselectedPKCS11Provider + ); + if (selectedService == null) { + spec.commandLine().getOut().println("Operation cancelled."); + return 0; + } + spec.commandLine().getOut().println("Using PKCS11 provider: " + selectedService.getProvider().getName()); + var properties = ConfigureSystemTrustStore.configurePKCS11TrustStore(selectedService); + new ConfigurationService().writeSecurityConfigToJvmOptionsFile(properties, confPath); + finishInstallation(spec, properties); + return 0; + } + + /** + * Presents an interactive menu for trust store configuration selection. + * + * @param spec the command specification for output + * @param options common command-line options + * @param confPath path to the OpenSearch configuration directory + * @return exit code (0 for success, 1 for failure) + */ + public Integer executeInteractiveSelection(CommandSpec spec, CommonOptions options, Path confPath) { + if (options.nonInteractive) { + spec.commandLine().getOut().println("Non-interactive mode: Using generated trust store (default)"); + return generateTrustStore(spec, options, confPath); + } + + var out = spec.commandLine().getOut(); + out.println("OpenSearch FIPS Demo Configuration Installer"); + out.println("Please select trust store configuration:"); + out.println(" 1. Generate new BCFKS trust store from system defaults"); + out.println(" 2. Use existing system PKCS11 trust store"); + + var choice = userInteraction.promptForChoice(spec, 2, 1); + + return choice == 1 ? generateTrustStore(spec, options, confPath) : useSystemTrustStore(spec, options, null, confPath); + } + + private static void finishInstallation(CommandSpec spec, ConfigurationProperties properties) { + spec.commandLine().getOut().println(); + spec.commandLine().getOut().println("### Success!"); + spec.commandLine().getOut().println("### Execute this script on all your nodes and then start all nodes"); + spec.commandLine().getOut().println("### Trust Store Configuration:"); + spec.commandLine().getOut().print(properties.toString()); + spec.commandLine().getOut().println(); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/UserInteractionService.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/UserInteractionService.java new file mode 100644 index 0000000000000..4518207882795 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/UserInteractionService.java @@ -0,0 +1,164 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.common.Randomness; + +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Scanner; + +import picocli.CommandLine; + +/** + * Service for handling user interaction in console applications. + * Provides utilities for confirmations and password input. + */ +public abstract class UserInteractionService { + + /** Shared scanner for reading console input. */ + public static final Scanner CONSOLE_SCANNER = new Scanner(System.in, StandardCharsets.UTF_8); + private static final String ALPHA_NUMERIC = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*"; + + static { + Runtime.getRuntime().addShutdownHook(new Thread(CONSOLE_SCANNER::close)); + } + + protected abstract Scanner getScanner(); + + public static UserInteractionService getInstance() { + return new UserInteractionService() { + @Override + protected Scanner getScanner() { + return CONSOLE_SCANNER; + } + }; + } + + /** + * Prompts the user to confirm an action. + * + * @param spec the command specification for output + * @param options common command-line options + * @param message confirmation message to display + * @return true if user confirms, false otherwise + */ + public boolean confirmAction(CommandLine.Model.CommandSpec spec, CommonOptions options, String message) { + var out = spec.commandLine().getOut(); + + if (options.nonInteractive) { + out.println(message + " - Auto-confirmed (non-interactive mode)"); + return true; + } + + out.print(message + " [y/N] "); + out.flush(); + + if (!getScanner().hasNextLine()) { + throwNoInputException(); + } + var response = getScanner().nextLine().trim(); + return response.equalsIgnoreCase("yes") || response.equalsIgnoreCase("y"); + } + + /** + * Prompts the user for a password with confirmation. + * + * @param spec the command specification for output + * @param options common command-line options + * @param message password prompt message + * @return the entered password + * @throws RuntimeException if passwords don't match or input is canceled + */ + public String promptForPasswordWithConfirmation(CommandLine.Model.CommandSpec spec, CommonOptions options, String message) { + var password = promptForPassword(spec, message); + var confirmPassword = promptForPassword(spec, "Confirm " + message.toLowerCase(java.util.Locale.ROOT)); + + if (!password.equals(confirmPassword)) { + throw new RuntimeException("Passwords do not match. Operation cancelled."); + } + + return password; + } + + protected String promptForPassword(CommandLine.Model.CommandSpec spec, String message) { + var out = spec.commandLine().getOut(); + var ansi = spec.commandLine().getColorScheme().ansi(); + + out.print(ansi.string("@|yellow " + message + " (WARNING: will be visible): |@")); + out.flush(); + + if (!getScanner().hasNextLine()) { + throwNoInputException(); + } + + var password = getScanner().nextLine().trim(); + if (password.isEmpty()) { + throw new RuntimeException("Password cannot be empty."); + } + + return password; + } + + /** + * Prompts the user to select from a numbered list of choices. + * + * @param spec the command specification for output + * @param choiceCount the number of valid choices (1-based) + * @param defaultChoice the default choice if user enters empty string (1-based, must be within valid range) + * @return the user's choice (1-based index) + */ + public int promptForChoice(CommandLine.Model.CommandSpec spec, int choiceCount, int defaultChoice) { + assert choiceCount > 0 : "Choice count must be greater than 0."; + assert defaultChoice >= 1 && defaultChoice <= choiceCount : "Default choice must be between 1 and " + choiceCount; + + var out = spec.commandLine().getOut(); + var scanner = getScanner(); + + while (true) { + out.printf(Locale.ROOT, "Enter choice (1-%s) [%s]: ", choiceCount, defaultChoice); + out.flush(); + + if (!scanner.hasNextLine()) { + throwNoInputException(); + } + + var input = scanner.nextLine().trim(); + + if (input.isEmpty()) { + return defaultChoice; + } + + try { + var choice = Integer.parseInt(input); + if (choice >= 1 && choice <= choiceCount) { + return choice; + } + } catch (NumberFormatException e) { + // fall through to the last statement + } + out.println("Invalid choice."); + } + } + + public String generateSecurePassword() { + var password = new StringBuilder(); + + for (int i = 0; i < 24; i++) { // 24 characters for a strong password + password.append(ALPHA_NUMERIC.charAt(Randomness.createSecure().nextInt(ALPHA_NUMERIC.length()))); + } + + return password.toString(); + } + + private static void throwNoInputException() { + throw new IllegalStateException("\nNo input available. Use the '--non-interactive option' to skip confirmations."); + } + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/package-info.java b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/package-info.java new file mode 100644 index 0000000000000..a792879d7c2ae --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/main/java/org/opensearch/tools/cli/fips/truststore/package-info.java @@ -0,0 +1,12 @@ +/* + * 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. + */ + +/** + * Tools to check and generate a FIPS-compliant trust store + */ +package org.opensearch.tools.cli.fips.truststore; diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigurationPropertiesTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigurationPropertiesTests.java new file mode 100644 index 0000000000000..559bd9c596354 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigurationPropertiesTests.java @@ -0,0 +1,43 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; + +public class ConfigurationPropertiesTests extends OpenSearchTestCase { + + public void testToStringWithPasswordSet() { + // given + var config = new ConfigurationProperties("/path/to/truststore.bcfks", "BCFKS", "p@ssw0rd!", "BCFIPS"); + + // when + String output = config.toString(); + + // then + assertTrue(output.contains("javax.net.ssl.trustStore: /path/to/truststore.bcfks")); + assertTrue(output.contains("javax.net.ssl.trustStoreType: BCFKS")); + assertTrue(output.contains("javax.net.ssl.trustStoreProvider: BCFIPS")); + assertTrue(output.contains("javax.net.ssl.trustStorePassword: [SET]")); + assertFalse(output.contains("changeit")); + } + + public void testToStringWithEmptyPassword() { + // given + var config = new ConfigurationProperties("/path/to/truststore.jks", "JKS", "", "SUN"); + + // when + String output = config.toString(); + + // then + assertTrue(output.contains("javax.net.ssl.trustStore: /path/to/truststore.jks")); + assertTrue(output.contains("javax.net.ssl.trustStoreType: JKS")); + assertTrue(output.contains("javax.net.ssl.trustStoreProvider: SUN")); + assertTrue(output.contains("javax.net.ssl.trustStorePassword: [NOT SET]")); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigurationServiceTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigurationServiceTests.java new file mode 100644 index 0000000000000..a4d4bc73c7189 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigurationServiceTests.java @@ -0,0 +1,252 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.common.util.io.IOUtils; +import org.opensearch.test.OpenSearchTestCase; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; + +import picocli.CommandLine; + +public class ConfigurationServiceTests extends OpenSearchTestCase { + + private static Path sharedTempDir; + + private Path jvmOptionsFile; + private CommandLine.Model.CommandSpec spec; + private StringWriter outputCapture; + + @BeforeClass + public static void setUpClass() throws Exception { + sharedTempDir = Files.createTempDirectory(Path.of(System.getProperty("java.io.tmpdir")), "config-test-"); + } + + @AfterClass + public static void tearDownClass() throws Exception { + if (sharedTempDir != null && Files.exists(sharedTempDir)) { + try (var walk = Files.walk(sharedTempDir)) { + walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> { + try { + Files.delete(path); + } catch (Exception e) { + // Ignore + } + }); + } + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + jvmOptionsFile = sharedTempDir.resolve("jvm.options"); + if (Files.exists(jvmOptionsFile)) { + Files.delete(jvmOptionsFile); + } + + @CommandLine.Command + class DummyCommand {} + + outputCapture = new StringWriter(); + CommandLine commandLine = new CommandLine(new DummyCommand()); + commandLine.setOut(new PrintWriter(outputCapture, true)); + spec = commandLine.getCommandSpec(); + } + + public void testVerifyJvmOptionsFileWithForceMode() throws Exception { + // given + var options = new CommonOptions(); + options.force = true; + + // when + ConfigurationService.verifyJvmOptionsFile(spec, options, sharedTempDir); + + // then + var output = outputCapture.toString(); + assertTrue(output.contains("WARNING: Force mode enabled")); + } + + public void testVerifyJvmOptionsFileDoesNotExist() { + var exception = expectThrows( + IllegalStateException.class, + () -> ConfigurationService.verifyJvmOptionsFile(spec, new CommonOptions(), sharedTempDir) + ); + assertTrue(exception.getMessage().contains("jvm.options file does not exist")); + } + + public void testVerifyJvmOptionsFileIsEmpty() throws Exception { + Files.createFile(jvmOptionsFile); + + var exception = expectThrows( + IllegalStateException.class, + () -> ConfigurationService.verifyJvmOptionsFile(spec, new CommonOptions(), sharedTempDir) + ); + assertTrue(exception.getMessage().contains("jvm.options file is empty")); + } + + public void testVerifyJvmOptionsFileContainsFipsConfiguration() throws Exception { + Files.writeString(jvmOptionsFile, """ + -Xms1g + -Djavax.net.ssl.trustStore=/path/to/store + """, StandardCharsets.UTF_8); + + var exception = expectThrows( + IllegalStateException.class, + () -> ConfigurationService.verifyJvmOptionsFile(spec, new CommonOptions(), sharedTempDir) + ); + assertTrue(exception.getMessage().contains("FIPS demo configuration already exists")); + assertTrue(exception.getMessage().contains("trustStor")); + } + + public void testWriteSecurityConfigToJvmOptionsFile() throws Exception { + // given + Files.writeString(jvmOptionsFile, """ + -Xms1g + -Xmx1g + """, StandardCharsets.UTF_8); + var properties = new ConfigurationProperties("/path/to/truststore.bcfks", "BCFKS", "changeit", "BCFIPS"); + var service = new ConfigurationService(); + + // when + service.writeSecurityConfigToJvmOptionsFile(properties, sharedTempDir); + + // then + var content = Files.readString(jvmOptionsFile, StandardCharsets.UTF_8); + assertTrue(content.contains("-Xms1g")); + assertTrue(content.contains("Start OpenSearch FIPS Demo Configuration")); + assertTrue(content.contains("WARNING: revise all the lines below before you go into production")); + assertTrue(content.contains("-Djavax.net.ssl.trustStore=/path/to/truststore.bcfks")); + assertTrue(content.contains("-Djavax.net.ssl.trustStoreType=BCFKS")); + assertTrue(content.contains("-Djavax.net.ssl.trustStorePassword=changeit")); + assertTrue(content.contains("-Djavax.net.ssl.trustStoreProvider=BCFIPS")); + } + + public void testWriteSecurityConfigCreatesFileIfNotExists() throws Exception { + // given + var properties = new ConfigurationProperties("/new/truststore.bcfks", "BCFKS", "password", "BCFIPS"); + var service = new ConfigurationService(); + + // when + service.writeSecurityConfigToJvmOptionsFile(properties, sharedTempDir); + + // then + assertTrue(Files.exists(jvmOptionsFile)); + String content = Files.readString(jvmOptionsFile, StandardCharsets.UTF_8); + assertTrue(content.contains("-Djavax.net.ssl.trustStore=/new/truststore.bcfks")); + } + + public void testWriteSecurityConfigFormatting() throws Exception { + // given + Files.createFile(jvmOptionsFile); + var properties = new ConfigurationProperties("/path/to/store", "BCFKS", "pass", "BCFIPS"); + var service = new ConfigurationService(); + + // when + service.writeSecurityConfigToJvmOptionsFile(properties, sharedTempDir); + + // then + var content = Files.readString(jvmOptionsFile, StandardCharsets.UTF_8); + + assertTrue(content.contains("################################################################")); + assertTrue(content.contains("## Start OpenSearch FIPS Demo Configuration")); + + assertEquals(4, content.lines().filter(line -> line.startsWith("-Djavax.net.ssl.trustStore")).count()); + } + + public void testVerifyJvmOptionsFileThrowsOnReadError() throws Exception { + assumeTrue("requires POSIX file permissions", (IOUtils.LINUX || IOUtils.MAC_OS_X)); + Files.createFile(jvmOptionsFile); + Files.writeString(jvmOptionsFile, "-Xms1g\n", StandardCharsets.UTF_8); + + Set originalPermissions = Files.getPosixFilePermissions(jvmOptionsFile); + Set noReadPermissions = PosixFilePermissions.fromString("---------"); + Files.setPosixFilePermissions(jvmOptionsFile, noReadPermissions); + + try { + var exception = expectThrows( + IllegalStateException.class, + () -> ConfigurationService.verifyJvmOptionsFile(spec, new CommonOptions(), sharedTempDir) + ); + assertTrue( + exception.getMessage().contains("jvm.options file is not readable") + || exception.getMessage().contains("Failed to read jvm.options file") + ); + } finally { + Files.setPosixFilePermissions(jvmOptionsFile, originalPermissions); + } + } + + public void testWriteSecurityConfigThrowsOnWriteError() throws Exception { + assumeTrue("requires POSIX file permissions", (IOUtils.LINUX || IOUtils.MAC_OS_X)); + Files.createFile(jvmOptionsFile); + + Set originalPermissions = Files.getPosixFilePermissions(jvmOptionsFile); + Set readOnlyPermissions = PosixFilePermissions.fromString("r--r--r--"); + Files.setPosixFilePermissions(jvmOptionsFile, readOnlyPermissions); + + var properties = new ConfigurationProperties("/path/to/store", "BCFKS", "pass", "BCFIPS"); + var service = new ConfigurationService(); + + try { + var exception = expectThrows( + RuntimeException.class, + () -> service.writeSecurityConfigToJvmOptionsFile(properties, sharedTempDir) + ); + assertTrue(exception.getMessage().contains("Exception writing security configuration")); + } finally { + Files.setPosixFilePermissions(jvmOptionsFile, originalPermissions); + } + } + + public void testValidateJvmOptionsContentWithValidFile() throws Exception { + Files.writeString(jvmOptionsFile, """ + -Xms1g + -Xmx1g + -XX:+UseG1GC + """, StandardCharsets.UTF_8); + + ConfigurationService.validateJvmOptionsContent(jvmOptionsFile); + } + + public void testValidateJvmOptionsContentThrowsOnEmptyFile() throws Exception { + Files.createFile(jvmOptionsFile); + + var exception = expectThrows(IllegalStateException.class, () -> ConfigurationService.validateJvmOptionsContent(jvmOptionsFile)); + assertTrue(exception.getMessage().contains("jvm.options file is empty")); + } + + public void testValidateJvmOptionsContentThrowsOnExistingFipsProperty() throws Exception { + Files.writeString(jvmOptionsFile, """ + -Xms1g + -Djavax.net.ssl.trustStore=/path/to/store + """, StandardCharsets.UTF_8); + + var exception = expectThrows(IllegalStateException.class, () -> ConfigurationService.validateJvmOptionsContent(jvmOptionsFile)); + assertTrue(exception.getMessage().contains("FIPS demo configuration already exists")); + assertTrue(exception.getMessage().contains("trustStor")); + } + + public void testValidateJvmOptionsContentThrowsOnIOException() throws Exception { + var nonExistentFile = sharedTempDir.resolve("does-not-exist.txt"); + + var exception = expectThrows(IllegalStateException.class, () -> ConfigurationService.validateJvmOptionsContent(nonExistentFile)); + assertTrue(exception.getMessage().contains("Failed to read jvm.options file")); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigureSystemTrustStoreTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigureSystemTrustStoreTests.java new file mode 100644 index 0000000000000..8d2bc6ee9d16b --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ConfigureSystemTrustStoreTests.java @@ -0,0 +1,34 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; + +import java.security.Provider; + +public class ConfigureSystemTrustStoreTests extends OpenSearchTestCase { + + public void testFindPKCS11ProviderServiceReturnsEmptyListWhenNoProviders() { + var services = ConfigureSystemTrustStore.findPKCS11ProviderService(); + assertNotNull(services); + } + + public void testConfigurePKCS11TrustStoreCreatesValidConfiguration() { + var mockProvider = new Provider("TestPKCS11Provider", "1.0", "Test provider") { + }; + var service = new Provider.Service(mockProvider, "KeyStore", "PKCS11", "java.security.KeyStore", null, null); + + var config = ConfigureSystemTrustStore.configurePKCS11TrustStore(service); + + assertNotNull(config); + assertEquals("NONE", config.trustStorePath()); + assertEquals("PKCS11", config.trustStoreType()); + assertEquals("TestPKCS11Provider", config.trustStoreProvider()); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/CreateFipsTrustStoreTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/CreateFipsTrustStoreTests.java new file mode 100644 index 0000000000000..1ac8f27658a7d --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/CreateFipsTrustStoreTests.java @@ -0,0 +1,237 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.util.Comparator; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import picocli.CommandLine; + +public class CreateFipsTrustStoreTests extends OpenSearchTestCase { + + private static final Path JAVA_HOME = Path.of(System.getProperty("java.home")); + private static Path sharedTempDir; + + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + + private CommandLine.Model.CommandSpec spec; + + @BeforeClass + public static void setUpClass() throws IOException { + sharedTempDir = Files.createTempDirectory(Path.of(System.getProperty("java.io.tmpdir")), "fips-test-"); + Path confDir = sharedTempDir.resolve("config"); + Files.createDirectories(confDir); + } + + @AfterClass + public static void tearDownClass() throws IOException { + if (sharedTempDir != null && Files.exists(sharedTempDir)) { + try (Stream walk = Files.walk(sharedTempDir)) { + walk.sorted(Comparator.reverseOrder()).forEach(path -> { + try { + Files.delete(path); + } catch (Exception e) { + // Ignore + } + }); + } + } + } + + @Before + public void setUp() throws Exception { + super.setUp(); + + @CommandLine.Command + class DummyCommand {} + + CommandLine commandLine = new CommandLine(new DummyCommand()); + commandLine.setOut(new PrintWriter(new LoggerWriter(logger::info), true)); + commandLine.setErr(new PrintWriter(new LoggerWriter(logger::error), true)); + spec = commandLine.getCommandSpec(); + + // Clean up any existing truststore file from previous tests + Path confDir = sharedTempDir.resolve("config"); + Path trustStorePath = confDir.resolve("opensearch-fips-truststore.bcfks"); + if (Files.exists(trustStorePath)) { + Files.delete(trustStorePath); + } + } + + /** Writer that delegates to a logger */ + private static class LoggerWriter extends Writer { + private final StringBuilder buffer = new StringBuilder(); + private final Consumer log; + + LoggerWriter(Consumer log) { + this.log = log; + } + + @Override + public void write(char[] cbuf, int off, int len) { + buffer.append(cbuf, off, len); + } + + @Override + public void flush() { + if (!buffer.isEmpty()) { + String message = buffer.toString().stripTrailing(); + if (!message.isEmpty()) log.accept(message); + buffer.setLength(0); + } + } + + @Override + public void close() { + flush(); + } + } + + public void testLoadJvmDefaultTrustStore() throws Exception { + // when + var keyStore = CreateFipsTrustStore.loadJvmDefaultTrustStore(spec, JAVA_HOME); + + // then + assertTrue("JVMs truststore is not empty", keyStore.size() > 0); + assertNotNull(keyStore); + } + + public void testLoadJvmDefaultTrustStoreWithInvalidPath() { + // given + Path invalidPath = Path.of("/non/existent/path"); + + // when/then + IllegalStateException exception = expectThrows( + IllegalStateException.class, + () -> CreateFipsTrustStore.loadJvmDefaultTrustStore(spec, invalidPath) + ); + assertTrue(exception.getMessage().contains("System cacerts not found at")); + } + + public void testConfigureBCFKSTrustStore() { + // given + Path bcfksPath = Path.of("/tmp/test-truststore.bcfks"); + String password = "testPassword123"; + + // when + ConfigurationProperties config = CreateFipsTrustStore.configureBCFKSTrustStore(bcfksPath, password); + + // then + assertNotNull(config); + assertEquals(bcfksPath.toAbsolutePath().toString(), config.trustStorePath()); + assertEquals("BCFKS", config.trustStoreType()); + assertEquals(password, config.trustStorePassword()); + assertEquals("BCFIPS", config.trustStoreProvider()); + } + + public void testConvertToBCFKS() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + // given + KeyStore sourceKeyStore = CreateFipsTrustStore.loadJvmDefaultTrustStore(spec, JAVA_HOME); + assertTrue("Source keystore should have certificates", sourceKeyStore.size() > 0); + + CommonOptions options = new CommonOptions(); + options.force = false; + String password = "testPassword123"; + Path confPath = sharedTempDir.resolve("config"); + + // when + Path result = CreateFipsTrustStore.convertToBCFKS(spec, sourceKeyStore, options, password, confPath); + + // then + assertNotNull(result); + assertTrue(Files.exists(result)); + assertTrue(result.toString().endsWith("opensearch-fips-truststore.bcfks")); + + // Verify the converted keystore has the same certificates + KeyStore bcfksStore = KeyStore.getInstance("BCFKS", "BCFIPS"); + try (var is = Files.newInputStream(result)) { + bcfksStore.load(is, password.toCharArray()); + } + assertEquals("Converted keystore should have same number of certificates", sourceKeyStore.size(), bcfksStore.size()); + } + + public void testConvertToBCFKSFileExistsWithoutForce() throws Exception { + // Skip if BCFIPS not available since the method needs it to check file handling + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + // given + KeyStore sourceKeyStore = CreateFipsTrustStore.loadJvmDefaultTrustStore(spec, JAVA_HOME); + assertTrue("Source keystore should have certificates", sourceKeyStore.size() > 0); + + CommonOptions options = new CommonOptions(); + options.force = false; + String password = "testPassword123"; + + // Create file first to simulate existing truststore + Path confPath = sharedTempDir.resolve("config"); + Path trustStorePath = confPath.resolve("opensearch-fips-truststore.bcfks"); + Files.createFile(trustStorePath); + + assertTrue("Test setup: file should exist", Files.exists(trustStorePath)); + + // when/then + RuntimeException exception = expectThrows( + RuntimeException.class, + () -> CreateFipsTrustStore.convertToBCFKS(spec, sourceKeyStore, options, password, confPath) + ); + assertEquals("Operation cancelled. Trust store file already exists.", exception.getMessage()); + } + + public void testConvertToBCFKSFileExistsWithForce() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + // given + KeyStore sourceKeyStore = CreateFipsTrustStore.loadJvmDefaultTrustStore(spec, JAVA_HOME); + assertTrue("Source keystore should have certificates", sourceKeyStore.size() > 0); + + CommonOptions options = new CommonOptions(); + options.force = true; + String password = "testPassword123"; + + // Create file first + Path confPath = sharedTempDir.resolve("config"); + Path trustStorePath = confPath.resolve("opensearch-fips-truststore.bcfks"); + Files.createFile(trustStorePath); + + assertTrue(Files.exists(trustStorePath)); + + // when + Path result = CreateFipsTrustStore.convertToBCFKS(spec, sourceKeyStore, options, password, confPath); + + // then + assertNotNull(result); + assertTrue(Files.exists(result)); + + // Verify the converted keystore has actual certificates + KeyStore bcfksStore = KeyStore.getInstance("BCFKS", "BCFIPS"); + try (var is = Files.newInputStream(result)) { + bcfksStore.load(is, password.toCharArray()); + } + assertTrue("Converted keystore should have certificates", bcfksStore.size() > 0); + assertEquals("Converted keystore should have same number of certificates", sourceKeyStore.size(), bcfksStore.size()); + } + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommandTestCase.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommandTestCase.java new file mode 100644 index 0000000000000..07e6329fdf214 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommandTestCase.java @@ -0,0 +1,81 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.cli.SuppressForbidden; +import org.opensearch.test.OpenSearchTestCase; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.Callable; + +import picocli.CommandLine; + +public abstract class FipsTrustStoreCommandTestCase extends OpenSearchTestCase { + + protected StringWriter outputCapture; + protected StringWriter errorCapture; + protected CommandLine commandLine; + protected static Path sharedTempDir; + + @BeforeClass + static void setUpClass() throws Exception { + sharedTempDir = Files.createTempDirectory(Path.of(System.getProperty("java.io.tmpdir")), "system-command-test-"); + setProperties(); + } + + @AfterClass + static void tearDownClass() throws Exception { + clearProperties(); + if (sharedTempDir != null && Files.exists(sharedTempDir)) { + try (var walk = Files.walk(sharedTempDir)) { + walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> { + try { + Files.delete(path); + } catch (Exception e) { + // Ignore + } + }); + } + } + } + + @SuppressForbidden(reason = "set system properties as part of test setup") + private static void setProperties() { + System.setProperty("opensearch.path.conf", sharedTempDir.toString()); + } + + @SuppressForbidden(reason = "clear system properties") + private static void clearProperties() { + System.clearProperty("opensearch.path.conf"); + } + + @Before + public void setUp() throws Exception { + super.setUp(); + errorCapture = new StringWriter(); + outputCapture = new StringWriter(); + commandLine = new CommandLine(getCut()).setOut(new PrintWriter(outputCapture, true)).setErr(new PrintWriter(errorCapture, true)); + + Path jvmOptionsFile = sharedTempDir.resolve("jvm.options"); + if (Files.exists(jvmOptionsFile)) { + Files.delete(jvmOptionsFile); + } + Files.writeString(jvmOptionsFile, "# JVM Options\n-Xms1g\n-Xmx1g\n", StandardCharsets.UTF_8); + } + + abstract Callable getCut(); + +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommandTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommandTests.java new file mode 100644 index 0000000000000..495a0f3a28a0f --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreCommandTests.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. + */ + +package org.opensearch.tools.cli.fips.truststore; + +import java.util.concurrent.Callable; + +public class FipsTrustStoreCommandTests extends FipsTrustStoreCommandTestCase { + + public void testWithUnmatchedArgs() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + var exitCode = commandLine.execute("--non-interactive", "--force", "-Ediscovery.type=single-node", "-Ehttp.port=9200"); + + assertEquals(0, exitCode); + assertTrue(errorCapture.toString().isEmpty()); + assertTrue( + outputCapture.toString().contains("Warning: Ignoring unrecognized arguments: [-Ediscovery.type=single-node, -Ehttp.port=9200]") + ); + assertTrue(outputCapture.toString().contains("Trust Store Configuration")); + assertTrue(outputCapture.toString().contains("javax.net.ssl.trustStoreType: BCFKS")); + } + + public void testWithEmptyUnmatchedArgs() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + var exitCode = commandLine.execute("--non-interactive", "--force"); + + assertEquals(0, exitCode); + assertTrue(errorCapture.toString().isEmpty()); + assertFalse( + outputCapture.toString().contains("Warning: Ignoring unrecognized arguments: [-Ediscovery.type=single-node, -Ehttp.port=9200]") + ); + assertTrue(outputCapture.toString().contains("Trust Store Configuration")); + assertTrue(outputCapture.toString().contains("javax.net.ssl.trustStoreType: BCFKS")); + } + + @Override + Callable getCut() { + return new FipsTrustStoreCommand(); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreExceptionHandlerTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreExceptionHandlerTests.java new file mode 100644 index 0000000000000..a5f984b387e3a --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/FipsTrustStoreExceptionHandlerTests.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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import picocli.CommandLine; + +public class FipsTrustStoreExceptionHandlerTests extends OpenSearchTestCase { + + public void testPrintsErrorMessage() { + // given + var handler = new FipsTrustStoreExceptionHandler(); + var errorCapture = new StringWriter(); + + var commandLine = new CommandLine(new FipsTrustStoreCommand()).setErr(new PrintWriter(errorCapture, true)); + + var testException = new RuntimeException("Test error message"); + + // when + int exitCode = handler.handleExecutionException(testException, commandLine, null); + + // then + var errorOutput = errorCapture.toString(); + assertEquals(1, exitCode); + assertEquals("Error: Test error message\n", errorOutput); + } + + public void testPrintsNullErrorMessage() { + // given + var handler = new FipsTrustStoreExceptionHandler(); + var errorCapture = new StringWriter(); + + var commandLine = new CommandLine(new FipsTrustStoreCommand()).setErr(new PrintWriter(errorCapture, true)); + + var testException = new RuntimeException(); + + // when + int exitCode = handler.handleExecutionException(testException, commandLine, null); + + // then + var errorOutput = errorCapture.toString(); + assertEquals(1, exitCode); + assertEquals("Error: [null]\n", errorOutput); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/GeneratedTrustStoreCommandTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/GeneratedTrustStoreCommandTests.java new file mode 100644 index 0000000000000..bf5a9df339491 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/GeneratedTrustStoreCommandTests.java @@ -0,0 +1,56 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.util.concurrent.Callable; + +public class GeneratedTrustStoreCommandTests extends FipsTrustStoreCommandTestCase { + + public void testNonInteractiveModeAutoGeneratesPassword() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + int exitCode = commandLine.execute("--non-interactive", "--force"); + + assertEquals(0, exitCode); + var output = outputCapture.toString(); + assertTrue(output.contains("Auto-confirmed (non-interactive mode)")); + assertTrue(output.contains("Generated secure password")); + assertTrue(output.contains("javax.net.ssl.trustStoreProvider: BCFIPS")); + } + + public void testNonInteractiveModeWithPasswordOption() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + int exitCode = commandLine.execute("--non-interactive", "--force", "--password", "MyPassword"); + + assertEquals(0, exitCode); + var output = outputCapture.toString(); + assertTrue(output.contains("Auto-confirmed (non-interactive mode)")); + assertFalse(output.contains("Generated secure password")); + assertTrue(output.contains("javax.net.ssl.trustStoreProvider: BCFIPS")); + } + + public void testCommandWithEmptyPassword() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + int exitCode = commandLine.execute("--non-interactive", "--force", "--password", ""); + + assertEquals(0, exitCode); + var output = outputCapture.toString(); + assertTrue(output.contains("Auto-confirmed (non-interactive mode)")); + assertFalse(output.contains("Generated secure password")); + assertTrue(output.contains("WARNING: Using empty password")); + assertTrue(output.contains("javax.net.ssl.trustStoreProvider: BCFIPS")); + } + + @Override + Callable getCut() { + return new GeneratedTrustStoreCommand(); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ProviderSelectionServiceTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ProviderSelectionServiceTests.java new file mode 100644 index 0000000000000..eee91aaccc60c --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ProviderSelectionServiceTests.java @@ -0,0 +1,159 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.Provider; +import java.util.List; + +import picocli.CommandLine; + +public class ProviderSelectionServiceTests extends OpenSearchTestCase { + + private ProviderSelectionService cut; + private StringWriter outputCapture; + private StringWriter errorCapture; + private CommandLine.Model.CommandSpec spec; + private CommonOptions options; + + @Override + public void setUp() throws Exception { + super.setUp(); + cut = new ProviderSelectionService(UserInteractionService.getInstance()); + outputCapture = new StringWriter(); + errorCapture = new StringWriter(); + + var testCommand = new TestCommand(); + CommandLine commandLine = new CommandLine(testCommand); + commandLine.setOut(new PrintWriter(outputCapture, true)); + commandLine.setErr(new PrintWriter(errorCapture, true)); + spec = commandLine.getCommandSpec(); + options = new CommonOptions(); + } + + public void testSelectProviderWithPreselectedProviderFound() { + // given + var service1 = createMockService("Provider1"); + var service2 = createMockService("Provider2"); + List serviceList = List.of(service1, service2); + + // when + var result = cut.selectProvider(spec, options, serviceList, "Provider2"); + + // then + assertEquals("Provider2", result.getProvider().getName()); + assertEquals(service2, result); + } + + public void testSelectProviderWithPreselectedProviderNotFound() { + // given + var service1 = createMockService("Provider1"); + List serviceList = List.of(service1); + + // when/then + var exception = expectThrows( + IllegalArgumentException.class, + () -> cut.selectProvider(spec, options, serviceList, "NonExistentProvider") + ); + + assertEquals("Provider not found: NonExistentProvider", exception.getMessage()); + assertTrue(errorCapture.toString().contains("ERROR: Specified PKCS11 provider 'NonExistentProvider' not found")); + assertTrue(errorCapture.toString().contains("Available providers:")); + assertTrue(errorCapture.toString().contains("- Provider1")); + } + + public void testSelectProviderSingleProviderNonInteractive() { + // given + options.nonInteractive = true; + var service1 = createMockService("OnlyProvider"); + List serviceList = List.of(service1); + + // when + var result = cut.selectProvider(spec, options, serviceList, null); + + // then + assertEquals("OnlyProvider", result.getProvider().getName()); + assertTrue(outputCapture.toString().contains("Using PKCS11 provider: OnlyProvider")); + } + + public void testSelectProviderMultipleProvidersNonInteractive() { + // given + options.nonInteractive = true; + var service1 = createMockService("FirstProvider"); + var service2 = createMockService("SecondProvider"); + List serviceList = List.of(service1, service2); + + // when + var result = cut.selectProvider(spec, options, serviceList, null); + + // then + assertEquals("FirstProvider", result.getProvider().getName()); + assertTrue(outputCapture.toString().contains("Using PKCS11 provider: FirstProvider")); + } + + public void testSelectProviderWithEmptyList() { + // given + List serviceProviderList = List.of(); + options.nonInteractive = true; + + // when/then + var exception = expectThrows(IllegalStateException.class, () -> cut.selectProvider(spec, options, serviceProviderList, null)); + + assertEquals("No PKCS11 providers available. Please ensure a PKCS11 provider is installed and configured.", exception.getMessage()); + } + + public void testSelectProviderInteractivelyWithThreeProvidersShowsAllOptions() { + // given + var service1 = createMockService("ProviderAlpha"); + var service2 = createMockService("ProviderBeta"); + var service3 = createMockService("ProviderGamma"); + List serviceList = List.of(service1, service2, service3); + + // when/then - no input available, throws IllegalStateException + expectThrows(IllegalStateException.class, () -> cut.selectProviderInteractively(spec, serviceList)); + + // Verify output displays all three providers with correct numbering + var output = outputCapture.toString(); + assertTrue(output.contains("Multiple PKCS11 providers found:")); + assertTrue(output.contains("1. ProviderAlpha (Algorithm: KeyStore)")); + assertTrue(output.contains("2. ProviderBeta (Algorithm: KeyStore)")); + assertTrue(output.contains("3. ProviderGamma (Algorithm: KeyStore)")); + assertTrue(output.contains("Select PKCS11 provider")); + } + + // Test: Multiple providers in interactive mode call selectProviderInteractively + public void testSelectProviderMultipleProvidersInteractiveModeCallsSelectProviderInteractively() { + // given + options.nonInteractive = false; + var service1 = createMockService("InteractiveProv1"); + var service2 = createMockService("InteractiveProv2"); + List serviceList = List.of(service1, service2); + + // when/then - no input available, throws IllegalStateException + expectThrows(IllegalStateException.class, () -> cut.selectProvider(spec, options, serviceList, null)); + + var output = outputCapture.toString(); + assertTrue(output.contains("Multiple PKCS11 providers found:")); + assertTrue(output.contains("1. InteractiveProv1")); + assertTrue(output.contains("2. InteractiveProv2")); + } + + private Provider.Service createMockService(String name) { + var provider = new Provider(name, "1.0", "Mock Provider for " + name) { + }; + return new Provider.Service(provider, "PKCS11", "KeyStore", "MockImplementation", null, null); + } + + // Simple test command for getting CommandSpec + @CommandLine.Command(name = "test") + static class TestCommand {} +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/SecurityProviderServiceTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/SecurityProviderServiceTests.java new file mode 100644 index 0000000000000..d192041f2b376 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/SecurityProviderServiceTests.java @@ -0,0 +1,84 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.security.Security; +import java.util.Arrays; + +import picocli.CommandLine; + +public class SecurityProviderServiceTests extends OpenSearchTestCase { + + private CommandLine.Model.CommandSpec spec; + private StringWriter outputCapture; + + @Override + public void setUp() throws Exception { + super.setUp(); + + @CommandLine.Command + class DummyCommand {} + + outputCapture = new StringWriter(); + CommandLine commandLine = new CommandLine(new DummyCommand()); + commandLine.setOut(new PrintWriter(outputCapture, true)); + spec = commandLine.getCommandSpec(); + } + + public void testPrintCurrentConfigurationBasicStructure() { + // given + var providers = Security.getProviders(); + + // when + SecurityProviderService.printCurrentConfiguration(spec); + + // then + String output = outputCapture.toString(); + + assertFalse(output.isEmpty()); + assertTrue(output.contains("Available Security Providers:")); + assertTrue(output.contains(" 1.")); + assertTrue(output.contains("(version")); + + long numberedItems = output.lines().filter(line -> line.trim().matches("^\\d+\\..*")).count(); + assertEquals(providers.length, numberedItems); + + var firstProvider = providers[0]; + assertTrue(output.contains(firstProvider.getName())); + String expectedVersionFormat = firstProvider.getName() + " (version " + firstProvider.getVersionStr() + ")"; + assertTrue(output.contains(expectedVersionFormat)); + } + + public void testPrintCurrentConfigurationKeyStoreServices() { + // given + var keyStoreAlgorithms = Arrays.stream(Security.getProviders()) + .flatMap(provider -> provider.getServices().stream()) + .filter(service -> "KeyStore".equals(service.getType())) + .map(java.security.Provider.Service::getAlgorithm) + .toList(); + + // when + SecurityProviderService.printCurrentConfiguration(spec); + + // then + String output = outputCapture.toString(); + + if (!keyStoreAlgorithms.isEmpty()) { + assertTrue(output.contains("└─ KeyStore.")); + + for (String algorithm : keyStoreAlgorithms) { + assertTrue(output.contains("KeyStore." + algorithm)); + } + } + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ShowProvidersCommandTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ShowProvidersCommandTests.java new file mode 100644 index 0000000000000..10410c2d1dc87 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/ShowProvidersCommandTests.java @@ -0,0 +1,28 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.util.concurrent.Callable; + +public class ShowProvidersCommandTests extends FipsTrustStoreCommandTestCase { + + public void testCallOutputsProviderInformation() { + commandLine.execute(); + + int exitCode = commandLine.execute(); + assertEquals(0, exitCode); + assertTrue(errorCapture.toString().isEmpty()); + assertTrue(outputCapture.toString().contains("Available Security Providers")); + } + + @Override + Callable getCut() { + return new ShowProvidersCommand(); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/SystemTrustStoreCommandTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/SystemTrustStoreCommandTests.java new file mode 100644 index 0000000000000..a3b87470f8eac --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/SystemTrustStoreCommandTests.java @@ -0,0 +1,78 @@ +/* + * 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.tools.cli.fips.truststore; + +import java.security.Provider; +import java.security.Security; +import java.util.concurrent.Callable; + +public class SystemTrustStoreCommandTests extends FipsTrustStoreCommandTestCase { + + public void testCommand() throws Exception { + // given + var mockProvider = new Provider("PKCS11-Mock", "1.0", "Mock PKCS11 Provider") { + @Override + public String toString() { + return "MockPKCS11Provider[" + getName() + "]"; + } + }; + + try { + mockProvider.put("KeyStore.PKCS11", "com.example.MockPKCS11KeyStore"); + mockProvider.put("KeyStore.PKCS11 KeySize", "1024|2048"); + Security.addProvider(mockProvider); + + // when + int exitCode = commandLine.execute("--non-interactive"); + + // then + assertEquals(0, exitCode); + var output = outputCapture.toString(); + assertTrue(errorCapture.toString().isEmpty()); + assertTrue(output.contains("Trust Store Configuration")); + assertTrue(output.contains("javax.net.ssl.trustStoreType: PKCS11")); + } finally { + Security.removeProvider(mockProvider.getName()); + } + } + + public void testCommandWithPreselectedProvider() throws Exception { + // given + var mockProvider = new Provider("PKCS11-Mock", "1.0", "Mock PKCS11 Provider") { + @Override + public String toString() { + return "MockPKCS11Provider[" + getName() + "]"; + } + }; + + try { + mockProvider.put("KeyStore.PKCS11", "com.example.MockPKCS11KeyStore"); + mockProvider.put("KeyStore.PKCS11 KeySize", "1024|2048"); + Security.addProvider(mockProvider); + + // when + int exitCode = commandLine.execute("--non-interactive", "--pkcs11-provider", "PKCS11-Mock"); + + // then + assertEquals(0, exitCode); + assertTrue(errorCapture.toString().isEmpty()); + var output = outputCapture.toString(); + assertTrue(output.contains("Trust Store Configuration")); + assertTrue(output.contains("javax.net.ssl.trustStoreType: PKCS11")); + assertTrue(output.contains("javax.net.ssl.trustStoreProvider: PKCS11-Mock")); + } finally { + Security.removeProvider(mockProvider.getName()); + } + } + + @Override + Callable getCut() { + return new SystemTrustStoreCommand(); + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/TrustStoreServiceTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/TrustStoreServiceTests.java new file mode 100644 index 0000000000000..a6eedcd226613 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/TrustStoreServiceTests.java @@ -0,0 +1,193 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import java.io.ByteArrayInputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.Provider; +import java.security.Security; +import java.util.Scanner; + +import picocli.CommandLine; + +import static org.opensearch.tools.cli.fips.truststore.ConfigureSystemTrustStore.findPKCS11ProviderService; + +public class TrustStoreServiceTests extends OpenSearchTestCase { + + private static Path sharedTempDir; + + private CommandLine.Model.CommandSpec spec; + private StringWriter outputCapture; + private Path confPath; + + @BeforeClass + public static void setUpClass() throws Exception { + sharedTempDir = Files.createTempDirectory(Path.of(System.getProperty("java.io.tmpdir")), "truststore-test-"); + } + + @AfterClass + public static void tearDownClass() throws Exception { + if (sharedTempDir != null && Files.exists(sharedTempDir)) { + try (var walk = Files.walk(sharedTempDir)) { + walk.sorted(java.util.Comparator.reverseOrder()).forEach(path -> { + try { + Files.delete(path); + } catch (Exception e) { + // Ignore + } + }); + } + } + } + + @Override + public void setUp() throws Exception { + super.setUp(); + outputCapture = new StringWriter(); + + @CommandLine.Command + class TestCommand {} + + var commandLine = new CommandLine(new TestCommand()); + commandLine.setOut(new PrintWriter(outputCapture, true)); + spec = commandLine.getCommandSpec(); + + confPath = Files.createTempDirectory(sharedTempDir, "conf-"); + } + + public void testUseSystemTrustStoreUserCancels() { + // given + var userInteraction = createUserInteractionService("no\n"); + var service = new TrustStoreService(userInteraction); + + // when + var result = service.useSystemTrustStore(spec, new CommonOptions(), null, confPath); + + // then + assertEquals(Integer.valueOf(0), result); + assertTrue(outputCapture.toString().contains("Operation cancelled.")); + } + + public void testUseSystemTrustStoreNoPKCS11ProvidersFound() { + assumeTrue("Should only run when PKCS11 provider is NOT installed.", findPKCS11ProviderService().isEmpty()); + // given + var options = new CommonOptions(); + options.nonInteractive = true; + var userInteraction = createUserInteractionService("yes\n"); + var service = new TrustStoreService(userInteraction); + + // when + var ex = assertThrows(IllegalStateException.class, () -> service.useSystemTrustStore(spec, options, null, confPath)); + + // then + assertTrue(ex.getMessage().contains("No PKCS11 provider found")); + } + + public void testUseSystemTrustStoreWithPKCS11Provider() { + // given + var mockProvider = new Provider("PKCS11-Mock", "1.0", "Mock PKCS11 Provider") { + @Override + public String toString() { + return "MockPKCS11Provider[" + getName() + "]"; + } + }; + + mockProvider.put("KeyStore.PKCS11", "com.example.MockPKCS11KeyStore"); + mockProvider.put("KeyStore.PKCS11 KeySize", "1024|2048"); + Security.addProvider(mockProvider); + + try { + var options = new CommonOptions(); + var userInteraction = createUserInteractionService("yes\nno\n"); // Select installation then cancel + var service = new TrustStoreService(userInteraction); + + // when + var result = service.useSystemTrustStore(spec, options, null, confPath); + + // then + assertEquals(Integer.valueOf(0), result); + assertTrue(outputCapture.toString().contains("Operation cancelled.")); + } finally { + Security.removeProvider("PKCS11-Mock"); + } + } + + public void testExecuteInteractiveSelectionNonInteractiveMode() { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + // given + var options = new CommonOptions(); + options.nonInteractive = true; + var userInteraction = createUserInteractionService("password123\npassword123\n"); + var service = new TrustStoreService(userInteraction); + + // when + var result = service.executeInteractiveSelection(spec, options, confPath); + + // then + assertTrue(outputCapture.toString().contains("Non-interactive mode: Using generated trust store (default)")); + assertNotNull(result); + } + + public void testExecuteInteractiveSelectionUserSelectsGenerate() { + // given + var options = new CommonOptions(); + var userInteraction = createUserInteractionService("1\nno\n"); // Select option 1, then cancel + var service = new TrustStoreService(userInteraction); + + // when + var result = service.executeInteractiveSelection(spec, options, confPath); + + // then + var output = outputCapture.toString(); + assertTrue(output.contains("OpenSearch FIPS Demo Configuration Installer")); + assertTrue(output.contains("Please select trust store configuration:")); + assertTrue(output.contains("1. Generate new BCFKS trust store from system defaults")); + assertTrue(output.contains("2. Use existing system PKCS11 trust store")); + assertEquals(Integer.valueOf(0), result); // Cancelled + } + + public void testExecuteInteractiveSelectionUserSelectsSystem() { + // given + var options = new CommonOptions(); + var userInteraction = createUserInteractionService("2\nno\n"); // Select option 2, then cancel + var service = new TrustStoreService(userInteraction); + + // when + var result = service.executeInteractiveSelection(spec, options, confPath); + + // then + var output = outputCapture.toString(); + assertTrue(output.contains("OpenSearch FIPS Demo Configuration Installer")); + assertEquals(Integer.valueOf(0), result); // Cancelled + } + + /** + * Creates a test UserInteractionService with simulated user input. + * Uses the same pattern as UserInteractionServiceTests. + */ + private UserInteractionService createUserInteractionService(String input) { + // Cache scanner outside anonymous class to maintain stream position across multiple getScanner() calls + var scanner = new Scanner(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); + return new UserInteractionService() { + @Override + protected Scanner getScanner() { + return scanner; + } + }; + } +} diff --git a/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/UserInteractionServiceTests.java b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/UserInteractionServiceTests.java new file mode 100644 index 0000000000000..5ca06b69007d6 --- /dev/null +++ b/distribution/tools/fips-demo-installer-cli/src/test/java/org/opensearch/tools/cli/fips/truststore/UserInteractionServiceTests.java @@ -0,0 +1,193 @@ +/* + * 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.tools.cli.fips.truststore; + +import org.opensearch.test.OpenSearchTestCase; + +import java.io.ByteArrayInputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; + +import picocli.CommandLine; + +public class UserInteractionServiceTests extends OpenSearchTestCase { + + private CommandLine.Model.CommandSpec spec; + private StringWriter outputCapture; + + @Override + public void setUp() throws Exception { + super.setUp(); + + @CommandLine.Command + class DummyCommand {} + + outputCapture = new StringWriter(); + var commandLine = new CommandLine(new DummyCommand()); + commandLine.setOut(new PrintWriter(outputCapture, true)); + spec = commandLine.getCommandSpec(); + } + + public void testConfirmActionYes() { + { + var service = createService("yes\n"); + assertTrue(service.confirmAction(spec, new CommonOptions(), "Proceed?")); + assertTrue(outputCapture.toString().contains("Proceed? [y/N]")); + } + { + var service = createService("YES\n"); + assertTrue(service.confirmAction(spec, new CommonOptions(), "Proceed?")); + assertTrue(outputCapture.toString().contains("Proceed? [y/N]")); + } + { + var service = createService("y\n"); + assertTrue(service.confirmAction(spec, new CommonOptions(), "Proceed?")); + assertTrue(outputCapture.toString().contains("Proceed? [y/N]")); + } + { + var service = createService("Y\n"); + assertTrue(service.confirmAction(spec, new CommonOptions(), "Proceed?")); + assertTrue(outputCapture.toString().contains("Proceed? [y/N]")); + } + } + + public void testConfirmActionNo() { + var service = createService("no\n"); + assertFalse(service.confirmAction(spec, new CommonOptions(), "Proceed?")); + } + + public void testConfirmActionNonInteractive() { + var options = new CommonOptions(); + options.nonInteractive = true; + var service = createService(""); + assertTrue(service.confirmAction(spec, options, "Proceed?")); + assertTrue(outputCapture.toString().contains("Auto-confirmed")); + } + + public void testConfirmActionNoInput() { + var service = createService(""); + var ex = assertThrows(IllegalStateException.class, () -> service.confirmAction(spec, new CommonOptions(), "Proceed?")); + assertTrue(ex.getMessage().contains("No input available")); + } + + public void testPromptForPasswordValid() { + var service = createService("secret123\n"); + assertEquals("secret123", service.promptForPassword(spec, "Enter password:")); + } + + public void testPromptForPasswordEmpty() { + var service = createService("\n"); + var ex = assertThrows(RuntimeException.class, () -> service.promptForPassword(spec, "Enter password:")); + assertEquals("Password cannot be empty.", ex.getMessage()); + } + + public void testPromptForPasswordNoInput() { + var service = createService(""); + var ex = assertThrows(IllegalStateException.class, () -> service.promptForPassword(spec, "Enter password:")); + assertTrue(ex.getMessage().contains("No input available")); + } + + public void testPromptForPasswordWithConfirmationMatching() { + var service = createService("pass123\npass123\n"); + assertEquals("pass123", service.promptForPasswordWithConfirmation(spec, new CommonOptions(), "Password:")); + } + + public void testPromptForPasswordWithConfirmationNonMatching() { + var service = createService("pass123\nwrong\n"); + var ex = assertThrows( + RuntimeException.class, + () -> service.promptForPasswordWithConfirmation(spec, new CommonOptions(), "Password:") + ); + assertTrue(ex.getMessage().contains("Passwords do not match")); + } + + public void testGenerateSecurePassword() { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + var service = UserInteractionService.getInstance(); + var password = service.generateSecurePassword(); + + assertEquals(24, password.length()); // default password length + assertTrue(password.chars().allMatch(c -> Character.isLetterOrDigit(c) || "!@#$%^&*".indexOf(c) >= 0)); + } + + public void testPromptForChoiceValidSelection() { + var service = createService("2\n"); + assertEquals(2, service.promptForChoice(spec, 3, 1)); + assertTrue(outputCapture.toString().contains("Enter choice (1-3) [1]:")); + } + + public void testPromptForChoiceDefaultSelection() { + var service = createService("\n"); + assertEquals(1, service.promptForChoice(spec, 3, 1)); + assertTrue(outputCapture.toString().contains("Enter choice (1-3) [1]:")); + } + + public void testPromptForChoiceNoInput() { + var service = createService(""); + var ex = assertThrows(IllegalStateException.class, () -> service.promptForChoice(spec, 3, 1)); + assertTrue(ex.getMessage().contains("No input available")); + } + + public void testPromptForChoiceMultipleInvalidAttempts() { + var service = createService("invalid\n0\n10\n-1\n3\n"); + assertEquals(3, service.promptForChoice(spec, 5, 2)); + var output = outputCapture.toString(); + // Should show "Invalid choice" 4 times before accepting valid input + assertEquals(4, output.split("Invalid choice\\.").length - 1); + } + + public void testPromptForChoiceMinBoundaryValue() { + var service = createService("1\n"); + assertEquals(1, service.promptForChoice(spec, 5, 3)); + } + + public void testPromptForChoiceMaxBoundaryValue() { + var service = createService("5\n"); + assertEquals(5, service.promptForChoice(spec, 5, 3)); + } + + public void testPromptForChoiceInvalidChoiceCountZero() { + var service = createService("1\n"); + var ex = expectThrows(AssertionError.class, () -> service.promptForChoice(spec, 0, 1)); + assertTrue(ex.getMessage().contains("Choice count must be greater than 0")); + } + + public void testPromptForChoiceInvalidChoiceCountNegative() { + var service = createService("1\n"); + var ex = expectThrows(AssertionError.class, () -> service.promptForChoice(spec, -1, 1)); + assertTrue(ex.getMessage().contains("Choice count must be greater than 0")); + } + + public void testPromptForChoiceInvalidDefaultTooLow() { + var service = createService("1\n"); + var ex = expectThrows(AssertionError.class, () -> service.promptForChoice(spec, 5, 0)); + assertTrue(ex.getMessage().contains("Default choice must be between 1 and 5")); + } + + public void testPromptForChoiceInvalidDefaultTooHigh() { + var service = createService("1\n"); + var ex = expectThrows(AssertionError.class, () -> service.promptForChoice(spec, 5, 6)); + assertTrue(ex.getMessage().contains("Default choice must be between 1 and 5")); + } + + private UserInteractionService createService(String input) { + // Cache scanner outside anonymous class to maintain stream position across multiple getScanner() calls + var scanner = new Scanner(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); + return new UserInteractionService() { + @Override + protected Scanner getScanner() { + return scanner; + } + }; + } + +} diff --git a/gradle/build-scan.gradle b/gradle/build-scan.gradle index 340fd23bc17dd..5cc61ecb310d3 100644 --- a/gradle/build-scan.gradle +++ b/gradle/build-scan.gradle @@ -27,7 +27,7 @@ buildScan { tag OS.current().name() // Tag if this build is run in FIPS mode - if (BuildParams.inFipsJvm) { + if (BuildParams.isInFipsJvm()) { tag 'FIPS' } } diff --git a/gradle/fips.gradle b/gradle/fips.gradle index a228632fa483d..aee26a19d454e 100644 --- a/gradle/fips.gradle +++ b/gradle/fips.gradle @@ -1,32 +1,100 @@ -// Create isolated configurations for FIPS-specific dependencies. -// These are used to isolate BC FIPS libraries from regular build classpaths, -// avoiding accidental propagation to non-FIPS code paths. -def fipsOnly = project.configurations.maybeCreate("fipsOnly") -def fipsRuntimeOnly = configurations.maybeCreate("fipsRuntimeOnly") -def testFipsOnly = configurations.maybeCreate("testFipsOnly") -def testFipsRuntimeOnly = configurations.maybeCreate("testFipsRuntimeOnly") - -// Map standard Gradle configurations to one or more custom FIPS configurations -def fipsWiring = [ - compileClasspath : [fipsOnly], - runtimeClasspath : [fipsOnly, fipsRuntimeOnly], - testCompileClasspath: [fipsOnly, testFipsOnly], - testRuntimeClasspath: [fipsOnly, fipsRuntimeOnly, testFipsOnly, testFipsRuntimeOnly] -] - -fipsWiring.each { configName, fipsConfigs -> - configurations.named(configName).configure { - extendsFrom(*fipsConfigs) +import org.opensearch.gradle.info.BuildParams + +// Configure FIPS dependencies for full Java projects +plugins.withId('java') { + // Create isolated configurations for FIPS-specific dependencies. + // These are used to isolate BC FIPS libraries from regular build classpaths, + // avoiding accidental propagation to non-FIPS code paths. + def fipsOnly = project.configurations.maybeCreate("fipsOnly") + def fipsRuntimeOnly = configurations.maybeCreate("fipsRuntimeOnly") + def testFipsOnly = configurations.maybeCreate("testFipsOnly") + def testFipsRuntimeOnly = configurations.maybeCreate("testFipsRuntimeOnly") + + // Map standard Gradle configurations to one or more custom FIPS configurations + def fipsWiring = [ + compileClasspath : [fipsOnly], + runtimeClasspath : [fipsOnly, fipsRuntimeOnly], + testCompileClasspath: [fipsOnly, testFipsOnly], + testRuntimeClasspath: [fipsOnly, fipsRuntimeOnly, testFipsOnly, testFipsRuntimeOnly] + ] + + fipsWiring.each { configName, fipsConfigs -> + configurations.named(configName).configure { + extendsFrom(*fipsConfigs) + } + } + + // This prevents unintended BC transitive dependencies (like bcutil, bc-fips) from being pulled in. + afterEvaluate { + def allFipsConfigs = fipsWiring.values().flatten().toSet(); + allFipsConfigs.each { config -> + config.dependencies.configureEach { dep -> + if (dep.metaClass.hasProperty(dep, "transitive")) { + dep.transitive = false + } + } + } + } +} + +// Configure FIPS dependencies for java-base projects (e.g., StandaloneRestTestPlugin) +// JavaBasePlugin doesn't create all standard configurations, so we only create test-related FIPS configs +plugins.withId('java-base') { + // Only create test FIPS configurations for java-base projects + def testFipsOnly = configurations.maybeCreate("testFipsOnly") + def testFipsRuntimeOnly = configurations.maybeCreate("testFipsRuntimeOnly") + + // Wire FIPS configurations only if the target configurations exist + afterEvaluate { + def fipsWiring = [ + testCompileClasspath: [testFipsOnly], + testRuntimeClasspath: [testFipsOnly, testFipsRuntimeOnly] + ] + + fipsWiring.each { configName, fipsConfigs -> + def config = configurations.findByName(configName) + if (config != null) { + config.extendsFrom(*fipsConfigs) + } + } + + // This prevents unintended BC transitive dependencies (like bcutil, bc-fips) from being pulled in. + [testFipsOnly, testFipsRuntimeOnly].each { config -> + config.dependencies.configureEach { dep -> + if (dep.metaClass.hasProperty(dep, "transitive")) { + dep.transitive = false + } + } + } + } +} + +plugins.withId('opensearch.testclusters') { + testClusters.all { cluster -> + if (BuildParams.isInFipsJvm()) { + keystorePassword 'notarealpasswordphrase' + extraConfigFile 'opensearch-fips-truststore.bcfks', + file("${project.rootDir}/buildSrc/src/main/resources/opensearch-fips-truststore.bcfks") + cluster.nodes.all { node -> + node.systemProperty 'javax.net.ssl.trustStore', '${OPENSEARCH_PATH_CONF}/opensearch-fips-truststore.bcfks' + node.systemProperty 'javax.net.ssl.trustStoreType', 'BCFKS' + node.systemProperty 'javax.net.ssl.trustStoreProvider', 'BCFIPS' + node.systemProperty 'javax.net.ssl.trustStorePassword', 'changeit' + } + } } } -// This prevents unintended BC transitive dependencies (like bcutil, bc-fips) from being pulled in. -afterEvaluate { - def allFipsConfigs = fipsWiring.values().flatten().toSet(); - allFipsConfigs.each { config -> - config.dependencies.configureEach { dep -> - if (dep.metaClass.hasProperty(dep, "transitive")) { - dep.transitive = false +// Configure regular test tasks (not using testclusters) for FIPS +// This applies to both 'java' and 'java-base' plugins +['java', 'java-base'].each { pluginId -> + plugins.withId(pluginId) { + tasks.withType(Test).configureEach { testTask -> + if (BuildParams.isInFipsJvm()) { + testTask.systemProperty 'javax.net.ssl.trustStore', "${project.rootDir}/buildSrc/src/main/resources/opensearch-fips-truststore.bcfks" + testTask.systemProperty 'javax.net.ssl.trustStoreType', 'BCFKS' + testTask.systemProperty 'javax.net.ssl.trustStoreProvider', 'BCFIPS' + testTask.systemProperty 'javax.net.ssl.trustStorePassword', 'changeit' } } } diff --git a/gradle/formatting.gradle b/gradle/formatting.gradle index 958afae9dcad7..6ca018a0aac1a 100644 --- a/gradle/formatting.gradle +++ b/gradle/formatting.gradle @@ -100,7 +100,7 @@ allprojects { format 'misc', { target '*.md', '*.gradle', '**/*.json', '**/*.yaml', '**/*.yml', '**/*.svg' - targetExclude '**/simple-bulk11.json', '**/simple-msearch5.json' + targetExclude '**/simple-bulk11.json', '**/simple-msearch5.json', '**/build/**', '**/build-bootstrap/**' trimTrailingWhitespace() endWithNewline() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5caa7cb23666..d66ceb598a3d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -70,6 +70,7 @@ bouncycastle_pkix = "2.1.9" bouncycastle_pg = "2.1.11" bouncycastle_util = "2.1.4" password4j = "1.8.3" +picocli = "4.7.7" # test dependencies randomizedrunner = "2.7.1" diff --git a/libs/ssl-config/src/test/java/org/opensearch/common/ssl/DefaultJdkTrustConfigTests.java b/libs/ssl-config/src/test/java/org/opensearch/common/ssl/DefaultJdkTrustConfigTests.java index 9a723fe491394..ddb5fca788260 100644 --- a/libs/ssl-config/src/test/java/org/opensearch/common/ssl/DefaultJdkTrustConfigTests.java +++ b/libs/ssl-config/src/test/java/org/opensearch/common/ssl/DefaultJdkTrustConfigTests.java @@ -32,13 +32,19 @@ package org.opensearch.common.ssl; +import org.bouncycastle.crypto.CryptoServicesRegistrar; import org.opensearch.test.OpenSearchTestCase; import org.junit.Assert; import javax.net.ssl.X509ExtendedTrustManager; +import java.security.Provider; +import java.security.Security; import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.Optional; import java.util.function.BiFunction; import java.util.stream.Stream; @@ -50,16 +56,39 @@ public class DefaultJdkTrustConfigTests extends OpenSearchTestCase { private static final BiFunction EMPTY_SYSTEM_PROPERTIES = (key, defaultValue) -> defaultValue; + private static final BiFunction PKCS11_SYSTEM_PROPERTIES = (key, defaultValue) -> { + if ("javax.net.ssl.trustStoreType".equals(key)) { + return "PKCS11"; + } + return defaultValue; + }; + private static final BiFunction BCFKS_SYSTEM_PROPERTIES = (key, defaultValue) -> { + if ("javax.net.ssl.trustStoreType".equals(key)) { + return "BCFKS"; + } + return defaultValue; + }; + private static final BiFunction FIPS_AWARE_SYSTEM_PROPERTIES = CryptoServicesRegistrar.isInApprovedOnlyMode() + ? BCFKS_SYSTEM_PROPERTIES + : EMPTY_SYSTEM_PROPERTIES; + + public void testGetSystemPKCS11TrustStoreWithSystemProperties() throws Exception { + assumeFalse("Should only run when PKCS11 provider is installed.", findPKCS11ProviderService().isEmpty()); + final DefaultJdkTrustConfig trustConfig = new DefaultJdkTrustConfig(PKCS11_SYSTEM_PROPERTIES); + assertThat(trustConfig.getDependentFiles(), emptyIterable()); + final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); + assertStandardIssuers(trustManager); + } public void testGetSystemTrustStoreWithNoSystemProperties() throws Exception { - final DefaultJdkTrustConfig trustConfig = new DefaultJdkTrustConfig(EMPTY_SYSTEM_PROPERTIES); + final DefaultJdkTrustConfig trustConfig = new DefaultJdkTrustConfig(FIPS_AWARE_SYSTEM_PROPERTIES); assertThat(trustConfig.getDependentFiles(), emptyIterable()); final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); assertStandardIssuers(trustManager); } public void testGetNonPKCS11TrustStoreWithPasswordSet() throws Exception { - final DefaultJdkTrustConfig trustConfig = new DefaultJdkTrustConfig(EMPTY_SYSTEM_PROPERTIES, "fakepassword".toCharArray()); + final DefaultJdkTrustConfig trustConfig = new DefaultJdkTrustConfig(FIPS_AWARE_SYSTEM_PROPERTIES, "fakepassword".toCharArray()); assertThat(trustConfig.getDependentFiles(), emptyIterable()); final X509ExtendedTrustManager trustManager = trustConfig.createTrustManager(); assertStandardIssuers(trustManager); @@ -88,4 +117,12 @@ private void assertHasTrustedIssuer(X509ExtendedTrustManager trustManager, Strin } } + private List findPKCS11ProviderService() { + return Arrays.stream(Security.getProviders()) + .filter(it -> it.getName().toUpperCase(Locale.ROOT).contains("PKCS11")) + .map(it -> it.getService("KeyStore", "PKCS11")) + .filter(Objects::nonNull) + .toList(); + } + } diff --git a/plugins/discovery-ec2/build.gradle b/plugins/discovery-ec2/build.gradle index 8aeae37742c19..6b54a1dd5eaaf 100644 --- a/plugins/discovery-ec2/build.gradle +++ b/plugins/discovery-ec2/build.gradle @@ -100,7 +100,7 @@ tasks.register("writeTestJavaPolicy") { throw new GradleException("failed to create temporary directory [${tmp}]") } final File javaPolicy = file("${tmp}/java.policy") - if (BuildParams.inFipsJvm) { + if (BuildParams.isInFipsJvm()) { javaPolicy.write( [ "grant {", @@ -140,7 +140,7 @@ tasks.named("test").configure { // Setting a custom policy to manipulate aws.ec2MetadataServiceEndpoint system property // it is better rather disable security manager at all with `systemProperty 'tests.security.manager', 'false'` - if (BuildParams.inFipsJvm){ + if (BuildParams.isInFipsJvm()) { // Using the key==value format to override default JVM security settings and policy // see also: https://docs.oracle.com/javase/8/docs/technotes/guides/security/PolicyFiles.html systemProperty 'java.security.policy', "=file://${buildDir}/tmp/java.policy" diff --git a/plugins/examples/build.gradle b/plugins/examples/build.gradle index 8fd3f3bbc55bd..07a86df3d225b 100644 --- a/plugins/examples/build.gradle +++ b/plugins/examples/build.gradle @@ -22,7 +22,7 @@ gradle.projectsEvaluated { // Disable example project testing with FIPS JVM tasks.withType(Test) { onlyIf { - BuildParams.inFipsJvm == false + BuildParams.isInFipsJvm() == false } } } diff --git a/plugins/ingest-attachment/build.gradle b/plugins/ingest-attachment/build.gradle index 706a570fa139a..e7d6ca4dfdda8 100644 --- a/plugins/ingest-attachment/build.gradle +++ b/plugins/ingest-attachment/build.gradle @@ -145,7 +145,7 @@ thirdPartyAudit { ) } -if (BuildParams.inFipsJvm) { +if (BuildParams.isInFipsJvm()) { // FIPS JVM includes many classes from bouncycastle which count as jar hell for the third party audit, // rather than provide a long list of exclusions, disable the check on FIPS. jarHell.enabled = false diff --git a/plugins/repository-hdfs/build.gradle b/plugins/repository-hdfs/build.gradle index 9eda11c9e4c9d..e9561c1eeb25a 100644 --- a/plugins/repository-hdfs/build.gradle +++ b/plugins/repository-hdfs/build.gradle @@ -134,7 +134,7 @@ for (String fixtureName : ['hdfsFixture', 'haHdfsFixture', 'secureHdfsFixture', executable = "${BuildParams.runtimeJavaHome}/bin/java" env 'CLASSPATH', "${-> configurations.hdfsFixture.asPath}" maxWaitInSeconds = 60 - onlyIf { BuildParams.inFipsJvm == false } + onlyIf { BuildParams.isInFipsJvm() == false } waitCondition = { fixture, ant -> // the hdfs.MiniHDFS fixture writes the ports file when // it's ready, so we can just wait for the file to exist @@ -202,7 +202,7 @@ for (String integTestTaskName : ['integTestHa', 'integTestSecure', 'integTestSec } } - onlyIf { BuildParams.inFipsJvm == false } + onlyIf { BuildParams.isInFipsJvm() == false } if (integTestTaskName.contains("Ha")) { Path portsFile File portsFileDir = file("${workingDir}/hdfsFixture") @@ -287,7 +287,7 @@ if (legalPath == false) { // Always ignore HA integration tests in the normal integration test runner, they are included below as // part of their own HA-specific integration test tasks. integTest { - onlyIf { BuildParams.inFipsJvm == false } + onlyIf { BuildParams.isInFipsJvm() == false } exclude('**/Ha*TestSuiteIT.class') } diff --git a/qa/fips-compliance/build.gradle b/qa/fips-compliance/build.gradle index ce74db326db2d..14f63f4e803a0 100644 --- a/qa/fips-compliance/build.gradle +++ b/qa/fips-compliance/build.gradle @@ -34,6 +34,13 @@ afterEvaluate { // configure cluster to start in FIPS JVM javaRestTest { keystorePassword 'notarealpasswordphrase' + extraConfigFile 'opensearch-fips-truststore.bcfks', + file("${project.rootDir}/buildSrc/src/main/resources/opensearch-fips-truststore.bcfks") + systemProperty 'javax.net.ssl.trustStore', '${OPENSEARCH_PATH_CONF}/opensearch-fips-truststore.bcfks' + systemProperty 'javax.net.ssl.trustStoreType', 'BCFKS' + systemProperty 'javax.net.ssl.trustStoreProvider', 'BCFIPS' + systemProperty 'javax.net.ssl.trustStorePassword', 'changeit' + configurations.bcFips.resolve().each { jarFile -> extraJarFile jarFile } diff --git a/qa/no-bootstrap-tests/src/test/java/org/opensearch/bootstrap/SpawnerNoBootstrapTests.java b/qa/no-bootstrap-tests/src/test/java/org/opensearch/bootstrap/SpawnerNoBootstrapTests.java index 8ca90791f649e..aec4335452a4f 100644 --- a/qa/no-bootstrap-tests/src/test/java/org/opensearch/bootstrap/SpawnerNoBootstrapTests.java +++ b/qa/no-bootstrap-tests/src/test/java/org/opensearch/bootstrap/SpawnerNoBootstrapTests.java @@ -32,6 +32,8 @@ package org.opensearch.bootstrap; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; + import org.apache.lucene.util.Constants; import org.apache.lucene.tests.util.LuceneTestCase; import org.opensearch.Version; @@ -40,6 +42,7 @@ import org.opensearch.env.TestEnvironment; import org.opensearch.plugins.PluginTestUtil; import org.opensearch.plugins.Platforms; +import org.opensearch.test.BouncyCastleThreadFilter; import java.io.BufferedReader; import java.io.IOException; @@ -70,6 +73,7 @@ * that prevents the Spawner class from doing its job. Also needs to run in a separate JVM to other * tests that extend OpenSearchTestCase for the same reason. */ +@ThreadLeakFilters(filters = BouncyCastleThreadFilter.class) public class SpawnerNoBootstrapTests extends LuceneTestCase { private static final String CONTROLLER_SOURCE = "#!/bin/bash\n" diff --git a/qa/remote-clusters/build.gradle b/qa/remote-clusters/build.gradle index 11ebf9af58426..a68b970562fff 100644 --- a/qa/remote-clusters/build.gradle +++ b/qa/remote-clusters/build.gradle @@ -72,10 +72,11 @@ tasks.named("preProcessFixture").configure { dockerCompose { tcpPortsToIgnoreWhenWaiting = [9600, 9601] - useComposeFiles = ['docker-compose.yml'] - if (BuildParams.inFipsJvm) { - environment.put("KEYSTORE_PASSWORD", "notarealpasswordphrase") + def composeFiles = ['docker-compose.yml'] + if (BuildParams.isInFipsJvm()) { + composeFiles.add('docker-compose.fips.yml') } + useComposeFiles = composeFiles } def createAndSetWritable(Object... locations) { diff --git a/qa/remote-clusters/docker-compose.fips.yml b/qa/remote-clusters/docker-compose.fips.yml new file mode 100644 index 0000000000000..fd862d079b644 --- /dev/null +++ b/qa/remote-clusters/docker-compose.fips.yml @@ -0,0 +1,9 @@ +services: + opensearch-1: + environment: + KEYSTORE_PASSWORD: notarealpasswordphrase + FIPS_GENERATE_TRUSTSTORE: true + opensearch-2: + environment: + KEYSTORE_PASSWORD: notarealpasswordphrase + FIPS_GENERATE_TRUSTSTORE: true diff --git a/qa/remote-clusters/docker-compose.yml b/qa/remote-clusters/docker-compose.yml index ceeb3a394ee27..2112da17efe6e 100644 --- a/qa/remote-clusters/docker-compose.yml +++ b/qa/remote-clusters/docker-compose.yml @@ -16,7 +16,6 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - node.store.allow_mmap=false - - "KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD}" volumes: - ./build/repo:/tmp/opensearch-repo - ./build/logs/1:/usr/share/opensearch/logs @@ -51,7 +50,6 @@ services: - cluster.routing.allocation.disk.watermark.high=1b - cluster.routing.allocation.disk.watermark.flood_stage=1b - node.store.allow_mmap=false - - "KEYSTORE_PASSWORD=${KEYSTORE_PASSWORD}" volumes: - ./build/repo:/tmp/opensearch-repo - ./build/logs/2:/usr/share/opensearch/logs diff --git a/qa/wildfly/build.gradle b/qa/wildfly/build.gradle index d3efb1cdf2eaf..b9ffe062e8f3e 100644 --- a/qa/wildfly/build.gradle +++ b/qa/wildfly/build.gradle @@ -85,11 +85,16 @@ preProcessFixture { dependsOn war, opensearch_distributions.docker } +tasks.named('spotlessJava').configure { + mustRunAfter preProcessFixture +} + dockerCompose { - useComposeFiles = ['docker-compose.yml'] - if (BuildParams.inFipsJvm) { - environment.put("KEYSTORE_PASSWORD", "notarealpasswordphrase") + def composeFiles = ['docker-compose.yml'] + if (BuildParams.isInFipsJvm()) { + composeFiles.add('docker-compose.fips.yml') } + useComposeFiles = composeFiles } tasks.register("integTest", TestTask) { diff --git a/qa/wildfly/docker-compose.fips.yml b/qa/wildfly/docker-compose.fips.yml new file mode 100644 index 0000000000000..bc1c202141742 --- /dev/null +++ b/qa/wildfly/docker-compose.fips.yml @@ -0,0 +1,5 @@ +services: + opensearch: + environment: + KEYSTORE_PASSWORD: notarealpasswordphrase + FIPS_GENERATE_TRUSTSTORE: true diff --git a/qa/wildfly/docker-compose.yml b/qa/wildfly/docker-compose.yml index f34033b388353..2a0779d999f25 100644 --- a/qa/wildfly/docker-compose.yml +++ b/qa/wildfly/docker-compose.yml @@ -19,7 +19,6 @@ services: opensearch: image: opensearch:test environment: - KEYSTORE_PASSWORD: ${KEYSTORE_PASSWORD} discovery.type: single-node ulimits: memlock: diff --git a/server/build.gradle b/server/build.gradle index c2b3b1b2788a1..0aa37b551799b 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -127,6 +127,8 @@ dependencies { exclude group: 'org.opensearch', module: 'server' } testFipsRuntimeOnly "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}" + testFipsRuntimeOnly "org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}" + testFipsRuntimeOnly "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}" } tasks.withType(JavaCompile).configureEach { diff --git a/server/src/main/java/org/opensearch/bootstrap/Bootstrap.java b/server/src/main/java/org/opensearch/bootstrap/Bootstrap.java index 0da5e335b0081..d8f1592d7e7a4 100644 --- a/server/src/main/java/org/opensearch/bootstrap/Bootstrap.java +++ b/server/src/main/java/org/opensearch/bootstrap/Bootstrap.java @@ -200,6 +200,7 @@ private void setup(boolean addShutdownHook, Environment environment) throws Boot if ("FIPS-140-3".equals(cryptoStandard) || "true".equalsIgnoreCase(System.getProperty("org.bouncycastle.fips.approved_only"))) { LogManager.getLogger(Bootstrap.class).info("running in FIPS-140-3 mode"); SecurityProviderManager.removeNonCompliantFipsProviders(); + FipsTrustStoreValidator.validate(); } // initialize probes before the security manager is installed diff --git a/server/src/main/java/org/opensearch/bootstrap/FipsTrustStoreValidator.java b/server/src/main/java/org/opensearch/bootstrap/FipsTrustStoreValidator.java new file mode 100644 index 0000000000000..e2eaff4b6198c --- /dev/null +++ b/server/src/main/java/org/opensearch/bootstrap/FipsTrustStoreValidator.java @@ -0,0 +1,155 @@ +/* + * 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.bootstrap; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Map.entry; + +/** + * Validator for FIPS-compliant SSL trust store configuration. + * + *

This utility validates that the required SSL trust store properties are properly configured + * for FIPS mode operation. It ensures that all necessary system properties are set and that + * the trust store file is accessible and valid.

+ */ +public class FipsTrustStoreValidator { + + private static final Logger LOGGER = LogManager.getLogger(FipsTrustStoreValidator.class); + private static final String TRUST_STORE_PATH_PROP = "javax.net.ssl.trustStore"; + private static final String TRUST_STORE_TYPE_PROP = "javax.net.ssl.trustStoreType"; + private static final String TRUST_STORE_PROVIDER_PROP = "javax.net.ssl.trustStoreProvider"; + private static final String TRUST_STORE_PASSWORD_PROP = "javax.net.ssl.trustStorePassword"; + private static final String ALLOWED_TYPE_PKCS11 = "PKCS11"; + private static final String ALLOWED_TYPE_BCFKS = "BCFKS"; + + /** + * Validates the FIPS trust store configuration from system properties. + * + * @throws IllegalStateException if the trust store configuration is invalid + */ + public static void validate() { + validate( + System.getProperty(TRUST_STORE_PATH_PROP, ""), + System.getProperty(TRUST_STORE_TYPE_PROP, ""), + System.getProperty(TRUST_STORE_PROVIDER_PROP, ""), + System.getProperty(TRUST_STORE_PASSWORD_PROP, "") + ); + } + + /** + * Validates the FIPS trust store configuration with explicit parameters. + * + * @param trustStorePath the path to the trust store file + * @param trustStoreType the type of the trust store (must be PKCS11 or BCFKS for FIPS compliance) + * @param trustStoreProvider the security provider name + * @param trustStorePassword the trust store password + * @throws IllegalStateException if the trust store configuration is invalid + */ + protected static void validate(String trustStorePath, String trustStoreType, String trustStoreProvider, String trustStorePassword) { + var requiredProperties = switch (trustStoreType) { + case ALLOWED_TYPE_PKCS11 -> Map.ofEntries( + entry(TRUST_STORE_TYPE_PROP, trustStoreType), + entry(TRUST_STORE_PROVIDER_PROP, trustStoreProvider) + ); + case ALLOWED_TYPE_BCFKS -> Map.ofEntries( + entry(TRUST_STORE_PATH_PROP, trustStorePath), + entry(TRUST_STORE_TYPE_PROP, trustStoreType), + entry(TRUST_STORE_PROVIDER_PROP, trustStoreProvider) + ); + default -> throw new IllegalStateException( + "Trust store type must be PKCS11 or BCFKS for FIPS compliance. Found: " + trustStoreType + ); + }; + + var missingProperties = requiredProperties.entrySet().stream().filter(entry -> entry.getValue().isBlank()).toList(); + if (!missingProperties.isEmpty()) { + throw new IllegalStateException( + "FIPS trust store is not set-up. Cannot find following JRE properties:\n" + + missingProperties.stream().map(Map.Entry::getKey).collect(Collectors.joining("\n")) + ); + } + + if (ALLOWED_TYPE_BCFKS.equals(trustStoreType)) { + validateBCFKSFile(trustStorePath); + } + + try { + var provider = Security.getProvider(trustStoreProvider); + if (provider == null) { + throw new IllegalStateException("Trust store provider not available: " + trustStoreProvider); + } + + var keyStore = KeyStore.getInstance(trustStoreType, provider); + + switch (trustStoreType) { + case ALLOWED_TYPE_PKCS11 -> keyStore.load(null, trustStorePassword.toCharArray()); + case ALLOWED_TYPE_BCFKS -> { + try (var inputStream = Files.newInputStream(Path.of(trustStorePath))) { + keyStore.load(inputStream, trustStorePassword.toCharArray()); + } + } + } + + if (keyStore.size() == 0) { + LOGGER.warn("Trust store is valid but contains no certificates (type: {})", trustStoreType); + } + + LOGGER.debug( + "Trust store validation successful (type: {}, provider: {}, certificates: {})", + trustStoreType, + provider.getName(), + keyStore.size() + ); + } catch (KeyStoreException e) { + throw new IllegalStateException("Invalid trust store type or provider: " + e.getMessage(), e); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("Trust store algorithm not supported: " + e.getMessage(), e); + } catch (CertificateException e) { + throw new IllegalStateException("Trust store contains invalid certificates: " + e.getMessage(), e); + } catch (IOException e) { + throw new UncheckedIOException("Cannot read trust store file (possibly wrong password): " + e.getMessage(), e); + } catch (Exception e) { + throw new IllegalStateException("Trust store validation failed: " + e.getMessage(), e); + } + } + + private static void validateBCFKSFile(String trustStorePath) { + var trustStoreFile = Path.of(trustStorePath); + if (!Files.exists(trustStoreFile)) { + throw new IllegalStateException("Trust store file does not exist: " + trustStorePath); + } + + if (!Files.isReadable(trustStoreFile)) { + throw new IllegalStateException("Trust store file is not readable: " + trustStorePath); + } + + try { + if (Files.size(trustStoreFile) == 0) { + throw new IllegalStateException("Trust store file is empty: " + trustStorePath); + } + } catch (IOException e) { + throw new UncheckedIOException("Cannot access trust store file: " + e.getMessage(), e); + } + } + +} diff --git a/server/src/test/java/org/opensearch/bootstrap/FipsTrustStoreValidatorTests.java b/server/src/test/java/org/opensearch/bootstrap/FipsTrustStoreValidatorTests.java new file mode 100644 index 0000000000000..be3067b362c96 --- /dev/null +++ b/server/src/test/java/org/opensearch/bootstrap/FipsTrustStoreValidatorTests.java @@ -0,0 +1,207 @@ +/* + * 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.bootstrap; + +import org.opensearch.common.util.io.IOUtils; +import org.opensearch.test.OpenSearchTestCase; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.KeyStore; +import java.security.KeyStoreException; + +public class FipsTrustStoreValidatorTests extends OpenSearchTestCase { + + private Path tempDir; + + @Override + public void setUp() throws Exception { + super.setUp(); + tempDir = createTempDir(); + } + + public void testValidateAcceptsPKCS11() throws Exception { + var exception = expectThrows( + IllegalStateException.class, + () -> FipsTrustStoreValidator.validate("", "PKCS11", "NonExistentPKCS11Provider", "") + ); + + // Proves PKCS11 is accepted as from valid type (didn't throw "must be PKCS11 or BCFKS") + assertTrue(exception.getMessage().contains("Trust store provider not available")); + } + + public void testValidateAcceptsBCFKSType() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + var trustStoreResource = getClass().getResource("/org/opensearch/bootstrap/truststore/opensearch-fips-truststore.bcfks"); + assertNotNull(trustStoreResource); + var trustStorePath = Path.of(trustStoreResource.toURI()); + + // Should not throw exception + FipsTrustStoreValidator.validate(trustStorePath.toString(), "BCFKS", "BCFIPS", "notarealpasswordphrase"); + } + + public void testBCFKSEmptyTrustStoreWarning() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + var trustStorePath = tempDir.resolve("empty_trust_store.bcfks"); + var password = "testPassword"; + + var keyStore = java.security.KeyStore.getInstance("BCFKS", "BCFIPS"); + keyStore.load(null, password.toCharArray()); + + try (var fos = Files.newOutputStream(trustStorePath)) { + keyStore.store(fos, password.toCharArray()); + } + + FipsTrustStoreValidator.validate(trustStorePath.toString(), "BCFKS", "BCFIPS", password); + } + + public void testTrustStoreWithWrongPassword() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + + var trustStoreResource = getClass().getResource("/org/opensearch/bootstrap/truststore/opensearch-fips-truststore.bcfks"); + assertNotNull("FIPS truststore resource not found", trustStoreResource); + var trustStorePath = Path.of(trustStoreResource.toURI()); + + var exception = expectThrows( + java.io.UncheckedIOException.class, + () -> FipsTrustStoreValidator.validate(trustStorePath.toString(), "BCFKS", "BCFIPS", "wrongPassword") + ); + + assertTrue(exception.getMessage().contains("possibly wrong password")); + } + + public void testTrustStoreWithInvalidType() throws Exception { + var exception = expectThrows( + IllegalStateException.class, + () -> FipsTrustStoreValidator.validate("test-truststore.p12", "PKCS12", "SUN", "changeit") + ); + + assertTrue(exception.getMessage().contains("Trust store type must be PKCS11 or BCFKS for FIPS compliance")); + } + + public void testBCFKSWithMissingProperties() { + { + var exception = expectThrows(IllegalStateException.class, () -> FipsTrustStoreValidator.validate("", "BCFKS", "BCFIPS", "")); + assertEquals( + "FIPS trust store is not set-up. Cannot find following JRE properties:\njavax.net.ssl.trustStore", + exception.getMessage() + ); + } + + { + var exception = expectThrows(IllegalStateException.class, () -> FipsTrustStoreValidator.validate(" ", "BCFKS", "BCFIPS", "")); + assertEquals( + "FIPS trust store is not set-up. Cannot find following JRE properties:\njavax.net.ssl.trustStore", + exception.getMessage() + ); + } + + { + var exception = expectThrows(IllegalStateException.class, () -> FipsTrustStoreValidator.validate("/path", "BCFKS", "", "")); + assertEquals( + "FIPS trust store is not set-up. Cannot find following JRE properties:\njavax.net.ssl.trustStoreProvider", + exception.getMessage() + ); + } + } + + public void testPKCS11WithMissingProperties() { + { + var exception = expectThrows(IllegalStateException.class, () -> FipsTrustStoreValidator.validate("", "PKCS11", "", "")); + assertEquals( + "FIPS trust store is not set-up. Cannot find following JRE properties:\njavax.net.ssl.trustStoreProvider", + exception.getMessage() + ); + } + + { + var exception = expectThrows(IllegalStateException.class, () -> FipsTrustStoreValidator.validate("", "PKCS11", " ", "")); + assertEquals( + "FIPS trust store is not set-up. Cannot find following JRE properties:\njavax.net.ssl.trustStoreProvider", + exception.getMessage() + ); + } + } + + public void testFileDoesNotExist() { + var nonExistentPath = tempDir.resolve("non-existent-truststore.p12"); + + var exception = expectThrows( + IllegalStateException.class, + () -> FipsTrustStoreValidator.validate(nonExistentPath.toString(), "BCFKS", "BCFIPS", "changeit") + ); + + assertTrue(exception.getMessage().contains("Trust store file does not exist")); + assertTrue(exception.getMessage().contains(nonExistentPath.toString())); + } + + public void testEmptyTrustStore() throws Exception { + var emptyFile = tempDir.resolve("empty-truststore.p12"); + Files.createFile(emptyFile); + + var exception = expectThrows( + IllegalStateException.class, + () -> FipsTrustStoreValidator.validate(emptyFile.toString(), "BCFKS", "BCFIPS", "changeit") + ); + + assertTrue(exception.getMessage().contains("Trust store file is empty")); + assertTrue(exception.getMessage().contains(emptyFile.toString())); + } + + public void testWithoutReadPermission() throws Exception { + assumeTrue("Should only run when BCFIPS provider is installed.", inFipsJvm()); + assumeTrue("requires POSIX file permissions", (IOUtils.LINUX || IOUtils.MAC_OS_X)); + var tempBcfksFile = Files.createTempFile(createTempDir(), "no-write", ".bcfks"); + Files.setPosixFilePermissions(tempBcfksFile, PosixFilePermissions.fromString("---------")); + + try { + var keyStore = KeyStore.getInstance("BCFKS", "BCFIPS"); + keyStore.load(null, "changeit".toCharArray()); + + var throwable = assertThrows( + IllegalStateException.class, + () -> FipsTrustStoreValidator.validate(tempBcfksFile.toAbsolutePath().toString(), "BCFKS", "BCFIPS", "changeit") + ); + assertTrue(throwable.getMessage().startsWith("Trust store file is not readable: " + tempBcfksFile)); + } finally { + Files.setPosixFilePermissions(tempBcfksFile, PosixFilePermissions.fromString("rwxrwxrwx")); + } + } + + public void testTrustStoreWithInvalidProvider() throws Exception { + var trustStorePath = tempDir.resolve("test-truststore.bcfks"); + var password = "changeit"; + Files.write(trustStorePath, new byte[100]); // Create a dummy file + + var exception = expectThrows( + IllegalStateException.class, + () -> FipsTrustStoreValidator.validate(trustStorePath.toString(), "BCFKS", "NON_EXISTENT_PROVIDER", password) + ); + + assertTrue(exception.getMessage().contains("Trust store provider not available")); + assertTrue(exception.getMessage().contains("NON_EXISTENT_PROVIDER")); + } + + public void testWithWrongProvider() throws Exception { + var trustStorePath = tempDir.resolve("test-truststore.bcfks"); + Files.write(trustStorePath, new byte[100]); // Create a dummy file + + var exception = expectThrows( + IllegalStateException.class, + () -> FipsTrustStoreValidator.validate(trustStorePath.toString(), "BCFKS", "SUN", "changeit") + ); + + assertTrue(exception.getMessage().contains("Invalid trust store type or provider")); + assertTrue(exception.getCause() instanceof KeyStoreException); + } + +} diff --git a/server/src/test/java/org/opensearch/bootstrap/SecurityProviderManagerTests.java b/server/src/test/java/org/opensearch/bootstrap/SecurityProviderManagerTests.java index 90eefcfc38f44..892e20ad14577 100644 --- a/server/src/test/java/org/opensearch/bootstrap/SecurityProviderManagerTests.java +++ b/server/src/test/java/org/opensearch/bootstrap/SecurityProviderManagerTests.java @@ -9,6 +9,7 @@ package org.opensearch.bootstrap; import org.opensearch.test.OpenSearchTestCase; +import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -35,16 +36,21 @@ public class SecurityProviderManagerTests extends OpenSearchTestCase { @Override public void setUp() throws Exception { super.setUp(); - if (Arrays.stream(Security.getProviders()).noneMatch(provider -> SUN_JCE.equals(provider.getName()))) { - var sunJceClass = Class.forName("com.sun.crypto.provider.SunJCE"); - var originalSunProvider = (Provider) sunJceClass.getConstructor().newInstance(); - Security.addProvider(originalSunProvider); + addSunJceProvider(); + } + + @After + @Override + public void tearDown() throws Exception { + if (!inFipsJvm()) { + addSunJceProvider(); } + super.tearDown(); } @AfterClass // restore the same state as before running the tests. - public static void removeSunJCE() { + public static void afterClass() throws Exception { if (inFipsJvm()) { SecurityProviderManager.removeNonCompliantFipsProviders(); } @@ -146,4 +152,12 @@ public void testGetPosition() { assertTrue(SUN_JCE + " is uninstalled", SecurityProviderManager.getPosition(SUN_JCE) < 0); } + private static void addSunJceProvider() throws Exception { + if (Arrays.stream(Security.getProviders()).noneMatch(provider -> SUN_JCE.equals(provider.getName()))) { + var sunJceClass = Class.forName("com.sun.crypto.provider.SunJCE"); + var originalSunProvider = (Provider) sunJceClass.getConstructor().newInstance(); + Security.addProvider(originalSunProvider); + } + } + } diff --git a/server/src/test/resources/org/opensearch/bootstrap/truststore/README.md b/server/src/test/resources/org/opensearch/bootstrap/truststore/README.md new file mode 100644 index 0000000000000..0e6025f7fc793 --- /dev/null +++ b/server/src/test/resources/org/opensearch/bootstrap/truststore/README.md @@ -0,0 +1,4 @@ +# generate BCFKS trust store for FipsTrustStoreValidatorTests.java +```bash +printf 'y\nnotarealpasswordphrase\nnotarealpasswordphrase' | sh ./opensearch-fips-demo-installer generated +``` diff --git a/server/src/test/resources/org/opensearch/bootstrap/truststore/opensearch-fips-truststore.bcfks b/server/src/test/resources/org/opensearch/bootstrap/truststore/opensearch-fips-truststore.bcfks new file mode 100644 index 0000000000000..a38be6aa4f494 Binary files /dev/null and b/server/src/test/resources/org/opensearch/bootstrap/truststore/opensearch-fips-truststore.bcfks differ diff --git a/settings.gradle b/settings.gradle index a1cd3c76e5c22..4b77ce8436d9d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -73,6 +73,7 @@ List projects = [ 'distribution:bwc:maintenance', 'distribution:bwc:minor', 'distribution:bwc:staged', + 'distribution:tools:fips-demo-installer-cli', 'distribution:tools:java-version-checker', 'distribution:tools:launchers', 'distribution:tools:plugin-cli', diff --git a/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java b/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java index b7ecf379e3f97..fcb54c7d9e01d 100644 --- a/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java +++ b/test/framework/src/main/java/org/opensearch/bootstrap/BootstrapForTesting.java @@ -139,6 +139,7 @@ public class BootstrapForTesting { IfConfig.logIfNecessary(); if (FipsMode.CHECK.isFipsEnabled()) { SecurityProviderManager.removeNonCompliantFipsProviders(); + FipsTrustStoreValidator.validate(); } // install security manager if requested diff --git a/test/framework/src/main/java/org/opensearch/test/OpenSearchTestCase.java b/test/framework/src/main/java/org/opensearch/test/OpenSearchTestCase.java index 147503ac70c21..a36a028b75e01 100644 --- a/test/framework/src/main/java/org/opensearch/test/OpenSearchTestCase.java +++ b/test/framework/src/main/java/org/opensearch/test/OpenSearchTestCase.java @@ -33,6 +33,7 @@ import com.carrotsearch.randomizedtesting.RandomizedTest; import com.carrotsearch.randomizedtesting.annotations.Listeners; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakLingering; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope.Scope; @@ -207,6 +208,7 @@ @Listeners({ ReproduceInfoPrinter.class, LoggingListener.class }) @ThreadLeakScope(Scope.SUITE) @ThreadLeakLingering(linger = 5000) // 5 sec lingering +@ThreadLeakFilters(filters = BouncyCastleThreadFilter.class) @TimeoutSuite(millis = 20 * TimeUnits.MINUTE) @LuceneTestCase.SuppressSysoutChecks(bugUrl = "we log a lot on purpose") // we suppress pretty much all the lucene codecs for now, except asserting diff --git a/test/framework/src/main/java/org/opensearch/test/junit/listeners/ReproduceInfoPrinter.java b/test/framework/src/main/java/org/opensearch/test/junit/listeners/ReproduceInfoPrinter.java index 4e1d56e3951de..dfbc607b058a5 100644 --- a/test/framework/src/main/java/org/opensearch/test/junit/listeners/ReproduceInfoPrinter.java +++ b/test/framework/src/main/java/org/opensearch/test/junit/listeners/ReproduceInfoPrinter.java @@ -197,6 +197,9 @@ private ReproduceErrorMessageBuilder appendESProperties() { if (FipsMode.CHECK.isFipsEnabled()) { appendProperties("org.bouncycastle.fips.approved_only"); } + if (System.getProperty("java.security.properties") != null) { + appendProperties("java.security.properties"); + } return this; }