001/*
002 * Copyright 2014-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2014-2017 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 */
021package com.unboundid.util.ssl;
022
023
024
025import java.net.InetAddress;
026import java.net.URI;
027import java.util.Collection;
028import java.util.List;
029import java.security.cert.Certificate;
030import java.security.cert.X509Certificate;
031import javax.net.ssl.SSLSession;
032import javax.net.ssl.SSLSocket;
033import javax.security.auth.x500.X500Principal;
034
035import com.unboundid.ldap.sdk.DN;
036import com.unboundid.ldap.sdk.LDAPException;
037import com.unboundid.ldap.sdk.RDN;
038import com.unboundid.ldap.sdk.ResultCode;
039import com.unboundid.util.Debug;
040import com.unboundid.util.NotMutable;
041import com.unboundid.util.StaticUtils;
042import com.unboundid.util.ThreadSafety;
043import com.unboundid.util.ThreadSafetyLevel;
044
045import static com.unboundid.util.ssl.SSLMessages.*;
046
047
048
049/**
050 * This class provides an implementation of an {@code SSLSocket} verifier that
051 * will verify that the presented server certificate includes the address to
052 * which the client intended to establish a connection.  It will check the CN
053 * attribute of the certificate subject, as well as certain subjectAltName
054 * extensions, including dNSName, uniformResourceIdentifier, and iPAddress.
055 */
056@NotMutable()
057@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
058public final class HostNameSSLSocketVerifier
059       extends SSLSocketVerifier
060{
061  // Indicates whether to allow wildcard certificates which contain an asterisk
062  // as the first component of a CN subject attribute or dNSName subjectAltName
063  // extension.
064  private final boolean allowWildcards;
065
066
067
068  /**
069   * Creates a new instance of this {@code SSLSocket} verifier.
070   *
071   * @param  allowWildcards  Indicates whether to allow wildcard certificates
072   *                         which contain an asterisk as the first component of
073   *                         a CN subject attribute or dNSName subjectAltName
074   *                         extension.
075   */
076  public HostNameSSLSocketVerifier(final boolean allowWildcards)
077  {
078    this.allowWildcards = allowWildcards;
079  }
080
081
082
083  /**
084   * Verifies that the provided {@code SSLSocket} is acceptable and the
085   * connection should be allowed to remain established.
086   *
087   * @param  host       The address to which the client intended the connection
088   *                    to be established.
089   * @param  port       The port to which the client intended the connection to
090   *                    be established.
091   * @param  sslSocket  The {@code SSLSocket} that should be verified.
092   *
093   * @throws  LDAPException  If a problem is identified that should prevent the
094   *                         provided {@code SSLSocket} from remaining
095   *                         established.
096   */
097  @Override()
098  public void verifySSLSocket(final String host, final int port,
099                              final SSLSocket sslSocket)
100         throws LDAPException
101  {
102    try
103    {
104      // Get the certificates presented during negotiation.  The certificates
105      // will be ordered so that the server certificate comes first.
106      final SSLSession sslSession = sslSocket.getSession();
107      if (sslSession == null)
108      {
109        throw new LDAPException(ResultCode.CONNECT_ERROR,
110             ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_SESSION.get(host, port));
111      }
112
113      final Certificate[] peerCertificates = sslSession.getPeerCertificates();
114      if ((peerCertificates == null) || (peerCertificates.length == 0))
115      {
116        throw new LDAPException(ResultCode.CONNECT_ERROR,
117             ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_PEER_CERTS.get(host, port));
118      }
119
120      if (peerCertificates[0] instanceof X509Certificate)
121      {
122        final StringBuilder certInfo = new StringBuilder();
123        if (! certificateIncludesHostname(host,
124             (X509Certificate) peerCertificates[0], allowWildcards, certInfo))
125        {
126          throw new LDAPException(ResultCode.CONNECT_ERROR,
127               ERR_HOST_NAME_SSL_SOCKET_VERIFIER_HOSTNAME_NOT_FOUND.get(host,
128                    certInfo.toString()));
129        }
130      }
131      else
132      {
133        throw new LDAPException(ResultCode.CONNECT_ERROR,
134             ERR_HOST_NAME_SSL_SOCKET_VERIFIER_PEER_NOT_X509.get(host, port,
135                  peerCertificates[0].getType()));
136      }
137    }
138    catch (final LDAPException le)
139    {
140      Debug.debugException(le);
141      throw le;
142    }
143    catch (final Exception e)
144    {
145      Debug.debugException(e);
146      throw new LDAPException(ResultCode.CONNECT_ERROR,
147           ERR_HOST_NAME_SSL_SOCKET_VERIFIER_EXCEPTION.get(host, port,
148                StaticUtils.getExceptionMessage(e)),
149           e);
150    }
151  }
152
153
154
155  /**
156   * Determines whether the provided certificate contains the specified
157   * hostname.
158   *
159   * @param  host            The address expected to be found in the provided
160   *                         certificate.
161   * @param  certificate     The peer certificate to be validated.
162   * @param  allowWildcards  Indicates whether to allow wildcard certificates
163   *                         which contain an asterisk as the first component of
164   *                         a CN subject attribute or dNSName subjectAltName
165   *                         extension.
166   * @param  certInfo        A buffer into which information will be provided
167   *                         about the provided certificate.
168   *
169   * @return  {@code true} if the expected hostname was found in the
170   *          certificate, or {@code false} if not.
171   */
172  static boolean certificateIncludesHostname(final String host,
173                                             final X509Certificate certificate,
174                                             final boolean allowWildcards,
175                                             final StringBuilder certInfo)
176  {
177    final String lowerHost = StaticUtils.toLowerCase(host);
178
179    // First, check the CN from the certificate subject.
180    final String subjectDN =
181         certificate.getSubjectX500Principal().getName(X500Principal.RFC2253);
182    certInfo.append("subject='");
183    certInfo.append(subjectDN);
184    certInfo.append('\'');
185
186    try
187    {
188      final DN dn = new DN(subjectDN);
189      for (final RDN rdn : dn.getRDNs())
190      {
191        final String[] names  = rdn.getAttributeNames();
192        final String[] values = rdn.getAttributeValues();
193        for (int i=0; i < names.length; i++)
194        {
195          final String lowerName = StaticUtils.toLowerCase(names[i]);
196          if (lowerName.equals("cn") || lowerName.equals("commonname") ||
197              lowerName.equals("2.5.4.3"))
198          {
199            final String lowerValue = StaticUtils.toLowerCase(values[i]);
200            if (lowerHost.equals(lowerValue))
201            {
202              return true;
203            }
204
205            if (allowWildcards && lowerValue.startsWith("*."))
206            {
207              final String withoutWildcard = lowerValue.substring(1);
208              if (lowerHost.endsWith(withoutWildcard))
209              {
210                return true;
211              }
212            }
213          }
214        }
215      }
216    }
217    catch (final Exception e)
218    {
219      // This shouldn't happen for a well-formed certificate subject, but we
220      // have to handle it anyway.
221      Debug.debugException(e);
222    }
223
224
225    // Next, check any supported subjectAltName extension values.
226    final Collection<List<?>> subjectAltNames;
227    try
228    {
229      subjectAltNames = certificate.getSubjectAlternativeNames();
230    }
231    catch (final Exception e)
232    {
233      Debug.debugException(e);
234      return false;
235    }
236
237    if (subjectAltNames != null)
238    {
239      for (final List<?> l : subjectAltNames)
240      {
241        try
242        {
243          final Integer type = (Integer) l.get(0);
244          switch (type)
245          {
246            case 2: // dNSName
247              final String dnsName = (String) l.get(1);
248              certInfo.append(" dNSName='");
249              certInfo.append(dnsName);
250              certInfo.append('\'');
251
252              final String lowerDNSName = StaticUtils.toLowerCase(dnsName);
253              if (lowerHost.equals(lowerDNSName))
254              {
255                return true;
256              }
257
258              // If the given DNS name starts with a "*.", then it's a wildcard
259              // certificate.  See if that's allowed, and if so whether it
260              // matches any acceptable name.
261              if (allowWildcards && lowerDNSName.startsWith("*."))
262              {
263                final String withoutWildcard = lowerDNSName.substring(1);
264                if (lowerHost.endsWith(withoutWildcard))
265                {
266                  return true;
267                }
268              }
269              break;
270
271            case 6: // uniformResourceIdentifier
272              final String uriString = (String) l.get(1);
273              certInfo.append(" uniformResourceIdentifier='");
274              certInfo.append(uriString);
275              certInfo.append('\'');
276
277              final URI uri = new URI(uriString);
278              if (lowerHost.equals(StaticUtils.toLowerCase(uri.getHost())))
279              {
280                return true;
281              }
282              break;
283
284            case 7: // iPAddress
285              final String ipAddressString = (String) l.get(1);
286              certInfo.append(" iPAddress='");
287              certInfo.append(ipAddressString);
288              certInfo.append('\'');
289
290              final InetAddress inetAddress =
291                   InetAddress.getByName(ipAddressString);
292              if (Character.isDigit(host.charAt(0)) || (host.indexOf(':') >= 0))
293              {
294                final InetAddress a = InetAddress.getByName(host);
295                if (inetAddress.equals(a))
296                {
297                  return true;
298                }
299              }
300              break;
301
302            case 0: // otherName
303            case 1: // rfc822Name
304            case 3: // x400Address
305            case 4: // directoryName
306            case 5: // ediPartyName
307            case 8: // registeredID
308            default:
309              // We won't do any checking for any of these formats.
310              break;
311          }
312        }
313        catch (final Exception e)
314        {
315          Debug.debugException(e);
316        }
317      }
318    }
319
320    return false;
321  }
322}