Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
641976e
Update plugin-cli tool to use BC FIPS 2.x as a runtime dependency wit…
ebarlas Dec 2, 2025
639ee6e
Simplify ClassLoader isolation by reusing existing plugin-cli classpath
ebarlas Dec 2, 2025
d23d394
[CI] Auto commit changes from spotless
Dec 3, 2025
399e42f
Use getConstructor rather than getDeclaredConstructor
ebarlas Dec 3, 2025
925edfc
Remove plugin-cli thirdPartyAudit ignoreViolations
ebarlas Dec 3, 2025
5f5ce43
Remove ParentLastUrlClassLoader. Use URLClassLoader with platform cla…
ebarlas Dec 3, 2025
e7d740a
Merge branch 'main' into plugin-cli-bc-fips
ebarlas Dec 4, 2025
ee9759b
Move BcPgpSignatureVerifier to separate Gradle source set. Use BiCons…
ebarlas Dec 9, 2025
b6bd145
Merge branch 'main' into plugin-cli-bc-fips
ebarlas Dec 14, 2025
ab580ba
Update docs/changelog/138949.yaml
ebarlas Dec 14, 2025
f3d9af1
Disable plugin-cli tests in FIPS mode due to jar hell
ebarlas Dec 14, 2025
6ef297e
Move bc source set to separate bc sub-project
ebarlas Dec 16, 2025
4a446c6
Scan cli-libs directory rather than assuming URLClassLoader
ebarlas Dec 16, 2025
237ff2e
Re-enable plugin-cli and qa-evil-tests in FIPS mode
ebarlas Dec 16, 2025
3d0b6d5
Use PathUtils.get rather than Path.of
ebarlas Dec 17, 2025
17a6887
Add @SuppressForbidden for Paths.get
ebarlas Dec 17, 2025
0353e28
Shadow plugin-cli JAR to isolate Bouncy Castle classes
ebarlas Dec 17, 2025
f56e8b8
Use latest BC PGP 1.83 for plugin-cli rather than BC FIPS
ebarlas Dec 17, 2025
9ec0c74
Merge branch 'main' into plugin-cli-bc-fips
ebarlas Dec 17, 2025
7f955cc
Only relocate BC class, avoid relocating asm, ES-core, etc
ebarlas Dec 18, 2025
fb411a9
Move PGP signature verifying using Bouncy Castle to bc sub-project wi…
ebarlas Dec 18, 2025
4b6f47f
Update Javadoc comments in PgpSignatureVerifier
ebarlas Dec 18, 2025
b889516
Move PgpSignatureVerifier to bc sub-package
ebarlas Dec 18, 2025
aad8aae
Another comment fix
ebarlas Dec 18, 2025
b776f4c
Minor formatting change in distribution/build.gradle
ebarlas Dec 18, 2025
6399f0b
Merge branch 'main' into plugin-cli-bc-fips
ebarlas Dec 18, 2025
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
34 changes: 34 additions & 0 deletions distribution/tools/plugin-cli/bc/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

apply plugin: 'elasticsearch.build'
apply plugin: 'com.gradleup.shadow'

base {
archivesName = 'elasticsearch-plugin-cli-bc'
}

dependencies {
implementation "org.bouncycastle:bcpg-jdk18on:1.83"
implementation "org.bouncycastle:bcprov-jdk18on:1.83"
implementation "org.bouncycastle:bcutil-jdk18on:1.83"
}

tasks.named("dependencyLicenses").configure {
mapping from: /bc.*/, to: 'bouncycastle'
}

tasks.named("shadowJar").configure {
relocate 'org.bouncycastle', 'shadow.org.bouncycastle'
}

