| /* | |
| * Copyright (c) 2015, 2024, 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.cert.Extension; | |
| import java.security.cert.X509Certificate; | |
| 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.ssl.X509Authentication.X509Possession; | |
| import sun.security.util.Cache; | |
| import sun.security.util.Debug; | |
| import sun.security.x509.PKIXExtensions; | |
| import sun.security.x509.SerialNumber; | |
| import static sun.security.ssl.CertStatusExtension.*; | |
| 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 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 = Integer.getInteger( | |
| "jdk.tls.stapling.cacheSize", | |
| DEFAULT_CACHE_SIZE); | |
| cacheCapacity = cap > 0 ? cap : 0; | |
| int life = Integer.getInteger( | |
| "jdk.tls.stapling.cacheLifetime", | |
| DEFAULT_CACHE_LIFETIME); | |
| cacheLifetime = life > 0 ? life : 0; | |
| String uriStr = System.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 = Boolean.getBoolean("jdk.tls.stapling.responderOverride"); | |
| ignoreExtensions = Boolean.getBoolean | |
| ("jdk.tls.stapling.ignoreExtensions"); | |
| threadMgr = new ScheduledThreadPoolExecutor(DEFAULT_CORE_THREADS, | |
| 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); | |
| } | |
| /** | |
| * 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) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "OCSP NoCheck extension found. OCSP will be skipped"); | |
| } | |
| return null; | |
| } else if (defaultResponder != null && respOverride) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Responder override: URI is " + defaultResponder); | |
| } | |
| return defaultResponder; | |
| } else { | |
| URI certURI = OCSP.getResponderURI(cert); | |
| return (certURI != null ? certURI : defaultResponder); | |
| } | |
| } | |
| /** | |
| * 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 CertStatusRequest} 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 | |
| */ | |
| Map<X509Certificate, byte[]> get(CertStatusRequestType type, | |
| CertStatusRequest request, X509Certificate[] chain, long delay, | |
| TimeUnit unit) { | |
| Map<X509Certificate, byte[]> responseMap = new HashMap<>(); | |
| List<OCSPFetchCall> requestList = new ArrayList<>(); | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "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 == CertStatusRequestType.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) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Exception during CertId creation: ", exc); | |
| } | |
| } | |
| } else if (type == CertStatusRequestType.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) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Exception during CertId creation: ", exc); | |
| } | |
| } | |
| } | |
| } else { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine("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()) { | |
| continue; | |
| } | |
| if (!task.isCancelled()) { | |
| StatusInfo info; | |
| try { | |
| info = task.get(); | |
| } catch (ExecutionException exc) { | |
| // Check for an underlying cause available and log | |
| // that, otherwise just log the ExecutionException | |
| Throwable cause = Optional.ofNullable( | |
| exc.getCause()).orElse(exc); | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine("Exception during OCSP fetch: " + | |
| cause); | |
| } | |
| continue; | |
| } | |
| if (info != null && info.responseData != null) { | |
| responseMap.put(info.cert, | |
| info.responseData.ocspBytes); | |
| } else if (SSLLogger.isOn && | |
| SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Completed task had no response data"); | |
| } | |
| } else { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine("Found cancelled task"); | |
| } | |
| } | |
| } | |
| } catch (InterruptedException intex) { | |
| // Log and reset the interrupt state | |
| Thread.currentThread().interrupt(); | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine("Interrupt occurred while fetching: " + | |
| intex); | |
| } | |
| } | |
| } | |
| 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.extensions) { | |
| if (ext.getId().equals( | |
| PKIXExtensions.OCSPNonce_Id.toString())) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "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())) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "nextUpdate threshold exceeded, purging from cache"); | |
| } | |
| respEntry = null; | |
| } | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Check cache for SN" + Debug.toString(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(); | |
| } | |
| /** | |
| * 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 an existing subject certificate | |
| * and its corresponding CertId. | |
| * | |
| * @param subjectCert the certificate to be checked for revocation | |
| * @param certId the CertId for {@code subjectCert} | |
| */ | |
| StatusInfo(X509Certificate subjectCert, CertId certId) { | |
| cert = subjectCert; | |
| cid = certId; | |
| responder = getURI(cert); | |
| responseData = null; | |
| } | |
| /** | |
| * Return a String representation of the {@code StatusInfo} | |
| * | |
| * @return a {@code String} representation of this object | |
| */ | |
| @Override | |
| public String toString() { | |
| return "StatusInfo:" + "\n\tCert: " + | |
| this.cert.getSubjectX500Principal() + | |
| "\n\tSerial: " + Debug.toString(this.cert.getSerialNumber()) + | |
| "\n\tResponder: " + this.responder + | |
| "\n\tResponse data: " + | |
| (this.responseData != null ? | |
| (this.responseData.ocspBytes.length + " bytes") : | |
| "<NULL>"); | |
| } | |
| } | |
| /** | |
| * 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 " + | |
| Debug.toString(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.extensions; | |
| responderIds = ocspRequest.responderIds; | |
| } | |
| /** | |
| * 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() { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Starting fetch for SN " + | |
| Debug.toString(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. | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Null URI detected, OCSP fetch aborted"); | |
| } | |
| return statInfo; | |
| } else { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "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); | |
| // Place the data into the response cache | |
| cacheEntry = new ResponseCacheEntry(respBytes, | |
| statInfo.cid); | |
| // Get the response status and act on it appropriately | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine("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); | |
| } | |
| } catch (IOException ioe) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine("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) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine("Not caching this OCSP response"); | |
| } | |
| } else { | |
| responseCache.put(certId, entry); | |
| if (SSLLogger.isOn && SSLLogger.isOn("respmgr")) { | |
| SSLLogger.fine( | |
| "Added response for SN " + | |
| Debug.toString(certId.getSerialNumber()) + | |
| " to cache"); | |
| } | |
| } | |
| } | |
| } | |
| static StaplingParameters processStapling(ServerHandshakeContext shc) { | |
| StaplingParameters params = null; | |
| SSLExtension ext = null; | |
| CertStatusRequestType type = null; | |
| CertStatusRequest req = null; | |
| Map<X509Certificate, byte[]> responses; | |
| // If this feature has not been enabled, then no more processing | |
| // is necessary. Also, we will only staple if we're doing a full | |
| // handshake. | |
| if (!shc.sslContext.isStaplingEnabled(false) || shc.isResumption) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine("Staping disabled or is a resumed session"); | |
| } | |
| return null; | |
| } | |
| // Check if the client has asserted the status_request[_v2] extension(s) | |
| Map<SSLExtension, SSLExtension.SSLExtensionSpec> exts = | |
| shc.handshakeExtensions; | |
| CertStatusRequestSpec statReq = (CertStatusRequestSpec)exts.get( | |
| SSLExtension.CH_STATUS_REQUEST); | |
| CertStatusRequestV2Spec statReqV2 = (CertStatusRequestV2Spec) | |
| exts.get(SSLExtension.CH_STATUS_REQUEST_V2); | |
| // Determine which type of stapling we are doing and assert the | |
| // proper extension in the server hello. | |
| // Favor status_request_v2 over status_request and ocsp_multi | |
| // over ocsp. | |
| // If multiple ocsp or ocsp_multi types exist, select the first | |
| // instance of a given type. Also since we don't support ResponderId | |
| // selection yet, only accept a request if the ResponderId field | |
| // is empty. Finally, we'll only do this in (D)TLS 1.2 or earlier. | |
| if (statReqV2 != null && !shc.negotiatedProtocol.useTLS13PlusSpec()) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake,verbose")) { | |
| SSLLogger.fine("SH Processing status_request_v2 extension"); | |
| } | |
| // RFC 6961 stapling | |
| ext = SSLExtension.CH_STATUS_REQUEST_V2; | |
| int ocspIdx = -1; | |
| int ocspMultiIdx = -1; | |
| CertStatusRequest[] reqItems = statReqV2.certStatusRequests; | |
| for (int pos = 0; (pos < reqItems.length && | |
| (ocspIdx == -1 || ocspMultiIdx == -1)); pos++) { | |
| CertStatusRequest item = reqItems[pos]; | |
| CertStatusRequestType curType = | |
| CertStatusRequestType.valueOf(item.statusType); | |
| if (ocspIdx < 0 && curType == CertStatusRequestType.OCSP) { | |
| OCSPStatusRequest ocspReq = (OCSPStatusRequest)item; | |
| // We currently only accept empty responder ID lists | |
| // but may support them in the future | |
| if (ocspReq.responderIds.isEmpty()) { | |
| ocspIdx = pos; | |
| } | |
| } else if (ocspMultiIdx < 0 && | |
| curType == CertStatusRequestType.OCSP_MULTI) { | |
| OCSPStatusRequest ocspReq = (OCSPStatusRequest)item; | |
| // We currently only accept empty responder ID lists | |
| // but may support them in the future | |
| if (ocspReq.responderIds.isEmpty()) { | |
| ocspMultiIdx = pos; | |
| } | |
| } | |
| } | |
| if (ocspMultiIdx >= 0) { | |
| req = reqItems[ocspMultiIdx]; | |
| type = CertStatusRequestType.valueOf(req.statusType); | |
| } else if (ocspIdx >= 0) { | |
| req = reqItems[ocspIdx]; | |
| type = CertStatusRequestType.valueOf(req.statusType); | |
| } else { | |
| if (SSLLogger.isOn && | |
| SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.finest("Warning: No suitable request " + | |
| "found in the status_request_v2 extension."); | |
| } | |
| } | |
| } | |
| // Only attempt to process a status_request extension if: | |
| // * The status_request extension is set AND | |
| // * either the status_request_v2 extension is not present OR | |
| // * none of the underlying OCSPStatusRequest structures is | |
| // suitable for stapling. | |
| // If either of the latter two bullet items is true the ext, | |
| // type and req variables should all be null. If any are null | |
| // we will try processing an asserted status_request. | |
| if ((statReq != null) && | |
| (ext == null || type == null || req == null)) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake,verbose")) { | |
| SSLLogger.fine("SH Processing status_request extension"); | |
| } | |
| ext = SSLExtension.CH_STATUS_REQUEST; | |
| type = CertStatusRequestType.valueOf( | |
| statReq.statusRequest.statusType); | |
| if (type == CertStatusRequestType.OCSP) { | |
| // If the type is OCSP, then the request is guaranteed | |
| // to be OCSPStatusRequest | |
| OCSPStatusRequest ocspReq = | |
| (OCSPStatusRequest)statReq.statusRequest; | |
| if (ocspReq.responderIds.isEmpty()) { | |
| req = ocspReq; | |
| } else { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.finest("Warning: No suitable request " + | |
| "found in the status_request extension."); | |
| } | |
| } | |
| } | |
| } | |
| // If, after walking through the extensions we were unable to | |
| // find a suitable StatusRequest, then stapling is disabled. | |
| // The ext, type and req variables must have been set to continue. | |
| if (type == null || req == null || ext == null) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.fine("No suitable status_request or " + | |
| "status_request_v2, stapling is disabled"); | |
| } | |
| return null; | |
| } | |
| // Get the cert chain since we'll need it for OCSP checking | |
| X509Possession x509Possession = null; | |
| for (SSLPossession possession : shc.handshakePossessions) { | |
| if (possession instanceof X509Possession) { | |
| x509Possession = (X509Possession)possession; | |
| break; | |
| } | |
| } | |
| if (x509Possession == null) { // unlikely | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.finest("Warning: no X.509 certificates found. " + | |
| "Stapling is disabled."); | |
| } | |
| return null; | |
| } | |
| // Get the OCSP responses from the StatusResponseManager | |
| X509Certificate[] certs = x509Possession.popCerts; | |
| StatusResponseManager statRespMgr = | |
| shc.sslContext.getStatusResponseManager(); | |
| if (statRespMgr != null) { | |
| // For the purposes of the fetch from the SRM, override the | |
| // type when it is TLS 1.3 so it always gets responses for | |
| // all certs it can. This should not change the type field | |
| // in the StaplingParameters though. | |
| CertStatusRequestType fetchType = | |
| shc.negotiatedProtocol.useTLS13PlusSpec() ? | |
| CertStatusRequestType.OCSP_MULTI : type; | |
| responses = statRespMgr.get(fetchType, req, certs, | |
| shc.statusRespTimeout, TimeUnit.MILLISECONDS); | |
| if (!responses.isEmpty()) { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.finest("Response manager returned " + | |
| responses.size() + " entries."); | |
| } | |
| // If this RFC 6066-style stapling (SSL cert only) then the | |
| // response cannot be zero length | |
| if (type == CertStatusRequestType.OCSP) { | |
| byte[] respDER = responses.get(certs[0]); | |
| if (respDER == null || respDER.length == 0) { | |
| if (SSLLogger.isOn && | |
| SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.finest("Warning: Null or zero-length " + | |
| "response found for leaf certificate. " + | |
| "Stapling is disabled."); | |
| } | |
| return null; | |
| } | |
| } | |
| params = new StaplingParameters(ext, type, req, responses); | |
| } else { | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.finest("Warning: no OCSP responses obtained. " + | |
| "Stapling is disabled."); | |
| } | |
| } | |
| } else { | |
| // This should not happen, but if lazy initialization of the | |
| // StatusResponseManager doesn't occur we should turn off stapling. | |
| if (SSLLogger.isOn && SSLLogger.isOn("ssl,handshake")) { | |
| SSLLogger.finest("Warning: lazy initialization " + | |
| "of the StatusResponseManager failed. " + | |
| "Stapling is disabled."); | |
| } | |
| } | |
| return params; | |
| } | |
| /** | |
| * Inner class used to hold stapling parameters needed by the handshaker | |
| * when stapling is active. | |
| */ | |
| static final class StaplingParameters { | |
| final SSLExtension statusRespExt; | |
| final CertStatusRequestType statReqType; | |
| final CertStatusRequest statReqData; | |
| final Map<X509Certificate, byte[]> responseMap; | |
| StaplingParameters(SSLExtension ext, CertStatusRequestType type, | |
| CertStatusRequest req, Map<X509Certificate, byte[]> responses) { | |
| statusRespExt = ext; | |
| statReqType = type; | |
| statReqData = req; | |
| responseMap = responses; | |
| } | |
| } | |
| } |