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