view src/share/classes/sun/security/provider/certpath/URICertStore.java @ 4980:4e37ba90acda

8013228: Create new system properties to control allowable OCSP clock skew and CRL connection timeout Reviewed-by: vinnie
author andrew
date Wed, 26 Jun 2013 22:37:21 -0500
parents 23bdcede4e39
children
line wrap: on
line source

/*
 * Copyright (c) 2006, 2013, 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;

import java.io.InputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertSelector;
import java.security.cert.CertStore;
import java.security.cert.CertStoreException;
import java.security.cert.CertStoreParameters;
import java.security.cert.CertStoreSpi;
import java.security.cert.CRLException;
import java.security.cert.CRLSelector;
import java.security.cert.X509Certificate;
import java.security.cert.X509CertSelector;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLSelector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import sun.security.action.GetIntegerAction;
import sun.security.x509.AccessDescription;
import sun.security.x509.GeneralNameInterface;
import sun.security.x509.URIName;
import sun.security.util.Cache;
import sun.security.util.Debug;

/**
 * A <code>CertStore</code> that retrieves <code>Certificates</code> or
 * <code>CRL</code>s from a URI, for example, as specified in an X.509
 * AuthorityInformationAccess or CRLDistributionPoint extension.
 * <p>
 * For CRLs, this implementation retrieves a single DER encoded CRL per URI.
 * For Certificates, this implementation retrieves a single DER encoded CRL or
 * a collection of Certificates encoded as a PKCS#7 "certs-only" CMS message.
 * <p>
 * This <code>CertStore</code> also implements Certificate/CRL caching.
 * Currently, the cache is shared between all applications in the VM and uses a
 * hardcoded policy. The cache has a maximum size of 185 entries, which are held
 * by SoftReferences. A request will be satisfied from the cache if we last
 * checked for an update within CHECK_INTERVAL (last 30 seconds). Otherwise,
 * we open an URLConnection to download the Certificate(s)/CRL using an
 * If-Modified-Since request (HTTP) if possible. Note that both positive and
 * negative responses are cached, i.e. if we are unable to open the connection
 * or the Certificate(s)/CRL cannot be parsed, we remember this result and
 * additional calls during the CHECK_INTERVAL period do not try to open another
 * connection.
 * <p>
 * The URICertStore is not currently a standard CertStore type. We should
 * consider adding a standard "URI" CertStore type.
 *
 * @author Andreas Sterbenz
 * @author Sean Mullan
 * @since 7.0
 */
class URICertStore extends CertStoreSpi {

    private static final Debug debug = Debug.getInstance("certpath");

    // interval between checks for update of cached Certificates/CRLs
    // (30 seconds)
    private final static int CHECK_INTERVAL = 30 * 1000;

    // size of the cache (see Cache class for sizing recommendations)
    private final static int CACHE_SIZE = 185;

    // X.509 certificate factory instance
    private final CertificateFactory factory;

    // cached Collection of X509Certificates (may be empty, never null)
    private Collection<X509Certificate> certs =
        Collections.<X509Certificate>emptySet();

    // cached X509CRL (may be null)
    private X509CRL crl;

    // time we last checked for an update
    private long lastChecked;

    // time server returned as last modified time stamp
    // or 0 if not available
    private long lastModified;

    // the URI of this CertStore
    private URI uri;

    // true if URI is ldap
    private boolean ldap = false;
    private CertStore ldapCertStore;
    private String ldapPath;

    /**
     * Holder class to lazily load LDAPCertStoreHelper if present.
     */
    private static class LDAP {
        private static final String CERT_STORE_HELPER =
            "sun.security.provider.certpath.ldap.LDAPCertStoreHelper";
        private static final CertStoreHelper helper =
            AccessController.doPrivileged(
                new PrivilegedAction<CertStoreHelper>() {
                    public CertStoreHelper run() {
                        try {
                            Class<?> c = Class.forName(CERT_STORE_HELPER, true, null);
                            return (CertStoreHelper)c.newInstance();
                        } catch (ClassNotFoundException cnf) {
                            return null;
                        } catch (InstantiationException e) {
                            throw new AssertionError(e);
                        } catch (IllegalAccessException e) {
                            throw new AssertionError(e);
                        }
                    }});
        static CertStoreHelper helper() {
            return helper;
        }
    }

