Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/releasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Date: 2026, TBD
- An ERSDataGroup recomputed its hash on every request rather than taking it from the cache ERSCachingData exists to provide: the group overrode getHash(), which left the calculateHash() the cache calls unreachable - and wrong, as it copied the member hashes with a loop bounded by the size of the empty list it was copying into, so it would have digested nothing had anything reached it. The computation is back in calculateHash() and the override is gone, so a group's hash is computed once per digest algorithm and previous-chain hash, as every other ERSData's is. The value itself is unchanged.
- ERSArchiveTimeStampGenerator rebuilt its reduced hash tree from the data objects on every call, so the usual generateTimeStampRequest() followed by generateArchiveTimeStamp() or generateArchiveTimeStamps() built it twice. The leaves are now built once and dropped when data or a previous chain is added. A Set of the data groups it had been given, which nothing ever read, has gone with it.
- Grain-128AEAD returned corrupted plaintext from a decryption driven in chunks. The stream cipher data operator splits a processBytes() call that spans the buffered authentication tag into two output segments, the bytes released from the tag buffer and then the bytes taken straight from the caller's input, and wrote the second segment at the caller's output offset instead of after the first, so the second segment overwrote the head of the first and the tail of the reported output was never written at all. The call still returned the full byte count, and because the engine's state update depends on the input and the keystream rather than on where the output lands, the tag still verified: the wrong plaintext came back with no error raised. Any chunk after the first that carried more than the 8 byte tag length was affected. Grain-128AEAD is the only engine that uses this operator, and one shot decryption, the encryption path and every other AEAD engine were unaffected. The second segment is now written at the advanced offset, matching the equivalent step of the general decryption path (github PR #2447).
- The certificate path validators looked a certificate up in an indirect CRL by serial number alone and then compared the issuer of whichever entry came first, so where two issuers had each revoked the same serial number - an indirect CRL (RFC 5280 sec. 5.2.5) lists certificates from more than one issuer, and a serial number is only unique within its issuer - a certificate whose own entry followed the other issuer's was reported as not revoked. The BC provider's PKIX validator, the pkix X509RevocationChecker and the legacy org.bouncycastle.x509 validator now check every entry carrying the serial number against the issuer it applies to, the one named by its certificateIssuer extension or inherited from the entries before it (sec. 5.3.3), and the provider's X509CRL implements getRevokedCertificate(X509Certificate), the JDK's lookup for indirect CRLs, the same way rather than inheriting the default that assumes a single issuer.
- The AEAD stream cipher data operator, which Grain-128AEAD alone uses, wrote its output without first checking that the caller's buffer was long enough, so a short output buffer surfaced as an ArrayIndexOutOfBoundsException from inside the engine rather than as the OutputLengthException the general path reports for every other AEAD engine. Both directions of processBytes(), and processByte(), now check before anything is written or buffered, and only when the call releases output, as the general path does.
- The RFC 9709 content-encryption AlgorithmIdentifier, which carries the real algorithm inside the parameters of an outer id-alg-cek-hkdf-sha256, was unwrapped at only one of the points where a CMS recipient makes a decision about it. Key-size validation was corrected for plain key transport in 1.86, but the same call in the KEK, RSA-KTS and KEM recipients, and in the key-transport recipient's own ORI-KEM branch, still compared the recovered key against the outer identifier, which registers no key size, so setKeySizeValidation(true) silently checked nothing there; the setAllowedContentAlgorithms allow-list and the setMinimumTagSize floor were applied to the outer identifier on every recipient family, including the one already corrected, so neither constrained an RFC 9709 message. The unwrap now happens once for the key-size check and once for the two policy checks, and every recipient polices and validates the content-encryption algorithm the message actually carries. A recipient with no allow-list, no tag floor and no key-size validation configured behaves exactly as before; a caller who listed id-alg-cek-hkdf-sha256 in an allow-list in order to admit RFC 9709 messages must now list the content-encryption algorithms themselves (github PR #2446).
- A CMS message whose EncryptedContentInfo named the RFC 9709 key derivation but carried no readable content-encryption AlgorithmIdentifier in its parameters was reported as a NullPointerException, or as an IllegalArgumentException from the ASN.1 decoder, out of methods declared to throw CMSException, RecipientInformation.getContent() among them. The four places that resolve the wrapper - the CEK derivation, the content cipher selection, the key-size check, and the recipient's allowed-algorithm and tag-size checks - now share one resolver, which reports an absent or unreadable inner algorithm as a CMSException.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,36 +403,12 @@ protected static void getCertStatus(

