Skip to content

Commit ec2f3f0

Browse files
committed
Add server-side OCSP stapling to the BCJSSE provider and the low-level TLS API, answering status_request and status_request_v2 with a certificate_status message for TLSv1.2 and, from the same TlsServer.getCertificateStatus callback, a per-CertificateEntry extension for TLSv1.3, behind jdk.tls.server.enableStatusRequestExtension and a per-SSLContext response cache, dropping the echoes a resumed handshake would otherwise replay, reading the TLSv1.3 form on the client, dropping a response too large for its CertificateEntry to carry rather than failing the handshake over it, and clearing the status_request_v2 and trusted_ca_keys a reused TlsServer would otherwise carry into a following handshake, relates to github #1157.
1 parent 2feaf10 commit ec2f3f0

31 files changed

Lines changed: 5066 additions & 104 deletions

CONTRIBUTORS.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@
607607
<li>Arpan Sharma &lt;https://github.com/Arpan0995&gt; - initial audit of BCPQC provider consistency starting with HQC, which led to the exposure of a number of issues in the JCA provider service interfaces for other BCPQC algorithms.</li>
608608
<li>Flowdalic &lt;https://github.com/Flowdalic&gt; - initial implementation of an AnimalSniffer-based Android API-level compatibility check for the Gradle build (PR #336).</li>
609609
<li>hannesa2 &lt;https://github.com/hannesa2&gt; - initial Dependabot configuration for the Gradle and GitHub Actions ecosystems (PR #883).</li>
610+
<li>vladhuma &lt;https://github.com/vladhuma&gt; - initial implementation of server-side OCSP stapling for the BCJSSE provider, on behalf of Thales Group (PR #1740).</li>
610611
</ul>
611612
</body>
612613
</html>

core/src/main/java/org/bouncycastle/util/Properties.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public class Properties
7070
public static final String X509_CRLDP_PROTOCOLS = "org.bouncycastle.x509.CRLDP_protocols";
7171

7272
/**
73-
* The largest OCSP response, in bytes, the CertPath validator will read from a responder.
73+
* The largest OCSP response, in bytes, the CertPath validator - or the JSSE server's OCSP
74+
* stapling fetch - will read from a responder.
7475
* A responder's Content-Length can narrow this but never widen it, so a responder declaring
7576
* (and sending) hundreds of megabytes is cut off rather than read into the heap. Default is
7677
* 64K, which is far above any real response; a value of zero or less is ignored and the

docs/releasenotes.html

Lines changed: 6 additions & 1 deletion
Large diffs are not rendered by default.

misc/src/main/java/org/bouncycastle/tls/examples/OCSPStaplingServerExample.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@
8686
* refreshed out of band, an in-process responder as here) is entirely the application's
8787
* choice, which is the point of the callback.
8888
* <p>
89-
* Two things the callback has to get right, both illustrated below:
89+
* Two things the callback has to get right up to (D)TLS 1.2, both illustrated below
90+
* (TLS 1.3 reads the shape differently &mdash; see <b>Scope</b>):
9091
* <ul>
9192
* <li>Which <i>shape</i> of response to return. Ask
9293
* {@code context.getSecurityParametersHandshake().getStatusRequestVersion()}: 1 means the
@@ -101,12 +102,20 @@
101102
* wants to answer RFC 6961 clients must override it.</li>
102103
* </ul>
103104
* <p>
104-
* <b>Scope:</b> the "certificate_status" handshake message exists only up to (D)TLS 1.2.
105-
* TLS 1.3 carries the response in a per-CertificateEntry "status_request" extension
106-
* instead (<a href="https://www.rfc-editor.org/rfc/rfc8446#section-4.4.2.1">RFC 8446 sec.
107-
* 4.4.2.1</a>) and {@code getCertificateStatus()} is not consulted there; this example
108-
* therefore negotiates TLS 1.2. Note also that this is the low-level TLS API &mdash; the
109-
* BCJSSE provider does not currently offer server-side stapling.
105+
* <b>Scope:</b> this example negotiates TLS 1.2, where the response travels as one
106+
* "certificate_status" handshake message covering the whole chain. That message does not
107+
* exist in TLS 1.3, which instead carries each response in a "status_request" extension of
108+
* the CertificateEntry holding the certificate it answers for
109+
* (<a href="https://www.rfc-editor.org/rfc/rfc8446#section-4.4.2.1">RFC 8446 sec.
110+
* 4.4.2.1</a>). The callback is the same one either way &mdash; {@code TlsServerProtocol}
111+
* distributes what it returns across those entries &mdash; so the only thing that changes
112+
* for an implementation is how the shape is chosen: the status request version is always 1
113+
* in TLS 1.3 ("status_request_v2" is left out of it by RFC 8446 sec. 4.2.1), so a server
114+
* with responses for the intermediates as well returns the
115+
* {@link CertificateStatusType#ocsp_multi} shape there regardless, answering positionally
116+
* against the chain. Note also that this is the low-level TLS API; the BCJSSE provider
117+
* fetches and staples responses of its own accord, behind the
118+
* {@code jdk.tls.server.enableStatusRequestExtension} system property.
110119
*/
111120
public class OCSPStaplingServerExample
112121
{

tls/src/main/java/org/bouncycastle/jsse/provider/ContextData.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ final class ContextData
3838
private final SignatureSchemeInfo.PerContext signatureSchemes;
3939
private final int maxHandshakeMessageSize;
4040
private final int handshakeTimeoutMillis;
41+
private final boolean serverEnableStatusRequest;
42+
private final OcspStapleCache ocspStapleCache;
4143

4244
ContextData(boolean fipsMode, JcaTlsCrypto crypto, BCX509ExtendedKeyManager x509KeyManager,
4345
BCX509ExtendedTrustManager x509TrustManager, Map<String, CipherSuiteInfo> supportedCipherSuites,
@@ -62,6 +64,15 @@ final class ContextData
6264
"jdk.tls.maxHandshakeMessageSize", 32768, 1024, Integer.MAX_VALUE);
6365
this.handshakeTimeoutMillis = PropertyUtils.getIntegerSystemProperty(
6466
"org.bouncycastle.jsse.handshakeTimeoutMillis", 0, 0, Integer.MAX_VALUE);
67+
/*
68+
* NOTE: read per context rather than once per class load, unlike the neighbouring jsse
69+
* switches. SunJSSE reads this one per context too (an SSLContextImpl instance field), so a
70+
* process can host one SSLContext that staples and another that does not.
71+
*/
72+
this.serverEnableStatusRequest = PropertyUtils.getBooleanSystemProperty(
73+
"jdk.tls.server.enableStatusRequestExtension", false);
74+
// nothing reaches the cache unless a server in this context staples, so don't build one
75+
this.ocspStapleCache = serverEnableStatusRequest ? OcspStapleCache.create(crypto.getHelper()) : null;
6576
}
6677

