Skip to content

Commit 8bf734b

Browse files
committed
Fixed TLS feature (RFC 7633) checks: TLS 1.3 features are satisfied by the end-entity CertificateEntry, status_request_v2 is not enforced under TLS 1.3, and must-staple now requires a stapled response for the end-entity certificate in every version, relates to github #2458.
1 parent db41e77 commit 8bf734b

5 files changed

Lines changed: 494 additions & 35 deletions

File tree

‎docs/releasenotes.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Date: 2026, TBD
7676
- The jdk1.4 and jdk1.3 providers registered the PKCS12-AES256-AES128, PKCS12-AES256-AES128-GCM, PKCS12-DEF-AES256-AES128 and PKCS12-DEF-AES256-AES128-GCM KeyStores against classes those distributions do not contain. They are no longer registered there; the legacy PKCS12 stores can still read PBES2/AES protected files.
7777
- The OCSPResp(byte[]) and OCSPReq(byte[]) constructors reported a malformed encoding only when the ASN.1 decode raised IllegalArgumentException, ClassCastException or ASN1Exception, so a top-level SEQUENCE that is empty or truncated - which makes OCSPResponse/OCSPRequest read seq.getObjectAt(0) out of bounds - escaped the declared IOException as an ArrayIndexOutOfBoundsException. Both parsers now surface any RuntimeException from the decode as a CertIOException, as X509CertificateHolder and X509CRLHolder already do, so an OCSP client parsing a response and an OCSP responder parsing a request reject malformed input through the contract rather than crashing. The cause was in the ASN.1 types, which read fixed positions of their SEQUENCE without checking its size: OCSPResponse, OCSPRequest, BasicOCSPResponse, ResponseData, TBSRequest and Request now reject a short one with IllegalArgumentException, as SingleResponse and CertID already did. The OCSP ASN.1 types also cast their elements rather than passing them to getInstance(), so an element of the wrong type surfaced as a ClassCastException and a signature BIT STRING in a legal encoding other than DER was refused; they now use getInstance() throughout, and BasicOCSPResponse and Signature accept any BIT STRING encoding. The provider's OCSP revocation checking parses these types directly and let an unchecked exception out of CertPathValidator.validate() for a response that would not decode, whether stapled by the caller or returned by the responder; a stapled one is now rejected with a CertPathValidatorException, and a fetched one is treated as an unreachable responder is, so the check can fall back to CRLs. A successful response carrying no response bytes, which failed with a NullPointerException, is rejected the same way.
7878

79+
- 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).
80+
- (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.
81+
- 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+
7983
### 2.1.3 Additional Features and Functionality
8084

8185
- The CRMF certificate request message controls now include the RFC 4211 sec. 6.6 protocolEncrKey control, which names the key a CA is to encrypt its response with: org.bouncycastle.cert.crmf.ProtocolEncrKeyControl carries the SubjectPublicKeyInfo the control is defined to take, and CertificateRequestMessage.getControl() recognises id-regCtrl-protocolEncrKey alongside the regToken, authenticator and pkiArchiveOptions controls it already returned (github PR #2443).
@@ -97,6 +101,8 @@ Date: 2026, TBD
97101

98102
- The provider adds ECDHwithSHA256HKDF, ECDHwithSHA384HKDF and ECDHwithSHA512HKDF KeyAgreement algorithms: ECDH followed by HKDF (RFC 5869), the counterparts for elliptic curve keys of the XDHwithSHA\*HKDF agreements used for the RFC 8418 dhSinglePass-stdDH-hkdf schemes. As with those, a UserKeyingMaterialSpec supplies the HKDF info and, optionally, its salt. They are registered by name only; the RFC 8418 scheme object identifiers remain registered to the XDH agreements.
99103

104+
- TLS: Added CertificateStatusType.getName and getText, and TlsUtils.hasExtension.
105+
100106
### 2.1.4 Additional Notes
101107

102108
- The sources and javadoc jars of the Ant-built distributions (jdk14, jdk15to18 and jdk13) no longer carry test material. Each module's javadoc target copies the package documentation it needs - org/bouncycastle/<area>/**/*.html - back into the module source directory that has already been compiled from, and zip-src zips that directory afterwards, so every test package's package.html arrived in the sources jar by that route; javadoc-util additionally copied org/bouncycastle/asn1/isismtt/**/*.java, which put test classes into the bcutil javadoc as generated pages, and javadoc-pg deliberately copied the gpg and bcpg test sources in order to document them. Separately the source copies excluded test material only one directory deep and only for *.java, because Ant reads ** as an any-depth wildcard just where it is a whole path segment, so anything nested further or with another extension - the PEM certificate fixtures under org/bouncycastle/est/test/san corrected in 1.86, and an ICAO master list under org/bouncycastle/asn1/icao/test - went through. The source and javadoc copies of every module now exclude test directories at any depth, and javadoc-pg no longer documents the test packages. org.bouncycastle.util.test is unaffected and still ships in the bcprov binary, sources and javadoc jars, as it does from the Gradle build: it is the SimpleTest framework the light-weight API's own test classes are written against, not test material of the distribution. No binary changes - the classes and resources of every Ant-built jar are identical to those of the 1.86 release - and the Gradle-built jdk18on artifacts never carried any of this.

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,25 @@ public CertificateStatus(short statusType, Object response)
2626
this.response = response;
2727
}
2828

29+
/**
30+
* Whether there exists an OCSP response for certificate 0 of the chain this status answers for:
31+
* always true for {@link CertificateStatusType#ocsp}, true for
32+
* {@link CertificateStatusType#ocsp_multi} only when there was a non-empty response for
33+
* position 0.
34+
*/
35+
boolean hasLeafResponse()
36+
{
37+
switch (statusType)
38+
{
39+
case CertificateStatusType.ocsp:
40+
return true;
41+
case CertificateStatusType.ocsp_multi:
42+
return null != ((Vector)response).elementAt(0);
43+
default:
44+
throw new IllegalStateException();
45+
}
46+
}
47+
2948
public short getStatusType()
3049
{
3150
return statusType;

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

Lines changed: 100 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4840,48 +4840,119 @@ else if (null == clientSigAlgsCert)
48404840
}
48414841
}
48424842

