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

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

Go to download

The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use Java API for communicating with LDAP directory servers and performing related tasks like reading and writing LDIF, encoding and decoding data using base64 and ASN.1 BER, and performing secure communication. This package contains the Standard Edition of the LDAP SDK, which is a complete, general-purpose library for communicating with LDAPv3 directory servers.

There is a newer version: 7.0.1
Show newest version
/*
 * Copyright 2014-2023 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2014-2023 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2014-2023 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.HostnameVerifier;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.security.auth.x500.X500Principal;

import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Filter;
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.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.ObjectPair;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.args.IPAddressArgumentValueValidator;

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
       implements HostnameVerifier
{
  /**
   * The name of a system property that can be used to specify the default
   * behavior that the verifier should exhibit when checking certificates that
   * contain both a CN attribute in the subject DN and a subject alternative
   * name extension that contains one or more dNSName,
   * uniformResourceIdentifier, or iPAddress values. Although RFC 6125 section
   * 6.4.4 indicates that the CN attribute should not be checked in certificates
   * that have an appropriate subject alternative name extension, LDAP clients
   * historically treat both sources as equally valid.
   */
  @NotNull public static final String
       PROPERTY_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT =
            HostNameSSLSocketVerifier.class.getName() +
                 ".checkCNWhenSubjectAltNameIsPresent";



  /**
   * Indicates whether to check the CN attribute in the peer certificate's
   * subject DN when that certificate also contains a subject alternative name
   * extension.
   */
  static final boolean DEFAULT_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT;
  static
  {
    boolean checkCN = true;
    final String propValue = StaticUtils.getSystemProperty(
         PROPERTY_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT);
    if ((propValue != null) && propValue.equalsIgnoreCase("false"))
    {
      checkCN = false;
    }

    DEFAULT_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT = checkCN;
  }



  // 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;

  // Indicates whether to check the CN attribute in the peer certificate's
  // subject DN if the certificate also contains a subject alternative name
  // extension that contains at least dNSName, uniformResourceIdentifier, or
  // iPAddress value.
  private final boolean checkCNWhenSubjectAltNameIsPresent;



  /**
   * Creates a new instance of this {@code SSLSocket} verifier.
   *
   * @param  allowWildcards  Indicates whether to allow wildcard certificates
   *                         that contain an asterisk in the leftmost component
   *                         of a hostname in the dNSName or
   *                         uniformResourceIdentifier of the subject
   *                         alternative name extension, or in the CN attribute
   *                         of the subject DN.
   */
  public HostNameSSLSocketVerifier(final boolean allowWildcards)
  {
    this(allowWildcards, DEFAULT_CHECK_CN_WHEN_SUBJECT_ALT_NAME_IS_PRESENT);
  }



  /**
   * Creates a new instance of this {@code SSLSocket} verifier.
   *
   * @param  allowWildcards
   *              Indicates whether to allow wildcard certificates that contain
   *              an asterisk in the leftmost component of a hostname in the
   *              dNSName or uniformResourceIdentifier of the subject
   *              alternative name extension, or in the CN attribute of the
   *              subject DN.
   * @param  checkCNWhenSubjectAltNameIsPresent
   *              Indicates whether to check the CN attribute in the peer
   *              certificate's subject DN if the certificate also contains a
   *              subject alternative name extension that contains at least one
   *              dNSName, uniformResourceIdentifier, or iPAddress value.
   *              Although RFC 6125 section 6.4.4 indicates that the CN
   *              attribute should not be checked in certificates that have an
   *              appropriate subject alternative name extension, LDAP clients
   *              historically treat both sources as equally valid.
   */
  public HostNameSSLSocketVerifier(final boolean allowWildcards,
              final boolean checkCNWhenSubjectAltNameIsPresent)
  {
    this.allowWildcards = allowWildcards;
    this.checkCNWhenSubjectAltNameIsPresent =
         checkCNWhenSubjectAltNameIsPresent;
  }



  /**
   * 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(@NotNull final String host, final int port,
                              @NotNull final SSLSocket sslSocket)
         throws LDAPException
  {
    verifySSLSession(host, port, sslSocket.getSession());
  }



  /**
   * Verifies that the provided {@code SSLSession} 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  sslSession  The SSL session that was negotiated.
   *
   * @throws  LDAPException  If a problem is identified that should prevent the
   *                         provided {@code SSLSocket} from remaining
   *                         established.
   */
  private void verifySSLSession(@NotNull final String host, final int port,
                               @NotNull final SSLSession sslSession)
          throws LDAPException
  {
    try
    {
      // Get the certificates presented during negotiation.  The certificates
      // will be ordered so that the server certificate comes first.
      if (sslSession == null)
      {
        throw new LDAPException(ResultCode.CONNECT_ERROR,
             ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_SESSION.get(host, port));
      }

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

      if (peerCertificateChain[0] instanceof X509Certificate)
      {
        final StringBuilder certInfo = new StringBuilder();
        if (! certificateIncludesHostname(host,
             (X509Certificate) peerCertificateChain[0], allowWildcards,
             checkCNWhenSubjectAltNameIsPresent, 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,
                  peerCertificateChain[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 that contain
   *              an asterisk in the leftmost component of a hostname in the
   *              dNSName or uniformResourceIdentifier of the subject
   *              alternative name extension, or in the CN attribute of the
   *              subject DN.
   * @param  checkCNWhenSubjectAltNameIsPresent
   *              Indicates whether to check the CN attribute in the peer
   *              certificate's subject DN if the certificate also contains a
   *              subject alternative name extension that contains at least one
   *              dNSName, uniformResourceIdentifier, or iPAddress value.  RFC
   *              6125 section 6.4.4 indicates that the CN attribute should not
   *              be checked in certificates that have an appropriate subject
   *              alternative name extension, although some clients may expect
   *              CN matching anyway.
   * @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(@NotNull final String host,
                      @NotNull final X509Certificate certificate,
                      final boolean allowWildcards,
                      final boolean checkCNWhenSubjectAltNameIsPresent,
                      @NotNull final StringBuilder certInfo)
  {
    // Check to see if the provided hostname is an IP address.
    InetAddress hostInetAddress = null;
    if (IPAddressArgumentValueValidator.isValidNumericIPAddress(host))
    {
      try
      {
        hostInetAddress =
             LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.getByName(host);

        // Loopback IP addresses (but not names like "localhost") should be
        // considered "potentially trustworthy" as per the W3C Secure Contexts
        // Candidate Recommendation at https://www.w3.org/TR/secure-contexts/.
        // That means that when connecting over a loopback, we can assume that
        // the connection is established to the server we intended, even if that
        // loopback IP address isn't in the certificate's subject alternative
        // name extension or the CN attribute of the subject DN.
        if (hostInetAddress.isLoopbackAddress())
        {
          return true;
        }
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }


    // Get the subject DN for the certificate and append it to the certInfo
    // buffer.
    final String subjectDNString =
         certificate.getSubjectX500Principal().getName(X500Principal.RFC2253);
    certInfo.append("subject='");
    certInfo.append(subjectDNString);
    certInfo.append('\'');


    // Check to see if the certificate has a subject alternative name extension.
    // If so, then check its dNSName, uniformResourceLocator, and iPAddress
    // elements.
    boolean hasAuthoritativeSubjectAlternativeName = false;
    try
    {
      final Collection> subjectAltNames;
      subjectAltNames = certificate.getSubjectAlternativeNames();
      if (subjectAltNames != null)
      {
        for (final List l : subjectAltNames)
        {
          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('\'');

              if (hostnameMatches(host, dnsName, allowWildcards))
              {
                return true;
              }

              hasAuthoritativeSubjectAlternativeName = true;
              break;

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

              final String uriHost = getHostFromURI(uriString);
              if (uriHost != null)
              {
                if (IPAddressArgumentValueValidator.isValidNumericIPAddress(
                     uriHost))
                {
                  if ((hostInetAddress != null) &&
                       ipAddressMatches(hostInetAddress, uriHost))
                  {
                    return true;
                  }
                }
                else if (hostnameMatches(host, uriHost, allowWildcards))
                {
                  return true;
                }
              }

              hasAuthoritativeSubjectAlternativeName = true;
              break;

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

              if ((hostInetAddress != null) &&
                   ipAddressMatches(hostInetAddress, ipAddressString))
              {
                return true;
              }

              hasAuthoritativeSubjectAlternativeName = true;
              break;
          }
        }
      }
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
    }


    // If we found an authoritative subject alternative name and we should not
    // check the subject DN to see if it contains a CN attribute, then indicate
    // that we didn't find a match.
    if (hasAuthoritativeSubjectAlternativeName &&
         (! checkCNWhenSubjectAltNameIsPresent))
    {
      return false;
    }


    // Look for any CN attributes in the certificate subject.
    try
    {
      final DN subjectDN = new DN(subjectDNString);
      for (final RDN rdn : subjectDN.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 cnValue = values[i];
            if (IPAddressArgumentValueValidator.
                 isValidNumericIPAddress(cnValue))
            {
              if ((hostInetAddress != null) &&
                   ipAddressMatches(hostInetAddress, cnValue))
              {
                return true;
              }
            }
            else
            {
              if (hostnameMatches(host, cnValue, allowWildcards))
              {
                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);
    }


    // If we've gotten here, then we can't consider the hostname a match.
    return false;
  }



  /**
   * Determines whether the provided client hostname matches the given
   * hostname from the certificate.
   *
   * @param  clientHostname
   *              The hostname that the client used when establishing the
   *              connection.
   * @param  certificateHostname
   *              A hostname obtained from the certificate.
   * @param  allowWildcards
   *              Indicates whether to allow wildcard certificates that contain
   *              an asterisk in the leftmost component of a hostname in the
   *              dNSName or uniformResourceIdentifier of the subject
   *              alternative name extension, or in the CN attribute of the
   *              subject DN.
   *
   * @return  {@code true} if the client hostname is considered a match for the
   *          certificate hostname, or {@code false} if not.
   */
  private static boolean hostnameMatches(@NotNull final String clientHostname,
                              @NotNull final String certificateHostname,
                              final boolean allowWildcards)
  {
    // If the provided certificate hostname does not contain any asterisks,
    // then we just need to do a case-insensitive match.
    if (! certificateHostname.contains("*"))
    {
      return clientHostname.equalsIgnoreCase(certificateHostname);
    }


    // The certificate hostname contains at least one wildcard.  See if that's
    // allowed.
    if (! allowWildcards)
    {
      return false;
    }


    // Get the first component and the remainder for both the client and
    // certificate hostnames.  If the remainder doesn't match, then it's not a
    // match.
    final ObjectPair clientFirstComponentAndRemainder =
         getFirstComponentAndRemainder(clientHostname);
    final ObjectPair certificateFirstComponentAndRemainder =
         getFirstComponentAndRemainder(certificateHostname);
    if (! clientFirstComponentAndRemainder.getSecond().equalsIgnoreCase(
         certificateFirstComponentAndRemainder.getSecond()))
    {
      return false;
    }


    // If the first component of the certificate hostname is just an asterisk,
    // then we can consider it a match.
    final String certificateFirstComponent =
         certificateFirstComponentAndRemainder.getFirst();
    if (certificateFirstComponent.equals("*"))
    {
      return true;
    }


    // The filter has wildcard and non-wildcard components.  At this point, the
    // easiest thing to do is to try to create a substring filter to get the
    // individual components of the filter.
    final Filter filter;
    try
    {
      filter = Filter.create("(hostname=" + certificateFirstComponent + ')');
      if (filter.getFilterType() != Filter.FILTER_TYPE_SUBSTRING)
      {
        return false;
      }
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      return false;
    }


    return CaseIgnoreStringMatchingRule.getInstance().matchesSubstring(
         new ASN1OctetString(clientFirstComponentAndRemainder.getFirst()),
         filter.getRawSubInitialValue(),
         filter.getRawSubAnyValues(), filter.getRawSubFinalValue());
  }



  /**
   * Separates the provided address into the leftmost component (everything up
   * to the first period) and the remainder (everything else, including the
   * first period).  If the provided address does not contain any periods, then
   * the leftmost component will be the entire value and the remainder will be
   * an empty string.
   *
   * @param  address  The address to be separated into the leftmost component
   *                  and the remainder.  It must not be {@code null}.
   *
   * @return  An object pair in which the first element is the leftmost
   *          component of the provided address and the second element is the
   *          remainder of the address.
   */
  @NotNull()
  private static ObjectPair getFirstComponentAndRemainder(
                                                @NotNull final String address)
  {
    final int periodPos = address.indexOf('.');
    if (periodPos < 0)
    {
      return new ObjectPair<>(address, "");
    }
    else
    {
      return new ObjectPair<>(address.substring(0, periodPos),
           address.substring(periodPos));
    }
  }



  /**
   * Determines whether the provided client IP address matches the IP address
   * represented by the provided string.
   *
   * @param  clientIPAddress
   *              The IP address that the client used when establishing the
   *              connection.
   * @param  certificateIPAddressString
   *              The string representation of an IP address obtained from the
   *              certificate.
   *
   * @return  {@code true} if the client hostname is considered a match for the
   *          certificate hostname, or {@code false} if not.
   */
  private static boolean ipAddressMatches(
                              @NotNull final InetAddress clientIPAddress,
                              @NotNull final String certificateIPAddressString)
  {
    final InetAddress certificateIPAddress;
    try
    {
      certificateIPAddress = LDAPConnectionOptions.DEFAULT_NAME_RESOLVER.
           getByName(certificateIPAddressString);
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      return false;
    }

    return clientIPAddress.equals(certificateIPAddress);
  }



  /**
   * Extracts the host from the URI with the given string representation.  Note
   * that the Java URI parser doesn't like hostnames that have wildcards, so we
   * have to handle them specially.
   *
   * @param  uriString  The string representation of the URI to parse.  It must
   *                    not be {@code null}.
   *
   * @return  The host extracted from the provided URI, or {@code null} if none
   *          is available (e.g., because the URI is malformed).
   */
  @Nullable()
  private static String getHostFromURI(@NotNull final String uriString)
  {
    final URI uri;
    try
    {
      uri = new URI(uriString);
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      return null;
    }

    final String uriHost = uri.getHost();
    if (uriHost != null)
    {
      return uriHost;
    }


    // Java's URI code can't handle hosts with wildcards.  See if the provided
    // URI string looks like it might contain a wildcard.  If not, then just
    // return null.
    if (! uriString.contains("*"))
    {
      return null;
    }


    // If Java was at least able to parse the scheme, and if the URI starts with
    // that scheme, then we can go ahead with our own parsing attempt.
    final String scheme = uri.getScheme();
    if ((scheme == null) || scheme.isEmpty() ||
         (! uriString.toLowerCase().startsWith(scheme)))
    {
      return null;
    }


    // Strip the scheme from the beginning of the URI.  Note that the scheme
    // probably won't contain the "://", so strip that separately.
    String paredDownURI = uriString.substring(scheme.length());
    if (paredDownURI.startsWith("://"))
    {
      paredDownURI = paredDownURI.substring(3);
    }


    // If the pared down URI contains a slash (which would separate the hostport
    // section from the path), then strip that off and everything after it.
    final int slashPos = paredDownURI.indexOf('/');
    if (slashPos >= 0)
    {
      paredDownURI = paredDownURI.substring(0, slashPos);
    }


    // If the pared down URI contains a colon (which would separate the host
    // from the port), then strip that off and everything after it.
    final int colonPos = paredDownURI.indexOf(':');
    if (colonPos >= 0)
    {
      paredDownURI = paredDownURI.substring(0, colonPos);
    }


    // If there's anything left, then it should be the host.
    if (! paredDownURI.isEmpty())
    {
      return paredDownURI;
    }

    return null;
  }



  /**
   * Verifies that the provided hostname is acceptable for use with the
   * negotiated SSL session.
   *
   * @param  hostname  The address to which the client intended the connection
   *                   to be established.
   * @param  session   The SSL session that was established.
   */
  @Override()
  public boolean verify(@NotNull final String hostname,
                        @NotNull final SSLSession session)
  {
    try
    {
      verifySSLSession(hostname, session.getPeerPort(), session);
      return true;
    }
    catch (final LDAPException e)
    {
      Debug.debugException(e);
      return false;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy