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