Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Use a hostname verifier that does hostname verification, backport #510,
close #197
- Loading branch information
Showing
with
255 additions
and 2 deletions.
- +3 −2 src/main/java/com/ning/http/client/AsyncHttpClientConfigDefaults.java
- +142 −0 src/main/java/com/ning/http/util/DefaultHostnameVerifier.java
- +27 −0 src/main/java/com/ning/http/util/HostnameChecker.java
- +83 −0 src/main/java/com/ning/http/util/ProxyHostnameChecker.java
- BIN src/test/resources/ssltest-cacerts.jks
- BIN src/test/resources/ssltest-keystore.jks
@@ -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); | ||
} | ||
} | ||
} |
@@ -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); | ||
} |
@@ -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); | ||
} | ||
} | ||
|
||
} |
Empty file.
Empty file.