001    /*
002     * Copyright 2014-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2014-2015 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.util.Collection;
028    import java.util.List;
029    import java.security.cert.Certificate;
030    import java.security.cert.X509Certificate;
031    import javax.net.ssl.SSLSession;
032    import javax.net.ssl.SSLSocket;
033    import javax.security.auth.x500.X500Principal;
034    
035    import com.unboundid.ldap.sdk.DN;
036    import com.unboundid.ldap.sdk.LDAPException;
037    import com.unboundid.ldap.sdk.RDN;
038    import com.unboundid.ldap.sdk.ResultCode;
039    import com.unboundid.util.Debug;
040    import com.unboundid.util.NotMutable;
041    import com.unboundid.util.StaticUtils;
042    import com.unboundid.util.ThreadSafety;
043    import com.unboundid.util.ThreadSafetyLevel;
044    
045    import 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)
058    public 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    }