Skip to content

Commit e530a65

Browse files
committed
SSL: client auth mode
- trust store support on SSLOptions - implements client auth mode
1 parent c79601c commit e530a65

File tree

7 files changed

+212
-28
lines changed

7 files changed

+212
-28
lines changed

jooby/src/main/java/io/jooby/SslOptions.java

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,26 @@
3232
* @since 2.3.0
3333
*/
3434
public final class SslOptions {
35+
/**
36+
* The desired SSL client authentication mode for SSL channels in server mode.
37+
*/
38+
public enum ClientAuth {
39+
/**
40+
* SSL client authentication is NOT requested.
41+
*/
42+
NONE,
43+
44+
/**
45+
* SSL client authentication is requested but not required.
46+
*/
47+
REQUESTED,
48+
49+
/**
50+
* SSL client authentication is required.
51+
*/
52+
REQUIRED
53+
}
54+
3555
/** X509 constant. */
3656
public static final String X509 = "X509";
3757

@@ -44,8 +64,14 @@ public final class SslOptions {
4464

4565
private String cert;
4666

67+
private String trustCert;
68+
69+
private String trustPassword;
70+
4771
private String privateKey;
4872

73+
private ClientAuth clientAuth = ClientAuth.NONE;
74+
4975
/**
5076
* Certificate type. Default is {@link #PKCS12}.
5177
*
@@ -89,6 +115,50 @@ public String getType() {
89115
return this;
90116
}
91117

118+
/**
119+
* A PKCS12 or X.509 certificate chain file in PEM format. It can be an absolute path or a
120+
* classpath resource. Required for {@link ClientAuth#REQUIRED} or {@link ClientAuth#REQUESTED}.
121+
*
122+
* @return A PKCS12 or X.509 certificate chain file in PEM format. It can be an absolute path or
123+
* a classpath resource. Required for {@link ClientAuth#REQUIRED} or
124+
* {@link ClientAuth#REQUESTED}.
125+
*/
126+
public @Nullable String getTrustCert() {
127+
return trustCert;
128+
}
129+
130+
/**
131+
* Set certificate path. A PKCS12 or X.509 certificate chain file in PEM format.
132+
* It can be an absolute path or a classpath resource. Required.
133+
*
134+
* @param trustCert Certificate path or location.
135+
* @return Ssl options.
136+
*/
137+
public @Nonnull SslOptions setTrustCert(@Nullable String trustCert) {
138+
this.trustCert = trustCert;
139+
return this;
140+
}
141+
142+
/**
143+
* Trust certificate password. Optional.
144+
*
145+
* @return Trust certificate password. Optional.
146+
*/
147+
public @Nullable String getTrustPassword() {
148+
return trustPassword;
149+
}
150+
151+
/**
152+
* Set trust certificate password.
153+
*
154+
* @param password Certificate password.
155+
* @return SSL options.
156+
*/
157+
public @Nonnull SslOptions setTrustPassword(@Nullable String password) {
158+
this.trustPassword = password;
159+
return this;
160+
}
161+
92162
/**
93163
* Private key file location. A PKCS#8 private key file in PEM format. It can be an absolute path
94164
* or a classpath resource. Required when using X.509 certificates.
@@ -161,6 +231,28 @@ public String getType() {
161231
return resource;
162232
}
163233

234+
/**
235+
* The desired SSL client authentication mode for SSL channels in server mode.
236+
*
237+
* Default is: {@link ClientAuth#REQUESTED}.
238+
*
239+
* @return desired SSL client authentication mode for SSL channels in server mode.
240+
*/
241+
public @Nonnull ClientAuth getClientAuth() {
242+
return clientAuth;
243+
}
244+
245+
/**
246+
* Set desired SSL client authentication mode for SSL channels in server mode.
247+
*
248+
* @param clientAuth The desired SSL client authentication mode for SSL channels in server mode.
249+
* @return This options.
250+
*/
251+
public @Nonnull SslOptions setClientAuth(@Nonnull ClientAuth clientAuth) {
252+
this.clientAuth = clientAuth;
253+
return this;
254+
}
255+
164256
@Override public String toString() {
165257
return type;
166258
}
@@ -307,10 +399,11 @@ public static SslOptions selfSigned(final String type) {
307399
String type = conf.hasPath(path + ".type")
308400
? conf.getString(path + ".type").toUpperCase()
309401
: PKCS12;
402+
SslOptions options;
310403
if (type.equalsIgnoreCase("self-signed")) {
311-
return SslOptions.selfSigned();
404+
options = SslOptions.selfSigned();
312405
} else {
313-
SslOptions options = new SslOptions();
406+
options = new SslOptions();
314407
options.setType(type);
315408
if (X509.equalsIgnoreCase(type)) {
316409
options.setCert(conf.getString(path + ".cert"));
@@ -324,8 +417,18 @@ public static SslOptions selfSigned(final String type) {
324417
} else {
325418
throw new UnsupportedOperationException("SSL type: " + type);
326419
}
327-
return options;
328420
}
421+
if (conf.hasPath(path + ".clientAuth")) {
422+
options.setClientAuth(ClientAuth.valueOf(conf.getString(path + ".clientAuth")
423+
.toUpperCase()));
424+
}
425+
if (conf.hasPath(path + ".trust.cert")) {
426+
options.setTrustCert(conf.getString(path + ".trust.cert"));
427+
}
428+
if (conf.hasPath(path + ".trust.password")) {
429+
options.setTrustPassword(conf.getString(path + ".trust.password"));
430+
}
431+
return options;
329432
});
330433
}
331434
}

