forked from apache/geode
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
GEODE-5338: Geode client to support Trust and Keystore rotation (apac…
…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
1 parent
f289dfe
commit 7890652
Showing
13 changed files
with
631 additions
and
34 deletions.
There are no files selected for viewing
270 changes: 270 additions & 0 deletions
270
...tedTest/java/org/apache/geode/cache/client/internal/CustomSSLProviderDistributedTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
...tedTest/java/org/apache/geode/cache/client/internal/provider/CustomKeyManagerFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.