// Add Lucene to forbiddenApis classpath (needed to parse signature files that reference Lucene classes)
tasks.named("forbiddenApisMain").configure {
classpath += project(":server").sourceSets.main.runtimeClasspath
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.plugins.cli.bc;

import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;

import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;

/**
* A PGP signature verifier that uses Bouncy Castle implementation.
* <p>
* This implementation was lifted from <code>InstallPluginAction</code> to isolate Bouncy Castle usage.
* </p>
*/
public class PgpSignatureVerifier {

/**
* @param publicKeyId the public key ID of the signing key that is expected to have signed the official plugin.
* @param urlString the URL source of the downloaded plugin ZIP
* @param pluginZipInputStream an input stream to the raw bytes of the plugin ZIP
* @param ascInputStream an input stream to the signature corresponding to the downloaded plugin zip
* @param publicKeyInputStream an input stream to the public key of the signing key.
*/
public static void verifySignature(
String publicKeyId,
String urlString,
InputStream pluginZipInputStream,
InputStream ascInputStream,
InputStream publicKeyInputStream
) throws IOException {

try (
InputStream fin = pluginZipInputStream;
InputStream sin = ascInputStream;
InputStream ain = new ArmoredInputStream(publicKeyInputStream) // input stream to the public key in ASCII-Armor format (RFC4880)
) {
final JcaPGPObjectFactory factory = new JcaPGPObjectFactory(PGPUtil.getDecoderStream(sin));
final PGPSignature signature = ((PGPSignatureList) factory.nextObject()).get(0);

// validate the signature has key ID matching our public key ID
final String keyId = Long.toHexString(signature.getKeyID()).toUpperCase(Locale.ROOT);
if (publicKeyId.equals(keyId) == false) {
throw new IllegalStateException("key id [" + keyId + "] does not match expected key id [" + publicKeyId + "]");
}

// compute the signature of the downloaded plugin zip
computeSignatureForDownloadedPlugin(fin, ain, signature);

// finally we verify the signature of the downloaded plugin zip matches the expected signature
if (signature.verify() == false) {
throw new IllegalStateException("signature verification for [" + urlString + "] failed");
}
} catch (PGPException e) {
throw new IOException("PGP exception during signature verification for [" + urlString + "]", e);
}
}

private static void computeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException,
IOException {
final PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(ain, new JcaKeyFingerprintCalculator());
final PGPPublicKey key = collection.getPublicKey(signature.getKeyID());
signature.init(new JcaPGPContentVerifierBuilderProvider(), key);
final byte[] buffer = new byte[1024];
int read;
while ((read = fin.read(buffer)) != -1) {
signature.update(buffer, 0, read);
}
}

}
42 changes: 10 additions & 32 deletions distribution/tools/plugin-cli/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,18 @@ dependencies {
implementation project(":libs:plugin-api")
implementation project(":libs:plugin-scanner")
implementation project(":libs:entitlement")
implementation project(path: "bc", configuration: 'shadow')
// TODO: asm is picked up from the plugin scanner and entitlements, we should consolidate so it is not defined twice
implementation 'org.ow2.asm:asm:9.9'
implementation 'org.ow2.asm:asm-tree:9.9'

api "org.bouncycastle:bcpg-fips:1.0.7.1"
api "org.bouncycastle:bc-fips:1.0.2.6"
testImplementation project(":test:framework")
testImplementation "com.google.jimfs:jimfs:${versions.jimfs}"
testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}"
}

tasks.named("dependencyLicenses").configure {
mapping from: /bc.*/, to: 'bouncycastle'
testImplementation "org.bouncycastle:bcpg-jdk18on:1.83"
testImplementation "org.bouncycastle:bcprov-jdk18on:1.83"
testImplementation "org.bouncycastle:bcutil-jdk18on:1.83"
}

tasks.named("test").configure {
Expand All @@ -51,31 +50,10 @@ tasks.named("test").configure {
}
}

