blob: 1b9a2b0b7585c3e056e7c6ab2db3c285f60e485e [file] [log] [blame]
/*
* Copyright (c) 2015, 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.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 java.security.*;
import java.security.cert.Certificate;
import java.security.cert.*;
import javax.naming.CommunicationException;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.security.auth.x500.X500Principal;
import sun.security.util.HexDumpEncoder;
import sun.security.provider.certpath.X509CertificatePair;
import sun.security.util.Cache;
import sun.security.util.Debug;
/**
* Core implementation of a LDAP Cert Store.
* @see java.security.cert.CertStore
*
* @since 9
*/
final class LDAPCertStoreImpl {
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";
/*
* Internal system property, that when set to "true", disables the
* JNDI application resource files lookup to prevent recursion issues
* when validating signed JARs with LDAP URLs in certificates.
*/
private final static String PROP_DISABLE_APP_RESOURCE_FILES =
"sun.security.certpath.ldap.disable.app.resource.files";
static {
String s = AccessController.doPrivileged(
(PrivilegedAction<String>) () -> System.getProperty(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 LdapContext ctx;
/**
* Flag indicating that communication error occurred.
*/
private boolean communicationError = false;
/**
* Flag indicating whether we should prefetch CRLs.
*/
private boolean prefetchCRLs = false;
private final Cache<String, byte[][]> valueCache;
private int cacheHits = 0;
private int cacheMisses = 0;
private int requests = 0;
/**
* Creates a <code>CertStore</code> with the specified parameters.
*/
LDAPCertStoreImpl(String serverName, int port)
throws InvalidAlgorithmParameterException {
createInitialDirContext(serverName, port);
// 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);
}
}
/**
* 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<String,Object> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, url);
// If property is set to true, disable application resource file lookup.
boolean disableAppResourceFiles = AccessController.doPrivileged(
(PrivilegedAction<Boolean>) () -> Boolean.getBoolean(PROP_DISABLE_APP_RESOURCE_FILES));
if (disableAppResourceFiles) {
if (debug != null) {
debug.println("LDAPCertStore disabling app resource files");
}
env.put("com.sun.naming.disable.app.resource.files", "true");
}
try {
ctx = new InitialLdapContext(env, null);
/*
* 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<String, byte[][]> valueMap;
private final List<String> 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 = valueCache.get(cacheKey);
if (values != null) {
cacheHits++;
return values;
}
cacheMisses++;
Map<String, byte[][]> 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<String, byte[][]> 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;
if (communicationError) {
ctx.reconnect(null);
communicationError = false;
}
try {
attrs = ctx.getAttributes(name, attrIds);
} catch (CommunicationException ce) {
communicationError = true;
throw ce;
} 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<X509Certificate> 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<X509Certificate> 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<X509CertificatePair> 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<X509CertificatePair> 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 X509Certificates stored in the forward component that match
* the forward X509CertSelector and all Certificates stored in the reverse
* component that match the reverse X509CertSelector.
* <p>
* 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 X509CertSelector (or null)
* @param reverse the reverse X509CertSelector (or null)
* @return a Collection of X509Certificates found
* @throws CertStoreException if an exception occurs
*/
private Collection<X509Certificate> getMatchingCrossCerts(
LDAPRequest request, X509CertSelector forward,
X509CertSelector reverse)
throws CertStoreException {
// Get the cert pairs
Collection<X509CertificatePair> certPairs =
getCertPairs(request, CROSS_CERT);
// Find Certificates that match and put them in a list
ArrayList<X509Certificate> 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 <code>Collection</code> of <code>X509Certificate</code>s that
* match the specified selector. If no <code>X509Certificate</code>s
* match the selector, an empty <code>Collection</code> will be returned.
* <p>
* It is not practical to search every entry in the LDAP database for
* matching <code>X509Certificate</code>s. Instead, the
* <code>X509CertSelector</code> is examined in order to determine where
* matching <code>Certificate</code>s 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
* <code>X509CertSelector</code>), a <code>CertStoreException</code> is
* thrown.
*
* @param selector a <code>X509CertSelector</code> used to select which
* <code>Certificate</code>s should be returned.
* @return a <code>Collection</code> of <code>X509Certificate</code>s that
* match the specified selector
* @throws CertStoreException if an exception occurs
*/
synchronized Collection<X509Certificate> getCertificates
(X509CertSelector xsel, String ldapDN) throws CertStoreException {
if (ldapDN == null) {
ldapDN = xsel.getSubjectAsString();
}
int basicConstraints = xsel.getBasicConstraints();
String issuer = xsel.getIssuerAsString();
HashSet<X509Certificate> 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 (ldapDN != null) {
if (debug != null) {
debug.println("LDAPCertStore.engineGetCertificates() "
+ " subject is not null");
}
LDAPRequest request = new LDAPRequest(ldapDN);
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 X509CRLSelector.
*
* @param name the location holding the attribute
* @param id the attribute identifier
* @param sel a X509CRLSelector that the CRLs must match
* @return a Collection of CRLs found
* @throws CertStoreException if an exception occurs
*/
private Collection<X509CRL> 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<X509CRL> 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 <code>Collection</code> of <code>X509CRL</code>s that
* match the specified selector. If no <code>X509CRL</code>s
* match the selector, an empty <code>Collection</code> will be returned.
* <p>
* It is not practical to search every entry in the LDAP database for
* matching <code>X509CRL</code>s. Instead, the <code>X509CRLSelector</code>
* is examined in order to determine where matching <code>X509CRL</code>s
* 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 <code>X509CRLSelector</code>), a
* <code>CertStoreException</code> is thrown.
*
* @param selector A <code>X509CRLSelector</code> used to select which
* <code>CRL</code>s should be returned. Specify <code>null</code>
* to return all <code>CRL</code>s.
* @return A <code>Collection</code> of <code>X509CRL</code>s that
* match the specified selector
* @throws CertStoreException if an exception occurs
*/
synchronized Collection<X509CRL> getCRLs(X509CRLSelector xsel,
String ldapDN) throws CertStoreException {
HashSet<X509CRL> crls = new HashSet<>();
// Look in directory entry for issuer of cert we're checking.
Collection<Object> issuerNames;
X509Certificate certChecking = xsel.getCertificateChecking();
if (certChecking != null) {
issuerNames = new HashSet<>();
X500Principal issuer = certChecking.getIssuerX500Principal();
issuerNames.add(issuer.getName(X500Principal.RFC2253));
} else {
// But if we don't know which cert we're checking, try the directory
// entries of all acceptable CRL issuers
if (ldapDN != null) {
issuerNames = new HashSet<>();
issuerNames.add(ldapDN);
} else {
issuerNames = xsel.getIssuerNames();
if (issuerNames == null) {
throw new CertStoreException("need issuerNames or"
+ " certChecking to find CRLs");
}
}
}
for (Object nameObject : issuerNames) {
String issuerName;
if (nameObject instanceof byte[]) {
try {
X500Principal issuer = new X500Principal((byte[])nameObject);
issuerName = issuer.getName(X500Principal.RFC2253);
} catch (IllegalArgumentException e) {
continue;
}
} else {
issuerName = (String)nameObject;
}
// If all we want is CA certs, try to get the (probably shorter) ARL
Collection<X509CRL> entryCRLs = Collections.emptySet();
if (certChecking == null || certChecking.getBasicConstraints() != -1) {
LDAPRequest request = new LDAPRequest(issuerName);
request.addRequestedAttribute(CROSS_CERT);
request.addRequestedAttribute(CA_CERT);
request.addRequestedAttribute(ARL);
if (prefetchCRLs) {
request.addRequestedAttribute(CRL);
}
try {
entryCRLs = getCRLs(request, ARL, xsel);
if (entryCRLs.isEmpty()) {
// no ARLs found. We assume that means that there are
// no ARLs on this server at all and prefetch the CRLs.
prefetchCRLs = true;
} else {
crls.addAll(entryCRLs);
}
} catch (CertStoreException e) {
if (debug != null) {
debug.println("LDAPCertStore.engineGetCRLs non-fatal error "
+ "retrieving ARLs:" + e);
e.printStackTrace();
}
}
}
// Otherwise, get the CRL
// if certChecking is null, we don't know if we should look in ARL or CRL
// attribute, so check both for matching CRLs.
if (entryCRLs.isEmpty() || certChecking == null) {
LDAPRequest request = new LDAPRequest(issuerName);
request.addRequestedAttribute(CRL);
entryCRLs = getCRLs(request, CRL, xsel);
crls.addAll(entryCRLs);
}
}
return crls;
}
}