001/*
002 * Copyright 2017-2021 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2017-2021 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-2021 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.Arrays;
050import java.util.Collection;
051import java.util.Collections;
052import java.util.Date;
053import java.util.Enumeration;
054import java.util.LinkedHashMap;
055import java.util.List;
056import java.util.Map;
057import java.util.concurrent.atomic.AtomicReference;
058import javax.net.ssl.X509TrustManager;
059
060import com.unboundid.asn1.ASN1OctetString;
061import com.unboundid.util.CryptoHelper;
062import com.unboundid.util.Debug;
063import com.unboundid.util.NotMutable;
064import com.unboundid.util.NotNull;
065import com.unboundid.util.Nullable;
066import com.unboundid.util.ObjectPair;
067import com.unboundid.util.StaticUtils;
068import com.unboundid.util.ThreadSafety;
069import com.unboundid.util.ThreadSafetyLevel;
070import com.unboundid.util.ssl.cert.AuthorityKeyIdentifierExtension;
071import com.unboundid.util.ssl.cert.SubjectKeyIdentifierExtension;
072import com.unboundid.util.ssl.cert.X509CertificateExtension;
073
074import static com.unboundid.util.ssl.SSLMessages.*;
075
076
077
078/**
079 * This class provides an implementation of a trust manager that relies on the
080 * JVM's default set of trusted issuers.  This is generally found in the
081 * {@code jre/lib/security/cacerts} or {@code lib/security/cacerts} file in the
082 * Java installation (in both Sun/Oracle and IBM-based JVMs), but if neither of
083 * those files exist (or if they cannot be parsed as a JKS or PKCS#12 keystore),
084 * then we will search for the file below the Java home directory.
085 */
086@NotMutable()
087@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
088public final class JVMDefaultTrustManager
089       implements X509TrustManager, Serializable
090{
091  /**
092   * A reference to the singleton instance of this class.
093   */
094  @NotNull private static final AtomicReference<JVMDefaultTrustManager>
095       INSTANCE = new AtomicReference<>();
096
097
098
099  /**
100   * The name of the system property that specifies the path to the Java
101   * installation for the currently-running JVM.
102   */
103  @NotNull private static final String PROPERTY_JAVA_HOME = "java.home";
104
105
106
107  /**
108   * A set of alternate file extensions that may be used by Java keystores.
109   */
110  @NotNull static final String[] FILE_EXTENSIONS  =
111  {
112    ".jks",
113    ".p12",
114    ".pkcs12",
115    ".pfx",
116  };
117
118
119
120  /**
121   * A pre-allocated empty certificate array.
122   */
123  @NotNull private static final X509Certificate[] NO_CERTIFICATES =
124       new X509Certificate[0];
125
126
127
128  /**
129   * The serial version UID for this serializable class.
130   */
131  private static final long serialVersionUID = -8587938729712485943L;
132
133
134
135  // A certificate exception that should be thrown for any attempt to use this
136  // trust store.
137  @Nullable private final CertificateException certificateException;
138
139  // The file from which they keystore was loaded.
140  @Nullable private final File caCertsFile;
141
142  // The keystore instance containing the JVM's default set of trusted issuers.
143  @Nullable private final KeyStore keystore;
144
145  // A map of the certificates in the keystore, indexed by signature.
146  @NotNull private final Map<ASN1OctetString,X509Certificate>
147       trustedCertsBySignature;
148
149  // A map of the certificates in the keystore, indexed by key ID.
150  @NotNull private final Map<ASN1OctetString,
151       com.unboundid.util.ssl.cert.X509Certificate> trustedCertsByKeyID;
152
153
154
155  /**
156   * Creates an instance of this trust manager.
157   *
158   * @param  javaHomePropertyName  The name of the system property that should
159   *                               specify the path to the Java installation.
160   */
161  JVMDefaultTrustManager(@NotNull final String javaHomePropertyName)
162  {
163    // Determine the path to the root of the Java installation.
164    final String javaHomePath =
165         StaticUtils.getSystemProperty(javaHomePropertyName);
166    if (javaHomePath == null)
167    {
168      certificateException = new CertificateException(
169           ERR_JVM_DEFAULT_TRUST_MANAGER_NO_JAVA_HOME.get(
170                javaHomePropertyName));
171      caCertsFile = null;
172      keystore = null;
173      trustedCertsBySignature = Collections.emptyMap();
174      trustedCertsByKeyID = Collections.emptyMap();
175      return;
176    }
177
178    final File javaHomeDirectory = new File(javaHomePath);
179    if ((! javaHomeDirectory.exists()) || (! javaHomeDirectory.isDirectory()))
180    {
181      certificateException = new CertificateException(
182           ERR_JVM_DEFAULT_TRUST_MANAGER_INVALID_JAVA_HOME.get(
183                javaHomePropertyName, javaHomePath));
184      caCertsFile = null;
185      keystore = null;
186      trustedCertsBySignature = Collections.emptyMap();
187      trustedCertsByKeyID = Collections.emptyMap();
188      return;
189    }
190
191
192    // Get a keystore instance that is loaded from the JVM's default set of
193    // trusted issuers.
194    final ObjectPair<KeyStore,File> keystorePair;
195    try
196    {
197      keystorePair = getJVMDefaultKeyStore(javaHomeDirectory);
198    }
199    catch (final CertificateException ce)
200    {
201      Debug.debugException(ce);
202      certificateException = ce;
203      caCertsFile = null;
204      keystore = null;
205      trustedCertsBySignature = Collections.emptyMap();
206      trustedCertsByKeyID = Collections.emptyMap();
207      return;
208    }
209
210    keystore = keystorePair.getFirst();
211    caCertsFile = keystorePair.getSecond();
212
213
214    // Iterate through the certificates in the keystore and load them into a
215    // map for faster and more reliable access.
216    final LinkedHashMap<ASN1OctetString,X509Certificate> certsBySignature =
217         new LinkedHashMap<>(StaticUtils.computeMapCapacity(50));
218    final LinkedHashMap<ASN1OctetString,
219         com.unboundid.util.ssl.cert.X509Certificate> certsByKeyID =
220         new LinkedHashMap<>(StaticUtils.computeMapCapacity(50));
221    try
222    {
223      final Enumeration<String> aliasEnumeration = keystore.aliases();
224      while (aliasEnumeration.hasMoreElements())
225      {
226        final String alias = aliasEnumeration.nextElement();
227
228        try
229        {
230          final X509Certificate certificate =
231               (X509Certificate) keystore.getCertificate(alias);
232          if (certificate != null)
233          {
234            certsBySignature.put(
235                 new ASN1OctetString(certificate.getSignature()),
236                 certificate);
237
238            try
239            {
240              final com.unboundid.util.ssl.cert.X509Certificate c =
241                   new com.unboundid.util.ssl.cert.X509Certificate(
242                        certificate.getEncoded());
243              for (final X509CertificateExtension e : c.getExtensions())
244              {
245                if (e instanceof SubjectKeyIdentifierExtension)
246                {
247                  final SubjectKeyIdentifierExtension skie =
248                       (SubjectKeyIdentifierExtension) e;
249                  certsByKeyID.put(
250                       new ASN1OctetString(skie.getKeyIdentifier().getValue()),
251                       c);
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        }
265      }
266    }
267    catch (final Exception e)
268    {
269      Debug.debugException(e);
270      certificateException = new CertificateException(
271           ERR_JVM_DEFAULT_TRUST_MANAGER_ERROR_ITERATING_THROUGH_CACERTS.get(
272                caCertsFile.getAbsolutePath(),
273                StaticUtils.getExceptionMessage(e)),
274           e);
275      trustedCertsBySignature = Collections.emptyMap();
276      trustedCertsByKeyID = Collections.emptyMap();
277      return;
278    }
279
280    trustedCertsBySignature = Collections.unmodifiableMap(certsBySignature);
281    trustedCertsByKeyID = Collections.unmodifiableMap(certsByKeyID);
282    certificateException = null;
283  }
284
285
286
287  /**
288   * Retrieves the singleton instance of this trust manager.
289   *
290   * @return  The singleton instance of this trust manager.
291   */
292  @NotNull()
293  public static JVMDefaultTrustManager getInstance()
294  {
295    final JVMDefaultTrustManager existingInstance = INSTANCE.get();
296    if (existingInstance != null)
297    {
298      return existingInstance;
299    }
300
301    final JVMDefaultTrustManager newInstance =
302         new JVMDefaultTrustManager(PROPERTY_JAVA_HOME);
303    if (INSTANCE.compareAndSet(null, newInstance))
304    {
305      return newInstance;
306    }
307    else
308    {
309      return INSTANCE.get();
310    }
311  }
312
313
314
315  /**
316   * Retrieves the keystore that backs this trust manager.
317   *
318   * @return  The keystore that backs this trust manager.
319   *
320   * @throws  CertificateException  If a problem was encountered while
321   *                                initializing this trust manager.
322   */
323  @NotNull()
324  KeyStore getKeyStore()
325           throws CertificateException
326  {
327    if (certificateException != null)
328    {
329      throw certificateException;
330    }
331
332    return keystore;
333  }
334
335
336
337  /**
338   * Retrieves the path to the the file containing the JVM's default set of
339   * trusted issuers.
340   *
341   * @return  The path to the file containing the JVM's default set of
342   *          trusted issuers.
343   *
344   * @throws  CertificateException  If a problem was encountered while
345   *                                initializing this trust manager.
346   */
347  @NotNull()
348  public File getCACertsFile()
349         throws CertificateException
350  {
351    if (certificateException != null)
352    {
353      throw certificateException;
354    }
355
356    return caCertsFile;
357  }
358
359
360
361  /**
362   * Retrieves the certificates included in this trust manager.
363   *
364   * @return  The certificates included in this trust manager.
365   *
366   * @throws  CertificateException  If a problem was encountered while
367   *                                initializing this trust manager.
368   */
369  @NotNull()
370  public Collection<X509Certificate> getTrustedIssuerCertificates()
371         throws CertificateException
372  {
373    if (certificateException != null)
374    {
375      throw certificateException;
376    }
377
378    return trustedCertsBySignature.values();
379  }
380
381
382
383  /**
384   * Checks to determine whether the provided client certificate chain should be
385   * trusted.
386   *
387   * @param  chain     The client certificate chain for which to make the
388   *                   determination.
389   * @param  authType  The authentication type based on the client certificate.
390   *
391   * @throws  CertificateException  If the provided client certificate chain
392   *                                should not be trusted.
393   */
394  @Override()
395  public void checkClientTrusted(@NotNull final X509Certificate[] chain,
396                                 @NotNull final String authType)
397         throws CertificateException
398  {
399    checkTrusted(chain);
400  }
401
402
403
404  /**
405   * Checks to determine whether the provided server certificate chain should be
406   * trusted.
407   *
408   * @param  chain     The server certificate chain for which to make the
409   *                   determination.
410   * @param  authType  The key exchange algorithm used.
411   *
412   * @throws  CertificateException  If the provided server certificate chain
413   *                                should not be trusted.
414   */
415  @Override()
416  public void checkServerTrusted(@NotNull final X509Certificate[] chain,
417                                 @NotNull final String authType)
418         throws CertificateException
419  {
420    checkTrusted(chain);
421  }
422
423
424
425  /**
426   * Retrieves the accepted issuer certificates for this trust manager.
427   *
428   * @return  The accepted issuer certificates for this trust manager, or an
429   *          empty set of accepted issuers if a problem was encountered while
430   *          initializing this trust manager.
431   */
432  @Override()
433  @NotNull()
434  public X509Certificate[] getAcceptedIssuers()
435  {
436    if (certificateException != null)
437    {
438      return NO_CERTIFICATES;
439    }
440
441    final X509Certificate[] acceptedIssuers =
442         new X509Certificate[trustedCertsBySignature.size()];
443    return trustedCertsBySignature.values().toArray(acceptedIssuers);
444  }
445
446
447
448  /**
449   * Retrieves a {@code KeyStore} that contains the JVM's default set of trusted
450   * issuers.
451   *
452   * @param  javaHomeDirectory  The path to the JVM installation home directory.
453   *
454   * @return  An {@code ObjectPair} that includes the keystore and the file from
455   *          which it was loaded.
456   *
457   * @throws  CertificateException  If the keystore could not be found or
458   *                                loaded.
459   */
460  @NotNull()
461  private static ObjectPair<KeyStore,File> getJVMDefaultKeyStore(
462                      @NotNull final File javaHomeDirectory)
463          throws CertificateException
464  {
465    final File libSecurityCACerts = StaticUtils.constructPath(javaHomeDirectory,
466         "lib", "security", "cacerts");
467    final File jreLibSecurityCACerts = StaticUtils.constructPath(
468         javaHomeDirectory, "jre", "lib", "security", "cacerts");
469
470    final ArrayList<File> tryFirstFiles =
471         new ArrayList<>(2 * FILE_EXTENSIONS.length + 2);
472    tryFirstFiles.add(libSecurityCACerts);
473    tryFirstFiles.add(jreLibSecurityCACerts);
474
475    for (final String extension : FILE_EXTENSIONS)
476    {
477      tryFirstFiles.add(
478           new File(libSecurityCACerts.getAbsolutePath() + extension));
479      tryFirstFiles.add(
480           new File(jreLibSecurityCACerts.getAbsolutePath() + extension));
481    }
482
483    for (final File f : tryFirstFiles)
484    {
485      final KeyStore keyStore = loadKeyStore(f);
486      if (keyStore != null)
487      {
488        return new ObjectPair<>(keyStore, f);
489      }
490    }
491
492
493    // If we didn't find it with known paths, then try to find it with a
494    // recursive filesystem search below the Java home directory.
495    final LinkedHashMap<File,CertificateException> exceptions =
496         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
497    final ObjectPair<KeyStore,File> keystorePair =
498         searchForKeyStore(javaHomeDirectory, exceptions);
499    if (keystorePair != null)
500    {
501      return keystorePair;
502    }
503
504
505    // If we've gotten here, then we couldn't find the keystore.  Construct a
506    // message from the set of exceptions.
507    if (exceptions.isEmpty())
508    {
509      throw new CertificateException(
510           ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_NO_EXCEPTION.get());
511    }
512    else
513    {
514      final StringBuilder buffer = new StringBuilder();
515      buffer.append(
516           ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_WITH_EXCEPTION.
517                get());
518      for (final Map.Entry<File,CertificateException> e : exceptions.entrySet())
519      {
520        if (buffer.charAt(buffer.length() - 1) != '.')
521        {
522          buffer.append('.');
523        }
524
525        buffer.append("  ");
526        buffer.append(ERR_JVM_DEFAULT_TRUST_MANAGER_LOAD_ERROR.get(
527             e.getKey().getAbsolutePath(),
528             StaticUtils.getExceptionMessage(e.getValue())));
529      }
530
531      throw new CertificateException(buffer.toString());
532    }
533  }
534
535
536
537  /**
538   * Recursively searches for a valid keystore file below the specified portion
539   * of the filesystem.  Any file named "cacerts", ignoring differences in
540   * capitalization, and optionally ending with a number of different file
541   * extensions, will be examined to see if it can be parsed as a Java keystore.
542   * The first keystore that we find meeting that criteria will be returned.
543   *
544   * @param  directory   The directory in which to search.  It must not be
545   *                     {@code null}.
546   * @param  exceptions  A map that correlates file paths with exceptions
547   *                     obtained while interacting with them.  If an exception
548   *                     is encountered while interacting with this file, then
549   *                     it will be added to this map.
550   *
551   * @return  The first valid keystore found that meets all the necessary
552   *          criteria, or {@code null} if no such keystore could be found.
553   */
554  @Nullable()
555  private static ObjectPair<KeyStore,File> searchForKeyStore(
556                      @NotNull final File directory,
557                      @NotNull final Map<File,CertificateException> exceptions)
558  {
559filesInDirectoryLoop:
560    for (final File f : directory.listFiles())
561    {
562      if (f.isDirectory())
563      {
564        final ObjectPair<KeyStore,File> p =searchForKeyStore(f, exceptions);
565        if (p != null)
566        {
567          return p;
568        }
569      }
570      else
571      {
572        final String lowerName = StaticUtils.toLowerCase(f.getName());
573        if (lowerName.equals("cacerts"))
574        {
575          try
576          {
577            final KeyStore keystore = loadKeyStore(f);
578            return new ObjectPair<>(keystore, f);
579          }
580          catch (final CertificateException ce)
581          {
582            Debug.debugException(ce);
583            exceptions.put(f, ce);
584          }
585        }
586        else
587        {
588          for (final String extension : FILE_EXTENSIONS)
589          {
590            if (lowerName.equals("cacerts" + extension))
591            {
592              try
593              {
594                final KeyStore keystore = loadKeyStore(f);
595                return new ObjectPair<>(keystore, f);
596              }
597              catch (final CertificateException ce)
598              {
599                Debug.debugException(ce);
600                exceptions.put(f, ce);
601                continue filesInDirectoryLoop;
602              }
603            }
604          }
605        }
606      }
607    }
608
609    return null;
610  }
611
612
613
614  /**
615   * Attempts to load the contents of the specified file as a Java keystore.
616   *
617   * @param  f  The file from which to load the keystore data.
618   *
619   * @return  The keystore that was loaded from the specified file.
620   *
621   * @throws  CertificateException  If a problem occurs while trying to load the
622   *
623   */
624  @Nullable()
625  private static KeyStore loadKeyStore(@NotNull final File f)
626          throws CertificateException
627  {
628    if ((! f.exists()) || (! f.isFile()))
629    {
630      return null;
631    }
632
633    CertificateException firstGetInstanceException = null;
634    CertificateException firstLoadException = null;
635    for (final String keyStoreType : new String[] { "JKS", "PKCS12" })
636    {
637      final KeyStore keyStore;
638      try
639      {
640        keyStore = CryptoHelper.getKeyStore(keyStoreType, null, true);
641      }
642      catch (final Exception e)
643      {
644        Debug.debugException(e);
645        if (firstGetInstanceException == null)
646        {
647          firstGetInstanceException = new CertificateException(
648               ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_INSTANTIATE_KEYSTORE.get(
649                    keyStoreType, StaticUtils.getExceptionMessage(e)),
650               e);
651        }
652        continue;
653      }
654
655      try (FileInputStream inputStream = new FileInputStream(f))
656      {
657        keyStore.load(inputStream, null);
658      }
659      catch (final Exception e)
660      {
661        Debug.debugException(e);
662        if (firstLoadException == null)
663        {
664          firstLoadException = new CertificateException(
665               ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_ERROR_LOADING_KEYSTORE.get(
666                    f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
667               e);
668        }
669        continue;
670      }
671
672      return keyStore;
673    }
674
675    if (firstLoadException != null)
676    {
677      throw firstLoadException;
678    }
679
680    throw firstGetInstanceException;
681  }
682
683
684
685  /**
686   * Ensures that the provided certificate chain should be considered trusted.
687   *
688   * @param  chain  The certificate chain to validate.  It must not be
689   *                {@code null}).
690   *
691   * @throws  CertificateException  If the provided certificate chain should not
692   *                                be considered trusted.
693   */
694  void checkTrusted(@NotNull final X509Certificate[] chain)
695       throws CertificateException
696  {
697    if (certificateException != null)
698    {
699      throw certificateException;
700    }
701
702    if ((chain == null) || (chain.length == 0))
703    {
704      throw new CertificateException(
705           ERR_JVM_DEFAULT_TRUST_MANAGER_NO_CERTS_IN_CHAIN.get());
706    }
707
708
709    // It is possible that the chain could rely on cross-signed certificates,
710    // and that we need to use a different path than the one presented in the
711    // provided chain.  This requires us to potentially compute signatures using
712    // each certificate in the JVM's default trust store, which can be
713    // expensive.  To avoid that, we'll first only try it if the presented
714    // chain has any certificates that are outside of their current validity
715    // window.  If we get back a chain that is different from the one provided
716    // to this method, then we shouldn't need to do any further validation.
717    final X509Certificate[] chainToValidate = getChainToValidate(chain, true);
718    if (! Arrays.equals(chainToValidate, chain))
719    {
720      return;
721    }
722
723
724    boolean foundIssuer = false;
725    final Date currentTime = new Date();
726    for (final X509Certificate cert : chainToValidate)
727    {
728      final ASN1OctetString signature =
729           new ASN1OctetString(cert.getSignature());
730      foundIssuer = (trustedCertsBySignature.get(signature) != null);
731      if (foundIssuer)
732      {
733        break;
734      }
735    }
736
737    if (! foundIssuer)
738    {
739      // It's possible that the server sent an incomplete chain.  Handle that
740      // possibility.
741      foundIssuer = checkIncompleteChain(chain);
742    }
743
744    if (! foundIssuer)
745    {
746      // We couldn't validate the presented chain, so see if we can find an
747      // alternative chain using a cross-signed certificate.  In this case,
748      // we'll perform the expensive check regardless of the validity dates in
749      // the presented chain.  If the attempt to find an alternative chain
750      // fails, then the getChainToValidate method will throw an exception.
751      // However, if the alternative chain contains only a single certificate,
752      // then that suggests the certificate is self-signed and not signed by
753      // any trusted issuer.
754      final X509Certificate[] alternativeChain =
755           getChainToValidate(chain, false);
756      if (Arrays.equals(alternativeChain, chain))
757      {
758        throw new CertificateException(
759             ERR_JVM_DEFAULT_TRUST_MANGER_NO_TRUSTED_ISSUER_FOUND.get(
760                  chainToString(chain)));
761      }
762    }
763  }
764
765
766
767  /**
768   * Retrieves a list containing the certificates in the chain that should
769   * actually be validated.  All certificates in the chain will have been
770   * confirmed to be in their validity window.
771   *
772   * @param  chain                     The chain for which to obtain the path to
773   *                                   validate.  It must not be {@code null} or
774   *                                   empty.
775   * @param  checkChainValidityWindow  Indicates whether to examine the validity
776   *                                   of certificates in the presented chain
777   *                                   when determining whether to examine
778   *                                   certificates by signature.  If this is
779   *                                   {@code true}, then the provided chain
780   *                                   will be returned as long as all of the
781   *                                   certificates in it are within their
782   *                                   validity window.  If this is
783   *                                   {@code false}, then an attempt to find a
784   *                                   chain based on signatures will be used
785   *                                   even if all of the certificates in the
786   *                                   presented chain are considered valid.
787   *
788   * @return  The chain to be validated.  It may be the same as the provided
789   *          chain, or an alternate chain if any certificate in the provided
790   *          chain was outside of its validity window but an alternative trust
791   *          path could be found.
792   *
793   * @throws  CertificateException  If the presented certificate chain included
794   *                                a certificate that is outside of its
795   *                                current validity window and no alternate
796   *                                path could be found.
797   */
798  @NotNull()
799  private X509Certificate[] getChainToValidate(
800                                 @NotNull final X509Certificate[] chain,
801                                 final boolean checkChainValidityWindow)
802          throws CertificateException
803  {
804    final Date currentDate = new Date();
805
806    // Check to see if any certificate in the provided chain is outside the
807    // current validity window.  If not, then just use the provided chain.
808    CertificateException firstException = null;
809    if (checkChainValidityWindow)
810    {
811      for (int i=0; i < chain.length; i++)
812      {
813        final X509Certificate cert = chain[i];
814
815        final Date notBefore = cert.getNotBefore();
816        if (currentDate.before(notBefore))
817        {
818          if (firstException == null)
819          {
820            firstException = new CertificateNotYetValidException(
821                 ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_NOT_YET_VALID.get(
822                      chainToString(chain), String.valueOf(cert.getSubjectDN()),
823                      String.valueOf(notBefore)));
824          }
825
826          if (i == 0)
827          {
828            // If the peer certificate is not yet valid, then the entire chain
829            // must be considered invalid.
830            throw firstException;
831          }
832          else
833          {
834            break;
835          }
836        }
837
838        final Date notAfter = cert.getNotAfter();
839        if (currentDate.after(notAfter))
840        {
841          if (firstException == null)
842          {
843            firstException = new CertificateExpiredException(
844                 ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_EXPIRED.get(
845                      chainToString(chain),
846                      String.valueOf(cert.getSubjectDN()),
847                      String.valueOf(notAfter)));
848          }
849
850          if (i == 0)
851          {
852            // If the peer certificate is expired, then the entire chain must be
853            // considered invalid.
854            throw firstException;
855          }
856          else
857          {
858            break;
859          }
860        }
861      }
862
863
864      // If all the certificates in the chain were within their validity window,
865      // then just use the provided chain.
866      if (firstException == null)
867      {
868        return chain;
869      }
870    }
871
872
873    // If we've gotten here, then we should try to find an alternative chain.
874    boolean foundAlternative = false;
875    final List<X509Certificate> alternativeChain = new ArrayList<>();
876chainLoop:
877    for (final X509Certificate c : chain)
878    {
879      alternativeChain.add(c);
880      try
881      {
882        final X509Certificate issuer = findIssuer(c, currentDate);
883        if (issuer == null)
884        {
885          break;
886        }
887        else
888        {
889          foundAlternative = true;
890          alternativeChain.add(issuer);
891
892          X509Certificate prevIssuer = issuer;
893          while (true)
894          {
895            try
896            {
897              final X509Certificate nextIssuer =
898                   findIssuer(prevIssuer, currentDate);
899              if (nextIssuer == null)
900              {
901                break chainLoop;
902              }
903              else
904              {
905                alternativeChain.add(nextIssuer);
906                prevIssuer = nextIssuer;
907              }
908            }
909            catch (final CertificateException e)
910            {
911              foundAlternative = false;
912              break chainLoop;
913            }
914          }
915        }
916      }
917      catch (final CertificateException e)
918      {
919        Debug.debugException(e);
920      }
921    }
922
923    if (foundAlternative)
924    {
925      return alternativeChain.toArray(NO_CERTIFICATES);
926    }
927    else
928    {
929      if (firstException == null)
930      {
931        throw new CertificateException(
932             ERR_JVM_DEFAULT_TRUST_MANGER_NO_TRUSTED_ISSUER_FOUND.get(
933                  chainToString(chain)));
934      }
935      else
936      {
937        throw firstException;
938      }
939    }
940  }
941
942
943
944  /**
945   * Finds the issuer for the provided certificate, if it is in the JVM-default
946   * trust store.
947   *
948   * @param  cert         The certificate for which to find the issuer.  It must
949   *                      have already been retrieved from the JVM-default trust
950   *                      store.
951   * @param  currentDate  The current date to use when verifying validity.
952   *
953   * @return  The issuer for the provided certificate, or {@code null} if the
954   *          provided certificate is self-signed.
955   *
956   * @throws  CertificateException  If the provided certificate is not
957   *                                self-signed but its issuer could not be
958   *                                found, or if the issuer certificate is
959   *                                not currently valid.
960   */
961  @Nullable()
962  private X509Certificate findIssuer(@NotNull final X509Certificate cert,
963                                     @NotNull final Date currentDate)
964          throws CertificateException
965  {
966    try
967    {
968      // More fully decode the provided certificate so that we can better
969      // examine it.
970      final com.unboundid.util.ssl.cert.X509Certificate c =
971           new com.unboundid.util.ssl.cert.X509Certificate(
972                cert.getEncoded());
973
974      // If the certificate is self-signed, then it doesn't have an issuer.
975      if (c.isSelfSigned())
976      {
977        return null;
978      }
979
980      // See if the certificate has an authority key identifier extension.  If
981      // so, then use it to try to find the issuer.
982      for (final X509CertificateExtension e : c.getExtensions())
983      {
984        if (e instanceof AuthorityKeyIdentifierExtension)
985        {
986          final AuthorityKeyIdentifierExtension akie =
987               (AuthorityKeyIdentifierExtension) e;
988          final ASN1OctetString authorityKeyID =
989               new ASN1OctetString(akie.getKeyIdentifier().getValue());
990          final com.unboundid.util.ssl.cert.X509Certificate issuer =
991               trustedCertsByKeyID.get(authorityKeyID);
992          if ((issuer != null) && issuer.isWithinValidityWindow(currentDate))
993          {
994            c.verifySignature(issuer);
995            return (X509Certificate) issuer.toCertificate();
996          }
997        }
998      }
999    }
1000    catch (final Exception e)
1001    {
1002      Debug.debugException(e);
1003    }
1004
1005    throw new CertificateException(
1006         ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_FIND_ISSUER.get(
1007              String.valueOf(cert.getSubjectDN())));
1008  }
1009
1010
1011
1012  /**
1013   * Checks to determine whether the provided certificate chain may be
1014   * incomplete, and if so, whether we can find and trust the issuer of the last
1015   * certificate in the chain.
1016   *
1017   * @param  chain  The chain to validate.
1018   *
1019   * @return  {@code true} if the chain could be validated, or {@code false} if
1020   *          not.
1021   */
1022  private boolean checkIncompleteChain(@NotNull final X509Certificate[] chain)
1023  {
1024    try
1025    {
1026      // Get the last certificate in the chain and decode it as one that we can
1027      // more fully inspect.
1028      final com.unboundid.util.ssl.cert.X509Certificate c =
1029           new com.unboundid.util.ssl.cert.X509Certificate(
1030                chain[chain.length - 1].getEncoded());
1031
1032      // If the certificate is self-signed, then it can't be trusted.
1033      if (c.isSelfSigned())
1034      {
1035        return false;
1036      }
1037
1038      // See if the certificate has an authority key identifier extension.  If
1039      // so, then use it to try to find the issuer.
1040      for (final X509CertificateExtension e : c.getExtensions())
1041      {
1042        if (e instanceof AuthorityKeyIdentifierExtension)
1043        {
1044          final AuthorityKeyIdentifierExtension akie =
1045               (AuthorityKeyIdentifierExtension) e;
1046          final ASN1OctetString authorityKeyID =
1047               new ASN1OctetString(akie.getKeyIdentifier().getValue());
1048          final com.unboundid.util.ssl.cert.X509Certificate issuer =
1049               trustedCertsByKeyID.get(authorityKeyID);
1050          if ((issuer != null) && issuer.isWithinValidityWindow())
1051          {
1052            c.verifySignature(issuer);
1053            return true;
1054          }
1055        }
1056      }
1057    }
1058    catch (final Exception e)
1059    {
1060      Debug.debugException(e);
1061    }
1062
1063    return false;
1064  }
1065
1066
1067
1068  /**
1069   * Constructs a string representation of the certificates in the provided
1070   * chain.  It will consist of a comma-delimited list of their subject DNs,
1071   * with each subject DN surrounded by single quotes.
1072   *
1073   * @param  chain  The chain for which to obtain the string representation.
1074   *
1075   * @return  A string representation of the provided certificate chain.
1076   */
1077  @NotNull()
1078  static String chainToString(@NotNull final X509Certificate[] chain)
1079  {
1080    final StringBuilder buffer = new StringBuilder();
1081
1082    switch (chain.length)
1083    {
1084      case 0:
1085        break;
1086      case 1:
1087        buffer.append('\'');
1088        buffer.append(chain[0].getSubjectDN());
1089        buffer.append('\'');
1090        break;
1091      case 2:
1092        buffer.append('\'');
1093        buffer.append(chain[0].getSubjectDN());
1094        buffer.append("' and '");
1095        buffer.append(chain[1].getSubjectDN());
1096        buffer.append('\'');
1097        break;
1098      default:
1099        for (int i=0; i < chain.length; i++)
1100        {
1101          if (i > 0)
1102          {
1103            buffer.append(", ");
1104          }
1105
1106          if (i == (chain.length - 1))
1107          {
1108            buffer.append("and ");
1109          }
1110
1111          buffer.append('\'');
1112          buffer.append(chain[i].getSubjectDN());
1113          buffer.append('\'');
1114        }
1115    }
1116
1117    return buffer.toString();
1118  }
1119}