Skip to content

Commit

Permalink
Use a hostname verifier that does hostname verification, backport #510,
Browse files Browse the repository at this point in the history
close #197
  • Loading branch information
Stephane Landelle committed Jul 10, 2014
1 parent 899fd7a commit a894583
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 2 deletions.
Expand Up @@ -12,9 +12,10 @@
*/
package com.ning.http.client;

import com.ning.http.util.AllowAllHostnameVerifier;
import static com.ning.http.util.MiscUtils.getBoolean;

import com.ning.http.util.DefaultHostnameVerifier;

import javax.net.ssl.HostnameVerifier;

public final class AsyncHttpClientConfigDefaults {
Expand Down Expand Up @@ -118,6 +119,6 @@ public static boolean defaultRemoveQueryParamOnRedirect() {
}

public static HostnameVerifier defaultHostnameVerifier() {
return new AllowAllHostnameVerifier();
return new DefaultHostnameVerifier();
}
}
142 changes: 142 additions & 0 deletions src/main/java/com/ning/http/util/DefaultHostnameVerifier.java
@@ -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 com.ning.http.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 src/main/java/com/ning/http/util/HostnameChecker.java
@@ -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 com.ning.http.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);
}
83 changes: 83 additions & 0 deletions src/main/java/com/ning/http/util/ProxyHostnameChecker.java
@@ -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 com.ning.http.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 src/test/resources/ssltest-cacerts.jks
Binary file not shown.
Binary file modified src/test/resources/ssltest-keystore.jks
Binary file not shown.

0 comments on commit a894583

Please sign in to comment.