Improved cert identity matching based on regex patterns

git-svn-id: https://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk@1619372 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Oleg Kalnichevski 2014-08-21 11:57:11 +00:00
parent 4f49584d6b
commit 268d6cc113
4 changed files with 68 additions and 56 deletions

View File

@ -59,6 +59,15 @@ public abstract class AbstractVerifier implements X509HostnameVerifier {
private final Log log = LogFactory.getLog(getClass());
final static String[] BAD_COUNTRY_2LDS =
{ "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
"lg", "ne", "net", "or", "org" };
static {
// Just in case developer forgot to manually sort the array. :-)
Arrays.sort(BAD_COUNTRY_2LDS);
}
@Override
public final void verify(final String host, final SSLSocket ssl)
throws IOException {
@ -179,7 +188,7 @@ public abstract class AbstractVerifier implements X509HostnameVerifier {
if (parts.length != 3 || parts[2].length() != 2) {
return true; // it's not an attempt to wildcard a 2TLD within a country code
}
return Arrays.binarySearch(DefaultHostnameVerifier.BAD_COUNTRY_2LDS, parts[1]) < 0;
return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
}
public static String[] getCNs(final X509Certificate cert) {
@ -219,7 +228,13 @@ public abstract class AbstractVerifier implements X509HostnameVerifier {
* @return number of dots
*/
public static int countDots(final String s) {
return DefaultHostnameVerifier.countDots(s);
int count = 0;
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) == '.') {
count++;
}
}
return count;
}
}

View File

@ -33,11 +33,11 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.naming.InvalidNameException;
import javax.naming.NamingException;
@ -65,6 +65,9 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
public static final DefaultHostnameVerifier INSTANCE = new DefaultHostnameVerifier();
private final static Pattern WILDCARD_PATTERN = Pattern.compile(
"^[a-z0-9\\-\\*]+(\\.[a-z0-9\\-]+){2,}$",
Pattern.CASE_INSENSITIVE);
/**
* This contains a list of 2nd-level domains that aren't allowed to
* have wildcards when combined with country-codes.
@ -75,14 +78,9 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
* Looks like we're the only implementation guarding against this.
* Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
*/
final static String[] BAD_COUNTRY_2LDS =
{ "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
"lg", "ne", "net", "or", "org" };
static {
// Just in case developer forgot to manually sort the array. :-)
Arrays.sort(BAD_COUNTRY_2LDS);
}
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 IP_ADDRESS_TYPE = 7;
@ -177,29 +175,37 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
if (host == null) {
return false;
}
final String normalizedHost = host.toLowerCase(Locale.ROOT);
final String normalizedIdentity = identity.toLowerCase(Locale.ROOT);
// The CN better have at least two dots if it wants wildcard
// action. It also can't be [*.co.uk] or [*.co.jp] or
// [*.org.uk], etc...
final String parts[] = normalizedIdentity.split("\\.");
final boolean doWildcard = parts.length >= 3 && parts[0].endsWith("*") &&
(!strict || validCountryWildcard(parts));
if (doWildcard) {
boolean match;
final String firstpart = parts[0];
if (firstpart.length() > 1) { // e.g. server*
final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server
final String suffix = normalizedIdentity.substring(firstpart.length()); // skip wildcard part from cn
final String hostSuffix = normalizedHost.substring(prefix.length()); // skip wildcard part from normalizedHost
match = normalizedHost.startsWith(prefix) && hostSuffix.endsWith(suffix);
} else {
match = normalizedHost.endsWith(normalizedIdentity.substring(1));
if (identity.contains("*") && WILDCARD_PATTERN.matcher(identity).matches()) {
if (!strict || !BAD_COUNTRY_WILDCARD_PATTERN.matcher(identity).matches()) {
final StringBuilder buf = new StringBuilder();
buf.append("^");
for (int i = 0; i < identity.length(); i++) {
final char ch = identity.charAt(i);
if (ch == '.') {
buf.append("\\.");
} else if (ch == '*') {
if (strict) {
buf.append("[a-z0-9\\-]*");
} else {
buf.append(".*");
}
} else {
buf.append(ch);
}
}
buf.append("$");
try {
final Pattern identityPattern = Pattern.compile(buf.toString(), Pattern.CASE_INSENSITIVE);
return identityPattern.matcher(host).matches();
} catch (PatternSyntaxException ignore) {
// do simple match
}
}
return match && (!strict || countDots(normalizedHost) == countDots(normalizedIdentity));
} else {
return normalizedHost.equals(normalizedIdentity);
}
return host.equalsIgnoreCase(identity);
}
static boolean matchIdentity(final String host, final String identity) {
@ -210,13 +216,6 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
return matchIdentity(host, identity, true);
}
static boolean validCountryWildcard(final String[] parts) {
if (parts.length != 3 || parts[2].length() != 2) {
return true; // it's not an attempt to wildcard a 2TLD within a country code
}
return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
}
static String extractCN(final String subjectPrincipal) throws SSLException {
if (subjectPrincipal == null) {
return null;
@ -268,21 +267,6 @@ public final class DefaultHostnameVerifier implements HostnameVerifier {
return subjectAltList;
}
/**
* Counts the number of dots "." in a string.
* @param s string to count dots from
* @return number of dots
*/
static int countDots(final String s) {
int count = 0;
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) == '.') {
count++;
}
}
return count;
}
/*
* Normalize IPv6 or DNS name.
*/

