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.http.ssl;
29  
30  import java.io.IOException;
31  import java.net.Socket;
32  import java.net.SocketAddress;
33  import java.security.cert.Certificate;
34  import java.security.cert.X509Certificate;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.Collection;
38  import java.util.List;
39  import java.util.Objects;
40  
41  import javax.net.ssl.HostnameVerifier;
42  import javax.net.ssl.SSLContext;
43  import javax.net.ssl.SSLEngine;
44  import javax.net.ssl.SSLException;
45  import javax.net.ssl.SSLHandshakeException;
46  import javax.net.ssl.SSLParameters;
47  import javax.net.ssl.SSLPeerUnverifiedException;
48  import javax.net.ssl.SSLSession;
49  import javax.net.ssl.SSLSocket;
50  import javax.security.auth.x500.X500Principal;
51  
52  import org.apache.hc.client5.http.config.TlsConfig;
53  import org.apache.hc.core5.annotation.Contract;
54  import org.apache.hc.core5.annotation.Internal;
55  import org.apache.hc.core5.annotation.ThreadingBehavior;
56  import org.apache.hc.core5.concurrent.FutureCallback;
57  import org.apache.hc.core5.http.HttpHost;
58  import org.apache.hc.core5.http.URIScheme;
59  import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
60  import org.apache.hc.core5.http.protocol.HttpContext;
61  import org.apache.hc.core5.http.ssl.TLS;
62  import org.apache.hc.core5.http.ssl.TlsCiphers;
63  import org.apache.hc.core5.http2.HttpVersionPolicy;
64  import org.apache.hc.core5.http2.ssl.ApplicationProtocol;
65  import org.apache.hc.core5.http2.ssl.H2TlsSupport;
66  import org.apache.hc.core5.io.Closer;
67  import org.apache.hc.core5.net.NamedEndpoint;
68  import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
69  import org.apache.hc.core5.reactor.ssl.TlsDetails;
70  import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
71  import org.apache.hc.core5.util.Args;
72  import org.apache.hc.core5.util.Timeout;
73  import org.slf4j.Logger;
74  import org.slf4j.LoggerFactory;
75  
76  @Contract(threading = ThreadingBehavior.STATELESS)
77  abstract class AbstractClientTlsStrategy implements TlsStrategy, TlsSocketStrategy {
78  
79      private static final Logger LOG = LoggerFactory.getLogger(AbstractClientTlsStrategy.class);
80  
81      private final SSLContext sslContext;
82      private final String[] supportedProtocols;
83      private final String[] supportedCipherSuites;
84      private final SSLBufferMode sslBufferManagement;
85      private final HostnameVerificationPolicy hostnameVerificationPolicy;
86      private final HostnameVerifier hostnameVerifier;
87  
88      AbstractClientTlsStrategy(
89              final SSLContext sslContext,
90              final String[] supportedProtocols,
91              final String[] supportedCipherSuites,
92              final SSLBufferMode sslBufferManagement,
93              final HostnameVerificationPolicy hostnameVerificationPolicy,
94              final HostnameVerifier hostnameVerifier) {
95          super();
96          this.sslContext = Args.notNull(sslContext, "SSL context");
97          this.supportedProtocols = supportedProtocols;
98          this.supportedCipherSuites = supportedCipherSuites;
99          this.sslBufferManagement = sslBufferManagement != null ? sslBufferManagement : SSLBufferMode.STATIC;
100         this.hostnameVerificationPolicy = hostnameVerificationPolicy != null ? hostnameVerificationPolicy : HostnameVerificationPolicy.BOTH;
101         this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier :
102                 (this.hostnameVerificationPolicy == HostnameVerificationPolicy.BUILTIN ? NoopHostnameVerifier.INSTANCE : HttpsSupport.getDefaultHostnameVerifier());
103     }
104 
105     /**
106      * @deprecated use {@link #upgrade(TransportSecurityLayer, NamedEndpoint, Object, Timeout, FutureCallback)}
107      */
108     @Deprecated
109     @Override
110     public boolean upgrade(
111             final TransportSecurityLayer tlsSession,
112             final HttpHost host,
113             final SocketAddress localAddress,
114             final SocketAddress remoteAddress,
115             final Object attachment,
116             final Timeout handshakeTimeout) {
117         upgrade(tlsSession, host, attachment, handshakeTimeout, null);
118         return true;
119     }
120 
121     @Override
122     public void upgrade(
123             final TransportSecurityLayer tlsSession,
124             final NamedEndpoint endpoint,
125             final Object attachment,
126             final Timeout handshakeTimeout,
127             final FutureCallback<TransportSecurityLayer> callback) {
128         tlsSession.startTls(sslContext, endpoint, sslBufferManagement, (e, sslEngine) -> {
129 
130             final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT;
131             final HttpVersionPolicy versionPolicy = tlsConfig.getHttpVersionPolicy();
132 
133             final SSLParameters sslParameters = sslEngine.getSSLParameters();
134             final String[] supportedProtocols = tlsConfig.getSupportedProtocols();
135             if (supportedProtocols != null) {
136                 sslParameters.setProtocols(supportedProtocols);
137             } else if (this.supportedProtocols != null) {
138                 sslParameters.setProtocols(this.supportedProtocols);
139             } else if (versionPolicy != HttpVersionPolicy.FORCE_HTTP_1) {
140                 sslParameters.setProtocols(TLS.excludeWeak(sslParameters.getProtocols()));
141             }
142             final String[] supportedCipherSuites = tlsConfig.getSupportedCipherSuites();
143             if (supportedCipherSuites != null) {
144                 sslParameters.setCipherSuites(supportedCipherSuites);
145             } else if (this.supportedCipherSuites != null) {
146                 sslParameters.setCipherSuites(this.supportedCipherSuites);
147             } else if (versionPolicy == HttpVersionPolicy.FORCE_HTTP_2) {
148                 sslParameters.setCipherSuites(TlsCiphers.excludeH2Blacklisted(sslParameters.getCipherSuites()));
149             }
150 
151             if (versionPolicy != HttpVersionPolicy.FORCE_HTTP_1) {
152                 H2TlsSupport.setEnableRetransmissions(sslParameters, false);
153             }
154 
155             applyParameters(sslEngine, sslParameters, H2TlsSupport.selectApplicationProtocols(versionPolicy));
156 
157             if (hostnameVerificationPolicy == HostnameVerificationPolicy.BUILTIN || hostnameVerificationPolicy == HostnameVerificationPolicy.BOTH) {
158                 sslParameters.setEndpointIdentificationAlgorithm(URIScheme.HTTPS.id);
159             }
160 
161             initializeEngine(sslEngine);
162 
163             if (LOG.isDebugEnabled()) {
164                 LOG.debug("Enabled protocols: {}", Arrays.asList(sslEngine.getEnabledProtocols()));
165                 LOG.debug("Enabled cipher suites: {}", Arrays.asList(sslEngine.getEnabledCipherSuites()));
166                 LOG.debug("Starting handshake ({})", handshakeTimeout);
167             }
168         }, (e, sslEngine) -> {
169             verifySession(endpoint.getHostName(), sslEngine.getSession());
170             final TlsDetails tlsDetails = createTlsDetails(sslEngine);
171             final String negotiatedCipherSuite = sslEngine.getSession().getCipherSuite();
172             if (tlsDetails != null && ApplicationProtocol.HTTP_2.id.equals(tlsDetails.getApplicationProtocol())) {
173                 if (TlsCiphers.isH2Blacklisted(negotiatedCipherSuite)) {
174                     throw new SSLHandshakeException("Cipher suite `" + negotiatedCipherSuite
175                         + "` does not provide adequate security for HTTP/2");
176                 }
177             }
178             return tlsDetails;
179         }, handshakeTimeout, callback);
180     }
181 
182     abstract void applyParameters(SSLEngine sslEngine, SSLParameters sslParameters, String[] appProtocols);
183 
184     abstract TlsDetails createTlsDetails(SSLEngine sslEngine);
185 
186     protected void initializeEngine(final SSLEngine sslEngine) {
187     }
188 
189     protected void initializeSocket(final SSLSocket socket) {
190     }
191 
192     protected void verifySession(
193             final String hostname,
194             final SSLSession sslsession) throws SSLException {
195         verifySession(hostname, sslsession,
196                 hostnameVerificationPolicy == HostnameVerificationPolicy.CLIENT || hostnameVerificationPolicy == HostnameVerificationPolicy.BOTH ? hostnameVerifier : null);
197     }
198 
199     @Override
200     public SSLSocket upgrade(final Socket socket,
201                              final String target,
202                              final int port,
203                              final Object attachment,
204                              final HttpContext context) throws IOException {
205         final SSLSocket upgradedSocket = (SSLSocket) sslContext.getSocketFactory().createSocket(
206                 socket,
207                 target,
208                 port,
209                 false);
210         try {
211             executeHandshake(upgradedSocket, target, attachment);
212             return upgradedSocket;
213         } catch (IOException | RuntimeException ex) {
214             Closer.closeQuietly(upgradedSocket);
215             throw ex;
216         }
217     }
218 
219     private void executeHandshake(
220             final SSLSocket upgradedSocket,
221             final String target,
222             final Object attachment) throws IOException {
223         final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT;
224 
225         final SSLParameters sslParameters = upgradedSocket.getSSLParameters();
226         if (supportedProtocols != null) {
227             sslParameters.setProtocols(supportedProtocols);
228         } else {
229             sslParameters.setProtocols(TLS.excludeWeak(upgradedSocket.getEnabledProtocols()));
230         }
231         if (supportedCipherSuites != null) {
232             sslParameters.setCipherSuites(supportedCipherSuites);
233         } else {
234             sslParameters.setCipherSuites(TlsCiphers.excludeWeak(upgradedSocket.getEnabledCipherSuites()));
235         }
236         if (hostnameVerificationPolicy == HostnameVerificationPolicy.BUILTIN || hostnameVerificationPolicy == HostnameVerificationPolicy.BOTH) {
237             sslParameters.setEndpointIdentificationAlgorithm(URIScheme.HTTPS.id);
238         }
239         upgradedSocket.setSSLParameters(sslParameters);
240 
241         final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout();
242         if (handshakeTimeout != null) {
243             upgradedSocket.setSoTimeout(handshakeTimeout.toMillisecondsIntBound());
244         }
245 
246         initializeSocket(upgradedSocket);
247 
248         if (LOG.isDebugEnabled()) {
249             LOG.debug("Enabled protocols: {}", (Object) upgradedSocket.getEnabledProtocols());
250             LOG.debug("Enabled cipher suites: {}", (Object) upgradedSocket.getEnabledCipherSuites());
251             LOG.debug("Starting handshake ({})", handshakeTimeout);
252         }
253         upgradedSocket.startHandshake();
254         verifySession(target, upgradedSocket.getSession());
255     }
256 
257     void verifySession(
258             final String hostname,
259             final SSLSession sslsession,
260             final HostnameVerifier hostnameVerifier) throws SSLException {
261 
262         if (LOG.isDebugEnabled()) {
263             LOG.debug("Secure session established");
264             LOG.debug(" negotiated protocol: {}", sslsession.getProtocol());
265             LOG.debug(" negotiated cipher suite: {}", sslsession.getCipherSuite());
266 
267             try {
268 
269                 final Certificate[] certs = sslsession.getPeerCertificates();
270                 final Certificate cert = certs[0];
271                 if (cert instanceof X509Certificate) {
272                     final X509Certificate x509 = (X509Certificate) cert;
273                     final X500Principal peer = x509.getSubjectX500Principal();
274 
275                     LOG.debug("Peer principal: {}", toEscapedString(peer));
276                     final Collection<List<?>> altNames1 = x509.getSubjectAlternativeNames();
277                     if (altNames1 != null) {
278                         final List<String> altNames = new ArrayList<>();
279                         for (final List<?> aC : altNames1) {
280                             if (!aC.isEmpty()) {
281                                 altNames.add(Objects.toString(aC.get(1), null));
282                             }
283                         }
284                         LOG.debug(" peer alternative names: {}", altNames);
285                     }
286 
287                     final X500Principal issuer = x509.getIssuerX500Principal();
288                     LOG.debug("Issuer principal: {}", toEscapedString(issuer));
289                     final Collection<List<?>> altNames2 = x509.getIssuerAlternativeNames();
290                     if (altNames2 != null) {
291                         final List<String> altNames = new ArrayList<>();
292                         for (final List<?> aC : altNames2) {
293                             if (!aC.isEmpty()) {
294                                 altNames.add(Objects.toString(aC.get(1), null));
295                             }
296                         }
297                         LOG.debug(" issuer alternative names: {}", altNames);
298                     }
299                 }
300             } catch (final Exception ignore) {
301             }
302         }
303 
304         if (hostnameVerifier != null) {
305             final Certificate[] certs = sslsession.getPeerCertificates();
306             if (certs.length < 1) {
307                 throw new SSLPeerUnverifiedException("Peer certificate chain is empty");
308             }
309             final Certificate peerCertificate = certs[0];
310             final X509Certificate x509Certificate;
311             if (peerCertificate instanceof X509Certificate) {
312                 x509Certificate = (X509Certificate) peerCertificate;
313             } else {
314                 throw new SSLPeerUnverifiedException("Unexpected certificate type: " + peerCertificate.getType());
315             }
316             if (hostnameVerifier instanceof HttpClientHostnameVerifier) {
317                 ((HttpClientHostnameVerifier) hostnameVerifier).verify(hostname, x509Certificate);
318             } else if (!hostnameVerifier.verify(hostname, sslsession)) {
319                 final List<SubjectName> subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509Certificate);
320                 throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " +
321                         "of the subject alternative names: " + subjectAlts);
322             }
323         }
324     }
325 
326     /**
327      * Converts an X500Principal to a cleaned string by escaping control characters.
328      * <p>
329      * This method processes the RFC2253 format of the X500Principal and escapes
330      * any ISO control characters to avoid issues in logging or other outputs.
331      * Control characters are replaced with their escaped hexadecimal representation.
332      * </p>
333      *
334      * <p><strong>Note:</strong> For testing purposes, this method is package-private
335      * to allow access within the same package. This allows tests to verify the correct
336      * behavior of the escaping process.</p>
337      *
338      * @param principal the X500Principal to escape
339      * @return the escaped string representation of the X500Principal
340      */
341     @Internal
342     String toEscapedString(final X500Principal principal) {
343         final String principalValue = principal.getName(X500Principal.RFC2253);
344         final StringBuilder sanitizedPrincipal = new StringBuilder(principalValue.length());
345         for (final char c : principalValue.toCharArray()) {
346             if (Character.isISOControl(c)) {
347                 sanitizedPrincipal.append(String.format("\\x%02x", (int) c));
348             } else {
349                 sanitizedPrincipal.append(c);
350             }
351         }
352         return sanitizedPrincipal.toString();
353     }
354 
355 }