001 /*
002 * Copyright 2008-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 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 }