Skip to content

Commit a24b5bd

Browse files
committed
CRL.isRevoked() no longer throws ClassCastException for a certificate of type "X.509" that is not an X509Certificate, and reads its serial number and issuer from its encoding instead, in both X509CRLImpl and the legacy jce.provider.X509CRLObject.
1 parent b64c278 commit a24b5bd

4 files changed

Lines changed: 140 additions & 59 deletions

File tree

‎docs/releasenotes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ Date: 2026, TBD
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.
8282
- 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.
8383

84+
- CRL.isRevoked() on the provider's CRLs threw ClassCastException for a certificate whose type is "X.509" but which is not a java.security.cert.X509Certificate, although the method checks only the type and goes on to handle exactly that case: it cast the certificate to X509Certificate for its serial number before looking at any entry, so the branch that reads the issuer from the certificate's encoding could never be reached. Such a certificate now has both its serial number and its issuer read from its encoding, in the provider's X509CRL and in the legacy org.bouncycastle.jce.provider.X509CRLObject alike.
85+
8486
### 2.1.3 Additional Features and Functionality
8587

8688
- 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).

‎pkix/src/test/java/org/bouncycastle/cert/test/IndirectCRLSerialCollisionTest.java‎

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.security.cert.CertPathValidator;
1414
import java.security.cert.CertPathValidatorException;
1515
import java.security.cert.CertStore;
16+
import java.security.cert.Certificate;
17+
import java.security.cert.CertificateEncodingException;
1618
import java.security.cert.CertificateFactory;
1719
import java.security.cert.CollectionCertStoreParameters;
1820
import java.security.cert.PKIXParameters;
@@ -30,6 +32,7 @@
3032
import org.bouncycastle.asn1.x500.X500Name;
3133
import org.bouncycastle.asn1.x509.BasicConstraints;
3234
import org.bouncycastle.asn1.x509.CRLReason;
35+
import org.bouncycastle.asn1.x509.CertificateList;
3336
import org.bouncycastle.asn1.x509.Extension;
3437
import org.bouncycastle.asn1.x509.ExtensionsGenerator;
3538
import org.bouncycastle.asn1.x509.GeneralName;
@@ -121,14 +124,82 @@ public void performTest()
121124

