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/bc/PgpSignatureVerifier.java b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/bc/PgpSignatureVerifier.java new file mode 100644 index 0000000000000..09d950c7a5fa1 --- /dev/null +++ b/distribution/tools/plugin-cli/bc/src/main/java/org/elasticsearch/plugins/cli/bc/PgpSignatureVerifier.java @@ -0,0 +1,88 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.plugins.cli.bc; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; +import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; +import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; + +/** + * A PGP signature verifier that uses Bouncy Castle implementation. + *

+ * 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 an input stream to the signature corresponding to the downloaded plugin zip + * @param publicKeyInputStream an input stream to the public key of the signing key. + */ + public static void verifySignature( + String publicKeyId, + String urlString, + InputStream pluginZipInputStream, + InputStream ascInputStream, + InputStream publicKeyInputStream + ) throws IOException { + + try ( + InputStream fin = pluginZipInputStream; + InputStream sin = ascInputStream; + InputStream ain = new ArmoredInputStream(publicKeyInputStream) // input stream to the public key in ASCII-Armor format (RFC4880) + ) { + final JcaPGPObjectFactory factory = new JcaPGPObjectFactory(PGPUtil.getDecoderStream(sin)); + final PGPSignature signature = ((PGPSignatureList) factory.nextObject()).get(0); + + // validate the signature has key ID matching our public key ID + final String keyId = Long.toHexString(signature.getKeyID()).toUpperCase(Locale.ROOT); + if (publicKeyId.equals(keyId) == false) { + throw new IllegalStateException("key id [" + keyId + "] does not match expected key id [" + publicKeyId + "]"); + } + + // compute the signature of the downloaded plugin zip + computeSignatureForDownloadedPlugin(fin, ain, signature); + + // finally we verify the signature of the downloaded plugin zip matches the expected signature + if (signature.verify() == false) { + throw new IllegalStateException("signature verification for [" + urlString + "] failed"); + } + } catch (PGPException e) { + throw new IOException("PGP exception during signature verification for [" + urlString + "]", e); + } + } + + private static void computeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, + IOException { + final PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(ain, new JcaKeyFingerprintCalculator()); + final PGPPublicKey key = collection.getPublicKey(signature.getKeyID()); + signature.init(new JcaPGPContentVerifierBuilderProvider(), key); + final byte[] buffer = new byte[1024]; + int read; + while ((read = fin.read(buffer)) != -1) { + signature.update(buffer, 0, read); + } + } + +} diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle index 3026ee74e00d0..44b79a7d181f8 100644 --- a/distribution/tools/plugin-cli/build.gradle +++ b/distribution/tools/plugin-cli/build.gradle @@ -25,19 +25,18 @@ dependencies { implementation project(":libs:plugin-api") implementation project(":libs:plugin-scanner") implementation project(":libs:entitlement") + implementation project(path: "bc", configuration: 'shadow') // TODO: asm is picked up from the plugin scanner and entitlements, we should consolidate so it is not defined twice implementation 'org.ow2.asm:asm:9.9' implementation 'org.ow2.asm:asm-tree:9.9' - api "org.bouncycastle:bcpg-fips:1.0.7.1" - api "org.bouncycastle:bc-fips:1.0.2.6" testImplementation project(":test:framework") testImplementation "com.google.jimfs:jimfs:${versions.jimfs}" testRuntimeOnly "com.google.guava:guava:${versions.jimfs_guava}" -} -tasks.named("dependencyLicenses").configure { - mapping from: /bc.*/, to: 'bouncycastle' + testImplementation "org.bouncycastle:bcpg-jdk18on:1.83" + testImplementation "org.bouncycastle:bcprov-jdk18on:1.83" + testImplementation "org.bouncycastle:bcutil-jdk18on:1.83" } tasks.named("test").configure { @@ -51,31 +50,10 @@ tasks.named("test").configure { } } -/* - * these two classes intentionally use the following JDK internal APIs in order to offer the necessary - * functionality - * - * sun.security.internal.spec.TlsKeyMaterialParameterSpec - * sun.security.internal.spec.TlsKeyMaterialSpec - * sun.security.internal.spec.TlsMasterSecretParameterSpec - * sun.security.internal.spec.TlsPrfParameterSpec - * sun.security.internal.spec.TlsRsaPremasterSecretParameterSpec - * sun.security.provider.SecureRandom - * - */ -tasks.named("thirdPartyAudit").configure { - ignoreViolations( - 'org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider$CoreSecureRandom', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$BaseTLSKeyGeneratorSpi', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSKeyMaterialGenerator', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSKeyMaterialGenerator$2', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSMasterSecretGenerator', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSMasterSecretGenerator$2', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSPRFKeyGenerator', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSRsaPreMasterSecretGenerator', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSRsaPreMasterSecretGenerator$2', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSExtendedMasterSecretGenerator', - 'org.bouncycastle.jcajce.provider.ProvSunTLSKDF$TLSExtendedMasterSecretGenerator$2' - ) +if (buildParams.inFipsJvm) { + // Disable tests in FIPS mode due to JAR hell between plugin-cli's + // BC dependencies and the BC FIPS dependencies added by the FIPS gradle config + // We support running plugin-cli with BC FIPS JARs in ES lib via shadowing. + // Running these tests with the JVM in FIPS mode isn't related. + tasks.named("test").configure { enabled = false } } 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..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 @@ -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; @@ -42,6 +31,7 @@ import org.elasticsearch.plugins.Platforms; import org.elasticsearch.plugins.PluginDescriptor; import org.elasticsearch.plugins.PluginsUtils; +import org.elasticsearch.plugins.cli.bc.PgpSignatureVerifier; import org.objectweb.asm.ClassReader; import java.io.BufferedReader; @@ -568,12 +558,11 @@ private InputStream urlOpenStream(final URL url) throws IOException { * @param officialPlugin true if the plugin is an official plugin * @return the path to the downloaded plugin ZIP * @throws IOException if an I/O exception occurs download or reading files and resources - * @throws PGPException if an exception occurs verifying the downloaded ZIP signature * @throws UserException if checksum validation fails * @throws URISyntaxException is the url is invalid */ private Path downloadAndValidate(final String urlString, final Path tmpDir, final boolean officialPlugin) throws IOException, - PGPException, UserException, URISyntaxException { + UserException, URISyntaxException { Path zip = downloadZip(urlString, tmpDir); pathsToDeleteOnShutdown.add(zip); String checksumUrlString = urlString + ".sha512"; @@ -667,41 +656,10 @@ private Path downloadAndValidate(final String urlString, final Path tmpDir, fina * * @param zip the path to the downloaded plugin ZIP * @param urlString the URL source of the downloaded plugin ZIP - * @throws IOException if an I/O exception occurs reading from various input streams - * @throws PGPException if the PGP implementation throws an internal exception during verification + * @throws IOException if an I/O exception occurs reading from various input streams or + * if the PGP implementation throws an internal exception during verification */ - void verifySignature(final Path zip, final String urlString) throws IOException, PGPException { - final String ascUrlString = urlString + ".asc"; - final URL ascUrl = openUrl(ascUrlString); - try ( - // fin is a file stream over the downloaded plugin zip whose signature to verify - InputStream fin = pluginZipInputStream(zip); - // sin is a URL stream to the signature corresponding to the downloaded plugin zip - InputStream sin = urlOpenStream(ascUrl); - // ain is a input stream to the public key in ASCII-Armor format (RFC4880) - InputStream ain = new ArmoredInputStream(getPublicKey()) - ) { - final JcaPGPObjectFactory factory = new JcaPGPObjectFactory(PGPUtil.getDecoderStream(sin)); - final PGPSignature signature = ((PGPSignatureList) factory.nextObject()).get(0); - - // validate the signature has key ID matching our public key ID - final String keyId = Long.toHexString(signature.getKeyID()).toUpperCase(Locale.ROOT); - if (getPublicKeyId().equals(keyId) == false) { - throw new IllegalStateException("key id [" + keyId + "] does not match expected key id [" + getPublicKeyId() + "]"); - } - - // compute the signature of the downloaded plugin zip, wrapped with long execution warning - timedComputeSignatureForDownloadedPlugin(fin, ain, signature); - - // finally we verify the signature of the downloaded plugin zip matches the expected signature - if (signature.verify() == false) { - throw new IllegalStateException("signature verification for [" + urlString + "] failed"); - } - } - } - - private void timedComputeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, - IOException { + void verifySignature(final Path zip, final String urlString) throws IOException { final Timer timer = new Timer(); try { @@ -712,22 +670,16 @@ public void run() { } }, acceptableSignatureVerificationDelay()); - computeSignatureForDownloadedPlugin(fin, ain, signature); + doVerifySignature(zip, urlString); } finally { timer.cancel(); } } - // package private for testing - void computeSignatureForDownloadedPlugin(InputStream fin, InputStream ain, PGPSignature signature) throws PGPException, IOException { - final PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(ain, new JcaKeyFingerprintCalculator()); - final PGPPublicKey key = collection.getPublicKey(signature.getKeyID()); - signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider(new BouncyCastleFipsProvider()), key); - final byte[] buffer = new byte[1024]; - int read; - while ((read = fin.read(buffer)) != -1) { - signature.update(buffer, 0, read); - } + void doVerifySignature(final Path zip, final String urlString) throws IOException { + final String ascUrlString = urlString + ".asc"; + final URL ascUrl = openUrl(ascUrlString); + PgpSignatureVerifier.verifySignature(getPublicKeyId(), urlString, pluginZipInputStream(zip), urlOpenStream(ascUrl), getPublicKey()); } // package private for testing 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..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 @@ -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,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; @@ -117,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; @@ -480,6 +478,21 @@ 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, @@ -488,14 +501,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(); @@ -505,12 +516,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 ")); @@ -522,10 +527,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); @@ -1020,7 +1021,8 @@ void assertInstallPluginFromUrl( shaExtension, shaCalculator, secretKey, - signature + signature, + () -> {} ); installPlugin(new InstallablePlugin(pluginId, pluginUrl), env.v1(), action); assertPlugin(pluginId, pluginDir, env.v2()); @@ -1035,7 +1037,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())); @@ -1068,7 +1071,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 { @@ -1078,7 +1081,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 @@ -1463,8 +1472,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 new file mode 100644 index 0000000000000..a0b085dabcdd0 --- /dev/null +++ b/docs/changelog/138949.yaml @@ -0,0 +1,5 @@ +pr: 138949 +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 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. 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',