View File

@ -113,7 +113,7 @@ public class TestDefaultHostnameVerifier {
x509 = (X509Certificate) cf.generateCertificate(in);
exceptionPlease(impl, "foo.com", x509);
impl.verify("www.foo.com", x509);
impl.verify("\u82b1\u5b50.foo.com", x509);
exceptionPlease(impl, "\u82b1\u5b50.foo.com", x509);
exceptionPlease(impl, "a.b.foo.com", x509);
in = new ByteArrayInputStream(CertificatesToPlayWith.X509_WILD_CO_JP);
@ -134,7 +134,7 @@ public class TestDefaultHostnameVerifier {
// try the bar.com variations
exceptionPlease(impl, "bar.com", x509);
impl.verify("www.bar.com", x509);
impl.verify("\u82b1\u5b50.bar.com", x509);
exceptionPlease(impl, "\u82b1\u5b50.bar.com", x509);
exceptionPlease(impl, "a.b.bar.com", x509);
in = new ByteArrayInputStream(CertificatesToPlayWith.X509_MULTIPLE_VALUE_AVA);
@ -191,6 +191,13 @@ public class TestDefaultHostnameVerifier {
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.b.c"));
Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.b.c"));
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.B.c", "*.b.c"));
Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("a.B.c", "*.b.c"));
Assert.assertTrue(DefaultHostnameVerifier.matchIdentity("a.b.C", "*.B.c"));
Assert.assertTrue(DefaultHostnameVerifier.matchIdentityStrict("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
@ -211,6 +218,12 @@ public class TestDefaultHostnameVerifier {
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("s.a.gov.uk", "a*.gov.uk")); // Bad 2TLD
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("s.a.gov.uk", "a*.gov.uk")); // Bad 2TLD/no subdomain allowed
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.b.*"));
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.b.*"));
Assert.assertFalse(DefaultHostnameVerifier.matchIdentity("a.b.c", "*.*.c"));
Assert.assertFalse(DefaultHostnameVerifier.matchIdentityStrict("a.b.c", "*.*.c"));
}
@Test

View File

@ -139,7 +139,7 @@ public class TestHostnameVerifier {
DEFAULT.verify("www.foo.com", x509);
STRICT.verify("www.foo.com", x509);
DEFAULT.verify("\u82b1\u5b50.foo.com", x509);
STRICT.verify("\u82b1\u5b50.foo.com", x509);
exceptionPlease(STRICT, "\u82b1\u5b50.foo.com", x509);
DEFAULT.verify("a.b.foo.com", x509);
exceptionPlease(STRICT, "a.b.foo.com", x509);
@ -171,7 +171,7 @@ public class TestHostnameVerifier {
DEFAULT.verify("www.bar.com", x509);
STRICT.verify("www.bar.com", x509);
DEFAULT.verify("\u82b1\u5b50.bar.com", x509);
STRICT.verify("\u82b1\u5b50.bar.com", x509);
exceptionPlease(STRICT, "\u82b1\u5b50.bar.com", x509);
DEFAULT.verify("a.b.bar.com", x509);
exceptionPlease(STRICT, "a.b.bar.com", x509);