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