From 819b7b37861def1f38f71f3f960f8e0d389ce935 Mon Sep 17 00:00:00 2001 From: Justin Chu Date: Sat, 2 May 2020 14:41:19 -0400 Subject: [PATCH] Fix SSLKsFactory issue Use Netty to create SslContext Add SSLKsFactoryTest Add keystore, truststore, pem files for testing Update markdown files --- .../cql/statements/core/CQLSessionCache.java | 139 ++++--------- driver-cql/src/main/resources/cql.md | 44 +---- driver-cql/src/main/resources/ssl.md | 56 ++++++ driver-tcp/src/main/resources/tcpclient.md | 8 +- driver-tcp/src/main/resources/tcpserver.md | 9 +- .../engine/api/util/SSLKsFactory.java | 182 ++++++++--------- .../engine/api/util/SSLKsFactoryTest.java | 187 ++++++++++++++++++ engine-api/src/test/resources/ssl/cacert.crt | 19 ++ engine-api/src/test/resources/ssl/client.key | 28 +++ engine-api/src/test/resources/ssl/client.p12 | Bin 0 -> 2389 bytes .../src/test/resources/ssl/client_cert.pem | 19 ++ .../test/resources/ssl/server_truststore.p12 | Bin 0 -> 1082 bytes 12 files changed, 446 insertions(+), 245 deletions(-) create mode 100644 driver-cql/src/main/resources/ssl.md create mode 100644 engine-api/src/test/java/io/nosqlbench/engine/api/util/SSLKsFactoryTest.java create mode 100644 engine-api/src/test/resources/ssl/cacert.crt create mode 100644 engine-api/src/test/resources/ssl/client.key create mode 100644 engine-api/src/test/resources/ssl/client.p12 create mode 100644 engine-api/src/test/resources/ssl/client_cert.pem create mode 100644 engine-api/src/test/resources/ssl/server_truststore.p12 diff --git a/driver-cql/src/main/java/io/nosqlbench/activitytype/cql/statements/core/CQLSessionCache.java b/driver-cql/src/main/java/io/nosqlbench/activitytype/cql/statements/core/CQLSessionCache.java index dc92a23ad..c2de632c2 100644 --- a/driver-cql/src/main/java/io/nosqlbench/activitytype/cql/statements/core/CQLSessionCache.java +++ b/driver-cql/src/main/java/io/nosqlbench/activitytype/cql/statements/core/CQLSessionCache.java @@ -1,25 +1,43 @@ package io.nosqlbench.activitytype.cql.statements.core; -import com.datastax.driver.core.*; -import com.datastax.driver.core.policies.*; -import com.datastax.driver.dse.DseCluster; -import io.nosqlbench.activitytype.cql.core.CQLOptions; -import io.nosqlbench.activitytype.cql.core.ProxyTranslator; -import io.nosqlbench.engine.api.activityapi.core.Shutdownable; -import io.nosqlbench.engine.api.activityimpl.ActivityDef; -import io.nosqlbench.engine.api.metrics.ActivityMetrics; -import io.nosqlbench.engine.api.scripting.NashornEvaluator; -import io.nosqlbench.engine.api.util.SSLKsFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.net.ssl.SSLContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.datastax.driver.core.Cluster; +import com.datastax.driver.core.ProtocolOptions; +import com.datastax.driver.core.RemoteEndpointAwareJdkSSLOptions; +import com.datastax.driver.core.RemoteEndpointAwareNettySSLOptions; +import com.datastax.driver.core.SSLOptions; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.policies.DefaultRetryPolicy; +import com.datastax.driver.core.policies.LoadBalancingPolicy; +import com.datastax.driver.core.policies.LoggingRetryPolicy; +import com.datastax.driver.core.policies.RetryPolicy; +import com.datastax.driver.core.policies.RoundRobinPolicy; +import com.datastax.driver.core.policies.SpeculativeExecutionPolicy; +import com.datastax.driver.core.policies.WhiteListPolicy; +import com.datastax.driver.dse.DseCluster; +import io.netty.handler.ssl.SslContext; +import io.nosqlbench.activitytype.cql.core.CQLOptions; +import io.nosqlbench.activitytype.cql.core.ProxyTranslator; +import io.nosqlbench.engine.api.activityapi.core.Shutdownable; +import io.nosqlbench.engine.api.activityimpl.ActivityDef; +import io.nosqlbench.engine.api.metrics.ActivityMetrics; +import io.nosqlbench.engine.api.scripting.NashornEvaluator; +import io.nosqlbench.engine.api.util.SSLKsFactory; public class CQLSessionCache implements Shutdownable { @@ -202,99 +220,12 @@ public class CQLSessionCache implements Shutdownable { .map(CQLOptions::withCompression) .ifPresent(builder::withCompression); - if (activityDef.getParams().getOptionalString("ssl").isPresent()) { - logger.info("Cluster builder proceeding with SSL but no Client Auth"); - Object context = SSLKsFactory.get().getContext(activityDef); - SSLOptions sslOptions; - if (context instanceof javax.net.ssl.SSLContext) { - sslOptions = RemoteEndpointAwareJdkSSLOptions.builder() - .withSSLContext((javax.net.ssl.SSLContext) context).build(); - builder.withSSL(sslOptions); - } else if (context instanceof io.netty.handler.ssl.SslContext) { - sslOptions = - new RemoteEndpointAwareNettySSLOptions((io.netty.handler.ssl.SslContext) context); - } else { - throw new RuntimeException("Unrecognized ssl context object type: " + context.getClass().getCanonicalName()); - } + SslContext context = SSLKsFactory.get().getContext(activityDef); + if (context != null) { + SSLOptions sslOptions = new RemoteEndpointAwareNettySSLOptions(context); builder.withSSL(sslOptions); } -// JdkSSLOptions sslOptions = RemoteEndpointAwareJdkSSLOptions -// .builder() -// .withSSLContext(context) -// .build(); -// builder.withSSL(sslOptions); -// -// } -// -// boolean sslEnabled = activityDef.getParams().getOptionalBoolean("ssl").orElse(false); -// boolean jdkSslEnabled = activityDef.getParams().getOptionalBoolean("jdkssl").orElse(false); -// if (jdkSslEnabled){ -// sslEnabled = true; -// } -// -// // used for OpenSSL -// boolean openSslEnabled = activityDef.getParams().getOptionalBoolean("openssl").orElse(false); -// -// if (sslEnabled && openSslEnabled) { -// logger.error("You cannot enable both OpenSSL and JDKSSL, please pick one and try again!"); -// System.exit(2); -// } -// -// if (sslEnabled) { -// logger.info("Cluster builder proceeding with SSL but no Client Auth"); -// SSLContext context = SSLKsFactory.get().getContext(activityDef); -// JdkSSLOptions sslOptions = RemoteEndpointAwareJdkSSLOptions -// .builder() -// .withSSLContext(context) -// .build(); -// builder.withSSL(sslOptions); -// } -// else if (openSslEnabled) { -// logger.info("Cluster builder proceeding with SSL and Client Auth"); -// String keyPassword = activityDef.getParams().getOptionalString("keyPassword").orElse(null); -// String caCertFileLocation = activityDef.getParams().getOptionalString("caCertFilePath").orElse(null); -// String certFileLocation = activityDef.getParams().getOptionalString("certFilePath").orElse(null); -// String keyFileLocation = activityDef.getParams().getOptionalString("keyFilePath").orElse(null); -// -// -// try { -// -// KeyStore ks = KeyStore.getInstance("JKS", "SUN"); -// ks.load(null, keyPassword.toCharArray()); -// -// X509Certificate cert = (X509Certificate) CertificateFactory. -// getInstance("X509"). -// generateCertificate(new FileInputStream(caCertFileLocation)); -// -// //set alias to cert -// ks.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert); -// -// TrustManagerFactory tMF = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); -// tMF.init(ks); -// -// -// SslContext sslContext = SslContextBuilder -// .forClient() -// /* configured with the TrustManagerFactory that has the cert from the ca.cert -// * This tells the driver to trust the server during the SSL handshake */ -// .trustManager(tMF) -// /* These are needed because the server is configured with require_client_auth -// * In this case the client's public key must be in the truststore on each DSE -// * server node and the CA configured */ -// .keyManager(new File(certFileLocation), new File(keyFileLocation)) -// .build(); -// -// RemoteEndpointAwareNettySSLOptions sslOptions = new RemoteEndpointAwareNettySSLOptions(sslContext); -// -// // Cluster builder with sslOptions -// builder.withSSL(sslOptions); -// -// } catch (Exception e) { -// throw new RuntimeException(e); -// } -// } - RetryPolicy retryPolicy = activityDef.getParams() .getOptionalString("retrypolicy") .map(CQLOptions::retryPolicyFor).orElse(DefaultRetryPolicy.INSTANCE); diff --git a/driver-cql/src/main/resources/cql.md b/driver-cql/src/main/resources/cql.md index 94f6ad7bb..8aeaf78d0 100644 --- a/driver-cql/src/main/resources/cql.md +++ b/driver-cql/src/main/resources/cql.md @@ -1,6 +1,6 @@ # cql driver -This is an driver which allows for the execution of CQL statements. This driver supports both sync and async modes, with +This is a driver which allows for the execution of CQL statements. This driver supports both sync and async modes, with detailed metrics provided for both. ### Example activity definitions @@ -38,7 +38,7 @@ activity types. with no spaces. Examples: - `host=192.168.1.25` - - `host=`192.168.1.25,testhost42` + - `host=192.168.1.25,testhost42` - **workload** - The workload definition which holds the schema and statement defs. see workload yaml location for additional details (no default, required) @@ -59,8 +59,8 @@ activity types. policy in the driver. If used, a WhitelistPolicy(RoundRobinPolicy()) will be created and added to the cluster builder on startup. Examples: - - whitelist=127.0.0.1 - - whitelist=127.0.0.1:9042,127.0.0.2:1234 + - `whitelist=127.0.0.1` + - `whitelist=127.0.0.1:9042,127.0.0.2:1234` - **retrypolicy** default: none - Applies a retry policy in the driver The only option supported for this version is `retrypolicy=logging`, which uses the default retry policy, but with logging added. @@ -107,7 +107,7 @@ activity types. Examples: - `socketoptions=read_timeout_ms=23423,connect_timeout_ms=4444` - - `socketoptions=tcp_no_delay=true + - `socketoptions=tcp_no_delay=true` - **tokens** default: unset - Only executes statements that fall within any of the specified token ranges. Others are counted in metrics @@ -133,36 +133,12 @@ activity types. ignored if passfile is also present. - **passfile** - the file to read the password from. The first line of this file is used as the password. + - **ssl** - specifies the type of the SSL implementation. - Disabled by default, possible values are `jdk`, and `openssl`. - Depending on type, additional parameters need to be provided. -- **tlsversion** - specify the TLS version to use for SSL. - Examples: - - `tlsversion=TLSv1.2` (the default) -- **truststore** (`jdk`, `openssl`) - specify the path to the SSL truststore. - Examples: - - `truststore=file.truststore` -- **tspass** (`jdk`, `openssl`) - specify the password for the SSL truststore. - Examples: - - `tspass=mypass` -- **keystore** (`jdk`) - specify the path to the SSL keystore. - Examples: - - `keystore=file.keystore` -- **kspass** (`jdk`) - specify the password for the SSL keystore. - Examples: - - `kspass=mypass` -- **keyFilePath** (`openssl`) - path to the OpenSSL key file. - Examples: - - `keyFilePath=file.key` -- **keyPassword** (`openssl`) - key password; - Examples: - - `keyPassword=password` -- **caCertFilePath** (`openssl`) - path to the X509 CA certificate file. - Examples: - - `caCertFilePath=cacert.pem` -- **certFilePath** (`openssl`) - path to the X509 certificate file. - Examples: - - `certFilePath=ca.pem` + Disabled by default, possible values are `jdk` and `openssl`. + + [Additional parameters may need to be provided](ssl.md). + - **jmxreporting** - enable JMX reporting if needed. Examples: - `jmxreporting=true` diff --git a/driver-cql/src/main/resources/ssl.md b/driver-cql/src/main/resources/ssl.md new file mode 100644 index 000000000..b6b4ece26 --- /dev/null +++ b/driver-cql/src/main/resources/ssl.md @@ -0,0 +1,56 @@ +# SSL + +Supported options: + +- **ssl** - specifies the type of the SSL implementation. + Disabled by default, possible values are `jdk`, and `openssl`. + +- **tlsversion** - specify the TLS version to use for SSL. + + Examples: + - `tlsversion=TLSv1.2` (the default) + +For `jdk` type, the following options are available: + +- **truststore** - specify the path to the SSL truststore. + + Examples: + - `truststore=file.truststore` + +- **tspass** - specify the password for the SSL truststore. + + Examples: + - `tspass=truststore_pass` + +- **keystore** - specify the path to the SSL keystore. + + Examples: + - `keystore=file.keystore` + +- **kspass** - specify the password for the SSL keystore. + + Examples: + - `kspass=keystore_pass` + +- **keyPassword** - specify the password for the key. + + Examples: + - `keyPassword=password` + + +For `openssl` type, the following options are available: + +- **caCertFilePath** - path to the X509 CA certificate file. + + Examples: + - `caCertFilePath=cacert.crt` + +- **certFilePath** - path to the X509 certificate file. + + Examples: + - `certFilePath=ca.pem` + +- **keyFilePath** - path to the OpenSSL key file. + + Examples: + - `keyFilePath=file.key` diff --git a/driver-tcp/src/main/resources/tcpclient.md b/driver-tcp/src/main/resources/tcpclient.md index 3d71bab0e..82ca1f4c6 100644 --- a/driver-tcp/src/main/resources/tcpclient.md +++ b/driver-tcp/src/main/resources/tcpclient.md @@ -37,6 +37,11 @@ Run a stdout activity named 'stdout-test', with definitions from activities/stdo - **ssl** - boolean to enable or disable ssl - default: false - dynamic: false + + To enable, specifies the type of the SSL implementation with either `jdk` or `openssl`. + + [Additional parameters may need to be provided](../../../../driver-cql/src/main/resources/ssl.md). + - **host** - this is the name to connect to (remote server IP address) - default: localhost - dynamic: false @@ -50,5 +55,4 @@ Run a stdout activity named 'stdout-test', with definitions from activities/stdo ## Statement Format -Refer to the help for the stdout driver for for details. - +Refer to the help for the stdout driver for details. diff --git a/driver-tcp/src/main/resources/tcpserver.md b/driver-tcp/src/main/resources/tcpserver.md index f21a4cc68..f3f679bd2 100644 --- a/driver-tcp/src/main/resources/tcpserver.md +++ b/driver-tcp/src/main/resources/tcpserver.md @@ -36,9 +36,15 @@ Run a stdout activity named 'stdout-test', with definitions from activities/stdo failed. - default: 3 - dynamic: false + - **ssl** - boolean to enable or disable ssl - default: false - dynamic: false + + To enable, specifies the type of the SSL implementation with either `jdk` or `openssl`. + + [Additional parameters may need to be provided](../../../../driver-cql/src/main/resources/ssl.md). + - **host** - this is the name to bind to (local interface address) - default: localhost - dynamic: false @@ -52,5 +58,4 @@ Run a stdout activity named 'stdout-test', with definitions from activities/stdo ## Statement Format -Refer to the help for the stdout driver for for details. - +Refer to the help for the stdout driver for details. diff --git a/engine-api/src/main/java/io/nosqlbench/engine/api/util/SSLKsFactory.java b/engine-api/src/main/java/io/nosqlbench/engine/api/util/SSLKsFactory.java index b3537f345..82f26f88f 100644 --- a/engine-api/src/main/java/io/nosqlbench/engine/api/util/SSLKsFactory.java +++ b/engine-api/src/main/java/io/nosqlbench/engine/api/util/SSLKsFactory.java @@ -17,29 +17,26 @@ package io.nosqlbench.engine.api.util; -import io.nosqlbench.engine.api.activityimpl.ActivityDef; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.io.File; +import java.security.KeyStore; +import java.util.Optional; import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStream; -import java.security.KeyStore; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.netty.handler.ssl.JdkSslContext; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.nosqlbench.engine.api.activityimpl.ActivityDef; public class SSLKsFactory { private final static Logger logger = LoggerFactory.getLogger(SSLKsFactory.class); - private static SSLKsFactory instance = new SSLKsFactory(); + private static final SSLKsFactory instance = new SSLKsFactory(); /** * Consider: https://gist.github.com/artem-smotrakov/bd14e4bde4d7238f7e5ab12c697a86a3 @@ -52,16 +49,28 @@ public class SSLKsFactory { } public ServerSocketFactory createSSLServerSocketFactory(ActivityDef def) { - return ((SSLContext) getContext(def)).getServerSocketFactory(); + SslContext context = getContext(def); + if (context == null) { + throw new IllegalArgumentException("SSL is not enabled."); + } + // FIXME: potential incompatibility issue + return ((JdkSslContext) context).context().getServerSocketFactory(); } public SocketFactory createSocketFactory(ActivityDef def) { - return ((SSLContext) getContext(def)).getSocketFactory(); + SslContext context = getContext(def); + if (context == null) { + throw new IllegalArgumentException("SSL is not enabled."); + } + // FIXME: potential incompatibility issue + return ((JdkSslContext) context).context().getSocketFactory(); } - public Object getContext(ActivityDef def) { + public SslContext getContext(ActivityDef def) { Optional sslParam = def.getParams().getOptionalString("ssl"); if (sslParam.isPresent()) { + String tlsVersion = def.getParams().getOptionalString("tlsversion").orElse("TLSv1.2"); + if (sslParam.get().equals("jdk") || sslParam.get().equals("true")) { if (sslParam.get().equals("true")) { logger.warn("Please update your 'ssl=true' parameter to 'ssl=jdk'"); @@ -69,104 +78,71 @@ public class SSLKsFactory { Optional keystorePath = def.getParams().getOptionalString("keystore"); Optional keystorePass = def.getParams().getOptionalString("kspass"); + char[] keyPassword = def.getParams().getOptionalString("keyPassword") + .map(String::toCharArray) + .orElse(null); Optional truststorePath = def.getParams().getOptionalString("truststore"); Optional truststorePass = def.getParams().getOptionalString("tspass"); - String tlsVersion = def.getParams().getOptionalString("tlsversion").orElse("TLSv1.2"); - if (keystorePath.isPresent() && keystorePass.isPresent() && truststorePath.isPresent() && truststorePass.isPresent()) { + KeyStore ks = keystorePath.map(ksPath -> { try { - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(new FileInputStream(keystorePath.get()), keystorePass.get().toCharArray()); - - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); - kmf.init(ks, keystorePass.get().toCharArray()); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - if (!truststorePath.get().isEmpty()) { - KeyStore ts = KeyStore.getInstance("JKS"); - InputStream trustStore = new FileInputStream(truststorePath.get()); - - String truststorePassword = truststorePass.get(); - ts.load(trustStore, truststorePassword.toCharArray()); - tmf.init(ts); - } else { - tmf.init(ks); - } - - SSLContext sc = SSLContext.getInstance(tlsVersion); - sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); - - return sc; + return KeyStore.getInstance(new File(ksPath), + keystorePass.map(String::toCharArray).orElse(null)); } catch (Exception e) { - throw new RuntimeException(e); + throw new RuntimeException("Unable to load the keystore. Please check.", e); } + }).orElse(null); - } else if (keystorePath.isEmpty() && keystorePass.isEmpty() && truststorePath.isPresent() && truststorePass.isPresent()) { - try { - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - KeyStore ts = KeyStore.getInstance("JKS"); - InputStream trustStore = new FileInputStream(truststorePath.get()); - String truststorePassword = truststorePass.get(); - ts.load(trustStore, truststorePassword.toCharArray()); - tmf.init(ts); - SSLContext sc = SSLContext.getInstance(tlsVersion); - sc.init(null, tmf.getTrustManagers(), null); - return sc; - } catch (Exception e) { - throw new RuntimeException(e); - } - } else { - throw new RuntimeException("SSL arguments are incorrectly configured. Please Check."); + KeyManagerFactory kmf; + try { + kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, keyPassword); + } catch (Exception e) { + throw new RuntimeException("Unable to init KeyManagerFactory. Please check.", e); } + KeyStore ts = truststorePath.map(tsPath -> { + try { + return KeyStore.getInstance(new File(tsPath), + truststorePass.map(String::toCharArray).orElse(null)); + } catch (Exception e) { + throw new RuntimeException("Unable to load the truststore. Please check.", e); + } + }).orElse(null); - } else if (sslParam.get().equals("openssl")) { - - logger.info("Cluster builder proceeding with SSL and Client Auth"); - String keyPassword = def.getParams().getOptionalString("keyPassword").orElse(null); - String caCertFileLocation = def.getParams().getOptionalString("caCertFilePath").orElse(null); - String certFileLocation = def.getParams().getOptionalString("certFilePath").orElse(null); - String keyFileLocation = def.getParams().getOptionalString("keyFilePath").orElse(null); - String truststorePath = def.getParams().getOptionalString("truststore").orElse(null); - String truststorePass = def.getParams().getOptionalString("tspass").orElse(null); + TrustManagerFactory tmf; + try { + tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(ts != null ? ts : ks); + } catch (Exception e) { + throw new RuntimeException("Unable to init TrustManagerFactory. Please check.", e); + } try { - KeyStore ks = KeyStore.getInstance("JKS", "SUN"); - char[] pass = keyPassword==null? null : keyPassword.toCharArray(); - ks.load(null, pass); - - X509Certificate cert = (X509Certificate) CertificateFactory. - getInstance("X509"). - generateCertificate(new FileInputStream(caCertFileLocation)); - - //set alias to cert - ks.setCertificateEntry(cert.getSubjectX500Principal().getName(), cert); - - TrustManagerFactory tMF = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - //String truststorePath = System.getProperty("javax.net.ssl.trustStore"); - - if (truststorePath != null && !truststorePath.isEmpty() && truststorePass != null) { - KeyStore ts = KeyStore.getInstance("JKS"); - InputStream trustStore = new FileInputStream(truststorePath); - ts.load(trustStore, truststorePass.toCharArray()); - tMF.init(ts); - } else { - tMF.init(ks); - } - - SslContext sslContext = SslContextBuilder - .forClient() - /* configured with the TrustManagerFactory that has the cert from the ca.cert - * This tells the driver to trust the server during the SSL handshake */ - .trustManager(tMF) - /* These are needed because the server is configured with require_client_auth - * In this case the client's public key must be in the truststore on each DSE - * server node and the CA configured */ - .keyManager(new File(certFileLocation), new File(keyFileLocation)) - .build(); - - return sslContext; + return SslContextBuilder.forClient() + .protocols(tlsVersion) + .trustManager(tmf) + .keyManager(kmf) + .build(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else if (sslParam.get().equals("openssl")) { + File caCertFileLocation = def.getParams().getOptionalString("caCertFilePath").map(File::new).orElse(null); + File certFileLocation = def.getParams().getOptionalString("certFilePath").map(File::new).orElse(null); + File keyFileLocation = def.getParams().getOptionalString("keyFilePath").map(File::new).orElse(null); + try { + return SslContextBuilder.forClient() + .protocols(tlsVersion) + /* configured with the TrustManagerFactory that has the cert from the ca.cert + * This tells the driver to trust the server during the SSL handshake */ + .trustManager(caCertFileLocation) + /* These are needed if the server is configured with require_client_auth + * In this case the client's public key must be in the truststore on each DSE + * server node and the CA configured */ + .keyManager(certFileLocation, keyFileLocation) + .build(); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/engine-api/src/test/java/io/nosqlbench/engine/api/util/SSLKsFactoryTest.java b/engine-api/src/test/java/io/nosqlbench/engine/api/util/SSLKsFactoryTest.java new file mode 100644 index 000000000..c225c4e8d --- /dev/null +++ b/engine-api/src/test/java/io/nosqlbench/engine/api/util/SSLKsFactoryTest.java @@ -0,0 +1,187 @@ +/* + * + * Copyright 2020 jshook + * Licensed 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 io.nosqlbench.engine.api.util; + +import org.junit.Test; + +import io.nosqlbench.engine.api.activityimpl.ActivityDef; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class SSLKsFactoryTest +{ + @Test + public void testJdkGetContextWithTruststoreAndKeystore() { + String[] params = { + "ssl=jdk", + "truststore=src/test/resources/ssl/server_truststore.p12", + "tspass=nosqlbench_server", + "keystore=src/test/resources/ssl/client.p12", + "kspass=nosqlbench_client", + "keyPassword=nosqlbench_client" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThat(SSLKsFactory.get().getContext(activityDef)).isNotNull(); + } + + @Test + public void testJdkGetContextWithTruststore() { + String[] params = { + "ssl=jdk", + "truststore=src/test/resources/ssl/server_truststore.p12", + "tspass=nosqlbench_server" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThat(SSLKsFactory.get().getContext(activityDef)).isNotNull(); + } + + @Test + public void testJdkGetContextWithKeystore() { + String[] params = { + "ssl=jdk", + "keystore=src/test/resources/ssl/client.p12", + "kspass=nosqlbench_client", + "keyPassword=nosqlbench_client" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThat(SSLKsFactory.get().getContext(activityDef)).isNotNull(); + } + + @Test + public void testOpenSSLGetContextWithCaCertAndClientCert() { + String[] params = { + "ssl=openssl", + "caCertFilePath=src/test/resources/ssl/cacert.crt", + "certFilePath=src/test/resources/ssl/client_cert.pem", + "keyFilePath=src/test/resources/ssl/client.key" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThat(SSLKsFactory.get().getContext(activityDef)).isNotNull(); + } + + @Test + public void testOpenSSLGetContextWithCaCert() { + String[] params = { + "ssl=openssl", + "caCertFilePath=src/test/resources/ssl/cacert.crt" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThat(SSLKsFactory.get().getContext(activityDef)).isNotNull(); + } + + @Test + public void testJdkGetContext() { + String[] params = { + "ssl=jdk", + "tlsversion=TLSv1.2", + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThat(SSLKsFactory.get().getContext(activityDef)).isNotNull(); + } + + @Test + public void testOpenSSLGetContext() { + String[] params = { + "ssl=openssl", + "tlsversion=TLSv1.2", + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThat(SSLKsFactory.get().getContext(activityDef)).isNotNull(); + } + + @Test + public void testLoadKeystoreError() { + String[] params = { + "ssl=jdk", + "keystore=src/test/resources/ssl/non_existing.p12", + "kspass=nosqlbench_client", + "keyPassword=nosqlbench_client" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> SSLKsFactory.get().getContext(activityDef)) + .withMessageMatching("Unable to load the keystore. Please check."); + } + + @Test + public void testInitKeyManagerFactoryError() { + String[] params = { + "ssl=jdk", + "keystore=src/test/resources/ssl/client.p12", + "kspass=nosqlbench_client", + "keyPassword=incorrect_password" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> SSLKsFactory.get().getContext(activityDef)) + .withMessageMatching("Unable to init KeyManagerFactory. Please check."); + } + + @Test + public void testLoadTruststoreError() { + String[] params = { + "ssl=jdk", + "truststore=src/test/resources/ssl/non_existing.p12", + "tspass=nosqlbench_server" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> SSLKsFactory.get().getContext(activityDef)) + .withMessageMatching("Unable to load the truststore. Please check."); + } + + @Test + public void testOpenSSLGetContextWithCaCertError() { + String[] params = { + "ssl=openssl", + "caCertFilePath=src/test/resources/ssl/non_existing.pem" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> SSLKsFactory.get().getContext(activityDef)) + .withMessageContaining("File does not contain valid certificates") + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testOpenSSLGetContextWithCertError() { + String[] params = { + "ssl=openssl", + "certFilePath=src/test/resources/ssl/non_existing.pem" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> SSLKsFactory.get().getContext(activityDef)) + .withMessageContaining("File does not contain valid certificates") + .withCauseInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testOpenSSLGetContextWithKeyError() { + String[] params = { + "ssl=openssl", + "keyFilePath=src/test/resources/ssl/non_existing.pem" + }; + ActivityDef activityDef = ActivityDef.parseActivityDef(String.join(";", params)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> SSLKsFactory.get().getContext(activityDef)) + .withMessageContaining("File does not contain valid private key") + .withCauseInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/engine-api/src/test/resources/ssl/cacert.crt b/engine-api/src/test/resources/ssl/cacert.crt new file mode 100644 index 000000000..b7f20bf0d --- /dev/null +++ b/engine-api/src/test/resources/ssl/cacert.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfsCFA3o5Ne8hiqC9/d5sJzmHg3t3iD8MA0GCSqGSIb3DQEBCwUAMEYx +DzANBgNVBAMMBnJvb3RDYTETMBEGA1UECwwKbm9zcWxiZW5jaDERMA8GA1UECgwI +RGF0YVN0YXgxCzAJBgNVBAYTAlVTMB4XDTIwMDUwNDIyMTcyOFoXDTMwMDUwMjIy +MTcyOFowRjEPMA0GA1UEAwwGcm9vdENhMRMwEQYDVQQLDApub3NxbGJlbmNoMREw +DwYDVQQKDAhEYXRhU3RheDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC5LFz/eR3k8d8F3cB6jnpsnOVIPonHwthcWKOXEy9Z8f31 +s70QhF7tkep3ExqUGhQJyjz4Cdk4cFvElMob9WuaoM5Qp4scKW5rQ47WCV660Ok1 +skKJXE/wLFc8edEn6vyvyUTpAce+B8HlY7cONiwA8mGALnYoPlsn2wFbTYIEGVVv +My01990zgkMYwZErEYe2XXvc3gZEC7An16xzYrfYmYFfs0CsH2GUuDQtcFSmyTKH +nYA+kn92JWEx1WMSsC07B8A0qwxdLJy6/d3VLHshbd3Aimf0Jhkg0mLHC5XlkVxr +3pVYrdVEMuBee6A8v4BG8pG42Fjb1xSYwGM9GelvAgMBAAEwDQYJKoZIhvcNAQEL +BQADggEBADz9MhhOA0/RF6iqW7gfWXlSpU2ks696IEhVRi+F9/y8jbKcwKHFchkN +FRc1W6Xyx6LVunOw3zlbeMh+E0DzNTWMF+njcBQH7S5A8TcumU2CDgGNBdl1692x +/z6nGhYIv4IBxaYEd30HPuRz3MXeVyfbGEnIU8jU0vsbfdtZqPdyEk3PNZoFvosk +WhE0Z4KhYoPjwJLhpAnQgW3/RRSNJySReen5YTWJ0qQGYt1HO0Oqz+YgTsthofWc +g/JbmCAD6L1YiCb6WCbDG5qrjwlzhw8bjiT3vw+y10ZIk07Vwbm4QlHSCoe0ayI4 +Pckz9yE0knBOV+Y1kV2DITKV6kYkWBw= +-----END CERTIFICATE----- diff --git a/engine-api/src/test/resources/ssl/client.key b/engine-api/src/test/resources/ssl/client.key new file mode 100644 index 000000000..c07dba0f9 --- /dev/null +++ b/engine-api/src/test/resources/ssl/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8NqAtNjapKtai +hauevzX64ikBE0vugZLbXjlqfHYMoPqYBcP+Qj7JM1QnRbU0p15NCi/eUuNbzuGQ +IzQFIo/IF3F3ivohOrTsunpFYdZlNCx3ixr4mJr4q5G/HbqGHPRSBGgyB/Sye550 +N819RiZh7d71dkSvig3CTnCK/d4NckSJ2dzYYEE/8ue+gfUsQZJrCPJcp2GTCEsq +kq+3FMyVyZHC0V/BiCO9RouC1P5uE70CHKedpPUdZBJU8vRUMM7y75TqzKIT9/xI +fZ0azUDWO89v/n99+O71M8uYhf4MMvS60DMgW8TfWqrToK+jYwnx4zVVburqu6b/ +JSsvODWvAgMBAAECggEANu+W1zuAvuXjGjL8Aez724vRvh+cxTQK4n7hMWS8rDj8 +jAz6xSce3mleAcyF9KV5j/EOQc1d0XlUO1cbIviQkS3Oj77//Vz+XC6d68x/4LBW +3lm6+J7KdRTXCLqrq+OdqKbipt/Nm58bg/6ZuxwTrffZYToxGC+qjnGIxfkNrEyS +jrTJRBOW7MlUhTjlnSYInsBZljRS7V2Zte3F53PCS8p2dbGRLu224bow/5oE5lt6 +x92cpjt+f/+a7Je+K2KoT/KAfG/SeGKzEQaYrqw1+WMEu4xwCvO+atJQDE0cShzl +4vYwUp3GEmMhwdML817MyxxBKpcz+inqvCatZWgrOQKBgQDtVMLvgFPLnpmKJ1g9 +TGZE5a7FeZQVdF/ALnxko4pvw0ZyqPStlklfkyVvzPZYa99CP5p8oFNyVMONsLVk +U57Y1pmU5Dm0e9E1Zqk4fMPL5h8FOv5F8g6vfg3ZEe2C0FvV7CekxchgcJKTmat6 +fzbwXPK+rW0Ld7etCiPAT+o/jQKBgQDLBMJ0gySusIkBG5gCHymAcgchYj16bbC0 +lbckxYzGXYJoqg/HpEfh7Auh6ofoXnYKRcZ696Rk7vQ6jTaNl3NOLMkxkdhHdQvK +gze1+zWmzsakntfk+auf70M+feh2bSPe7tRoY6y+WYpTuxCGYgEV2WRNL1syEeXW +8lmoDrLtKwKBgQC/MRWJU9wtoSsX/PI9D5sjzdSqCXOehQ3OCKT1fjo8JxhNrobO +gM/DSwtRsdCTEvPcrtiJpa8T3+1Z7A11MVg6X0eChwlluImld3rDot8pF83NrOTC +/GmQPwBw6txoEeqpv4GAYEU4S/gJKDbYjDt6D6cOrS+3mU5C/HQorTiM0QKBgFu0 +1rIS22sdy4V4lX2/3dtrptTpr6OyEPRB/OzbX+/rJZFp4J7qEp53JfoKG0JYCTIy +uqmpW9VMK36Xc2EaXLefe3Ks0unUcXMVOwE1bNLg7NJH/nYsYd5pEhMUhQGZ4248 +rC5LeCi0Acw44AoUEzFvdeN31NYVR6GE8AL+QMzpAoGAJYdZbz/i+EoV23TiWnEE +q0vBZ9dCKhYA3SaV/cwesFANGLOtFhhiUDA9/Jkw8P9eHoWfsqY4ud/nGGWeXvUu +Ox576QcrYoIW6g0Gk7bwS2tSC943LAxS2YkAdMM27BhCZw7jAkmoxbQhf9t2IBMc +ixHr+et58pQWFjF73/Ex16k= +-----END PRIVATE KEY----- diff --git a/engine-api/src/test/resources/ssl/client.p12 b/engine-api/src/test/resources/ssl/client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..78ea24e6c1c9ae937dce366a5e538d5932648e5e GIT binary patch literal 2389 zcmY+FX*d*$9>!-gn>f_a*cl{aFpPLlIc26?IJn2m=;V z5O4?u0r`mmcnbLOKM}N;0>1YXm4Sf2lEQxyAYK-V{CC41AQUeOfzR<>oTwt703Z+? z5K94vOf-H?Zac*ZDI3akm{4dFz;U6PM_LT{Sl*}HxeGoJhZb60@;{$7lu4Uv=Gh{5!6w@h1D=rF% z%YiYIYQj99j|vVl&dYCzqYxIlp6!axQmR1n>{pDzP(`mh%n*!g3#S9ZqSXV=~v zFxA*&Vw~jb7NDcj%S}NF43i!Ozk4Ta+KRS$LNdt1xdYMiQybYQ|8~n#Sp_UHN60IN zA0yR|5yB2ubAopPUKuY4^>vn?uVN5W`NnK%uaM8Rt~2E%CC+o50LKGl!X!QQb<4gT z7l*`(NKsn5tXp|N`giM2^G}c0NMrq<7#fMsR7zs*AK3rM+SXZa;cewqX))3(ZHTW- zxwUM^U$}>hjq#aJxn(a&FevdI%-U~bt=>C*Tk%i0q~PNGJ&s+oH_tz5j`c&<^SCRO z9A6!t#wbO=gi}icR`5SwTch_D36s==Hi&o}tQ6kAgCv@97YZAR3MUq3-8jf@yx^X^pVV0ss zA-+J)@hkW!VwR`W=-q71mh+oEuY{s+UCg#E%;LTYjN|S;PLEI(%yvD4I#9jEBiNDrM97wcQ=6;BXM~rAdK@WyDry1=5Hk6myA`b6W#SAnoVA9wW#j>svg=cb87j*^Dm*d z0q;}8vbYisIxS(t3^~*5`E~{(i=+6Z#!FqB_XC~LDKdCvNw;P7K#R^XF>TScsGpt+ znf;HT#S|f32t`QiC&vD~15n}r=!yh_iYbsqJOwiMe-?v(S=??Yz91T@6#C0z2nE9H ziG6Zhj~ThZf_8KP;|sCHQP*^v9>4@rrQ@c~lLC}`Thumtcv~fQG{<&ZK1gjVrzUIs zST|tDqvl~T1s-2;-&lA^Kz(Zz6o}L#EVCZ6Tq4!+Y~AePyQH(2Zg3yj zBaaZ0(_)0%*nWeOUyRX6z#C|K8S&)q#CYDNB~NPCpwYFYrHEGbXA#0twgKIp-snR; z`R%%cTcKYr*~IUCSfW2Khf1DT`JUe1-#P4an`217qPM@rX3F1Be&trF1^ojb%@_H! zHE(ZdV)GX!6Q3oK5Y&BBy{2oax!?f`_+3l-68U#6+%_|Jp6(Rmv;Fl-j82}QZDq*P z6W7A5^8&5qYExHhQig2wB4ulwNV#j*F|K|fm&+h8QK>$MoGr)H-sEYRYQ;%P=T#*I zugFR``w*0#9>136S|HC9-8mg&u}jqGYWyZ@uHyEk zM-3M6=<-|@mP#GQ(#-O=-r4Gzy`rZ(X5iPM;oUe7pYa|x(z#OA(1gUk)rLLp&2=HNtT)zA zl``{hTjdyj!^DrRXU^oINlxL;$Ekm^lvVUbO=(ujzOPK+m}kdMyw_LuXQzd29Z}b` zzp=jHLd! zRWhe`@Oul~{9{f>#oliL>v8t5(X7^IeJ@P6E-d>>#|ADxNc?l0C1>H3ayO`ptGefC zy~L{A^L_8eG$j>HaJctnBbBG^9V{}2i$=yVHk#_BztUwU;m+Gv3fTll(c!az4*AYu zEq^;gH238hW&CS;DTL>rxW+2j7{$ItBkB+RfRdKMsKQ^f_JA`am1$y z5jPuE-7CUv%Pk%qNV5yDLVa`E#y3E~o4K4k><_Ycm_4%mX4hciUgzJeYB|?X+7B84 za)_1Y{s+<5>%tU>n*L(QQ^W)k2fmu^?Nyq4a~ID)%~tSsFg=`oq4(IKQS`VTLz?W;xlE=;erU{8p!ICv+b-Hc};8=h*|^z%WZo|%4m?#^@9zjLS6mJiMnZ?tQ= zQ!jmQFw3n)6y8L#QM}&lqBm_kPVQD4r6S(I_0`-A5iiW%n zB@Mg|UI~wY3TcRfLDE712rBr%=Ql-h(gjrf)XA&;6@s{FTpI*V?hf5IVLjLKYbE~$ D!sAz; literal 0 HcmV?d00001 diff --git a/engine-api/src/test/resources/ssl/client_cert.pem b/engine-api/src/test/resources/ssl/client_cert.pem new file mode 100644 index 000000000..70a4e7bf1 --- /dev/null +++ b/engine-api/src/test/resources/ssl/client_cert.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEzCCAfsCFBuaP3upDuMBExO4l649oUURC/SkMA0GCSqGSIb3DQEBCwUAMEYx +DzANBgNVBAMMBnJvb3RDYTETMBEGA1UECwwKbm9zcWxiZW5jaDERMA8GA1UECgwI +RGF0YVN0YXgxCzAJBgNVBAYTAlVTMB4XDTIwMDUwNDIzMTQ1MloXDTMwMDUwMjIz +MTQ1MlowRjEPMA0GA1UEAwwGY2xpZW50MRMwEQYDVQQLDApub3NxbGJlbmNoMREw +DwYDVQQKDAhEYXRhU3RheDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQC8NqAtNjapKtaihauevzX64ikBE0vugZLbXjlqfHYMoPqY +BcP+Qj7JM1QnRbU0p15NCi/eUuNbzuGQIzQFIo/IF3F3ivohOrTsunpFYdZlNCx3 +ixr4mJr4q5G/HbqGHPRSBGgyB/Sye550N819RiZh7d71dkSvig3CTnCK/d4NckSJ +2dzYYEE/8ue+gfUsQZJrCPJcp2GTCEsqkq+3FMyVyZHC0V/BiCO9RouC1P5uE70C +HKedpPUdZBJU8vRUMM7y75TqzKIT9/xIfZ0azUDWO89v/n99+O71M8uYhf4MMvS6 +0DMgW8TfWqrToK+jYwnx4zVVburqu6b/JSsvODWvAgMBAAEwDQYJKoZIhvcNAQEL +BQADggEBAFx3oSbS/6PfEYQjfesUMdHIPHGosrLfjzk0KUtSwRbmIJLIzujaUz4s +UEyuxQhA6ERFNN8AUaighAO6IScYgVlxTeykeE3nGP+tZ+EuIxo6wXoo8WI85ivk +OGspI+2Ne/mo45xSNdvnALnsPLEZM870LBe0UvHcSnDYR0V0d28BzzuAZfMRyT4z +DkBSdouxahzzXIMi/TR0DDu4VMsVl55MNACgV+JDRAAwdYjN6kwJrtfVrsYBrIew +TMUFXuEa1vDcsvAoALDN4+wGa/8TdPnAJuRirHVVfxoKb1XlcDCy/79pCOirUmbn +ib/nCyq+MlFFYrE1YQSVZ2IM8x8n5d4= +-----END CERTIFICATE----- diff --git a/engine-api/src/test/resources/ssl/server_truststore.p12 b/engine-api/src/test/resources/ssl/server_truststore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..0cfe4a010729e9fda48c8f1832a7e3857de27189 GIT binary patch literal 1082 zcmV-A1jYL>f&?}K0Ru3C1Mdb2Duzgg_YDCD0ic2d-~@sL+%SRz*f4?v)CLJEhDe6@ z4FLxRpn?O%FoFZY0s#Opf&;n+2`Yw2hW8Bt2LUiC1_~;MNQUKO3DRky`R{U8ZWI0s{cUP=JC1fN;zv9PFPeO%-;v+T?a$y^h=D7GzS?d@SeZ z5}rGd1rLC)WW2GxuqDn%(1Jrjv&4tUz6gb-mbLu~x63mQ45&^J&%VpV4C-FsPW z!X6%Uh$-99V$kN9kQvAqn6^QKZe2NP@MBI%JKCh(L*A%INkYNw&vktk4cKbU)rcm= z`dLZtaAHrX=uao|sG{2;_d)H!vS02KnF(E(39S&F7%fYA}5TpvL9@_Rp{i9q<061)9q(sunVPkAic zva=(z&qp7=_3XE59%d5jxp_wLnp}TtYz^3B@xz>?6<=J@HHoFZA!QL(Rq4$y-piEm zEQ_p+4BxYf>NcX$S8WYThQZa4YP7QCfRiR&>-KI%l)aFcXoCuenB%SQAqDvAljkM3 zLb^117GW%evq8=?R`m>>wc_eUzVh$_X8x?T@ERJGtO>4SaZg&kczO0KbJT(z>664g zZ*KFuva?9aMH3oYhP{JH2fZA;SqwEiQMe7%WA1GfQxr?Ulbf$I*Jx!_pYKIck*y1o z%m&rVD5H?nyUgxL@CQK10$OPiRhV>F@4SA3rJsNxv$I9f7W~eEKUds_oF;Sx8JnWu z(KYn8E^D)bYnvJ{E=%^}vwT}R(T2=IXjjJ=KWTz}V7ik-1P+~5vxAo*L#5x}&(*YH zt$4tTbPKIUM}w#E%QUWG&LBNkjFe6RI+NxhpcUTqE0n$9Tf1+qD?F)A?xlB|+@=|! ziWQ{^7HZ5s)&b5)5Lc(-|D0qBe%;~jSUre_b)W1+Z`~wwvf6A}H{jMO#zcX07dg~9 z+n}fplz@r!ahY@&(=n`XxBy--TOnh9f#eF${8<&!=E1LS$Zt>9R?GHLUVy5a=&SgC^0s{etpx=1? Av;Y7A literal 0 HcmV?d00001