001/*
002 * Copyright 2017-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2017-2019 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.util.ssl;
022
023
024
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.Serializable;
028import java.security.KeyStore;
029import java.security.cert.CertificateException;
030import java.security.cert.CertificateExpiredException;
031import java.security.cert.CertificateNotYetValidException;
032import java.security.cert.X509Certificate;
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.Date;
037import java.util.Enumeration;
038import java.util.LinkedHashMap;
039import java.util.Map;
040import java.util.concurrent.atomic.AtomicReference;
041import javax.net.ssl.X509TrustManager;
042
043import com.unboundid.asn1.ASN1OctetString;
044import com.unboundid.util.Debug;
045import com.unboundid.util.NotMutable;
046import com.unboundid.util.ObjectPair;
047import com.unboundid.util.StaticUtils;
048import com.unboundid.util.ThreadSafety;
049import com.unboundid.util.ThreadSafetyLevel;
050
051import static com.unboundid.util.ssl.SSLMessages.*;
052
053
054
055/**
056 * This class provides an implementation of a trust manager that relies on the
057 * JVM's default set of trusted issuers.  This is generally found in the
058 * {@code jre/lib/security/cacerts} or {@code lib/security/cacerts} file in the
059 * Java installation (in both Sun/Oracle and IBM-based JVMs), but if neither of
060 * those files exist (or if they cannot be parsed as a JKS or PKCS#12 keystore),
061 * then we will search for the file below the Java home directory.
062 */
063@NotMutable()
064@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
065public final class JVMDefaultTrustManager
066       implements X509TrustManager, Serializable
067{
068  /**
069   * A reference to the singleton instance of this class.
070   */
071  private static final AtomicReference<JVMDefaultTrustManager> INSTANCE =
072       new AtomicReference<>();
073
074
075
076  /**
077   * The name of the system property that specifies the path to the Java
078   * installation for the currently-running JVM.
079   */
080  private static final String PROPERTY_JAVA_HOME = "java.home";
081
082
083
084  /**
085   * A set of alternate file extensions that may be used by Java keystores.
086   */
087  static final String[] FILE_EXTENSIONS  =
088  {
089    ".jks",
090    ".p12",
091    ".pkcs12",
092    ".pfx",
093  };
094
095
096
097  /**
098   * A pre-allocated empty certificate array.
099   */
100  private static final X509Certificate[] NO_CERTIFICATES =
101       new X509Certificate[0];
102
103
104
105  /**
106   * The serial version UID for this serializable class.
107   */
108  private static final long serialVersionUID = -8587938729712485943L;
109
110
111
112  // A certificate exception that should be thrown for any attempt to use this
113  // trust store.
114  private final CertificateException certificateException;
115
116  // The file from which they keystore was loaded.
117  private final File caCertsFile;
118
119  // The keystore instance containing the JVM's default set of trusted issuers.
120  private final KeyStore keystore;
121
122  // A map of the certificates in the keystore, indexed by signature.
123  private final Map<ASN1OctetString,X509Certificate> trustedCertificateMap;
124
125
126
127  /**
128   * Creates an instance of this trust manager.
129   *
130   * @param  javaHomePropertyName  The name of the system property that should
131   *                               specify the path to the Java installation.
132   */
133  JVMDefaultTrustManager(final String javaHomePropertyName)
134  {
135    // Determine the path to the root of the Java installation.
136    final String javaHomePath =
137         StaticUtils.getSystemProperty(javaHomePropertyName);
138    if (javaHomePath == null)
139    {
140      certificateException = new CertificateException(
141           ERR_JVM_DEFAULT_TRUST_MANAGER_NO_JAVA_HOME.get(
142                javaHomePropertyName));
143      caCertsFile = null;
144      keystore = null;
145      trustedCertificateMap = Collections.emptyMap();
146      return;
147    }
148
149    final File javaHomeDirectory = new File(javaHomePath);
150    if ((! javaHomeDirectory.exists()) || (! javaHomeDirectory.isDirectory()))
151    {
152      certificateException = new CertificateException(
153           ERR_JVM_DEFAULT_TRUST_MANAGER_INVALID_JAVA_HOME.get(
154                javaHomePropertyName, javaHomePath));
155      caCertsFile = null;
156      keystore = null;
157      trustedCertificateMap = Collections.emptyMap();
158      return;
159    }
160
161
162    // Get a keystore instance that is loaded from the JVM's default set of
163    // trusted issuers.
164    final ObjectPair<KeyStore,File> keystorePair;
165    try
166    {
167      keystorePair = getJVMDefaultKeyStore(javaHomeDirectory);
168    }
169    catch (final CertificateException ce)
170    {
171      Debug.debugException(ce);
172      certificateException = ce;
173      caCertsFile = null;
174      keystore = null;
175      trustedCertificateMap = Collections.emptyMap();
176      return;
177    }
178
179    keystore = keystorePair.getFirst();
180    caCertsFile = keystorePair.getSecond();
181
182
183    // Iterate through the certificates in the keystore and load them into a
184    // map for faster and more reliable access.
185    final LinkedHashMap<ASN1OctetString,X509Certificate> certificateMap =
186         new LinkedHashMap<>(StaticUtils.computeMapCapacity(50));
187    try
188    {
189      final Enumeration<String> aliasEnumeration = keystore.aliases();
190      while (aliasEnumeration.hasMoreElements())
191      {
192        final String alias = aliasEnumeration.nextElement();
193
194        try
195        {
196          final X509Certificate certificate =
197               (X509Certificate) keystore.getCertificate(alias);
198          if (certificate != null)
199          {
200            certificateMap.put(new ASN1OctetString(certificate.getSignature()),
201                 certificate);
202          }
203        }
204        catch (final Exception e)
205        {
206          Debug.debugException(e);
207        }
208      }
209    }
210    catch (final Exception e)
211    {
212      Debug.debugException(e);
213      certificateException = new CertificateException(
214           ERR_JVM_DEFAULT_TRUST_MANAGER_ERROR_ITERATING_THROUGH_CACERTS.get(
215                caCertsFile.getAbsolutePath(),
216                StaticUtils.getExceptionMessage(e)),
217           e);
218      trustedCertificateMap = Collections.emptyMap();
219      return;
220    }
221
222    trustedCertificateMap = Collections.unmodifiableMap(certificateMap);
223    certificateException = null;
224  }
225
226
227
228  /**
229   * Retrieves the singleton instance of this trust manager.
230   *
231   * @return  The singleton instance of this trust manager.
232   */
233  public static JVMDefaultTrustManager getInstance()
234  {
235    final JVMDefaultTrustManager existingInstance = INSTANCE.get();
236    if (existingInstance != null)
237    {
238      return existingInstance;
239    }
240
241    final JVMDefaultTrustManager newInstance =
242         new JVMDefaultTrustManager(PROPERTY_JAVA_HOME);
243    if (INSTANCE.compareAndSet(null, newInstance))
244    {
245      return newInstance;
246    }
247    else
248    {
249      return INSTANCE.get();
250    }
251  }
252
253
254
255  /**
256   * Retrieves the keystore that backs this trust manager.
257   *
258   * @return  The keystore that backs this trust manager.
259   *
260   * @throws  CertificateException  If a problem was encountered while
261   *                                initializing this trust manager.
262   */
263  KeyStore getKeyStore()
264           throws CertificateException
265  {
266    if (certificateException != null)
267    {
268      throw certificateException;
269    }
270
271    return keystore;
272  }
273
274
275
276  /**
277   * Retrieves the path to the the file containing the JVM's default set of
278   * trusted issuers.
279   *
280   * @return  The path to the file containing the JVM's default set of
281   *          trusted issuers.
282   *
283   * @throws  CertificateException  If a problem was encountered while
284   *                                initializing this trust manager.
285   */
286  public File getCACertsFile()
287         throws CertificateException
288  {
289    if (certificateException != null)
290    {
291      throw certificateException;
292    }
293
294    return caCertsFile;
295  }
296
297
298
299  /**
300   * Retrieves the certificates included in this trust manager.
301   *
302   * @return  The certificates included in this trust manager.
303   *
304   * @throws  CertificateException  If a problem was encountered while
305   *                                initializing this trust manager.
306   */
307  public Collection<X509Certificate> getTrustedIssuerCertificates()
308         throws CertificateException
309  {
310    if (certificateException != null)
311    {
312      throw certificateException;
313    }
314
315    return trustedCertificateMap.values();
316  }
317
318
319
320  /**
321   * Checks to determine whether the provided client certificate chain should be
322   * trusted.
323   *
324   * @param  chain     The client certificate chain for which to make the
325   *                   determination.
326   * @param  authType  The authentication type based on the client certificate.
327   *
328   * @throws  CertificateException  If the provided client certificate chain
329   *                                should not be trusted.
330   */
331  @Override()
332  public void checkClientTrusted(final X509Certificate[] chain,
333                                 final String authType)
334         throws CertificateException
335  {
336    checkTrusted(chain);
337  }
338
339
340
341  /**
342   * Checks to determine whether the provided server certificate chain should be
343   * trusted.
344   *
345   * @param  chain     The server certificate chain for which to make the
346   *                   determination.
347   * @param  authType  The key exchange algorithm used.
348   *
349   * @throws  CertificateException  If the provided server certificate chain
350   *                                should not be trusted.
351   */
352  @Override()
353  public void checkServerTrusted(final X509Certificate[] chain,
354                                 final String authType)
355         throws CertificateException
356  {
357    checkTrusted(chain);
358  }
359
360
361
362  /**
363   * Retrieves the accepted issuer certificates for this trust manager.
364   *
365   * @return  The accepted issuer certificates for this trust manager, or an
366   *          empty set of accepted issuers if a problem was encountered while
367   *          initializing this trust manager.
368   */
369  @Override()
370  public X509Certificate[] getAcceptedIssuers()
371  {
372    if (certificateException != null)
373    {
374      return NO_CERTIFICATES;
375    }
376
377    final X509Certificate[] acceptedIssuers =
378         new X509Certificate[trustedCertificateMap.size()];
379    return trustedCertificateMap.values().toArray(acceptedIssuers);
380  }
381
382
383
384  /**
385   * Retrieves a {@code KeyStore} that contains the JVM's default set of trusted
386   * issuers.
387   *
388   * @param  javaHomeDirectory  The path to the JVM installation home directory.
389   *
390   * @return  An {@code ObjectPair} that includes the keystore and the file from
391   *          which it was loaded.
392   *
393   * @throws  CertificateException  If the keystore could not be found or
394   *                                loaded.
395   */
396  private static ObjectPair<KeyStore,File> getJVMDefaultKeyStore(
397                                                final File javaHomeDirectory)
398          throws CertificateException
399  {
400    final File libSecurityCACerts = StaticUtils.constructPath(javaHomeDirectory,
401         "lib", "security", "cacerts");
402    final File jreLibSecurityCACerts = StaticUtils.constructPath(
403         javaHomeDirectory, "jre", "lib", "security", "cacerts");
404
405    final ArrayList<File> tryFirstFiles =
406         new ArrayList<>(2 * FILE_EXTENSIONS.length + 2);
407    tryFirstFiles.add(libSecurityCACerts);
408    tryFirstFiles.add(jreLibSecurityCACerts);
409
410    for (final String extension : FILE_EXTENSIONS)
411    {
412      tryFirstFiles.add(
413           new File(libSecurityCACerts.getAbsolutePath() + extension));
414      tryFirstFiles.add(
415           new File(jreLibSecurityCACerts.getAbsolutePath() + extension));
416    }
417
418    for (final File f : tryFirstFiles)
419    {
420      final KeyStore keyStore = loadKeyStore(f);
421      if (keyStore != null)
422      {
423        return new ObjectPair<>(keyStore, f);
424      }
425    }
426
427
428    // If we didn't find it with known paths, then try to find it with a
429    // recursive filesystem search below the Java home directory.
430    final LinkedHashMap<File,CertificateException> exceptions =
431         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
432    final ObjectPair<KeyStore,File> keystorePair =
433         searchForKeyStore(javaHomeDirectory, exceptions);
434    if (keystorePair != null)
435    {
436      return keystorePair;
437    }
438
439
440    // If we've gotten here, then we couldn't find the keystore.  Construct a
441    // message from the set of exceptions.
442    if (exceptions.isEmpty())
443    {
444      throw new CertificateException(
445           ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_NO_EXCEPTION.get());
446    }
447    else
448    {
449      final StringBuilder buffer = new StringBuilder();
450      buffer.append(
451           ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_WITH_EXCEPTION.
452                get());
453      for (final Map.Entry<File,CertificateException> e : exceptions.entrySet())
454      {
455        if (buffer.charAt(buffer.length() - 1) != '.')
456        {
457          buffer.append('.');
458        }
459
460        buffer.append("  ");
461        buffer.append(ERR_JVM_DEFAULT_TRUST_MANAGER_LOAD_ERROR.get(
462             e.getKey().getAbsolutePath(),
463             StaticUtils.getExceptionMessage(e.getValue())));
464      }
465
466      throw new CertificateException(buffer.toString());
467    }
468  }
469
470
471
472  /**
473   * Recursively searches for a valid keystore file below the specified portion
474   * of the filesystem.  Any file named "cacerts", ignoring differences in
475   * capitalization, and optionally ending with a number of different file
476   * extensions, will be examined to see if it can be parsed as a Java keystore.
477   * The first keystore that we find meeting that criteria will be returned.
478   *
479   * @param  directory   The directory in which to search.  It must not be
480   *                     {@code null}.
481   * @param  exceptions  A map that correlates file paths with exceptions
482   *                     obtained while interacting with them.  If an exception
483   *                     is encountered while interacting with this file, then
484   *                     it will be added to this map.
485   *
486   * @return  The first valid keystore found that meets all the necessary
487   *          criteria, or {@code null} if no such keystore could be found.
488   */
489  private static ObjectPair<KeyStore,File> searchForKeyStore(
490                      final File directory,
491                      final Map<File,CertificateException> exceptions)
492  {
493filesInDirectoryLoop:
494    for (final File f : directory.listFiles())
495    {
496      if (f.isDirectory())
497      {
498        final ObjectPair<KeyStore,File> p =searchForKeyStore(f, exceptions);
499        if (p != null)
500        {
501          return p;
502        }
503      }
504      else
505      {
506        final String lowerName = StaticUtils.toLowerCase(f.getName());
507        if (lowerName.equals("cacerts"))
508        {
509          try
510          {
511            final KeyStore keystore = loadKeyStore(f);
512            return new ObjectPair<>(keystore, f);
513          }
514          catch (final CertificateException ce)
515          {
516            Debug.debugException(ce);
517            exceptions.put(f, ce);
518          }
519        }
520        else
521        {
522          for (final String extension : FILE_EXTENSIONS)
523          {
524            if (lowerName.equals("cacerts" + extension))
525            {
526              try
527              {
528                final KeyStore keystore = loadKeyStore(f);
529                return new ObjectPair<>(keystore, f);
530              }
531              catch (final CertificateException ce)
532              {
533                Debug.debugException(ce);
534                exceptions.put(f, ce);
535                continue filesInDirectoryLoop;
536              }
537            }
538          }
539        }
540      }
541    }
542
543    return null;
544  }
545
546
547
548  /**
549   * Attempts to load the contents of the specified file as a Java keystore.
550   *
551   * @param  f  The file from which to load the keystore data.
552   *
553   * @return  The keystore that was loaded from the specified file.
554   *
555   * @throws  CertificateException  If a problem occurs while trying to load the
556   *
557   */
558  private static KeyStore loadKeyStore(final File f)
559          throws CertificateException
560  {
561    if ((! f.exists()) || (! f.isFile()))
562    {
563      return null;
564    }
565
566    CertificateException firstGetInstanceException = null;
567    CertificateException firstLoadException = null;
568    for (final String keyStoreType : new String[] { "JKS", "PKCS12" })
569    {
570      final KeyStore keyStore;
571      try
572      {
573        keyStore = KeyStore.getInstance(keyStoreType);
574      }
575      catch (final Exception e)
576      {
577        Debug.debugException(e);
578        if (firstGetInstanceException == null)
579        {
580          firstGetInstanceException = new CertificateException(
581               ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_INSTANTIATE_KEYSTORE.get(
582                    keyStoreType, StaticUtils.getExceptionMessage(e)),
583               e);
584        }
585        continue;
586      }
587
588      try (FileInputStream inputStream = new FileInputStream(f))
589      {
590        keyStore.load(inputStream, null);
591      }
592      catch (final Exception e)
593      {
594        Debug.debugException(e);
595        if (firstLoadException == null)
596        {
597          firstLoadException = new CertificateException(
598               ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_ERROR_LOADING_KEYSTORE.get(
599                    f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
600               e);
601        }
602        continue;
603      }
604
605      return keyStore;
606    }
607
608    if (firstLoadException != null)
609    {
610      throw firstLoadException;
611    }
612
613    throw firstGetInstanceException;
614  }
615
616
617
618  /**
619   * Ensures that the provided certificate chain should be considered trusted.
620   *
621   * @param  chain  The certificate chain to validate.  It must not be
622   *                {@code null}).
623   *
624   * @throws  CertificateException  If the provided certificate chain should not
625   *                                be considered trusted.
626   */
627  void checkTrusted(final X509Certificate[] chain)
628       throws CertificateException
629  {
630    if (certificateException != null)
631    {
632      throw certificateException;
633    }
634
635    if ((chain == null) || (chain.length == 0))
636    {
637      throw new CertificateException(
638           ERR_JVM_DEFAULT_TRUST_MANAGER_NO_CERTS_IN_CHAIN.get());
639    }
640
641    boolean foundIssuer = false;
642    final Date currentTime = new Date();
643    for (final X509Certificate cert : chain)
644    {
645      // Make sure that the certificate is currently within its validity window.
646      final Date notBefore = cert.getNotBefore();
647      if (currentTime.before(notBefore))
648      {
649        throw new CertificateNotYetValidException(
650             ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_NOT_YET_VALID.get(
651                  chainToString(chain), String.valueOf(cert.getSubjectDN()),
652                  String.valueOf(notBefore)));
653      }
654
655      final Date notAfter = cert.getNotAfter();
656      if (currentTime.after(notAfter))
657      {
658        throw new CertificateExpiredException(
659             ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_EXPIRED.get(
660                  chainToString(chain),
661                  String.valueOf(cert.getSubjectDN()),
662                  String.valueOf(notAfter)));
663      }
664
665      final ASN1OctetString signature =
666           new ASN1OctetString(cert.getSignature());
667      foundIssuer |= (trustedCertificateMap.get(signature) != null);
668    }
669
670    if (! foundIssuer)
671    {
672      throw new CertificateException(
673           ERR_JVM_DEFAULT_TRUST_MANGER_NO_TRUSTED_ISSUER_FOUND.get(
674                chainToString(chain)));
675    }
676  }
677
678
679
680  /**
681   * Constructs a string representation of the certificates in the provided
682   * chain.  It will consist of a comma-delimited list of their subject DNs,
683   * with each subject DN surrounded by single quotes.
684   *
685   * @param  chain  The chain for which to obtain the string representation.
686   *
687   * @return  A string representation of the provided certificate chain.
688   */
689  static String chainToString(final X509Certificate[] chain)
690  {
691    final StringBuilder buffer = new StringBuilder();
692
693    switch (chain.length)
694    {
695      case 0:
696        break;
697      case 1:
698        buffer.append('\'');
699        buffer.append(chain[0].getSubjectDN());
700        buffer.append('\'');
701        break;
702      case 2:
703        buffer.append('\'');
704        buffer.append(chain[0].getSubjectDN());
705        buffer.append("' and '");
706        buffer.append(chain[1].getSubjectDN());
707        buffer.append('\'');
708        break;
709      default:
710        for (int i=0; i < chain.length; i++)
711        {
712          if (i > 0)
713          {
714            buffer.append(", ");
715          }
716
717          if (i == (chain.length - 1))
718          {
719            buffer.append("and ");
720          }
721
722          buffer.append('\'');
723          buffer.append(chain[i].getSubjectDN());
724          buffer.append('\'');
725        }
726    }
727
728    return buffer.toString();
729  }
730}