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
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2007-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    
029    
030    
031    import java.security.MessageDigest;
032    import java.security.cert.Certificate;
033    import java.security.cert.X509Certificate;
034    import javax.security.auth.x500.X500Principal;
035    import java.util.ArrayList;
036    import java.util.Collection;
037    import java.util.List;
038    import java.util.Set;
039    
040    import org.opends.messages.Message;
041    import org.opends.server.admin.server.ConfigurationChangeListener;
042    import org.opends.server.admin.std.server.CertificateMapperCfg;
043    import org.opends.server.admin.std.server.FingerprintCertificateMapperCfg;
044    import org.opends.server.api.Backend;
045    import org.opends.server.api.CertificateMapper;
046    import org.opends.server.config.ConfigException;
047    import org.opends.server.core.DirectoryServer;
048    import org.opends.server.loggers.ErrorLogger;
049    import org.opends.server.loggers.debug.DebugTracer;
050    import org.opends.server.protocols.internal.InternalClientConnection;
051    import org.opends.server.protocols.internal.InternalSearchOperation;
052    import org.opends.server.types.DirectoryException;
053    import org.opends.server.types.AttributeType;
054    import org.opends.server.types.AttributeValue;
055    import org.opends.server.types.ConfigChangeResult;
056    import org.opends.server.types.DebugLogLevel;
057    import org.opends.server.types.DN;
058    import org.opends.server.types.Entry;
059    import org.opends.server.types.IndexType;
060    import org.opends.server.types.InitializationException;
061    import org.opends.server.types.ResultCode;
062    import org.opends.server.types.SearchFilter;
063    import org.opends.server.types.SearchResultEntry;
064    import org.opends.server.types.SearchScope;
065    
066    import static org.opends.messages.ExtensionMessages.*;
067    import static org.opends.server.loggers.debug.DebugLogger.*;
068    import static org.opends.server.util.StaticUtils.*;
069    
070    
071    
072    /**
073     * This class implements a very simple Directory Server certificate mapper that
074     * will map a certificate to a user only if that user's entry contains an
075     * attribute with the fingerprint of the client certificate.  There must be
076     * exactly one matching user entry for the mapping to be successful.
077     */
078    public class FingerprintCertificateMapper
079           extends CertificateMapper<FingerprintCertificateMapperCfg>
080           implements ConfigurationChangeListener<
081                           FingerprintCertificateMapperCfg>
082    {
083      /**
084       * The tracer object for the debug logger.
085       */
086      private static final DebugTracer TRACER = getTracer();
087    
088    
089    
090      // The DN of the configuration entry for this certificate mapper.
091      private DN configEntryDN;
092    
093      // The current configuration for this certificate mapper.
094      private FingerprintCertificateMapperCfg currentConfig;
095    
096      // The algorithm that will be used to generate the fingerprint.
097      private String fingerprintAlgorithm;
098    
099    
100    
101      /**
102       * Creates a new instance of this certificate mapper.  Note that all actual
103       * initialization should be done in the
104       * <CODE>initializeCertificateMapper</CODE> method.
105       */
106      public FingerprintCertificateMapper()
107      {
108        super();
109      }
110    
111    
112    
113      /**
114       * {@inheritDoc}
115       */
116      public void initializeCertificateMapper(
117                       FingerprintCertificateMapperCfg configuration)
118             throws ConfigException, InitializationException
119      {
120        configuration.addFingerprintChangeListener(this);
121    
122        currentConfig = configuration;
123        configEntryDN = configuration.dn();
124    
125    
126        // Get the algorithm that will be used to generate the fingerprint.
127        switch (configuration.getFingerprintAlgorithm())
128        {
129          case MD5:
130            fingerprintAlgorithm = "MD5";
131            break;
132          case SHA1:
133            fingerprintAlgorithm = "SHA1";
134            break;
135        }
136    
137    
138        // Make sure that the fingerprint attribute is configured for equality in
139        // all appropriate backends.
140        Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
141        if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
142        {
143          cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
144        }
145    
146        AttributeType t = configuration.getFingerprintAttribute();
147        for (DN baseDN : cfgBaseDNs)
148        {
149          Backend b = DirectoryServer.getBackend(baseDN);
150          if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY)))
151          {
152            Message message = WARN_SATUACM_ATTR_UNINDEXED.get(
153                configuration.dn().toString(),
154                t.getNameOrOID(), b.getBackendID());
155            ErrorLogger.logError(message);
156          }
157        }
158      }
159    
160    
161    
162      /**
163       * {@inheritDoc}
164       */
165      public void finalizeCertificateMapper()
166      {
167        currentConfig.removeFingerprintChangeListener(this);
168      }
169    
170    
171    
172      /**
173       * {@inheritDoc}
174       */
175      public Entry mapCertificateToUser(Certificate[] certificateChain)
176             throws DirectoryException
177      {
178        FingerprintCertificateMapperCfg config = currentConfig;
179        AttributeType fingerprintAttributeType = config.getFingerprintAttribute();
180        String fingerprintAlgorithm = this.fingerprintAlgorithm;
181    
182        // Make sure that a peer certificate was provided.
183        if ((certificateChain == null) || (certificateChain.length == 0))
184        {
185          Message message = ERR_FCM_NO_PEER_CERTIFICATE.get();
186          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
187        }
188    
189    
190        // Get the first certificate in the chain.  It must be an X.509 certificate.
191        X509Certificate peerCertificate;
192        try
193        {
194          peerCertificate = (X509Certificate) certificateChain[0];
195        }
196        catch (Exception e)
197        {
198          if (debugEnabled())
199          {
200            TRACER.debugCaught(DebugLogLevel.ERROR, e);
201          }
202    
203          Message message = ERR_FCM_PEER_CERT_NOT_X509.get(
204              String.valueOf(certificateChain[0].getType()));
205          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
206        }
207    
208    
209        // Get the signature from the peer certificate and create a digest of it
210        // using the configured algorithm.
211        String fingerprintString;
212        try
213        {
214          MessageDigest digest = MessageDigest.getInstance(fingerprintAlgorithm);
215          byte[] fingerprintBytes = digest.digest(peerCertificate.getEncoded());
216          fingerprintString = bytesToColonDelimitedHex(fingerprintBytes);
217        }
218        catch (Exception e)
219        {
220          if (debugEnabled())
221          {
222            TRACER.debugCaught(DebugLogLevel.ERROR, e);
223          }
224    
225          String peerSubject = peerCertificate.getSubjectX500Principal().getName(
226                                    X500Principal.RFC2253);
227    
228          Message message = ERR_FCM_CANNOT_CALCULATE_FINGERPRINT.get(
229              peerSubject, getExceptionMessage(e));
230          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
231        }
232    
233    
234        // Create the search filter from the fingerprint.
235        AttributeValue value =
236             new AttributeValue(fingerprintAttributeType, fingerprintString);
237        SearchFilter filter =
238             SearchFilter.createEqualityFilter(fingerprintAttributeType, value);
239    
240    
241        // If we have an explicit set of base DNs, then use it.  Otherwise, use the
242        // set of public naming contexts in the server.
243        Collection<DN> baseDNs = config.getUserBaseDN();
244        if ((baseDNs == null) || baseDNs.isEmpty())
245        {
246          baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
247        }
248    
249    
250        // For each base DN, issue an internal search in an attempt to map the
251        // certificate.
252        Entry userEntry = null;
253        InternalClientConnection conn =
254             InternalClientConnection.getRootConnection();
255        for (DN baseDN : baseDNs)
256        {
257          InternalSearchOperation searchOperation =
258               conn.processSearch(baseDN, SearchScope.WHOLE_SUBTREE, filter);
259          for (SearchResultEntry entry : searchOperation.getSearchEntries())
260          {
261            if (userEntry == null)
262            {
263              userEntry = entry;
264            }
265            else
266            {
267              Message message = ERR_FCM_MULTIPLE_MATCHING_ENTRIES.
268                  get(fingerprintString, String.valueOf(userEntry.getDN()),
269                      String.valueOf(entry.getDN()));
270              throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message);
271            }
272          }
273        }
274    
275    
276        // If we've gotten here, then we either found exactly one user entry or we
277        // didn't find any.  Either way, return the entry or null to the caller.
278        return userEntry;
279      }
280    
281    
282    
283      /**
284       * {@inheritDoc}
285       */
286      @Override()
287      public boolean isConfigurationAcceptable(CertificateMapperCfg configuration,
288                                               List<Message> unacceptableReasons)
289      {
290        FingerprintCertificateMapperCfg config =
291             (FingerprintCertificateMapperCfg) configuration;
292        return isConfigurationChangeAcceptable(config, unacceptableReasons);
293      }
294    
295    
296    
297      /**
298       * {@inheritDoc}
299       */
300      public boolean isConfigurationChangeAcceptable(
301                          FingerprintCertificateMapperCfg configuration,
302                          List<Message> unacceptableReasons)
303      {
304        boolean configAcceptable = true;
305    
306        return configAcceptable;
307      }
308    
309    
310    
311      /**
312       * {@inheritDoc}
313       */
314      public ConfigChangeResult applyConfigurationChange(
315                  FingerprintCertificateMapperCfg configuration)
316      {
317        ResultCode        resultCode          = ResultCode.SUCCESS;
318        boolean           adminActionRequired = false;
319        ArrayList<Message> messages            = new ArrayList<Message>();
320    
321    
322        // Get the algorithm that will be used to generate the fingerprint.
323        String newFingerprintAlgorithm = null;
324        switch (configuration.getFingerprintAlgorithm())
325        {
326          case MD5:
327            newFingerprintAlgorithm = "MD5";
328            break;
329          case SHA1:
330            newFingerprintAlgorithm = "SHA1";
331            break;
332        }
333    
334    
335        if (resultCode == ResultCode.SUCCESS)
336        {
337          fingerprintAlgorithm = newFingerprintAlgorithm;
338          currentConfig        = configuration;
339        }
340    
341        // Make sure that the fingerprint attribute is configured for equality in
342        // all appropriate backends.
343        Set<DN> cfgBaseDNs = configuration.getUserBaseDN();
344        if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
345        {
346          cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
347        }
348    
349        AttributeType t = configuration.getFingerprintAttribute();
350        for (DN baseDN : cfgBaseDNs)
351        {
352          Backend b = DirectoryServer.getBackend(baseDN);
353          if ((b != null) && (! b.isIndexed(t, IndexType.EQUALITY)))
354          {
355            Message message = WARN_SATUACM_ATTR_UNINDEXED.get(
356                configuration.dn().toString(),
357                t.getNameOrOID(), b.getBackendID());
358            messages.add(message);
359            ErrorLogger.logError(message);
360          }
361        }
362    
363       return new ConfigChangeResult(resultCode, adminActionRequired, messages);
364      }
365    }
366