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 2007-2008 Sun Microsystems, Inc.
015 * Portions Copyright 2012-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import java.security.MessageDigest;
020import java.security.cert.Certificate;
021import java.security.cert.X509Certificate;
022import java.util.Collection;
023import java.util.LinkedHashSet;
024import java.util.List;
025import java.util.Set;
026
027import javax.security.auth.x500.X500Principal;
028
029import org.forgerock.i18n.LocalizableMessage;
030import org.forgerock.i18n.slf4j.LocalizedLogger;
031import org.forgerock.opendj.config.server.ConfigChangeResult;
032import org.forgerock.opendj.config.server.ConfigException;
033import org.forgerock.opendj.config.server.ConfigurationChangeListener;
034import org.forgerock.opendj.ldap.ByteString;
035import org.forgerock.opendj.ldap.DN;
036import org.forgerock.opendj.ldap.ResultCode;
037import org.forgerock.opendj.ldap.SearchScope;
038import org.forgerock.opendj.ldap.schema.AttributeType;
039import org.forgerock.opendj.server.config.server.CertificateMapperCfg;
040import org.forgerock.opendj.server.config.server.FingerprintCertificateMapperCfg;
041import org.opends.server.api.Backend;
042import org.opends.server.api.CertificateMapper;
043import org.opends.server.core.DirectoryServer;
044import org.opends.server.protocols.internal.InternalClientConnection;
045import org.opends.server.protocols.internal.InternalSearchOperation;
046import org.opends.server.protocols.internal.SearchRequest;
047import org.opends.server.types.DirectoryException;
048import org.opends.server.types.Entry;
049import org.opends.server.types.IndexType;
050import org.opends.server.types.InitializationException;
051import org.opends.server.types.SearchFilter;
052import org.opends.server.types.SearchResultEntry;
053
054import static org.opends.messages.ExtensionMessages.*;
055import static org.opends.server.protocols.internal.InternalClientConnection.*;
056import static org.opends.server.protocols.internal.Requests.*;
057import static org.opends.server.util.CollectionUtils.*;
058import static org.opends.server.util.StaticUtils.*;
059
060/**
061 * This class implements a very simple Directory Server certificate mapper that
062 * will map a certificate to a user only if that user's entry contains an
063 * attribute with the fingerprint of the client certificate.  There must be
064 * exactly one matching user entry for the mapping to be successful.
065 */
066public class FingerprintCertificateMapper
067       extends CertificateMapper<FingerprintCertificateMapperCfg>
068       implements ConfigurationChangeListener<
069                       FingerprintCertificateMapperCfg>
070{
071  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
072
073  /** The current configuration for this certificate mapper. */
074  private FingerprintCertificateMapperCfg currentConfig;
075  /** The algorithm that will be used to generate the fingerprint. */
076  private String fingerprintAlgorithm;
077  /** The set of attributes to return in search result entries. */
078  private LinkedHashSet<String> requestedAttributes;
079
080  /**
081   * Creates a new instance of this certificate mapper.  Note that all actual
082   * initialization should be done in the
083   * <CODE>initializeCertificateMapper</CODE> method.
084   */
085  public FingerprintCertificateMapper()
086  {
087    super();
088  }
089
090  @Override
091  public void initializeCertificateMapper(
092                   FingerprintCertificateMapperCfg configuration)
093         throws ConfigException, InitializationException
094  {
095    configuration.addFingerprintChangeListener(this);
096
097    currentConfig = configuration;
098
099    // Get the algorithm that will be used to generate the fingerprint.
100    switch (configuration.getFingerprintAlgorithm())
101    {
102      case MD5:
103        fingerprintAlgorithm = "MD5";
104        break;
105      case SHA1:
106        fingerprintAlgorithm = "SHA1";
107        break;
108    }
109
110    // Make sure that the fingerprint attribute is configured for equality in
111    // all appropriate backends.
112    Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
113    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
114    {
115      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
116    }
117
118    AttributeType t = configuration.getFingerprintAttribute();
119    for (DN baseDN : cfgBaseDNs)
120    {
121      Backend<?> b = DirectoryServer.getBackend(baseDN);
122      if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
123      {
124        logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(),
125            t.getNameOrOID(), b.getBackendID());
126      }
127    }
128
129    // Create the attribute list to include in search requests.  We want to
130    // include all user and operational attributes.
131    requestedAttributes = newLinkedHashSet("*", "+");
132  }
133
134  @Override
135  public void finalizeCertificateMapper()
136  {
137    currentConfig.removeFingerprintChangeListener(this);
138  }
139
140  @Override
141  public Entry mapCertificateToUser(Certificate[] certificateChain)
142         throws DirectoryException
143  {
144    FingerprintCertificateMapperCfg config = currentConfig;
145    AttributeType fingerprintAttributeType = config.getFingerprintAttribute();
146    String theFingerprintAlgorithm = this.fingerprintAlgorithm;
147
148    // Make sure that a peer certificate was provided.
149    if (certificateChain == null || certificateChain.length == 0)
150    {
151      LocalizableMessage message = ERR_FCM_NO_PEER_CERTIFICATE.get();
152      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
153    }
154
155    // Get the first certificate in the chain.  It must be an X.509 certificate.
156    X509Certificate peerCertificate;
157    try
158    {
159      peerCertificate = (X509Certificate) certificateChain[0];
160    }
161    catch (Exception e)
162    {
163      logger.traceException(e);
164
165      LocalizableMessage message = ERR_FCM_PEER_CERT_NOT_X509.get(
166          certificateChain[0].getType());
167      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
168    }
169
170    // Get the signature from the peer certificate and create a digest of it
171    // using the configured algorithm.
172    String fingerprintString;
173    try
174    {
175      MessageDigest digest = MessageDigest.getInstance(theFingerprintAlgorithm);
176      byte[] fingerprintBytes = digest.digest(peerCertificate.getEncoded());
177      fingerprintString = bytesToColonDelimitedHex(fingerprintBytes);
178    }
179    catch (Exception e)
180    {
181      logger.traceException(e);
182
183      String peerSubject = peerCertificate.getSubjectX500Principal().getName(
184                                X500Principal.RFC2253);
185
186      LocalizableMessage message = ERR_FCM_CANNOT_CALCULATE_FINGERPRINT.get(
187          peerSubject, getExceptionMessage(e));
188      throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
189    }
190
191    // Create the search filter from the fingerprint.
192    ByteString value = ByteString.valueOfUtf8(fingerprintString);
193    SearchFilter filter =
194         SearchFilter.createEqualityFilter(fingerprintAttributeType, value);
195
196    // If we have an explicit set of base DNs, then use it.  Otherwise, use the
197    // set of public naming contexts in the server.
198    Collection<DN> baseDNs = config.getUserBaseDN();
199    if (baseDNs == null || baseDNs.isEmpty())
200    {
201      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
202    }
203
204    // For each base DN, issue an internal search in an attempt to map the
205    // certificate.
206    Entry userEntry = null;
207    InternalClientConnection conn = getRootConnection();
208    for (DN baseDN : baseDNs)
209    {
210      final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter)
211          .setSizeLimit(1)
212          .setTimeLimit(10)
213          .addAttribute(requestedAttributes);
214      InternalSearchOperation searchOperation = conn.processSearch(request);
215
216      switch (searchOperation.getResultCode().asEnum())
217      {
218        case SUCCESS:
219          // This is fine.  No action needed.
220          break;
221
222        case NO_SUCH_OBJECT:
223          // The search base doesn't exist.  Not an ideal situation, but we'll
224          // ignore it.
225          break;
226
227        case SIZE_LIMIT_EXCEEDED:
228          // Multiple entries matched the filter.  This is not acceptable.
229          LocalizableMessage message = ERR_FCM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get(
230                        fingerprintString);
231          throw new DirectoryException(
232                  ResultCode.INVALID_CREDENTIALS, message);
233
234        case TIME_LIMIT_EXCEEDED:
235        case ADMIN_LIMIT_EXCEEDED:
236          // The search criteria was too inefficient.
237          message = ERR_FCM_INEFFICIENT_SEARCH.get(fingerprintString, searchOperation.getErrorMessage());
238          throw new DirectoryException(searchOperation.getResultCode(),
239              message);
240
241        default:
242          // Just pass on the failure that was returned for this search.
243          message = ERR_FCM_SEARCH_FAILED.get(fingerprintString, searchOperation.getErrorMessage());
244          throw new DirectoryException(searchOperation.getResultCode(),
245              message);
246      }
247
248      for (SearchResultEntry entry : searchOperation.getSearchEntries())
249      {
250        if (userEntry == null)
251        {
252          userEntry = entry;
253        }
254        else
255        {
256          LocalizableMessage message = ERR_FCM_MULTIPLE_MATCHING_ENTRIES.
257              get(fingerprintString, userEntry.getName(), entry.getName());
258          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
259        }
260      }
261    }
262
263    // If we've gotten here, then we either found exactly one user entry or we
264    // didn't find any.  Either way, return the entry or null to the caller.
265    return userEntry;
266  }
267
268  @Override
269  public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
270                                           List<LocalizableMessage> unacceptableReasons)
271  {
272    FingerprintCertificateMapperCfg config =
273         (FingerprintCertificateMapperCfg) configuration;
274    return isConfigurationChangeAcceptable(config, unacceptableReasons);
275  }
276
277  @Override
278  public boolean isConfigurationChangeAcceptable(
279                      FingerprintCertificateMapperCfg configuration,
280                      List<LocalizableMessage> unacceptableReasons)
281  {
282    return true;
283  }
284
285  @Override
286  public ConfigChangeResult applyConfigurationChange(
287              FingerprintCertificateMapperCfg configuration)
288  {
289    final ConfigChangeResult ccr = new ConfigChangeResult();
290
291    // Get the algorithm that will be used to generate the fingerprint.
292    String newFingerprintAlgorithm = null;
293    switch (configuration.getFingerprintAlgorithm())
294    {
295      case MD5:
296        newFingerprintAlgorithm = "MD5";
297        break;
298      case SHA1:
299        newFingerprintAlgorithm = "SHA1";
300        break;
301    }
302
303    if (ccr.getResultCode() == ResultCode.SUCCESS)
304    {
305      fingerprintAlgorithm = newFingerprintAlgorithm;
306      currentConfig        = configuration;
307    }
308
309    // Make sure that the fingerprint attribute is configured for equality in
310    // all appropriate backends.
311    Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
312    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
313    {
314      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
315    }
316
317    AttributeType t = configuration.getFingerprintAttribute();
318    for (DN baseDN : cfgBaseDNs)
319    {
320      Backend<?> b = DirectoryServer.getBackend(baseDN);
321      if (b != null && ! b.isIndexed(t, IndexType.EQUALITY))
322      {
323        LocalizableMessage message = WARN_SATUACM_ATTR_UNINDEXED.get(
324            configuration.dn(), t.getNameOrOID(), b.getBackendID());
325        ccr.addMessage(message);
326        logger.error(message);
327      }
328    }
329
330   return ccr;
331  }
332}