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 */
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 }