122125
/**
123126
* CRL.isRevoked() stopped at the first entry carrying the serial number, and reported the
124-
* certificate as not revoked when that entry was another issuer's.
127+
* certificate as not revoked when that entry was another issuer's. The same checks run with the
128+
* certificates presented as a Certificate of type "X.509" that is not an X509Certificate, which
129+
* isRevoked() cast to one for its serial number, and against the legacy provider CRL class.
125130
*/
126131
private void isRevoked(X509Certificate ee, X509Certificate other, X509CRL caFirst, X509CRL otherFirst, X509CRL otherOnly)
132+
throws Exception
133+
{
134+
Certificate opaqueEe = new OpaqueCertificate(ee);
135+
Certificate opaqueOther = new OpaqueCertificate(other);
136+
137+
isRevoked("", ee, other, caFirst, otherFirst, otherOnly);
138+
isRevoked(" [not an X509Certificate]", opaqueEe, opaqueOther, caFirst, otherFirst, otherOnly);
139+
140+
X509CRL legacyCaFirst = legacyCrl(caFirst);
141+
X509CRL legacyOtherFirst = legacyCrl(otherFirst);
142+
X509CRL legacyOtherOnly = legacyCrl(otherOnly);
143+
144+
isRevoked(" [legacy CRL]", ee, other, legacyCaFirst, legacyOtherFirst, legacyOtherOnly);
145+
isRevoked(" [legacy CRL, not an X509Certificate]", opaqueEe, opaqueOther, legacyCaFirst, legacyOtherFirst, legacyOtherOnly);
146+
}
147+
148+
private void isRevoked(String label, Certificate ee, Certificate other, X509CRL caFirst, X509CRL otherFirst, X509CRL otherOnly)
149+
{
150+
isTrue("revoked certificate not reported by isRevoked() (own entry first)" + label, caFirst.isRevoked(ee));
151+
isTrue("revoked certificate not reported by isRevoked() (other issuer's entry first)" + label, otherFirst.isRevoked(ee));
152+
isTrue("other issuer's revoked certificate not reported by isRevoked()" + label, otherFirst.isRevoked(other));
153+
isTrue("another issuer's entry reported by isRevoked()" + label, !otherOnly.isRevoked(ee));
154+
}
155+
156+
private static X509CRL legacyCrl(X509CRL crl)
157+
throws Exception
127158
{
128-
isTrue("revoked certificate not reported by isRevoked() (own entry first)", caFirst.isRevoked(ee));
129-
isTrue("revoked certificate not reported by isRevoked() (other issuer's entry first)", otherFirst.isRevoked(ee));
130-
isTrue("other issuer's revoked certificate not reported by isRevoked()", otherFirst.isRevoked(other));
131-
isTrue("another issuer's entry reported by isRevoked()", !otherOnly.isRevoked(ee));
159+
return new org.bouncycastle.jce.provider.X509CRLObject(CertificateList.getInstance(crl.getEncoded()));
160+
}
161+
162+
/**
163+
* A certificate of type "X.509" that is not a java.security.cert.X509Certificate, as another
164+
* provider's CertificateFactory may return.
165+
*/
166+
private static class OpaqueCertificate
167+
extends Certificate
168+
{
169+
private final X509Certificate cert;
170+
171+
OpaqueCertificate(X509Certificate cert)
172+
{
173+
super("X.509");
174+
175+
this.cert = cert;
176+
}
177+
178+
public byte[] getEncoded()
179+
throws CertificateEncodingException
180+
{
181+
return cert.getEncoded();
182+
}
183+
184+
public void verify(PublicKey key)
185+
{
186+
throw new UnsupportedOperationException();
187+
}
188+
189+
public void verify(PublicKey key, String sigProvider)
190+
{
191+
throw new UnsupportedOperationException();
192+
}
193+
194+
public String toString()
195+
{
196+
return "OpaqueCertificate: " + cert.getSubjectX500Principal();
197+
}
198+
199+
public PublicKey getPublicKey()
200+
{
201+
return cert.getPublicKey();
202+
}
132203
}
133204

134205
private static X509CRL jdkCrl(X509CRL crl)

‎prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/x509/X509CRLImpl.java‎

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -664,49 +664,53 @@ public boolean isRevoked(Certificate cert)
664664
throw new IllegalArgumentException("X.509 CRL used with non X.509 Cert");
665665
}
666666

667-
Enumeration certs = c.getRevokedCertificateEnumeration();
667+
Enumeration revokedCerts = c.getRevokedCertificateEnumeration();
668668

669669
X500Name caName = c.getIssuer();
670670

