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 }