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.testing.SSLTestContexts;
50  import org.apache.hc.core5.http.HttpHost;
51  import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
52  import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
53  import org.apache.hc.core5.http.protocol.BasicHttpContext;
54  import org.apache.hc.core5.http.protocol.HttpContext;
55  import org.apache.hc.core5.io.CloseMode;
56  import org.apache.hc.core5.ssl.SSLContexts;
57  import org.apache.hc.core5.ssl.TrustStrategy;
58  import org.apache.hc.core5.util.TimeValue;
59  import org.apache.hc.core5.util.Timeout;
60  import org.hamcrest.CoreMatchers;
61  import org.junit.jupiter.api.AfterEach;
62  import org.junit.jupiter.api.Assertions;
63  import org.junit.jupiter.api.Test;
64  
65  /**
66   * Unit tests for {@link SSLConnectionSocketFactory}.
67   */
68  public class TestSSLSocketFactory {
69  
70      private HttpServer server;
71  
72      @AfterEach
73      public void shutDown() throws Exception {
74          if (this.server != null) {
75              this.server.close(CloseMode.GRACEFUL);
76          }
77      }
78  
79      static class TestX509HostnameVerifier implements HostnameVerifier {
80  
81          private boolean fired;
82  
83          @Override
84          public boolean verify(final String host, final SSLSession session) {
85              this.fired = true;
86              return true;
87          }
88  
89          public boolean isFired() {
90              return this.fired;
91          }
92  
93      }
94  
95      @Test
96      public void testBasicSSL() throws Exception {
97          // @formatter:off
98          this.server = ServerBootstrap.bootstrap()
99                  .setSslContext(SSLTestContexts.createServerSSLContext())
100                 .create();
101         // @formatter:on
102         this.server.start();
103 
104         final HttpContext context = new BasicHttpContext();
105         final TestX509HostnameVerifier hostVerifier = new TestX509HostnameVerifier();
106         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
107                 SSLTestContexts.createClientSSLContext(), hostVerifier);
108         try (final Socket socket = socketFactory.createSocket(context)) {
109             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
110             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
111             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
112                     TimeValue.ZERO_MILLISECONDS,
113                     socket,
114                     target,
115                     remoteAddress,
116                     null,
117                     context)) {
118                 final SSLSession sslsession = sslSocket.getSession();
119 
120                 Assertions.assertNotNull(sslsession);
121                 Assertions.assertTrue(hostVerifier.isFired());
122             }
123         }
124     }
125 
126     @Test
127     public void testBasicSslConnectOverride() throws Exception {
128         this.server = ServerBootstrap.bootstrap()
129                 .setSslContext(SSLTestContexts.createServerSSLContext())
130                 .create();
131         this.server.start();
132 
133         final HttpContext context = new BasicHttpContext();
134         final AtomicBoolean connectCalled = new AtomicBoolean();
135         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
136                 SSLTestContexts.createClientSSLContext()) {
137             @Override
138             protected void connectSocket(
139                     final Socket sock,
140                     final InetSocketAddress remoteAddress,
141                     final Timeout connectTimeout,
142                     final HttpContext context) throws IOException {
143                 connectCalled.set(true);
144                 super.connectSocket(sock, remoteAddress, connectTimeout, context);
145             }
146         };
147         try (final Socket socket = socketFactory.createSocket(context)) {
148             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
149             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
150             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
151                     TimeValue.ZERO_MILLISECONDS,
152                     socket,
153                     target,
154                     remoteAddress,
155                     null,
156                     context)) {
157                 final SSLSession sslsession = sslSocket.getSession();
158                 Assertions.assertNotNull(sslsession);
159                 Assertions.assertTrue(connectCalled.get());
160             }
161         }
162     }
163 
164     @Test
165     public void testBasicDefaultHostnameVerifier() throws Exception {
166         // @formatter:off
167         this.server = ServerBootstrap.bootstrap()
168                 .setSslContext(SSLTestContexts.createServerSSLContext())
169                 .create();
170         // @formatter:on
171         this.server.start();
172 
173         final HttpContext context = new BasicHttpContext();
174         final SSLConnectionSocketFactory socketFactory = SSLConnectionSocketFactoryBuilder.create()
175                 .setSslContext(SSLTestContexts.createClientSSLContext())
176                 .build();
177         try (final Socket socket = socketFactory.createSocket(context)) {
178             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
179             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
180             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
181                     TimeValue.ZERO_MILLISECONDS,
182                     socket,
183                     target,
184                     remoteAddress,
185                     null,
186                     context)) {
187                 final SSLSession sslsession = sslSocket.getSession();
188 
189                 Assertions.assertNotNull(sslsession);
190             }
191         }
192     }
193 
194     @Test
195     public void testClientAuthSSL() throws Exception {
196         // @formatter:off
197         this.server = ServerBootstrap.bootstrap()
198                 .setSslContext(SSLTestContexts.createServerSSLContext())
199                 .create();
200         // @formatter:on
201         this.server.start();
202 
203         final HttpContext context = new BasicHttpContext();
204         final TestX509HostnameVerifier hostVerifier = new TestX509HostnameVerifier();
205         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
206                 SSLTestContexts.createClientSSLContext(), hostVerifier);
207         try (final Socket socket = socketFactory.createSocket(context)) {
208             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
209             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
210             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
211                     TimeValue.ZERO_MILLISECONDS,
212                     socket,
213                     target,
214                     remoteAddress,
215                     null,
216                     context)) {
217                 final SSLSession sslsession = sslSocket.getSession();
218 
219                 Assertions.assertNotNull(sslsession);
220                 Assertions.assertTrue(hostVerifier.isFired());
221             }
222         }
223     }
224 
225     @Test
226     public void testClientAuthSSLFailure() throws Exception {
227         // @formatter:off
228         this.server = ServerBootstrap.bootstrap()
229                 .setSslContext(SSLTestContexts.createServerSSLContext())
230                 .setSslSetupHandler(sslParameters -> sslParameters.setNeedClientAuth(true))
231                 .create();
232         // @formatter:on
233         this.server.start();
234 
235         final HttpContext context = new BasicHttpContext();
236         final TestX509HostnameVerifier hostVerifier = new TestX509HostnameVerifier();
237         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
238                 SSLTestContexts.createClientSSLContext(), hostVerifier);
239         try (final Socket socket = socketFactory.createSocket(context)) {
240             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
241             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
242             Assertions.assertThrows(IOException.class, () -> {
243                 try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
244                         TimeValue.ZERO_MILLISECONDS,
245                         socket, target,
246                         remoteAddress,
247                         null,
248                         context)) {
249                     final SSLSession sslsession = sslSocket.getSession();
250 
251                     Assertions.assertNotNull(sslsession);
252                     Assertions.assertTrue(hostVerifier.isFired());
253                     sslSocket.getInputStream().read();
254                 }
255             });
256         }
257     }
258 
259     @Test
260     public void testSSLTrustVerification() throws Exception {
261         // @formatter:off
262         this.server = ServerBootstrap.bootstrap()
263                 .setSslContext(SSLTestContexts.createServerSSLContext())
264                 .create();
265         // @formatter:on
266         this.server.start();
267 
268         final HttpContext context = new BasicHttpContext();
269         // Use default SSL context
270         final SSLContext defaultSslContext = SSLContexts.createDefault();
271 
272         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(defaultSslContext,
273                 NoopHostnameVerifier.INSTANCE);
274 
275         try (final Socket socket = socketFactory.createSocket(context)) {
276             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
277             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
278             Assertions.assertThrows(SSLException.class, () -> {
279                 try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(
280                         TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress, null, context)) {
281                     // empty for now
282                 }
283             });
284         }
285     }
286 
287     @Test
288     public void testSSLTrustVerificationOverrideWithCustom() throws Exception {
289         final TrustStrategy trustStrategy = (chain, authType) -> chain.length == 1;
290         testSSLTrustVerificationOverride(trustStrategy);
291     }
292 
293     private void testSSLTrustVerificationOverride(final TrustStrategy trustStrategy)
294             throws Exception, IOException, NoSuchAlgorithmException, KeyManagementException, KeyStoreException {
295         // @formatter:off
296         this.server = ServerBootstrap.bootstrap()
297                 .setSslContext(SSLTestContexts.createServerSSLContext())
298                 .create();
299         // @formatter:on
300         this.server.start();
301 
302         final HttpContext context = new BasicHttpContext();
303 
304         // @formatter:off
305         final SSLContext sslContext = SSLContexts.custom()
306             .loadTrustMaterial(null, trustStrategy)
307             .build();
308         // @formatter:on
309         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext,
310                 NoopHostnameVerifier.INSTANCE);
311 
312         try (final Socket socket = socketFactory.createSocket(context)) {
313             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
314             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
315             try (final SSLSocket sslSocket = (SSLSocket) socketFactory.connectSocket(TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress,
316                     null, context)) {
317                 // empty for now
318             }
319         }
320     }
321 
322     @Test
323     public void testSSLDisabledByDefault() throws Exception {
324         // @formatter:off
325         this.server = ServerBootstrap.bootstrap()
326                 .setSslContext(SSLTestContexts.createServerSSLContext())
327                 .setSslSetupHandler(sslParameters -> sslParameters.setProtocols(new String[] {"SSLv3"}))
328                 .create();
329         // @formatter:on
330         this.server.start();
331 
332         final HttpContext context = new BasicHttpContext();
333         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
334                 SSLTestContexts.createClientSSLContext());
335         try (final Socket socket = socketFactory.createSocket(context)) {
336             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
337             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
338             Assertions.assertThrows(IOException.class, () ->
339                     socketFactory.connectSocket(
340                             TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress, null, context));
341         }
342     }
343 
344     @Test
345     public void testWeakCiphersDisabledByDefault() {
346         final String[] weakCiphersSuites = {
347                 "SSL_RSA_WITH_RC4_128_SHA",
348                 "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
349                 "TLS_DH_anon_WITH_AES_128_CBC_SHA",
350                 "SSL_RSA_EXPORT_WITH_DES40_CBC_SHA",
351                 "SSL_RSA_WITH_NULL_SHA",
352                 "SSL_RSA_WITH_3DES_EDE_CBC_SHA",
353                 "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
354                 "TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA",
355                 "TLS_DH_anon_WITH_AES_256_GCM_SHA384",
356                 "TLS_ECDH_anon_WITH_AES_256_CBC_SHA",
357                 "TLS_RSA_WITH_NULL_SHA256",
358                 "SSL_RSA_EXPORT_WITH_RC4_40_MD5",
359                 "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
360                 "TLS_KRB5_EXPORT_WITH_RC4_40_SHA",
361                 "SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5"
362         };
363         for (final String cipherSuite : weakCiphersSuites) {
364             final Exception exception = Assertions.assertThrows(Exception.class, () ->
365                     testWeakCipherDisabledByDefault(cipherSuite));
366             assertThat(exception, CoreMatchers.anyOf(
367                     CoreMatchers.instanceOf(IOException.class),
368                     CoreMatchers.instanceOf(IllegalArgumentException.class)));
369         }
370     }
371 
372     private void testWeakCipherDisabledByDefault(final String cipherSuite) throws Exception {
373         // @formatter:off
374         this.server = ServerBootstrap.bootstrap()
375                 .setSslContext(SSLTestContexts.createServerSSLContext())
376                 .setSslSetupHandler(sslParameters -> sslParameters.setProtocols(new String[] {cipherSuite}))
377                 .create();
378         // @formatter:on
379         this.server.start();
380 
381         final HttpContext context = new BasicHttpContext();
382         final SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
383                 SSLTestContexts.createClientSSLContext());
384         try (final Socket socket = socketFactory.createSocket(context)) {
385             final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", this.server.getLocalPort());
386             final HttpHost target = new HttpHost("https", "localhost", this.server.getLocalPort());
387             socketFactory.connectSocket(TimeValue.ZERO_MILLISECONDS, socket, target, remoteAddress, null, context);
388         }
389     }
390 }