001/*
002 * Copyright 2008-2017 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2017 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.security.MessageDigest;
034import java.security.cert.CertificateException;
035import java.security.cert.X509Certificate;
036import java.util.Date;
037import java.util.concurrent.ConcurrentHashMap;
038import javax.net.ssl.X509TrustManager;
039import javax.security.auth.x500.X500Principal;
040
041import com.unboundid.util.NotMutable;
042import com.unboundid.util.ThreadSafety;
043import com.unboundid.util.ThreadSafetyLevel;
044
045import static com.unboundid.util.Debug.*;
046import static com.unboundid.util.StaticUtils.*;
047import static com.unboundid.util.ssl.SSLMessages.*;
048
049
050
051/**
052 * This class provides an SSL trust manager that will interactively prompt the
053 * user to determine whether to trust any certificate that is presented to it.
054 * It provides the ability to cache information about certificates that had been
055 * previously trusted so that the user is not prompted about the same
056 * certificate repeatedly, and it can be configured to store trusted
057 * certificates in a file so that the trust information can be persisted.
058 */
059@NotMutable()
060@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
061public final class PromptTrustManager
062       implements X509TrustManager
063{
064  /**
065   * The message digest that will be used for MD5 hashes.
066   */
067  private static final MessageDigest MD5;
068
069
070
071  /**
072   * The message digest that will be used for SHA-1 hashes.
073   */
074  private static final MessageDigest SHA1;
075
076
077
078  /**
079   * The message digest that will be used for 256-bit SHA-2 hashes.
080   */
081  private static final MessageDigest SHA256;
082
083
084
085  /**
086   * A pre-allocated empty certificate array.
087   */
088  private static final X509Certificate[] NO_CERTIFICATES =
089       new X509Certificate[0];
090
091
092
093  static
094  {
095    MessageDigest d = null;
096    try
097    {
098      d = MessageDigest.getInstance("MD5");
099    }
100    catch (final Exception e)
101    {
102      debugException(e);
103      throw new RuntimeException(e);
104    }
105    MD5 = d;
106
107    d = null;
108    try
109    {
110      d = MessageDigest.getInstance("SHA-1");
111    }
112    catch (final Exception e)
113    {
114      debugException(e);
115      throw new RuntimeException(e);
116    }
117    SHA1 = d;
118
119    d = null;
120    try
121    {
122      d = MessageDigest.getInstance("SHA-256");
123    }
124    catch (final Exception e)
125    {
126      debugException(e);
127      throw new RuntimeException(e);
128    }
129    SHA256 = d;
130  }
131
132
133
134  // Indicates whether to examine the validity dates for the certificate in
135  // addition to whether the certificate has been previously trusted.
136  private final boolean examineValidityDates;
137
138  // The set of previously-accepted certificates.  The certificates will be
139  // mapped from an all-lowercase hexadecimal string representation of the
140  // certificate signature to a flag that indicates whether the certificate has
141  // already been manually trusted even if it is outside of the validity window.
142  private final ConcurrentHashMap<String,Boolean> acceptedCerts;
143
144  // The input stream from which the user input will be read.
145  private final InputStream in;
146
147  // The print stream that will be used to display the prompt.
148  private final PrintStream out;
149
150  // The path to the file to which the set of accepted certificates should be
151  // persisted.
152  private final String acceptedCertsFile;
153
154
155
156  /**
157   * Creates a new instance of this prompt trust manager.  It will cache trust
158   * information in memory but not on disk.
159   */
160  public PromptTrustManager()
161  {
162    this(null, true, null, null);
163  }
164
165
166
167  /**
168   * Creates a new instance of this prompt trust manager.  It may optionally
169   * cache trust information on disk.
170   *
171   * @param  acceptedCertsFile  The path to a file in which the certificates
172   *                            that have been previously accepted will be
173   *                            cached.  It may be {@code null} if the cache
174   *                            should only be maintained in memory.
175   */
176  public PromptTrustManager(final String acceptedCertsFile)
177  {
178    this(acceptedCertsFile, true, null, null);
179  }
180
181
182
183  /**
184   * Creates a new instance of this prompt trust manager.  It may optionally
185   * cache trust information on disk, and may also be configured to examine or
186   * ignore validity dates.
187   *
188   * @param  acceptedCertsFile     The path to a file in which the certificates
189   *                               that have been previously accepted will be
190   *                               cached.  It may be {@code null} if the cache
191   *                               should only be maintained in memory.
192   * @param  examineValidityDates  Indicates whether to reject certificates if
193   *                               the current time is outside the validity
194   *                               window for the certificate.
195   * @param  in                    The input stream that will be used to read
196   *                               input from the user.  If this is {@code null}
197   *                               then {@code System.in} will be used.
198   * @param  out                   The print stream that will be used to display
199   *                               the prompt to the user.  If this is
200   *                               {@code null} then System.out will be used.
201   */
202  public PromptTrustManager(final String acceptedCertsFile,
203                            final boolean examineValidityDates,
204                            final InputStream in, final PrintStream out)
205  {
206    this.acceptedCertsFile    = acceptedCertsFile;
207    this.examineValidityDates = examineValidityDates;
208
209    if (in == null)
210    {
211      this.in = System.in;
212    }
213    else
214    {
215      this.in = in;
216    }
217
218    if (out == null)
219    {
220      this.out = System.out;
221    }
222    else
223    {
224      this.out = out;
225    }
226
227    acceptedCerts = new ConcurrentHashMap<String,Boolean>();
228
229    if (acceptedCertsFile != null)
230    {
231      BufferedReader r = null;
232      try
233      {
234        final File f = new File(acceptedCertsFile);
235        if (f.exists())
236        {
237          r = new BufferedReader(new FileReader(f));
238          while (true)
239          {
240            final String line = r.readLine();
241            if (line == null)
242            {
243              break;
244            }
245            acceptedCerts.put(line, false);
246          }
247        }
248      }
249      catch (final Exception e)
250      {
251        debugException(e);
252      }
253      finally
254      {
255        if (r != null)
256        {
257          try
258          {
259            r.close();
260          }
261          catch (final Exception e)
262          {
263            debugException(e);
264          }
265        }
266      }
267    }
268  }
269
270
271
272  /**
273   * Writes an updated copy of the trusted certificate cache to disk.
274   *
275   * @throws  IOException  If a problem occurs.
276   */
277  private void writeCacheFile()
278          throws IOException
279  {
280    final File tempFile = new File(acceptedCertsFile + ".new");
281
282    BufferedWriter w = null;
283    try
284    {
285      w = new BufferedWriter(new FileWriter(tempFile));
286
287      for (final String certBytes : acceptedCerts.keySet())
288      {
289        w.write(certBytes);
290        w.newLine();
291      }
292    }
293    finally
294    {
295      if (w != null)
296      {
297        w.close();
298      }
299    }
300
301    final File cacheFile = new File(acceptedCertsFile);
302    if (cacheFile.exists())
303    {
304      final File oldFile = new File(acceptedCertsFile + ".previous");
305      if (oldFile.exists())
306      {
307        oldFile.delete();
308      }
309
310      cacheFile.renameTo(oldFile);
311    }
312
313    tempFile.renameTo(cacheFile);
314  }
315
316
317
318  /**
319   * Indicates whether this trust manager would interactively prompt the user
320   * about whether to trust the provided certificate chain.
321   *
322   * @param  chain  The chain of certificates for which to make the
323   *                determination.
324   *
325   * @return  {@code true} if this trust manger would interactively prompt the
326   *          user about whether to trust the certificate chain, or
327   *          {@code false} if not (e.g., because the certificate is already
328   *          known to be trusted).
329   */
330  public synchronized boolean wouldPrompt(final X509Certificate[] chain)
331  {
332    // See if the certificate is in the cache.  If it isn't then we will
333    // prompt no matter what.
334    final X509Certificate c = chain[0];
335    final String certBytes = toLowerCase(toHex(c.getSignature()));
336    final Boolean acceptedRegardlessOfValidity = acceptedCerts.get(certBytes);
337    if (acceptedRegardlessOfValidity == null)
338    {
339      return true;
340    }
341
342
343    // If we shouldn't check validity dates, or if the certificate has already
344    // been accepted when it's outside the validity window, then we won't
345    // prompt.
346    if (acceptedRegardlessOfValidity || (! examineValidityDates))
347    {
348      return false;
349    }
350
351
352    // If the certificate is within the validity window, then we won't prompt.
353    // If it's outside the validity window, then we will prompt to make sure the
354    // user still wants to trust it.
355    final Date currentDate = new Date();
356    return (! (currentDate.before(c.getNotBefore()) ||
357               currentDate.after(c.getNotAfter())));
358  }
359
360
361
362  /**
363   * Performs the necessary validity check for the provided certificate array.
364   *
365   * @param  chain       The chain of certificates for which to make the
366   *                     determination.
367   * @param  serverCert  Indicates whether the certificate was presented as a
368   *                     server certificate or as a client certificate.
369   *
370   * @throws  CertificateException  If the provided certificate chain should not
371   *                                be trusted.
372   */
373  private synchronized void checkCertificateChain(final X509Certificate[] chain,
374                                                  final boolean serverCert)
375          throws CertificateException
376  {
377    // See if the certificate is currently within the validity window.
378    String validityWarning = null;
379    final Date currentDate = new Date();
380    final X509Certificate c = chain[0];
381    if (examineValidityDates)
382    {
383      if (currentDate.before(c.getNotBefore()))
384      {
385        validityWarning = WARN_PROMPT_NOT_YET_VALID.get();
386      }
387      else if (currentDate.after(c.getNotAfter()))
388      {
389        validityWarning = WARN_PROMPT_EXPIRED.get();
390      }
391    }
392
393
394    // If the certificate is within the validity window, or if we don't care
395    // about validity dates, then see if it's in the cache.
396    if ((! examineValidityDates) || (validityWarning == null))
397    {
398      final String certBytes = toLowerCase(toHex(c.getSignature()));
399      final Boolean accepted = acceptedCerts.get(certBytes);
400      if (accepted != null)
401      {
402        if ((validityWarning == null) || (! examineValidityDates) ||
403            Boolean.TRUE.equals(accepted))
404        {
405          // The certificate was found in the cache.  It's either in the
406          // validity window, we don't care about the validity window, or has
407          // already been manually trusted outside of the validity window.
408          // We'll consider it trusted without the need to re-prompt.
409          return;
410        }
411      }
412    }
413
414
415    // If we've gotten here, then we need to display a prompt to the user.
416    if (serverCert)
417    {
418      out.println(INFO_PROMPT_SERVER_HEADING.get());
419    }
420    else
421    {
422      out.println(INFO_PROMPT_CLIENT_HEADING.get());
423    }
424
425    out.println('\t' + INFO_PROMPT_SUBJECT.get(
426         c.getSubjectX500Principal().getName(X500Principal.CANONICAL)));
427    out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get(
428         getFingerprint(c, MD5)));
429    out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get(
430         getFingerprint(c, SHA1)));
431    out.println("\t\t" + INFO_PROMPT_SHA256_FINGERPRINT.get(
432         getFingerprint(c, SHA256)));
433
434    for (int i=1; i < chain.length; i++)
435    {
436      out.println('\t' + INFO_PROMPT_ISSUER_SUBJECT.get(i,
437           chain[i].getSubjectX500Principal().getName(
438                X500Principal.CANONICAL)));
439      out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get(
440           getFingerprint(chain[i], MD5)));
441      out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get(
442           getFingerprint(chain[i], SHA1)));
443    }
444
445    out.println(INFO_PROMPT_VALIDITY.get(String.valueOf(c.getNotBefore()),
446         String.valueOf(c.getNotAfter())));
447
448    if (chain.length == 1)
449    {
450      out.println();
451      out.println(WARN_PROMPT_SELF_SIGNED.get());
452    }
453
454    if (validityWarning != null)
455    {
456      out.println();
457      out.println(validityWarning);
458    }
459
460    final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
461    while (true)
462    {
463      try
464      {
465        out.println();
466        out.print(INFO_PROMPT_MESSAGE.get());
467        out.flush();
468        final String line = reader.readLine();
469        if (line == null)
470        {
471          // The input stream has been closed, so we can't prompt for trust,
472          // and should assume it is not trusted.
473          throw new CertificateException(
474               ERR_CERTIFICATE_REJECTED_BY_END_OF_STREAM.get(
475                    SSLUtil.certificateToString(chain[0])));
476        }
477        else if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes"))
478        {
479          // The certificate should be considered trusted.
480          break;
481        }
482        else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no"))
483        {
484          // The certificate should not be trusted.
485          throw new CertificateException(
486               ERR_CERTIFICATE_REJECTED_BY_USER.get(
487                    SSLUtil.certificateToString(chain[0])));
488        }
489      }
490      catch (final CertificateException ce)
491      {
492        throw ce;
493      }
494      catch (final Exception e)
495      {
496        debugException(e);
497      }
498    }
499
500    final String certBytes = toLowerCase(toHex(c.getSignature()));
501    acceptedCerts.put(certBytes, (validityWarning != null));
502
503    if (acceptedCertsFile != null)
504    {
505      try
506      {
507        writeCacheFile();
508      }
509      catch (final Exception e)
510      {
511        debugException(e);
512      }
513    }
514  }
515
516
517
518  /**
519   * Computes the fingerprint for the provided certificate using the given
520   * digest.
521   *
522   * @param  c  The certificate for which to obtain the fingerprint.
523   * @param  d  The message digest to use when creating the fingerprint.
524   *
525   * @return  The generated certificate fingerprint.
526   *
527   * @throws  CertificateException  If a problem is encountered while generating
528   *                                the certificate fingerprint.
529   */
530  private static String getFingerprint(final X509Certificate c,
531                                       final MessageDigest d)
532          throws CertificateException
533  {
534    final byte[] encodedCertBytes = c.getEncoded();
535
536    final byte[] digestBytes;
537    synchronized (d)
538    {
539      digestBytes = d.digest(encodedCertBytes);
540    }
541
542    final StringBuilder buffer = new StringBuilder(3 * encodedCertBytes.length);
543    toHex(digestBytes, ":", buffer);
544    return buffer.toString();
545  }
546
547
548
549  /**
550   * Indicate whether to prompt about certificates contained in the cache if the
551   * current time is outside the validity window for the certificate.
552   *
553   * @return  {@code true} if the certificate validity time should be examined
554   *          for cached certificates and the user should be prompted if they
555   *          are expired or not yet valid, or {@code false} if cached
556   *          certificates should be accepted even outside of the validity
557   *          window.
558   */
559  public boolean examineValidityDates()
560  {
561    return examineValidityDates;
562  }
563
564
565
566  /**
567   * Checks to determine whether the provided client certificate chain should be
568   * trusted.
569   *
570   * @param  chain     The client certificate chain for which to make the
571   *                   determination.
572   * @param  authType  The authentication type based on the client certificate.
573   *
574   * @throws  CertificateException  If the provided client certificate chain
575   *                                should not be trusted.
576   */
577  @Override()
578  public void checkClientTrusted(final X509Certificate[] chain,
579                                 final String authType)
580         throws CertificateException
581  {
582    checkCertificateChain(chain, false);
583  }
584
585
586
587  /**
588   * Checks to determine whether the provided server certificate chain should be
589   * trusted.
590   *
591   * @param  chain     The server certificate chain for which to make the
592   *                   determination.
593   * @param  authType  The key exchange algorithm used.
594   *
595   * @throws  CertificateException  If the provided server certificate chain
596   *                                should not be trusted.
597   */
598  @Override()
599  public void checkServerTrusted(final X509Certificate[] chain,
600                                 final String authType)
601         throws CertificateException
602  {
603    checkCertificateChain(chain, true);
604  }
605
606
607
608  /**
609   * Retrieves the accepted issuer certificates for this trust manager.  This
610   * will always return an empty array.
611   *
612   * @return  The accepted issuer certificates for this trust manager.
613   */
614  @Override()
615  public X509Certificate[] getAcceptedIssuers()
616  {
617    return NO_CERTIFICATES;
618  }
619}