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