| /* |
| * 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.ssl; |
| |
| import java.io.IOException; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.security.AccessController; |
| import java.security.cert.X509Certificate; |
| import java.security.cert.Extension; |
| import java.util.*; |
| import java.util.concurrent.*; |
| |
| import sun.security.provider.certpath.CertId; |
| import sun.security.provider.certpath.OCSP; |
| import sun.security.provider.certpath.OCSPResponse; |
| import sun.security.provider.certpath.ResponderId; |
| import sun.security.util.Cache; |
| import sun.security.x509.PKIXExtensions; |
| import sun.security.x509.SerialNumber; |
| import sun.security.action.GetBooleanAction; |
| import sun.security.action.GetIntegerAction; |
| import sun.security.action.GetPropertyAction; |
| |
| final class StatusResponseManager { |
| private static final int DEFAULT_CORE_THREADS = 8; |
| private static final int DEFAULT_CACHE_SIZE = 256; |
| private static final int DEFAULT_CACHE_LIFETIME = 3600; // seconds |
| private static final Debug debug = Debug.getInstance("ssl"); |
| |
| private final ScheduledThreadPoolExecutor threadMgr; |
| private final Cache<CertId, ResponseCacheEntry> responseCache; |
| private final URI defaultResponder; |
| private final boolean respOverride; |
| private final int cacheCapacity; |
| private final int cacheLifetime; |
| private final boolean ignoreExtensions; |
| |
| /** |
| * Create a StatusResponseManager with default parameters. |
| */ |
| StatusResponseManager() { |
| int cap = AccessController.doPrivileged( |
| new GetIntegerAction("jdk.tls.stapling.cacheSize", |
| DEFAULT_CACHE_SIZE)); |
| cacheCapacity = cap > 0 ? cap : 0; |
| |
| int life = AccessController.doPrivileged( |
| new GetIntegerAction("jdk.tls.stapling.cacheLifetime", |
| DEFAULT_CACHE_LIFETIME)); |
| cacheLifetime = life > 0 ? life : 0; |
| |
| String uriStr = |
| GetPropertyAction.getProperty("jdk.tls.stapling.responderURI"); |
| URI tmpURI; |
| try { |
| tmpURI = ((uriStr != null && !uriStr.isEmpty()) ? |
| new URI(uriStr) : null); |
| } catch (URISyntaxException urise) { |
| tmpURI = null; |
| } |
| defaultResponder = tmpURI; |
| |
| respOverride = AccessController.doPrivileged( |
| new GetBooleanAction("jdk.tls.stapling.responderOverride")); |
| ignoreExtensions = AccessController.doPrivileged( |
| new GetBooleanAction("jdk.tls.stapling.ignoreExtensions")); |
| |
| threadMgr = new ScheduledThreadPoolExecutor(DEFAULT_CORE_THREADS, |
| new ThreadFactory() { |
| @Override |
| public Thread newThread(Runnable r) { |
| Thread t = Executors.defaultThreadFactory().newThread(r); |
| t.setDaemon(true); |
| return t; |
| } |
| }); |
| threadMgr.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); |
| threadMgr.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); |
| threadMgr.setKeepAliveTime(5000, TimeUnit.MILLISECONDS); |
| responseCache = Cache.newSoftMemoryCache(cacheCapacity, cacheLifetime); |
| } |
| |
| /** |
| * Get the current cache lifetime setting |
| * |
| * @return the current cache lifetime value |
| */ |
| int getCacheLifetime() { |
| return cacheLifetime; |
| } |
| |
| /** |
| * Get the current maximum cache size. |
| * |
| * @return the current maximum cache size |
| */ |
| int getCacheCapacity() { |
| return cacheCapacity; |
| } |
| |
| /** |
| * Get the default OCSP responder URI, if previously set. |
| * |
| * @return the current default OCSP responder URI, or {@code null} if |
| * it has not been set. |
| */ |
| URI getDefaultResponder() { |
| return defaultResponder; |
| } |
| |
| /** |
| * Get the URI override setting |
| * |
| * @return {@code true} if URI override has been set, {@code false} |
| * otherwise. |
| */ |
| boolean getURIOverride() { |
| return respOverride; |
| } |
| |
| /** |
| * Get the ignore extensions setting. |
| * |
| * @return {@code true} if the {@code StatusResponseManager} will not |
| * pass OCSP Extensions in the TLS {@code status_request[_v2]} extensions, |
| * {@code false} if extensions will be passed (the default). |
| */ |
| boolean getIgnoreExtensions() { |
| return ignoreExtensions; |
| } |
| |
| /** |
| * Clear the status response cache |
| */ |
| void clear() { |
| debugLog("Clearing response cache"); |
| responseCache.clear(); |
| } |
| |
| /** |
| * Returns the number of currently valid objects in the response cache. |
| * |
| * @return the number of valid objects in the response cache. |
| */ |
| int size() { |
| return responseCache.size(); |
| } |
| |
| /** |
| * Obtain the URI use by the {@code StatusResponseManager} during lookups. |
| * This method takes into account not only the AIA extension from a |
| * certificate to be checked, but also any default URI and possible |
| * override settings for the response manager. |
| * |
| * @param cert the subject to get the responder URI from |
| * |
| * @return a {@code URI} containing the address to the OCSP responder, or |
| * {@code null} if no AIA extension exists in the certificate and no |
| * default responder has been configured. |
| * |
| * @throws NullPointerException if {@code cert} is {@code null}. |
| */ |
| URI getURI(X509Certificate cert) { |
| Objects.requireNonNull(cert); |
| |
| if (cert.getExtensionValue( |
| PKIXExtensions.OCSPNoCheck_Id.toString()) != null) { |
| debugLog("OCSP NoCheck extension found. OCSP will be skipped"); |
| return null; |
| } else if (defaultResponder != null && respOverride) { |
| debugLog("Responder override: URI is " + defaultResponder); |
| return defaultResponder; |
| } else { |
| URI certURI = OCSP.getResponderURI(cert); |
| return (certURI != null ? certURI : defaultResponder); |
| } |
| } |
| |
| /** |
| * Shutdown the thread pool |
| */ |
| void shutdown() { |
| debugLog("Shutting down " + threadMgr.getActiveCount() + |
| " active threads"); |
| threadMgr.shutdown(); |
| } |
| |
| /** |
| * Get a list of responses for a chain of certificates. |
| * This will find OCSP responses from the cache, or failing that, directly |
| * contact the OCSP responder. It is assumed that the certificates in |
| * the provided chain are in their proper order (from end-entity to |
| * trust anchor). |
| * |
| * @param type the type of request being made of the |
| * {@code StatusResponseManager} |
| * @param request the {@code StatusRequest} from the status_request or |
| * status_request_v2 ClientHello extension. A value of {@code null} |
| * is interpreted as providing no responder IDs or extensions. |
| * @param chain an array of 2 or more certificates. Each certificate must |
| * be issued by the next certificate in the chain. |
| * @param delay the number of time units to delay before returning |
| * responses. |
| * @param unit the unit of time applied to the {@code delay} parameter |
| * |
| * @return an unmodifiable {@code Map} containing the certificate and |
| * its usually |
| * |
| * @throws SSLHandshakeException if an unsupported {@code StatusRequest} |
| * is provided. |
| */ |
| Map<X509Certificate, byte[]> get(StatusRequestType type, |
| StatusRequest request, X509Certificate[] chain, long delay, |
| TimeUnit unit) { |
| Map<X509Certificate, byte[]> responseMap = new HashMap<>(); |
| List<OCSPFetchCall> requestList = new ArrayList<>(); |
| |
| debugLog("Beginning check: Type = " + type + ", Chain length = " + |
| chain.length); |
| |
| // It is assumed that the caller has ordered the certs in the chain |
| // in the proper order (each certificate is issued by the next entry |
| // in the provided chain). |
| if (chain.length < 2) { |
| return Collections.emptyMap(); |
| } |
| |
| if (type == StatusRequestType.OCSP) { |
| try { |
| // For type OCSP, we only check the end-entity certificate |
| OCSPStatusRequest ocspReq = (OCSPStatusRequest)request; |
| CertId cid = new CertId(chain[1], |
| new SerialNumber(chain[0].getSerialNumber())); |
| ResponseCacheEntry cacheEntry = getFromCache(cid, ocspReq); |
| if (cacheEntry != null) { |
| responseMap.put(chain[0], cacheEntry.ocspBytes); |
| } else { |
| StatusInfo sInfo = new StatusInfo(chain[0], cid); |
| requestList.add(new OCSPFetchCall(sInfo, ocspReq)); |
| } |
| } catch (IOException exc) { |
| debugLog("Exception during CertId creation: " + exc); |
| } |
| } else if (type == StatusRequestType.OCSP_MULTI) { |
| // For type OCSP_MULTI, we check every cert in the chain that |
| // has a direct issuer at the next index. We won't have an issuer |
| // certificate for the last certificate in the chain and will |
| // not be able to create a CertId because of that. |
| OCSPStatusRequest ocspReq = (OCSPStatusRequest)request; |
| int ctr; |
| for (ctr = 0; ctr < chain.length - 1; ctr++) { |
| try { |
| // The cert at "ctr" is the subject cert, "ctr + 1" is the |
| // issuer certificate. |
| CertId cid = new CertId(chain[ctr + 1], |
| new SerialNumber(chain[ctr].getSerialNumber())); |
| ResponseCacheEntry cacheEntry = getFromCache(cid, ocspReq); |
| if (cacheEntry != null) { |
| responseMap.put(chain[ctr], cacheEntry.ocspBytes); |
| } else { |
| StatusInfo sInfo = new StatusInfo(chain[ctr], cid); |
| requestList.add(new OCSPFetchCall(sInfo, ocspReq)); |
| } |
| } catch (IOException exc) { |
| debugLog("Exception during CertId creation: " + exc); |
| } |
| } |
| } else { |
| debugLog("Unsupported status request type: " + type); |
| } |
| |
| // If we were able to create one or more Fetches, go and run all |
| // of them in separate threads. For all the threads that completed |
| // in the allotted time, put those status responses into the returned |
| // Map. |
| if (!requestList.isEmpty()) { |
| try { |
| // Set a bunch of threads to go do the fetching |
| List<Future<StatusInfo>> resultList = |
| threadMgr.invokeAll(requestList, delay, unit); |
| |
| // Go through the Futures and from any non-cancelled task, |
| // get the bytes and attach them to the responseMap. |
| for (Future<StatusInfo> task : resultList) { |
| if (task.isDone()) { |
| if (!task.isCancelled()) { |
| StatusInfo info = task.get(); |
| if (info != null && info.responseData != null) { |
| responseMap.put(info.cert, |
| info.responseData.ocspBytes); |
| } else { |
| debugLog("Completed task had no response data"); |
| } |
| } else { |
| debugLog("Found cancelled task"); |
| } |
| } |
| } |
| } catch (InterruptedException | ExecutionException exc) { |
| // Not sure what else to do here |
| debugLog("Exception when getting data: " + exc); |
| } |
| } |
| |
| return Collections.unmodifiableMap(responseMap); |
| } |
| |
| /** |
| * Check the cache for a given {@code CertId}. |
| * |
| * @param cid the CertId of the response to look up |
| * @param ocspRequest the OCSP request structure sent by the client |
| * in the TLS status_request[_v2] hello extension. |
| * |
| * @return the {@code ResponseCacheEntry} for a specific CertId, or |
| * {@code null} if it is not found or a nonce extension has been |
| * requested by the caller. |
| */ |
| private ResponseCacheEntry getFromCache(CertId cid, |
| OCSPStatusRequest ocspRequest) { |
| // Determine if the nonce extension is present in the request. If |
| // so, then do not attempt to retrieve the response from the cache. |
| for (Extension ext : ocspRequest.getExtensions()) { |
| if (ext.getId().equals(PKIXExtensions.OCSPNonce_Id.toString())) { |
| debugLog("Nonce extension found, skipping cache check"); |
| return null; |
| } |
| } |
| |
| ResponseCacheEntry respEntry = responseCache.get(cid); |
| |
| // If the response entry has a nextUpdate and it has expired |
| // before the cache expiration, purge it from the cache |
| // and do not return it as a cache hit. |
| if (respEntry != null && respEntry.nextUpdate != null && |
| respEntry.nextUpdate.before(new Date())) { |
| debugLog("nextUpdate threshold exceeded, purging from cache"); |
| respEntry = null; |
| } |
| |
| debugLog("Check cache for SN" + cid.getSerialNumber() + ": " + |
| (respEntry != null ? "HIT" : "MISS")); |
| return respEntry; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder("StatusResponseManager: "); |
| |
| sb.append("Core threads: ").append(threadMgr.getCorePoolSize()); |
| sb.append(", Cache timeout: "); |
| if (cacheLifetime > 0) { |
| sb.append(cacheLifetime).append(" seconds"); |
| } else { |
| sb.append(" indefinite"); |
| } |
| |
| sb.append(", Cache MaxSize: "); |
| if (cacheCapacity > 0) { |
| sb.append(cacheCapacity).append(" items"); |
| } else { |
| sb.append(" unbounded"); |
| } |
| |
| sb.append(", Default URI: "); |
| if (defaultResponder != null) { |
| sb.append(defaultResponder); |
| } else { |
| sb.append("NONE"); |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Log messages through the SSL Debug facility. |
| * |
| * @param message the message to be displayed |
| */ |
| static void debugLog(String message) { |
| if (debug != null && Debug.isOn("respmgr")) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("[").append(Thread.currentThread().getName()); |
| sb.append("] ").append(message); |
| System.out.println(sb.toString()); |
| } |
| } |
| |
| /** |
| * Inner class used to group request and response data. |
| */ |
| class StatusInfo { |
| final X509Certificate cert; |
| final CertId cid; |
| final URI responder; |
| ResponseCacheEntry responseData; |
| |
| /** |
| * Create a StatusInfo object from certificate data. |
| * |
| * @param subjectCert the certificate to be checked for revocation |
| * @param issuerCert the issuer of the {@code subjectCert} |
| * |
| * @throws IOException if CertId creation from the certificates fails |
| */ |
| StatusInfo(X509Certificate subjectCert, X509Certificate issuerCert) |
| throws IOException { |
| this(subjectCert, new CertId(issuerCert, |
| new SerialNumber(subjectCert.getSerialNumber()))); |
| } |
| |
| /** |
| * Create a StatusInfo object from an existing subject certificate |
| * and its corresponding CertId. |
| * |
| * @param subjectCert the certificate to be checked for revocation |
| * @param cid the CertId for {@code subjectCert} |
| */ |
| StatusInfo(X509Certificate subjectCert, CertId certId) { |
| cert = subjectCert; |
| cid = certId; |
| responder = getURI(cert); |
| responseData = null; |
| } |
| |
| /** |
| * Copy constructor (used primarily for rescheduling). |
| * This will do a member-wise copy with the exception of the |
| * responseData and extensions fields, which should not persist |
| * in a rescheduled fetch. |
| * |
| * @param orig the original {@code StatusInfo} |
| */ |
| StatusInfo(StatusInfo orig) { |
| this.cert = orig.cert; |
| this.cid = orig.cid; |
| this.responder = orig.responder; |
| this.responseData = null; |
| } |
| |
| /** |
| * Return a String representation of the {@code StatusInfo} |
| * |
| * @return a {@code String} representation of this object |
| */ |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder("StatusInfo:"); |
| sb.append("\n\tCert: ").append(this.cert.getSubjectX500Principal()); |
| sb.append("\n\tSerial: ").append(this.cert.getSerialNumber()); |
| sb.append("\n\tResponder: ").append(this.responder); |
| sb.append("\n\tResponse data: ").append(this.responseData != null ? |
| (this.responseData.ocspBytes.length + " bytes") : "<NULL>"); |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * Static nested class used as the data kept in the response cache. |
| */ |
| static class ResponseCacheEntry { |
| final OCSPResponse.ResponseStatus status; |
| final byte[] ocspBytes; |
| final Date nextUpdate; |
| final OCSPResponse.SingleResponse singleResp; |
| final ResponderId respId; |
| |
| /** |
| * Create a new cache entry from the raw bytes of the response |
| * |
| * @param responseBytes the DER encoding for the OCSP response |
| * |
| * @throws IOException if an {@code OCSPResponse} cannot be created from |
| * the encoded bytes. |
| */ |
| ResponseCacheEntry(byte[] responseBytes, CertId cid) |
| throws IOException { |
| Objects.requireNonNull(responseBytes, |
| "Non-null responseBytes required"); |
| Objects.requireNonNull(cid, "Non-null Cert ID required"); |
| |
| ocspBytes = responseBytes.clone(); |
| OCSPResponse oResp = new OCSPResponse(ocspBytes); |
| status = oResp.getResponseStatus(); |
| respId = oResp.getResponderId(); |
| singleResp = oResp.getSingleResponse(cid); |
| if (status == OCSPResponse.ResponseStatus.SUCCESSFUL) { |
| if (singleResp != null) { |
| // Pull out the nextUpdate field in advance because the |
| // Date is cloned. |
| nextUpdate = singleResp.getNextUpdate(); |
| } else { |
| throw new IOException("Unable to find SingleResponse for " + |
| "SN " + cid.getSerialNumber()); |
| } |
| } else { |
| nextUpdate = null; |
| } |
| } |
| } |
| |
| /** |
| * Inner Callable class that does the actual work of looking up OCSP |
| * responses, first looking at the cache and doing OCSP requests if |
| * a cache miss occurs. |
| */ |
| class OCSPFetchCall implements Callable<StatusInfo> { |
| StatusInfo statInfo; |
| OCSPStatusRequest ocspRequest; |
| List<Extension> extensions; |
| List<ResponderId> responderIds; |
| |
| /** |
| * A constructor that builds the OCSPFetchCall from the provided |
| * StatusInfo and information from the status_request[_v2] extension. |
| * |
| * @param info the {@code StatusInfo} containing the subject |
| * certificate, CertId, and other supplemental info. |
| * @param request the {@code OCSPStatusRequest} containing any |
| * responder IDs and extensions. |
| */ |
| public OCSPFetchCall(StatusInfo info, OCSPStatusRequest request) { |
| statInfo = Objects.requireNonNull(info, |
| "Null StatusInfo not allowed"); |
| ocspRequest = Objects.requireNonNull(request, |
| "Null OCSPStatusRequest not allowed"); |
| extensions = ocspRequest.getExtensions(); |
| responderIds = ocspRequest.getResponderIds(); |
| } |
| |
| /** |
| * Get an OCSP response, either from the cache or from a responder. |
| * |
| * @return The StatusInfo object passed into the {@code OCSPFetchCall} |
| * constructor, with the {@code responseData} field filled in with the |
| * response or {@code null} if no response can be obtained. |
| */ |
| @Override |
| public StatusInfo call() { |
| debugLog("Starting fetch for SN " + statInfo.cid.getSerialNumber()); |
| try { |
| ResponseCacheEntry cacheEntry; |
| List<Extension> extsToSend; |
| |
| if (statInfo.responder == null) { |
| // If we have no URI then there's nothing to do but return |
| debugLog("Null URI detected, OCSP fetch aborted."); |
| return statInfo; |
| } else { |
| debugLog("Attempting fetch from " + statInfo.responder); |
| } |
| |
| // If the StatusResponseManager has been configured to not |
| // forward extensions, then set extensions to an empty list. |
| // We will forward the extensions unless one of two conditions |
| // occur: (1) The jdk.tls.stapling.ignoreExtensions property is |
| // true or (2) There is a non-empty ResponderId list. |
| // ResponderId selection is a feature that will be |
| // supported in the future. |
| extsToSend = (ignoreExtensions || !responderIds.isEmpty()) ? |
| Collections.emptyList() : extensions; |
| |
| byte[] respBytes = OCSP.getOCSPBytes( |
| Collections.singletonList(statInfo.cid), |
| statInfo.responder, extsToSend); |
| |
| if (respBytes != null) { |
| // Place the data into the response cache |
| cacheEntry = new ResponseCacheEntry(respBytes, |
| statInfo.cid); |
| |
| // Get the response status and act on it appropriately |
| debugLog("OCSP Status: " + cacheEntry.status + |
| " (" + respBytes.length + " bytes)"); |
| if (cacheEntry.status == |
| OCSPResponse.ResponseStatus.SUCCESSFUL) { |
| // Set the response in the returned StatusInfo |
| statInfo.responseData = cacheEntry; |
| |
| // Add the response to the cache (if applicable) |
| addToCache(statInfo.cid, cacheEntry); |
| } |
| } else { |
| debugLog("No data returned from OCSP Responder"); |
| } |
| } catch (IOException ioe) { |
| debugLog("Caught exception: " + ioe); |
| } |
| |
| return statInfo; |
| } |
| |
| /** |
| * Add a response to the cache. |
| * |
| * @param certId The {@code CertId} for the OCSP response |
| * @param entry A cache entry containing the response bytes and |
| * the {@code OCSPResponse} built from those bytes. |
| */ |
| private void addToCache(CertId certId, ResponseCacheEntry entry) { |
| // If no cache lifetime has been set on entries then |
| // don't cache this response if there is no nextUpdate field |
| if (entry.nextUpdate == null && cacheLifetime == 0) { |
| debugLog("Not caching this OCSP response"); |
| } else { |
| responseCache.put(certId, entry); |
| debugLog("Added response for SN " + certId.getSerialNumber() + |
| " to cache"); |
| } |
| } |
| |
| /** |
| * Determine the delay to use when scheduling the task that will |
| * update the OCSP response. This is the shorter time between the |
| * cache lifetime and the nextUpdate. If no nextUpdate is present in |
| * the response, then only the cache lifetime is used. |
| * If cache timeouts are disabled (a zero value) and there's no |
| * nextUpdate, then the entry is not cached and no rescheduling will |
| * take place. |
| * |
| * @param nextUpdate a {@code Date} object corresponding to the |
| * next update time from a SingleResponse. |
| * |
| * @return the number of seconds of delay before the next fetch |
| * should be executed. A zero value means that the fetch |
| * should happen immediately, while a value less than zero |
| * indicates no rescheduling should be done. |
| */ |
| private long getNextTaskDelay(Date nextUpdate) { |
| long delaySec; |
| int lifetime = getCacheLifetime(); |
| |
| if (nextUpdate != null) { |
| long nuDiffSec = (nextUpdate.getTime() - |
| System.currentTimeMillis()) / 1000; |
| delaySec = lifetime > 0 ? Long.min(nuDiffSec, lifetime) : |
| nuDiffSec; |
| } else { |
| delaySec = lifetime > 0 ? lifetime : -1; |
| } |
| |
| return delaySec; |
| } |
| } |
| } |