001: /*
002: * Copyright 2002-2006 Sun Microsystems, Inc. All Rights Reserved.
003: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004: *
005: * This code is free software; you can redistribute it and/or modify it
006: * under the terms of the GNU General Public License version 2 only, as
007: * published by the Free Software Foundation. Sun designates this
008: * particular file as subject to the "Classpath" exception as provided
009: * by Sun in the LICENSE file that accompanied this code.
010: *
011: * This code is distributed in the hope that it will be useful, but WITHOUT
012: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014: * version 2 for more details (a copy is included in the LICENSE file that
015: * accompanied this code).
016: *
017: * You should have received a copy of the GNU General Public License version
018: * 2 along with this work; if not, write to the Free Software Foundation,
019: * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020: *
021: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022: * CA 95054 USA or visit www.sun.com if you need additional information or
023: * have any questions.
024: */
025:
026: package sun.security.util;
027:
028: import java.io.IOException;
029: import java.util.*;
030:
031: import java.security.Principal;
032: import java.security.cert.*;
033:
034: import javax.security.auth.x500.X500Principal;
035: import javax.security.auth.kerberos.KerberosPrincipal;
036:
037: import sun.security.x509.X500Name;
038: import sun.security.krb5.PrincipalName;
039:
040: import sun.net.util.IPAddressUtil;
041:
042: /**
043: * Class to check hostnames against the names specified in a certificate as
044: * required for TLS and LDAP.
045: *
046: * @version 1.14, 05/05/07
047: */
048: public class HostnameChecker {
049:
050: // Constant for a HostnameChecker for TLS
051: public final static byte TYPE_TLS = 1;
052: private final static HostnameChecker INSTANCE_TLS = new HostnameChecker(
053: TYPE_TLS);
054:
055: // Constant for a HostnameChecker for LDAP
056: public final static byte TYPE_LDAP = 2;
057: private final static HostnameChecker INSTANCE_LDAP = new HostnameChecker(
058: TYPE_LDAP);
059:
060: // constants for subject alt names of type DNS and IP
061: private final static int ALTNAME_DNS = 2;
062: private final static int ALTNAME_IP = 7;
063:
064: // the algorithm to follow to perform the check. Currently unused.
065: private final byte checkType;
066:
067: private HostnameChecker(byte checkType) {
068: this .checkType = checkType;
069: }
070:
071: /**
072: * Get a HostnameChecker instance. checkType should be one of the
073: * TYPE_* constants defined in this class.
074: */
075: public static HostnameChecker getInstance(byte checkType) {
076: if (checkType == TYPE_TLS) {
077: return INSTANCE_TLS;
078: } else if (checkType == TYPE_LDAP) {
079: return INSTANCE_LDAP;
080: }
081: throw new IllegalArgumentException("Unknown check type: "
082: + checkType);
083: }
084:
085: /**
086: * Perform the check.
087: *
088: * @exception CertificateException if the name does not match any of
089: * the names specified in the certificate
090: */
091: public void match(String expectedName, X509Certificate cert)
092: throws CertificateException {
093: if (isIpAddress(expectedName)) {
094: matchIP(expectedName, cert);
095: } else {
096: matchDNS(expectedName, cert);
097: }
098: }
099:
100: /**
101: * Perform the check for Kerberos.
102: */
103: public static boolean match(String expectedName,
104: KerberosPrincipal principal) {
105: String hostName = getServerName(principal);
106: return (expectedName.equalsIgnoreCase(hostName));
107: }
108:
109: /**
110: * Return the Server name from Kerberos principal.
111: */
112: public static String getServerName(KerberosPrincipal principal) {
113: if (principal == null) {
114: return null;
115: }
116: String hostName = null;
117: try {
118: PrincipalName princName = new PrincipalName(principal
119: .getName(), PrincipalName.KRB_NT_SRV_HST);
120: String[] nameParts = princName.getNameStrings();
121: if (nameParts.length >= 2) {
122: hostName = nameParts[1];
123: }
124: } catch (Exception e) {
125: // ignore
126: }
127: return hostName;
128: }
129:
130: /**
131: * Test whether the given hostname looks like a literal IPv4 or IPv6
132: * address. The hostname does not need to be a fully qualified name.
133: *
134: * This is not a strict check that performs full input validation.
135: * That means if the method returns true, name need not be a correct
136: * IP address, rather that it does not represent a valid DNS hostname.
137: * Likewise for IP addresses when it returns false.
138: */
139: private static boolean isIpAddress(String name) {
140: if (IPAddressUtil.isIPv4LiteralAddress(name)
141: || IPAddressUtil.isIPv6LiteralAddress(name)) {
142: return true;
143: } else {
144: return false;
145: }
146: }
147:
148: /**
149: * Check if the certificate allows use of the given IP address.
150: *
151: * From RFC2818:
152: * In some cases, the URI is specified as an IP address rather than a
153: * hostname. In this case, the iPAddress subjectAltName must be present
154: * in the certificate and must exactly match the IP in the URI.
155: */
156: private static void matchIP(String expectedIP, X509Certificate cert)
157: throws CertificateException {
158: Collection<List<?>> subjAltNames = cert
159: .getSubjectAlternativeNames();
160: if (subjAltNames == null) {
161: throw new CertificateException(
162: "No subject alternative names present");
163: }
164: for (List<?> next : subjAltNames) {
165: // For IP address, it needs to be exact match
166: if (((Integer) next.get(0)).intValue() == ALTNAME_IP) {
167: String ipAddress = (String) next.get(1);
168: if (expectedIP.equalsIgnoreCase(ipAddress)) {
169: return;
170: }
171: }
172: }
173: throw new CertificateException("No subject alternative "
174: + "names matching " + "IP address " + expectedIP
175: + " found");
176: }
177:
178: /**
179: * Check if the certificate allows use of the given DNS name.
180: *
181: * From RFC2818:
182: * If a subjectAltName extension of type dNSName is present, that MUST
183: * be used as the identity. Otherwise, the (most specific) Common Name
184: * field in the Subject field of the certificate MUST be used. Although
185: * the use of the Common Name is existing practice, it is deprecated and
186: * Certification Authorities are encouraged to use the dNSName instead.
187: *
188: * Matching is performed using the matching rules specified by
189: * [RFC2459]. If more than one identity of a given type is present in
190: * the certificate (e.g., more than one dNSName name, a match in any one
191: * of the set is considered acceptable.)
192: */
193: private void matchDNS(String expectedName, X509Certificate cert)
194: throws CertificateException {
195: Collection<List<?>> subjAltNames = cert
196: .getSubjectAlternativeNames();
197: if (subjAltNames != null) {
198: boolean foundDNS = false;
199: for (List<?> next : subjAltNames) {
200: if (((Integer) next.get(0)).intValue() == ALTNAME_DNS) {
201: foundDNS = true;
202: String dnsName = (String) next.get(1);
203: if (isMatched(expectedName, dnsName)) {
204: return;
205: }
206: }
207: }
208: if (foundDNS) {
209: // if certificate contains any subject alt names of type DNS
210: // but none match, reject
211: throw new CertificateException(
212: "No subject alternative DNS "
213: + "name matching " + expectedName
214: + " found.");
215: }
216: }
217: X500Name subjectName = getSubjectX500Name(cert);
218: DerValue derValue = subjectName
219: .findMostSpecificAttribute(X500Name.commonName_oid);
220: if (derValue != null) {
221: try {
222: if (isMatched(expectedName, derValue.getAsString())) {
223: return;
224: }
225: } catch (IOException e) {
226: // ignore
227: }
228: }
229: String msg = "No name matching " + expectedName + " found";
230: throw new CertificateException(msg);
231: }
232:
233: /**
234: * Return the subject of a certificate as X500Name, by reparsing if
235: * necessary. X500Name should only be used if access to name components
236: * is required, in other cases X500Principal is to be prefered.
237: *
238: * This method is currently used from within JSSE, do not remove.
239: */
240: public static X500Name getSubjectX500Name(X509Certificate cert)
241: throws CertificateParsingException {
242: try {
243: Principal subjectDN = cert.getSubjectDN();
244: if (subjectDN instanceof X500Name) {
245: return (X500Name) subjectDN;
246: } else {
247: X500Principal subjectX500 = cert
248: .getSubjectX500Principal();
249: return new X500Name(subjectX500.getEncoded());
250: }
251: } catch (IOException e) {
252: throw (CertificateParsingException) new CertificateParsingException()
253: .initCause(e);
254: }
255: }
256:
257: /**
258: * Returns true if name matches against template.<p>
259: *
260: * The matching is performed as per RFC 2818 rules for TLS and
261: * RFC 2830 rules for LDAP.<p>
262: *
263: * The <code>name</code> parameter should represent a DNS name.
264: * The <code>template</code> parameter
265: * may contain the wildcard character *
266: */
267: private boolean isMatched(String name, String template) {
268: if (checkType == TYPE_TLS) {
269: return matchAllWildcards(name, template);
270: } else if (checkType == TYPE_LDAP) {
271: return matchLeftmostWildcard(name, template);
272: } else {
273: return false;
274: }
275: }
276:
277: /**
278: * Returns true if name matches against template.<p>
279: *
280: * According to RFC 2818, section 3.1 -
281: * Names may contain the wildcard character * which is
282: * considered to match any single domain name component
283: * or component fragment.
284: * E.g., *.a.com matches foo.a.com but not
285: * bar.foo.a.com. f*.com matches foo.com but not bar.com.
286: */
287: private static boolean matchAllWildcards(String name,
288: String template) {
289: name = name.toLowerCase();
290: template = template.toLowerCase();
291: StringTokenizer nameSt = new StringTokenizer(name, ".");
292: StringTokenizer templateSt = new StringTokenizer(template, ".");
293:
294: if (nameSt.countTokens() != templateSt.countTokens()) {
295: return false;
296: }
297:
298: while (nameSt.hasMoreTokens()) {
299: if (!matchWildCards(nameSt.nextToken(), templateSt
300: .nextToken())) {
301: return false;
302: }
303: }
304: return true;
305: }
306:
307: /**
308: * Returns true if name matches against template.<p>
309: *
310: * As per RFC 2830, section 3.6 -
311: * The "*" wildcard character is allowed. If present, it applies only
312: * to the left-most name component.
313: * E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not
314: * bar.com.
315: */
316: private static boolean matchLeftmostWildcard(String name,
317: String template) {
318: name = name.toLowerCase();
319: template = template.toLowerCase();
320:
321: // Retreive leftmost component
322: int templateIdx = template.indexOf(".");
323: int nameIdx = name.indexOf(".");
324:
325: if (templateIdx == -1)
326: templateIdx = template.length();
327: if (nameIdx == -1)
328: nameIdx = name.length();
329:
330: if (matchWildCards(name.substring(0, nameIdx), template
331: .substring(0, templateIdx))) {
332:
333: // match rest of the name
334: return template.substring(templateIdx).equals(
335: name.substring(nameIdx));
336: } else {
337: return false;
338: }
339: }
340:
341: /**
342: * Returns true if the name matches against the template that may
343: * contain wildcard char * <p>
344: */
345: private static boolean matchWildCards(String name, String template) {
346:
347: int wildcardIdx = template.indexOf("*");
348: if (wildcardIdx == -1)
349: return name.equals(template);
350:
351: boolean isBeginning = true;
352: String beforeWildcard = "";
353: String afterWildcard = template;
354:
355: while (wildcardIdx != -1) {
356:
357: // match in sequence the non-wildcard chars in the template.
358: beforeWildcard = afterWildcard.substring(0, wildcardIdx);
359: afterWildcard = afterWildcard.substring(wildcardIdx + 1);
360:
361: int beforeStartIdx = name.indexOf(beforeWildcard);
362: if ((beforeStartIdx == -1)
363: || (isBeginning && beforeStartIdx != 0)) {
364: return false;
365: }
366: isBeginning = false;
367:
368: // update the match scope
369: name = name.substring(beforeStartIdx
370: + beforeWildcard.length());
371: wildcardIdx = afterWildcard.indexOf("*");
372: }
373: return name.endsWith(afterWildcard);
374: }
375: }
|