Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for FIPS compliance mode #14912

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG-3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- GHA to verify checklist items completion in PR descriptions ([#10800](https://github.com/opensearch-project/OpenSearch/pull/10800))
- Allow to pass the list settings through environment variables (like [], ["a", "b", "c"], ...) ([#10625](https://github.com/opensearch-project/OpenSearch/pull/10625))
- Views, simplify data access and manipulation by providing a virtual layer over one or more indices ([#11957](https://github.com/opensearch-project/OpenSearch/pull/11957))
- Support for FIPS-140-3 compliance through environment variable ([#3420](https://github.com/opensearch-project/OpenSearch/pull/14912))

### Dependencies

Expand Down
12 changes: 10 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ apply from: 'gradle/ide.gradle'
apply from: 'gradle/forbidden-dependencies.gradle'
apply from: 'gradle/formatting.gradle'
apply from: 'gradle/local-distribution.gradle'
apply from: 'gradle/fips.gradle'
beanuwave marked this conversation as resolved.
Show resolved Hide resolved
apply from: 'gradle/run.gradle'
apply from: 'gradle/missing-javadoc.gradle'
apply from: 'gradle/code-coverage.gradle'
Expand Down Expand Up @@ -472,8 +471,8 @@ gradle.projectsEvaluated {
}
}

// test retry configuration
subprojects {
// test retry configuration
tasks.withType(Test).configureEach {
develocity.testRetry {
if (BuildParams.isCi()) {
Expand Down Expand Up @@ -559,6 +558,15 @@ subprojects {
}
}
}

// test with FIPS-140-3 enabled
plugins.withType(JavaPlugin).configureEach {
tasks.withType(Test).configureEach { testTask ->
if (System.getenv('OPENSEARCH_CRYPTO_STANDARD') == 'FIPS-140-3') {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use BuildParams consistently please:

Suggested change
if (System.getenv('OPENSEARCH_CRYPTO_STANDARD') == 'FIPS-140-3') {
if (BuildParams.inFipsJvm == true) {

testTask.jvmArgs += "-Dorg.bouncycastle.fips.approved_only=true"
}
}
}
}

// eclipse configuration
Expand Down
6 changes: 2 additions & 4 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ dependencies {
api 'org.jruby.joni:joni:2.2.1'
api "com.fasterxml.jackson.core:jackson-databind:${props.getProperty('jackson_databind')}"
api "org.ajoberstar.grgit:grgit-core:5.2.1"
api "org.bouncycastle:bc-fips:${props.getProperty('bouncycastle_jce')}"
Copy link
Collaborator

@reta reta Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is optional dependency and should not be included by default (this is build tooling which mostly every single plugin will depend upon:

Suggested change
api "org.bouncycastle:bc-fips:${props.getProperty('bouncycastle_jce')}"
if (System.getenv('OPENSEARCH_CRYPTO_STANDARD') == 'FIPS-140-3') {
api "org.bouncycastle:bc-fips:${props.getProperty('bouncycastle_jce')}"
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(we sadly cannot use BuildParams here)



testFixturesApi "junit:junit:${props.getProperty('junit')}"
testFixturesApi "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${props.getProperty('randomizedrunner')}"
Expand Down Expand Up @@ -229,12 +231,8 @@ if (project != rootProject) {

forbiddenPatterns {
exclude '**/*.wav'
exclude '**/*.p12'
exclude '**/*.jks'
exclude '**/*.crt'
// the file that actually defines nocommit
exclude '**/ForbiddenPatternsTask.java'
exclude '**/*.bcfks'
}

testingConventions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@
test.systemProperty("tests.seed", BuildParams.getTestSeed());
}

var securityFile = BuildParams.isInFipsJvm() ? "fips_java.security" : "java.security";
test.systemProperty(

Check warning on line 168 in buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java#L168

Added line #L168 was not covered by tests
"java.security.properties",
project.getRootProject().getLayout().getProjectDirectory() + "/distribution/src/config/" + securityFile

Check warning on line 170 in buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/OpenSearchTestBasePlugin.java#L170

Added line #L170 was not covered by tests
);

// don't track these as inputs since they contain absolute paths and break cache relocatability
File gradleHome = project.getGradle().getGradleUserHomeDir();
String gradleVersion = project.getGradle().getGradleVersion();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@

package org.opensearch.gradle.http;

import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.internal.impldep.com.jcraft.jsch.annotations.SuppressForbiddenApi;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import org.gradle.internal.impldep.com.jcraft.jsch.annotations.SuppressForbiddenApi;
import de.thetaphi.forbiddenapis.SuppressForbidden;


import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
Expand All @@ -51,7 +53,6 @@
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Arrays;
Expand Down Expand Up @@ -216,15 +217,15 @@
}

private KeyStore buildTrustStoreFromFile() throws GeneralSecurityException, IOException {
KeyStore keyStore = KeyStore.getInstance(trustStoreFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
var keyStore = getKeyStoreInstance(trustStoreFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
try (InputStream input = new FileInputStream(trustStoreFile)) {
keyStore.load(input, trustStorePassword == null ? null : trustStorePassword.toCharArray());
}
return keyStore;
}

private KeyStore buildTrustStoreFromCA() throws GeneralSecurityException, IOException {
final KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
var store = getKeyStoreInstance(KeyStore.getDefaultType());
store.load(null, null);
final CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
int counter = 0;
Expand All @@ -239,12 +240,17 @@
return store;
}

@SuppressForbiddenApi("runs exclusively in test-context without KeyStoreFactory on classpath.")
Copy link
Collaborator

@reta reta Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@SuppressForbiddenApi("runs exclusively in test-context without KeyStoreFactory on classpath.")
@SuppressForbidden

private KeyStore getKeyStoreInstance(String type) throws KeyStoreException {
return KeyStore.getInstance(type);
}

private SSLContext createSslContext(KeyStore trustStore) throws GeneralSecurityException {
checkForTrustEntry(trustStore);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(new KeyManager[0], tmf.getTrustManagers(), new SecureRandom());
sslContext.init(new KeyManager[0], tmf.getTrustManagers(), CryptoServicesRegistrar.getSecureRandom());

Check warning on line 253 in buildSrc/src/main/java/org/opensearch/gradle/http/WaitForHttpResource.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/http/WaitForHttpResource.java#L253

Added line #L253 was not covered by tests
Copy link
Collaborator

@reta reta Jan 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since FIPS dependency has to be optional, we need to make its usage so as well:

Suggested change
sslContext.init(new KeyManager[0], tmf.getTrustManagers(), CryptoServicesRegistrar.getSecureRandom());
private static final class SecureRandomProvider {
private final static MethodHandle MH_BC_SECURE_RANDOM;
static {
MethodHandle mh = null;
if (FipsBuildParams.isInFipsMode()) {
try {
final Class<?> cryptoServicesRegistrarClass = Class.forName("org.bouncycastle.crypto.CryptoServicesRegistrar");
mh = MethodHandles.publicLookup()
.findStatic(cryptoServicesRegistrarClass, "getSecureRandom", MethodType.methodType(SecureRandom.class));
} catch (final Throwable ex) {
throw new SecurityException("Unable to find org.bouncycastle.crypto.CryptoServicesRegistrar class", ex);
}
}
MH_BC_SECURE_RANDOM = mh;
}
public static SecureRandom getSecureRandom() throws GeneralSecurityException {
if (MH_BC_SECURE_RANDOM == null) {
return new SecureRandom();
} else try {
return (SecureRandom) MH_BC_SECURE_RANDOM.invoke();
} catch (final Throwable ex) {
throw new GeneralSecurityException(ex);
}
}
}
...
sslContext.init(new KeyManager[0], tmf.getTrustManagers(), SecureRandomProvider.getSecureRandom());

return sslContext;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
private static final Logger LOGGER = Logging.getLogger(GlobalBuildInfoPlugin.class);
private static final String DEFAULT_LEGACY_VERSION_JAVA_FILE_PATH = "libs/core/src/main/java/org/opensearch/LegacyESVersion.java";
private static final String DEFAULT_VERSION_JAVA_FILE_PATH = "libs/core/src/main/java/org/opensearch/Version.java";
protected static final String OPENSEARCH_CRYPTO_STANDARD = "OPENSEARCH_CRYPTO_STANDARD";
private static Integer _defaultParallel = null;

private final JvmMetadataDetector jvmMetadataDetector;
Expand Down Expand Up @@ -112,6 +113,8 @@
BuildParams.init(params -> {
// Initialize global build parameters
boolean isInternal = GlobalBuildInfoPlugin.class.getResource("/buildSrc.marker") != null;
var cryptoStandard = System.getenv(OPENSEARCH_CRYPTO_STANDARD);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to extract this into FipsBuild helper class (or similar), so we could consolidate OPENSEARCH_CRYPTO_STANDARD usage in 1 place:

public final class FipsBuildParams {
    private static final String fipsMode = System.getenv("OPENSEARCH_CRYPTO_STANDARD");
    
    private FipsBuildParams() {
    }
    
    public static boolean isInFipsMode() {
        return "FIPS-140-3".equals(fipsMode);
    }
    
    public static String getFipsMode() {
        return fipsMode;
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var inFipsJvm = "FIPS-140-3".equals(cryptoStandard);

params.reset();
params.setRuntimeJavaHome(runtimeJavaHome);
Expand All @@ -129,7 +132,7 @@
params.setIsCi(System.getenv("JENKINS_URL") != null);
params.setIsInternal(isInternal);
params.setDefaultParallel(findDefaultParallel(project));
params.setInFipsJvm(Util.getBooleanProperty("tests.fips.enabled", false));
params.setInFipsJvm(inFipsJvm);
params.setIsSnapshotBuild(Util.getBooleanProperty("build.snapshot", true));
if (isInternal) {
params.setBwcVersions(resolveBwcVersions(rootDir));
Expand Down Expand Up @@ -163,6 +166,7 @@
final String osArch = System.getProperty("os.arch");
final Jvm gradleJvm = Jvm.current();
final String gradleJvmDetails = getJavaInstallation(gradleJvm.getJavaHome()).getDisplayName();
final String cryptStandard = System.getenv(OPENSEARCH_CRYPTO_STANDARD);

Check warning on line 169 in buildSrc/src/main/java/org/opensearch/gradle/info/GlobalBuildInfoPlugin.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/info/GlobalBuildInfoPlugin.java#L169

Added line #L169 was not covered by tests

LOGGER.quiet("=======================================");
LOGGER.quiet("OpenSearch Build Hamster says Hello!");
Expand All @@ -179,7 +183,11 @@
LOGGER.quiet(" JAVA_HOME : " + gradleJvm.getJavaHome());
}
LOGGER.quiet(" Random Testing Seed : " + BuildParams.getTestSeed());
LOGGER.quiet(" In FIPS 140 mode : " + BuildParams.isInFipsJvm());
if (cryptStandard != null && cryptStandard.equals("FIPS-140-3")) {
LOGGER.quiet(" Crypto Standard : FIPS-140-3");

Check warning on line 187 in buildSrc/src/main/java/org/opensearch/gradle/info/GlobalBuildInfoPlugin.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/info/GlobalBuildInfoPlugin.java#L187

Added line #L187 was not covered by tests
} else {
LOGGER.quiet(" Crypto Standard : any-supported");

Check warning on line 189 in buildSrc/src/main/java/org/opensearch/gradle/info/GlobalBuildInfoPlugin.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/info/GlobalBuildInfoPlugin.java#L189

Added line #L189 was not covered by tests
}
LOGGER.quiet("=======================================");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,13 @@ public class ForbiddenPatternsTask extends DefaultTask {
.exclude("**/*.ico")
.exclude("**/*.jar")
.exclude("**/*.zip")
.exclude("**/*.p12")
.exclude("**/*.jks")
.exclude("**/*.crt")
.exclude("**/*.der")
.exclude("**/*.pem")
.exclude("**/*.key")
.exclude("**/*.bcfks")
.exclude("**/*.keystore")
.exclude("**/*.png");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,15 +548,15 @@

logToProcessStdout("Creating opensearch keystore with password set to [" + keystorePassword + "]");
if (keystorePassword.length() > 0) {
runOpenSearchBinScriptWithInput(keystorePassword + "\n" + keystorePassword, "opensearch-keystore", "create", "-p");
runOpenSearchBinScriptWithInput(keystorePassword + "\n" + keystorePassword + "\n", "opensearch-keystore", "create", "-p");

Check warning on line 551 in buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java#L551

Added line #L551 was not covered by tests
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

historically opensearch-keystore tool didn't know how to correctly load BC-FJA module (and associated jars) nor able to create a keystore in bcfks format. Is this tool now able to require long enough password (14 characters) and create keystore in bcfks format? Note one may need to set JAVA_TOOL_OPTIONS" : "--module-path=/path/to/dir/with/bouncycastle-fips/jars/" or similarly use maven extract plugin to uber-jar all bcfips jars; and then extract them to a temp location.

Copy link
Author

@beanuwave beanuwave Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - this circumstance is a big issue with the current branch. KeyStoreWrapper states that internal keystore has built-in FIPS compliance starting from ES 6.3. The keystore uses the AES/GCM/NoPadding algorithm with the PBKDF2WithHmacSHA512 key derivation function, and the maximum key length is limited to 128 chars in UTF-8.

However, when the keystore is created using opensearch-cli, it does not run in FIPS mode with the BC libraries on the classpath. As a result KeyStoreWrapper falls back to the built-in Sun/Oracle security providers. This situation causes issues in the current integration tests because the $OPENSEARCH_CRYPTO_STANDARD environment variable does not propagate in this scenario.

To address this, as defined in opensearch-cli, the $OPENSEARCH_ADDITIONAL_CLASSPATH_DIRECTORIES variable can be used to include BC libraries.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note otherprojects have hit this too; it would be really nice if opensearch-cli could query the default keystore type; and really try hard to use it; and fail if it is incapable of doing it.

At chainguard we do set default keystore type to bcfks, but lots of tools fail to honor it; or have ability to specify additional --module-path or --classpath to their java invocations, and/or otherwise vendor-in (uber-jar'ed-in) the bcfips jars to ensure the bcfks keystore creation "just works" upon launch as "normal".

I do question the many places that currently uses the deprecated, even in Java8, keystore type of JKS. As imho all references to JKS should be eliminated towards requesting default keystore type which in most contexts today would be pkcs12, nss/pkcs11/nssdb-fips for the likes of rhel-ubi, and bcks / bcfks for the bcfips based jdks (like the ones from chainguard).

I guess I also should finally make a public docker image of a universal keytool that "just works" and has support for all keystore types, properly, out of the box. For efficient keystore creation / import / export / migration. To avoid need for individual projects of implementing that, yet again, themselves.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see where you're coming from and thank you for explaining the background.

The internal KeyStore is not intended to store certificate chains; instead, it acts like a key/value map with specific requirements that should operate w/o a FIPS context. I believe this is why it was designed in that manner. At least I don't see the request for possible migration to BCFKS standard as part of this PR.

Regarding Key- & TrustStores that are used for TLS communication, their type is usually predefined. If not, it is inferred from the file extension (see KeyStoreType). KeyStoreFactory prevents non-secure KeyStore types from being instantiated. For example, if the application is running in a FIPS context but the TrustStore type has a JKS extension, a runtime exception will be thrown.

} else {
runOpenSearchBinScript("opensearch-keystore", "-v", "create");
}

if (keystoreSettings.isEmpty() == false || keystoreFiles.isEmpty() == false) {
logToProcessStdout("Adding " + keystoreSettings.size() + " keystore settings and " + keystoreFiles.size() + " keystore files");

keystoreSettings.forEach((key, value) -> runKeystoreCommandWithPassword(keystorePassword, value.toString(), "add", "-x", key));
keystoreSettings.forEach((key, value) -> runKeystoreCommandWithPassword(keystorePassword, value.toString(), "add", key));

Check warning on line 559 in buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java#L559

Added line #L559 was not covered by tests

for (Map.Entry<String, File> entry : keystoreFiles.entrySet()) {
File file = entry.getValue();
Expand Down Expand Up @@ -738,7 +738,12 @@
}

private void runKeystoreCommandWithPassword(String keystorePassword, String input, CharSequence... args) {
final String actualInput = keystorePassword.length() > 0 ? keystorePassword + "\n" + input : input;
final String actualInput;
if (keystorePassword.length() > 0) {
actualInput = keystorePassword + "\n" + input + "\n" + input;

Check warning on line 743 in buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java#L743

Added line #L743 was not covered by tests
beanuwave marked this conversation as resolved.
Show resolved Hide resolved
} else {
actualInput = input + "\n" + input;

Check warning on line 745 in buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java

View check run for this annotation

Codecov / codecov/patch

buildSrc/src/main/java/org/opensearch/gradle/testclusters/OpenSearchNode.java#L745

Added line #L745 was not covered by tests
}
runOpenSearchBinScriptWithInput(actualInput, "opensearch-keystore", args);
}

Expand Down
Binary file removed buildSrc/src/main/resources/cacerts.bcfks
Binary file not shown.
29 changes: 0 additions & 29 deletions buildSrc/src/main/resources/fips_java_bcjsse_11.policy

This file was deleted.

34 changes: 0 additions & 34 deletions buildSrc/src/main/resources/fips_java_bcjsse_8.policy

This file was deleted.

Loading
Loading