Moved DomainNameChecker to android common.
- Moved DomainNameChecker from android.net.http to android common, and renamed to DomainNameValidator. - Added a simplified version of DNParser, which DomainNameValidator uses instead of X509Name in order to extract Subject Name from a certificate. - Added unit tests for DomainNameChecker and DNParser. There's a suspicious comment in DomainNameChecker saying something like "X509Certificate fails to parse a certificate when a subject alt name begins with '*'". I think we should fix it if it's really the case -- otherwise certificates with the wildcard wouldn't work. I'll see if it's true after submitting this patch.
This commit is contained in:
447
common/java/com/android/common/DNParser.java
Normal file
447
common/java/com/android/common/DNParser.java
Normal file
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.common;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
* A simple distinguished name(DN) parser.
|
||||
*
|
||||
* <p>This class is based on org.apache.harmony.security.x509.DNParser. It's customized to remove
|
||||
* external references which are unnecessary for our requirements.
|
||||
*
|
||||
* <p>This class is only meant for extracting a string value from a DN. e.g. it doesn't support
|
||||
* values in the hex-string style.
|
||||
*
|
||||
* <p>This class is used by {@link DomainNameValidator} only. However, in order to make this
|
||||
* class visible from unit tests, it's made public.
|
||||
*/
|
||||
public final class DNParser {
|
||||
private static final String TAG = "DNParser";
|
||||
|
||||
/** DN to be parsed. */
|
||||
private final String dn;
|
||||
|
||||
// length of distinguished name string
|
||||
private final int length;
|
||||
|
||||
private int pos, beg, end;
|
||||
|
||||
// tmp vars to store positions of the currently parsed item
|
||||
private int cur;
|
||||
|
||||
// distinguished name chars
|
||||
private char[] chars;
|
||||
|
||||
/**
|
||||
* Exception message thrown when we failed to parse DN, which shouldn't happen because we
|
||||
* only handle DNs that {@link X500Principal#getName} returns, which shouldn't be malformed.
|
||||
*/
|
||||
private static final String ERROR_PARSE_ERROR = "Failed to parse DN";
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param principal - {@link X500Principal} to be parsed
|
||||
*/
|
||||
public DNParser(X500Principal principal) {
|
||||
this.dn = principal.getName(X500Principal.RFC2253);
|
||||
this.length = dn.length();
|
||||
}
|
||||
|
||||
// gets next attribute type: (ALPHA 1*keychar) / oid
|
||||
private String nextAT() throws IOException {
|
||||
|
||||
// skip preceding space chars, they can present after
|
||||
// comma or semicolon (compatibility with RFC 1779)
|
||||
for (; pos < length && chars[pos] == ' '; pos++) {
|
||||
}
|
||||
if (pos == length) {
|
||||
return null; // reached the end of DN
|
||||
}
|
||||
|
||||
// mark the beginning of attribute type
|
||||
beg = pos;
|
||||
|
||||
// attribute type chars
|
||||
pos++;
|
||||
for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) {
|
||||
// we don't follow exact BNF syntax here:
|
||||
// accept any char except space and '='
|
||||
}
|
||||
if (pos >= length) {
|
||||
// unexpected end of DN
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
// mark the end of attribute type
|
||||
end = pos;
|
||||
|
||||
// skip trailing space chars between attribute type and '='
|
||||
// (compatibility with RFC 1779)
|
||||
if (chars[pos] == ' ') {
|
||||
for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) {
|
||||
}
|
||||
|
||||
if (chars[pos] != '=' || pos == length) {
|
||||
// unexpected end of DN
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
pos++; //skip '=' char
|
||||
|
||||
// skip space chars between '=' and attribute value
|
||||
// (compatibility with RFC 1779)
|
||||
for (; pos < length && chars[pos] == ' '; pos++) {
|
||||
}
|
||||
|
||||
// in case of oid attribute type skip its prefix: "oid." or "OID."
|
||||
// (compatibility with RFC 1779)
|
||||
if ((end - beg > 4) && (chars[beg + 3] == '.')
|
||||
&& (chars[beg] == 'O' || chars[beg] == 'o')
|
||||
&& (chars[beg + 1] == 'I' || chars[beg + 1] == 'i')
|
||||
&& (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) {
|
||||
beg += 4;
|
||||
}
|
||||
|
||||
return new String(chars, beg, end - beg);
|
||||
}
|
||||
|
||||
// gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION
|
||||
private String quotedAV() throws IOException {
|
||||
|
||||
pos++;
|
||||
beg = pos;
|
||||
end = beg;
|
||||
while (true) {
|
||||
|
||||
if (pos == length) {
|
||||
// unexpected end of DN
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
if (chars[pos] == '"') {
|
||||
// enclosing quotation was found
|
||||
pos++;
|
||||
break;
|
||||
} else if (chars[pos] == '\\') {
|
||||
chars[end] = getEscaped();
|
||||
} else {
|
||||
// shift char: required for string with escaped chars
|
||||
chars[end] = chars[pos];
|
||||
}
|
||||
pos++;
|
||||
end++;
|
||||
}
|
||||
|
||||
// skip trailing space chars before comma or semicolon.
|
||||
// (compatibility with RFC 1779)
|
||||
for (; pos < length && chars[pos] == ' '; pos++) {
|
||||
}
|
||||
|
||||
return new String(chars, beg, end - beg);
|
||||
}
|
||||
|
||||
// gets hex string attribute value: "#" hexstring
|
||||
private String hexAV() throws IOException {
|
||||
|
||||
if (pos + 4 >= length) {
|
||||
// encoded byte array must be not less then 4 c
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
beg = pos; // store '#' position
|
||||
pos++;
|
||||
while (true) {
|
||||
|
||||
// check for end of attribute value
|
||||
// looks for space and component separators
|
||||
if (pos == length || chars[pos] == '+' || chars[pos] == ','
|
||||
|| chars[pos] == ';') {
|
||||
end = pos;
|
||||
break;
|
||||
}
|
||||
|
||||
if (chars[pos] == ' ') {
|
||||
end = pos;
|
||||
pos++;
|
||||
// skip trailing space chars before comma or semicolon.
|
||||
// (compatibility with RFC 1779)
|
||||
for (; pos < length && chars[pos] == ' '; pos++) {
|
||||
}
|
||||
break;
|
||||
} else if (chars[pos] >= 'A' && chars[pos] <= 'F') {
|
||||
chars[pos] += 32; //to low case
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
// verify length of hex string
|
||||
// encoded byte array must be not less then 4 and must be even number
|
||||
int hexLen = end - beg; // skip first '#' char
|
||||
if (hexLen < 5 || (hexLen & 1) == 0) {
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
// get byte encoding from string representation
|
||||
byte[] encoded = new byte[hexLen / 2];
|
||||
for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) {
|
||||
encoded[i] = (byte) getByte(p);
|
||||
}
|
||||
|
||||
return new String(chars, beg, hexLen);
|
||||
}
|
||||
|
||||
// gets string attribute value: *( stringchar / pair )
|
||||
private String escapedAV() throws IOException {
|
||||
|
||||
beg = pos;
|
||||
end = pos;
|
||||
while (true) {
|
||||
|
||||
if (pos >= length) {
|
||||
// the end of DN has been found
|
||||
return new String(chars, beg, end - beg);
|
||||
}
|
||||
|
||||
switch (chars[pos]) {
|
||||
case '+':
|
||||
case ',':
|
||||
case ';':
|
||||
// separator char has beed found
|
||||
return new String(chars, beg, end - beg);
|
||||
case '\\':
|
||||
// escaped char
|
||||
chars[end++] = getEscaped();
|
||||
pos++;
|
||||
break;
|
||||
case ' ':
|
||||
// need to figure out whether space defines
|
||||
// the end of attribute value or not
|
||||
cur = end;
|
||||
|
||||
pos++;
|
||||
chars[end++] = ' ';
|
||||
|
||||
for (; pos < length && chars[pos] == ' '; pos++) {
|
||||
chars[end++] = ' ';
|
||||
}
|
||||
if (pos == length || chars[pos] == ',' || chars[pos] == '+'
|
||||
|| chars[pos] == ';') {
|
||||
// separator char or the end of DN has beed found
|
||||
return new String(chars, beg, cur - beg);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
chars[end++] = chars[pos];
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns escaped char
|
||||
private char getEscaped() throws IOException {
|
||||
|
||||
pos++;
|
||||
if (pos == length) {
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
switch (chars[pos]) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case ',':
|
||||
case '=':
|
||||
case '+':
|
||||
case '<':
|
||||
case '>':
|
||||
case '#':
|
||||
case ';':
|
||||
case ' ':
|
||||
case '*':
|
||||
case '%':
|
||||
case '_':
|
||||
//FIXME: escaping is allowed only for leading or trailing space char
|
||||
return chars[pos];
|
||||
default:
|
||||
// RFC doesn't explicitly say that escaped hex pair is
|
||||
// interpreted as UTF-8 char. It only contains an example of such DN.
|
||||
return getUTF8();
|
||||
}
|
||||
}
|
||||
|
||||
// decodes UTF-8 char
|
||||
// see http://www.unicode.org for UTF-8 bit distribution table
|
||||
private char getUTF8() throws IOException {
|
||||
|
||||
int res = getByte(pos);
|
||||
pos++; //FIXME tmp
|
||||
|
||||
if (res < 128) { // one byte: 0-7F
|
||||
return (char) res;
|
||||
} else if (res >= 192 && res <= 247) {
|
||||
|
||||
int count;
|
||||
if (res <= 223) { // two bytes: C0-DF
|
||||
count = 1;
|
||||
res = res & 0x1F;
|
||||
} else if (res <= 239) { // three bytes: E0-EF
|
||||
count = 2;
|
||||
res = res & 0x0F;
|
||||
} else { // four bytes: F0-F7
|
||||
count = 3;
|
||||
res = res & 0x07;
|
||||
}
|
||||
|
||||
int b;
|
||||
for (int i = 0; i < count; i++) {
|
||||
pos++;
|
||||
if (pos == length || chars[pos] != '\\') {
|
||||
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
|
||||
}
|
||||
pos++;
|
||||
|
||||
b = getByte(pos);
|
||||
pos++; //FIXME tmp
|
||||
if ((b & 0xC0) != 0x80) {
|
||||
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
|
||||
}
|
||||
|
||||
res = (res << 6) + (b & 0x3F);
|
||||
}
|
||||
return (char) res;
|
||||
} else {
|
||||
return 0x3F; //FIXME failed to decode UTF-8 char - return '?'
|
||||
}
|
||||
}
|
||||
|
||||
// Returns byte representation of a char pair
|
||||
// The char pair is composed of DN char in
|
||||
// specified 'position' and the next char
|
||||
// According to BNF syntax:
|
||||
// hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
|
||||
// / "a" / "b" / "c" / "d" / "e" / "f"
|
||||
private int getByte(int position) throws IOException {
|
||||
|
||||
if ((position + 1) >= length) {
|
||||
// to avoid ArrayIndexOutOfBoundsException
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
int b1, b2;
|
||||
|
||||
b1 = chars[position];
|
||||
if (b1 >= '0' && b1 <= '9') {
|
||||
b1 = b1 - '0';
|
||||
} else if (b1 >= 'a' && b1 <= 'f') {
|
||||
b1 = b1 - 87; // 87 = 'a' - 10
|
||||
} else if (b1 >= 'A' && b1 <= 'F') {
|
||||
b1 = b1 - 55; // 55 = 'A' - 10
|
||||
} else {
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
b2 = chars[position + 1];
|
||||
if (b2 >= '0' && b2 <= '9') {
|
||||
b2 = b2 - '0';
|
||||
} else if (b2 >= 'a' && b2 <= 'f') {
|
||||
b2 = b2 - 87; // 87 = 'a' - 10
|
||||
} else if (b2 >= 'A' && b2 <= 'F') {
|
||||
b2 = b2 - 55; // 55 = 'A' - 10
|
||||
} else {
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
return (b1 << 4) + b2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the DN and returns the attribute value for an attribute type.
|
||||
*
|
||||
* @param attributeType attribute type to look for (e.g. "ca")
|
||||
* @return value of the attribute that first found, or null if none found
|
||||
*/
|
||||
public String find(String attributeType) {
|
||||
try {
|
||||
// Initialize internal state.
|
||||
pos = 0;
|
||||
beg = 0;
|
||||
end = 0;
|
||||
cur = 0;
|
||||
chars = dn.toCharArray();
|
||||
|
||||
String attType = nextAT();
|
||||
if (attType == null) {
|
||||
return null;
|
||||
}
|
||||
while (true) {
|
||||
String attValue = "";
|
||||
|
||||
if (pos == length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (chars[pos]) {
|
||||
case '"':
|
||||
attValue = quotedAV();
|
||||
break;
|
||||
case '#':
|
||||
attValue = hexAV();
|
||||
break;
|
||||
case '+':
|
||||
case ',':
|
||||
case ';': // compatibility with RFC 1779: semicolon can separate RDNs
|
||||
//empty attribute value
|
||||
break;
|
||||
default:
|
||||
attValue = escapedAV();
|
||||
}
|
||||
|
||||
if (attributeType.equalsIgnoreCase(attType)) {
|
||||
return attValue;
|
||||
}
|
||||
|
||||
if (pos >= length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (chars[pos] == ',' || chars[pos] == ';') {
|
||||
} else if (chars[pos] != '+') {
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
|
||||
pos++;
|
||||
attType = nextAT();
|
||||
if (attType == null) {
|
||||
throw new IOException(ERROR_PARSE_ERROR);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Parse error shouldn't happen, because we only handle DNs that
|
||||
// X500Principal.getName() returns, which shouldn't be malformed.
|
||||
Log.e(TAG, "Failed to parse DN: " + dn);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -13,29 +13,29 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.common;
|
||||
|
||||
package android.net.http;
|
||||
|
||||
import org.bouncycastle.asn1.x509.X509Name;
|
||||
import android.util.Config;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* Implements basic domain-name validation as specified by RFC2818.
|
||||
*
|
||||
* {@hide}
|
||||
*/
|
||||
public class DomainNameChecker {
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
public class DomainNameValidator {
|
||||
private final static String TAG = "DomainNameValidator";
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
private static final boolean LOG_ENABLED = DEBUG ? Config.LOGD : Config.LOGV;
|
||||
|
||||
private static Pattern QUICK_IP_PATTERN;
|
||||
static {
|
||||
try {
|
||||
@@ -84,8 +84,8 @@ public class DomainNameChecker {
|
||||
errorMessage = "unknown host exception";
|
||||
}
|
||||
|
||||
if (HttpLog.LOGV) {
|
||||
HttpLog.v("DomainNameChecker.isIpAddress(): " + errorMessage);
|
||||
if (LOG_ENABLED) {
|
||||
Log.v(TAG, "DomainNameValidator.isIpAddress(): " + errorMessage);
|
||||
}
|
||||
|
||||
rval = false;
|
||||
@@ -102,8 +102,8 @@ public class DomainNameChecker {
|
||||
* @return True iff if there is a domain match as specified by RFC2818
|
||||
*/
|
||||
private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) {
|
||||
if (HttpLog.LOGV) {
|
||||
HttpLog.v("DomainNameChecker.matchIpAddress(): this domain: " + thisDomain);
|
||||
if (LOG_ENABLED) {
|
||||
Log.v(TAG, "DomainNameValidator.matchIpAddress(): this domain: " + thisDomain);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -118,8 +118,8 @@ public class DomainNameChecker {
|
||||
if (altNameType.intValue() == ALT_IPA_NAME) {
|
||||
String altName = (String)(altNameEntry.get(1));
|
||||
if (altName != null) {
|
||||
if (HttpLog.LOGV) {
|
||||
HttpLog.v("alternative IP: " + altName);
|
||||
if (LOG_ENABLED) {
|
||||
Log.v(TAG, "alternative IP: " + altName);
|
||||
}
|
||||
if (thisDomain.equalsIgnoreCase(altName)) {
|
||||
return true;
|
||||
@@ -171,26 +171,26 @@ public class DomainNameChecker {
|
||||
// spec (a valid DNS name must start with a letter); there is no
|
||||
// good way around this, and in order to be compatible we proceed
|
||||
// to check the common name (ie, ignore alternative names)
|
||||
if (HttpLog.LOGV) {
|
||||
if (LOG_ENABLED) {
|
||||
String errorMessage = e.getMessage();
|
||||
if (errorMessage == null) {
|
||||
errorMessage = "failed to parse certificate";
|
||||
}
|
||||
|
||||
if (HttpLog.LOGV) {
|
||||
HttpLog.v("DomainNameChecker.matchDns(): " + errorMessage);
|
||||
}
|
||||
Log.v(TAG, "DomainNameValidator.matchDns(): " + errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDns) {
|
||||
X509Name xName = new X509Name(certificate.getSubjectDN().getName());
|
||||
Vector val = xName.getValues();
|
||||
Vector oid = xName.getOIDs();
|
||||
for (int i = 0; i < oid.size(); i++) {
|
||||
if (oid.elementAt(i).equals(X509Name.CN)) {
|
||||
return matchDns(thisDomain, (String)(val.elementAt(i)));
|
||||
}
|
||||
final String cn = new DNParser(certificate.getSubjectX500Principal())
|
||||
.find("cn");
|
||||
if (LOG_ENABLED) {
|
||||
Log.v(TAG, "Validating subject: DN:"
|
||||
+ certificate.getSubjectX500Principal().getName(X500Principal.CANONICAL)
|
||||
+ " CN:" + cn);
|
||||
}
|
||||
if (cn != null) {
|
||||
return matchDns(thisDomain, cn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,9 +202,10 @@ public class DomainNameChecker {
|
||||
* @param thatDomain The domain name from the certificate
|
||||
* @return True iff thisDomain matches thatDomain as specified by RFC2818
|
||||
*/
|
||||
private static boolean matchDns(String thisDomain, String thatDomain) {
|
||||
if (HttpLog.LOGV) {
|
||||
HttpLog.v("DomainNameChecker.matchDns():" +
|
||||
// not private for testing
|
||||
public static boolean matchDns(String thisDomain, String thatDomain) {
|
||||
if (LOG_ENABLED) {
|
||||
Log.v(TAG, "DomainNameValidator.matchDns():" +
|
||||
" this domain: " + thisDomain +
|
||||
" that domain: " + thatDomain);
|
||||
}
|
||||
49
common/tests/src/com/android/common/DNParserTest.java
Normal file
49
common/tests/src/com/android/common/DNParserTest.java
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.common;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class DNParserTest extends TestCase {
|
||||
public void testFind() {
|
||||
checkFind("", "cn", null);
|
||||
checkFind("ou=xxx", "cn", null);
|
||||
checkFind("ou=xxx,cn=xxx", "cn", "xxx");
|
||||
checkFind("ou=xxx+cn=yyy,cn=zzz+cn=abc", "cn", "yyy");
|
||||
checkFind("2.5.4.3=a,ou=xxx", "cn", "a"); // OID
|
||||
checkFind("cn=a,cn=b", "cn", "a");
|
||||
checkFind("ou=Cc,ou=Bb,ou=Aa", "ou", "Cc");
|
||||
checkFind("cn=imap.gmail.com", "cn", "imap.gmail.com");
|
||||
|
||||
// Quoted string (see http://www.ietf.org/rfc/rfc2253.txt)
|
||||
checkFind("o=\"\\\" a ,=<>#;\"", "o", "\" a ,=<>#;");
|
||||
checkFind("o=abc\\,def", "o", "abc,def");
|
||||
|
||||
// UTF-8 (example in rfc 2253)
|
||||
checkFind("cn=Lu\\C4\\8Di\\C4\\87", "cn", "\u004c\u0075\u010d\u0069\u0107");
|
||||
|
||||
// whitespaces
|
||||
checkFind("ou=a, o= a b ,cn=x", "o", "a b");
|
||||
checkFind("o=\" a b \" ,cn=x", "o", " a b ");
|
||||
}
|
||||
|
||||
private void checkFind(String dn, String attrType, String expected) {
|
||||
String actual = new DNParser(new X500Principal(dn)).find(attrType);
|
||||
assertEquals("dn:" + dn + " attr:" + attrType, expected, actual);
|
||||
}
|
||||
}
|
||||
336
common/tests/src/com/android/common/DomainNameValidatorTest.java
Normal file
336
common/tests/src/com/android/common/DomainNameValidatorTest.java
Normal file
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.common;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.Principal;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.CertificateParsingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class DomainNameValidatorTest extends TestCase {
|
||||
private static final int ALT_UNKNOWN = 0;
|
||||
private static final int ALT_DNS_NAME = 2;
|
||||
private static final int ALT_IPA_NAME = 7;
|
||||
|
||||
/**
|
||||
* Tests {@link DomainNameValidator#match}
|
||||
*/
|
||||
public void testMatch() {
|
||||
// TODO Use actual X509Certificate objects, instead of StubX509Certificate.
|
||||
// Comment in DomainNameValidator suggests X509Certificate fails to parse a certificate
|
||||
// if subject alternative names contain a domain name that begins with '*'.
|
||||
// This test won't cover this kind of errors.
|
||||
|
||||
checkMatch("11", new StubX509Certificate("cn=imap.g.com"), "imap.g.com", true);
|
||||
checkMatch("12", new StubX509Certificate("cn=imap2.g.com"), "imap.g.com", false);
|
||||
checkMatch("13", new StubX509Certificate("cn=sub.imap.g.com"), "imap.g.com", false);
|
||||
|
||||
// If a subjectAltName extension of type dNSName is present, that MUST
|
||||
// be used as the identity
|
||||
checkMatch("21", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "a.y.com")
|
||||
, "imap.g.com", false);
|
||||
checkMatch("22", new StubX509Certificate("cn=imap.g.com") // This cn should be ignored
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "a.y.com")
|
||||
, "imap.g.com", false);
|
||||
checkMatch("23", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "imap.g.com")
|
||||
, "imap.g.com", true);
|
||||
|
||||
// With wildcards
|
||||
checkMatch("24", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "*.g.com")
|
||||
, "imap.g.com", true);
|
||||
|
||||
|
||||
// host name is ip address
|
||||
checkMatch("31", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "1.2.3.4")
|
||||
, "1.2.3.4", true);
|
||||
checkMatch("32", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "1.2.3.4")
|
||||
, "1.2.3.5", false);
|
||||
checkMatch("32", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "1.2.3.4")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "192.168.100.1")
|
||||
, "192.168.100.1", true);
|
||||
|
||||
// Has unknown subject alternative names
|
||||
checkMatch("41", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 1")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 2")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "a.b.c.d")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "*.google.com")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "imap.g.com")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "2.33.44.55")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 3")
|
||||
, "imap.g.com", true);
|
||||
|
||||
checkMatch("42", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 1")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 2")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "a.b.c.d")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "*.google.com")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "imap.g.com")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "2.33.44.55")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 3")
|
||||
, "2.33.44.55", true);
|
||||
|
||||
checkMatch("43", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 1")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 2")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "a.b.c.d")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "*.google.com")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "imap.g.com")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "2.33.44.55")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 3")
|
||||
, "g.com", false);
|
||||
|
||||
checkMatch("44", new StubX509Certificate("")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 1")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 2")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "a.b.c.d")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "*.google.com")
|
||||
.addSubjectAlternativeName(ALT_DNS_NAME, "imap.g.com")
|
||||
.addSubjectAlternativeName(ALT_IPA_NAME, "2.33.44.55")
|
||||
.addSubjectAlternativeName(ALT_UNKNOWN, "random string 3")
|
||||
, "2.33.44.1", false);
|
||||
}
|
||||
|
||||
private void checkMatch(String message, X509Certificate certificate, String thisDomain,
|
||||
boolean expected) {
|
||||
Boolean actual = DomainNameValidator.match(certificate, thisDomain);
|
||||
assertEquals(message, (Object) expected, (Object) actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests {@link DomainNameValidator#matchDns}
|
||||
*/
|
||||
public void testMatchDns() {
|
||||
checkMatchDns("11", "a.b.c.d", "a.b.c.d", true);
|
||||
checkMatchDns("12", "a.b.c.d", "*.b.c.d", true);
|
||||
checkMatchDns("13", "b.c.d", "*.b.c.d", true);
|
||||
checkMatchDns("14", "b.c.d", "b*.c.d", true);
|
||||
|
||||
checkMatchDns("15", "a.b.c.d", "*.*.c.d", false);
|
||||
checkMatchDns("16", "a.b.c.d", "*.c.d", false);
|
||||
|
||||
checkMatchDns("21", "imap.google.com", "imap.google.com", true);
|
||||
checkMatchDns("22", "imap2.google.com", "imap.google.com", false);
|
||||
checkMatchDns("23", "imap.google.com", "*.google.com", true);
|
||||
checkMatchDns("24", "imap2.google.com", "*.google.com", true);
|
||||
checkMatchDns("25", "imap.google.com", "*.googl.com", false);
|
||||
checkMatchDns("26", "imap2.google2.com", "*.google3.com", false);
|
||||
checkMatchDns("27", "imap.google.com", "ima*.google.com", true);
|
||||
checkMatchDns("28", "imap.google.com", "imap*.google.com", true);
|
||||
checkMatchDns("29", "imap.google.com", "*.imap.google.com", true);
|
||||
|
||||
checkMatchDns("41", "imap.google.com", "a*.google.com", false);
|
||||
checkMatchDns("42", "imap.google.com", "ix*.google.com", false);
|
||||
|
||||
checkMatchDns("51", "imap.google.com", "iMap.Google.Com", true);
|
||||
}
|
||||
|
||||
private void checkMatchDns(String message, String thisDomain, String thatDomain,
|
||||
boolean expected) {
|
||||
boolean actual = DomainNameValidator.matchDns(thisDomain, thatDomain);
|
||||
assertEquals(message, expected, actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal {@link X509Certificate} implementation for {@link DomainNameValidator}.
|
||||
*/
|
||||
private static class StubX509Certificate extends X509Certificate {
|
||||
private final X500Principal subjectX500Principal;
|
||||
private Collection<List<?>> subjectAlternativeNames;
|
||||
|
||||
public StubX509Certificate(String subjectDn) {
|
||||
subjectX500Principal = new X500Principal(subjectDn);
|
||||
subjectAlternativeNames = null;
|
||||
}
|
||||
|
||||
public StubX509Certificate addSubjectAlternativeName(int type, String name) {
|
||||
if (subjectAlternativeNames == null) {
|
||||
subjectAlternativeNames = new ArrayList<List<?>>();
|
||||
}
|
||||
LinkedList<Object> entry = new LinkedList<Object>();
|
||||
entry.add(type);
|
||||
entry.add(name);
|
||||
subjectAlternativeNames.add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<List<?>> getSubjectAlternativeNames() throws CertificateParsingException {
|
||||
return subjectAlternativeNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public X500Principal getSubjectX500Principal() {
|
||||
return subjectX500Principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValidity() throws CertificateExpiredException,
|
||||
CertificateNotYetValidException {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkValidity(Date date) throws CertificateExpiredException,
|
||||
CertificateNotYetValidException {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBasicConstraints() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getIssuerDN() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getIssuerUniqueID() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getKeyUsage() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getNotAfter() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getNotBefore() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BigInteger getSerialNumber() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSigAlgName() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSigAlgOID() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSigAlgParams() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSignature() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Principal getSubjectDN() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean[] getSubjectUniqueID() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getTBSCertificate() throws CertificateEncodingException {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getEncoded() throws CertificateEncodingException {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getPublicKey() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException,
|
||||
InvalidKeyException, NoSuchProviderException, SignatureException {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(PublicKey key, String sigProvider) throws CertificateException,
|
||||
NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException,
|
||||
SignatureException {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
public Set<String> getCriticalExtensionOIDs() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
public byte[] getExtensionValue(String oid) {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
public Set<String> getNonCriticalExtensionOIDs() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
|
||||
public boolean hasUnsupportedCriticalExtension() {
|
||||
throw new RuntimeException("Method not implemented");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,12 @@
|
||||
|
||||
package android.net;
|
||||
|
||||
import android.net.http.DomainNameChecker;
|
||||
import android.os.SystemProperties;
|
||||
import android.util.Config;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.common.DomainNameValidator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
@@ -200,7 +201,7 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory {
|
||||
|
||||
X509Certificate lastChainCert = (X509Certificate) certs[0];
|
||||
|
||||
if (!DomainNameChecker.match(lastChainCert, destHost)) {
|
||||
if (!DomainNameValidator.match(lastChainCert, destHost)) {
|
||||
if (Config.LOGD) {
|
||||
Log.d(LOG_TAG,"validateSocket(): domain name check failed");
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package android.net.http;
|
||||
|
||||
import com.android.common.DomainNameValidator;
|
||||
|
||||
import org.apache.harmony.xnet.provider.jsse.SSLParameters;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -112,7 +114,7 @@ class CertificateChainValidator {
|
||||
closeSocketThrowException(
|
||||
sslSocket, "certificate for this site is null");
|
||||
} else {
|
||||
if (!DomainNameChecker.match(currCertificate, domain)) {
|
||||
if (!DomainNameValidator.match(currCertificate, domain)) {
|
||||
String errorMessage = "certificate not for this host: " + domain;
|
||||
|
||||
if (HttpLog.LOGV) {
|
||||
|
||||
@@ -225,7 +225,6 @@ android.net.Uri$PathPart
|
||||
android.net.Uri$PathSegments
|
||||
android.net.Uri$StringUri
|
||||
android.net.WebAddress
|
||||
android.net.http.DomainNameChecker
|
||||
android.net.http.CertificateChainValidator
|
||||
android.net.http.EventHandler
|
||||
android.net.http.HttpsConnection
|
||||
|
||||
Reference in New Issue
Block a user