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}