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