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