Skip to content

Commit 3caefee

Browse files
authored
Handle unavailable MD5 in ES|QL (#130158) (#130532)
In Java 14 the `MessageDigest` specification was changed so that the "MD5" hash function is no longer required. It is permissible for a JRE to ship without support for MD5 hashes. This commit modifies the ES|QL MD5 hash function implementation so that if the specified `MessageDigest` object is not available on startup, then the error is non-fatal, and the node will still boot successfully. If an ES|QL query attempts to make use of md5, and it is unavailable, then the query will fail with an ES|QL verification exception Resolves: #129689
1 parent e310d0a commit 3caefee

File tree

6 files changed

+209
-3
lines changed

6 files changed

+209
-3
lines changed

docs/changelog/130158.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 130158
2+
summary: Handle unavailable MD5 in ES|QL
3+
area: ES|QL
4+
type: bug
5+
issues: []
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.common.util;
11+
12+
import org.elasticsearch.common.CheckedSupplier;
13+
14+
import java.util.Optional;
15+
16+
/**
17+
* A wrapper around either
18+
* <ul>
19+
* <li>a successful result of parameterized type {@code V}</li>
20+
* <li>a failure with exception type {@code E}</li>
21+
* </ul>
22+
*/
23+
public abstract class Result<V, E extends Exception> implements CheckedSupplier<V, E> {
24+
25+
public static <V, E extends Exception> Result<V, E> of(V value) {
26+
return new Success<>(value);
27+
}
28+
29+
public static <V, E extends Exception> Result<V, E> failure(E exception) {
30+
return new Failure<>(exception);
31+
}
32+
33+
private Result() {}
34+
35+
public abstract V get() throws E;
36+
37+
public abstract Optional<E> failure();
38+
39+
public abstract boolean isSuccessful();
40+
41+
public boolean isFailure() {
42+
return isSuccessful() == false;
43+
};
44+
45+
public abstract Optional<V> asOptional();
46+
47+
private static class Success<V, E extends Exception> extends Result<V, E> {
48+
private final V value;
49+
50+
Success(V value) {
51+
this.value = value;
52+
}
53+
54+
@Override
55+
public V get() throws E {
56+
return value;
57+
}
58+
59+
@Override
60+
public Optional<E> failure() {
61+
return Optional.empty();
62+
}
63+
64+
@Override
65+
public boolean isSuccessful() {
66+
return true;
67+
}
68+
69+
@Override
70+
public Optional<V> asOptional() {
71+
return Optional.of(value);
72+
}
73+
}
74+
75+
private static class Failure<V, E extends Exception> extends Result<V, E> {
76+
private final E exception;
77+
78+
Failure(E exception) {
79+
this.exception = exception;
80+
}
81+
82+
@Override
83+
public V get() throws E {
84+
throw exception;
85+
}
86+
87+
@Override
88+
public Optional<E> failure() {
89+
return Optional.of(exception);
90+
}
91+
92+
@Override
93+
public boolean isSuccessful() {
94+
return false;
95+
}
96+
97+
@Override
98+
public Optional<V> asOptional() {
99+
return Optional.empty();
100+
}
101+
}
102+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.common.util;
11+
12+
import org.elasticsearch.ElasticsearchException;
13+
import org.elasticsearch.ElasticsearchStatusException;
14+
import org.elasticsearch.rest.RestStatus;
15+
import org.elasticsearch.test.ESTestCase;
16+
17+
import static org.elasticsearch.test.hamcrest.OptionalMatchers.isEmpty;
18+
import static org.elasticsearch.test.hamcrest.OptionalMatchers.isPresentWith;
19+
import static org.hamcrest.Matchers.is;
20+
import static org.hamcrest.Matchers.sameInstance;
21+
22+
public class ResultTests extends ESTestCase {
23+
24+
public void testSuccess() {
25+
final String str = randomAlphaOfLengthBetween(3, 8);
26+
final Result<String, ElasticsearchException> result = Result.of(str);
27+
assertThat(result.isSuccessful(), is(true));
28+
assertThat(result.isFailure(), is(false));
29+
assertThat(result.get(), sameInstance(str));
30+
assertThat(result.failure(), isEmpty());
31+
assertThat(result.asOptional(), isPresentWith(str));
32+
}
33+
34+
public void testFailure() {
35+
final ElasticsearchException exception = new ElasticsearchStatusException(
36+
randomAlphaOfLengthBetween(10, 30),
37+
RestStatus.INTERNAL_SERVER_ERROR
38+
);
39+
final Result<String, ElasticsearchException> result = Result.failure(exception);
40+
assertThat(result.isSuccessful(), is(false));
41+
assertThat(result.isFailure(), is(true));
42+
assertThat(expectThrows(Exception.class, result::get), sameInstance(exception));
43+
assertThat(result.failure(), isPresentWith(sameInstance(exception)));
44+
assertThat(result.asOptional(), isEmpty());
45+
}
46+
47+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Hash.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1212
import org.elasticsearch.common.io.stream.StreamInput;
1313
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.common.util.Result;
1415
import org.elasticsearch.compute.ann.Evaluator;
1516
import org.elasticsearch.compute.ann.Fixed;
1617
import org.elasticsearch.compute.operator.BreakingBytesRefBuilder;
@@ -202,6 +203,14 @@ public static HashFunction create(BytesRef literal) throws NoSuchAlgorithmExcept
202203
return new HashFunction(algorithm, MessageDigest.getInstance(algorithm));
203204
}
204205

206+
public static Result<HashFunction, NoSuchAlgorithmException> tryCreate(String algorithm) {
207+
try {
208+
return Result.of(new HashFunction(algorithm, MessageDigest.getInstance(algorithm)));
209+
} catch (NoSuchAlgorithmException e) {
210+
return Result.failure(e);
211+
}
212+
}
213+
205214
public HashFunction copy() {
206215
try {
207216
return new HashFunction(algorithm, MessageDigest.getInstance(algorithm));

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/Md5.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1111
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.util.Result;
13+
import org.elasticsearch.xpack.esql.VerificationException;
1214
import org.elasticsearch.xpack.esql.core.expression.Expression;
1315
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
1416
import org.elasticsearch.xpack.esql.core.tree.Source;
@@ -18,17 +20,24 @@
1820
import org.elasticsearch.xpack.esql.expression.function.scalar.string.Hash.HashFunction;
1921

2022
import java.io.IOException;
23+
import java.security.MessageDigest;
24+
import java.security.NoSuchAlgorithmException;
2125
import java.util.List;
2226

2327
public class Md5 extends AbstractHashFunction {
2428

2529
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "MD5", Md5::new);
2630

27-
private static final HashFunction MD5 = HashFunction.create("MD5");
31+
/**
32+
* As of Java 14, it is permissible for a JRE to ship without the {@code MD5} {@link MessageDigest}.
33+
* We want the "md5" function in ES|QL to fail at runtime on such platforms (rather than at startup)
34+
* so we wrap the {@link HashFunction} in a {@link Result}.
35+
*/
36+
private static final Result<HashFunction, NoSuchAlgorithmException> MD5 = HashFunction.tryCreate("MD5");
2837

2938
@FunctionInfo(
3039
returnType = "keyword",
31-
description = "Computes the MD5 hash of the input.",
40+
description = "Computes the MD5 hash of the input (if the MD5 hash is available on the JVM).",
3241
examples = { @Example(file = "hash", tag = "md5") }
3342
)
3443
public Md5(Source source, @Param(name = "input", type = { "keyword", "text" }, description = "Input to hash.") Expression input) {
@@ -41,7 +50,12 @@ private Md5(StreamInput in) throws IOException {
4150

4251
@Override
4352
protected HashFunction getHashFunction() {
44-
return MD5;
53+
try {
54+
return MD5.get();
55+
} catch (NoSuchAlgorithmException e) {
56+
// Throw a new exception so that the stack trace reflects this call (rather than the static initializer for the MD5 field)
57+
throw new VerificationException("function 'md5' is not available on this platform: {}", e.getMessage());
58+
}
4559
}
4660

4761
@Override

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/string/HashStaticTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,30 @@
1313
import org.elasticsearch.common.util.BigArrays;
1414
import org.elasticsearch.common.util.MockBigArrays;
1515
import org.elasticsearch.common.util.PageCacheRecycler;
16+
import org.elasticsearch.common.util.Result;
1617
import org.elasticsearch.compute.data.BlockFactory;
1718
import org.elasticsearch.compute.operator.DriverContext;
1819
import org.elasticsearch.test.ESTestCase;
20+
import org.elasticsearch.test.hamcrest.OptionalMatchers;
1921
import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
2022
import org.elasticsearch.xpack.esql.core.expression.Literal;
2123
import org.elasticsearch.xpack.esql.core.tree.Source;
2224
import org.elasticsearch.xpack.esql.core.type.DataType;
2325
import org.junit.After;
2426

27+
import java.security.NoSuchAlgorithmException;
28+
import java.security.Provider;
29+
import java.security.Security;
2530
import java.util.ArrayList;
2631
import java.util.Collections;
2732
import java.util.List;
2833

34+
import static org.elasticsearch.test.TestMatchers.throwableWithMessage;
2935
import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.evaluator;
3036
import static org.elasticsearch.xpack.esql.expression.function.AbstractFunctionTestCase.field;
37+
import static org.hamcrest.Matchers.containsString;
3138
import static org.hamcrest.Matchers.equalTo;
39+
import static org.hamcrest.Matchers.is;
3240
import static org.hamcrest.Matchers.startsWith;
3341

3442
public class HashStaticTests extends ESTestCase {
@@ -45,6 +53,27 @@ public void testInvalidAlgorithmLiteral() {
4553
assertThat(e.getMessage(), startsWith("invalid algorithm for [hast(\"invalid\", input)]: invalid MessageDigest not available"));
4654
}
4755

56+
public void testTryCreateUnavailableMd5() throws NoSuchAlgorithmException {
57+
assumeFalse("We run with different security providers in FIPS, and changing them at runtime is more complicated", inFipsJvm());
58+
final Provider sunProvider = Security.getProvider("SUN");
59+
try {
60+
Security.removeProvider("SUN");
61+
final Result<Hash.HashFunction, NoSuchAlgorithmException> result = Hash.HashFunction.tryCreate("MD5");
62+
assertThat(result.isSuccessful(), is(false));
63+
assertThat(result.failure(), OptionalMatchers.isPresentWith(throwableWithMessage(containsString("MD5"))));
64+
expectThrows(NoSuchAlgorithmException.class, result::get);
65+
} finally {
66+
Security.addProvider(sunProvider);
67+
}
68+
69+
{
70+
final Result<Hash.HashFunction, NoSuchAlgorithmException> result = Hash.HashFunction.tryCreate("MD5");
71+
assertThat(result.isSuccessful(), is(true));
72+
assertThat(result.failure(), OptionalMatchers.isEmpty());
73+
assertThat(result.get().algorithm(), is("MD5"));
74+
}
75+
}
76+
4877
/**
4978
* The following fields and methods were borrowed from AbstractScalarFunctionTestCase
5079
*/

0 commit comments

Comments
 (0)