diff --git a/libs/common/src/main/java/org/opensearch/common/crypto/MasterKeyProvider.java b/libs/common/src/main/java/org/opensearch/common/crypto/MasterKeyProvider.java index 31d2dcd0dba3d..47c1e72125ef7 100644 --- a/libs/common/src/main/java/org/opensearch/common/crypto/MasterKeyProvider.java +++ b/libs/common/src/main/java/org/opensearch/common/crypto/MasterKeyProvider.java @@ -10,6 +10,7 @@ import org.opensearch.common.annotation.ExperimentalApi; import java.io.Closeable; +import java.util.List; import java.util.Map; /** @@ -44,4 +45,10 @@ public interface MasterKeyProvider extends Closeable { * @return encryption context associated with this master key. */ Map getEncryptionContext(); + + /** + * Returns grant tokens for KMS operations. + * @return list of grant tokens or empty list if not configured + */ + List getGrantTokens(); } diff --git a/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsClientSettings.java b/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsClientSettings.java index 187a80a6355f7..d90e572a34150 100644 --- a/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsClientSettings.java +++ b/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsClientSettings.java @@ -64,6 +64,9 @@ public class KmsClientSettings { Property.NodeScope ); + /** Source context for grant-based access. */ + static final Setting SOURCE_CONTEXT_SETTING = Setting.simpleString("kms.source_context"); + private static final Logger logger = LogManager.getLogger(KmsClientSettings.class); /** Credentials to authenticate with kms. */ @@ -98,6 +101,9 @@ public class KmsClientSettings { /** The read timeout for the kms client. */ final int readTimeoutMillis; + /** The source context for the kms client. */ + final String sourceContext; + protected KmsClientSettings( AwsCredentials credentials, String endpoint, @@ -116,6 +122,29 @@ protected KmsClientSettings( this.proxyUsername = proxyUsername; this.proxyPassword = proxyPassword; this.readTimeoutMillis = readTimeoutMillis; + this.sourceContext = ""; + } + + protected KmsClientSettings( + AwsCredentials credentials, + String endpoint, + String region, + String proxyHost, + int proxyPort, + String proxyUsername, + String proxyPassword, + int readTimeoutMillis, + String sourceContext + ) { + this.credentials = credentials; + this.endpoint = endpoint; + this.region = region; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.proxyUsername = proxyUsername; + this.proxyPassword = proxyPassword; + this.readTimeoutMillis = readTimeoutMillis; + this.sourceContext = sourceContext != null ? sourceContext : ""; } static AwsCredentials loadCredentials(Settings settings) { @@ -181,7 +210,8 @@ static KmsClientSettings getClientSettings(Settings settings) { PROXY_PORT_SETTING.get(settings), proxyUsername.toString(), proxyPassword.toString(), - (int) READ_TIMEOUT_SETTING.get(settings).millis() + (int) READ_TIMEOUT_SETTING.get(settings).millis(), + SOURCE_CONTEXT_SETTING.get(settings) ); } } @@ -212,6 +242,7 @@ KmsClientSettings getMetadataSettings(Settings settings) { normalizedSettings, TimeValue.timeValueMillis(this.readTimeoutMillis) ); + String newSourceContext = getCryptoMetadataSettingOrExisting(SOURCE_CONTEXT_SETTING, normalizedSettings, this.sourceContext); return new KmsClientSettings( newCredentials, @@ -221,7 +252,8 @@ KmsClientSettings getMetadataSettings(Settings settings) { newProxyPort, newProxyUsername, newProxyPassword, - (int) newReadTimeout.millis() + (int) newReadTimeout.millis(), + newSourceContext ); } @@ -248,11 +280,22 @@ public boolean equals(final Object o) { && Objects.equals(proxyHost, that.proxyHost) && Objects.equals(proxyPort, that.proxyPort) && Objects.equals(proxyUsername, that.proxyUsername) - && Objects.equals(proxyPassword, that.proxyPassword); + && Objects.equals(proxyPassword, that.proxyPassword) + && Objects.equals(sourceContext, that.sourceContext); } @Override public int hashCode() { - return Objects.hash(readTimeoutMillis, credentials, endpoint, region, proxyHost, proxyPort, proxyUsername, proxyPassword); + return Objects.hash( + readTimeoutMillis, + credentials, + endpoint, + region, + proxyHost, + proxyPort, + proxyUsername, + proxyPassword, + sourceContext + ); } } diff --git a/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsMasterKeyProvider.java b/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsMasterKeyProvider.java index 76380a072a271..7f56817a0f666 100644 --- a/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsMasterKeyProvider.java +++ b/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsMasterKeyProvider.java @@ -21,12 +21,15 @@ import org.opensearch.common.crypto.MasterKeyProvider; import org.opensearch.secure_sm.AccessController; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.function.Supplier; public class KmsMasterKeyProvider implements MasterKeyProvider { private final Map encryptionContext; private final String keyArn; + private final List grantTokens; private final Supplier clientReferenceSupplier; private static final Logger logger = LogManager.getLogger(KmsMasterKeyProvider.class); @@ -38,6 +41,19 @@ public KmsMasterKeyProvider( ) { this.encryptionContext = encryptionContext; this.keyArn = keyArn; + this.grantTokens = Collections.emptyList(); + this.clientReferenceSupplier = clientReferenceSupplier; + } + + public KmsMasterKeyProvider( + Map encryptionContext, + String keyArn, + List grantTokens, + Supplier clientReferenceSupplier + ) { + this.encryptionContext = encryptionContext; + this.keyArn = keyArn; + this.grantTokens = grantTokens != null ? grantTokens : Collections.emptyList(); this.clientReferenceSupplier = clientReferenceSupplier; } @@ -45,14 +61,20 @@ public KmsMasterKeyProvider( public DataKeyPair generateDataPair() { logger.info("Generating new data key pair"); try (AmazonKmsClientReference clientReference = clientReferenceSupplier.get()) { - GenerateDataKeyRequest request = GenerateDataKeyRequest.builder() + GenerateDataKeyRequest.Builder requestBuilder = GenerateDataKeyRequest.builder() .encryptionContext(encryptionContext) // Currently only 32 byte data key is supported. To add support for other key sizes add key providers // in org.opensearch.encryption.CryptoManagerFactory.createCryptoProvider. .keySpec(DataKeySpec.AES_256) - .keyId(keyArn) - .build(); - GenerateDataKeyResponse dataKeyPair = AccessController.doPrivileged(() -> clientReference.get().generateDataKey(request)); + .keyId(keyArn); + + if (grantTokens != null && !grantTokens.isEmpty()) { + requestBuilder.grantTokens(grantTokens); + } + + GenerateDataKeyResponse dataKeyPair = AccessController.doPrivileged( + () -> clientReference.get().generateDataKey(requestBuilder.build()) + ); return new DataKeyPair(dataKeyPair.plaintext().asByteArray(), dataKeyPair.ciphertextBlob().asByteArray()); } } @@ -60,11 +82,15 @@ public DataKeyPair generateDataPair() { @Override public byte[] decryptKey(byte[] encryptedKey) { try (AmazonKmsClientReference clientReference = clientReferenceSupplier.get()) { - DecryptRequest decryptRequest = DecryptRequest.builder() + DecryptRequest.Builder requestBuilder = DecryptRequest.builder() .ciphertextBlob(SdkBytes.fromByteArray(encryptedKey)) - .encryptionContext(encryptionContext) - .build(); - DecryptResponse decryptResponse = AccessController.doPrivileged(() -> clientReference.get().decrypt(decryptRequest)); + .encryptionContext(encryptionContext); + + if (grantTokens != null && !grantTokens.isEmpty()) { + requestBuilder.grantTokens(grantTokens); + } + + DecryptResponse decryptResponse = AccessController.doPrivileged(() -> clientReference.get().decrypt(requestBuilder.build())); return decryptResponse.plaintext().asByteArray(); } } @@ -81,4 +107,9 @@ public Map getEncryptionContext() { @Override public void close() {} + + @Override + public List getGrantTokens() { + return grantTokens; + } } diff --git a/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsService.java b/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsService.java index b4f108857ffc0..89d117af72ffb 100644 --- a/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsService.java +++ b/plugins/crypto-kms/src/main/java/org/opensearch/crypto/kms/KmsService.java @@ -12,7 +12,11 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.core.interceptor.Context; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.apache.ApacheHttpClient; import software.amazon.awssdk.http.apache.ProxyConfiguration; import software.amazon.awssdk.profiles.ProfileFileSystemSetting; @@ -36,7 +40,9 @@ import java.net.URISyntaxException; import java.time.Duration; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -54,6 +60,8 @@ public class KmsService implements Closeable { static final Setting KEY_ARN_SETTING = Setting.simpleString("kms.key_arn", Setting.Property.NodeScope); + static final Setting GRANT_TOKENS_SETTING = Setting.simpleString("kms.grant_tokens", Setting.Property.NodeScope); + private volatile Map clientsCache = emptyMap(); /** @@ -73,7 +81,7 @@ public KmsService() { private KmsClient buildClient(KmsClientSettings clientSettings) { AccessController.doPrivileged(KmsService::setDefaultAwsProfilePath); final AwsCredentialsProvider awsCredentialsProvider = buildCredentials(clientSettings); - final ClientOverrideConfiguration overrideConfiguration = buildOverrideConfiguration(); + final ClientOverrideConfiguration overrideConfiguration = buildOverrideConfiguration(clientSettings); final ProxyConfiguration proxyConfiguration = AccessController.doPrivileged(() -> buildProxyConfiguration(clientSettings)); return buildClient( awsCredentialsProvider, @@ -133,6 +141,24 @@ ProxyConfiguration buildProxyConfiguration(KmsClientSettings clientSettings) { } } + ClientOverrideConfiguration buildOverrideConfiguration(KmsClientSettings clientSettings) { + ClientOverrideConfiguration.Builder builder = ClientOverrideConfiguration.builder().retryPolicy(buildRetryPolicy()); + + if (Strings.hasText(clientSettings.sourceContext)) { + Map sourceHeaders = parseKeyValuePairs(clientSettings.sourceContext, "source_context"); + builder.addExecutionInterceptor(new ExecutionInterceptor() { + @Override + public SdkHttpRequest modifyHttpRequest(Context.ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + SdkHttpRequest.Builder requestBuilder = context.httpRequest().toBuilder(); + sourceHeaders.forEach((key, value) -> { requestBuilder.putHeader(key, value); }); + return requestBuilder.build(); + } + }); + } + + return builder.build(); + } + ClientOverrideConfiguration buildOverrideConfiguration() { return ClientOverrideConfiguration.builder().retryPolicy(buildRetryPolicy()).build(); } @@ -252,22 +278,28 @@ public MasterKeyProvider createMasterKeyProvider(CryptoMetadata cryptoMetadata) String kmsEncCtx = ENC_CTX_SETTING.get(cryptoSettings); Map encCtx; if (Strings.hasText(kmsEncCtx)) { - try { - encCtx = Arrays.stream(kmsEncCtx.split(",")) - .map(s -> s.split("=")) - .collect(Collectors.toMap(e -> e[0].trim(), e -> e[1].trim())); - } catch (Exception ex) { - throw new IllegalArgumentException(ENC_CTX_SETTING.getKey() + " Format should be: Name1=Value1, Name2=Value2"); - } + encCtx = parseKeyValuePairs(kmsEncCtx, ENC_CTX_SETTING.getKey()); } else { encCtx = new HashMap<>(); } + // Extract grant details + String grantTokensStr = GRANT_TOKENS_SETTING.get(cryptoSettings); + List grantTokens = Strings.hasText(grantTokensStr) ? Arrays.asList(grantTokensStr.split(",")) : Collections.emptyList(); + // Verify client creation is successful to early detect any failure. try (AmazonKmsClientReference clientReference = client(cryptoMetadata)) { clientReference.get(); } - return new KmsMasterKeyProvider(encCtx, keyArn, () -> client(cryptoMetadata)); + return new KmsMasterKeyProvider(encCtx, keyArn, grantTokens, () -> client(cryptoMetadata)); + } + + private static Map parseKeyValuePairs(String input, String settingName) { + try { + return Arrays.stream(input.split(",")).map(s -> s.split("=")).collect(Collectors.toMap(e -> e[0].trim(), e -> e[1].trim())); + } catch (Exception ex) { + throw new IllegalArgumentException(settingName + " Format should be: Name1=Value1, Name2=Value2"); + } } }