001/* 002 * Copyright 2008-2017 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 Ping Identity Corporation 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 /** 079 * The message digest that will be used for 256-bit SHA-2 hashes. 080 */ 081 private static final MessageDigest SHA256; 082 083 084 085 /** 086 * A pre-allocated empty certificate array. 087 */ 088 private static final X509Certificate[] NO_CERTIFICATES = 089 new X509Certificate[0]; 090 091 092 093 static 094 { 095 MessageDigest d = null; 096 try 097 { 098 d = MessageDigest.getInstance("MD5"); 099 } 100 catch (final Exception e) 101 { 102 debugException(e); 103 throw new RuntimeException(e); 104 } 105 MD5 = d; 106 107 d = null; 108 try 109 { 110 d = MessageDigest.getInstance("SHA-1"); 111 } 112 catch (final Exception e) 113 { 114 debugException(e); 115 throw new RuntimeException(e); 116 } 117 SHA1 = d; 118 119 d = null; 120 try 121 { 122 d = MessageDigest.getInstance("SHA-256"); 123 } 124 catch (final Exception e) 125 { 126 debugException(e); 127 throw new RuntimeException(e); 128 } 129 SHA256 = d; 130 } 131 132 133 134 // Indicates whether to examine the validity dates for the certificate in 135 // addition to whether the certificate has been previously trusted. 136 private final boolean examineValidityDates; 137 138 // The set of previously-accepted certificates. The certificates will be 139 // mapped from an all-lowercase hexadecimal string representation of the 140 // certificate signature to a flag that indicates whether the certificate has 141 // already been manually trusted even if it is outside of the validity window. 142 private final ConcurrentHashMap<String,Boolean> acceptedCerts; 143 144 // The input stream from which the user input will be read. 145 private final InputStream in; 146 147 // The print stream that will be used to display the prompt. 148 private final PrintStream out; 149 150 // The path to the file to which the set of accepted certificates should be 151 // persisted. 152 private final String acceptedCertsFile; 153 154 155 156 /** 157 * Creates a new instance of this prompt trust manager. It will cache trust 158 * information in memory but not on disk. 159 */ 160 public PromptTrustManager() 161 { 162 this(null, true, null, null); 163 } 164 165 166 167 /** 168 * Creates a new instance of this prompt trust manager. It may optionally 169 * cache trust information on disk. 170 * 171 * @param acceptedCertsFile The path to a file in which the certificates 172 * that have been previously accepted will be 173 * cached. It may be {@code null} if the cache 174 * should only be maintained in memory. 175 */ 176 public PromptTrustManager(final String acceptedCertsFile) 177 { 178 this(acceptedCertsFile, true, null, null); 179 } 180 181 182 183 /** 184 * Creates a new instance of this prompt trust manager. It may optionally 185 * cache trust information on disk, and may also be configured to examine or 186 * ignore validity dates. 187 * 188 * @param acceptedCertsFile The path to a file in which the certificates 189 * that have been previously accepted will be 190 * cached. It may be {@code null} if the cache 191 * should only be maintained in memory. 192 * @param examineValidityDates Indicates whether to reject certificates if 193 * the current time is outside the validity 194 * window for the certificate. 195 * @param in The input stream that will be used to read 196 * input from the user. If this is {@code null} 197 * then {@code System.in} will be used. 198 * @param out The print stream that will be used to display 199 * the prompt to the user. If this is 200 * {@code null} then System.out will be used. 201 */ 202 public PromptTrustManager(final String acceptedCertsFile, 203 final boolean examineValidityDates, 204 final InputStream in, final PrintStream out) 205 { 206 this.acceptedCertsFile = acceptedCertsFile; 207 this.examineValidityDates = examineValidityDates; 208 209 if (in == null) 210 { 211 this.in = System.in; 212 } 213 else 214 { 215 this.in = in; 216 } 217 218 if (out == null) 219 { 220 this.out = System.out; 221 } 222 else 223 { 224 this.out = out; 225 } 226 227 acceptedCerts = new ConcurrentHashMap<String,Boolean>(); 228 229 if (acceptedCertsFile != null) 230 { 231 BufferedReader r = null; 232 try 233 { 234 final File f = new File(acceptedCertsFile); 235 if (f.exists()) 236 { 237 r = new BufferedReader(new FileReader(f)); 238 while (true) 239 { 240 final String line = r.readLine(); 241 if (line == null) 242 { 243 break; 244 } 245 acceptedCerts.put(line, false); 246 } 247 } 248 } 249 catch (final Exception e) 250 { 251 debugException(e); 252 } 253 finally 254 { 255 if (r != null) 256 { 257 try 258 { 259 r.close(); 260 } 261 catch (final Exception e) 262 { 263 debugException(e); 264 } 265 } 266 } 267 } 268 } 269 270 271 272 /** 273 * Writes an updated copy of the trusted certificate cache to disk. 274 * 275 * @throws IOException If a problem occurs. 276 */ 277 private void writeCacheFile() 278 throws IOException 279 { 280 final File tempFile = new File(acceptedCertsFile + ".new"); 281 282 BufferedWriter w = null; 283 try 284 { 285 w = new BufferedWriter(new FileWriter(tempFile)); 286 287 for (final String certBytes : acceptedCerts.keySet()) 288 { 289 w.write(certBytes); 290 w.newLine(); 291 } 292 } 293 finally 294 { 295 if (w != null) 296 { 297 w.close(); 298 } 299 } 300 301 final File cacheFile = new File(acceptedCertsFile); 302 if (cacheFile.exists()) 303 { 304 final File oldFile = new File(acceptedCertsFile + ".previous"); 305 if (oldFile.exists()) 306 { 307 oldFile.delete(); 308 } 309 310 cacheFile.renameTo(oldFile); 311 } 312 313 tempFile.renameTo(cacheFile); 314 } 315 316 317 318 /** 319 * Indicates whether this trust manager would interactively prompt the user 320 * about whether to trust the provided certificate chain. 321 * 322 * @param chain The chain of certificates for which to make the 323 * determination. 324 * 325 * @return {@code true} if this trust manger would interactively prompt the 326 * user about whether to trust the certificate chain, or 327 * {@code false} if not (e.g., because the certificate is already 328 * known to be trusted). 329 */ 330 public synchronized boolean wouldPrompt(final X509Certificate[] chain) 331 { 332 // See if the certificate is in the cache. If it isn't then we will 333 // prompt no matter what. 334 final X509Certificate c = chain[0]; 335 final String certBytes = toLowerCase(toHex(c.getSignature())); 336 final Boolean acceptedRegardlessOfValidity = acceptedCerts.get(certBytes); 337 if (acceptedRegardlessOfValidity == null) 338 { 339 return true; 340 } 341 342 343 // If we shouldn't check validity dates, or if the certificate has already 344 // been accepted when it's outside the validity window, then we won't 345 // prompt. 346 if (acceptedRegardlessOfValidity || (! examineValidityDates)) 347 { 348 return false; 349 } 350 351 352 // If the certificate is within the validity window, then we won't prompt. 353 // If it's outside the validity window, then we will prompt to make sure the 354 // user still wants to trust it. 355 final Date currentDate = new Date(); 356 return (! (currentDate.before(c.getNotBefore()) || 357 currentDate.after(c.getNotAfter()))); 358 } 359 360 361 362 /** 363 * Performs the necessary validity check for the provided certificate array. 364 * 365 * @param chain The chain of certificates for which to make the 366 * determination. 367 * @param serverCert Indicates whether the certificate was presented as a 368 * server certificate or as a client certificate. 369 * 370 * @throws CertificateException If the provided certificate chain should not 371 * be trusted. 372 */ 373 private synchronized void checkCertificateChain(final X509Certificate[] chain, 374 final boolean serverCert) 375 throws CertificateException 376 { 377 // See if the certificate is currently within the validity window. 378 String validityWarning = null; 379 final Date currentDate = new Date(); 380 final X509Certificate c = chain[0]; 381 if (examineValidityDates) 382 { 383 if (currentDate.before(c.getNotBefore())) 384 { 385 validityWarning = WARN_PROMPT_NOT_YET_VALID.get(); 386 } 387 else if (currentDate.after(c.getNotAfter())) 388 { 389 validityWarning = WARN_PROMPT_EXPIRED.get(); 390 } 391 } 392 393 394 // If the certificate is within the validity window, or if we don't care 395 // about validity dates, then see if it's in the cache. 396 if ((! examineValidityDates) || (validityWarning == null)) 397 { 398 final String certBytes = toLowerCase(toHex(c.getSignature())); 399 final Boolean accepted = acceptedCerts.get(certBytes); 400 if (accepted != null) 401 { 402 if ((validityWarning == null) || (! examineValidityDates) || 403 Boolean.TRUE.equals(accepted)) 404 { 405 // The certificate was found in the cache. It's either in the 406 // validity window, we don't care about the validity window, or has 407 // already been manually trusted outside of the validity window. 408 // We'll consider it trusted without the need to re-prompt. 409 return; 410 } 411 } 412 } 413 414 415 // If we've gotten here, then we need to display a prompt to the user. 416 if (serverCert) 417 { 418 out.println(INFO_PROMPT_SERVER_HEADING.get()); 419 } 420 else 421 { 422 out.println(INFO_PROMPT_CLIENT_HEADING.get()); 423 } 424 425 out.println('\t' + INFO_PROMPT_SUBJECT.get( 426 c.getSubjectX500Principal().getName(X500Principal.CANONICAL))); 427 out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get( 428 getFingerprint(c, MD5))); 429 out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get( 430 getFingerprint(c, SHA1))); 431 out.println("\t\t" + INFO_PROMPT_SHA256_FINGERPRINT.get( 432 getFingerprint(c, SHA256))); 433 434 for (int i=1; i < chain.length; i++) 435 { 436 out.println('\t' + INFO_PROMPT_ISSUER_SUBJECT.get(i, 437 chain[i].getSubjectX500Principal().getName( 438 X500Principal.CANONICAL))); 439 out.println("\t\t" + INFO_PROMPT_MD5_FINGERPRINT.get( 440 getFingerprint(chain[i], MD5))); 441 out.println("\t\t" + INFO_PROMPT_SHA1_FINGERPRINT.get( 442 getFingerprint(chain[i], SHA1))); 443 } 444 445 out.println(INFO_PROMPT_VALIDITY.get(String.valueOf(c.getNotBefore()), 446 String.valueOf(c.getNotAfter()))); 447 448 if (chain.length == 1) 449 { 450 out.println(); 451 out.println(WARN_PROMPT_SELF_SIGNED.get()); 452 } 453 454 if (validityWarning != null) 455 { 456 out.println(); 457 out.println(validityWarning); 458 } 459 460 final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); 461 while (true) 462 { 463 try 464 { 465 out.println(); 466 out.print(INFO_PROMPT_MESSAGE.get()); 467 out.flush(); 468 final String line = reader.readLine(); 469 if (line == null) 470 { 471 // The input stream has been closed, so we can't prompt for trust, 472 // and should assume it is not trusted. 473 throw new CertificateException( 474 ERR_CERTIFICATE_REJECTED_BY_END_OF_STREAM.get( 475 SSLUtil.certificateToString(chain[0]))); 476 } 477 else if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes")) 478 { 479 // The certificate should be considered trusted. 480 break; 481 } 482 else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no")) 483 { 484 // The certificate should not be trusted. 485 throw new CertificateException( 486 ERR_CERTIFICATE_REJECTED_BY_USER.get( 487 SSLUtil.certificateToString(chain[0]))); 488 } 489 } 490 catch (final CertificateException ce) 491 { 492 throw ce; 493 } 494 catch (final Exception e) 495 { 496 debugException(e); 497 } 498 } 499 500 final String certBytes = toLowerCase(toHex(c.getSignature())); 501 acceptedCerts.put(certBytes, (validityWarning != null)); 502 503 if (acceptedCertsFile != null) 504 { 505 try 506 { 507 writeCacheFile(); 508 } 509 catch (final Exception e) 510 { 511 debugException(e); 512 } 513 } 514 } 515 516 517 518 /** 519 * Computes the fingerprint for the provided certificate using the given 520 * digest. 521 * 522 * @param c The certificate for which to obtain the fingerprint. 523 * @param d The message digest to use when creating the fingerprint. 524 * 525 * @return The generated certificate fingerprint. 526 * 527 * @throws CertificateException If a problem is encountered while generating 528 * the certificate fingerprint. 529 */ 530 private static String getFingerprint(final X509Certificate c, 531 final MessageDigest d) 532 throws CertificateException 533 { 534 final byte[] encodedCertBytes = c.getEncoded(); 535 536 final byte[] digestBytes; 537 synchronized (d) 538 { 539 digestBytes = d.digest(encodedCertBytes); 540 } 541 542 final StringBuilder buffer = new StringBuilder(3 * encodedCertBytes.length); 543 toHex(digestBytes, ":", buffer); 544 return buffer.toString(); 545 } 546 547 548 549 /** 550 * Indicate whether to prompt about certificates contained in the cache if the 551 * current time is outside the validity window for the certificate. 552 * 553 * @return {@code true} if the certificate validity time should be examined 554 * for cached certificates and the user should be prompted if they 555 * are expired or not yet valid, or {@code false} if cached 556 * certificates should be accepted even outside of the validity 557 * window. 558 */ 559 public boolean examineValidityDates() 560 { 561 return examineValidityDates; 562 } 563 564 565 566 /** 567 * Checks to determine whether the provided client certificate chain should be 568 * trusted. 569 * 570 * @param chain The client certificate chain for which to make the 571 * determination. 572 * @param authType The authentication type based on the client certificate. 573 * 574 * @throws CertificateException If the provided client certificate chain 575 * should not be trusted. 576 */ 577 @Override() 578 public void checkClientTrusted(final X509Certificate[] chain, 579 final String authType) 580 throws CertificateException 581 { 582 checkCertificateChain(chain, false); 583 } 584 585 586 587 /** 588 * Checks to determine whether the provided server certificate chain should be 589 * trusted. 590 * 591 * @param chain The server certificate chain for which to make the 592 * determination. 593 * @param authType The key exchange algorithm used. 594 * 595 * @throws CertificateException If the provided server certificate chain 596 * should not be trusted. 597 */ 598 @Override() 599 public void checkServerTrusted(final X509Certificate[] chain, 600 final String authType) 601 throws CertificateException 602 { 603 checkCertificateChain(chain, true); 604 } 605 606 607 608 /** 609 * Retrieves the accepted issuer certificates for this trust manager. This 610 * will always return an empty array. 611 * 612 * @return The accepted issuer certificates for this trust manager. 613 */ 614 @Override() 615 public X509Certificate[] getAcceptedIssuers() 616 { 617 return NO_CERTIFICATES; 618 } 619}