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    import com.unboundid.util.NotMutable;
042    import com.unboundid.util.ThreadSafety;
043    import com.unboundid.util.ThreadSafetyLevel;
044    
045    import static com.unboundid.util.Debug.*;
046    import static com.unboundid.util.StaticUtils.*;
047    import 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)
061    public 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    }