001/* 002 * Copyright 2008-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2019 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.StaticUtils; 048import com.unboundid.util.ThreadSafety; 049import com.unboundid.util.ThreadSafetyLevel; 050import com.unboundid.util.ssl.cert.CertException; 051 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<>(StaticUtils.computeMapCapacity(20)); 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 Debug.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 Debug.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 Files.delete(oldFile.toPath()); 350 } 351 352 Files.move(cacheFile.toPath(), oldFile.toPath()); 353 } 354 355 Files.move(tempFile.toPath(), cacheFile.toPath()); 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 StaticUtils.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 StaticUtils.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 StaticUtils.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 StaticUtils.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 StaticUtils.wrapLine(warningMessage, 504 (StaticUtils.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 Debug.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 Debug.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 StaticUtils.toLowerCase( 677 StaticUtils.toHex(x509Certificate.getSignature())); 678 } 679 680 681 682 /** 683 * Converts the provided certificate chain from Java's representation of 684 * X.509 certificates to the LDAP SDK's version. 685 * 686 * @param chain The chain to be converted. 687 * 688 * @return The converted certificate chain. 689 * 690 * @throws CertificateException If a problem occurs while performing the 691 * conversion. 692 */ 693 static com.unboundid.util.ssl.cert.X509Certificate[] 694 convertChain(final Certificate[] chain) 695 throws CertificateException 696 { 697 final com.unboundid.util.ssl.cert.X509Certificate[] convertedChain = 698 new com.unboundid.util.ssl.cert.X509Certificate[chain.length]; 699 for (int i=0; i < chain.length; i++) 700 { 701 try 702 { 703 convertedChain[i] = new com.unboundid.util.ssl.cert.X509Certificate( 704 chain[i].getEncoded()); 705 } 706 catch (final CertException ce) 707 { 708 Debug.debugException(ce); 709 throw new CertificateException(ce.getMessage(), ce); 710 } 711 } 712 713 return convertedChain; 714 } 715}