Skip to content

Commit 1942c79

Browse files
committed
Only reuse a cached OCSP response that states a nextUpdate, as RFC 6960 sec. 4.2.2.1 has an absent one mean newer information is always available, and treat a response dated ahead of the validation time by more than the clock skew allowance as unreliable.
1 parent e597b7a commit 1942c79

4 files changed

Lines changed: 164 additions & 2 deletions

File tree

‎docs/releasenotes.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ <h3>2.1.2 Defects Fixed</h3>
114114
<li>X509RevocationChecker downloaded CRLs from a certificate's CRL Distribution Points extension whenever the CRLs it had been given could not answer for a certificate, with no way for a caller to prevent it - unlike the provider's CertPath validator, which has always required the org.bouncycastle.x509.enableCRLDP property to be set, and unlike what that property's own javadoc describes ("the BC CertPath validator and X509RevocationChecker will attempt to download CRLs ..."). The checker now honours the property, so the URI taken from a certificate is only dereferenced when a caller has asked for that. <b>This is a behavioural change for anyone relying on the previous automatic fetch</b>: with the property unset the checker now behaves exactly as an unproductive fetch always did, soft failing where setSoftFail was configured and reporting no CRL found where it was not, so such callers should set org.bouncycastle.x509.enableCRLDP to "true" to keep the old behaviour.</li>
115115
<li>OcspCache.getOcspResponse read an OCSP response up to the length the responder itself declared in its Content-Length header, applying its own 32K ceiling only when that header was absent, so a responder (or anything able to answer in its place) that declared and sent hundreds of megabytes was read into the caller's heap. The declared length may now only narrow the read, never widen it: it is used when present and no larger than the ceiling, which is otherwise applied. The ceiling is now configurable through the new org.bouncycastle.ocsp.max_response_size property and its default has been raised from 32K to 64K, still far above any real response; a configured value of zero or less is ignored so a mistyped value cannot turn the limit off. An over-long response fails the OCSP check the same way an unreachable responder does - a caller with CRLs configured falls back to those - and now says so, as Streams.readAllLimited reports the condition only as "Data Overflow", which is restated in terms of the limit and the property that sets it.</li>
116116
<li>ArmoredInputStream parses the OpenPGP ASCII armor headers as the stream is constructed, and bounded neither the length of a header line nor the number of them, so merely wrapping an untrusted stream could exhaust the heap before the caller had read a byte. Two shapes reached it: a "header line" that never arrives at a line terminator, accumulated without limit, and short header lines that never stop arriving, added to the header list without limit - the second is the companion case, reachable even where every line is well formed. Both are now capped, at 4096 bytes per header line and 64 headers (counting the armor header line itself), each configurable through the new org.bouncycastle.openpgp.max_armor_header_length and org.bouncycastle.openpgp.max_armor_headers properties; a value of zero or less is ignored, so a mistyped value cannot turn a limit off. Exceeding either raises ArmoredInputException, as the other malformed-header cases in that parser do. RFC 9580 sec. 6.2 bounds neither, but the headers it describes - Version, Comment and the like - are short, so the defaults sit far above any real armor. The equivalent accumulation in org.bouncycastle.util.io.pem.PemReader is deliberately left unbounded: a PEM body can legitimately be enormous - a CRL in the gigabyte range - so a default cap there would break working deployments.</li>
117+
<li>An OCSP response carrying no nextUpdate could be cached and reused as though it stated a validity interval, so a response could go on answering for a certificate after the responder had newer information about it - after a revocation, in particular. Any such reuse was bounded only by garbage collection rather than by an interval: OcspCache holds each responder's response map through a WeakReference, itself in a WeakHashMap, and nothing else refers to that map once the call returns, so entries last until the next collection. That is unpredictable rather than long - short on a busy JVM, potentially much longer on a large heap that collects rarely - and it is not a window the responder or the caller had any say in. RFC 6960 sec. 4.2.2.1 says the opposite of what that assumes: "if nextUpdate is not set, the responder is indicating that newer revocation information is available all the time", which is a statement that there is no interval to reuse the response over, not that it never expires. OcspCache now separates the two questions it had been asking with one method: a response arriving from the responder is accepted as before, whether or not it states a nextUpdate, while only a response that states one may be served from the cache afterwards. Nothing is rejected that was previously accepted - a responder that omits nextUpdate simply costs another request per validation, which is what "available all the time" asks for. Additionally, both the cached and the caller-supplied (stapled) paths now apply RFC 6960 sec. 4.2.2.1's other freshness rule, "responses whose thisUpdate time is later than the local system time SHOULD be considered unreliable", which neither had checked: a response dated ahead of the time being validated for by more than a 15 minute clock-skew allowance is treated as unreliable, raising "OCSP response not yet valid" on the stapled path.</li>
117118
</ul>
118119

