Skip to content

Commit 20a5611

Browse files
iigoninbennygoerzigKarstenSchnitterKai Sternad
committed
Add build-tooling to run in FIPS environment
Signed-off-by: Igonin <[email protected]> Co-authored-by: Benny Goerzig <[email protected]> Co-authored-by: Karsten Schnitter <[email protected]> Co-authored-by: Kai Sternad <[email protected]>
1 parent a9b6d7a commit 20a5611

File tree

10 files changed

+421
-7
lines changed

10 files changed

+421
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
4545
- Added approximation support for range queries with now in date field ([#18511](https://github.com/opensearch-project/OpenSearch/pull/18511))
4646
- Upgrade to protobufs 0.6.0 and clean up deprecated TermQueryProtoUtils code ([#18880](https://github.com/opensearch-project/OpenSearch/pull/18880))
4747
- Prevent shard initialization failure due to streaming consumer errors ([#18877](https://github.com/opensearch-project/OpenSearch/pull/18877))
48+
- Add build-tooling to run in the FIPS environment
4849

4950
### Changed
5051
- Update Subject interface to use CheckedRunnable ([#18570](https://github.com/opensearch-project/OpenSearch/issues/18570))

build.gradle

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,11 @@ gradle.projectsEvaluated {
437437
jvmArgs += ["-javaagent:" + project(':libs:agent-sm:agent').jar.archiveFile.get()]
438438
}
439439
if (BuildParams.inFipsJvm) {
440-
task.jvmArgs += ["-Dorg.bouncycastle.fips.approved_only=true"]
440+
def fipsSecurityFile = project.rootProject.file('distribution/src/config/fips_java.security')
441+
task.jvmArgs += [
442+
"-Dorg.bouncycastle.fips.approved_only=true",
443+
"-Djava.security.properties=${fipsSecurityFile}"
444+
]
441445
}
442446
}
443447
}

buildSrc/src/main/groovy/org/opensearch/gradle/test/StandaloneRestTestPlugin.groovy

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
package org.opensearch.gradle.test
3232

3333
import groovy.transform.CompileStatic
34+
import org.gradle.api.artifacts.VersionCatalog
35+
import org.gradle.api.artifacts.VersionCatalogsExtension
3436
import org.opensearch.gradle.OpenSearchJavaPlugin
3537
import org.opensearch.gradle.ExportOpenSearchBuildResourcesTask
3638
import org.opensearch.gradle.RepositoriesSetupPlugin
@@ -92,6 +94,10 @@ class StandaloneRestTestPlugin implements Plugin<Project> {
9294
// create a compileOnly configuration as others might expect it
9395
project.configurations.create("compileOnly")
9496
project.dependencies.add('testImplementation', project.project(':test:framework'))
97+
if (BuildParams.inFipsJvm) {
98+
VersionCatalog libs = project.extensions.getByType(VersionCatalogsExtension).named("libs")
99+
project.dependencies.add('testImplementation', libs.findBundle("bouncycastle").get())
100+
}
95101

96102
EclipseModel eclipse = project.extensions.getByType(EclipseModel)
97103
eclipse.classpath.sourceSets = [testSourceSet]

buildSrc/src/main/java/org/opensearch/gradle/test/rest/RestTestUtil.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ static void setupDependencies(Project project, SourceSet sourceSet) {
102102
);
103103
}
104104

105+
if (BuildParams.isInFipsJvm()) {
106+
project.getDependencies()
107+
.add(
108+
sourceSet.getImplementationConfigurationName(),
109+
"org.bouncycastle:bc-fips:" + VersionProperties.getVersions().get("bouncycastle_jce")
110+
);
111+
project.getDependencies()
112+
.add(
113+
sourceSet.getImplementationConfigurationName(),
114+
"org.bouncycastle:bctls-fips:" + VersionProperties.getVersions().get("bouncycastle_tls")
115+
);
116+
}
105117
}
106118

107119
}

server/src/main/java/org/opensearch/bootstrap/Bootstrap.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ private void setup(boolean addShutdownHook, Environment environment) throws Boot
200200
if ("FIPS-140-3".equals(cryptoStandard) || "true".equalsIgnoreCase(System.getProperty("org.bouncycastle.fips.approved_only"))) {
201201
LogManager.getLogger(Bootstrap.class).info("running in FIPS-140-3 mode");
202202
SecurityProviderManager.removeNonCompliantFipsProviders();
203+
MultiProviderTrustStoreHandler.configureTrustStore(environment.tmpDir(), Path.of(System.getProperty("java.home")));
203204
}
204205

205206
// initialize probes before the security manager is installed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.bootstrap;
10+
11+
import org.opensearch.common.SuppressForbidden;
12+
13+
import java.io.IOException;
14+
import java.io.PrintWriter;
15+
import java.io.StringWriter;
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.security.GeneralSecurityException;
19+
import java.security.KeyStore;
20+
import java.security.KeyStoreException;
21+
import java.security.NoSuchAlgorithmException;
22+
import java.security.Permissions;
23+
import java.security.Provider;
24+
import java.security.Security;
25+
import java.security.cert.Certificate;
26+
import java.security.cert.CertificateException;
27+
import java.util.Arrays;
28+
import java.util.Enumeration;
29+
import java.util.List;
30+
import java.util.Locale;
31+
import java.util.Objects;
32+
import java.util.Optional;
33+
import java.util.concurrent.atomic.AtomicInteger;
34+
import java.util.logging.Level;
35+
import java.util.logging.Logger;
36+
import java.util.stream.Collectors;
37+
38+
/**
39+
* This class is responsible for handling and configuring trust stores from multiple providers.
40+
* It facilitates the interception of trust store configurations, conversion of default
41+
* system trust stores into desired formats, and provides utilities to manage the trust store
42+
* configuration in runtime environments.
43+
* The class supports handling PKCS#11 and BCFKS trust store types. If a custom trust store is
44+
* not specified, it intercepts the default "cacerts" trust store and converts it to an alternative
45+
* format if applicable. It uses security providers to dynamically determine and configure appropriate
46+
* KeyStore services.
47+
*/
48+
public class MultiProviderTrustStoreHandler {
49+
50+
private static final Logger logger = Logger.getLogger(MultiProviderTrustStoreHandler.class.getName());
51+
private static final String TEMP_TRUSTSTORE_PREFIX = "converted-truststore-";
52+
private static final String TEMP_TRUSTSTORE_SUFFIX = ".bcfks";
53+
private static final String TRUST_STORE_PASSWORD = Security.getProperty("javax.net.ssl.trustStorePassword");
54+
private static final String JVM_DEFAULT_PASSWORD = Objects.requireNonNullElse(TRUST_STORE_PASSWORD, "changeit");
55+
private static final String SYSTEMSTORE_PASSWORD = Objects.requireNonNullElse(TRUST_STORE_PASSWORD, "");
56+
private static final String PKCS_11 = "PKCS11";
57+
private static final String BCFKS = "BCFKS";
58+
private static final String BCFIPS = "BCFIPS";
59+
private static final List<String> KNOWN_JDK_TRUSTSTORE_TYPES = List.of("PKCS12", "JKS");
60+
61+
/**
62+
* Encapsulates properties related to a TrustStore configuration. Is primarily used to manage and
63+
* log details of TrustStore configurations within the system.
64+
*/
65+
private record TrustStoreProperties(String trustStorePath, String trustStoreType, String trustStorePassword,
66+
String trustStoreProvider) {
67+
void logout() {
68+
var detailLog = new StringWriter();
69+
var writer = new PrintWriter(detailLog);
70+
var passwordSetStatus = trustStorePassword.isEmpty() ? "[NOT SET]" : "[SET]";
71+
72+
writer.printf(Locale.ROOT, "\njavax.net.ssl.trustStore: " + trustStorePath);
73+
writer.printf(Locale.ROOT, "\njavax.net.ssl.trustStoreType: " + trustStoreType);
74+
writer.printf(Locale.ROOT, "\njavax.net.ssl.trustStoreProvider: " + trustStoreProvider);
75+
writer.printf(Locale.ROOT, "\njavax.net.ssl.trustStorePassword: " + passwordSetStatus);
76+
writer.flush();
77+
78+
logger.info("\nChanged TrustStore configuration:" + detailLog);
79+
}
80+
}
81+
82+
/**
83+
* Configures the Java TrustStore by setting up appropriate truststore properties.
84+
* If a custom TrustStore path is provided, it will use it directly;
85+
* otherwise, it will dynamically configure a TrustStore using a suitable provider.
86+
*
87+
* @param tmpDir the directory to store temporary TrustStore files
88+
* @param javaHome the path to the Java Home, used for accessing default system TrustStore
89+
*/
90+
public static void configureTrustStore(Path tmpDir, Path javaHome) {
91+
var existingTrustStorePath = existingTrustStorePath();
92+
if (existingTrustStorePath != null) {
93+
logger.info("Custom truststore already specified: " + existingTrustStorePath);
94+
return;
95+
}
96+
97+
logger.info("No custom truststore specified - intercepting default cacerts usage");
98+
var pkcs11ProviderService = findPKCS11ProviderService();
99+
var properties = pkcs11ProviderService.map(MultiProviderTrustStoreHandler::configurePKCS11TrustStore)
100+
.orElseGet(() -> MultiProviderTrustStoreHandler.configureBCFKSTrustStore(javaHome, tmpDir));
101+
102+
setProperties(properties);
103+
logger.info("Converted system truststore to %s format".formatted(properties.trustStoreProvider));
104+
properties.logout();
105+
printCurrentConfiguration(Level.FINEST);
106+
}
107+
108+
@SuppressForbidden(reason = "check system properties for TrustStore configuration")
109+
private static Path existingTrustStorePath() {
110+
var property = Optional.ofNullable(System.getProperty("javax.net.ssl.trustStore"));
111+
if (property.isPresent() && Path.of(property.get()).toFile().exists()) {
112+
return Path.of(property.get());
113+
}
114+
return null;
115+
}
116+
117+
@SuppressForbidden(reason = "sets system properties for TrustStore configuration")
118+
protected static void setProperties(TrustStoreProperties properties) {
119+
System.setProperty("javax.net.ssl.trustStore", properties.trustStorePath());
120+
System.setProperty("javax.net.ssl.trustStorePassword", properties.trustStorePassword());
121+
System.setProperty("javax.net.ssl.trustStoreType", properties.trustStoreType());
122+
System.setProperty("javax.net.ssl.trustStoreProvider", properties.trustStoreProvider());
123+
}
124+
125+
private static TrustStoreProperties configurePKCS11TrustStore(Provider.Service pkcs11ProviderService) {
126+
logger.info("Configuring PKCS11 truststore...");
127+
return new TrustStoreProperties("NONE", PKCS_11, SYSTEMSTORE_PASSWORD, pkcs11ProviderService.getProvider().getName());
128+
}
129+
130+
private static TrustStoreProperties configureBCFKSTrustStore(Path javaHome, Path tmpDir) {
131+
logger.info("No PKCS11 provider found - converting system truststore to BCFKS...");
132+
var systemTrustStore = loadSystemDefaultTrustStore(javaHome);
133+
var bcfksTrustStore = convertToBCFKS(systemTrustStore, tmpDir);
134+
return new TrustStoreProperties(bcfksTrustStore.toAbsolutePath().toString(), BCFKS, JVM_DEFAULT_PASSWORD, BCFIPS);
135+
}
136+
137+
public static Optional<Provider.Service> findPKCS11ProviderService() {
138+
return Arrays.stream(Security.getProviders())
139+
.filter(it -> it.getName().toUpperCase(Locale.ROOT).contains(PKCS_11))
140+
.map(it -> it.getService("KeyStore", PKCS_11))
141+
.filter(Objects::nonNull)
142+
.findFirst();
143+
}
144+
145+
private static KeyStore loadSystemDefaultTrustStore(Path javaHome) {
146+
try {
147+
FilePermissionUtils.addSingleFilePath(new Permissions(), javaHome, "read,readlink");
148+
} catch (IOException e) {
149+
throw new RuntimeException(e);
150+
}
151+
var cacertsPath = javaHome.resolve("lib").resolve("security").resolve("cacerts");
152+
if (!Files.exists(cacertsPath) && Files.isReadable(cacertsPath)) {
153+
throw new IllegalStateException("System cacerts not found at: " + cacertsPath);
154+
}
155+
156+
logger.info("Loading system truststore from: " + cacertsPath);
157+
158+
KeyStore systemKs = null;
159+
for (var type : KNOWN_JDK_TRUSTSTORE_TYPES) {
160+
try {
161+
systemKs = KeyStore.getInstance(type);
162+
FilePermissionUtils.addSingleFilePath(new Permissions(), javaHome, "read,readlink");
163+
try (var is = Files.newInputStream(cacertsPath)) {
164+
systemKs.load(is, JVM_DEFAULT_PASSWORD.toCharArray());
165+
}
166+
int certCount = systemKs.size();
167+
logger.info("Loaded " + certCount + " certificates from system truststore");
168+
logger.info("Successfully loaded cacerts as " + type + " format");
169+
break;
170+
} catch (Exception e) {
171+
logger.info("Failed to load cacerts as " + type + ": " + e.getMessage());
172+
// continue
173+
}
174+
}
175+
176+
if (systemKs == null) {
177+
throw new IllegalStateException(
178+
"Could not load system cacerts in any known format"
179+
+ KNOWN_JDK_TRUSTSTORE_TYPES.stream().collect(Collectors.joining(", ", "[", "]"))
180+
);
181+
}
182+
183+
return systemKs;
184+
}
185+
186+
/**
187+
* Creates a temporary BCFKS formatted trustStore
188+
*/
189+
private static Path convertToBCFKS(KeyStore sourceKeyStore, Path tmpDir) {
190+
Path tempBcfksFile;
191+
try {
192+
tempBcfksFile = Files.createTempFile(tmpDir, TEMP_TRUSTSTORE_PREFIX, TEMP_TRUSTSTORE_SUFFIX);
193+
} catch (IOException ex) {
194+
throw new IllegalStateException("Could not create temporary truststore file", ex);
195+
}
196+
197+
// Register for deletion on JVM exit
198+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
199+
try {
200+
Files.deleteIfExists(tempBcfksFile);
201+
} catch (IOException e) {
202+
logger.warning("Failed to delete temporary file: " + e.getMessage());
203+
}
204+
}));
205+
206+
logger.info("Converting to BCFKS format: " + tempBcfksFile.toAbsolutePath());
207+
208+
// Create new BCFKS keystore
209+
int copiedCount = 0;
210+
KeyStore bcfksKeyStore;
211+
try {
212+
bcfksKeyStore = KeyStore.getInstance(BCFKS, BCFIPS);
213+
bcfksKeyStore.load(null, null);
214+
215+
// Copy all certificates from source to BCFKS
216+
Enumeration<String> aliases = sourceKeyStore.aliases();
217+
218+
while (aliases.hasMoreElements()) {
219+
String alias = aliases.nextElement();
220+
221+
if (sourceKeyStore.isCertificateEntry(alias)) {
222+
Certificate cert = sourceKeyStore.getCertificate(alias);
223+
if (cert != null) {
224+
try {
225+
bcfksKeyStore.setCertificateEntry(alias, cert);
226+
copiedCount++;
227+
} catch (Exception e) {
228+
logger.warning("Failed to copy certificate '" + alias + "': " + e.getMessage());
229+
// Continue with other certificates
230+
}
231+
}
232+
}
233+
}
234+
} catch (GeneralSecurityException | IOException e) {
235+
throw new SecurityException(e);
236+
}
237+
238+
// Save BCFKS keystore to file using NIO.2
239+
try (var file = Files.newOutputStream(tempBcfksFile)) {
240+
bcfksKeyStore.store(file, JVM_DEFAULT_PASSWORD.toCharArray());
241+
logger.info("Successfully converted " + copiedCount + " certificates to BCFKS format");
242+
} catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException e) {
243+
throw new IllegalStateException("Failed to write BCFKS keystore", e);
244+
}
245+
246+
return tempBcfksFile;
247+
}
248+
249+
/**
250+
* Utility method to check the current configuration.
251+
*/
252+
public static void printCurrentConfiguration(Level logLevel) {
253+
if (logger.isLoggable(logLevel)) {
254+
var detailLog = new StringWriter();
255+
var writer = new PrintWriter(detailLog);
256+
var counter = new AtomicInteger();
257+
Arrays.stream(Security.getProviders())
258+
.peek(
259+
provider -> writer.printf(
260+
Locale.ROOT,
261+
" %d. %s (version %s)\n".formatted(counter.incrementAndGet(), provider.getName(), provider.getVersionStr())
262+
)
263+
)
264+
.flatMap(provider -> provider.getServices().stream().filter(service -> "KeyStore".equals(service.getType())))
265+
.forEach(service -> writer.printf(Locale.ROOT, "\t\tKeyStore.%s\n".formatted(service.getAlgorithm())));
266+
267+
writer.flush();
268+
logger.log(logLevel, "\nAvailable Security Providers:\n" + detailLog);
269+
}
270+
}
271+
}

0 commit comments

Comments
 (0)