blob: a4d1c15d1ca07d6a7ec3d284907edbbbe1c443a5 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2002-2006 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package sun.security.util;
27
28import java.io.IOException;
29import java.util.*;
30
31import java.security.Principal;
32import java.security.cert.*;
33
34import javax.security.auth.x500.X500Principal;
35import javax.security.auth.kerberos.KerberosPrincipal;
36
37import sun.security.x509.X500Name;
38import sun.security.krb5.PrincipalName;
39
40import sun.net.util.IPAddressUtil;
41
42/**
43 * Class to check hostnames against the names specified in a certificate as
44 * required for TLS and LDAP.
45 *
46 */
47public class HostnameChecker {
48
49 // Constant for a HostnameChecker for TLS
50 public final static byte TYPE_TLS = 1;
51 private final static HostnameChecker INSTANCE_TLS =
52 new HostnameChecker(TYPE_TLS);
53
54 // Constant for a HostnameChecker for LDAP
55 public final static byte TYPE_LDAP = 2;
56 private final static HostnameChecker INSTANCE_LDAP =
57 new HostnameChecker(TYPE_LDAP);
58
59 // constants for subject alt names of type DNS and IP
60 private final static int ALTNAME_DNS = 2;
61 private final static int ALTNAME_IP = 7;
62
63 // the algorithm to follow to perform the check. Currently unused.
64 private final byte checkType;
65
66 private HostnameChecker(byte checkType) {
67 this.checkType = checkType;
68 }
69
70 /**
71 * Get a HostnameChecker instance. checkType should be one of the
72 * TYPE_* constants defined in this class.
73 */
74 public static HostnameChecker getInstance(byte checkType) {
75 if (checkType == TYPE_TLS) {
76 return INSTANCE_TLS;
77 } else if (checkType == TYPE_LDAP) {
78 return INSTANCE_LDAP;
79 }
80 throw new IllegalArgumentException("Unknown check type: " + checkType);
81 }
82
83 /**
84 * Perform the check.
85 *
86 * @exception CertificateException if the name does not match any of
87 * the names specified in the certificate
88 */
89 public void match(String expectedName, X509Certificate cert)
90 throws CertificateException {
91 if (isIpAddress(expectedName)) {
92 matchIP(expectedName, cert);
93 } else {
94 matchDNS(expectedName, cert);
95 }
96 }
97
98 /**
99 * Perform the check for Kerberos.
100 */
101 public static boolean match(String expectedName,
102 KerberosPrincipal principal) {
103 String hostName = getServerName(principal);
104 return (expectedName.equalsIgnoreCase(hostName));
105 }
106
107 /**
108 * Return the Server name from Kerberos principal.
109 */
110 public static String getServerName(KerberosPrincipal principal) {
111 if (principal == null) {
112 return null;
113 }
114 String hostName = null;
115 try {
116 PrincipalName princName =
117 new PrincipalName(principal.getName(),
118 PrincipalName.KRB_NT_SRV_HST);
119 String[] nameParts = princName.getNameStrings();
120 if (nameParts.length >= 2) {
121 hostName = nameParts[1];
122 }
123 } catch (Exception e) {
124 // ignore
125 }
126 return hostName;
127 }
128
129 /**
130 * Test whether the given hostname looks like a literal IPv4 or IPv6
131 * address. The hostname does not need to be a fully qualified name.
132 *
133 * This is not a strict check that performs full input validation.
134 * That means if the method returns true, name need not be a correct
135 * IP address, rather that it does not represent a valid DNS hostname.
136 * Likewise for IP addresses when it returns false.
137 */
138 private static boolean isIpAddress(String name) {
139 if (IPAddressUtil.isIPv4LiteralAddress(name) ||
140 IPAddressUtil.isIPv6LiteralAddress(name)) {
141 return true;
142 } else {
143 return false;
144 }
145 }
146
147 /**
148 * Check if the certificate allows use of the given IP address.
149 *
150 * From RFC2818:
151 * In some cases, the URI is specified as an IP address rather than a
152 * hostname. In this case, the iPAddress subjectAltName must be present
153 * in the certificate and must exactly match the IP in the URI.
154 */
155 private static void matchIP(String expectedIP, X509Certificate cert)
156 throws CertificateException {
157 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames();
158 if (subjAltNames == null) {
159 throw new CertificateException
160 ("No subject alternative names present");
161 }
162 for (List<?> next : subjAltNames) {
163 // For IP address, it needs to be exact match
164 if (((Integer)next.get(0)).intValue() == ALTNAME_IP) {
165 String ipAddress = (String)next.get(1);
166 if (expectedIP.equalsIgnoreCase(ipAddress)) {
167 return;
168 }
169 }
170 }
171 throw new CertificateException("No subject alternative " +
172 "names matching " + "IP address " +
173 expectedIP + " found");
174 }
175
176 /**
177 * Check if the certificate allows use of the given DNS name.
178 *
179 * From RFC2818:
180 * If a subjectAltName extension of type dNSName is present, that MUST
181 * be used as the identity. Otherwise, the (most specific) Common Name
182 * field in the Subject field of the certificate MUST be used. Although
183 * the use of the Common Name is existing practice, it is deprecated and
184 * Certification Authorities are encouraged to use the dNSName instead.
185 *
186 * Matching is performed using the matching rules specified by
187 * [RFC2459]. If more than one identity of a given type is present in
188 * the certificate (e.g., more than one dNSName name, a match in any one
189 * of the set is considered acceptable.)
190 */
191 private void matchDNS(String expectedName, X509Certificate cert)
192 throws CertificateException {
193 Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames();
194 if (subjAltNames != null) {
195 boolean foundDNS = false;
196 for ( List<?> next : subjAltNames) {
197 if (((Integer)next.get(0)).intValue() == ALTNAME_DNS) {
198 foundDNS = true;
199 String dnsName = (String)next.get(1);
200 if (isMatched(expectedName, dnsName)) {
201 return;
202 }
203 }
204 }
205 if (foundDNS) {
206 // if certificate contains any subject alt names of type DNS
207 // but none match, reject
208 throw new CertificateException("No subject alternative DNS "
209 + "name matching " + expectedName + " found.");
210 }
211 }
212 X500Name subjectName = getSubjectX500Name(cert);
213 DerValue derValue = subjectName.findMostSpecificAttribute
214 (X500Name.commonName_oid);
215 if (derValue != null) {
216 try {
217 if (isMatched(expectedName, derValue.getAsString())) {
218 return;
219 }
220 } catch (IOException e) {
221 // ignore
222 }
223 }
224 String msg = "No name matching " + expectedName + " found";
225 throw new CertificateException(msg);
226 }
227
228
229 /**
230 * Return the subject of a certificate as X500Name, by reparsing if
231 * necessary. X500Name should only be used if access to name components
232 * is required, in other cases X500Principal is to be prefered.
233 *
234 * This method is currently used from within JSSE, do not remove.
235 */
236 public static X500Name getSubjectX500Name(X509Certificate cert)
237 throws CertificateParsingException {
238 try {
239 Principal subjectDN = cert.getSubjectDN();
240 if (subjectDN instanceof X500Name) {
241 return (X500Name)subjectDN;
242 } else {
243 X500Principal subjectX500 = cert.getSubjectX500Principal();
244 return new X500Name(subjectX500.getEncoded());
245 }
246 } catch (IOException e) {
247 throw(CertificateParsingException)
248 new CertificateParsingException().initCause(e);
249 }
250 }
251
252
253 /**
254 * Returns true if name matches against template.<p>
255 *
256 * The matching is performed as per RFC 2818 rules for TLS and
257 * RFC 2830 rules for LDAP.<p>
258 *
259 * The <code>name</code> parameter should represent a DNS name.
260 * The <code>template</code> parameter
261 * may contain the wildcard character *
262 */
263 private boolean isMatched(String name, String template) {
264 if (checkType == TYPE_TLS) {
265 return matchAllWildcards(name, template);
266 } else if (checkType == TYPE_LDAP) {
267 return matchLeftmostWildcard(name, template);
268 } else {
269 return false;
270 }
271 }
272
273
274 /**
275 * Returns true if name matches against template.<p>
276 *
277 * According to RFC 2818, section 3.1 -
278 * Names may contain the wildcard character * which is
279 * considered to match any single domain name component
280 * or component fragment.
281 * E.g., *.a.com matches foo.a.com but not
282 * bar.foo.a.com. f*.com matches foo.com but not bar.com.
283 */
284 private static boolean matchAllWildcards(String name,
285 String template) {
286 name = name.toLowerCase();
287 template = template.toLowerCase();
288 StringTokenizer nameSt = new StringTokenizer(name, ".");
289 StringTokenizer templateSt = new StringTokenizer(template, ".");
290
291 if (nameSt.countTokens() != templateSt.countTokens()) {
292 return false;
293 }
294
295 while (nameSt.hasMoreTokens()) {
296 if (!matchWildCards(nameSt.nextToken(),
297 templateSt.nextToken())) {
298 return false;
299 }
300 }
301 return true;
302 }
303
304
305 /**
306 * Returns true if name matches against template.<p>
307 *
308 * As per RFC 2830, section 3.6 -
309 * The "*" wildcard character is allowed. If present, it applies only
310 * to the left-most name component.
311 * E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not
312 * bar.com.
313 */
314 private static boolean matchLeftmostWildcard(String name,
315 String template) {
316 name = name.toLowerCase();
317 template = template.toLowerCase();
318
319 // Retreive leftmost component
320 int templateIdx = template.indexOf(".");
321 int nameIdx = name.indexOf(".");
322
323 if (templateIdx == -1)
324 templateIdx = template.length();
325 if (nameIdx == -1)
326 nameIdx = name.length();
327
328 if (matchWildCards(name.substring(0, nameIdx),
329 template.substring(0, templateIdx))) {
330
331 // match rest of the name
332 return template.substring(templateIdx).equals(
333 name.substring(nameIdx));
334 } else {
335 return false;
336 }
337 }
338
339
340 /**
341 * Returns true if the name matches against the template that may
342 * contain wildcard char * <p>
343 */
344 private static boolean matchWildCards(String name, String template) {
345
346 int wildcardIdx = template.indexOf("*");
347 if (wildcardIdx == -1)
348 return name.equals(template);
349
350 boolean isBeginning = true;
351 String beforeWildcard = "";
352 String afterWildcard = template;
353
354 while (wildcardIdx != -1) {
355
356 // match in sequence the non-wildcard chars in the template.
357 beforeWildcard = afterWildcard.substring(0, wildcardIdx);
358 afterWildcard = afterWildcard.substring(wildcardIdx + 1);
359
360 int beforeStartIdx = name.indexOf(beforeWildcard);
361 if ((beforeStartIdx == -1) ||
362 (isBeginning && beforeStartIdx != 0)) {
363 return false;
364 }
365 isBeginning = false;
366
367 // update the match scope
368 name = name.substring(beforeStartIdx + beforeWildcard.length());
369 wildcardIdx = afterWildcard.indexOf("*");
370 }
371 return name.endsWith(afterWildcard);
372 }
373}