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