Repository: confluentinc/schema-registry
Pull request #3656: DGS-20245 Add minimal validation for certain KMS key IDs
Author: login: rayokota
Created: 2025-04-07T23:46:18Z | Merged: 2025-04-08T15:44:24Z

Add minimal validation for certain KMS key IDs when creating KEKs in the DEK Registry.

KMS key ids are ultimately validated when used by the KMS client. However, for the KMS types that we support, add some minimal validation (such as ensuring an ARN has a correct prefix).

<!--
Is there any breaking changes?  If so this is a major release, make sure '#major' is in at least one
commit message to get CI to bump the major.  This will prevent automatic down stream dependency
bumping / consuming.  For more information about semantic versioning see: https://semver.org/


Suggested PR template: Fill/delete/add sections as needed. Optionally delete any commented block.
-->
What
----
<!--
Briefly describe **what** you have changed and **why**.
Optionally include implementation strategy.
-->

Checklist
------------------
- [x] Contains customer facing changes? Including API/behavior changes <!-- This can help identify if it has introduced any breaking changes -->
- [ ] Is this change gated behind feature flag(s)?
    - List the LD flags needed to be set to enable this change
- [x] Did you add sufficient unit test and/or integration test coverage for this PR?
    - If not, please explain why it is not required
- [ ] Does this change require modifying existing system tests or adding new system tests? <!-- Primarily for changes that could impact CCloud integrations -->
    - If so, include tracking information for the system test changes

References
----------
JIRA:
<!--
Copy&paste links: to Jira ticket, other PRs, issues, Slack conversations...
For code bumps: link to PR, tag or GitHub `/compare/master...master`
-->

Test & Review
------------
<!--
Has it been tested? how?
Copy&paste any handy instructions, steps or requirements that can save time to the reviewer or any reader.
-->

Open questions / Follow-ups
--------------------------
<!--
Optional: anything open to discussion for the reviewer, out of scope, or follow ups.
-->

<!--
Review stakeholders
------------------
<!--
Optional: mention stakeholders or if special context that is required to review.
-->



Diff:
diff --git a/dek-registry-client/pom.xml b/dek-registry-client/pom.xml
index 99294850d6d..15ef31972ff 100644
--- a/dek-registry-client/pom.xml
+++ b/dek-registry-client/pom.xml
@@ -65,6 +65,11 @@
             <artifactId>tink</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/dek-registry-client/src/main/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequest.java b/dek-registry-client/src/main/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequest.java
index cff07932278..76c69d1befd 100644
--- a/dek-registry-client/src/main/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequest.java
+++ b/dek-registry-client/src/main/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequest.java
@@ -154,4 +154,48 @@ public String toString() {
   public String toJson() throws IOException {
     return JacksonMapper.INSTANCE.writeValueAsString(this);
   }
+
+  public void validate() {
+    // Perform minimal validation, the actual validation is done by the KMS client
+    switch (kmsType) {
+      case "aws-kms":
+        validateAwsKeyId();
+        break;
+      case "azure-kms":
+        validateAzureKeyId();
+        break;
+      case "gcp-kms":
+        validateGcpKeyId();
+        break;
+      case "hcvault":
+        validateHcVaultKeyId();
+        break;
+      default:
+        break;
+    }
+  }
+
+  private void validateAwsKeyId() {
+    if (!kmsKeyId.startsWith("arn:aws:kms:")) {
+      throw new IllegalStateException("Invalid AWS KMS key ID");
+    }
+  }
+
+  private void validateAzureKeyId() {
+    if (!kmsKeyId.startsWith("https://")) {
+      throw new IllegalStateException("Invalid Azure Vault key ID");
+    }
+  }
+
+  private void validateGcpKeyId() {
+    if (!kmsKeyId.startsWith("projects/")) {
+      throw new IllegalStateException("Invalid GCP KMS key ID");
+    }
+  }
+
+  private void validateHcVaultKeyId() {
+    if (!kmsKeyId.startsWith("https://") && !kmsKeyId.startsWith("http://")) {
+      throw new IllegalStateException("Invalid HashiCorp Vault key ID");
+    }
+  }
 }
