/*
 * Copyright (c) 2015, 2016, 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.
/** * 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 .privilegedGetProperty("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; } }, new ThreadPoolExecutor.DiscardPolicy()); threadMgr.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); threadMgr.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); threadMgr.setKeepAliveTime(5000, TimeUnit.MILLISECONDS); threadMgr.allowCoreThreadTimeOut(true); responseCache = Cache.newSoftMemoryCache(cacheCapacity, cacheLifetime); }
Get the current cache lifetime setting
Returns:the current cache lifetime value
/** * Get the current cache lifetime setting * * @return the current cache lifetime value */
int getCacheLifetime() { return cacheLifetime; }
Get the current maximum cache size.
Returns:the current maximum cache size
/** * 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.
Returns:the current default OCSP responder URI, or null if it has not been set.
/** * 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
Returns:true if URI override has been set, false otherwise.
/** * 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.
Returns:true if the StatusResponseManager will not pass OCSP Extensions in the TLS status_request[_v2] extensions, false if extensions will be passed (the default).
/** * 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
/** * Clear the status response cache */
void clear() { debugLog("Clearing response cache"); responseCache.clear(); }
Returns the number of currently valid objects in the response cache.
Returns:the number of valid objects in the response cache.
/** * 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 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.
Params:
  • cert – the subject to get the responder URI from
Throws:
Returns:a URI containing the address to the OCSP responder, or null if no AIA extension exists in the certificate and no default responder has been configured.
/** * 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
/** * 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).
Params:
  • type – the type of request being made of the StatusResponseManager
  • request – the StatusRequest from the status_request or status_request_v2 ClientHello extension. A value of null is interpreted as providing no responder IDs or extensions.
  • chain – an array of 2 or more certificates. Each certificate must be issued by the next certificate in the chain.
  • delay – the number of time units to delay before returning responses.
  • unit – the unit of time applied to the delay parameter
Throws:
  • SSLHandshakeException – if an unsupported StatusRequest is provided.
Returns:an unmodifiable Map containing the certificate and its usually
/** * 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 CertId.
Params:
  • cid – the CertId of the response to look up
  • ocspRequest – the OCSP request structure sent by the client in the TLS status_request[_v2] hello extension.
Returns:the ResponseCacheEntry for a specific CertId, or null if it is not found or a nonce extension has been requested by the caller.
/** * 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.
Params:
  • message – the message to be displayed
/** * 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.
/** * 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.
Params:
  • subjectCert – the certificate to be checked for revocation
  • issuerCert – the issuer of the subjectCert
Throws:
  • IOException – if CertId creation from the certificates fails
/** * 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.
Params:
  • subjectCert – the certificate to be checked for revocation
  • cid – the CertId for subjectCert
/** * 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.
Params:
  • orig – the original StatusInfo
/** * 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 StatusInfo
Returns:a String representation of this object
/** * 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 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
Params:
  • responseBytes – the DER encoding for the OCSP response
Throws:
  • IOException – if an OCSPResponse cannot be created from the encoded bytes.
/** * 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.
/** * 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.
Params:
  • info – the StatusInfo containing the subject certificate, CertId, and other supplemental info.
  • request – the OCSPStatusRequest containing any responder IDs and extensions.
/** * 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.
Returns:The StatusInfo object passed into the OCSPFetchCall constructor, with the responseData field filled in with the response or null if no response can be obtained.
/** * 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.
Params:
  • certId – The CertId for the OCSP response
  • entry – A cache entry containing the response bytes and the OCSPResponse built from those bytes.
/** * 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.
Params:
  • nextUpdate – a Date object corresponding to the next update time from a SingleResponse.
Returns: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.
/** * 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; } } }