Skip to content

Commit 50e1874

Browse files
authored
Merge pull request #18 from SourceLabOrg/sp/clientCert
Add ability to send a client certificate with requests by defining a keystore
2 parents 9c3f057 + c332832 commit 50e1874

File tree

12 files changed

+608
-51
lines changed

12 files changed

+608
-51
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
The format is based on [Keep a Changelog](http://keepachangelog.com/)
33
and this project adheres to [Semantic Versioning](http://semver.org/).
44

5+
## 1.3.0 (UNRELEASED)
6+
7+
### New Features
8+
- Added ability to configure sending a client certificate in order to authenicate Kafka-Connect REST endpoints.
9+
510
## 1.2.0 (02/02/2019)
611

712
### New Features

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ final Configuration configuration = new Configuration("https://hostname.for.kafk
7373
* with.
7474
*/
7575
configuration.useTrustStore(
76-
new File("/path/to/truststore.jks"), "TrustStorePasswordHere or NULL"
76+
new File("/path/to/truststore.jks"), "Optional Password Here or NULL"
7777
);
7878

7979
/*
@@ -83,6 +83,14 @@ configuration.useTrustStore(
8383
*/
8484
//configuration.useInsecureSslCertificates();
8585

86+
/*
87+
* If your Kafka-Connect instance is configured to validate client certificates, you can configure a KeyStore for
88+
* the client to send with each request:
89+
*/
90+
configuration.useKeyStore(
91+
new File("/path/to/keystore.jks"), "Optional Password Here or NULL"
92+
);
93+
8694
/*
8795
* Create an instance of KafkaConnectClient, passing your configuration.
8896
*/

pom.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>org.sourcelab</groupId>
88
<artifactId>kafka-connect-client</artifactId>
9-
<version>1.2.0</version>
9+
<version>1.3.0</version>
1010
<packaging>jar</packaging>
1111

1212
<!-- Require Maven 3.3.9 -->
@@ -161,6 +161,14 @@
161161
<version>2.6</version>
162162
<scope>test</scope>
163163
</dependency>
164+
165+
<!-- Test Http/Https Client -->
166+
<dependency>
167+
<groupId>org.eclipse.jetty</groupId>
168+
<artifactId>jetty-server</artifactId>
169+
<version>9.4.14.v20181114</version>
170+
<scope>test</scope>
171+
</dependency>
164172
</dependencies>
165173

166174
<build>

src/main/java/org/sourcelab/kafka/connect/apiclient/Configuration.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ public final class Configuration {
3636
private String basicAuthUsername = null;
3737
private String basicAuthPassword = null;
3838

39-
// Optional SSL options
39+
// Optional settings to validate Kafka-Connect's SSL certificate.
4040
private boolean ignoreInvalidSslCertificates = false;
4141
private File trustStoreFile = null;
4242
private String trustStorePassword = null;
4343

44+
// Optional settings to send a client certificate with request.
45+
private File keyStoreFile = null;
46+
private String keyStorePassword = null;
47+
4448
// Optional Proxy Configuration
4549
private String proxyHost = null;
4650
private int proxyPort = 0;
@@ -59,7 +63,7 @@ public Configuration(final String kafkaConnectHost) {
5963
throw new NullPointerException("Kafka Connect Host parameter cannot be null!");
6064
}
6165

62-
// Normalize into "http://<hostname>"
66+
// Normalize into "http://<hostname>" if not specified.
6367
if (kafkaConnectHost.startsWith("http://") || kafkaConnectHost.startsWith("https://")) {
6468
this.apiHost = kafkaConnectHost;
6569
} else {
@@ -120,21 +124,36 @@ public Configuration useInsecureSslCertificates() {
120124
}
121125

122126
/**
123-
* You can supply a path to a JKS trust store to be used to validate SSL certificates with.
127+
* (Optional) Supply a path to a JKS trust store to be used to validate SSL certificates with. You'll need this
128+
* if you're using Self Signed certificates.
124129
*
125130
* Alternatively you can can explicitly add your certificate to the JVM's truststore using a command like:
126131
* keytool -importcert -keystore truststore.jks -file servercert.pem
127132
*
128-
* @param trustStorePath file path to truststore.
129-
* @param password (optional) Password for truststore.
133+
* @param trustStoreFile file path to truststore.
134+
* @param password (optional) Password for truststore. Pass null if no password.
130135
* @return Configuration instance.
131136
*/
132-
public Configuration useTrustStore(final File trustStorePath, final String password) {
133-
this.trustStoreFile = Objects.requireNonNull(trustStorePath);
137+
public Configuration useTrustStore(final File trustStoreFile, final String password) {
138+
this.trustStoreFile = Objects.requireNonNull(trustStoreFile);
134139
this.trustStorePassword = password;
135140
return this;
136141
}
137142

143+
/**
144+
* (Optional) Supply a path to a JKS key store to be used for client validation. You'll need this if your
145+
* Kafka-Connect instance is configured to only accept requests from clients with a valid certificate.
146+
*
147+
* @param keyStoreFile file path to keystore.
148+
* @param password (optional) Password for keystore. Pass null if no password.
149+
* @return Configuration instance.
150+
*/
151+
public Configuration useKeyStore(final File keyStoreFile, final String password) {
152+
this.keyStoreFile = Objects.requireNonNull(keyStoreFile);
153+
this.keyStorePassword = password;
154+
return this;
155+
}
156+
138157
/**
139158
* Set the request timeout value, in seconds.
140159
* @param requestTimeoutInSeconds How long before a request times out, in seconds.
@@ -185,6 +204,14 @@ public int getRequestTimeoutInSeconds() {
185204
return requestTimeoutInSeconds;
186205
}
187206

207+
public File getKeyStoreFile() {
208+
return keyStoreFile;
209+
}
210+
211+
public String getKeyStorePassword() {
212+
return keyStorePassword;
213+
}
214+
188215
public String getBasicAuthUsername() {
189216
return basicAuthUsername;
190217
}

src/main/java/org/sourcelab/kafka/connect/apiclient/rest/HttpClientRestClient.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.sourcelab.kafka.connect.apiclient.rest.exceptions.ResultParsingException;
4949
import org.sourcelab.kafka.connect.apiclient.rest.handlers.RestResponseHandler;
5050

51+
import javax.net.ssl.SSLHandshakeException;
5152
import java.io.IOException;
5253
import java.net.MalformedURLException;
5354
import java.net.SocketException;
@@ -261,8 +262,8 @@ private <T> T submitGetRequest(final String url, final Map<String, String> getPa
261262

262263
// Execute and return
263264
return httpClient.execute(get, responseHandler, httpClientContext);
264-
} catch (final ClientProtocolException | SocketException | URISyntaxException connectionException) {
265-
// Typically this is a connection issue.
265+
} catch (final ClientProtocolException | SocketException | URISyntaxException | SSLHandshakeException connectionException) {
266+
// Typically this is a connection or certificate issue.
266267
throw new ConnectionException(connectionException.getMessage(), connectionException);
267268
} catch (final IOException ioException) {
268269
// Typically this is a parse error.
@@ -294,7 +295,7 @@ private <T> T submitPostRequest(final String url, final Object requestBody, fina
294295

295296
// Execute and return
296297
return httpClient.execute(post, responseHandler, httpClientContext);
297-
} catch (final ClientProtocolException | SocketException connectionException) {
298+
} catch (final ClientProtocolException | SocketException | SSLHandshakeException connectionException) {
298299
// Typically this is a connection issue.
299300
throw new ConnectionException(connectionException.getMessage(), connectionException);
300301
} catch (final IOException ioException) {
@@ -326,7 +327,7 @@ private <T> T submitPutRequest(final String url, final Object requestBody, final
326327

327328
// Execute and return
328329
return httpClient.execute(put, responseHandler, httpClientContext);
329-
} catch (final ClientProtocolException | SocketException connectionException) {
330+
} catch (final ClientProtocolException | SocketException | SSLHandshakeException connectionException) {
330331
// Typically this is a connection issue.
331332
throw new ConnectionException(connectionException.getMessage(), connectionException);
332333
} catch (final IOException ioException) {
@@ -357,7 +358,7 @@ private <T> T submitDeleteRequest(final String url, final Object requestBody, fi
357358

358359
// Execute and return
359360
return httpClient.execute(delete, responseHandler, httpClientContext);
360-
} catch (final ClientProtocolException | SocketException connectionException) {
361+
} catch (final ClientProtocolException | SocketException | SSLHandshakeException connectionException) {
361362
// Typically this is a connection issue.
362363
throw new ConnectionException(connectionException.getMessage(), connectionException);
363364
} catch (final IOException ioException) {

src/main/java/org/sourcelab/kafka/connect/apiclient/rest/HttpsContextBuilder.java

Lines changed: 95 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,19 @@
2727

2828
import javax.net.ssl.HostnameVerifier;
2929
import javax.net.ssl.KeyManager;
30+
import javax.net.ssl.KeyManagerFactory;
3031
import javax.net.ssl.SSLContext;
3132
import javax.net.ssl.TrustManager;
3233
import javax.net.ssl.TrustManagerFactory;
3334
import java.io.FileInputStream;
35+
import java.io.FileNotFoundException;
3436
import java.io.IOException;
3537
import java.security.KeyManagementException;
3638
import java.security.KeyStore;
3739
import java.security.KeyStoreException;
3840
import java.security.NoSuchAlgorithmException;
3941
import java.security.SecureRandom;
42+
import java.security.UnrecoverableKeyException;
4043
import java.security.cert.CertificateException;
4144
import java.util.Objects;
4245

@@ -60,10 +63,28 @@ class HttpsContextBuilder {
6063
* Constructor.
6164
* @param configuration client configuration instance.
6265
*/
63-
HttpsContextBuilder(final Configuration configuration) {
66+
public HttpsContextBuilder(final Configuration configuration) {
6467
this.configuration = Objects.requireNonNull(configuration);
6568
}
6669

70+
/**
71+
* Properly configured SslSocketFactory based on client configuration.
72+
* @return SslSocketFactory instance.
73+
*/
74+
public LayeredConnectionSocketFactory createSslSocketFactory() {
75+
// Emit an warning letting everyone know we're using an insecure configuration.
76+
if (configuration.getIgnoreInvalidSslCertificates()) {
77+
logger.warn("Using insecure configuration, skipping server-side certificate validation checks.");
78+
}
79+
80+
return new SSLConnectionSocketFactory(
81+
getSslContext(),
82+
getSslProtocols(),
83+
null,
84+
getHostnameVerifier()
85+
);
86+
}
87+
6788
/**
6889
* Get HostnameVerifier instance based on client configuration.
6990
* @return HostnameVerifier instance.
@@ -83,28 +104,78 @@ HostnameVerifier getHostnameVerifier() {
83104
* @return SSLContext instance.
84105
*/
85106
SSLContext getSslContext() {
86-
// Create default SSLContext
87-
final SSLContext sslcontext = SSLContexts.createDefault();
107+
try {
108+
// Create default SSLContext
109+
final SSLContext sslcontext = SSLContexts.createDefault();
110+
111+
// Initialize ssl context with configured key and trust managers.
112+
sslcontext.init(getKeyManagers(), getTrustManagers(), new SecureRandom());
113+
114+
return sslcontext;
115+
} catch (final KeyManagementException e) {
116+
throw new RuntimeException(e.getMessage(), e);
117+
}
118+
}
119+
120+
/**
121+
* Based on client configuration, construct KeyManager instances to use.
122+
* @return Array of 0 or more KeyManagers.
123+
*/
124+
KeyManager[] getKeyManagers() {
125+
// If not configured to use a KeyStore
126+
if (configuration.getKeyStoreFile() == null) {
127+
// Return null array.
128+
return new KeyManager[0];
129+
}
130+
131+
// If configured to use a key store
132+
try {
133+
final KeyManagerFactory keyFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
134+
135+
// New JKS Keystore.
136+
final KeyStore keyStore = KeyStore.getInstance("JKS");
137+
final char[] password;
138+
if (configuration.getKeyStorePassword() == null) {
139+
password = new char[0];
140+
} else {
141+
password = configuration.getKeyStorePassword().toCharArray();
142+
}
143+
144+
try (final FileInputStream keyStoreFileInput = new FileInputStream(configuration.getKeyStoreFile())) {
145+
keyStore.load(keyStoreFileInput, password);
146+
}
147+
keyFactory.init(keyStore, password);
148+
return keyFactory.getKeyManagers();
149+
} catch (final FileNotFoundException exception) {
150+
throw new RuntimeException(
151+
"Unable to find configured KeyStore file \"" + configuration.getKeyStoreFile() + "\"",
152+
exception
153+
);
154+
} catch (final KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException e) {
155+
throw new RuntimeException(e.getMessage(), e);
156+
}
157+
}
88158

159+
/**
160+
* Based on Client Configuration, construct TrustManager instances to use.
161+
* @return Array of 0 or more TrustManager instances.
162+
*/
163+
TrustManager[] getTrustManagers() {
89164
try {
165+
final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
166+
90167
// If client configuration is set to ignore invalid certificates
91168
if (configuration.getIgnoreInvalidSslCertificates()) {
92169
// Initialize ssl context with a TrustManager instance that just accepts everything blindly.
93170
// HIGHLY INSECURE / NOT RECOMMENDED!
94-
sslcontext.init(new KeyManager[0], new TrustManager[]{new NoopTrustManager()}, new SecureRandom());
171+
return new TrustManager[]{ new NoopTrustManager() };
95172

96173
// If client configuration has a trust store defined.
97174
} else if (configuration.getTrustStoreFile() != null) {
98-
99-
final TrustManagerFactory trustManagerFactory = TrustManagerFactory
100-
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
101-
102-
103-
// New JKS Keystore.
104-
final KeyStore keyStore = KeyStore.getInstance("JKS");
105-
106175
// Attempt to read the trust store from disk.
107176
try (final FileInputStream trustStoreFileInput = new FileInputStream(configuration.getTrustStoreFile())) {
177+
// New JKS Keystore.
178+
final KeyStore keyStore = KeyStore.getInstance("JKS");
108179

109180
// If no trust store password is set.
110181
if (configuration.getTrustStorePassword() == null) {
@@ -114,15 +185,20 @@ SSLContext getSslContext() {
114185
}
115186
trustManagerFactory.init(keyStore);
116187
}
117-
118-
// Initialize ssl context with our custom loaded trust store.
119-
sslcontext.init(new KeyManager[0], trustManagerFactory.getTrustManagers(), new SecureRandom());
188+
return trustManagerFactory.getTrustManagers();
189+
} else {
190+
// use default TrustManager instances
191+
trustManagerFactory.init((KeyStore) null);
192+
return trustManagerFactory.getTrustManagers();
120193
}
121-
} catch (final KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException e) {
122-
throw new RuntimeException(e.getMessage(), e);
194+
} catch (final FileNotFoundException exception) {
195+
throw new RuntimeException(
196+
"Unable to find configured TrustStore file \"" + configuration.getTrustStoreFile() + "\"",
197+
exception
198+
);
199+
} catch (final KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException exception) {
200+
throw new RuntimeException(exception.getMessage(), exception);
123201
}
124-
125-
return sslcontext;
126202
}
127203

128204
/**
@@ -132,22 +208,4 @@ SSLContext getSslContext() {
132208
private String[] getSslProtocols() {
133209
return sslProtocols;
134210
}
135-
136-
/**
137-
* Properly configured SslSocketFactory based on client configuration.
138-
* @return SslSocketFactory instance.
139-
*/
140-
LayeredConnectionSocketFactory createSslSocketFactory() {
141-
// Emit an warning letting everyone know we're using an insecure configuration.
142-
if (configuration.getIgnoreInvalidSslCertificates()) {
143-
logger.warn("Using insecure configuration, skipping server-side certificate validation checks.");
144-
}
145-
146-
return new SSLConnectionSocketFactory(
147-
getSslContext(),
148-
getSslProtocols(),
149-
null,
150-
getHostnameVerifier()
151-
);
152-
}
153211
}

0 commit comments

Comments
 (0)