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 performBackendPreInitializationProcessing(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 performBackendPostFinalizationProcessing(Backend<?> backend)
243  {
244    // No implementation required.
245  }
246
247  @Override
248  public void performBackendPostInitializationProcessing(Backend<?> backend) {
249    // Nothing to do.
250  }
251
252  @Override
253  public void performBackendPreFinalizationProcessing(Backend<?> backend) {
254    // Nothing to do.
255  }
256
257  private void handleInternalSearchEntry(SearchResultEntry searchEntry)
258       throws DirectoryException
259  {
260    if (searchEntry.hasObjectClass(ocInstanceKey))
261    {
262      handleInstanceKeySearchEntry(searchEntry);
263    }
264    else
265    {
266      try
267      {
268        if (searchEntry.hasObjectClass(ocCipherKey))
269        {
270          DirectoryServer.getCryptoManager().importCipherKeyEntry(searchEntry);
271        }
272        else if (searchEntry.hasObjectClass(ocMacKey))
273        {
274          DirectoryServer.getCryptoManager().importMacKeyEntry(searchEntry);
275        }
276      }
277      catch (CryptoManagerException e)
278      {
279        throw new DirectoryException(
280             DirectoryServer.getServerErrorResultCode(), e);
281      }
282    }
283  }
284
285
286  private void handleInstanceKeySearchEntry(SearchResultEntry searchEntry)
287       throws DirectoryException
288  {
289    RDN srcRDN = searchEntry.getName().rdn();
290
291    // Only process the entry if it has the expected form of RDN.
292    if (!srcRDN.isMultiValued() &&
293         srcRDN.getAttributeType(0).equals(attrAlias))
294    {
295      DN dstDN = trustStoreRootDN.child(srcRDN);
296
297      // Extract any change notification control.
298      EntryChangeNotificationControl ecn = null;
299      List<Control> controls = searchEntry.getControls();
300      try
301      {
302        for (Control c : controls)
303        {
304          if (OID_ENTRY_CHANGE_NOTIFICATION.equals(c.getOID()))
305          {
306            if (c instanceof LDAPControl)
307            {
308              ecn = EntryChangeNotificationControl.DECODER.decode(c
309                  .isCritical(), ((LDAPControl) c).getValue());
310            }
311            else
312            {
313              ecn = (EntryChangeNotificationControl)c;
314            }
315          }
316        }
317      }
318      catch (DirectoryException e)
319      {
320        // ignore
321      }
322
323      // Get any existing local trust store entry.
324      Entry dstEntry = DirectoryServer.getEntry(dstDN);
325
326      if (ecn != null &&
327           ecn.getChangeType() == PersistentSearchChangeType.DELETE)
328      {
329        // entry was deleted so remove it from the local trust store
330        if (dstEntry != null)
331        {
332          deleteEntry(dstDN);
333        }
334      }
335      else if (searchEntry.hasAttribute(attrCompromisedTime))
336      {
337        // key was compromised so remove it from the local trust store
338        if (dstEntry != null)
339        {
340          deleteEntry(dstDN);
341        }
342      }
343      else if (dstEntry == null)
344      {
345        // The entry was added
346        addEntry(searchEntry, dstDN);
347      }
348      else
349      {
350        // The entry was modified
351        modifyEntry(searchEntry, dstEntry);
352      }
353    }
354  }
355
356
357  /**
358   * Modify an entry in the local trust store if it differs from an entry in
359   * the ADS branch.
360   * @param srcEntry The instance key entry in the ADS branch.
361   * @param dstEntry The local trust store entry.
362   */
363  private void modifyEntry(Entry srcEntry, Entry dstEntry)
364  {
365    List<Attribute> srcList = srcEntry.getAttribute(attrCert);
366    List<Attribute> dstList = dstEntry.getAttribute(attrCert);
367
368    // Check for changes to the certificate value.
369    boolean differ = false;
370    if (srcList == null)
371    {
372      if (dstList != null)
373      {
374        differ = true;
375      }
376    }
377    else if (dstList == null
378        || srcList.size() != dstList.size()
379        || !srcList.equals(dstList))
380    {
381      differ = true;
382    }
383
384    if (differ)
385    {
386      // The trust store backend does not implement modify so we need to
387      // delete then add.
388      DN dstDN = dstEntry.getName();
389      deleteEntry(dstDN);
390      addEntry(srcEntry, dstDN);
391    }
392  }
393
394
395  /**
396   * Delete an entry from the local trust store.
397   * @param dstDN The DN of the entry to be deleted in the local trust store.
398   */
399  private static void deleteEntry(DN dstDN)
400  {
401    InternalClientConnection conn =
402         InternalClientConnection.getRootConnection();
403
404    DeleteOperation delOperation = conn.processDelete(dstDN);
405
406    if (delOperation.getResultCode() != ResultCode.SUCCESS)
407    {
408      logger.debug(INFO_TRUSTSTORESYNC_DELETE_FAILED, dstDN, delOperation.getErrorMessage());
409    }
410  }
411
412
413  /**
414   * Add an entry to the local trust store.
415   * @param srcEntry The instance key entry in the ADS branch.
416   * @param dstDN The DN of the entry to be added in the local trust store.
417   */
418  private void addEntry(Entry srcEntry, DN dstDN)
419  {
420    Map<ObjectClass, String> ocMap = new LinkedHashMap<>(2);
421    ocMap.put(DirectoryServer.getTopObjectClass(), OC_TOP);
422    ocMap.put(ocInstanceKey, OC_CRYPTO_INSTANCE_KEY);
423
424    Map<AttributeType, List<Attribute>> userAttrs = new HashMap<>();
425
426    List<Attribute> attrList;
427    attrList = srcEntry.getAttribute(attrAlias);
428    if (attrList != null)
429    {
430      userAttrs.put(attrAlias, attrList);
431    }
432    attrList = srcEntry.getAttribute(attrCert);
433    if (attrList != null)
434    {
435      userAttrs.put(attrCert, attrList);
436    }
437
438    Entry addEntry = new Entry(dstDN, ocMap, userAttrs, null);
439
440    InternalClientConnection conn =
441         InternalClientConnection.getRootConnection();
442
443    AddOperation addOperation = conn.processAdd(addEntry);
444    if (addOperation.getResultCode() != ResultCode.SUCCESS)
445    {
446      logger.debug(INFO_TRUSTSTORESYNC_ADD_FAILED, dstDN, addOperation.getErrorMessage());
447    }
448  }
449
450  /** {@inheritDoc} */
451  @Override
452  public PostResponse doPostResponse(PostResponseAddOperation op)
453  {
454    if (op.getResultCode() != ResultCode.SUCCESS)
455    {
456      return PostResponse.continueOperationProcessing();
457    }
458
459    final Entry entry = op.getEntryToAdd();
460    final DN entryDN = op.getEntryDN();
461    if (entryDN.isDescendantOf(instanceKeysDN))
462    {
463      handleInstanceKeyAddOperation(entry);
464    }
465    else if (entryDN.isDescendantOf(secretKeysDN))
466    {
467      try
468      {
469        if (entry.hasObjectClass(ocCipherKey))
470        {
471          DirectoryServer.getCryptoManager().importCipherKeyEntry(entry);
472        }
473        else if (entry.hasObjectClass(ocMacKey))
474        {
475          DirectoryServer.getCryptoManager().importMacKeyEntry(entry);
476        }
477      }
478      catch (CryptoManagerException e)
479      {
480        logger.error(LocalizableMessage.raw(
481            "Failed to import key entry: %s", e.getMessage()));
482      }
483    }
484    return PostResponse.continueOperationProcessing();
485  }
486
487
488  private void handleInstanceKeyAddOperation(Entry entry)
489  {
490    RDN srcRDN = entry.getName().rdn();
491
492    // Only process the entry if it has the expected form of RDN.
493    if (!srcRDN.isMultiValued() &&
494         srcRDN.getAttributeType(0).equals(attrAlias))
495    {
496      DN dstDN = trustStoreRootDN.child(srcRDN);
497
498      if (!entry.hasAttribute(attrCompromisedTime))
499      {
500        addEntry(entry, dstDN);
501      }
502    }
503  }
504
505  /** {@inheritDoc} */
506  @Override
507  public PostResponse doPostResponse(PostResponseDeleteOperation op)
508  {
509    if (op.getResultCode() != ResultCode.SUCCESS
510        || !op.getEntryDN().isDescendantOf(instanceKeysDN))
511    {
512      return PostResponse.continueOperationProcessing();
513    }
514
515    RDN srcRDN = op.getEntryToDelete().getName().rdn();
516
517    // Only process the entry if it has the expected form of RDN.
518    // FIXME: Technically it is possible to perform a subtree in
519    // this case however such subtree delete would essentially be
520    // removing configuration branches which should not happen.
521    if (!srcRDN.isMultiValued() &&
522         srcRDN.getAttributeType(0).equals(attrAlias))
523    {
524      DN destDN = trustStoreRootDN.child(srcRDN);
525      deleteEntry(destDN);
526    }
527    return PostResponse.continueOperationProcessing();
528  }
529
530  /** {@inheritDoc} */
531  @Override
532  public PostResponse doPostResponse(PostResponseModifyOperation op)
533  {
534    if (op.getResultCode() != ResultCode.SUCCESS)
535    {
536      return PostResponse.continueOperationProcessing();
537    }
538
539    final Entry newEntry = op.getModifiedEntry();
540    final DN entryDN = op.getEntryDN();
541    if (entryDN.isDescendantOf(instanceKeysDN))
542    {
543      handleInstanceKeyModifyOperation(newEntry);
544    }
545    else if (entryDN.isDescendantOf(secretKeysDN))
546    {
547      try
548      {
549        if (newEntry.hasObjectClass(ocCipherKey))
550        {
551          DirectoryServer.getCryptoManager().importCipherKeyEntry(newEntry);
552        }
553        else if (newEntry.hasObjectClass(ocMacKey))
554        {
555          DirectoryServer.getCryptoManager().importMacKeyEntry(newEntry);
556        }
557      }
558      catch (CryptoManagerException e)
559      {
560        logger.error(LocalizableMessage.raw(
561            "Failed to import modified key entry: %s", e.getMessage()));
562      }
563    }
564    return PostResponse.continueOperationProcessing();
565  }
566
567  private void handleInstanceKeyModifyOperation(Entry newEntry)
568  {
569    RDN srcRDN = newEntry.getName().rdn();
570
571    // Only process the entry if it has the expected form of RDN.
572    if (!srcRDN.isMultiValued() &&
573         srcRDN.getAttributeType(0).equals(attrAlias))
574    {
575      DN dstDN = trustStoreRootDN.child(srcRDN);
576
577      // Get any existing local trust store entry.
578      Entry dstEntry = null;
579      try
580      {
581        dstEntry = DirectoryServer.getEntry(dstDN);
582      }
583      catch (DirectoryException e)
584      {
585        // ignore
586      }
587
588      if (newEntry.hasAttribute(attrCompromisedTime))
589      {
590        // The key was compromised so we should remove it from the local
591        // trust store.
592        if (dstEntry != null)
593        {
594          deleteEntry(dstDN);
595        }
596      }
597      else if (dstEntry == null)
598      {
599        addEntry(newEntry, dstDN);
600      }
601      else
602      {
603        modifyEntry(newEntry, dstEntry);
604      }
605    }
606  }
607}