com.youtube.vitess.client.grpc.GrpcClientFactory Maven / Gradle / Ivy
package com.youtube.vitess.client.grpc;
import com.youtube.vitess.client.Context;
import com.youtube.vitess.client.RpcClient;
import com.youtube.vitess.client.RpcClientFactory;
import com.youtube.vitess.client.grpc.tls.TlsOptions;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import javax.net.ssl.SSLException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableEntryException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
/**
* GrpcClientFactory creates RpcClients with the gRPC implementation.
*/
public class GrpcClientFactory implements RpcClientFactory {
/**
* Factory method to construct a gRPC client connection with no transport-layer security.
*
* @param ctx TODO: This parameter is not actually used, but probably SHOULD be so that timeout duration and caller ID settings aren't discarded
* @param address
* @return
*/
@Override
public RpcClient create(Context ctx, InetSocketAddress address) {
return new GrpcClient(
NettyChannelBuilder.forAddress(address).negotiationType(NegotiationType.PLAINTEXT).build());
}
/**
* Factory method to construct a gRPC client connection with transport-layer security.
*
* Within the tlsOptions
parameter value, the trustStore
field should
* always be populated. All other fields are optional.
*
* @param ctx TODO: This parameter is not actually used, but probably SHOULD be so that timeout duration and caller ID settings aren't discarded
* @param address
* @param tlsOptions
* @return
*/
@Override
public RpcClient createTls(Context ctx, InetSocketAddress address, TlsOptions tlsOptions) {
final SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient();
// trustManager should always be set
final KeyStore trustStore = loadKeyStore(tlsOptions.getTrustStore(), tlsOptions.getTrustStorePassword());
if (trustStore == null) {
throw new RuntimeException("Could not load trustStore");
}
final X509Certificate[] trustCertCollection = tlsOptions.getTrustAlias() == null
? loadCertCollection(trustStore)
: loadCertCollectionForAlias(trustStore, tlsOptions.getTrustAlias());
sslContextBuilder.trustManager(trustCertCollection);
// keyManager should only be set if a keyStore is specified (meaning that client authentication is enabled)
final KeyStore keyStore = loadKeyStore(tlsOptions.getKeyStore(), tlsOptions.getKeyStorePassword());
if (keyStore != null) {
final PrivateKeyWrapper privateKeyWrapper = tlsOptions.getKeyAlias() == null
? loadPrivateKeyEntry(keyStore, tlsOptions.getKeyStorePassword(), tlsOptions.getKeyPassword())
: loadPrivateKeyEntryForAlias(keyStore, tlsOptions.getKeyAlias(), tlsOptions.getKeyStorePassword(), tlsOptions.getKeyPassword());
if (privateKeyWrapper == null) {
throw new RuntimeException("Could not retrieve private key and certificate chain from keyStore");
}
sslContextBuilder.keyManager(
privateKeyWrapper.getPrivateKey(),
privateKeyWrapper.getPassword(),
privateKeyWrapper.getCertificateChain()
);
}
final SslContext sslContext;
try {
sslContext = sslContextBuilder.build();
} catch (SSLException e) {
throw new RuntimeException(e);
}
return new GrpcClient(
NettyChannelBuilder.forAddress(address).negotiationType(NegotiationType.TLS).sslContext(sslContext).build());
}
/**
* Opens a JKS keystore file from the filesystem.
*
* Returns null
if the file is inaccessible for any reason, or if the password fails to unlock it.
*
* @param keyStoreFile
* @param keyStorePassword
* @return
* @throws KeyStoreException
* @throws IOException
* @throws CertificateException
* @throws NoSuchAlgorithmException
*/
private KeyStore loadKeyStore(final File keyStoreFile, String keyStorePassword) {
if (keyStoreFile == null) {
return null;
}
try {
final KeyStore keyStore = KeyStore.getInstance(Constants.KEYSTORE_TYPE);
final char[] password = keyStorePassword == null ? null : keyStorePassword.toCharArray();
try (final FileInputStream fis = new FileInputStream(keyStoreFile)) {
keyStore.load(fis, password);
}
return keyStore;
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
return null;
}
}
/**
* Loads an X509 certificate from a keystore using a given alias, and returns it as a one-element
* array so that it can be passed to {@link SslContextBuilder#trustManager(File)}.
*
* Returns null
if there is any problem accessing the keystore, or if the alias does
* not match an X509 certificate.
*
* @param keyStore
* @param alias
* @return
*/
private X509Certificate[] loadCertCollectionForAlias(final KeyStore keyStore, final String alias) {
if (keyStore == null) {
return null;
}
try {
return new X509Certificate[] {
(X509Certificate) keyStore.getCertificate(alias)
};
} catch (KeyStoreException | ClassCastException e) {
return null;
}
}
/**
* Loads the first valid X509 certificate found in the keystore, and returns it as a one-element
* array so that it can be passed to {@link SslContextBuilder#trustManager(File)}.
*
* Returns null
if there is any problem accessing the keystore, or if no X509 certificates
* are found at all.
*
* @param keyStore
* @return
*/
private X509Certificate[] loadCertCollection(final KeyStore keyStore) {
if (keyStore == null) {
return null;
}
final Enumeration aliases;
try {
aliases = keyStore.aliases();
} catch (KeyStoreException e) {
return null;
}
while (aliases.hasMoreElements()) {
final String alias = aliases.nextElement();
final X509Certificate[] certCollection = loadCertCollectionForAlias(keyStore, alias);
if (certCollection != null) {
return certCollection;
}
}
return null;
}
/**
* Loads from a keystore the private key entry matching a given alias, and returns it parsed and
* ready to be passed to {@link SslContextBuilder#keyManager(PrivateKey, String, X509Certificate...)}.
*
* To access the private key, this method will first try using the keyPassword
parameter
* value (this parameter can be set to null
to explicitly indicate that there is no password).
* If that fails, then this method will then try using the keyStorePassword
parameter value
* as the key value too.
*
* Returns null if there is any problem accessing the keystore, if neither keyPassword
* or keyStorePassword
can unlock the private key, or if no private key entry matches
* alias
.
*
* @param keyStore
* @param alias
* @param keyStorePassword
* @param keyPassword
* @return
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
* @throws UnrecoverableEntryException
*/
private PrivateKeyWrapper loadPrivateKeyEntryForAlias(final KeyStore keyStore, final String alias,
final String keyStorePassword, final String keyPassword) {
if (keyStore == null || alias == null) {
return null;
}
try {
if (!keyStore.entryInstanceOf(alias, KeyStore.PrivateKeyEntry.class)) {
// There is no private key matching this alias
return null;
}
} catch (KeyStoreException e) {
return null;
}
// Try loading the private key with the key password (which can be null)
try {
final char[] pass = keyPassword == null ? null : keyPassword.toCharArray();
final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, new KeyStore.PasswordProtection(pass));
return new PrivateKeyWrapper(entry.getPrivateKey(), keyPassword, entry.getCertificateChain());
} catch (KeyStoreException | NoSuchAlgorithmException e) {
return null;
} catch (UnrecoverableEntryException e) {
// The key password didn't work (or just wasn't set). Try using the keystore password.
final char[] pass = keyStorePassword == null ? null : keyStorePassword.toCharArray();
try {
final KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, new KeyStore.PasswordProtection(pass));
return new PrivateKeyWrapper(entry.getPrivateKey(), keyPassword, entry.getCertificateChain());
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableEntryException e1) {
// Neither password worked.
return null;
}
}
}
/**
* Loads from a keystore the first valid private key entry found, and returns it parsed and ready to be
* passed to {@link SslContextBuilder#keyManager(PrivateKey, String, X509Certificate...)}.
*
* To access the private key, this method will first try using the keyPassword
parameter
* value (this parameter can be set to null
to explicitly indicate that there is no password).
* If that fails, then this method will then try using the keyStorePassword
parameter value
* as the key value too.
*
* Returns null if there is any problem accessing the keystore, if neither keyPassword
* or keyStorePassword
can unlock the private key, or if no private key entry matches
* alias
.
*
* @param keyStore
* @param keyStorePassword
* @param keyPassword
* @return
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
*/
private PrivateKeyWrapper loadPrivateKeyEntry(final KeyStore keyStore, final String keyStorePassword,
final String keyPassword) {
if (keyStore == null) {
return null;
}
final Enumeration aliases;
try {
aliases = keyStore.aliases();
} catch (KeyStoreException e) {
return null;
}
while (aliases.hasMoreElements()) {
final String alias = aliases.nextElement();
final PrivateKeyWrapper privateKeyWrapper = loadPrivateKeyEntryForAlias(keyStore, alias, keyStorePassword, keyPassword);
if (privateKeyWrapper != null) {
return privateKeyWrapper;
}
}
return null;
}
/**
* A container for the values returned by {@link this#loadPrivateKeyEntry(KeyStore, String, String)} and
* {@link this#loadPrivateKeyEntryForAlias(KeyStore, String, String, String)}, and passed in turn
* to {@link SslContextBuilder#keyManager(PrivateKey, String, X509Certificate...)}. Helpful since Java
* methods cannot return multiple values.
*/
private class PrivateKeyWrapper {
private PrivateKey privateKey;
private String password;
private X509Certificate[] certificateChain;
public PrivateKeyWrapper(final PrivateKey privateKey, final String password, final Certificate[] certificateChain) {
this.privateKey = privateKey;
this.password = password;
this.certificateChain = Arrays.copyOf(certificateChain, certificateChain.length, X509Certificate[].class);
}
public PrivateKey getPrivateKey() {
return privateKey;
}
public String getPassword() {
return password;
}
public X509Certificate[] getCertificateChain() {
return certificateChain;
}
}
}