4843-
static void checkTlsFeatures(Certificate serverCertificate, Hashtable clientExtensions, Hashtable serverExtensions) throws IOException
4843+
static void checkTlsFeatures(TlsClientContext clientContext, TlsServerCertificate serverCertificate,
4844+
Hashtable clientExtensions, Hashtable serverExtensions) throws IOException
48444845
{
48454846
/*
48464847
* RFC 7633 4.3.3. A client MUST treat a certificate with a TLS feature extension as an
48474848
* invalid certificate if the features offered by the server do not contain all features
48484849
* present in both the client's ClientHello message and the TLS feature extension.
48494850
*/
4850-
byte[] tlsFeatures = serverCertificate.getCertificateAt(0).getExtension(TlsObjectIdentifiers.id_pe_tlsfeature);
4851-
if (tlsFeatures != null)
4851+
4852+
Certificate certificate = serverCertificate.getCertificate();
4853+
if (certificate.isEmpty())
4854+
{
4855+
throw new TlsFatalAlert(AlertDescription.internal_error);
4856+
}
4857+
4858+
CertificateEntry eeEntry = certificate.getCertificateEntryAt(0);
4859+
4860+
byte[] tlsFeatures = eeEntry.getCertificate().getExtension(TlsObjectIdentifiers.id_pe_tlsfeature);
4861+
if (tlsFeatures == null)
4862+
{
4863+
return;
4864+
}
4865+
4866+
// TODO[tls] Proper ASN.1 type class for this extension?
4867+
ASN1Primitive tlsFeaturesObj = readASN1Object(tlsFeatures);
4868+
if (!(tlsFeaturesObj instanceof ASN1Sequence))
4869+
{
4870+
throw new TlsFatalAlert(AlertDescription.bad_certificate,
4871+
"Server certificate has invalid TLS Features extension");
4872+
}
4873+
4874+
ASN1Sequence tlsFeaturesSeq = (ASN1Sequence)tlsFeaturesObj;
4875+
for (int i = 0; i < tlsFeaturesSeq.size(); ++i)
48524876
{
4853-
// TODO[tls] Proper ASN.1 type class for this extension?
4854-
ASN1Primitive tlsFeaturesObj = readASN1Object(tlsFeatures);
4855-
if (!(tlsFeaturesObj instanceof ASN1Sequence))
4877+
if (!(tlsFeaturesSeq.getObjectAt(i) instanceof ASN1Integer))
48564878
{
48574879
throw new TlsFatalAlert(AlertDescription.bad_certificate,
48584880
"Server certificate has invalid TLS Features extension");
48594881
}
4882+
}
4883+
4884+
requireDEREncoding(tlsFeaturesSeq, tlsFeatures);
48604885

4861-
ASN1Sequence tlsFeaturesSeq = (ASN1Sequence)tlsFeaturesObj;
4862-
for (int i = 0; i < tlsFeaturesSeq.size(); ++i)
4886+
boolean isTLSv13 = isTLSv13(clientContext);
4887+
boolean mustStaple = false;
4888+
Hashtable eeExtensions = eeEntry.getExtensions();
4889+
4890+
for (int i = 0; i < tlsFeaturesSeq.size(); ++i)
4891+
{
4892+
BigInteger tlsExtension = ((ASN1Integer)tlsFeaturesSeq.getObjectAt(i)).getPositiveValue();
4893+
if (tlsExtension.bitLength() > 16)
48634894
{
4864-
if (!(tlsFeaturesSeq.getObjectAt(i) instanceof ASN1Integer))
4865-
{
4866-
throw new TlsFatalAlert(AlertDescription.bad_certificate,
4867-
"Server certificate has invalid TLS Features extension");
4868-
}
4895+
// Feature has invalid value - couldn't legally appear in ClientHello extensions
4896+
continue;
48694897
}
48704898

4871-
requireDEREncoding(tlsFeaturesSeq, tlsFeatures);
4899+
int extensionType = tlsExtension.intValue();
4900+
Integer extensionTypeKey = Integers.valueOf(extensionType);
48724901

4873-
for (int i = 0; i < tlsFeaturesSeq.size(); ++i)
4902+
if (!hasExtension(clientExtensions, extensionTypeKey))
48744903
{
4875-
BigInteger tlsExtension = ((ASN1Integer)tlsFeaturesSeq.getObjectAt(i)).getPositiveValue();
4876-
if (tlsExtension.bitLength() <= 16)
4904+
// Feature only in force if actually in ClientHello extensions
4905+
continue;
4906+
}
4907+
4908+
if (isTLSv13 && ExtensionType.isRecognized(extensionType))
4909+
{
4910+
if (!isPermittedExtensionType13(HandshakeType.client_hello, extensionType))
48774911
{
4878-
Integer extensionType = Integers.valueOf(tlsExtension.intValue());
4879-
if (clientExtensions.containsKey(extensionType) && !serverExtensions.containsKey(extensionType))
4880-
{
4881-
throw new TlsFatalAlert(AlertDescription.certificate_unknown,
4882-
"Server extensions missing TLS Feature " + extensionType);
4883-
}
4912+
// Feature not in force if extension not relevant to negotiated version
4913+
continue;
48844914
}
4915+
4916+
// Note that no stapling extensions are skipped here
4917+
if (isPermittedExtensionType13(HandshakeType.server_hello, extensionType))
4918+
{
4919+
// Feature not checked for TLS 1.3 ServerHello extensions - existing permitted extensions are
4920+
// either not relevant to server certificate authentication, or always added. Revisit this if
4921+
// one that is relevant becomes permitted there (e.g. tls_cert_with_extern_psk, RFC 8773).
4922+
continue;
4923+
}
4924+
}
4925+
4926+
// We now consider the feature requirement to be "in force" - check it's satisfied
4927+
4928+
if (ExtensionType.status_request == extensionType || ExtensionType.status_request_v2 == extensionType)
4929+
{
4930+
mustStaple = true;
4931+
}
4932+
4933+
if (hasExtension(serverExtensions, extensionTypeKey))
4934+
{
4935+
// Feature satisfied by the ServerHello extensions (EncryptedExtensions in TLS 1.3)
4936+
continue;
4937+
}
4938+
4939+
if (hasExtension(eeExtensions, extensionTypeKey))
4940+
{
4941+
// Feature satisfied by the CertificateEntry extensions (only in TLS 1.3+)
4942+
continue;
4943+
}
4944+
4945+
throw new TlsFatalAlert(AlertDescription.certificate_unknown,
4946+
"Server extensions missing TLS Feature " + ExtensionType.getText(extensionType));
4947+
}
4948+
4949+
if (mustStaple)
4950+
{
4951+
CertificateStatus certificateStatus = serverCertificate.getCertificateStatus();
4952+
if (certificateStatus == null || !certificateStatus.hasLeafResponse())
4953+
{
4954+
throw new TlsFatalAlert(AlertDescription.certificate_unknown,
4955+
"Server failed TLS Feature 'Must-Staple'");
48854956
}
48864957
}
48874958
}
@@ -4953,8 +5024,6 @@ static void processServerCertificate(TlsClientContext clientContext,
49535024

49545025
Certificate serverCertificate = securityParameters.getPeerCertificate();
49555026

4956-
checkTlsFeatures(serverCertificate, clientExtensions, serverExtensions);
4957-
49585027
CertificateStatus certificateStatus;
49595028
CertificateStatus[] certificateStatuses;
49605029

@@ -4975,8 +5044,12 @@ static void processServerCertificate(TlsClientContext clientContext,
49755044
certificateStatuses = spreadCertificateStatus(serverCertificate, serverCertificateStatus);
49765045
}
49775046

4978-
clientAuthentication.notifyServerCertificate(
4979-
new TlsServerCertificateImpl(serverCertificate, certificateStatus, certificateStatuses));
5047+
TlsServerCertificate tlsServerCertificate = new TlsServerCertificateImpl(serverCertificate,
5048+
certificateStatus, certificateStatuses);
5049+
5050+
checkTlsFeatures(clientContext, tlsServerCertificate, clientExtensions, serverExtensions);
5051+
5052+
clientAuthentication.notifyServerCertificate(tlsServerCertificate);
49805053
}
49815054

