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}