001    /*
002     * Copyright 2008-2013 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2013 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       * Performs the necessary validity check for the provided certificate array.
293       *
294       * @param  chain       The chain of certificates for which to make the
295       *                     determination.
296       * @param  serverCert  Indicates whether the certificate was presented as a
297       *                     server certificate or as a client certificate.
298       *
299       * @throws  CertificateException  If the provided certificate chain should not
300       *                                be trusted.
301       */
302      private synchronized void checkCertificateChain(final X509Certificate[] chain,
303                                                      final boolean serverCert)
304              throws CertificateException
305      {
306        // See if the certificate is currently within the validity window.
307        String validityWarning = null;
308        final Date currentDate = new Date();
309        final X509Certificate c = chain[0];
310        if (examineValidityDates)
311        {
312          if (currentDate.before(c.getNotBefore()))
313          {
314            validityWarning = WARN_PROMPT_NOT_YET_VALID.get();
315          }
316          else if (currentDate.after(c.getNotAfter()))
317          {
318            validityWarning = WARN_PROMPT_EXPIRED.get();
319          }
320        }
321    
322    
323        // If the certificate is within the validity window, or if we don't care
324        // about validity dates, then see if it's in the cache.
325        if ((! examineValidityDates) || (validityWarning == null))
326        {
327          final String certBytes = toLowerCase(toHex(c.getSignature()));
328          final Boolean accepted = acceptedCerts.get(certBytes);
329          if (accepted != null)
330          {
331            if ((validityWarning == null) || (! examineValidityDates) ||
332                Boolean.TRUE.equals(accepted))
333            {
334              // The certificate was found in the cache.  It's either in the
335              // validity window, we don't care about the validity window, or has
336              // already been manually trusted outside of the validity window.
337              // We'll consider it trusted without the need to re-prompt.
338              return;
339            }
340          }
341        }
342    
343    
344        // If we've gotten here, then we need to display a prompt to the user.
345        if (serverCert)
346        {
347          out.println(INFO_PROMPT_SERVER_HEADING.get());
348        }
349        else
350        {
351          out.println(INFO_PROMPT_CLIENT_HEADING.get());
352        }
353    
354        out.println('\t' + INFO_PROMPT_SUBJECT.get(
355             c.getSubjectX500Principal().getName(X500Principal.CANONICAL)));
356        out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get(
357             getFingerprint(c, MD5)));
358        out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get(
359             getFingerprint(c, SHA1)));
360    
361        for (int i=1; i < chain.length; i++)
362        {
363          out.println('\t' + INFO_PROMPT_ISSUER_SUBJECT.get(i,
364               chain[i].getSubjectX500Principal().getName(
365                    X500Principal.CANONICAL)));
366          out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get(
367               getFingerprint(chain[i], MD5)));
368          out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get(
369               getFingerprint(chain[i], SHA1)));
370        }
371    
372        out.println(INFO_PROMPT_VALIDITY.get(String.valueOf(c.getNotBefore()),
373             String.valueOf(c.getNotAfter())));
374    
375        if (chain.length == 1)
376        {
377          out.println();
378          out.println(WARN_PROMPT_SELF_SIGNED.get());
379        }
380    
381        if (validityWarning != null)
382        {
383          out.println();
384          out.println(validityWarning);
385        }
386    
387        final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
388        while (true)
389        {
390          try
391          {
392            out.println();
393            out.println(INFO_PROMPT_MESSAGE.get());
394            out.flush();
395            final String line = reader.readLine();
396            if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes"))
397            {
398              // The certificate should be considered trusted.
399              break;
400            }
401            else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no"))
402            {
403              // The certificate should not be trusted.
404              throw new CertificateException(
405                   ERR_CERTIFICATE_REJECTED_BY_USER.get());
406            }
407          }
408          catch (CertificateException ce)
409          {
410            throw ce;
411          }
412          catch (Exception e)
413          {
414            debugException(e);
415          }
416        }
417    
418        final String certBytes = toLowerCase(toHex(c.getSignature()));
419        acceptedCerts.put(certBytes, (validityWarning != null));
420    
421        if (acceptedCertsFile != null)
422        {
423          try
424          {
425            writeCacheFile();
426          }
427          catch (Exception e)
428          {
429            debugException(e);
430          }
431        }
432      }
433    
434    
435    
436      /**
437       * Computes the fingerprint for the provided certificate using the given
438       * digest.
439       *
440       * @param  c  The certificate for which to obtain the fingerprint.
441       * @param  d  The message digest to use when creating the fingerprint.
442       *
443       * @return  The generated certificate fingerprint.
444       *
445       * @throws  CertificateException  If a problem is encountered while generating
446       *                                the certificate fingerprint.
447       */
448      private static String getFingerprint(final X509Certificate c,
449                                           final MessageDigest d)
450              throws CertificateException
451      {
452        final byte[] encodedCertBytes = c.getEncoded();
453    
454        final byte[] digestBytes;
455        synchronized (d)
456        {
457          digestBytes = d.digest(encodedCertBytes);
458        }
459    
460        final StringBuilder buffer = new StringBuilder(3 * encodedCertBytes.length);
461        toHex(digestBytes, ":", buffer);
462        return buffer.toString();
463      }
464    
465    
466    
467      /**
468       * Indicate whether to prompt about certificates contained in the cache if the
469       * current time is outside the validity window for the certificate.
470       *
471       * @return  {@code true} if the certificate validity time should be examined
472       *          for cached certificates and the user should be prompted if they
473       *          are expired or not yet valid, or {@code false} if cached
474       *          certificates should be accepted even outside of the validity
475       *          window.
476       */
477      public boolean examineValidityDates()
478      {
479        return examineValidityDates;
480      }
481    
482    
483    
484      /**
485       * Checks to determine whether the provided client certificate chain should be
486       * trusted.
487       *
488       * @param  chain     The client certificate chain for which to make the
489       *                   determination.
490       * @param  authType  The authentication type based on the client certificate.
491       *
492       * @throws  CertificateException  If the provided client certificate chain
493       *                                should not be trusted.
494       */
495      public void checkClientTrusted(final X509Certificate[] chain,
496                                     final String authType)
497             throws CertificateException
498      {
499        checkCertificateChain(chain, false);
500      }
501    
502    
503    
504      /**
505       * Checks to determine whether the provided server certificate chain should be
506       * trusted.
507       *
508       * @param  chain     The server certificate chain for which to make the
509       *                   determination.
510       * @param  authType  The key exchange algorithm used.
511       *
512       * @throws  CertificateException  If the provided server certificate chain
513       *                                should not be trusted.
514       */
515      public void checkServerTrusted(final X509Certificate[] chain,
516                                     final String authType)
517             throws CertificateException
518      {
519        checkCertificateChain(chain, true);
520      }
521    
522    
523    
524      /**
525       * Retrieves the accepted issuer certificates for this trust manager.  This
526       * will always return an empty array.
527       *
528       * @return  The accepted issuer certificates for this trust manager.
529       */
530      public X509Certificate[] getAcceptedIssuers()
531      {
532        return new X509Certificate[0];
533      }
534    }