49825055
static SignatureAndHashAlgorithm getCertSigAndHashAlg(TlsCertificate subjectCert, TlsCertificate issuerCert)

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

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.bouncycastle.tls.crypto.TlsEncryptor;
1414
import org.bouncycastle.tls.crypto.TlsVerifier;
1515
import org.bouncycastle.tls.crypto.Tls13Verifier;
16+
import org.bouncycastle.tls.crypto.impl.bc.BcTlsCrypto;
1617

1718
import junit.framework.TestCase;
1819
import junit.framework.TestSuite;
@@ -40,7 +41,7 @@ public void testNonSequenceTlsFeaturesRejected()
4041

4142
try
4243
{
43-
TlsUtils.checkTlsFeatures(serverCertificate, new Hashtable(), new Hashtable());
44+
checkTlsFeatures(serverCertificate, new Hashtable(), new Hashtable());
4445
fail("expected TlsFatalAlert for non-SEQUENCE TLS Features extension");
4546
}
4647
catch (TlsFatalAlert e)
@@ -65,7 +66,7 @@ public void testValidTlsFeaturesAccepted()
6566
Certificate serverCertificate = makeCertificate(seq);
6667

6768
// No client extensions requested, so nothing to enforce: must complete cleanly.
68-
TlsUtils.checkTlsFeatures(serverCertificate, new Hashtable(), new Hashtable());
69+
checkTlsFeatures(serverCertificate, new Hashtable(), new Hashtable());
6970
}
7071

