Skip to content

Commit

Permalink
PEM format support (Ericsson#303)
Browse files Browse the repository at this point in the history
Introducing configuration for certificates in PEM format for CQL client and Tomcat server.

Closes Ericsson#300
  • Loading branch information
valmiranogueira committed Sep 30, 2022
1 parent 212ad14 commit aff281d
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 37 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Add metric for failed and succeeded repair tasks - Issue #295
* Remove deprecated v1 REST interface
* Migrate to datastax driver-4.14.1 - Issue #269
* Add PEM format support - Issue #300

## Version 3.0.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
import com.datastax.oss.driver.api.core.metadata.EndPoint;
import com.ericsson.bss.cassandra.ecchronos.application.config.TLSConfig;
import com.ericsson.bss.cassandra.ecchronos.connection.CertificateHandler;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -35,6 +38,7 @@
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

Expand All @@ -55,17 +59,18 @@ public final SSLEngine newSslEngine(final EndPoint remoteEndpoint)
{
Context context = getContext();
TLSConfig tlsConfig = context.getTlsConfig();
SSLContext sslContext = context.getSSLContext();
SslContext sslContext = context.getSSLContext();

SSLEngine sslEngine;
if (remoteEndpoint != null)
{
InetSocketAddress socketAddress = (InetSocketAddress) remoteEndpoint.resolve();
sslEngine = sslContext.createSSLEngine(socketAddress.getHostName(), socketAddress.getPort());
sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT, socketAddress.getHostName(),
socketAddress.getPort());
}
else
{
sslEngine = sslContext.createSSLEngine();
sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT);
}
sslEngine.setUseClientMode(true);

Expand Down Expand Up @@ -118,7 +123,7 @@ public void close() throws Exception
protected static final class Context
{
private final TLSConfig tlsConfig;
private final SSLContext sslContext;
private final SslContext sslContext;

Context(final TLSConfig aTLSConfig) throws NoSuchAlgorithmException, IOException, UnrecoverableKeyException,
CertificateException, KeyStoreException, KeyManagementException
Expand All @@ -137,26 +142,44 @@ boolean sameConfig(final TLSConfig aTLSConfig)
return this.tlsConfig.equals(aTLSConfig);
}

SSLContext getSSLContext()
SslContext getSSLContext()
{
return sslContext;
}
}

protected static SSLContext createSSLContext(final TLSConfig tlsConfig) throws IOException,
protected static SslContext createSSLContext(TLSConfig tlsConfig) throws IOException,
NoSuchAlgorithmException,
KeyStoreException,
CertificateException,
UnrecoverableKeyException,
KeyManagementException
UnrecoverableKeyException
{
SSLContext sslContext = SSLContext.getInstance(tlsConfig.getProtocol());
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(tlsConfig);
TrustManagerFactory trustManagerFactory = getTrustManagerFactory(tlsConfig);

sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
SslContextBuilder builder = SslContextBuilder.forClient();

return sslContext;
if (tlsConfig.getCertificate().isPresent() &&
tlsConfig.getCertificatePrivateKey().isPresent() &&
tlsConfig.getTrustCertificate().isPresent())
{
File certificateFile = new File(tlsConfig.getCertificate().get());
File certificatePrivateKeyFile = new File(tlsConfig.getCertificatePrivateKey().get());
File trustCertificateFile = new File(tlsConfig.getTrustCertificate().get());

builder.keyManager(certificateFile, certificatePrivateKeyFile);
builder.trustManager(trustCertificateFile);
}
else
{
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(tlsConfig);
TrustManagerFactory trustManagerFactory = getTrustManagerFactory(tlsConfig);
builder.keyManager(keyManagerFactory);
builder.trustManager(trustManagerFactory);
}
if (tlsConfig.getCipher_suites().isPresent())
{
builder.ciphers(Arrays.asList(tlsConfig.getCipher_suites().get()));
}
return builder.protocols(tlsConfig.getProtocols()).build();
}

protected static KeyManagerFactory getKeyManagerFactory(final TLSConfig tlsConfig) throws IOException,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public class TLSConfig
private String truststore;
private String truststore_password;

private String certificate;
private String certificate_private_key;
private String trust_certificate;

