Skip to content

Commit 3e99d71

Browse files
authored
[8.x] Default to SSHA-256 as API key stored credential hasher (elastic#120997) (elastic#121229)
1 parent 04a55be commit 3e99d71

File tree

14 files changed

+279
-54
lines changed

14 files changed

+279
-54
lines changed

docs/changelog/120997.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 120997
2+
summary: Allow `SSHA-256` for API key credential hash
3+
area: Authentication
4+
type: enhancement
5+
issues: []

docs/reference/settings/security-hash-settings.asciidoc

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,68 @@ following:
124124
initial input with SHA512 first.
125125
|=======================
126126

127+
Furthermore, {es} supports authentication via securely-generated high entropy tokens,
128+
for instance <<security-api-create-api-key,API keys>>.
129+
Analogous to passwords, only the tokens' hashes are stored. Since the tokens are guaranteed
130+
to have sufficiently high entropy to resist offline attacks, secure salted hash functions are supported
131+
in addition to the password-hashing algorithms mentioned above.
127132

133+
You can configure the algorithm for API key stored credential hashing
134+
by setting the <<static-cluster-setting,static>>
135+
`xpack.security.authc.api_key.hashing.algorithm` setting to one of the
136+
following
137+
138+
[[secure-token-hashing-algorithms]]
139+
.Secure token hashing algorithms
140+
|=======================
141+
| Algorithm | | | Description
142+
143+
| `ssha256` | | | Uses a salted `sha-256` algorithm. (default)
144+
| `bcrypt` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds.
145+
| `bcrypt4` | | | Uses `bcrypt` algorithm with salt generated in 16 rounds.
146+
| `bcrypt5` | | | Uses `bcrypt` algorithm with salt generated in 32 rounds.
147+
| `bcrypt6` | | | Uses `bcrypt` algorithm with salt generated in 64 rounds.
148+
| `bcrypt7` | | | Uses `bcrypt` algorithm with salt generated in 128 rounds.
149+
| `bcrypt8` | | | Uses `bcrypt` algorithm with salt generated in 256 rounds.
150+
| `bcrypt9` | | | Uses `bcrypt` algorithm with salt generated in 512 rounds.
151+
| `bcrypt10` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds.
152+
| `bcrypt11` | | | Uses `bcrypt` algorithm with salt generated in 2048 rounds.
153+
| `bcrypt12` | | | Uses `bcrypt` algorithm with salt generated in 4096 rounds.
154+
| `bcrypt13` | | | Uses `bcrypt` algorithm with salt generated in 8192 rounds.
155+
| `bcrypt14` | | | Uses `bcrypt` algorithm with salt generated in 16384 rounds.
156+
| `pbkdf2` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
157+
pseudorandom function using 10000 iterations.
158+
| `pbkdf2_1000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
159+
pseudorandom function using 1000 iterations.
160+
| `pbkdf2_10000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
161+
pseudorandom function using 10000 iterations.
162+
| `pbkdf2_50000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
163+
pseudorandom function using 50000 iterations.
164+
| `pbkdf2_100000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
165+
pseudorandom function using 100000 iterations.
166+
| `pbkdf2_500000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
167+
pseudorandom function using 500000 iterations.
168+
| `pbkdf2_1000000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
169+
pseudorandom function using 1000000 iterations.
170+
| `pbkdf2_stretch` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
171+
pseudorandom function using 10000 iterations, after hashing the
172+
initial input with SHA512 first.
173+
| `pbkdf2_stretch_1000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
174+
pseudorandom function using 1000 iterations, after hashing the
175+
initial input with SHA512 first.
176+
| `pbkdf2_stretch_10000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
177+
pseudorandom function using 10000 iterations, after hashing the
178+
initial input with SHA512 first.
179+
| `pbkdf2_stretch_50000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
180+
pseudorandom function using 50000 iterations, after hashing the
181+
initial input with SHA512 first.
182+
| `pbkdf2_stretch_100000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
183+
pseudorandom function using 100000 iterations, after hashing the
184+
initial input with SHA512 first.
185+
| `pbkdf2_stretch_500000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
186+
pseudorandom function using 500000 iterations, after hashing the
187+
initial input with SHA512 first.
188+
| `pbkdf2_stretch_1000000`| | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a
189+
pseudorandom function using 1000000 iterations, after hashing the
190+
initial input with SHA512 first.
191+
|=======================

docs/reference/settings/security-settings.asciidoc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ For more information about creating and updating the {es} keystore, see
2323
==== General security settings
2424
`xpack.security.enabled`::
2525
(<<static-cluster-setting,Static>>)
26-
Defaults to `true`, which enables {es} {security-features} on the node.
27-
This setting must be enabled to use Elasticsearch's authentication,
26+
Defaults to `true`, which enables {es} {security-features} on the node.
27+
This setting must be enabled to use Elasticsearch's authentication,
2828
authorization and audit features. +
2929
+
3030
--
@@ -229,7 +229,7 @@ Defaults to `7d`.
229229

230230
--
231231
NOTE: Large real-time clock inconsistency across cluster nodes can cause problems
232-
with evaluating the API key retention period. That is, if the clock on the node
232+
with evaluating the API key retention period. That is, if the clock on the node
233233
invalidating the API key is significantly different than the one performing the deletion,
234234
the key may be retained for longer or shorter than the configured retention period.
235235

@@ -252,7 +252,7 @@ Sets the timeout of the internal search and delete call.
252252
`xpack.security.authc.api_key.hashing.algorithm`::
253253
(<<static-cluster-setting,Static>>)
254254
Specifies the hashing algorithm that is used for securing API key credentials.
255-
See <<password-hashing-algorithms>>. Defaults to `pbkdf2`.
255+
See <<secure-token-hashing-algorithms>>. Defaults to `ssha256`.
256256

257257
[discrete]
258258
[[security-domain-settings]]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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;
11+
12+
import org.elasticsearch.common.settings.SecureString;
13+
import org.elasticsearch.core.CharArrays;
14+
15+
import java.util.Arrays;
16+
import java.util.Base64;
17+
18+
public final class SecureRandomUtils {
19+
private SecureRandomUtils() {}
20+
21+
/**
22+
* Returns a cryptographically secure Base64 encoded {@link SecureString} of {@code numBytes} random bytes.
23+
*/
24+
public static SecureString getBase64SecureRandomString(int numBytes) {
25+
byte[] randomBytes = null;
26+
byte[] encodedBytes = null;
27+
try {
28+
randomBytes = new byte[numBytes];
29+
SecureRandomHolder.INSTANCE.nextBytes(randomBytes);
30+
encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(randomBytes);
31+
return new SecureString(CharArrays.utf8BytesToChars(encodedBytes));
32+
} finally {
33+
if (randomBytes != null) {
34+
Arrays.fill(randomBytes, (byte) 0);
35+
}
36+
if (encodedBytes != null) {
37+
Arrays.fill(encodedBytes, (byte) 0);
38+
}
39+
}
40+
}
41+
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ public Iterator<Setting<?>> settings() {
240240

241241
public static final List<String> DEFAULT_CIPHERS = JDK12_CIPHERS;
242242

243-
public static final Setting<String> PASSWORD_HASHING_ALGORITHM = defaultStoredHashAlgorithmSetting(
243+
public static final Setting<String> PASSWORD_HASHING_ALGORITHM = defaultStoredPasswordHashAlgorithmSetting(
244244
"xpack.security.authc.password_hashing.algorithm",
245245
(s) -> {
246246
if (XPackSettings.FIPS_MODE_ENABLED.get(s)) {
@@ -251,19 +251,56 @@ public Iterator<Setting<?>> settings() {
251251
}
252252
);
253253

254-
public static final Setting<String> SERVICE_TOKEN_HASHING_ALGORITHM = defaultStoredHashAlgorithmSetting(
254+
public static final Setting<String> SERVICE_TOKEN_HASHING_ALGORITHM = defaultStoredPasswordHashAlgorithmSetting(
255255
"xpack.security.authc.service_token_hashing.algorithm",
256256
(s) -> Hasher.PBKDF2_STRETCH.name()
257257
);
258258

259259
/*
260260
* Do not allow insecure hashing algorithms to be used for password hashing
261261
*/
262-
public static Setting<String> defaultStoredHashAlgorithmSetting(String key, Function<Settings, String> defaultHashingAlgorithm) {
262+
public static Setting<String> defaultStoredPasswordHashAlgorithmSetting(
263+
String key,
264+
Function<Settings, String> defaultHashingAlgorithm
265+
) {
263266
return new Setting<>(new Setting.SimpleKey(key), defaultHashingAlgorithm, Function.identity(), v -> {
264-
if (Hasher.getAvailableAlgoStoredHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
267+
if (Hasher.getAvailableAlgoStoredPasswordHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
265268
throw new IllegalArgumentException(
266-
"Invalid algorithm: " + v + ". Valid values for password hashing are " + Hasher.getAvailableAlgoStoredHash().toString()
269+
"Invalid algorithm: "
270+
+ v
271+
+ ". Valid values for password hashing are "
272+
+ Hasher.getAvailableAlgoStoredPasswordHash().toString()
273+
);
274+
} else if (v.regionMatches(true, 0, "pbkdf2", 0, "pbkdf2".length())) {
275+
try {
276+
SecretKeyFactory.getInstance("PBKDF2withHMACSHA512");
277+
} catch (NoSuchAlgorithmException e) {
278+
throw new IllegalArgumentException(
279+
"Support for PBKDF2WithHMACSHA512 must be available in order to use any of the PBKDF2 algorithms for the ["
280+
+ key
281+
+ "] setting.",
282+
e
283+
);
284+
}
285+
}
286+
}, Property.NodeScope);
287+
}
288+
289+
/**
290+
* Similar to {@link #defaultStoredPasswordHashAlgorithmSetting(String, Function)} but for secure, high-entropy tokens so salted secure
291+
* hashing algorithms are allowed, in addition to algorithms that are suitable for password hashing.
292+
*/
293+
public static Setting<String> defaultStoredSecureTokenHashAlgorithmSetting(
294+
String key,
295+
Function<Settings, String> defaultHashingAlgorithm
296+
) {
297+
return new Setting<>(new Setting.SimpleKey(key), defaultHashingAlgorithm, Function.identity(), v -> {
298+
if (Hasher.getAvailableAlgoStoredSecureTokenHash().contains(v.toLowerCase(Locale.ROOT)) == false) {
299+
throw new IllegalArgumentException(
300+
"Invalid algorithm: "
301+
+ v
302+
+ ". Valid values for secure token hashing are "
303+
+ Hasher.getAvailableAlgoStoredSecureTokenHash().toString()
267304
);
268305
} else if (v.regionMatches(true, 0, "pbkdf2", 0, "pbkdf2".length())) {
269306
try {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ public PutUserRequestBuilder email(String email) {
8989
public PutUserRequestBuilder passwordHash(char[] passwordHash, Hasher configuredHasher) {
9090
final Hasher resolvedHasher = Hasher.resolveFromHash(passwordHash);
9191
if (resolvedHasher.equals(configuredHasher) == false
92-
&& Hasher.getAvailableAlgoStoredHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
92+
&& Hasher.getAvailableAlgoStoredPasswordHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
9393
throw new IllegalArgumentException(
9494
"The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. "
9595
+ "The supported password hash algorithms are "
96-
+ Hasher.getAvailableAlgoStoredHash().toString()
96+
+ Hasher.getAvailableAlgoStoredPasswordHash().toString()
9797
);
9898
}
9999
if (request.passwordHash() != null) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -734,14 +734,28 @@ private static boolean verifyBcryptHash(SecureString text, char[] hash) {
734734
* an instance of the appropriate {@link Hasher} by using {@link #resolve(String) resolve()}
735735
*/
736736
@SuppressForbidden(reason = "This is the only allowed way to get available values")
737-
public static List<String> getAvailableAlgoStoredHash() {
737+
public static List<String> getAvailableAlgoStoredPasswordHash() {
738738
return Arrays.stream(Hasher.values())
739739
.map(Hasher::name)
740740
.map(name -> name.toLowerCase(Locale.ROOT))
741741
.filter(name -> (name.startsWith("pbkdf2") || name.startsWith("bcrypt")))
742742
.collect(Collectors.toList());
743743
}
744744

745+
/**
746+
* Returns a list of lower case String identifiers for the Hashing algorithm and parameter
747+
* combinations that can be used for secure token hashing. The identifiers can be used to get
748+
* an instance of the appropriate {@link Hasher} by using {@link #resolve(String) resolve()}
749+
*/
750+
@SuppressForbidden(reason = "This is the only allowed way to get available values")
751+
public static List<String> getAvailableAlgoStoredSecureTokenHash() {
752+
return Arrays.stream(Hasher.values())
753+
.map(Hasher::name)
754+
.map(name -> name.toLowerCase(Locale.ROOT))
755+
.filter(name -> (name.startsWith("pbkdf2") || name.startsWith("bcrypt") || name.equals("ssha256")))
756+
.collect(Collectors.toList());
757+
}
758+
745759
/**
746760
* Returns a list of lower case String identifiers for the Hashing algorithm and parameter
747761
* combinations that can be used for password hashing in the cache. The identifiers can be used to get

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,7 +1475,7 @@ public static List<Setting<?>> getSettings(List<SecurityExtension> securityExten
14751475
settingsList.add(TokenService.DELETE_INTERVAL);
14761476
settingsList.add(TokenService.DELETE_TIMEOUT);
14771477
settingsList.addAll(SSLConfigurationSettings.getProfileSettings());
1478-
settingsList.add(ApiKeyService.PASSWORD_HASHING_ALGORITHM);
1478+
settingsList.add(ApiKeyService.STORED_HASH_ALGO_SETTING);
14791479
settingsList.add(ApiKeyService.DELETE_TIMEOUT);
14801480
settingsList.add(ApiKeyService.DELETE_INTERVAL);
14811481
settingsList.add(ApiKeyService.DELETE_RETENTION_PERIOD);
@@ -1821,17 +1821,30 @@ static void validateForFips(Settings settings) {
18211821
+ " ] setting."
18221822
);
18231823
}
1824-
Stream.of(ApiKeyService.PASSWORD_HASHING_ALGORITHM, XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM).forEach((setting) -> {
1825-
final var storedHashAlgo = setting.get(settings);
1826-
if (storedHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) {
1827-
// log instead of validation error for backwards compatibility
1828-
logger.warn(
1829-
"Only PBKDF2 is allowed for stored credential hashing in a FIPS 140 JVM. "
1830-
+ "Please set the appropriate value for [{}] setting.",
1831-
setting.getKey()
1832-
);
1833-
}
1834-
});
1824+
1825+
final var serviceTokenStoredHashSettings = XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM;
1826+
final var serviceTokenStoredHashAlgo = serviceTokenStoredHashSettings.get(settings);
1827+
if (serviceTokenStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) {
1828+
// log instead of validation error for backwards compatibility
1829+
logger.warn(
1830+
"Only PBKDF2 is allowed for stored credential hashing in a FIPS 140 JVM. "
1831+
+ "Please set the appropriate value for [{}] setting.",
1832+
serviceTokenStoredHashSettings.getKey()
1833+
);
1834+
}
1835+
1836+
final var apiKeyStoredHashSettings = ApiKeyService.STORED_HASH_ALGO_SETTING;
1837+
final var apiKeyStoredHashAlgo = apiKeyStoredHashSettings.get(settings);
1838+
if (apiKeyStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("ssha256") == false
1839+
&& apiKeyStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) {
1840+
// log instead of validation error for backwards compatibility
1841+
logger.warn(
1842+
"[{}] is not recommended for stored API key hashing in a FIPS 140 JVM. The recommended hasher for [{}] is SSHA256.",
1843+
apiKeyStoredHashSettings,
1844+
apiKeyStoredHashSettings.getKey()
1845+
);
1846+
}
1847+
18351848
final var cacheHashAlgoSettings = settings.filter(k -> k.endsWith(".cache.hash_algo"));
18361849
cacheHashAlgoSettings.keySet().forEach((key) -> {
18371850
final var setting = cacheHashAlgoSettings.get(key);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ public ChangePasswordRequestBuilder password(char[] password, Hasher hasher) {
7272
public ChangePasswordRequestBuilder passwordHash(char[] passwordHashChars, Hasher configuredHasher) {
7373
final Hasher resolvedHasher = Hasher.resolveFromHash(passwordHashChars);
7474
if (resolvedHasher.equals(configuredHasher) == false
75-
&& Hasher.getAvailableAlgoStoredHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
75+
&& Hasher.getAvailableAlgoStoredPasswordHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) {
7676
throw new IllegalArgumentException(
7777
"The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. "
7878
+ "The supported password hash algorithms are "
79-
+ Hasher.getAvailableAlgoStoredHash().toString()
79+
+ Hasher.getAvailableAlgoStoredPasswordHash().toString()
8080
);
8181
}
8282
if (request.passwordHash() != null) {

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ protected void doExecute(Task task, ChangePasswordRequest request, ActionListene
5252
final Hasher requestPwdHashAlgo = Hasher.resolveFromHash(request.passwordHash());
5353
final Hasher configPwdHashAlgo = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings));
5454
if (requestPwdHashAlgo.equals(configPwdHashAlgo) == false
55-
&& Hasher.getAvailableAlgoStoredHash().contains(requestPwdHashAlgo.name().toLowerCase(Locale.ROOT)) == false) {
55+
&& Hasher.getAvailableAlgoStoredPasswordHash().contains(requestPwdHashAlgo.name().toLowerCase(Locale.ROOT)) == false) {
5656
listener.onFailure(
5757
new IllegalArgumentException(
5858
"The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. "
5959
+ "The supported password hash algorithms are "
60-
+ Hasher.getAvailableAlgoStoredHash().toString()
60+
+ Hasher.getAvailableAlgoStoredPasswordHash().toString()
6161
)
6262
);
6363
return;

0 commit comments

Comments
 (0)