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 2008-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.server.crypto;
028
029import static org.opends.messages.CoreMessages.*;
030import static org.opends.server.api.plugin.PluginType.*;
031import static org.opends.server.config.ConfigConstants.*;
032import static org.opends.server.core.DirectoryServer.*;
033import static org.opends.server.protocols.internal.InternalClientConnection.*;
034import static org.opends.server.protocols.internal.Requests.*;
035import static org.opends.server.util.ServerConstants.*;
036import static org.opends.server.util.StaticUtils.*;
037
038import java.util.EnumSet;
039import java.util.HashMap;
040import java.util.LinkedHashMap;
041import java.util.List;
042import java.util.Map;
043
044import org.forgerock.i18n.LocalizableMessage;
045import org.forgerock.i18n.slf4j.LocalizedLogger;
046import org.forgerock.opendj.ldap.ResultCode;
047import org.forgerock.opendj.ldap.SearchScope;
048import org.opends.admin.ads.ADSContext;
049import org.opends.server.api.Backend;
050import org.opends.server.api.BackendInitializationListener;
051import org.opends.server.api.plugin.InternalDirectoryServerPlugin;
052import org.opends.server.api.plugin.PluginResult.PostResponse;
053import org.opends.server.config.ConfigConstants;
054import org.opends.server.controls.EntryChangeNotificationControl;
055import org.opends.server.controls.PersistentSearchChangeType;
056import org.opends.server.core.AddOperation;
057import org.opends.server.core.DeleteOperation;
058import org.opends.server.core.DirectoryServer;
059import org.opends.server.protocols.internal.InternalClientConnection;
060import org.opends.server.protocols.internal.InternalSearchOperation;
061import org.opends.server.protocols.internal.SearchRequest;
062import org.opends.server.protocols.ldap.LDAPControl;
063import org.opends.server.types.Attribute;
064import org.opends.server.types.AttributeType;
065import org.opends.server.types.Control;
066import org.opends.server.types.CryptoManagerException;
067import org.opends.server.types.DN;
068import org.opends.server.types.DirectoryException;
069import org.opends.server.types.Entry;
070import org.opends.server.types.InitializationException;
071import org.opends.server.types.ObjectClass;
072import org.opends.server.types.RDN;
073import org.opends.server.types.SearchFilter;
074import org.opends.server.types.SearchResultEntry;
075import org.opends.server.types.operation.PostResponseAddOperation;
076import org.opends.server.types.operation.PostResponseDeleteOperation;
077import org.opends.server.types.operation.PostResponseModifyOperation;
078
079/**
080 * This class defines an object that synchronizes certificates from the admin
081 * data branch into the trust store backend, and synchronizes secret-key entries
082 * from the admin data branch to the crypto manager secret-key cache.
083 */
084public class CryptoManagerSync extends InternalDirectoryServerPlugin
085     implements BackendInitializationListener
086{
087  /** The debug log tracer for this object. */
088  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
089
090  /** The DN of the administration suffix. */
091  private DN adminSuffixDN;
092
093  /** The DN of the instance keys container within the admin suffix. */
094  private DN instanceKeysDN;
095
096  /** The DN of the secret keys container within the admin suffix. */
097  private DN secretKeysDN;
098
099  /** The DN of the trust store root. */
100  private DN trustStoreRootDN;
101
102  /** The attribute type that is used to specify a server instance certificate. */
103  private final AttributeType attrCert;
104
105  /** The attribute type that holds a server certificate identifier. */
106  private final AttributeType attrAlias;
107
108  /** The attribute type that holds the time a key was compromised. */
109  private final AttributeType attrCompromisedTime;
110
111  /** A filter on object class to select key entries. */
112  private SearchFilter keySearchFilter;
113
114  /** The instance key objectclass. */
115  private final ObjectClass ocInstanceKey;
116
117  /** The cipher key objectclass. */
118  private final ObjectClass ocCipherKey;
119
120  /** The mac key objectclass. */
121  private final ObjectClass ocMacKey;
122
123  /** Dummy configuration DN. */
124  private static final String CONFIG_DN = "cn=Crypto Manager Sync,cn=config";
125
126  /**
127   * Creates a new instance of this trust store synchronization thread.
128   *
129   * @throws InitializationException in case an exception occurs during
130   * initialization, such as a failure to publish the instance-key-pair
131   * public-key-certificate in ADS.
132   */
133  public CryptoManagerSync() throws InitializationException
134  {
135    super(toDN(CONFIG_DN), EnumSet.of(
136        // No implementation required for modify_dn operations
137        // FIXME: Technically it is possible to perform a subtree modDN
138        // in this case however such subtree modDN would essentially be
139        // moving configuration branches which should not happen.
140        POST_RESPONSE_ADD, POST_RESPONSE_MODIFY, POST_RESPONSE_DELETE),
141        true);
142    try {
143      CryptoManagerImpl.publishInstanceKeyEntryInADS();
144    }
145    catch (CryptoManagerException ex) {
146      throw new InitializationException(ex.getMessageObject());
147    }
148    DirectoryServer.registerBackendInitializationListener(this);
149
150    try
151    {
152      adminSuffixDN = DN.valueOf(ADSContext.getAdministrationSuffixDN());
153      instanceKeysDN = adminSuffixDN.child(DN.valueOf("cn=instance keys"));
154      secretKeysDN = adminSuffixDN.child(DN.valueOf("cn=secret keys"));
155      trustStoreRootDN = DN.valueOf(ConfigConstants.DN_TRUST_STORE_ROOT);
156      keySearchFilter =
157           SearchFilter.createFilterFromString("(|" +
158                "(objectclass=" + OC_CRYPTO_INSTANCE_KEY + ")" +
159                "(objectclass=" + OC_CRYPTO_CIPHER_KEY + ")" +
160                "(objectclass=" + OC_CRYPTO_MAC_KEY + ")" +
161                ")");
162    }
163    catch (DirectoryException e)
164    {
165    }
166
167    ocInstanceKey = DirectoryServer.getObjectClass(OC_CRYPTO_INSTANCE_KEY, true);
168    ocCipherKey = DirectoryServer.getObjectClass(OC_CRYPTO_CIPHER_KEY, true);
169    ocMacKey = DirectoryServer.getObjectClass(OC_CRYPTO_MAC_KEY, true);
170
171    attrCert = getAttributeTypeOrDefault(ATTR_CRYPTO_PUBLIC_KEY_CERTIFICATE);
172    attrAlias = getAttributeTypeOrDefault(ATTR_CRYPTO_KEY_ID);
173    attrCompromisedTime = getAttributeTypeOrDefault(ATTR_CRYPTO_KEY_COMPROMISED_TIME);
174
175    if (DirectoryServer.getBackendWithBaseDN(adminSuffixDN) != null)
176    {
177      searchAdminSuffix();
178    }
179
180    DirectoryServer.registerInternalPlugin(this);
181  }
182
183  private static DN toDN(final String dn) throws InitializationException
184  {
185    try
186    {
187      return DN.valueOf(dn);
188    }
189    catch (DirectoryException e)
190    {
191      throw new RuntimeException(e);
192    }
193  }
194
195
196  private void searchAdminSuffix()
197  {
198    SearchRequest request = newSearchRequest(adminSuffixDN, SearchScope.WHOLE_SUBTREE, keySearchFilter);
199    InternalSearchOperation searchOperation = getRootConnection().processSearch(request);
200    ResultCode resultCode = searchOperation.getResultCode();
201    if (resultCode != ResultCode.SUCCESS)
202    {
203      logger.debug(INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED, adminSuffixDN,
204                searchOperation.getErrorMessage());
205    }
206
207    for (SearchResultEntry searchEntry : searchOperation.getSearchEntries())
208    {
209      try
210      {
211        handleInternalSearchEntry(searchEntry);
212      }
213      catch (DirectoryException e)
214      {
215        logger.traceException(e);
216
217        logger.error(ERR_TRUSTSTORESYNC_EXCEPTION, stackTraceToSingleLineString(e));
218      }
219    }
220  }
221
222
223  /** {@inheritDoc} */
224  @Override
225  public void performBackendInitializationProcessing(Backend<?> backend)
226  {
227    DN[] baseDNs = backend.getBaseDNs();
228    if (baseDNs != null)
229    {
230      for (DN baseDN : baseDNs)
231      {
232        if (baseDN.equals(adminSuffixDN))
233        {
234          searchAdminSuffix();
235        }
236      }
237    }
238  }
239
240  /** {@inheritDoc} */
241  @Override
242  public void performBackendFinalizationProcessing(Backend<?> backend)
243  {
244    // No implementation required.
245  }
246
247  private void handleInternalSearchEntry(SearchResultEntry searchEntry)
248       throws DirectoryException
249  {
250    if (searchEntry.hasObjectClass(ocInstanceKey))
251    {
252      handleInstanceKeySearchEntry(searchEntry);
253    }
254    else
255    {
256      try
257      {
258        if (searchEntry.hasObjectClass(ocCipherKey))
259        {
260          DirectoryServer.getCryptoManager().importCipherKeyEntry(searchEntry);
261        }
262        else if (searchEntry.hasObjectClass(ocMacKey))
263        {
264          DirectoryServer.getCryptoManager().importMacKeyEntry(searchEntry);
265        }
266      }
267      catch (CryptoManagerException e)
268      {
269        throw new DirectoryException(
270             DirectoryServer.getServerErrorResultCode(), e);
271      }
272    }
273  }
274
275
276  private void handleInstanceKeySearchEntry(SearchResultEntry searchEntry)
277       throws DirectoryException
278  {
279    RDN srcRDN = searchEntry.getName().rdn();
280
281    // Only process the entry if it has the expected form of RDN.
282    if (!srcRDN.isMultiValued() &&
283         srcRDN.getAttributeType(0).equals(attrAlias))
284    {
285      DN dstDN = trustStoreRootDN.child(srcRDN);
286
287      // Extract any change notification control.
288      EntryChangeNotificationControl ecn = null;
289      List<Control> controls = searchEntry.getControls();
290      try
291      {
292        for (Control c : controls)
293        {
294          if (OID_ENTRY_CHANGE_NOTIFICATION.equals(c.getOID()))
295          {
296            if (c instanceof LDAPControl)
297            {
298              ecn = EntryChangeNotificationControl.DECODER.decode(c
299                  .isCritical(), ((LDAPControl) c).getValue());
300            }
301            else
302            {
303              ecn = (EntryChangeNotificationControl)c;
304            }
305          }
306        }
307      }
308      catch (DirectoryException e)
309      {
310        // ignore
311      }
312
313      // Get any existing local trust store entry.
314      Entry dstEntry = DirectoryServer.getEntry(dstDN);
315
316      if (ecn != null &&
317           ecn.getChangeType() == PersistentSearchChangeType.DELETE)
318      {
319        // entry was deleted so remove it from the local trust store
320        if (dstEntry != null)
321        {
322          deleteEntry(dstDN);
323        }
324      }
325      else if (searchEntry.hasAttribute(attrCompromisedTime))
326      {
327        // key was compromised so remove it from the local trust store
328        if (dstEntry != null)
329        {
330          deleteEntry(dstDN);
331        }
332      }
333      else if (dstEntry == null)
334      {
335        // The entry was added
336        addEntry(searchEntry, dstDN);
337      }
338      else
339      {
340        // The entry was modified
341        modifyEntry(searchEntry, dstEntry);
342      }
343    }
344  }
345
346
347  /**
348   * Modify an entry in the local trust store if it differs from an entry in
349   * the ADS branch.
350   * @param srcEntry The instance key entry in the ADS branch.
351   * @param dstEntry The local trust store entry.
352   */
353  private void modifyEntry(Entry srcEntry, Entry dstEntry)
354  {
355    List<Attribute> srcList = srcEntry.getAttribute(attrCert);
356    List<Attribute> dstList = dstEntry.getAttribute(attrCert);
357
358    // Check for changes to the certificate value.
359    boolean differ = false;
360    if (srcList == null)
361    {
362      if (dstList != null)
363      {
364        differ = true;
365      }
366    }
367    else if (dstList == null
368        || srcList.size() != dstList.size()
369        || !srcList.equals(dstList))
370    {
371      differ = true;
372    }
373
374    if (differ)
375    {
376      // The trust store backend does not implement modify so we need to
377      // delete then add.
378      DN dstDN = dstEntry.getName();
379      deleteEntry(dstDN);
380      addEntry(srcEntry, dstDN);
381    }
382  }
383
384
385  /**
386   * Delete an entry from the local trust store.
387   * @param dstDN The DN of the entry to be deleted in the local trust store.
388   */
389  private static void deleteEntry(DN dstDN)
390  {
391    InternalClientConnection conn =
392         InternalClientConnection.getRootConnection();
393
394    DeleteOperation delOperation = conn.processDelete(dstDN);
395
396    if (delOperation.getResultCode() != ResultCode.SUCCESS)
397    {
398      logger.debug(INFO_TRUSTSTORESYNC_DELETE_FAILED, dstDN, delOperation.getErrorMessage());
399    }
400  }
401
402
403  /**
404   * Add an entry to the local trust store.
405   * @param srcEntry The instance key entry in the ADS branch.
406   * @param dstDN The DN of the entry to be added in the local trust store.
407   */
408  private void addEntry(Entry srcEntry, DN dstDN)
409  {
410    Map<ObjectClass, String> ocMap = new LinkedHashMap<>(2);
411    ocMap.put(DirectoryServer.getTopObjectClass(), OC_TOP);
412    ocMap.put(ocInstanceKey, OC_CRYPTO_INSTANCE_KEY);
413
414    Map<AttributeType, List<Attribute>> userAttrs = new HashMap<>();
415
416    List<Attribute> attrList;
417    attrList = srcEntry.getAttribute(attrAlias);
418    if (attrList != null)
419    {
420      userAttrs.put(attrAlias, attrList);
421    }
422    attrList = srcEntry.getAttribute(attrCert);
423    if (attrList != null)
424    {
425      userAttrs.put(attrCert, attrList);
426    }
427
428    Entry addEntry = new Entry(dstDN, ocMap, userAttrs, null);
429
430    InternalClientConnection conn =
431         InternalClientConnection.getRootConnection();
432
433    AddOperation addOperation = conn.processAdd(addEntry);
434    if (addOperation.getResultCode() != ResultCode.SUCCESS)
435    {
436      logger.debug(INFO_TRUSTSTORESYNC_ADD_FAILED, dstDN, addOperation.getErrorMessage());
437    }
438  }
439
440  /** {@inheritDoc} */
441  @Override
442  public PostResponse doPostResponse(PostResponseAddOperation op)
443  {
444    if (op.getResultCode() != ResultCode.SUCCESS)
445    {
446      return PostResponse.continueOperationProcessing();
447    }
448
449    final Entry entry = op.getEntryToAdd();
450    final DN entryDN = op.getEntryDN();
451    if (entryDN.isDescendantOf(instanceKeysDN))
452    {
453      handleInstanceKeyAddOperation(entry);
454    }
455    else if (entryDN.isDescendantOf(secretKeysDN))
456    {
457      try
458      {
459        if (entry.hasObjectClass(ocCipherKey))
460        {
461          DirectoryServer.getCryptoManager().importCipherKeyEntry(entry);
462        }
463        else if (entry.hasObjectClass(ocMacKey))
464        {
465          DirectoryServer.getCryptoManager().importMacKeyEntry(entry);
466        }
467      }
468      catch (CryptoManagerException e)
469      {
470        logger.error(LocalizableMessage.raw(
471            "Failed to import key entry: %s", e.getMessage()));
472      }
473    }
474    return PostResponse.continueOperationProcessing();
475  }
476
477
478  private void handleInstanceKeyAddOperation(Entry entry)
479  {
480    RDN srcRDN = entry.getName().rdn();
481
482    // Only process the entry if it has the expected form of RDN.
483    if (!srcRDN.isMultiValued() &&
484         srcRDN.getAttributeType(0).equals(attrAlias))
485    {
486      DN dstDN = trustStoreRootDN.child(srcRDN);
487
488      if (!entry.hasAttribute(attrCompromisedTime))
489      {
490        addEntry(entry, dstDN);
491      }
492    }
493  }
494
495  /** {@inheritDoc} */
496  @Override
497  public PostResponse doPostResponse(PostResponseDeleteOperation op)
498  {
499    if (op.getResultCode() != ResultCode.SUCCESS
500        || !op.getEntryDN().isDescendantOf(instanceKeysDN))
501    {
502      return PostResponse.continueOperationProcessing();
503    }
504
505    RDN srcRDN = op.getEntryToDelete().getName().rdn();
506
507    // Only process the entry if it has the expected form of RDN.
508    // FIXME: Technically it is possible to perform a subtree in
509    // this case however such subtree delete would essentially be
510    // removing configuration branches which should not happen.
511    if (!srcRDN.isMultiValued() &&
512         srcRDN.getAttributeType(0).equals(attrAlias))
513    {
514      DN destDN = trustStoreRootDN.child(srcRDN);
515      deleteEntry(destDN);
516    }
517    return PostResponse.continueOperationProcessing();
518  }
519
520  /** {@inheritDoc} */
521  @Override
522  public PostResponse doPostResponse(PostResponseModifyOperation op)
523  {
524    if (op.getResultCode() != ResultCode.SUCCESS)
525    {
526      return PostResponse.continueOperationProcessing();
527    }
528
529    final Entry newEntry = op.getModifiedEntry();
530    final DN entryDN = op.getEntryDN();
531    if (entryDN.isDescendantOf(instanceKeysDN))
532    {
533      handleInstanceKeyModifyOperation(newEntry);
534    }
535    else if (entryDN.isDescendantOf(secretKeysDN))
536    {
537      try
538      {
539        if (newEntry.hasObjectClass(ocCipherKey))
540        {
541          DirectoryServer.getCryptoManager().importCipherKeyEntry(newEntry);
542        }
543        else if (newEntry.hasObjectClass(ocMacKey))
544        {
545          DirectoryServer.getCryptoManager().importMacKeyEntry(newEntry);
546        }
547      }
548      catch (CryptoManagerException e)
549      {
550        logger.error(LocalizableMessage.raw(
551            "Failed to import modified key entry: %s", e.getMessage()));
552      }
553    }
554    return PostResponse.continueOperationProcessing();
555  }
556
557  private void handleInstanceKeyModifyOperation(Entry newEntry)
558  {
559    RDN srcRDN = newEntry.getName().rdn();
560
561    // Only process the entry if it has the expected form of RDN.
562    if (!srcRDN.isMultiValued() &&
563         srcRDN.getAttributeType(0).equals(attrAlias))
564    {
565      DN dstDN = trustStoreRootDN.child(srcRDN);
566
567      // Get any existing local trust store entry.
568      Entry dstEntry = null;
569      try
570      {
571        dstEntry = DirectoryServer.getEntry(dstDN);
572      }
573      catch (DirectoryException e)
574      {
575        // ignore
576      }
577
578      if (newEntry.hasAttribute(attrCompromisedTime))
579      {
580        // The key was compromised so we should remove it from the local
581        // trust store.
582        if (dstEntry != null)
583        {
584          deleteEntry(dstDN);
585        }
586      }
587      else if (dstEntry == null)
588      {
589        addEntry(newEntry, dstDN);
590      }
591      else
592      {
593        modifyEntry(newEntry, dstEntry);
594      }
595    }
596  }
597}