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 2007-2008 Sun Microsystems, Inc. 025 * Portions Copyright 2012-2015 ForgeRock AS 026 */ 027package org.opends.server.extensions; 028 029import java.security.MessageDigest; 030import java.security.cert.Certificate; 031import java.security.cert.X509Certificate; 032import java.util.Collection; 033import java.util.LinkedHashSet; 034import java.util.List; 035import java.util.Set; 036 037import javax.security.auth.x500.X500Principal; 038 039import org.forgerock.i18n.LocalizableMessage; 040import org.forgerock.i18n.slf4j.LocalizedLogger; 041import org.forgerock.opendj.config.server.ConfigChangeResult; 042import org.forgerock.opendj.config.server.ConfigException; 043import org.forgerock.opendj.ldap.ByteString; 044import org.forgerock.opendj.ldap.ResultCode; 045import org.forgerock.opendj.ldap.SearchScope; 046import org.opends.server.admin.server.ConfigurationChangeListener; 047import org.opends.server.admin.std.server.CertificateMapperCfg; 048import org.opends.server.admin.std.server.FingerprintCertificateMapperCfg; 049import org.opends.server.api.Backend; 050import org.opends.server.api.CertificateMapper; 051import org.opends.server.core.DirectoryServer; 052import org.opends.server.protocols.internal.InternalClientConnection; 053import org.opends.server.protocols.internal.InternalSearchOperation; 054import org.opends.server.protocols.internal.SearchRequest; 055import static org.opends.server.protocols.internal.Requests.*; 056import org.opends.server.types.*; 057 058import static org.opends.messages.ExtensionMessages.*; 059import static org.opends.server.protocols.internal.InternalClientConnection.*; 060import static org.opends.server.util.CollectionUtils.*; 061import static org.opends.server.util.StaticUtils.*; 062 063/** 064 * This class implements a very simple Directory Server certificate mapper that 065 * will map a certificate to a user only if that user's entry contains an 066 * attribute with the fingerprint of the client certificate. There must be 067 * exactly one matching user entry for the mapping to be successful. 068 */ 069public class FingerprintCertificateMapper 070 extends CertificateMapper<FingerprintCertificateMapperCfg> 071 implements ConfigurationChangeListener< 072 FingerprintCertificateMapperCfg> 073{ 074 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 075 076 077 078 /** The DN of the configuration entry for this certificate mapper. */ 079 private DN configEntryDN; 080 081 /** The current configuration for this certificate mapper. */ 082 private FingerprintCertificateMapperCfg currentConfig; 083 084 /** The algorithm that will be used to generate the fingerprint. */ 085 private String fingerprintAlgorithm; 086 087 /** The set of attributes to return in search result entries. */ 088 private LinkedHashSet<String> requestedAttributes; 089 090 091 /** 092 * Creates a new instance of this certificate mapper. Note that all actual 093 * initialization should be done in the 094 * <CODE>initializeCertificateMapper</CODE> method. 095 */ 096 public FingerprintCertificateMapper() 097 { 098 super(); 099 } 100 101 102 103 /** {@inheritDoc} */ 104 @Override 105 public void initializeCertificateMapper( 106 FingerprintCertificateMapperCfg configuration) 107 throws ConfigException, InitializationException 108 { 109 configuration.addFingerprintChangeListener(this); 110 111 currentConfig = configuration; 112 configEntryDN = configuration.dn(); 113 114 115 // Get the algorithm that will be used to generate the fingerprint. 116 switch (configuration.getFingerprintAlgorithm()) 117 { 118 case MD5: 119 fingerprintAlgorithm = "MD5"; 120 break; 121 case SHA1: 122 fingerprintAlgorithm = "SHA1"; 123 break; 124 } 125 126 127 // Make sure that the fingerprint attribute is configured for equality in 128 // all appropriate backends. 129 Set<DN> cfgBaseDNs = configuration.getUserBaseDN(); 130 if (cfgBaseDNs == null || cfgBaseDNs.isEmpty()) 131 { 132 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 133 } 134 135 AttributeType t = configuration.getFingerprintAttribute(); 136 for (DN baseDN : cfgBaseDNs) 137 { 138 Backend b = DirectoryServer.getBackend(baseDN); 139 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 140 { 141 logger.warn(WARN_SATUACM_ATTR_UNINDEXED, configuration.dn(), 142 t.getNameOrOID(), b.getBackendID()); 143 } 144 } 145 146 // Create the attribute list to include in search requests. We want to 147 // include all user and operational attributes. 148 requestedAttributes = newLinkedHashSet("*", "+"); 149 } 150 151 152 153 /** {@inheritDoc} */ 154 @Override 155 public void finalizeCertificateMapper() 156 { 157 currentConfig.removeFingerprintChangeListener(this); 158 } 159 160 161 162 /** {@inheritDoc} */ 163 @Override 164 public Entry mapCertificateToUser(Certificate[] certificateChain) 165 throws DirectoryException 166 { 167 FingerprintCertificateMapperCfg config = currentConfig; 168 AttributeType fingerprintAttributeType = config.getFingerprintAttribute(); 169 String theFingerprintAlgorithm = this.fingerprintAlgorithm; 170 171 // Make sure that a peer certificate was provided. 172 if (certificateChain == null || certificateChain.length == 0) 173 { 174 LocalizableMessage message = ERR_FCM_NO_PEER_CERTIFICATE.get(); 175 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 176 } 177 178 179 // Get the first certificate in the chain. It must be an X.509 certificate. 180 X509Certificate peerCertificate; 181 try 182 { 183 peerCertificate = (X509Certificate) certificateChain[0]; 184 } 185 catch (Exception e) 186 { 187 logger.traceException(e); 188 189 LocalizableMessage message = ERR_FCM_PEER_CERT_NOT_X509.get( 190 certificateChain[0].getType()); 191 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 192 } 193 194 195 // Get the signature from the peer certificate and create a digest of it 196 // using the configured algorithm. 197 String fingerprintString; 198 try 199 { 200 MessageDigest digest = MessageDigest.getInstance(theFingerprintAlgorithm); 201 byte[] fingerprintBytes = digest.digest(peerCertificate.getEncoded()); 202 fingerprintString = bytesToColonDelimitedHex(fingerprintBytes); 203 } 204 catch (Exception e) 205 { 206 logger.traceException(e); 207 208 String peerSubject = peerCertificate.getSubjectX500Principal().getName( 209 X500Principal.RFC2253); 210 211 LocalizableMessage message = ERR_FCM_CANNOT_CALCULATE_FINGERPRINT.get( 212 peerSubject, getExceptionMessage(e)); 213 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 214 } 215 216 217 // Create the search filter from the fingerprint. 218 ByteString value = ByteString.valueOfUtf8(fingerprintString); 219 SearchFilter filter = 220 SearchFilter.createEqualityFilter(fingerprintAttributeType, value); 221 222 223 // If we have an explicit set of base DNs, then use it. Otherwise, use the 224 // set of public naming contexts in the server. 225 Collection<DN> baseDNs = config.getUserBaseDN(); 226 if (baseDNs == null || baseDNs.isEmpty()) 227 { 228 baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 229 } 230 231 232 // For each base DN, issue an internal search in an attempt to map the 233 // certificate. 234 Entry userEntry = null; 235 InternalClientConnection conn = getRootConnection(); 236 for (DN baseDN : baseDNs) 237 { 238 final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, filter) 239 .setSizeLimit(1) 240 .setTimeLimit(10) 241 .addAttribute(requestedAttributes); 242 InternalSearchOperation searchOperation = conn.processSearch(request); 243 244 switch (searchOperation.getResultCode().asEnum()) 245 { 246 case SUCCESS: 247 // This is fine. No action needed. 248 break; 249 250 case NO_SUCH_OBJECT: 251 // The search base doesn't exist. Not an ideal situation, but we'll 252 // ignore it. 253 break; 254 255 case SIZE_LIMIT_EXCEEDED: 256 // Multiple entries matched the filter. This is not acceptable. 257 LocalizableMessage message = ERR_FCM_MULTIPLE_SEARCH_MATCHING_ENTRIES.get( 258 fingerprintString); 259 throw new DirectoryException( 260 ResultCode.INVALID_CREDENTIALS, message); 261 262 263 case TIME_LIMIT_EXCEEDED: 264 case ADMIN_LIMIT_EXCEEDED: 265 // The search criteria was too inefficient. 266 message = ERR_FCM_INEFFICIENT_SEARCH.get(fingerprintString, searchOperation.getErrorMessage()); 267 throw new DirectoryException(searchOperation.getResultCode(), 268 message); 269 270 default: 271 // Just pass on the failure that was returned for this search. 272 message = ERR_FCM_SEARCH_FAILED.get(fingerprintString, searchOperation.getErrorMessage()); 273 throw new DirectoryException(searchOperation.getResultCode(), 274 message); 275 } 276 277 for (SearchResultEntry entry : searchOperation.getSearchEntries()) 278 { 279 if (userEntry == null) 280 { 281 userEntry = entry; 282 } 283 else 284 { 285 LocalizableMessage message = ERR_FCM_MULTIPLE_MATCHING_ENTRIES. 286 get(fingerprintString, userEntry.getName(), entry.getName()); 287 throw new DirectoryException(ResultCode.INVALID_CREDENTIALS, message); 288 } 289 } 290 } 291 292 293 // If we've gotten here, then we either found exactly one user entry or we 294 // didn't find any. Either way, return the entry or null to the caller. 295 return userEntry; 296 } 297 298 299 300 /** {@inheritDoc} */ 301 @Override 302 public boolean isConfigurationAcceptable(CertificateMapperCfg configuration, 303 List<LocalizableMessage> unacceptableReasons) 304 { 305 FingerprintCertificateMapperCfg config = 306 (FingerprintCertificateMapperCfg) configuration; 307 return isConfigurationChangeAcceptable(config, unacceptableReasons); 308 } 309 310 311 312 /** {@inheritDoc} */ 313 @Override 314 public boolean isConfigurationChangeAcceptable( 315 FingerprintCertificateMapperCfg configuration, 316 List<LocalizableMessage> unacceptableReasons) 317 { 318 return true; 319 } 320 321 322 323 /** {@inheritDoc} */ 324 @Override 325 public ConfigChangeResult applyConfigurationChange( 326 FingerprintCertificateMapperCfg configuration) 327 { 328 final ConfigChangeResult ccr = new ConfigChangeResult(); 329 330 331 // Get the algorithm that will be used to generate the fingerprint. 332 String newFingerprintAlgorithm = null; 333 switch (configuration.getFingerprintAlgorithm()) 334 { 335 case MD5: 336 newFingerprintAlgorithm = "MD5"; 337 break; 338 case SHA1: 339 newFingerprintAlgorithm = "SHA1"; 340 break; 341 } 342 343 344 if (ccr.getResultCode() == ResultCode.SUCCESS) 345 { 346 fingerprintAlgorithm = newFingerprintAlgorithm; 347 currentConfig = configuration; 348 } 349 350 // Make sure that the fingerprint attribute is configured for equality in 351 // all appropriate backends. 352 Set<DN> cfgBaseDNs = configuration.getUserBaseDN(); 353 if (cfgBaseDNs == null || cfgBaseDNs.isEmpty()) 354 { 355 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 356 } 357 358 AttributeType t = configuration.getFingerprintAttribute(); 359 for (DN baseDN : cfgBaseDNs) 360 { 361 Backend b = DirectoryServer.getBackend(baseDN); 362 if (b != null && ! b.isIndexed(t, IndexType.EQUALITY)) 363 { 364 LocalizableMessage message = WARN_SATUACM_ATTR_UNINDEXED.get( 365 configuration.dn(), t.getNameOrOID(), b.getBackendID()); 366 ccr.addMessage(message); 367 logger.error(message); 368 } 369 } 370 371 return ccr; 372 } 373}