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