001/* 002 * Copyright 2008-2018 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2018 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.cert.Certificate; 034import java.security.cert.CertificateException; 035import java.security.cert.X509Certificate; 036import java.util.ArrayList; 037import java.util.Collection; 038import java.util.Collections; 039import java.util.List; 040import java.util.concurrent.ConcurrentHashMap; 041import javax.net.ssl.X509TrustManager; 042 043import com.unboundid.util.Debug; 044import com.unboundid.util.NotMutable; 045import com.unboundid.util.ObjectPair; 046import com.unboundid.util.ThreadSafety; 047import com.unboundid.util.ThreadSafetyLevel; 048import com.unboundid.util.ssl.cert.CertException; 049 050import static com.unboundid.util.Debug.*; 051import static com.unboundid.util.StaticUtils.*; 052import static com.unboundid.util.ssl.SSLMessages.*; 053 054 055 056/** 057 * This class provides an SSL trust manager that will interactively prompt the 058 * user to determine whether to trust any certificate that is presented to it. 059 * It provides the ability to cache information about certificates that had been 060 * previously trusted so that the user is not prompted about the same 061 * certificate repeatedly, and it can be configured to store trusted 062 * certificates in a file so that the trust information can be persisted. 063 */ 064@NotMutable() 065@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 066public final class PromptTrustManager 067 implements X509TrustManager 068{ 069 /** 070 * A pre-allocated empty certificate array. 071 */ 072 private static final X509Certificate[] NO_CERTIFICATES = 073 new X509Certificate[0]; 074 075 076 077 // Indicates whether to examine the validity dates for the certificate in 078 // addition to whether the certificate has been previously trusted. 079 private final boolean examineValidityDates; 080 081 // The set of previously-accepted certificates. The certificates will be 082 // mapped from an all-lowercase hexadecimal string representation of the 083 // certificate signature to a flag that indicates whether the certificate has 084 // already been manually trusted even if it is outside of the validity window. 085 private final ConcurrentHashMap<String,Boolean> acceptedCerts; 086 087 // The input stream from which the user input will be read. 088 private final InputStream in; 089 090 // A list of the addresses that the client is expected to use to connect to 091 // one of the target servers. 092 private final List<String> expectedAddresses; 093 094 // The print stream that will be used to display the prompt. 095 private final PrintStream out; 096 097 // The path to the file to which the set of accepted certificates should be 098 // persisted. 099 private final String acceptedCertsFile; 100 101 102 103 /** 104 * Creates a new instance of this prompt trust manager. It will cache trust 105 * information in memory but not on disk. 106 */ 107 public PromptTrustManager() 108 { 109 this(null, true, null, null); 110 } 111 112 113 114 /** 115 * Creates a new instance of this prompt trust manager. It may optionally 116 * cache trust information on disk. 117 * 118 * @param acceptedCertsFile The path to a file in which the certificates 119 * that have been previously accepted will be 120 * cached. It may be {@code null} if the cache 121 * should only be maintained in memory. 122 */ 123 public PromptTrustManager(final String acceptedCertsFile) 124 { 125 this(acceptedCertsFile, true, null, null); 126 } 127 128 129 130 /** 131 * Creates a new instance of this prompt trust manager. It may optionally 132 * cache trust information on disk, and may also be configured to examine or 133 * ignore validity dates. 134 * 135 * @param acceptedCertsFile The path to a file in which the certificates 136 * that have been previously accepted will be 137 * cached. It may be {@code null} if the cache 138 * should only be maintained in memory. 139 * @param examineValidityDates Indicates whether to reject certificates if 140 * the current time is outside the validity 141 * window for the certificate. 142 * @param in The input stream that will be used to read 143 * input from the user. If this is {@code null} 144 * then {@code System.in} will be used. 145 * @param out The print stream that will be used to display 146 * the prompt to the user. If this is 147 * {@code null} then System.out will be used. 148 */ 149 public PromptTrustManager(final String acceptedCertsFile, 150 final boolean examineValidityDates, 151 final InputStream in, final PrintStream out) 152 { 153 this(acceptedCertsFile, examineValidityDates, 154 Collections.<String>emptyList(), in, out); 155 } 156 157 158 159 /** 160 * Creates a new instance of this prompt trust manager. It may optionally 161 * cache trust information on disk, and may also be configured to examine or 162 * ignore validity dates. 163 * 164 * @param acceptedCertsFile The path to a file in which the certificates 165 * that have been previously accepted will be 166 * cached. It may be {@code null} if the cache 167 * should only be maintained in memory. 168 * @param examineValidityDates Indicates whether to reject certificates if 169 * the current time is outside the validity 170 * window for the certificate. 171 * @param expectedAddress An optional address that the client is 172 * expected to use to connect to the target 173 * server. This may be {@code null} if no 174 * expected address is available, if this trust 175 * manager is only expected to be used to 176 * validate client certificates, or if no server 177 * address validation should be performed. If a 178 * non-{@code null} value is provided, then the 179 * trust manager may issue a warning if the 180 * certificate does not contain that address. 181 * @param in The input stream that will be used to read 182 * input from the user. If this is {@code null} 183 * then {@code System.in} will be used. 184 * @param out The print stream that will be used to display 185 * the prompt to the user. If this is 186 * {@code null} then System.out will be used. 187 */ 188 public PromptTrustManager(final String acceptedCertsFile, 189 final boolean examineValidityDates, 190 final String expectedAddress, final InputStream in, 191 final PrintStream out) 192 { 193 this(acceptedCertsFile, examineValidityDates, 194 (expectedAddress == null) 195 ? Collections.<String>emptyList() 196 : Collections.singletonList(expectedAddress), 197 in, out); 198 } 199 200 201 202 /** 203 * Creates a new instance of this prompt trust manager. It may optionally 204 * cache trust information on disk, and may also be configured to examine or 205 * ignore validity dates. 206 * 207 * @param acceptedCertsFile The path to a file in which the certificates 208 * that have been previously accepted will be 209 * cached. It may be {@code null} if the cache 210 * should only be maintained in memory. 211 * @param examineValidityDates Indicates whether to reject certificates if 212 * the current time is outside the validity 213 * window for the certificate. 214 * @param expectedAddresses An optional collection of the addresses that 215 * the client is expected to use to connect to 216 * one of the target servers. This may be 217 * {@code null} or empty if no expected 218 * addresses are available, if this trust 219 * manager is only expected to be used to 220 * validate client certificates, or if no server 221 * address validation should be performed. If a 222 * non-empty collection is provided, then the 223 * trust manager may issue a warning if the 224 * certificate does not contain any of these 225 * addresses. 226 * @param in The input stream that will be used to read 227 * input from the user. If this is {@code null} 228 * then {@code System.in} will be used. 229 * @param out The print stream that will be used to display 230 * the prompt to the user. If this is 231 * {@code null} then System.out will be used. 232 */ 233 public PromptTrustManager(final String acceptedCertsFile, 234 final boolean examineValidityDates, 235 final Collection<String> expectedAddresses, 236 final InputStream in, final PrintStream out) 237 { 238 this.acceptedCertsFile = acceptedCertsFile; 239 this.examineValidityDates = examineValidityDates; 240 241 if (expectedAddresses == null) 242 { 243 this.expectedAddresses = Collections.emptyList(); 244 } 245 else 246 { 247 this.expectedAddresses = 248 Collections.unmodifiableList(new ArrayList<>(expectedAddresses)); 249 } 250 251 if (in == null) 252 { 253 this.in = System.in; 254 } 255 else 256 { 257 this.in = in; 258 } 259 260 if (out == null) 261 { 262 this.out = System.out; 263 } 264 else 265 { 266 this.out = out; 267 } 268 269 acceptedCerts = new ConcurrentHashMap<String,Boolean>(); 270 271 if (acceptedCertsFile != null) 272 { 273 BufferedReader r = null; 274 try 275 { 276 final File f = new File(acceptedCertsFile); 277 if (f.exists()) 278 { 279 r = new BufferedReader(new FileReader(f)); 280 while (true) 281 { 282 final String line = r.readLine(); 283 if (line == null) 284 { 285 break; 286 } 287 acceptedCerts.put(line, false); 288 } 289 } 290 } 291 catch (final Exception e) 292 { 293 debugException(e); 294 } 295 finally 296 { 297 if (r != null) 298 { 299 try 300 { 301 r.close(); 302 } 303 catch (final Exception e) 304 { 305 debugException(e); 306 } 307 } 308 } 309 } 310 } 311 312 313 314 /** 315 * Writes an updated copy of the trusted certificate cache to disk. 316 * 317 * @throws IOException If a problem occurs. 318 */ 319 private void writeCacheFile() 320 throws IOException 321 { 322 final File tempFile = new File(acceptedCertsFile + ".new"); 323 324 BufferedWriter w = null; 325 try 326 { 327 w = new BufferedWriter(new FileWriter(tempFile)); 328 329 for (final String certBytes : acceptedCerts.keySet()) 330 { 331 w.write(certBytes); 332 w.newLine(); 333 } 334 } 335 finally 336 { 337 if (w != null) 338 { 339 w.close(); 340 } 341 } 342 343 final File cacheFile = new File(acceptedCertsFile); 344 if (cacheFile.exists()) 345 { 346 final File oldFile = new File(acceptedCertsFile + ".previous"); 347 if (oldFile.exists()) 348 { 349 oldFile.delete(); 350 } 351 352 cacheFile.renameTo(oldFile); 353 } 354 355 tempFile.renameTo(cacheFile); 356 } 357 358 359 360 /** 361 * Indicates whether this trust manager would interactively prompt the user 362 * about whether to trust the provided certificate chain. 363 * 364 * @param chain The chain of certificates for which to make the 365 * determination. 366 * 367 * @return {@code true} if this trust manger would interactively prompt the 368 * user about whether to trust the certificate chain, or 369 * {@code false} if not (e.g., because the certificate is already 370 * known to be trusted). 371 */ 372 public synchronized boolean wouldPrompt(final X509Certificate[] chain) 373 { 374 try 375 { 376 final String cacheKey = getCacheKey(chain[0]); 377 return PromptTrustManagerProcessor.shouldPrompt(cacheKey, 378 convertChain(chain), false, examineValidityDates, acceptedCerts, 379 null).getFirst(); 380 } 381 catch (final Exception e) 382 { 383 Debug.debugException(e); 384 return false; 385 } 386 } 387 388 389 390 /** 391 * Performs the necessary validity check for the provided certificate array. 392 * 393 * @param chain The chain of certificates for which to make the 394 * determination. 395 * @param serverCert Indicates whether the certificate was presented as a 396 * server certificate or as a client certificate. 397 * 398 * @throws CertificateException If the provided certificate chain should not 399 * be trusted. 400 */ 401 private synchronized void checkCertificateChain(final X509Certificate[] chain, 402 final boolean serverCert) 403 throws CertificateException 404 { 405 final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain = 406 convertChain(chain); 407 408 final String cacheKey = getCacheKey(chain[0]); 409 final ObjectPair<Boolean,List<String>> shouldPromptResult = 410 PromptTrustManagerProcessor.shouldPrompt(cacheKey, convertedChain, 411 serverCert, examineValidityDates, acceptedCerts, 412 expectedAddresses); 413 414 if (! shouldPromptResult.getFirst()) 415 { 416 return; 417 } 418 419 if (serverCert) 420 { 421 out.println(INFO_PROMPT_SERVER_HEADING.get()); 422 } 423 else 424 { 425 out.println(INFO_PROMPT_CLIENT_HEADING.get()); 426 } 427 428 out.println(); 429 out.println(" " + 430 INFO_PROMPT_SUBJECT.get(convertedChain[0].getSubjectDN())); 431 out.println(" " + 432 INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate( 433 convertedChain[0].getNotBeforeDate()))); 434 out.println(" " + 435 INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate( 436 convertedChain[0].getNotAfterDate()))); 437 438 try 439 { 440 final byte[] sha1Fingerprint = convertedChain[0].getSHA1Fingerprint(); 441 final StringBuilder buffer = new StringBuilder(); 442 toHex(sha1Fingerprint, ":", buffer); 443 out.println(" " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer)); 444 } 445 catch (final Exception e) 446 { 447 Debug.debugException(e); 448 } 449 try 450 { 451 final byte[] sha256Fingerprint = convertedChain[0].getSHA256Fingerprint(); 452 final StringBuilder buffer = new StringBuilder(); 453 toHex(sha256Fingerprint, ":", buffer); 454 out.println(" " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer)); 455 } 456 catch (final Exception e) 457 { 458 Debug.debugException(e); 459 } 460 461 462 for (int i=1; i < chain.length; i++) 463 { 464 out.println(" -"); 465 out.println(" " + 466 INFO_PROMPT_ISSUER_SUBJECT.get(i, convertedChain[i].getSubjectDN())); 467 out.println(" " + 468 INFO_PROMPT_VALID_FROM.get(PromptTrustManagerProcessor.formatDate( 469 convertedChain[i].getNotBeforeDate()))); 470 out.println(" " + 471 INFO_PROMPT_VALID_TO.get(PromptTrustManagerProcessor.formatDate( 472 convertedChain[i].getNotAfterDate()))); 473 474 try 475 { 476 final byte[] sha1Fingerprint = convertedChain[i].getSHA1Fingerprint(); 477 final StringBuilder buffer = new StringBuilder(); 478 toHex(sha1Fingerprint, ":", buffer); 479 out.println(" " + INFO_PROMPT_SHA1_FINGERPRINT.get(buffer)); 480 } 481 catch (final Exception e) 482 { 483 Debug.debugException(e); 484 } 485 try 486 { 487 final byte[] sha256Fingerprint = 488 convertedChain[i].getSHA256Fingerprint(); 489 final StringBuilder buffer = new StringBuilder(); 490 toHex(sha256Fingerprint, ":", buffer); 491 out.println(" " + INFO_PROMPT_SHA256_FINGERPRINT.get(buffer)); 492 } 493 catch (final Exception e) 494 { 495 Debug.debugException(e); 496 } 497 } 498 499 for (final String warningMessage : shouldPromptResult.getSecond()) 500 { 501 out.println(); 502 for (final String line : 503 wrapLine(warningMessage, (TERMINAL_WIDTH_COLUMNS - 1))) 504 { 505 out.println(line); 506 } 507 } 508 509 final BufferedReader reader = new BufferedReader(new InputStreamReader(in)); 510 while (true) 511 { 512 try 513 { 514 out.println(); 515 out.print(INFO_PROMPT_MESSAGE.get() + ' '); 516 out.flush(); 517 final String line = reader.readLine(); 518 if (line == null) 519 { 520 // The input stream has been closed, so we can't prompt for trust, 521 // and should assume it is not trusted. 522 throw new CertificateException( 523 ERR_CERTIFICATE_REJECTED_BY_END_OF_STREAM.get( 524 SSLUtil.certificateToString(chain[0]))); 525 } 526 else if (line.equalsIgnoreCase("y") || line.equalsIgnoreCase("yes")) 527 { 528 // The certificate should be considered trusted. 529 break; 530 } 531 else if (line.equalsIgnoreCase("n") || line.equalsIgnoreCase("no")) 532 { 533 // The certificate should not be trusted. 534 throw new CertificateException( 535 ERR_CERTIFICATE_REJECTED_BY_USER.get( 536 SSLUtil.certificateToString(chain[0]))); 537 } 538 } 539 catch (final CertificateException ce) 540 { 541 throw ce; 542 } 543 catch (final Exception e) 544 { 545 debugException(e); 546 } 547 } 548 549 boolean isOutsideValidityWindow = false; 550 for (final com.unboundid.util.ssl.cert.X509Certificate c : convertedChain) 551 { 552 if (! c.isWithinValidityWindow()) 553 { 554 isOutsideValidityWindow = true; 555 break; 556 } 557 } 558 559 acceptedCerts.put(cacheKey, isOutsideValidityWindow); 560 561 if (acceptedCertsFile != null) 562 { 563 try 564 { 565 writeCacheFile(); 566 } 567 catch (final Exception e) 568 { 569 debugException(e); 570 } 571 } 572 } 573 574 575 576 /** 577 * Indicate whether to prompt about certificates contained in the cache if the 578 * current time is outside the validity window for the certificate. 579 * 580 * @return {@code true} if the certificate validity time should be examined 581 * for cached certificates and the user should be prompted if they 582 * are expired or not yet valid, or {@code false} if cached 583 * certificates should be accepted even outside of the validity 584 * window. 585 */ 586 public boolean examineValidityDates() 587 { 588 return examineValidityDates; 589 } 590 591 592 593 /** 594 * Retrieves a list of the addresses that the client is expected to use to 595 * communicate with the server, if available. 596 * 597 * @return A list of the addresses that the client is expected to use to 598 * communicate with the server, or an empty list if this is not 599 * available or applicable. 600 */ 601 public List<String> getExpectedAddresses() 602 { 603 return expectedAddresses; 604 } 605 606 607 608 /** 609 * Checks to determine whether the provided client certificate chain should be 610 * trusted. 611 * 612 * @param chain The client certificate chain for which to make the 613 * determination. 614 * @param authType The authentication type based on the client certificate. 615 * 616 * @throws CertificateException If the provided client certificate chain 617 * should not be trusted. 618 */ 619 @Override() 620 public void checkClientTrusted(final X509Certificate[] chain, 621 final String authType) 622 throws CertificateException 623 { 624 checkCertificateChain(chain, false); 625 } 626 627 628 629 /** 630 * Checks to determine whether the provided server certificate chain should be 631 * trusted. 632 * 633 * @param chain The server certificate chain for which to make the 634 * determination. 635 * @param authType The key exchange algorithm used. 636 * 637 * @throws CertificateException If the provided server certificate chain 638 * should not be trusted. 639 */ 640 @Override() 641 public void checkServerTrusted(final X509Certificate[] chain, 642 final String authType) 643 throws CertificateException 644 { 645 checkCertificateChain(chain, true); 646 } 647 648 649 650 /** 651 * Retrieves the accepted issuer certificates for this trust manager. This 652 * will always return an empty array. 653 * 654 * @return The accepted issuer certificates for this trust manager. 655 */ 656 @Override() 657 public X509Certificate[] getAcceptedIssuers() 658 { 659 return NO_CERTIFICATES; 660 } 661 662 663 664 /** 665 * Retrieves the cache key used to identify the provided certificate in the 666 * map of accepted certificates. 667 * 668 * @param certificate The certificate for which to get the cache key. 669 * 670 * @return The generated cache key. 671 */ 672 static String getCacheKey(final Certificate certificate) 673 { 674 final X509Certificate x509Certificate = (X509Certificate) certificate; 675 return toLowerCase(toHex(x509Certificate.getSignature())); 676 } 677 678 679 680 /** 681 * Converts the provided certificate chain from Java's representation of 682 * X.509 certificates to the LDAP SDK's version. 683 * 684 * @param chain The chain to be converted. 685 * 686 * @return The converted certificate chain. 687 * 688 * @throws CertificateException If a problem occurs while performing the 689 * conversion. 690 */ 691 static com.unboundid.util.ssl.cert.X509Certificate[] 692 convertChain(final Certificate[] chain) 693 throws CertificateException 694 { 695 final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain = 696 new com.unboundid.util.ssl.cert.X509Certificate[chain.length]; 697 for (int i=0; i < chain.length; i++) 698 { 699 try 700 { 701 convertedChain[i] = new com.unboundid.util.ssl.cert.X509Certificate( 702 chain[i].getEncoded()); 703 } 704 catch (final CertException ce) 705 { 706 Debug.debugException(ce); 707 throw new CertificateException(ce.getMessage(), ce); 708 } 709 } 710 711 return convertedChain; 712 } 713}