View Javadoc
1   /*
2    * Copyright 2019-2021 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * @author Hakan Altindag
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()) // Adding additional trust material forces usage of CompositeX509ExtendedTrustManager and verbose logging
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 }