From c7fa0b1dadd86d465c35056964c9acdf90c30f7f Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Sun, 21 Jan 2024 18:40:52 +0100 Subject: [PATCH] Standard client TLS strategy implementations to support upgrade of blocking sockets --- .../http/ssl/AbstractClientTlsStrategy.java | 136 +++++++++++++++++- .../http/ssl/SSLConnectionSocketFactory.java | 80 ++++++++++- .../client5/http/ssl/TlsSessionValidator.java | 122 ---------------- .../client5/http/ssl/TlsSocketStrategy.java | 64 +++++++++ 4 files changed, 273 insertions(+), 129 deletions(-) delete mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSessionValidator.java create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSocketStrategy.java diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java index 8818afb91..b52e63649 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/AbstractClientTlsStrategy.java @@ -27,8 +27,16 @@ package org.apache.hc.client5.http.ssl; +import java.io.IOException; +import java.net.Socket; import java.net.SocketAddress; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Objects; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; @@ -36,7 +44,10 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.security.auth.x500.X500Principal; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.core5.annotation.Contract; @@ -44,6 +55,7 @@ import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.concurrent.FutureCallback; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.http.ssl.TLS; import org.apache.hc.core5.http.ssl.TlsCiphers; import org.apache.hc.core5.http2.HttpVersionPolicy; @@ -59,7 +71,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Contract(threading = ThreadingBehavior.STATELESS) -abstract class AbstractClientTlsStrategy implements TlsStrategy { +abstract class AbstractClientTlsStrategy implements TlsStrategy, TlsSocketStrategy { private static final Logger LOG = LoggerFactory.getLogger(AbstractClientTlsStrategy.class); @@ -68,7 +80,6 @@ abstract class AbstractClientTlsStrategy implements TlsStrategy { private final String[] supportedCipherSuites; private final SSLBufferMode sslBufferManagement; private final HostnameVerifier hostnameVerifier; - private final TlsSessionValidator tlsSessionValidator; AbstractClientTlsStrategy( final SSLContext sslContext, @@ -82,7 +93,6 @@ abstract class AbstractClientTlsStrategy implements TlsStrategy { this.supportedCipherSuites = supportedCipherSuites; this.sslBufferManagement = sslBufferManagement != null ? sslBufferManagement : SSLBufferMode.STATIC; this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : HttpsSupport.getDefaultHostnameVerifier(); - this.tlsSessionValidator = new TlsSessionValidator(LOG); } /** @@ -165,10 +175,128 @@ abstract class AbstractClientTlsStrategy implements TlsStrategy { protected void initializeEngine(final SSLEngine sslEngine) { } + protected void initializeSocket(final SSLSocket socket) { + } + protected void verifySession( final String hostname, final SSLSession sslsession) throws SSLException { - tlsSessionValidator.verifySession(hostname, sslsession, hostnameVerifier); + verifySession(hostname, sslsession, hostnameVerifier); + } + + @Override + public SSLSocket upgrade(final Socket socket, + final String target, + final int port, + final Object attachment, + final HttpContext context) throws IOException { + final SSLSocket upgradedSocket = (SSLSocket) sslContext.getSocketFactory().createSocket( + socket, + target, + port, + true); + executeHandshake(upgradedSocket, target, attachment); + return upgradedSocket; + } + + private void executeHandshake( + final SSLSocket upgradedSocket, + final String target, + final Object attachment) throws IOException { + final TlsConfig tlsConfig = attachment instanceof TlsConfig ? (TlsConfig) attachment : TlsConfig.DEFAULT; + if (supportedProtocols != null) { + upgradedSocket.setEnabledProtocols(supportedProtocols); + } else { + upgradedSocket.setEnabledProtocols((TLS.excludeWeak(upgradedSocket.getEnabledProtocols()))); + } + if (supportedCipherSuites != null) { + upgradedSocket.setEnabledCipherSuites(supportedCipherSuites); + } else { + upgradedSocket.setEnabledCipherSuites(TlsCiphers.excludeWeak(upgradedSocket.getEnabledCipherSuites())); + } + final Timeout handshakeTimeout = tlsConfig.getHandshakeTimeout(); + if (handshakeTimeout != null) { + upgradedSocket.setSoTimeout(handshakeTimeout.toMillisecondsIntBound()); + } + + initializeSocket(upgradedSocket); + + if (LOG.isDebugEnabled()) { + LOG.debug("Enabled protocols: {}", (Object) upgradedSocket.getEnabledProtocols()); + LOG.debug("Enabled cipher suites: {}", (Object) upgradedSocket.getEnabledCipherSuites()); + LOG.debug("Starting handshake ({})", handshakeTimeout); + } + upgradedSocket.startHandshake(); + verifySession(target, upgradedSocket.getSession()); + } + + void verifySession( + final String hostname, + final SSLSession sslsession, + final HostnameVerifier hostnameVerifier) throws SSLException { + + if (LOG.isDebugEnabled()) { + LOG.debug("Secure session established"); + LOG.debug(" negotiated protocol: {}", sslsession.getProtocol()); + LOG.debug(" negotiated cipher suite: {}", sslsession.getCipherSuite()); + + try { + + final Certificate[] certs = sslsession.getPeerCertificates(); + final Certificate cert = certs[0]; + if (cert instanceof X509Certificate) { + final X509Certificate x509 = (X509Certificate) cert; + final X500Principal peer = x509.getSubjectX500Principal(); + + LOG.debug(" peer principal: {}", peer); + final Collection> altNames1 = x509.getSubjectAlternativeNames(); + if (altNames1 != null) { + final List altNames = new ArrayList<>(); + for (final List aC : altNames1) { + if (!aC.isEmpty()) { + altNames.add(Objects.toString(aC.get(1), null)); + } + } + LOG.debug(" peer alternative names: {}", altNames); + } + + final X500Principal issuer = x509.getIssuerX500Principal(); + LOG.debug(" issuer principal: {}", issuer); + final Collection> altNames2 = x509.getIssuerAlternativeNames(); + if (altNames2 != null) { + final List altNames = new ArrayList<>(); + for (final List aC : altNames2) { + if (!aC.isEmpty()) { + altNames.add(Objects.toString(aC.get(1), null)); + } + } + LOG.debug(" issuer alternative names: {}", altNames); + } + } + } catch (final Exception ignore) { + } + } + + if (hostnameVerifier != null) { + final Certificate[] certs = sslsession.getPeerCertificates(); + if (certs.length < 1) { + throw new SSLPeerUnverifiedException("Peer certificate chain is empty"); + } + final Certificate peerCertificate = certs[0]; + final X509Certificate x509Certificate; + if (peerCertificate instanceof X509Certificate) { + x509Certificate = (X509Certificate) peerCertificate; + } else { + throw new SSLPeerUnverifiedException("Unexpected certificate type: " + peerCertificate.getType()); + } + if (hostnameVerifier instanceof HttpClientHostnameVerifier) { + ((HttpClientHostnameVerifier) hostnameVerifier).verify(hostname, x509Certificate); + } else if (!hostnameVerifier.verify(hostname, sslsession)) { + final List subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509Certificate); + throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " + + "of the subject alternative names: " + subjectAlts); + } + } } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java index 9925c9a80..b8cbde45c 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/SSLConnectionSocketFactory.java @@ -36,17 +36,24 @@ import java.net.SocketAddress; import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.regex.Pattern; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; +import javax.security.auth.x500.X500Principal; import org.apache.hc.client5.http.config.TlsConfig; import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory; @@ -128,7 +135,6 @@ public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactor private final HostnameVerifier hostnameVerifier; private final String[] supportedProtocols; private final String[] supportedCipherSuites; - private final TlsSessionValidator tlsSessionValidator; public SSLConnectionSocketFactory(final SSLContext sslContext) { this(sslContext, HttpsSupport.getDefaultHostnameVerifier()); @@ -176,7 +182,6 @@ public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactor this.supportedProtocols = supportedProtocols; this.supportedCipherSuites = supportedCipherSuites; this.hostnameVerifier = hostnameVerifier != null ? hostnameVerifier : HttpsSupport.getDefaultHostnameVerifier(); - this.tlsSessionValidator = new TlsSessionValidator(LOG); } /** @@ -379,7 +384,76 @@ public class SSLConnectionSocketFactory implements LayeredConnectionSocketFactor protected void verifySession( final String hostname, final SSLSession sslSession) throws SSLException { - tlsSessionValidator.verifySession(hostname, sslSession, hostnameVerifier); + verifySession(hostname, sslSession, hostnameVerifier); + } + + void verifySession( + final String hostname, + final SSLSession sslsession, + final HostnameVerifier hostnameVerifier) throws SSLException { + + if (LOG.isDebugEnabled()) { + LOG.debug("Secure session established"); + LOG.debug(" negotiated protocol: {}", sslsession.getProtocol()); + LOG.debug(" negotiated cipher suite: {}", sslsession.getCipherSuite()); + + try { + + final Certificate[] certs = sslsession.getPeerCertificates(); + final Certificate cert = certs[0]; + if (cert instanceof X509Certificate) { + final X509Certificate x509 = (X509Certificate) cert; + final X500Principal peer = x509.getSubjectX500Principal(); + + LOG.debug(" peer principal: {}", peer); + final Collection> altNames1 = x509.getSubjectAlternativeNames(); + if (altNames1 != null) { + final List altNames = new ArrayList<>(); + for (final List aC : altNames1) { + if (!aC.isEmpty()) { + altNames.add(Objects.toString(aC.get(1), null)); + } + } + LOG.debug(" peer alternative names: {}", altNames); + } + + final X500Principal issuer = x509.getIssuerX500Principal(); + LOG.debug(" issuer principal: {}", issuer); + final Collection> altNames2 = x509.getIssuerAlternativeNames(); + if (altNames2 != null) { + final List altNames = new ArrayList<>(); + for (final List aC : altNames2) { + if (!aC.isEmpty()) { + altNames.add(Objects.toString(aC.get(1), null)); + } + } + LOG.debug(" issuer alternative names: {}", altNames); + } + } + } catch (final Exception ignore) { + } + } + + if (hostnameVerifier != null) { + final Certificate[] certs = sslsession.getPeerCertificates(); + if (certs.length < 1) { + throw new SSLPeerUnverifiedException("Peer certificate chain is empty"); + } + final Certificate peerCertificate = certs[0]; + final X509Certificate x509Certificate; + if (peerCertificate instanceof X509Certificate) { + x509Certificate = (X509Certificate) peerCertificate; + } else { + throw new SSLPeerUnverifiedException("Unexpected certificate type: " + peerCertificate.getType()); + } + if (hostnameVerifier instanceof HttpClientHostnameVerifier) { + ((HttpClientHostnameVerifier) hostnameVerifier).verify(hostname, x509Certificate); + } else if (!hostnameVerifier.verify(hostname, sslsession)) { + final List subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509Certificate); + throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " + + "of the subject alternative names: " + subjectAlts); + } + } } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSessionValidator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSessionValidator.java deleted file mode 100644 index 76541ff46..000000000 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSessionValidator.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * ==================================================================== - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * ==================================================================== - * - * This software consists of voluntary contributions made by many - * individuals on behalf of the Apache Software Foundation. For more - * information on the Apache Software Foundation, please see - * . - * - */ - -package org.apache.hc.client5.http.ssl; - -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; -import javax.security.auth.x500.X500Principal; - -import org.slf4j.Logger; - -final class TlsSessionValidator { - - private final Logger log; - - TlsSessionValidator(final Logger log) { - this.log = log; - } - - void verifySession( - final String hostname, - final SSLSession sslsession, - final HostnameVerifier hostnameVerifier) throws SSLException { - - if (log.isDebugEnabled()) { - log.debug("Secure session established"); - log.debug(" negotiated protocol: {}", sslsession.getProtocol()); - log.debug(" negotiated cipher suite: {}", sslsession.getCipherSuite()); - - try { - - final Certificate[] certs = sslsession.getPeerCertificates(); - final Certificate cert = certs[0]; - if (cert instanceof X509Certificate) { - final X509Certificate x509 = (X509Certificate) cert; - final X500Principal peer = x509.getSubjectX500Principal(); - - log.debug(" peer principal: {}", peer); - final Collection> altNames1 = x509.getSubjectAlternativeNames(); - if (altNames1 != null) { - final List altNames = new ArrayList<>(); - for (final List aC : altNames1) { - if (!aC.isEmpty()) { - altNames.add(Objects.toString(aC.get(1), null)); - } - } - log.debug(" peer alternative names: {}", altNames); - } - - final X500Principal issuer = x509.getIssuerX500Principal(); - log.debug(" issuer principal: {}", issuer); - final Collection> altNames2 = x509.getIssuerAlternativeNames(); - if (altNames2 != null) { - final List altNames = new ArrayList<>(); - for (final List aC : altNames2) { - if (!aC.isEmpty()) { - altNames.add(Objects.toString(aC.get(1), null)); - } - } - log.debug(" issuer alternative names: {}", altNames); - } - } - } catch (final Exception ignore) { - } - } - - if (hostnameVerifier != null) { - final Certificate[] certs = sslsession.getPeerCertificates(); - if (certs.length < 1) { - throw new SSLPeerUnverifiedException("Peer certificate chain is empty"); - } - final Certificate peerCertificate = certs[0]; - final X509Certificate x509Certificate; - if (peerCertificate instanceof X509Certificate) { - x509Certificate = (X509Certificate) peerCertificate; - } else { - throw new SSLPeerUnverifiedException("Unexpected certificate type: " + peerCertificate.getType()); - } - if (hostnameVerifier instanceof HttpClientHostnameVerifier) { - ((HttpClientHostnameVerifier) hostnameVerifier).verify(hostname, x509Certificate); - } else if (!hostnameVerifier.verify(hostname, sslsession)) { - final List subjectAlts = DefaultHostnameVerifier.getSubjectAltNames(x509Certificate); - throw new SSLPeerUnverifiedException("Certificate for <" + hostname + "> doesn't match any " + - "of the subject alternative names: " + subjectAlts); - } - } - } - -} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSocketStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSocketStrategy.java new file mode 100644 index 000000000..97af381f6 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/ssl/TlsSocketStrategy.java @@ -0,0 +1,64 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.ssl; + +import java.io.IOException; +import java.net.Socket; + +import javax.net.ssl.SSLSocket; + +import org.apache.hc.core5.annotation.Contract; +import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.http.protocol.HttpContext; + +/** + * TLS protocol upgrade strategy for blocking {@link Socket}s. + * + * @since 5.4 + */ +@Contract(threading = ThreadingBehavior.STATELESS) +public interface TlsSocketStrategy { + + /** + * Upgrades the given plain socket and executes the TLS handshake over it. + * + * @param socket the existing plain socket + * @param target the name of the target host. + * @param port the port to connect to on the target host. + * @param context the actual HTTP context. + * @param attachment connect request attachment. + * @return socket upgraded to TLS. + */ + SSLSocket upgrade( + Socket socket, + String target, + int port, + Object attachment, + HttpContext context) throws IOException; + +}