Home | History | Annotate | Download | only in net
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.internal.net;
     17 
     18 
     19 import android.util.Config;
     20 import android.util.Log;
     21 
     22 import java.net.InetAddress;
     23 import java.net.UnknownHostException;
     24 import java.security.cert.CertificateParsingException;
     25 import java.security.cert.X509Certificate;
     26 import java.util.Collection;
     27 import java.util.Iterator;
     28 import java.util.List;
     29 import java.util.regex.Pattern;
     30 import java.util.regex.PatternSyntaxException;
     31 
     32 import javax.security.auth.x500.X500Principal;
     33 
     34 /** @hide */
     35 public class DomainNameValidator {
     36     private final static String TAG = "DomainNameValidator";
     37 
     38     private static final boolean DEBUG = false;
     39     private static final boolean LOG_ENABLED = DEBUG ? Config.LOGD : Config.LOGV;
     40 
     41     private static Pattern QUICK_IP_PATTERN;
     42     static {
     43         try {
     44             QUICK_IP_PATTERN = Pattern.compile("^[a-f0-9\\.:]+$");
     45         } catch (PatternSyntaxException e) {}
     46     }
     47 
     48     private static final int ALT_DNS_NAME = 2;
     49     private static final int ALT_IPA_NAME = 7;
     50 
     51     /**
     52      * Checks the site certificate against the domain name of the site being visited
     53      * @param certificate The certificate to check
     54      * @param thisDomain The domain name of the site being visited
     55      * @return True iff if there is a domain match as specified by RFC2818
     56      */
     57     public static boolean match(X509Certificate certificate, String thisDomain) {
     58         if (certificate == null || thisDomain == null || thisDomain.length() == 0) {
     59             return false;
     60         }
     61 
     62         thisDomain = thisDomain.toLowerCase();
     63         if (!isIpAddress(thisDomain)) {
     64             return matchDns(certificate, thisDomain);
     65         } else {
     66             return matchIpAddress(certificate, thisDomain);
     67         }
     68     }
     69 
     70     /**
     71      * @return True iff the domain name is specified as an IP address
     72      */
     73     private static boolean isIpAddress(String domain) {
     74         boolean rval = (domain != null && domain.length() != 0);
     75         if (rval) {
     76             try {
     77                 // do a quick-dirty IP match first to avoid DNS lookup
     78                 rval = QUICK_IP_PATTERN.matcher(domain).matches();
     79                 if (rval) {
     80                     rval = domain.equals(
     81                         InetAddress.getByName(domain).getHostAddress());
     82                 }
     83             } catch (UnknownHostException e) {
     84                 String errorMessage = e.getMessage();
     85                 if (errorMessage == null) {
     86                   errorMessage = "unknown host exception";
     87                 }
     88 
     89                 if (LOG_ENABLED) {
     90                     Log.v(TAG, "DomainNameValidator.isIpAddress(): " + errorMessage);
     91                 }
     92 
     93                 rval = false;
     94             }
     95         }
     96 
     97         return rval;
     98     }
     99 
    100     /**
    101      * Checks the site certificate against the IP domain name of the site being visited
    102      * @param certificate The certificate to check
    103      * @param thisDomain The DNS domain name of the site being visited
    104      * @return True iff if there is a domain match as specified by RFC2818
    105      */
    106     private static boolean matchIpAddress(X509Certificate certificate, String thisDomain) {
    107         if (LOG_ENABLED) {
    108             Log.v(TAG, "DomainNameValidator.matchIpAddress(): this domain: " + thisDomain);
    109         }
    110 
    111         try {
    112             Collection subjectAltNames = certificate.getSubjectAlternativeNames();
    113             if (subjectAltNames != null) {
    114                 Iterator i = subjectAltNames.iterator();
    115                 while (i.hasNext()) {
    116                     List altNameEntry = (List)(i.next());
    117                     if (altNameEntry != null && 2 <= altNameEntry.size()) {
    118                         Integer altNameType = (Integer)(altNameEntry.get(0));
    119                         if (altNameType != null) {
    120                             if (altNameType.intValue() == ALT_IPA_NAME) {
    121                                 String altName = (String)(altNameEntry.get(1));
    122                                 if (altName != null) {
    123                                     if (LOG_ENABLED) {
    124                                         Log.v(TAG, "alternative IP: " + altName);
    125                                     }
    126                                     if (thisDomain.equalsIgnoreCase(altName)) {
    127                                         return true;
    128                                     }
    129                                 }
    130                             }
    131                         }
    132                     }
    133                 }
    134             }
    135         } catch (CertificateParsingException e) {}
    136 
    137         return false;
    138     }
    139 
    140     /**
    141      * Checks the site certificate against the DNS domain name of the site being visited
    142      * @param certificate The certificate to check
    143      * @param thisDomain The DNS domain name of the site being visited
    144      * @return True iff if there is a domain match as specified by RFC2818
    145      */
    146     private static boolean matchDns(X509Certificate certificate, String thisDomain) {
    147         boolean hasDns = false;
    148         try {
    149             Collection subjectAltNames = certificate.getSubjectAlternativeNames();
    150             if (subjectAltNames != null) {
    151                 Iterator i = subjectAltNames.iterator();
    152                 while (i.hasNext()) {
    153                     List altNameEntry = (List)(i.next());
    154                     if (altNameEntry != null && 2 <= altNameEntry.size()) {
    155                         Integer altNameType = (Integer)(altNameEntry.get(0));
    156                         if (altNameType != null) {
    157                             if (altNameType.intValue() == ALT_DNS_NAME) {
    158                                 hasDns = true;
    159                                 String altName = (String)(altNameEntry.get(1));
    160                                 if (altName != null) {
    161                                     if (matchDns(thisDomain, altName)) {
    162                                         return true;
    163                                     }
    164                                 }
    165                             }
    166                         }
    167                     }
    168                 }
    169             }
    170         } catch (CertificateParsingException e) {
    171             String errorMessage = e.getMessage();
    172             if (errorMessage == null) {
    173                 errorMessage = "failed to parse certificate";
    174             }
    175 
    176             Log.w(TAG, "DomainNameValidator.matchDns(): " + errorMessage);
    177             return false;
    178         }
    179 
    180         if (!hasDns) {
    181             final String cn = new DNParser(certificate.getSubjectX500Principal())
    182                     .find("cn");
    183             if (LOG_ENABLED) {
    184                 Log.v(TAG, "Validating subject: DN:"
    185                         + certificate.getSubjectX500Principal().getName(X500Principal.CANONICAL)
    186                         + "  CN:" + cn);
    187             }
    188             if (cn != null) {
    189                 return matchDns(thisDomain, cn);
    190             }
    191         }
    192 
    193         return false;
    194     }
    195 
    196     /**
    197      * @param thisDomain The domain name of the site being visited
    198      * @param thatDomain The domain name from the certificate
    199      * @return True iff thisDomain matches thatDomain as specified by RFC2818
    200      */
    201     // not private for testing
    202     public static boolean matchDns(String thisDomain, String thatDomain) {
    203         if (LOG_ENABLED) {
    204             Log.v(TAG, "DomainNameValidator.matchDns():" +
    205                       " this domain: " + thisDomain +
    206                       " that domain: " + thatDomain);
    207         }
    208 
    209         if (thisDomain == null || thisDomain.length() == 0 ||
    210             thatDomain == null || thatDomain.length() == 0) {
    211             return false;
    212         }
    213 
    214         thatDomain = thatDomain.toLowerCase();
    215 
    216         // (a) domain name strings are equal, ignoring case: X matches X
    217         boolean rval = thisDomain.equals(thatDomain);
    218         if (!rval) {
    219             String[] thisDomainTokens = thisDomain.split("\\.");
    220             String[] thatDomainTokens = thatDomain.split("\\.");
    221 
    222             int thisDomainTokensNum = thisDomainTokens.length;
    223             int thatDomainTokensNum = thatDomainTokens.length;
    224 
    225             // (b) OR thatHost is a '.'-suffix of thisHost: Z.Y.X matches X
    226             if (thisDomainTokensNum >= thatDomainTokensNum) {
    227                 for (int i = thatDomainTokensNum - 1; i >= 0; --i) {
    228                     rval = thisDomainTokens[i].equals(thatDomainTokens[i]);
    229                     if (!rval) {
    230                         // (c) OR we have a special *-match:
    231                         // *.Y.X matches Z.Y.X but *.X doesn't match Z.Y.X
    232                         rval = (i == 0 && thisDomainTokensNum == thatDomainTokensNum);
    233                         if (rval) {
    234                             rval = thatDomainTokens[0].equals("*");
    235                             if (!rval) {
    236                                 // (d) OR we have a *-component match:
    237                                 // f*.com matches foo.com but not bar.com
    238                                 rval = domainTokenMatch(
    239                                     thisDomainTokens[0], thatDomainTokens[0]);
    240                             }
    241                         }
    242                         break;
    243                     }
    244                 }
    245             } else {
    246               // (e) OR thatHost has a '*.'-prefix of thisHost:
    247               // *.Y.X matches Y.X
    248               rval = thatDomain.equals("*." + thisDomain);
    249             }
    250         }
    251 
    252         return rval;
    253     }
    254 
    255     /**
    256      * @param thisDomainToken The domain token from the current domain name
    257      * @param thatDomainToken The domain token from the certificate
    258      * @return True iff thisDomainToken matches thatDomainToken, using the
    259      * wildcard match as specified by RFC2818-3.1. For example, f*.com must
    260      * match foo.com but not bar.com
    261      */
    262     private static boolean domainTokenMatch(String thisDomainToken, String thatDomainToken) {
    263         if (thisDomainToken != null && thatDomainToken != null) {
    264             int starIndex = thatDomainToken.indexOf('*');
    265             if (starIndex >= 0) {
    266                 if (thatDomainToken.length() - 1 <= thisDomainToken.length()) {
    267                     String prefix = thatDomainToken.substring(0,  starIndex);
    268                     String suffix = thatDomainToken.substring(starIndex + 1);
    269 
    270                     return thisDomainToken.startsWith(prefix) && thisDomainToken.endsWith(suffix);
    271                 }
    272             }
    273         }
    274 
    275         return false;
    276     }
    277 }
    278