    // Default maximum connect timeout in milliseconds (15 seconds)
    // allowed when downloading CRLs
    private static final int DEFAULT_CRL_CONNECT_TIMEOUT = 15000;

    /**
     * Integer value indicating the connect timeout, in seconds, to be
     * used for the CRL download. A timeout of zero is interpreted as
     * an infinite timeout.
     */
    private static final int CRL_CONNECT_TIMEOUT = initializeTimeout();

    /**
     * Initialize the timeout length by getting the CRL timeout
     * system property. If the property has not been set, or if its
     * value is negative, set the timeout length to the default.
     */
    private static int initializeTimeout() {
        Integer tmp = java.security.AccessController.doPrivileged(
                new GetIntegerAction("com.sun.security.crl.timeout"));
        if (tmp == null || tmp < 0) {
            return DEFAULT_CRL_CONNECT_TIMEOUT;
        }
        // Convert to milliseconds, as the system property will be
        // specified in seconds
        return tmp * 1000;
    }

    /**
     * Creates a URICertStore.
     *
     * @param parameters specifying the URI
     */
    URICertStore(CertStoreParameters params)
        throws InvalidAlgorithmParameterException, NoSuchAlgorithmException {
        super(params);
        if (!(params instanceof URICertStoreParameters)) {
            throw new InvalidAlgorithmParameterException
                ("params must be instanceof URICertStoreParameters");
        }
        this.uri = ((URICertStoreParameters) params).uri;
        // if ldap URI, use an LDAPCertStore to fetch certs and CRLs
        if (uri.getScheme().toLowerCase(Locale.ENGLISH).equals("ldap")) {
            if (LDAP.helper() == null)
                throw new NoSuchAlgorithmException("LDAP not present");
            ldap = true;
            ldapCertStore = LDAP.helper().getCertStore(uri);
            ldapPath = uri.getPath();
            // strip off leading '/'
            if (ldapPath.charAt(0) == '/') {
                ldapPath = ldapPath.substring(1);
            }
        }
        try {
            factory = CertificateFactory.getInstance("X.509");
        } catch (CertificateException e) {
            throw new RuntimeException();
        }
    }