/*
* these two classes intentionally use the following JDK internal APIs in order to offer the necessary
* functionality
*
* sun.security.internal.spec.TlsKeyMaterialParameterSpec
* sun.security.internal.spec.TlsKeyMaterialSpec
* sun.security.internal.spec.TlsMasterSecretParameterSpec
* sun.security.internal.spec.TlsPrfParameterSpec
* sun.security.internal.spec.TlsRsaPremasterSecretParameterSpec
* sun.security.provider.SecureRandom
*
*/
tasks.named("thirdPartyAudit").configure {
ignoreViolations(
'org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider$CoreSecureRandom',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$BaseTLSKeyGeneratorSpi',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSKeyMaterialGenerator',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSKeyMaterialGenerator$2',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSMasterSecretGenerator',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSMasterSecretGenerator$2',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSPRFKeyGenerator',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSRsaPreMasterSecretGenerator',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSRsaPreMasterSecretGenerator$2',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSExtendedMasterSecretGenerator',
'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSExtendedMasterSecretGenerator$2'
)
if (buildParams.inFipsJvm) {
// Disable tests in FIPS mode due to JAR hell between plugin-cli's
// BC dependencies and the BC FIPS dependencies added by the FIPS gradle config
// We support running plugin-cli with BC FIPS JARs in ES lib via shadowing.
// Running these tests with the JVM in FIPS mode isn't related.
tasks.named("test").configure { enabled = false }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,6 @@
import org.apache.lucene.search.spell.LevenshteinDistance;
import org.apache.lucene.util.CollectionUtil;
import org.apache.lucene.util.Constants;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.elasticsearch.Build;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
Expand All @@ -42,6 +31,7 @@
import org.elasticsearch.plugins.Platforms;
import org.elasticsearch.plugins.PluginDescriptor;
import org.elasticsearch.plugins.PluginsUtils;
import org.elasticsearch.plugins.cli.bc.PgpSignatureVerifier;
import org.objectweb.asm.ClassReader;

import java.io.BufferedReader;
Expand Down Expand Up @@ -568,12 +558,11 @@ private InputStream urlOpenStream(final URL url) throws IOException {
* @param officialPlugin true if the plugin is an official plugin
* @return the path to the downloaded plugin ZIP
* @throws IOException if an I/O exception occurs download or reading files and resources
* @throws PGPException if an exception occurs verifying the downloaded ZIP signature
* @throws UserException if checksum validation fails
* @throws URISyntaxException is the url is invalid
*/
private Path downloadAndValidate(final String urlString, final Path tmpDir, final boolean officialPlugin) throws IOException,
PGPException, UserException, URISyntaxException {
UserException, URISyntaxException {
Path zip = downloadZip(urlString, tmpDir);
pathsToDeleteOnShutdown.add(zip);
String checksumUrlString = urlString + ".sha512";
Expand Down Expand Up @@ -667,41 +656,10 @@ private Path downloadAndValidate(final String urlString, final Path tmpDir, fina
*
* @param zip the path to the downloaded plugin ZIP
* @param urlString the URL source of the downloaded plugin ZIP
* @throws IOException if an I/O exception occurs reading from various input streams
* @throws PGPException if the PGP implementation throws an internal exception during verification
* @throws IOException if an I/O exception occurs reading from various input streams or
* if the PGP implementation throws an internal exception during verification
*/
void verifySignature(final Path zip, final String urlString) throws IOException, PGPException {
final String ascUrlString = urlString + ".asc";
final URL ascUrl = openUrl(ascUrlString);
try (
// fin is a file stream over the downloaded plugin zip whose signature to verify
InputStream fin = pluginZipInputStream(zip);
// sin is a URL stream to the signature corresponding to the downloaded plugin zip
InputStream sin = urlOpenStream(ascUrl);
// ain is a input stream to the public key in ASCII-Armor format (RFC4880)
InputStream ain = new ArmoredInputStream(getPublicKey())
) {
final JcaPGPObjectFactory factory = new JcaPGPObjectFactory(PGPUtil.getDecoderStream(sin));
final PGPSignature signature = ((PGPSignatureList) factory.nextObject()).get(0);

// validate the signature has key ID matching our public key ID
final String keyId = Long.toHexString(signature.getKeyID()).toUpperCase(Locale.ROOT);
if (getPublicKeyId().equals(keyId) == false) {
throw new IllegalStateException("key id [" + keyId + "] does not match expected key id [" + getPublicKeyId() + "]");
}

// compute the signature of the downloaded plugin zip, wrapped with long execution warning
timedComputeSignatureForDownloadedPlugin(fin, ain, signature);

// finally we verify the signature of the downloaded plugin zip matches the expected signature
if (signature.verify() == false) {
throw new IllegalStateException("signature verification for [" + urlString + "] failed");
}
}
}

private void timedComputeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException,
IOException {
void verifySignature(final Path zip, final String urlString) throws IOException {
final Timer timer = new Timer();

try {
Expand All @@ -712,22 +670,16 @@ public void run() {
}
}, acceptableSignatureVerificationDelay());

computeSignatureForDownloadedPlugin(fin, ain, signature);
doVerifySignature(zip, urlString);
} finally {
timer.cancel();
}
}

// package private for testing
void computeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, IOException {
final PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(ain, new JcaKeyFingerprintCalculator());
final PGPPublicKey key = collection.getPublicKey(signature.getKeyID());
signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider(new BouncyCastleFipsProvider()), key);
final byte[] buffer = new byte[1024];
int read;
while ((read = fin.read(buffer)) != -1) {
signature.update(buffer, 0, read);
}
void doVerifySignature(final Path zip, final String urlString) throws IOException {
final String ascUrlString = urlString + ".asc";
final URL ascUrl = openUrl(ascUrlString);
PgpSignatureVerifier.verifySignature(getPublicKeyId(), urlString, pluginZipInputStream(zip), urlOpenStream(ascUrl), getPublicKey());
}

// package private for testing
Expand Down
Loading