001/*
002 * Copyright 2021-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2021-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) 2021-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.IOException;
042import java.io.Serializable;
043import java.security.KeyStoreException;
044import java.security.cert.CertificateException;
045import java.security.cert.X509Certificate;
046import java.util.ArrayList;
047import java.util.Collections;
048import java.util.HashMap;
049import java.util.List;
050import java.util.Map;
051import javax.net.ssl.X509TrustManager;
052import javax.security.auth.x500.X500Principal;
053
054import com.unboundid.util.Debug;
055import com.unboundid.util.NotMutable;
056import com.unboundid.util.NotNull;
057import com.unboundid.util.StaticUtils;
058import com.unboundid.util.ThreadSafety;
059import com.unboundid.util.ThreadSafetyLevel;
060import com.unboundid.util.Validator;
061import com.unboundid.util.ssl.cert.CertException;
062import com.unboundid.util.ssl.cert.X509PEMFileReader;
063
064import static com.unboundid.util.ssl.SSLMessages.*;
065
066
067
068/**
069 * This class provides an implementation of an X.509 trust manager that can
070 * obtain information about trusted issuers from one or more PEM files.
071 */
072@NotMutable()
073@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
074public final class PEMFileTrustManager
075       implements X509TrustManager, Serializable
076{
077  /**
078   * The serial version UID for this serializable class.
079   */
080  private static final   long serialVersionUID = 1973401278035832777L;
081
082
083
084  // The map of trusted certificates read from the PEM files.
085  @NotNull private final Map<com.unboundid.util.ssl.cert.X509Certificate,
086     X509Certificate> trustedCertificates;
087
088
089
090  /**
091   * Creates a new PEM file trust manager that will read trusted certificate
092   * information from the specified PEM files.
093   *
094   * @param  pemFiles  The PEM files from which to read the trusted certificate
095   *                   information.  It must not be {@code null} or empty, and
096   *                   all files must exist.  Each element may be a file (which
097   *                   may contain one or more PEM-formatted certificates) or a
098   *                   directory (in which case all of the files in that
099   *                   directory, including subdirectories will be recursively
100   *                   processed).
101   *
102   * @throws  KeyStoreException  If a problem occurs while trying to read or
103   *                             decode any of the certificates.
104   */
105  public PEMFileTrustManager(@NotNull final File... pemFiles)
106         throws KeyStoreException
107  {
108    this(StaticUtils.toList(pemFiles));
109  }
110
111
112
113  /**
114   * Creates a new PEM file trust manager that will read trusted certificate
115   * information from the specified PEM files.
116   *
117   * @param  pemFiles  The PEM files from which to read the trusted certificate
118   *                   information.  It must not be {@code null} or empty, and
119   *                   all files must exist.  Each element may be a file (which
120   *                   may contain one or more PEM-formatted certificates) or a
121   *                   directory (in which case all of the files in that
122   *                   directory, including subdirectories will be recursively
123   *                   processed).
124   *
125   * @throws  KeyStoreException  If a problem occurs while trying to read or
126   *                             decode any of the certificates.
127   */
128  public PEMFileTrustManager(@NotNull final List<File> pemFiles)
129         throws KeyStoreException
130  {
131    Validator.ensureNotNullWithMessage(pemFiles,
132         "PEMFileTrustManager.pemFiles must not be null.");
133    Validator.ensureFalse(pemFiles.isEmpty(),
134         "PEMFileTrustManager.pemFiles must not be empty.");
135
136    final Map<com.unboundid.util.ssl.cert.X509Certificate,X509Certificate>
137         certMap = new HashMap<>();
138    for (final File f : pemFiles)
139    {
140      readTrustedCertificates(f, certMap);
141    }
142
143    trustedCertificates = Collections.unmodifiableMap(certMap);
144  }
145
146
147
148  /**
149   * Reads trusted certificate information from the specified PEM file.
150   *
151   * @param  f  The PEM file to examine.  It must not be {@code null}, and it
152   *            must reference a file that exists.  If it is a directory, then
153   *            all files contained in it (including subdirectories) will be
154   *            recursively processed.
155   * @param  m  The map to be updated wth the certificates read from the PEM
156   *            files.  It must not be {@code null} and must be updatable.
157   *
158   * @throws  KeyStoreException  If a problem is encountered while reading
159   *                             trusted certificate information from the
160   *                             specified file.
161   */
162  private static void readTrustedCertificates(@NotNull final File f,
163               @NotNull final Map<com.unboundid.util.ssl.cert.X509Certificate,
164                    X509Certificate> m)
165          throws KeyStoreException
166  {
167    if (! f.exists())
168    {
169      throw new KeyStoreException(
170           ERR_PEM_FILE_TRUST_MANAGER_NO_SUCH_FILE.get(f.getAbsolutePath()));
171    }
172
173    try
174    {
175      if (f.isDirectory())
176      {
177        for (final File fileInDir : f.listFiles())
178        {
179          readTrustedCertificates(fileInDir, m);
180        }
181      }
182      else
183      {
184        try (X509PEMFileReader r = new X509PEMFileReader(f))
185        {
186          boolean readCert = false;
187          while (true)
188          {
189            final com.unboundid.util.ssl.cert.X509Certificate cert =
190                 r.readCertificate();
191            if (cert == null)
192            {
193              if (! readCert)
194              {
195                throw new KeyStoreException(
196                     ERR_PEM_FILE_TRUST_MANAGER_EMPTY_FILE.get(
197                          f.getAbsolutePath()));
198              }
199
200              break;
201            }
202
203            readCert = true;
204
205            final X509Certificate c = (X509Certificate) cert.toCertificate();
206            m.put(cert, c);
207          }
208        }
209      }
210    }
211    catch (final KeyStoreException e)
212    {
213      Debug.debugException(e);
214      throw e;
215    }
216    catch (final IOException e)
217    {
218      Debug.debugException(e);
219      throw new KeyStoreException(
220           ERR_PEM_FILE_TRUST_MANAGER_ERROR_READING_FILE.get(
221                f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
222           e);
223    }
224    catch (final CertException e)
225    {
226      Debug.debugException(e);
227      throw new KeyStoreException(
228           ERR_PEM_FILE_TRUST_MANAGER_ERROR_PARSING_CERT.get(
229                f.getAbsolutePath(), e.getMessage()),
230           e);
231    }
232    catch (final Exception e)
233    {
234      Debug.debugException(e);
235      throw new KeyStoreException(
236           ERR_PEM_FILE_TRUST_MANAGER_ERROR_PROCESSING_FILE.get(
237                f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
238           e);
239    }
240  }
241
242
243
244  /**
245   * Determines whether the provided client certificate chain should be
246   * considered trusted based on the trusted certificate information read from
247   * PEM files.
248   *
249   * @param  chain     The client certificate chain for which to make the
250   *                   determination.  It must not be {@code null} or empty.
251   * @param  authType  The type of authentication to use based on the client
252   *                   certificate.  It must not be {@code null}.
253   *
254   * @throws  CertificateException  If the provided certificate chain should not
255   *                                be considered trusted.
256   */
257  @Override()
258  public void checkClientTrusted(@NotNull final X509Certificate[] chain,
259                                 @NotNull final String authType)
260         throws CertificateException
261  {
262    try
263    {
264      checkTrusted(chain);
265    }
266    catch (final CertificateException e)
267    {
268      Debug.debugException(e);
269      throw new CertificateException(
270           ERR_PEM_FILE_TRUST_MANAGER_CLIENT_NOT_TRUSTED.get(e.getMessage()),
271           e);
272    }
273  }
274
275
276
277  /**
278   * Determines whether the provided server certificate chain should be
279   * considered trusted based on the trusted certificate information read from
280   * PEM files.
281   *
282   * @param  chain     The server certificate chain for which to make the
283   *                   determination.  It must not be {@code null} or empty.
284   * @param  authType  The type of authentication to use based on the server
285   *                   certificate.  It must not be {@code null}.
286   *
287   * @throws  CertificateException  If the provided certificate chain should not
288   *                                be considered trusted.
289   */
290  @Override()
291  public void checkServerTrusted(@NotNull final X509Certificate[] chain,
292                                 @NotNull final String authType)
293         throws CertificateException
294  {
295    try
296    {
297      checkTrusted(chain);
298    }
299    catch (final CertificateException e)
300    {
301      Debug.debugException(e);
302      throw new CertificateException(
303           ERR_PEM_FILE_TRUST_MANAGER_SERVER_NOT_TRUSTED.get(e.getMessage()),
304           e);
305    }
306  }
307
308
309
310  /**
311   * Determines whether the provided certificate chain should be considered
312   * trusted based on the trusted certificate information read from PEM files.
313   * Note that this method assumes that the trusted certificate information read
314   * from PEM files should be authoritative, and therefore doesn't perform some
315   * types of validation (like ensuring that all issuer certificates are trusted
316   * rather than validating that at least one is trusted, or checking extensions
317   * like basic constraints).
318   *
319   * @param  chain  The certificate chain for which to make the determination.
320   *                It must not be {@code null} or empty.
321   *
322   * @throws  CertificateException  If the provided certificate chain should not
323   *                                be considered trusted.
324   */
325  private void checkTrusted(@NotNull final X509Certificate[] chain)
326          throws CertificateException
327  {
328    // If the chain is null or empty, then it cannot be trusted.
329    if ((chain == null) || (chain.length == 0))
330    {
331      throw new CertificateException(
332           ERR_PEM_FILE_TRUST_MANAGER_EMPTY_CHAIN.get());
333    }
334
335
336    // Iterate through all the certificates in the chain, parsing them using the
337    // LDAP SDK's X.509 certificate representation, and performing all of the
338    // following validation:
339    //
340    // - Make sure that the certificate is within the validity window.
341    //
342    // - Make sure that each subsequent certificate in the chain is the issuer
343    //   for the previous certificate.
344    //
345    // - Check to see whether at least one of the certificates in the chain
346    //   matches one read from the set of PEM files.
347    boolean foundCertificate = false;
348    com.unboundid.util.ssl.cert.X509Certificate firstCertificate = null;
349    com.unboundid.util.ssl.cert.X509Certificate previousCertificate = null;
350    for (final X509Certificate c : chain)
351    {
352      final com.unboundid.util.ssl.cert.X509Certificate parsedCertificate;
353      try
354      {
355        parsedCertificate = new com.unboundid.util.ssl.cert.X509Certificate(
356             c.getEncoded());
357      }
358      catch (final CertException e)
359      {
360        Debug.debugException(e);
361        throw new CertificateException(
362             ERR_PEM_FILE_TRUST_MANAGER_CANNOT_PARSE_CERT_FROM_CHAIN.get(
363                  c.getSubjectX500Principal().getName(X500Principal.RFC2253),
364                  StaticUtils.getExceptionMessage(e)),
365             e);
366      }
367
368      if (firstCertificate == null)
369      {
370        firstCertificate = parsedCertificate;
371      }
372
373      if (! parsedCertificate.isWithinValidityWindow())
374      {
375        throw new CertificateException(
376             ERR_PEM_FILE_TRUST_MANAGER_CERT_NOT_VALID.get(
377                  String.valueOf(parsedCertificate.getSubjectDN()),
378                  StaticUtils.encodeRFC3339Time(
379                       parsedCertificate.getNotBeforeDate()),
380                  StaticUtils.encodeRFC3339Time(
381                       parsedCertificate.getNotAfterDate())));
382      }
383
384      if ((previousCertificate != null) &&
385           (! parsedCertificate.isIssuerFor(previousCertificate)))
386      {
387        throw new CertificateException(
388             ERR_PEM_FILE_TRUST_MANAGER_CERT_NOT_ISSUER.get(
389                  String.valueOf(parsedCertificate.getSubjectDN()),
390                  String.valueOf(previousCertificate.getSubjectDN())));
391      }
392
393      foundCertificate |= trustedCertificates.containsKey(parsedCertificate);
394      previousCertificate = parsedCertificate;
395    }
396
397
398    // If we didn't find any of the presented certificates in the trust store,
399    // then it may be that an incomplete chain was presented.  If the last
400    // certificate in the chain is not self-signed, then check to see if any of
401    // the certificates in the trust store were an issuer for that certificate.
402    if ((! foundCertificate) && (! previousCertificate.isSelfSigned()))
403    {
404      for (final com.unboundid.util.ssl.cert.X509Certificate c :
405           trustedCertificates.keySet())
406      {
407        if (c.isIssuerFor(previousCertificate))
408        {
409          foundCertificate = true;
410          break;
411        }
412      }
413    }
414
415    if (! foundCertificate)
416    {
417      throw new CertificateException(ERR_PEM_FILE_TRUST_MANAGER_NOT_TRUSTED.get(
418           String.valueOf(firstCertificate.getSubjectDN())));
419    }
420  }
421
422
423
424  /**
425   * Retrieves an array of the issuer certificates that will be considered
426   * trusted.
427   *
428   * @return  An array of the issuer certificates that will be considered
429   *          trusted, or an empty array if no issuers will be trusted.
430   */
431  @Override()
432  @NotNull()
433  public X509Certificate[] getAcceptedIssuers()
434  {
435    // Include all certificates that are currently within their validity window.
436    final long currentTime = System.currentTimeMillis();
437    final List<X509Certificate> certList =
438         new ArrayList<>(trustedCertificates.size());
439    for (final Map.Entry<com.unboundid.util.ssl.cert.X509Certificate,
440              X509Certificate> e : trustedCertificates.entrySet())
441    {
442      if (e.getKey().isWithinValidityWindow(currentTime))
443      {
444        certList.add(e.getValue());
445      }
446    }
447
448    final X509Certificate[] certArray = new X509Certificate[certList.size()];
449    return certList.toArray(certArray);
450  }
451}