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}