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:
parent
4f49584d6b
commit
268d6cc113
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue