From 4ff1a29aae561cad6851a13666e4375a7645c6ef Mon Sep 17 00:00:00 2001 From: prmoore77 Date: Mon, 6 Nov 2023 14:55:28 -0500 Subject: [PATCH] GH-38460: [Java][FlightRPC] Add mTLS support for Flight SQL JDBC driver (#38461) ### Rationale for this change I wanted to add additional security capabilities to the Arrow Flight SQL JDBC driver so that it catches up to ADBC. ADBC already supports mTLS - and it is a great security feature. I wanted to bring this to the JDBC driver as well. ### What changes are included in this PR? This PR adds support for mTLS (client certificate verification/authentication) to the Arrow Flight SQL JDBC driver. ### Are these changes tested? Yes, I've added tests of the new mTLS functionality - and have ensured that the change is backward compatible by verifying all existing tests pass. ### Are there any user-facing changes? Yes - but the end-user documentation for the Arrow Flight SQL JDBC driver has been updated in the PR itself. * Closes: #38460 Lead-authored-by: prmoore77 Co-authored-by: David Li Signed-off-by: David Li --- docs/source/java/flight_sql_jdbc_driver.rst | 15 + .../org/apache/arrow/flight/FlightServer.java | 100 +++- .../driver/jdbc/ArrowFlightConnection.java | 3 + .../client/ArrowFlightSqlClientHandler.java | 54 ++- .../utils/ClientAuthenticationUtils.java | 56 ++- .../ArrowFlightConnectionConfigImpl.java | 15 + .../driver/jdbc/ConnectionMutualTlsTest.java | 427 ++++++++++++++++++ .../jdbc/ConnectionTlsRootCertsTest.java | 352 +++++++++++++++ .../driver/jdbc/FlightServerTestRule.java | 22 +- .../jdbc/utils/FlightSqlTestCertificates.java | 10 + 10 files changed, 1045 insertions(+), 9 deletions(-) create mode 100644 java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionMutualTlsTest.java create mode 100644 java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionTlsRootCertsTest.java diff --git a/docs/source/java/flight_sql_jdbc_driver.rst b/docs/source/java/flight_sql_jdbc_driver.rst index 8268242302488..34ccfea47f9e3 100644 --- a/docs/source/java/flight_sql_jdbc_driver.rst +++ b/docs/source/java/flight_sql_jdbc_driver.rst @@ -114,6 +114,21 @@ case-sensitive. The supported parameters are: - null - When TLS is enabled, the password for the certificate store + * - tlsRootCerts + - null + - Path to PEM-encoded root certificates for TLS - use this as + an alternative to ``trustStore`` + + * - clientCertificate + - null + - Path to PEM-encoded client mTLS certificate when the Flight + SQL server requires client verification. + + * - clientKey + - null + - Path to PEM-encoded client mTLS key when the Flight + SQL server requires client verification. + * - useEncryption - true - Whether to use TLS (the default is an encrypted connection) diff --git a/java/flight/flight-core/src/main/java/org/apache/arrow/flight/FlightServer.java b/java/flight/flight-core/src/main/java/org/apache/arrow/flight/FlightServer.java index 7f15798f6a326..234c9bdcaacc1 100644 --- a/java/flight/flight-core/src/main/java/org/apache/arrow/flight/FlightServer.java +++ b/java/flight/flight-core/src/main/java/org/apache/arrow/flight/FlightServer.java @@ -34,6 +34,8 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; +import javax.net.ssl.SSLException; + import org.apache.arrow.flight.auth.ServerAuthHandler; import org.apache.arrow.flight.auth.ServerAuthInterceptor; import org.apache.arrow.flight.auth2.Auth2Constants; @@ -49,9 +51,14 @@ import io.grpc.Server; import io.grpc.ServerInterceptors; +import io.grpc.netty.GrpcSslContexts; import io.grpc.netty.NettyServerBuilder; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; + /** * Generic server of flight data that is customized via construction with delegate classes for the @@ -172,6 +179,8 @@ public static final class Builder { private int maxInboundMessageSize = MAX_GRPC_MESSAGE_SIZE; private InputStream certChain; private InputStream key; + private InputStream mTlsCACert; + private SslContext sslContext; private final List> interceptors; // Keep track of inserted interceptors private final Set interceptorKeys; @@ -245,7 +254,25 @@ public FlightServer build() { } if (certChain != null) { - builder.useTransportSecurity(certChain, key); + SslContextBuilder sslContextBuilder = GrpcSslContexts + .forServer(certChain, key); + + if (mTlsCACert != null) { + sslContextBuilder + .clientAuth(ClientAuth.REQUIRE) + .trustManager(mTlsCACert); + } + try { + sslContext = sslContextBuilder.build(); + } catch (SSLException e) { + throw new RuntimeException(e); + } finally { + closeMTlsCACert(); + closeCertChain(); + closeKey(); + } + + builder.sslContext(sslContext); } // Share one executor between the gRPC service, DoPut, and Handshake @@ -306,14 +333,69 @@ public Builder maxInboundMessageSize(int maxMessageSize) { return this; } + /** + * A small utility function to ensure that InputStream attributes. + * are closed if they are not null + * @param stream The InputStream to close (if it is not null). + */ + private void closeInputStreamIfNotNull(InputStream stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException ignored) { + } + } + } + + /** + * A small utility function to ensure that the certChain attribute + * is closed if it is not null. It then sets the attribute to null. + */ + private void closeCertChain() { + closeInputStreamIfNotNull(certChain); + certChain = null; + } + + /** + * A small utility function to ensure that the key attribute + * is closed if it is not null. It then sets the attribute to null. + */ + private void closeKey() { + closeInputStreamIfNotNull(key); + key = null; + } + + /** + * A small utility function to ensure that the mTlsCACert attribute + * is closed if it is not null. It then sets the attribute to null. + */ + private void closeMTlsCACert() { + closeInputStreamIfNotNull(mTlsCACert); + mTlsCACert = null; + } + /** * Enable TLS on the server. * @param certChain The certificate chain to use. * @param key The private key to use. */ public Builder useTls(final File certChain, final File key) throws IOException { + closeCertChain(); this.certChain = new FileInputStream(certChain); + + closeKey(); this.key = new FileInputStream(key); + + return this; + } + + /** + * Enable Client Verification via mTLS on the server. + * @param mTlsCACert The CA certificate to use for verifying clients. + */ + public Builder useMTlsClientVerification(final File mTlsCACert) throws IOException { + closeMTlsCACert(); + this.mTlsCACert = new FileInputStream(mTlsCACert); return this; } @@ -322,9 +404,23 @@ public Builder useTls(final File certChain, final File key) throws IOException { * @param certChain The certificate chain to use. * @param key The private key to use. */ - public Builder useTls(final InputStream certChain, final InputStream key) { + public Builder useTls(final InputStream certChain, final InputStream key) throws IOException { + closeCertChain(); this.certChain = certChain; + + closeKey(); this.key = key; + + return this; + } + + /** + * Enable mTLS on the server. + * @param mTlsCACert The CA certificate to use for verifying clients. + */ + public Builder useMTlsClientVerification(final InputStream mTlsCACert) throws IOException { + closeMTlsCACert(); + this.mTlsCACert = mTlsCACert; return this; } diff --git a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java index 79bc04d27fe01..fdbb9381c0a55 100644 --- a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java +++ b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/ArrowFlightConnection.java @@ -101,6 +101,9 @@ private static ArrowFlightSqlClientHandler createNewClientHandler( .withTrustStorePath(config.getTrustStorePath()) .withTrustStorePassword(config.getTrustStorePassword()) .withSystemTrustStore(config.useSystemTrustStore()) + .withTlsRootCertificates(config.getTlsRootCertificatesPath()) + .withClientCertificate(config.getClientCertificatePath()) + .withClientKey(config.getClientKeyPath()) .withBufferAllocator(allocator) .withEncryption(config.useEncryption()) .withDisableCertificateVerification(config.getDisableCertificateVerification()) diff --git a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java index 66372092b8e99..75e80d45dc669 100644 --- a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java +++ b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/ArrowFlightSqlClientHandler.java @@ -433,6 +433,9 @@ public static final class Builder { private boolean useEncryption; private boolean disableCertificateVerification; private boolean useSystemTrustStore; + private String tlsRootCertificatesPath; + private String clientCertificatePath; + private String clientKeyPath; private BufferAllocator allocator; public Builder() { @@ -457,6 +460,9 @@ private Builder(Builder original) { this.useEncryption = original.useEncryption; this.disableCertificateVerification = original.disableCertificateVerification; this.useSystemTrustStore = original.useSystemTrustStore; + this.tlsRootCertificatesPath = original.tlsRootCertificatesPath; + this.clientCertificatePath = original.clientCertificatePath; + this.clientKeyPath = original.clientKeyPath; this.allocator = original.allocator; } @@ -560,7 +566,42 @@ public Builder withSystemTrustStore(final boolean useSystemTrustStore) { } /** - * Sets the token used in the token authetication. + * Sets the TLS root certificate path as an alternative to using the System + * or other Trust Store. The path must contain a valid PEM file. + * + * @param tlsRootCertificatesPath the TLS root certificate path (if TLS is required). + * @return this instance. + */ + public Builder withTlsRootCertificates(final String tlsRootCertificatesPath) { + this.tlsRootCertificatesPath = tlsRootCertificatesPath; + return this; + } + + /** + * Sets the mTLS client certificate path (if mTLS is required). + * + * @param clientCertificatePath the mTLS client certificate path (if mTLS is required). + * @return this instance. + */ + public Builder withClientCertificate(final String clientCertificatePath) { + this.clientCertificatePath = clientCertificatePath; + return this; + } + + /** + * Sets the mTLS client certificate private key path (if mTLS is required). + * + * @param clientKeyPath the mTLS client certificate private key path (if mTLS is required). + * @return this instance. + */ + public Builder withClientKey(final String clientKeyPath) { + this.clientKeyPath = clientKeyPath; + return this; + } + + /** + * Sets the token used in the token authentication. + * * @param token the token value. * @return this builder instance. */ @@ -660,7 +701,10 @@ public ArrowFlightSqlClientHandler build() throws SQLException { if (disableCertificateVerification) { clientBuilder.verifyServer(false); } else { - if (useSystemTrustStore) { + if (tlsRootCertificatesPath != null) { + clientBuilder.trustedCertificates( + ClientAuthenticationUtils.getTlsRootCertificatesStream(tlsRootCertificatesPath)); + } else if (useSystemTrustStore) { clientBuilder.trustedCertificates( ClientAuthenticationUtils.getCertificateInputStreamFromSystem(trustStorePassword)); } else if (trustStorePath != null) { @@ -668,6 +712,12 @@ public ArrowFlightSqlClientHandler build() throws SQLException { ClientAuthenticationUtils.getCertificateStream(trustStorePath, trustStorePassword)); } } + + if (clientCertificatePath != null && clientKeyPath != null) { + clientBuilder.clientCertificate( + ClientAuthenticationUtils.getClientCertificateStream(clientCertificatePath), + ClientAuthenticationUtils.getClientKeyStream(clientKeyPath)); + } } client = clientBuilder.build(); diff --git a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/utils/ClientAuthenticationUtils.java b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/utils/ClientAuthenticationUtils.java index 6d9880bd27048..d50dc385a62e1 100644 --- a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/utils/ClientAuthenticationUtils.java +++ b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/client/utils/ClientAuthenticationUtils.java @@ -227,14 +227,64 @@ public static InputStream getCertificateStream(final String keyStorePath, final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); try (final InputStream keyStoreStream = Files - .newInputStream(Paths.get(Preconditions.checkNotNull(keyStorePath)))) { - keyStore.load(keyStoreStream, - Preconditions.checkNotNull(keyStorePass).toCharArray()); + .newInputStream(Paths.get(keyStorePath))) { + keyStore.load(keyStoreStream, keyStorePass.toCharArray()); } return getSingleCertificateInputStream(keyStore); } + /** + * Generates an {@link InputStream} that contains certificates for path-based + * TLS Root Certificates. + * + * @param tlsRootsCertificatesPath The path of the TLS Root Certificates. + * @return a new {code InputStream} containing the certificates. + * @throws GeneralSecurityException on error. + * @throws IOException on error. + */ + public static InputStream getTlsRootCertificatesStream(final String tlsRootsCertificatesPath) + throws GeneralSecurityException, IOException { + Preconditions.checkNotNull(tlsRootsCertificatesPath, "TLS Root certificates path cannot be null!"); + + return Files + .newInputStream(Paths.get(tlsRootsCertificatesPath)); + } + + /** + * Generates an {@link InputStream} that contains certificates for a path-based + * mTLS Client Certificate. + * + * @param clientCertificatePath The path of the mTLS Client Certificate. + * @return a new {code InputStream} containing the certificates. + * @throws GeneralSecurityException on error. + * @throws IOException on error. + */ + public static InputStream getClientCertificateStream(final String clientCertificatePath) + throws GeneralSecurityException, IOException { + Preconditions.checkNotNull(clientCertificatePath, "Client certificate path cannot be null!"); + + return Files + .newInputStream(Paths.get(clientCertificatePath)); + } + + /** + * Generates an {@link InputStream} that contains certificates for a path-based + * mTLS Client Key. + * + * @param clientKeyPath The path of the mTLS Client Key. + * @return a new {code InputStream} containing the certificates. + * @throws GeneralSecurityException on error. + * @throws IOException on error. + */ + public static InputStream getClientKeyStream(final String clientKeyPath) + throws GeneralSecurityException, IOException { + Preconditions.checkNotNull(clientKeyPath, "Client key path cannot be null!"); + + return Files + .newInputStream(Paths.get(clientKeyPath)); + } + private static InputStream getSingleCertificateInputStream(KeyStore keyStore) throws KeyStoreException, IOException, CertificateException { final Enumeration aliases = keyStore.aliases(); diff --git a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java index ac338a85d6292..59118e1d6f788 100644 --- a/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java +++ b/java/flight/flight-sql-jdbc-core/src/main/java/org/apache/arrow/driver/jdbc/utils/ArrowFlightConnectionConfigImpl.java @@ -109,6 +109,18 @@ public boolean useSystemTrustStore() { return ArrowFlightConnectionProperty.USE_SYSTEM_TRUST_STORE.getBoolean(properties); } + public String getTlsRootCertificatesPath() { + return ArrowFlightConnectionProperty.TLS_ROOT_CERTS.getString(properties); + } + + public String getClientCertificatePath() { + return ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.getString(properties); + } + + public String getClientKeyPath() { + return ArrowFlightConnectionProperty.CLIENT_KEY.getString(properties); + } + /** * Whether to use TLS encryption. * @@ -175,6 +187,9 @@ public enum ArrowFlightConnectionProperty implements ConnectionProperty { TRUST_STORE("trustStore", null, Type.STRING, false), TRUST_STORE_PASSWORD("trustStorePassword", null, Type.STRING, false), USE_SYSTEM_TRUST_STORE("useSystemTrustStore", true, Type.BOOLEAN, false), + TLS_ROOT_CERTS("tlsRootCerts", null, Type.STRING, false), + CLIENT_CERTIFICATE("clientCertificate", null, Type.STRING, false), + CLIENT_KEY("clientKey", null, Type.STRING, false), THREAD_POOL_SIZE("threadPoolSize", 1, Type.NUMBER, false), TOKEN("token", null, Type.STRING, false); diff --git a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionMutualTlsTest.java b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionMutualTlsTest.java new file mode 100644 index 0000000000000..783e0c41e9269 --- /dev/null +++ b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionMutualTlsTest.java @@ -0,0 +1,427 @@ +/* + * 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.arrow.driver.jdbc; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.io.File; +import java.net.URLEncoder; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +import org.apache.arrow.driver.jdbc.authentication.UserPasswordAuthentication; +import org.apache.arrow.driver.jdbc.client.ArrowFlightSqlClientHandler; +import org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty; +import org.apache.arrow.driver.jdbc.utils.FlightSqlTestCertificates; +import org.apache.arrow.driver.jdbc.utils.MockFlightSqlProducer; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.util.AutoCloseables; +import org.apache.calcite.avatica.org.apache.http.auth.UsernamePasswordCredentials; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +/** + * Tests encrypted connections. + */ +public class ConnectionMutualTlsTest { + + @ClassRule + public static final FlightServerTestRule FLIGHT_SERVER_TEST_RULE; + private static final String tlsRootCertsPath; + + private static final String serverMTlsCACertPath; + private static final String clientMTlsCertPath; + private static final String badClientMTlsCertPath; + private static final String clientMTlsKeyPath; + private static final String badClientMTlsKeyPath; + private static final MockFlightSqlProducer PRODUCER = new MockFlightSqlProducer(); + private static final String userTest = "user1"; + private static final String passTest = "pass1"; + + static { + final FlightSqlTestCertificates.CertKeyPair + certKey = FlightSqlTestCertificates.exampleTlsCerts().get(0); + + tlsRootCertsPath = certKey.cert.getPath(); + + final File serverMTlsCACert = FlightSqlTestCertificates.exampleCACert(); + + serverMTlsCACertPath = serverMTlsCACert.getPath(); + + final FlightSqlTestCertificates.CertKeyPair + clientMTlsCertKey = FlightSqlTestCertificates.exampleTlsCerts().get(1); + + clientMTlsCertPath = clientMTlsCertKey.cert.getPath(); + clientMTlsKeyPath = clientMTlsCertKey.key.getPath(); + + badClientMTlsCertPath = clientMTlsCertPath + ".bad"; + badClientMTlsKeyPath = clientMTlsKeyPath + ".bad"; + + UserPasswordAuthentication authentication = new UserPasswordAuthentication.Builder() + .user(userTest, passTest) + .build(); + + FLIGHT_SERVER_TEST_RULE = new FlightServerTestRule.Builder() + .authentication(authentication) + .useEncryption(certKey.cert, certKey.key) + .useMTlsClientVerification(serverMTlsCACert) + .producer(PRODUCER) + .build(); + } + + private BufferAllocator allocator; + + @Before + public void setUp() throws Exception { + allocator = new RootAllocator(Long.MAX_VALUE); + } + + @After + public void tearDown() throws Exception { + allocator.getChildAllocators().forEach(BufferAllocator::close); + AutoCloseables.close(allocator); + } + + /** + * Try to instantiate an encrypted FlightClient. + * + * @throws Exception on error. + */ + @Test + public void testGetEncryptedClientAuthenticated() throws Exception { + final UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( + userTest, passTest); + + try (ArrowFlightSqlClientHandler client = + new ArrowFlightSqlClientHandler.Builder() + .withHost(FLIGHT_SERVER_TEST_RULE.getHost()) + .withPort(FLIGHT_SERVER_TEST_RULE.getPort()) + .withUsername(credentials.getUserName()) + .withPassword(credentials.getPassword()) + .withTlsRootCertificates(tlsRootCertsPath) + .withClientCertificate(clientMTlsCertPath) + .withClientKey(clientMTlsKeyPath) + .withBufferAllocator(allocator) + .withEncryption(true) + .build()) { + assertNotNull(client); + } + } + + /** + * Try to instantiate an encrypted FlightClient providing a bad mTLS Cert Path. It's expected to + * receive the SQLException. + */ + @Test + public void testGetEncryptedClientWithBadMTlsCertPath() { + final UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( + userTest, passTest); + + assertThrows(SQLException.class, () -> new ArrowFlightSqlClientHandler.Builder() + .withHost(FLIGHT_SERVER_TEST_RULE.getHost()) + .withPort(FLIGHT_SERVER_TEST_RULE.getPort()) + .withUsername(credentials.getUserName()) + .withPassword(credentials.getPassword()) + .withTlsRootCertificates(tlsRootCertsPath) + .withClientCertificate(badClientMTlsCertPath) + .withClientKey(clientMTlsKeyPath) + .withBufferAllocator(allocator) + .withEncryption(true) + .build()); + } + + /** + * Try to instantiate an encrypted FlightClient providing a bad mTLS Key Path. It's expected to + * receive the SQLException. + */ + @Test + public void testGetEncryptedClientWithBadMTlsKeyPath() { + final UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( + userTest, passTest); + + assertThrows(SQLException.class, () -> new ArrowFlightSqlClientHandler.Builder() + .withHost(FLIGHT_SERVER_TEST_RULE.getHost()) + .withPort(FLIGHT_SERVER_TEST_RULE.getPort()) + .withUsername(credentials.getUserName()) + .withPassword(credentials.getPassword()) + .withTlsRootCertificates(tlsRootCertsPath) + .withClientCertificate(clientMTlsCertPath) + .withClientKey(badClientMTlsKeyPath) + .withBufferAllocator(allocator) + .withEncryption(true) + .build()); + } + + /** + * Try to instantiate an encrypted FlightClient without credentials. + * + * @throws Exception on error. + */ + @Test + public void testGetNonAuthenticatedEncryptedClientNoAuth() throws Exception { + try (ArrowFlightSqlClientHandler client = + new ArrowFlightSqlClientHandler.Builder() + .withHost(FLIGHT_SERVER_TEST_RULE.getHost()) + .withTlsRootCertificates(tlsRootCertsPath) + .withClientCertificate(clientMTlsCertPath) + .withClientKey(clientMTlsKeyPath) + .withBufferAllocator(allocator) + .withEncryption(true) + .build()) { + assertNotNull(client); + } + } + + /** + * Check if an encrypted connection can be established successfully when the + * provided valid credentials and a valid TLS Root Certs path. + * + * @throws Exception on error. + */ + @Test + public void testGetEncryptedConnectionWithValidCredentialsAndTlsRootsPath() throws Exception { + final Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.HOST.camelName(), "localhost"); + properties.put(ArrowFlightConnectionProperty.PORT.camelName(), + FLIGHT_SERVER_TEST_RULE.getPort()); + properties.put(ArrowFlightConnectionProperty.USER.camelName(), + userTest); + properties.put(ArrowFlightConnectionProperty.PASSWORD.camelName(), + passTest); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), + tlsRootCertsPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), + clientMTlsCertPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), + clientMTlsKeyPath); + + final ArrowFlightJdbcDataSource dataSource = + ArrowFlightJdbcDataSource.createNewDataSource(properties); + try (final Connection connection = dataSource.getConnection()) { + assert connection.isValid(300); + } + } + + /** + * Check if an encrypted connection can be established successfully when not + * providing authentication. + * + * @throws Exception on error. + */ + @Test + public void testGetNonAuthenticatedEncryptedConnection() throws Exception { + final Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.HOST.camelName(), FLIGHT_SERVER_TEST_RULE.getHost()); + properties.put(ArrowFlightConnectionProperty.PORT.camelName(), FLIGHT_SERVER_TEST_RULE.getPort()); + properties.put(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), true); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), clientMTlsCertPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), clientMTlsKeyPath); + + final ArrowFlightJdbcDataSource dataSource = ArrowFlightJdbcDataSource.createNewDataSource(properties); + try (final Connection connection = dataSource.getConnection()) { + assert connection.isValid(300); + } + } + + /** + * Check if an encrypted connection can be established successfully when connecting through + * the DriverManager using just a connection url. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueCorrectCastUrlWithDriverManager() throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + final String jdbcUrl = String.format( + "jdbc:arrow-flight-sql://localhost:%s?user=%s&password=%s" + + "&useEncryption=true&%s=%s&%s=%s&%s=%s", + FLIGHT_SERVER_TEST_RULE.getPort(), + userTest, + passTest, + ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), + URLEncoder.encode(tlsRootCertsPath, "UTF-8"), + ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), + URLEncoder.encode(clientMTlsCertPath, "UTF-8"), + ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), + URLEncoder.encode(clientMTlsKeyPath, "UTF-8")); + + try (Connection connection = DriverManager.getConnection(jdbcUrl)) { + Assert.assertTrue(connection.isValid(0)); + } + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with String K-V pairs. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueCorrectCastUrlAndPropertiesUsingSetPropertyWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.setProperty(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.setProperty(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.setProperty(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + properties.setProperty(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), "true"); + properties.setProperty(ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), clientMTlsCertPath); + properties.setProperty(ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), clientMTlsKeyPath); + + final String jdbcUrl = String.format( + "jdbc:arrow-flight-sql://localhost:%s", + FLIGHT_SERVER_TEST_RULE.getPort()); + + try (Connection connection = DriverManager.getConnection(jdbcUrl, properties)) { + Assert.assertTrue(connection.isValid(0)); + } + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with Object K-V pairs. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueCorrectCastUrlAndPropertiesUsingPutWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.put(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.put(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), true); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), clientMTlsCertPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), clientMTlsKeyPath); + + final String jdbcUrl = String.format( + "jdbc:arrow-flight-sql://localhost:%s", + FLIGHT_SERVER_TEST_RULE.getPort()); + + try (Connection connection = DriverManager.getConnection(jdbcUrl, properties)) { + Assert.assertTrue(connection.isValid(0)); + } + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * just a connection url and using 0 and 1 as ssl values. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueIntegerCorrectCastUrlWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + final String jdbcUrl = String.format( + "jdbc:arrow-flight-sql://localhost:%s?user=%s&password=%s" + + "&useEncryption=1&useSystemTrustStore=0&%s=%s&%s=%s&%s=%s", + FLIGHT_SERVER_TEST_RULE.getPort(), + userTest, + passTest, + ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), + URLEncoder.encode(tlsRootCertsPath, "UTF-8"), + ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), + URLEncoder.encode(clientMTlsCertPath, "UTF-8"), + ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), + URLEncoder.encode(clientMTlsKeyPath, "UTF-8")); + + try (Connection connection = DriverManager.getConnection(jdbcUrl)) { + Assert.assertTrue(connection.isValid(0)); + } + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with String K-V pairs and using 0 and 1 as ssl values. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueIntegerCorrectCastUrlAndPropertiesUsingSetPropertyWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.setProperty(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.setProperty(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.setProperty(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + properties.setProperty(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), "1"); + properties.setProperty(ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), clientMTlsCertPath); + properties.setProperty(ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), clientMTlsKeyPath); + + final String jdbcUrl = String.format("jdbc:arrow-flight-sql://localhost:%s", FLIGHT_SERVER_TEST_RULE.getPort()); + + try (Connection connection = DriverManager.getConnection(jdbcUrl, properties)) { + Assert.assertTrue(connection.isValid(0)); + } + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with Object K-V pairs and using 0 and 1 as ssl values. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueIntegerCorrectCastUrlAndPropertiesUsingPutWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.put(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.put(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), 1); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_CERTIFICATE.camelName(), clientMTlsCertPath); + properties.put(ArrowFlightConnectionProperty.CLIENT_KEY.camelName(), clientMTlsKeyPath); + + final String jdbcUrl = String.format("jdbc:arrow-flight-sql://localhost:%s", + FLIGHT_SERVER_TEST_RULE.getPort()); + + try (Connection connection = DriverManager.getConnection(jdbcUrl, properties)) { + Assert.assertTrue(connection.isValid(0)); + } + } +} diff --git a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionTlsRootCertsTest.java b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionTlsRootCertsTest.java new file mode 100644 index 0000000000000..333c474824e52 --- /dev/null +++ b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/ConnectionTlsRootCertsTest.java @@ -0,0 +1,352 @@ +/* + * 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.arrow.driver.jdbc; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; + +import java.net.URLEncoder; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Properties; + +import org.apache.arrow.driver.jdbc.authentication.UserPasswordAuthentication; +import org.apache.arrow.driver.jdbc.client.ArrowFlightSqlClientHandler; +import org.apache.arrow.driver.jdbc.utils.ArrowFlightConnectionConfigImpl.ArrowFlightConnectionProperty; +import org.apache.arrow.driver.jdbc.utils.FlightSqlTestCertificates; +import org.apache.arrow.driver.jdbc.utils.MockFlightSqlProducer; +import org.apache.arrow.memory.BufferAllocator; +import org.apache.arrow.memory.RootAllocator; +import org.apache.arrow.util.AutoCloseables; +import org.apache.calcite.avatica.org.apache.http.auth.UsernamePasswordCredentials; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + + +/** + * Tests encrypted connections. + */ +public class ConnectionTlsRootCertsTest { + + @ClassRule + public static final FlightServerTestRule FLIGHT_SERVER_TEST_RULE; + private static final String tlsRootCertsPath; + private static final String badTlsRootCertsPath; + private static final MockFlightSqlProducer PRODUCER = new MockFlightSqlProducer(); + private static final String userTest = "user1"; + private static final String passTest = "pass1"; + + static { + final FlightSqlTestCertificates.CertKeyPair + certKey = FlightSqlTestCertificates.exampleTlsCerts().get(0); + + tlsRootCertsPath = certKey.cert.getPath(); + + badTlsRootCertsPath = certKey.cert.getPath() + ".bad"; + + UserPasswordAuthentication authentication = new UserPasswordAuthentication.Builder() + .user(userTest, passTest) + .build(); + + FLIGHT_SERVER_TEST_RULE = new FlightServerTestRule.Builder() + .authentication(authentication) + .useEncryption(certKey.cert, certKey.key) + .producer(PRODUCER) + .build(); + } + + private BufferAllocator allocator; + + @Before + public void setUp() throws Exception { + allocator = new RootAllocator(Long.MAX_VALUE); + } + + @After + public void tearDown() throws Exception { + allocator.getChildAllocators().forEach(BufferAllocator::close); + AutoCloseables.close(allocator); + } + + /** + * Try to instantiate an encrypted FlightClient. + * + * @throws Exception on error. + */ + @Test + public void testGetEncryptedClientAuthenticated() throws Exception { + final UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( + userTest, passTest); + + try (ArrowFlightSqlClientHandler client = + new ArrowFlightSqlClientHandler.Builder() + .withHost(FLIGHT_SERVER_TEST_RULE.getHost()) + .withPort(FLIGHT_SERVER_TEST_RULE.getPort()) + .withUsername(credentials.getUserName()) + .withPassword(credentials.getPassword()) + .withTlsRootCertificates(tlsRootCertsPath) + .withBufferAllocator(allocator) + .withEncryption(true) + .build()) { + assertNotNull(client); + } + } + + /** + * Try to instantiate an encrypted FlightClient providing a bad TLS Root Certs Path. It's expected to + * receive the SQLException. + */ + @Test + public void testGetEncryptedClientWithNoCertificateOnKeyStore() { + assertThrows(SQLException.class, () -> new ArrowFlightSqlClientHandler.Builder() + .withHost(FLIGHT_SERVER_TEST_RULE.getHost()) + .withTlsRootCertificates(badTlsRootCertsPath) + .withBufferAllocator(allocator) + .withEncryption(true) + .build()); + } + + /** + * Try to instantiate an encrypted FlightClient without credentials. + * + * @throws Exception on error. + */ + @Test + public void testGetNonAuthenticatedEncryptedClientNoAuth() throws Exception { + try (ArrowFlightSqlClientHandler client = + new ArrowFlightSqlClientHandler.Builder() + .withHost(FLIGHT_SERVER_TEST_RULE.getHost()) + .withTlsRootCertificates(tlsRootCertsPath) + .withBufferAllocator(allocator) + .withEncryption(true) + .build()) { + assertNotNull(client); + } + } + + /** + * Check if an encrypted connection can be established successfully when the + * provided valid credentials and a valid TLS Root Certs path. + * + * @throws Exception on error. + */ + @Test + public void testGetEncryptedConnectionWithValidCredentialsAndTlsRootsPath() throws Exception { + final Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.HOST.camelName(), "localhost"); + properties.put(ArrowFlightConnectionProperty.PORT.camelName(), + FLIGHT_SERVER_TEST_RULE.getPort()); + properties.put(ArrowFlightConnectionProperty.USER.camelName(), + userTest); + properties.put(ArrowFlightConnectionProperty.PASSWORD.camelName(), + passTest); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), + tlsRootCertsPath); + + final ArrowFlightJdbcDataSource dataSource = + ArrowFlightJdbcDataSource.createNewDataSource(properties); + try (final Connection connection = dataSource.getConnection()) { + assert connection.isValid(300); + } + } + + /** + * Check if an encrypted connection can be established successfully when not + * providing authentication. + * + * @throws Exception on error. + */ + @Test + public void testGetNonAuthenticatedEncryptedConnection() throws Exception { + final Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.HOST.camelName(), FLIGHT_SERVER_TEST_RULE.getHost()); + properties.put(ArrowFlightConnectionProperty.PORT.camelName(), FLIGHT_SERVER_TEST_RULE.getPort()); + properties.put(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), true); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + + final ArrowFlightJdbcDataSource dataSource = ArrowFlightJdbcDataSource.createNewDataSource(properties); + try (final Connection connection = dataSource.getConnection()) { + assert connection.isValid(300); + } + } + + /** + * Check if an encrypted connection can be established successfully when connecting through + * the DriverManager using just a connection url. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueCorrectCastUrlWithDriverManager() throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + final Connection connection = DriverManager.getConnection( + String.format( + "jdbc:arrow-flight-sql://localhost:%s?user=%s&password=%s" + + "&useEncryption=true&%s=%s", + FLIGHT_SERVER_TEST_RULE.getPort(), + userTest, + passTest, + ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), + URLEncoder.encode(tlsRootCertsPath, "UTF-8"))); + Assert.assertTrue(connection.isValid(0)); + connection.close(); + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with String K-V pairs. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueCorrectCastUrlAndPropertiesUsingSetPropertyWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.setProperty(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.setProperty(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.setProperty(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + properties.setProperty(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), "true"); + + final Connection connection = DriverManager.getConnection( + String.format( + "jdbc:arrow-flight-sql://localhost:%s", + FLIGHT_SERVER_TEST_RULE.getPort()), + properties); + Assert.assertTrue(connection.isValid(0)); + connection.close(); + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with Object K-V pairs. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueCorrectCastUrlAndPropertiesUsingPutWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.put(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.put(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), true); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + + final Connection connection = DriverManager.getConnection( + String.format( + "jdbc:arrow-flight-sql://localhost:%s", + FLIGHT_SERVER_TEST_RULE.getPort()), + properties); + Assert.assertTrue(connection.isValid(0)); + connection.close(); + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * just a connection url and using 0 and 1 as ssl values. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueIntegerCorrectCastUrlWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + final Connection connection = DriverManager.getConnection( + String.format( + "jdbc:arrow-flight-sql://localhost:%s?user=%s&password=%s" + + "&useEncryption=1&useSystemTrustStore=0&%s=%s", + FLIGHT_SERVER_TEST_RULE.getPort(), + userTest, + passTest, + ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), + URLEncoder.encode(tlsRootCertsPath, "UTF-8"))); + Assert.assertTrue(connection.isValid(0)); + connection.close(); + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with String K-V pairs and using 0 and 1 as ssl values. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueIntegerCorrectCastUrlAndPropertiesUsingSetPropertyWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.setProperty(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.setProperty(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.setProperty(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + properties.setProperty(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), "1"); + + final Connection connection = DriverManager.getConnection( + String.format("jdbc:arrow-flight-sql://localhost:%s", FLIGHT_SERVER_TEST_RULE.getPort()), + properties); + Assert.assertTrue(connection.isValid(0)); + connection.close(); + } + + /** + * Check if an encrypted connection can be established successfully when connecting through the DriverManager using + * a connection url and properties with Object K-V pairs and using 0 and 1 as ssl values. + * + * @throws Exception on error. + */ + @Test + public void testTLSConnectionPropertyTrueIntegerCorrectCastUrlAndPropertiesUsingPutWithDriverManager() + throws Exception { + final Driver driver = new ArrowFlightJdbcDriver(); + DriverManager.registerDriver(driver); + + Properties properties = new Properties(); + + properties.put(ArrowFlightConnectionProperty.USER.camelName(), userTest); + properties.put(ArrowFlightConnectionProperty.PASSWORD.camelName(), passTest); + properties.put(ArrowFlightConnectionProperty.USE_ENCRYPTION.camelName(), 1); + properties.put(ArrowFlightConnectionProperty.TLS_ROOT_CERTS.camelName(), tlsRootCertsPath); + + final Connection connection = DriverManager.getConnection( + String.format("jdbc:arrow-flight-sql://localhost:%s", + FLIGHT_SERVER_TEST_RULE.getPort()), + properties); + Assert.assertTrue(connection.isValid(0)); + connection.close(); + } +} diff --git a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestRule.java b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestRule.java index 733145892ec3e..df7cbea56ee2f 100644 --- a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestRule.java +++ b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/FlightServerTestRule.java @@ -63,6 +63,7 @@ public class FlightServerTestRule implements TestRule, AutoCloseable { private final FlightSqlProducer producer; private final Authentication authentication; private final CertKeyPair certKeyPair; + private final File mTlsCACert; private final MiddlewareCookie.Factory middlewareCookieFactory = new MiddlewareCookie.Factory(); @@ -71,13 +72,15 @@ private FlightServerTestRule(final Properties properties, final BufferAllocator allocator, final FlightSqlProducer producer, final Authentication authentication, - final CertKeyPair certKeyPair) { + final CertKeyPair certKeyPair, + final File mTlsCACert) { this.properties = Preconditions.checkNotNull(properties); this.config = Preconditions.checkNotNull(config); this.allocator = Preconditions.checkNotNull(allocator); this.producer = Preconditions.checkNotNull(producer); this.authentication = authentication; this.certKeyPair = certKeyPair; + this.mTlsCACert = mTlsCACert; } /** @@ -142,6 +145,9 @@ private FlightServer initiateServer(Location location) throws IOException { if (certKeyPair != null) { builder.useTls(certKeyPair.cert, certKeyPair.key); } + if (mTlsCACert != null) { + builder.useMTlsClientVerification(mTlsCACert); + } return builder.build(); } @@ -212,6 +218,7 @@ public static final class Builder { private FlightSqlProducer producer; private Authentication authentication; private CertKeyPair certKeyPair; + private File mTlsCACert; public Builder() { this.properties = new Properties(); @@ -254,6 +261,17 @@ public Builder useEncryption(final File certChain, final File key) { return this; } + /** + * Enable Client Verification via mTLS on the server. + * + * @param mTlsCACert The CA certificate to use for client verification. + * @return the Builder. + */ + public Builder useMTlsClientVerification(final File mTlsCACert) { + this.mTlsCACert = mTlsCACert; + return this; + } + /** * Builds the {@link FlightServerTestRule} using the provided values. * @@ -262,7 +280,7 @@ public Builder useEncryption(final File certChain, final File key) { public FlightServerTestRule build() { authentication.populateProperties(properties); return new FlightServerTestRule(properties, new ArrowFlightConnectionConfigImpl(properties), - new RootAllocator(Long.MAX_VALUE), producer, authentication, certKeyPair); + new RootAllocator(Long.MAX_VALUE), producer, authentication, certKeyPair, mTlsCACert); } } diff --git a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/FlightSqlTestCertificates.java b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/FlightSqlTestCertificates.java index a2b1864c02657..89e27be9f53da 100644 --- a/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/FlightSqlTestCertificates.java +++ b/java/flight/flight-sql-jdbc-core/src/test/java/org/apache/arrow/driver/jdbc/utils/FlightSqlTestCertificates.java @@ -51,6 +51,16 @@ static Path getFlightTestDataRoot() { return getTestDataRoot().resolve("flight"); } + /** + * Create File object with the CA certificate. + * + * @return A File containing the Root CA certificate. + */ + public static File exampleCACert() { + final Path root = getFlightTestDataRoot(); + return root.resolve("root-ca.pem").toFile(); + } + /** * Create CertKeyPair object with the certificates and keys. *