From 17d8237e1cc8a2ee39e933e85632657b297e28cb Mon Sep 17 00:00:00 2001 From: Sahith Kurapati Date: Mon, 20 Jan 2025 20:39:44 +0000 Subject: [PATCH] [PLAT-16211] [PLAT-16218] [PLAT-16222] [PLAT-16215] Add support for creating CipherTrust KMS config Summary: This diff addresses the following tickets: 1. [PLAT-16211] Added support for creating a CipherTrust KMS config. 2. [PLAT-16218] Add support for refresh token auth flow for CipherTrust KMS. 3. [PLAT-16222] Add validation for CipherTrust KMS. 4. [PLAT-16215] Hide the CipherTrust KMS feature behind a runtime config. Runtime config: `yb.kms.allow_ciphertrust`. Sample request: ``` curl --location 'http://localhost:9000/api/v1/customers//kms_configs/CIPHERTRUST' \ --header 'Content-Type: application/json' \ --header 'X-AUTH-YW-API-TOKEN: ' \ --data '{ "name": "ct1", "CIPHERTRUST_MANAGER_URL": "", "REFRESH_TOKEN": "", "KEY_ALGORITHM": "AES", "KEY_SIZE": 256, "KEY_NAME": "testkey4" }' ``` Case 1: If a key with the name `testkey4` already exists on the ciphertrust manager, we just validate it and reuse the key in the KMS config. We do a test encrypt and decrypt for a small random text. Case 2: If a key with the given name doesn't already exist on the ciphertrust manager, we will create it on the ciphertrust manager and use that in the KMS config. For both the above cases, we will update the local auth config object with the correct details as on the ciphertrust manager. Test Plan: Manually tested multiple flows: 1. Create KMS config without pre-existing key on CT manager console. 2. Create KMS config with pre-existing key on CT manager console. 3. Create a KMS config with pre-existing key on CT manager console, but with different key algorithm and key size in the API. (Ensured the correct details are updated locally) Will add UTs in a separate ticket. Run UTs. Run itests. Reviewers: vkumar Reviewed By: vkumar Subscribers: yugaware Differential Revision: https://phorge.dev.yugabyte.com/D41358 --- managed/src/main/java/MainModule.java | 2 + .../yw/common/config/GlobalConfKeys.java | 8 + .../kms/algorithms/CipherTrustAlgorithm.java | 29 ++ .../kms/services/CiphertrustEARService.java | 118 ++++++++ .../kms/util/CiphertrustEARServiceUtil.java | 240 +++++++++++++++ .../kms/util/CiphertrustManagerClient.java | 284 ++++++++++++++++++ .../yw/common/kms/util/KeyProvider.java | 6 +- .../EncryptionAtRestController.java | 41 +++ .../yw/models/helpers/CommonUtils.java | 9 +- managed/src/main/resources/reference.conf | 1 + .../kms/services/AzuEARServiceTest.java | 2 +- 11 files changed, 736 insertions(+), 4 deletions(-) create mode 100644 managed/src/main/java/com/yugabyte/yw/common/kms/algorithms/CipherTrustAlgorithm.java create mode 100644 managed/src/main/java/com/yugabyte/yw/common/kms/services/CiphertrustEARService.java create mode 100644 managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustEARServiceUtil.java create mode 100644 managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustManagerClient.java diff --git a/managed/src/main/java/MainModule.java b/managed/src/main/java/MainModule.java index 6cd837413fb..7487b3678ca 100644 --- a/managed/src/main/java/MainModule.java +++ b/managed/src/main/java/MainModule.java @@ -75,6 +75,7 @@ import com.yugabyte.yw.common.ha.PlatformReplicationManager; import com.yugabyte.yw.common.inject.StaticInjectorHolder; import com.yugabyte.yw.common.kms.EncryptionAtRestManager; +import com.yugabyte.yw.common.kms.util.CiphertrustEARServiceUtil; import com.yugabyte.yw.common.kms.util.EncryptionAtRestUniverseKeyCache; import com.yugabyte.yw.common.kms.util.GcpEARServiceUtil; import com.yugabyte.yw.common.metrics.PlatformMetricsProcessor; @@ -227,6 +228,7 @@ public void configure() { bind(PlatformScheduler.class).asEagerSingleton(); bind(AccessKeyRotationUtil.class).asEagerSingleton(); bind(GcpEARServiceUtil.class).asEagerSingleton(); + bind(CiphertrustEARServiceUtil.class).asEagerSingleton(); bind(YbcUpgrade.class).asEagerSingleton(); bind(XClusterScheduler.class).asEagerSingleton(); bind(PerfAdvisorScheduler.class).asEagerSingleton(); diff --git a/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java b/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java index 66e2f2f6323..8ae7306fd1d 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java +++ b/managed/src/main/java/com/yugabyte/yw/common/config/GlobalConfKeys.java @@ -469,6 +469,14 @@ public class GlobalConfKeys extends RuntimeConfigKeysModule { "Default refresh interval for the KMS providers.", ConfDataType.DurationType, ImmutableList.of(ConfKeyTags.PUBLIC)); + public static final ConfKeyInfo kmsAllowCiphertrust = + new ConfKeyInfo<>( + "yb.kms.allow_ciphertrust", + ScopeType.GLOBAL, + "Allow CipherTrust KMS", + "Allow the usage of CipherTrust KMS.", + ConfDataType.BooleanType, + ImmutableList.of(ConfKeyTags.INTERNAL)); // TODO() Add metadata public static final ConfKeyInfo startMasterOnStopNode = new ConfKeyInfo<>( diff --git a/managed/src/main/java/com/yugabyte/yw/common/kms/algorithms/CipherTrustAlgorithm.java b/managed/src/main/java/com/yugabyte/yw/common/kms/algorithms/CipherTrustAlgorithm.java new file mode 100644 index 00000000000..83cf3b72018 --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/common/kms/algorithms/CipherTrustAlgorithm.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 YugaByte, Inc. and Contributors + * + * Licensed under the Polyform Free Trial License 1.0.0 (the "License"); you + * may not use this file except in compliance with the License. You + * may obtain a copy of the License at + * + * https://github.com/YugaByte/yugabyte-db/blob/master/licenses/ + * POLYFORM-FREE-TRIAL-LICENSE-1.0.0.txt + */ + +package com.yugabyte.yw.common.kms.algorithms; + +import java.util.Arrays; +import java.util.List; + +public enum CipherTrustAlgorithm implements SupportedAlgorithmInterface { + AES(Arrays.asList(128, 192, 256)); + + private final List keySizes; + + public List getKeySizes() { + return this.keySizes; + } + + CipherTrustAlgorithm(List keySizes) { + this.keySizes = keySizes; + } +} diff --git a/managed/src/main/java/com/yugabyte/yw/common/kms/services/CiphertrustEARService.java b/managed/src/main/java/com/yugabyte/yw/common/kms/services/CiphertrustEARService.java new file mode 100644 index 00000000000..c34035e99ee --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/common/kms/services/CiphertrustEARService.java @@ -0,0 +1,118 @@ +package com.yugabyte.yw.common.kms.services; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.yugabyte.yw.common.config.RuntimeConfGetter; +import com.yugabyte.yw.common.kms.algorithms.CipherTrustAlgorithm; +import com.yugabyte.yw.common.kms.util.CiphertrustEARServiceUtil; +import com.yugabyte.yw.common.kms.util.CiphertrustEARServiceUtil.CipherTrustKmsAuthConfigField; +import com.yugabyte.yw.common.kms.util.CiphertrustManagerClient; +import com.yugabyte.yw.common.kms.util.KeyProvider; +import com.yugabyte.yw.forms.EncryptionAtRestConfig; +import com.yugabyte.yw.models.KmsConfig; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class CiphertrustEARService extends EncryptionAtRestService { + public static final int numBytes = 32; + + public CiphertrustEARService(RuntimeConfGetter confGetter) { + super(KeyProvider.CIPHERTRUST); + } + + public CiphertrustEARServiceUtil getCiphertrustEARServiceUtil() { + return new CiphertrustEARServiceUtil(); + } + + @Override + protected CipherTrustAlgorithm[] getSupportedAlgorithms() { + return CipherTrustAlgorithm.values(); + } + + @Override + protected ObjectNode createAuthConfigWithService(UUID configUUID, ObjectNode config) { + CiphertrustEARServiceUtil ciphertrustEARServiceUtil = getCiphertrustEARServiceUtil(); + try { + UUID customerUUID = KmsConfig.getOrBadRequest(configUUID).getCustomerUUID(); + CiphertrustManagerClient ciphertrustManagerClient = + ciphertrustEARServiceUtil.getCiphertrustManagerClient(config); + + // Check if a key with the given name exists on CipherTrust manager + boolean keyExists = ciphertrustEARServiceUtil.checkifKeyExists(config); + if (keyExists) { + log.info( + "Key already exists on CipherTrust manager. Using the existing key '{}'.", + ciphertrustEARServiceUtil.getConfigFieldValue( + config, CipherTrustKmsAuthConfigField.KEY_NAME.fieldName)); + } else { + log.info( + "Key does not exist on CipherTrust manager. Creating a new key '{}'.", + ciphertrustEARServiceUtil.getConfigFieldValue( + config, CipherTrustKmsAuthConfigField.KEY_NAME.fieldName)); + // Create a new key on CipherTrust manager. + String newKeyName = ciphertrustEARServiceUtil.createKey(config); + } + + // Update the authConfig with the correct key details. + ciphertrustEARServiceUtil.updateAuthConfigFromKeyDetails(config); + UpdateAuthConfigProperties(customerUUID, configUUID, config); + log.info( + "Updated authConfig from key details for CipherTrust KMS configUUID '{}'.", configUUID); + + // Test the encryption and decryption of a random key for the KMS config key. + ciphertrustEARServiceUtil.testEncryptAndDecrypt(config); + log.info( + "Successfully tested encryption and decryption of a random key for CipherTrust KMS" + + " configUUID '{}'.", + configUUID.toString()); + + return config; + } catch (Exception e) { + final String errMsg = + String.format( + "Error attempting to create or retrieve Key in CIPHERTRUST KMS with config %s.", + configUUID.toString()); + LOG.error(errMsg, e); + return null; + } + // return config; + } + + @Override + protected byte[] createKeyWithService( + UUID universeUUID, UUID configUUID, EncryptionAtRestConfig config) { + return null; + } + + @Override + protected byte[] rotateKeyWithService( + UUID universeUUID, UUID configUUID, EncryptionAtRestConfig config) { + return null; + } + + @Override + public byte[] retrieveKeyWithService(UUID configUUID, byte[] keyRef) { + return null; + } + + @Override + public byte[] encryptKeyWithService(UUID configUUID, byte[] universeKey) { + return null; + } + + @Override + protected byte[] validateRetrieveKeyWithService( + UUID configUUID, byte[] keyRef, ObjectNode authConfig) { + return null; + } + + @Override + public void refreshKmsWithService(UUID configUUID, ObjectNode authConfig) { + // TODO: Implement this method. + } + + @Override + public ObjectNode getKeyMetadata(UUID configUUID) { + return null; + } +} diff --git a/managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustEARServiceUtil.java b/managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustEARServiceUtil.java new file mode 100644 index 00000000000..1273cddccf7 --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustEARServiceUtil.java @@ -0,0 +1,240 @@ +package com.yugabyte.yw.common.kms.util; + +import static play.mvc.Http.Status.BAD_REQUEST; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.yugabyte.yw.common.PlatformServiceException; +import com.yugabyte.yw.common.certmgmt.castore.CustomCAStoreManager; +import com.yugabyte.yw.common.inject.StaticInjectorHolder; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +@Slf4j +public class CiphertrustEARServiceUtil { + + // All fields in Azure KMS authConfig object sent from UI + public enum CipherTrustKmsAuthConfigField { + CIPHERTRUST_MANAGER_URL("CIPHERTRUST_MANAGER_URL", false, true), + REFRESH_TOKEN("REFRESH_TOKEN", true, false), + KEY_NAME("KEY_NAME", false, true), + KEY_ALGORITHM("KEY_ALGORITHM", false, true), + KEY_SIZE("KEY_SIZE", false, true); + + public final String fieldName; + public final boolean isEditable; + public final boolean isMetadata; + + CipherTrustKmsAuthConfigField(String fieldName, boolean isEditable, boolean isMetadata) { + this.fieldName = fieldName; + this.isEditable = isEditable; + this.isMetadata = isMetadata; + } + + public static List getEditableFields() { + return Arrays.asList(values()).stream() + .filter(configField -> configField.isEditable) + .map(configField -> configField.fieldName) + .collect(Collectors.toList()); + } + + public static List getNonEditableFields() { + return Arrays.asList(values()).stream() + .filter(configField -> !configField.isEditable) + .map(configField -> configField.fieldName) + .collect(Collectors.toList()); + } + + public static List getMetadataFields() { + return Arrays.asList(values()).stream() + .filter(configField -> configField.isMetadata) + .map(configField -> configField.fieldName) + .collect(Collectors.toList()); + } + } + + public CustomCAStoreManager getCustomCAStoreManager() { + return StaticInjectorHolder.injector().instanceOf(CustomCAStoreManager.class); + } + + public CiphertrustManagerClient getCiphertrustManagerClient(ObjectNode authConfig) { + CustomCAStoreManager customCAStoreManager = getCustomCAStoreManager(); + + // Get the base URL for the Ciphertrust Manager from auth config. + String baseUrl = + authConfig.path(CipherTrustKmsAuthConfigField.CIPHERTRUST_MANAGER_URL.fieldName).asText(); + // Get the refresh token for the Ciphertrust Manager from auth config. + String refreshToken = + authConfig.path(CipherTrustKmsAuthConfigField.REFRESH_TOKEN.fieldName).asText(); + // Get the key name for the Ciphertrust Manager from auth config. + String keyName = authConfig.path(CipherTrustKmsAuthConfigField.KEY_NAME.fieldName).asText(); + + // Get the key algorithm for the Ciphertrust Manager from auth config. + String keyAlgorithm = + authConfig + .path(CipherTrustKmsAuthConfigField.KEY_ALGORITHM.fieldName) + .asText() + .toLowerCase(); + + // Get the key size for the Ciphertrust Manager from auth config. + int keySize = authConfig.path(CipherTrustKmsAuthConfigField.KEY_SIZE.fieldName).asInt(); + + return new CiphertrustManagerClient( + baseUrl, + refreshToken, + customCAStoreManager.getYbaAndJavaKeyStore(), + keyName, + keyAlgorithm, + keySize); + } + + public boolean checkifKeyExists(ObjectNode authConfig) { + CiphertrustManagerClient ciphertrustManagerClient = getCiphertrustManagerClient(authConfig); + Map keyDetails = ciphertrustManagerClient.getKeyDetails(); + if (keyDetails == null || keyDetails.isEmpty()) { + // Key does not exist + return false; + } else { + return true; + } + } + + public String createKey(ObjectNode authConfig) { + CiphertrustManagerClient ciphertrustManagerClient = getCiphertrustManagerClient(authConfig); + String keyName = ciphertrustManagerClient.createKey(); + if (keyName == null || keyName.isEmpty()) { + log.error("Error creating key with CIPHERTRUST KMS."); + return null; + } else { + return keyName; + } + } + + public byte[] generateRandomBytes(int numBytes) { + byte[] randomBytes = new byte[numBytes]; + try { + SecureRandom.getInstanceStrong().nextBytes(randomBytes); + } catch (NoSuchAlgorithmException e) { + log.warn("Could not generate CIPHERTRUST random bytes, no such algorithm."); + return null; + } + return randomBytes; + } + + public Map encryptKey(ObjectNode authConfig, byte[] keyBytes) { + CiphertrustManagerClient ciphertrustManagerClient = getCiphertrustManagerClient(authConfig); + // Recheck the conversion from bytes -> string once again. + Map encryptResponse = + ciphertrustManagerClient.encryptText(Base64.getEncoder().encodeToString(keyBytes)); + if (encryptResponse == null || encryptResponse.isEmpty()) { + log.error("Error encrypting key with CIPHERTRUST KMS."); + return null; + } + return encryptResponse; + } + + public byte[] decryptKey(ObjectNode authConfig, Map encryptedKeyMaterial) { + CiphertrustManagerClient ciphertrustManagerClient = getCiphertrustManagerClient(authConfig); + // Recheck the conversion from bytes -> string once again. + String decryptResponse = ciphertrustManagerClient.decryptText(encryptedKeyMaterial); + if (decryptResponse == null || decryptResponse.isEmpty()) { + log.error("Error decrypting key with CIPHERTRUST KMS."); + return null; + } + return Base64.getDecoder().decode(decryptResponse); + } + + public String getConfigFieldValue(ObjectNode authConfig, String fieldName) { + String fieldValue = ""; + if (authConfig.has(fieldName)) { + fieldValue = authConfig.path(fieldName).asText(); + } else { + log.warn( + String.format( + "Could not get '%s' from CipherTrust authConfig. '%s' not found.", + fieldName, fieldName)); + return null; + } + return fieldValue; + } + + public void updateAuthConfigFromKeyDetails(ObjectNode authConfig) { + CiphertrustManagerClient ciphertrustManagerClient = getCiphertrustManagerClient(authConfig); + Map keyDetails = ciphertrustManagerClient.getKeyDetails(); + if (keyDetails == null || keyDetails.isEmpty()) { + log.error("Error getting key details from CIPHERTRUST KMS."); + return; + } + + // Get the key algorithm and size from the auth config and the actual key details. + String authConfigKeyAlgorithm = + keyDetails + .getOrDefault(CipherTrustKmsAuthConfigField.KEY_ALGORITHM.fieldName, "") + .toString(); + int authConfigKeySize = + Integer.parseInt( + keyDetails + .getOrDefault(CipherTrustKmsAuthConfigField.KEY_SIZE.fieldName, 0) + .toString()); + + String actualKeyAlgorithm = keyDetails.getOrDefault("algorithm", "").toString(); + int actualKeySize = Integer.parseInt(keyDetails.getOrDefault("size", 0).toString()); + + // Update the auth config with the actual key algorithm and size if any mismatch. + if (!actualKeyAlgorithm.isEmpty() && !actualKeyAlgorithm.equals(authConfigKeyAlgorithm)) { + authConfig.put(CipherTrustKmsAuthConfigField.KEY_ALGORITHM.fieldName, actualKeyAlgorithm); + log.warn( + "Key algorithm mismatch between auth config and actual key in CIPHERTRUST KMS. Changed" + + " key algorithm from {} to {} in the KMS config.", + authConfigKeyAlgorithm, + actualKeyAlgorithm); + } + if (actualKeySize != 0 && actualKeySize != authConfigKeySize) { + authConfig.put(CipherTrustKmsAuthConfigField.KEY_SIZE.fieldName, actualKeySize); + log.warn( + "Key size mismatch between auth config and actual key in CIPHERTRUST KMS. Changed key" + + " size from {} to {} in the KMS config.", + authConfigKeySize, + actualKeySize); + } + } + + public void testEncryptAndDecrypt(ObjectNode authConfig) { + byte[] randomTestKey = generateRandomBytes(32); + Map encryptedKeyMaterial = encryptKey(authConfig, randomTestKey); + byte[] decryptedKeyBytes = decryptKey(authConfig, encryptedKeyMaterial); + if (!Arrays.equals(randomTestKey, decryptedKeyBytes)) { + String errMsg = "Encrypt and decrypt operations gave different outputs in CipherTrust KMS."; + log.error(errMsg); + throw new PlatformServiceException(BAD_REQUEST, errMsg); + } + } + + public void validateKMSProviderConfigFormData(ObjectNode formData) throws Exception { + // Required fields. + List fieldsList = + Arrays.asList( + CipherTrustKmsAuthConfigField.CIPHERTRUST_MANAGER_URL.fieldName, + CipherTrustKmsAuthConfigField.REFRESH_TOKEN.fieldName, + CipherTrustKmsAuthConfigField.KEY_NAME.fieldName, + CipherTrustKmsAuthConfigField.KEY_ALGORITHM.fieldName, + CipherTrustKmsAuthConfigField.KEY_SIZE.fieldName); + List fieldsNotPresent = + fieldsList.stream() + .filter( + field -> + !formData.has(field) || StringUtils.isBlank(formData.path(field).toString())) + .collect(Collectors.toList()); + if (!fieldsNotPresent.isEmpty()) { + throw new PlatformServiceException( + BAD_REQUEST, + "CipherTrust KMS missing the required fields: " + fieldsNotPresent.toString()); + } + } +} diff --git a/managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustManagerClient.java b/managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustManagerClient.java new file mode 100644 index 00000000000..afe9ce88484 --- /dev/null +++ b/managed/src/main/java/com/yugabyte/yw/common/kms/util/CiphertrustManagerClient.java @@ -0,0 +1,284 @@ +package com.yugabyte.yw.common.kms.util; + +import static play.mvc.Http.Status.INTERNAL_SERVER_ERROR; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import com.yugabyte.yw.common.PlatformServiceException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +@Slf4j +public class CiphertrustManagerClient { + public static final String TOKEN_ENDPOINT = "/api/v1/auth/tokens/"; + public static final String KEYS_ENDPOINT = "/api/v1/vault/keys2"; + public static final String ENCRYPT_ENDPOINT = "/api/v1/crypto/encrypt"; + public static final String DECRYPT_ENDPOINT = "/api/v1/crypto/decrypt"; + public static final Map keyLabelsToAdd = + ImmutableMap.of("created_by_yugabyte", "true"); + + public String baseUrl; + public String refreshToken; + public KeyStore ybaAndJavaKeyStore; + public String keyName; + public String jwt; + public String keyAlgorithm; + public int keySize; + + public CiphertrustManagerClient( + String baseUrl, + String refreshToken, + KeyStore ybaAndJavaKeyStore, + String keyName, + String keyAlgorithm, + int keySize) { + this.baseUrl = baseUrl; + this.refreshToken = refreshToken; + this.ybaAndJavaKeyStore = ybaAndJavaKeyStore; + this.keyName = keyName; + this.jwt = getJwt(); + this.keyAlgorithm = keyAlgorithm; + this.keySize = keySize; + } + + public String constructUrl(String endpoint) { + return StringUtils.stripEnd(baseUrl, "/") + endpoint; + } + + public String formatHttpResponse(HttpResponse response) { + return String.format( + "URI: %s%nStatus Code: %d%nResponse Body:%n%s", + response.uri(), response.statusCode(), response.body()); + } + + public SSLContext getSslContext() throws Exception { + TrustManagerFactory trustFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustFactory.init(ybaAndJavaKeyStore); + TrustManager[] ybaJavaTrustManagers = trustFactory.getTrustManagers(); + SecureRandom secureRandom = new SecureRandom(); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, ybaJavaTrustManagers, secureRandom); + return sslContext; + } + + public Map sendGetHttpRequestAndGetBody(String apiUrl) { + Map responseBody = new HashMap<>(); + try { + // Convert the map to a JSON string + ObjectMapper objectMapper = new ObjectMapper(); + + // Create the HTTP request + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + this.jwt) + .GET() + .build(); + + // Send the request + HttpClient client = HttpClient.newBuilder().sslContext(getSslContext()).build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + // Check the response status code + if (response.statusCode() == 200) { + // Resource found successfully. + // Extract the response body. + responseBody = objectMapper.readValue(response.body(), Map.class); + return responseBody; + } else if (response.statusCode() == 404) { + // Resource not found. + log.error("Could not find the resource. Response: " + formatHttpResponse(response)); + return null; + } else { + log.error( + "CipherTrust GET request was not successful. Response: " + + formatHttpResponse(response)); + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "CipherTrust GET request was not successful."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Map sendPostHttpRequestAndGetBody( + String apiUrl, Map requestBodyMap, boolean withJwt) { + Map responseBody = new HashMap<>(); + try { + // Convert the map to a JSON string + ObjectMapper objectMapper = new ObjectMapper(); + String requestBody = objectMapper.writeValueAsString(requestBodyMap); + + // Create the HTTP request + HttpRequest request; + if (withJwt) { + request = + HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .header("Authorization", "Bearer " + this.jwt) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + } else { + request = + HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + } + + // Send the request + HttpClient client = HttpClient.newBuilder().sslContext(getSslContext()).build(); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + + // Check the response status code + if (response.statusCode() == 200 || response.statusCode() == 201) { + // Extract the response body. + responseBody = objectMapper.readValue(response.body(), Map.class); + return responseBody; + } else { + log.error( + "CipherTrust POST request was not successful. Response: " + + formatHttpResponse(response)); + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "CipherTrust POST request was not successful."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String getJwt() { + // Construct the API URL + String apiUrl = constructUrl(TOKEN_ENDPOINT); + + String jwt = null; + try { + // Prepare the request body + Map requestBodyMap = new HashMap<>(); + requestBodyMap.put("grant_type", "refresh_token"); + requestBodyMap.put("refresh_token", refreshToken); + + // Send the request and get the response body + Map responseBody = + sendPostHttpRequestAndGetBody(apiUrl, requestBodyMap, false); + + if (responseBody.containsKey("jwt")) { + jwt = responseBody.get("jwt").toString(); + return jwt; + } else { + log.error("No JWT present in response body."); + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "No JWT present in response body."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String createKey() { + // Construct the API URL + String apiUrl = constructUrl(KEYS_ENDPOINT); + + try { + // Prepare the request body + Map requestBodyMap = new HashMap<>(); + requestBodyMap.put("name", keyName); + requestBodyMap.put("usageMask", 12); + requestBodyMap.put("algorithm", this.keyAlgorithm); + requestBodyMap.put("size", this.keySize); + requestBodyMap.put("assignSelfAsOwner", true); + requestBodyMap.put("labels", keyLabelsToAdd); + + // Send the request and get the response body + Map responseBody = + sendPostHttpRequestAndGetBody(apiUrl, requestBodyMap, true); + + if (responseBody.containsKey("name")) { + return responseBody.get("name").toString(); + } else { + log.error("Create key response body does not contain key name."); + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "Create key response body does not contain key name."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Map getKeyDetails() { + // Construct the API URL + String apiUrl = constructUrl(KEYS_ENDPOINT) + "/" + keyName; + + try { + // Send the request and get the response body + Map responseBody = sendGetHttpRequestAndGetBody(apiUrl); + + return responseBody; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Map encryptText(String plainText) { + // Construct the API URL + String apiUrl = constructUrl(ENCRYPT_ENDPOINT); + + try { + // Prepare the request body + Map requestBodyMap = new HashMap<>(); + requestBodyMap.put("type", "name"); + requestBodyMap.put("id", this.keyName); + requestBodyMap.put("plaintext", plainText); + + // Send the request and get the response body + Map responseBody = + sendPostHttpRequestAndGetBody(apiUrl, requestBodyMap, true); + + if (!responseBody.isEmpty()) { + return responseBody; + } else { + log.error("Encrypt text response body is empty."); + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "Encrypt text response body is empty."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String decryptText(Map cipherTextMap) { + // Construct the API URL + String apiUrl = constructUrl(DECRYPT_ENDPOINT); + + try { + // Send the request and get the response body + Map responseBody = sendPostHttpRequestAndGetBody(apiUrl, cipherTextMap, true); + + if (responseBody.containsKey("plaintext")) { + return responseBody.get("plaintext").toString(); + } else { + log.error("Decrypt text response body does not contain plaintext."); + throw new PlatformServiceException( + INTERNAL_SERVER_ERROR, "Decrypt text response body does not contain plaintext."); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/managed/src/main/java/com/yugabyte/yw/common/kms/util/KeyProvider.java b/managed/src/main/java/com/yugabyte/yw/common/kms/util/KeyProvider.java index cfb05559552..32a72606e06 100644 --- a/managed/src/main/java/com/yugabyte/yw/common/kms/util/KeyProvider.java +++ b/managed/src/main/java/com/yugabyte/yw/common/kms/util/KeyProvider.java @@ -13,6 +13,7 @@ import com.yugabyte.yw.common.kms.algorithms.SupportedAlgorithmInterface; import com.yugabyte.yw.common.kms.services.AwsEARService; import com.yugabyte.yw.common.kms.services.AzuEARService; +import com.yugabyte.yw.common.kms.services.CiphertrustEARService; import com.yugabyte.yw.common.kms.services.EncryptionAtRestService; import com.yugabyte.yw.common.kms.services.GcpEARService; import com.yugabyte.yw.common.kms.services.HashicorpEARService; @@ -37,7 +38,10 @@ public enum KeyProvider { GCP(GcpEARService.class), @EnumValue("AZU") - AZU(AzuEARService.class); + AZU(AzuEARService.class), + + @EnumValue("CIPHERTRUST") + CIPHERTRUST(CiphertrustEARService.class); private final Class providerService; diff --git a/managed/src/main/java/com/yugabyte/yw/controllers/EncryptionAtRestController.java b/managed/src/main/java/com/yugabyte/yw/controllers/EncryptionAtRestController.java index b0b0f47113b..82a3bf7c4e7 100644 --- a/managed/src/main/java/com/yugabyte/yw/controllers/EncryptionAtRestController.java +++ b/managed/src/main/java/com/yugabyte/yw/controllers/EncryptionAtRestController.java @@ -19,11 +19,15 @@ import com.yugabyte.yw.commissioner.tasks.params.KMSConfigTaskParams; import com.yugabyte.yw.common.PlatformServiceException; import com.yugabyte.yw.common.Util; +import com.yugabyte.yw.common.config.GlobalConfKeys; +import com.yugabyte.yw.common.config.RuntimeConfGetter; import com.yugabyte.yw.common.kms.EncryptionAtRestManager; import com.yugabyte.yw.common.kms.services.SmartKeyEARService; import com.yugabyte.yw.common.kms.util.AwsEARServiceUtil.AwsKmsAuthConfigField; import com.yugabyte.yw.common.kms.util.AzuEARServiceUtil; import com.yugabyte.yw.common.kms.util.AzuEARServiceUtil.AzuKmsAuthConfigField; +import com.yugabyte.yw.common.kms.util.CiphertrustEARServiceUtil; +import com.yugabyte.yw.common.kms.util.CiphertrustEARServiceUtil.CipherTrustKmsAuthConfigField; import com.yugabyte.yw.common.kms.util.EncryptionAtRestUtil; import com.yugabyte.yw.common.kms.util.GcpEARServiceUtil; import com.yugabyte.yw.common.kms.util.GcpEARServiceUtil.GcpKmsAuthConfigField; @@ -101,6 +105,10 @@ public class EncryptionAtRestController extends AuthenticatedController { @Inject AzuEARServiceUtil azuEARServiceUtil; + @Inject CiphertrustEARServiceUtil ciphertrustEARServiceUtil; + + @Inject RuntimeConfGetter confGetter; + private void checkIfKMSConfigExists(UUID customerUUID, ObjectNode formData) { String kmsConfigName = formData.get("name").asText(); if (KmsConfig.listKMSConfigs(customerUUID).stream() @@ -188,6 +196,18 @@ private void validateKMSProviderConfigFormData( throw new PlatformServiceException(BAD_REQUEST, e.toString()); } break; + case CIPHERTRUST: + try { + ciphertrustEARServiceUtil.validateKMSProviderConfigFormData(formData); + LOG.info( + "Finished validating Ciphertrust provider config form data for key '{}'.", + ciphertrustEARServiceUtil.getConfigFieldValue( + formData, CipherTrustKmsAuthConfigField.KEY_NAME.fieldName)); + } catch (Exception e) { + LOG.warn("Could not finish validating Ciphertrust provider config form data."); + throw new PlatformServiceException(BAD_REQUEST, e.toString()); + } + break; default: throw new PlatformServiceException( BAD_REQUEST, "Unrecognized key provider: " + keyProvider); @@ -273,6 +293,9 @@ private void checkEditableFields(ObjectNode formData, KeyProvider keyProvider, U } LOG.info("Verified that all the fields in the AZU edit request are editable"); break; + case CIPHERTRUST: + // TO BE ADDED. + break; default: throw new PlatformServiceException( BAD_REQUEST, "Unrecognized key provider while editing kms config: " + keyProvider); @@ -395,6 +418,19 @@ private ObjectNode addNonEditableFieldsData( return formData; } + public void checkCipherTrustRuntimeFlag(KeyProvider keyProvider) { + if (KeyProvider.CIPHERTRUST.equals(keyProvider)) { + boolean allowCiphertrustKms = confGetter.getGlobalConf(GlobalConfKeys.kmsAllowCiphertrust); + if (!allowCiphertrustKms) { + throw new PlatformServiceException( + BAD_REQUEST, + String.format( + "Ciphertrust KMS is not allowed. Please enable the runtime flag '%s' first.", + GlobalConfKeys.kmsAllowCiphertrust.getKey())); + } + } + } + @ApiOperation(value = "Create a KMS configuration", response = YBPTask.class) @ApiImplicitParams({ @ApiImplicitParam( @@ -417,6 +453,7 @@ public Result createKMSConfig(UUID customerUUID, String keyProvider, Http.Reques customerUUID.toString(), keyProvider)); Customer customer = Customer.getOrBadRequest(customerUUID); try { + checkCipherTrustRuntimeFlag(Enum.valueOf(KeyProvider.class, keyProvider)); TaskType taskType = TaskType.CreateKMSConfig; ObjectNode formData = (ObjectNode) request.body().asJson(); // checks if a already KMS Config exists with the requested name @@ -481,6 +518,7 @@ public Result editKMSConfig(UUID customerUUID, UUID configUUID, Http.Request req + customerUUID; throw new PlatformServiceException(BAD_REQUEST, errMsg); } + checkCipherTrustRuntimeFlag(config.getKeyProvider()); try { TaskType taskType = TaskType.EditKMSConfig; ObjectNode formData = (ObjectNode) request.body().asJson(); @@ -537,6 +575,7 @@ public Result getKMSConfig(UUID customerUUID, UUID configUUID) { LOG.info(String.format("Retrieving KMS configuration %s", configUUID.toString())); Customer.getOrBadRequest(customerUUID); KmsConfig config = KmsConfig.getOrBadRequest(customerUUID, configUUID); + checkCipherTrustRuntimeFlag(config.getKeyProvider()); ObjectNode kmsConfig = keyManager.getServiceInstance(config.getKeyProvider().name()).getAuthConfig(configUUID); if (kmsConfig == null) { @@ -607,6 +646,7 @@ public Result deleteKMSConfig(UUID customerUUID, UUID configUUID, Http.Request r Customer customer = Customer.getOrBadRequest(customerUUID); try { KmsConfig config = KmsConfig.getOrBadRequest(customerUUID, configUUID); + checkCipherTrustRuntimeFlag(config.getKeyProvider()); TaskType taskType = TaskType.DeleteKMSConfig; KMSConfigTaskParams taskParams = new KMSConfigTaskParams(); taskParams.kmsProvider = config.getKeyProvider(); @@ -661,6 +701,7 @@ public Result refreshKMSConfig(UUID customerUUID, UUID configUUID, Request reque customerUUID.toString()); Customer.getOrBadRequest(customerUUID); KmsConfig kmsConfig = KmsConfig.getOrBadRequest(customerUUID, configUUID); + checkCipherTrustRuntimeFlag(kmsConfig.getKeyProvider()); keyManager.getServiceInstance(kmsConfig.getKeyProvider().name()).refreshKms(configUUID); auditService() .createAuditEntryWithReqBody( diff --git a/managed/src/main/java/com/yugabyte/yw/models/helpers/CommonUtils.java b/managed/src/main/java/com/yugabyte/yw/models/helpers/CommonUtils.java index 0ecc963e2b6..e4c32262a34 100644 --- a/managed/src/main/java/com/yugabyte/yw/models/helpers/CommonUtils.java +++ b/managed/src/main/java/com/yugabyte/yw/models/helpers/CommonUtils.java @@ -91,7 +91,8 @@ public class CommonUtils { "POLICY", "HC_VAULT_TOKEN", "VAULTTOKEN", - "SAS_TOKEN"); + "SAS_TOKEN", + "REFRESH_TOKEN"); // Exclude following strings from being sensitive fields private static final List excludedFieldNames = Arrays.asList( @@ -107,7 +108,11 @@ public class CommonUtils { "KEYSPACETABLELIST", // General API field "KEYSPACE", - "APITOKENVERSION"); + "APITOKENVERSION", + // CipherTrust KMS fields + "KEY_ALGORITHM", + "KEY_SIZE", + "KEY_NAME"); public static final Map CHAR_MAP = Map.ofEntries( diff --git a/managed/src/main/resources/reference.conf b/managed/src/main/resources/reference.conf index 3debb4c3d4a..02ea7bb386a 100644 --- a/managed/src/main/resources/reference.conf +++ b/managed/src/main/resources/reference.conf @@ -1333,6 +1333,7 @@ yb { kms { refresh_interval = 12 hours + allow_ciphertrust = false } api { diff --git a/managed/src/test/java/com/yugabyte/yw/common/kms/services/AzuEARServiceTest.java b/managed/src/test/java/com/yugabyte/yw/common/kms/services/AzuEARServiceTest.java index 83e5d491d2b..3ab5ae7ce54 100644 --- a/managed/src/test/java/com/yugabyte/yw/common/kms/services/AzuEARServiceTest.java +++ b/managed/src/test/java/com/yugabyte/yw/common/kms/services/AzuEARServiceTest.java @@ -42,7 +42,7 @@ @RunWith(MockitoJUnitRunner.class) public class AzuEARServiceTest extends FakeDBApplication { - public static final Logger LOG = LoggerFactory.getLogger(GcpEARService.class); + public static final Logger LOG = LoggerFactory.getLogger(AzuEARService.class); // Create fake auth config details public ObjectMapper mapper = new ObjectMapper();