Default HostnameVerifier to check server identity against public suffix list
git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1622866 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
3bb9aa5099
commit
6f706c2328
|
@ -256,13 +256,7 @@ public abstract class AbstractVerifier implements X509HostnameVerifier {
|
||||||
* @return number of dots
|
* @return number of dots
|
||||||
*/
|
*/
|
||||||
public static int countDots(final String s) {
|
public static int countDots(final String s) {
|
||||||
int count = 0;
|
return DefaultHostnameVerifier.countDots(s);
|
||||||
for(int i = 0; i < s.length(); i++) {
|
|
||||||
if(s.charAt(i) == '.') {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,6 @@ import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.NoSuchElementException;
|
import java.util.NoSuchElementException;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import javax.naming.InvalidNameException;
|
import javax.naming.InvalidNameException;
|
||||||
import javax.naming.NamingException;
|
import javax.naming.NamingException;
|
||||||
|
@ -54,6 +53,7 @@ import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.apache.http.annotation.Immutable;
|
import org.apache.http.annotation.Immutable;
|
||||||
import org.apache.http.conn.util.InetAddressUtils;
|
import org.apache.http.conn.util.InetAddressUtils;
|
||||||
|
import org.apache.http.conn.util.PublicSuffixMatcher;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default {@link javax.net.ssl.HostnameVerifier} implementation.
|
* Default {@link javax.net.ssl.HostnameVerifier} implementation.
|
||||||
|
@ -63,26 +63,21 @@ import org.apache.http.conn.util.InetAddressUtils;
|
||||||
@Immutable
|
@Immutable
|
||||||
public final class DefaultHostnameVerifier implements HostnameVerifier {
|
public final class DefaultHostnameVerifier implements HostnameVerifier {
|
||||||
|
|
||||||
/**
|
|
||||||
* This contains a list of 2nd-level domains that aren't allowed to
|
|
||||||
* have wildcards when combined with country-codes.
|
|
||||||
* For example: [*.co.uk].
|
|
||||||
* <p>
|
|
||||||
* The [*.co.uk] problem is an interesting one. Should we just hope
|
|
||||||
* that CA's would never foolishly allow such a certificate to happen?
|
|
||||||
* Looks like we're the only implementation guarding against this.
|
|
||||||
* Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
private final static Pattern BAD_COUNTRY_WILDCARD_PATTERN = Pattern.compile(
|
|
||||||
"^[a-z0-9\\-\\*]+\\.(ac|co|com|ed|edu|go|gouv|gov|info|lg|ne|net|or|org)\\.[a-z0-9\\-]{2}$",
|
|
||||||
Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
final static int DNS_NAME_TYPE = 2;
|
final static int DNS_NAME_TYPE = 2;
|
||||||
final static int IP_ADDRESS_TYPE = 7;
|
final static int IP_ADDRESS_TYPE = 7;
|
||||||
|
|
||||||
private final Log log = LogFactory.getLog(getClass());
|
private final Log log = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
|
private final PublicSuffixMatcher publicSuffixMatcher;
|
||||||
|
|
||||||
|
public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
|
||||||
|
this.publicSuffixMatcher = publicSuffixMatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultHostnameVerifier() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public final boolean verify(final String host, final SSLSession session) {
|
public final boolean verify(final String host, final SSLSession session) {
|
||||||
try {
|
try {
|
||||||
|
@ -110,7 +105,7 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
|
||||||
} else if (ipv6) {
|
} else if (ipv6) {
|
||||||
matchIPv6Address(host, subjectAlts);
|
matchIPv6Address(host, subjectAlts);
|
||||||
} else {
|
} else {
|
||||||
matchDNSName(host, subjectAlts);
|
matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// CN matching has been deprecated by rfc2818 and can be used
|
// CN matching has been deprecated by rfc2818 and can be used
|
||||||
|
@ -121,7 +116,7 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
|
||||||
throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
|
throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
|
||||||
"a common name and does not have alternative names");
|
"a common name and does not have alternative names");
|
||||||
}
|
}
|
||||||
matchCN(host, cn);
|
matchCN(host, cn, this.publicSuffixMatcher);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,12 +144,13 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
|
||||||
"of the subject alternative names: " + subjectAlts);
|
"of the subject alternative names: " + subjectAlts);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void matchDNSName(final String host, final List<String> subjectAlts) throws SSLException {
|
static void matchDNSName(final String host, final List<String> subjectAlts,
|
||||||
|
final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
|
||||||
final String normalizedHost = host.toLowerCase(Locale.ROOT);
|
final String normalizedHost = host.toLowerCase(Locale.ROOT);
|
||||||
for (int i = 0; i < subjectAlts.size(); i++) {
|
for (int i = 0; i < subjectAlts.size(); i++) {
|
||||||
final String subjectAlt = subjectAlts.get(i);
|
final String subjectAlt = subjectAlts.get(i);
|
||||||
final String normalizedSubjectAlt = subjectAlt.toLowerCase(Locale.ROOT);
|
final String normalizedSubjectAlt = subjectAlt.toLowerCase(Locale.ROOT);
|
||||||
if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt)) {
|
if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,17 +158,37 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
|
||||||
"of the subject alternative names: " + subjectAlts);
|
"of the subject alternative names: " + subjectAlts);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void matchCN(final String host, final String cn) throws SSLException {
|
static void matchCN(final String host, final String cn,
|
||||||
if (!matchIdentityStrict(host, cn)) {
|
final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
|
||||||
|
if (!matchIdentityStrict(host, cn, publicSuffixMatcher)) {
|
||||||
throw new SSLException("Certificate for <" + host + "> doesn't match " +
|
throw new SSLException("Certificate for <" + host + "> doesn't match " +
|
||||||
"common name of the certificate subject: " + cn);
|
"common name of the certificate subject: " + cn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean matchIdentity(final String host, final String identity, final boolean strict) {
|
private static boolean matchIdentity(final String host, final String identity,
|
||||||
|
final PublicSuffixMatcher publicSuffixMatcher,
|
||||||
|
final boolean strict) {
|
||||||
if (host == null) {
|
if (host == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (publicSuffixMatcher != null) {
|
||||||
|
String domainRoot = publicSuffixMatcher.getDomainRoot(identity);
|
||||||
|
if (domainRoot == null) {
|
||||||
|
// Public domain
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
domainRoot = "." + domainRoot;
|
||||||
|
if (!host.endsWith(domainRoot)) {
|
||||||
|
// Domain root mismatch
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (strict && countDots(identity) != countDots(domainRoot)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RFC 2818, 3.1. Server Identity
|
// RFC 2818, 3.1. Server Identity
|
||||||
// "...Names may contain the wildcard
|
// "...Names may contain the wildcard
|
||||||
// character * which is considered to match any single domain name
|
// character * which is considered to match any single domain name
|
||||||
|
@ -180,7 +196,6 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
|
||||||
// Based on this statement presuming only singular wildcard is legal
|
// Based on this statement presuming only singular wildcard is legal
|
||||||
final int asteriskIdx = identity.indexOf('*');
|
final int asteriskIdx = identity.indexOf('*');
|
||||||
if (asteriskIdx != -1) {
|
if (asteriskIdx != -1) {
|
||||||
if (!strict || !BAD_COUNTRY_WILDCARD_PATTERN.matcher(identity).matches()) {
|
|
||||||
final String prefix = identity.substring(0, asteriskIdx);
|
final String prefix = identity.substring(0, asteriskIdx);
|
||||||
final String suffix = identity.substring(asteriskIdx + 1);
|
final String suffix = identity.substring(asteriskIdx + 1);
|
||||||
if (!prefix.isEmpty() && !host.startsWith(prefix)) {
|
if (!prefix.isEmpty() && !host.startsWith(prefix)) {
|
||||||
|
@ -199,16 +214,35 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return host.equalsIgnoreCase(identity);
|
return host.equalsIgnoreCase(identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int countDots(final String s) {
|
||||||
|
int count = 0;
|
||||||
|
for(int i = 0; i < s.length(); i++) {
|
||||||
|
if(s.charAt(i) == '.') {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean matchIdentity(final String host, final String identity,
|
||||||
|
final PublicSuffixMatcher publicSuffixMatcher) {
|
||||||
|
return matchIdentity(host, identity, publicSuffixMatcher, false);
|
||||||
|
}
|
||||||
|
|
||||||
static boolean matchIdentity(final String host, final String identity) {
|
static boolean matchIdentity(final String host, final String identity) {
|
||||||
return matchIdentity(host, identity, false);
|
return matchIdentity(host, identity, null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean matchIdentityStrict(final String host, final String identity,
|
||||||
|
final PublicSuffixMatcher publicSuffixMatcher) {
|
||||||
|
return matchIdentity(host, identity, publicSuffixMatcher, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean matchIdentityStrict(final String host, final String identity) {
|
static boolean matchIdentityStrict(final String host, final String identity) {
|
||||||
return matchIdentity(host, identity, true);
|
return matchIdentity(host, identity, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String extractCN(final String subjectPrincipal) throws SSLException {
|
static String extractCN(final String subjectPrincipal) throws SSLException {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import java.util.Arrays;
|
||||||
|
|
||||||
import javax.net.ssl.SSLException;
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
|
import org.apache.http.conn.util.PublicSuffixMatcher;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -45,10 +46,14 @@ import org.junit.Test;
|
||||||
public class TestDefaultHostnameVerifier {
|
public class TestDefaultHostnameVerifier {
|
||||||
|
|
||||||
private DefaultHostnameVerifier impl;
|
private DefaultHostnameVerifier impl;
|
||||||
|
private PublicSuffixMatcher publicSuffixMatcher;
|
||||||
|
private DefaultHostnameVerifier implWithPublicSuffixCheck;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setup() {
|
public void setup() {
|
||||||
impl = new DefaultHostnameVerifier();
|
impl = new DefaultHostnameVerifier();
|
||||||
|
publicSuffixMatcher = new PublicSuffixMatcher(Arrays.asList("com", "co.jp", "gov.uk"), null);
|
||||||
|
implWithPublicSuffixCheck = new DefaultHostnameVerifier(publicSuffixMatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -121,8 +126,11 @@ public class TestDefaultHostnameVerifier {
|
||||||
// Silly test because no-one would ever be able to lookup an IP address
|
// Silly test because no-one would ever be able to lookup an IP address
|
||||||
// using "*.co.jp".
|
// using "*.co.jp".
|
||||||
impl.verify("*.co.jp", x509);
|
impl.verify("*.co.jp", x509);
|
||||||
exceptionPlease(impl, "foo.co.jp", x509);
|
impl.verify("foo.co.jp", x509);
|
||||||
exceptionPlease(impl, "\u82b1\u5b50.co.jp", x509);
|
impl.verify("\u82b1\u5b50.co.jp", x509);
|
||||||
|
|
||||||
|
exceptionPlease(implWithPublicSuffixCheck, "foo.co.jp", x509);
|
||||||
|
exceptionPlease(implWithPublicSuffixCheck, "\u82b1\u5b50.co.jp", x509);
|
||||||
|
|
||||||
in = new ByteArrayInputStream(CertificatesToPlayWith.X509_WILD_FOO_BAR_HANAKO);
|
in = new ByteArrayInputStream(CertificatesToPlayWith.X509_WILD_FOO_BAR_HANAKO);
|
||||||
x509 = (X509Certificate) cf.generateCertificate(in);
|
x509 = (X509Certificate) cf.generateCertificate(in);
|
||||||
|
@ -194,23 +202,26 @@ public class TestDefaultHostnameVerifier {
|
||||||
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("s.a.b.c", "*.b.c"));
|
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("s.a.b.c", "*.b.c"));
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.b.c", "*.b.c")); // subdomain not OK
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.b.c", "*.b.c")); // subdomain not OK
|
||||||
|
|
||||||
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.gov.uk", "*.gov.uk"));
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.gov.uk", "*.gov.uk", publicSuffixMatcher));
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.gov.uk", "*.gov.uk")); // Bad 2TLD
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.gov.uk", "*.gov.uk", publicSuffixMatcher)); // Bad 2TLD
|
||||||
|
|
||||||
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("s.a.gov.uk", "*.gov.uk"));
|
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("s.a.gov.uk", "*.a.gov.uk", publicSuffixMatcher));
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.uk", "*.gov.uk")); // BBad 2TLD/no subdomain allowed
|
Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.uk", "*.a.gov.uk", publicSuffixMatcher));
|
||||||
|
|
||||||
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.gov.com", "*.gov.com"));
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("s.a.gov.uk", "*.gov.uk", publicSuffixMatcher));
|
||||||
Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("a.gov.com", "*.gov.com"));
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.uk", "*.gov.uk", publicSuffixMatcher)); // BBad 2TLD/no subdomain allowed
|
||||||
|
|
||||||
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("s.a.gov.com", "*.gov.com"));
|
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.gov.com", "*.gov.com", publicSuffixMatcher));
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.com", "*.gov.com")); // no subdomain allowed
|
Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("a.gov.com", "*.gov.com", publicSuffixMatcher));
|
||||||
|
|
||||||
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.gov.uk", "a*.gov.uk"));
|
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("s.a.gov.com", "*.gov.com", publicSuffixMatcher));
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.gov.uk", "a*.gov.uk")); // Bad 2TLD
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.com", "*.gov.com", publicSuffixMatcher)); // no subdomain allowed
|
||||||
|
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("s.a.gov.uk", "a*.gov.uk")); // Bad 2TLD
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.gov.uk", "a*.gov.uk", publicSuffixMatcher));
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.uk", "a*.gov.uk")); // Bad 2TLD/no subdomain allowed
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.gov.uk", "a*.gov.uk", publicSuffixMatcher)); // Bad 2TLD
|
||||||
|
|
||||||
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("s.a.gov.uk", "a*.gov.uk", publicSuffixMatcher)); // Bad 2TLD
|
||||||
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.uk", "a*.gov.uk", publicSuffixMatcher)); // Bad 2TLD/no subdomain allowed
|
||||||
|
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.b.*"));
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.b.*"));
|
||||||
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.b.*"));
|
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.b.*"));
|
||||||
|
|
Loading…
Reference in New Issue