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