/*
 * Copyright (c) 2015, 2023, 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.CompositeName;
import javax.naming.InvalidNameException;
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.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.security.auth.x500.X500Principal;

import com.sun.jndi.ldap.LdapReferralException;
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.GetBooleanAction;
import sun.security.action.GetPropertyAction;

/**
 * A <code>CertStore</code> that retrieves <code>Certificates</code> and
 * <code>CRL</code>s from an LDAP directory, using the PKIX LDAP V2 Schema
 * (RFC 2587):
 * <a href="http://www.ietf.org/rfc/rfc2587.txt">
 * http://www.ietf.org/rfc/rfc2587.txt</a>.
 * <p>
 * Before calling the {@link #engineGetCertificates engineGetCertificates} or
 * {@link #engineGetCRLs engineGetCRLs} methods, the
 * {@link #LDAPCertStore(CertStoreParameters)
 * LDAPCertStore(CertStoreParameters)} constructor is called to create the
 * <code>CertStore</code> and establish the DNS name and port of the LDAP
 * server from which <code>Certificate</code>s and <code>CRL</code>s will be
 * retrieved.
 * <p>
 * <b>Concurrent Access</b>
 * <p>
 * As described in the javadoc for <code>CertStoreSpi</code>, the
 * <code>engineGetCertificates</code> and <code>engineGetCRLs</code> methods
 * must be thread-safe. That is, multiple threads may concurrently
 * invoke these methods on a single <code>LDAPCertStore</code> object
 * (or more than one) with no ill effects. This allows a
 * <code>CertPathBuilder</code> to search for a CRL while simultaneously
 * searching for further certificates, for instance.
 * <p>
 * This is achieved by adding the <code>synchronized</code> keyword to the
 * <code>engineGetCertificates</code> and <code>engineGetCRLs</code> methods.
 * <p>
 * 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.
 * <p>
 * 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 final 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";

    /*
     * 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(
                                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<String, byte[][]> valueCache;

    private int cacheHits = 0;
    private int cacheMisses = 0;
    private int requests = 0;

    /**
     * Creates a <code>CertStore</code> with the specified parameters.
     * For this class, the parameters object must be an instance of
     * <code>LDAPCertStoreParameters</code>.
     *
     * @param params the algorithm parameters
     * @exception InvalidAlgorithmParameterException if params is not an
     *   instance of <code>LDAPCertStoreParameters</code>
     */
    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<LDAPCertStoreParameters, CertStore>
        certStoreCache = Cache.newSoftMemoryCache(185);
    static synchronized CertStore getInstance(LDAPCertStoreParameters params)
        throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {

        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkConnect(params.getServerName(), params.getPort());
        }

        CertStore lcs = 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<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(
            new GetBooleanAction(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 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, "throw");
            }
        } 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 static String checkName(String name) throws CertStoreException {
        if (name == null) {
            throw new CertStoreException("Name absent");
        }
        try {
            if (new CompositeName(name).size() > 1) {
                throw new CertStoreException("Invalid name: " + name);
            }
        } catch (InvalidNameException ine) {
            throw new CertStoreException("Invalid name: " + name, ine);
        }
        return name;
    }

    /**
     * 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 static 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;
    }

    /**
     * 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 LdapName name;
        private Map<String, byte[][]> valueMap;
        private final List<String> requestedAttributes;

        LDAPRequest(String name) throws CertStoreException {
            try {
                // Convert DN to an LdapName so that it is not treated as a
                // composite name by JNDI. In JNDI, using a string name is
                // equivalent to calling new CompositeName(stringName).
                this.name = new LdapName(name);
            } catch (InvalidNameException ine) {
                throw new CertStoreException("Invalid name: " + name, ine);
            }
            requestedAttributes = new ArrayList<>(5);
        }

        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 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;
            try {
                attrs = ctx.getAttributes(name, attrIds);
            } catch (LdapReferralException lre) {
                // LdapCtx has a hopCount field to avoid infinite loop
                while (true) {
                    try {
                        String newName = (String) lre.getReferralInfo();
                        URI newUri = new URI(newName);
                        if (!newUri.getScheme().equalsIgnoreCase("ldap")) {
                            throw new IllegalArgumentException("Not LDAP");
                        }
                        String newDn = newUri.getPath();
                        if (newDn != null && newDn.charAt(0) == '/') {
                            newDn = newDn.substring(1);
                        }
                        // In JNDI, it is not possible to use an LdapName for
                        // the referral DN, so we must validate the syntax of
                        // the string DN.
                        checkName(newDn);
                    } catch (Exception e) {
                        throw new NamingException("Cannot follow referral to "
                                + lre.getReferralInfo());
                    }
                    LdapContext refCtx =
                            (LdapContext)lre.getReferralContext();

                    // repeat the original operation at the new context
                    try {
                        attrs = refCtx.getAttributes(name, attrIds);
                        break;
                    } catch (LdapReferralException re) {
                        lre = re;
                        continue;
                    } finally {
                        // Make sure we close referral context
                        refCtx.close();
                    }
                }
            } 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);
        }
    }

    /*
     * 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 Certificates stored in the forward component that match
     * the forward CertSelector and all Certificates stored in the reverse
     * component that match the reverse CertSelector.
     * <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 CertSelector (or null)
     * @param reverse the reverse CertSelector (or null)
     * @return a Collection of Certificates 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>Certificate</code>s that
     * match the specified selector. If no <code>Certificate</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>Certificate</code>s. Instead, the <code>CertSelector</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>CertSelector</code> used to select which
     *  <code>Certificate</code>s should be returned.
     * @return a <code>Collection</code> of <code>Certificate</code>s that
     *         match the specified selector
     * @throws CertStoreException if an exception occurs
     */
    public synchronized Collection<X509Certificate> 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<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 (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<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>CRL</code>s that
     * match the specified selector. If no <code>CRL</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>CRL</code>s. Instead, the <code>CRLSelector</code>
     * is examined in order to determine where matching <code>CRL</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>CRLSelector</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>CRL</code>s that
     *         match the specified selector
     * @throws CertStoreException if an exception occurs
     */
    public synchronized Collection<X509CRL> 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<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
            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 if (nameObject instanceof String) {
                issuerName = (String)nameObject;
            } else {
                throw new CertStoreException(
                    "unrecognized issuerName: must be String or byte[]");
            }

            // 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;
    }

    // converts an LDAP URI into LDAPCertStoreParameters
    static LDAPCertStoreParameters getParameters(URI uri) {
        String host = uri.getHost();
        if (host == null) {
            return new SunLDAPCertStoreParameters();
        } else {
            int port = uri.getPort();
            return (port == -1
                    ? new SunLDAPCertStoreParameters(host)
                    : new SunLDAPCertStoreParameters(host, port));
        }
    }

    /*
     * Subclass of LDAPCertStoreParameters with overridden equals/hashCode
     * methods. This is necessary because the parameters are used as
     * keys in the LDAPCertStore cache.
     */
    private static class SunLDAPCertStoreParameters
        extends LDAPCertStoreParameters {

        private volatile int hashCode = 0;

        SunLDAPCertStoreParameters(String serverName, int port) {
            super(serverName, port);
        }
        SunLDAPCertStoreParameters(String serverName) {
            super(serverName);
        }
        SunLDAPCertStoreParameters() {
            super();
        }
        public boolean equals(Object obj) {
            if (!(obj instanceof LDAPCertStoreParameters)) {
                return false;
            }
            LDAPCertStoreParameters params = (LDAPCertStoreParameters) obj;
            return (getPort() == params.getPort() &&
                    getServerName().equalsIgnoreCase(params.getServerName()));
        }
        public int hashCode() {
            if (hashCode == 0) {
                int result = 17;
                result = 37*result + getPort();
                result = 37*result +
                    getServerName().toLowerCase(Locale.ENGLISH).hashCode();
                hashCode = result;
            }
            return hashCode;
        }
    }

    /*
     * This inner class wraps an existing X509CertSelector and adds
     * additional criteria to match on when the certificate's subject is
     * different than the LDAP Distinguished Name entry. The LDAPCertStore
     * implementation uses the subject DN as the directory entry for
     * looking up certificates. This can be problematic if the certificates
     * that you want to fetch have a different subject DN than the entry
     * where they are stored. You could set the selector's subject to the
     * LDAP DN entry, but then the resulting match would fail to find the
     * desired certificates because the subject DNs would not match. This
     * class avoids that problem by introducing a certSubject which should
     * be set to the certificate's subject DN when it is different than
     * the LDAP DN.
     */
    static class LDAPCertSelector extends X509CertSelector {

        private X500Principal certSubject;
        private X509CertSelector selector;
        private X500Principal subject;

        /**
         * Creates an LDAPCertSelector.
         *
         * @param selector the X509CertSelector to wrap
         * @param certSubject the subject DN of the certificate that you want
         *      to retrieve via LDAP
         * @param ldapDN the LDAP DN where the certificate is stored
         */
        LDAPCertSelector(X509CertSelector selector, X500Principal certSubject,
            String ldapDN) throws IOException {
            this.selector = selector == null ? new X509CertSelector() : selector;
            this.certSubject = certSubject;
            this.subject = new X500Name(ldapDN).asX500Principal();
        }

        // we only override the get (accessor methods) since the set methods
        // will not be invoked by the code that uses this LDAPCertSelector.
        public X509Certificate getCertificate() {
            return selector.getCertificate();
        }
        public BigInteger getSerialNumber() {
            return selector.getSerialNumber();
        }
        public X500Principal getIssuer() {
            return selector.getIssuer();
        }
        public String getIssuerAsString() {
            return selector.getIssuerAsString();
        }
        public byte[] getIssuerAsBytes() throws IOException {
            return selector.getIssuerAsBytes();
        }
        public X500Principal getSubject() {
            // return the ldap DN
            return subject;
        }
        public String getSubjectAsString() {
            // return the ldap DN
            return subject.getName();
        }
        public byte[] getSubjectAsBytes() throws IOException {
            // return the encoded ldap DN
            return subject.getEncoded();
        }
        public byte[] getSubjectKeyIdentifier() {
            return selector.getSubjectKeyIdentifier();
        }
        public byte[] getAuthorityKeyIdentifier() {
            return selector.getAuthorityKeyIdentifier();
        }
        public Date getCertificateValid() {
            return selector.getCertificateValid();
        }
        public Date getPrivateKeyValid() {
            return selector.getPrivateKeyValid();
        }
        public String getSubjectPublicKeyAlgID() {
            return selector.getSubjectPublicKeyAlgID();
        }
        public PublicKey getSubjectPublicKey() {
            return selector.getSubjectPublicKey();
        }
        public boolean[] getKeyUsage() {
            return selector.getKeyUsage();
        }
        public Set<String> getExtendedKeyUsage() {
            return selector.getExtendedKeyUsage();
        }
        public boolean getMatchAllSubjectAltNames() {
            return selector.getMatchAllSubjectAltNames();
        }
        public Collection<List<?>> getSubjectAlternativeNames() {
            return selector.getSubjectAlternativeNames();
        }
        public byte[] getNameConstraints() {
            return selector.getNameConstraints();
        }
        public int getBasicConstraints() {
            return selector.getBasicConstraints();
        }
        public Set<String> getPolicy() {
            return selector.getPolicy();
        }
        public Collection<List<?>> getPathToNames() {
            return selector.getPathToNames();
        }

        public boolean match(Certificate cert) {
            // temporarily set the subject criterion to the certSubject
            // so that match will not reject the desired certificates
            selector.setSubject(certSubject);
            boolean match = selector.match(cert);
            selector.setSubject(subject);
            return match;
        }
    }

    /**
     * This class has the same purpose as LDAPCertSelector except it is for
     * X.509 CRLs.
     */
    static class LDAPCRLSelector extends X509CRLSelector {

        private X509CRLSelector selector;
        private Collection<X500Principal> certIssuers;
        private Collection<X500Principal> issuers;
        private HashSet<Object> issuerNames;

        /**
         * Creates an LDAPCRLSelector.
         *
         * @param selector the X509CRLSelector to wrap
         * @param certIssuers the issuer DNs of the CRLs that you want
         *      to retrieve via LDAP
         * @param ldapDN the LDAP DN where the CRL is stored
         */
        LDAPCRLSelector(X509CRLSelector selector,
            Collection<X500Principal> certIssuers, String ldapDN)
            throws IOException {
            this.selector = selector == null ? new X509CRLSelector() : selector;
            this.certIssuers = certIssuers;
            issuerNames = new HashSet<>();
            issuerNames.add(ldapDN);
            issuers = new HashSet<>();
            issuers.add(new X500Name(ldapDN).asX500Principal());
        }
        // we only override the get (accessor methods) since the set methods
        // will not be invoked by the code that uses this LDAPCRLSelector.
        public Collection<X500Principal> getIssuers() {
            // return the ldap DN
            return Collections.unmodifiableCollection(issuers);
        }
        public Collection<Object> getIssuerNames() {
            // return the ldap DN
            return Collections.unmodifiableCollection(issuerNames);
        }
        public BigInteger getMinCRL() {
            return selector.getMinCRL();
        }
        public BigInteger getMaxCRL() {
            return selector.getMaxCRL();
        }
        public Date getDateAndTime() {
            return selector.getDateAndTime();
        }
        public X509Certificate getCertificateChecking() {
            return selector.getCertificateChecking();
        }
        public boolean match(CRL crl) {
            // temporarily set the issuer criterion to the certIssuers
            // so that match will not reject the desired CRL
            selector.setIssuers(certIssuers);
            boolean match = selector.match(crl);
            selector.setIssuers(issuers);
            return match;
        }
    }
}
