diff options
author | Dianne Hackborn <hackbod@google.com> | 2010-02-24 19:54:22 -0800 |
---|---|---|
committer | Dianne Hackborn <hackbod@google.com> | 2010-02-25 11:39:33 -0800 |
commit | 2269d1572e5fcfb725ea55f5764d8c3280d69f6d (patch) | |
tree | 7ce0788be6d85972d42ee7135cdce82aa323fcc7 /core/java/com/android/internal/net/DomainNameValidator.java | |
parent | e46145f7c114b9ac6d19c6a7886e9239463f91e1 (diff) | |
download | frameworks_base-2269d1572e5fcfb725ea55f5764d8c3280d69f6d.zip frameworks_base-2269d1572e5fcfb725ea55f5764d8c3280d69f6d.tar.gz frameworks_base-2269d1572e5fcfb725ea55f5764d8c3280d69f6d.tar.bz2 |
Re-arrange android-common so framework no longer links with it.
This is the framework part, moving classes around so the framework
no longer needs to link to android-common. Makes some APIs public,
others that didn't need to be public are private in the framework,
some small things are copied.
Diffstat (limited to 'core/java/com/android/internal/net/DomainNameValidator.java')
-rw-r--r-- | core/java/com/android/internal/net/DomainNameValidator.java | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/core/java/com/android/internal/net/DomainNameValidator.java b/core/java/com/android/internal/net/DomainNameValidator.java new file mode 100644 index 0000000..dbd5019 --- /dev/null +++ b/core/java/com/android/internal/net/DomainNameValidator.java @@ -0,0 +1,277 @@ +/* + * 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.internal.net; + + +import android.util.Config; +import android.util.Log; + +import java.net.InetAddress; +import java.net.UnknownHostException; +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.Pattern; +import java.util.regex.PatternSyntaxException; + +import javax.security.auth.x500.X500Principal; + +/** @hide */ +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 { + QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$"); + } catch (PatternSyntaxException e) {} + } + + private static final int ALT_DNS_NAME = 2; + private static final int ALT_IPA_NAME = 7; + + /** + * Checks the site certificate against the domain name of the site being visited + * @param certificate The certificate to check + * @param thisDomain The domain name of the site being visited + * @return True iff if there is a domain match as specified by RFC2818 + */ + public static boolean match(X509Certificate certificate, String thisDomain) { + if (certificate == null || thisDomain == null || thisDomain.length() == 0) { + return false; + } + + thisDomain = thisDomain.toLowerCase(); + if (!isIpAddress(thisDomain)) { + return matchDns(certificate, thisDomain); + } else { + return matchIpAddress(certificate, thisDomain); + } + } + + /** + * @return True iff the domain name is specified as an IP address + */ + private static boolean isIpAddress(String domain) { + boolean rval = (domain != null && domain.length() != 0); + if (rval) { + try { + // do a quick-dirty IP match first to avoid DNS lookup + rval = QUICK_IP_PATTERN.matcher(domain).matches(); + if (rval) { + rval = domain.equals( + InetAddress.getByName(domain).getHostAddress()); + } + } catch (UnknownHostException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "unknown host exception"; + } + + if (LOG_ENABLED) { + Log.v(TAG, "DomainNameValidator.isIpAddress(): " + errorMessage); + } + + rval = false; + } + } + + return rval; + } + + /** + * Checks the site certificate against the IP domain name of the site being visited + * @param certificate The certificate to check + * @param thisDomain The DNS domain name of the site being visited + * @return True iff if there is a domain match as specified by RFC2818 + */ + private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) { + if (LOG_ENABLED) { + Log.v(TAG, "DomainNameValidator.matchIpAddress(): this domain: " + thisDomain); + } + + try { + Collection subjectAltNames = certificate.getSubjectAlternativeNames(); + if (subjectAltNames != null) { + Iterator i = subjectAltNames.iterator(); + while (i.hasNext()) { + List altNameEntry = (List)(i.next()); + if (altNameEntry != null && 2 <= altNameEntry.size()) { + Integer altNameType = (Integer)(altNameEntry.get(0)); + if (altNameType != null) { + if (altNameType.intValue() == ALT_IPA_NAME) { + String altName = (String)(altNameEntry.get(1)); + if (altName != null) { + if (LOG_ENABLED) { + Log.v(TAG, "alternative IP: " + altName); + } + if (thisDomain.equalsIgnoreCase(altName)) { + return true; + } + } + } + } + } + } + } + } catch (CertificateParsingException e) {} + + return false; + } + + /** + * Checks the site certificate against the DNS domain name of the site being visited + * @param certificate The certificate to check + * @param thisDomain The DNS domain name of the site being visited + * @return True iff if there is a domain match as specified by RFC2818 + */ + private static boolean matchDns(X509Certificate certificate, String thisDomain) { + boolean hasDns = false; + try { + Collection subjectAltNames = certificate.getSubjectAlternativeNames(); + if (subjectAltNames != null) { + Iterator i = subjectAltNames.iterator(); + while (i.hasNext()) { + List altNameEntry = (List)(i.next()); + if (altNameEntry != null && 2 <= altNameEntry.size()) { + Integer altNameType = (Integer)(altNameEntry.get(0)); + if (altNameType != null) { + if (altNameType.intValue() == ALT_DNS_NAME) { + hasDns = true; + String altName = (String)(altNameEntry.get(1)); + if (altName != null) { + if (matchDns(thisDomain, altName)) { + return true; + } + } + } + } + } + } + } + } catch (CertificateParsingException e) { + String errorMessage = e.getMessage(); + if (errorMessage == null) { + errorMessage = "failed to parse certificate"; + } + + Log.w(TAG, "DomainNameValidator.matchDns(): " + errorMessage); + return false; + } + + if (!hasDns) { + 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); + } + } + + return false; + } + + /** + * @param thisDomain The domain name of the site being visited + * @param thatDomain The domain name from the certificate + * @return True iff thisDomain matches thatDomain as specified by RFC2818 + */ + // 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); + } + + if (thisDomain == null || thisDomain.length() == 0 || + thatDomain == null || thatDomain.length() == 0) { + return false; + } + + thatDomain = thatDomain.toLowerCase(); + + // (a) domain name strings are equal, ignoring case: X matches X + boolean rval = thisDomain.equals(thatDomain); + if (!rval) { + String[] thisDomainTokens = thisDomain.split("\\."); + String[] thatDomainTokens = thatDomain.split("\\."); + + int thisDomainTokensNum = thisDomainTokens.length; + int thatDomainTokensNum = thatDomainTokens.length; + + // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X + if (thisDomainTokensNum >= thatDomainTokensNum) { + for (int i = thatDomainTokensNum - 1; i >= 0; --i) { + rval = thisDomainTokens[i].equals(thatDomainTokens[i]); + if (!rval) { + // (c) OR we have a special *-match: + // *.Y.X matches Z.Y.X but *.X doesn't match Z.Y.X + rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum); + if (rval) { + rval = thatDomainTokens[0].equals("*"); + if (!rval) { + // (d) OR we have a *-component match: + // f*.com matches foo.com but not bar.com + rval = domainTokenMatch( + thisDomainTokens[0], thatDomainTokens[0]); + } + } + break; + } + } + } else { + // (e) OR thatHost has a '*.'-prefix of thisHost: + // *.Y.X matches Y.X + rval = thatDomain.equals("*." + thisDomain); + } + } + + return rval; + } + + /** + * @param thisDomainToken The domain token from the current domain name + * @param thatDomainToken The domain token from the certificate + * @return True iff thisDomainToken matches thatDomainToken, using the + * wildcard match as specified by RFC2818-3.1. For example, f*.com must + * match foo.com but not bar.com + */ + private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) { + if (thisDomainToken != null && thatDomainToken != null) { + int starIndex = thatDomainToken.indexOf('*'); + if (starIndex >= 0) { + if (thatDomainToken.length() - 1 <= thisDomainToken.length()) { + String prefix = thatDomainToken.substring(0, starIndex); + String suffix = thatDomainToken.substring(starIndex + 1); + + return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix); + } + } + } + + return false; + } +} |