jooby/src/main/java/io/jooby/internal/SslPkcs12Provider.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
*/
66
package io.jooby.internal;
77

8-
import io.jooby.SslOptions;
98
import io.jooby.SneakyThrows;
9+
import io.jooby.SslOptions;
1010

1111
import javax.net.ssl.KeyManager;
1212
import javax.net.ssl.KeyManagerFactory;
1313
import javax.net.ssl.SSLContext;
14+
import javax.net.ssl.TrustManager;
15+
import javax.net.ssl.TrustManagerFactory;
1416
import java.io.InputStream;
1517
import java.security.KeyStore;
1618

@@ -21,18 +23,44 @@ public class SslPkcs12Provider implements SslContextProvider {
2123
}
2224

2325
@Override public SSLContext create(ClassLoader loader, SslOptions options) {
24-
try (InputStream crt = options.getResource(loader, options.getCert())) {
25-
KeyStore store = KeyStore.getInstance(options.getType());
26-
store.load(crt, options.getPassword().toCharArray());
26+
try {
27+
KeyStore store = keystore(options, loader, options.getCert(), options.getPassword());
2728
KeyManagerFactory kmf = KeyManagerFactory
2829
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
29-
kmf.init(store, options.getPassword().toCharArray());
30+
kmf.init(store, toCharArray(options.getPassword()));
3031
KeyManager[] kms = kmf.getKeyManagers();
3132
SSLContext context = SSLContext.getInstance("TLS");
32-
context.init(kms, null, null);
33+
34+
TrustManager[] tms;
35+
if (options.getTrustCert() != null) {
36+
KeyStore trustStore = keystore(options, loader, options.getTrustCert(),
37+
options.getTrustPassword());
38+
39+
TrustManagerFactory tmf = TrustManagerFactory
40+
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
41+
tmf.init(trustStore);
42+
tms = tmf.getTrustManagers();
43+
} else {
44+
tms = null;
45+
}
46+
47+
context.init(kms, tms, null);
3348
return context;
3449
} catch (Exception x) {
3550
throw SneakyThrows.propagate(x);
3651
}
3752
}
53+
54+
private KeyStore keystore(SslOptions options, ClassLoader loader, String file, String password)
55+
throws Exception {
56+
try (InputStream crt = options.getResource(loader, file)) {
57+
KeyStore store = KeyStore.getInstance(options.getType());
58+
store.load(crt, toCharArray(password));
59+
return store;
60+
}
61+
}
62+
63+
private char[] toCharArray(String password) {
64+
return password == null ? null : password.toCharArray();
65+
}
3866
}