    /**
     * Returns a URI CertStore. This method consults a cache of
     * CertStores (shared per JVM) using the URI as a key.
     */
    private static final Cache certStoreCache =
        Cache.newSoftMemoryCache(CACHE_SIZE);
    static synchronized CertStore getInstance(URICertStoreParameters params)
        throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
        if (debug != null) {
            debug.println("CertStore URI:" + params.uri);
        }
        CertStore ucs = (CertStore) certStoreCache.get(params);
        if (ucs == null) {
            ucs = new UCS(new URICertStore(params), null, "URI", params);
            certStoreCache.put(params, ucs);
        } else {
            if (debug != null) {
                debug.println("URICertStore.getInstance: cache hit");
            }
        }
        return ucs;
    }

    /**
     * Creates a CertStore from information included in the AccessDescription
     * object of a certificate's Authority Information Access Extension.
     */
    static CertStore getInstance(AccessDescription ad) {
        if (!ad.getAccessMethod().equals(AccessDescription.Ad_CAISSUERS_Id)) {
            return null;
        }
        GeneralNameInterface gn = ad.getAccessLocation().getName();
        if (!(gn instanceof URIName)) {
            return null;
        }
        URI uri = ((URIName) gn).getURI();
        try {
            return URICertStore.getInstance
                (new URICertStore.URICertStoreParameters(uri));
        } catch (Exception ex) {
            if (debug != null) {
                debug.println("exception creating CertStore: " + ex);
                ex.printStackTrace();
            }
            return null;
        }
    }

    /**
     * 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.
     *
     * @param selector a <code>CertSelector</code> used to select which
     *  <code>X509Certificate</code>s should be returned. Specify
     *  <code>null</code> to return all <code>X509Certificate</code>s.
     * @return a <code>Collection</code> of <code>X509Certificate</code>s that
     *         match the specified selector
     * @throws CertStoreException if an exception occurs
     */
    public synchronized Collection<X509Certificate> engineGetCertificates
        (CertSelector selector) throws CertStoreException {

        // if ldap URI we wrap the CertSelector in an LDAPCertSelector to
        // avoid LDAP DN matching issues (see LDAPCertSelector for more info)
        if (ldap) {
            X509CertSelector xsel = (X509CertSelector) selector;
            try {
                xsel = LDAP.helper().wrap(xsel, xsel.getSubject(), ldapPath);
            } catch (IOException ioe) {
                throw new CertStoreException(ioe);
            }
            // Fetch the certificates via LDAP. LDAPCertStore has its own
            // caching mechanism, see the class description for more info.
            return (Collection<X509Certificate>)
                ldapCertStore.getCertificates(xsel);
        }

        // Return the Certificates for this entry. It returns the cached value
        // if it is still current and fetches the Certificates otherwise.
        // For the caching details, see the top of this class.
        long time = System.currentTimeMillis();
        if (time - lastChecked < CHECK_INTERVAL) {
            if (debug != null) {
                debug.println("Returning certificates from cache");
            }
            return getMatchingCerts(certs, selector);
        }
        lastChecked = time;
        InputStream in = null;
        try {
            URLConnection connection = uri.toURL().openConnection();
            if (lastModified != 0) {
                connection.setIfModifiedSince(lastModified);
            }
            in = connection.getInputStream();
            long oldLastModified = lastModified;
            lastModified = connection.getLastModified();
            if (oldLastModified != 0) {
                if (oldLastModified == lastModified) {
                    if (debug != null) {
                        debug.println("Not modified, using cached copy");
                    }
                    return getMatchingCerts(certs, selector);
                } else if (connection instanceof HttpURLConnection) {
                    // some proxy servers omit last modified
                    HttpURLConnection hconn = (HttpURLConnection) connection;
                    if (hconn.getResponseCode()
                                == HttpURLConnection.HTTP_NOT_MODIFIED) {
                        if (debug != null) {
                            debug.println("Not modified, using cached copy");
                        }
                        return getMatchingCerts(certs, selector);
                    }
                }
            }
            if (debug != null) {
                debug.println("Downloading new certificates...");
            }
            certs = (Collection<X509Certificate>)
                factory.generateCertificates(in);
            return getMatchingCerts(certs, selector);
        } catch (IOException e) {
            if (debug != null) {
                debug.println("Exception fetching certificates:");
                e.printStackTrace();
            }
        } catch (CertificateException e) {
            if (debug != null) {
                debug.println("Exception fetching certificates:");
                e.printStackTrace();
            }
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
        // exception, forget previous values
        lastModified = 0;
        certs = Collections.<X509Certificate>emptySet();
        return certs;
    }

    /**
     * Iterates over the specified Collection of X509Certificates and
     * returns only those that match the criteria specified in the
     * CertSelector.
     */
    private static Collection<X509Certificate> getMatchingCerts
        (Collection<X509Certificate> certs, CertSelector selector) {
        // if selector not specified, all certs match
        if (selector == null) {
            return certs;
        }
        List<X509Certificate> matchedCerts =
            new ArrayList<X509Certificate>(certs.size());
        for (X509Certificate cert : certs) {
            if (selector.match(cert)) {
                matchedCerts.add(cert);
            }
        }
        return matchedCerts;
    }

    /**
     * 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.
     *
     * @param selector A <code>CRLSelector</code> used to select which
     *  <code>X509CRL</code>s should be returned. Specify <code>null</code>
     *  to return all <code>X509CRL</code>s.
     * @return A <code>Collection</code> of <code>X509CRL</code>s that
     *         match the specified selector
     * @throws CertStoreException if an exception occurs
     */
    public synchronized Collection<X509CRL> engineGetCRLs(CRLSelector selector)
        throws CertStoreException {

        // if ldap URI we wrap the CRLSelector in an LDAPCRLSelector to
        // avoid LDAP DN matching issues (see LDAPCRLSelector for more info)
        if (ldap) {
            X509CRLSelector xsel = (X509CRLSelector) selector;
            try {
                xsel = LDAP.helper().wrap(xsel, null, ldapPath);
            } catch (IOException ioe) {
                throw new CertStoreException(ioe);
            }
            // Fetch the CRLs via LDAP. LDAPCertStore has its own
            // caching mechanism, see the class description for more info.
            return (Collection<X509CRL>) ldapCertStore.getCRLs(xsel);
        }

        // Return the CRLs for this entry. It returns the cached value
        // if it is still current and fetches the CRLs otherwise.
        // For the caching details, see the top of this class.
        long time = System.currentTimeMillis();
        if (time - lastChecked < CHECK_INTERVAL) {
            if (debug != null) {
                debug.println("Returning CRL from cache");
            }
            return getMatchingCRLs(crl, selector);
        }
        lastChecked = time;
        InputStream in = null;
        try {
            URLConnection connection = uri.toURL().openConnection();
            if (lastModified != 0) {
                connection.setIfModifiedSince(lastModified);
            }
            connection.setConnectTimeout(CRL_CONNECT_TIMEOUT);
            in = connection.getInputStream();
            long oldLastModified = lastModified;
            lastModified = connection.getLastModified();
            if (oldLastModified != 0) {
                if (oldLastModified == lastModified) {
                    if (debug != null) {
                        debug.println("Not modified, using cached copy");
                    }
                    return getMatchingCRLs(crl, selector);
                } else if (connection instanceof HttpURLConnection) {
                    // some proxy servers omit last modified
                    HttpURLConnection hconn = (HttpURLConnection) connection;
                    if (hconn.getResponseCode()
                                == HttpURLConnection.HTTP_NOT_MODIFIED) {
                        if (debug != null) {
                            debug.println("Not modified, using cached copy");
                        }
                        return getMatchingCRLs(crl, selector);
                    }
                }
            }
            if (debug != null) {
                debug.println("Downloading new CRL...");
            }
            crl = (X509CRL) factory.generateCRL(in);
            return getMatchingCRLs(crl, selector);
        } catch (IOException e) {
            if (debug != null) {
                debug.println("Exception fetching CRL:");
                e.printStackTrace();
            }
        } catch (CRLException e) {
            if (debug != null) {
                debug.println("Exception fetching CRL:");
                e.printStackTrace();
            }
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
        // exception, forget previous values
        lastModified = 0;
        crl = null;
        return Collections.<X509CRL>emptyList();
    }

    /**
     * Checks if the specified X509CRL matches the criteria specified in the
     * CRLSelector.
     */
    private static Collection<X509CRL> getMatchingCRLs
        (X509CRL crl, CRLSelector selector) {
        if (selector == null || (crl != null && selector.match(crl))) {
            return Collections.<X509CRL>singletonList(crl);
        } else {
            return Collections.<X509CRL>emptyList();
        }
    }

    /**
     * CertStoreParameters for the URICertStore.
     */
    static class URICertStoreParameters implements CertStoreParameters {
        private final URI uri;
        private volatile int hashCode = 0;
        URICertStoreParameters(URI uri) {
            this.uri = uri;
        }
        public boolean equals(Object obj) {
            if (!(obj instanceof URICertStoreParameters)) {
                return false;
            }
            URICertStoreParameters params = (URICertStoreParameters) obj;
            return uri.equals(params.uri);
        }
        public int hashCode() {
            if (hashCode == 0) {
                int result = 17;
                result = 37*result + uri.hashCode();
                hashCode = result;
            }
            return hashCode;
        }
        public Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {
                /* Cannot happen */
                throw new InternalError(e.toString());
            }
        }
    }

    /**
     * This class allows the URICertStore to be accessed as a CertStore.
     */
    private static class UCS extends CertStore {
        protected UCS(CertStoreSpi spi, Provider p, String type,
            CertStoreParameters params) {
            super(spi, p, type, params);
        }
    }
}