001/* 002 * Copyright 2017-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2017-2019 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util.ssl; 022 023 024 025import java.io.File; 026import java.io.FileInputStream; 027import java.io.Serializable; 028import java.security.KeyStore; 029import java.security.cert.CertificateException; 030import java.security.cert.CertificateExpiredException; 031import java.security.cert.CertificateNotYetValidException; 032import java.security.cert.X509Certificate; 033import java.util.ArrayList; 034import java.util.Collection; 035import java.util.Collections; 036import java.util.Date; 037import java.util.Enumeration; 038import java.util.LinkedHashMap; 039import java.util.Map; 040import java.util.concurrent.atomic.AtomicReference; 041import javax.net.ssl.X509TrustManager; 042 043import com.unboundid.asn1.ASN1OctetString; 044import com.unboundid.util.Debug; 045import com.unboundid.util.NotMutable; 046import com.unboundid.util.ObjectPair; 047import com.unboundid.util.StaticUtils; 048import com.unboundid.util.ThreadSafety; 049import com.unboundid.util.ThreadSafetyLevel; 050 051import static com.unboundid.util.ssl.SSLMessages.*; 052 053 054 055/** 056 * This class provides an implementation of a trust manager that relies on the 057 * JVM's default set of trusted issuers. This is generally found in the 058 * {@code jre/lib/security/cacerts} or {@code lib/security/cacerts} file in the 059 * Java installation (in both Sun/Oracle and IBM-based JVMs), but if neither of 060 * those files exist (or if they cannot be parsed as a JKS or PKCS#12 keystore), 061 * then we will search for the file below the Java home directory. 062 */ 063@NotMutable() 064@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 065public final class JVMDefaultTrustManager 066 implements X509TrustManager, Serializable 067{ 068 /** 069 * A reference to the singleton instance of this class. 070 */ 071 private static final AtomicReference<JVMDefaultTrustManager> INSTANCE = 072 new AtomicReference<>(); 073 074 075 076 /** 077 * The name of the system property that specifies the path to the Java 078 * installation for the currently-running JVM. 079 */ 080 private static final String PROPERTY_JAVA_HOME = "java.home"; 081 082 083 084 /** 085 * A set of alternate file extensions that may be used by Java keystores. 086 */ 087 static final String[] FILE_EXTENSIONS = 088 { 089 ".jks", 090 ".p12", 091 ".pkcs12", 092 ".pfx", 093 }; 094 095 096 097 /** 098 * A pre-allocated empty certificate array. 099 */ 100 private static final X509Certificate[] NO_CERTIFICATES = 101 new X509Certificate[0]; 102 103 104 105 /** 106 * The serial version UID for this serializable class. 107 */ 108 private static final long serialVersionUID = -8587938729712485943L; 109 110 111 112 // A certificate exception that should be thrown for any attempt to use this 113 // trust store. 114 private final CertificateException certificateException; 115 116 // The file from which they keystore was loaded. 117 private final File caCertsFile; 118 119 // The keystore instance containing the JVM's default set of trusted issuers. 120 private final KeyStore keystore; 121 122 // A map of the certificates in the keystore, indexed by signature. 123 private final Map<ASN1OctetString,X509Certificate> trustedCertificateMap; 124 125 126 127 /** 128 * Creates an instance of this trust manager. 129 * 130 * @param javaHomePropertyName The name of the system property that should 131 * specify the path to the Java installation. 132 */ 133 JVMDefaultTrustManager(final String javaHomePropertyName) 134 { 135 // Determine the path to the root of the Java installation. 136 final String javaHomePath = System.getProperty(javaHomePropertyName); 137 if (javaHomePath == null) 138 { 139 certificateException = new CertificateException( 140 ERR_JVM_DEFAULT_TRUST_MANAGER_NO_JAVA_HOME.get( 141 javaHomePropertyName)); 142 caCertsFile = null; 143 keystore = null; 144 trustedCertificateMap = Collections.emptyMap(); 145 return; 146 } 147 148 final File javaHomeDirectory = new File(javaHomePath); 149 if ((! javaHomeDirectory.exists()) || (! javaHomeDirectory.isDirectory())) 150 { 151 certificateException = new CertificateException( 152 ERR_JVM_DEFAULT_TRUST_MANAGER_INVALID_JAVA_HOME.get( 153 javaHomePropertyName, javaHomePath)); 154 caCertsFile = null; 155 keystore = null; 156 trustedCertificateMap = Collections.emptyMap(); 157 return; 158 } 159 160 161 // Get a keystore instance that is loaded from the JVM's default set of 162 // trusted issuers. 163 final ObjectPair<KeyStore,File> keystorePair; 164 try 165 { 166 keystorePair = getJVMDefaultKeyStore(javaHomeDirectory); 167 } 168 catch (final CertificateException ce) 169 { 170 Debug.debugException(ce); 171 certificateException = ce; 172 caCertsFile = null; 173 keystore = null; 174 trustedCertificateMap = Collections.emptyMap(); 175 return; 176 } 177 178 keystore = keystorePair.getFirst(); 179 caCertsFile = keystorePair.getSecond(); 180 181 182 // Iterate through the certificates in the keystore and load them into a 183 // map for faster and more reliable access. 184 final LinkedHashMap<ASN1OctetString,X509Certificate> certificateMap = 185 new LinkedHashMap<>(StaticUtils.computeMapCapacity(50)); 186 try 187 { 188 final Enumeration<String> aliasEnumeration = keystore.aliases(); 189 while (aliasEnumeration.hasMoreElements()) 190 { 191 final String alias = aliasEnumeration.nextElement(); 192 193 try 194 { 195 final X509Certificate certificate = 196 (X509Certificate) keystore.getCertificate(alias); 197 if (certificate != null) 198 { 199 certificateMap.put(new ASN1OctetString(certificate.getSignature()), 200 certificate); 201 } 202 } 203 catch (final Exception e) 204 { 205 Debug.debugException(e); 206 } 207 } 208 } 209 catch (final Exception e) 210 { 211 Debug.debugException(e); 212 certificateException = new CertificateException( 213 ERR_JVM_DEFAULT_TRUST_MANAGER_ERROR_ITERATING_THROUGH_CACERTS.get( 214 caCertsFile.getAbsolutePath(), 215 StaticUtils.getExceptionMessage(e)), 216 e); 217 trustedCertificateMap = Collections.emptyMap(); 218 return; 219 } 220 221 trustedCertificateMap = Collections.unmodifiableMap(certificateMap); 222 certificateException = null; 223 } 224 225 226 227 /** 228 * Retrieves the singleton instance of this trust manager. 229 * 230 * @return The singleton instance of this trust manager. 231 */ 232 public static JVMDefaultTrustManager getInstance() 233 { 234 final JVMDefaultTrustManager existingInstance = INSTANCE.get(); 235 if (existingInstance != null) 236 { 237 return existingInstance; 238 } 239 240 final JVMDefaultTrustManager newInstance = 241 new JVMDefaultTrustManager(PROPERTY_JAVA_HOME); 242 if (INSTANCE.compareAndSet(null, newInstance)) 243 { 244 return newInstance; 245 } 246 else 247 { 248 return INSTANCE.get(); 249 } 250 } 251 252 253 254 /** 255 * Retrieves the keystore that backs this trust manager. 256 * 257 * @return The keystore that backs this trust manager. 258 * 259 * @throws CertificateException If a problem was encountered while 260 * initializing this trust manager. 261 */ 262 KeyStore getKeyStore() 263 throws CertificateException 264 { 265 if (certificateException != null) 266 { 267 throw certificateException; 268 } 269 270 return keystore; 271 } 272 273 274 275 /** 276 * Retrieves the path to the the file containing the JVM's default set of 277 * trusted issuers. 278 * 279 * @return The path to the file containing the JVM's default set of 280 * trusted issuers. 281 * 282 * @throws CertificateException If a problem was encountered while 283 * initializing this trust manager. 284 */ 285 public File getCACertsFile() 286 throws CertificateException 287 { 288 if (certificateException != null) 289 { 290 throw certificateException; 291 } 292 293 return caCertsFile; 294 } 295 296 297 298 /** 299 * Retrieves the certificates included in this trust manager. 300 * 301 * @return The certificates included in this trust manager. 302 * 303 * @throws CertificateException If a problem was encountered while 304 * initializing this trust manager. 305 */ 306 public Collection<X509Certificate> getTrustedIssuerCertificates() 307 throws CertificateException 308 { 309 if (certificateException != null) 310 { 311 throw certificateException; 312 } 313 314 return trustedCertificateMap.values(); 315 } 316 317 318 319 /** 320 * Checks to determine whether the provided client certificate chain should be 321 * trusted. 322 * 323 * @param chain The client certificate chain for which to make the 324 * determination. 325 * @param authType The authentication type based on the client certificate. 326 * 327 * @throws CertificateException If the provided client certificate chain 328 * should not be trusted. 329 */ 330 @Override() 331 public void checkClientTrusted(final X509Certificate[] chain, 332 final String authType) 333 throws CertificateException 334 { 335 checkTrusted(chain); 336 } 337 338 339 340 /** 341 * Checks to determine whether the provided server certificate chain should be 342 * trusted. 343 * 344 * @param chain The server certificate chain for which to make the 345 * determination. 346 * @param authType The key exchange algorithm used. 347 * 348 * @throws CertificateException If the provided server certificate chain 349 * should not be trusted. 350 */ 351 @Override() 352 public void checkServerTrusted(final X509Certificate[] chain, 353 final String authType) 354 throws CertificateException 355 { 356 checkTrusted(chain); 357 } 358 359 360 361 /** 362 * Retrieves the accepted issuer certificates for this trust manager. 363 * 364 * @return The accepted issuer certificates for this trust manager, or an 365 * empty set of accepted issuers if a problem was encountered while 366 * initializing this trust manager. 367 */ 368 @Override() 369 public X509Certificate[] getAcceptedIssuers() 370 { 371 if (certificateException != null) 372 { 373 return NO_CERTIFICATES; 374 } 375 376 final X509Certificate[] acceptedIssuers = 377 new X509Certificate[trustedCertificateMap.size()]; 378 return trustedCertificateMap.values().toArray(acceptedIssuers); 379 } 380 381 382 383 /** 384 * Retrieves a {@code KeyStore} that contains the JVM's default set of trusted 385 * issuers. 386 * 387 * @param javaHomeDirectory The path to the JVM installation home directory. 388 * 389 * @return An {@code ObjectPair} that includes the keystore and the file from 390 * which it was loaded. 391 * 392 * @throws CertificateException If the keystore could not be found or 393 * loaded. 394 */ 395 private static ObjectPair<KeyStore,File> getJVMDefaultKeyStore( 396 final File javaHomeDirectory) 397 throws CertificateException 398 { 399 final File libSecurityCACerts = StaticUtils.constructPath(javaHomeDirectory, 400 "lib", "security", "cacerts"); 401 final File jreLibSecurityCACerts = StaticUtils.constructPath( 402 javaHomeDirectory, "jre", "lib", "security", "cacerts"); 403 404 final ArrayList<File> tryFirstFiles = 405 new ArrayList<>(2 * FILE_EXTENSIONS.length + 2); 406 tryFirstFiles.add(libSecurityCACerts); 407 tryFirstFiles.add(jreLibSecurityCACerts); 408 409 for (final String extension : FILE_EXTENSIONS) 410 { 411 tryFirstFiles.add( 412 new File(libSecurityCACerts.getAbsolutePath() + extension)); 413 tryFirstFiles.add( 414 new File(jreLibSecurityCACerts.getAbsolutePath() + extension)); 415 } 416 417 for (final File f : tryFirstFiles) 418 { 419 final KeyStore keyStore = loadKeyStore(f); 420 if (keyStore != null) 421 { 422 return new ObjectPair<>(keyStore, f); 423 } 424 } 425 426 427 // If we didn't find it with known paths, then try to find it with a 428 // recursive filesystem search below the Java home directory. 429 final LinkedHashMap<File,CertificateException> exceptions = 430 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 431 final ObjectPair<KeyStore,File> keystorePair = 432 searchForKeyStore(javaHomeDirectory, exceptions); 433 if (keystorePair != null) 434 { 435 return keystorePair; 436 } 437 438 439 // If we've gotten here, then we couldn't find the keystore. Construct a 440 // message from the set of exceptions. 441 if (exceptions.isEmpty()) 442 { 443 throw new CertificateException( 444 ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_NO_EXCEPTION.get()); 445 } 446 else 447 { 448 final StringBuilder buffer = new StringBuilder(); 449 buffer.append( 450 ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_WITH_EXCEPTION. 451 get()); 452 for (final Map.Entry<File,CertificateException> e : exceptions.entrySet()) 453 { 454 if (buffer.charAt(buffer.length() - 1) != '.') 455 { 456 buffer.append('.'); 457 } 458 459 buffer.append(" "); 460 buffer.append(ERR_JVM_DEFAULT_TRUST_MANAGER_LOAD_ERROR.get( 461 e.getKey().getAbsolutePath(), 462 StaticUtils.getExceptionMessage(e.getValue()))); 463 } 464 465 throw new CertificateException(buffer.toString()); 466 } 467 } 468 469 470 471 /** 472 * Recursively searches for a valid keystore file below the specified portion 473 * of the filesystem. Any file named "cacerts", ignoring differences in 474 * capitalization, and optionally ending with a number of different file 475 * extensions, will be examined to see if it can be parsed as a Java keystore. 476 * The first keystore that we find meeting that criteria will be returned. 477 * 478 * @param directory The directory in which to search. It must not be 479 * {@code null}. 480 * @param exceptions A map that correlates file paths with exceptions 481 * obtained while interacting with them. If an exception 482 * is encountered while interacting with this file, then 483 * it will be added to this map. 484 * 485 * @return The first valid keystore found that meets all the necessary 486 * criteria, or {@code null} if no such keystore could be found. 487 */ 488 private static ObjectPair<KeyStore,File> searchForKeyStore( 489 final File directory, 490 final Map<File,CertificateException> exceptions) 491 { 492filesInDirectoryLoop: 493 for (final File f : directory.listFiles()) 494 { 495 if (f.isDirectory()) 496 { 497 final ObjectPair<KeyStore,File> p =searchForKeyStore(f, exceptions); 498 if (p != null) 499 { 500 return p; 501 } 502 } 503 else 504 { 505 final String lowerName = StaticUtils.toLowerCase(f.getName()); 506 if (lowerName.equals("cacerts")) 507 { 508 try 509 { 510 final KeyStore keystore = loadKeyStore(f); 511 return new ObjectPair<>(keystore, f); 512 } 513 catch (final CertificateException ce) 514 { 515 Debug.debugException(ce); 516 exceptions.put(f, ce); 517 } 518 } 519 else 520 { 521 for (final String extension : FILE_EXTENSIONS) 522 { 523 if (lowerName.equals("cacerts" + extension)) 524 { 525 try 526 { 527 final KeyStore keystore = loadKeyStore(f); 528 return new ObjectPair<>(keystore, f); 529 } 530 catch (final CertificateException ce) 531 { 532 Debug.debugException(ce); 533 exceptions.put(f, ce); 534 continue filesInDirectoryLoop; 535 } 536 } 537 } 538 } 539 } 540 } 541 542 return null; 543 } 544 545 546 547 /** 548 * Attempts to load the contents of the specified file as a Java keystore. 549 * 550 * @param f The file from which to load the keystore data. 551 * 552 * @return The keystore that was loaded from the specified file. 553 * 554 * @throws CertificateException If a problem occurs while trying to load the 555 * 556 */ 557 private static KeyStore loadKeyStore(final File f) 558 throws CertificateException 559 { 560 if ((! f.exists()) || (! f.isFile())) 561 { 562 return null; 563 } 564 565 CertificateException firstGetInstanceException = null; 566 CertificateException firstLoadException = null; 567 for (final String keyStoreType : new String[] { "JKS", "PKCS12" }) 568 { 569 final KeyStore keyStore; 570 try 571 { 572 keyStore = KeyStore.getInstance(keyStoreType); 573 } 574 catch (final Exception e) 575 { 576 Debug.debugException(e); 577 if (firstGetInstanceException == null) 578 { 579 firstGetInstanceException = new CertificateException( 580 ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_INSTANTIATE_KEYSTORE.get( 581 keyStoreType, StaticUtils.getExceptionMessage(e)), 582 e); 583 } 584 continue; 585 } 586 587 try (FileInputStream inputStream = new FileInputStream(f)) 588 { 589 keyStore.load(inputStream, null); 590 } 591 catch (final Exception e) 592 { 593 Debug.debugException(e); 594 if (firstLoadException == null) 595 { 596 firstLoadException = new CertificateException( 597 ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_ERROR_LOADING_KEYSTORE.get( 598 f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)), 599 e); 600 } 601 continue; 602 } 603 604 return keyStore; 605 } 606 607 if (firstLoadException != null) 608 { 609 throw firstLoadException; 610 } 611 612 throw firstGetInstanceException; 613 } 614 615 616 617 /** 618 * Ensures that the provided certificate chain should be considered trusted. 619 * 620 * @param chain The certificate chain to validate. It must not be 621 * {@code null}). 622 * 623 * @throws CertificateException If the provided certificate chain should not 624 * be considered trusted. 625 */ 626 void checkTrusted(final X509Certificate[] chain) 627 throws CertificateException 628 { 629 if (certificateException != null) 630 { 631 throw certificateException; 632 } 633 634 if ((chain == null) || (chain.length == 0)) 635 { 636 throw new CertificateException( 637 ERR_JVM_DEFAULT_TRUST_MANAGER_NO_CERTS_IN_CHAIN.get()); 638 } 639 640 boolean foundIssuer = false; 641 final Date currentTime = new Date(); 642 for (final X509Certificate cert : chain) 643 { 644 // Make sure that the certificate is currently within its validity window. 645 final Date notBefore = cert.getNotBefore(); 646 if (currentTime.before(notBefore)) 647 { 648 throw new CertificateNotYetValidException( 649 ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_NOT_YET_VALID.get( 650 chainToString(chain), String.valueOf(cert.getSubjectDN()), 651 String.valueOf(notBefore))); 652 } 653 654 final Date notAfter = cert.getNotAfter(); 655 if (currentTime.after(notAfter)) 656 { 657 throw new CertificateExpiredException( 658 ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_EXPIRED.get( 659 chainToString(chain), 660 String.valueOf(cert.getSubjectDN()), 661 String.valueOf(notAfter))); 662 } 663 664 final ASN1OctetString signature = 665 new ASN1OctetString(cert.getSignature()); 666 foundIssuer |= (trustedCertificateMap.get(signature) != null); 667 } 668 669 if (! foundIssuer) 670 { 671 throw new CertificateException( 672 ERR_JVM_DEFAULT_TRUST_MANGER_NO_TRUSTED_ISSUER_FOUND.get( 673 chainToString(chain))); 674 } 675 } 676 677 678 679 /** 680 * Constructs a string representation of the certificates in the provided 681 * chain. It will consist of a comma-delimited list of their subject DNs, 682 * with each subject DN surrounded by single quotes. 683 * 684 * @param chain The chain for which to obtain the string representation. 685 * 686 * @return A string representation of the provided certificate chain. 687 */ 688 static String chainToString(final X509Certificate[] chain) 689 { 690 final StringBuilder buffer = new StringBuilder(); 691 692 switch (chain.length) 693 { 694 case 0: 695 break; 696 case 1: 697 buffer.append('\''); 698 buffer.append(chain[0].getSubjectDN()); 699 buffer.append('\''); 700 break; 701 case 2: 702 buffer.append('\''); 703 buffer.append(chain[0].getSubjectDN()); 704 buffer.append("' and '"); 705 buffer.append(chain[1].getSubjectDN()); 706 buffer.append('\''); 707 break; 708 default: 709 for (int i=0; i < chain.length; i++) 710 { 711 if (i > 0) 712 { 713 buffer.append(", "); 714 } 715 716 if (i == (chain.length - 1)) 717 { 718 buffer.append("and "); 719 } 720 721 buffer.append('\''); 722 buffer.append(chain[i].getSubjectDN()); 723 buffer.append('\''); 724 } 725 } 726 727 return buffer.toString(); 728 } 729}