Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEM format support #303

Merged
merged 12 commits into from Sep 30, 2022
Expand Up @@ -14,18 +14,25 @@
*/
package com.ericsson.bss.cassandra.ecchronos.application;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.security.*;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import javax.net.ssl.*;

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 io.netty.channel.socket.SocketChannel;
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
import io.netty.handler.ssl.SslHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -59,17 +66,17 @@ public SSLEngine newSSLEngine(EndPoint remoteEndpoint)
{
Context context = getContext();
TLSConfig tlsConfig = context.getTlsConfig();
SSLContext sslContext = context.getSSLContext();
SslContext sslContext = context.getSSLContext();

SSLEngine sslEngine;
if (remoteEndpoint != null)
{
InetSocketAddress socketAddress = remoteEndpoint.resolve();
sslEngine = sslContext.createSSLEngine(socketAddress.getHostName(), socketAddress.getPort());
sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT, socketAddress.getHostName(), socketAddress.getPort());
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
sslEngine = sslContext.createSSLEngine();
sslEngine = sslContext.newEngine(ByteBufAllocator.DEFAULT);
}
sslEngine.setUseClientMode(true);

Expand Down Expand Up @@ -128,7 +135,7 @@ protected Context getContext()
protected static final class Context
{
private final TLSConfig tlsConfig;
private final SSLContext sslContext;
private final SslContext sslContext;

Context(TLSConfig tlsConfig) throws NoSuchAlgorithmException, IOException, UnrecoverableKeyException,
CertificateException, KeyStoreException, KeyManagementException
Expand All @@ -147,22 +154,41 @@ boolean sameConfig(TLSConfig tlsConfig)
return this.tlsConfig.equals(tlsConfig);
}

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

protected static SSLContext createSSLContext(TLSConfig tlsConfig) throws IOException, NoSuchAlgorithmException,
KeyStoreException, CertificateException, UnrecoverableKeyException, KeyManagementException
protected static SslContext createSSLContext(TLSConfig tlsConfig) throws IOException, NoSuchAlgorithmException,
KeyStoreException, CertificateException, 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.getCertificate_key().isPresent() &&
tlsConfig.getCertificate_authorities().isPresent())
{
File certificateFile = new File(tlsConfig.getCertificate().get());
File certificateKeyFile = new File(tlsConfig.getCertificate_key().get());
File certificateAuthorityFile = new File(tlsConfig.getCertificate_authorities().get());

valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
builder.keyManager(certificateFile, certificateKeyFile);
builder.trustManager(certificateAuthorityFile);
}
else
{
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(tlsConfig);
TrustManagerFactory trustManagerFactory = getTrustManagerFactory(tlsConfig);
builder.keyManager(keyManagerFactory);
builder.trustManager(trustManagerFactory);
}
if (tlsConfig.getCipherSuites().isPresent())
{
builder.ciphers(Arrays.asList(tlsConfig.getCipherSuites().get()));
}
return builder.protocols(tlsConfig.getProtocols()).build();
}

protected static KeyManagerFactory getKeyManagerFactory(TLSConfig tlsConfig) throws IOException,
Expand Down
Expand Up @@ -28,6 +28,10 @@ public class TLSConfig
private String truststore;
private String truststore_password;

private String certificate;
private String certificate_key;
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
private String certificate_authorities;

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

public Optional<String> getCertificate()
{
if (this.certificate == null)
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
{
return Optional.empty();
}
return Optional.of(certificate);
}

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

public Optional<String> getCertificate_key()
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
{
if (this.certificate_key == null)
{
return Optional.empty();
}
return Optional.of(certificate_key);
}

public void setCertificate_key(String certificate_key)
{
this.certificate_key = certificate_key;
}

public Optional<String> getCertificate_authorities()
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
{
if (this.certificate_authorities == null)
{
return Optional.empty();
}
return Optional.of(certificate_authorities);
}

public void setCertificate_authorities(String certificate_authorities)
{
this.certificate_authorities = certificate_authorities;
}

public String getProtocol()
{
return protocol;
}

public String[] getProtocols()
{
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
return protocol.split(",");
}

public void setProtocol(String protocol)
{
this.protocol = protocol;
Expand Down Expand Up @@ -159,6 +210,9 @@ public boolean equals(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_key, tlsConfig.certificate_key) &&
Objects.equals(certificate_authorities, tlsConfig.certificate_authorities) &&
Objects.equals(protocol, tlsConfig.protocol) &&
Objects.equals(algorithm, tlsConfig.algorithm) &&
Objects.equals(store_type, tlsConfig.store_type) &&
Expand All @@ -168,8 +222,8 @@ public boolean equals(Object o)
@Override
public 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_key, certificate_authorities, protocol, algorithm, store_type, require_endpoint_verification);
result = 31 * result + Arrays.hashCode(cipher_suites);
return result;
}
Expand Down
Expand Up @@ -16,13 +16,23 @@


import org.apache.coyote.http11.Http11NioProtocol;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.apache.tomcat.util.net.jsse.PEMFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.cert.X509Certificate;

