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