119120
<h3>2.1.3 Additional Features and Functionality</h3>

‎prov/src/main/java/org/bouncycastle/jce/provider/OcspCache.java‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ class OcspCache
5050
private static final int DEFAULT_TIMEOUT = 15000;
5151
private static final int DEFAULT_MAX_RESPONSE_SIZE = 64 * 1024;
5252

53+
/**
54+
* Tolerance between our clock and the responder's when judging whether a response is dated in
55+
* the future. 15 minutes, matching what the JDK's own OCSP client allows.
56+
*/
57+
static final long MAX_CLOCK_SKEW_MS = 15 * 60 * 1000L;
58+
5359
private static Map<URI, WeakReference<Map<CertID, OCSPResponse>>> cache
5460
= Collections.synchronizedMap(new WeakHashMap<URI, WeakReference<Map<CertID, OCSPResponse>>>());
5561

@@ -75,7 +81,7 @@ static synchronized OCSPResponse getOcspResponse(
7581
BasicOCSPResponse basicResp = BasicOCSPResponse.getInstance(
7682
ASN1OctetString.getInstance(response.getResponseBytes().getResponse()).getOctets());
7783

78-
boolean matchFound = isCertIDFoundAndCurrent(basicResp, parameters.getValidDate(), certID);
84+
boolean matchFound = isCertIDFoundAndReusable(basicResp, parameters.getValidDate(), certID);
7985
if (matchFound)
8086
{
8187
return response;
@@ -262,7 +268,32 @@ static int getResponseSizeLimit(int contentLength)
262268
return contentLength;
263269
}
264270

265-
private static boolean isCertIDFoundAndCurrent(BasicOCSPResponse basicResp, Date validDate, CertID certID)
271+
/**
272+
* Whether the response answers for certID and is usable at validDate. Applied to a response as
273+
* it arrives from the responder, so a missing nextUpdate is no objection - the responder is
274+
* entitled not to state one.
275+
*/
276+
static boolean isCertIDFoundAndCurrent(BasicOCSPResponse basicResp, Date validDate, CertID certID)
277+
{
278+
return findResponse(basicResp, validDate, certID, false);
279+
}
280+
281+
/**
282+
* Whether a response already held in the cache may answer for certID at validDate - the same
283+
* question, plus the one the cache adds: does it state a validity interval to reuse it over?
284+
* <p/>
285+
* RFC 6960 sec. 4.2.2.1 reads "if nextUpdate is not set, the responder is indicating that newer
286+
* revocation information is available all the time", so a response without one is never
287+
* reusable. Nothing is rejected by this: such a response is still used for the check it arrived
288+
* for, it just costs another request next time rather than being served from here.
289+
*/
290+
static boolean isCertIDFoundAndReusable(BasicOCSPResponse basicResp, Date validDate, CertID certID)
291+
{
292+
return findResponse(basicResp, validDate, certID, true);
293+
}
294+
295+
private static boolean findResponse(BasicOCSPResponse basicResp, Date validDate, CertID certID,
296+
boolean requireNextUpdate)
266297
{
267298
ResponseData responseData = ResponseData.getInstance(basicResp.getTbsResponseData());
268299
ASN1Sequence s = responseData.getResponses();
@@ -274,12 +305,24 @@ private static boolean isCertIDFoundAndCurrent(BasicOCSPResponse basicResp, Date
274305
if (certID.equals(resp.getCertID()))
275306
{
276307
ASN1GeneralizedTime nextUp = resp.getNextUpdate();
308+
if (nextUp == null && requireNextUpdate)
309+
{
310+
return false;
311+
}
312+
277313
try
278314
{
279315
if (nextUp != null && validDate.after(nextUp.getDate()))
280316
{
281317
return false;
282318
}
319+
320+
// "Responses whose thisUpdate time is later than the local system time SHOULD
321+
// be considered unreliable" - RFC 6960 sec. 4.2.2.1, allowing for clock skew
322+
if (isFromTheFuture(resp.getThisUpdate(), validDate))
323+
{
324+
return false;
325+
}
283326
}
284327
catch (ParseException e)
285328
{
@@ -293,4 +336,14 @@ private static boolean isCertIDFoundAndCurrent(BasicOCSPResponse basicResp, Date
293336

294337
return false;
295338
}
339+
340+
/**
341+
* Whether thisUpdate is later than the time being validated for, by more than the clock skew
342+
* allowed between us and the responder.
343+
*/
344+
static boolean isFromTheFuture(ASN1GeneralizedTime thisUpdate, Date validDate)
345+
throws ParseException
346+
{
347+
return thisUpdate != null && thisUpdate.getDate().getTime() > (validDate.getTime() + MAX_CLOCK_SKEW_MS);
348+
}
296349
}

‎prov/src/main/java/org/bouncycastle/jce/provider/ProvOcspRevocationChecker.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,13 @@ public void check(Certificate certificate)
279279
{
280280
throw new CertPathValidatorException("OCSP response expired");
281281
}
282+
// "Responses whose thisUpdate time is later than the local
283+
// system time SHOULD be considered unreliable" - RFC 6960
284+
// sec. 4.2.2.1, allowing for clock skew
285+
if (OcspCache.isFromTheFuture(resp.getThisUpdate(), parameters.getValidDate()))
286+
{
287+
throw new CertPathValidatorException("OCSP response not yet valid");
288+
}
282289
if (certID == null || !isEqualAlgId(certID.getHashAlgorithm(), resp.getCertID().getHashAlgorithm()))
283290
{
284291
org.bouncycastle.asn1.x509.Certificate issuer = extractCert();

‎prov/src/test/java/org/bouncycastle/jce/provider/OcspCacheTest.java‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,26 @@
11
package org.bouncycastle.jce.provider;
22

33
import java.io.ByteArrayInputStream;
4+
import java.math.BigInteger;
5+
import java.util.Date;
46

57
import junit.framework.TestCase;
8+
import org.bouncycastle.asn1.ASN1GeneralizedTime;
9+
import org.bouncycastle.asn1.ASN1Integer;
10+
import org.bouncycastle.asn1.ASN1Sequence;
11+
import org.bouncycastle.asn1.DERBitString;
12+
import org.bouncycastle.asn1.DEROctetString;
13+
import org.bouncycastle.asn1.DERSequence;
14+
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
15+
import org.bouncycastle.asn1.ocsp.BasicOCSPResponse;
16+
import org.bouncycastle.asn1.ocsp.CertID;
17+
import org.bouncycastle.asn1.ocsp.CertStatus;
18+
import org.bouncycastle.asn1.ocsp.ResponderID;
19+
import org.bouncycastle.asn1.ocsp.ResponseData;
20+
import org.bouncycastle.asn1.ocsp.SingleResponse;
21+
import org.bouncycastle.asn1.x500.X500Name;
22+
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
23+
import org.bouncycastle.asn1.x509.Extensions;
624
import org.bouncycastle.util.Arrays;
725
import org.bouncycastle.util.Properties;
826
import org.bouncycastle.util.io.StreamOverflowException;
@@ -90,4 +108,87 @@ public void testOverLongResponseNamesTheLimit()
90108

91109
assertTrue(Arrays.areEqual(response, OcspCache.readResponse(new ByteArrayInputStream(response), 1024)));
92110
}
111+
/**
112+
* A cached response is only reusable while it states a validity interval covering the time
113+
* being validated for. RFC 6960 sec. 4.2.2.1: "if nextUpdate is not set, the responder is
114+
* indicating that newer revocation information is available all the time" - so there is no
115+
* interval to reuse it over, and the cache must go back to the responder.
116+
*/
117+
public void testResponseWithoutNextUpdateIsNeverCurrent()
118+
throws Exception
119+
{
120+
Date now = new Date();
121+
CertID certID = certID();
122+
123+
BasicOCSPResponse withNextUpdate = response(certID, minutesFromNow(now, -5), minutesFromNow(now, 60));
124+
assertTrue("response inside its own validity interval was not current",
125+
OcspCache.isCertIDFoundAndCurrent(withNextUpdate, now, certID));
126+
127+
BasicOCSPResponse expired = response(certID, minutesFromNow(now, -120), minutesFromNow(now, -60));
128+
assertFalse("expired response was current", OcspCache.isCertIDFoundAndCurrent(expired, now, certID));
129+
130+
BasicOCSPResponse noNextUpdate = response(certID, minutesFromNow(now, -5), null);
131+
assertFalse("response with no nextUpdate was reused from the cache",
132+
OcspCache.isCertIDFoundAndReusable(noNextUpdate, now, certID));
133+
134+
// however old it is
135+
BasicOCSPResponse ancient = response(certID, minutesFromNow(now, -60 * 24 * 365), null);
136+
assertFalse("year-old response with no nextUpdate was reused from the cache",
137+
OcspCache.isCertIDFoundAndReusable(ancient, now, certID));
138+
139+
// but nothing is rejected by that: a responder is entitled not to state a nextUpdate, and
140+
// the response it just gave us is used for the check it arrived for
141+
assertTrue("freshly fetched response with no nextUpdate was refused",
142+
OcspCache.isCertIDFoundAndCurrent(noNextUpdate, now, certID));
143+
144+
// the interval is still honoured where one is stated
145+
assertTrue("response inside its validity interval was not reusable",
146+
OcspCache.isCertIDFoundAndReusable(withNextUpdate, now, certID));
147+
assertFalse("expired response was reusable", OcspCache.isCertIDFoundAndReusable(expired, now, certID));
148+
}
149+
150+
/**
151+
* "Responses whose thisUpdate time is later than the local system time SHOULD be considered
152+
* unreliable" - RFC 6960 sec. 4.2.2.1. Clock skew between us and the responder is allowed for.
153+
*/
154+
public void testResponseDatedInTheFuture()
155+
throws Exception
156+
{
157+
Date now = new Date();
158+
CertID certID = certID();
159+
160+
BasicOCSPResponse withinSkew = response(certID, minutesFromNow(now, 5), minutesFromNow(now, 60));
161+
assertTrue("response inside the clock skew allowance was rejected",
162+
OcspCache.isCertIDFoundAndCurrent(withinSkew, now, certID));
163+
164+
BasicOCSPResponse fromTheFuture = response(certID, minutesFromNow(now, 60), minutesFromNow(now, 120));
165+
assertFalse("response dated an hour ahead was current",
166+
OcspCache.isCertIDFoundAndCurrent(fromTheFuture, now, certID));
167+
168+
assertFalse("absent thisUpdate treated as future", OcspCache.isFromTheFuture(null, now));
169+
}
170+
171+
private static ASN1GeneralizedTime minutesFromNow(Date now, int minutes)
172+
{
173+
return new ASN1GeneralizedTime(new Date(now.getTime() + (minutes * 60 * 1000L)));
174+
}
175+
176+
private static CertID certID()
177+
{
178+
return new CertID(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256),
179+
new DEROctetString(new byte[32]), new DEROctetString(new byte[32]), new ASN1Integer(BigInteger.ONE));
180+
}
181+
182+
private static BasicOCSPResponse response(CertID certID, ASN1GeneralizedTime thisUpdate,
183+
ASN1GeneralizedTime nextUpdate)
184+
{
185+
SingleResponse single = new SingleResponse(certID, new CertStatus(), thisUpdate, nextUpdate,
186+
(Extensions)null);
187+
188+
ResponseData responseData = new ResponseData(new ResponderID(new X500Name("CN=Test Responder")),
189+
thisUpdate, new DERSequence(single), (Extensions)null);
190+
191+
return new BasicOCSPResponse(responseData, new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256),
192+
new DERBitString(new byte[]{ 1 }), (ASN1Sequence)null);
193+
}
93194
}

0 commit comments

Comments
 (0)