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