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