001/* 002 * Copyright 2017-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2017-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) 2017-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 039 040import java.io.File; 041import java.io.FileInputStream; 042import java.io.Serializable; 043import java.security.KeyStore; 044import java.security.cert.CertificateException; 045import java.security.cert.CertificateExpiredException; 046import java.security.cert.CertificateNotYetValidException; 047import java.security.cert.X509Certificate; 048import java.util.ArrayList; 049import java.util.Arrays; 050import java.util.Collection; 051import java.util.Collections; 052import java.util.Date; 053import java.util.Enumeration; 054import java.util.LinkedHashMap; 055import java.util.List; 056import java.util.Map; 057import java.util.concurrent.atomic.AtomicReference; 058import javax.net.ssl.X509TrustManager; 059 060import com.unboundid.asn1.ASN1OctetString; 061import com.unboundid.util.CryptoHelper; 062import com.unboundid.util.Debug; 063import com.unboundid.util.NotMutable; 064import com.unboundid.util.NotNull; 065import com.unboundid.util.Nullable; 066import com.unboundid.util.ObjectPair; 067import com.unboundid.util.StaticUtils; 068import com.unboundid.util.ThreadSafety; 069import com.unboundid.util.ThreadSafetyLevel; 070import com.unboundid.util.ssl.cert.AuthorityKeyIdentifierExtension; 071import com.unboundid.util.ssl.cert.SubjectKeyIdentifierExtension; 072import com.unboundid.util.ssl.cert.X509CertificateExtension; 073 074import static com.unboundid.util.ssl.SSLMessages.*; 075 076 077 078/** 079 * This class provides an implementation of a trust manager that relies on the 080 * JVM's default set of trusted issuers. 081 * <BR><BR> 082 * This implementation will first look for the trust store in the following 083 * locations within the Java installation, in the following order: 084 * <OL> 085 * <LI>{@code lib/security/jssecacerts}</LI> 086 * <LI>{@code jre/lib/security/jssecacerts}</LI> 087 * <LI>{@code lib/security/cacerts}</LI> 088 * <LI>{@code jre/lib/security/cacerts}</LI> 089 * </OL> 090 * If none of those files exist (or if they cannot be parsed as a JKS or PKCS 091 * #12 key store), then we will search for a {@code jssecacerts} or 092 * {@code cacerts} file below the Java home directory. 093 */ 094@NotMutable() 095@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 096public final class JVMDefaultTrustManager 097 implements X509TrustManager, Serializable 098{ 099 /** 100 * A reference to the singleton instance of this class. 101 */ 102 @NotNull private static final AtomicReference<JVMDefaultTrustManager> 103 INSTANCE = new AtomicReference<>(); 104 105 106 107 /** 108 * The name of the system property that specifies the path to the Java 109 * installation for the currently-running JVM. 110 */ 111 @NotNull private static final String PROPERTY_JAVA_HOME = "java.home"; 112 113 114 115 /** 116 * A set of alternate file extensions that may be used by Java keystores. 117 */ 118 @NotNull static final String[] FILE_EXTENSIONS = 119 { 120 ".jks", 121 ".p12", 122 ".pkcs12", 123 ".pfx", 124 }; 125 126 127 128 /** 129 * A pre-allocated empty certificate array. 130 */ 131 @NotNull private static final X509Certificate[] NO_CERTIFICATES = 132 new X509Certificate[0]; 133 134 135 136 /** 137 * The serial version UID for this serializable class. 138 */ 139 private static final long serialVersionUID = -8587938729712485943L; 140 141 142 143 // A certificate exception that should be thrown for any attempt to use this 144 // trust store. 145 @Nullable private final CertificateException certificateException; 146 147 // The file from which they keystore was loaded. 148 @Nullable private final File caCertsFile; 149 150 // The keystore instance containing the JVM's default set of trusted issuers. 151 @Nullable private final KeyStore keystore; 152 153 // A map of the certificates in the keystore, indexed by signature. 154 @NotNull private final Map<ASN1OctetString,X509Certificate> 155 trustedCertsBySignature; 156 157 // A map of the certificates in the keystore, indexed by key ID. 158 @NotNull private final Map<ASN1OctetString, 159 com.unboundid.util.ssl.cert.X509Certificate> trustedCertsByKeyID; 160 161 162 163 /** 164 * Creates an instance of this trust manager. 165 * 166 * @param javaHomePropertyName The name of the system property that should 167 * specify the path to the Java installation. 168 */ 169 JVMDefaultTrustManager(@NotNull final String javaHomePropertyName) 170 { 171 // Determine the path to the root of the Java installation. 172 final String javaHomePath = 173 StaticUtils.getSystemProperty(javaHomePropertyName); 174 if (javaHomePath == null) 175 { 176 certificateException = new CertificateException( 177 ERR_JVM_DEFAULT_TRUST_MANAGER_NO_JAVA_HOME.get( 178 javaHomePropertyName)); 179 caCertsFile = null; 180 keystore = null; 181 trustedCertsBySignature = Collections.emptyMap(); 182 trustedCertsByKeyID = Collections.emptyMap(); 183 return; 184 } 185 186 final File javaHomeDirectory = new File(javaHomePath); 187 if ((! javaHomeDirectory.exists()) || (! javaHomeDirectory.isDirectory())) 188 { 189 certificateException = new CertificateException( 190 ERR_JVM_DEFAULT_TRUST_MANAGER_INVALID_JAVA_HOME.get( 191 javaHomePropertyName, javaHomePath)); 192 caCertsFile = null; 193 keystore = null; 194 trustedCertsBySignature = Collections.emptyMap(); 195 trustedCertsByKeyID = Collections.emptyMap(); 196 return; 197 } 198 199 200 // Get a keystore instance that is loaded from the JVM's default set of 201 // trusted issuers. 202 final ObjectPair<KeyStore,File> keystorePair; 203 try 204 { 205 keystorePair = getJVMDefaultKeyStore(javaHomeDirectory); 206 } 207 catch (final CertificateException ce) 208 { 209 Debug.debugException(ce); 210 certificateException = ce; 211 caCertsFile = null; 212 keystore = null; 213 trustedCertsBySignature = Collections.emptyMap(); 214 trustedCertsByKeyID = Collections.emptyMap(); 215 return; 216 } 217 218 keystore = keystorePair.getFirst(); 219 caCertsFile = keystorePair.getSecond(); 220 221 222 // Iterate through the certificates in the keystore and load them into a 223 // map for faster and more reliable access. 224 final LinkedHashMap<ASN1OctetString,X509Certificate> certsBySignature = 225 new LinkedHashMap<>(StaticUtils.computeMapCapacity(50)); 226 final LinkedHashMap<ASN1OctetString, 227 com.unboundid.util.ssl.cert.X509Certificate> certsByKeyID = 228 new LinkedHashMap<>(StaticUtils.computeMapCapacity(50)); 229 try 230 { 231 final Enumeration<String> aliasEnumeration = keystore.aliases(); 232 while (aliasEnumeration.hasMoreElements()) 233 { 234 final String alias = aliasEnumeration.nextElement(); 235 236 try 237 { 238 final X509Certificate certificate = 239 (X509Certificate) keystore.getCertificate(alias); 240 if (certificate != null) 241 { 242 certsBySignature.put( 243 new ASN1OctetString(certificate.getSignature()), 244 certificate); 245 246 try 247 { 248 final com.unboundid.util.ssl.cert.X509Certificate c = 249 new com.unboundid.util.ssl.cert.X509Certificate( 250 certificate.getEncoded()); 251 for (final X509CertificateExtension e : c.getExtensions()) 252 { 253 if (e instanceof SubjectKeyIdentifierExtension) 254 { 255 final SubjectKeyIdentifierExtension skie = 256 (SubjectKeyIdentifierExtension) e; 257 certsByKeyID.put( 258 new ASN1OctetString(skie.getKeyIdentifier().getValue()), 259 c); 260 } 261 } 262 } 263 catch (final Exception e) 264 { 265 Debug.debugException(e); 266 } 267 } 268 } 269 catch (final Exception e) 270 { 271 Debug.debugException(e); 272 } 273 } 274 } 275 catch (final Exception e) 276 { 277 Debug.debugException(e); 278 certificateException = new CertificateException( 279 ERR_JVM_DEFAULT_TRUST_MANAGER_ERROR_ITERATING_THROUGH_CACERTS.get( 280 caCertsFile.getAbsolutePath(), 281 StaticUtils.getExceptionMessage(e)), 282 e); 283 trustedCertsBySignature = Collections.emptyMap(); 284 trustedCertsByKeyID = Collections.emptyMap(); 285 return; 286 } 287 288 trustedCertsBySignature = Collections.unmodifiableMap(certsBySignature); 289 trustedCertsByKeyID = Collections.unmodifiableMap(certsByKeyID); 290 certificateException = null; 291 } 292 293 294 295 /** 296 * Retrieves the singleton instance of this trust manager. 297 * 298 * @return The singleton instance of this trust manager. 299 */ 300 @NotNull() 301 public static JVMDefaultTrustManager getInstance() 302 { 303 final JVMDefaultTrustManager existingInstance = INSTANCE.get(); 304 if (existingInstance != null) 305 { 306 return existingInstance; 307 } 308 309 final JVMDefaultTrustManager newInstance = 310 new JVMDefaultTrustManager(PROPERTY_JAVA_HOME); 311 if (INSTANCE.compareAndSet(null, newInstance)) 312 { 313 return newInstance; 314 } 315 else 316 { 317 return INSTANCE.get(); 318 } 319 } 320 321 322 323 /** 324 * Retrieves the keystore that backs this trust manager. 325 * 326 * @return The keystore that backs this trust manager. 327 * 328 * @throws CertificateException If a problem was encountered while 329 * initializing this trust manager. 330 */ 331 @NotNull() 332 KeyStore getKeyStore() 333 throws CertificateException 334 { 335 if (certificateException != null) 336 { 337 throw certificateException; 338 } 339 340 return keystore; 341 } 342 343 344 345 /** 346 * Retrieves the path to the the file containing the JVM's default set of 347 * trusted issuers. 348 * 349 * @return The path to the file containing the JVM's default set of 350 * trusted issuers. 351 * 352 * @throws CertificateException If a problem was encountered while 353 * initializing this trust manager. 354 */ 355 @NotNull() 356 public File getCACertsFile() 357 throws CertificateException 358 { 359 if (certificateException != null) 360 { 361 throw certificateException; 362 } 363 364 return caCertsFile; 365 } 366 367 368 369 /** 370 * Retrieves the certificates included in this trust manager. 371 * 372 * @return The certificates included in this trust manager. 373 * 374 * @throws CertificateException If a problem was encountered while 375 * initializing this trust manager. 376 */ 377 @NotNull() 378 public Collection<X509Certificate> getTrustedIssuerCertificates() 379 throws CertificateException 380 { 381 if (certificateException != null) 382 { 383 throw certificateException; 384 } 385 386 return trustedCertsBySignature.values(); 387 } 388 389 390 391 /** 392 * Checks to determine whether the provided client certificate chain should be 393 * trusted. 394 * 395 * @param chain The client certificate chain for which to make the 396 * determination. 397 * @param authType The authentication type based on the client certificate. 398 * 399 * @throws CertificateException If the provided client certificate chain 400 * should not be trusted. 401 */ 402 @Override() 403 public void checkClientTrusted(@NotNull final X509Certificate[] chain, 404 @NotNull final String authType) 405 throws CertificateException 406 { 407 checkTrusted(chain); 408 } 409 410 411 412 /** 413 * Checks to determine whether the provided server certificate chain should be 414 * trusted. 415 * 416 * @param chain The server certificate chain for which to make the 417 * determination. 418 * @param authType The key exchange algorithm used. 419 * 420 * @throws CertificateException If the provided server certificate chain 421 * should not be trusted. 422 */ 423 @Override() 424 public void checkServerTrusted(@NotNull final X509Certificate[] chain, 425 @NotNull final String authType) 426 throws CertificateException 427 { 428 checkTrusted(chain); 429 } 430 431 432 433 /** 434 * Retrieves the accepted issuer certificates for this trust manager. 435 * 436 * @return The accepted issuer certificates for this trust manager, or an 437 * empty set of accepted issuers if a problem was encountered while 438 * initializing this trust manager. 439 */ 440 @Override() 441 @NotNull() 442 public X509Certificate[] getAcceptedIssuers() 443 { 444 if (certificateException != null) 445 { 446 return NO_CERTIFICATES; 447 } 448 449 final X509Certificate[] acceptedIssuers = 450 new X509Certificate[trustedCertsBySignature.size()]; 451 return trustedCertsBySignature.values().toArray(acceptedIssuers); 452 } 453 454 455 456 /** 457 * Retrieves a {@code KeyStore} that contains the JVM's default set of trusted 458 * issuers. 459 * 460 * @param javaHomeDirectory The path to the JVM installation home directory. 461 * 462 * @return An {@code ObjectPair} that includes the keystore and the file from 463 * which it was loaded. 464 * 465 * @throws CertificateException If the keystore could not be found or 466 * loaded. 467 */ 468 @NotNull() 469 private static ObjectPair<KeyStore,File> getJVMDefaultKeyStore( 470 @NotNull final File javaHomeDirectory) 471 throws CertificateException 472 { 473 final File libSecurityJSSECACerts = StaticUtils.constructPath( 474 javaHomeDirectory, "lib", "security", "jssecacerts"); 475 final File jreLibSecurityJSSECACerts = StaticUtils.constructPath( 476 javaHomeDirectory, "jre", "lib", "security", "jssecacerts"); 477 final File libSecurityCACerts = StaticUtils.constructPath(javaHomeDirectory, 478 "lib", "security", "cacerts"); 479 final File jreLibSecurityCACerts = StaticUtils.constructPath( 480 javaHomeDirectory, "jre", "lib", "security", "cacerts"); 481 482 final ArrayList<File> tryFirstFiles = 483 new ArrayList<>(4 * FILE_EXTENSIONS.length + 2); 484 tryFirstFiles.add(libSecurityCACerts); 485 tryFirstFiles.add(jreLibSecurityCACerts); 486 487 for (final String extension : FILE_EXTENSIONS) 488 { 489 tryFirstFiles.add( 490 new File(libSecurityJSSECACerts.getAbsolutePath() + extension)); 491 tryFirstFiles.add( 492 new File(jreLibSecurityJSSECACerts.getAbsolutePath() + extension)); 493 tryFirstFiles.add( 494 new File(libSecurityCACerts.getAbsolutePath() + extension)); 495 tryFirstFiles.add( 496 new File(jreLibSecurityCACerts.getAbsolutePath() + extension)); 497 } 498 499 for (final File f : tryFirstFiles) 500 { 501 final KeyStore keyStore = loadKeyStore(f); 502 if (keyStore != null) 503 { 504 return new ObjectPair<>(keyStore, f); 505 } 506 } 507 508 509 // If we didn't find it with known paths, then try to find it with a 510 // recursive filesystem search below the Java home directory. 511 final LinkedHashMap<File,CertificateException> exceptions = 512 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 513 final ObjectPair<KeyStore,File> keystorePair = 514 searchForKeyStore(javaHomeDirectory, exceptions); 515 if (keystorePair != null) 516 { 517 return keystorePair; 518 } 519 520 521 // If we've gotten here, then we couldn't find the keystore. Construct a 522 // message from the set of exceptions. 523 if (exceptions.isEmpty()) 524 { 525 throw new CertificateException( 526 ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_NO_EXCEPTION.get()); 527 } 528 else 529 { 530 final StringBuilder buffer = new StringBuilder(); 531 buffer.append( 532 ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_WITH_EXCEPTION. 533 get()); 534 for (final Map.Entry<File,CertificateException> e : exceptions.entrySet()) 535 { 536 if (buffer.charAt(buffer.length() - 1) != '.') 537 { 538 buffer.append('.'); 539 } 540 541 buffer.append(" "); 542 buffer.append(ERR_JVM_DEFAULT_TRUST_MANAGER_LOAD_ERROR.get( 543 e.getKey().getAbsolutePath(), 544 StaticUtils.getExceptionMessage(e.getValue()))); 545 } 546 547 throw new CertificateException(buffer.toString()); 548 } 549 } 550 551 552 553 /** 554 * Recursively searches for a valid keystore file below the specified portion 555 * of the filesystem. Any file named "cacerts", ignoring differences in 556 * capitalization, and optionally ending with a number of different file 557 * extensions, will be examined to see if it can be parsed as a Java keystore. 558 * The first keystore that we find meeting that criteria will be returned. 559 * 560 * @param directory The directory in which to search. It must not be 561 * {@code null}. 562 * @param exceptions A map that correlates file paths with exceptions 563 * obtained while interacting with them. If an exception 564 * is encountered while interacting with this file, then 565 * it will be added to this map. 566 * 567 * @return The first valid keystore found that meets all the necessary 568 * criteria, or {@code null} if no such keystore could be found. 569 */ 570 @Nullable() 571 private static ObjectPair<KeyStore,File> searchForKeyStore( 572 @NotNull final File directory, 573 @NotNull final Map<File,CertificateException> exceptions) 574 { 575filesInDirectoryLoop: 576 for (final File f : directory.listFiles()) 577 { 578 if (f.isDirectory()) 579 { 580 final ObjectPair<KeyStore,File> p = searchForKeyStore(f, exceptions); 581 if (p != null) 582 { 583 return p; 584 } 585 } 586 else 587 { 588 final String lowerName = StaticUtils.toLowerCase(f.getName()); 589 if (lowerName.equals("jssecacerts") || lowerName.equals("cacerts")) 590 { 591 try 592 { 593 final KeyStore keystore = loadKeyStore(f); 594 return new ObjectPair<>(keystore, f); 595 } 596 catch (final CertificateException ce) 597 { 598 Debug.debugException(ce); 599 exceptions.put(f, ce); 600 } 601 } 602 else 603 { 604 for (final String extension : FILE_EXTENSIONS) 605 { 606 if (lowerName.equals("jssecacerts" + extension) || 607 lowerName.equals("cacerts" + extension)) 608 { 609 try 610 { 611 final KeyStore keystore = loadKeyStore(f); 612 return new ObjectPair<>(keystore, f); 613 } 614 catch (final CertificateException ce) 615 { 616 Debug.debugException(ce); 617 exceptions.put(f, ce); 618 continue filesInDirectoryLoop; 619 } 620 } 621 } 622 } 623 } 624 } 625 626 return null; 627 } 628 629 630 631 /** 632 * Attempts to load the contents of the specified file as a Java keystore. 633 * 634 * @param f The file from which to load the keystore data. 635 * 636 * @return The keystore that was loaded from the specified file. 637 * 638 * @throws CertificateException If a problem occurs while trying to load the 639 * 640 */ 641 @Nullable() 642 private static KeyStore loadKeyStore(@NotNull final File f) 643 throws CertificateException 644 { 645 if ((! f.exists()) || (! f.isFile())) 646 { 647 return null; 648 } 649 650 CertificateException firstGetInstanceException = null; 651 CertificateException firstLoadException = null; 652 for (final String keyStoreType : new String[] { "JKS", "PKCS12" }) 653 { 654 final KeyStore keyStore; 655 try 656 { 657 keyStore = CryptoHelper.getKeyStore(keyStoreType, null, true); 658 } 659 catch (final Exception e) 660 { 661 Debug.debugException(e); 662 if (firstGetInstanceException == null) 663 { 664 firstGetInstanceException = new CertificateException( 665 ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_INSTANTIATE_KEYSTORE.get( 666 keyStoreType, StaticUtils.getExceptionMessage(e)), 667 e); 668 } 669 continue; 670 } 671 672 try (FileInputStream inputStream = new FileInputStream(f)) 673 { 674 keyStore.load(inputStream, null); 675 } 676 catch (final Exception e) 677 { 678 Debug.debugException(e); 679 if (firstLoadException == null) 680 { 681 firstLoadException = new CertificateException( 682 ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_ERROR_LOADING_KEYSTORE.get( 683 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)), 684 e); 685 } 686 continue; 687 } 688 689 return keyStore; 690 } 691 692 if (firstLoadException != null) 693 { 694 throw firstLoadException; 695 } 696 697 throw firstGetInstanceException; 698 } 699 700 701 702 /** 703 * Ensures that the provided certificate chain should be considered trusted. 704 * 705 * @param chain The certificate chain to validate. It must not be 706 * {@code null}). 707 * 708 * @throws CertificateException If the provided certificate chain should not 709 * be considered trusted. 710 */ 711 void checkTrusted(@NotNull final X509Certificate[] chain) 712 throws CertificateException 713 { 714 if (certificateException != null) 715 { 716 throw certificateException; 717 } 718 719 if ((chain == null) || (chain.length == 0)) 720 { 721 throw new CertificateException( 722 ERR_JVM_DEFAULT_TRUST_MANAGER_NO_CERTS_IN_CHAIN.get()); 723 } 724 725 726 // It is possible that the chain could rely on cross-signed certificates, 727 // and that we need to use a different path than the one presented in the 728 // provided chain. This requires us to potentially compute signatures using 729 // each certificate in the JVM's default trust store, which can be 730 // expensive. To avoid that, we'll first only try it if the presented 731 // chain has any certificates that are outside of their current validity 732 // window. If we get back a chain that is different from the one provided 733 // to this method, then we shouldn't need to do any further validation. 734 final X509Certificate[] chainToValidate = getChainToValidate(chain, true); 735 if (! Arrays.equals(chainToValidate, chain)) 736 { 737 return; 738 } 739 740 741 boolean foundIssuer = false; 742 final Date currentTime = new Date(); 743 for (final X509Certificate cert : chainToValidate) 744 { 745 final ASN1OctetString signature = 746 new ASN1OctetString(cert.getSignature()); 747 foundIssuer = (trustedCertsBySignature.get(signature) != null); 748 if (foundIssuer) 749 { 750 break; 751 } 752 } 753 754 if (! foundIssuer) 755 { 756 // It's possible that the server sent an incomplete chain. Handle that 757 // possibility. 758 foundIssuer = checkIncompleteChain(chain); 759 } 760 761 if (! foundIssuer) 762 { 763 // We couldn't validate the presented chain, so see if we can find an 764 // alternative chain using a cross-signed certificate. In this case, 765 // we'll perform the expensive check regardless of the validity dates in 766 // the presented chain. If the attempt to find an alternative chain 767 // fails, then the getChainToValidate method will throw an exception. 768 // However, if the alternative chain contains only a single certificate, 769 // then that suggests the certificate is self-signed and not signed by 770 // any trusted issuer. 771 final X509Certificate[] alternativeChain = 772 getChainToValidate(chain, false); 773 if (Arrays.equals(alternativeChain, chain)) 774 { 775 throw new CertificateException( 776 ERR_JVM_DEFAULT_TRUST_MANGER_NO_TRUSTED_ISSUER_FOUND.get( 777 chainToString(chain))); 778 } 779 } 780 } 781 782 783 784 /** 785 * Retrieves a list containing the certificates in the chain that should 786 * actually be validated. All certificates in the chain will have been 787 * confirmed to be in their validity window. 788 * 789 * @param chain The chain for which to obtain the path to 790 * validate. It must not be {@code null} or 791 * empty. 792 * @param checkChainValidityWindow Indicates whether to examine the validity 793 * of certificates in the presented chain 794 * when determining whether to examine 795 * certificates by signature. If this is 796 * {@code true}, then the provided chain 797 * will be returned as long as all of the 798 * certificates in it are within their 799 * validity window. If this is 800 * {@code false}, then an attempt to find a 801 * chain based on signatures will be used 802 * even if all of the certificates in the 803 * presented chain are considered valid. 804 * 805 * @return The chain to be validated. It may be the same as the provided 806 * chain, or an alternate chain if any certificate in the provided 807 * chain was outside of its validity window but an alternative trust 808 * path could be found. 809 * 810 * @throws CertificateException If the presented certificate chain included 811 * a certificate that is outside of its 812 * current validity window and no alternate 813 * path could be found. 814 */ 815 @NotNull() 816 private X509Certificate[] getChainToValidate( 817 @NotNull final X509Certificate[] chain, 818 final boolean checkChainValidityWindow) 819 throws CertificateException 820 { 821 final Date currentDate = new Date(); 822 823 // Check to see if any certificate in the provided chain is outside the 824 // current validity window. If not, then just use the provided chain. 825 CertificateException firstException = null; 826 if (checkChainValidityWindow) 827 { 828 for (int i=0; i < chain.length; i++) 829 { 830 final X509Certificate cert = chain[i]; 831 832 final Date notBefore = cert.getNotBefore(); 833 if (currentDate.before(notBefore)) 834 { 835 if (firstException == null) 836 { 837 firstException = new CertificateNotYetValidException( 838 ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_NOT_YET_VALID.get( 839 chainToString(chain), String.valueOf(cert.getSubjectDN()), 840 String.valueOf(notBefore))); 841 } 842 843 if (i == 0) 844 { 845 // If the peer certificate is not yet valid, then the entire chain 846 // must be considered invalid. 847 throw firstException; 848 } 849 else 850 { 851 break; 852 } 853 } 854 855 final Date notAfter = cert.getNotAfter(); 856 if (currentDate.after(notAfter)) 857 { 858 if (firstException == null) 859 { 860 firstException = new CertificateExpiredException( 861 ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_EXPIRED.get( 862 chainToString(chain), 863 String.valueOf(cert.getSubjectDN()), 864 String.valueOf(notAfter))); 865 } 866 867 if (i == 0) 868 { 869 // If the peer certificate is expired, then the entire chain must be 870 // considered invalid. 871 throw firstException; 872 } 873 else 874 { 875 break; 876 } 877 } 878 } 879 880 881 // If all the certificates in the chain were within their validity window, 882 // then just use the provided chain. 883 if (firstException == null) 884 { 885 return chain; 886 } 887 } 888 889 890 // If we've gotten here, then we should try to find an alternative chain. 891 boolean foundAlternative = false; 892 final List<X509Certificate> alternativeChain = new ArrayList<>(); 893chainLoop: 894 for (final X509Certificate c : chain) 895 { 896 alternativeChain.add(c); 897 try 898 { 899 final X509Certificate issuer = findIssuer(c, currentDate); 900 if (issuer == null) 901 { 902 break; 903 } 904 else 905 { 906 foundAlternative = true; 907 alternativeChain.add(issuer); 908 909 X509Certificate prevIssuer = issuer; 910 while (true) 911 { 912 try 913 { 914 final X509Certificate nextIssuer = 915 findIssuer(prevIssuer, currentDate); 916 if (nextIssuer == null) 917 { 918 break chainLoop; 919 } 920 else 921 { 922 alternativeChain.add(nextIssuer); 923 prevIssuer = nextIssuer; 924 } 925 } 926 catch (final CertificateException e) 927 { 928 foundAlternative = false; 929 break chainLoop; 930 } 931 } 932 } 933 } 934 catch (final CertificateException e) 935 { 936 Debug.debugException(e); 937 } 938 } 939 940 if (foundAlternative) 941 { 942 return alternativeChain.toArray(NO_CERTIFICATES); 943 } 944 else 945 { 946 if (firstException == null) 947 { 948 throw new CertificateException( 949 ERR_JVM_DEFAULT_TRUST_MANGER_NO_TRUSTED_ISSUER_FOUND.get( 950 chainToString(chain))); 951 } 952 else 953 { 954 throw firstException; 955 } 956 } 957 } 958 959 960 961 /** 962 * Finds the issuer for the provided certificate, if it is in the JVM-default 963 * trust store. 964 * 965 * @param cert The certificate for which to find the issuer. It must 966 * have already been retrieved from the JVM-default trust 967 * store. 968 * @param currentDate The current date to use when verifying validity. 969 * 970 * @return The issuer for the provided certificate, or {@code null} if the 971 * provided certificate is self-signed. 972 * 973 * @throws CertificateException If the provided certificate is not 974 * self-signed but its issuer could not be 975 * found, or if the issuer certificate is 976 * not currently valid. 977 */ 978 @Nullable() 979 private X509Certificate findIssuer(@NotNull final X509Certificate cert, 980 @NotNull final Date currentDate) 981 throws CertificateException 982 { 983 try 984 { 985 // More fully decode the provided certificate so that we can better 986 // examine it. 987 final com.unboundid.util.ssl.cert.X509Certificate c = 988 new com.unboundid.util.ssl.cert.X509Certificate( 989 cert.getEncoded()); 990 991 // If the certificate is self-signed, then it doesn't have an issuer. 992 if (c.isSelfSigned()) 993 { 994 return null; 995 } 996 997 // See if the certificate has an authority key identifier extension. If 998 // so, then use it to try to find the issuer. 999 for (final X509CertificateExtension e : c.getExtensions()) 1000 { 1001 if (e instanceof AuthorityKeyIdentifierExtension) 1002 { 1003 final AuthorityKeyIdentifierExtension akie = 1004 (AuthorityKeyIdentifierExtension) e; 1005 final ASN1OctetString authorityKeyID = 1006 new ASN1OctetString(akie.getKeyIdentifier().getValue()); 1007 final com.unboundid.util.ssl.cert.X509Certificate issuer = 1008 trustedCertsByKeyID.get(authorityKeyID); 1009 if ((issuer != null) && issuer.isWithinValidityWindow(currentDate)) 1010 { 1011 c.verifySignature(issuer); 1012 return (X509Certificate) issuer.toCertificate(); 1013 } 1014 } 1015 } 1016 } 1017 catch (final Exception e) 1018 { 1019 Debug.debugException(e); 1020 } 1021 1022 throw new CertificateException( 1023 ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_FIND_ISSUER.get( 1024 String.valueOf(cert.getSubjectDN()))); 1025 } 1026 1027 1028 1029 /** 1030 * Checks to determine whether the provided certificate chain may be 1031 * incomplete, and if so, whether we can find and trust the issuer of the last 1032 * certificate in the chain. 1033 * 1034 * @param chain The chain to validate. 1035 * 1036 * @return {@code true} if the chain could be validated, or {@code false} if 1037 * not. 1038 */ 1039 private boolean checkIncompleteChain(@NotNull final X509Certificate[] chain) 1040 { 1041 try 1042 { 1043 // Get the last certificate in the chain and decode it as one that we can 1044 // more fully inspect. 1045 final com.unboundid.util.ssl.cert.X509Certificate c = 1046 new com.unboundid.util.ssl.cert.X509Certificate( 1047 chain[chain.length - 1].getEncoded()); 1048 1049 // If the certificate is self-signed, then it can't be trusted. 1050 if (c.isSelfSigned()) 1051 { 1052 return false; 1053 } 1054 1055 // See if the certificate has an authority key identifier extension. If 1056 // so, then use it to try to find the issuer. 1057 for (final X509CertificateExtension e : c.getExtensions()) 1058 { 1059 if (e instanceof AuthorityKeyIdentifierExtension) 1060 { 1061 final AuthorityKeyIdentifierExtension akie = 1062 (AuthorityKeyIdentifierExtension) e; 1063 final ASN1OctetString authorityKeyID = 1064 new ASN1OctetString(akie.getKeyIdentifier().getValue()); 1065 final com.unboundid.util.ssl.cert.X509Certificate issuer = 1066 trustedCertsByKeyID.get(authorityKeyID); 1067 if ((issuer != null) && issuer.isWithinValidityWindow()) 1068 { 1069 c.verifySignature(issuer); 1070 return true; 1071 } 1072 } 1073 } 1074 } 1075 catch (final Exception e) 1076 { 1077 Debug.debugException(e); 1078 } 1079 1080 return false; 1081 } 1082 1083 1084 1085 /** 1086 * Constructs a string representation of the certificates in the provided 1087 * chain. It will consist of a comma-delimited list of their subject DNs, 1088 * with each subject DN surrounded by single quotes. 1089 * 1090 * @param chain The chain for which to obtain the string representation. 1091 * 1092 * @return A string representation of the provided certificate chain. 1093 */ 1094 @NotNull() 1095 static String chainToString(@NotNull final X509Certificate[] chain) 1096 { 1097 final StringBuilder buffer = new StringBuilder(); 1098 1099 switch (chain.length) 1100 { 1101 case 0: 1102 break; 1103 case 1: 1104 buffer.append('\''); 1105 buffer.append(chain[0].getSubjectDN()); 1106 buffer.append('\''); 1107 break; 1108 case 2: 1109 buffer.append('\''); 1110 buffer.append(chain[0].getSubjectDN()); 1111 buffer.append("' and '"); 1112 buffer.append(chain[1].getSubjectDN()); 1113 buffer.append('\''); 1114 break; 1115 default: 1116 for (int i=0; i < chain.length; i++) 1117 { 1118 if (i > 0) 1119 { 1120 buffer.append(", "); 1121 } 1122 1123 if (i == (chain.length - 1)) 1124 { 1125 buffer.append("and "); 1126 } 1127 1128 buffer.append('\''); 1129 buffer.append(chain[i].getSubjectDN()); 1130 buffer.append('\''); 1131 } 1132 } 1133 1134 return buffer.toString(); 1135 } 1136}