Index: commons-httpclient-3.1/src/java/org/apache/commons/httpclient/protocol/SSLProtocolSocketFactory.java =================================================================== --- commons-httpclient-3.1.orig/src/java/org/apache/commons/httpclient/protocol/SSLProtocolSocketFactory.java +++ commons-httpclient-3.1/src/java/org/apache/commons/httpclient/protocol/SSLProtocolSocketFactory.java @@ -31,10 +31,17 @@ package org.apache.commons.httpclient.protocol; import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import org.apache.commons.httpclient.ConnectTimeoutException; @@ -79,12 +86,17 @@ public class SSLProtocolSocketFactory im InetAddress clientHost, int clientPort) throws IOException, UnknownHostException { - return SSLSocketFactory.getDefault().createSocket( + SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket( host, port, clientHost, clientPort ); + + verifyHostName( host, (SSLSocket) socket ); + + // verifyHostName() didn't blowup - good! + return socket; } /** @@ -124,15 +136,18 @@ public class SSLProtocolSocketFactory im } int timeout = params.getConnectionTimeout(); if (timeout == 0) { - return createSocket(host, port, localAddress, localPort); + SSLSocket socket = (SSLSocket) createSocket(host, port, localAddress, localPort); + verifyHostName(host, (SSLSocket) socket); + return socket; } else { // To be eventually deprecated when migrated to Java 1.4 or above - Socket socket = ReflectionSocketFactory.createSocket( + SSLSocket socket =(SSLSocket) ReflectionSocketFactory.createSocket( "javax.net.ssl.SSLSocketFactory", host, port, localAddress, localPort, timeout); if (socket == null) { - socket = ControllerThreadSocketFactory.createSocket( + socket = (SSLSocket) ControllerThreadSocketFactory.createSocket( this, host, port, localAddress, localPort, timeout); } + verifyHostName(host, (SSLSocket) socket); return socket; } } @@ -142,10 +157,12 @@ public class SSLProtocolSocketFactory im */ public Socket createSocket(String host, int port) throws IOException, UnknownHostException { - return SSLSocketFactory.getDefault().createSocket( + SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket( host, port ); + verifyHostName( host, (SSLSocket) socket ); + return socket; } /** @@ -157,14 +174,133 @@ public class SSLProtocolSocketFactory im int port, boolean autoClose) throws IOException, UnknownHostException { - return ((SSLSocketFactory) SSLSocketFactory.getDefault()).createSocket( + SSLSocket s = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory.getDefault()).createSocket( socket, host, port, autoClose ); + verifyHostName( host, (SSLSocket) socket ); + + // verifyHostName() didn't blowup - good! + return s; + } + + private static void verifyHostName( String host, SSLSocket ssl ) + throws IOException { + if ( host == null ) { + throw new NullPointerException( "host to verify was null" ); + } + + SSLSession session = ssl.getSession(); + if ( session == null ) { + // In our experience this only happens under IBM 1.4.x when + // spurious (unrelated) certificates show up in the server's chain. + // Hopefully this will unearth the real problem: + InputStream in = ssl.getInputStream(); + in.available(); + /* + If you're looking at the 2 lines of code above because you're + running into a problem, you probably have two options: + + #1. Clean up the certificate chain that your server + is presenting (e.g. edit "/etc/apache2/server.crt" or + wherever it is your server's certificate chain is + defined). + + OR + + #2. Upgrade to an IBM 1.5.x or greater JVM, or switch to a + non-IBM JVM. + */ + + // If ssl.getInputStream().available() didn't cause an exception, + // maybe at least now the session is available? + session = ssl.getSession(); + if ( session == null ) { + // If it's still null, probably a startHandshake() will + // unearth the real problem. + ssl.startHandshake(); + + // Okay, if we still haven't managed to cause an exception, + // might as well go for the NPE. Or maybe we're okay now? + session = ssl.getSession(); + } + } + + Certificate[] certs = session.getPeerCertificates(); + X509Certificate x509 = (X509Certificate) certs[ 0 ]; + String cn = getCN( x509 ); + if ( cn == null ) { + String subject = x509.getSubjectX500Principal().toString(); + String msg = "certificate doesn't contain CN: " + subject; + throw new SSLException( msg ); + } + // I'm okay with being case-insensitive when comparing the host we used + // to establish the socket to the hostname in the certificate. + // Don't trim the CN, though. + cn = cn.toLowerCase(); + host = host.trim().toLowerCase(); + boolean doWildcard = false; + if ( cn.startsWith( "*." ) ) { + // The CN better have at least two dots if it wants wildcard action, + // but can't be [*.co.uk] or [*.co.jp] or [*.org.uk], etc... + String withoutCountryCode = ""; + if ( cn.length() >= 7 && cn.length() <= 9 ) { + withoutCountryCode = cn.substring( 2, cn.length() - 2 ); + } + doWildcard = cn.lastIndexOf( '.' ) >= 0 && + !"ac.".equals( withoutCountryCode ) && + !"co.".equals( withoutCountryCode ) && + !"com.".equals( withoutCountryCode ) && + !"ed.".equals( withoutCountryCode ) && + !"edu.".equals( withoutCountryCode ) && + !"go.".equals( withoutCountryCode ) && + !"gouv.".equals( withoutCountryCode ) && + !"gov.".equals( withoutCountryCode ) && + !"info.".equals( withoutCountryCode ) && + !"lg.".equals( withoutCountryCode ) && + !"ne.".equals( withoutCountryCode ) && + !"net.".equals( withoutCountryCode ) && + !"or.".equals( withoutCountryCode ) && + !"org.".equals( withoutCountryCode ); + + // The [*.co.uk] problem is an interesting one. Should we just + // hope that CA's would never foolishly allow such a + // certificate to happen? + } + + boolean match; + if ( doWildcard ) { + match = host.endsWith( cn.substring( 1 ) ); + } else { + match = host.equals( cn ); + } + if ( !match ) { + throw new SSLException( "hostname in certificate didn't match: <" + host + "> != <" + cn + ">" ); + } } + private static String getCN( X509Certificate cert ) { + // Note: toString() seems to do a better job than getName() + // + // For example, getName() gives me this: + // 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d + // + // whereas toString() gives me this: + // EMAILADDRESS=juliusdavies@cucbc.com + String subjectPrincipal = cert.getSubjectX500Principal().toString(); + int x = subjectPrincipal.indexOf( "CN=" ); + if ( x >= 0 ) { + int y = subjectPrincipal.indexOf( ',', x ); + // If there are no more commas, then CN= is the last entry. + y = ( y >= 0 ) ? y : subjectPrincipal.length(); + return subjectPrincipal.substring( x + 3, y ); + } else { + return null; + } + } + /** * All instances of SSLProtocolSocketFactory are the same. */