diff --git a/dek-registry-client/src/test/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequestTest.java b/dek-registry-client/src/test/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequestTest.java
new file mode 100644
index 00000000000..5e72dea2a42
--- /dev/null
+++ b/dek-registry-client/src/test/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequestTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2025 Confluent Inc.
+ *
+ * Licensed under the Confluent Community License (the "License"); you may not use
+ * this file except in compliance with the License.  You may obtain a copy of the
+ * License at
+ *
+ * http://www.confluent.io/confluent-community-license
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package io.confluent.dekregistry.client.rest.entities;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+
+public class CreateKekRequestTest {
+
+  @Test
+  public void testValidKmsKeyIds() {
+    CreateKekRequest request = new CreateKekRequest();
+    request.setKmsType("aws-kms");
+    request.setKmsKeyId("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012");
+    assertTrue(validate(request));
+
+    request.setKmsType("azure-kms");
+    request.setKmsKeyId("https://myvault.vault.azure.net/keys/mykey/1234567890abcdef");
+    assertTrue(validate(request));
+
+    request.setKmsType("gcp-kms");
+    request.setKmsKeyId("projects/my-project/locations/global/keyRings/my-keyring/cryptoKeys/my-key");
+    assertTrue(validate(request));
+
+    request.setKmsType("hcvault");
+    request.setKmsKeyId("http://localhost:8200/transit/keys/my-key");
+    assertTrue(validate(request));
+  }
+
+  @Test
+  public void testInvalidKmsKeyIds() {
+    CreateKekRequest request = new CreateKekRequest();
+    request.setKmsType("aws-kms");
+    request.setKmsKeyId("bad key id");
+    assertFalse(validate(request));
+
+    request.setKmsType("azure-kms");
+    request.setKmsKeyId("bad key id");
+    assertFalse(validate(request));
+
+    request.setKmsType("gcp-kms");
+    request.setKmsKeyId("bad key id");
+    assertFalse(validate(request));
+
+    request.setKmsType("hcvault");
+    request.setKmsKeyId("bad key id");
+    assertFalse(validate(request));
+  }
+
+  private boolean validate(CreateKekRequest request) {
+    try {
+      request.validate();
+      return true;
+    } catch (Exception e) {
+      return false;
+    }
+  }
+}
+
diff --git a/dek-registry/src/main/java/io/confluent/dekregistry/web/rest/resources/DekRegistryResource.java b/dek-registry/src/main/java/io/confluent/dekregistry/web/rest/resources/DekRegistryResource.java
index 56655104b0f..6b99432f952 100644
--- a/dek-registry/src/main/java/io/confluent/dekregistry/web/rest/resources/DekRegistryResource.java
+++ b/dek-registry/src/main/java/io/confluent/dekregistry/web/rest/resources/DekRegistryResource.java
@@ -316,6 +316,12 @@ public void createKek(
       throw DekRegistryErrors.invalidOrMissingKeyInfo("kmsType");
     }
 
+    try {
+      request.validate();
+    } catch (Exception e) {
+      throw DekRegistryErrors.invalidOrMissingKeyInfo("kmsKeyId");
+    }
+
     Map<String, String> headerProperties = requestHeaderBuilder.buildRequestHeaders(
         headers, getSchemaRegistry().config().whitelistHeaders());
 


Reviews:
user: login: djibodu
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T00:30:01Z
user: login: akhileshm1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T21:57:53Z
user: login: rayokota
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-16T22:01:27Z