private String protocol;
private String algorithm;
private String store_type;
Expand Down Expand Up @@ -89,11 +93,46 @@ public final void setTruststore_password(final String truststorePassword)
this.truststore_password = truststorePassword;
}

public Optional<String> getCertificate()
{
return Optional.ofNullable(certificate);
}

public void setCertificate(String certificate)
{
this.certificate = certificate;
}

public Optional<String> getCertificatePrivateKey()
{
return Optional.ofNullable(certificate_private_key);
}

public void setCertificate_private_key(String certificate_private_key)
{
this.certificate_private_key = certificate_private_key;
}

public Optional<String> getTrustCertificate()
{
return Optional.ofNullable(trust_certificate);
}

public void setTrust_certificate(String trust_certificate)
{
this.trust_certificate = trust_certificate;
}

public final String getProtocol()
{
return protocol;
}

public final String[] getProtocols()
{
return protocol.split(",");
}

public final void setProtocol(final String aProtocol)
{
this.protocol = aProtocol;
Expand Down Expand Up @@ -167,6 +206,9 @@ public final boolean equals(final Object o)
&& Objects.equals(keystore_password, tlsConfig.keystore_password)
&& Objects.equals(truststore, tlsConfig.truststore)
&& Objects.equals(truststore_password, tlsConfig.truststore_password)
&& Objects.equals(certificate, tlsConfig.certificate)
&& Objects.equals(certificate_private_key, tlsConfig.certificate_private_key)
&& Objects.equals(trust_certificate, tlsConfig.trust_certificate)
&& Objects.equals(protocol, tlsConfig.protocol)
&& Objects.equals(algorithm, tlsConfig.algorithm)
&& Objects.equals(store_type, tlsConfig.store_type)
Expand All @@ -176,8 +218,8 @@ public final boolean equals(final Object o)
@Override
public final int hashCode()
{
int result = Objects.hash(enabled, keystore, keystore_password, truststore, truststore_password, protocol,
algorithm, store_type, require_endpoint_verification);
int result = Objects.hash(enabled, keystore, keystore_password, truststore, truststore_password, certificate,
certificate_private_key, trust_certificate, protocol, algorithm, store_type, require_endpoint_verification);
result = HASH_SEED * result + Arrays.hashCode(cipher_suites);
return result;
}
Expand Down
17 changes: 13 additions & 4 deletions application/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ springdoc:
#server:
# ssl:
# enabled: false
# client-auth: <client auth, any of "need", "want" or "none">
# enabled-protocols: <enabled protocols, for example "TLSv1.2">
# ciphers: <enabled cipher suites>
#
# SSL configuration using certificate stores
# key-store: <path to keystore>
# key-store-password: <password>
# key-store-type: <keystore type>
Expand All @@ -50,9 +55,13 @@ springdoc:
# trust-store: <path to truststore>
# trust-store-password: <password>
# trust-store-type: <keystore type>
# client-auth: <client auth, any of "need", "want" or "none">
# enabled-protocols: <enabled protocols, for example "TLSv1.2">
# ciphers: <enabled cipher suites>
#
# Rate at which certificate are reloaded automatically
# SSL configuration using certificates in PEM format
# This configuration takes precedence when certificate store settings are also specified
# Only certificates using RSA algorithm are supported
# certificate: <path to certificate>
# certificate-private-key: <path to certificate private key>
# trust-certificate: <path to certificate>
#
# Rate at which certificates are reloaded automatically
# refresh-rate-in-ms: 60000
3 changes: 3 additions & 0 deletions application/src/main/resources/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ cql:
keystore_password: ecchronos
truststore: /path/to/truststore
truststore_password: ecchronos
certificate:
certificate_private_key:
trust_certificate:
protocol: TLSv1.2
algorithm:
store_type: JKS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public void testDefault() throws Exception
cqlTlsConfig.setKeystore_password("ecchronos");
cqlTlsConfig.setTruststore("/path/to/truststore");
cqlTlsConfig.setTruststore_password("ecchronos");
cqlTlsConfig.setCertificate(null);
cqlTlsConfig.setCertificate_private_key(null);
cqlTlsConfig.setTrust_certificate(null);
cqlTlsConfig.setProtocol("TLSv1.2");
cqlTlsConfig.setAlgorithm(null);
cqlTlsConfig.setStore_type("JKS");
Expand Down Expand Up @@ -102,4 +105,29 @@ public void testEnabled() throws Exception
assertThat(config.getCql().getTls()).isEqualTo(cqlTlsConfig);
assertThat(config.getJmx().getTls()).isEqualTo(jmxTlsConfig);
}