671-
if (certs.hasMoreElements())
671+
if (revokedCerts.hasMoreElements())
672672
{
673-
BigInteger serial = ((X509Certificate)cert).getSerialNumber();
673+
BigInteger serial;
674+
X500Name issuer;
675+
676+
if (cert instanceof X509Certificate)
677+
{
678+
X509Certificate x509Cert = (X509Certificate)cert;
679+
680+
serial = x509Cert.getSerialNumber();
681+
issuer = X500Name.getInstance(x509Cert.getIssuerX500Principal().getEncoded());
682+
}
683+
else
684+
{
685+
// an "X.509" type does not make it an X509Certificate, so both come from the encoding.
686+
org.bouncycastle.asn1.x509.Certificate certStruct;
687+
try
688+
{
689+
certStruct = org.bouncycastle.asn1.x509.Certificate.getInstance(cert.getEncoded());
690+
}
691+
catch (CertificateEncodingException e)
692+
{
693+
throw Exceptions.illegalArgumentException("Cannot process certificate", e);
694+
}
695+
696+
serial = certStruct.getSerialNumber().getValue();
697+
issuer = certStruct.getIssuer();
698+
}
674699

675-
while (certs.hasMoreElements())
700+
while (revokedCerts.hasMoreElements())
676701
{
677-
TBSCertList.CRLEntry entry = TBSCertList.CRLEntry.getInstance(certs.nextElement());
702+
TBSCertList.CRLEntry entry = TBSCertList.CRLEntry.getInstance(revokedCerts.nextElement());
678703

679704
caName = X509CRLEntryObject.loadCertificateIssuer(entry, isIndirect, caName);
680705
if (caName == null)
681706
{
682707
caName = c.getIssuer(); // an entry with no certificate issuer of its own is the CRL issuer's
683708
}
684709

685-
if (entry.getUserCertificate().hasValue(serial))
710+
// a serial number is only unique within its issuer, so another issuer's entry is not this one.
711+
if (entry.getUserCertificate().hasValue(serial) && caName.equals(issuer))
686712
{
687-
X500Name issuer;
688-
689-
if (cert instanceof X509Certificate)
690-
{
691-
issuer = X500Name.getInstance(((X509Certificate)cert).getIssuerX500Principal().getEncoded());
692-
}
693-
else
694-
{
695-
try
696-
{
697-
issuer = org.bouncycastle.asn1.x509.Certificate.getInstance(cert.getEncoded()).getIssuer();
698-
}
699-
catch (CertificateEncodingException e)
700-
{
701-
throw Exceptions.illegalArgumentException("Cannot process certificate", e);
702-
}
703-
}
704-
705-
// a serial number is only unique within its issuer, so another issuer's entry is not this one.
706-
if (caName.equals(issuer))
707-
{
708-
return true;
709-
}
713+
return true;
710714
}
711715
}
712716
}

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

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,32 @@ public boolean isRevoked(Certificate cert)
533533

534534
if (certs != null)
535535
{
536-
BigInteger serial = ((X509Certificate)cert).getSerialNumber();
536+
BigInteger serial;
537+
X500Name issuer;
538+
539+
if (cert instanceof X509Certificate)
540+
{
541+
X509Certificate x509Cert = (X509Certificate)cert;
542+
543+
serial = x509Cert.getSerialNumber();
544+
issuer = X500Name.getInstance(x509Cert.getIssuerX500Principal().getEncoded());
545+
}
546+
else
547+
{
548+
// an "X.509" type does not make it an X509Certificate, so both come from the encoding.
549+
org.bouncycastle.asn1.x509.Certificate certStruct;
550+
try
551+
{
552+
certStruct = org.bouncycastle.asn1.x509.Certificate.getInstance(cert.getEncoded());
553+
}
554+
catch (CertificateEncodingException e)
555+
{
556+
throw new RuntimeException("Cannot process certificate");
557+
}
558+
559+
serial = certStruct.getSerialNumber().getValue();
560+
issuer = certStruct.getIssuer();
561+
}
537562

538563
while (certs.hasMoreElements())
539564
{
@@ -545,31 +570,10 @@ public boolean isRevoked(Certificate cert)
545570
caName = c.getIssuer(); // an entry with no certificate issuer of its own is the CRL issuer's
546571
}
547572

548-
if (entry.getUserCertificate().hasValue(serial))
573+
// a serial number is only unique within its issuer, so another issuer's entry is not this one.
574+
if (entry.getUserCertificate().hasValue(serial) && caName.equals(issuer))
549575
{
550-
X500Name issuer;
551-
552-
if (cert instanceof X509Certificate)
553-
{
554-
issuer = X500Name.getInstance(((X509Certificate)cert).getIssuerX500Principal().getEncoded());
555-
}
556-
else
557-
{
558-
try
559-
{
560-
issuer = org.bouncycastle.asn1.x509.Certificate.getInstance(cert.getEncoded()).getIssuer();
561-
}
562-
catch (CertificateEncodingException e)
563-
{
564-
throw new RuntimeException("Cannot process certificate");
565-
}
566-
}
567-
568-
// a serial number is only unique within its issuer, so another issuer's entry is not this one.
569-
if (caName.equals(issuer))
570-
{
571-
return true;
572-
}
576+
return true;
573577
}
574578
}
575579
}

0 commit comments

Comments
 (0)