7172
/**
@@ -76,7 +77,59 @@ public void testNoTlsFeaturesExtension()
7677
{
7778
Certificate serverCertificate = makeCertificate(null);
7879

79-
TlsUtils.checkTlsFeatures(serverCertificate, new Hashtable(), new Hashtable());
80+
checkTlsFeatures(serverCertificate, new Hashtable(), new Hashtable());
81+
}
82+
83+
/**
84+
* A (D)TLS 1.2 ServerHello may carry no extensions at all, leaving the server extensions null. A
85+
* feature in force is then simply unsatisfied, and must fail with an alert rather than a
86+
* NullPointerException.
87+
*/
88+
public void testFeatureUnsatisfiedByAbsentServerExtensions()
89+
throws Exception
90+
{
91+
ASN1Encodable[] features = new ASN1Encodable[]{ new ASN1Integer(ExtensionType.status_request) };
92+
byte[] seq = new DERSequence(features).getEncoded(ASN1Encoding.DER);
93+
94+
Certificate serverCertificate = makeCertificate(seq);
95+
96+
Hashtable clientExtensions = new Hashtable();
97+
TlsExtensionsUtils.addStatusRequestExtension(clientExtensions,
98+
new CertificateStatusRequest(CertificateStatusType.ocsp, new OCSPStatusRequest(null, null)));
99+
100+
try
101+
{
102+
checkTlsFeatures(serverCertificate, clientExtensions, null);
103+
fail("expected TlsFatalAlert for an unsatisfied TLS Feature");
104+
}
105+
catch (TlsFatalAlert e)
106+
{
107+
assertEquals(AlertDescription.certificate_unknown, e.getAlertDescription());
108+
}
109+
}
110+
111+
/**
112+
* Runs the check as a (D)TLS 1.2 client would, for a server that stapled nothing.
113+
*/
114+
private static void checkTlsFeatures(Certificate serverCertificate, Hashtable clientExtensions,
115+
Hashtable serverExtensions) throws IOException
116+
{
117+
BcTlsCrypto crypto = new BcTlsCrypto();
118+
119+
TlsClientContextImpl clientContext = new TlsClientContextImpl(crypto);
120+
clientContext.handshakeBeginning(new DefaultTlsClient(crypto)
121+
{
122+
public TlsAuthentication getAuthentication()
123+
{
124+
throw new UnsupportedOperationException();
125+
}
126+
});
127+
clientContext.getSecurityParametersHandshake().negotiatedVersion = ProtocolVersion.TLSv12;
128+
129+
TlsServerCertificate tlsServerCertificate = new TlsServerCertificateImpl(serverCertificate, null,
130+
new CertificateStatus[serverCertificate.getLength()]);
131+
132+
TlsUtils.checkTlsFeatures(clientContext, tlsServerCertificate, clientExtensions, serverExtensions);
80133
}
81134

82135
private static Certificate makeCertificate(byte[] tlsFeaturesExtensionContent)

0 commit comments

Comments
 (0)