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