SSL/TLS: Added support for different certs per profile

In order to run on different certs per port, we needed to adapt
the logic of starting up.

Also different profiles can now be applied to the N2NAuthenticator, so that
a different profile can allow/deny different hosts.

In addition minor refactorings have been done
* Group keystore/truststore settings instead of using underscores
* Change to transport profile settings instead of using specific shield ones

Documentation has been updated as well

Closes elastic/elasticsearch#290

Original commit: elastic/x-pack-elasticsearch@ad1ab974ea
This commit is contained in:
Alexander Reelsen 2014-11-21 17:02:58 +01:00
parent f2abfb35f9
commit 2b108203fb
24 changed files with 401 additions and 157 deletions

View File

@ -23,24 +23,29 @@ import java.util.Arrays;
* get SSLEngines and SocketFactories.
*/
public class SSLService extends AbstractComponent {
static final String[] DEFAULT_CIPHERS = new String[] { "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" };
static final String[] DEFAULT_CIPHERS = new String[]{"TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"};
public static final String SHIELD_TRANSPORT_SSL = "shield.transport.ssl";
public static final String SHIELD_HTTP_SSL = "shield.http.ssl";
public static final String SHIELD_AUTHC_LDAP_URL = "shield.authc.ldap.url";
private final TrustManagerFactory trustFactory;
private final SSLContext sslContext;
private final String[] ciphers;
private final KeyManagerFactory keyManagerFactory;
private final String sslProtocol;
@Inject
public SSLService(Settings settings) {
super(settings);
String keyStorePath = componentSettings.get("keystore", System.getProperty("javax.net.ssl.keyStore"));
String keyStorePassword = componentSettings.get("keystore_password", System.getProperty("javax.net.ssl.keyStorePassword"));
String keyStoreAlgorithm = componentSettings.get("keystore_algorithm", System.getProperty("ssl.KeyManagerFactory.algorithm", KeyManagerFactory.getDefaultAlgorithm()));
String keyStorePath = componentSettings.get("keystore.path", System.getProperty("javax.net.ssl.keyStore"));
String keyStorePassword = componentSettings.get("keystore.password", System.getProperty("javax.net.ssl.keyStorePassword"));
String keyStoreAlgorithm = componentSettings.get("keystore.algorithm", System.getProperty("ssl.KeyManagerFactory.algorithm", KeyManagerFactory.getDefaultAlgorithm()));
String trustStorePath = componentSettings.get("truststore", System.getProperty("javax.net.ssl.trustStore"));
String trustStorePassword = componentSettings.get("truststore_password", System.getProperty("javax.net.ssl.trustStorePassword"));
String trustStoreAlgorithm = componentSettings.get("truststore_algorithm", System.getProperty("ssl.TrustManagerFactory.algorithm", TrustManagerFactory.getDefaultAlgorithm()));
String trustStorePath = componentSettings.get("truststore.path", System.getProperty("javax.net.ssl.trustStore"));
String trustStorePassword = componentSettings.get("truststore.password", System.getProperty("javax.net.ssl.trustStorePassword"));
String trustStoreAlgorithm = componentSettings.get("truststore.algorithm", System.getProperty("ssl.TrustManagerFactory.algorithm", TrustManagerFactory.getDefaultAlgorithm()));
if (trustStorePath == null) {
//the keystore will also be the truststore
@ -57,46 +62,14 @@ public class SSLService extends AbstractComponent {
this.ciphers = componentSettings.getAsArray("ciphers", DEFAULT_CIPHERS);
//protocols supported: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext
String sslProtocol = componentSettings.get("protocol", "TLS");
this.sslProtocol = componentSettings.get("protocol", "TLS");
logger.debug("using keyStore[{}], keyAlgorithm[{}], trustStore[{}], truststoreAlgorithm[{}], ciphersuites[{}], TLS protocol[{}]",
keyStorePath, keyStoreAlgorithm, trustStorePath, trustStoreAlgorithm, ciphers, sslProtocol);
final TrustManagerFactory trustFactory;
try (FileInputStream in = new FileInputStream(trustStorePath)) {
// Load TrustStore
KeyStore trustStore = KeyStore.getInstance("jks");
trustStore.load(in, trustStorePassword == null ? null : trustStorePassword.toCharArray());
// Initialize a trust manager factory with the trusted store
trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm);
trustFactory.init(trustStore);
} catch (Exception e) {
throw new ElasticsearchException("Failed to initialize a TrustManagerFactory", e);
}
KeyStore keyStore;
KeyManagerFactory keyManagerFactory;
try (FileInputStream in = new FileInputStream(keyStorePath)){
// Load KeyStore
keyStore = KeyStore.getInstance("jks");
keyStore.load(in, keyStorePassword.toCharArray());
// Initialize KeyManagerFactory
keyManagerFactory = KeyManagerFactory.getInstance(keyStoreAlgorithm);
keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
} catch (Exception e) {
throw new ElasticsearchSSLException("Failed to initialize a KeyManagerFactory", e);
}
// Initialize sslContext
try {
sslContext = SSLContext.getInstance(sslProtocol);
sslContext.init(keyManagerFactory.getKeyManagers(), trustFactory.getTrustManagers(), null);
} catch (Exception e) {
throw new ElasticsearchSSLException("Failed to initialize the SSLContext", e);
}
this.trustFactory = getTrustFactory(trustStorePath, trustStorePassword, trustStoreAlgorithm);
this.keyManagerFactory = createKeyManagerFactory(keyStorePath, keyStorePassword, keyStoreAlgorithm);
this.sslContext = createSslContext(trustFactory);
}
/**
@ -110,26 +83,86 @@ public class SSLService extends AbstractComponent {
* This engine is configured with a trust manager and a keystore that should have only one private key.
* Four possible usages for elasticsearch exist:
* Node-to-Node outbound:
* - sslEngine.setUseClientMode(true)
* - sslEngine.setUseClientMode(true)
* Node-to-Node inbound:
* - sslEngine.setUseClientMode(false)
* - sslEngine.setNeedClientAuth(true)
* - sslEngine.setUseClientMode(false)
* - sslEngine.setNeedClientAuth(true)
* Client-to-Node:
* - sslEngine.setUseClientMode(true)
* - sslEngine.setUseClientMode(true)
* Http Client-to-Node (inbound):
* - sslEngine.setUserClientMode(false)
* - sslEngine.setNeedClientAuth(false)
* - sslEngine.setUserClientMode(false)
* - sslEngine.setNeedClientAuth(false)
*/
public SSLEngine createSSLEngine() {
return createSSLEngine(this.sslContext);
}
public SSLEngine createSSLEngineWithTruststore(Settings settings) {
String trustStore = settings.get("truststore.path", System.getProperty("javax.net.ssl.trustStore"));
String trustStorePassword = settings.get("truststore.password", System.getProperty("javax.net.ssl.trustStorePassword"));
String trustStoreAlgorithm = settings.get("truststore.algorithm", System.getProperty("ssl.TrustManagerFactory.algorithm", TrustManagerFactory.getDefaultAlgorithm()));
// no truststore or password, return regular ssl engine
if (trustStore == null || trustStorePassword == null) {
return createSSLEngine();
}
TrustManagerFactory trustFactory = getTrustFactory(trustStore, trustStorePassword, trustStoreAlgorithm);
SSLContext customTruststoreSSLContext = createSslContext(trustFactory);
return createSSLEngine(customTruststoreSSLContext);
}
private SSLEngine createSSLEngine(SSLContext sslContext) {
SSLEngine sslEngine = sslContext.createSSLEngine();
try {
sslEngine.setEnabledCipherSuites(ciphers);
} catch (Throwable t) {
throw new ElasticsearchSSLException("Error loading cipher suites ["+ Arrays.asList(ciphers)+"]", t);
throw new ElasticsearchSSLException("Error loading cipher suites [" + Arrays.asList(ciphers) + "]", t);
}
return sslEngine;
}
private KeyManagerFactory createKeyManagerFactory(String keyStore, String keyStorePassword, String keyStoreAlgorithm) {
try (FileInputStream in = new FileInputStream(keyStore)) {
// Load KeyStore
KeyStore ks = KeyStore.getInstance("jks");
ks.load(in, keyStorePassword.toCharArray());
// Initialize KeyManagerFactory
KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyStoreAlgorithm);
kmf.init(ks, keyStorePassword.toCharArray());
return kmf;
} catch (Exception e) {
throw new ElasticsearchSSLException("Failed to initialize a KeyManagerFactory", e);
}
}
private SSLContext createSslContext(TrustManagerFactory trustFactory) {
// Initialize sslContext
try {
SSLContext sslContext = SSLContext.getInstance(sslProtocol);
sslContext.init(keyManagerFactory.getKeyManagers(), trustFactory.getTrustManagers(), null);
return sslContext;
} catch (Exception e) {
throw new ElasticsearchSSLException("Failed to initialize the SSLContext", e);
}
}
private TrustManagerFactory getTrustFactory(String trustStore, String trustStorePassword, String trustStoreAlgorithm) {
try (FileInputStream in = new FileInputStream(trustStore)) {
// Load TrustStore
KeyStore ks = KeyStore.getInstance("jks");
ks.load(in, trustStorePassword == null ? null : trustStorePassword.toCharArray());
// Initialize a trust manager factory with the trusted store
TrustManagerFactory trustFactory = TrustManagerFactory.getInstance(trustStoreAlgorithm);
trustFactory.init(ks);
return trustFactory;
} catch (Exception e) {
throw new ElasticsearchException("Failed to initialize a TrustManagerFactory", e);
}
}
public static boolean isSSLEnabled(Settings settings) {
return settings.getAsBoolean(SHIELD_TRANSPORT_SSL, false) ||
settings.getAsBoolean(SHIELD_HTTP_SSL, false) ||

View File

@ -8,13 +8,11 @@ package org.elasticsearch.shield.transport;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.inject.PreProcessModule;
import org.elasticsearch.common.inject.util.Providers;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.shield.SecurityFilter;
import org.elasticsearch.shield.plugin.ShieldPlugin;
import org.elasticsearch.shield.support.AbstractShieldModule;
import org.elasticsearch.shield.transport.n2n.IPFilteringN2NAuthenticator;
import org.elasticsearch.shield.transport.netty.N2NNettyUpstreamHandler;
import org.elasticsearch.shield.transport.netty.NettySecuredHttpServerTransportModule;
import org.elasticsearch.shield.transport.netty.NettySecuredTransportModule;
import org.elasticsearch.transport.TransportModule;
@ -52,7 +50,6 @@ public class SecuredTransportModule extends AbstractShieldModule.Spawn implement
if (clientMode) {
// no ip filtering on the client
bind(N2NNettyUpstreamHandler.class).toProvider(Providers.<N2NNettyUpstreamHandler>of(null));
bind(ServerTransportFilter.class).toInstance(ServerTransportFilter.NOOP);
return;
}
@ -60,9 +57,6 @@ public class SecuredTransportModule extends AbstractShieldModule.Spawn implement
bind(ServerTransportFilter.class).to(SecurityFilter.ServerTransport.class).asEagerSingleton();
if (settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true)) {
bind(IPFilteringN2NAuthenticator.class).asEagerSingleton();
bind(N2NNettyUpstreamHandler.class).asEagerSingleton();
} else {
bind(N2NNettyUpstreamHandler.class).toProvider(Providers.<N2NNettyUpstreamHandler>of(null));
}
}
}

