Skip to content

Commit

Permalink
GEODE-5338: Geode client to support Trust and Keystore rotation (apac…
Browse files Browse the repository at this point in the history
…he#2244)

A new SSL property 'ssl-use-default-context' is added to let Geode use
default SSL context. When set to true Geode uses default SSL context as
returned by SSLContext.getInstance('Default') or uses the context as set
by using SSLContext.setDefault().

Hostname validation is enabled when using default context
  • Loading branch information
sboorlagadda authored Sep 17, 2018
1 parent f289dfe commit 7890652
Show file tree
Hide file tree
Showing 13 changed files with 631 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
/*
* 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.geode.cache.client.internal;

import static org.apache.geode.distributed.ConfigurationProperties.SSL_ENABLED_COMPONENTS;
import static org.apache.geode.distributed.ConfigurationProperties.SSL_ENDPOINT_IDENTIFICATION_ENABLED;
import static org.apache.geode.distributed.ConfigurationProperties.SSL_KEYSTORE;
import static org.apache.geode.distributed.ConfigurationProperties.SSL_REQUIRE_AUTHENTICATION;
import static org.apache.geode.distributed.ConfigurationProperties.SSL_TRUSTSTORE;
import static org.apache.geode.distributed.ConfigurationProperties.SSL_USE_DEFAULT_CONTEXT;
import static org.apache.geode.security.SecurableCommunicationChannels.ALL;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

import java.io.IOException;
import java.net.InetAddress;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.Properties;

import javax.net.ssl.SSLContext;

import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionFactory;
import org.apache.geode.cache.RegionShortcut;
import org.apache.geode.cache.client.ClientCache;
import org.apache.geode.cache.client.ClientCacheFactory;
import org.apache.geode.cache.client.ClientRegionFactory;
import org.apache.geode.cache.client.ClientRegionShortcut;
import org.apache.geode.cache.client.NoAvailableServersException;
import org.apache.geode.cache.client.internal.provider.CustomKeyManagerFactory;
import org.apache.geode.cache.client.internal.provider.CustomTrustManagerFactory;
import org.apache.geode.cache.ssl.CertStores;
import org.apache.geode.cache.ssl.TestSSLUtils.CertificateBuilder;
import org.apache.geode.distributed.internal.tcpserver.LocatorCancelException;
import org.apache.geode.internal.net.SocketCreatorFactory;
import org.apache.geode.test.dunit.IgnoredException;
import org.apache.geode.test.dunit.rules.ClusterStartupRule;
import org.apache.geode.test.dunit.rules.MemberVM;
import org.apache.geode.test.junit.categories.ClientServerTest;

@Category({ClientServerTest.class})
public class CustomSSLProviderDistributedTest {
private static MemberVM locator;
private static MemberVM server;

@Rule
public ClusterStartupRule cluster = new ClusterStartupRule();

private CustomKeyManagerFactory.PKIXFactory keyManagerFactory;
private CustomTrustManagerFactory.PKIXFactory trustManagerFactory;

private void setupCluster(Properties locatorSSLProps, Properties serverSSLProps) {
// create a cluster
locator = cluster.startLocatorVM(0, locatorSSLProps);
server = cluster.startServerVM(1, serverSSLProps, locator.getPort());

// create region
server.invoke(CustomSSLProviderDistributedTest::createServerRegion);
locator.waitUntilRegionIsReadyOnExactlyThisManyServers("/region", 1);
}

private static void createServerRegion() {
RegionFactory factory =
ClusterStartupRule.getCache().createRegionFactory(RegionShortcut.REPLICATE);
Region r = factory.create("region");
r.put("serverkey", "servervalue");
}

@Test
public void hostNameIsValidatedWhenUsingDefaultContext() throws Exception {
CertificateBuilder locatorCertificate = new CertificateBuilder()
.commonName("locator")
// ClusterStartupRule uses 'localhost' as locator host
.sanDnsName(InetAddress.getLoopbackAddress().getHostName())
.sanDnsName(InetAddress.getLocalHost().getHostName())
.sanIpAddress(InetAddress.getLocalHost())
.sanIpAddress(InetAddress.getByName("0.0.0.0")); // to pass on windows

CertificateBuilder serverCertificate = new CertificateBuilder()
.commonName("server")
.sanDnsName(InetAddress.getLocalHost().getHostName())
.sanIpAddress(InetAddress.getLocalHost());

CertificateBuilder clientCertificate = new CertificateBuilder()
.commonName("client");

validateClientSSLConnection(locatorCertificate, serverCertificate, clientCertificate, true,
true, false, null);
}

@Test
public void clientCanChooseNotToValidateHostName() throws Exception {
CertificateBuilder locatorCertificate = new CertificateBuilder()
.commonName("locator");

CertificateBuilder serverCertificate = new CertificateBuilder()
.commonName("server");

CertificateBuilder clientCertificate = new CertificateBuilder()
.commonName("client");

validateClientSSLConnection(locatorCertificate, serverCertificate, clientCertificate, false,
false, true, null);
}

@Test
public void clientConnectionFailsIfNoHostNameInLocatorKey() throws Exception {
CertificateBuilder locatorCertificate = new CertificateBuilder()
.commonName("locator");

CertificateBuilder serverCertificate = new CertificateBuilder()
.commonName("server");

CertificateBuilder clientCertificate = new CertificateBuilder()
.commonName("client");

validateClientSSLConnection(locatorCertificate, serverCertificate, clientCertificate, false,
false, false, LocatorCancelException.class);
}

@Test
public void clientConnectionFailsWhenWrongHostNameInLocatorKey() throws Exception {
CertificateBuilder locatorCertificate = new CertificateBuilder()
.commonName("locator")
.sanDnsName("example.com");;

CertificateBuilder serverCertificate = new CertificateBuilder()
.commonName("server")
.sanDnsName("example.com");;

CertificateBuilder clientCertificate = new CertificateBuilder()
.commonName("client");

validateClientSSLConnection(locatorCertificate, serverCertificate, clientCertificate, false,
false,
false,
LocatorCancelException.class);
}

@Test
public void expectConnectionFailureWhenNoHostNameInServerKey() throws Exception {
CertificateBuilder locatorCertificateWithSan = new CertificateBuilder()
.commonName("locator")
.sanDnsName(InetAddress.getLoopbackAddress().getHostName())
.sanDnsName(InetAddress.getLocalHost().getHostName())
.sanIpAddress(InetAddress.getLocalHost());

CertificateBuilder serverCertificateWithNoSan = new CertificateBuilder()
.commonName("server");

CertificateBuilder clientCertificate = new CertificateBuilder()
.commonName("client");

validateClientSSLConnection(locatorCertificateWithSan, serverCertificateWithNoSan,
clientCertificate, false, false, false,
NoAvailableServersException.class);
}

private void validateClientSSLConnection(CertificateBuilder locatorCertificate,
CertificateBuilder serverCertificate, CertificateBuilder clientCertificate,
boolean enableHostNameVerficationForLocator, boolean enableHostNameVerificationForServer,
boolean disableHostNameVerificationForClient,
Class expectedExceptionOnClient)
throws GeneralSecurityException, IOException {

CertStores locatorStore = CertStores.locatorStore();
locatorStore.withCertificate(locatorCertificate);

CertStores serverStore = CertStores.serverStore();
serverStore.withCertificate(serverCertificate);

CertStores clientStore = CertStores.clientStore();
clientStore.withCertificate(clientCertificate);

Properties locatorSSLProps = locatorStore
.trustSelf()
.trust(serverStore.alias(), serverStore.certificate())
.trust(clientStore.alias(), clientStore.certificate())
.propertiesWith(ALL, false, enableHostNameVerficationForLocator);

Properties serverSSLProps = serverStore
.trustSelf()
.trust(locatorStore.alias(), locatorStore.certificate())
.trust(clientStore.alias(), clientStore.certificate())
.propertiesWith(ALL, true, enableHostNameVerificationForServer);

// this props is only to create temp keystore and truststore and get paths
Properties clientSSLProps = clientStore
.trust(locatorStore.alias(), locatorStore.certificate())
.trust(serverStore.alias(), serverStore.certificate())
.propertiesWith(ALL, true, true);

setupCluster(locatorSSLProps, serverSSLProps);

// setup client
keyManagerFactory =
new CustomKeyManagerFactory.PKIXFactory(clientSSLProps.getProperty(SSL_KEYSTORE));
keyManagerFactory.engineInit(null, null);

trustManagerFactory =
new CustomTrustManagerFactory.PKIXFactory(clientSSLProps.getProperty(SSL_TRUSTSTORE));
trustManagerFactory.engineInit((KeyStore) null);

SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(keyManagerFactory.engineGetKeyManagers(),
trustManagerFactory.engineGetTrustManagers(), null);
// set default context
SSLContext.setDefault(sslContext);

Properties clientSSLProperties = new Properties();
clientSSLProperties.setProperty(SSL_ENABLED_COMPONENTS, ALL);
clientSSLProperties.setProperty(SSL_REQUIRE_AUTHENTICATION, String.valueOf("true"));
clientSSLProperties.setProperty(SSL_USE_DEFAULT_CONTEXT, String.valueOf("true"));

if (disableHostNameVerificationForClient) {
// client chose to override default
clientSSLProperties.setProperty(SSL_ENDPOINT_IDENTIFICATION_ENABLED, String.valueOf("false"));
}

ClientCacheFactory clientCacheFactory = new ClientCacheFactory(clientSSLProperties);
clientCacheFactory.addPoolLocator(locator.getVM().getHost().getHostName(), locator.getPort());
ClientCache clientCache = clientCacheFactory.create();

ClientRegionFactory<String, String> regionFactory =
clientCache.createClientRegionFactory(ClientRegionShortcut.PROXY);

if (expectedExceptionOnClient != null) {
IgnoredException.addIgnoredException("javax.net.ssl.SSLHandshakeException");
IgnoredException.addIgnoredException("java.net.SocketException");

Region<String, String> clientRegion = regionFactory.create("region");
assertThatExceptionOfType(expectedExceptionOnClient)
.isThrownBy(() -> clientRegion.put("clientkey", "clientvalue"));
} else {
// test client can read and write to server
Region<String, String> clientRegion = regionFactory.create("region");
assertThat("servervalue").isEqualTo(clientRegion.get("serverkey"));
clientRegion.put("clientkey", "clientvalue");

// test server can see data written by client
server.invoke(CustomSSLProviderDistributedTest::doServerRegionTest);
}

SocketCreatorFactory.close();
}

private static void doServerRegionTest() {
Region<String, String> region = ClusterStartupRule.getCache().getRegion("region");
assertThat("servervalue").isEqualTo(region.get("serverkey"));
assertThat("clientvalue").isEqualTo(region.get("clientkey"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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.geode.cache.client.internal.provider;

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.UndeclaredThrowableException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.logging.Logger;

import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManagerFactorySpi;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.X509ExtendedKeyManager;


public abstract class CustomKeyManagerFactory extends KeyManagerFactorySpi {

private final Logger logger = Logger.getLogger(this.getClass().getName());

private final String algorithm;
private final String keyStorePath;
private KeyManagerFactory customKeyManagerFactory;
private X509ExtendedKeyManager customKeyManager;

private CustomKeyManagerFactory(String algorithm, String keyStorePath) {
this.algorithm = algorithm;
this.keyStorePath = keyStorePath;
}

@Override
public final KeyManager[] engineGetKeyManagers() {
X509ExtendedKeyManager systemKeyManager = getCustomKeyManager();
return new KeyManager[] {systemKeyManager};
}

@Override
protected final void engineInit(ManagerFactoryParameters managerFactoryParameters) {
// not supported right now
throw new UnsupportedOperationException("use engineInit with keystore");
}

@Override
public final void engineInit(KeyStore keyStore, char[] chars) {
// ignore the passed in keystore as it will be null
init();
}

private void init() {
String SSL_KEYSTORE_TYPE = "JKS";
String SSL_KEYSTORE_PASSWORD = "password";

try {
FileInputStream fileInputStream = new FileInputStream(keyStorePath);
KeyStore keyStore = KeyStore.getInstance(SSL_KEYSTORE_TYPE);
keyStore.load(fileInputStream, SSL_KEYSTORE_PASSWORD.toCharArray());
this.customKeyManagerFactory = KeyManagerFactory.getInstance(this.algorithm, "SunJSSE");
this.customKeyManagerFactory.init(keyStore, SSL_KEYSTORE_PASSWORD.toCharArray());
} catch (NoSuchAlgorithmException | IOException | CertificateException
| UnrecoverableKeyException | KeyStoreException | NoSuchProviderException e) {
throw new UndeclaredThrowableException(e);
}
}

private X509ExtendedKeyManager getCustomKeyManager() {
if (this.customKeyManager == null) {
for (KeyManager candidate : this.customKeyManagerFactory.getKeyManagers()) {
if (candidate instanceof X509ExtendedKeyManager) {
this.logger.info("Adding System Key Manager");
this.customKeyManager = (X509ExtendedKeyManager) candidate;
break;
}
}
}
return this.customKeyManager;
}

public static final class PKIXFactory extends CustomKeyManagerFactory {
public PKIXFactory(String keyStorePath) {
super("PKIX", keyStorePath);
}
}

public static final class SimpleFactory extends CustomKeyManagerFactory {
public SimpleFactory(String keyStorePath) {
super("SunX509", keyStorePath);
}
}
}
Loading

0 comments on commit 7890652

Please sign in to comment.