Skip to content
Draft
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
36 changes: 25 additions & 11 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@


import com.diffplug.gradle.spotless.JavaExtension
import org.opensearch.gradle.info.FipsBuildParams
import org.opensearch.gradle.test.RestIntegTestTask
import groovy.json.JsonBuilder

Expand Down Expand Up @@ -77,6 +76,7 @@ apply plugin: 'opensearch.pluginzip'
apply plugin: 'opensearch.rest-test'
apply plugin: 'opensearch.testclusters'
apply from: 'gradle/formatting.gradle'
apply from: 'gradle/fips.gradle'

licenseFile = rootProject.file('LICENSE.txt')
noticeFile = rootProject.file('NOTICE.txt')
Expand Down Expand Up @@ -236,6 +236,7 @@ tasks.register("listTasksAsJSON") {
}
}


tasks.register('copyExtraTestResources', Copy) {
dependsOn testClasses

Expand Down Expand Up @@ -548,6 +549,9 @@ subprojects {
configurations.integrationTestImplementation.extendsFrom configurations.implementation
configurations.integrationTestRuntimeOnly.extendsFrom configurations.runtimeOnly
}
tasks.withType(Test).configureEach { t ->
rootProject.ext.configureFipsJvmArgs(t)
}
}
}
}
Expand Down Expand Up @@ -593,6 +597,7 @@ allprojects {
integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}"
integrationTestImplementation 'org.hamcrest:hamcrest:2.2'
integrationTestImplementation "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}"
integrationTestImplementation "org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}"
integrationTestImplementation "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}"
integrationTestImplementation "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}"
integrationTestImplementation('org.awaitility:awaitility:4.3.0') {
Expand Down Expand Up @@ -697,16 +702,10 @@ dependencies {
implementation libs.google.guava
implementation 'org.greenrobot:eventbus-java:3.3.1'
implementation 'commons-cli:commons-cli:1.11.0'
// When building with crypto.standard=FIPS-140-3 (set in gradle.properties), bcFips jars are provided by OpenSearch
if (FipsBuildParams.isInFipsMode()) {
compileOnly "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}"
compileOnly "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}"
compileOnly "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}"
} else {
implementation "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}"
implementation "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}"
implementation "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}"
}
compileOnly "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}"
compileOnly "org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}"
compileOnly "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}"
compileOnly "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}"
implementation 'org.ldaptive:ldaptive:1.2.3'
implementation 'com.nimbusds:nimbus-jose-jwt:10.9.1'
implementation 'com.rfksystems:blake2b:2.0.0'
Expand Down Expand Up @@ -812,6 +811,7 @@ dependencies {
exclude(group: 'org.hamcrest', module: 'hamcrest')
}
testImplementation "org.bouncycastle:bc-fips:${versions.bouncycastle_jce}"
testImplementation "org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}"
testImplementation "org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}"
testImplementation "org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}"
// JUnit build requirement
Expand Down Expand Up @@ -849,6 +849,18 @@ tasks.register('testsJar', Jar) {
from(sourceSets.test.output)
}

def configureSecurityAdminBcFips(AbstractArchiveTask task) {
def bcFipsJars = configurations.detachedConfiguration(
dependencies.create("org.bouncycastle:bc-fips:${versions.bouncycastle_jce}"),
dependencies.create("org.bouncycastle:bctls-fips:${versions.bouncycastle_tls}"),
dependencies.create("org.bouncycastle:bcpkix-fips:${versions.bouncycastle_pkix}"),
dependencies.create("org.bouncycastle:bcutil-fips:${versions.bouncycastle_util}")
)
task.from(bcFipsJars) {
into 'deps/'
}
}

tasks.register("bundleSecurityAdminStandalone", Zip) {
dependsOn(jar)
archiveClassifier = 'securityadmin-standalone'
Expand All @@ -865,6 +877,7 @@ tasks.register("bundleSecurityAdminStandalone", Zip) {
into 'deps/securityconfig'
}
}
configureSecurityAdminBcFips(bundleSecurityAdminStandalone)

tasks.register("bundleSecurityAdminStandaloneTarGz", Tar) {
dependsOn(jar)
Expand All @@ -884,6 +897,7 @@ tasks.register("bundleSecurityAdminStandaloneTarGz", Tar) {
into 'deps/securityconfig'
}
}
configureSecurityAdminBcFips(bundleSecurityAdminStandaloneTarGz)

buildRpm {
arch = 'NOARCH'
Expand Down
152 changes: 152 additions & 0 deletions gradle/fips.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* 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.
*/

/*
* Single place where "FIPS mode" is decided and applied to the build's test surface, following the
* schema used by the OpenSearch core repository (gradle/fips.gradle). It owns two things:
*
* 1. which test classes exist and run in each mode (the *FipsTests / *FipsIT split);
* 2. the JVM arguments the test workers need to actually be in FIPS mode.
*
* Keeping them together matters because they have to agree: selecting the FIPS classes without the
* matching JVM arguments produces a suite that looks FIPS-shaped but runs on the ordinary
* providers. Deciding the mode twice is what makes that drift possible, so it is decided once here.
* The BC FIPS artifacts themselves stay in build.gradle with every other dependency -- they are on
* the classpath in both modes, so they are not part of what "FIPS mode" switches.
*
* A FIPS variant is a class named '<Base>FipsTests' (unit tests) or '<Base>FipsIT' (integration
* tests) that extends its baseline, overrides whatever the FIPS providers change, and adds the
* assertions that only hold under FIPS. Baseline classes therefore stay free of
* 'FipsMode.isEnabled()' branches.
*
* The two modes select disjoint sets of classes:
*
* non-FIPS the variants are excluded from compilation and from every Test task,
* so only the baseline classes run.
* FIPS the variants are compiled and run, and each baseline class that has a
* variant is excluded, so the variant replaces it rather than adding to it.
*
* FIPS mode is driven by the OPENSEARCH_FIPS_MODE environment variable.
*/

ext.fipsTestsEnabled = 'true'.equalsIgnoreCase(System.getenv('OPENSEARCH_FIPS_MODE'))

// Source file patterns identifying a FIPS variant.
ext.fipsSourcePatterns = ['**/*FipsTests.java', '**/*FipsIT.java']

// Compiled counterparts of the patterns above.
def fipsClassSuffixes = ['FipsTests.class', 'FipsIT.class']

// Suffixes a baseline class may carry in this repository. Test classes here are named
// inconsistently ('...Test', '...Tests', '...IT', '...IntegrationTest', and names such as
// 'LdapBackendTestNewStyleConfig' that end in none of those), so a variant is matched against
// every combination rather than a single suffix rewrite. The trailing '.class' entry is the
// "no suffix" case: the variant is then simply the full baseline name plus the FIPS suffix.
def baselineClassSuffixes = ['IntegrationTest.class', 'Tests.class', 'Test.class', 'IT.class', '.class']

/**
* Keeps FIPS variants out of the given source set unless FIPS mode is on. They reference
* BC FIPS types and assert FIPS-only behaviour, so compiling them outside FIPS mode is
* pointless and would drag BC FIPS onto the non-FIPS compile classpath.
*/
ext.excludeFipsSources = { sourceSet ->
if (!project.ext.fipsTestsEnabled) {
project.ext.fipsSourcePatterns.each { pattern -> sourceSet.java.exclude pattern }
}
}

/**
* Puts a test worker into (or deliberately out of) FIPS mode. Shared with the subprojects block in
* build.gradle so every test JVM in the build agrees on the security providers it is running with.
*/
ext.configureFipsJvmArgs = { Test task ->
if (project.rootProject.ext.fipsTestsEnabled) {
def fipsSecurityFile = project.rootProject.file('src/test/resources/fips_java_test.security')
// BCFKS truststore is sourced from the core project: buildSrc/src/main/resources/opensearch-fips-truststore.bcfks
// It can also be regenerated by running distribution/src/bin/opensearch-fips-demo-installer
def fipsTrustStore = project.rootProject.file('src/test/resources/fips-jvm-truststore.bcfks')
// Silence BC FIPS JSSE per-handshake INFO chatter on tasks using the default JUL LogManager.
// Tasks that bridge JUL to log4j (integrationTest) ignore this and are muted via their
// log4j2-test.properties instead.
def fipsJulLoggingFile = project.rootProject.file('src/test/resources/fips-jul-test-logging.properties')
task.jvmArgs += "-Djava.security.properties==${fipsSecurityFile}"
task.jvmArgs += "-Djava.util.logging.config.file=${fipsJulLoggingFile}"
task.jvmArgs += "-Dorg.bouncycastle.fips.approved_only=true"
task.jvmArgs += "-Djavax.net.ssl.trustStore=${fipsTrustStore}"
task.jvmArgs += "-Djavax.net.ssl.trustStoreProvider=BCFIPS"
task.jvmArgs += "-Djavax.net.ssl.trustStoreType=BCFKS"
task.jvmArgs += "-Djavax.net.ssl.trustStorePassword=changeit"
} else {
def nonFipsSecurityFile = project.rootProject.file('src/test/resources/java_test.security')
task.jvmArgs += "-Djava.security.properties==${nonFipsSecurityFile}"
}
}

/**
* Restricts a Test task to the class set matching the current mode. Applied to every Test task so
* that 'test', 'citest', the split CI tasks and 'integrationTest' all agree.
*/
ext.configureFipsTestScope = { Test task ->
if (!project.ext.fipsTestsEnabled) {
fipsClassSuffixes.each { suffix -> task.exclude "**/*${suffix}" }
return
}

// Report each replacement once per task rather than once per candidate class file.
def reported = [] as Set

task.exclude { fileTreeElement ->
def name = fileTreeElement.name
// Nested and synthetic classes carry a '$'; they are pulled in by their outer class.
if (!name.endsWith('.class') || name.contains('$')) {
return false
}
// A variant never excludes itself.
if (fipsClassSuffixes.any { name.endsWith(it) }) {
return false
}
def parent = fileTreeElement.file.parentFile
if (parent == null) {
return false
}
for (baselineSuffix in baselineClassSuffixes) {
if (!name.endsWith(baselineSuffix)) {
continue
}
def baseName = name.substring(0, name.length() - baselineSuffix.length())
for (fipsSuffix in fipsClassSuffixes) {
if (!new File(parent, baseName + fipsSuffix).exists()) {
continue
}
def replaced = name - '.class'
if (reported.add(replaced)) {
task.logger.lifecycle(
"FIPS mode: excluding ${replaced} in favour of ${baseName}${fipsSuffix - '.class'}"
)
}
return true
}
}
return false
}
}

// Lazy on purpose: this fires for 'test' straight away and for 'integrationTest' when build.gradle
// creates it further down, so the script can be applied next to the other 'apply from' lines
// instead of having to sit below the source-set declarations.
sourceSets.matching { it.name in ['test', 'integrationTest'] }.configureEach { sourceSet ->
project.ext.excludeFipsSources(sourceSet)
}

// 'test', 'citest', the split CI tasks and 'integrationTest' are all Test tasks, so a single hook
// keeps scope and providers consistent no matter which one CI invokes -- and a Test task added
// later cannot silently miss its FIPS setup.
tasks.withType(Test).configureEach { task ->
project.ext.configureFipsTestScope(task)
project.ext.configureFipsJvmArgs(task)
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,19 @@ public class UntrustedLdapServerCertificateTest {
@Rule
public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.ldap.backend.LDAPAuthenticationBackend");

/** Exception the JSSE provider logs for an untrusted server certificate; BCJSSE raises its own. */
protected String getUntrustedCertificateExceptionName() {
return "javax.net.ssl.SSLHandshakeException";
}

@Test
public void shouldNotAuthenticateUserWithLdap() {
try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) {
TestRestClient.HttpResponse response = client.getAuthInfo();

response.assertStatusCode(401);
}
logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException");
logsRule.assertThatStackTraceContain(getUntrustedCertificateExceptionName());
}

}
6 changes: 6 additions & 0 deletions src/integrationTest/resources/log4j2-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ logger.securenetty4transport.name = org.opensearch.transport.netty4.ssl.SecureNe
logger.securenetty4transport.level = error
logger.securenetty4transport.appenderRef.capturing.ref = logCapturingAppender

# BC FIPS JSSE logs every TLS handshake lifecycle event at INFO via JUL, which this task
# bridges to log4j. Raise it to warn so genuine handshake failures still surface while the
# per-handshake chatter is silenced.
logger.bcjsse.name = org.bouncycastle.jsse
logger.bcjsse.level = warn

logger.privileges.name=org.opensearch.security.privileges
logger.privileges.level=INFO
logger.privileges.appenderRef.capturing.ref = logCapturingAppender
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ private static LdapEntry existsSearchingAllBases(
);

if (isDebugEnabled) {
log.debug("Results for LDAP search for " + user + " in base " + entry.getKey() + ":\n" + result);
log.debug("Results for LDAP search for {} in base {}:\n{}", user, entry.getKey(), foundEntries);
}

if (foundEntries != null) {
Expand Down
Loading
Loading