@Test
public void testEnabledWithCertificate() throws Exception
{
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
File file = new File(classLoader.getResource("enabled_certificate_security.yml").getFile());

ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());

Security config = objectMapper.readValue(file, Security.class);

Credentials expectedCqlCredentials = new Credentials(true, "cqluser", "cqlpassword");

TLSConfig cqlTlsConfig = new TLSConfig();
cqlTlsConfig.setEnabled(true);
cqlTlsConfig.setCertificate("/path/to/cql/certificate");
cqlTlsConfig.setCertificate_private_key("/path/to/cql/certificate_key");
cqlTlsConfig.setTrust_certificate("/path/to/cql/certificate_authorities");
cqlTlsConfig.setProtocol("TLSv1.2");
cqlTlsConfig.setCipher_suites("VALID_CIPHER_SUITE,VALID_CIPHER_SUITE2");
cqlTlsConfig.setRequire_endpoint_verification(true);

assertThat(config.getCql().getCredentials()).isEqualTo(expectedCqlCredentials);
assertThat(config.getCql().getTls()).isEqualTo(cqlTlsConfig);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,8 @@

import static org.assertj.core.api.Assertions.assertThat;

@RunWith (SpringRunner.class)
@SpringBootTest (webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(initializers = TestTomcatWebServerCustomizer.PropertyOverrideContextInitializer.class)
public class TestTomcatWebServerCustomizer
public abstract class TestTomcatWebServerCustomizer
{
private static final String SERVER_KEYSTORE = "src/test/resources/server/ks.p12";
private static final String SERVER_TRUSTSTORE = "src/test/resources/server/ts.p12";
private static final String CLIENT_VALID_PATH = "valid/";
private static final String CLIENT_EXPIRED_PATH = "expired/";
private static final int REFRESH_RATE = 100;
Expand Down Expand Up @@ -156,14 +151,6 @@ public void initialize(ConfigurableApplicationContext configurableApplicationCon
{
addInlinedPropertiesToEnvironment(configurableApplicationContext,
"server.ssl.enabled=true",
"server.ssl.key-store=" + SERVER_KEYSTORE,
"server.ssl.key-store-password=",
"server.ssl.key-store-type=PKCS12",
"server.ssl.key-alias=cert",
"server.ssl.key-password=",
"server.ssl.trust-store=" + SERVER_TRUSTSTORE,
"server.ssl.trust-store-password=",
"server.ssl.trust-store-type=PKCS12",
"server.ssl.client-auth=need",
"server.ssl.refresh-rate-in-ms=" + REFRESH_RATE);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2022 Telefonaktiebolaget LM Ericsson
*
* 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 com.ericsson.bss.cassandra.ecchronos.application.spring;

import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith (SpringRunner.class)
@SpringBootTest (webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"server.ssl.key-store=src/test/resources/server/ks.p12",
"server.ssl.key-store-password=",
"server.ssl.key-store-type=PKCS12",
"server.ssl.key-alias=cert",
"server.ssl.key-password=",
"server.ssl.trust-store=src/test/resources/server/ts.p12",
"server.ssl.trust-store-password=",
"server.ssl.trust-store-type=PKCS12"
})
@ContextConfiguration(initializers = TestTomcatWebServerCustomizer.PropertyOverrideContextInitializer.class)
public class TestTomcatWebServerCustomizerKeystore extends TestTomcatWebServerCustomizer {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2022 Telefonaktiebolaget LM Ericsson
*
* 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 com.ericsson.bss.cassandra.ecchronos.application.spring;

import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith (SpringRunner.class)
@SpringBootTest (webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"server.ssl.certificate=src/test/resources/server/cert.crt",
"server.ssl.certificate-private-key=src/test/resources/server/key.pem",
"server.ssl.trust-certificate=src/test/resources/server/ca.crt"
})
@ContextConfiguration(initializers = TestTomcatWebServerCustomizer.PropertyOverrideContextInitializer.class)
public class TestTomcatWebServerCustomizerPem extends TestTomcatWebServerCustomizer {}

0 comments on commit aff281d

Please sign in to comment.