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 = System.getProperty(javaHomePropertyName);
137    if (javaHomePath == null)
138    {
139      certificateException = new CertificateException(
140           ERR_JVM_DEFAULT_TRUST_MANAGER_NO_JAVA_HOME.get(
141                javaHomePropertyName));
142      caCertsFile = null;
143      keystore = null;
144      trustedCertificateMap = Collections.emptyMap();
145      return;
146    }
147
148    final File javaHomeDirectory = new File(javaHomePath);
149    if ((! javaHomeDirectory.exists()) || (! javaHomeDirectory.isDirectory()))
150    {
151      certificateException = new CertificateException(
152           ERR_JVM_DEFAULT_TRUST_MANAGER_INVALID_JAVA_HOME.get(
153                javaHomePropertyName, javaHomePath));
154      caCertsFile = null;
155      keystore = null;
156      trustedCertificateMap = Collections.emptyMap();
157      return;
158    }
159
160
161    // Get a keystore instance that is loaded from the JVM's default set of
162    // trusted issuers.
163    final ObjectPair<KeyStore,File> keystorePair;
164    try
165    {
166      keystorePair = getJVMDefaultKeyStore(javaHomeDirectory);
167    }
168    catch (final CertificateException ce)
169    {
170      Debug.debugException(ce);
171      certificateException = ce;
172      caCertsFile = null;
173      keystore = null;
174      trustedCertificateMap = Collections.emptyMap();
175      return;
176    }
177
178    keystore = keystorePair.getFirst();
179    caCertsFile = keystorePair.getSecond();
180
181
182    // Iterate through the certificates in the keystore and load them into a
183    // map for faster and more reliable access.
184    final LinkedHashMap<ASN1OctetString,X509Certificate> certificateMap =
185         new LinkedHashMap<>(StaticUtils.computeMapCapacity(50));
186    try
187    {
188      final Enumeration<String> aliasEnumeration = keystore.aliases();
189      while (aliasEnumeration.hasMoreElements())
190      {
191        final String alias = aliasEnumeration.nextElement();
192
193        try
194        {
195          final X509Certificate certificate =
196               (X509Certificate) keystore.getCertificate(alias);
197          if (certificate != null)
198          {
199            certificateMap.put(new ASN1OctetString(certificate.getSignature()),
200                 certificate);
201          }
202        }
203        catch (final Exception e)
204        {
205          Debug.debugException(e);
206        }
207      }
208    }
209    catch (final Exception e)
210    {
211      Debug.debugException(e);
212      certificateException = new CertificateException(
213           ERR_JVM_DEFAULT_TRUST_MANAGER_ERROR_ITERATING_THROUGH_CACERTS.get(
214                caCertsFile.getAbsolutePath(),
215                StaticUtils.getExceptionMessage(e)),
216           e);
217      trustedCertificateMap = Collections.emptyMap();
218      return;
219    }
220
221    trustedCertificateMap = Collections.unmodifiableMap(certificateMap);
222    certificateException = null;
223  }
224
225
226
227  /**
228   * Retrieves the singleton instance of this trust manager.
229   *
230   * @return  The singleton instance of this trust manager.
231   */
232  public static JVMDefaultTrustManager getInstance()
233  {
234    final JVMDefaultTrustManager existingInstance = INSTANCE.get();
235    if (existingInstance != null)
236    {
237      return existingInstance;
238    }
239
240    final JVMDefaultTrustManager newInstance =
241         new JVMDefaultTrustManager(PROPERTY_JAVA_HOME);
242    if (INSTANCE.compareAndSet(null, newInstance))
243    {
244      return newInstance;
245    }
246    else
247    {
248      return INSTANCE.get();
249    }
250  }
251
252
253
254  /**
255   * Retrieves the keystore that backs this trust manager.
256   *
257   * @return  The keystore that backs this trust manager.
258   *
259   * @throws  CertificateException  If a problem was encountered while
260   *                                initializing this trust manager.
261   */
262  KeyStore getKeyStore()
263           throws CertificateException
264  {
265    if (certificateException != null)
266    {
267      throw certificateException;
268    }
269
270    return keystore;
271  }
272
273
274
275  /**
276   * Retrieves the path to the the file containing the JVM's default set of
277   * trusted issuers.
278   *
279   * @return  The path to the file containing the JVM's default set of
280   *          trusted issuers.
281   *
282   * @throws  CertificateException  If a problem was encountered while
283   *                                initializing this trust manager.
284   */
285  public File getCACertsFile()
286         throws CertificateException
287  {
288    if (certificateException != null)
289    {
290      throw certificateException;
291    }
292
293    return caCertsFile;
294  }
295
296
297
298  /**
299   * Retrieves the certificates included in this trust manager.
300   *
301   * @return  The certificates included in this trust manager.
302   *
303   * @throws  CertificateException  If a problem was encountered while
304   *                                initializing this trust manager.
305   */
306  public Collection<X509Certificate> getTrustedIssuerCertificates()
307         throws CertificateException
308  {
309    if (certificateException != null)
310    {
311      throw certificateException;
312    }
313
314    return trustedCertificateMap.values();
315  }
316
317
318
319  /**
320   * Checks to determine whether the provided client certificate chain should be
321   * trusted.
322   *
323   * @param  chain     The client certificate chain for which to make the
324   *                   determination.
325   * @param  authType  The authentication type based on the client certificate.
326   *
327   * @throws  CertificateException  If the provided client certificate chain
328   *                                should not be trusted.
329   */
330  @Override()
331  public void checkClientTrusted(final X509Certificate[] chain,
332                                 final String authType)
333         throws CertificateException
334  {
335    checkTrusted(chain);
336  }
337
338
339
340  /**
341   * Checks to determine whether the provided server certificate chain should be
342   * trusted.
343   *
344   * @param  chain     The server certificate chain for which to make the
345   *                   determination.
346   * @param  authType  The key exchange algorithm used.
347   *
348   * @throws  CertificateException  If the provided server certificate chain
349   *                                should not be trusted.
350   */
351  @Override()
352  public void checkServerTrusted(final X509Certificate[] chain,
353                                 final String authType)
354         throws CertificateException
355  {
356    checkTrusted(chain);
357  }
358
359
360
361  /**
362   * Retrieves the accepted issuer certificates for this trust manager.
363   *
364   * @return  The accepted issuer certificates for this trust manager, or an
365   *          empty set of accepted issuers if a problem was encountered while
366   *          initializing this trust manager.
367   */
368  @Override()
369  public X509Certificate[] getAcceptedIssuers()
370  {
371    if (certificateException != null)
372    {
373      return NO_CERTIFICATES;
374    }
375
376    final X509Certificate[] acceptedIssuers =
377         new X509Certificate[trustedCertificateMap.size()];
378    return trustedCertificateMap.values().toArray(acceptedIssuers);
379  }
380
381
382
383  /**
384   * Retrieves a {@code KeyStore} that contains the JVM's default set of trusted
385   * issuers.
386   *
387   * @param  javaHomeDirectory  The path to the JVM installation home directory.
388   *
389   * @return  An {@code ObjectPair} that includes the keystore and the file from
390   *          which it was loaded.
391   *
392   * @throws  CertificateException  If the keystore could not be found or
393   *                                loaded.
394   */
395  private static ObjectPair<KeyStore,File> getJVMDefaultKeyStore(
396                                                final File javaHomeDirectory)
397          throws CertificateException
398  {
399    final File libSecurityCACerts = StaticUtils.constructPath(javaHomeDirectory,
400         "lib", "security", "cacerts");
401    final File jreLibSecurityCACerts = StaticUtils.constructPath(
402         javaHomeDirectory, "jre", "lib", "security", "cacerts");
403
404    final ArrayList<File> tryFirstFiles =
405         new ArrayList<>(2 * FILE_EXTENSIONS.length + 2);
406    tryFirstFiles.add(libSecurityCACerts);
407    tryFirstFiles.add(jreLibSecurityCACerts);
408
409    for (final String extension : FILE_EXTENSIONS)
410    {
411      tryFirstFiles.add(
412           new File(libSecurityCACerts.getAbsolutePath() + extension));
413      tryFirstFiles.add(
414           new File(jreLibSecurityCACerts.getAbsolutePath() + extension));
415    }
416
417    for (final File f : tryFirstFiles)
418    {
419      final KeyStore keyStore = loadKeyStore(f);
420      if (keyStore != null)
421      {
422        return new ObjectPair<>(keyStore, f);
423      }
424    }
425
426
427    // If we didn't find it with known paths, then try to find it with a
428    // recursive filesystem search below the Java home directory.
429    final LinkedHashMap<File,CertificateException> exceptions =
430         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
431    final ObjectPair<KeyStore,File> keystorePair =
432         searchForKeyStore(javaHomeDirectory, exceptions);
433    if (keystorePair != null)
434    {
435      return keystorePair;
436    }
437
438
439    // If we've gotten here, then we couldn't find the keystore.  Construct a
440    // message from the set of exceptions.
441    if (exceptions.isEmpty())
442    {
443      throw new CertificateException(
444           ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_NO_EXCEPTION.get());
445    }
446    else
447    {
448      final StringBuilder buffer = new StringBuilder();
449      buffer.append(
450           ERR_JVM_DEFAULT_TRUST_MANAGER_CACERTS_NOT_FOUND_WITH_EXCEPTION.
451                get());
452      for (final Map.Entry<File,CertificateException> e : exceptions.entrySet())
453      {
454        if (buffer.charAt(buffer.length() - 1) != '.')
455        {
456          buffer.append('.');
457        }
458
459        buffer.append("  ");
460        buffer.append(ERR_JVM_DEFAULT_TRUST_MANAGER_LOAD_ERROR.get(
461             e.getKey().getAbsolutePath(),
462             StaticUtils.getExceptionMessage(e.getValue())));
463      }
464
465      throw new CertificateException(buffer.toString());
466    }
467  }
468
469
470
471  /**
472   * Recursively searches for a valid keystore file below the specified portion
473   * of the filesystem.  Any file named "cacerts", ignoring differences in
474   * capitalization, and optionally ending with a number of different file
475   * extensions, will be examined to see if it can be parsed as a Java keystore.
476   * The first keystore that we find meeting that criteria will be returned.
477   *
478   * @param  directory   The directory in which to search.  It must not be
479   *                     {@code null}.
480   * @param  exceptions  A map that correlates file paths with exceptions
481   *                     obtained while interacting with them.  If an exception
482   *                     is encountered while interacting with this file, then
483   *                     it will be added to this map.
484   *
485   * @return  The first valid keystore found that meets all the necessary
486   *          criteria, or {@code null} if no such keystore could be found.
487   */
488  private static ObjectPair<KeyStore,File> searchForKeyStore(
489                      final File directory,
490                      final Map<File,CertificateException> exceptions)
491  {
492filesInDirectoryLoop:
493    for (final File f : directory.listFiles())
494    {
495      if (f.isDirectory())
496      {
497        final ObjectPair<KeyStore,File> p =searchForKeyStore(f, exceptions);
498        if (p != null)
499        {
500          return p;
501        }
502      }
503      else
504      {
505        final String lowerName = StaticUtils.toLowerCase(f.getName());
506        if (lowerName.equals("cacerts"))
507        {
508          try
509          {
510            final KeyStore keystore = loadKeyStore(f);
511            return new ObjectPair<>(keystore, f);
512          }
513          catch (final CertificateException ce)
514          {
515            Debug.debugException(ce);
516            exceptions.put(f, ce);
517          }
518        }
519        else
520        {
521          for (final String extension : FILE_EXTENSIONS)
522          {
523            if (lowerName.equals("cacerts" + extension))
524            {
525              try
526              {
527                final KeyStore keystore = loadKeyStore(f);
528                return new ObjectPair<>(keystore, f);
529              }
530              catch (final CertificateException ce)
531              {
532                Debug.debugException(ce);
533                exceptions.put(f, ce);
534                continue filesInDirectoryLoop;
535              }
536            }
537          }
538        }
539      }
540    }
541
542    return null;
543  }
544
545
546
547  /**
548   * Attempts to load the contents of the specified file as a Java keystore.
549   *
550   * @param  f  The file from which to load the keystore data.
551   *
552   * @return  The keystore that was loaded from the specified file.
553   *
554   * @throws  CertificateException  If a problem occurs while trying to load the
555   *
556   */
557  private static KeyStore loadKeyStore(final File f)
558          throws CertificateException
559  {
560    if ((! f.exists()) || (! f.isFile()))
561    {
562      return null;
563    }
564
565    CertificateException firstGetInstanceException = null;
566    CertificateException firstLoadException = null;
567    for (final String keyStoreType : new String[] { "JKS", "PKCS12" })
568    {
569      final KeyStore keyStore;
570      try
571      {
572        keyStore = KeyStore.getInstance(keyStoreType);
573      }
574      catch (final Exception e)
575      {
576        Debug.debugException(e);
577        if (firstGetInstanceException == null)
578        {
579          firstGetInstanceException = new CertificateException(
580               ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_INSTANTIATE_KEYSTORE.get(
581                    keyStoreType, StaticUtils.getExceptionMessage(e)),
582               e);
583        }
584        continue;
585      }
586
587      try (FileInputStream inputStream = new FileInputStream(f))
588      {
589        keyStore.load(inputStream, null);
590      }
591      catch (final Exception e)
592      {
593        Debug.debugException(e);
594        if (firstLoadException == null)
595        {
596          firstLoadException = new CertificateException(
597               ERR_JVM_DEFAULT_TRUST_MANAGER_CANNOT_ERROR_LOADING_KEYSTORE.get(
598                    f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
599               e);
600        }
601        continue;
602      }
603
604      return keyStore;
605    }
606
607    if (firstLoadException != null)
608    {
609      throw firstLoadException;
610    }
611
612    throw firstGetInstanceException;
613  }
614
615
616
617  /**
618   * Ensures that the provided certificate chain should be considered trusted.
619   *
620   * @param  chain  The certificate chain to validate.  It must not be
621   *                {@code null}).
622   *
623   * @throws  CertificateException  If the provided certificate chain should not
624   *                                be considered trusted.
625   */
626  void checkTrusted(final X509Certificate[] chain)
627       throws CertificateException
628  {
629    if (certificateException != null)
630    {
631      throw certificateException;
632    }
633
634    if ((chain == null) || (chain.length == 0))
635    {
636      throw new CertificateException(
637           ERR_JVM_DEFAULT_TRUST_MANAGER_NO_CERTS_IN_CHAIN.get());
638    }
639
640    boolean foundIssuer = false;
641    final Date currentTime = new Date();
642    for (final X509Certificate cert : chain)
643    {
644      // Make sure that the certificate is currently within its validity window.
645      final Date notBefore = cert.getNotBefore();
646      if (currentTime.before(notBefore))
647      {
648        throw new CertificateNotYetValidException(
649             ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_NOT_YET_VALID.get(
650                  chainToString(chain), String.valueOf(cert.getSubjectDN()),
651                  String.valueOf(notBefore)));
652      }
653
654      final Date notAfter = cert.getNotAfter();
655      if (currentTime.after(notAfter))
656      {
657        throw new CertificateExpiredException(
658             ERR_JVM_DEFAULT_TRUST_MANAGER_CERT_EXPIRED.get(
659                  chainToString(chain),
660                  String.valueOf(cert.getSubjectDN()),
661                  String.valueOf(notAfter)));
662      }
663
664      final ASN1OctetString signature =
665           new ASN1OctetString(cert.getSignature());
666      foundIssuer |= (trustedCertificateMap.get(signature) != null);
667    }
668
669    if (! foundIssuer)
670    {
671      throw new CertificateException(
672           ERR_JVM_DEFAULT_TRUST_MANGER_NO_TRUSTED_ISSUER_FOUND.get(
673                chainToString(chain)));
674    }
675  }
676
677
678
679  /**
680   * Constructs a string representation of the certificates in the provided
681   * chain.  It will consist of a comma-delimited list of their subject DNs,
682   * with each subject DN surrounded by single quotes.
683   *
684   * @param  chain  The chain for which to obtain the string representation.
685   *
686   * @return  A string representation of the provided certificate chain.
687   */
688  static String chainToString(final X509Certificate[] chain)
689  {
690    final StringBuilder buffer = new StringBuilder();
691
692    switch (chain.length)
693    {
694      case 0:
695        break;
696      case 1:
697        buffer.append('\'');
698        buffer.append(chain[0].getSubjectDN());
699        buffer.append('\'');
700        break;
701      case 2:
702        buffer.append('\'');
703        buffer.append(chain[0].getSubjectDN());
704        buffer.append("' and '");
705        buffer.append(chain[1].getSubjectDN());
706        buffer.append('\'');
707        break;
708      default:
709        for (int i=0; i < chain.length; i++)
710        {
711          if (i > 0)
712          {
713            buffer.append(", ");
714          }
715
716          if (i == (chain.length - 1))
717          {
718            buffer.append("and ");
719          }
720
721          buffer.append('\'');
722          buffer.append(chain[i].getSubjectDN());
723          buffer.append('\'');
724        }
725    }
726
727    return buffer.toString();
728  }
729}