6778
int[] getActiveCipherSuites(JcaTlsCrypto crypto, ProvSSLParameters sslParameters,
@@ -207,6 +218,29 @@ int getHandshakeTimeoutMillis()
207218
return handshakeTimeoutMillis;
208219
}
209220

221+
/**
222+
* Whether a server in this context offers RFC 6066 status_request / RFC 6961 status_request_v2,
223+
* from <code>jdk.tls.server.enableStatusRequestExtension</code>. Off unless asked for, since
224+
* enabling it has the server make outbound OCSP requests on a client's behalf.
225+
*/
226+
boolean isServerEnableStatusRequest()
227+
{
228+
return serverEnableStatusRequest;
229+
}
230+
231+
/**
232+
* The OCSP responses available for a server in this context to staple. Held per context, as the
233+
* JDK holds its own StatusResponseManager, so that connections share it - a cache scoped to a
234+
* connection would never see a second hit.
235+
*
236+
* @return the cache, or null when {@link #isServerEnableStatusRequest()} is false and nothing
237+
* would ever ask it for a response.
238+
*/
239+
OcspStapleCache getOcspStapleCache()
240+
{
241+
return ocspStapleCache;
242+
}
243+
210244
NamedGroupInfo.PerConnection getNamedGroupsClient(ProvSSLParameters sslParameters,
211245
ProtocolVersion[] activeProtocolVersions)
212246
{

tls/src/main/java/org/bouncycastle/jsse/provider/JsseUtils.java

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import org.bouncycastle.tls.TlsContext;
5858
import org.bouncycastle.tls.TlsCredentialedDecryptor;
5959
import org.bouncycastle.tls.TlsCredentialedSigner;
60+
import org.bouncycastle.tls.TlsExtensionsUtils;
6061
import org.bouncycastle.tls.TlsUtils;
6162
import org.bouncycastle.tls.TrustedAuthority;
6263
import org.bouncycastle.tls.crypto.TlsCertificate;
@@ -471,10 +472,12 @@ static Certificate getCertificateMessage13(JcaTlsCrypto crypto, X509Certificate[
471472
{
472473
JcaTlsCertificate certificate = new JcaTlsCertificate(crypto, chain[i]);
473474

474-
// TODO[tls13] Support various extensions
475-
Hashtable<Integer, byte[]> extensions = null;
476-
477-
certificateEntryList[i] = new CertificateEntry(certificate, extensions);
475+
/*
476+
* TODO[tls13] Support various extensions. The OCSP staples of RFC 8446 sec. 4.4.2.1 are
477+
* attached by the protocol layer from getCertificateStatus() - see
478+
* TlsUtils.add13CertificateStatus.
479+
*/
480+
certificateEntryList[i] = new CertificateEntry(certificate, null);
478481
}
479482

480483
return new Certificate(certificateRequestContext, certificateEntryList);
@@ -615,6 +618,76 @@ static List<byte[]> getStatusResponses(CertificateStatus certificateStatus) thro
615618
return null;
616619
}
617620

621+
/**
622+
* The stapled responses from a TLS 1.3 Certificate message, where RFC 8446 sec. 4.4.2.1 carries
623+
* each one in a "status_request" extension of the CertificateEntry it belongs to rather than in a
624+
* CertificateStatus handshake message.
625+
* <p/>
626+
* The result is positional: element <code>i</code> answers for certificate <code>i</code> of the
627+
* chain, with a zero-length element where an entry carried no staple. That is the contract
628+
* {@link ProvX509TrustManager} relies on when it pairs the responses with the chain - appending
629+
* only the staples that are present would attribute an intermediate's response to the
630+
* end-entity.
631+
*
632+
* @return the responses, or null if no entry carried one.
633+
*/
634+
static List<byte[]> getStatusResponses13(TlsContext context, Certificate certificateMessage)
635+
throws IOException
636+
{
637+
/*
638+
* RFC 8446 4.2: a server does not answer an extension the client did not send. An
639+
* unsolicited staple is ignored rather than made a handshake failure, which is how this
640+
* library has always treated one.
641+
*/
642+
if (context.getSecurityParametersHandshake().getStatusRequestVersion() < 1)
643+
{
644+
return null;
645+
}
646+
647+
CertificateEntry[] certificateEntryList = certificateMessage.getCertificateEntryList();
648+
int count = certificateEntryList.length;
649+
650+
ArrayList<byte[]> statusResponses = new ArrayList<byte[]>(count);
651+
652+
boolean anyStatusResponse = false;
653+
for (int i = 0; i < count; ++i)
654+
{
655+
byte[] statusResponse = getStatusResponse13(context, certificateEntryList[i]);
656+
657+
anyStatusResponse |= (statusResponse.length > 0);
658+
statusResponses.add(statusResponse);
659+
}
660+
661+
return anyStatusResponse ? Collections.unmodifiableList(statusResponses) : null;
662+
}
663+
664+
private static byte[] getStatusResponse13(TlsContext context, CertificateEntry certificateEntry)
665+
throws IOException
666+
{
667+
Hashtable<?, ?> extensions = certificateEntry.getExtensions();
668+
if (null == extensions)
669+
{
670+
return TlsUtils.EMPTY_BYTES;
671+
}
672+
673+
byte[] extensionData = (byte[])extensions.get(TlsExtensionsUtils.EXT_status_request);
674+
if (null == extensionData)
675+
{
676+
return TlsUtils.EMPTY_BYTES;
677+
}
678+
679+
/*
680+
* RFC 8446 4.4.2.1: "the body of the "status_request" extension from the server MUST be a
681+
* CertificateStatus structure as defined in [RFC6066]" - so a single response, never the
682+
* ocsp_multi form. A malformed one is a decode_error, as it already is for the TLS 1.2
683+
* CertificateStatus handshake message.
684+
*/
685+
CertificateStatus certificateStatus = TlsExtensionsUtils.readStatusRequestExtension13(context,
686+
extensionData);
687+
688+
return getStatusResponse(certificateStatus.getOCSPResponse());
689+
}
690+
618691
static X500Principal[] getTrustedIssuers(Vector<TrustedAuthority> trustedCAKeys) throws IOException
619692
{
620693
if (null == trustedCAKeys || trustedCAKeys.isEmpty())
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.bouncycastle.jsse.provider;
2+
3+
import java.util.Date;
4+
5+
import org.bouncycastle.asn1.ocsp.OCSPResponse;
6+
7+
/**
8+
* An OCSP response held for stapling, together with the validity interval the responder stated for
9+
* the certificate it answers about.
10+
* <p/>
11+
* The response is carried verbatim as it arrived from the responder; it is <b>not</b> validated
12+
* here. A stapling server is a relay - RFC 6066 sec. 8 has it pass the responder's answer through to
13+
* the client, and the client is the party that verifies the responder's signature and decides what
14+
* the answer means. The times are extracted only so that {@link OcspStapleCache} can tell how long
15+
* the response may be reused, which is a caching question rather than a trust one.
16+
*/
17+
final class OcspStaple
18+
{
19+
private final OCSPResponse response;
20+
private final Date thisUpdate;
21+
private final Date nextUpdate;
22+
23+
/**
24+
* @param response the response as received from the responder.
25+
* @param thisUpdate the thisUpdate of the SingleResponse answering for the certificate.
26+
* @param nextUpdate the nextUpdate of that SingleResponse, or null if it stated none.
27+
*/
28+
OcspStaple(OCSPResponse response, Date thisUpdate, Date nextUpdate)
29+
{
30+
this.response = response;
31+
this.thisUpdate = copy(thisUpdate);
32+
this.nextUpdate = copy(nextUpdate);
33+
}
34+
35+
OCSPResponse getResponse()
36+
{
37+
return response;
38+
}
39+
40+
Date getThisUpdate()
41+
{
42+
return copy(thisUpdate);
43+
}
44+
45+
/**
46+
* @return the stated nextUpdate, or null if the responder stated none. RFC 6960 sec. 4.2.2.1
47+
* reads an absent nextUpdate as "newer revocation information is available all the
48+
* time", so a response without one is never reused from the cache.
49+
*/
50+
Date getNextUpdate()
51+
{
52+
return copy(nextUpdate);
53+
}
54+
55+
private static Date copy(Date date)
56+
{
57+
return null == date ? null : new Date(date.getTime());
58+
}
59+
}

0 commit comments

Comments
 (0)