001/*
002 * Copyright 2020-2022 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2022 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) 2020-2022 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.ldap.sdk.unboundidds;
037
038
039
040import java.io.ByteArrayInputStream;
041import java.io.File;
042import java.io.IOException;
043import java.io.Serializable;
044import java.security.cert.CertificateException;
045import java.security.cert.X509Certificate;
046import java.util.Collections;
047import java.util.HashSet;
048import java.util.Set;
049import java.util.concurrent.atomic.AtomicLong;
050import java.util.concurrent.atomic.AtomicReference;
051import javax.net.ssl.X509TrustManager;
052import javax.security.auth.x500.X500Principal;
053
054import com.unboundid.ldap.sdk.Attribute;
055import com.unboundid.ldap.sdk.Entry;
056import com.unboundid.ldif.LDIFException;
057import com.unboundid.ldif.LDIFReader;
058import com.unboundid.util.Base64;
059import com.unboundid.util.CryptoHelper;
060import com.unboundid.util.Debug;
061import com.unboundid.util.NotNull;
062import com.unboundid.util.StaticUtils;
063import com.unboundid.util.ThreadSafety;
064import com.unboundid.util.ThreadSafetyLevel;
065
066import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;
067
068
069
070/**
071 * This class provides an implementation of an X.509 trust manager that can be
072 * used to trust certificates listed in the topology registry of a Ping Identity
073 * Directory Server instance.  It will read the topology registry from the
074 * server's configuration file rather than communicating with it over LDAP, so
075 * it is only available for use when run from LDAP tools provided with the
076 * Ping Identity Directory Server.
077 * <BR>
078 * <BLOCKQUOTE>
079 *   <B>NOTE:</B>  This class, and other classes within the
080 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
081 *   supported for use against Ping Identity, UnboundID, and
082 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
083 *   for proprietary functionality or for external specifications that are not
084 *   considered stable or mature enough to be guaranteed to work in an
085 *   interoperable way with other types of LDAP servers.
086 * </BLOCKQUOTE>
087 */
088@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
089public final class TopologyRegistryTrustManager
090       implements X509TrustManager, Serializable
091{
092  /**
093   * The name of the object class that will be used in entries that may provide
094   * information about inter-server certificates in the topology registry.
095   */
096  @NotNull private static final String INTER_SERVER_CERT_OC =
097       "ds-cfg-server-instance";
098
099
100
101  /**
102   * The name of the attribute type for attributes that provide information
103   * about inter-server certificates in the topology registry.
104   */
105  @NotNull private static final String INTER_SERVER_CERT_AT =
106       "ds-cfg-inter-server-certificate";
107
108
109
110  /**
111   * The name of the object class that will be used in entries that may provide
112   * information about listener certificates in the topology registry.
113   */
114  @NotNull private static final String LISTENER_CERT_OC =
115       "ds-cfg-server-instance-listener";
116
117
118
119  /**
120   * The name of the attribute type for attributes that provide information
121   * about listener certificates in the topology registry.
122   */
123  @NotNull private static final String LISTENER_CERT_AT =
124       "ds-cfg-listener-certificate";
125
126
127
128  /**
129   * A pre-allocated empty certificate array.
130   */
131  @NotNull 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 = -1535917071172094611L;
140
141
142
143  // The time that the cached certificates will expire.
144  @NotNull private final AtomicLong cacheExpirationTime;
145
146  // The certificates that have been cached.
147  @NotNull private final AtomicReference<Set<X509Certificate>>
148       cachedCertificates;
149
150  // The configuration file from which the certificate records will be read.
151  @NotNull private final File configurationFile;
152
153  // The maximum length of time in milliseconds that previously loaded
154  // certificates may be cached.
155  private final long cacheDurationMillis;
156
157
158
159  /**
160   * Creates a new instance of this trust manager with the provided settings.
161   *
162   * @param  configurationFile    The configuration file for the Ping Identity
163   *                              Directory Server instance that holds the
164   *                              topology registry data.
165   * @param  cacheDurationMillis  The maximum length of time in milliseconds
166   *                              that previously loaded certificates may be
167   *                              cached.  If this is less than or equal to
168   *                              zero, then certificates will not be cached.
169   */
170  public TopologyRegistryTrustManager(@NotNull final File configurationFile,
171                                      final long cacheDurationMillis)
172  {
173    this.configurationFile = configurationFile;
174    this.cacheDurationMillis = cacheDurationMillis;
175
176    cacheExpirationTime = new AtomicLong(0L);
177    cachedCertificates = new AtomicReference<>(
178         Collections.<X509Certificate>emptySet());
179  }
180
181
182
183  /**
184   * Checks to determine whether the provided client certificate chain should be
185   * trusted.
186   *
187   * @param  chain     The client certificate chain for which to make the
188   *                   determination.
189   * @param  authType  The authentication type based on the client certificate.
190   *
191   * @throws  CertificateException  If the provided client certificate chain
192   *                                should not be trusted.
193   */
194  @Override()
195  public void checkClientTrusted(@NotNull final X509Certificate[] chain,
196                                 @NotNull final String authType)
197       throws CertificateException
198  {
199    checkTrusted(chain);
200  }
201
202
203
204  /**
205   * Checks to determine whether the provided server certificate chain should be
206   * trusted.
207   *
208   * @param  chain     The server certificate chain for which to make the
209   *                   determination.
210   * @param  authType  The key exchange algorithm used.
211   *
212   * @throws  CertificateException  If the provided server certificate chain
213   *                                should not be trusted.
214   */
215  @Override()
216  public void checkServerTrusted(@NotNull final X509Certificate[] chain,
217                                 @NotNull final String authType)
218       throws CertificateException
219  {
220    checkTrusted(chain);
221  }
222
223
224
225  /**
226   * Ensures that the provided certificate chain should be trusted.
227   *
228   * @param  chain  The certificate chain to validated.
229   *
230   * @throws  CertificateException  If the certificate chain should not be
231   *                                trusted.
232   */
233  private void checkTrusted(@NotNull final X509Certificate[] chain)
234          throws CertificateException
235  {
236    // Make sure that the chain is not null or empty.
237    if ((chain == null) || (chain.length == 0))
238    {
239      throw new CertificateException(ERR_TR_TM_NO_CHAIN.get());
240    }
241
242
243    // Validate that the peer certificate is currently within its validity
244    // window.
245    final long currentTime = System.currentTimeMillis();
246    final X509Certificate peerCert = chain[0];
247    if (currentTime < peerCert.getNotBefore().getTime())
248    {
249      throw new CertificateException(ERR_TR_TM_PEER_NOT_YET_VALID.get(
250           peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253),
251           String.valueOf(peerCert.getNotBefore())));
252    }
253
254    if (currentTime > peerCert.getNotAfter().getTime())
255    {
256      throw new CertificateException(ERR_TR_TM_PEER_EXPIRED.get(
257           peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253),
258           String.valueOf(peerCert.getNotAfter())));
259    }
260
261
262    // Validate that all of the issuer certificates are also valid.
263    for (int i=1; i < chain.length; i++)
264    {
265      final X509Certificate issuerCert = chain[i];
266      if (currentTime < issuerCert.getNotBefore().getTime())
267      {
268        throw new CertificateException(ERR_TR_TM_ISSUER_NOT_YET_VALID.get(
269             peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253),
270             issuerCert.getSubjectX500Principal().getName(
271                  X500Principal.RFC2253),
272             String.valueOf(peerCert.getNotBefore())));
273      }
274
275      if (currentTime > issuerCert.getNotAfter().getTime())
276      {
277        throw new CertificateException(ERR_TR_TM_ISSUER_EXPIRED.get(
278             peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253),
279             issuerCert.getSubjectX500Principal().getName(
280                  X500Principal.RFC2253),
281             String.valueOf(peerCert.getNotAfter())));
282      }
283    }
284
285
286    // If the cache is valid and it contains the certificate, then we'll trust
287    // it.
288    if ((cacheExpirationTime.get() >= currentTime) &&
289         cachedCertificates.get().contains(peerCert))
290    {
291      // The certificate is trusted.  Return without throwing an exception.
292      return;
293    }
294
295
296    // Read the config file and get the certificates it contains.
297    final Set<X509Certificate> topologyRegistryCertificates =
298         readTopologyRegistryCertificates(peerCert);
299    if (cacheDurationMillis > 0L)
300    {
301      cachedCertificates.set(topologyRegistryCertificates);
302      cacheExpirationTime.set(currentTime + cacheDurationMillis);
303    }
304
305    if (topologyRegistryCertificates.contains(peerCert))
306    {
307      // The certificate is trusted.  Return without throwing an exception.
308      return;
309    }
310    else
311    {
312      throw new CertificateException(ERR_TP_TM_PEER_NOT_FOUND.get(
313           peerCert.getSubjectX500Principal().getName(X500Principal.RFC2253)));
314    }
315  }
316
317
318
319  /**
320   * Reads the certificates defined in the topology registry.
321   *
322   * @param  peerCert  The peer certificate presented for evaluation.
323   *
324   * @return  A set containing the certificates defined in the topology
325   *          registry, or an empty set if no certificates are found.
326   *
327   * @throws  CertificateException  If a problem is encountered while reading
328   *                                certificates from the topology registry.
329   */
330  @NotNull()
331  private Set<X509Certificate> readTopologyRegistryCertificates(
332                                    @NotNull final X509Certificate peerCert)
333          throws CertificateException
334  {
335    try (LDIFReader ldifReader = new LDIFReader(configurationFile))
336    {
337      final Set<X509Certificate> certs = new HashSet<>();
338      while (true)
339      {
340        final Entry entry;
341        try
342        {
343          entry = ldifReader.readEntry();
344        }
345        catch (final LDIFException e)
346        {
347          Debug.debugException(e);
348          if (e.mayContinueReading())
349          {
350            continue;
351          }
352          else
353          {
354            throw new CertificateException(
355                 ERR_TP_TM_MALFORMED_CONFIG.get(
356                      peerCert.getSubjectX500Principal().getName(
357                           X500Principal.RFC2253),
358                      configurationFile.getAbsolutePath(),
359                      StaticUtils.getExceptionMessage(e)),
360                 e);
361          }
362        }
363
364        if (entry == null)
365        {
366          return Collections.unmodifiableSet(certs);
367        }
368
369        if (entry.hasObjectClass(INTER_SERVER_CERT_OC) &&
370             entry.hasAttribute(INTER_SERVER_CERT_AT))
371        {
372          parseCertificates(certs, entry.getAttribute(INTER_SERVER_CERT_AT));
373        }
374        else if (entry.hasObjectClass(LISTENER_CERT_OC) &&
375             entry.hasAttribute(LISTENER_CERT_AT))
376        {
377          parseCertificates(certs, entry.getAttribute(LISTENER_CERT_AT));
378        }
379      }
380    }
381    catch (final IOException e)
382    {
383      Debug.debugException(e);
384      throw new CertificateException(
385           ERR_TP_TM_ERROR_READING_CONFIG_FILE.get(
386                peerCert.getSubjectX500Principal().getName(
387                     X500Principal.RFC2253),
388                configurationFile.getAbsolutePath(),
389                StaticUtils.getExceptionMessage(e)),
390           e);
391    }
392  }
393
394
395
396  /**
397   * Parses any values of the provided attribute as a set of X.509 certificates.
398   *
399   * @param  certs  The set that should be updated with the certificates that
400   *                are parsed.
401   * @param  attr   The attribute whose values should be parsed.
402   */
403  private void parseCertificates(@NotNull final Set<X509Certificate> certs,
404                                 @NotNull final Attribute attr)
405  {
406    final StringBuilder certBase64 = new StringBuilder();
407    for (final String value : attr.getValues())
408    {
409      try
410      {
411        for (final String line : StaticUtils.stringToLines(value))
412        {
413          if (line.equalsIgnoreCase("-----BEGIN CERTIFICATE-----"))
414          {
415            continue;
416          }
417          else if (line.equalsIgnoreCase("-----END CERTIFICATE-----"))
418          {
419            final byte[] certBytes = Base64.decode(certBase64.toString());
420            certBase64.setLength(0);
421
422            certs.add((X509Certificate) CryptoHelper.getCertificateFactory(
423                 "X.509").generateCertificate(new ByteArrayInputStream(
424                      certBytes)));
425          }
426          else
427          {
428            certBase64.append(line);
429          }
430        }
431      }
432      catch (final Exception e)
433      {
434        Debug.debugException(e);
435      }
436    }
437  }
438
439
440
441  /**
442   * Retrieves the accepted issuer certificates for this trust manager.
443   *
444   * @return  The accepted issuer certificates for this trust manager, or an
445   *          empty set of accepted issuers if a problem was encountered while
446   *          initializing this trust manager.
447   */
448  @Override()
449  @NotNull()
450  public X509Certificate[] getAcceptedIssuers()
451  {
452    return NO_CERTIFICATES;
453  }
454}