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