001    /*
002     * Copyright 2012-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2012-2013 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.util.ssl;
022    
023    
024    
025    import java.net.InetAddress;
026    import java.net.URI;
027    import java.security.cert.CertificateException;
028    import java.security.cert.X509Certificate;
029    import java.util.Collection;
030    import java.util.Collections;
031    import java.util.LinkedHashSet;
032    import java.util.List;
033    import java.util.Set;
034    import javax.net.ssl.X509TrustManager;
035    import javax.security.auth.x500.X500Principal;
036    
037    import com.unboundid.ldap.sdk.DN;
038    import com.unboundid.ldap.sdk.RDN;
039    import com.unboundid.util.NotMutable;
040    import com.unboundid.util.StaticUtils;
041    import com.unboundid.util.ThreadSafety;
042    import com.unboundid.util.ThreadSafetyLevel;
043    import com.unboundid.util.Validator;
044    
045    import static com.unboundid.util.Debug.*;
046    import static com.unboundid.util.ssl.SSLMessages.*;
047    
048    
049    
050    /**
051     * This class provides an SSL trust manager that will only accept certificates
052     * whose hostname (as contained in the CN subject attribute or a subjectAltName
053     * extension) matches an expected value.  Only the dNSName, iPAddress, and
054     * uniformResourceIdentifier subjectAltName formats are supported.
055     * <BR><BR>
056     * This implementation optionally supports wildcard certificates, which have a
057     * hostname that starts with an asterisk followed by a period and domain or
058     * subdomain.  For example, "*.example.com" could be considered a match for
059     * anything in the "example.com" domain.  If wildcards are allowed, then only
060     * the CN subject attribute and dNSName subjectAltName extension will be
061     * examined, and only the leftmost element of a hostname may be a wildcard
062     * character.
063     * <BR><BR>
064     * Note that no other elements of the certificate are examined, so it is
065     * strongly recommended that this trust manager be used in an
066     * {@link AggregateTrustManager} in conjunction with other trust managers that
067     * perform other forms of validation.
068     */
069    @NotMutable()
070    @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
071    public final class HostNameTrustManager
072           implements X509TrustManager
073    {
074      /**
075       * A pre-allocated empty certificate array.
076       */
077      private static final X509Certificate[] NO_CERTIFICATES =
078           new X509Certificate[0];
079    
080    
081    
082      // Indicates whether to allow wildcard certificates (which
083      private final boolean allowWildcards;
084    
085      // The set of hostname values that will be considered acceptable.
086      private final Set<String> acceptableHostNames;
087    
088    
089    
090      /**
091       * Creates a new hostname trust manager with the provided information.
092       *
093       * @param  allowWildcards       Indicates whether to allow wildcard
094       *                              certificates which contain an asterisk as the
095       *                              first component of a CN subject attribute or
096       *                              dNSName subjectAltName extension.
097       * @param  acceptableHostNames  The set of hostnames and/or IP addresses that
098       *                              will be considered acceptable.  Only
099       *                              certificates with a CN or subjectAltName value
100       *                              that exactly matches one of these names
101       *                              (ignoring differences in capitalization) will
102       *                              be considered acceptable.  It must not be
103       *                              {@code null} or empty.
104       */
105      public HostNameTrustManager(final boolean allowWildcards,
106                                  final String... acceptableHostNames)
107      {
108        this(allowWildcards, StaticUtils.toList(acceptableHostNames));
109      }
110    
111    
112    
113      /**
114       * Creates a new hostname trust manager with the provided information.
115       *
116       * @param  allowWildcards       Indicates whether to allow wildcard
117       *                              certificates which contain an asterisk as the
118       *                              first component of a CN subject attribute or
119       *                              dNSName subjectAltName extension.
120       * @param  acceptableHostNames  The set of hostnames and/or IP addresses that
121       *                              will be considered acceptable.  Only
122       *                              certificates with a CN or subjectAltName value
123       *                              that exactly matches one of these names
124       *                              (ignoring differences in capitalization) will
125       *                              be considered acceptable.  It must not be
126       *                              {@code null} or empty.
127       */
128      public HostNameTrustManager(final boolean allowWildcards,
129                                  final Collection<String> acceptableHostNames)
130      {
131        Validator.ensureNotNull(acceptableHostNames);
132        Validator.ensureFalse(acceptableHostNames.isEmpty(),
133             "The set of acceptable host names must not be empty.");
134    
135        this.allowWildcards = allowWildcards;
136    
137        final LinkedHashSet<String> nameSet =
138             new LinkedHashSet<String>(acceptableHostNames.size());
139        for (final String s : acceptableHostNames)
140        {
141          nameSet.add(StaticUtils.toLowerCase(s));
142        }
143    
144        this.acceptableHostNames = Collections.unmodifiableSet(nameSet);
145      }
146    
147    
148    
149      /**
150       * Indicates whether wildcard certificates should be allowed, which may
151       * match multiple hosts in a given domain or subdomain.
152       *
153       * @return  {@code true} if wildcard certificates should be allowed, or
154       *          {@code false} if not.
155       */
156      public boolean allowWildcards()
157      {
158        return allowWildcards;
159      }
160    
161    
162    
163      /**
164       * Retrieves the set of hostnames that will be considered acceptable.
165       *
166       * @return  The set of hostnames that will be considered acceptable.
167       */
168      public Set<String> getAcceptableHostNames()
169      {
170        return acceptableHostNames;
171      }
172    
173    
174    
175      /**
176       * Checks to determine whether the provided client certificate chain should be
177       * trusted.
178       *
179       * @param  chain     The client certificate chain for which to make the
180       *                   determination.
181       * @param  authType  The authentication type based on the client certificate.
182       *
183       * @throws  CertificateException  If the provided client certificate chain
184       *                                should not be trusted.
185       */
186      public void checkClientTrusted(final X509Certificate[] chain,
187                                     final String authType)
188             throws CertificateException
189      {
190        checkCertificate(chain[0]);
191      }
192    
193    
194    
195      /**
196       * Checks to determine whether the provided server certificate chain should be
197       * trusted.
198       *
199       * @param  chain     The server certificate chain for which to make the
200       *                   determination.
201       * @param  authType  The key exchange algorithm used.
202       *
203       * @throws  CertificateException  If the provided server certificate chain
204       *                                should not be trusted.
205       */
206      public void checkServerTrusted(final X509Certificate[] chain,
207                                     final String authType)
208             throws CertificateException
209      {
210        checkCertificate(chain[0]);
211      }
212    
213    
214    
215      /**
216       * Performs the appropriate validation for the given certificate.
217       *
218       * @param  c  The certificate to be validated.
219       *
220       * @throws  CertificateException  If the provided certificate does not have a
221       *                                CN or subjectAltName value that matches one
222       *                                of the acceptable hostnames.
223       */
224      private void checkCertificate(final X509Certificate c)
225              throws CertificateException
226      {
227        // First, check the CN from the certificate subject.
228        final String subjectDN =
229             c.getSubjectX500Principal().getName(X500Principal.RFC2253);
230        try
231        {
232          final DN dn = new DN(subjectDN);
233          for (final RDN rdn : dn.getRDNs())
234          {
235            final String[] names  = rdn.getAttributeNames();
236            final String[] values = rdn.getAttributeValues();
237            for (int i=0; i < names.length; i++)
238            {
239              final String lowerName = StaticUtils.toLowerCase(names[i]);
240              if (lowerName.equals("cn") || lowerName.equals("commonname") ||
241                  lowerName.equals("2.5.4.3"))
242              {
243                final String lowerValue = StaticUtils.toLowerCase(values[i]);
244                if (acceptableHostNames.contains(lowerValue))
245                {
246                  return;
247                }
248    
249                if (allowWildcards && lowerValue.startsWith("*."))
250                {
251                  final String withoutWildcard = lowerValue.substring(1);
252                  for (final String s : acceptableHostNames)
253                  {
254                    if (s.endsWith(withoutWildcard))
255                    {
256                      return;
257                    }
258                  }
259                }
260              }
261            }
262          }
263        }
264        catch (final Exception e)
265        {
266          // This shouldn't happen for a well-formed certificate subject, but we
267          // have to handle it anyway.
268          debugException(e);
269        }
270    
271    
272        // Next, check any subjectAltName extension values.
273        final Collection<List<?>> subjectAltNames = c.getSubjectAlternativeNames();
274        if (subjectAltNames != null)
275        {
276          for (final List<?> l : subjectAltNames)
277          {
278            try
279            {
280              final Integer type = (Integer) l.get(0);
281              switch (type)
282              {
283                case 2: // dNSName
284                  final String dnsName = StaticUtils.toLowerCase((String) l.get(1));
285                  if (acceptableHostNames.contains(dnsName))
286                  {
287                    // We found a matching DNS host name.
288                    return;
289                  }
290    
291                  // If the given DNS name starts with a "*.", then it's a wildcard
292                  // certificate.  See if that's allowed, and if so whether it
293                  // matches any acceptable name.
294                  if (allowWildcards && dnsName.startsWith("*."))
295                  {
296                    final String withoutWildcard = dnsName.substring(1);
297                    for (final String s : acceptableHostNames)
298                    {
299                      if (s.endsWith(withoutWildcard))
300                      {
301                        return;
302                      }
303                    }
304                  }
305                  break;
306    
307                case 6: // uniformResourceIdentifier
308                  final URI uri = new URI((String) l.get(1));
309                  if (acceptableHostNames.contains(
310                       StaticUtils.toLowerCase(uri.getHost())))
311                  {
312                    // The URI had a matching address.
313                    return;
314                  }
315                  break;
316    
317                case 7: // iPAddress
318                  final InetAddress inetAddress =
319                       InetAddress.getByName((String) l.get(1));
320                  for (final String s : acceptableHostNames)
321                  {
322                    if (Character.isDigit(s.charAt(0)) || (s.indexOf(':') >= 0))
323                    {
324                      final InetAddress a = InetAddress.getByName(s);
325                      if (inetAddress.equals(a))
326                      {
327                        return;
328                      }
329                    }
330                  }
331                  break;
332    
333                case 0: // otherName
334                case 1: // rfc822Name
335                case 3: // x400Address
336                case 4: // directoryName
337                case 5: // ediPartyName
338                case 8: // registeredID
339                default:
340                  // We won't do any checking for any of these formats.
341                  break;
342              }
343            }
344            catch (final Exception e)
345            {
346              debugException(e);
347            }
348          }
349        }
350    
351    
352        // If we've gotten here, then we didn't find a match.
353        throw new CertificateException(ERR_HOSTNAME_NOT_FOUND.get(subjectDN));
354      }
355    
356    
357    
358      /**
359       * Retrieves the accepted issuer certificates for this trust manager.  This
360       * will always return an empty array.
361       *
362       * @return  The accepted issuer certificates for this trust manager.
363       */
364      public X509Certificate[] getAcceptedIssuers()
365      {
366        return NO_CERTIFICATES;
367      }
368    }