jooby/src/main/java/io/jooby/internal/SslX509Provider.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ public class SslX509Provider implements SslContextProvider {
1919

2020
@Override public SSLContext create(ClassLoader loader, SslOptions options) {
2121
try {
22-
InputStream trustCert = null;
22+
InputStream trustCert;
23+
if (options.getTrustCert() == null) {
24+
trustCert = null;
25+
} else {
26+
trustCert = options.getResource(loader, options.getTrustCert());
27+
}
2328
InputStream keyStoreCert = options.getResource(loader, options.getCert());
2429
InputStream keyStoreKey = options.getResource(loader, options.getPrivateKey());
2530
String keyStorePass = null;

jooby/src/test/java/io/jooby/SslOptionsTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@ public void shouldLoadPKCS12FromConfig() {
4444
.withValue("ssl.type", fromAnyRef("pkcs12"))
4545
.withValue("ssl.cert", fromAnyRef("ssl/test.p12"))
4646
.withValue("ssl.password", fromAnyRef("changeit"))
47+
.withValue("ssl.trust.cert", fromAnyRef("ssl/trust.p12"))
48+
.withValue("ssl.trust.password", fromAnyRef("pass"))
4749
.resolve();
4850

4951
SslOptions options = SslOptions.from(config).get();
5052
assertEquals(SslOptions.PKCS12, options.getType());
5153
assertEquals("ssl/test.p12", options.getCert());
5254
assertEquals("changeit", options.getPassword());
55+
assertEquals("ssl/trust.p12", options.getTrustCert());
56+
assertEquals("pass", options.getTrustPassword());
5357
}
5458

5559
@Test

modules/jooby-jetty/src/main/java/io/jooby/jetty/Jetty.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.jooby.Jooby;
1010
import io.jooby.ServerOptions;
1111
import io.jooby.SneakyThrows;
12+
import io.jooby.SslOptions;
1213
import io.jooby.WebSocket;
1314
import io.jooby.internal.jetty.JettyHandler;
1415
import io.jooby.internal.jetty.JettyWebSocket;
@@ -32,6 +33,7 @@
3233
import java.net.BindException;
3334
import java.util.ArrayList;
3435
import java.util.List;
36+
import java.util.Optional;
3537
import java.util.concurrent.TimeUnit;
3638

3739
/**
@@ -100,10 +102,19 @@ public class Jetty extends io.jooby.Server.Base {
100102
server.addConnector(http);
101103

102104
if (options.isSSLEnabled()) {
103-
SslContextFactory sslContextFactory = new SslContextFactory.Server();
105+
SslContextFactory.Server sslContextFactory = new SslContextFactory.Server();
104106
sslContextFactory
105107
.setSslContext(options.getSSLContext(application.getEnvironment().getClassLoader()));
106108

109+
SslOptions.ClientAuth clientAuth = Optional.ofNullable(options.getSsl())
110+
.map(SslOptions::getClientAuth)
111+
.orElse(SslOptions.ClientAuth.NONE);
112+
if (clientAuth == SslOptions.ClientAuth.REQUESTED) {
113+
sslContextFactory.setWantClientAuth(true);
114+
} else if (clientAuth == SslOptions.ClientAuth.REQUIRED) {
115+
sslContextFactory.setNeedClientAuth(true);
116+
}
117+
107118
HttpConfiguration httpsConf = new HttpConfiguration(httpConf);
108119
httpsConf.addCustomizer(new SecureRequestCustomizer());
109120

modules/jooby-netty/src/main/java/io/jooby/netty/Netty.java

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.jooby.Server;
1010
import io.jooby.ServerOptions;
1111
import io.jooby.SneakyThrows;
12+
import io.jooby.SslOptions;
1213
import io.jooby.internal.netty.NettyPipeline;
1314
import io.jooby.internal.netty.NettyTransport;
1415
import io.netty.bootstrap.ServerBootstrap;
@@ -30,6 +31,7 @@
3031
import java.net.BindException;
3132
import java.util.ArrayList;
3233
import java.util.List;
34+
import java.util.Optional;
3335
import java.util.concurrent.ExecutionException;
3436
import java.util.concurrent.ExecutorService;
3537
import java.util.concurrent.Executors;
@@ -109,8 +111,13 @@ public class Netty extends Server.Base {
109111
http.bind(options.getHost(), options.getPort()).get();
110112

111113
if (options.isSSLEnabled()) {
114+
SSLContext javaSslContext = options
115+
.getSSLContext(application.getEnvironment().getClassLoader());
116+
SslOptions.ClientAuth clientAuth = Optional.ofNullable(options.getSsl())
117+
.map(SslOptions::getClientAuth)
118+
.orElse(SslOptions.ClientAuth.NONE);
112119
ServerBootstrap https = transport.configure(acceptorloop, eventloop)
113-
.childHandler(newPipeline(factory, options.getSSLContext(application.getEnvironment().getClassLoader())))
120+
.childHandler(newPipeline(factory, wrap(javaSslContext, toClientAuth(clientAuth))))
114121
.childOption(ChannelOption.SO_REUSEADDR, true)
115122
.childOption(ChannelOption.TCP_NODELAY, true);
116123

@@ -130,17 +137,28 @@ public class Netty extends Server.Base {
130137
return this;
131138
}
132139

133-
private NettyPipeline newPipeline(HttpDataFactory factory, SSLContext sslContext) {
140+
private ClientAuth toClientAuth(SslOptions.ClientAuth clientAuth) {
141+
switch (clientAuth) {
142+
case REQUIRED:
143+
return ClientAuth.REQUIRE;
144+
case REQUESTED:
145+
return ClientAuth.OPTIONAL;
146+
default:
147+
return ClientAuth.NONE;
148+
}
149+
}
150+
151+
private NettyPipeline newPipeline(HttpDataFactory factory, SslContext sslContext) {
134152
return new NettyPipeline(
135-
acceptorloop.next(),
136-
applications.get(0),
137-
factory,
138-
wrap(sslContext),
139-
options.getDefaultHeaders(),
140-
options.getCompressionLevel(),
141-
options.getBufferSize(),
142-
options.getMaxRequestSize()
143-
);
153+
acceptorloop.next(),
154+
applications.get(0),
155+
factory,
156+
sslContext,
157+
options.getDefaultHeaders(),
158+
options.getCompressionLevel(),
159+
options.getBufferSize(),
160+
options.getMaxRequestSize()
161+
);
144162
}
145163

146164
@Nonnull @Override public synchronized Server stop() {
@@ -160,11 +178,8 @@ private NettyPipeline newPipeline(HttpDataFactory factory, SSLContext sslContext
160178
return this;
161179
}
162180

163-
private SslContext wrap(SSLContext sslContext) {
164-
if (sslContext != null) {
165-
return new JdkSslContext(sslContext, false, null, IdentityCipherSuiteFilter.INSTANCE,
166-
ApplicationProtocolConfig.DISABLED, ClientAuth.NONE, null, false);
167-
}
168-
return null;
181+
private SslContext wrap(SSLContext sslContext, ClientAuth clientAuth) {
182+
return new JdkSslContext(sslContext, false, null, IdentityCipherSuiteFilter.INSTANCE,
183+
ApplicationProtocolConfig.DISABLED, clientAuth, null, false);
169184
}
170185
}

0 commit comments

Comments
 (0)