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