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