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