Refactored custom trusted CA strategy

The connector no longer stores the final SSLContext, but the trusted
X509Certificate object and creates a SSLSocketFactory as required.
This commit is contained in:
Stefan Kalscheuer 2018-03-24 12:17:10 +01:00
parent 0c23f47bd5
commit 1a18f9f6b7
7 changed files with 120 additions and 95 deletions

View File

@ -1,3 +1,8 @@
## 0.8.0 [unreleased]
* **[breaking]** Removed support for `HTTPVaultConnectorFactory#withSslContext()` in favor of `#withTrustedCA()` due to
refactoring of the internal SSL handling.
* [improvement] `VaultConnector` extends `java.io.Serializable`
## 0.7.1 [2018-03-17]
* [improvement] Added automatic module name for JPMS compatibility
* [dependencies] Minor dependency updates

View File

@ -4,7 +4,7 @@
<groupId>de.stklcode.jvault</groupId>
<artifactId>connector</artifactId>
<version>0.7.1</version>
<version>0.8.0-SNAPSHOT</version>
<packaging>jar</packaging>

View File

@ -29,18 +29,23 @@ import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -74,14 +79,14 @@ public class HTTPVaultConnector implements VaultConnector {
private final ObjectMapper jsonMapper;
private final String baseURL; /* Base URL of Vault */
private final SSLContext sslContext; /* Custom SSLSocketFactory */
private final int retries; /* Number of retries on 5xx errors */
private final Integer timeout; /* Timeout in milliseconds */
private final String baseURL; // Base URL of Vault.
private final X509Certificate trustedCaCert; // Trusted CA certificate.
private final int retries; // Number of retries on 5xx errors.
private final Integer timeout; // Timeout in milliseconds.
private boolean authorized = false; /* authorization status */
private String token; /* current token */
private long tokenTTL = 0; /* expiration time for current token */
private boolean authorized = false; // Authorization status.
private String token; // Current token.
private long tokenTTL = 0; // Expiration time for current token.
/**
* Create connector using hostname and schema.
@ -126,14 +131,14 @@ public class HTTPVaultConnector implements VaultConnector {
* @param useTLS If TRUE, use HTTPS, otherwise HTTP
* @param port The port
* @param prefix HTTP API prefix (default: /v1/)
* @param sslContext Custom SSL Context
* @param trustedCaCert Trusted CA certificate
*/
public HTTPVaultConnector(final String hostname,
final boolean useTLS,
final Integer port,
final String prefix,
final SSLContext sslContext) {
this(hostname, useTLS, port, prefix, sslContext, 0, null);
final X509Certificate trustedCaCert) {
this(hostname, useTLS, port, prefix, trustedCaCert, 0, null);
}
/**
@ -143,7 +148,7 @@ public class HTTPVaultConnector implements VaultConnector {
* @param useTLS If TRUE, use HTTPS, otherwise HTTP
* @param port The port
* @param prefix HTTP API prefix (default: /v1/)
* @param sslContext Custom SSL Context
* @param trustedCaCert Trusted CA certificate
* @param numberOfRetries Number of retries on 5xx errors
* @param timeout Timeout for HTTP requests (milliseconds)
*/
@ -151,14 +156,14 @@ public class HTTPVaultConnector implements VaultConnector {
final boolean useTLS,
final Integer port,
final String prefix,
final SSLContext sslContext,
final X509Certificate trustedCaCert,
final int numberOfRetries,
final Integer timeout) {
this(((useTLS) ? "https" : "http")
+ "://" + hostname
+ ((port != null) ? ":" + port : "")
+ prefix,
sslContext,
trustedCaCert,
numberOfRetries,
timeout);
}
@ -176,37 +181,37 @@ public class HTTPVaultConnector implements VaultConnector {
* Create connector using full URL and trusted certificate.
*
* @param baseURL The URL
* @param sslContext Custom SSL Context
* @param trustedCaCert Trusted CA certificate
*/
public HTTPVaultConnector(final String baseURL, final SSLContext sslContext) {
this(baseURL, sslContext, 0, null);
public HTTPVaultConnector(final String baseURL, final X509Certificate trustedCaCert) {
this(baseURL, trustedCaCert, 0, null);
}
/**
* Create connector using full URL and trusted certificate.
*
* @param baseURL The URL
* @param sslContext Custom SSL Context
* @param trustedCaCert Trusted CA certificate
* @param numberOfRetries Number of retries on 5xx errors
*/
public HTTPVaultConnector(final String baseURL, final SSLContext sslContext, final int numberOfRetries) {
this(baseURL, sslContext, numberOfRetries, null);
public HTTPVaultConnector(final String baseURL, final X509Certificate trustedCaCert, final int numberOfRetries) {
this(baseURL, trustedCaCert, numberOfRetries, null);
}
/**
* Create connector using full URL and trusted certificate.
*
* @param baseURL The URL
* @param sslContext Custom SSL Context
* @param trustedCaCert Trusted CA certificate
* @param numberOfRetries Number of retries on 5xx errors
* @param timeout Timeout for HTTP requests (milliseconds)
*/
public HTTPVaultConnector(final String baseURL,
final SSLContext sslContext,
final X509Certificate trustedCaCert,
final int numberOfRetries,
final Integer timeout) {
this.baseURL = baseURL;
this.sslContext = sslContext;
this.trustedCaCert = trustedCaCert;
this.retries = numberOfRetries;
this.timeout = timeout;
this.jsonMapper = new ObjectMapper();
@ -818,8 +823,11 @@ public class HTTPVaultConnector implements VaultConnector {
/* Set JSON Header */
base.addHeader("accept", "application/json");
HttpResponse response = null;
try (CloseableHttpClient httpClient = HttpClientBuilder.create().setSSLContext(sslContext).build()) {
CloseableHttpResponse response = null;
try (CloseableHttpClient httpClient = HttpClientBuilder.create()
.setSSLSocketFactory(createSSLSocketFactory())
.build()) {
/* Set custom timeout, if defined */
if (this.timeout != null)
base.setConfig(RequestConfig.copy(RequestConfig.DEFAULT).setConnectTimeout(timeout).build());
@ -901,6 +909,39 @@ public class HTTPVaultConnector implements VaultConnector {
}
}
/**
* Create a custom socket factory from trusted CA certificate.
*
* @return The factory.
* @throws TlsException An error occured during initialization of the SSL context.
* @since 0.8.0
*/
private SSLConnectionSocketFactory createSSLSocketFactory() throws TlsException {
try {
// Create Keystore with trusted certificate.
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("trustedCert", trustedCaCert);
// Initialize TrustManager.
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
// Create context usint this TrustManager.
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), new SecureRandom());
return new SSLConnectionSocketFactory(
context,
null,
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()
);
} catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException | KeyManagementException e) {
throw new TlsException(Error.INIT_SSL_CONTEXT, e);
}
}
/**
* Inner class to bundle common error messages.
*/
@ -910,6 +951,7 @@ public class HTTPVaultConnector implements VaultConnector {
private static final String UNEXPECTED_RESPONSE = "Received response where none was expected";
private static final String URI_FORMAT = "Invalid URI format";
private static final String RESPONSE_CODE = "Invalid response code";
private static final String INIT_SSL_CONTEXT = "Unable to intialize SSLContext";
/**
* Constructor hidden, this class should not be instantiated.

View File

@ -21,6 +21,7 @@ import de.stklcode.jvault.connector.exception.VaultConnectorException;
import de.stklcode.jvault.connector.model.*;
import de.stklcode.jvault.connector.model.response.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -33,7 +34,7 @@ import java.util.Map;
* @author Stefan Kalscheuer
* @since 0.1
*/
public interface VaultConnector extends AutoCloseable {
public interface VaultConnector extends AutoCloseable, Serializable {
/**
* Default sub-path for Vault secrets.
*/

View File

@ -22,8 +22,6 @@ import de.stklcode.jvault.connector.exception.TlsException;
import de.stklcode.jvault.connector.exception.VaultConnectorException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
@ -31,7 +29,6 @@ import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@ -58,7 +55,7 @@ public final class HTTPVaultConnectorFactory extends VaultConnectorFactory {
private Integer port;
private boolean tls;
private String prefix;
private SSLContext sslContext;
private X509Certificate trustedCA;
private int numberOfRetries;
private Integer timeout;
private String token;
@ -146,8 +143,23 @@ public final class HTTPVaultConnectorFactory extends VaultConnectorFactory {
* @since 0.4.0
*/
public HTTPVaultConnectorFactory withTrustedCA(final Path cert) throws VaultConnectorException {
if (cert != null)
return withSslContext(createSslContext(cert));
if (cert != null) {
return withTrustedCA(certificateFromFile(cert));
} else {
this.trustedCA = null;
}
return this;
}
/**
* Add a trusted CA certifiate for HTTPS connections.
*
* @param cert path to certificate file
* @return self
* @since 0.8.0
*/
public HTTPVaultConnectorFactory withTrustedCA(final X509Certificate cert) {
this.trustedCA = cert;
return this;
}
@ -158,10 +170,10 @@ public final class HTTPVaultConnectorFactory extends VaultConnectorFactory {
* @param sslContext the SSL context
* @return self
* @since 0.4.0
* @deprecated As of 0.8.0 this is no longer supported, please use {@link #withTrustedCA(Path)} or {@link #withTrustedCA(X509Certificate)}.
*/
public HTTPVaultConnectorFactory withSslContext(final SSLContext sslContext) {
this.sslContext = sslContext;
return this;
throw new UnsupportedOperationException("Use of deprecated method, please switch to withTrustedCA()");
}
/**
@ -241,59 +253,18 @@ public final class HTTPVaultConnectorFactory extends VaultConnectorFactory {
@Override
public HTTPVaultConnector build() {
return new HTTPVaultConnector(host, tls, port, prefix, sslContext, numberOfRetries, timeout);
return new HTTPVaultConnector(host, tls, port, prefix, trustedCA, numberOfRetries, timeout);
}
@Override
public HTTPVaultConnector buildAndAuth() throws VaultConnectorException {
if (token == null)
throw new ConnectionException("No vault token provided, unable to authenticate.");
HTTPVaultConnector con = new HTTPVaultConnector(host, tls, port, prefix, sslContext, numberOfRetries, timeout);
HTTPVaultConnector con = new HTTPVaultConnector(host, tls, port, prefix, trustedCA, numberOfRetries, timeout);
con.authToken(token);
return con;
}
/**
* Create SSL Context trusting only provided certificate.
*
* @param trustedCert Path to trusted CA certificate
* @return SSL context
* @throws TlsException on errors
* @since 0.4.0
*/
private SSLContext createSslContext(final Path trustedCert) throws TlsException {
try {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, createTrustManager(trustedCert), new SecureRandom());
return context;
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new TlsException("Unable to intialize SSLContext", e);
}
}
/**
* Create a custom TrustManager for given CA certificate file.
*
* @param trustedCert Path to trusted CA certificate
* @return TrustManger
* @throws TlsException on error
* @since 0.4.0
*/
private TrustManager[] createTrustManager(final Path trustedCert) throws TlsException {
try {
/* Create Keystore with trusted certificate */
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("trustedCert", certificateFromFile(trustedCert));
/* Initialize TrustManager */
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(keyStore);
return tmf.getTrustManagers();
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
throw new TlsException("Unable to initialize TrustManager", e);
}
}
/**
* Read given certificate file to X.509 certificate.
*

View File

@ -34,10 +34,12 @@ import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collections;
import static net.bytebuddy.implementation.MethodDelegation.to;
@ -89,7 +91,7 @@ public class HTTPVaultConnectorOfflineTest {
.load(HttpClientBuilder.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
// Ignore SSL context settings.
when(httpMockBuilder.setSSLContext(null)).thenReturn(httpMockBuilder);
when(httpMockBuilder.setSSLSocketFactory(any())).thenReturn(httpMockBuilder);
// Re-initialize HTTP mock to ensure fresh (empty) results.
httpMock = mock(CloseableHttpClient.class);
@ -159,7 +161,7 @@ public class HTTPVaultConnectorOfflineTest {
* Test constductors of the {@link HTTPVaultConnector} class.
*/
@Test
public void constructorTest() throws NoSuchAlgorithmException {
public void constructorTest() throws IOException, CertificateException {
final String url = "https://vault.example.net/test/";
final String hostname = "vault.example.com";
final Integer port = 1337;
@ -168,7 +170,11 @@ public class HTTPVaultConnectorOfflineTest {
final String expectedNoTls = "http://" + hostname + "/v1/";
final String expectedCustomPort = "https://" + hostname + ":" + port + "/v1/";
final String expectedCustomPrefix = "https://" + hostname + ":" + port + prefix;
final SSLContext sslContext = SSLContext.getInstance("TLS");
X509Certificate trustedCaCert = null;
try (InputStream is = getClass().getResourceAsStream("/tls/ca.pem")) {
trustedCaCert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is);
}
// Most basic constructor expects complete URL.
HTTPVaultConnector connector = new HTTPVaultConnector(url);
@ -185,15 +191,15 @@ public class HTTPVaultConnectorOfflineTest {
// Specify custom prefix.
connector = new HTTPVaultConnector(hostname, true, port, prefix);
assertThat("Unexpected base URL with custom prefix", getPrivate(connector, "baseURL"), is(expectedCustomPrefix));
assertThat("SSL context set, but not specified", getPrivate(connector, "sslContext"), is(nullValue()));
assertThat("Trusted CA cert set, but not specified", getPrivate(connector, "trustedCaCert"), is(nullValue()));
// Provide custom SSL context.
connector = new HTTPVaultConnector(hostname, true, port, prefix, sslContext);
connector = new HTTPVaultConnector(hostname, true, port, prefix, trustedCaCert);
assertThat("Unexpected base URL with custom prefix", getPrivate(connector, "baseURL"), is(expectedCustomPrefix));
assertThat("SSL context not filled correctly", getPrivate(connector, "sslContext"), is(sslContext));
assertThat("Trusted CA cert not filled correctly", getPrivate(connector, "trustedCaCert"), is(trustedCaCert));
// Specify number of retries.
connector = new HTTPVaultConnector(url, sslContext, retries);
connector = new HTTPVaultConnector(url, trustedCaCert, retries);
assertThat("Number of retries not set correctly", getPrivate(connector, "retries"), is(retries));
}

View File

@ -69,7 +69,7 @@ public class HTTPVaultConnectorFactoryTest {
connector = factory.build();
assertThat("URL nor set correctly", getPrivate(connector, "baseURL"), is(equalTo(VAULT_ADDR + "/v1/")));
assertThat("SSL context set when no cert provided", getPrivate(connector, "sslContext"), is(nullValue()));
assertThat("Trusted CA cert set when no cert provided", getPrivate(connector, "trustedCaCert"), is(nullValue()));
assertThat("Non-default number of retries, when none set", getPrivate(connector, "retries"), is(0));
/* Provide address and number of retries */
@ -83,7 +83,7 @@ public class HTTPVaultConnectorFactoryTest {
connector = factory.build();
assertThat("URL nor set correctly", getPrivate(connector, "baseURL"), is(equalTo(VAULT_ADDR + "/v1/")));
assertThat("SSL context set when no cert provided", getPrivate(connector, "sslContext"), is(nullValue()));
assertThat("Trusted CA cert set when no cert provided", getPrivate(connector, "trustedCaCert"), is(nullValue()));
assertThat("Number of retries not set correctly", getPrivate(connector, "retries"), is(VAULT_MAX_RETRIES));
/* Provide CA certificate */