if (isIndirect)
{
crl_entry = crl.getRevokedCertificate(getSerialNumber(cert));
crl_entry = getIndirectCRLEntry(crl, getSerialNumber(cert), getEncodedIssuerPrincipal(cert));

if (crl_entry == null)
{
return;
}

X500Principal certIssuer;
try
{
certIssuer = crl_entry.getCertificateIssuer();
}
catch (RuntimeException e)
{
// getCertificateIssuer() builds a new X500Principal from the entry's certificateIssuer
// DN, which can throw an unchecked IllegalArgumentException on a name that decodes
// structurally but is semantically invalid. Fail closed with the checked contract type
// rather than let it escape (or swallow it to null, which would fail revocation open).
throw new AnnotatedException("CRL entry certificate issuer could not be parsed.", e);
}

if (certIssuer == null)
{
certIssuer = getIssuerPrincipal(crl);
}

if (!getEncodedIssuerPrincipal(cert).equals(certIssuer))
{
return;
}
}
else if (!getEncodedIssuerPrincipal(cert).equals(getIssuerPrincipal(crl)))
{
Expand Down Expand Up @@ -479,6 +455,60 @@ else if (!getEncodedIssuerPrincipal(cert).equals(getIssuerPrincipal(crl)))
}
}

/**
* Find the entry of an indirect CRL for the certificate with the given serial number and issuer.
* An indirect CRL (RFC 5280 sec. 5.2.5) lists certificates from more than one issuer and a serial
* number is only unique within its issuer, so the entry cannot be located by serial number alone:
* every entry carrying the serial number is checked against the issuer it applies to, the one
* named by its certificateIssuer extension or inherited from the entries before it (sec. 5.3.3).
*
* @return the entry naming the certificate, or null if the CRL carries none.
*/
private static X509CRLEntry getIndirectCRLEntry(X509CRL crl, BigInteger serialNumber, X500Principal certIssuer)
throws AnnotatedException
{
Set entries = crl.getRevokedCertificates();
if (entries == null)
{
return null;
}

for (Iterator it = entries.iterator(); it.hasNext();)
{
X509CRLEntry entry = (X509CRLEntry)it.next();
if (!serialNumber.equals(entry.getSerialNumber()))
{
continue;
}

X500Principal entryIssuer;
try
{
entryIssuer = entry.getCertificateIssuer();
}
catch (RuntimeException e)
{
// getCertificateIssuer() builds a new X500Principal from the entry's certificateIssuer
// DN, which can throw an unchecked IllegalArgumentException on a name that decodes
// structurally but is semantically invalid. Fail closed with the checked contract type
// rather than let it escape (or swallow it to null, which would fail revocation open).
throw new AnnotatedException("CRL entry certificate issuer could not be parsed.", e);
}

if (entryIssuer == null)
{
entryIssuer = getIssuerPrincipal(crl);
}

if (certIssuer.equals(entryIssuer))
{
return entry;
}
}

return null;
}

/**
* Return the next working key inheriting DSA parameters if necessary.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public void testSimpleTests()
new GOST3410_2012CMSTest(),
new GOSTR3410_2012_256GenerateCertificate(),
new IndirectCRLSignerTest(),
new IndirectCRLSerialCollisionTest(),
new MLDSACredentialsTest(),
new PKCS10Test(),
new RelatedCertificateDescriptorTest(),
Expand Down
Loading
Loading