001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2008-2009 Sun Microsystems, Inc.
025 *      Portions Copyright 2009 Parametric Technology Corporation (PTC)
026 *      Portions Copyright 2011-2015 ForgeRock AS
027 */
028
029package org.opends.admin.ads.util;
030
031import java.security.KeyStore;
032import java.security.KeyStoreException;
033import java.security.NoSuchAlgorithmException;
034import java.security.NoSuchProviderException;
035import java.security.cert.CertificateException;
036import java.security.cert.X509Certificate;
037import java.util.ArrayList;
038
039import javax.naming.ldap.LdapName;
040import javax.naming.ldap.Rdn;
041import javax.net.ssl.TrustManager;
042import javax.net.ssl.TrustManagerFactory;
043import javax.net.ssl.X509TrustManager;
044
045import org.forgerock.i18n.LocalizableMessage;
046import org.forgerock.i18n.slf4j.LocalizedLogger;
047import org.opends.server.util.Platform;
048
049/**
050 * This class is in charge of checking whether the certificates that are
051 * presented are trusted or not.
052 * This implementation tries to check also that the subject DN of the
053 * certificate corresponds to the host passed using the setHostName method.
054 *
055 * The constructor tries to use a default TrustManager from the system and if
056 * it cannot be retrieved this class will only accept the certificates
057 * explicitly accepted by the user (and specified by calling acceptCertificate).
058 *
059 * NOTE: this class is not aimed to be used when we have connections in
060 * parallel.
061 */
062public class ApplicationTrustManager implements X509TrustManager
063{
064  /**
065   * The enumeration for the different causes for which the trust manager can
066   * refuse to accept a certificate.
067   */
068  public enum Cause
069  {
070    /**
071     * The certificate was not trusted.
072     */
073    NOT_TRUSTED,
074    /**
075     * The certificate's subject DN's value and the host name we tried to
076     * connect to do not match.
077     */
078    HOST_NAME_MISMATCH
079  }
080  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
081
082  private X509TrustManager trustManager;
083  private String lastRefusedAuthType;
084  private X509Certificate[] lastRefusedChain;
085  private Cause lastRefusedCause;
086  private KeyStore keystore;
087
088  /**
089   * The following ArrayList contain information about the certificates
090   * explicitly accepted by the user.
091   */
092  private ArrayList<X509Certificate[]> acceptedChains = new ArrayList<>();
093  private ArrayList<String> acceptedAuthTypes = new ArrayList<>();
094  private ArrayList<String> acceptedHosts = new ArrayList<>();
095
096  private String host;
097
098
099  /**
100   * The default constructor.
101   *
102   * @param keystore The keystore to use for this trustmanager.
103   */
104  public ApplicationTrustManager(KeyStore keystore)
105  {
106    this.keystore = keystore;
107    String userSpecifiedAlgo = System.getProperty("org.opends.admin.trustmanageralgo");
108    String userSpecifiedProvider = System.getProperty("org.opends.admin.trustmanagerprovider");
109
110    //Handle IBM specific cases if the user did not specify a algorithm and/or provider.
111    if(userSpecifiedAlgo == null && Platform.isVendor("IBM"))
112    {
113      userSpecifiedAlgo = "IbmX509";
114    }
115    if(userSpecifiedProvider == null && Platform.isVendor("IBM"))
116    {
117      userSpecifiedProvider = "IBMJSSE2";
118    }
119
120    // Have some fallbacks to choose the provider and algorithm of the key manager.
121    // First see if the user wanted to use something specific,
122    // then try with the SunJSSE provider and SunX509 algorithm.
123    // Finally,fallback to the default algorithm of the JVM.
124    String[] preferredProvider =
125        { userSpecifiedProvider, "SunJSSE", null, null };
126    String[] preferredAlgo =
127        { userSpecifiedAlgo, "SunX509", "SunX509",
128          TrustManagerFactory.getDefaultAlgorithm() };
129
130      for (int i=0; i<preferredProvider.length && trustManager == null; i++)
131      {
132        String provider = preferredProvider[i];
133        String algo = preferredAlgo[i];
134        if (algo == null)
135        {
136          continue;
137        }
138        try
139        {
140          TrustManagerFactory tmf = null;
141          if (provider != null)
142          {
143            tmf = TrustManagerFactory.getInstance(algo, provider);
144          }
145          else
146          {
147            tmf = TrustManagerFactory.getInstance(algo);
148          }
149          tmf.init(keystore);
150          for (TrustManager tm : tmf.getTrustManagers())
151          {
152            if (tm instanceof X509TrustManager)
153            {
154              trustManager = (X509TrustManager) tm;
155              break;
156            }
157          }
158        }
159        catch (NoSuchProviderException e)
160        {
161          logger.warn(LocalizableMessage.raw("Error with the provider: "+provider, e));
162        }
163        catch (NoSuchAlgorithmException e)
164        {
165          logger.warn(LocalizableMessage.raw("Error with the algorithm: "+algo, e));
166        }
167        catch (KeyStoreException e)
168        {
169          logger.warn(LocalizableMessage.raw("Error with the keystore", e));
170        }
171      }
172  }
173
174  /** {@inheritDoc} */
175  public void checkClientTrusted(X509Certificate[] chain, String authType)
176  throws CertificateException
177  {
178    boolean explicitlyAccepted = false;
179    try
180    {
181      if (trustManager != null)
182      {
183        try
184        {
185          trustManager.checkClientTrusted(chain, authType);
186        }
187        catch (CertificateException ce)
188        {
189          verifyAcceptedCertificates(chain, authType);
190          explicitlyAccepted = true;
191        }
192      }
193      else
194      {
195        verifyAcceptedCertificates(chain, authType);
196        explicitlyAccepted = true;
197      }
198    }
199    catch (CertificateException ce)
200    {
201      manageException(chain, authType, ce, Cause.NOT_TRUSTED);
202    }
203
204    if (!explicitlyAccepted)
205    {
206      try
207      {
208        verifyHostName(chain, authType);
209      }
210      catch (CertificateException ce)
211      {
212        manageException(chain, authType, ce, Cause.HOST_NAME_MISMATCH);
213      }
214    }
215  }
216
217  /** {@inheritDoc} */
218  public void checkServerTrusted(X509Certificate[] chain,
219      String authType) throws CertificateException
220  {
221    boolean explicitlyAccepted = false;
222    try
223    {
224      if (trustManager != null)
225      {
226        try
227        {
228          trustManager.checkServerTrusted(chain, authType);
229        }
230        catch (CertificateException ce)
231        {
232          verifyAcceptedCertificates(chain, authType);
233          explicitlyAccepted = true;
234        }
235      }
236      else
237      {
238        verifyAcceptedCertificates(chain, authType);
239        explicitlyAccepted = true;
240      }
241    }
242    catch (CertificateException ce)
243    {
244      manageException(chain, authType, ce, Cause.NOT_TRUSTED);
245    }
246
247    if (!explicitlyAccepted)
248    {
249      try
250      {
251        verifyHostName(chain, authType);
252      }
253      catch (CertificateException ce)
254      {
255        manageException(chain, authType, ce, Cause.HOST_NAME_MISMATCH);
256      }
257    }
258  }
259
260  private void manageException(final X509Certificate[] chain,
261      final String authType, final CertificateException ce, final Cause cause)
262      throws OpendsCertificateException
263  {
264    lastRefusedChain = chain;
265    lastRefusedAuthType = authType;
266    lastRefusedCause = cause;
267    throw new OpendsCertificateException(chain, ce);
268  }
269
270  /** {@inheritDoc} */
271  public X509Certificate[] getAcceptedIssuers()
272  {
273    if (trustManager != null)
274    {
275      return trustManager.getAcceptedIssuers();
276    }
277    return new X509Certificate[0];
278  }
279
280  /**
281   * This method is called when the user accepted a certificate.
282   * @param chain the certificate chain accepted by the user.
283   * @param authType the authentication type.
284   * @param host the host we tried to connect and that presented the certificate.
285   */
286  public void acceptCertificate(X509Certificate[] chain, String authType,
287      String host)
288  {
289    acceptedChains.add(chain);
290    acceptedAuthTypes.add(authType);
291    acceptedHosts.add(host);
292  }
293
294  /**
295   * Sets the host name we are trying to contact in a secure mode.  This
296   * method is used if we want to verify the correspondence between the
297   * hostname and the subject DN of the certificate that is being presented.
298   * If this method is never called (or called passing null) no verification
299   * will be made on the host name.
300   * @param host the host name we are trying to contact in a secure mode.
301   */
302  public void setHost(String host)
303  {
304    this.host = host;
305  }
306
307  /**
308   * This is a method used to set to null the different members that provide
309   * information about the last refused certificate.  It is recommended to
310   * call this method before trying to establish a connection using this
311   * trust manager.
312   */
313  public void resetLastRefusedItems()
314  {
315    lastRefusedAuthType = null;
316    lastRefusedChain = null;
317    lastRefusedCause = null;
318  }
319
320  /**
321   * Creates a copy of this ApplicationTrustManager.
322   * @return a copy of this ApplicationTrustManager.
323   */
324  public ApplicationTrustManager createCopy()
325  {
326    ApplicationTrustManager copy = new ApplicationTrustManager(keystore);
327    copy.lastRefusedAuthType = lastRefusedAuthType;
328    copy.lastRefusedChain = lastRefusedChain;
329    copy.lastRefusedCause = lastRefusedCause;
330    copy.acceptedChains.addAll(acceptedChains);
331    copy.acceptedAuthTypes.addAll(acceptedAuthTypes);
332    copy.acceptedHosts.addAll(acceptedHosts);
333
334    copy.host = host;
335
336    return copy;
337  }
338
339  /**
340   * Verifies whether the provided chain and authType have been already accepted
341   * by the user or not.  If they have not a CertificateException is thrown.
342   * @param chain the certificate chain to analyze.
343   * @param authType the authentication type.
344   * @throws CertificateException if the provided certificate chain and the
345   * authentication type have not been accepted explicitly by the user.
346   */
347  private void verifyAcceptedCertificates(X509Certificate[] chain,
348      String authType) throws CertificateException
349  {
350    boolean found = false;
351    for (int i=0; i<acceptedChains.size() && !found; i++)
352    {
353      if (authType.equals(acceptedAuthTypes.get(i)))
354      {
355        X509Certificate[] current = acceptedChains.get(i);
356        found = current.length == chain.length;
357        for (int j=0; j<chain.length && found; j++)
358        {
359          found = chain[j].equals(current[j]);
360        }
361      }
362    }
363    if (!found)
364    {
365      throw new OpendsCertificateException(
366          "Certificate not in list of accepted certificates", chain);
367    }
368  }
369
370  /**
371   * Verifies that the provided certificate chains subject DN corresponds to the
372   * host name specified with the setHost method.
373   * @param chain the certificate chain to analyze.
374   * @throws CertificateException if the subject DN of the certificate does
375   * not match with the host name specified with the method setHost.
376   */
377  private void verifyHostName(X509Certificate[] chain, String authType)
378  throws CertificateException
379  {
380    if (host != null)
381    {
382      boolean matches = false;
383      try
384      {
385        LdapName dn =
386          new LdapName(chain[0].getSubjectX500Principal().getName());
387        Rdn rdn = dn.getRdn(dn.getRdns().size() - 1);
388        String value = rdn.getValue().toString();
389        matches = hostMatch(value, host);
390        if (!matches)
391        {
392          logger.warn(LocalizableMessage.raw("Subject DN RDN value is: "+value+
393              " and does not match host value: "+host));
394          // Try with the accepted hosts names
395          for (int i =0; i<acceptedHosts.size() && !matches; i++)
396          {
397            if (hostMatch(acceptedHosts.get(i), host))
398            {
399              X509Certificate[] current = acceptedChains.get(i);
400              matches = current.length == chain.length;
401              for (int j=0; j<chain.length && matches; j++)
402              {
403                matches = chain[j].equals(current[j]);
404              }
405            }
406          }
407        }
408      }
409      catch (Throwable t)
410      {
411        logger.warn(LocalizableMessage.raw("Error parsing subject dn: "+
412            chain[0].getSubjectX500Principal(), t));
413      }
414
415      if (!matches)
416      {
417        throw new OpendsCertificateException(
418            "Hostname mismatch between host name " + host
419                + " and subject DN: " + chain[0].getSubjectX500Principal(),
420            chain);
421      }
422    }
423  }
424
425  /**
426   * Returns the authentication type for the last refused certificate.
427   * @return the authentication type for the last refused certificate.
428   */
429  public String getLastRefusedAuthType()
430  {
431    return lastRefusedAuthType;
432  }
433
434  /**
435   * Returns the last cause for refusal of a certificate.
436   * @return the last cause for refusal of a certificate.
437   */
438  public Cause getLastRefusedCause()
439  {
440    return lastRefusedCause;
441  }
442
443  /**
444   * Returns the certificate chain for the last refused certificate.
445   * @return the certificate chain for the last refused certificate.
446   */
447  public X509Certificate[] getLastRefusedChain()
448  {
449    return lastRefusedChain;
450  }
451
452  /**
453   * Checks whether two host names match.  It accepts the use of wildcard in the
454   * host name.
455   * @param host1 the first host name.
456   * @param host2 the second host name.
457   * @return <CODE>true</CODE> if the host match and <CODE>false</CODE>
458   * otherwise.
459   */
460  private boolean hostMatch(String host1, String host2)
461  {
462    if (host1 == null)
463    {
464      throw new IllegalArgumentException("The host1 parameter cannot be null");
465    }
466    if (host2 == null)
467    {
468      throw new IllegalArgumentException("The host2 parameter cannot be null");
469    }
470    String[] h1 = host1.split("\\.");
471    String[] h2 = host2.split("\\.");
472
473    boolean hostMatch = h1.length == h2.length;
474    for (int i=0; i<h1.length && hostMatch; i++)
475    {
476      if (!"*".equals(h1[i]) && !"*".equals(h2[i]))
477      {
478        hostMatch = h1[i].equalsIgnoreCase(h2[i]);
479      }
480    }
481    return hostMatch;
482  }
483}