/*
|
* The contents of this file are subject to the terms of the Common Development and
|
* Distribution License (the License). You may not use this file except in compliance with the
|
* License.
|
*
|
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
|
* specific language governing permission and limitations under the License.
|
*
|
* When distributing Covered Software, include this CDDL Header Notice in each file and include
|
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
|
* Header, with the fields enclosed by brackets [] replaced by your own identifying
|
* information: "Portions Copyright [year] [name of copyright owner]".
|
*
|
* Copyright 2008-2009 Sun Microsystems, Inc.
|
* Portions Copyright 2009 Parametric Technology Corporation (PTC)
|
* Portions Copyright 2011-2015 ForgeRock AS.
|
*/
|
|
package org.opends.admin.ads.util;
|
|
import java.security.KeyStore;
|
import java.security.KeyStoreException;
|
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchProviderException;
|
import java.security.cert.CertificateException;
|
import java.security.cert.X509Certificate;
|
import java.util.ArrayList;
|
|
import javax.naming.ldap.LdapName;
|
import javax.naming.ldap.Rdn;
|
import javax.net.ssl.TrustManager;
|
import javax.net.ssl.TrustManagerFactory;
|
import javax.net.ssl.X509TrustManager;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.opends.server.util.Platform;
|
|
/**
|
* This class is in charge of checking whether the certificates that are
|
* presented are trusted or not.
|
* This implementation tries to check also that the subject DN of the
|
* certificate corresponds to the host passed using the setHostName method.
|
*
|
* The constructor tries to use a default TrustManager from the system and if
|
* it cannot be retrieved this class will only accept the certificates
|
* explicitly accepted by the user (and specified by calling acceptCertificate).
|
*
|
* NOTE: this class is not aimed to be used when we have connections in
|
* parallel.
|
*/
|
public class ApplicationTrustManager implements X509TrustManager
|
{
|
/**
|
* The enumeration for the different causes for which the trust manager can
|
* refuse to accept a certificate.
|
*/
|
public enum Cause
|
{
|
/**
|
* The certificate was not trusted.
|
*/
|
NOT_TRUSTED,
|
/**
|
* The certificate's subject DN's value and the host name we tried to
|
* connect to do not match.
|
*/
|
HOST_NAME_MISMATCH
|
}
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
private X509TrustManager trustManager;
|
private String lastRefusedAuthType;
|
private X509Certificate[] lastRefusedChain;
|
private Cause lastRefusedCause;
|
private KeyStore keystore;
|
|
/**
|
* The following ArrayList contain information about the certificates
|
* explicitly accepted by the user.
|
*/
|
private ArrayList<X509Certificate[]> acceptedChains = new ArrayList<>();
|
private ArrayList<String> acceptedAuthTypes = new ArrayList<>();
|
private ArrayList<String> acceptedHosts = new ArrayList<>();
|
|
private String host;
|
|
|
/**
|
* The default constructor.
|
*
|
* @param keystore The keystore to use for this trustmanager.
|
*/
|
public ApplicationTrustManager(KeyStore keystore)
|
{
|
this.keystore = keystore;
|
String userSpecifiedAlgo = System.getProperty("org.opends.admin.trustmanageralgo");
|
String userSpecifiedProvider = System.getProperty("org.opends.admin.trustmanagerprovider");
|
|
//Handle IBM specific cases if the user did not specify a algorithm and/or provider.
|
if(userSpecifiedAlgo == null && Platform.isVendor("IBM"))
|
{
|
userSpecifiedAlgo = "IbmX509";
|
}
|
if(userSpecifiedProvider == null && Platform.isVendor("IBM"))
|
{
|
userSpecifiedProvider = "IBMJSSE2";
|
}
|
|
// Have some fallbacks to choose the provider and algorithm of the key manager.
|
// First see if the user wanted to use something specific,
|
// then try with the SunJSSE provider and SunX509 algorithm.
|
// Finally,fallback to the default algorithm of the JVM.
|
String[] preferredProvider =
|
{ userSpecifiedProvider, "SunJSSE", null, null };
|
String[] preferredAlgo =
|
{ userSpecifiedAlgo, "SunX509", "SunX509",
|
TrustManagerFactory.getDefaultAlgorithm() };
|
|
for (int i=0; i<preferredProvider.length && trustManager == null; i++)
|
{
|
String provider = preferredProvider[i];
|
String algo = preferredAlgo[i];
|
if (algo == null)
|
{
|
continue;
|
}
|
try
|
{
|
TrustManagerFactory tmf = null;
|
if (provider != null)
|
{
|
tmf = TrustManagerFactory.getInstance(algo, provider);
|
}
|
else
|
{
|
tmf = TrustManagerFactory.getInstance(algo);
|
}
|
tmf.init(keystore);
|
for (TrustManager tm : tmf.getTrustManagers())
|
{
|
if (tm instanceof X509TrustManager)
|
{
|
trustManager = (X509TrustManager) tm;
|
break;
|
}
|
}
|
}
|
catch (NoSuchProviderException e)
|
{
|
logger.warn(LocalizableMessage.raw("Error with the provider: "+provider, e));
|
}
|
catch (NoSuchAlgorithmException e)
|
{
|
logger.warn(LocalizableMessage.raw("Error with the algorithm: "+algo, e));
|
}
|
catch (KeyStoreException e)
|
{
|
logger.warn(LocalizableMessage.raw("Error with the keystore", e));
|
}
|
}
|
}
|
|
/** {@inheritDoc} */
|
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
throws CertificateException
|
{
|
boolean explicitlyAccepted = false;
|
try
|
{
|
if (trustManager != null)
|
{
|
try
|
{
|
trustManager.checkClientTrusted(chain, authType);
|
}
|
catch (CertificateException ce)
|
{
|
verifyAcceptedCertificates(chain, authType);
|
explicitlyAccepted = true;
|
}
|
}
|
else
|
{
|
verifyAcceptedCertificates(chain, authType);
|
explicitlyAccepted = true;
|
}
|
}
|
catch (CertificateException ce)
|
{
|
manageException(chain, authType, ce, Cause.NOT_TRUSTED);
|
}
|
|
if (!explicitlyAccepted)
|
{
|
try
|
{
|
verifyHostName(chain, authType);
|
}
|
catch (CertificateException ce)
|
{
|
manageException(chain, authType, ce, Cause.HOST_NAME_MISMATCH);
|
}
|
}
|
}
|
|
/** {@inheritDoc} */
|
public void checkServerTrusted(X509Certificate[] chain,
|
String authType) throws CertificateException
|
{
|
boolean explicitlyAccepted = false;
|
try
|
{
|
if (trustManager != null)
|
{
|
try
|
{
|
trustManager.checkServerTrusted(chain, authType);
|
}
|
catch (CertificateException ce)
|
{
|
verifyAcceptedCertificates(chain, authType);
|
explicitlyAccepted = true;
|
}
|
}
|
else
|
{
|
verifyAcceptedCertificates(chain, authType);
|
explicitlyAccepted = true;
|
}
|
}
|
catch (CertificateException ce)
|
{
|
manageException(chain, authType, ce, Cause.NOT_TRUSTED);
|
}
|
|
if (!explicitlyAccepted)
|
{
|
try
|
{
|
verifyHostName(chain, authType);
|
}
|
catch (CertificateException ce)
|
{
|
manageException(chain, authType, ce, Cause.HOST_NAME_MISMATCH);
|
}
|
}
|
}
|
|
private void manageException(final X509Certificate[] chain,
|
final String authType, final CertificateException ce, final Cause cause)
|
throws OpendsCertificateException
|
{
|
lastRefusedChain = chain;
|
lastRefusedAuthType = authType;
|
lastRefusedCause = cause;
|
throw new OpendsCertificateException(chain, ce);
|
}
|
|
/** {@inheritDoc} */
|
public X509Certificate[] getAcceptedIssuers()
|
{
|
if (trustManager != null)
|
{
|
return trustManager.getAcceptedIssuers();
|
}
|
return new X509Certificate[0];
|
}
|
|
/**
|
* This method is called when the user accepted a certificate.
|
* @param chain the certificate chain accepted by the user.
|
* @param authType the authentication type.
|
* @param host the host we tried to connect and that presented the certificate.
|
*/
|
public void acceptCertificate(X509Certificate[] chain, String authType,
|
String host)
|
{
|
acceptedChains.add(chain);
|
acceptedAuthTypes.add(authType);
|
acceptedHosts.add(host);
|
}
|
|
/**
|
* Sets the host name we are trying to contact in a secure mode. This
|
* method is used if we want to verify the correspondence between the
|
* hostname and the subject DN of the certificate that is being presented.
|
* If this method is never called (or called passing null) no verification
|
* will be made on the host name.
|
* @param host the host name we are trying to contact in a secure mode.
|
*/
|
public void setHost(String host)
|
{
|
this.host = host;
|
}
|
|
/**
|
* This is a method used to set to null the different members that provide
|
* information about the last refused certificate. It is recommended to
|
* call this method before trying to establish a connection using this
|
* trust manager.
|
*/
|
public void resetLastRefusedItems()
|
{
|
lastRefusedAuthType = null;
|
lastRefusedChain = null;
|
lastRefusedCause = null;
|
}
|
|
/**
|
* Creates a copy of this ApplicationTrustManager.
|
* @return a copy of this ApplicationTrustManager.
|
*/
|
public ApplicationTrustManager createCopy()
|
{
|
ApplicationTrustManager copy = new ApplicationTrustManager(keystore);
|
copy.lastRefusedAuthType = lastRefusedAuthType;
|
copy.lastRefusedChain = lastRefusedChain;
|
copy.lastRefusedCause = lastRefusedCause;
|
copy.acceptedChains.addAll(acceptedChains);
|
copy.acceptedAuthTypes.addAll(acceptedAuthTypes);
|
copy.acceptedHosts.addAll(acceptedHosts);
|
|
copy.host = host;
|
|
return copy;
|
}
|
|
/**
|
* Verifies whether the provided chain and authType have been already accepted
|
* by the user or not. If they have not a CertificateException is thrown.
|
* @param chain the certificate chain to analyze.
|
* @param authType the authentication type.
|
* @throws CertificateException if the provided certificate chain and the
|
* authentication type have not been accepted explicitly by the user.
|
*/
|
private void verifyAcceptedCertificates(X509Certificate[] chain,
|
String authType) throws CertificateException
|
{
|
boolean found = false;
|
for (int i=0; i<acceptedChains.size() && !found; i++)
|
{
|
if (authType.equals(acceptedAuthTypes.get(i)))
|
{
|
X509Certificate[] current = acceptedChains.get(i);
|
found = current.length == chain.length;
|
for (int j=0; j<chain.length && found; j++)
|
{
|
found = chain[j].equals(current[j]);
|
}
|
}
|
}
|
if (!found)
|
{
|
throw new OpendsCertificateException(
|
"Certificate not in list of accepted certificates", chain);
|
}
|
}
|
|
/**
|
* Verifies that the provided certificate chains subject DN corresponds to the
|
* host name specified with the setHost method.
|
* @param chain the certificate chain to analyze.
|
* @throws CertificateException if the subject DN of the certificate does
|
* not match with the host name specified with the method setHost.
|
*/
|
private void verifyHostName(X509Certificate[] chain, String authType)
|
throws CertificateException
|
{
|
if (host != null)
|
{
|
boolean matches = false;
|
try
|
{
|
LdapName dn =
|
new LdapName(chain[0].getSubjectX500Principal().getName());
|
Rdn rdn = dn.getRdn(dn.getRdns().size() - 1);
|
String value = rdn.getValue().toString();
|
matches = hostMatch(value, host);
|
if (!matches)
|
{
|
logger.warn(LocalizableMessage.raw("Subject DN RDN value is: "+value+
|
" and does not match host value: "+host));
|
// Try with the accepted hosts names
|
for (int i =0; i<acceptedHosts.size() && !matches; i++)
|
{
|
if (hostMatch(acceptedHosts.get(i), host))
|
{
|
X509Certificate[] current = acceptedChains.get(i);
|
matches = current.length == chain.length;
|
for (int j=0; j<chain.length && matches; j++)
|
{
|
matches = chain[j].equals(current[j]);
|
}
|
}
|
}
|
}
|
}
|
catch (Throwable t)
|
{
|
logger.warn(LocalizableMessage.raw("Error parsing subject dn: "+
|
chain[0].getSubjectX500Principal(), t));
|
}
|
|
if (!matches)
|
{
|
throw new OpendsCertificateException(
|
"Hostname mismatch between host name " + host
|
+ " and subject DN: " + chain[0].getSubjectX500Principal(),
|
chain);
|
}
|
}
|
}
|
|
/**
|
* Returns the authentication type for the last refused certificate.
|
* @return the authentication type for the last refused certificate.
|
*/
|
public String getLastRefusedAuthType()
|
{
|
return lastRefusedAuthType;
|
}
|
|
/**
|
* Returns the last cause for refusal of a certificate.
|
* @return the last cause for refusal of a certificate.
|
*/
|
public Cause getLastRefusedCause()
|
{
|
return lastRefusedCause;
|
}
|
|
/**
|
* Returns the certificate chain for the last refused certificate.
|
* @return the certificate chain for the last refused certificate.
|
*/
|
public X509Certificate[] getLastRefusedChain()
|
{
|
return lastRefusedChain;
|
}
|
|
/**
|
* Checks whether two host names match. It accepts the use of wildcard in the
|
* host name.
|
* @param host1 the first host name.
|
* @param host2 the second host name.
|
* @return <CODE>true</CODE> if the host match and <CODE>false</CODE>
|
* otherwise.
|
*/
|
private boolean hostMatch(String host1, String host2)
|
{
|
if (host1 == null)
|
{
|
throw new IllegalArgumentException("The host1 parameter cannot be null");
|
}
|
if (host2 == null)
|
{
|
throw new IllegalArgumentException("The host2 parameter cannot be null");
|
}
|
String[] h1 = host1.split("\\.");
|
String[] h2 = host2.split("\\.");
|
|
boolean hostMatch = h1.length == h2.length;
|
for (int i=0; i<h1.length && hostMatch; i++)
|
{
|
if (!"*".equals(h1[i]) && !"*".equals(h2[i]))
|
{
|
hostMatch = h1[i].equalsIgnoreCase(h2[i]);
|
}
|
}
|
return hostMatch;
|
}
|
}
|