1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package nl.altindag.ssl;
18
19 import com.sun.net.httpserver.HttpExchange;
20 import com.sun.net.httpserver.HttpHandler;
21 import com.sun.net.httpserver.HttpsConfigurator;
22 import com.sun.net.httpserver.HttpsParameters;
23 import com.sun.net.httpserver.HttpsServer;
24 import nl.altindag.log.LogCaptor;
25 import nl.altindag.ssl.util.KeyManagerUtils;
26 import nl.altindag.ssl.util.KeyStoreUtils;
27 import nl.altindag.ssl.util.SSLSessionUtils;
28 import nl.altindag.ssl.util.TrustManagerUtils;
29 import org.junit.jupiter.api.Test;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 import javax.net.ssl.HttpsURLConnection;
34 import javax.net.ssl.SSLException;
35 import javax.net.ssl.SSLSocketFactory;
36 import javax.net.ssl.X509ExtendedKeyManager;
37 import javax.net.ssl.X509ExtendedTrustManager;
38 import java.io.BufferedReader;
39 import java.io.IOException;
40 import java.io.InputStreamReader;
41 import java.io.OutputStream;
42 import java.net.InetSocketAddress;
43 import java.net.SocketException;
44 import java.net.URL;
45 import java.nio.charset.StandardCharsets;
46 import java.util.Collections;
47 import java.util.HashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.concurrent.Executor;
51 import java.util.concurrent.ExecutorService;
52 import java.util.concurrent.Executors;
53 import java.util.stream.Collectors;
54
55 import static nl.altindag.ssl.TestConstants.KEYSTORE_LOCATION;
56 import static org.assertj.core.api.Assertions.assertThat;
57 import static org.assertj.core.api.Assertions.assertThatThrownBy;
58
59
60
61
62 class SSLFactoryIT {
63
64 private static final Logger LOGGER = LoggerFactory.getLogger(SSLFactoryIT.class);
65
66 @Test
67 void executeHttpsRequestWithMutualAuthentication() throws IOException {
68 LogCaptor logCaptor = LogCaptor.forName("nl.altindag.ssl");
69
70 SSLFactory sslFactory = SSLFactory.builder()
71 .withIdentityMaterial(KEYSTORE_LOCATION + "badssl-identity.p12", "badssl.com".toCharArray())
72 .withTrustMaterial(KEYSTORE_LOCATION + "badssl-truststore.p12", "badssl.com".toCharArray())
73 .withTrustMaterial(KeyStoreUtils.createKeyStore())
74 .build();
75
76 HttpsURLConnection connection = (HttpsURLConnection) new URL("https://client.badssl.com/").openConnection();
77 connection.setSSLSocketFactory(sslFactory.getSslSocketFactory());
78 connection.setHostnameVerifier(sslFactory.getHostnameVerifier());
79 connection.setRequestMethod("GET");
80
81 int statusCode = connection.getResponseCode();
82
83 if (statusCode == 400) {
84 LOGGER.warn("Certificate may have expired and needs to be updated");
85 } else {
86 assertThat(statusCode).isEqualTo(200);
87 assertThat(logCaptor.getLogs()).containsExactly("Received the following server certificate: [CN=*.badssl.com, O=Lucas Garron Torres, L=Walnut Creek, ST=California, C=US]");
88 }
89 }
90
91 @Test
92 void executeRequestToTwoServersWithMutualAuthenticationWithSingleHttpClientAndSingleSslConfiguration() throws IOException {
93 ExecutorService executorService = Executors.newSingleThreadExecutor();
94
95 char[] keyStorePassword = "secret".toCharArray();
96 SSLFactory sslFactoryForServerOne = SSLFactory.builder()
97 .withIdentityMaterial("keystores-for-unit-tests/client-server/server-one/identity.jks", keyStorePassword)
98 .withTrustMaterial("keystores-for-unit-tests/client-server/server-one/truststore.jks", keyStorePassword)
99 .withNeedClientAuthentication()
100 .withProtocols("TLSv1.2")
101 .build();
102
103 SSLFactory sslFactoryForServerTwo = SSLFactory.builder()
104 .withIdentityMaterial("keystores-for-unit-tests/client-server/server-two/identity.jks", keyStorePassword)
105 .withTrustMaterial("keystores-for-unit-tests/client-server/server-two/truststore.jks", keyStorePassword)
106 .withNeedClientAuthentication()
107 .withProtocols("TLSv1.2")
108 .build();
109
110 HttpsServer serverOne = ServerUtils.createServer(8443, sslFactoryForServerOne, executorService, "Hello from server one");
111 HttpsServer serverTwo = ServerUtils.createServer(8444, sslFactoryForServerTwo, executorService, "Hello from server two");
112
113 serverOne.start();
114 serverTwo.start();
115
116 SSLFactory sslFactoryForClient = SSLFactory.builder()
117 .withIdentityMaterial("keystores-for-unit-tests/client-server/client-one/identity.jks", keyStorePassword)
118 .withIdentityMaterial("keystores-for-unit-tests/client-server/client-two/identity.jks", keyStorePassword)
119 .withTrustMaterial("keystores-for-unit-tests/client-server/client-one/truststore.jks", keyStorePassword)
120 .withTrustMaterial("keystores-for-unit-tests/client-server/client-two/truststore.jks", keyStorePassword)
121 .withProtocols("TLSv1.2")
122 .build();
123
124 Response response = executeRequest("https://localhost:8443/api/hello", sslFactoryForClient.getSslSocketFactory());
125
126 assertThat(response.getStatusCode()).isEqualTo(200);
127 assertThat(response.getBody()).contains("Hello from server one");
128
129 response = executeRequest("https://localhost:8444/api/hello", sslFactoryForClient.getSslSocketFactory());
130
131 assertThat(response.getStatusCode()).isEqualTo(200);
132 assertThat(response.getBody()).contains("Hello from server two");
133
134 serverOne.stop(0);
135 serverTwo.stop(0);
136 executorService.shutdownNow();
137 }
138
139 @Test
140 void executeRequestToTwoServersWithMutualAuthenticationWithReroutingClientCertificates() throws IOException {
141 ExecutorService executorService = Executors.newSingleThreadExecutor();
142
143 char[] keyStorePassword = "secret".toCharArray();
144 SSLFactory sslFactoryForServerOne = SSLFactory.builder()
145 .withIdentityMaterial("keystores-for-unit-tests/client-server/server-one/identity.jks", keyStorePassword)
146 .withTrustMaterial("keystores-for-unit-tests/client-server/server-one/truststore.jks", keyStorePassword)
147 .withNeedClientAuthentication()
148 .withSessionTimeout(1)
149 .withProtocols("TLSv1.2")
150 .build();
151
152 SSLFactory sslFactoryForServerTwo = SSLFactory.builder()
153 .withIdentityMaterial("keystores-for-unit-tests/client-server/server-two/identity.jks", keyStorePassword)
154 .withTrustMaterial("keystores-for-unit-tests/client-server/server-two/truststore.jks", keyStorePassword)
155 .withNeedClientAuthentication()
156 .withSessionTimeout(1)
157 .withProtocols("TLSv1.2")
158 .build();
159
160 HttpsServer serverOne = ServerUtils.createServer(8443, sslFactoryForServerOne, executorService, "Hello from server one");
161 HttpsServer serverTwo = ServerUtils.createServer(8444, sslFactoryForServerTwo, executorService, "Hello from server two");
162
163 serverOne.start();
164 serverTwo.start();
165
166 Map<String, List<String>> clientAliasesToHosts = new HashMap<>();
167 clientAliasesToHosts.put("client-one", Collections.singletonList("https://localhost:8443/api/hello"));
168 clientAliasesToHosts.put("client-two", Collections.singletonList("https://localhost:8444/api/hello"));
169
170 SSLFactory sslFactoryForClient = SSLFactory.builder()
171 .withIdentityMaterial("keystores-for-unit-tests/client-server/client-one/identity.jks", keyStorePassword)
172 .withIdentityMaterial("keystores-for-unit-tests/client-server/client-two/identity.jks", keyStorePassword)
173 .withTrustMaterial("keystores-for-unit-tests/client-server/client-one/truststore.jks", keyStorePassword)
174 .withTrustMaterial("keystores-for-unit-tests/client-server/client-two/truststore.jks", keyStorePassword)
175 .withClientIdentityRoute(clientAliasesToHosts)
176 .build();
177
178 SSLSocketFactory sslSocketFactoryWithCorrectClientRoutes = sslFactoryForClient.getSslSocketFactory();
179
180 Response response = executeRequest("https://localhost:8443/api/hello", sslSocketFactoryWithCorrectClientRoutes);
181
182 assertThat(response.getStatusCode()).isEqualTo(200);
183 assertThat(response.getBody()).contains("Hello from server one");
184
185 response = executeRequest("https://localhost:8444/api/hello", sslSocketFactoryWithCorrectClientRoutes);
186
187 assertThat(response.getStatusCode()).isEqualTo(200);
188 assertThat(response.getBody()).contains("Hello from server two");
189
190 sslFactoryForClient = SSLFactory.builder()
191 .withIdentityMaterial("keystores-for-unit-tests/client-server/client-one/identity.jks", keyStorePassword)
192 .withIdentityMaterial("keystores-for-unit-tests/client-server/client-two/identity.jks", keyStorePassword)
193 .withTrustMaterial("keystores-for-unit-tests/client-server/client-one/truststore.jks", keyStorePassword)
194 .withTrustMaterial("keystores-for-unit-tests/client-server/client-two/truststore.jks", keyStorePassword)
195 .withClientIdentityRoute("client-one", "https://localhost:8444/api/hello")
196 .withClientIdentityRoute("client-two", "https://localhost:8443/api/hello")
197 .build();
198
199 SSLSocketFactory sslSocketFactoryWithIncorrectClientRoutes = sslFactoryForClient.getSslSocketFactory();
200 assertThatThrownBy(() -> executeRequest("https://localhost:8443/api/hello", sslSocketFactoryWithIncorrectClientRoutes))
201 .isInstanceOfAny(SocketException.class, SSLException.class);
202 assertThatThrownBy(() -> executeRequest("https://localhost:8444/api/hello", sslSocketFactoryWithIncorrectClientRoutes))
203 .isInstanceOfAny(SocketException.class, SSLException.class);
204
205 serverOne.stop(0);
206 serverTwo.stop(0);
207 executorService.shutdownNow();
208 }
209
210 @Test
211 @SuppressWarnings("OptionalGetWithoutIsPresent")
212 void executeRequestToTwoServersWithMutualAuthenticationWithSwappingClientIdentityAndTrustMaterial() throws IOException, InterruptedException {
213 ExecutorService executorService = Executors.newSingleThreadExecutor();
214
215 char[] keyStorePassword = "secret".toCharArray();
216
217 SSLFactory sslFactoryForServerOne = SSLFactory.builder()
218 .withIdentityMaterial("keystores-for-unit-tests/client-server/server-one/identity.jks", keyStorePassword)
219 .withTrustMaterial("keystores-for-unit-tests/client-server/server-one/truststore.jks", keyStorePassword)
220 .withNeedClientAuthentication()
221 .withProtocols("TLSv1.2")
222 .build();
223
224 SSLFactory sslFactoryForServerTwo = SSLFactory.builder()
225 .withIdentityMaterial("keystores-for-unit-tests/client-server/server-two/identity.jks", keyStorePassword)
226 .withTrustMaterial("keystores-for-unit-tests/client-server/server-two/truststore.jks", keyStorePassword)
227 .withNeedClientAuthentication()
228 .withProtocols("TLSv1.2")
229 .build();
230
231 HttpsServer serverOne = ServerUtils.createServer(8443, sslFactoryForServerOne, executorService, "Hello from server one");
232 HttpsServer serverTwo = ServerUtils.createServer(8444, sslFactoryForServerTwo, executorService, "Hello from server two");
233
234 serverOne.start();
235 serverTwo.start();
236
237 SSLFactory sslFactoryForClient = SSLFactory.builder()
238 .withIdentityMaterial("keystores-for-unit-tests/client-server/client-one/identity.jks", keyStorePassword)
239 .withTrustMaterial("keystores-for-unit-tests/client-server/client-one/truststore.jks", keyStorePassword)
240 .withSwappableIdentityMaterial()
241 .withSwappableTrustMaterial()
242 .build();
243
244 SSLSocketFactory sslSocketFactory = sslFactoryForClient.getSslSocketFactory();
245
246 Response response = executeRequest("https://localhost:8443/api/hello", sslSocketFactory);
247 assertThat(response.getStatusCode()).isEqualTo(200);
248 assertThat(response.getBody()).contains("Hello from server one");
249
250 assertThatThrownBy(() -> executeRequest("https://localhost:8444/api/hello", sslSocketFactory))
251 .isInstanceOfAny(SocketException.class, SSLException.class);
252
253 X509ExtendedKeyManager swappableKeyManager = sslFactoryForClient.getKeyManager().get();
254 X509ExtendedKeyManager toBeSwappedKeyManager = KeyManagerUtils.createKeyManager(
255 KeyStoreUtils.loadKeyStore("keystores-for-unit-tests/client-server/client-two/identity.jks", keyStorePassword), "secret".toCharArray()
256 );
257
258 KeyManagerUtils.swapKeyManager(swappableKeyManager, toBeSwappedKeyManager);
259
260 X509ExtendedTrustManager swappableTrustManager = sslFactoryForClient.getTrustManager().get();
261 X509ExtendedTrustManager toBeSwappedTrustManager = TrustManagerUtils.createTrustManager(
262 KeyStoreUtils.loadKeyStore("keystores-for-unit-tests/client-server/client-two/truststore.jks", keyStorePassword)
263 );
264
265 TrustManagerUtils.swapTrustManager(swappableTrustManager, toBeSwappedTrustManager);
266
267 SSLSessionUtils.invalidateCaches(sslFactoryForClient);
268
269 assertThatThrownBy(() -> executeRequest("https://localhost:8443/api/hello", sslSocketFactory))
270 .isInstanceOfAny(SocketException.class, SSLException.class);
271
272 response = executeRequest("https://localhost:8444/api/hello", sslFactoryForClient.getSslSocketFactory());
273 assertThat(response.getStatusCode()).isEqualTo(200);
274 assertThat(response.getBody()).contains("Hello from server two");
275
276 serverOne.stop(0);
277 serverTwo.stop(0);
278 executorService.shutdownNow();
279 }
280
281 private Response executeRequest(String url, SSLSocketFactory sslSocketFactory) throws IOException {
282 HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
283 connection.setSSLSocketFactory(sslSocketFactory);
284 connection.setRequestMethod("GET");
285
286 int statusCode = connection.getResponseCode();
287 String body = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))
288 .lines()
289 .collect(Collectors.joining("\n"));
290
291 connection.disconnect();
292 return new Response(statusCode, body);
293 }
294
295
296 private static final class Response {
297 private final int statusCode;
298 private final String body;
299
300 Response(int statusCode, String body) {
301 this.statusCode = statusCode;
302 this.body = body;
303 }
304
305 public int getStatusCode() {
306 return statusCode;
307 }
308
309 public String getBody() {
310 return body;
311 }
312 }
313
314 }