Skip to content

Fix for #197 -- use a hostname verifier that does hostname verification #510

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 30, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -596,7 +596,7 @@ public static class Builder {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
27 changes: 27 additions & 0 deletions api/src/main/java/org/asynchttpclient/util/HostnameChecker.java
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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 modified api/src/test/resources/ssltest-cacerts.jks
Binary file not shown.
Binary file modified api/src/test/resources/ssltest-keystore.jks
Binary file not shown.
Binary file modified providers/grizzly/src/test/resources/ssltest-cacerts.jks
Binary file not shown.
Binary file modified providers/grizzly/src/test/resources/ssltest-keystore.jks
Binary file not shown.
Binary file modified providers/netty/src/test/resources/ssltest-cacerts.jks
Binary file not shown.
Binary file modified providers/netty/src/test/resources/ssltest-keystore.jks
Binary file not shown.