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