Skip to content

Commit 9492bab

Browse files
committed
TLS 1.3 Certificate entry extensions must now correspond to ones in the ClientHello or CertificateRequest (unsupported_extension), and a server requires the client's certificate_request_context to match its CertificateRequest (illegal_parameter).
1 parent 8bf734b commit 9492bab

9 files changed

Lines changed: 405 additions & 36 deletions

File tree

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Date: 2026, TBD
7979
- TLS: A TLS 1.3 client rejected every server certificate whose TLS feature extension (RFC 7633) listed status_request ("must-staple") whenever it had offered status_request itself, since the feature was looked for only in EncryptedExtensions, where RFC 8446 does not permit it; TLS 1.3 carries the OCSP response in the end-entity certificate's CertificateEntry. A feature is now also satisfied by the extensions of that CertificateEntry, and a feature whose extension has no place in TLS 1.3, such as status_request_v2, is no longer enforced once TLS 1.3 is negotiated (github #2458).
8080
- (D)TLS: A server certificate whose TLS feature extension listed status_request or status_request_v2 was accepted without any OCSP response for it when the server acknowledged the status request but then sent no CertificateStatus message, which RFC 6066 permits, or sent an ocsp_multi list with no response for the end-entity certificate. Such a certificate now requires a stapled OCSP response for the end-entity certificate, in every protocol version. A TLS feature in force against a (D)TLS 1.2 ServerHello that carried no extensions at all now fails with a certificate_unknown alert rather than a NullPointerException.
8181
- TLS: A TLS 1.3 client aborted the handshake with an illegal_parameter alert when the server sent the cached_info or record_size_limit extension in EncryptedExtensions, or the server_name extension in a CertificateRequest, since these were missing from the table of extensions permitted in each TLS 1.3 message.
82+
- TLS: The extensions of the CertificateEntry list in a TLS 1.3 Certificate message were not checked against what the receiving endpoint had asked for, although RFC 8446 sec. 4.4.2 requires those in a server's Certificate to correspond to extensions in the ClientHello and those in a client's Certificate to extensions in the CertificateRequest: a client ignored an OCSP response the server stapled without being asked, and a server accepted whatever a client attached. An unrequested extension in any entry now aborts the handshake with an unsupported_extension alert, as RFC 8446 sec. 4.2 requires. A TLS 1.3 server also now rejects a client Certificate whose certificate_request_context differs from that of its CertificateRequest, with an illegal_parameter alert.
8283

8384
### 2.1.3 Additional Features and Functionality
8485

‎tls/src/main/java/org/bouncycastle/tls/CertificateRequest.java‎

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -173,20 +173,7 @@ public void encode(TlsContext context, OutputStream output)
173173
{
174174
TlsUtils.writeOpaque8(certificateRequestContext, output);
175175

176-
Hashtable extensions = new Hashtable();
177-
TlsExtensionsUtils.addSignatureAlgorithmsExtension(extensions, supportedSignatureAlgorithms);
178-
179-
if (null != supportedSignatureAlgorithmsCert)
180-
{
181-
TlsExtensionsUtils.addSignatureAlgorithmsCertExtension(extensions, supportedSignatureAlgorithmsCert);
182-
}
183-
184-
if (null != certificateAuthorities)
185-
{
186-
TlsExtensionsUtils.addCertificateAuthoritiesExtension(extensions, certificateAuthorities);
187-
}
188-
189-
byte[] extEncoding = TlsProtocol.writeExtensionsData(extensions);
176+
byte[] extEncoding = TlsProtocol.writeExtensionsData(createExtensions13());
190177

191178
TlsUtils.writeOpaque16(extEncoding, output);
192179
return;
@@ -227,6 +214,28 @@ public void encode(TlsContext context, OutputStream output)
227214
}
228215
}
229216

