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