Review comments:
diff_hunk: @@ -154,4 +154,48 @@ public String toString() {
   public String toJson() throws IOException {
     return JacksonMapper.INSTANCE.writeValueAsString(this);
   }
+
+  public void validate() {
+    // Perform minimal validation, the actual validation is done by the KMS client
+    switch (kmsType) {
+      case "aws-kms":
+        validateAwsKeyId();
+        break;
+      case "azure-kms":
+        validateAzureKeyId();
+        break;
+      case "gcp-kms":
+        validateGcpKeyId();
+        break;
+      case "hcvault":
+        validateHcVaultKeyId();
+        break;
+      default:
+        break;
+    }
+  }
+
+  private void validateAwsKeyId() {
+    if (!kmsKeyId.startsWith("arn:aws:kms:")) {
path: dek-registry-client/src/main/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequest.java
user: login: akhileshm1
body: @rayokota , can the kmsKeyId be null here?
created_at: 2025-04-16T21:57:53Z
author_association: MEMBER
diff_hunk: @@ -154,4 +154,48 @@ public String toString() {
   public String toJson() throws IOException {
     return JacksonMapper.INSTANCE.writeValueAsString(this);
   }
+
+  public void validate() {
+    // Perform minimal validation, the actual validation is done by the KMS client
+    switch (kmsType) {
+      case "aws-kms":
+        validateAwsKeyId();
+        break;
+      case "azure-kms":
+        validateAzureKeyId();
+        break;
+      case "gcp-kms":
+        validateGcpKeyId();
+        break;
+      case "hcvault":
+        validateHcVaultKeyId();
+        break;
+      default:
+        break;
+    }
+  }
+
+  private void validateAwsKeyId() {
+    if (!kmsKeyId.startsWith("arn:aws:kms:")) {
path: dek-registry-client/src/main/java/io/confluent/dekregistry/client/rest/entities/CreateKekRequest.java
user: login: rayokota
body: No
created_at: 2025-04-16T22:01:27Z
author_association: MEMBER

PR comments:
user: login: confluent-cla-assistant[bot]
created_at: 2025-04-07T23:46:28Z
body: :tada: All Contributor License Agreements have been signed. Ready to merge. <br/><sub>Please push an empty commit if you would like to re-run the checks to verify CLA status for all contributors.<sub>
author_association: NONE
user: login: sonarqube-confluent[bot]
created_at: 2025-04-08T00:40:44Z
body: ![Failed](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 54.90% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 258 Issues
- ![Bug](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/common/bug.svg?sanitize=true) 9 Bugs
- ![Vulnerability](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 249 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/checks/CoverageChart/50.svg?sanitize=true) 54.90% Coverage (68.30% Estimated after merge)
- ![Duplications](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/checks/Duplications/NoDuplicationInfo.svg?sanitize=true) No duplication information (2.30% Estimated after merge)

**Project ID:** schema-registry

[View in SonarQube](https://sonarqube.dp.confluent.io/dashboard?id=schema-registry&pullRequest=3656)


author_association: NONE
user: login: sonarqube-confluent[bot]
created_at: 2025-04-08T00:45:45Z
body: ![Failed](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/checks/QualityGateBadge/failed.svg?sanitize=true)

- 55.10% Coverage on New Code (is less than 80.00%)

# Analysis Details
## 258 Issues
- ![Bug](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/common/bug.svg?sanitize=true) 9 Bugs
- ![Vulnerability](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/common/vulnerability.svg?sanitize=true) 0 Vulnerabilities
- ![Code Smell](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/common/code_smell.svg?sanitize=true) 249 Code Smells

## Coverage and Duplications
- ![Coverage](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/checks/CoverageChart/50.svg?sanitize=true) 55.10% Coverage (68.30% Estimated after merge)
- ![Duplications](https://sonarqube.dp.confluent.io/static/communityBranchPlugin/checks/Duplications/NoDuplicationInfo.svg?sanitize=true) No duplication information (2.30% Estimated after merge)

**Project ID:** schema-registry

[View in SonarQube](https://sonarqube.dp.confluent.io/dashboard?id=schema-registry&pullRequest=3656)


author_association: NONE