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}