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