View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.testing.sync;
29  
30  import static org.hamcrest.MatcherAssert.assertThat;
31  
32  import java.io.IOException;
33  import java.net.InetSocketAddress;
34  import java.net.Socket;
35  import java.security.KeyManagementException;
36  import java.security.KeyStoreException;
37  import java.security.NoSuchAlgorithmException;
38  import java.util.concurrent.atomic.AtomicBoolean;
39  
40  import javax.net.ssl.HostnameVerifier;
41  import javax.net.ssl.SSLContext;
42  import javax.net.ssl.SSLException;
43  import javax.net.ssl.SSLSession;
44  import javax.net.ssl.SSLSocket;
45  
46  import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
47  import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
48  import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
49  import org.apache.hc.client5.http.ssl.TrustAllStrategy;
50  import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy;
51  import org.apache.hc.client5.testing.SSLTestContexts;
52  import org.apache.hc.core5.http.HttpHost;
53  import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
54  import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
55  import org.apache.hc.core5.http.protocol.BasicHttpContext;
56  import org.apache.hc.core5.http.protocol.HttpContext;
57  import org.apache.hc.core5.io.CloseMode;
58  import org.apache.hc.core5.ssl.SSLContexts;
59  import org.apache.hc.core5.ssl.TrustStrategy;
60  import org.apache.hc.core5.util.TimeValue;
61  import org.apache.hc.core5.util.Timeout;
62  import org.hamcrest.CoreMatchers;
63  import org.junit.jupiter.api.AfterEach;
64  import org.junit.jupiter.api.Assertions;
65  import org.junit.jupiter.api.Test;
66  
67  /**
68   * Unit tests for {@link SSLConnectionSocketFactory}.
69   */
70  public class TestSSLSocketFactory {
71  
72      private HttpServer server;
73  
74      @AfterEach
75      public void shutDown() throws Exception {
76          if (this.server != null) {
77              this.server.close(CloseMode.GRACEFUL);
78          }
79      }
80  
81      static class TestX509HostnameVerifier implements HostnameVerifier {
82  
83          private boolean fired;
84  
85          @Override
86          public boolean verify(final String host, final SSLSession session) {
87              this.fired = true;
88              return true;
89          }
90  
91          public boolean isFired() {
92              return this.fired;
93          }
94  
95      }
96  
97      @Test
98      public void testBasicSSL() throws Exception {
99          // @formatter:off
100         this.server = ServerBootstrap.bootstrap()
101                 .setSslContext(SSLTestContexts.createServerSSLContext())
102                 .create();
103         // @formatter:on
104         this.server.start();
105 
106         final HttpContext context = new BasicHttpContext();
107         final TestX509HostnameVerifier hostVerifier = new TestX509HostnameVerifier();
108         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
109                 SSLTestContexts.createClientSSLContext(), hostVerifier);
110         try (final Socket socket = socketFactory.createSocket(context)) {
111             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
112             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
113             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
114                     TimeValue.ZERO_MILLISECONDS,
115                     socket,
116                     target,
117                     remoteAddress,
118                     null,
119                     context)) {
120                 final SSLSession sslsession = sslSocket.getSession();
121 
122                 Assertions.assertNotNull(sslsession);
123                 Assertions.assertTrue(hostVerifier.isFired());
124             }
125         }
126     }
127 
128     @Test
129     public void testBasicSslConnectOverride() throws Exception {
130         this.server = ServerBootstrap.bootstrap()
131                 .setSslContext(SSLTestContexts.createServerSSLContext())
132                 .create();
133         this.server.start();
134 
135         final HttpContext context = new BasicHttpContext();
136         final AtomicBoolean connectCalled = new AtomicBoolean();
137         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
138                 SSLTestContexts.createClientSSLContext()) {
139             @Override
140             protected void connectSocket(
141                     final Socket sock,
142                     final InetSocketAddress remoteAddress,
143                     final Timeout connectTimeout,
144                     final HttpContext context) throws IOException {
145                 connectCalled.set(true);
146                 super.connectSocket(sock, remoteAddress, connectTimeout, context);
147             }
148         };
149         try (final Socket socket = socketFactory.createSocket(context)) {
150             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
151             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
152             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
153                     TimeValue.ZERO_MILLISECONDS,
154                     socket,
155                     target,
156                     remoteAddress,
157                     null,
158                     context)) {
159                 final SSLSession sslsession = sslSocket.getSession();
160                 Assertions.assertNotNull(sslsession);
161                 Assertions.assertTrue(connectCalled.get());
162             }
163         }
164     }
165 
166     @Test
167     public void testBasicDefaultHostnameVerifier() throws Exception {
168         // @formatter:off
169         this.server = ServerBootstrap.bootstrap()
170                 .setSslContext(SSLTestContexts.createServerSSLContext())
171                 .create();
172         // @formatter:on
173         this.server.start();
174 
175         final HttpContext context = new BasicHttpContext();
176         final SSLConnectionSocketFactory socketFactory = SSLConnectionSocketFactoryBuilder.create()
177                 .setSslContext(SSLTestContexts.createClientSSLContext())
178                 .build();
179         try (final Socket socket = socketFactory.createSocket(context)) {
180             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
181             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
182             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
183                     TimeValue.ZERO_MILLISECONDS,
184                     socket,
185                     target,
186                     remoteAddress,
187                     null,
188                     context)) {
189                 final SSLSession sslsession = sslSocket.getSession();
190 
191                 Assertions.assertNotNull(sslsession);
192             }
193         }
194     }
195 
196     @Test
197     public void testClientAuthSSL() throws Exception {
198         // @formatter:off
199         this.server = ServerBootstrap.bootstrap()
200                 .setSslContext(SSLTestContexts.createServerSSLContext())
201                 .create();
202         // @formatter:on
203         this.server.start();
204 
205         final HttpContext context = new BasicHttpContext();
206         final TestX509HostnameVerifier hostVerifier = new TestX509HostnameVerifier();
207         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
208                 SSLTestContexts.createClientSSLContext(), hostVerifier);
209         try (final Socket socket = socketFactory.createSocket(context)) {
210             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
211             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
212             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
213                     TimeValue.ZERO_MILLISECONDS,
214                     socket,
215                     target,
216                     remoteAddress,
217                     null,
218                     context)) {
219                 final SSLSession sslsession = sslSocket.getSession();
220 
221                 Assertions.assertNotNull(sslsession);
222                 Assertions.assertTrue(hostVerifier.isFired());
223             }
224         }
225     }
226 
227     @Test
228     public void testClientAuthSSLFailure() throws Exception {
229         // @formatter:off
230         this.server = ServerBootstrap.bootstrap()
231                 .setSslContext(SSLTestContexts.createServerSSLContext())
232                 .setSslSetupHandler(sslParameters -> sslParameters.setNeedClientAuth(true))
233                 .create();
234         // @formatter:on
235         this.server.start();
236 
237         final HttpContext context = new BasicHttpContext();
238         final TestX509HostnameVerifier hostVerifier = new TestX509HostnameVerifier();
239         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
240                 SSLTestContexts.createClientSSLContext(), hostVerifier);
241         try (final Socket socket = socketFactory.createSocket(context)) {
242             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
243             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
244             Assertions.assertThrows(IOException.class, () -> {
245                 try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
246                         TimeValue.ZERO_MILLISECONDS,
247                         socket, target,
248                         remoteAddress,
249                         null,
250                         context)) {
251                     final SSLSession sslsession = sslSocket.getSession();
252 
253                     Assertions.assertNotNull(sslsession);
254                     Assertions.assertTrue(hostVerifier.isFired());
255                     sslSocket.getInputStream().read();
256                 }
257             });
258         }
259     }
260 
261     @Test
262     public void testSSLTrustVerification() throws Exception {
263         // @formatter:off
264         this.server = ServerBootstrap.bootstrap()
265                 .setSslContext(SSLTestContexts.createServerSSLContext())
266                 .create();
267         // @formatter:on
268         this.server.start();
269 
270         final HttpContext context = new BasicHttpContext();
271         // Use default SSL context
272         final SSLContext defaultSslContext = SSLContexts.createDefault();
273 
274         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(defaultSslContext,
275                 NoopHostnameVerifier.INSTANCE);
276 
277         try (final Socket socket = socketFactory.createSocket(context)) {
278             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
279             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
280             Assertions.assertThrows(SSLException.class, () -> {
281                 try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
282                         TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress, null, context)) {
283                     // empty for now
284                 }
285             });
286         }
287     }
288 
289     @Test
290     public void testSSLTrustVerificationOverrideWithCustsom() throws Exception {
291         final TrustStrategy trustStrategy = (chain, authType) -> chain.length == 1;
292         testSSLTrustVerificationOverride(trustStrategy);
293     }
294 
295     @Test
296     public void testSSLTrustVerificationOverrideWithTrustSelfSignedStrategy() throws Exception {
297         testSSLTrustVerificationOverride(TrustSelfSignedStrategy.INSTANCE);
298     }
299 
300     @Test
301     public void testSSLTrustVerificationOverrideWithTrustAllStrategy() throws Exception {
302         testSSLTrustVerificationOverride(TrustAllStrategy.INSTANCE);
303     }
304 
305     private void testSSLTrustVerificationOverride(final TrustStrategy trustStrategy)
306             throws Exception, IOException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
307         // @formatter:off
308         this.server = ServerBootstrap.bootstrap()
309                 .setSslContext(SSLTestContexts.createServerSSLContext())
310                 .create();
311         // @formatter:on
312         this.server.start();
313 
314         final HttpContext context = new BasicHttpContext();
315 
316         // @formatter:off
317         final SSLContext sslContext = SSLContexts.custom()
318             .loadTrustMaterial(null, trustStrategy)
319             .build();
320         // @formatter:on
321         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext,
322                 NoopHostnameVerifier.INSTANCE);
323 
324         try (final Socket socket = socketFactory.createSocket(context)) {
325             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
326             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
327             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress,
328                     null, context)) {
329                 // empty for now
330             }
331         }
332     }
333 
334     @Test
335     public void testSSLDisabledByDefault() throws Exception {
336         // @formatter:off
337         this.server = ServerBootstrap.bootstrap()
338                 .setSslContext(SSLTestContexts.createServerSSLContext())
339                 .setSslSetupHandler(sslParameters -> sslParameters.setProtocols(new String[] {"SSLv3"}))
340                 .create();
341         // @formatter:on
342         this.server.start();
343 
344         final HttpContext context = new BasicHttpContext();
345         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
346                 SSLTestContexts.createClientSSLContext());
347         try (final Socket socket = socketFactory.createSocket(context)) {
348             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
349             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
350             Assertions.assertThrows(IOException.class, () ->
351                     socketFactory.connectSocket(
352                             TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress, null, context));
353         }
354     }
355 
356     @Test
357     public void testWeakCiphersDisabledByDefault() {
358         final String[] weakCiphersSuites = {
359                 "SSL_RSA_WITH_RC4_128_SHA",
360                 "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
361                 "TLS_DH_anon_WITH_AES_128_CBC_SHA",
362                 "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
363                 "SSL_RSA_WITH_NULL_SHA",
364                 "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
365                 "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
366                 "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
367                 "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
368                 "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
369                 "TLS_RSA_WITH_NULL_SHA256",
370                 "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
371                 "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
372                 "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
373                 "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5"
374         };
375         for (final String cipherSuite : weakCiphersSuites) {
376             final Exception exception = Assertions.assertThrows(Exception.class, () ->
377                     testWeakCipherDisabledByDefault(cipherSuite));
378             assertThat(exception, CoreMatchers.anyOf(
379                     CoreMatchers.instanceOf(IOException.class),
380                     CoreMatchers.instanceOf(IllegalArgumentException.class)));
381         }
382     }
383 
384     private void testWeakCipherDisabledByDefault(final String cipherSuite) throws Exception {
385         // @formatter:off
386         this.server = ServerBootstrap.bootstrap()
387                 .setSslContext(SSLTestContexts.createServerSSLContext())
388                 .setSslSetupHandler(sslParameters -> sslParameters.setProtocols(new String[] {cipherSuite}))
389                 .create();
390         // @formatter:on
391         this.server.start();
392 
393         final HttpContext context = new BasicHttpContext();
394         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
395                 SSLTestContexts.createClientSSLContext());
396         try (final Socket socket = socketFactory.createSocket(context)) {
397             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
398             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
399             socketFactory.connectSocket(TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress, null, context);
400         }
401     }
402 }