-
Notifications
You must be signed in to change notification settings - Fork 25k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Verify signatures on official plugins #30800
Verify signatures on official plugins #30800
Conversation
We sign our official plugins yet this is not well-advertised and not at all consumed during plugin installation. For plugins that are installed over the intertubes, verifying that the downloaded artifact is signed by our signing key would establish both integrity and validity of the downloaded artifact. The chain of trust here is simple: our installable artifacts (archive and package distributions) so that if a user trusts our packages via their signatures, and our plugin installer (which would be executing trusted code) verifies the downloaded plugin, then the user can trust the downloaded plugin too. This commit adds verification of official plugins downloaded during installation. We do not add verification for offline plugin installs; a user can download our signatures and verify the artifacts themselves. This commit also needs to solve a few interesting challenges. One of these is that we want the bouncy castle JARs on the classpath only for the plugin installer, but not for the runtime Elasticsearch. Additionally, we want these JARs to not be present for the JAR hell checks. To address this, we shift these JARs into a sub-directory of lib (lib/tools/plugin-cli) that is only loaded for the plugin installer, and in the plugin installer we filter any JARs in this directory from the JAR hell check.
Pinging @elastic/es-core-infra |
Could I ask someone from @elastic/es-security to review this PR too? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM overall. I left a bunch of nits and minor requests
distribution/build.gradle
Outdated
from { project(':server').jar } | ||
from { project(':server').configurations.runtime } | ||
from { project(':libs:plugin-classloader').jar } | ||
// delay add tools using closures, since they have not yet been configured, so no jar task exists yet |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is important to keep in some form. It is the reason all these from
statements use a closure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this comment back.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: try on its own line please
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made this change.
return Files.newInputStream(zip); | ||
} | ||
|
||
String getPublicKeyId() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At least a one line javadoc on each of these methods explaining these are not constants so tests can override would be nice
Security.addProvider(new BouncyCastleProvider()); | ||
} | ||
|
||
void verifySignature(final Path zip, final String urlString) throws IOException, PGPException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a simple one line javadoc would be nice here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a Javadoc.
@@ -605,11 +707,21 @@ private PluginInfo loadPluginInfo(Terminal terminal, Path pluginRoot, Environmen | |||
return info; | |||
} | |||
|
|||
private static final String LIB_TOOLS_PLUGIN_CLI_CLASSPATH_JAR; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: please put this at the top of the file with other constants/static blocks
@@ -21,9 +21,11 @@ | |||
|
|||
import org.elasticsearch.test.ESTestCase; | |||
|
|||
import java.io.File; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file seems untouched except for added imports?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what happened here, I reverted the changes.
private static final String LIB_TOOLS_PLUGIN_CLI_CLASSPATH_JAR; | ||
|
||
static { | ||
LIB_TOOLS_PLUGIN_CLI_CLASSPATH_JAR = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: shouldn't this be jars, plural (ie it matches many jars, not just one)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is the pattern for matching a JAR under the lib/tools/plugin-cli path, it is only that there can be multiple matches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks right to me, but it's been years since I've used the BC PGP API, so I'll need to do another pass when I'm a bit more awake (unless someone else beats me to it).
} | ||
|
||
/** | ||
* Downlaods a ZIP from the URL. This method also validates the downloaded plugin ZIP via the following means: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s/Downlaods/Downloads/
"https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/" + icu + "-" + Version.CURRENT + ".zip"; | ||
final MessageDigest digest = MessageDigest.getInstance("SHA-512"); | ||
/* | ||
* To setup a situation where the expected public key ID does not match the public key ID used for singing, we generate a new public |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/sing/sign/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks good. I left some comments about the use of the BC provider; I'd prefer if we did not have to install it and we only use it for what we need to (the PGP stuff).
} | ||
|
||
return zip; | ||
} | ||
|
||
static { | ||
Security.addProvider(new BouncyCastleProvider()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if these BC apis will allow for this, but is it possible that we just use the provider directly rather that install it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jaymode Would you explain the problem with the simplest approach here, this is only in the CLI tool?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be considered paranoia on my part, but I just am not a fan of adding a security provider to be available globally even if it is just in a CLI tool. The provider being available allows for code that does not need bouncy castle to obtain a crypto primitive, like a SHA-256
message digest, from this provider. If the BC provider has a security bug in its implementation, this drives the need for us to ship a new version quickly; whereas if we rely on the JVMs configuration then the bug would most likely not necessitate a new release of our software.
As to a simple approach, here is a diff that removes the installation of the provider and instead uses an instance of it. gradle check
passes on the plugin-cli project.
diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java
index 1cfa9b20b75..8e0daa49d7a 100644
--- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java
+++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java
@@ -526,9 +526,7 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
return zip;
}
- static {
- Security.addProvider(new BouncyCastleProvider());
- }
+ static final BouncyCastleProvider BOUNCY_CASTLE_PROVIDER = new BouncyCastleProvider();
/**
* Verify the signature of the downloaded plugin ZIP. The signature is obtained from the source of the downloaded plugin by appending
@@ -561,7 +559,7 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
// compute the signature of the downloaded plugin zip
final PGPPublicKeyRingCollection collection = new PGPPublicKeyRingCollection(pin, new JcaKeyFingerprintCalculator());
final PGPPublicKey key = collection.getPublicKey(signature.getKeyID());
- signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), key);
+ signature.init(new JcaPGPContentVerifierBuilderProvider().setProvider(BOUNCY_CASTLE_PROVIDER), key);
final byte[] buffer = new byte[1024];
int read;
while ((read = fin.read(buffer)) != -1) {
diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java
index 89c747b6c2a..da6b2a11a2a 100644
--- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java
+++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java
@@ -1143,7 +1143,7 @@ public class InstallPluginCommandTests extends ESTestCase {
}
public PGPSecretKey newSecretKey() throws NoSuchAlgorithmException, NoSuchProviderException, PGPException {
- final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC");
+ final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
final KeyPair pair = kpg.generateKeyPair();
final PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(HashAlgorithmTags.SHA1);
@@ -1156,7 +1156,9 @@ public class InstallPluginCommandTests extends ESTestCase {
null,
null,
new JcaPGPContentSignerBuilder(pkp.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1),
- new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.CAST5, sha1Calc).setProvider("BC").build("passphrase".toCharArray()));
+ new JcePBESecretKeyEncryptorBuilder(PGPEncryptedData.CAST5, sha1Calc)
+ .setProvider(InstallPluginCommand.BOUNCY_CASTLE_PROVIDER)
+ .build("passphrase".toCharArray()));
}
private Function<byte[], String> checksum(final MessageDigest digest) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | ||
|
||
public PGPSecretKey newSecretKey() throws NoSuchAlgorithmException, NoSuchProviderException, PGPException { | ||
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "BC"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to use the BC
provider generating a RSA key pair?
* master: (25 commits) [DOCS] Splits auditing.asciidoc into smaller files Reintroduce mandatory http pipelining support (elastic#30820) Painless: Types Section Clean Up (elastic#30283) Add support for indexed shape routing in geo_shape query (elastic#30760) [test] java tests for archive packaging (elastic#30734) Revert "Make http pipelining support mandatory (elastic#30695)" (elastic#30813) [DOCS] Fix more edit URLs in Stack Overview (elastic#30704) Use correct cluster state version for node fault detection (elastic#30810) Change serialization version of doc-value fields. [DOCS] Fixes broken link for native realm [DOCS] Clarified audit.index.client.hosts (elastic#30797) [TEST] Don't expect acks when isolating nodes Add a `format` option to `docvalue_fields`. (elastic#29639) Fixes UpdateSettingsRequestStreamableTests mutate bug Mustes {p0=snapshot.get_repository/10_basic/*} YAML test Revert "Mutes MachineLearningTests.testNoAttributes_givenSameAndMlEnabled" Only allow x-pack metadata if all nodes are ready (elastic#30743) Mutes MachineLearningTests.testNoAttributes_givenSameAndMlEnabled Use original settings on full-cluster restart (elastic#30780) Only ack cluster state updates successfully applied on all nodes (elastic#30672) ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
distribution/packages/build.gradle
Outdated
eachFile { FileCopyDetails fcp -> | ||
String[] segments = fcp.relativePath.segments | ||
for (int i = segments.length - 2; i > 0 && segments[i] != 'lib'; --i) { | ||
System.out.println(segments[0..i]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover println
distribution/packages/build.gradle
Outdated
eachFile { FileCopyDetails fcp -> | ||
String[] segments = fcp.relativePath.segments | ||
for (int i = segments.length - 2; i > 0 && segments[i] != 'modules'; --i) { | ||
System.out.println(segments[0..i]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leftover println
* master: Update the version checks around ip_range bucket keys, now that the change was backported. Mute IndexMasterFailoverIT.testMasterFailoverDuringIndexingWithMappingChanges Use geohash cell instead of just a corner in geo_bounding_box (elastic#30698) Limit user to single concurrent auth per realm (elastic#30794) [Tests] Move templated _rank_eval tests (elastic#30679) Security: fix dynamic mapping updates with aliases (elastic#30787) Ensure that ip_range aggregations always return bucket keys. (elastic#30701) Use remote client in TransportFieldCapsAction (elastic#30838) Move Watcher versioning setting to meta field (elastic#30832) [Docs] Explain incomplete dates in range queries (elastic#30689) Move persistent task registrations to core (elastic#30755) Decouple ClusterStateTaskListener & ClusterApplier (elastic#30809) Send client headers from TransportClient (elastic#30803) Packaging: Ensure upgrade_is_oss flag file is always deleted (elastic#30732) Force stable file modes for built packages (elastic#30823) [DOCS] Fixes typos in security settings Fix GeoShapeQueryBuilder serialization after backport
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
We sign our official plugins yet this is not well-advertised and not at all consumed during plugin installation. For plugins that are installed over the intertubes, verifying that the downloaded artifact is signed by our signing key would establish both integrity and validity of the downloaded artifact. The chain of trust here is simple: our installable artifacts (archive and package distributions) so that if a user trusts our packages via their signatures, and our plugin installer (which would be executing trusted code) verifies the downloaded plugin, then the user can trust the downloaded plugin too. This commit adds verification of official plugins downloaded during installation. We do not add verification for offline plugin installs; a user can download our signatures and verify the artifacts themselves. This commit also needs to solve a few interesting challenges. One of these is that we want the bouncy castle JARs on the classpath only for the plugin installer, but not for the runtime Elasticsearch. Additionally, we want these JARs to not be present for the JAR hell checks. To address this, we shift these JARs into a sub-directory of lib (lib/tools/plugin-cli) that is only loaded for the plugin installer, and in the plugin installer we filter any JARs in this directory from the JAR hell check.
* 6.x: Fix double semicolon in import statement [TEST] Fix minor random bug from #30794 Enabling testing against an external cluster (#30885) SQL: Remove the last remaining server dependencies from jdbc (#30771) Add public key header/footer (#30877) Include size of snapshot in snapshot metadata (#29602) QA: Test template creation during rolling restart (#30850) REST high-level client: add put ingest pipeline API (#30793) Do not serialize basic license exp in x-pack info (#30848) [docs] explainer for java packaging tests (#30825) Verify signatures on official plugins (#30800) [DOCS] Document index name limitations (#30826) [Docs] Add reindex.remote.whitelist example (#30828)
We sign our official plugins yet this is not well-advertised and not at all consumed during plugin installation. For plugins that are installed over the intertubes, verifying that the downloaded artifact is signed by our signing key would establish both integrity and validity of the downloaded artifact. The chain of trust here is simple: our installable artifacts (archive and package distributions) are signed so that if a user trusts our packages via their signatures, and our plugin installer (which would be executing trusted code) verifies the downloaded plugin, then the user can trust the downloaded plugin too. This commit adds verification of official plugins downloaded during installation. We do not add verification for offline plugin installs; a user can download our signatures and verify the artifacts themselves.
This commit also needs to solve a few interesting challenges. One of these is that we want the bouncy castle JARs on the classpath only for the plugin installer, but not for the runtime Elasticsearch. Additionally, we want these JARs to not be present for the JAR hell checks. To address this, we shift these JARs into a sub-directory of lib (lib/tools/plugin-cli) that is only loaded for the plugin installer, and in the plugin installer we filter any JARs in this directory from the JAR hell check.