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 */ 021package com.unboundid.util.ssl; 022 023 024import java.io.BufferedReader; 025import java.io.BufferedWriter; 026import java.io.File; 027import java.io.FileReader; 028import java.io.FileWriter; 029import java.io.InputStream; 030import java.io.InputStreamReader; 031import java.io.IOException; 032import java.io.PrintStream; 033import java.security.MessageDigest; 034import java.security.cert.CertificateException; 035import java.security.cert.X509Certificate; 036import java.util.Date; 037import java.util.concurrent.ConcurrentHashMap; 038import javax.net.ssl.X509TrustManager; 039import javax.security.auth.x500.X500Principal; 040 041import com.unboundid.util.NotMutable; 042import com.unboundid.util.ThreadSafety; 043import com.unboundid.util.ThreadSafetyLevel; 044 045import static com.unboundid.util.Debug.*; 046import static com.unboundid.util.StaticUtils.*; 047import 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) 061public 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}