@Component
@EnableScheduling
public class TomcatWebServerCustomizer implements WebServerFactoryCustomizer<TomcatServletWebServerFactory>
Expand All @@ -32,26 +42,95 @@ public class TomcatWebServerCustomizer implements WebServerFactoryCustomizer<Tom
@Value("${server.ssl.enabled:false}")
private Boolean sslIsEnabled;

@Value("${server.ssl.enabled-certificate:false}")
private Boolean sslIsEnabledForCertificate;

@Value("${server.ssl.certificate:#{null}}")
private String certificate;

@Value("${server.ssl.certificate-key:#{null}}")
private String certificateKey;

@Value("${server.ssl.certificate-authorities:#{null}}")
private String certificateAuthorities;

@Value("${server.ssl.certificate-client-auth:#{null}}")
private String clientAuth;

@Value("${server.ssl.certificate-protocol:#{null}}")
private String protocol;

@Value("${server.ssl.certificate-ciphers:#{null}}")
private String ciphers;

private static final Logger LOG = LoggerFactory.getLogger(TomcatWebServerCustomizer.class);

@Override
public void customize(TomcatServletWebServerFactory factory)
{
if (sslIsEnabled)
if (sslIsEnabled || sslIsEnabledForCertificate)
{
factory.addConnectorCustomizers(connector ->
{
http11NioProtocol = (Http11NioProtocol) connector.getProtocolHandler();
if (sslIsEnabledForCertificate)
{
http11NioProtocol.addSslHostConfig(getSslHostConfiguration());
http11NioProtocol.setSSLEnabled(true);
}
});
}
}

private SSLHostConfig getSslHostConfiguration()
{
SSLHostConfig sslHostConfig = new SSLHostConfig();
SSLHostConfigCertificate certificateConfig = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.DEFAULT_TYPE);
certificateConfig.setCertificateFile(certificate);
certificateConfig.setCertificateKeyFile(certificateKey);
sslHostConfig.addCertificate(certificateConfig);
sslHostConfig.setCertificateVerification(clientAuth);
sslHostConfig.setTrustStore(getTrustStore());
if (this.protocol != null)
{
sslHostConfig.setProtocols(protocol);
}
if (this.ciphers != null)
{
sslHostConfig.setCiphers(ciphers);
}
return sslHostConfig;
}

private KeyStore getTrustStore()
{
KeyStore trustStore = null;
try
{
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null);

PEMFile certificateBundle = new PEMFile(certificateAuthorities);
for (X509Certificate certificate : certificateBundle.getCertificates())
{
trustStore.setCertificateEntry(certificate.getSerialNumber().toString(), certificate);
}
}
catch(GeneralSecurityException | IOException exception)
{
LOG.warn("Unable to load certificate authorities", exception);
}
return trustStore;
}

/**
* Reload the {@code SSLHostConfig} if SSL is enabled. Doing so should update ssl settings and fetch certificates from Keystores
* Reload the {@code SSLHostConfig} if SSL is enabled. Doing so should update ssl settings and reload certificates
* It reloads them every 60 seconds by default
*/
@Scheduled (initialDelayString = "${server.ssl.refresh-rate-in-ms:60000}", fixedRateString = "${server.ssl.refresh-rate-in-ms:60000}")
public void reloadSslContext()
{
if (sslIsEnabled && http11NioProtocol != null)
if ((sslIsEnabled || sslIsEnabledForCertificate) && http11NioProtocol != null)
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
{
http11NioProtocol.reloadSslHostConfigs();
}
Expand Down
14 changes: 13 additions & 1 deletion application/src/main/resources/application.yml
Expand Up @@ -28,11 +28,23 @@ management:
# Security Config
#server:
# ssl:
# SSL configuration using certificate stores
# enabled: false
# key-store: <path to keystore>
# key-store-password: <password>
# key-store-type: <keystore type>
# key-alias: <key alias>
# key-password: <key password>
# Rate at which certificate are reloaded automatically
# client-auth: false
# protocol: <protocol>
# ciphers: <ciphers>
masokol marked this conversation as resolved.
Show resolved Hide resolved
# SSL configuration using certificates in PEM format
# enabled-certificate: false
# certificate: <path to certificate>
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
# certificate-key: <path to certificate private key>
# certificate-authorities: <path to certificate bundle>
# certificate-client-auth: false
# certificate-protocol: <protocol>
# certificate-ciphers: <ciphers>
# Rate at which certificates are reloaded automatically
# refresh-rate-in-ms: 60000
3 changes: 3 additions & 0 deletions application/src/main/resources/security.yml
Expand Up @@ -24,6 +24,9 @@ cql:
keystore_password: ecchronos
truststore: /path/to/truststore
truststore_password: ecchronos
certificate:
valmiranogueira marked this conversation as resolved.
Show resolved Hide resolved
certificate_key:
certificate_authorities:
protocol: TLSv1.2
algorithm:
store_type: JKS
Expand Down
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_key(null);
cqlTlsConfig.setCertificate_authorities(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_key("/path/to/cql/certificate_key");
cqlTlsConfig.setCertificate_authorities("/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);
}
}