217+
/**
218+
* The extensions of this (TLS 1.3) CertificateRequest, as {@link #encode(TlsContext, OutputStream)}
219+
* sends them.
220+
*/
221+
Hashtable createExtensions13() throws IOException
222+
{
223+
Hashtable extensions = new Hashtable();
224+
TlsExtensionsUtils.addSignatureAlgorithmsExtension(extensions, supportedSignatureAlgorithms);
225+
226+
if (null != supportedSignatureAlgorithmsCert)
227+
{
228+
TlsExtensionsUtils.addSignatureAlgorithmsCertExtension(extensions, supportedSignatureAlgorithmsCert);
229+
}
230+
231+
if (null != certificateAuthorities)
232+
{
233+
TlsExtensionsUtils.addCertificateAuthoritiesExtension(extensions, certificateAuthorities);
234+
}
235+
236+
return extensions;
237+
}
238+
230239
/**
231240
* Parse a {@link CertificateRequest} from an {@link InputStream}.
232241
*

‎tls/src/main/java/org/bouncycastle/tls/DTLSServerProtocol.java‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -793,11 +793,6 @@ protected void invalidateSession(ServerHandshakeState state)
793793
protected void notifyClientCertificate(ServerHandshakeState state, Certificate clientCertificate)
794794
throws IOException
795795
{
796-
if (null == state.certificateRequest)
797-
{
798-
throw new TlsFatalAlert(AlertDescription.internal_error);
799-
}
800-
801796
TlsUtils.processClientCertificate(state.serverContext, clientCertificate, state.keyExchange, state.server);
802797
}
803798

‎tls/src/main/java/org/bouncycastle/tls/TlsClientProtocol.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1643,7 +1643,8 @@ protected void receive13ServerCertificate(ByteArrayInputStream buf)
16431643
throw new TlsFatalAlert(AlertDescription.unexpected_message);
16441644
}
16451645

1646-
this.authentication = TlsUtils.receive13ServerCertificate(tlsClientContext, tlsClient, buf);
1646+
this.authentication = TlsUtils.receive13ServerCertificate(tlsClientContext, tlsClient, clientExtensions,
1647+
buf);
16471648

16481649
// NOTE: In TLS 1.3 we don't have to wait for a possible CertificateStatus message.
16491650
handleServerCertificate();

‎tls/src/main/java/org/bouncycastle/tls/TlsServerProtocol.java‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,11 +1441,6 @@ protected void handleAlertWarningMessage(short alertDescription)
14411441
protected void notifyClientCertificate(Certificate clientCertificate)
14421442
throws IOException
14431443
{
1444-
if (null == certificateRequest)
1445-
{
1446-
throw new TlsFatalAlert(AlertDescription.internal_error);
1447-
}
1448-
14491444
TlsUtils.processClientCertificate(tlsServerContext, clientCertificate, keyExchange, tlsServer);
14501445
}
14511446

@@ -1467,6 +1462,17 @@ protected void receive13ClientCertificate(ByteArrayInputStream buf)
14671462

14681463
assertEmpty(buf);
14691464

1465+
/*
1466+
* RFC 8446 4.4.2. If this message is in response to a CertificateRequest, the value of
1467+
* certificate_request_context in that message.
1468+
*/
1469+
if (!certificateRequest.hasCertificateRequestContext(clientCertificate.getCertificateRequestContext()))
1470+
{
1471+
throw new TlsFatalAlert(AlertDescription.illegal_parameter);
1472+
}
1473+
1474+
TlsUtils.check13CertificateEntryExtensions(clientCertificate, certificateRequest.createExtensions13());
1475+
14701476
notifyClientCertificate(clientCertificate);
14711477
}
14721478

‎tls/src/main/java/org/bouncycastle/tls/TlsUtils.java‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5406,7 +5406,7 @@ static TlsAuthentication receiveServerCertificate(TlsClientContext clientContext
54065406
}
54075407

54085408
static TlsAuthentication receive13ServerCertificate(TlsClientContext clientContext, TlsClient client,
5409-
ByteArrayInputStream buf) throws IOException
5409+
Hashtable clientExtensions, ByteArrayInputStream buf) throws IOException
54105410
{
54115411
SecurityParameters securityParameters = clientContext.getSecurityParametersHandshake();
54125412
if (null != securityParameters.getPeerCertificate())
@@ -5432,6 +5432,8 @@ static TlsAuthentication receive13ServerCertificate(TlsClientContext clientConte
54325432
throw new TlsFatalAlert(AlertDescription.decode_error);
54335433
}
54345434

5435+
check13CertificateEntryExtensions(serverCertificate, clientExtensions);
5436+
54355437
securityParameters.peerCertificate = serverCertificate;
54365438
securityParameters.tlsServerEndPoint = null;
54375439

@@ -6378,6 +6380,47 @@ static void checkExtensionData13(Hashtable extensions, int handshakeType, short
63786380
}
63796381
}
63806382

