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

com.unboundid.util.ssl.PromptTrustManager 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.

The newest version!
/*
 * Copyright 2008-2024 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2008-2024 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) 2008-2024 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.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import javax.net.ssl.X509TrustManager;

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.ssl.cert.CertException;

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



/**
 * This class provides an SSL trust manager that will interactively prompt the
 * user to determine whether to trust any certificate that is presented to it.
 * It provides the ability to cache information about certificates that had been
 * previously trusted so that the user is not prompted about the same
 * certificate repeatedly, and it can be configured to store trusted
 * certificates in a file so that the trust information can be persisted.
 */
@NotMutable()
@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
public final class PromptTrustManager
       implements X509TrustManager
{
  /**
   * A pre-allocated empty certificate array.
   */
  @NotNull private static final X509Certificate[] NO_CERTIFICATES =
       new X509Certificate[0];



  // Indicates whether to examine the validity dates for the certificate in
  // addition to whether the certificate has been previously trusted.
  private final boolean examineValidityDates;

  // The set of previously-accepted certificates.  The certificates will be
  // mapped from an all-lowercase hexadecimal string representation of the
  // certificate signature to a flag that indicates whether the certificate has
  // already been manually trusted even if it is outside of the validity window.
  @NotNull private final ConcurrentHashMap acceptedCerts;

  // The input stream from which the user input will be read.
  @NotNull private final InputStream in;

  // A list of the addresses that the client is expected to use to connect to
  // one of the target servers.
  @NotNull private final List expectedAddresses;

  // The print stream that will be used to display the prompt.
  @NotNull private final PrintStream out;

  // The path to the file to which the set of accepted certificates should be
  // persisted.
  @Nullable private final String acceptedCertsFile;



  /**
   * Creates a new instance of this prompt trust manager.  It will cache trust
   * information in memory but not on disk.
   */
  public PromptTrustManager()
  {
    this(null, true, null, null);
  }



  /**
   * Creates a new instance of this prompt trust manager.  It may optionally
   * cache trust information on disk.
   *
   * @param  acceptedCertsFile  The path to a file in which the certificates
   *                            that have been previously accepted will be
   *                            cached.  It may be {@code null} if the cache
   *                            should only be maintained in memory.
   */
  public PromptTrustManager(@Nullable final String acceptedCertsFile)
  {
    this(acceptedCertsFile, true, null, null);
  }



  /**
   * Creates a new instance of this prompt trust manager.  It may optionally
   * cache trust information on disk, and may also be configured to examine or
   * ignore validity dates.
   *
   * @param  acceptedCertsFile     The path to a file in which the certificates
   *                               that have been previously accepted will be
   *                               cached.  It may be {@code null} if the cache
   *                               should only be maintained in memory.
   * @param  examineValidityDates  Indicates whether to reject certificates if
   *                               the current time is outside the validity
   *                               window for the certificate.
   * @param  in                    The input stream that will be used to read
   *                               input from the user.  If this is {@code null}
   *                               then {@code System.in} will be used.
   * @param  out                   The print stream that will be used to display
   *                               the prompt to the user.  If this is
   *                               {@code null} then System.out will be used.
   */
  public PromptTrustManager(@Nullable final String acceptedCertsFile,
                            final boolean examineValidityDates,
                            @Nullable final InputStream in,
                            @Nullable final PrintStream out)
  {
    this(acceptedCertsFile, examineValidityDates,
         Collections.emptyList(), in, out);
  }



  /**
   * Creates a new instance of this prompt trust manager.  It may optionally
   * cache trust information on disk, and may also be configured to examine or
   * ignore validity dates.
   *
   * @param  acceptedCertsFile     The path to a file in which the certificates
   *                               that have been previously accepted will be
   *                               cached.  It may be {@code null} if the cache
   *                               should only be maintained in memory.
   * @param  examineValidityDates  Indicates whether to reject certificates if
   *                               the current time is outside the validity
   *                               window for the certificate.
   * @param  expectedAddress       An optional address that the client is
   *                               expected to use to connect to the target
   *                               server.  This may be {@code null} if no
   *                               expected address is available, if this trust
   *                               manager is only expected to be used to
   *                               validate client certificates, or if no server
   *                               address validation should be performed.  If a
   *                               non-{@code null} value is provided, then the
   *                               trust manager may issue a warning if the
   *                               certificate does not contain that address.
   * @param  in                    The input stream that will be used to read
   *                               input from the user.  If this is {@code null}
   *                               then {@code System.in} will be used.
   * @param  out                   The print stream that will be used to display
   *                               the prompt to the user.  If this is
   *                               {@code null} then System.out will be used.
   */
  public PromptTrustManager(@Nullable final String acceptedCertsFile,
                            final boolean examineValidityDates,
                            @Nullable final String expectedAddress,
                            @Nullable final InputStream in,
                            @Nullable final PrintStream out)
  {
    this(acceptedCertsFile, examineValidityDates,
         (expectedAddress == null)
              ? Collections.emptyList()
              : Collections.singletonList(expectedAddress),
         in, out);
  }



  /**
   * Creates a new instance of this prompt trust manager.  It may optionally
   * cache trust information on disk, and may also be configured to examine or
   * ignore validity dates.
   *
   * @param  acceptedCertsFile     The path to a file in which the certificates
   *                               that have been previously accepted will be
   *                               cached.  It may be {@code null} if the cache
   *                               should only be maintained in memory.
   * @param  examineValidityDates  Indicates whether to reject certificates if
   *                               the current time is outside the validity
   *                               window for the certificate.
   * @param  expectedAddresses     An optional collection of the addresses that
   *                               the client is expected to use to connect to
   *                               one of the target servers.  This may be
   *                               {@code null} or empty if no expected
   *                               addresses are available, if this trust
   *                               manager is only expected to be used to
   *                               validate client certificates, or if no server
   *                               address validation should be performed.  If a
   *                               non-empty collection is provided, then the
   *                               trust manager may issue a warning if the
   *                               certificate does not contain any of these
   *                               addresses.
   * @param  in                    The input stream that will be used to read
   *                               input from the user.  If this is {@code null}
   *                               then {@code System.in} will be used.
   * @param  out                   The print stream that will be used to display
   *                               the prompt to the user.  If this is
   *                               {@code null} then System.out will be used.
   */
  public PromptTrustManager(@Nullable final String acceptedCertsFile,
              final boolean examineValidityDates,
              @Nullable final Collection expectedAddresses,
              @Nullable final InputStream in,
              @Nullable final PrintStream out)
  {
    this.acceptedCertsFile    = acceptedCertsFile;
    this.examineValidityDates = examineValidityDates;

    if (expectedAddresses == null)
    {
      this.expectedAddresses = Collections.emptyList();
    }
    else
    {
      this.expectedAddresses =
           Collections.unmodifiableList(new ArrayList<>(expectedAddresses));
    }

    if (in == null)
    {
      this.in = System.in;
    }
    else
    {
      this.in = in;
    }

    if (out == null)
    {
      this.out = System.out;
    }
    else
    {
      this.out = out;
    }

    acceptedCerts = new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(20));

    if (acceptedCertsFile != null)
    {
      BufferedReader r = null;
      try
      {
        final File f = new File(acceptedCertsFile);
        if (f.exists())
        {
          r = new BufferedReader(new FileReader(f));
          while (true)
          {
            final String line = r.readLine();
            if (line == null)
            {
              break;
            }
            acceptedCerts.put(line, false);
          }
        }
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
      finally
      {
        if (r != null)
        {
          try
          {
            r.close();
          }
          catch (final Exception e)
          {
            Debug.debugException(e);
          }
        }
      }
    }
  }



  /**
   * Writes an updated copy of the trusted certificate cache to disk.
   *
   * @throws  IOException  If a problem occurs.
   */
  private void writeCacheFile()
          throws IOException
  {
    final File tempFile = new File(acceptedCertsFile + ".new");

    BufferedWriter w = null;
    try
    {
      w = new BufferedWriter(new FileWriter(tempFile));

      for (final String certBytes : acceptedCerts.keySet())
      {
        w.write(certBytes);
        w.newLine();
      }
    }
    finally
    {
      if (w != null)
      {
        w.close();
      }
    }

    final File cacheFile = new File(acceptedCertsFile);
    if (cacheFile.exists())
    {
      final File oldFile = new File(acceptedCertsFile + ".previous");
      if (oldFile.exists())
      {
        Files.delete(oldFile.toPath());
      }

      Files.move(cacheFile.toPath(), oldFile.toPath());
    }

    Files.move(tempFile.toPath(), cacheFile.toPath());
  }



  /**
   * Indicates whether this trust manager would interactively prompt the user
   * about whether to trust the provided certificate chain.
   *
   * @param  chain  The chain of certificates for which to make the
   *                determination.
   *
   * @return  {@code true} if this trust manger would interactively prompt the
   *          user about whether to trust the certificate chain, or
   *          {@code false} if not (e.g., because the certificate is already
   *          known to be trusted).
   */
  public synchronized boolean wouldPrompt(
                                   @NotNull final X509Certificate[] chain)
  {
    try
    {
      final String cacheKey = getCacheKey(chain[0]);
      return PromptTrustManagerProcessor.shouldPrompt(cacheKey,
           convertChain(chain), false, examineValidityDates, acceptedCerts,
           null).getFirst();
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      return false;
    }
  }



  /**
   * Performs the necessary validity check for the provided certificate array.
   *
   * @param  chain       The chain of certificates for which to make the
   *                     determination.
   * @param  serverCert  Indicates whether the certificate was presented as a
   *                     server certificate or as a client certificate.
   *
   * @throws  CertificateException  If the provided certificate chain should not
   *                                be trusted.
   */
  private synchronized void checkCertificateChain(
                                 @NotNull final X509Certificate[] chain,
                                 final boolean serverCert)
          throws CertificateException
  {
    final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain =
         convertChain(chain);

    final String cacheKey = getCacheKey(chain[0]);
    final ObjectPair> shouldPromptResult =
         PromptTrustManagerProcessor.shouldPrompt(cacheKey, convertedChain,
              serverCert, examineValidityDates, acceptedCerts,
              expectedAddresses);

    if (! shouldPromptResult.getFirst())
    {
      return;
    }

    if (serverCert)
    {
      out.println(INFO_PROMPT_SERVER_HEADING.get());
    }
    else
    {
      out.println(INFO_PROMPT_CLIENT_HEADING.get());
    }

    out.println();
    out.println("     " +
         INFO_PROMPT_SUBJECT.get(convertedChain[0].getSubjectDN()));
    out.println("     " +
         INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate(
              convertedChain[0].getNotBeforeDate())));
    out.println("     " +
         INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate(
              convertedChain[0].getNotAfterDate())));

    try
    {
      final byte[] sha1Fingerprint = convertedChain[0].getSHA1Fingerprint();
      final StringBuilder buffer = new StringBuilder();
      StaticUtils.toHex(sha1Fingerprint, ":", buffer);
      out.println("     " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer));
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
    }
    try
    {
      final byte[] sha256Fingerprint = convertedChain[0].getSHA256Fingerprint();
      final StringBuilder buffer = new StringBuilder();
      StaticUtils.toHex(sha256Fingerprint, ":", buffer);
      out.println("     " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer));
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
    }


    for (int i=1; i < chain.length; i++)
    {
      out.println("     -");
      out.println("     " +
           INFO_PROMPT_ISSUER_SUBJECT.get(i, convertedChain[i].getSubjectDN()));
      out.println("     " +
           INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate(
                convertedChain[i].getNotBeforeDate())));
      out.println("     " +
           INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate(
                convertedChain[i].getNotAfterDate())));

      try
      {
        final byte[] sha1Fingerprint = convertedChain[i].getSHA1Fingerprint();
        final StringBuilder buffer = new StringBuilder();
        StaticUtils.toHex(sha1Fingerprint, ":", buffer);
        out.println("     " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer));
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
      try
      {
        final byte[] sha256Fingerprint =
             convertedChain[i].getSHA256Fingerprint();
        final StringBuilder buffer = new StringBuilder();
        StaticUtils.toHex(sha256Fingerprint, ":", buffer);
        out.println("     " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer));
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }

    for (final String warningMessage : shouldPromptResult.getSecond())
    {
      out.println();
      for (final String line :
           StaticUtils.wrapLine(warningMessage,
                (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)))
      {
        out.println(line);
      }
    }

    final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
    while (true)
    {
      try
      {
        out.println();
        out.print(INFO_PROMPT_MESSAGE.get() + ' ');
        out.flush();
        final String line = reader.readLine();
        if (line == null)
        {
          // The input stream has been closed, so we can't prompt for trust,
          // and should assume it is not trusted.
          throw new CertificateException(
               ERR_CERTIFICATE_REJECTED_BY_END_OF_STREAM.get(
                    SSLUtil.certificateToString(chain[0])));
        }
        else if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes"))
        {
          // The certificate should be considered trusted.
          break;
        }
        else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no"))
        {
          // The certificate should not be trusted.
          throw new CertificateException(
               ERR_CERTIFICATE_REJECTED_BY_USER.get(
                    SSLUtil.certificateToString(chain[0])));
        }
      }
      catch (final CertificateException ce)
      {
        throw ce;
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }

    boolean isOutsideValidityWindow = false;
    for (final com.unboundid.util.ssl.cert.X509Certificate c : convertedChain)
    {
      if (! c.isWithinValidityWindow())
      {
        isOutsideValidityWindow = true;
        break;
      }
    }

    acceptedCerts.put(cacheKey, isOutsideValidityWindow);

    if (acceptedCertsFile != null)
    {
      try
      {
        writeCacheFile();
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }
  }



  /**
   * Indicate whether to prompt about certificates contained in the cache if the
   * current time is outside the validity window for the certificate.
   *
   * @return  {@code true} if the certificate validity time should be examined
   *          for cached certificates and the user should be prompted if they
   *          are expired or not yet valid, or {@code false} if cached
   *          certificates should be accepted even outside of the validity
   *          window.
   */
  public boolean examineValidityDates()
  {
    return examineValidityDates;
  }



  /**
   * Retrieves a list of the addresses that the client is expected to use to
   * communicate with the server, if available.
   *
   * @return  A list of the addresses that the client is expected to use to
   *          communicate with the server, or an empty list if this is not
   *          available or applicable.
   */
  @NotNull()
  public List getExpectedAddresses()
  {
    return expectedAddresses;
  }



  /**
   * Checks to determine whether the provided client certificate chain should be
   * trusted.
   *
   * @param  chain     The client certificate chain for which to make the
   *                   determination.
   * @param  authType  The authentication type based on the client certificate.
   *
   * @throws  CertificateException  If the provided client certificate chain
   *                                should not be trusted.
   */
  @Override()
  public void checkClientTrusted(@NotNull final X509Certificate[] chain,
                                 @NotNull final String authType)
         throws CertificateException
  {
    checkCertificateChain(chain, false);
  }



  /**
   * Checks to determine whether the provided server certificate chain should be
   * trusted.
   *
   * @param  chain     The server certificate chain for which to make the
   *                   determination.
   * @param  authType  The key exchange algorithm used.
   *
   * @throws  CertificateException  If the provided server certificate chain
   *                                should not be trusted.
   */
  @Override()
  public void checkServerTrusted(@NotNull final X509Certificate[] chain,
                                 @NotNull final String authType)
         throws CertificateException
  {
    checkCertificateChain(chain, true);
  }



  /**
   * Retrieves the accepted issuer certificates for this trust manager.  This
   * will always return an empty array.
   *
   * @return  The accepted issuer certificates for this trust manager.
   */
  @Override()
  @NotNull()
  public X509Certificate[] getAcceptedIssuers()
  {
    return NO_CERTIFICATES;
  }



  /**
   * Retrieves the cache key used to identify the provided certificate in the
   * map of accepted certificates.
   *
   * @param  certificate  The certificate for which to get the cache key.
   *
   * @return  The generated cache key.
   */
  @NotNull()
  static String getCacheKey(@NotNull final Certificate certificate)
  {
    final X509Certificate x509Certificate = (X509Certificate) certificate;
    return StaticUtils.toLowerCase(
         StaticUtils.toHex(x509Certificate.getSignature()));
  }



  /**
   * Converts the provided certificate chain from Java's representation of
   * X.509 certificates to the LDAP SDK's version.
   *
   * @param  chain  The chain to be converted.
   *
   * @return  The converted certificate chain.
   *
   * @throws  CertificateException  If a problem occurs while performing the
   *                                conversion.
   */
  @NotNull()
  static com.unboundid.util.ssl.cert.X509Certificate[] convertChain(
              @NotNull final Certificate[] chain)
         throws CertificateException
  {
    final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain =
         new com.unboundid.util.ssl.cert.X509Certificate[chain.length];
    for (int i=0; i < chain.length; i++)
    {
      try
      {
        convertedChain[i] = new com.unboundid.util.ssl.cert.X509Certificate(
             chain[i].getEncoded());
      }
      catch (final CertException ce)
      {
        Debug.debugException(ce);
        throw new CertificateException(ce.getMessage(), ce);
      }
    }

    return convertedChain;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy