diff --git a/docs/changelog/120997.yaml b/docs/changelog/120997.yaml new file mode 100644 index 0000000000000..6b56578404371 --- /dev/null +++ b/docs/changelog/120997.yaml @@ -0,0 +1,5 @@ +pr: 120997 +summary: Allow `SSHA-256` for API key credential hash +area: Authentication +type: enhancement +issues: [] diff --git a/docs/reference/settings/security-hash-settings.asciidoc b/docs/reference/settings/security-hash-settings.asciidoc index 93350a7749405..79819e4a389aa 100644 --- a/docs/reference/settings/security-hash-settings.asciidoc +++ b/docs/reference/settings/security-hash-settings.asciidoc @@ -124,4 +124,68 @@ following: initial input with SHA512 first. |======================= +Furthermore, {es} supports authentication via securely-generated high entropy tokens, +for instance <>. +Analogous to passwords, only the tokens' hashes are stored. Since the tokens are guaranteed +to have sufficiently high entropy to resist offline attacks, secure salted hash functions are supported +in addition to the password-hashing algorithms mentioned above. +You can configure the algorithm for API key stored credential hashing +by setting the <> +`xpack.security.authc.api_key.hashing.algorithm` setting to one of the +following + +[[secure-token-hashing-algorithms]] +.Secure token hashing algorithms +|======================= +| Algorithm | | | Description + +| `ssha256` | | | Uses a salted `sha-256` algorithm. (default) +| `bcrypt` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds. +| `bcrypt4` | | | Uses `bcrypt` algorithm with salt generated in 16 rounds. +| `bcrypt5` | | | Uses `bcrypt` algorithm with salt generated in 32 rounds. +| `bcrypt6` | | | Uses `bcrypt` algorithm with salt generated in 64 rounds. +| `bcrypt7` | | | Uses `bcrypt` algorithm with salt generated in 128 rounds. +| `bcrypt8` | | | Uses `bcrypt` algorithm with salt generated in 256 rounds. +| `bcrypt9` | | | Uses `bcrypt` algorithm with salt generated in 512 rounds. +| `bcrypt10` | | | Uses `bcrypt` algorithm with salt generated in 1024 rounds. +| `bcrypt11` | | | Uses `bcrypt` algorithm with salt generated in 2048 rounds. +| `bcrypt12` | | | Uses `bcrypt` algorithm with salt generated in 4096 rounds. +| `bcrypt13` | | | Uses `bcrypt` algorithm with salt generated in 8192 rounds. +| `bcrypt14` | | | Uses `bcrypt` algorithm with salt generated in 16384 rounds. +| `pbkdf2` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations. +| `pbkdf2_1000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000 iterations. +| `pbkdf2_10000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations. +| `pbkdf2_50000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 50000 iterations. +| `pbkdf2_100000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 100000 iterations. +| `pbkdf2_500000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 500000 iterations. +| `pbkdf2_1000000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000000 iterations. +| `pbkdf2_stretch` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations, after hashing the + initial input with SHA512 first. +| `pbkdf2_stretch_1000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000 iterations, after hashing the + initial input with SHA512 first. +| `pbkdf2_stretch_10000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 10000 iterations, after hashing the + initial input with SHA512 first. +| `pbkdf2_stretch_50000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 50000 iterations, after hashing the + initial input with SHA512 first. +| `pbkdf2_stretch_100000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 100000 iterations, after hashing the + initial input with SHA512 first. +| `pbkdf2_stretch_500000` | | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 500000 iterations, after hashing the + initial input with SHA512 first. +| `pbkdf2_stretch_1000000`| | | Uses `PBKDF2` key derivation function with `HMAC-SHA512` as a + pseudorandom function using 1000000 iterations, after hashing the + initial input with SHA512 first. +|======================= diff --git a/docs/reference/settings/security-settings.asciidoc b/docs/reference/settings/security-settings.asciidoc index 0fc4d59e72350..db95ac48f5be8 100644 --- a/docs/reference/settings/security-settings.asciidoc +++ b/docs/reference/settings/security-settings.asciidoc @@ -23,8 +23,8 @@ For more information about creating and updating the {es} keystore, see ==== General security settings `xpack.security.enabled`:: (<>) -Defaults to `true`, which enables {es} {security-features} on the node. -This setting must be enabled to use Elasticsearch's authentication, +Defaults to `true`, which enables {es} {security-features} on the node. +This setting must be enabled to use Elasticsearch's authentication, authorization and audit features. + + -- @@ -229,7 +229,7 @@ Defaults to `7d`. -- NOTE: Large real-time clock inconsistency across cluster nodes can cause problems -with evaluating the API key retention period. That is, if the clock on the node +with evaluating the API key retention period. That is, if the clock on the node invalidating the API key is significantly different than the one performing the deletion, the key may be retained for longer or shorter than the configured retention period. @@ -252,7 +252,7 @@ Sets the timeout of the internal search and delete call. `xpack.security.authc.api_key.hashing.algorithm`:: (<>) Specifies the hashing algorithm that is used for securing API key credentials. -See <>. Defaults to `pbkdf2`. +See <>. Defaults to `ssha256`. [discrete] [[security-domain-settings]] diff --git a/server/src/main/java/org/elasticsearch/common/SecureRandomUtils.java b/server/src/main/java/org/elasticsearch/common/SecureRandomUtils.java new file mode 100644 index 0000000000000..bdde158b95db7 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/SecureRandomUtils.java @@ -0,0 +1,41 @@ +/* + * 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.common; + +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.core.CharArrays; + +import java.util.Arrays; +import java.util.Base64; + +public final class SecureRandomUtils { + private SecureRandomUtils() {} + + /** + * Returns a cryptographically secure Base64 encoded {@link SecureString} of {@code numBytes} random bytes. + */ + public static SecureString getBase64SecureRandomString(int numBytes) { + byte[] randomBytes = null; + byte[] encodedBytes = null; + try { + randomBytes = new byte[numBytes]; + SecureRandomHolder.INSTANCE.nextBytes(randomBytes); + encodedBytes = Base64.getUrlEncoder().withoutPadding().encode(randomBytes); + return new SecureString(CharArrays.utf8BytesToChars(encodedBytes)); + } finally { + if (randomBytes != null) { + Arrays.fill(randomBytes, (byte) 0); + } + if (encodedBytes != null) { + Arrays.fill(encodedBytes, (byte) 0); + } + } + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java index 6aef618288fd2..3b4d4aec776d1 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java @@ -240,7 +240,7 @@ public Iterator> settings() { public static final List DEFAULT_CIPHERS = JDK12_CIPHERS; - public static final Setting PASSWORD_HASHING_ALGORITHM = defaultStoredHashAlgorithmSetting( + public static final Setting PASSWORD_HASHING_ALGORITHM = defaultStoredPasswordHashAlgorithmSetting( "xpack.security.authc.password_hashing.algorithm", (s) -> { if (XPackSettings.FIPS_MODE_ENABLED.get(s)) { @@ -251,7 +251,7 @@ public Iterator> settings() { } ); - public static final Setting SERVICE_TOKEN_HASHING_ALGORITHM = defaultStoredHashAlgorithmSetting( + public static final Setting SERVICE_TOKEN_HASHING_ALGORITHM = defaultStoredPasswordHashAlgorithmSetting( "xpack.security.authc.service_token_hashing.algorithm", (s) -> Hasher.PBKDF2_STRETCH.name() ); @@ -259,11 +259,48 @@ public Iterator> settings() { /* * Do not allow insecure hashing algorithms to be used for password hashing */ - public static Setting defaultStoredHashAlgorithmSetting(String key, Function defaultHashingAlgorithm) { + public static Setting defaultStoredPasswordHashAlgorithmSetting( + String key, + Function defaultHashingAlgorithm + ) { return new Setting<>(key, defaultHashingAlgorithm, Function.identity(), v -> { - if (Hasher.getAvailableAlgoStoredHash().contains(v.toLowerCase(Locale.ROOT)) == false) { + if (Hasher.getAvailableAlgoStoredPasswordHash().contains(v.toLowerCase(Locale.ROOT)) == false) { throw new IllegalArgumentException( - "Invalid algorithm: " + v + ". Valid values for password hashing are " + Hasher.getAvailableAlgoStoredHash().toString() + "Invalid algorithm: " + + v + + ". Valid values for password hashing are " + + Hasher.getAvailableAlgoStoredPasswordHash().toString() + ); + } else if (v.regionMatches(true, 0, "pbkdf2", 0, "pbkdf2".length())) { + try { + SecretKeyFactory.getInstance("PBKDF2withHMACSHA512"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException( + "Support for PBKDF2WithHMACSHA512 must be available in order to use any of the PBKDF2 algorithms for the [" + + key + + "] setting.", + e + ); + } + } + }, Property.NodeScope); + } + + /** + * Similar to {@link #defaultStoredPasswordHashAlgorithmSetting(String, Function)} but for secure, high-entropy tokens so salted secure + * hashing algorithms are allowed, in addition to algorithms that are suitable for password hashing. + */ + public static Setting defaultStoredSecureTokenHashAlgorithmSetting( + String key, + Function defaultHashingAlgorithm + ) { + return new Setting<>(key, defaultHashingAlgorithm, Function.identity(), v -> { + if (Hasher.getAvailableAlgoStoredSecureTokenHash().contains(v.toLowerCase(Locale.ROOT)) == false) { + throw new IllegalArgumentException( + "Invalid algorithm: " + + v + + ". Valid values for secure token hashing are " + + Hasher.getAvailableAlgoStoredSecureTokenHash().toString() ); } else if (v.regionMatches(true, 0, "pbkdf2", 0, "pbkdf2".length())) { try { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java index 7ae915d2db791..81f6b7489d8c8 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequestBuilder.java @@ -89,11 +89,11 @@ public PutUserRequestBuilder email(String email) { public PutUserRequestBuilder passwordHash(char[] passwordHash, Hasher configuredHasher) { final Hasher resolvedHasher = Hasher.resolveFromHash(passwordHash); if (resolvedHasher.equals(configuredHasher) == false - && Hasher.getAvailableAlgoStoredHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) { + && Hasher.getAvailableAlgoStoredPasswordHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) { throw new IllegalArgumentException( "The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. " + "The supported password hash algorithms are " - + Hasher.getAvailableAlgoStoredHash().toString() + + Hasher.getAvailableAlgoStoredPasswordHash().toString() ); } if (request.passwordHash() != null) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java index bf24919a39495..7e4780bf4f5b3 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/Hasher.java @@ -735,7 +735,7 @@ private static boolean verifyBcryptHash(SecureString text, char[] hash) { * an instance of the appropriate {@link Hasher} by using {@link #resolve(String) resolve()} */ @SuppressForbidden(reason = "This is the only allowed way to get available values") - public static List getAvailableAlgoStoredHash() { + public static List getAvailableAlgoStoredPasswordHash() { return Arrays.stream(Hasher.values()) .map(Hasher::name) .map(name -> name.toLowerCase(Locale.ROOT)) @@ -743,6 +743,20 @@ public static List getAvailableAlgoStoredHash() { .collect(Collectors.toList()); } + /** + * Returns a list of lower case String identifiers for the Hashing algorithm and parameter + * combinations that can be used for secure token hashing. The identifiers can be used to get + * an instance of the appropriate {@link Hasher} by using {@link #resolve(String) resolve()} + */ + @SuppressForbidden(reason = "This is the only allowed way to get available values") + public static List getAvailableAlgoStoredSecureTokenHash() { + return Arrays.stream(Hasher.values()) + .map(Hasher::name) + .map(name -> name.toLowerCase(Locale.ROOT)) + .filter(name -> (name.startsWith("pbkdf2") || name.startsWith("bcrypt") || name.equals("ssha256"))) + .collect(Collectors.toList()); + } + /** * Returns a list of lower case String identifiers for the Hashing algorithm and parameter * combinations that can be used for password hashing in the cache. The identifiers can be used to get diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 6004f8ebf95c4..804610f8dd341 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -1472,7 +1472,7 @@ public static List> getSettings(List securityExten settingsList.add(TokenService.DELETE_INTERVAL); settingsList.add(TokenService.DELETE_TIMEOUT); settingsList.addAll(SSLConfigurationSettings.getProfileSettings()); - settingsList.add(ApiKeyService.PASSWORD_HASHING_ALGORITHM); + settingsList.add(ApiKeyService.STORED_HASH_ALGO_SETTING); settingsList.add(ApiKeyService.DELETE_TIMEOUT); settingsList.add(ApiKeyService.DELETE_INTERVAL); settingsList.add(ApiKeyService.DELETE_RETENTION_PERIOD); @@ -1818,17 +1818,30 @@ static void validateForFips(Settings settings) { + " ] setting." ); } - Stream.of(ApiKeyService.PASSWORD_HASHING_ALGORITHM, XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM).forEach((setting) -> { - final var storedHashAlgo = setting.get(settings); - if (storedHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) { - // log instead of validation error for backwards compatibility - logger.warn( - "Only PBKDF2 is allowed for stored credential hashing in a FIPS 140 JVM. " - + "Please set the appropriate value for [{}] setting.", - setting.getKey() - ); - } - }); + + final var serviceTokenStoredHashSettings = XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM; + final var serviceTokenStoredHashAlgo = serviceTokenStoredHashSettings.get(settings); + if (serviceTokenStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) { + // log instead of validation error for backwards compatibility + logger.warn( + "Only PBKDF2 is allowed for stored credential hashing in a FIPS 140 JVM. " + + "Please set the appropriate value for [{}] setting.", + serviceTokenStoredHashSettings.getKey() + ); + } + + final var apiKeyStoredHashSettings = ApiKeyService.STORED_HASH_ALGO_SETTING; + final var apiKeyStoredHashAlgo = apiKeyStoredHashSettings.get(settings); + if (apiKeyStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("ssha256") == false + && apiKeyStoredHashAlgo.toLowerCase(Locale.ROOT).startsWith("pbkdf2") == false) { + // log instead of validation error for backwards compatibility + logger.warn( + "[{}] is not recommended for stored API key hashing in a FIPS 140 JVM. The recommended hasher for [{}] is SSHA256.", + apiKeyStoredHashSettings, + apiKeyStoredHashSettings.getKey() + ); + } + final var cacheHashAlgoSettings = settings.filter(k -> k.endsWith(".cache.hash_algo")); cacheHashAlgoSettings.keySet().forEach((key) -> { final var setting = cacheHashAlgoSettings.get(key); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilder.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilder.java index c792fa364a74a..fc09681ac26ed 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilder.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilder.java @@ -72,11 +72,11 @@ public ChangePasswordRequestBuilder password(char[] password, Hasher hasher) { public ChangePasswordRequestBuilder passwordHash(char[] passwordHashChars, Hasher configuredHasher) { final Hasher resolvedHasher = Hasher.resolveFromHash(passwordHashChars); if (resolvedHasher.equals(configuredHasher) == false - && Hasher.getAvailableAlgoStoredHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) { + && Hasher.getAvailableAlgoStoredPasswordHash().contains(resolvedHasher.name().toLowerCase(Locale.ROOT)) == false) { throw new IllegalArgumentException( "The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. " + "The supported password hash algorithms are " - + Hasher.getAvailableAlgoStoredHash().toString() + + Hasher.getAvailableAlgoStoredPasswordHash().toString() ); } if (request.passwordHash() != null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java index 96323836aa005..541bbdddd657e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/user/TransportChangePasswordAction.java @@ -52,12 +52,12 @@ protected void doExecute(Task task, ChangePasswordRequest request, ActionListene final Hasher requestPwdHashAlgo = Hasher.resolveFromHash(request.passwordHash()); final Hasher configPwdHashAlgo = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings)); if (requestPwdHashAlgo.equals(configPwdHashAlgo) == false - && Hasher.getAvailableAlgoStoredHash().contains(requestPwdHashAlgo.name().toLowerCase(Locale.ROOT)) == false) { + && Hasher.getAvailableAlgoStoredPasswordHash().contains(requestPwdHashAlgo.name().toLowerCase(Locale.ROOT)) == false) { listener.onFailure( new IllegalArgumentException( "The provided password hash is not a hash or it could not be resolved to a supported hash algorithm. " + "The supported password hash algorithms are " - + Hasher.getAvailableAlgoStoredHash().toString() + + Hasher.getAvailableAlgoStoredPasswordHash().toString() ) ); return; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index c2d1370c2cbf3..5fee747a3f73f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -38,7 +38,6 @@ import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; @@ -139,6 +138,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import static org.elasticsearch.common.SecureRandomUtils.getBase64SecureRandomString; import static org.elasticsearch.core.Strings.format; import static org.elasticsearch.search.SearchService.DEFAULT_KEEPALIVE_SETTING; import static org.elasticsearch.transport.RemoteClusterPortSettings.TRANSPORT_VERSION_ADVANCED_REMOTE_CLUSTER_SECURITY; @@ -158,9 +158,9 @@ public class ApiKeyService implements Closeable { private static final Logger logger = LogManager.getLogger(ApiKeyService.class); private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(ApiKeyService.class); - public static final Setting PASSWORD_HASHING_ALGORITHM = XPackSettings.defaultStoredHashAlgorithmSetting( + public static final Setting STORED_HASH_ALGO_SETTING = XPackSettings.defaultStoredSecureTokenHashAlgorithmSetting( "xpack.security.authc.api_key.hashing.algorithm", - (s) -> Hasher.PBKDF2.name() + (s) -> Hasher.SSHA256.name() ); public static final Setting DELETE_TIMEOUT = Setting.timeSetting( "xpack.security.authc.api_key.delete.timeout", @@ -181,7 +181,7 @@ public class ApiKeyService implements Closeable { ); public static final Setting CACHE_HASH_ALGO_SETTING = Setting.simpleString( "xpack.security.authc.api_key.cache.hash_algo", - "ssha256", + Hasher.SSHA256.name(), Setting.Property.NodeScope ); public static final Setting CACHE_TTL_SETTING = Setting.timeSetting( @@ -217,9 +217,9 @@ public class ApiKeyService implements Closeable { private final ThreadPool threadPool; private final ApiKeyDocCache apiKeyDocCache; - // The API key secret is a Base64 encoded v4 UUID without padding. The UUID is 128 bits, i.e. 16 byte, - // which requires 22 digits of Base64 characters for encoding without padding. - // See also UUIDs.randomBase64UUIDSecureString + private static final int API_KEY_SECRET_NUM_BYTES = 16; + // The API key secret is a Base64 encoded string of 128 random bits. + // See getBase64SecureRandomString() private static final int API_KEY_SECRET_LENGTH = 22; private static final long EVICTION_MONITOR_INTERVAL_SECONDS = 300L; // 5 minutes private static final long EVICTION_MONITOR_INTERVAL_NANOS = EVICTION_MONITOR_INTERVAL_SECONDS * 1_000_000_000L; @@ -245,7 +245,7 @@ public ApiKeyService( this.securityIndex = securityIndex; this.clusterService = clusterService; this.enabled = XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.get(settings); - this.hasher = Hasher.resolve(PASSWORD_HASHING_ALGORITHM.get(settings)); + this.hasher = Hasher.resolve(STORED_HASH_ALGO_SETTING.get(settings)); this.settings = settings; this.inactiveApiKeysRemover = new InactiveApiKeysRemover(settings, client, clusterService); this.threadPool = threadPool; @@ -545,7 +545,7 @@ private void createApiKeyAndIndexIt( ) { final Instant created = clock.instant(); final Instant expiration = getApiKeyExpiration(created, request.getExpiration()); - final SecureString apiKey = UUIDs.randomBase64UUIDSecureString(); + final SecureString apiKey = getBase64SecureRandomString(API_KEY_SECRET_NUM_BYTES); assert ApiKey.Type.CROSS_CLUSTER != request.getType() || API_KEY_SECRET_LENGTH == apiKey.length() : "Invalid API key (name=[" + request.getName() + "], type=[" + request.getType() + "], length=[" + apiKey.length() + "])"; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java index 5c6c3e8c7933c..3ff8f16165547 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java @@ -548,7 +548,7 @@ public void testValidateForFipsKeystoreWithImplicitJksType() { .put( XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), randomFrom( - Hasher.getAvailableAlgoStoredHash() + Hasher.getAvailableAlgoStoredPasswordHash() .stream() .filter(alg -> alg.startsWith("pbkdf2") == false) .collect(Collectors.toList()) @@ -567,7 +567,10 @@ public void testValidateForFipsKeystoreWithExplicitJksType() { .put( XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), randomFrom( - Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList()) + Hasher.getAvailableAlgoStoredPasswordHash() + .stream() + .filter(alg -> alg.startsWith("pbkdf2")) + .collect(Collectors.toList()) ) ) .build(); @@ -581,7 +584,7 @@ public void testValidateForFipsInvalidPasswordHashingAlgorithm() { .put( XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), randomFrom( - Hasher.getAvailableAlgoStoredHash() + Hasher.getAvailableAlgoStoredPasswordHash() .stream() .filter(alg -> alg.startsWith("pbkdf2") == false) .collect(Collectors.toList()) @@ -626,7 +629,7 @@ public void testValidateForFipsMultipleValidationErrors() { .put( XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), randomFrom( - Hasher.getAvailableAlgoStoredHash() + Hasher.getAvailableAlgoStoredPasswordHash() .stream() .filter(alg -> alg.startsWith("pbkdf2") == false) .collect(Collectors.toList()) @@ -646,19 +649,28 @@ public void testValidateForFipsNoErrorsOrLogs() throws IllegalAccessException { .put( XPackSettings.PASSWORD_HASHING_ALGORITHM.getKey(), randomFrom( - Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList()) + Hasher.getAvailableAlgoStoredPasswordHash() + .stream() + .filter(alg -> alg.startsWith("pbkdf2")) + .collect(Collectors.toList()) ) ) .put( XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM.getKey(), randomFrom( - Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList()) + Hasher.getAvailableAlgoStoredPasswordHash() + .stream() + .filter(alg -> alg.startsWith("pbkdf2")) + .collect(Collectors.toList()) ) ) .put( - ApiKeyService.PASSWORD_HASHING_ALGORITHM.getKey(), + ApiKeyService.STORED_HASH_ALGO_SETTING.getKey(), randomFrom( - Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2")).collect(Collectors.toList()) + Hasher.getAvailableAlgoStoredPasswordHash() + .stream() + .filter(alg -> alg.startsWith("pbkdf2")) + .collect(Collectors.toList()) ) ) .put( @@ -683,13 +695,37 @@ public void testValidateForFipsNonFipsCompliantCacheHashAlgoWarningLog() throws assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantCacheHash(key)); } - public void testValidateForFipsNonFipsCompliantStoredHashAlgoWarningLog() throws IllegalAccessException { - String key = randomFrom(ApiKeyService.PASSWORD_HASHING_ALGORITHM, XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM).getKey(); + public void testValidateForFipsNonFipsCompliantStoredHashAlgoWarningLog() { + String key = XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM.getKey(); final Settings settings = Settings.builder() .put(XPackSettings.FIPS_MODE_ENABLED.getKey(), true) - .put(key, randomNonFipsCompliantStoredHash()) + .put(key, randomNonFipsCompliantStoredPasswordHash()) .build(); - assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantStoredHash(key)); + assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantStoredPasswordHash(key)); + } + + public void testValidateForFipsNonFipsCompliantApiKeyStoredHashAlgoWarningLog() { + var nonCompliant = randomFrom( + Hasher.getAvailableAlgoStoredPasswordHash() + .stream() + .filter(alg -> alg.startsWith("pbkdf2") == false && alg.startsWith("ssha256") == false) + .collect(Collectors.toList()) + ); + String key = ApiKeyService.STORED_HASH_ALGO_SETTING.getKey(); + final Settings settings = Settings.builder().put(XPackSettings.FIPS_MODE_ENABLED.getKey(), true).put(key, nonCompliant).build(); + assertThatLogger(() -> Security.validateForFips(settings), Security.class, logEventForNonCompliantStoredApiKeyHash(key)); + } + + public void testValidateForFipsFipsCompliantApiKeyStoredHashAlgoWarningLog() { + var compliant = randomFrom( + Hasher.getAvailableAlgoStoredPasswordHash() + .stream() + .filter(alg -> alg.startsWith("pbkdf2") || alg.startsWith("ssha256")) + .collect(Collectors.toList()) + ); + String key = ApiKeyService.STORED_HASH_ALGO_SETTING.getKey(); + final Settings settings = Settings.builder().put(XPackSettings.FIPS_MODE_ENABLED.getKey(), true).put(key, compliant).build(); + assertThatLogger(() -> Security.validateForFips(settings), Security.class); } public void testValidateForMultipleNonFipsCompliantCacheHashAlgoWarningLogs() throws IllegalAccessException { @@ -1135,9 +1171,12 @@ private String randomNonFipsCompliantCacheHash() { ); } - private String randomNonFipsCompliantStoredHash() { + private String randomNonFipsCompliantStoredPasswordHash() { return randomFrom( - Hasher.getAvailableAlgoStoredHash().stream().filter(alg -> alg.startsWith("pbkdf2") == false).collect(Collectors.toList()) + Hasher.getAvailableAlgoStoredPasswordHash() + .stream() + .filter(alg -> alg.startsWith("pbkdf2") == false) + .collect(Collectors.toList()) ); } @@ -1153,7 +1192,19 @@ private MockLog.SeenEventExpectation logEventForNonCompliantCacheHash(String set ); } - private MockLog.SeenEventExpectation logEventForNonCompliantStoredHash(String settingKey) { + private MockLog.SeenEventExpectation logEventForNonCompliantStoredApiKeyHash(String settingKey) { + return new MockLog.SeenEventExpectation( + "cache hash not fips compliant", + Security.class.getName(), + Level.WARN, + "[*] is not recommended for stored API key hashing in a FIPS 140 JVM. " + + "The recommended hasher for [" + + settingKey + + "] is SSHA256." + ); + } + + private MockLog.SeenEventExpectation logEventForNonCompliantStoredPasswordHash(String settingKey) { return new MockLog.SeenEventExpectation( "stored hash not fips compliant", Security.class.getName(), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilderTests.java index df5cebdf735ac..af2a5c11e6e73 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/ChangePasswordRequestBuilderTests.java @@ -76,7 +76,7 @@ public void testWithHashedPasswordWithDifferentAlgo() throws IOException { } public void testWithHashedPasswordNotHash() { - final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredHash()).toUpperCase(Locale.ROOT)); + final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredPasswordHash()).toUpperCase(Locale.ROOT)); final char[] hash = randomAlphaOfLength(20).toCharArray(); final String json = Strings.format(""" { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestBuilderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestBuilderTests.java index cb30c8f117f22..018ffa7b09651 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestBuilderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/user/PutUserRequestBuilderTests.java @@ -205,7 +205,7 @@ public void testWithDifferentPasswordHashingAlgorithm() throws IOException { } public void testWithPasswordHashThatsNotReallyAHash() throws IOException { - final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredHash()).toUpperCase(Locale.ROOT)); + final Hasher systemHasher = Hasher.valueOf(randomFrom(Hasher.getAvailableAlgoStoredPasswordHash()).toUpperCase(Locale.ROOT)); final char[] hash = randomAlphaOfLengthBetween(14, 20).toCharArray(); final String json = Strings.format(""" {