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 */ 16package com.android.internal.net; 17 18 19import android.util.Config; 20import android.util.Log; 21 22import java.net.InetAddress; 23import java.net.UnknownHostException; 24import java.security.cert.CertificateParsingException; 25import java.security.cert.X509Certificate; 26import java.util.Collection; 27import java.util.Iterator; 28import java.util.List; 29import java.util.regex.Pattern; 30import java.util.regex.PatternSyntaxException; 31 32import javax.security.auth.x500.X500Principal; 33 34/** @hide */ 35public 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