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