/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2014-2015 ForgeRock AS */ package org.opends.server.crypto; import static org.opends.messages.CoreMessages.*; import static org.opends.server.api.plugin.PluginType.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.core.DirectoryServer.*; import static org.opends.server.protocols.internal.InternalClientConnection.*; import static org.opends.server.protocols.internal.Requests.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.opends.admin.ads.ADSContext; import org.opends.server.api.Backend; import org.opends.server.api.BackendInitializationListener; import org.opends.server.api.plugin.InternalDirectoryServerPlugin; import org.opends.server.api.plugin.PluginResult.PostResponse; import org.opends.server.config.ConfigConstants; import org.opends.server.controls.EntryChangeNotificationControl; import org.opends.server.controls.PersistentSearchChangeType; import org.opends.server.core.AddOperation; import org.opends.server.core.DeleteOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.protocols.internal.SearchRequest; import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.Control; import org.opends.server.types.CryptoManagerException; import org.opends.server.types.DN; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.InitializationException; import org.opends.server.types.ObjectClass; import org.opends.server.types.RDN; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.operation.PostResponseAddOperation; import org.opends.server.types.operation.PostResponseDeleteOperation; import org.opends.server.types.operation.PostResponseModifyOperation; /** * This class defines an object that synchronizes certificates from the admin * data branch into the trust store backend, and synchronizes secret-key entries * from the admin data branch to the crypto manager secret-key cache. */ public class CryptoManagerSync extends InternalDirectoryServerPlugin implements BackendInitializationListener { /** The debug log tracer for this object. */ private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The DN of the administration suffix. */ private DN adminSuffixDN; /** The DN of the instance keys container within the admin suffix. */ private DN instanceKeysDN; /** The DN of the secret keys container within the admin suffix. */ private DN secretKeysDN; /** The DN of the trust store root. */ private DN trustStoreRootDN; /** The attribute type that is used to specify a server instance certificate. */ private final AttributeType attrCert; /** The attribute type that holds a server certificate identifier. */ private final AttributeType attrAlias; /** The attribute type that holds the time a key was compromised. */ private final AttributeType attrCompromisedTime; /** A filter on object class to select key entries. */ private SearchFilter keySearchFilter; /** The instance key objectclass. */ private final ObjectClass ocInstanceKey; /** The cipher key objectclass. */ private final ObjectClass ocCipherKey; /** The mac key objectclass. */ private final ObjectClass ocMacKey; /** Dummy configuration DN. */ private static final String CONFIG_DN = "cn=Crypto Manager Sync,cn=config"; /** * Creates a new instance of this trust store synchronization thread. * * @throws InitializationException in case an exception occurs during * initialization, such as a failure to publish the instance-key-pair * public-key-certificate in ADS. */ public CryptoManagerSync() throws InitializationException { super(toDN(CONFIG_DN), EnumSet.of( // No implementation required for modify_dn operations // FIXME: Technically it is possible to perform a subtree modDN // in this case however such subtree modDN would essentially be // moving configuration branches which should not happen. POST_RESPONSE_ADD, POST_RESPONSE_MODIFY, POST_RESPONSE_DELETE), true); try { CryptoManagerImpl.publishInstanceKeyEntryInADS(); } catch (CryptoManagerException ex) { throw new InitializationException(ex.getMessageObject()); } DirectoryServer.registerBackendInitializationListener(this); try { adminSuffixDN = DN.valueOf(ADSContext.getAdministrationSuffixDN()); instanceKeysDN = adminSuffixDN.child(DN.valueOf("cn=instance keys")); secretKeysDN = adminSuffixDN.child(DN.valueOf("cn=secret keys")); trustStoreRootDN = DN.valueOf(ConfigConstants.DN_TRUST_STORE_ROOT); keySearchFilter = SearchFilter.createFilterFromString("(|" + "(objectclass=" + OC_CRYPTO_INSTANCE_KEY + ")" + "(objectclass=" + OC_CRYPTO_CIPHER_KEY + ")" + "(objectclass=" + OC_CRYPTO_MAC_KEY + ")" + ")"); } catch (DirectoryException e) { } ocInstanceKey = DirectoryServer.getObjectClass(OC_CRYPTO_INSTANCE_KEY, true); ocCipherKey = DirectoryServer.getObjectClass(OC_CRYPTO_CIPHER_KEY, true); ocMacKey = DirectoryServer.getObjectClass(OC_CRYPTO_MAC_KEY, true); attrCert = getAttributeTypeOrDefault(ATTR_CRYPTO_PUBLIC_KEY_CERTIFICATE); attrAlias = getAttributeTypeOrDefault(ATTR_CRYPTO_KEY_ID); attrCompromisedTime = getAttributeTypeOrDefault(ATTR_CRYPTO_KEY_COMPROMISED_TIME); if (DirectoryServer.getBackendWithBaseDN(adminSuffixDN) != null) { searchAdminSuffix(); } DirectoryServer.registerInternalPlugin(this); } private static DN toDN(final String dn) throws InitializationException { try { return DN.valueOf(dn); } catch (DirectoryException e) { throw new RuntimeException(e); } } private void searchAdminSuffix() { SearchRequest request = newSearchRequest(adminSuffixDN, SearchScope.WHOLE_SUBTREE, keySearchFilter); InternalSearchOperation searchOperation = getRootConnection().processSearch(request); ResultCode resultCode = searchOperation.getResultCode(); if (resultCode != ResultCode.SUCCESS) { logger.debug(INFO_TRUSTSTORESYNC_ADMIN_SUFFIX_SEARCH_FAILED, adminSuffixDN, searchOperation.getErrorMessage()); } for (SearchResultEntry searchEntry : searchOperation.getSearchEntries()) { try { handleInternalSearchEntry(searchEntry); } catch (DirectoryException e) { logger.traceException(e); logger.error(ERR_TRUSTSTORESYNC_EXCEPTION, stackTraceToSingleLineString(e)); } } } /** {@inheritDoc} */ @Override public void performBackendPreInitializationProcessing(Backend<?> backend) { DN[] baseDNs = backend.getBaseDNs(); if (baseDNs != null) { for (DN baseDN : baseDNs) { if (baseDN.equals(adminSuffixDN)) { searchAdminSuffix(); } } } } /** {@inheritDoc} */ @Override public void performBackendPostFinalizationProcessing(Backend<?> backend) { // No implementation required. } @Override public void performBackendPostInitializationProcessing(Backend<?> backend) { // Nothing to do. } @Override public void performBackendPreFinalizationProcessing(Backend<?> backend) { // Nothing to do. } private void handleInternalSearchEntry(SearchResultEntry searchEntry) throws DirectoryException { if (searchEntry.hasObjectClass(ocInstanceKey)) { handleInstanceKeySearchEntry(searchEntry); } else { try { if (searchEntry.hasObjectClass(ocCipherKey)) { DirectoryServer.getCryptoManager().importCipherKeyEntry(searchEntry); } else if (searchEntry.hasObjectClass(ocMacKey)) { DirectoryServer.getCryptoManager().importMacKeyEntry(searchEntry); } } catch (CryptoManagerException e) { throw new DirectoryException( DirectoryServer.getServerErrorResultCode(), e); } } } private void handleInstanceKeySearchEntry(SearchResultEntry searchEntry) throws DirectoryException { RDN srcRDN = searchEntry.getName().rdn(); // Only process the entry if it has the expected form of RDN. if (!srcRDN.isMultiValued() && srcRDN.getAttributeType(0).equals(attrAlias)) { DN dstDN = trustStoreRootDN.child(srcRDN); // Extract any change notification control. EntryChangeNotificationControl ecn = null; List<Control> controls = searchEntry.getControls(); try { for (Control c : controls) { if (OID_ENTRY_CHANGE_NOTIFICATION.equals(c.getOID())) { if (c instanceof LDAPControl) { ecn = EntryChangeNotificationControl.DECODER.decode(c .isCritical(), ((LDAPControl) c).getValue()); } else { ecn = (EntryChangeNotificationControl)c; } } } } catch (DirectoryException e) { // ignore } // Get any existing local trust store entry. Entry dstEntry = DirectoryServer.getEntry(dstDN); if (ecn != null && ecn.getChangeType() == PersistentSearchChangeType.DELETE) { // entry was deleted so remove it from the local trust store if (dstEntry != null) { deleteEntry(dstDN); } } else if (searchEntry.hasAttribute(attrCompromisedTime)) { // key was compromised so remove it from the local trust store if (dstEntry != null) { deleteEntry(dstDN); } } else if (dstEntry == null) { // The entry was added addEntry(searchEntry, dstDN); } else { // The entry was modified modifyEntry(searchEntry, dstEntry); } } } /** * Modify an entry in the local trust store if it differs from an entry in * the ADS branch. * @param srcEntry The instance key entry in the ADS branch. * @param dstEntry The local trust store entry. */ private void modifyEntry(Entry srcEntry, Entry dstEntry) { List<Attribute> srcList = srcEntry.getAttribute(attrCert); List<Attribute> dstList = dstEntry.getAttribute(attrCert); // Check for changes to the certificate value. boolean differ = false; if (srcList == null) { if (dstList != null) { differ = true; } } else if (dstList == null || srcList.size() != dstList.size() || !srcList.equals(dstList)) { differ = true; } if (differ) { // The trust store backend does not implement modify so we need to // delete then add. DN dstDN = dstEntry.getName(); deleteEntry(dstDN); addEntry(srcEntry, dstDN); } } /** * Delete an entry from the local trust store. * @param dstDN The DN of the entry to be deleted in the local trust store. */ private static void deleteEntry(DN dstDN) { InternalClientConnection conn = InternalClientConnection.getRootConnection(); DeleteOperation delOperation = conn.processDelete(dstDN); if (delOperation.getResultCode() != ResultCode.SUCCESS) { logger.debug(INFO_TRUSTSTORESYNC_DELETE_FAILED, dstDN, delOperation.getErrorMessage()); } } /** * Add an entry to the local trust store. * @param srcEntry The instance key entry in the ADS branch. * @param dstDN The DN of the entry to be added in the local trust store. */ private void addEntry(Entry srcEntry, DN dstDN) { Map<ObjectClass, String> ocMap = new LinkedHashMap<>(2); ocMap.put(DirectoryServer.getTopObjectClass(), OC_TOP); ocMap.put(ocInstanceKey, OC_CRYPTO_INSTANCE_KEY); Map<AttributeType, List<Attribute>> userAttrs = new HashMap<>(); List<Attribute> attrList; attrList = srcEntry.getAttribute(attrAlias); if (attrList != null) { userAttrs.put(attrAlias, attrList); } attrList = srcEntry.getAttribute(attrCert); if (attrList != null) { userAttrs.put(attrCert, attrList); } Entry addEntry = new Entry(dstDN, ocMap, userAttrs, null); InternalClientConnection conn = InternalClientConnection.getRootConnection(); AddOperation addOperation = conn.processAdd(addEntry); if (addOperation.getResultCode() != ResultCode.SUCCESS) { logger.debug(INFO_TRUSTSTORESYNC_ADD_FAILED, dstDN, addOperation.getErrorMessage()); } } /** {@inheritDoc} */ @Override public PostResponse doPostResponse(PostResponseAddOperation op) { if (op.getResultCode() != ResultCode.SUCCESS) { return PostResponse.continueOperationProcessing(); } final Entry entry = op.getEntryToAdd(); final DN entryDN = op.getEntryDN(); if (entryDN.isDescendantOf(instanceKeysDN)) { handleInstanceKeyAddOperation(entry); } else if (entryDN.isDescendantOf(secretKeysDN)) { try { if (entry.hasObjectClass(ocCipherKey)) { DirectoryServer.getCryptoManager().importCipherKeyEntry(entry); } else if (entry.hasObjectClass(ocMacKey)) { DirectoryServer.getCryptoManager().importMacKeyEntry(entry); } } catch (CryptoManagerException e) { logger.error(LocalizableMessage.raw( "Failed to import key entry: %s", e.getMessage())); } } return PostResponse.continueOperationProcessing(); } private void handleInstanceKeyAddOperation(Entry entry) { RDN srcRDN = entry.getName().rdn(); // Only process the entry if it has the expected form of RDN. if (!srcRDN.isMultiValued() && srcRDN.getAttributeType(0).equals(attrAlias)) { DN dstDN = trustStoreRootDN.child(srcRDN); if (!entry.hasAttribute(attrCompromisedTime)) { addEntry(entry, dstDN); } } } /** {@inheritDoc} */ @Override public PostResponse doPostResponse(PostResponseDeleteOperation op) { if (op.getResultCode() != ResultCode.SUCCESS || !op.getEntryDN().isDescendantOf(instanceKeysDN)) { return PostResponse.continueOperationProcessing(); } RDN srcRDN = op.getEntryToDelete().getName().rdn(); // Only process the entry if it has the expected form of RDN. // FIXME: Technically it is possible to perform a subtree in // this case however such subtree delete would essentially be // removing configuration branches which should not happen. if (!srcRDN.isMultiValued() && srcRDN.getAttributeType(0).equals(attrAlias)) { DN destDN = trustStoreRootDN.child(srcRDN); deleteEntry(destDN); } return PostResponse.continueOperationProcessing(); } /** {@inheritDoc} */ @Override public PostResponse doPostResponse(PostResponseModifyOperation op) { if (op.getResultCode() != ResultCode.SUCCESS) { return PostResponse.continueOperationProcessing(); } final Entry newEntry = op.getModifiedEntry(); final DN entryDN = op.getEntryDN(); if (entryDN.isDescendantOf(instanceKeysDN)) { handleInstanceKeyModifyOperation(newEntry); } else if (entryDN.isDescendantOf(secretKeysDN)) { try { if (newEntry.hasObjectClass(ocCipherKey)) { DirectoryServer.getCryptoManager().importCipherKeyEntry(newEntry); } else if (newEntry.hasObjectClass(ocMacKey)) { DirectoryServer.getCryptoManager().importMacKeyEntry(newEntry); } } catch (CryptoManagerException e) { logger.error(LocalizableMessage.raw( "Failed to import modified key entry: %s", e.getMessage())); } } return PostResponse.continueOperationProcessing(); } private void handleInstanceKeyModifyOperation(Entry newEntry) { RDN srcRDN = newEntry.getName().rdn(); // Only process the entry if it has the expected form of RDN. if (!srcRDN.isMultiValued() && srcRDN.getAttributeType(0).equals(attrAlias)) { DN dstDN = trustStoreRootDN.child(srcRDN); // Get any existing local trust store entry. Entry dstEntry = null; try { dstEntry = DirectoryServer.getEntry(dstDN); } catch (DirectoryException e) { // ignore } if (newEntry.hasAttribute(attrCompromisedTime)) { // The key was compromised so we should remove it from the local // trust store. if (dstEntry != null) { deleteEntry(dstDN); } } else if (dstEntry == null) { addEntry(newEntry, dstDN); } else { modifyEntry(newEntry, dstEntry); } } } }