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