This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new d1731cf8e1 [#12299] feat(server-common): CI-enforced tests for
server-side HTTPS and mTLS (#12389)
d1731cf8e1 is described below
commit d1731cf8e1cdd1f2019fdac37fbe02f8a2edd4be
Author: Octavio Herrera Contreras <[email protected]>
AuthorDate: Tue Aug 11 21:01:02 2026 -0700
[#12299] feat(server-common): CI-enforced tests for server-side HTTPS and
mTLS (#12389)
### What changes were proposed in this pull request?
- Added TestHttpsServerAuthentication test file to verify server side
HTTPS across all 7 requested tests.
- These changes introduce 6 new certificate fixtures for https server
TLS/mTLS testing.
- Includes new README.md file for instructions on certificate
regeneration + regen script file.
- Added TestTlsServerUtils to provide a reusable Jetty TLS server setup
for future test modules
- Updated server-common/build.gradle.kts to publish test classes and
fixtures via test artifacts configuration
Fix: #12299
### Does this PR introduce _any_ user-facing change?
No user facing changes
### How was this patch tested?
Ran:
./gradlew rat
Passed
./gradlew :server-common:test --tests
"org.apache.gravitino.server.web.TestHttpsServerAuthentication"
Passed
---
server-common/build.gradle.kts | 13 +
.../server/web/TestHttpsServerAuthentication.java | 283 +++++++++++++++++++++
.../gravitino/server/web/TestTlsServerUtils.java | 72 ++++++
server-common/src/test/resources/tls/README.md | 166 ++++++++++++
server-common/src/test/resources/tls/regenerate.sh | 210 +++++++++++++++
.../test/resources/tls/test-client-truststore.p12 | Bin 0 -> 1350 bytes
.../test/resources/tls/test-server-keystore.p12 | Bin 0 -> 2834 bytes
.../test/resources/tls/test-server-truststore.p12 | Bin 0 -> 1366 bytes
.../resources/tls/test-trusted-client-keystore.p12 | Bin 0 -> 2866 bytes
.../tls/test-untrusted-client-keystore.p12 | Bin 0 -> 2870 bytes
.../tls/test-untrusted-server-truststore.p12 | Bin 0 -> 1398 bytes
11 files changed, 744 insertions(+)
diff --git a/server-common/build.gradle.kts b/server-common/build.gradle.kts
index fdcdf2f931..aa48f71060 100644
--- a/server-common/build.gradle.kts
+++ b/server-common/build.gradle.kts
@@ -72,3 +72,16 @@ tasks {
environment("GRAVITINO_TEST", "true")
}
}
+
+val testJar by tasks.registering(Jar::class) {
+ archiveClassifier.set("tests")
+ from(sourceSets["test"].output)
+}
+
+configurations {
+ create("testArtifacts")
+}
+
+artifacts {
+ add("testArtifacts", testJar)
+}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpsServerAuthentication.java
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpsServerAuthentication.java
new file mode 100644
index 0000000000..35cfbf1b82
--- /dev/null
+++
b/server-common/src/test/java/org/apache/gravitino/server/web/TestHttpsServerAuthentication.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.server.web;
+
+import static
org.apache.gravitino.server.web.TestTlsServerUtils.TEST_STORE_PASSWORD;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.KeyStore;
+import java.time.Duration;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.TrustManagerFactory;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.rest.RESTUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class TestHttpsServerAuthentication {
+
+ private static final String TEST_STORE_TYPE = "PKCS12";
+ private static final char[] TEST_STORE_PASSWORD_CHARS =
TEST_STORE_PASSWORD.toCharArray();
+
+ private static final String TEST_PATH = "/tls-test";
+ private static final String TEST_RESPONSE = "success";
+ private static final String SERVLET_RESPONSE = "success";
+
+ private JettyServer jettyServer;
+
+ @BeforeEach
+ public void setUp() {
+ jettyServer = new JettyServer();
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ if (jettyServer != null) {
+ jettyServer.stop();
+ }
+ }
+
+ @Test
+ public void testClientAuthRejectsMissingTrustStore() throws Exception {
+ Config config = new Config(false) {};
+
+ config.set(JettyServerConfig.ENABLE_HTTPS, true);
+ config.set(JettyServerConfig.WEBSERVER_HTTPS_PORT,
RESTUtils.findAvailablePort(6000, 7000));
+
+ config.set(
+ JettyServerConfig.SSL_KEYSTORE_PATH,
+
TestTlsServerUtils.testResource("test-server-keystore.p12").toString());
+ config.set(JettyServerConfig.SSL_KEYSTORE_PASSWORD, TEST_STORE_PASSWORD);
+ config.set(JettyServerConfig.SSL_MANAGER_PASSWORD, TEST_STORE_PASSWORD);
+ config.set(JettyServerConfig.ENABLE_CLIENT_AUTH, true);
+
+ assertThrows(IllegalArgumentException.class, () ->
JettyServerConfig.fromConfig(config));
+ }
+
+ @Test
+ public void testMutualTlsAcceptsTrustedClientCertificate() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(jettyServer, true,
createTestServlet(), TEST_PATH);
+
+ HttpClient client =
+ createHttpsClient(
+ TestTlsServerUtils.testResource("test-client-truststore.p12"),
+
TestTlsServerUtils.testResource("test-trusted-client-keystore.p12"));
+
+ HttpRequest request = createHttpsRequest(port);
+
+ HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(HttpServletResponse.SC_OK, response.statusCode());
+ assertEquals(TEST_RESPONSE, response.body());
+ }
+
+ @Test
+ public void testMutualTlsRejectsMissingClientCertificate() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(jettyServer, true,
createTestServlet(), TEST_PATH);
+
+ HttpClient client =
+
createHttpsClient(TestTlsServerUtils.testResource("test-client-truststore.p12"),
null);
+
+ HttpRequest request = createHttpsRequest(port);
+
+ Exception exception =
+ assertThrows(
+ Exception.class, () -> client.send(request,
HttpResponse.BodyHandlers.ofString()));
+
+ assertHandshakeFailure(exception);
+ }
+
+ @Test
+ public void testMutualTlsRejectsUntrustedClientCertificate() throws
Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(jettyServer, true,
createTestServlet(), TEST_PATH);
+
+ HttpClient client =
+ createHttpsClient(
+ TestTlsServerUtils.testResource("test-client-truststore.p12"),
+
TestTlsServerUtils.testResource("test-untrusted-client-keystore.p12"));
+
+ HttpRequest request = createHttpsRequest(port);
+
+ Exception exception =
+ assertThrows(
+ Exception.class, () -> client.send(request,
HttpResponse.BodyHandlers.ofString()));
+
+ assertHandshakeFailure(exception);
+ }
+
+ @Test
+ public void testClientRejectsUntrustedServerCertificate() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(jettyServer, false,
createTestServlet(), TEST_PATH);
+
+ HttpClient client =
+ createHttpsClient(
+
TestTlsServerUtils.testResource("test-untrusted-server-truststore.p12"), null);
+
+ HttpRequest request = createHttpsRequest(port);
+
+ Exception exception =
+ assertThrows(
+ Exception.class, () -> client.send(request,
HttpResponse.BodyHandlers.ofString()));
+
+ assertHandshakeFailure(exception);
+ }
+
+ @Test
+ public void testHttpsWithoutClientAuthentication() throws Exception {
+ int port =
+ TestTlsServerUtils.startHttpsServer(jettyServer, false,
createTestServlet(), TEST_PATH);
+
+ HttpClient client =
+
createHttpsClient(TestTlsServerUtils.testResource("test-client-truststore.p12"),
null);
+
+ HttpRequest request = createHttpsRequest(port);
+
+ HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(HttpServletResponse.SC_OK, response.statusCode());
+ assertEquals(TEST_RESPONSE, response.body());
+ }
+
+ @Test
+ public void testHttpWithoutCustomTlsConfiguration() throws Exception {
+ int port = startHttpServer(jettyServer, createTestServlet(), TEST_PATH);
+
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .uri(URI.create("http://localhost:" + port + TEST_PATH))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+
+ HttpResponse<String> response =
+ HttpClient.newHttpClient().send(request,
HttpResponse.BodyHandlers.ofString());
+
+ assertEquals(HttpServletResponse.SC_OK, response.statusCode());
+ assertEquals(TEST_RESPONSE, response.body());
+ }
+
+ public static int startHttpServer(JettyServer server, HttpServlet servlet,
String servletPath)
+ throws Exception {
+
+ int port = RESTUtils.findAvailablePort(5000, 6000);
+
+ Config config = new Config(false) {};
+ config.set(JettyServerConfig.WEBSERVER_HTTP_PORT, port);
+
+ server.initialize(JettyServerConfig.fromConfig(config), "test", false);
+ server.start();
+ server.addServlet(servlet, servletPath);
+
+ return port;
+ }
+
+ private static HttpClient createHttpsClient(Path trustStorePath, Path
clientKeyStorePath)
+ throws Exception {
+ KeyStore trustStore = loadStore(trustStorePath);
+
+ TrustManagerFactory trustManagerFactory =
+
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStore);
+
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+
+ if (clientKeyStorePath == null) {
+ sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
+ } else {
+ KeyStore clientKeyStore = loadStore(clientKeyStorePath);
+
+ KeyManagerFactory keyManagerFactory =
+
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(clientKeyStore, TEST_STORE_PASSWORD_CHARS);
+
+ sslContext.init(
+ keyManagerFactory.getKeyManagers(),
trustManagerFactory.getTrustManagers(), null);
+ }
+
+ return HttpClient.newBuilder()
+ .sslContext(sslContext)
+ .connectTimeout(Duration.ofSeconds(10))
+ .build();
+ }
+
+ private static HttpRequest createHttpsRequest(int port) {
+ return HttpRequest.newBuilder()
+ .uri(URI.create("https://localhost:" + port + TEST_PATH))
+ .timeout(Duration.ofSeconds(10))
+ .GET()
+ .build();
+ }
+
+ private static KeyStore loadStore(Path path) throws Exception {
+ KeyStore keyStore = KeyStore.getInstance(TEST_STORE_TYPE);
+
+ try (InputStream inputStream = Files.newInputStream(path)) {
+ keyStore.load(inputStream, TEST_STORE_PASSWORD_CHARS);
+ }
+
+ return keyStore;
+ }
+
+ private static HttpServlet createTestServlet() {
+ return new HttpServlet() {
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse
response)
+ throws IOException {
+ response.setStatus(HttpServletResponse.SC_OK);
+ response.setContentType("text/plain");
+ response.getWriter().write(SERVLET_RESPONSE);
+ }
+ };
+ }
+
+ private static void assertHandshakeFailure(Throwable throwable) {
+ Throwable current = throwable;
+
+ while (current != null) {
+ if (current instanceof SSLHandshakeException) {
+ return;
+ }
+
+ current = current.getCause();
+ }
+
+ fail("Expected an SSL handshake failure, but received: " + throwable,
throwable);
+ }
+}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/web/TestTlsServerUtils.java
b/server-common/src/test/java/org/apache/gravitino/server/web/TestTlsServerUtils.java
new file mode 100644
index 0000000000..83e3b48b50
--- /dev/null
+++
b/server-common/src/test/java/org/apache/gravitino/server/web/TestTlsServerUtils.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.server.web;
+
+import java.nio.file.Path;
+import java.util.Objects;
+import javax.servlet.http.HttpServlet;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.rest.RESTUtils;
+
+public final class TestTlsServerUtils {
+
+ public static final String TEST_STORE_PASSWORD = "changeit";
+
+ private TestTlsServerUtils() {}
+
+ public static int startHttpsServer(
+ JettyServer server, boolean clientAuth, HttpServlet servlet, String
servletPath)
+ throws Exception {
+
+ int port = RESTUtils.findAvailablePort(6000, 7000);
+
+ Config config = new Config(false) {};
+ config.set(JettyServerConfig.ENABLE_HTTPS, true);
+ config.set(JettyServerConfig.WEBSERVER_HTTPS_PORT, port);
+
+ config.set(
+ JettyServerConfig.SSL_KEYSTORE_PATH,
testResource("test-server-keystore.p12").toString());
+ config.set(JettyServerConfig.SSL_KEYSTORE_PASSWORD, TEST_STORE_PASSWORD);
+ config.set(JettyServerConfig.SSL_MANAGER_PASSWORD, TEST_STORE_PASSWORD);
+
+ config.set(JettyServerConfig.ENABLE_CLIENT_AUTH, clientAuth);
+
+ if (clientAuth) {
+ config.set(
+ JettyServerConfig.SSL_TRUST_STORE_PATH,
+ testResource("test-server-truststore.p12").toString());
+ config.set(JettyServerConfig.SSL_TRUST_STORE_PASSWORD,
TEST_STORE_PASSWORD);
+ }
+
+ server.initialize(JettyServerConfig.fromConfig(config), "test", false);
+ server.start();
+ server.addServlet(servlet, servletPath);
+
+ return port;
+ }
+
+ public static Path testResource(String filename) throws Exception {
+ return Path.of(
+ Objects.requireNonNull(
+ TestTlsServerUtils.class.getResource("/tls/" + filename),
+ "Missing TLS test resource: " + filename)
+ .toURI());
+ }
+}
diff --git a/server-common/src/test/resources/tls/README.md
b/server-common/src/test/resources/tls/README.md
new file mode 100644
index 0000000000..6f468ac067
--- /dev/null
+++ b/server-common/src/test/resources/tls/README.md
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+# TLS Integration Test Fixtures
+
+This directory contains static PKCS12 keystore and truststore fixtures used by
the TLS integration tests.
+
+These files are committed to the repository so the tests can perform real TLS
and mutual TLS handshakes.
+
+## Test-only warning
+
+All certificates, private keys, aliases, and passwords in this directory are
for testing only.
+
+They are intentionally stored in the repository and must not be used in
production or for any security-sensitive purpose.
+
+## Passwords
+
+All PKCS12 stores use the following password:
+
+```text
+changeit
+```
+
+Private-key entries also use:
+
+```text
+changeit
+```
+
+The same password is used for all fixtures to keep the integration-test
configuration simple.
+
+## Fixture files
+
+| Filename | Purpose
| Alias | Certificate subject
|
+| -------------------------------------- |
-------------------------------------------------------------------------------------------------------------------------------
| ----------------------- |
-----------------------------------------------------------------------------------------------
|
+| `test-server-keystore.p12` | Contains the private key and
certificate presented by the Jetty HTTPS server.
| `test-server` | `CN=localhost, OU=TLS
Integration Tests, O=Apache Gravitino, L=Test, ST=Test, C=US` |
+| `test-server-truststore.p12` | Contains the trusted client
certificate used by the server during mutual TLS authentication.
| `test-trusted-client` | `CN=test-trusted-client, OU=TLS
Integration Tests, O=Apache Gravitino, L=Test, ST=Test, C=US` |
+| `test-client-truststore.p12` | Contains the server certificate
trusted by the HTTPS client.
| `test-server` | `CN=localhost, OU=TLS Integration
Tests, O=Apache Gravitino, L=Test, ST=Test, C=US` |
+| `test-trusted-client-keystore.p12` | Contains the private key and
certificate for the client that should be accepted by the server during mutual
TLS authentication. | `test-trusted-client` | `CN=test-trusted-client, OU=TLS
Integration Tests, O=Apache Gravitino, L=Test, ST=Test, C=US` |
+| `test-untrusted-client-keystore.p12` | Contains a client private key and
certificate that is not trusted by the server and should cause the TLS
handshake to fail. | `test-untrusted-client` | `CN=test-untrusted-client,
OU=TLS Integration Tests, O=Apache Gravitino, L=Test, ST=Test, C=US` |
+| `test-untrusted-server-truststore.p12` | Contains an unrelated trusted
certificate, causing the client to reject the test server certificate.
| `test-unrelated-server` | `CN=test-unrelated-server, OU=TLS
Integration Tests, O=Apache Gravitino, L=Test, ST=Test, C=US` |
+
+## Server certificate hostname
+
+The test server is accessed using:
+
+```text
+https://localhost:<port>
+```
+
+The server certificate must therefore contain the following Subject
Alternative Name:
+
+```text
+DNS:localhost
+```
+
+## Expected entry types
+
+Identity keystores should contain a private-key entry:
+
+```text
+Entry type: PrivateKeyEntry
+```
+
+This applies to:
+
+* `test-server-keystore.p12`
+* `test-trusted-client-keystore.p12`
+* `test-untrusted-client-keystore.p12`
+
+Truststores should contain trusted certificate entries:
+
+```text
+Entry type: trustedCertEntry
+```
+
+This applies to:
+
+* `test-server-truststore.p12`
+* `test-client-truststore.p12`
+* `test-untrusted-server-truststore.p12`
+
+## Inspecting a fixture
+
+Use the following command to inspect a PKCS12 fixture:
+
+```bash
+keytool -list -v \
+ -storetype PKCS12 \
+ -keystore test-server-keystore.p12 \
+ -storepass changeit
+```
+
+Replace the filename as needed.
+
+The output should be used to verify:
+
+* alias name
+* entry type
+* certificate subject
+* certificate issuer
+* certificate validity dates
+* Subject Alternative Names
+* certificate chain
+
+## Regeneration requirements
+
+When regenerating the fixtures, preserve the following requirements:
+
+1. All stores must use the PKCS12 format.
+2. All store passwords must be `changeit`.
+3. All private-key passwords must be `changeit`.
+4. The server certificate must include `DNS:localhost` as a Subject
Alternative Name.
+5. The trusted client certificate must be present in the server truststore.
+6. The server certificate must be present in the client truststore.
+7. The untrusted client certificate must not be present in the server
truststore.
+8. The untrusted server truststore must not contain the test server
certificate or any certificate authority that issued it.
+9. Trusted and untrusted identities must use different private keys and
certificates.
+10. The filenames, aliases, and certificate subjects documented above must be
updated if they change.
+
+## Regeneration instructions
+
+The fixtures can be regenerated using the JDK `keytool` utility.
+
+Run the fixture-generation script from this directory:
+
+```bash
+./regenerate.sh
+```
+
+The script recreates all PKCS12 keystores and truststores using the passwords,
aliases, subjects, and trust relationships documented above.
+
+After regeneration, inspect each fixture using:
+
+```bash
+keytool -list -v \
+ -storetype PKCS12 \
+ -keystore <fixture-name>.p12 \
+ -storepass changeit
+```
+
+The generated certificates are valid for 3,650 days (10 years) and must be
regenerated before they expire.
+
+## Running the integration test
+
+From the repository root, run:
+
+```bash
+./gradlew :server-common:test --tests
"org.apache.gravitino.server.web.TestJettyServer"
+```
\ No newline at end of file
diff --git a/server-common/src/test/resources/tls/regenerate.sh
b/server-common/src/test/resources/tls/regenerate.sh
new file mode 100755
index 0000000000..8369dafc90
--- /dev/null
+++ b/server-common/src/test/resources/tls/regenerate.sh
@@ -0,0 +1,210 @@
+#!/usr/bin/env bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+
+set -euo pipefail
+
+STORE_PASSWORD="changeit"
+KEY_PASSWORD="changeit"
+STORE_TYPE="PKCS12"
+VALIDITY_DAYS="3650"
+KEY_ALGORITHM="RSA"
+KEY_SIZE="2048"
+SIGNATURE_ALGORITHM="SHA256withRSA"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "${SCRIPT_DIR}"
+
+SERVER_KEYSTORE="test-server-keystore.p12"
+SERVER_TRUSTSTORE="test-server-truststore.p12"
+CLIENT_TRUSTSTORE="test-client-truststore.p12"
+TRUSTED_CLIENT_KEYSTORE="test-trusted-client-keystore.p12"
+UNTRUSTED_CLIENT_KEYSTORE="test-untrusted-client-keystore.p12"
+UNTRUSTED_SERVER_TRUSTSTORE="test-untrusted-server-truststore.p12"
+
+SERVER_ALIAS="test-server"
+TRUSTED_CLIENT_ALIAS="test-trusted-client"
+UNTRUSTED_CLIENT_ALIAS="test-untrusted-client"
+UNRELATED_SERVER_ALIAS="test-unrelated-server"
+
+SERVER_SUBJECT="CN=localhost,OU=TLS Integration Tests,O=Apache
Gravitino,L=Test,ST=Test,C=US"
+TRUSTED_CLIENT_SUBJECT="CN=test-trusted-client,OU=TLS Integration
Tests,O=Apache Gravitino,L=Test,ST=Test,C=US"
+UNTRUSTED_CLIENT_SUBJECT="CN=test-untrusted-client,OU=TLS Integration
Tests,O=Apache Gravitino,L=Test,ST=Test,C=US"
+UNRELATED_SERVER_SUBJECT="CN=test-unrelated-server,OU=TLS Integration
Tests,O=Apache Gravitino,L=Test,ST=Test,C=US"
+
+TEMP_DIR="$(mktemp -d)"
+
+cleanup() {
+ rm -rf "${TEMP_DIR}"
+}
+trap cleanup EXIT
+
+if ! command -v keytool >/dev/null 2>&1; then
+ echo "Error: keytool was not found on PATH." >&2
+ echo "Install a JDK or add its bin directory to PATH." >&2
+ exit 1
+fi
+
+echo "Removing existing TLS test fixtures..."
+
+rm -f \
+ "${SERVER_KEYSTORE}" \
+ "${SERVER_TRUSTSTORE}" \
+ "${CLIENT_TRUSTSTORE}" \
+ "${TRUSTED_CLIENT_KEYSTORE}" \
+ "${UNTRUSTED_CLIENT_KEYSTORE}" \
+ "${UNTRUSTED_SERVER_TRUSTSTORE}"
+
+echo "Generating server identity..."
+
+keytool -genkeypair \
+ -alias "${SERVER_ALIAS}" \
+ -dname "${SERVER_SUBJECT}" \
+ -ext "SAN=dns:localhost" \
+ -ext "EKU=serverAuth" \
+ -keyalg "${KEY_ALGORITHM}" \
+ -keysize "${KEY_SIZE}" \
+ -sigalg "${SIGNATURE_ALGORITHM}" \
+ -validity "${VALIDITY_DAYS}" \
+ -storetype "${STORE_TYPE}" \
+ -keystore "${SERVER_KEYSTORE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -keypass "${KEY_PASSWORD}" \
+ -noprompt
+
+echo "Generating trusted client identity..."
+
+keytool -genkeypair \
+ -alias "${TRUSTED_CLIENT_ALIAS}" \
+ -dname "${TRUSTED_CLIENT_SUBJECT}" \
+ -ext "EKU=clientAuth" \
+ -keyalg "${KEY_ALGORITHM}" \
+ -keysize "${KEY_SIZE}" \
+ -sigalg "${SIGNATURE_ALGORITHM}" \
+ -validity "${VALIDITY_DAYS}" \
+ -storetype "${STORE_TYPE}" \
+ -keystore "${TRUSTED_CLIENT_KEYSTORE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -keypass "${KEY_PASSWORD}" \
+ -noprompt
+
+echo "Generating untrusted client identity..."
+
+keytool -genkeypair \
+ -alias "${UNTRUSTED_CLIENT_ALIAS}" \
+ -dname "${UNTRUSTED_CLIENT_SUBJECT}" \
+ -ext "EKU=clientAuth" \
+ -keyalg "${KEY_ALGORITHM}" \
+ -keysize "${KEY_SIZE}" \
+ -sigalg "${SIGNATURE_ALGORITHM}" \
+ -validity "${VALIDITY_DAYS}" \
+ -storetype "${STORE_TYPE}" \
+ -keystore "${UNTRUSTED_CLIENT_KEYSTORE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -keypass "${KEY_PASSWORD}" \
+ -noprompt
+
+echo "Generating unrelated server certificate..."
+
+keytool -genkeypair \
+ -alias "${UNRELATED_SERVER_ALIAS}" \
+ -dname "${UNRELATED_SERVER_SUBJECT}" \
+ -ext "SAN=dns:unrelated.invalid" \
+ -ext "EKU=serverAuth" \
+ -keyalg "${KEY_ALGORITHM}" \
+ -keysize "${KEY_SIZE}" \
+ -sigalg "${SIGNATURE_ALGORITHM}" \
+ -validity "${VALIDITY_DAYS}" \
+ -storetype "${STORE_TYPE}" \
+ -keystore "${TEMP_DIR}/unrelated-server-keystore.p12" \
+ -storepass "${STORE_PASSWORD}" \
+ -keypass "${KEY_PASSWORD}" \
+ -noprompt
+
+echo "Exporting certificates..."
+
+keytool -exportcert \
+ -alias "${SERVER_ALIAS}" \
+ -keystore "${SERVER_KEYSTORE}" \
+ -storetype "${STORE_TYPE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -file "${TEMP_DIR}/server.crt" \
+ -rfc
+
+keytool -exportcert \
+ -alias "${TRUSTED_CLIENT_ALIAS}" \
+ -keystore "${TRUSTED_CLIENT_KEYSTORE}" \
+ -storetype "${STORE_TYPE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -file "${TEMP_DIR}/trusted-client.crt" \
+ -rfc
+
+keytool -exportcert \
+ -alias "${UNRELATED_SERVER_ALIAS}" \
+ -keystore "${TEMP_DIR}/unrelated-server-keystore.p12" \
+ -storetype "${STORE_TYPE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -file "${TEMP_DIR}/unrelated-server.crt" \
+ -rfc
+
+echo "Creating server truststore..."
+
+keytool -importcert \
+ -alias "${TRUSTED_CLIENT_ALIAS}" \
+ -file "${TEMP_DIR}/trusted-client.crt" \
+ -keystore "${SERVER_TRUSTSTORE}" \
+ -storetype "${STORE_TYPE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -noprompt
+
+echo "Creating client truststore..."
+
+keytool -importcert \
+ -alias "${SERVER_ALIAS}" \
+ -file "${TEMP_DIR}/server.crt" \
+ -keystore "${CLIENT_TRUSTSTORE}" \
+ -storetype "${STORE_TYPE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -noprompt
+
+echo "Creating untrusted server truststore..."
+
+keytool -importcert \
+ -alias "${UNRELATED_SERVER_ALIAS}" \
+ -file "${TEMP_DIR}/unrelated-server.crt" \
+ -keystore "${UNTRUSTED_SERVER_TRUSTSTORE}" \
+ -storetype "${STORE_TYPE}" \
+ -storepass "${STORE_PASSWORD}" \
+ -noprompt
+
+echo
+echo "TLS integration-test fixtures regenerated successfully."
+echo
+echo "Generated files:"
+printf ' %s\n' \
+ "${SERVER_KEYSTORE}" \
+ "${SERVER_TRUSTSTORE}" \
+ "${CLIENT_TRUSTSTORE}" \
+ "${TRUSTED_CLIENT_KEYSTORE}" \
+ "${UNTRUSTED_CLIENT_KEYSTORE}" \
+ "${UNTRUSTED_SERVER_TRUSTSTORE}"
+
+echo
+echo "All store and private-key passwords: ${STORE_PASSWORD}"
+echo "These fixtures are for testing only and must not be used in production."
\ No newline at end of file
diff --git a/server-common/src/test/resources/tls/test-client-truststore.p12
b/server-common/src/test/resources/tls/test-client-truststore.p12
new file mode 100644
index 0000000000..abae73c911
Binary files /dev/null and
b/server-common/src/test/resources/tls/test-client-truststore.p12 differ
diff --git a/server-common/src/test/resources/tls/test-server-keystore.p12
b/server-common/src/test/resources/tls/test-server-keystore.p12
new file mode 100644
index 0000000000..08c61ab36d
Binary files /dev/null and
b/server-common/src/test/resources/tls/test-server-keystore.p12 differ
diff --git a/server-common/src/test/resources/tls/test-server-truststore.p12
b/server-common/src/test/resources/tls/test-server-truststore.p12
new file mode 100644
index 0000000000..2bacf50a46
Binary files /dev/null and
b/server-common/src/test/resources/tls/test-server-truststore.p12 differ
diff --git
a/server-common/src/test/resources/tls/test-trusted-client-keystore.p12
b/server-common/src/test/resources/tls/test-trusted-client-keystore.p12
new file mode 100644
index 0000000000..e6ef643640
Binary files /dev/null and
b/server-common/src/test/resources/tls/test-trusted-client-keystore.p12 differ
diff --git
a/server-common/src/test/resources/tls/test-untrusted-client-keystore.p12
b/server-common/src/test/resources/tls/test-untrusted-client-keystore.p12
new file mode 100644
index 0000000000..2105f7027a
Binary files /dev/null and
b/server-common/src/test/resources/tls/test-untrusted-client-keystore.p12 differ
diff --git
a/server-common/src/test/resources/tls/test-untrusted-server-truststore.p12
b/server-common/src/test/resources/tls/test-untrusted-server-truststore.p12
new file mode 100644
index 0000000000..2677d26c49
Binary files /dev/null and
b/server-common/src/test/resources/tls/test-untrusted-server-truststore.p12
differ