Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-38460: [Java][FlightRPC] Add mTLS support for Flight SQL JDBC driver #38461

Merged
merged 16 commits into from
Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
477e1c4
Got mTLS and TLS_ROOT_CERTS capability working for Arrow Flight SQL J…
prmoore77 Oct 23, 2023
fa52fdc
Added support for FlightServer mTLS. Added tests for Flight SQL JDBC…
prmoore77 Oct 24, 2023
6cfbe98
Got mTLS and TLS_ROOT_CERTS capability working for Arrow Flight SQL J…
prmoore77 Oct 23, 2023
a41d551
Added support for FlightServer mTLS. Added tests for Flight SQL JDBC…
prmoore77 Oct 24, 2023
c45c243
Merge remote-tracking branch 'origin/feature/flight-sql-jdbc-driver-m…
prmoore77 Oct 25, 2023
cc56b64
Minor name fix
prmoore77 Oct 25, 2023
39ffc76
Changed mTLS Java tests to use try-with-resources as recommended by @…
prmoore77 Oct 26, 2023
d852653
Merge branch 'apache:main' into feature/flight-sql-jdbc-driver-mtls
prmoore77 Oct 30, 2023
6ff25f6
Now checking if InputStream attributes are not null (and then closing…
prmoore77 Oct 30, 2023
953f5cd
Added documentation to method: withTlsRootCertificates
prmoore77 Oct 30, 2023
7a04483
Merge branch 'apache:main' into feature/flight-sql-jdbc-driver-mtls
prmoore77 Oct 31, 2023
e9410ce
Merge branch 'apache:main' into feature/flight-sql-jdbc-driver-mtls
prmoore77 Nov 1, 2023
8e46cb9
Update java/flight/flight-core/src/main/java/org/apache/arrow/flight/…
prmoore77 Nov 3, 2023
40d0589
Changes to address feedback on PR
prmoore77 Nov 3, 2023
9697e50
Merge branch 'apache:main' into feature/flight-sql-jdbc-driver-mtls
prmoore77 Nov 6, 2023
1975786
Added new fields to new copy constructor (from: #38521)
prmoore77 Nov 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/source/java/flight_sql_jdbc_driver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
prmoore77 marked this conversation as resolved.
Show resolved Hide resolved
- null
- Path to PEM-encoded client mTLS certificate when the Flight
SQL server requires client verification.

* - clientKey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it be necessary to provide an optional client key password argument if this client private key is password-protected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello – it could be added, but the Java Flight client doesn’t yet support that to my knowledge – so I just wanted to catch up to that for now. We can add it in a later PR, unless you feel strongly that it should be included here.

- null
- Path to PEM-encoded client mTLS key when the Flight
prmoore77 marked this conversation as resolved.
Show resolved Hide resolved
SQL server requires client verification.

* - useEncryption
- true
- Whether to use TLS (the default is an encrypted connection)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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<KeyFactory<?>> interceptors;
// Keep track of inserted interceptors
private final Set<String> interceptorKeys;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Copy link
Member

@jduo jduo Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user calls this method repeatedly we will leak File handles. Need to close the current one if it exists. Perhaps it would be better to defer opening the physical file until starting the connection, though that delays notification if there's a problem opening it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @jduo - I'm not a very good Java developer - but I'm learning (thanks for your patience). Would it be like this?

    public Builder useMTlsClientVerification(final File mTlsCACert) throws IOException {
      if (this.mTlsCACert != null) {
            this.mTlsCACert.close();
        }
      this.mTlsCACert = new FileInputStream(mTlsCACert); 

Thanks for your help!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this would work. However I'm leaning towards deferring opening mTlsCACert until the connection is made. It's odd for builders to have more logic than setting fields.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jduo just for my knowledge and to clarify a point, would it be more easier if we use try-with-resources here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @jduo - this is the FlightServer - I'd rather not change the overall design of the current class's builder. It already uses this approach for useTls - and I think my change is consistent with that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vibhatha , try-with-resources wouldn't help here since we need the stream to have a longer lifetime than this builder function. This is more of a case of tracking ownership.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks @jduo

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jduo - I've added small changes that detect if the InputStream attributes are not null, and if so - closes them. Could you please re-review?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @prmoore77 minor comment about another possible error scenario in useTls() that applies here.

return this;
}

Expand All @@ -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;
Copy link
Member

@jduo jduo Oct 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment as above about this potentially leaking a file. Also, now it is ambiguous whether the Flight client should close() mTlsCACert (presumably it should but there should be a comment).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mirrored the existing useTls method here

Does that code have the issue as well?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. It looks to have this issue too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vibhatha @davisusanibar can you file an issue to fix this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lidavidm I will file one. Will study a bit and file one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that this issue will be filed separately - is it acceptable to approve/merge this PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #38586

return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -560,7 +563,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) {
prmoore77 marked this conversation as resolved.
Show resolved Hide resolved
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.
*/
Expand Down Expand Up @@ -660,14 +698,23 @@ 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) {
clientBuilder.trustedCertificates(
ClientAuthenticationUtils.getCertificateStream(trustStorePath, trustStorePassword));
}
}

if (clientCertificatePath != null && clientKeyPath != null) {
clientBuilder.clientCertificate(
ClientAuthenticationUtils.getClientCertificateStream(clientCertificatePath),
ClientAuthenticationUtils.getClientKeyStream(clientKeyPath));
}
}

client = clientBuilder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> aliases = keyStore.aliases();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);

Expand Down
Loading