/*
* Copyright (c) 2000, 2006, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.provider.certpath.ldap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.util.*;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NameNotFoundException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.*;
import javax.security.auth.x500.X500Principal;
import sun.misc.HexDumpEncoder;
import sun.security.provider.certpath.X509CertificatePair;
import sun.security.util.Cache;
import sun.security.util.Debug;
import sun.security.x509.X500Name;
import sun.security.action.GetPropertyAction;
/**
* A CertStore that retrieves Certificates and
* CRLs from an LDAP directory, using the PKIX LDAP V2 Schema
* (RFC 2587):
*
* http://www.ietf.org/rfc/rfc2587.txt.
*
* Before calling the {@link #engineGetCertificates engineGetCertificates} or
* {@link #engineGetCRLs engineGetCRLs} methods, the
* {@link #LDAPCertStore(CertStoreParameters)
* LDAPCertStore(CertStoreParameters)} constructor is called to create the
* CertStore and establish the DNS name and port of the LDAP
* server from which Certificates and CRLs will be
* retrieved.
*
* Concurrent Access
*
* As described in the javadoc for CertStoreSpi, the
* engineGetCertificates and engineGetCRLs methods
* must be thread-safe. That is, multiple threads may concurrently
* invoke these methods on a single LDAPCertStore object
* (or more than one) with no ill effects. This allows a
* CertPathBuilder to search for a CRL while simultaneously
* searching for further certificates, for instance.
*
* This is achieved by adding the synchronized keyword to the
* engineGetCertificates and engineGetCRLs methods.
*
* This classes uses caching and requests multiple attributes at once to
* minimize LDAP round trips. The cache is associated with the CertStore
* instance. It uses soft references to hold the values to minimize impact
* on footprint and currently has a maximum size of 750 attributes and a
* 30 second default lifetime.
*
* We always request CA certificates, cross certificate pairs, and ARLs in
* a single LDAP request when any one of them is needed. The reason is that
* we typically need all of them anyway and requesting them in one go can
* reduce the number of requests to a third. Even if we don't need them,
* these attributes are typically small enough not to cause a noticeable
* overhead. In addition, when the prefetchCRLs flag is true, we also request
* the full CRLs. It is currently false initially but set to true once any
* request for an ARL to the server returns an null value. The reason is
* that CRLs could be rather large but are rarely used. This implementation
* should improve performance in most cases.
*
* @see java.security.cert.CertStore
*
* @since 1.4
* @author Steve Hanna
* @author Andreas Sterbenz
*/
public class LDAPCertStore extends CertStoreSpi {
private static final Debug debug = Debug.getInstance("certpath");
private final static boolean DEBUG = false;
/**
* LDAP attribute identifiers.
*/
private static final String USER_CERT = "userCertificate;binary";
private static final String CA_CERT = "cACertificate;binary";
private static final String CROSS_CERT = "crossCertificatePair;binary";
private static final String CRL = "certificateRevocationList;binary";
private static final String ARL = "authorityRevocationList;binary";
private static final String DELTA_CRL = "deltaRevocationList;binary";
// Constants for various empty values
private final static String[] STRING0 = new String[0];
private final static byte[][] BB0 = new byte[0][];
private final static Attributes EMPTY_ATTRIBUTES = new BasicAttributes();
// cache related constants
private final static int DEFAULT_CACHE_SIZE = 750;
private final static int DEFAULT_CACHE_LIFETIME = 30;
private final static int LIFETIME;
private final static String PROP_LIFETIME =
"sun.security.certpath.ldap.cache.lifetime";
static {
String s = AccessController.doPrivileged(
new GetPropertyAction(PROP_LIFETIME));
if (s != null) {
LIFETIME = Integer.parseInt(s); // throws NumberFormatException
} else {
LIFETIME = DEFAULT_CACHE_LIFETIME;
}
}
/**
* The CertificateFactory used to decode certificates from
* their binary stored form.
*/
private CertificateFactory cf;
/**
* The JNDI directory context.
*/
private DirContext ctx;
/**
* Flag indicating whether we should prefetch CRLs.
*/
private boolean prefetchCRLs = false;
private final Cache valueCache;
private int cacheHits = 0;
private int cacheMisses = 0;
private int requests = 0;
/**
* Creates a CertStore with the specified parameters.
* For this class, the parameters object must be an instance of
* LDAPCertStoreParameters.
*
* @param params the algorithm parameters
* @exception InvalidAlgorithmParameterException if params is not an
* instance of LDAPCertStoreParameters
*/
public LDAPCertStore(CertStoreParameters params)
throws InvalidAlgorithmParameterException {
super(params);
if (!(params instanceof LDAPCertStoreParameters))
throw new InvalidAlgorithmParameterException(
"parameters must be LDAPCertStoreParameters");
LDAPCertStoreParameters lparams = (LDAPCertStoreParameters) params;
// Create InitialDirContext needed to communicate with the server
createInitialDirContext(lparams.getServerName(), lparams.getPort());
// Create CertificateFactory for use later on
try {
cf = CertificateFactory.getInstance("X.509");
} catch (CertificateException e) {
throw new InvalidAlgorithmParameterException(
"unable to create CertificateFactory for X.509");
}
if (LIFETIME == 0) {
valueCache = Cache.newNullCache();
} else if (LIFETIME < 0) {
valueCache = Cache.newSoftMemoryCache(DEFAULT_CACHE_SIZE);
} else {
valueCache = Cache.newSoftMemoryCache(DEFAULT_CACHE_SIZE, LIFETIME);
}
}
/**
* Returns an LDAP CertStore. This method consults a cache of
* CertStores (shared per JVM) using the LDAP server/port as a key.
*/
private static final Cache certStoreCache = Cache.newSoftMemoryCache(185);
static synchronized CertStore getInstance(LDAPCertStoreParameters params)
throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
CertStore lcs = (CertStore) certStoreCache.get(params);
if (lcs == null) {
lcs = CertStore.getInstance("LDAP", params);
certStoreCache.put(params, lcs);
} else {
if (debug != null) {
debug.println("LDAPCertStore.getInstance: cache hit");
}
}
return lcs;
}
/**
* Create InitialDirContext.
*
* @param server Server DNS name hosting LDAP service
* @param port Port at which server listens for requests
* @throws InvalidAlgorithmParameterException if creation fails
*/
private void createInitialDirContext(String server, int port)
throws InvalidAlgorithmParameterException {
String url = "ldap://" + server + ":" + port;
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, url);
try {
ctx = new InitialDirContext(env);
/*
* By default, follow referrals unless application has
* overridden property in an application resource file.
*/
Hashtable,?> currentEnv = ctx.getEnvironment();
if (currentEnv.get(Context.REFERRAL) == null) {
ctx.addToEnvironment(Context.REFERRAL, "follow");
}
} catch (NamingException e) {
if (debug != null) {
debug.println("LDAPCertStore.engineInit about to throw "
+ "InvalidAlgorithmParameterException");
e.printStackTrace();
}
Exception ee = new InvalidAlgorithmParameterException
("unable to create InitialDirContext using supplied parameters");
ee.initCause(e);
throw (InvalidAlgorithmParameterException)ee;
}
}
/**
* Private class encapsulating the actual LDAP operations and cache
* handling. Use:
*
* LDAPRequest request = new LDAPRequest(dn);
* request.addRequestedAttribute(CROSS_CERT);
* request.addRequestedAttribute(CA_CERT);
* byte[][] crossValues = request.getValues(CROSS_CERT);
* byte[][] caValues = request.getValues(CA_CERT);
*
* At most one LDAP request is sent for each instance created. If all
* getValues() calls can be satisfied from the cache, no request
* is sent at all. If a request is sent, all requested attributes
* are always added to the cache irrespective of whether the getValues()
* method is called.
*/
private class LDAPRequest {
private final String name;
private Map valueMap;
private final List requestedAttributes;
LDAPRequest(String name) {
this.name = name;
requestedAttributes = new ArrayList(5);
}
String getName() {
return name;
}
void addRequestedAttribute(String attrId) {
if (valueMap != null) {
throw new IllegalStateException("Request already sent");
}
requestedAttributes.add(attrId);
}
/**
* Gets one or more binary values from an attribute.
*
* @param name the location holding the attribute
* @param attrId the attribute identifier
* @return an array of binary values (byte arrays)
* @throws NamingException if a naming exception occurs
*/
byte[][] getValues(String attrId) throws NamingException {
if (DEBUG && ((cacheHits + cacheMisses) % 50 == 0)) {
System.out.println("Cache hits: " + cacheHits + "; misses: "
+ cacheMisses);
}
String cacheKey = name + "|" + attrId;
byte[][] values = (byte[][])valueCache.get(cacheKey);
if (values != null) {
cacheHits++;
return values;
}
cacheMisses++;
Map attrs = getValueMap();
values = attrs.get(attrId);
return values;
}
/**
* Get a map containing the values for this request. The first time
* this method is called on an object, the LDAP request is sent,
* the results parsed and added to a private map and also to the
* cache of this LDAPCertStore. Subsequent calls return the private
* map immediately.
*
* The map contains an entry for each requested attribute. The
* attribute name is the key, values are byte[][]. If there are no
* values for that attribute, values are byte[0][].
*
* @return the value Map
* @throws NamingException if a naming exception occurs
*/
private Map getValueMap() throws NamingException {
if (valueMap != null) {
return valueMap;
}
if (DEBUG) {
System.out.println("Request: " + name + ":" + requestedAttributes);
requests++;
if (requests % 5 == 0) {
System.out.println("LDAP requests: " + requests);
}
}
valueMap = new HashMap(8);
String[] attrIds = requestedAttributes.toArray(STRING0);
Attributes attrs;
try {
attrs = ctx.getAttributes(name, attrIds);
} catch (NameNotFoundException e) {
// name does not exist on this LDAP server
// treat same as not attributes found
attrs = EMPTY_ATTRIBUTES;
}
for (String attrId : requestedAttributes) {
Attribute attr = attrs.get(attrId);
byte[][] values = getAttributeValues(attr);
cacheAttribute(attrId, values);
valueMap.put(attrId, values);
}
return valueMap;
}
/**
* Add the values to the cache.
*/
private void cacheAttribute(String attrId, byte[][] values) {
String cacheKey = name + "|" + attrId;
valueCache.put(cacheKey, values);
}
/**
* Get the values for the given attribute. If the attribute is null
* or does not contain any values, a zero length byte array is
* returned. NOTE that it is assumed that all values are byte arrays.
*/
private byte[][] getAttributeValues(Attribute attr)
throws NamingException {
byte[][] values;
if (attr == null) {
values = BB0;
} else {
values = new byte[attr.size()][];
int i = 0;
NamingEnumeration> enum_ = attr.getAll();
while (enum_.hasMore()) {
Object obj = enum_.next();
if (debug != null) {
if (obj instanceof String) {
debug.println("LDAPCertStore.getAttrValues() "
+ "enum.next is a string!: " + obj);
}
}
byte[] value = (byte[])obj;
values[i++] = value;
}
}
return values;
}
}
/*
* Gets certificates from an attribute id and location in the LDAP
* directory. Returns a Collection containing only the Certificates that
* match the specified CertSelector.
*
* @param name the location holding the attribute
* @param id the attribute identifier
* @param sel a CertSelector that the Certificates must match
* @return a Collection of Certificates found
* @throws CertStoreException if an exception occurs
*/
private Collection getCertificates(LDAPRequest request,
String id, X509CertSelector sel) throws CertStoreException {
/* fetch encoded certs from storage */
byte[][] encodedCert;
try {
encodedCert = request.getValues(id);
} catch (NamingException namingEx) {
throw new CertStoreException(namingEx);
}
int n = encodedCert.length;
if (n == 0) {
return Collections.emptySet();
}
List certs = new ArrayList(n);
/* decode certs and check if they satisfy selector */
for (int i = 0; i < n; i++) {
ByteArrayInputStream bais = new ByteArrayInputStream(encodedCert[i]);
try {
Certificate cert = cf.generateCertificate(bais);
if (sel.match(cert)) {
certs.add((X509Certificate)cert);
}
} catch (CertificateException e) {
if (debug != null) {
debug.println("LDAPCertStore.getCertificates() encountered "
+ "exception while parsing cert, skipping the bad data: ");
HexDumpEncoder encoder = new HexDumpEncoder();
debug.println(
"[ " + encoder.encodeBuffer(encodedCert[i]) + " ]");
}
}
}
return certs;
}
/*
* Gets certificate pairs from an attribute id and location in the LDAP
* directory.
*
* @param name the location holding the attribute
* @param id the attribute identifier
* @return a Collection of X509CertificatePairs found
* @throws CertStoreException if an exception occurs
*/
private Collection getCertPairs(
LDAPRequest request, String id) throws CertStoreException {
/* fetch the encoded cert pairs from storage */
byte[][] encodedCertPair;
try {
encodedCertPair = request.getValues(id);
} catch (NamingException namingEx) {
throw new CertStoreException(namingEx);
}
int n = encodedCertPair.length;
if (n == 0) {
return Collections.emptySet();
}
List certPairs =
new ArrayList(n);
/* decode each cert pair and add it to the Collection */
for (int i = 0; i < n; i++) {
try {
X509CertificatePair certPair =
X509CertificatePair.generateCertificatePair(encodedCertPair[i]);
certPairs.add(certPair);
} catch (CertificateException e) {
if (debug != null) {
debug.println(
"LDAPCertStore.getCertPairs() encountered exception "
+ "while parsing cert, skipping the bad data: ");
HexDumpEncoder encoder = new HexDumpEncoder();
debug.println(
"[ " + encoder.encodeBuffer(encodedCertPair[i]) + " ]");
}
}
}
return certPairs;
}
/*
* Looks at certificate pairs stored in the crossCertificatePair attribute
* at the specified location in the LDAP directory. Returns a Collection
* containing all Certificates stored in the forward component that match
* the forward CertSelector and all Certificates stored in the reverse
* component that match the reverse CertSelector.
*
* If either forward or reverse is null, all certificates from the
* corresponding component will be rejected.
*
* @param name the location to look in
* @param forward the forward CertSelector (or null)
* @param reverse the reverse CertSelector (or null)
* @return a Collection of Certificates found
* @throws CertStoreException if an exception occurs
*/
private Collection getMatchingCrossCerts(
LDAPRequest request, X509CertSelector forward,
X509CertSelector reverse)
throws CertStoreException {
// Get the cert pairs
Collection certPairs =
getCertPairs(request, CROSS_CERT);
// Find Certificates that match and put them in a list
ArrayList matchingCerts =
new ArrayList();
for (X509CertificatePair certPair : certPairs) {
X509Certificate cert;
if (forward != null) {
cert = certPair.getForward();
if ((cert != null) && forward.match(cert)) {
matchingCerts.add(cert);
}
}
if (reverse != null) {
cert = certPair.getReverse();
if ((cert != null) && reverse.match(cert)) {
matchingCerts.add(cert);
}
}
}
return matchingCerts;
}
/**
* Returns a Collection of Certificates that
* match the specified selector. If no Certificates
* match the selector, an empty Collection will be returned.
*
* It is not practical to search every entry in the LDAP database for
* matching Certificates. Instead, the CertSelector
* is examined in order to determine where matching Certificates
* are likely to be found (according to the PKIX LDAPv2 schema, RFC 2587).
* If the subject is specified, its directory entry is searched. If the
* issuer is specified, its directory entry is searched. If neither the
* subject nor the issuer are specified (or the selector is not an
* X509CertSelector), a CertStoreException is
* thrown.
*
* @param selector a CertSelector used to select which
* Certificates should be returned.
* @return a Collection of Certificates that
* match the specified selector
* @throws CertStoreException if an exception occurs
*/
public synchronized Collection engineGetCertificates
(CertSelector selector) throws CertStoreException {
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() selector: "
+ String.valueOf(selector));
}
if (selector == null) {
selector = new X509CertSelector();
}
if (!(selector instanceof X509CertSelector)) {
throw new CertStoreException("LDAPCertStore needs an X509CertSelector " +
"to find certs");
}
X509CertSelector xsel = (X509CertSelector) selector;
int basicConstraints = xsel.getBasicConstraints();
String subject = xsel.getSubjectAsString();
String issuer = xsel.getIssuerAsString();
HashSet certs = new HashSet();
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() basicConstraints: "
+ basicConstraints);
}
// basicConstraints:
// -2: only EE certs accepted
// -1: no check is done
// 0: any CA certificate accepted
// >1: certificate's basicConstraints extension pathlen must match
if (subject != null) {
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() "
+ "subject is not null");
}
LDAPRequest request = new LDAPRequest(subject);
if (basicConstraints > -2) {
request.addRequestedAttribute(CROSS_CERT);
request.addRequestedAttribute(CA_CERT);
request.addRequestedAttribute(ARL);
if (prefetchCRLs) {
request.addRequestedAttribute(CRL);
}
}
if (basicConstraints < 0) {
request.addRequestedAttribute(USER_CERT);
}
if (basicConstraints > -2) {
certs.addAll(getMatchingCrossCerts(request, xsel, null));
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() after "
+ "getMatchingCrossCerts(subject,xsel,null),certs.size(): "
+ certs.size());
}
certs.addAll(getCertificates(request, CA_CERT, xsel));
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() after "
+ "getCertificates(subject,CA_CERT,xsel),certs.size(): "
+ certs.size());
}
}
if (basicConstraints < 0) {
certs.addAll(getCertificates(request, USER_CERT, xsel));
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() after "
+ "getCertificates(subject,USER_CERT, xsel),certs.size(): "
+ certs.size());
}
}
} else {
if (debug != null) {
debug.println
("LDAPCertStore.engineGetCertificates() subject is null");
}
if (basicConstraints == -2) {
throw new CertStoreException("need subject to find EE certs");
}
if (issuer == null) {
throw new CertStoreException("need subject or issuer to find certs");
}
}
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() about to "
+ "getMatchingCrossCerts...");
}
if ((issuer != null) && (basicConstraints > -2)) {
LDAPRequest request = new LDAPRequest(issuer);
request.addRequestedAttribute(CROSS_CERT);
request.addRequestedAttribute(CA_CERT);
request.addRequestedAttribute(ARL);
if (prefetchCRLs) {
request.addRequestedAttribute(CRL);
}
certs.addAll(getMatchingCrossCerts(request, null, xsel));
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() after "
+ "getMatchingCrossCerts(issuer,null,xsel),certs.size(): "
+ certs.size());
}
certs.addAll(getCertificates(request, CA_CERT, xsel));
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() after "
+ "getCertificates(issuer,CA_CERT,xsel),certs.size(): "
+ certs.size());
}
}
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() returning certs");
}
return certs;
}
/*
* Gets CRLs from an attribute id and location in the LDAP directory.
* Returns a Collection containing only the CRLs that match the
* specified CRLSelector.
*
* @param name the location holding the attribute
* @param id the attribute identifier
* @param sel a CRLSelector that the CRLs must match
* @return a Collection of CRLs found
* @throws CertStoreException if an exception occurs
*/
private Collection getCRLs(LDAPRequest request, String id,
X509CRLSelector sel) throws CertStoreException {
/* fetch the encoded crls from storage */
byte[][] encodedCRL;
try {
encodedCRL = request.getValues(id);
} catch (NamingException namingEx) {
throw new CertStoreException(namingEx);
}
int n = encodedCRL.length;
if (n == 0) {
return Collections.emptySet();
}
List crls = new ArrayList(n);
/* decode each crl and check if it matches selector */
for (int i = 0; i < n; i++) {
try {
CRL crl = cf.generateCRL(new ByteArrayInputStream(encodedCRL[i]));
if (sel.match(crl)) {
crls.add((X509CRL)crl);
}
} catch (CRLException e) {
if (debug != null) {
debug.println("LDAPCertStore.getCRLs() encountered exception"
+ " while parsing CRL, skipping the bad data: ");
HexDumpEncoder encoder = new HexDumpEncoder();
debug.println("[ " + encoder.encodeBuffer(encodedCRL[i]) + " ]");
}
}
}
return crls;
}
/**
* Returns a Collection of CRLs that
* match the specified selector. If no CRLs
* match the selector, an empty Collection will be returned.
*
* It is not practical to search every entry in the LDAP database for
* matching CRLs. Instead, the CRLSelector
* is examined in order to determine where matching CRLs
* are likely to be found (according to the PKIX LDAPv2 schema, RFC 2587).
* If issuerNames or certChecking are specified, the issuer's directory
* entry is searched. If neither issuerNames or certChecking are specified
* (or the selector is not an X509CRLSelector), a
* CertStoreException is thrown.
*
* @param selector A CRLSelector used to select which
* CRLs should be returned. Specify null
* to return all CRLs.
* @return A Collection of CRLs that
* match the specified selector
* @throws CertStoreException if an exception occurs
*/
public synchronized Collection engineGetCRLs(CRLSelector selector)
throws CertStoreException {
if (debug != null) {
debug.println("LDAPCertStore.engineGetCRLs() selector: "
+ selector);
}
// Set up selector and collection to hold CRLs
if (selector == null) {
selector = new X509CRLSelector();
}
if (!(selector instanceof X509CRLSelector)) {
throw new CertStoreException("need X509CRLSelector to find CRLs");
}
X509CRLSelector xsel = (X509CRLSelector) selector;
HashSet crls = new HashSet();
// Look in directory entry for issuer of cert we're checking.
Collection