6383+
/**
6384+
* RFC 8446 4.4.2. Extensions in the Certificate message from the server MUST correspond to ones
6385+
* from the ClientHello message. Extensions in the Certificate message from the client MUST
6386+
* correspond to extensions in the CertificateRequest message from the server.
6387+
*
6388+
* @param certificate a TLS 1.3 Certificate message received from the peer.
6389+
* @param requestExtensions the extensions of the ClientHello (for a server Certificate) or the
6390+
* CertificateRequest (for a client Certificate) that this endpoint sent.
6391+
*/
6392+
static void check13CertificateEntryExtensions(Certificate certificate, Hashtable requestExtensions)
6393+
throws IOException
6394+
{
6395+
int count = certificate.getLength();
6396+
for (int i = 0; i < count; ++i)
6397+
{
6398+
Hashtable extensions = certificate.getCertificateEntryAt(i).getExtensions();
6399+
if (null == extensions)
6400+
{
6401+
continue;
6402+
}
6403+
6404+
Enumeration e = extensions.keys();
6405+
while (e.hasMoreElements())
6406+
{
6407+
Integer extensionType = (Integer)e.nextElement();
6408+
6409+
/*
6410+
* RFC 8446 4.2. Implementations MUST NOT send extension responses if the remote
6411+
* endpoint did not send the corresponding extension requests [..]. Upon receiving
6412+
* such an extension, an endpoint MUST abort the handshake with an
6413+
* "unsupported_extension" alert.
6414+
*/
6415+
if (!hasExtension(requestExtensions, extensionType))
6416+
{
6417+
throw new TlsFatalAlert(AlertDescription.unsupported_extension,
6418+
"Unrequested extension in CertificateEntry: " + ExtensionType.getText(extensionType.intValue()));
6419+
}
6420+
}
6421+
}
6422+
}
6423+
63816424
/**
63826425
* Generate a pre_master_secret and send it encrypted to the server.
63836426
*/

‎tls/src/test/java/org/bouncycastle/tls/test/AllTests.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static Test suite()
4040
suite.addTestSuite(OCSPTest.class);
4141
suite.addTestSuite(PRFTest.class);
4242
suite.addTestSuite(Tls13CertificateStatusTest.class);
43+
suite.addTestSuite(Tls13ClientCertificateTest.class);
4344
suite.addTestSuite(Tls13PSKProtocolTest.class);
4445
suite.addTestSuite(TlsHandshakeTimeoutTest.class);
4546
suite.addTestSuite(TlsProtocolNonBlockingTest.class);

‎tls/src/test/java/org/bouncycastle/tls/test/Tls13CertificateStatusTest.java‎

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -218,24 +218,47 @@ public void testClientReadsNullOutsideTheChain()
218218
}
219219

220220
/**
221-
* RFC 8446 sec. 4.2: a server answers only the extensions the client sent, so a staple the client
222-
* never asked for is ignored - not read, and not a reason to fail the handshake. The staple has to
223-
* be attached to the Certificate by hand here, the protocol declining to consult
224-
* getCertificateStatus() for a client that did not ask.
221+
* RFC 8446 sec. 4.2: a server answers only the extensions the client sent, and an endpoint
222+
* receiving any other MUST abort the handshake with unsupported_extension - so a staple the client
223+
* never asked for fails it. The staple has to be attached to the Certificate by hand here, the
224+
* protocol declining to consult getCertificateStatus() for a client that did not ask.
225225
*/
226-
public void testClientIgnoresAnUnsolicitedStaple()
226+
public void testClientRejectsAnUnsolicitedStaple()
227227
throws Exception
228228
{
229229
byte[] extensionData = TlsExtensionsUtils.createStatusRequestExtension13(
230230
new CertificateStatus(CertificateStatusType.ocsp, ocspResponse("unsolicited")));
231231

232-
CapturingTlsClient client = runHandshake(null, false, false, extensionData);
232+
CapturingTlsClient client = new CapturingTlsClient(false);
233233

234-
assertEquals("the staple should still have been on the wire", "unsolicited",
235-
getStapledMarker(client.certificateEntryList[0]));
234+
try
235+
{
236+
runHandshake(client, null, false, extensionData);
237+
fail("expected an unsupported_extension");
238+
}
239+
catch (Exception e)
240+
{
241+
assertClientFailedWith(client, AlertDescription.unsupported_extension);
242+
}
243+
}
236244

237-
assertNull(client.serverCertificate.getCertificateStatus());
238-
assertNull(client.serverCertificate.getCertificateStatusAt(0));
245+
/**
246+
* Every CertificateEntry is held to the ClientHello, not just the end-entity certificate's.
247+
*/
248+
public void testClientRejectsAnUnsolicitedStapleOnAnIntermediate()
249+
throws Exception
250+
{
251+
CapturingTlsClient client = new CapturingTlsClient(false);
252+
253+
try
254+
{
255+
runHandshake(client, null, true, null);
256+
fail("expected an unsupported_extension");
257+
}
258+
catch (Exception e)
259+
{
260+
assertClientFailedWith(client, AlertDescription.unsupported_extension);
261+
}
239262
}
240263

241264
/**

0 commit comments

Comments
 (0)