Skip to content

Commit 43e0a72

Browse files
committed
Merge branch 'main' of gitlab.cryptoworkshop.com:root/bc-java
2 parents acc070f + b2cd946 commit 43e0a72

4 files changed

Lines changed: 84 additions & 10 deletions

File tree

core/src/main/java/org/bouncycastle/crypto/engines/ISAACEngine.java

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ public class ISAACEngine
2727

2828
// Engine state
2929
private int index = 0;
30-
private byte[] keyStream = new byte[stateArraySize<<2], // results expanded into bytes
31-
workingKey = null;
30+
private byte[] workingKey = null;
3231
private boolean initialised = false;
3332

3433
/**
@@ -61,14 +60,13 @@ public void init(
6160

6261
public byte returnByte(byte in)
6362
{
64-
if (index == 0)
63+
if (index == 0)
6564
{
6665
isaac();
67-
keyStream = Pack.intToBigEndian(results);
6866
}
69-
byte out = (byte)(keyStream[index]^in);
67+
byte out = (byte)(keyStreamByte(index)^in);
7068
index = (index + 1) & 1023;
71-
69+
7270
return out;
7371
}
7472

@@ -96,12 +94,11 @@ public int processBytes(
9694

9795
for (int i = 0; i < len; i++)
9896
{
99-
if (index == 0)
97+
if (index == 0)
10098
{
10199
isaac();
102-
keyStream = Pack.intToBigEndian(results);
103100
}
104-
out[i+outOff] = (byte)(keyStream[index]^in[i+inOff]);
101+
out[i+outOff] = (byte)(keyStreamByte(index)^in[i+inOff]);
105102
index = (index + 1) & 1023;
106103
}
107104

@@ -189,6 +186,16 @@ private void setKey(byte[] keyBytes)
189186
initialised = true;
190187
}
191188

189+
/*
190+
* Extract byte idx (0..1023) of the current keystream block directly from the
191+
* generated words, in the same big-endian order Pack.intToBigEndian produces:
192+
* byte j of results[w] is results[w] >>> (24 - 8*j).
193+
*/
194+
private byte keyStreamByte(int idx)
195+
{
196+
return (byte)(results[idx >>> 2] >>> (24 - ((idx & 3) << 3)));
197+
}
198+
192199
private void isaac()
193200
{
194201
int i, x, y;

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ <h3>2.1.2 Defects Fixed</h3>
7575
<li>PKCS12KeyStoreSpi.processKeyBag and PKCS12PBMAC1KeyStoreSpi.processKeyBag threw a NullPointerException when loading a keystore containing an RFC 7292 sec. 4.2.1 keyBag (unencrypted PrivateKeyInfo) that either carried no bagAttributes or carried bagAttributes without a localKeyId — getBagAttributes() and the recovered localId were dereferenced unconditionally, unlike the sibling processShroudedKeyBag / processSecretBag which already guard both. Both keyBag handlers now skip the attribute scan when bagAttributes is absent and stash a localKeyId-less key under the "unmarked" alias, matching the shrouded-key-bag path, so such a keystore loads instead of failing on parse.</li>
7676
<li>OtherName (RFC 5280 sec. 4.2.1.6), SafeBag (RFC 7292 sec. 4.2) and SignerInfo (RFC 2315 sec. 9.2, the legacy PKCS#7 type) getInstance now validate the decoded SEQUENCE length — exactly 2 for OtherName, 2 or 3 for SafeBag, 5 to 7 for SignerInfo — and route element extraction through the typed getInstance helpers. A structurally invalid SEQUENCE (wrong element count, or an element of the wrong ASN.1 type) now fails fast with "Bad sequence size: &lt;n&gt;" / an IllegalArgumentException, rather than leaking an ArrayIndexOutOfBoundsException or ClassCastException out of the parse, matching the strict-parse behaviour of the neighbouring x509 / pkcs ASN.1 types.</li>
7777
<li>The OpenSSL PEM parsing path hardened its handling of malformed encryption metadata. A PEM block whose "Proc-Type: 4,ENCRYPTED" header was present but whose "DEK-Info" header was missing, or whose DEK-Info value lacked the IV component, previously leaked a NullPointerException (KeyPairParser) or NoSuchElementException out of PEMParser; both the "RSA/DSA/EC PRIVATE KEY" (KeyPairParser) and "PRIVATE KEY" (PrivateKeyParser) paths now reject such input with a PEMException ("malformed PEM data: missing or invalid DEK-Info header"). Separately, PemReader.readPemObject wrapped a malformed base64 body in a DecoderException (a RuntimeException) that escaped the method's declared IOException contract; the Base64 decode is now caught and re-thrown as an IOException with the original cause attached.</li>
78+
<li>Follow-up to CVE-2026-5588: the legacy id_alg_composite CompositeVerifier (org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder, used by X509CertificateHolder.isSignatureValid, CMS and TSP) iterated the component count from the supplied signature sequence rather than from the key, so although the earlier fix rejected an empty signature sequence, a composite signature truncated to a verifying prefix (e.g. stripped of its post-quantum or its classical component) still verified. The verifier now requires the signature to carry exactly one component for every component key, rejecting both under- and over-length composite signatures. The final-draft (IANA-OID) composite path was unaffected — it parses a fixed-length concatenation and already verifies every component.</li>
7879
</ul>
7980
<h3>2.2.3 Additional Features and Functionality</h3>
8081
<ul>

pkix/src/main/java/org/bouncycastle/operator/jcajce/JcaContentVerifierProviderBuilder.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,9 +430,18 @@ public boolean verify(byte[] expected)
430430
try
431431
{
432432
ASN1Sequence sigSeq = ASN1Sequence.getInstance(expected);
433+
434+
// A composite signature MUST carry exactly one component for every
435+
// component key; reject a signature that has had components removed
436+
// (e.g. stripped to a single verifiable component) or added.
437+
if (sigSeq.size() != sigs.length)
438+
{
439+
return false;
440+
}
441+
433442
boolean failed = false;
434443
boolean atLeastOneChecked = false;
435-
444+
436445
for (int i = 0; i != sigSeq.size(); i++)
437446
{
438447
if (sigs[i] != null)

pkix/src/test/java/org/bouncycastle/openssl/test/CompositeKeyTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.ByteArrayInputStream;
44
import java.io.FileOutputStream;
55
import java.io.IOException;
6+
import java.io.OutputStream;
67
import java.io.StringReader;
78
import java.io.StringWriter;
89
import java.math.BigInteger;
@@ -22,6 +23,9 @@
2223
import java.util.List;
2324

2425
import junit.framework.TestCase;
26+
import org.bouncycastle.asn1.ASN1Encoding;
27+
import org.bouncycastle.asn1.ASN1Sequence;
28+
import org.bouncycastle.asn1.DERSequence;
2529
import org.bouncycastle.asn1.cms.ContentInfo;
2630
import org.bouncycastle.asn1.iana.IANAObjectIdentifiers;
2731
import org.bouncycastle.asn1.misc.MiscObjectIdentifiers;
@@ -52,6 +56,7 @@
5256
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
5357
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
5458
import org.bouncycastle.operator.ContentSigner;
59+
import org.bouncycastle.operator.ContentVerifier;
5560
import org.bouncycastle.operator.ContentVerifierProvider;
5661
import org.bouncycastle.operator.DigestCalculatorProvider;
5762
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
@@ -327,6 +332,58 @@ public void testRSAAndECCompositeGen()
327332
// doOutput("/tmp/comp_pub_1.pem", pubKeyStr);
328333
}
329334

335+
336+
public void testCompositeSignatureStripping()
337+
throws Exception
338+
{
339+
// CVE-2026-5588 follow-up: the legacy id_alg_composite ContentVerifier
340+
// accepted a composite signature truncated to a single verifiable
341+
// component. The empty-sequence case was fixed; an under-length sequence
342+
// still passed. A composite signature MUST carry every component.
343+
KeyPairGenerator ecKpg = KeyPairGenerator.getInstance("EC", "BC");
344+
ecKpg.initialize(new ECNamedCurveGenParameterSpec("P-256"));
345+
KeyPair ecKp = ecKpg.generateKeyPair();
346+
347+
KeyPairGenerator rsaKpg = KeyPairGenerator.getInstance("RSA", "BC");
348+
rsaKpg.initialize(new RSAKeyGenParameterSpec(3072, RSAKeyGenParameterSpec.F4));
349+
KeyPair rsaKp = rsaKpg.generateKeyPair();
350+
351+
// component 0 = ECDSA, component 1 = RSA
352+
CompositeAlgorithmSpec compAlgSpec = new CompositeAlgorithmSpec.Builder()
353+
.add("SHA256withECDSA")
354+
.add("SHA256withRSA")
355+
.build();
356+
CompositePublicKey compPub = new CompositePublicKey(ecKp.getPublic(), rsaKp.getPublic());
357+
CompositePrivateKey compPriv = new CompositePrivateKey(ecKp.getPrivate(), rsaKp.getPrivate());
358+
359+
ContentSigner sigGen = new JcaContentSignerBuilder("Composite", compAlgSpec).build(compPriv);
360+
361+
X500Name name = new X500Name("CN=Composite Strip Test");
362+
X509CertificateHolder certHldr = new JcaX509v3CertificateBuilder(
363+
name, BigInteger.valueOf(1),
364+
new Date(System.currentTimeMillis() - 50000), new Date(System.currentTimeMillis() + 50000),
365+
name, compPub).build(sigGen);
366+
367+
// the genuine 2-of-2 composite signature verifies
368+
assertTrue("genuine composite signature should verify",
369+
certHldr.isSignatureValid(new JcaContentVerifierProviderBuilder().build(compPub)));
370+
371+
AlgorithmIdentifier sigAlgId = certHldr.getSignatureAlgorithm();
372+
byte[] tbs = certHldr.toASN1Structure().getTBSCertificate().getEncoded(ASN1Encoding.DER);
373+
ASN1Sequence sigSeq = ASN1Sequence.getInstance(certHldr.getSignature());
374+
assertTrue("expected a two-component composite signature", sigSeq.size() == 2);
375+
376+
// strip the RSA component, leaving only the (genuine) ECDSA component
377+
byte[] strippedSig = new DERSequence(sigSeq.getObjectAt(0)).getEncoded(ASN1Encoding.DER);
378+
379+
ContentVerifier cv = new JcaContentVerifierProviderBuilder().build(compPub).get(sigAlgId);
380+
OutputStream sOut = cv.getOutputStream();
381+
sOut.write(tbs);
382+
sOut.close();
383+
384+
assertFalse("composite signature stripped of a component must not verify", cv.verify(strippedSig));
385+
}
386+
330387
public void testRSAAndECCompositeSignedDataGen()
331388
throws Exception
332389
{

0 commit comments

Comments
 (0)