View File

@ -41,11 +41,11 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2
private static final Pattern COMMA_DELIM = Pattern.compile("\\s*,\\s*");
private static final String DEFAULT_FILE = "ip_filter.yml";
private static final IpFilterRule[] NO_RULES = new IpFilterRule[0];
private static final ProfileIpFilterRule[] NO_RULES = new ProfileIpFilterRule[0];
private final Path file;
private volatile IpFilterRule[] rules = NO_RULES;
private volatile ProfileIpFilterRule[] rules = NO_RULES;
@Inject
public IPFilteringN2NAuthenticator(Settings settings, Environment env, ResourceWatcherService watcherService) {
@ -65,12 +65,12 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2
return Paths.get(location);
}
public static IpFilterRule[] parseFile(Path path, ESLogger logger) {
public static ProfileIpFilterRule[] parseFile(Path path, ESLogger logger) {
if (!Files.exists(path)) {
return NO_RULES;
}
List<IpFilterRule> rules = new ArrayList<>();
List<ProfileIpFilterRule> rules = new ArrayList<>();
try (XContentParser parser = YamlXContent.yamlXContent.createParser(Files.newInputStream(path))) {
XContentParser.Token token;
@ -78,8 +78,8 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT && token != null) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
if (!"allow".equals(currentFieldName) && !"deny".equals(currentFieldName)) {
throw new ElasticsearchParseException("Field name [" + currentFieldName + "] not valid. Must be [allow] or [deny]");
if (!currentFieldName.endsWith("allow") && !currentFieldName.endsWith("deny")) {
throw new ElasticsearchParseException("Field name [" + currentFieldName + "] not valid. Must be [allow] or [deny] or using a profile");
}
} else if (token == XContentParser.Token.VALUE_STRING && currentFieldName != null) {
String value = parser.text();
@ -87,14 +87,15 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2
throw new ElasticsearchParseException("Field value for fieldname [" + currentFieldName + "] must not be empty");
}
boolean isAllowRule = currentFieldName.equals("allow");
boolean isAllowRule = currentFieldName.endsWith("allow");
String profile = currentFieldName.contains(".") ? currentFieldName.substring(0, currentFieldName.indexOf(".")) : "default";
if (value.contains(",")) {
for (String rule : COMMA_DELIM.split(parser.text().trim())) {
rules.add(getRule(isAllowRule, rule));
rules.add(new ProfileIpFilterRule(profile, getRule(isAllowRule, rule)));
}
} else {
rules.add(getRule(isAllowRule, value));
rules.add(new ProfileIpFilterRule(profile, getRule(isAllowRule, value)));
}
}
@ -108,7 +109,7 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2
}
logger.debug("Loaded {} ip filtering rules", rules.size());
return rules.toArray(new IpFilterRule[rules.size()]);
return rules.toArray(new ProfileIpFilterRule[rules.size()]);
}
private static IpFilterRule getRule(boolean isAllowRule, String value) throws UnknownHostException {
@ -124,9 +125,9 @@ public class IPFilteringN2NAuthenticator extends AbstractComponent implements N2
}
@Override
public boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort) {
for (IpFilterRule rule : rules) {
if (rule.contains(peerAddress)) {
public boolean authenticate(@Nullable Principal peerPrincipal, String profile, InetAddress peerAddress, int peerPort) {
for (ProfileIpFilterRule rule : rules) {
if (rule.contains(profile, peerAddress)) {
boolean isAllowed = rule.isAllowRule();
logger.trace("Authentication rule matched for host [{}]: {}", peerAddress, isAllowed);
return isAllowed;

View File

@ -15,32 +15,6 @@ import java.security.Principal;
*/
public interface N2NAuthenticator {
N2NAuthenticator NO_AUTH = new N2NAuthenticator() {
@Override
public boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort) {
return true;
}
};
boolean authenticate(@Nullable Principal peerPrincipal, @Nullable String profile, InetAddress peerAddress, int peerPort);
boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort);
class Compound implements N2NAuthenticator {
private N2NAuthenticator[] authenticators;
public Compound(N2NAuthenticator... authenticators) {
this.authenticators = authenticators;
}
@Override
public boolean authenticate(@Nullable Principal peerPrincipal, InetAddress peerAddress, int peerPort) {
for (int i = 0; i < authenticators.length; i++) {
if (!authenticators[i].authenticate(peerPrincipal, peerAddress, peerPort)) {
return false;
}
}
return true;
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.transport.n2n;
import org.elasticsearch.common.netty.handler.ipfilter.IpFilterRule;
import java.net.InetAddress;
/**
* helper interface for filter rules, which takes a tcp transport profile into account
*/
public class ProfileIpFilterRule {
private final String profile;
private final IpFilterRule ipFilterRule;
public ProfileIpFilterRule(String profile, IpFilterRule ipFilterRule) {
this.profile = profile;
this.ipFilterRule = ipFilterRule;
}
public boolean contains(String profile, InetAddress inetAddress) {
return this.profile.equals(profile) && ipFilterRule.contains(inetAddress);
}
public boolean isAllowRule() {
return ipFilterRule.isAllowRule();
}
public boolean isDenyRule() {
return ipFilterRule.isDenyRule();
}
}

View File

@ -5,7 +5,6 @@
*/
package org.elasticsearch.shield.transport.netty;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.netty.channel.ChannelEvent;
import org.elasticsearch.common.netty.channel.ChannelHandler;
import org.elasticsearch.common.netty.channel.ChannelHandlerContext;
@ -20,17 +19,18 @@ import java.net.InetSocketAddress;
@ChannelHandler.Sharable
public class N2NNettyUpstreamHandler extends IpFilteringHandlerImpl {
private IPFilteringN2NAuthenticator authenticator;
private final IPFilteringN2NAuthenticator authenticator;
private final String profile;
@Inject
public N2NNettyUpstreamHandler(IPFilteringN2NAuthenticator authenticator) {
public N2NNettyUpstreamHandler(IPFilteringN2NAuthenticator authenticator, String profile) {
this.authenticator = authenticator;
this.profile = profile;
}
@Override
protected boolean accept(ChannelHandlerContext channelHandlerContext, ChannelEvent channelEvent, InetSocketAddress inetSocketAddress) throws Exception {
// at this stage no auth has happened, so we do not have any principal anyway
return authenticator.authenticate(null, inetSocketAddress.getAddress(), inetSocketAddress.getPort());
return authenticator.authenticate(null, profile, inetSocketAddress.getAddress(), inetSocketAddress.getPort());
}
}

View File

@ -15,6 +15,7 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.BigArrays;
import org.elasticsearch.http.netty.NettyHttpServerTransport;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.shield.transport.n2n.IPFilteringN2NAuthenticator;
import javax.net.ssl.SSLEngine;
@ -24,17 +25,19 @@ import javax.net.ssl.SSLEngine;
public class NettySecuredHttpServerTransport extends NettyHttpServerTransport {
private final boolean ssl;
private final N2NNettyUpstreamHandler shieldUpstreamHandler;
private final boolean ipFilterEnabled;
private final IPFilteringN2NAuthenticator authenticator;
private final SSLService sslService;
@Inject
public NettySecuredHttpServerTransport(Settings settings, NetworkService networkService, BigArrays bigArrays,
@Nullable N2NNettyUpstreamHandler shieldUpstreamHandler, @Nullable SSLService sslService) {
@Nullable IPFilteringN2NAuthenticator authenticator, @Nullable SSLService sslService) {
super(settings, networkService, bigArrays);
this.authenticator = authenticator;
this.ssl = settings.getAsBoolean("shield.http.ssl", false);
this.sslService = sslService;
this.shieldUpstreamHandler = shieldUpstreamHandler;
assert !ssl || sslService != null : "ssl is enabled yet the ssl service is null";
this.ipFilterEnabled = settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true);
}
@Override
@ -58,8 +61,8 @@ public class NettySecuredHttpServerTransport extends NettyHttpServerTransport {
pipeline.addFirst("ssl", new SslHandler(engine));
}
if (shieldUpstreamHandler != null) {
pipeline.addFirst("ipfilter", shieldUpstreamHandler);
if (ipFilterEnabled) {
pipeline.addFirst("ipfilter", new N2NNettyUpstreamHandler(authenticator, "default"));
}
return pipeline;
}

View File

@ -15,6 +15,7 @@ import org.elasticsearch.common.network.NetworkService;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.BigArrays;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.shield.transport.n2n.IPFilteringN2NAuthenticator;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.netty.NettyTransport;
@ -26,16 +27,18 @@ import javax.net.ssl.SSLEngine;
public class NettySecuredTransport extends NettyTransport {
private final boolean ssl;
private final N2NNettyUpstreamHandler shieldUpstreamHandler;
private final SSLService sslService;
private final boolean ipFilterEnabled;
private final IPFilteringN2NAuthenticator authenticator;
@Inject
public NettySecuredTransport(Settings settings, ThreadPool threadPool, NetworkService networkService, BigArrays bigArrays, Version version,
@Nullable N2NNettyUpstreamHandler shieldUpstreamHandler, @Nullable SSLService sslService) {
@Nullable IPFilteringN2NAuthenticator authenticator, @Nullable SSLService sslService) {
super(settings, threadPool, networkService, bigArrays, version);
this.shieldUpstreamHandler = shieldUpstreamHandler;
this.ssl = settings.getAsBoolean("shield.transport.ssl", false);
this.authenticator = authenticator;
this.ipFilterEnabled = settings.getAsBoolean("shield.transport.n2n.ip_filter.enabled", true);
this.sslService = sslService;
this.ssl = settings.getAsBoolean("shield.transport.ssl", false);
assert !ssl || sslService != null : "ssl is enabled yet the ssl service is null";
}
@ -51,23 +54,31 @@ public class NettySecuredTransport extends NettyTransport {
private class SslServerChannelPipelineFactory extends ServerChannelPipelineFactory {
private final Settings profileSettings;
public SslServerChannelPipelineFactory(NettyTransport nettyTransport, String name, Settings settings, Settings profileSettings) {
super(nettyTransport, name, settings);
this.profileSettings = profileSettings;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = super.getPipeline();
if (ssl) {
SSLEngine serverEngine = sslService.createSSLEngine();
SSLEngine serverEngine;
if (profileSettings.get("shield.truststore.path") != null) {
serverEngine = sslService.createSSLEngineWithTruststore(profileSettings.getByPrefix("shield."));
} else {
serverEngine = sslService.createSSLEngine();
}
serverEngine.setUseClientMode(false);
serverEngine.setNeedClientAuth(true);
pipeline.addFirst("ssl", new SslHandler(serverEngine));
pipeline.replace("dispatcher", "dispatcher", new SecuredMessageChannelHandler(nettyTransport, logger));
}
if (shieldUpstreamHandler != null) {
pipeline.addFirst("ipfilter", shieldUpstreamHandler);
if (ipFilterEnabled) {
pipeline.addFirst("ipfilter", new N2NNettyUpstreamHandler(authenticator, name));
}
return pipeline;
}

View File

@ -37,8 +37,8 @@ public class ActiveDirectoryFactoryTests extends ElasticsearchTestCase {
public static void setTrustStore() throws URISyntaxException {
File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile();
LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore", filename)
.put("shield.ssl.keystore_password", "changeit")
.put("shield.ssl.keystore.path", filename)
.put("shield.ssl.keystore.password", "changeit")
.build()));
}

View File

@ -7,8 +7,8 @@ package org.elasticsearch.shield.authc.ldap;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.shield.authc.support.SecuredStringTests;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.shield.authc.support.ldap.LdapSslSocketFactory;
import org.elasticsearch.shield.ssl.SSLService;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.junit.AfterClass;
@ -25,14 +25,13 @@ import static org.hamcrest.Matchers.hasItem;
public class OpenLdapTests extends ElasticsearchTestCase {
public static final String OPEN_LDAP_URL = "ldaps://54.200.235.244:636";
public static final String PASSWORD = "NickFuryHeartsES";
public static final String SETTINGS_PREFIX = LdapRealm.class.getPackage().getName().substring("com.elasticsearch.".length()) + '.';
@BeforeClass
public static void setTrustStore() throws URISyntaxException {
File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile();
LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore", filename)
.put("shield.ssl.keystore_password", "changeit")
.put("shield.ssl.keystore.path", filename)
.put("shield.ssl.keystore.password", "changeit")
.build()));
}

View File

@ -28,8 +28,8 @@ public class LdapSslSocketFactoryTests extends ElasticsearchTestCase {
public static void setTrustStore() throws URISyntaxException {
File filename = new File(LdapConnectionTests.class.getResource("../support/ldap/ldaptrust.jks").toURI()).getAbsoluteFile();
LdapSslSocketFactory.init(new SSLService(ImmutableSettings.builder()
.put("shield.ssl.keystore", filename)
.put("shield.ssl.keystore_password", "changeit")
.put("shield.ssl.keystore.path", filename)
.put("shield.ssl.keystore.password", "changeit")
.build()));
}

View File

@ -14,7 +14,6 @@ import org.elasticsearch.http.HttpServerTransport;
import org.elasticsearch.node.internal.InternalNode;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.test.ShieldIntegrationTest;
import org.elasticsearch.test.junit.annotations.TestLogging;
import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
import org.elasticsearch.test.rest.client.http.HttpResponse;
import org.junit.Test;

View File

@ -5,17 +5,18 @@
*/
package org.elasticsearch.shield.ssl;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import javax.net.ssl.SSLEngine;
import java.io.File;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.*;
public class SSLServiceTests extends ElasticsearchTestCase {
@ -30,10 +31,10 @@ public class SSLServiceTests extends ElasticsearchTestCase {
public void testThatInvalidProtocolThrowsException() throws Exception {
new SSLService(settingsBuilder()
.put("shield.ssl.protocol", "non-existing")
.put("shield.ssl.keystore", testnodeStore.getPath())
.put("shield.ssl.keystore_password", "testnode")
.put("shield.ssl.truststore", testnodeStore.getPath())
.put("shield.ssl.truststore_password", "testnode")
.put("shield.ssl.keystore.path", testnodeStore.getPath())
.put("shield.ssl.keystore.password", "testnode")
.put("shield.ssl.truststore.path", testnodeStore.getPath())
.put("shield.ssl.truststore.password", "testnode")
.build());
}
@ -41,10 +42,10 @@ public class SSLServiceTests extends ElasticsearchTestCase {
public void testSpecificProtocol() {
SSLService ssl = new SSLService(settingsBuilder()
.put("shield.ssl.protocol", "TLSv1.2")
.put("shield.ssl.keystore", testnodeStore.getPath())
.put("shield.ssl.keystore_password", "testnode")
.put("shield.ssl.truststore", testnodeStore.getPath())
.put("shield.ssl.truststore_password", "testnode")
.put("shield.ssl.keystore.path", testnodeStore.getPath())
.put("shield.ssl.keystore.password", "testnode")
.put("shield.ssl.truststore.path", testnodeStore.getPath())
.put("shield.ssl.truststore.password", "testnode")
.build());
assertThat(ssl.createSSLEngine().getSSLParameters().getProtocols(), arrayContaining("TLSv1.2"));
}
@ -111,6 +112,23 @@ public class SSLServiceTests extends ElasticsearchTestCase {
assertSSLEnabled(settings);
}
@Test
public void testThatCustomTruststoreCanBeSpecified() throws Exception {
File testClientStore = new File(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient.jks").toURI());
SSLService sslService = new SSLService(settingsBuilder()
.put("shield.ssl.keystore.path", testnodeStore.getPath())
.put("shield.ssl.keystore.password", "testnode")
.build());
ImmutableSettings.Builder settingsBuilder = settingsBuilder()
.put("truststore.path", testClientStore.getPath())
.put("truststore.password", "testclient");
SSLEngine sslEngineWithTruststore = sslService.createSSLEngineWithTruststore(settingsBuilder.build());
assertThat(sslEngineWithTruststore, is(not(nullValue())));
}
private void assertSSLEnabled(Settings settings) {
assertThat(SSLService.isSSLEnabled(settings), is(true));
}

View File

@ -9,6 +9,9 @@ import com.google.common.base.Charsets;
import com.google.common.net.InetAddresses;
import org.apache.lucene.util.AbstractRandomizedTest;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.os.OsUtils;
import org.elasticsearch.common.settings.ImmutableSettings;
@ -34,6 +37,7 @@ import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilde
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.basicAuthHeaderValue;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTimeout;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
@ -158,13 +162,13 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
ImmutableSettings.Builder builder = settingsBuilder()
.put("shield.transport.ssl", true)
.put("shield.ssl.keystore", store.getPath())
.put("shield.ssl.keystore_password", password)
.put("shield.ssl.keystore.path", store.getPath())
.put("shield.ssl.keystore.password", password)
.put("shield.http.ssl", true);
if (randomBoolean()) {
builder.put("shield.ssl.truststore", store.getPath())
.put("shield.ssl.truststore_password", password);
builder.put("shield.ssl.truststore.path", store.getPath())
.put("shield.ssl.truststore.password", password);
}
return builder.build();
@ -179,4 +183,10 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
return null;
}
}
protected void assertGreenClusterState(Client client) {
ClusterHealthResponse clusterHealthResponse = client.admin().cluster().prepareHealth().get();
assertNoTimeout(clusterHealthResponse);
assertThat(clusterHealthResponse.getStatus(), is(ClusterHealthStatus.GREEN));
}
}

View File

@ -14,6 +14,7 @@ import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ElasticsearchTestCase;
import org.elasticsearch.test.junit.annotations.Network;
import org.elasticsearch.test.junit.annotations.TestLogging;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.junit.After;
@ -141,6 +142,17 @@ public class IPFilteringN2NAuthenticatorTests extends ElasticsearchTestCase {
assertAddressIsAllowed("127.0.0.1");
}
@Test
@TestLogging("_root:TRACE")
public void testThatProfilesAreSupported() throws Exception {
writeConfigFile("allow: localhost\ndeny: all\nclient.allow: 192.168.0.1\nclient.deny: all");
assertAddressIsAllowed("127.0.0.1");
assertAddressIsDenied("192.168.0.1");
assertAddressIsAllowedForProfile("client", "192.168.0.1");
assertAddressIsDeniedForProfile("client", "192.168.0.2");
}
@Test(expected = ElasticsearchParseException.class)
public void testThatInvalidFileThrowsCorrectException() throws Exception {
writeConfigFile("deny: all allow: all \n\n");
@ -155,17 +167,25 @@ public class IPFilteringN2NAuthenticatorTests extends ElasticsearchTestCase {
ipFilteringN2NAuthenticator = new IPFilteringN2NAuthenticator(settings, new Environment(), resourceWatcherService);
}
private void assertAddressIsAllowed(String ... inetAddresses) {
private void assertAddressIsAllowedForProfile(String profile, String ... inetAddresses) {
for (String inetAddress : inetAddresses) {
String message = String.format(Locale.ROOT, "Expected address %s to be allowed", inetAddress);
assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, InetAddresses.forString(inetAddress), 1024), is(true));
assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, profile, InetAddresses.forString(inetAddress), 1024), is(true));
}
}
private void assertAddressIsAllowed(String ... inetAddresses) {
assertAddressIsAllowedForProfile("default", inetAddresses);
}
private void assertAddressIsDeniedForProfile(String profile, String ... inetAddresses) {
for (String inetAddress : inetAddresses) {
String message = String.format(Locale.ROOT, "Expected address %s to be denied", inetAddress);
assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, profile, InetAddresses.forString(inetAddress), 1024), is(false));
}
}
private void assertAddressIsDenied(String ... inetAddresses) {
for (String inetAddress : inetAddresses) {
String message = String.format(Locale.ROOT, "Expected address %s to be denied", inetAddress);
assertThat(message, ipFilteringN2NAuthenticator.authenticate(NULL_PRINCIPAL, InetAddresses.forString(inetAddress), 1024), is(false));
}
assertAddressIsDeniedForProfile("default", inetAddresses);
}
}

View File

@ -47,7 +47,7 @@ public class N2NNettyUpstreamHandlerTests extends ElasticsearchTestCase {
Settings settings = settingsBuilder().put("shield.transport.n2n.ip_filter.file", configFile.getPath()).build();
IPFilteringN2NAuthenticator ipFilteringN2NAuthenticator = new IPFilteringN2NAuthenticator(settings, new Environment(), resourceWatcherService);
nettyUpstreamHandler = new N2NNettyUpstreamHandler(ipFilteringN2NAuthenticator);
nettyUpstreamHandler = new N2NNettyUpstreamHandler(ipFilteringN2NAuthenticator, "default");
}
@After

View File

@ -181,10 +181,4 @@ public class SslIntegrationTests extends ShieldIntegrationTest {
String data = Streams.copyToString(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8));
assertThat(data, containsString("You Know, for Search"));
}
private void assertGreenClusterState(Client client) {
ClusterHealthResponse clusterHealthResponse = client.admin().cluster().prepareHealth().get();
assertNoTimeout(clusterHealthResponse);
assertThat(clusterHealthResponse.getStatus(), is(ClusterHealthStatus.GREEN));
}
}

View File

@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.transport.ssl;
import org.elasticsearch.client.transport.NoNodeAvailableException;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.plugins.PluginsService;
import org.elasticsearch.shield.test.ShieldIntegrationTest;
import org.elasticsearch.shield.transport.netty.NettySecuredTransport;
import org.elasticsearch.transport.Transport;
import org.elasticsearch.transport.TransportModule;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.File;
import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
import static org.hamcrest.Matchers.is;
/**
*
*/
public class SslMultiPortTests extends ShieldIntegrationTest {
private ImmutableSettings.Builder builder;
private static int randomClientPort;
@BeforeClass
public static void getRandomPort() {
randomClientPort = randomIntBetween(49000, 65500); // ephemeral port
}
@Before
public void setupBuilder() {
builder = settingsBuilder()
.put(TransportModule.TRANSPORT_TYPE_KEY, NettySecuredTransport.class.getName())
.put("plugins." + PluginsService.LOAD_PLUGIN_FROM_CLASSPATH, false)
.put("node.mode", "network")
.put("cluster.name", internalCluster().getClusterName());
setUser(builder);
}
@Override
protected Settings nodeSettings(int nodeOrdinal) {
String randomClientPortRange = randomClientPort + "-" + (randomClientPort+100);
File store;
try {
store = new File(getClass().getResource("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode-client-profile.jks").toURI());
assertThat(store.exists(), is(true));
} catch (Exception e) {
throw new RuntimeException(e);
}
return settingsBuilder()
.put(super.nodeSettings(nodeOrdinal))
// settings for default key profile
.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testnode.jks", "testnode"))
// client set up here
.put("transport.profiles.client.port", randomClientPortRange)
.put("transport.profiles.client.bind_host", "localhost") // make sure this is "localhost", no matter if ipv4 or ipv6, but be consistent
.put("transport.profiles.client.shield.truststore.path", store.getAbsolutePath()) // settings for client truststore
.put("transport.profiles.client.shield.truststore.password", "testnode-client-profile")
.put("shield.audit.enabled", false )
.build();
}
@Test
public void testThatStandardTransportClientCanConnectToDefaultProfile() throws Exception {
builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient.jks", "testclient"));
try (TransportClient transportClient = new TransportClient(builder, false)) {
TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress();
transportClient.addTransportAddress(transportAddress);
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatStandardTransportClientCannotConnectToClientProfile() throws Exception {
builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient.jks", "testclient"));
try (TransportClient transportClient = new TransportClient(builder, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", randomClientPort));
transportClient.admin().cluster().prepareHealth().get();
}
}
@Test
public void testThatProfileTransportClientCanConnectToClientProfile() throws Exception {
builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile"));
try (TransportClient transportClient = new TransportClient(builder, false)) {
transportClient.addTransportAddress(new InetSocketTransportAddress("localhost", randomClientPort));
assertGreenClusterState(transportClient);
}
}
@Test(expected = NoNodeAvailableException.class)
public void testThatProfileTransportClientCannotConnectToDefaultProfile() throws Exception {
builder.put(getSSLSettingsForStore("/org/elasticsearch/shield/transport/ssl/certs/simple/testclient-client-profile.jks", "testclient-client-profile"));
try (TransportClient transportClient = new TransportClient(builder, false)) {
TransportAddress transportAddress = internalCluster().getInstance(Transport.class).boundAddress().boundAddress();
transportClient.addTransportAddress(transportAddress);
transportClient.admin().cluster().prepareHealth().get();
}
}
}

View File

@ -109,10 +109,10 @@ public class ShieldRestTests extends ElasticsearchRestTests {
.put("shield.authz.store.files.roles", createFile(folder, "roles.yml", CONFIG_ROLE_ALLOW_ALL))
.put("shield.transport.n2n.ip_filter.file", createFile(folder, "ip_filter.yml", CONFIG_IPFILTER_ALLOW_ALL))
.put("shield.transport.ssl", ENABLE_TRANSPORT_SSL)
.put("shield.ssl.keystore", store.getPath())
.put("shield.ssl.keystore_password", password)
.put("shield.ssl.truststore", store.getPath())
.put("shield.ssl.truststore_password", password)
.put("shield.ssl.keystore.path", store.getPath())
.put("shield.ssl.keystore.password", password)
.put("shield.ssl.truststore.path", store.getPath())
.put("shield.ssl.truststore.password", password)
.put("shield.http.ssl", false)
.put("transport.tcp.port", BASE_PORT_RANGE)
.putArray("discovery.zen.ping.unicast.hosts", "127.0.0.1:" + BASE_PORT, "127.0.0.1:" + (BASE_PORT + 1), "127.0.0.1:" + (BASE_PORT + 2), "127.0.0.1:" + (BASE_PORT + 3))
@ -146,10 +146,10 @@ public class ShieldRestTests extends ElasticsearchRestTests {
.put("node.mode", "network")
.put("shield.transport.n2n.ip_filter.file", createFile(folder, "ip_filter.yml", CONFIG_IPFILTER_ALLOW_ALL))
.put("shield.transport.ssl", ENABLE_TRANSPORT_SSL)
.put("shield.ssl.keystore", store.getPath())
.put("shield.ssl.keystore_password", password)
.put("shield.ssl.truststore", store.getPath())
.put("shield.ssl.truststore_password", password)
.put("shield.ssl.keystore.path", store.getPath())
.put("shield.ssl.keystore.password", password)
.put("shield.ssl.truststore.path", store.getPath())
.put("shield.ssl.truststore.password", password)
.put("cluster.name", internalCluster().getClusterName())
.build();
}

View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEWHpBBTANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAw
DgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYD
VQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MTEwNTEwMzMxMVoXDTE1MDIwMzEw
MzMxMVowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5r
bm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93
bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANMk6NoZl5kbrt9ycsCAms/aivFvmd17
OTwNPqVvsEa7/uCdaDAiYvUtdjs8LMh7uN5s/6DuimpmbKh/XmT9wljWGpT/zPQQhhontvxefXCr
Gp6z8Bs6z/xrbN8GU+M6D4AFOOnZ8YdlXEmFrCCdp7Nu6eqEUa5ui/1HLrTvey0xN2geZHwfHyPw
ZY9eRlLn2v9lzilzrd3H8AIJ4vBBUx44/CP90ocYUSArLOm8TFvRSOfp/vqz0j6gnGOebN9e4a0B
gAZVQmN8g+SrJsRNGgjGgLj7AjQxh9iThpWIWNeJ2UTeuswvANvxna0zRcr6fejmtaAYO9SST76c
oaZSvS0CAwEAAaMhMB8wHQYDVR0OBBYEFJnOrwiIIEz8XMVIXL8g3QRgtUafMA0GCSqGSIb3DQEB
CwUAA4IBAQCYQW+1efFngQbxDs1jZp+rBAeQ2rQFc4arWx4HOCaRyjlPCwNpjwlN3JM+OtqqR4Z/
1HMRpPjgdayiTQ3HsVRWzMVm4NCVHx5LmahMCHv+1mru4Ut7BbbupgAlsQ3vtKcgKIdTVhO8vJlP
IYprm388k06/t3CuQJSaCNxElpe3kIldXMeFRKi2TcqOXjb/Nw2L+gZz/+XJLWLOmoAy+2Pq629f
bk1d/lG3pf0QV/X1kcMiHV320iI/CZJWvwuJK73Ukg1RG8CTYt649R1Trqns6bimFyOMMnBlNtD5
PJiZ0+528XTNbA6Vhz/bXN2g7lDtedy6xJSO1ULFFE9/iUKX
-----END CERTIFICATE-----

View File

@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEHucrFzANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdVbmtub3duMRAw
DgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMRAwDgYD
VQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3duMB4XDTE0MTEwNDE3MTUxNVoXDTE1MDIwMjE3
MTUxNVowbDEQMA4GA1UEBhMHVW5rbm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5r
bm93bjEQMA4GA1UEChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93
bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKrBa2XNWQUk8+TdwG1ZSiwGfQOKNQko
JoX7Cx977L3RQIEs9Q2JxsSRM3wh4uBZzrZ/NCnxKOtw3bKC6B9dUJLXXwZFc7YTtNfcZr8S2000
UW6mre/4u54wnkJD/ahuoZ/FCUE7ETB+Jeg3uDhyoUCcySo96OPvZpy/ctXTmkDuai3h+0NvUgpY
yll2LcWBaottW0X6YveCwF78CNDVCSLRjGKqLa3QWFGFMQWdmYrzIaCb1e4U0/8WHM0ylw/vYuvz
u+BDXOsoIPUn4eDdeWtxXA/ZGhjDCfTb0GWSSBfmciMY6yIBpQalt/yRQ/2AL9t+G2fc0th2FzD6
2UZOexMCAwEAAaMhMB8wHQYDVR0OBBYEFEL891TiYG3R/E5kGjxVY+SwAY5BMA0GCSqGSIb3DQEB
CwUAA4IBAQB372cFMKkLlnH2JbMMtVooXWjF40TJdUOU/ImB+i7rLdVUFX/HmexiL3nDziwOMhTH
N3iEDlxcBeVP+XxZouStwAZP0MmezoGiEjRG53w8gBHSkiWmkKBHYZe1JedeZxEWQCCI0zMh14PY
j5kmgD/sFSvWJCP8UpJnSKTj4ZKAiITmsgSJL/0A5R2Af2lTD5k5sQyvNt1im6atuKdnO96Iqthr
8WRpeyOh5xDZht/KGphZSQyjEpF/RVXWXstkqrKMZRZlKW0EcuBHX4EsTNuzRYC19ReD7d2/CM5M
u9u+iTR1Kws0r3YX4cMnlLXVzJPlAzUrbXmYAMYtpbbYT9QW
-----END CERTIFICATE-----