All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.util.ssl.HostNameSSLSocketVerifier Maven / Gradle / Ivy

/*
 * Copyright 2014-2019 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright (C) 2014-2019 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.util.ssl;



import java.net.InetAddress;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.security.auth.x500.X500Principal;

import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Debug;
import com.unboundid.util.NotMutable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;

import static com.unboundid.util.ssl.SSLMessages.*;



/**
 * This class provides an implementation of an {@code SSLSocket} verifier that
 * will verify that the presented server certificate includes the address to
 * which the client intended to establish a connection.  It will check the CN
 * attribute of the certificate subject, as well as certain subjectAltName
 * extensions, including dNSName, uniformResourceIdentifier, and iPAddress.
 */
@NotMutable()
@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
public final class HostNameSSLSocketVerifier
       extends SSLSocketVerifier
{
  // Indicates whether to allow wildcard certificates which contain an asterisk
  // as the first component of a CN subject attribute or dNSName subjectAltName
  // extension.
  private final boolean allowWildcards;



  /**
   * Creates a new instance of this {@code SSLSocket} verifier.
   *
   * @param  allowWildcards  Indicates whether to allow wildcard certificates
   *                         which contain an asterisk as the first component of
   *                         a CN subject attribute or dNSName subjectAltName
   *                         extension.
   */
  public HostNameSSLSocketVerifier(final boolean allowWildcards)
  {
    this.allowWildcards = allowWildcards;
  }



  /**
   * Verifies that the provided {@code SSLSocket} is acceptable and the
   * connection should be allowed to remain established.
   *
   * @param  host       The address to which the client intended the connection
   *                    to be established.
   * @param  port       The port to which the client intended the connection to
   *                    be established.
   * @param  sslSocket  The {@code SSLSocket} that should be verified.
   *
   * @throws  LDAPException  If a problem is identified that should prevent the
   *                         provided {@code SSLSocket} from remaining
   *                         established.
   */
  @Override()
  public void verifySSLSocket(final String host, final int port,
                              final SSLSocket sslSocket)
         throws LDAPException
  {
    try
    {
      // Get the certificates presented during negotiation.  The certificates
      // will be ordered so that the server certificate comes first.
      final SSLSession sslSession = sslSocket.getSession();
      if (sslSession == null)
      {
        throw new LDAPException(ResultCode.CONNECT_ERROR,
             ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_SESSION.get(host, port));
      }

      final Certificate[] peerCertificates = sslSession.getPeerCertificates();
      if ((peerCertificates == null) || (peerCertificates.length == 0))
      {
        throw new LDAPException(ResultCode.CONNECT_ERROR,
             ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_PEER_CERTS.get(host, port));
      }

      if (peerCertificates[0] instanceof X509Certificate)
      {
        final StringBuilder certInfo = new StringBuilder();
        if (! certificateIncludesHostname(host,
             (X509Certificate) peerCertificates[0], allowWildcards, certInfo))
        {
          throw new LDAPException(ResultCode.CONNECT_ERROR,
               ERR_HOST_NAME_SSL_SOCKET_VERIFIER_HOSTNAME_NOT_FOUND.get(host,
                    certInfo.toString()));
        }
      }
      else
      {
        throw new LDAPException(ResultCode.CONNECT_ERROR,
             ERR_HOST_NAME_SSL_SOCKET_VERIFIER_PEER_NOT_X509.get(host, port,
                  peerCertificates[0].getType()));
      }
    }
    catch (final LDAPException le)
    {
      Debug.debugException(le);
      throw le;
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.CONNECT_ERROR,
           ERR_HOST_NAME_SSL_SOCKET_VERIFIER_EXCEPTION.get(host, port,
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Determines whether the provided certificate contains the specified
   * hostname.
   *
   * @param  host            The address expected to be found in the provided
   *                         certificate.
   * @param  certificate     The peer certificate to be validated.
   * @param  allowWildcards  Indicates whether to allow wildcard certificates
   *                         which contain an asterisk as the first component of
   *                         a CN subject attribute or dNSName subjectAltName
   *                         extension.
   * @param  certInfo        A buffer into which information will be provided
   *                         about the provided certificate.
   *
   * @return  {@code true} if the expected hostname was found in the
   *          certificate, or {@code false} if not.
   */
  static boolean certificateIncludesHostname(final String host,
                                             final X509Certificate certificate,
                                             final boolean allowWildcards,
                                             final StringBuilder certInfo)
  {
    final String lowerHost = StaticUtils.toLowerCase(host);

    // First, check the CN from the certificate subject.
    final String subjectDN =
         certificate.getSubjectX500Principal().getName(X500Principal.RFC2253);
    certInfo.append("subject='");
    certInfo.append(subjectDN);
    certInfo.append('\'');

    try
    {
      final DN dn = new DN(subjectDN);
      for (final RDN rdn : dn.getRDNs())
      {
        final String[] names  = rdn.getAttributeNames();
        final String[] values = rdn.getAttributeValues();
        for (int i=0; i < names.length; i++)
        {
          final String lowerName = StaticUtils.toLowerCase(names[i]);
          if (lowerName.equals("cn") || lowerName.equals("commonname") ||
              lowerName.equals("2.5.4.3"))
          {
            final String lowerValue = StaticUtils.toLowerCase(values[i]);
            if (lowerHost.equals(lowerValue))
            {
              return true;
            }

            if (allowWildcards && lowerValue.startsWith("*."))
            {
              final String withoutWildcard = lowerValue.substring(1);
              if (lowerHost.endsWith(withoutWildcard))
              {
                return true;
              }
            }
          }
        }
      }
    }
    catch (final Exception e)
    {
      // This shouldn't happen for a well-formed certificate subject, but we
      // have to handle it anyway.
      Debug.debugException(e);
    }


    // Next, check any supported subjectAltName extension values.
    final Collection> subjectAltNames;
    try
    {
      subjectAltNames = certificate.getSubjectAlternativeNames();
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      return false;
    }

    if (subjectAltNames != null)
    {
      for (final List l : subjectAltNames)
      {
        try
        {
          final Integer type = (Integer) l.get(0);
          switch (type)
          {
            case 2: // dNSName
              final String dnsName = (String) l.get(1);
              certInfo.append(" dNSName='");
              certInfo.append(dnsName);
              certInfo.append('\'');

              final String lowerDNSName = StaticUtils.toLowerCase(dnsName);
              if (lowerHost.equals(lowerDNSName))
              {
                return true;
              }

              // If the given DNS name starts with a "*.", then it's a wildcard
              // certificate.  See if that's allowed, and if so whether it
              // matches any acceptable name.
              if (allowWildcards && lowerDNSName.startsWith("*."))
              {
                final String withoutWildcard = lowerDNSName.substring(1);
                if (lowerHost.endsWith(withoutWildcard))
                {
                  return true;
                }
              }
              break;

            case 6: // uniformResourceIdentifier
              final String uriString = (String) l.get(1);
              certInfo.append(" uniformResourceIdentifier='");
              certInfo.append(uriString);
              certInfo.append('\'');

              final URI uri = new URI(uriString);
              if (lowerHost.equals(StaticUtils.toLowerCase(uri.getHost())))
              {
                return true;
              }
              break;

            case 7: // iPAddress
              final String ipAddressString = (String) l.get(1);
              certInfo.append(" iPAddress='");
              certInfo.append(ipAddressString);
              certInfo.append('\'');

              final InetAddress inetAddress =
                   LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.
                        getByName(ipAddressString);
              if (Character.isDigit(host.charAt(0)) || (host.indexOf(':') >= 0))
              {
                final InetAddress a = InetAddress.getByName(host);
                if (inetAddress.equals(a))
                {
                  return true;
                }
              }
              break;

            case 0: // otherName
            case 1: // rfc822Name
            case 3: // x400Address
            case 4: // directoryName
            case 5: // ediPartyName
            case 8: // registeredID
            default:
              // We won't do any checking for any of these formats.
              break;
          }
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }
      }
    }

    return false;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy