Permalink
Browse files

Merge pull request #510 from wsargent/fix-197

Fix for #197 -- use a hostname verifier that does hostname verification
  • Loading branch information...
jfarcand committed Mar 30, 2014
2 parents c9b5391 + bbdc1b3 commit 3c9152e2c75f7e8b654beec40383748a14c6b51b
@@ -21,7 +21,7 @@
import org.asynchttpclient.filter.IOExceptionFilter;
import org.asynchttpclient.filter.RequestFilter;
import org.asynchttpclient.filter.ResponseFilter;
import org.asynchttpclient.util.AllowAllHostnameVerifier;
import org.asynchttpclient.util.DefaultHostnameVerifier;
import org.asynchttpclient.util.ProxyUtils;
import javax.net.ssl.HostnameVerifier;
@@ -596,7 +596,7 @@ public TimeConverter getTimeConverter() {
private boolean allowSslConnectionPool = true;
private boolean useRawUrl = false;
private boolean removeQueryParamOnRedirect = true;
private HostnameVerifier hostnameVerifier = new AllowAllHostnameVerifier();
private HostnameVerifier hostnameVerifier = new DefaultHostnameVerifier();
private int ioThreadMultiplier = 2;
private boolean strict302Handling;
private boolean spdyEnabled;
@@ -15,6 +15,7 @@
import org.asynchttpclient.filter.IOExceptionFilter;
import org.asynchttpclient.filter.RequestFilter;
import org.asynchttpclient.filter.ResponseFilter;
import org.asynchttpclient.util.DefaultHostnameVerifier;
import org.asynchttpclient.util.ProxyUtils;
import javax.net.ssl.HostnameVerifier;
@@ -70,12 +71,7 @@ void configureDefaults() {
allowSslConnectionPool = true;
useRawUrl = false;
removeQueryParamOnRedirect = true;
hostnameVerifier = new HostnameVerifier() {
public boolean verify(String s, SSLSession sslSession) {
return true;
}
};
hostnameVerifier = new DefaultHostnameVerifier();
}
void configureExecutors() {
@@ -0,0 +1,142 @@
/*
* To the extent possible under law, Kevin Locke has waived all copyright and
* related or neighboring rights to this work.
* <p/>
* A legal description of this waiver is available in <a href="https://gist.github.com/kevinoid/3829665">LICENSE.txt</a>
*/
package org.asynchttpclient.util;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.security.auth.kerberos.KerberosPrincipal;
import java.security.Principal;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Uses the internal HostnameChecker to verify the server's hostname matches with the
* certificate. This is a requirement for HTTPS, but the raw SSLEngine does not have
* this functionality. As such, it has to be added in manually. For a more complete
* description of hostname verification and why it's important,
* please read
* <a href="http://tersesystems.com/2014/03/23/fixing-hostname-verification/">Fixing
* Hostname Verification</a>.
* <p/>
* This code is based on Kevin Locke's <a href="http://kevinlocke.name/bits/2012/10/03/ssl-certificate-verification-in-dispatch-and-asynchttpclient/">guide</a> .
* <p/>
*/
public class DefaultHostnameVerifier implements HostnameVerifier {
private HostnameChecker checker;
private HostnameVerifier extraHostnameVerifier;
// Logger to log exceptions.
private static final Logger log = Logger.getLogger(DefaultHostnameVerifier.class.getName());
/**
* A hostname verifier that uses the {{sun.security.util.HostnameChecker}} under the hood.
*/
public DefaultHostnameVerifier() {
this.checker = new ProxyHostnameChecker();
}
/**
* A hostname verifier that takes an external hostname checker. Useful for testing.
*
* @param checker a hostnamechecker.
*/
public DefaultHostnameVerifier(HostnameChecker checker) {
this.checker = checker;
}
/**
* A hostname verifier that falls back to another hostname verifier if not found.
*
* @param extraHostnameVerifier another hostname verifier.
*/
public DefaultHostnameVerifier(HostnameVerifier extraHostnameVerifier) {
this.checker = new ProxyHostnameChecker();
this.extraHostnameVerifier = extraHostnameVerifier;
}
/**
* A hostname verifier with a hostname checker, that falls back to another hostname verifier if not found.
*
* @param checker a custom HostnameChecker.
* @param extraHostnameVerifier another hostname verifier.
*/
public DefaultHostnameVerifier(HostnameChecker checker, HostnameVerifier extraHostnameVerifier) {
this.checker = checker;
this.extraHostnameVerifier = extraHostnameVerifier;
}
/**
* Matches the hostname against the peer certificate in the session.
*
* @param hostname the IP address or hostname of the expected server.
* @param session the SSL session containing the certificates with the ACTUAL hostname/ipaddress.
* @return true if the hostname matches, false otherwise.
*/
private boolean hostnameMatches(String hostname, SSLSession session) {
log.log(Level.FINE, "hostname = {0}, session = {1}", new Object[] { hostname, Base64.encode(session.getId()) });
try {
final Certificate[] peerCertificates = session.getPeerCertificates();
if (peerCertificates.length == 0) {
log.log(Level.FINE, "No peer certificates");
return false;
}
if (peerCertificates[0] instanceof X509Certificate) {
X509Certificate peerCertificate = (X509Certificate) peerCertificates[0];
log.log(Level.FINE, "peerCertificate = {0}", peerCertificate);
try {
checker.match(hostname, peerCertificate);
// Certificate matches hostname if no exception is thrown.
return true;
} catch (CertificateException ex) {
log.log(Level.FINE, "Certificate does not match hostname", ex);
}
} else {
log.log(Level.FINE, "Peer does not have any certificates or they aren't X.509");
}
return false;
} catch (SSLPeerUnverifiedException ex) {
log.log(Level.FINE, "Not using certificates for peers, try verifying the principal");
try {
Principal peerPrincipal = session.getPeerPrincipal();
log.log(Level.FINE, "peerPrincipal = {0}", peerPrincipal);
if (peerPrincipal instanceof KerberosPrincipal) {
return checker.match(hostname, (KerberosPrincipal) peerPrincipal);
} else {
log.log(Level.FINE, "Can't verify principal, not Kerberos");
}
} catch (SSLPeerUnverifiedException ex2) {
// Can't verify principal, no principal
log.log(Level.FINE, "Can't verify principal, no principal", ex2);
}
return false;
}
}
/**
* Verifies the hostname against the peer certificates in a session. Falls back to extraHostnameVerifier if
* there is no match.
*
* @param hostname the IP address or hostname of the expected server.
* @param session the SSL session containing the certificates with the ACTUAL hostname/ipaddress.
* @return true if the hostname matches, false otherwise.
*/
public boolean verify(String hostname, SSLSession session) {
if (hostnameMatches(hostname, session)) {
return true;
} else {
return extraHostnameVerifier != null && extraHostnameVerifier.verify(hostname, session);
}
}
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) Will Sargent. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package org.asynchttpclient.util;
import java.security.Principal;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* Hostname checker interface.
*/
public interface HostnameChecker {
public void match(String hostname, X509Certificate peerCertificate) throws CertificateException;
public boolean match(String hostname, Principal principal);
}
@@ -0,0 +1,83 @@
/*
* Copyright (c) Will Sargent. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package org.asynchttpclient.util;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* A HostnameChecker proxy.
*/
public class ProxyHostnameChecker implements HostnameChecker {
public final static byte TYPE_TLS = 1;
private final Object checker = getHostnameChecker();
public ProxyHostnameChecker() {
}
private Object getHostnameChecker() {
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
final Class<Object> hostnameCheckerClass = (Class<Object>) classLoader.loadClass("sun.security.util.HostnameChecker");
final Method instanceMethod = hostnameCheckerClass.getMethod("getInstance", Byte.TYPE);
return instanceMethod.invoke(null, TYPE_TLS);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
public void match(String hostname, X509Certificate peerCertificate) throws CertificateException {
try {
final Class<?> hostnameCheckerClass = checker.getClass();
final Method checkMethod = hostnameCheckerClass.getMethod("match", String.class, X509Certificate.class);
checkMethod.invoke(checker, hostname, peerCertificate);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof CertificateException) {
throw (CertificateException) t;
} else {
throw new IllegalStateException(e);
}
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
public boolean match(String hostname, Principal principal) {
try {
final Class<?> hostnameCheckerClass = checker.getClass();
final Method checkMethod = hostnameCheckerClass.getMethod("match", String.class, Principal.class);
return (Boolean) checkMethod.invoke(null, hostname, principal);
} catch (NoSuchMethodException e) {
throw new IllegalStateException(e);
} catch (InvocationTargetException e) {
throw new IllegalStateException(e);
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 3c9152e

Please sign in to comment.