001/*
002 * Copyright 2008-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 Ping Identity Corporation
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 */
021package com.unboundid.util.ssl;
022
023
024import java.io.BufferedReader;
025import java.io.BufferedWriter;
026import java.io.File;
027import java.io.FileReader;
028import java.io.FileWriter;
029import java.io.InputStream;
030import java.io.InputStreamReader;
031import java.io.IOException;
032import java.io.PrintStream;
033import java.nio.file.Files;
034import java.security.cert.Certificate;
035import java.security.cert.CertificateException;
036import java.security.cert.X509Certificate;
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.Collections;
040import java.util.List;
041import java.util.concurrent.ConcurrentHashMap;
042import javax.net.ssl.X509TrustManager;
043
044import com.unboundid.util.Debug;
045import com.unboundid.util.NotMutable;
046import com.unboundid.util.ObjectPair;
047import com.unboundid.util.StaticUtils;
048import com.unboundid.util.ThreadSafety;
049import com.unboundid.util.ThreadSafetyLevel;
050import com.unboundid.util.ssl.cert.CertException;
051
052import static com.unboundid.util.ssl.SSLMessages.*;
053
054
055
056/**
057 * This class provides an SSL trust manager that will interactively prompt the
058 * user to determine whether to trust any certificate that is presented to it.
059 * It provides the ability to cache information about certificates that had been
060 * previously trusted so that the user is not prompted about the same
061 * certificate repeatedly, and it can be configured to store trusted
062 * certificates in a file so that the trust information can be persisted.
063 */
064@NotMutable()
065@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
066public final class PromptTrustManager
067       implements X509TrustManager
068{
069  /**
070   * A pre-allocated empty certificate array.
071   */
072  private static final X509Certificate[] NO_CERTIFICATES =
073       new X509Certificate[0];
074
075
076
077  // Indicates whether to examine the validity dates for the certificate in
078  // addition to whether the certificate has been previously trusted.
079  private final boolean examineValidityDates;
080
081  // The set of previously-accepted certificates.  The certificates will be
082  // mapped from an all-lowercase hexadecimal string representation of the
083  // certificate signature to a flag that indicates whether the certificate has
084  // already been manually trusted even if it is outside of the validity window.
085  private final ConcurrentHashMap<String,Boolean> acceptedCerts;
086
087  // The input stream from which the user input will be read.
088  private final InputStream in;
089
090  // A list of the addresses that the client is expected to use to connect to
091  // one of the target servers.
092  private final List<String> expectedAddresses;
093
094  // The print stream that will be used to display the prompt.
095  private final PrintStream out;
096
097  // The path to the file to which the set of accepted certificates should be
098  // persisted.
099  private final String acceptedCertsFile;
100
101
102
103  /**
104   * Creates a new instance of this prompt trust manager.  It will cache trust
105   * information in memory but not on disk.
106   */
107  public PromptTrustManager()
108  {
109    this(null, true, null, null);
110  }
111
112
113
114  /**
115   * Creates a new instance of this prompt trust manager.  It may optionally
116   * cache trust information on disk.
117   *
118   * @param  acceptedCertsFile  The path to a file in which the certificates
119   *                            that have been previously accepted will be
120   *                            cached.  It may be {@code null} if the cache
121   *                            should only be maintained in memory.
122   */
123  public PromptTrustManager(final String acceptedCertsFile)
124  {
125    this(acceptedCertsFile, true, null, null);
126  }
127
128
129
130  /**
131   * Creates a new instance of this prompt trust manager.  It may optionally
132   * cache trust information on disk, and may also be configured to examine or
133   * ignore validity dates.
134   *
135   * @param  acceptedCertsFile     The path to a file in which the certificates
136   *                               that have been previously accepted will be
137   *                               cached.  It may be {@code null} if the cache
138   *                               should only be maintained in memory.
139   * @param  examineValidityDates  Indicates whether to reject certificates if
140   *                               the current time is outside the validity
141   *                               window for the certificate.
142   * @param  in                    The input stream that will be used to read
143   *                               input from the user.  If this is {@code null}
144   *                               then {@code System.in} will be used.
145   * @param  out                   The print stream that will be used to display
146   *                               the prompt to the user.  If this is
147   *                               {@code null} then System.out will be used.
148   */
149  public PromptTrustManager(final String acceptedCertsFile,
150                            final boolean examineValidityDates,
151                            final InputStream in, final PrintStream out)
152  {
153    this(acceptedCertsFile, examineValidityDates,
154         Collections.<String>emptyList(), in, out);
155  }
156
157
158
159  /**
160   * Creates a new instance of this prompt trust manager.  It may optionally
161   * cache trust information on disk, and may also be configured to examine or
162   * ignore validity dates.
163   *
164   * @param  acceptedCertsFile     The path to a file in which the certificates
165   *                               that have been previously accepted will be
166   *                               cached.  It may be {@code null} if the cache
167   *                               should only be maintained in memory.
168   * @param  examineValidityDates  Indicates whether to reject certificates if
169   *                               the current time is outside the validity
170   *                               window for the certificate.
171   * @param  expectedAddress       An optional address that the client is
172   *                               expected to use to connect to the target
173   *                               server.  This may be {@code null} if no
174   *                               expected address is available, if this trust
175   *                               manager is only expected to be used to
176   *                               validate client certificates, or if no server
177   *                               address validation should be performed.  If a
178   *                               non-{@code null} value is provided, then the
179   *                               trust manager may issue a warning if the
180   *                               certificate does not contain that address.
181   * @param  in                    The input stream that will be used to read
182   *                               input from the user.  If this is {@code null}
183   *                               then {@code System.in} will be used.
184   * @param  out                   The print stream that will be used to display
185   *                               the prompt to the user.  If this is
186   *                               {@code null} then System.out will be used.
187   */
188  public PromptTrustManager(final String acceptedCertsFile,
189                            final boolean examineValidityDates,
190                            final String expectedAddress, final InputStream in,
191                            final PrintStream out)
192  {
193    this(acceptedCertsFile, examineValidityDates,
194         (expectedAddress == null)
195              ? Collections.<String>emptyList()
196              : Collections.singletonList(expectedAddress),
197         in, out);
198  }
199
200
201
202  /**
203   * Creates a new instance of this prompt trust manager.  It may optionally
204   * cache trust information on disk, and may also be configured to examine or
205   * ignore validity dates.
206   *
207   * @param  acceptedCertsFile     The path to a file in which the certificates
208   *                               that have been previously accepted will be
209   *                               cached.  It may be {@code null} if the cache
210   *                               should only be maintained in memory.
211   * @param  examineValidityDates  Indicates whether to reject certificates if
212   *                               the current time is outside the validity
213   *                               window for the certificate.
214   * @param  expectedAddresses     An optional collection of the addresses that
215   *                               the client is expected to use to connect to
216   *                               one of the target servers.  This may be
217   *                               {@code null} or empty if no expected
218   *                               addresses are available, if this trust
219   *                               manager is only expected to be used to
220   *                               validate client certificates, or if no server
221   *                               address validation should be performed.  If a
222   *                               non-empty collection is provided, then the
223   *                               trust manager may issue a warning if the
224   *                               certificate does not contain any of these
225   *                               addresses.
226   * @param  in                    The input stream that will be used to read
227   *                               input from the user.  If this is {@code null}
228   *                               then {@code System.in} will be used.
229   * @param  out                   The print stream that will be used to display
230   *                               the prompt to the user.  If this is
231   *                               {@code null} then System.out will be used.
232   */
233  public PromptTrustManager(final String acceptedCertsFile,
234                            final boolean examineValidityDates,
235                            final Collection<String> expectedAddresses,
236                            final InputStream in, final PrintStream out)
237  {
238    this.acceptedCertsFile    = acceptedCertsFile;
239    this.examineValidityDates = examineValidityDates;
240
241    if (expectedAddresses == null)
242    {
243      this.expectedAddresses = Collections.emptyList();
244    }
245    else
246    {
247      this.expectedAddresses =
248           Collections.unmodifiableList(new ArrayList<>(expectedAddresses));
249    }
250
251    if (in == null)
252    {
253      this.in = System.in;
254    }
255    else
256    {
257      this.in = in;
258    }
259
260    if (out == null)
261    {
262      this.out = System.out;
263    }
264    else
265    {
266      this.out = out;
267    }
268
269    acceptedCerts = new ConcurrentHashMap<>(20);
270
271    if (acceptedCertsFile != null)
272    {
273      BufferedReader r = null;
274      try
275      {
276        final File f = new File(acceptedCertsFile);
277        if (f.exists())
278        {
279          r = new BufferedReader(new FileReader(f));
280          while (true)
281          {
282            final String line = r.readLine();
283            if (line == null)
284            {
285              break;
286            }
287            acceptedCerts.put(line, false);
288          }
289        }
290      }
291      catch (final Exception e)
292      {
293        Debug.debugException(e);
294      }
295      finally
296      {
297        if (r != null)
298        {
299          try
300          {
301            r.close();
302          }
303          catch (final Exception e)
304          {
305            Debug.debugException(e);
306          }
307        }
308      }
309    }
310  }
311
312
313
314  /**
315   * Writes an updated copy of the trusted certificate cache to disk.
316   *
317   * @throws  IOException  If a problem occurs.
318   */
319  private void writeCacheFile()
320          throws IOException
321  {
322    final File tempFile = new File(acceptedCertsFile + ".new");
323
324    BufferedWriter w = null;
325    try
326    {
327      w = new BufferedWriter(new FileWriter(tempFile));
328
329      for (final String certBytes : acceptedCerts.keySet())
330      {
331        w.write(certBytes);
332        w.newLine();
333      }
334    }
335    finally
336    {
337      if (w != null)
338      {
339        w.close();
340      }
341    }
342
343    final File cacheFile = new File(acceptedCertsFile);
344    if (cacheFile.exists())
345    {
346      final File oldFile = new File(acceptedCertsFile + ".previous");
347      if (oldFile.exists())
348      {
349        Files.delete(oldFile.toPath());
350      }
351
352      Files.move(cacheFile.toPath(), oldFile.toPath());
353    }
354
355    Files.move(tempFile.toPath(), cacheFile.toPath());
356  }
357
358
359
360  /**
361   * Indicates whether this trust manager would interactively prompt the user
362   * about whether to trust the provided certificate chain.
363   *
364   * @param  chain  The chain of certificates for which to make the
365   *                determination.
366   *
367   * @return  {@code true} if this trust manger would interactively prompt the
368   *          user about whether to trust the certificate chain, or
369   *          {@code false} if not (e.g., because the certificate is already
370   *          known to be trusted).
371   */
372  public synchronized boolean wouldPrompt(final X509Certificate[] chain)
373  {
374    try
375    {
376      final String cacheKey = getCacheKey(chain[0]);
377      return PromptTrustManagerProcessor.shouldPrompt(cacheKey,
378           convertChain(chain), false, examineValidityDates, acceptedCerts,
379           null).getFirst();
380    }
381    catch (final Exception e)
382    {
383      Debug.debugException(e);
384      return false;
385    }
386  }
387
388
389
390  /**
391   * Performs the necessary validity check for the provided certificate array.
392   *
393   * @param  chain       The chain of certificates for which to make the
394   *                     determination.
395   * @param  serverCert  Indicates whether the certificate was presented as a
396   *                     server certificate or as a client certificate.
397   *
398   * @throws  CertificateException  If the provided certificate chain should not
399   *                                be trusted.
400   */
401  private synchronized void checkCertificateChain(final X509Certificate[] chain,
402                                                  final boolean serverCert)
403          throws CertificateException
404  {
405    final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain =
406         convertChain(chain);
407
408    final String cacheKey = getCacheKey(chain[0]);
409    final ObjectPair<Boolean,List<String>> shouldPromptResult =
410         PromptTrustManagerProcessor.shouldPrompt(cacheKey, convertedChain,
411              serverCert, examineValidityDates, acceptedCerts,
412              expectedAddresses);
413
414    if (! shouldPromptResult.getFirst())
415    {
416      return;
417    }
418
419    if (serverCert)
420    {
421      out.println(INFO_PROMPT_SERVER_HEADING.get());
422    }
423    else
424    {
425      out.println(INFO_PROMPT_CLIENT_HEADING.get());
426    }
427
428    out.println();
429    out.println("     " +
430         INFO_PROMPT_SUBJECT.get(convertedChain[0].getSubjectDN()));
431    out.println("     " +
432         INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate(
433              convertedChain[0].getNotBeforeDate())));
434    out.println("     " +
435         INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate(
436              convertedChain[0].getNotAfterDate())));
437
438    try
439    {
440      final byte[] sha1Fingerprint = convertedChain[0].getSHA1Fingerprint();
441      final StringBuilder buffer = new StringBuilder();
442      StaticUtils.toHex(sha1Fingerprint, ":", buffer);
443      out.println("     " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer));
444    }
445    catch (final Exception e)
446    {
447      Debug.debugException(e);
448    }
449    try
450    {
451      final byte[] sha256Fingerprint = convertedChain[0].getSHA256Fingerprint();
452      final StringBuilder buffer = new StringBuilder();
453      StaticUtils.toHex(sha256Fingerprint, ":", buffer);
454      out.println("     " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer));
455    }
456    catch (final Exception e)
457    {
458      Debug.debugException(e);
459    }
460
461
462    for (int i=1; i < chain.length; i++)
463    {
464      out.println("     -");
465      out.println("     " +
466           INFO_PROMPT_ISSUER_SUBJECT.get(i, convertedChain[i].getSubjectDN()));
467      out.println("     " +
468           INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate(
469                convertedChain[i].getNotBeforeDate())));
470      out.println("     " +
471           INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate(
472                convertedChain[i].getNotAfterDate())));
473
474      try
475      {
476        final byte[] sha1Fingerprint = convertedChain[i].getSHA1Fingerprint();
477        final StringBuilder buffer = new StringBuilder();
478        StaticUtils.toHex(sha1Fingerprint, ":", buffer);
479        out.println("     " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer));
480      }
481      catch (final Exception e)
482      {
483        Debug.debugException(e);
484      }
485      try
486      {
487        final byte[] sha256Fingerprint =
488             convertedChain[i].getSHA256Fingerprint();
489        final StringBuilder buffer = new StringBuilder();
490        StaticUtils.toHex(sha256Fingerprint, ":", buffer);
491        out.println("     " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer));
492      }
493      catch (final Exception e)
494      {
495        Debug.debugException(e);
496      }
497    }
498
499    for (final String warningMessage : shouldPromptResult.getSecond())
500    {
501      out.println();
502      for (final String line :
503           StaticUtils.wrapLine(warningMessage,
504                (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)))
505      {
506        out.println(line);
507      }
508    }
509
510    final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
511    while (true)
512    {
513      try
514      {
515        out.println();
516        out.print(INFO_PROMPT_MESSAGE.get() + ' ');
517        out.flush();
518        final String line = reader.readLine();
519        if (line == null)
520        {
521          // The input stream has been closed, so we can't prompt for trust,
522          // and should assume it is not trusted.
523          throw new CertificateException(
524               ERR_CERTIFICATE_REJECTED_BY_END_OF_STREAM.get(
525                    SSLUtil.certificateToString(chain[0])));
526        }
527        else if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes"))
528        {
529          // The certificate should be considered trusted.
530          break;
531        }
532        else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no"))
533        {
534          // The certificate should not be trusted.
535          throw new CertificateException(
536               ERR_CERTIFICATE_REJECTED_BY_USER.get(
537                    SSLUtil.certificateToString(chain[0])));
538        }
539      }
540      catch (final CertificateException ce)
541      {
542        throw ce;
543      }
544      catch (final Exception e)
545      {
546        Debug.debugException(e);
547      }
548    }
549
550    boolean isOutsideValidityWindow = false;
551    for (final com.unboundid.util.ssl.cert.X509Certificate c : convertedChain)
552    {
553      if (! c.isWithinValidityWindow())
554      {
555        isOutsideValidityWindow = true;
556        break;
557      }
558    }
559
560    acceptedCerts.put(cacheKey, isOutsideValidityWindow);
561
562    if (acceptedCertsFile != null)
563    {
564      try
565      {
566        writeCacheFile();
567      }
568      catch (final Exception e)
569      {
570        Debug.debugException(e);
571      }
572    }
573  }
574
575
576
577  /**
578   * Indicate whether to prompt about certificates contained in the cache if the
579   * current time is outside the validity window for the certificate.
580   *
581   * @return  {@code true} if the certificate validity time should be examined
582   *          for cached certificates and the user should be prompted if they
583   *          are expired or not yet valid, or {@code false} if cached
584   *          certificates should be accepted even outside of the validity
585   *          window.
586   */
587  public boolean examineValidityDates()
588  {
589    return examineValidityDates;
590  }
591
592
593
594  /**
595   * Retrieves a list of the addresses that the client is expected to use to
596   * communicate with the server, if available.
597   *
598   * @return  A list of the addresses that the client is expected to use to
599   *          communicate with the server, or an empty list if this is not
600   *          available or applicable.
601   */
602  public List<String> getExpectedAddresses()
603  {
604    return expectedAddresses;
605  }
606
607
608
609  /**
610   * Checks to determine whether the provided client certificate chain should be
611   * trusted.
612   *
613   * @param  chain     The client certificate chain for which to make the
614   *                   determination.
615   * @param  authType  The authentication type based on the client certificate.
616   *
617   * @throws  CertificateException  If the provided client certificate chain
618   *                                should not be trusted.
619   */
620  @Override()
621  public void checkClientTrusted(final X509Certificate[] chain,
622                                 final String authType)
623         throws CertificateException
624  {
625    checkCertificateChain(chain, false);
626  }
627
628
629
630  /**
631   * Checks to determine whether the provided server certificate chain should be
632   * trusted.
633   *
634   * @param  chain     The server certificate chain for which to make the
635   *                   determination.
636   * @param  authType  The key exchange algorithm used.
637   *
638   * @throws  CertificateException  If the provided server certificate chain
639   *                                should not be trusted.
640   */
641  @Override()
642  public void checkServerTrusted(final X509Certificate[] chain,
643                                 final String authType)
644         throws CertificateException
645  {
646    checkCertificateChain(chain, true);
647  }
648
649
650
651  /**
652   * Retrieves the accepted issuer certificates for this trust manager.  This
653   * will always return an empty array.
654   *
655   * @return  The accepted issuer certificates for this trust manager.
656   */
657  @Override()
658  public X509Certificate[] getAcceptedIssuers()
659  {
660    return NO_CERTIFICATES;
661  }
662
663
664
665  /**
666   * Retrieves the cache key used to identify the provided certificate in the
667   * map of accepted certificates.
668   *
669   * @param  certificate  The certificate for which to get the cache key.
670   *
671   * @return  The generated cache key.
672   */
673  static String getCacheKey(final Certificate certificate)
674  {
675    final X509Certificate x509Certificate = (X509Certificate) certificate;
676    return StaticUtils.toLowerCase(
677         StaticUtils.toHex(x509Certificate.getSignature()));
678  }
679
680
681
682  /**
683   * Converts the provided certificate chain from Java's representation of
684   * X.509 certificates to the LDAP SDK's version.
685   *
686   * @param  chain  The chain to be converted.
687   *
688   * @return  The converted certificate chain.
689   *
690   * @throws  CertificateException  If a problem occurs while performing the
691   *                                conversion.
692   */
693  static com.unboundid.util.ssl.cert.X509Certificate[]
694             convertChain(final Certificate[] chain)
695         throws CertificateException
696  {
697    final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain =
698         new com.unboundid.util.ssl.cert.X509Certificate[chain.length];
699    for (int i=0; i < chain.length; i++)
700    {
701      try
702      {
703        convertedChain[i] = new com.unboundid.util.ssl.cert.X509Certificate(
704             chain[i].getEncoded());
705      }
706      catch (final CertException ce)
707      {
708        Debug.debugException(ce);
709        throw new CertificateException(ce.getMessage(), ce);
710      }
711    }
712
713    return convertedChain;
714  }
715}