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