diff --git a/CHANGELOG.md b/CHANGELOG.md index 18f4095..febc0a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/pom.xml b/pom.xml index d471d67..2e7c9e9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ de.stklcode.jvault connector - 0.7.1 + 0.8.0-SNAPSHOT jar diff --git a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java index ad2f22c..d00b1fd 100644 --- a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java @@ -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. @@ -122,18 +127,18 @@ public class HTTPVaultConnector implements VaultConnector { /** * Create connector using hostname, schema, port, path and trusted certificate. * - * @param hostname The hostname - * @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 hostname The hostname + * @param useTLS If TRUE, use HTTPS, otherwise HTTP + * @param port The port + * @param prefix HTTP API prefix (default: /v1/) + * @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); } @@ -175,38 +180,38 @@ public class HTTPVaultConnector implements VaultConnector { /** * Create connector using full URL and trusted certificate. * - * @param baseURL The URL - * @param sslContext Custom SSL Context + * @param baseURL The URL + * @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()); @@ -890,7 +898,7 @@ public class HTTPVaultConnector implements VaultConnector { new InputStreamReader(response.getEntity().getContent()))) { String responseString = br.lines().collect(Collectors.joining("\n")); ErrorResponse er = jsonMapper.readValue(responseString, ErrorResponse.class); - /* Check for "permission denied" response */ + /* Check for "permission denied" response */ if (!er.getErrors().isEmpty() && er.getErrors().get(0).equals("permission denied")) throw new PermissionDeniedException(); throw new InvalidResponseException(Error.RESPONSE_CODE, @@ -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. diff --git a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java index 92c1483..c6b48ac 100644 --- a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java @@ -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. */ diff --git a/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java b/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java index ec1b8f0..7d80c83 100644 --- a/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java +++ b/src/main/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactory.java @@ -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. * diff --git a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorOfflineTest.java b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorOfflineTest.java index 1d09826..1b1ffcc 100644 --- a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorOfflineTest.java +++ b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorOfflineTest.java @@ -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)); } @@ -466,7 +472,7 @@ public class HTTPVaultConnectorOfflineTest { private void setPrivate(Object target, String fieldName, Object value) { try { Field field = target.getClass().getDeclaredField(fieldName); - boolean accessible =field.isAccessible(); + boolean accessible = field.isAccessible(); field.setAccessible(true); field.set(target, value); field.setAccessible(accessible); diff --git a/src/test/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactoryTest.java b/src/test/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactoryTest.java index 6e067fb..8e4901b 100644 --- a/src/test/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactoryTest.java +++ b/src/test/java/de/stklcode/jvault/connector/factory/HTTPVaultConnectorFactoryTest.java @@ -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 */