From 641976ed98853d391448ec6f11b1bf78514942a8 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 2 Dec 2025 15:14:43 -0800 Subject: [PATCH 01/22] Update plugin-cli tool to use BC FIPS 2.x as a runtime dependency with class loader isolation. --- distribution/build.gradle | 6 +- distribution/tools/plugin-cli/build.gradle | 31 +++- .../plugins/cli/BcPgpSignatureVerifier.java | 164 ++++++++++++++++++ .../plugins/cli/InstallPluginAction.java | 135 ++------------ .../cli/IsolatedBcPgpSignatureVerifier.java | 72 ++++++++ .../plugins/cli/ParentLastUrlClassLoader.java | 55 ++++++ .../plugins/cli/PgpSignatureVerifier.java | 25 +++ .../plugins/cli/InstallPluginActionTests.java | 64 ++++--- .../cli/ParentLastUrlClassLoaderTests.java | 50 ++++++ gradle/verification-metadata.xml | 15 ++ 10 files changed, 463 insertions(+), 154 deletions(-) create mode 100644 distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java create mode 100644 distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java create mode 100644 distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java create mode 100644 distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java create mode 100644 distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java diff --git a/distribution/build.gradle b/distribution/build.gradle index e5a132f529733..15efe31337c76 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -295,7 +295,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { * Properties to expand when copying packaging files * *****************************************************************************/ configurations { - ['libs', 'libsVersionChecker', 'libsCliLauncher', 'libsServerCli', 'libsWindowsServiceCli', 'libsPluginCli', 'libsKeystoreCli', 'libsSecurityCli', 'libsGeoIpCli', 'libsAnsiConsole', 'libsNative', 'libsEntitlementAgent'].each { + ['libs', 'libsVersionChecker', 'libsCliLauncher', 'libsServerCli', 'libsWindowsServiceCli', 'libsPluginCli', 'libsPluginCliBcfips', 'libsKeystoreCli', 'libsSecurityCli', 'libsGeoIpCli', 'libsAnsiConsole', 'libsNative', 'libsEntitlementAgent'].each { create(it) { canBeConsumed = false canBeResolved = true @@ -338,6 +338,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { libsWindowsServiceCli project(':distribution:tools:windows-service-cli') libsAnsiConsole project(':distribution:tools:ansi-console') libsPluginCli project(':distribution:tools:plugin-cli') + libsPluginCliBcfips project(path: ':distribution:tools:plugin-cli', configuration: 'bcfips') libsKeystoreCli project(path: ':distribution:tools:keystore-cli') libsSecurityCli project(':x-pack:plugin:security:cli') libsGeoIpCli project(':distribution:tools:geoip-cli') @@ -372,6 +373,9 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { } into('tools/plugin-cli') { from(configurations.libsPluginCli) + into('bcfips') { + from(configurations.libsPluginCliBcfips) + } } into('tools/keystore-cli') { from(configurations.libsKeystoreCli) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 3026ee74e00d0..8b454b3edc872 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -19,6 +19,21 @@ tasks.named("dependencyLicenses").configure { mapping from: /asm-.*/, to: 'asm' } +// Bouncy Castle FIPS dependencies are loaded in an isolated classloader at runtime. +// They are bundled separately in lib/tools/plugin-cli/bcfips/ rather than the main classpath. +configurations { + bcfips { + canBeConsumed = true + canBeResolved = true + } +} + +def bcfipsDeps = [ + "org.bouncycastle:bcpg-fips:2.0.12", + "org.bouncycastle:bc-fips:2.0.1", + "org.bouncycastle:bcutil-fips:2.0.5" +] + dependencies { compileOnly project(":server") compileOnly project(":libs:cli") @@ -29,8 +44,12 @@ dependencies { 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" + // BC FIPS JARs: compileOnly for compilation, bcfips config for bundling in isolated subdirectory + for (dep in bcfipsDeps) { + compileOnly dep + bcfips dep + } + testImplementation project(":test:framework") testImplementation "com.google.jimfs:jimfs:${versions.jimfs}" testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}" @@ -40,6 +59,14 @@ tasks.named("dependencyLicenses").configure { mapping from: /bc.*/, to: 'bouncycastle' } +// BC FIPS is on compileOnly (excluded from default license check) but we still distribute it, +// so we need to explicitly add bcfips JARs to the license check +afterEvaluate { + tasks.named("dependencyLicenses").configure { + dependencies = dependencies + configurations.bcfips + } +} + tasks.named("test").configure { // TODO: find a way to add permissions for the tests in this module systemProperty 'tests.security.manager', 'false' diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java new file mode 100644 index 0000000000000..3e26ab7e456e2 --- /dev/null +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java @@ -0,0 +1,164 @@ +/* + * 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; + +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.cli.Terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; + +/** + * A PGP signature verifier that uses Bouncy Castle implementation. + * This implementation was lifted from {@link InstallPluginAction} to isolate Bouncy Castle usage. + */ +public class BcPgpSignatureVerifier implements PgpSignatureVerifier { + + private final Terminal terminal; + + public BcPgpSignatureVerifier(Terminal terminal) { + this.terminal = terminal; + } + + /** + * Verify the signature of the downloaded plugin ZIP. The signature is obtained from the source of the downloaded plugin by appending + * ".asc" to the URL. It is expected that the plugin is signed with the Elastic signing key with ID D27D666CD88E42B4. + * + * @param zip the path to the downloaded plugin ZIP + * @param urlString the URL source of the downloaded plugin ZIP + * @param ascInputStream the URL source of the PGP signature for the downloaded plugin ZIP + * @throws IOException if an I/O exception occurs reading from various input streams or if the PGP implementation throws an + * internal exception during verification + */ + @Override + public void verifySignature(final Path libDir, final Path zip, final String urlString, final InputStream ascInputStream) + throws IOException { + + 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 = ascInputStream; + // 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"); + } + } catch (PGPException e) { + throw new IOException("PGP exception during signature verification for [" + urlString + "]", e); + } + } + + private void timedComputeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, + IOException { + final Timer timer = new Timer(); + + try { + timer.schedule(new TimerTask() { + @Override + public void run() { + reportLongSignatureVerification(); + } + }, acceptableSignatureVerificationDelay()); + + computeSignatureForDownloadedPlugin(fin, ain, signature); + } 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); + } + } + + // package private for testing + void reportLongSignatureVerification() { + terminal.println( + "The plugin installer is trying to verify the signature of the downloaded plugin " + + "but this verification is taking longer than expected. This is often because the " + + "plugin installer is waiting for your system to supply it with random numbers. " + + ((System.getProperty("os.name").startsWith("Windows") == false) + ? "Ensure that your system has sufficient entropy so that reads from /dev/random do not block." + : "") + ); + } + + // package private for testing + long acceptableSignatureVerificationDelay() { + return 5_000; + } + + /** + * An input stream to the raw bytes of the plugin ZIP. + * + * @param zip the path to the downloaded plugin ZIP + * @return an input stream to the raw bytes of the plugin ZIP. + * @throws IOException if an I/O exception occurs preparing the input stream + */ + InputStream pluginZipInputStream(final Path zip) throws IOException { + return Files.newInputStream(zip); + } + + /** + * Return the public key ID of the signing key that is expected to have signed the official plugin. + * + * @return the public key ID + */ + String getPublicKeyId() { + return "D27D666CD88E42B4"; + } + + /** + * An input stream to the public key of the signing key. + * + * @return an input stream to the public key + */ + InputStream getPublicKey() { + return InstallPluginAction.class.getResourceAsStream("/public_key.asc"); + } + +} diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 0aaa19846f6d0..9b7f8ab5867b6 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -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; @@ -82,8 +71,6 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -568,12 +555,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"; @@ -661,119 +647,20 @@ private Path downloadAndValidate(final String urlString, final Path tmpDir, fina return zip; } - /** - * Verify the signature of the downloaded plugin ZIP. The signature is obtained from the source of the downloaded plugin by appending - * ".asc" to the URL. It is expected that the plugin is signed with the Elastic signing key with ID D27D666CD88E42B4. - * - * @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 - */ - 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"); - } + void verifySignature(final Path zip, final String urlString) throws IOException { + String ascUrl = urlString + ".asc"; + URL url = openUrl(ascUrl); + if (url == null) { + throw new IOException("Plugin signature missing: " + ascUrl); } - } - - private void timedComputeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, - IOException { - final Timer timer = new Timer(); - - try { - timer.schedule(new TimerTask() { - @Override - public void run() { - reportLongSignatureVerification(); - } - }, acceptableSignatureVerificationDelay()); - - computeSignatureForDownloadedPlugin(fin, ain, signature); - } 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); + try (InputStream ascInputStream = urlOpenStream(url)) { + Path libPath = env.libDir().resolve("tools").resolve("plugin-cli"); + pgpSignatureVerifier(terminal).verifySignature(libPath, zip, urlString, ascInputStream); } } - // package private for testing - void reportLongSignatureVerification() { - terminal.println( - "The plugin installer is trying to verify the signature of the downloaded plugin " - + "but this verification is taking longer than expected. This is often because the " - + "plugin installer is waiting for your system to supply it with random numbers. " - + ((System.getProperty("os.name").startsWith("Windows") == false) - ? "Ensure that your system has sufficient entropy so that reads from /dev/random do not block." - : "") - ); - } - - // package private for testing - long acceptableSignatureVerificationDelay() { - return 5_000; - } - - /** - * An input stream to the raw bytes of the plugin ZIP. - * - * @param zip the path to the downloaded plugin ZIP - * @return an input stream to the raw bytes of the plugin ZIP. - * @throws IOException if an I/O exception occurs preparing the input stream - */ - InputStream pluginZipInputStream(final Path zip) throws IOException { - return Files.newInputStream(zip); - } - - /** - * Return the public key ID of the signing key that is expected to have signed the official plugin. - * - * @return the public key ID - */ - String getPublicKeyId() { - return "D27D666CD88E42B4"; - } - - /** - * An input stream to the public key of the signing key. - * - * @return an input stream to the public key - */ - InputStream getPublicKey() { - return InstallPluginAction.class.getResourceAsStream("/public_key.asc"); + PgpSignatureVerifier pgpSignatureVerifier(Terminal terminal) { + return new IsolatedBcPgpSignatureVerifier(terminal); } /** diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java new file mode 100644 index 0000000000000..f3a206ea0a654 --- /dev/null +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java @@ -0,0 +1,72 @@ +/* + * 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; + +import org.elasticsearch.cli.Terminal; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +/** + * A PGP signature verifier that delegates to Bouncy Castle implementation loaded in an isolated classloader. + */ +public class IsolatedBcPgpSignatureVerifier implements PgpSignatureVerifier { + + private final Terminal terminal; + + public IsolatedBcPgpSignatureVerifier(Terminal terminal) { + this.terminal = terminal; + } + + @Override + public void verifySignature(Path libDir, Path zip, String urlString, InputStream ascInputStream) throws IOException { + try (ParentLastUrlClassLoader classLoader = classLoader(libDir)) { + Class clazz = Class.forName("org.elasticsearch.plugins.cli.BcPgpSignatureVerifier", true, classLoader); + Constructor constructor = clazz.getDeclaredConstructor(Terminal.class); + Method method = clazz.getMethod("verifySignature", Path.class, Path.class, String.class, InputStream.class); + method.invoke(constructor.newInstance(terminal), libDir, zip, urlString, ascInputStream); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + private static ParentLastUrlClassLoader classLoader(Path libDir) throws IOException { + return new ParentLastUrlClassLoader( + urls(libDir), + IsolatedBcPgpSignatureVerifier.class.getClassLoader(), + name -> name.startsWith("org.bouncycastle.") || name.startsWith("org.elasticsearch.plugins.cli.") + ); + } + + private static URL[] urls(Path libDir) throws IOException { + List urls = new ArrayList<>(); + addJarFileUrls(urls, libDir); // Other plugin-cli JARs for BcPgpSignatureVerifier + addJarFileUrls(urls, libDir.resolve("bcfips")); // Bouncy Castle FIPS jars + return urls.toArray(URL[]::new); + } + + private static void addJarFileUrls(List urls, Path dir) throws IOException { + try (Stream jarFiles = Files.list(dir)) { + List jars = jarFiles.filter(p -> p.toString().endsWith(".jar")).toList(); + for (Path jar : jars) { + urls.add(jar.toUri().toURL()); + } + } + } + +} diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java new file mode 100644 index 0000000000000..f476b7e964554 --- /dev/null +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java @@ -0,0 +1,55 @@ +/* + * 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; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.function.Predicate; + +/** + * A URLClassLoader that attempts to load classes from its URLs before delegating + * to its parent classloader, for classes that match a provided filter. + */ +class ParentLastUrlClassLoader extends URLClassLoader { + + private final Predicate filter; + + ParentLastUrlClassLoader(URL[] urls, ClassLoader parent, Predicate filter) { + super(urls, parent); + this.filter = filter; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + // The following implementation mirrors that of ClassLoader.loadClass, except that + // we attempt to load the class from this classloader first, and only delegate + // to the parent (1) if the filter does not match or (2) if the class is not found locally + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + if (filter.test(name)) { + try { + c = findClass(name); + } catch (ClassNotFoundException e) { + // ignore and try parent + } + } + if (c == null) { + c = super.loadClass(name, resolve); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + +} diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java new file mode 100644 index 0000000000000..ec4a218a4c6a8 --- /dev/null +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java @@ -0,0 +1,25 @@ +/* + * 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; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; + +interface PgpSignatureVerifier { + + /** + * Verify the signature of a zip file using the provided signature input stream. + * @see BcPgpSignatureVerifier + * @see IsolatedBcPgpSignatureVerifier + */ + void verifySignature(Path libDir, Path zip, String urlString, InputStream ascInputStream) throws IOException; + +} diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index ead08787269c3..e01d0831f1e35 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -488,10 +488,11 @@ public void testSlowSignatureVerificationMessage() throws Exception { ".sha512", checksumAndFilename(digest, url), newSecretKey(), - this::signature + this::signature, + true ); - final InstallPluginAction spied = spy(action); + final BcPgpSignatureVerifier spied = (BcPgpSignatureVerifier) action.pgpSignatureVerifier(terminal); // Control for timeout on waiting for signature verification to complete CountDownLatch countDownLatch = new CountDownLatch(1); @@ -511,7 +512,7 @@ public void testSlowSignatureVerificationMessage() throws Exception { return null; }).when(spied).computeSignatureForDownloadedPlugin(any(InputStream.class), any(InputStream.class), any(PGPSignature.class)); - installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), spied); + installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), action); assertThat(terminal.getOutput(), containsString("The plugin installer is trying to verify the signature ")); // Clean-up the exiting plugin, let's try to reinstall with 'fast' random numbers @@ -528,7 +529,7 @@ public void testSlowSignatureVerificationMessage() throws Exception { terminal.reset(); - installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), spied); + installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), action); assertThat(terminal.getOutput(), not(containsString("The plugin installer is trying to verify the signature "))); } @@ -1020,7 +1021,8 @@ void assertInstallPluginFromUrl( shaExtension, shaCalculator, secretKey, - signature + signature, + false ); installPlugin(new InstallablePlugin(pluginId, pluginUrl), env.v1(), action); assertPlugin(pluginId, pluginDir, env.v2()); @@ -1035,10 +1037,36 @@ private InstallPluginAction makeActionPluginThatDownloads( final String shaExtension, final Function shaCalculator, final PGPSecretKey secretKey, - final BiFunction signature + final BiFunction signature, + final boolean spyVerifier ) throws Exception { InstallablePlugin pluginZip = createPlugin(pluginId, pluginDir); Path pluginZipPath = Path.of(URI.create(pluginZip.getLocation())); + PgpSignatureVerifier psv = new BcPgpSignatureVerifier(terminal) { + @Override + InputStream pluginZipInputStream(Path zip) throws IOException { + return new ByteArrayInputStream(Files.readAllBytes(zip)); + } + + @Override + String getPublicKeyId() { + return Long.toHexString(secretKey.getKeyID()).toUpperCase(Locale.ROOT); + } + + @Override + InputStream getPublicKey() { + try { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final ArmoredOutputStream armored = new ArmoredOutputStream(output); + secretKey.getPublicKey().encode(armored); + armored.close(); + return new ByteArrayInputStream(output.toByteArray()); + } catch (final IOException e) { + throw new AssertionError(e); + } + } + }; + PgpSignatureVerifier verifier = spyVerifier ? spy(psv) : psv; InstallPluginAction action = new InstallPluginAction(terminal, env.v2(), false) { @Override Path downloadZip(String urlString, Path tmpDir) throws IOException { @@ -1068,7 +1096,7 @@ URL openUrl(String urlString) throws IOException { } @Override - void verifySignature(Path zip, String urlString) throws IOException, PGPException { + void verifySignature(Path zip, String urlString) throws IOException { if (InstallPluginAction.OFFICIAL_PLUGINS.contains(pluginId)) { super.verifySignature(zip, urlString); } else { @@ -1077,26 +1105,8 @@ void verifySignature(Path zip, String urlString) throws IOException, PGPExceptio } @Override - InputStream pluginZipInputStream(Path zip) throws IOException { - return new ByteArrayInputStream(Files.readAllBytes(zip)); - } - - @Override - String getPublicKeyId() { - return Long.toHexString(secretKey.getKeyID()).toUpperCase(Locale.ROOT); - } - - @Override - InputStream getPublicKey() { - try { - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - final ArmoredOutputStream armored = new ArmoredOutputStream(output); - secretKey.getPublicKey().encode(armored); - armored.close(); - return new ByteArrayInputStream(output.toByteArray()); - } catch (final IOException e) { - throw new AssertionError(e); - } + PgpSignatureVerifier pgpSignatureVerifier(Terminal terminal) { + return verifier; } @Override diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java new file mode 100644 index 0000000000000..801abf73f533f --- /dev/null +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java @@ -0,0 +1,50 @@ +/* + * 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; + +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.compiler.InMemoryJavaCompiler; +import org.elasticsearch.test.jar.JarUtils; + +import java.net.URL; +import java.nio.file.Path; +import java.util.Map; +import java.util.function.Predicate; + +public class ParentLastUrlClassLoaderTests extends ESTestCase { + + public void testLoadClass() throws Exception { + Path tmpDir = createTempDir(); + Path jar = tmpDir.resolve("lib.jar"); + String src = """ + package widget; + public class Widget { + public static int run() { + return 42; + } + } + """; + JarUtils.createJarWithEntries(jar, Map.of("widget/Widget.class", InMemoryJavaCompiler.compile("widget.Widget", src))); + URL[] urls = { jar.toUri().toURL() }; + Predicate filter = name -> name.startsWith("widget."); + try (var classLoader = new ParentLastUrlClassLoader(urls, getClass().getClassLoader(), filter)) { + for (int i = 0; i < 2; i++) { + boolean resolve = i == 0; + Class widgetClass = classLoader.loadClass("widget.Widget", resolve); + assertEquals(classLoader, widgetClass.getClassLoader()); + int result = (int) widgetClass.getMethod("run").invoke(null); + assertEquals(42, result); + Class strClass = classLoader.loadClass("java.lang.String", resolve); + assertNotEquals(classLoader, strClass.getClassLoader()); + } + } + } + +} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index fc4a1ebbe11bc..a83f3f9c5324c 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3638,6 +3638,11 @@ + + + + + @@ -3648,6 +3653,11 @@ + + + + + @@ -3693,6 +3703,11 @@ + + + + + From 639ee6e8090fe8a266b1895680e0a6c04076d98d Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 2 Dec 2025 15:53:44 -0800 Subject: [PATCH 02/22] Simplify ClassLoader isolation by reusing existing plugin-cli classpath --- distribution/build.gradle | 6 +--- distribution/tools/plugin-cli/build.gradle | 32 ++--------------- .../plugins/cli/BcPgpSignatureVerifier.java | 2 +- .../plugins/cli/InstallPluginAction.java | 3 +- .../cli/IsolatedBcPgpSignatureVerifier.java | 34 ++++++------------- .../plugins/cli/PgpSignatureVerifier.java | 2 +- 6 files changed, 18 insertions(+), 61 deletions(-) diff --git a/distribution/build.gradle b/distribution/build.gradle index 15efe31337c76..e5a132f529733 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -295,7 +295,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { * Properties to expand when copying packaging files * *****************************************************************************/ configurations { - ['libs', 'libsVersionChecker', 'libsCliLauncher', 'libsServerCli', 'libsWindowsServiceCli', 'libsPluginCli', 'libsPluginCliBcfips', 'libsKeystoreCli', 'libsSecurityCli', 'libsGeoIpCli', 'libsAnsiConsole', 'libsNative', 'libsEntitlementAgent'].each { + ['libs', 'libsVersionChecker', 'libsCliLauncher', 'libsServerCli', 'libsWindowsServiceCli', 'libsPluginCli', 'libsKeystoreCli', 'libsSecurityCli', 'libsGeoIpCli', 'libsAnsiConsole', 'libsNative', 'libsEntitlementAgent'].each { create(it) { canBeConsumed = false canBeResolved = true @@ -338,7 +338,6 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { libsWindowsServiceCli project(':distribution:tools:windows-service-cli') libsAnsiConsole project(':distribution:tools:ansi-console') libsPluginCli project(':distribution:tools:plugin-cli') - libsPluginCliBcfips project(path: ':distribution:tools:plugin-cli', configuration: 'bcfips') libsKeystoreCli project(path: ':distribution:tools:keystore-cli') libsSecurityCli project(':x-pack:plugin:security:cli') libsGeoIpCli project(':distribution:tools:geoip-cli') @@ -373,9 +372,6 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { } into('tools/plugin-cli') { from(configurations.libsPluginCli) - into('bcfips') { - from(configurations.libsPluginCliBcfips) - } } into('tools/keystore-cli') { from(configurations.libsKeystoreCli) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 8b454b3edc872..9acddfc710587 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -19,21 +19,6 @@ tasks.named("dependencyLicenses").configure { mapping from: /asm-.*/, to: 'asm' } -// Bouncy Castle FIPS dependencies are loaded in an isolated classloader at runtime. -// They are bundled separately in lib/tools/plugin-cli/bcfips/ rather than the main classpath. -configurations { - bcfips { - canBeConsumed = true - canBeResolved = true - } -} - -def bcfipsDeps = [ - "org.bouncycastle:bcpg-fips:2.0.12", - "org.bouncycastle:bc-fips:2.0.1", - "org.bouncycastle:bcutil-fips:2.0.5" -] - dependencies { compileOnly project(":server") compileOnly project(":libs:cli") @@ -44,12 +29,9 @@ dependencies { implementation 'org.ow2.asm:asm:9.9' implementation 'org.ow2.asm:asm-tree:9.9' - // BC FIPS JARs: compileOnly for compilation, bcfips config for bundling in isolated subdirectory - for (dep in bcfipsDeps) { - compileOnly dep - bcfips dep - } - + api "org.bouncycastle:bcpg-fips:2.0.12" + api "org.bouncycastle:bcutil-fips:2.0.5" + api "org.bouncycastle:bc-fips:2.0.1" testImplementation project(":test:framework") testImplementation "com.google.jimfs:jimfs:${versions.jimfs}" testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}" @@ -59,14 +41,6 @@ tasks.named("dependencyLicenses").configure { mapping from: /bc.*/, to: 'bouncycastle' } -// BC FIPS is on compileOnly (excluded from default license check) but we still distribute it, -// so we need to explicitly add bcfips JARs to the license check -afterEvaluate { - tasks.named("dependencyLicenses").configure { - dependencies = dependencies + configurations.bcfips - } -} - tasks.named("test").configure { // TODO: find a way to add permissions for the tests in this module systemProperty 'tests.security.manager', 'false' diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java index 3e26ab7e456e2..2f692096a0102 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java @@ -53,7 +53,7 @@ public BcPgpSignatureVerifier(Terminal terminal) { * internal exception during verification */ @Override - public void verifySignature(final Path libDir, final Path zip, final String urlString, final InputStream ascInputStream) + public void verifySignature(final Path zip, final String urlString, final InputStream ascInputStream) throws IOException { try ( diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 9b7f8ab5867b6..4a2424dacab83 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -654,8 +654,7 @@ void verifySignature(final Path zip, final String urlString) throws IOException throw new IOException("Plugin signature missing: " + ascUrl); } try (InputStream ascInputStream = urlOpenStream(url)) { - Path libPath = env.libDir().resolve("tools").resolve("plugin-cli"); - pgpSignatureVerifier(terminal).verifySignature(libPath, zip, urlString, ascInputStream); + pgpSignatureVerifier(terminal).verifySignature(zip, urlString, ascInputStream); } } diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java index f3a206ea0a654..3fdc7ff5acab9 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java @@ -16,11 +16,8 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URL; -import java.nio.file.Files; +import java.net.URLClassLoader; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Stream; /** * A PGP signature verifier that delegates to Bouncy Castle implementation loaded in an isolated classloader. @@ -34,39 +31,30 @@ public IsolatedBcPgpSignatureVerifier(Terminal terminal) { } @Override - public void verifySignature(Path libDir, Path zip, String urlString, InputStream ascInputStream) throws IOException { - try (ParentLastUrlClassLoader classLoader = classLoader(libDir)) { + public void verifySignature(Path zip, String urlString, InputStream ascInputStream) throws IOException { + try (ParentLastUrlClassLoader classLoader = classLoader()) { Class clazz = Class.forName("org.elasticsearch.plugins.cli.BcPgpSignatureVerifier", true, classLoader); Constructor constructor = clazz.getDeclaredConstructor(Terminal.class); - Method method = clazz.getMethod("verifySignature", Path.class, Path.class, String.class, InputStream.class); - method.invoke(constructor.newInstance(terminal), libDir, zip, urlString, ascInputStream); + Method method = clazz.getMethod("verifySignature", Path.class, String.class, InputStream.class); + method.invoke(constructor.newInstance(terminal), zip, urlString, ascInputStream); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } - private static ParentLastUrlClassLoader classLoader(Path libDir) throws IOException { + private static ParentLastUrlClassLoader classLoader() { return new ParentLastUrlClassLoader( - urls(libDir), + urls(), IsolatedBcPgpSignatureVerifier.class.getClassLoader(), name -> name.startsWith("org.bouncycastle.") || name.startsWith("org.elasticsearch.plugins.cli.") ); } - private static URL[] urls(Path libDir) throws IOException { - List urls = new ArrayList<>(); - addJarFileUrls(urls, libDir); // Other plugin-cli JARs for BcPgpSignatureVerifier - addJarFileUrls(urls, libDir.resolve("bcfips")); // Bouncy Castle FIPS jars - return urls.toArray(URL[]::new); - } - - private static void addJarFileUrls(List urls, Path dir) throws IOException { - try (Stream jarFiles = Files.list(dir)) { - List jars = jarFiles.filter(p -> p.toString().endsWith(".jar")).toList(); - for (Path jar : jars) { - urls.add(jar.toUri().toURL()); - } + private static URL[] urls() { + if (IsolatedBcPgpSignatureVerifier.class.getClassLoader() instanceof URLClassLoader ucl) { + return ucl.getURLs(); } + throw new IllegalStateException("URLClassLoader required"); } } diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java index ec4a218a4c6a8..5ca1dad75c641 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java @@ -20,6 +20,6 @@ interface PgpSignatureVerifier { * @see BcPgpSignatureVerifier * @see IsolatedBcPgpSignatureVerifier */ - void verifySignature(Path libDir, Path zip, String urlString, InputStream ascInputStream) throws IOException; + void verifySignature(Path zip, String urlString, InputStream ascInputStream) throws IOException; } From d23d394dde8123fa129c70dc714256e7c7ef49ba Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 3 Dec 2025 00:00:41 +0000 Subject: [PATCH 03/22] [CI] Auto commit changes from spotless --- .../org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java index 2f692096a0102..b82f8d4065dfa 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java @@ -53,8 +53,7 @@ public BcPgpSignatureVerifier(Terminal terminal) { * internal exception during verification */ @Override - public void verifySignature(final Path zip, final String urlString, final InputStream ascInputStream) - throws IOException { + public void verifySignature(final Path zip, final String urlString, final InputStream ascInputStream) throws IOException { try ( // fin is a file stream over the downloaded plugin zip whose signature to verify From 399e42f3fab85c794fd7ed7833266148e869de6c Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 2 Dec 2025 17:31:14 -0800 Subject: [PATCH 04/22] Use getConstructor rather than getDeclaredConstructor --- .../plugins/cli/IsolatedBcPgpSignatureVerifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java index 3fdc7ff5acab9..288806523f772 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java @@ -34,7 +34,7 @@ public IsolatedBcPgpSignatureVerifier(Terminal terminal) { public void verifySignature(Path zip, String urlString, InputStream ascInputStream) throws IOException { try (ParentLastUrlClassLoader classLoader = classLoader()) { Class clazz = Class.forName("org.elasticsearch.plugins.cli.BcPgpSignatureVerifier", true, classLoader); - Constructor constructor = clazz.getDeclaredConstructor(Terminal.class); + Constructor constructor = clazz.getConstructor(Terminal.class); Method method = clazz.getMethod("verifySignature", Path.class, String.class, InputStream.class); method.invoke(constructor.newInstance(terminal), zip, urlString, ascInputStream); } catch (ReflectiveOperationException e) { From 925edfcb261f808e6a9903d2e088c2417acfa0d9 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 2 Dec 2025 19:40:04 -0800 Subject: [PATCH 05/22] Remove plugin-cli thirdPartyAudit ignoreViolations --- distribution/tools/plugin-cli/build.gradle | 28 ---------------------- 1 file changed, 28 deletions(-) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 9acddfc710587..40959851e7c90 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -52,31 +52,3 @@ 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' - ) -} From 5f5ce43f835ba7bcd0f1c3f8f55c3eedb1fba51f Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Wed, 3 Dec 2025 10:32:26 -0800 Subject: [PATCH 06/22] Remove ParentLastUrlClassLoader. Use URLClassLoader with platform class loader parent to achieve isolation. --- .../plugins/cli/BcPgpSignatureVerifier.java | 8 +-- .../plugins/cli/InstallPluginAction.java | 2 +- .../cli/IsolatedBcPgpSignatureVerifier.java | 19 +++---- .../plugins/cli/ParentLastUrlClassLoader.java | 55 ------------------- .../plugins/cli/InstallPluginActionTests.java | 2 +- .../cli/ParentLastUrlClassLoaderTests.java | 50 ----------------- 6 files changed, 13 insertions(+), 123 deletions(-) delete mode 100644 distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java delete mode 100644 distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java index b82f8d4065dfa..b825886669830 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java @@ -20,7 +20,6 @@ import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; -import org.elasticsearch.cli.Terminal; import java.io.IOException; import java.io.InputStream; @@ -29,6 +28,7 @@ import java.util.Locale; import java.util.Timer; import java.util.TimerTask; +import java.util.function.Consumer; /** * A PGP signature verifier that uses Bouncy Castle implementation. @@ -36,9 +36,9 @@ */ public class BcPgpSignatureVerifier implements PgpSignatureVerifier { - private final Terminal terminal; + private final Consumer terminal; - public BcPgpSignatureVerifier(Terminal terminal) { + public BcPgpSignatureVerifier(Consumer terminal) { this.terminal = terminal; } @@ -116,7 +116,7 @@ void computeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSi // package private for testing void reportLongSignatureVerification() { - terminal.println( + terminal.accept( "The plugin installer is trying to verify the signature of the downloaded plugin " + "but this verification is taking longer than expected. This is often because the " + "plugin installer is waiting for your system to supply it with random numbers. " diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 4a2424dacab83..6a5e90a15b655 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -659,7 +659,7 @@ void verifySignature(final Path zip, final String urlString) throws IOException } PgpSignatureVerifier pgpSignatureVerifier(Terminal terminal) { - return new IsolatedBcPgpSignatureVerifier(terminal); + return new IsolatedBcPgpSignatureVerifier(terminal::println); } /** diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java index 288806523f772..5f4f3f596a620 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java @@ -9,8 +9,6 @@ package org.elasticsearch.plugins.cli; -import org.elasticsearch.cli.Terminal; - import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; @@ -18,23 +16,24 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; +import java.util.function.Consumer; /** * A PGP signature verifier that delegates to Bouncy Castle implementation loaded in an isolated classloader. */ public class IsolatedBcPgpSignatureVerifier implements PgpSignatureVerifier { - private final Terminal terminal; + private final Consumer terminal; - public IsolatedBcPgpSignatureVerifier(Terminal terminal) { + public IsolatedBcPgpSignatureVerifier(Consumer terminal) { this.terminal = terminal; } @Override public void verifySignature(Path zip, String urlString, InputStream ascInputStream) throws IOException { - try (ParentLastUrlClassLoader classLoader = classLoader()) { + try (URLClassLoader classLoader = classLoader()) { Class clazz = Class.forName("org.elasticsearch.plugins.cli.BcPgpSignatureVerifier", true, classLoader); - Constructor constructor = clazz.getConstructor(Terminal.class); + Constructor constructor = clazz.getConstructor(Consumer.class); Method method = clazz.getMethod("verifySignature", Path.class, String.class, InputStream.class); method.invoke(constructor.newInstance(terminal), zip, urlString, ascInputStream); } catch (ReflectiveOperationException e) { @@ -42,12 +41,8 @@ public void verifySignature(Path zip, String urlString, InputStream ascInputStre } } - private static ParentLastUrlClassLoader classLoader() { - return new ParentLastUrlClassLoader( - urls(), - IsolatedBcPgpSignatureVerifier.class.getClassLoader(), - name -> name.startsWith("org.bouncycastle.") || name.startsWith("org.elasticsearch.plugins.cli.") - ); + private static URLClassLoader classLoader() { + return new URLClassLoader(urls(), ClassLoader.getPlatformClassLoader()); } private static URL[] urls() { diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java deleted file mode 100644 index f476b7e964554..0000000000000 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoader.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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; - -import java.net.URL; -import java.net.URLClassLoader; -import java.util.function.Predicate; - -/** - * A URLClassLoader that attempts to load classes from its URLs before delegating - * to its parent classloader, for classes that match a provided filter. - */ -class ParentLastUrlClassLoader extends URLClassLoader { - - private final Predicate filter; - - ParentLastUrlClassLoader(URL[] urls, ClassLoader parent, Predicate filter) { - super(urls, parent); - this.filter = filter; - } - - @Override - protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - // The following implementation mirrors that of ClassLoader.loadClass, except that - // we attempt to load the class from this classloader first, and only delegate - // to the parent (1) if the filter does not match or (2) if the class is not found locally - synchronized (getClassLoadingLock(name)) { - Class c = findLoadedClass(name); - if (c == null) { - if (filter.test(name)) { - try { - c = findClass(name); - } catch (ClassNotFoundException e) { - // ignore and try parent - } - } - if (c == null) { - c = super.loadClass(name, resolve); - } - } - if (resolve) { - resolveClass(c); - } - return c; - } - } - -} diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index e01d0831f1e35..a7364e2ff5eb7 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -1042,7 +1042,7 @@ private InstallPluginAction makeActionPluginThatDownloads( ) throws Exception { InstallablePlugin pluginZip = createPlugin(pluginId, pluginDir); Path pluginZipPath = Path.of(URI.create(pluginZip.getLocation())); - PgpSignatureVerifier psv = new BcPgpSignatureVerifier(terminal) { + PgpSignatureVerifier psv = new BcPgpSignatureVerifier(terminal::println) { @Override InputStream pluginZipInputStream(Path zip) throws IOException { return new ByteArrayInputStream(Files.readAllBytes(zip)); diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java deleted file mode 100644 index 801abf73f533f..0000000000000 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/ParentLastUrlClassLoaderTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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; - -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.test.compiler.InMemoryJavaCompiler; -import org.elasticsearch.test.jar.JarUtils; - -import java.net.URL; -import java.nio.file.Path; -import java.util.Map; -import java.util.function.Predicate; - -public class ParentLastUrlClassLoaderTests extends ESTestCase { - - public void testLoadClass() throws Exception { - Path tmpDir = createTempDir(); - Path jar = tmpDir.resolve("lib.jar"); - String src = """ - package widget; - public class Widget { - public static int run() { - return 42; - } - } - """; - JarUtils.createJarWithEntries(jar, Map.of("widget/Widget.class", InMemoryJavaCompiler.compile("widget.Widget", src))); - URL[] urls = { jar.toUri().toURL() }; - Predicate filter = name -> name.startsWith("widget."); - try (var classLoader = new ParentLastUrlClassLoader(urls, getClass().getClassLoader(), filter)) { - for (int i = 0; i < 2; i++) { - boolean resolve = i == 0; - Class widgetClass = classLoader.loadClass("widget.Widget", resolve); - assertEquals(classLoader, widgetClass.getClassLoader()); - int result = (int) widgetClass.getMethod("run").invoke(null); - assertEquals(42, result); - Class strClass = classLoader.loadClass("java.lang.String", resolve); - assertNotEquals(classLoader, strClass.getClassLoader()); - } - } - } - -} From ee9759bb04fd10f64f47055e9749ac05877f3e54 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 9 Dec 2025 10:26:07 -0800 Subject: [PATCH 07/22] Move BcPgpSignatureVerifier to separate Gradle source set. Use BiConsumer interface rather than PgpSignatureVerifier. --- distribution/tools/plugin-cli/build.gradle | 26 +++++++++++++++-- .../plugins/cli/BcPgpSignatureVerifier.java | 25 +++++++++++------ ...java => BcPgpSignatureVerifierLoader.java} | 28 ++++++++++++------- .../plugins/cli/InstallPluginAction.java | 8 ++++-- .../plugins/cli/PgpSignatureVerifier.java | 25 ----------------- .../plugins/cli/InstallPluginActionTests.java | 12 ++++---- 6 files changed, 70 insertions(+), 54 deletions(-) rename distribution/tools/plugin-cli/src/{main => bc}/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java (88%) rename distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/{IsolatedBcPgpSignatureVerifier.java => BcPgpSignatureVerifierLoader.java} (59%) delete mode 100644 distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 40959851e7c90..3f837afc69c02 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -19,6 +19,12 @@ tasks.named("dependencyLicenses").configure { mapping from: /asm-.*/, to: 'asm' } +// Create 'bc' source set for Bouncy Castle code (automatically uses src/bc/java). +// Main code cannot directly depend on bc classes at compile time. +sourceSets { + bc +} + dependencies { compileOnly project(":server") compileOnly project(":libs:cli") @@ -29,14 +35,28 @@ dependencies { implementation 'org.ow2.asm:asm:9.9' implementation 'org.ow2.asm:asm-tree:9.9' - api "org.bouncycastle:bcpg-fips:2.0.12" - api "org.bouncycastle:bcutil-fips:2.0.5" - api "org.bouncycastle:bc-fips:2.0.1" + bcImplementation "org.bouncycastle:bcpg-fips:2.0.12" + bcImplementation "org.bouncycastle:bcutil-fips:2.0.5" + bcImplementation "org.bouncycastle:bc-fips:2.0.1" + testImplementation project(":test:framework") testImplementation "com.google.jimfs:jimfs:${versions.jimfs}" + testImplementation sourceSets.bc.output // tests use BcPgpSignatureVerifier testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}" } +jar.from sourceSets.bc.output + +configurations { + runtimeOnly.extendsFrom bcImplementation // BC JARs needed at runtime + testImplementation.extendsFrom bcImplementation // tests use BC directly for signature generation +} + +// Add Lucene to forbiddenApis classpath for bc source set (needed to parse signatures) +tasks.named("forbiddenApisBc").configure { + classpath += sourceSets.main.compileClasspath +} + tasks.named("dependencyLicenses").configure { mapping from: /bc.*/, to: 'bouncycastle' } diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/bc/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java similarity index 88% rename from distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java rename to distribution/tools/plugin-cli/src/bc/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java index b825886669830..ea225b9645603 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/bc/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java @@ -28,17 +28,26 @@ import java.util.Locale; import java.util.Timer; import java.util.TimerTask; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** * A PGP signature verifier that uses Bouncy Castle implementation. - * This implementation was lifted from {@link InstallPluginAction} to isolate Bouncy Castle usage. + *

+ * This implementation was lifted from InstallPluginAction to isolate Bouncy Castle usage. + *

*/ -public class BcPgpSignatureVerifier implements PgpSignatureVerifier { +public class BcPgpSignatureVerifier implements BiConsumer { + private final String urlString; private final Consumer terminal; - public BcPgpSignatureVerifier(Consumer terminal) { + /** + * @param urlString the URL source of the downloaded plugin ZIP + * @param terminal a terminal consumer for reporting messages + */ + public BcPgpSignatureVerifier(String urlString, Consumer terminal) { + this.urlString = urlString; this.terminal = terminal; } @@ -47,13 +56,11 @@ public BcPgpSignatureVerifier(Consumer terminal) { * ".asc" to the URL. It is expected that the plugin is signed with the Elastic signing key with ID D27D666CD88E42B4. * * @param zip the path to the downloaded plugin ZIP - * @param urlString the URL source of the downloaded plugin ZIP * @param ascInputStream the URL source of the PGP signature for the downloaded plugin ZIP - * @throws IOException if an I/O exception occurs reading from various input streams or if the PGP implementation throws an * internal exception during verification */ @Override - public void verifySignature(final Path zip, final String urlString, final InputStream ascInputStream) throws IOException { + public void accept(final Path zip, final InputStream ascInputStream) { try ( // fin is a file stream over the downloaded plugin zip whose signature to verify @@ -80,7 +87,9 @@ public void verifySignature(final Path zip, final String urlString, final InputS throw new IllegalStateException("signature verification for [" + urlString + "] failed"); } } catch (PGPException e) { - throw new IOException("PGP exception during signature verification for [" + urlString + "]", e); + throw new RuntimeException("PGP exception during signature verification for [" + urlString + "]", e); + } catch (IOException e) { + throw new RuntimeException("I/O exception during signature verification for [" + urlString + "]", e); } } @@ -157,7 +166,7 @@ String getPublicKeyId() { * @return an input stream to the public key */ InputStream getPublicKey() { - return InstallPluginAction.class.getResourceAsStream("/public_key.asc"); + return getClass().getResourceAsStream("/public_key.asc"); } } diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java similarity index 59% rename from distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java rename to distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java index 5f4f3f596a620..aec3a415be590 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/IsolatedBcPgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java @@ -12,32 +12,40 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Path; +import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Supplier; /** * A PGP signature verifier that delegates to Bouncy Castle implementation loaded in an isolated classloader. */ -public class IsolatedBcPgpSignatureVerifier implements PgpSignatureVerifier { +class BcPgpSignatureVerifierLoader implements Supplier> { + private final String urlString; private final Consumer terminal; - public IsolatedBcPgpSignatureVerifier(Consumer terminal) { + BcPgpSignatureVerifierLoader(String urlString, Consumer terminal) { + this.urlString = urlString; this.terminal = terminal; } @Override - public void verifySignature(Path zip, String urlString, InputStream ascInputStream) throws IOException { + public BiConsumer get() { + return this::verifySignature; + } + + @SuppressWarnings("unchecked") + public void verifySignature(Path zip, InputStream ascInputStream) { try (URLClassLoader classLoader = classLoader()) { Class clazz = Class.forName("org.elasticsearch.plugins.cli.BcPgpSignatureVerifier", true, classLoader); - Constructor constructor = clazz.getConstructor(Consumer.class); - Method method = clazz.getMethod("verifySignature", Path.class, String.class, InputStream.class); - method.invoke(constructor.newInstance(terminal), zip, urlString, ascInputStream); - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); + Constructor constructor = clazz.getConstructor(String.class, Consumer.class); + BiConsumer bc = (BiConsumer) constructor.newInstance(urlString, terminal); + bc.accept(zip, ascInputStream); + } catch (ReflectiveOperationException | IOException e) { + throw new IllegalStateException(e); } } @@ -46,7 +54,7 @@ private static URLClassLoader classLoader() { } private static URL[] urls() { - if (IsolatedBcPgpSignatureVerifier.class.getClassLoader() instanceof URLClassLoader ucl) { + if (BcPgpSignatureVerifierLoader.class.getClassLoader() instanceof URLClassLoader ucl) { return ucl.getURLs(); } throw new IllegalStateException("URLClassLoader required"); diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 6a5e90a15b655..2e1c184c42f7d 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -71,6 +71,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -654,12 +656,12 @@ void verifySignature(final Path zip, final String urlString) throws IOException throw new IOException("Plugin signature missing: " + ascUrl); } try (InputStream ascInputStream = urlOpenStream(url)) { - pgpSignatureVerifier(terminal).verifySignature(zip, urlString, ascInputStream); + pgpSignatureVerifier(urlString, terminal).get().accept(zip, ascInputStream); } } - PgpSignatureVerifier pgpSignatureVerifier(Terminal terminal) { - return new IsolatedBcPgpSignatureVerifier(terminal::println); + Supplier> pgpSignatureVerifier(String urlString, Terminal terminal) { + return new BcPgpSignatureVerifierLoader(urlString, terminal::println); } /** diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java deleted file mode 100644 index 5ca1dad75c641..0000000000000 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Path; - -interface PgpSignatureVerifier { - - /** - * Verify the signature of a zip file using the provided signature input stream. - * @see BcPgpSignatureVerifier - * @see IsolatedBcPgpSignatureVerifier - */ - void verifySignature(Path zip, String urlString, InputStream ascInputStream) throws IOException; - -} diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index a7364e2ff5eb7..590afe97ac65d 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -98,8 +98,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -492,7 +494,7 @@ public void testSlowSignatureVerificationMessage() throws Exception { true ); - final BcPgpSignatureVerifier spied = (BcPgpSignatureVerifier) action.pgpSignatureVerifier(terminal); + final BcPgpSignatureVerifier spied = (BcPgpSignatureVerifier) action.pgpSignatureVerifier(url, terminal).get(); // Control for timeout on waiting for signature verification to complete CountDownLatch countDownLatch = new CountDownLatch(1); @@ -1042,7 +1044,7 @@ private InstallPluginAction makeActionPluginThatDownloads( ) throws Exception { InstallablePlugin pluginZip = createPlugin(pluginId, pluginDir); Path pluginZipPath = Path.of(URI.create(pluginZip.getLocation())); - PgpSignatureVerifier psv = new BcPgpSignatureVerifier(terminal::println) { + BcPgpSignatureVerifier psv = new BcPgpSignatureVerifier(url, terminal::println) { @Override InputStream pluginZipInputStream(Path zip) throws IOException { return new ByteArrayInputStream(Files.readAllBytes(zip)); @@ -1066,7 +1068,7 @@ InputStream getPublicKey() { } } }; - PgpSignatureVerifier verifier = spyVerifier ? spy(psv) : psv; + BcPgpSignatureVerifier verifier = spyVerifier ? spy(psv) : psv; InstallPluginAction action = new InstallPluginAction(terminal, env.v2(), false) { @Override Path downloadZip(String urlString, Path tmpDir) throws IOException { @@ -1105,8 +1107,8 @@ void verifySignature(Path zip, String urlString) throws IOException { } @Override - PgpSignatureVerifier pgpSignatureVerifier(Terminal terminal) { - return verifier; + Supplier> pgpSignatureVerifier(String urlString, Terminal terminal) { + return () -> verifier; } @Override From ab580bafe9a9ebc5e73536c9ae337033f0cad3d7 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Sun, 14 Dec 2025 11:37:24 -0800 Subject: [PATCH 08/22] Update docs/changelog/138949.yaml --- docs/changelog/138949.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changelog/138949.yaml diff --git a/docs/changelog/138949.yaml b/docs/changelog/138949.yaml new file mode 100644 index 0000000000000..3c47fcb7e6f62 --- /dev/null +++ b/docs/changelog/138949.yaml @@ -0,0 +1,6 @@ +pr: 138949 +summary: Update plugin-cli tool to use BC FIPS 2.x as a runtime dependency with class + loader isolation +area: Security +type: enhancement +issues: [] From f3d9af10d325ab08a93f279bcd60332e81a48c3a Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Sun, 14 Dec 2025 14:43:36 -0800 Subject: [PATCH 09/22] Disable plugin-cli tests in FIPS mode due to jar hell --- distribution/tools/plugin-cli/build.gradle | 7 +++++++ qa/evil-tests/build.gradle | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 3f837afc69c02..bfa40df16667a 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -72,3 +72,10 @@ tasks.named("test").configure { } } +if (buildParams.inFipsJvm) { + // Disable tests in FIPS mode due to jar hell between plugin-cli's + // BouncyCastle dependencies and the BC FIPS dependencies added by the FIPS gradle config + tasks.named("test").configure { + enabled = false + } +} diff --git a/qa/evil-tests/build.gradle b/qa/evil-tests/build.gradle index 242fb381e176d..3662ed2cfd223 100644 --- a/qa/evil-tests/build.gradle +++ b/qa/evil-tests/build.gradle @@ -27,3 +27,11 @@ dependencies { tasks.named("test").configure { systemProperty 'tests.security.manager', 'false' } + +if (buildParams.inFipsJvm) { + // Disable evil tests in FIPS mode due to jar hell between plugin-cli's + // BouncyCastle dependencies and the BC FIPS dependencies added by the FIPS gradle config + tasks.named("test").configure { + enabled = false + } +} From 6ef297ece567d2127b60162c38ce7a04fff53440 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Mon, 15 Dec 2025 21:04:14 -0800 Subject: [PATCH 10/22] Move bc source set to separate bc sub-project --- distribution/tools/plugin-cli/bc/build.gradle | 29 +++++++++++++++++++ .../licenses/bouncycastle-LICENSE.txt | 0 .../{ => bc}/licenses/bouncycastle-NOTICE.txt | 0 .../plugins/cli/BcPgpSignatureVerifier.java | 0 distribution/tools/plugin-cli/build.gradle | 29 ++----------------- settings.gradle | 1 + 6 files changed, 33 insertions(+), 26 deletions(-) create mode 100644 distribution/tools/plugin-cli/bc/build.gradle rename distribution/tools/plugin-cli/{ => bc}/licenses/bouncycastle-LICENSE.txt (100%) rename distribution/tools/plugin-cli/{ => bc}/licenses/bouncycastle-NOTICE.txt (100%) rename distribution/tools/plugin-cli/{src/bc => bc/src/main}/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java (100%) diff --git a/distribution/tools/plugin-cli/bc/build.gradle b/distribution/tools/plugin-cli/bc/build.gradle new file mode 100644 index 0000000000000..f7a7909e06f64 --- /dev/null +++ b/distribution/tools/plugin-cli/bc/build.gradle @@ -0,0 +1,29 @@ +/* + * 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' + +base { + archivesName = 'elasticsearch-plugin-cli-bc' +} + +dependencies { + api "org.bouncycastle:bcpg-fips:2.0.12" + api "org.bouncycastle:bcutil-fips:2.0.5" + api "org.bouncycastle:bc-fips:2.0.1" +} + +tasks.named("dependencyLicenses").configure { + mapping from: /bc.*/, to: '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 +} diff --git a/distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt b/distribution/tools/plugin-cli/bc/licenses/bouncycastle-LICENSE.txt similarity index 100% rename from distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt rename to distribution/tools/plugin-cli/bc/licenses/bouncycastle-LICENSE.txt diff --git a/distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt b/distribution/tools/plugin-cli/bc/licenses/bouncycastle-NOTICE.txt similarity index 100% rename from distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt rename to distribution/tools/plugin-cli/bc/licenses/bouncycastle-NOTICE.txt diff --git a/distribution/tools/plugin-cli/src/bc/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java similarity index 100% rename from distribution/tools/plugin-cli/src/bc/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java rename to distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index bfa40df16667a..22e33faf1ec3a 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -19,12 +19,6 @@ tasks.named("dependencyLicenses").configure { mapping from: /asm-.*/, to: 'asm' } -// Create 'bc' source set for Bouncy Castle code (automatically uses src/bc/java). -// Main code cannot directly depend on bc classes at compile time. -sourceSets { - bc -} - dependencies { compileOnly project(":server") compileOnly project(":libs:cli") @@ -35,32 +29,15 @@ dependencies { implementation 'org.ow2.asm:asm:9.9' implementation 'org.ow2.asm:asm-tree:9.9' - bcImplementation "org.bouncycastle:bcpg-fips:2.0.12" - bcImplementation "org.bouncycastle:bcutil-fips:2.0.5" - bcImplementation "org.bouncycastle:bc-fips:2.0.1" + // Bouncy Castle subproject for PGP signature verification + runtimeOnly project('bc') + testImplementation project('bc') testImplementation project(":test:framework") testImplementation "com.google.jimfs:jimfs:${versions.jimfs}" - testImplementation sourceSets.bc.output // tests use BcPgpSignatureVerifier testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}" } -jar.from sourceSets.bc.output - -configurations { - runtimeOnly.extendsFrom bcImplementation // BC JARs needed at runtime - testImplementation.extendsFrom bcImplementation // tests use BC directly for signature generation -} - -// Add Lucene to forbiddenApis classpath for bc source set (needed to parse signatures) -tasks.named("forbiddenApisBc").configure { - classpath += sourceSets.main.compileClasspath -} - -tasks.named("dependencyLicenses").configure { - mapping from: /bc.*/, to: 'bouncycastle' -} - tasks.named("test").configure { // TODO: find a way to add permissions for the tests in this module systemProperty 'tests.security.manager', 'false' diff --git a/settings.gradle b/settings.gradle index 82fbc566efb02..22f9b2a098784 100644 --- a/settings.gradle +++ b/settings.gradle @@ -91,6 +91,7 @@ List projects = [ 'distribution:tools:server-cli', 'distribution:tools:windows-service-cli', 'distribution:tools:plugin-cli', + 'distribution:tools:plugin-cli:bc', 'distribution:tools:keystore-cli', 'distribution:tools:geoip-cli', 'distribution:tools:ansi-console', From 4a446c6c04d86b08c51237f88ac70368d4560217 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 16 Dec 2025 15:11:56 -0800 Subject: [PATCH 11/22] Scan cli-libs directory rather than assuming URLClassLoader --- .../cli/BcPgpSignatureVerifierLoader.java | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java index aec3a415be590..9750c6b0b0755 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java @@ -14,10 +14,14 @@ import java.lang.reflect.Constructor; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Stream; /** * A PGP signature verifier that delegates to Bouncy Castle implementation loaded in an isolated classloader. @@ -49,15 +53,34 @@ public void verifySignature(Path zip, InputStream ascInputStream) { } } - private static URLClassLoader classLoader() { + private static URLClassLoader classLoader() throws IOException { return new URLClassLoader(urls(), ClassLoader.getPlatformClassLoader()); } - private static URL[] urls() { - if (BcPgpSignatureVerifierLoader.class.getClassLoader() instanceof URLClassLoader ucl) { - return ucl.getURLs(); + private static URL[] urls() throws IOException { + String esHome = System.getProperty("es.path.home"); + if (esHome == null) { + throw new IllegalStateException("es.path.home system property is required"); } - throw new IllegalStateException("URLClassLoader required"); + String cliLibs = System.getProperty("cli.libs"); + if (cliLibs == null || cliLibs.isBlank()) { + throw new IllegalStateException("cli.libs system property is required"); + } + return urls(esHome, cliLibs); + } + + private static URL[] urls(String esHome, String cliLibs) throws IOException { + Path homeDir = Path.of(esHome); + List urls = new ArrayList<>(); + for (String lib : cliLibs.split(",")) { + Path libDir = homeDir.resolve(lib); + try (Stream jarFiles = Files.list(libDir)) { + for (Path p : jarFiles.filter(p -> p.getFileName().toString().endsWith(".jar")).toList()) { + urls.add(p.toUri().toURL()); + } + } + } + return urls.toArray(URL[]::new); } } From 237ff2ef5a7dcee869e83dd7469aa2ccc017c73f Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 16 Dec 2025 15:58:49 -0800 Subject: [PATCH 12/22] Re-enable plugin-cli and qa-evil-tests in FIPS mode --- distribution/tools/plugin-cli/bc/build.gradle | 13 ++++++++++--- distribution/tools/plugin-cli/build.gradle | 7 ------- qa/evil-tests/build.gradle | 8 -------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/distribution/tools/plugin-cli/bc/build.gradle b/distribution/tools/plugin-cli/bc/build.gradle index f7a7909e06f64..2e29135391bf8 100644 --- a/distribution/tools/plugin-cli/bc/build.gradle +++ b/distribution/tools/plugin-cli/bc/build.gradle @@ -14,9 +14,16 @@ base { } dependencies { - api "org.bouncycastle:bcpg-fips:2.0.12" - api "org.bouncycastle:bcutil-fips:2.0.5" - api "org.bouncycastle:bc-fips:2.0.1" + if (buildParams.inFipsJvm) { + // Use BC FIPS 1.x series to match the FIPS test infrastructure + api "org.bouncycastle:bcpg-fips:1.0.7.1" + api "org.bouncycastle:bc-fips:1.0.2.6" + } else { + // Use BC FIPS 2.x series for non-FIPS testing + api "org.bouncycastle:bcpg-fips:2.0.12" + api "org.bouncycastle:bcutil-fips:2.0.5" + api "org.bouncycastle:bc-fips:2.0.1" + } } tasks.named("dependencyLicenses").configure { diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 22e33faf1ec3a..f72ca5b859a07 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -49,10 +49,3 @@ tasks.named("test").configure { } } -if (buildParams.inFipsJvm) { - // Disable tests in FIPS mode due to jar hell between plugin-cli's - // BouncyCastle dependencies and the BC FIPS dependencies added by the FIPS gradle config - tasks.named("test").configure { - enabled = false - } -} diff --git a/qa/evil-tests/build.gradle b/qa/evil-tests/build.gradle index 3662ed2cfd223..242fb381e176d 100644 --- a/qa/evil-tests/build.gradle +++ b/qa/evil-tests/build.gradle @@ -27,11 +27,3 @@ dependencies { tasks.named("test").configure { systemProperty 'tests.security.manager', 'false' } - -if (buildParams.inFipsJvm) { - // Disable evil tests in FIPS mode due to jar hell between plugin-cli's - // BouncyCastle dependencies and the BC FIPS dependencies added by the FIPS gradle config - tasks.named("test").configure { - enabled = false - } -} From 3d0b6d53ac36465347e5c5432ccf298f4cd9af30 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 16 Dec 2025 16:48:00 -0800 Subject: [PATCH 13/22] Use PathUtils.get rather than Path.of --- .../plugins/cli/BcPgpSignatureVerifierLoader.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java index 9750c6b0b0755..493f6e731ff39 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java @@ -9,6 +9,8 @@ package org.elasticsearch.plugins.cli; +import org.elasticsearch.core.PathUtils; + import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; @@ -70,7 +72,7 @@ private static URL[] urls() throws IOException { } private static URL[] urls(String esHome, String cliLibs) throws IOException { - Path homeDir = Path.of(esHome); + Path homeDir = PathUtils.get(esHome); List urls = new ArrayList<>(); for (String lib : cliLibs.split(",")) { Path libDir = homeDir.resolve(lib); From 17a688703f62fdaa8cbdee73121465e4c5155e18 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Tue, 16 Dec 2025 23:05:44 -0800 Subject: [PATCH 14/22] Add @SuppressForbidden for Paths.get --- .../plugins/cli/BcPgpSignatureVerifierLoader.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java index 493f6e731ff39..cb6c4e4d19cb7 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java @@ -9,7 +9,7 @@ package org.elasticsearch.plugins.cli; -import org.elasticsearch.core.PathUtils; +import org.elasticsearch.core.SuppressForbidden; import java.io.IOException; import java.io.InputStream; @@ -71,8 +71,9 @@ private static URL[] urls() throws IOException { return urls(esHome, cliLibs); } + @SuppressForbidden(reason = "Need to use Paths#get") private static URL[] urls(String esHome, String cliLibs) throws IOException { - Path homeDir = PathUtils.get(esHome); + Path homeDir = Path.of(esHome); List urls = new ArrayList<>(); for (String lib : cliLibs.split(",")) { Path libDir = homeDir.resolve(lib); From 0353e286cdc9d065e98cf12538e837b1178f6191 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Wed, 17 Dec 2025 13:02:31 -0800 Subject: [PATCH 15/22] Shadow plugin-cli JAR to isolate Bouncy Castle classes --- distribution/build.gradle | 2 +- distribution/tools/plugin-cli/bc/build.gradle | 36 ---- .../plugins/cli/BcPgpSignatureVerifier.java | 172 ------------------ distribution/tools/plugin-cli/build.gradle | 45 ++++- .../licenses/bouncycastle-LICENSE.txt | 0 .../{bc => }/licenses/bouncycastle-NOTICE.txt | 0 .../cli/BcPgpSignatureVerifierLoader.java | 89 --------- .../plugins/cli/InstallPluginAction.java | 135 ++++++++++++-- .../plugins/cli/InstallPluginActionTests.java | 70 +++---- docs/changelog/138949.yaml | 3 +- gradle/verification-metadata.xml | 15 -- settings.gradle | 1 - 12 files changed, 194 insertions(+), 374 deletions(-) delete mode 100644 distribution/tools/plugin-cli/bc/build.gradle delete mode 100644 distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java rename distribution/tools/plugin-cli/{bc => }/licenses/bouncycastle-LICENSE.txt (100%) rename distribution/tools/plugin-cli/{bc => }/licenses/bouncycastle-NOTICE.txt (100%) delete mode 100644 distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java diff --git a/distribution/build.gradle b/distribution/build.gradle index e5a132f529733..7fd50469342ba 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -337,7 +337,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { libsServerCli project(':distribution:tools:server-cli') libsWindowsServiceCli project(':distribution:tools:windows-service-cli') libsAnsiConsole project(':distribution:tools:ansi-console') - libsPluginCli project(':distribution:tools:plugin-cli') + libsPluginCli project(path: ':distribution:tools:plugin-cli', configuration: 'shadow') libsKeystoreCli project(path: ':distribution:tools:keystore-cli') libsSecurityCli project(':x-pack:plugin:security:cli') libsGeoIpCli project(':distribution:tools:geoip-cli') diff --git a/distribution/tools/plugin-cli/bc/build.gradle b/distribution/tools/plugin-cli/bc/build.gradle deleted file mode 100644 index 2e29135391bf8..0000000000000 --- a/distribution/tools/plugin-cli/bc/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -/* - * 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' - -base { - archivesName = 'elasticsearch-plugin-cli-bc' -} - -dependencies { - if (buildParams.inFipsJvm) { - // Use BC FIPS 1.x series to match the FIPS test infrastructure - api "org.bouncycastle:bcpg-fips:1.0.7.1" - api "org.bouncycastle:bc-fips:1.0.2.6" - } else { - // Use BC FIPS 2.x series for non-FIPS testing - api "org.bouncycastle:bcpg-fips:2.0.12" - api "org.bouncycastle:bcutil-fips:2.0.5" - api "org.bouncycastle:bc-fips:2.0.1" - } -} - -tasks.named("dependencyLicenses").configure { - mapping from: /bc.*/, to: '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 -} diff --git a/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java deleted file mode 100644 index ea225b9645603..0000000000000 --- a/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifier.java +++ /dev/null @@ -1,172 +0,0 @@ -/* - * 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; - -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 java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Locale; -import java.util.Timer; -import java.util.TimerTask; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -/** - * A PGP signature verifier that uses Bouncy Castle implementation. - *

- * This implementation was lifted from InstallPluginAction to isolate Bouncy Castle usage. - *

- */ -public class BcPgpSignatureVerifier implements BiConsumer { - - private final String urlString; - private final Consumer terminal; - - /** - * @param urlString the URL source of the downloaded plugin ZIP - * @param terminal a terminal consumer for reporting messages - */ - public BcPgpSignatureVerifier(String urlString, Consumer terminal) { - this.urlString = urlString; - this.terminal = terminal; - } - - /** - * Verify the signature of the downloaded plugin ZIP. The signature is obtained from the source of the downloaded plugin by appending - * ".asc" to the URL. It is expected that the plugin is signed with the Elastic signing key with ID D27D666CD88E42B4. - * - * @param zip the path to the downloaded plugin ZIP - * @param ascInputStream the URL source of the PGP signature for the downloaded plugin ZIP - * internal exception during verification - */ - @Override - public void accept(final Path zip, final InputStream ascInputStream) { - - 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 = ascInputStream; - // 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"); - } - } catch (PGPException e) { - throw new RuntimeException("PGP exception during signature verification for [" + urlString + "]", e); - } catch (IOException e) { - throw new RuntimeException("I/O exception during signature verification for [" + urlString + "]", e); - } - } - - private void timedComputeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, - IOException { - final Timer timer = new Timer(); - - try { - timer.schedule(new TimerTask() { - @Override - public void run() { - reportLongSignatureVerification(); - } - }, acceptableSignatureVerificationDelay()); - - computeSignatureForDownloadedPlugin(fin, ain, signature); - } 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); - } - } - - // package private for testing - void reportLongSignatureVerification() { - terminal.accept( - "The plugin installer is trying to verify the signature of the downloaded plugin " - + "but this verification is taking longer than expected. This is often because the " - + "plugin installer is waiting for your system to supply it with random numbers. " - + ((System.getProperty("os.name").startsWith("Windows") == false) - ? "Ensure that your system has sufficient entropy so that reads from /dev/random do not block." - : "") - ); - } - - // package private for testing - long acceptableSignatureVerificationDelay() { - return 5_000; - } - - /** - * An input stream to the raw bytes of the plugin ZIP. - * - * @param zip the path to the downloaded plugin ZIP - * @return an input stream to the raw bytes of the plugin ZIP. - * @throws IOException if an I/O exception occurs preparing the input stream - */ - InputStream pluginZipInputStream(final Path zip) throws IOException { - return Files.newInputStream(zip); - } - - /** - * Return the public key ID of the signing key that is expected to have signed the official plugin. - * - * @return the public key ID - */ - String getPublicKeyId() { - return "D27D666CD88E42B4"; - } - - /** - * An input stream to the public key of the signing key. - * - * @return an input stream to the public key - */ - InputStream getPublicKey() { - return getClass().getResourceAsStream("/public_key.asc"); - } - -} diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index f72ca5b859a07..9144b5fb1bb77 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -10,6 +10,7 @@ import org.elasticsearch.gradle.OS apply plugin: 'elasticsearch.build' +apply plugin: 'com.gradleup.shadow' base { archivesName = 'elasticsearch-plugin-cli' @@ -29,16 +30,24 @@ dependencies { implementation 'org.ow2.asm:asm:9.9' implementation 'org.ow2.asm:asm-tree:9.9' - // Bouncy Castle subproject for PGP signature verification - runtimeOnly project('bc') - testImplementation project('bc') - + 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' +} + +tasks.named("shadowJar").configure { + relocate 'org.bouncycastle', 'shadow.org.bouncycastle' +} + tasks.named("test").configure { + // Use original classpath, not the shadow JAR, since test code imports non-relocated packages + classpath = sourceSets.test.runtimeClasspath // TODO: find a way to add permissions for the tests in this module systemProperty 'tests.security.manager', 'false' // These tests are "heavy" on the secure number generator. On Linux, the NativePRNG defaults to /dev/random for the seeds, and @@ -49,3 +58,31 @@ 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' + ) +} diff --git a/distribution/tools/plugin-cli/bc/licenses/bouncycastle-LICENSE.txt b/distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt similarity index 100% rename from distribution/tools/plugin-cli/bc/licenses/bouncycastle-LICENSE.txt rename to distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt diff --git a/distribution/tools/plugin-cli/bc/licenses/bouncycastle-NOTICE.txt b/distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt similarity index 100% rename from distribution/tools/plugin-cli/bc/licenses/bouncycastle-NOTICE.txt rename to distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java deleted file mode 100644 index cb6c4e4d19cb7..0000000000000 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/BcPgpSignatureVerifierLoader.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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; - -import org.elasticsearch.core.SuppressForbidden; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; - -/** - * A PGP signature verifier that delegates to Bouncy Castle implementation loaded in an isolated classloader. - */ -class BcPgpSignatureVerifierLoader implements Supplier> { - - private final String urlString; - private final Consumer terminal; - - BcPgpSignatureVerifierLoader(String urlString, Consumer terminal) { - this.urlString = urlString; - this.terminal = terminal; - } - - @Override - public BiConsumer get() { - return this::verifySignature; - } - - @SuppressWarnings("unchecked") - public void verifySignature(Path zip, InputStream ascInputStream) { - try (URLClassLoader classLoader = classLoader()) { - Class clazz = Class.forName("org.elasticsearch.plugins.cli.BcPgpSignatureVerifier", true, classLoader); - Constructor constructor = clazz.getConstructor(String.class, Consumer.class); - BiConsumer bc = (BiConsumer) constructor.newInstance(urlString, terminal); - bc.accept(zip, ascInputStream); - } catch (ReflectiveOperationException | IOException e) { - throw new IllegalStateException(e); - } - } - - private static URLClassLoader classLoader() throws IOException { - return new URLClassLoader(urls(), ClassLoader.getPlatformClassLoader()); - } - - private static URL[] urls() throws IOException { - String esHome = System.getProperty("es.path.home"); - if (esHome == null) { - throw new IllegalStateException("es.path.home system property is required"); - } - String cliLibs = System.getProperty("cli.libs"); - if (cliLibs == null || cliLibs.isBlank()) { - throw new IllegalStateException("cli.libs system property is required"); - } - return urls(esHome, cliLibs); - } - - @SuppressForbidden(reason = "Need to use Paths#get") - private static URL[] urls(String esHome, String cliLibs) throws IOException { - Path homeDir = Path.of(esHome); - List urls = new ArrayList<>(); - for (String lib : cliLibs.split(",")) { - Path libDir = homeDir.resolve(lib); - try (Stream jarFiles = Files.list(libDir)) { - for (Path p : jarFiles.filter(p -> p.getFileName().toString().endsWith(".jar")).toList()) { - urls.add(p.toUri().toURL()); - } - } - } - return urls.toArray(URL[]::new); - } - -} diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 2e1c184c42f7d..c2966da88db3d 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -12,6 +12,16 @@ 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.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; @@ -71,8 +81,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Supplier; +import java.util.Timer; +import java.util.TimerTask; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -557,11 +567,12 @@ 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, - UserException, URISyntaxException { + PGPException, UserException, URISyntaxException { Path zip = downloadZip(urlString, tmpDir); pathsToDeleteOnShutdown.add(zip); String checksumUrlString = urlString + ".sha512"; @@ -649,19 +660,119 @@ private Path downloadAndValidate(final String urlString, final Path tmpDir, fina return zip; } - void verifySignature(final Path zip, final String urlString) throws IOException { - String ascUrl = urlString + ".asc"; - URL url = openUrl(ascUrl); - if (url == null) { - throw new IOException("Plugin signature missing: " + ascUrl); + /** + * Verify the signature of the downloaded plugin ZIP. The signature is obtained from the source of the downloaded plugin by appending + * ".asc" to the URL. It is expected that the plugin is signed with the Elastic signing key with ID D27D666CD88E42B4. + * + * @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 + */ + 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"); + } } - try (InputStream ascInputStream = urlOpenStream(url)) { - pgpSignatureVerifier(urlString, terminal).get().accept(zip, ascInputStream); + } + + private void timedComputeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, + IOException { + final Timer timer = new Timer(); + + try { + timer.schedule(new TimerTask() { + @Override + public void run() { + reportLongSignatureVerification(); + } + }, acceptableSignatureVerificationDelay()); + + computeSignatureForDownloadedPlugin(fin, ain, signature); + } 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(), key); + final byte[] buffer = new byte[1024]; + int read; + while ((read = fin.read(buffer)) != -1) { + signature.update(buffer, 0, read); } } - Supplier> pgpSignatureVerifier(String urlString, Terminal terminal) { - return new BcPgpSignatureVerifierLoader(urlString, terminal::println); + // package private for testing + void reportLongSignatureVerification() { + terminal.println( + "The plugin installer is trying to verify the signature of the downloaded plugin " + + "but this verification is taking longer than expected. This is often because the " + + "plugin installer is waiting for your system to supply it with random numbers. " + + ((System.getProperty("os.name").startsWith("Windows") == false) + ? "Ensure that your system has sufficient entropy so that reads from /dev/random do not block." + : "") + ); + } + + // package private for testing + long acceptableSignatureVerificationDelay() { + return 5_000; + } + + /** + * An input stream to the raw bytes of the plugin ZIP. + * + * @param zip the path to the downloaded plugin ZIP + * @return an input stream to the raw bytes of the plugin ZIP. + * @throws IOException if an I/O exception occurs preparing the input stream + */ + InputStream pluginZipInputStream(final Path zip) throws IOException { + return Files.newInputStream(zip); + } + + /** + * Return the public key ID of the signing key that is expected to have signed the official plugin. + * + * @return the public key ID + */ + String getPublicKeyId() { + return "D27D666CD88E42B4"; + } + + /** + * An input stream to the public key of the signing key. + * + * @return an input stream to the public key + */ + InputStream getPublicKey() { + return InstallPluginAction.class.getResourceAsStream("/public_key.asc"); } /** diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index 590afe97ac65d..bbd5f4f2eb911 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -17,7 +17,6 @@ import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.HashAlgorithmTags; -import org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; @@ -98,10 +97,8 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; -import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipEntry; @@ -490,11 +487,10 @@ public void testSlowSignatureVerificationMessage() throws Exception { ".sha512", checksumAndFilename(digest, url), newSecretKey(), - this::signature, - true + this::signature ); - final BcPgpSignatureVerifier spied = (BcPgpSignatureVerifier) action.pgpSignatureVerifier(url, terminal).get(); + final InstallPluginAction spied = spy(action); // Control for timeout on waiting for signature verification to complete CountDownLatch countDownLatch = new CountDownLatch(1); @@ -514,7 +510,7 @@ public void testSlowSignatureVerificationMessage() throws Exception { return null; }).when(spied).computeSignatureForDownloadedPlugin(any(InputStream.class), any(InputStream.class), any(PGPSignature.class)); - installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), action); + installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), spied); assertThat(terminal.getOutput(), containsString("The plugin installer is trying to verify the signature ")); // Clean-up the exiting plugin, let's try to reinstall with 'fast' random numbers @@ -531,7 +527,7 @@ public void testSlowSignatureVerificationMessage() throws Exception { terminal.reset(); - installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), action); + installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), spied); assertThat(terminal.getOutput(), not(containsString("The plugin installer is trying to verify the signature "))); } @@ -1023,8 +1019,7 @@ void assertInstallPluginFromUrl( shaExtension, shaCalculator, secretKey, - signature, - false + signature ); installPlugin(new InstallablePlugin(pluginId, pluginUrl), env.v1(), action); assertPlugin(pluginId, pluginDir, env.v2()); @@ -1039,36 +1034,10 @@ private InstallPluginAction makeActionPluginThatDownloads( final String shaExtension, final Function shaCalculator, final PGPSecretKey secretKey, - final BiFunction signature, - final boolean spyVerifier + final BiFunction signature ) throws Exception { InstallablePlugin pluginZip = createPlugin(pluginId, pluginDir); Path pluginZipPath = Path.of(URI.create(pluginZip.getLocation())); - BcPgpSignatureVerifier psv = new BcPgpSignatureVerifier(url, terminal::println) { - @Override - InputStream pluginZipInputStream(Path zip) throws IOException { - return new ByteArrayInputStream(Files.readAllBytes(zip)); - } - - @Override - String getPublicKeyId() { - return Long.toHexString(secretKey.getKeyID()).toUpperCase(Locale.ROOT); - } - - @Override - InputStream getPublicKey() { - try { - final ByteArrayOutputStream output = new ByteArrayOutputStream(); - final ArmoredOutputStream armored = new ArmoredOutputStream(output); - secretKey.getPublicKey().encode(armored); - armored.close(); - return new ByteArrayInputStream(output.toByteArray()); - } catch (final IOException e) { - throw new AssertionError(e); - } - } - }; - BcPgpSignatureVerifier verifier = spyVerifier ? spy(psv) : psv; InstallPluginAction action = new InstallPluginAction(terminal, env.v2(), false) { @Override Path downloadZip(String urlString, Path tmpDir) throws IOException { @@ -1098,7 +1067,7 @@ URL openUrl(String urlString) throws IOException { } @Override - void verifySignature(Path zip, String urlString) throws IOException { + void verifySignature(Path zip, String urlString) throws IOException, PGPException { if (InstallPluginAction.OFFICIAL_PLUGINS.contains(pluginId)) { super.verifySignature(zip, urlString); } else { @@ -1107,8 +1076,26 @@ void verifySignature(Path zip, String urlString) throws IOException { } @Override - Supplier> pgpSignatureVerifier(String urlString, Terminal terminal) { - return () -> verifier; + InputStream pluginZipInputStream(Path zip) throws IOException { + return new ByteArrayInputStream(Files.readAllBytes(zip)); + } + + @Override + String getPublicKeyId() { + return Long.toHexString(secretKey.getKeyID()).toUpperCase(Locale.ROOT); + } + + @Override + InputStream getPublicKey() { + try { + final ByteArrayOutputStream output = new ByteArrayOutputStream(); + final ArmoredOutputStream armored = new ArmoredOutputStream(output); + secretKey.getPublicKey().encode(armored); + armored.close(); + return new ByteArrayInputStream(output.toByteArray()); + } catch (final IOException e) { + throw new AssertionError(e); + } } @Override @@ -1475,8 +1462,7 @@ public PGPSecretKey newSecretKey() throws NoSuchAlgorithmException, PGPException null, null, new JcaPGPContentSignerBuilder(pkp.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA256), - new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_192, sha1Calc).setProvider(new BouncyCastleFipsProvider()) - .build("passphrase".toCharArray()) + new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.AES_192, sha1Calc).build("passphrase".toCharArray()) ); } diff --git a/docs/changelog/138949.yaml b/docs/changelog/138949.yaml index 3c47fcb7e6f62..a0b085dabcdd0 100644 --- a/docs/changelog/138949.yaml +++ b/docs/changelog/138949.yaml @@ -1,6 +1,5 @@ pr: 138949 -summary: Update plugin-cli tool to use BC FIPS 2.x as a runtime dependency with class - loader isolation +summary: Shadow plugin-cli JAR to avoid conflicts with BC FIPS classes area: Security type: enhancement issues: [] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index e934cb20589c8..5e3bd1ceedac7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3673,11 +3673,6 @@
- - - - - @@ -3688,11 +3683,6 @@ - - - - - @@ -3738,11 +3728,6 @@ - - - - - diff --git a/settings.gradle b/settings.gradle index 22f9b2a098784..82fbc566efb02 100644 --- a/settings.gradle +++ b/settings.gradle @@ -91,7 +91,6 @@ List projects = [ 'distribution:tools:server-cli', 'distribution:tools:windows-service-cli', 'distribution:tools:plugin-cli', - 'distribution:tools:plugin-cli:bc', 'distribution:tools:keystore-cli', 'distribution:tools:geoip-cli', 'distribution:tools:ansi-console', From f56e8b855c0f93d26bc440788cd83e84cf895d25 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Wed, 17 Dec 2025 13:47:33 -0800 Subject: [PATCH 16/22] Use latest BC PGP 1.83 for plugin-cli rather than BC FIPS --- distribution/tools/plugin-cli/build.gradle | 38 +++++----------------- gradle/verification-metadata.xml | 15 +++++++++ qa/evil-tests/build.gradle | 1 - 3 files changed, 24 insertions(+), 30 deletions(-) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 9144b5fb1bb77..0b2bec7ebb06f 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -30,8 +30,9 @@ dependencies { 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" + api "org.bouncycastle:bcpg-jdk18on:1.83" + api "org.bouncycastle:bcprov-jdk18on:1.83" + api "org.bouncycastle:bcutil-jdk18on:1.83" testImplementation project(":test:framework") testImplementation "com.google.jimfs:jimfs:${versions.jimfs}" testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}" @@ -58,31 +59,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 } } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5e3bd1ceedac7..af62ecffd549d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3688,6 +3688,11 @@ + + + + + @@ -3723,6 +3728,11 @@ + + + + + @@ -3738,6 +3748,11 @@ + + + + + diff --git a/qa/evil-tests/build.gradle b/qa/evil-tests/build.gradle index 242fb381e176d..c7fbd56cb3c71 100644 --- a/qa/evil-tests/build.gradle +++ b/qa/evil-tests/build.gradle @@ -20,7 +20,6 @@ dependencies { testImplementation "com.google.jimfs:jimfs:1.3.0" testImplementation "com.google.guava:guava:${versions.jimfs_guava}" testImplementation project(":test:framework") - testImplementation project(':distribution:tools:plugin-cli') } // TODO: give each evil test its own fresh JVM for more isolation. From 7f955ccee2efacb0f833ebdda5a26690a8a5e739 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Wed, 17 Dec 2025 21:03:18 -0800 Subject: [PATCH 17/22] Only relocate BC class, avoid relocating asm, ES-core, etc --- distribution/tools/plugin-cli/build.gradle | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 0b2bec7ebb06f..fa06ef823d5d0 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -23,16 +23,19 @@ tasks.named("dependencyLicenses").configure { dependencies { compileOnly project(":server") compileOnly project(":libs:cli") - implementation project(":libs:plugin-api") - implementation project(":libs:plugin-scanner") - implementation project(":libs:entitlement") + shadow project(":libs:plugin-api") + shadow project(":libs:plugin-scanner") + shadow project(":libs:entitlement") // 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' + shadow 'org.ow2.asm:asm:9.9' + shadow 'org.ow2.asm:asm-tree:9.9' - api "org.bouncycastle:bcpg-jdk18on:1.83" - api "org.bouncycastle:bcprov-jdk18on:1.83" - api "org.bouncycastle:bcutil-jdk18on:1.83" + // Only Bouncy Castle gets bundled and relocated into the shadow jar + implementation "org.bouncycastle:bcpg-jdk18on:1.83" + implementation "org.bouncycastle:bcprov-jdk18on:1.83" + implementation "org.bouncycastle:bcutil-jdk18on:1.83" + + testImplementation project(":libs:plugin-scanner") testImplementation project(":test:framework") testImplementation "com.google.jimfs:jimfs:${versions.jimfs}" testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}" From fb411a99d0397af17c11a3eafc6cb19091958922 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Thu, 18 Dec 2025 12:04:15 -0800 Subject: [PATCH 18/22] Move PGP signature verifying using Bouncy Castle to bc sub-project within plugin-cli and shadow that bc sub-library only --- distribution/build.gradle | 2 +- distribution/tools/plugin-cli/bc/build.gradle | 34 +++++++ .../licenses/bouncycastle-LICENSE.txt | 0 .../{ => bc}/licenses/bouncycastle-NOTICE.txt | 0 .../plugins/cli/PgpSignatureVerifier.java | 91 +++++++++++++++++++ distribution/tools/plugin-cli/build.gradle | 30 ++---- .../plugins/cli/InstallPluginAction.java | 66 ++------------ .../plugins/cli/InstallPluginActionTests.java | 51 +++++++---- settings.gradle | 1 + 9 files changed, 176 insertions(+), 99 deletions(-) create mode 100644 distribution/tools/plugin-cli/bc/build.gradle rename distribution/tools/plugin-cli/{ => bc}/licenses/bouncycastle-LICENSE.txt (100%) rename distribution/tools/plugin-cli/{ => bc}/licenses/bouncycastle-NOTICE.txt (100%) create mode 100644 distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java diff --git a/distribution/build.gradle b/distribution/build.gradle index 7fd50469342ba..3d478ef236676 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -337,7 +337,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { libsServerCli project(':distribution:tools:server-cli') libsWindowsServiceCli project(':distribution:tools:windows-service-cli') libsAnsiConsole project(':distribution:tools:ansi-console') - libsPluginCli project(path: ':distribution:tools:plugin-cli', configuration: 'shadow') + libsPluginCli project(path: ':distribution:tools:plugin-cli') libsKeystoreCli project(path: ':distribution:tools:keystore-cli') libsSecurityCli project(':x-pack:plugin:security:cli') libsGeoIpCli project(':distribution:tools:geoip-cli') diff --git a/distribution/tools/plugin-cli/bc/build.gradle b/distribution/tools/plugin-cli/bc/build.gradle new file mode 100644 index 0000000000000..38b05119aaf5d --- /dev/null +++ b/distribution/tools/plugin-cli/bc/build.gradle @@ -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 +} diff --git a/distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt b/distribution/tools/plugin-cli/bc/licenses/bouncycastle-LICENSE.txt similarity index 100% rename from distribution/tools/plugin-cli/licenses/bouncycastle-LICENSE.txt rename to distribution/tools/plugin-cli/bc/licenses/bouncycastle-LICENSE.txt diff --git a/distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt b/distribution/tools/plugin-cli/bc/licenses/bouncycastle-NOTICE.txt similarity index 100% rename from distribution/tools/plugin-cli/licenses/bouncycastle-NOTICE.txt rename to distribution/tools/plugin-cli/bc/licenses/bouncycastle-NOTICE.txt diff --git a/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java new file mode 100644 index 0000000000000..8d6e0e9633b3f --- /dev/null +++ b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java @@ -0,0 +1,91 @@ +/* + * 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; + +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. + *

+ * This implementation was lifted from InstallPluginAction to isolate Bouncy Castle usage. + *

+ */ +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 the URL source of the PGP signature for 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 ( + // fin is a file stream over the downloaded plugin zip whose signature to verify + InputStream fin = pluginZipInputStream; + // sin is a URL stream to the signature corresponding to the downloaded plugin zip + InputStream sin = ascInputStream; + // ain is a input stream to the public key in ASCII-Armor format (RFC4880) + InputStream ain = new ArmoredInputStream(publicKeyInputStream) + ) { + 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); + } + } + +} diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index fa06ef823d5d0..44b79a7d181f8 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -10,7 +10,6 @@ import org.elasticsearch.gradle.OS apply plugin: 'elasticsearch.build' -apply plugin: 'com.gradleup.shadow' base { archivesName = 'elasticsearch-plugin-cli' @@ -23,35 +22,24 @@ tasks.named("dependencyLicenses").configure { dependencies { compileOnly project(":server") compileOnly project(":libs:cli") - shadow project(":libs:plugin-api") - shadow project(":libs:plugin-scanner") - shadow project(":libs:entitlement") + 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 - shadow 'org.ow2.asm:asm:9.9' - shadow 'org.ow2.asm:asm-tree:9.9' + implementation 'org.ow2.asm:asm:9.9' + implementation 'org.ow2.asm:asm-tree:9.9' - // Only Bouncy Castle gets bundled and relocated into the shadow jar - implementation "org.bouncycastle:bcpg-jdk18on:1.83" - implementation "org.bouncycastle:bcprov-jdk18on:1.83" - implementation "org.bouncycastle:bcutil-jdk18on:1.83" - - testImplementation project(":libs:plugin-scanner") 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' -} -tasks.named("shadowJar").configure { - relocate 'org.bouncycastle', 'shadow.org.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 { - // Use original classpath, not the shadow JAR, since test code imports non-relocated packages - classpath = sourceSets.test.runtimeClasspath // TODO: find a way to add permissions for the tests in this module systemProperty 'tests.security.manager', 'false' // These tests are "heavy" on the secure number generator. On Linux, the NativePRNG defaults to /dev/random for the seeds, and diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index c2966da88db3d..4da5b8fd92709 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -12,16 +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.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; @@ -567,12 +557,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"; @@ -666,41 +655,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 { @@ -711,22 +669,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(), 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 diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index bbd5f4f2eb911..e8ccddf62346d 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -97,6 +97,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -116,9 +117,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -479,6 +478,22 @@ public void testSlowSignatureVerificationMessage() throws Exception { + ".zip"; final MessageDigest digest = MessageDigest.getInstance("SHA-512"); + // Control for timeout on waiting for signature verification to complete + CountDownLatch countDownLatch = new CountDownLatch(1); + + // + AtomicBoolean called = new AtomicBoolean(false); + + Runnable callback = () -> { + if (called.compareAndSet(false, true)) { + try { + countDownLatch.await(); // wait until we trip the timer + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }; + InstallPluginAction action = makeActionPluginThatDownloads( "analysis-icu", url, @@ -487,14 +502,12 @@ public void testSlowSignatureVerificationMessage() throws Exception { ".sha512", checksumAndFilename(digest, url), newSecretKey(), - this::signature + this::signature, + callback ); final InstallPluginAction spied = spy(action); - // Control for timeout on waiting for signature verification to complete - CountDownLatch countDownLatch = new CountDownLatch(1); - doAnswer(i -> { i.callRealMethod(); countDownLatch.countDown(); @@ -504,12 +517,6 @@ public void testSlowSignatureVerificationMessage() throws Exception { // Make the slow verification acceptable delay artificially low for testing doReturn(100L).when(spied).acceptableSignatureVerificationDelay(); - doAnswer(i -> { - countDownLatch.await(); // wait until we trip the timer - i.callRealMethod(); - return null; - }).when(spied).computeSignatureForDownloadedPlugin(any(InputStream.class), any(InputStream.class), any(PGPSignature.class)); - installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), spied); assertThat(terminal.getOutput(), containsString("The plugin installer is trying to verify the signature ")); @@ -521,10 +528,6 @@ public void testSlowSignatureVerificationMessage() throws Exception { // Divide by two to meet the limitation of the Timer.schedule API. doReturn(Long.MAX_VALUE / 2).when(spied).acceptableSignatureVerificationDelay(); - // Make sure we don't see any slow error messages when the signature verification is fast - doCallRealMethod().when(spied) - .computeSignatureForDownloadedPlugin(any(InputStream.class), any(InputStream.class), any(PGPSignature.class)); - terminal.reset(); installPlugin(new InstallablePlugin("analysis-icu", null), env.v1(), spied); @@ -1019,7 +1022,8 @@ void assertInstallPluginFromUrl( shaExtension, shaCalculator, secretKey, - signature + signature, + () -> {} ); installPlugin(new InstallablePlugin(pluginId, pluginUrl), env.v1(), action); assertPlugin(pluginId, pluginDir, env.v2()); @@ -1034,7 +1038,8 @@ private InstallPluginAction makeActionPluginThatDownloads( final String shaExtension, final Function shaCalculator, final PGPSecretKey secretKey, - final BiFunction signature + final BiFunction signature, + final Runnable onReadPluginZip ) throws Exception { InstallablePlugin pluginZip = createPlugin(pluginId, pluginDir); Path pluginZipPath = Path.of(URI.create(pluginZip.getLocation())); @@ -1067,7 +1072,7 @@ URL openUrl(String urlString) throws IOException { } @Override - void verifySignature(Path zip, String urlString) throws IOException, PGPException { + void verifySignature(Path zip, String urlString) throws IOException { if (InstallPluginAction.OFFICIAL_PLUGINS.contains(pluginId)) { super.verifySignature(zip, urlString); } else { @@ -1077,7 +1082,13 @@ void verifySignature(Path zip, String urlString) throws IOException, PGPExceptio @Override InputStream pluginZipInputStream(Path zip) throws IOException { - return new ByteArrayInputStream(Files.readAllBytes(zip)); + return new ByteArrayInputStream(Files.readAllBytes(zip)) { + @Override + public int read(byte[] b) throws IOException { + onReadPluginZip.run(); + return super.read(b); + } + }; } @Override diff --git a/settings.gradle b/settings.gradle index 82fbc566efb02..22f9b2a098784 100644 --- a/settings.gradle +++ b/settings.gradle @@ -91,6 +91,7 @@ List projects = [ 'distribution:tools:server-cli', 'distribution:tools:windows-service-cli', 'distribution:tools:plugin-cli', + 'distribution:tools:plugin-cli:bc', 'distribution:tools:keystore-cli', 'distribution:tools:geoip-cli', 'distribution:tools:ansi-console', From 4b6f47fb1848164ef451e4e945c70b4090e606c3 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Thu, 18 Dec 2025 12:10:07 -0800 Subject: [PATCH 19/22] Update Javadoc comments in PgpSignatureVerifier --- .../elasticsearch/plugins/cli/PgpSignatureVerifier.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java index 8d6e0e9633b3f..4039787daa231 100644 --- a/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java @@ -35,8 +35,8 @@ 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 the URL source of the PGP signature for 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( @@ -48,12 +48,9 @@ public static void verifySignature( ) throws IOException { try ( - // fin is a file stream over the downloaded plugin zip whose signature to verify InputStream fin = pluginZipInputStream; - // sin is a URL stream to the signature corresponding to the downloaded plugin zip InputStream sin = ascInputStream; - // ain is a input stream to the public key in ASCII-Armor format (RFC4880) - InputStream ain = new ArmoredInputStream(publicKeyInputStream) + 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); From b889516b27ca51e833bbf22073186d3615722161 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Thu, 18 Dec 2025 12:14:51 -0800 Subject: [PATCH 20/22] Move PgpSignatureVerifier to bc sub-package --- .../plugins/cli/{ => bc}/PgpSignatureVerifier.java | 2 +- .../java/org/elasticsearch/plugins/cli/InstallPluginAction.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/{ => bc}/PgpSignatureVerifier.java (98%) diff --git a/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/bc/PgpSignatureVerifier.java similarity index 98% rename from distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java rename to distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/bc/PgpSignatureVerifier.java index 4039787daa231..09d950c7a5fa1 100644 --- a/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/PgpSignatureVerifier.java +++ b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/bc/PgpSignatureVerifier.java @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -package org.elasticsearch.plugins.cli; +package org.elasticsearch.plugins.cli.bc; import org.bouncycastle.bcpg.ArmoredInputStream; import org.bouncycastle.openpgp.PGPException; diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java index 4da5b8fd92709..7525adc326eda 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/cli/InstallPluginAction.java @@ -31,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; From aad8aaee905b9a78ed9f1ea5a7fd56334b8a0159 Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Thu, 18 Dec 2025 12:19:55 -0800 Subject: [PATCH 21/22] Another comment fix --- .../org/elasticsearch/plugins/cli/InstallPluginActionTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java index e8ccddf62346d..bf020e7e6e58f 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/cli/InstallPluginActionTests.java @@ -481,7 +481,6 @@ public void testSlowSignatureVerificationMessage() throws Exception { // Control for timeout on waiting for signature verification to complete CountDownLatch countDownLatch = new CountDownLatch(1); - // AtomicBoolean called = new AtomicBoolean(false); Runnable callback = () -> { From b776f4cb434e5f07574c875a6318a1156081182e Mon Sep 17 00:00:00 2001 From: Elliot Barlas Date: Thu, 18 Dec 2025 12:58:29 -0800 Subject: [PATCH 22/22] Minor formatting change in distribution/build.gradle --- distribution/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/distribution/build.gradle b/distribution/build.gradle index 3d478ef236676..e5a132f529733 100644 --- a/distribution/build.gradle +++ b/distribution/build.gradle @@ -337,7 +337,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) { libsServerCli project(':distribution:tools:server-cli') libsWindowsServiceCli project(':distribution:tools:windows-service-cli') libsAnsiConsole project(':distribution:tools:ansi-console') - libsPluginCli project(path: ':distribution:tools:plugin-cli') + libsPluginCli project(':distribution:tools:plugin-cli') libsKeystoreCli project(path: ':distribution:tools:keystore-cli') libsSecurityCli project(':x-pack:plugin:security:cli') libsGeoIpCli project(':distribution:tools:geoip-cli')