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.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ <h2>2.0 Release History</h2>
Date:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2026, TBD
<h3>2.1.2 Defects Fixed</h3>
<ul>
<li>The CMS SignerInfo decoder org.bouncycastle.asn1.cms.SignerInfo cast the version element directly to ASN1Integer and the trailing unsignedAttrs element directly to ASN1TaggedObject, so a SignerInfo whose version is not an INTEGER (or whose unsignedAttrs slot is not a context-tagged object) leaked an unchecked ClassCastException instead of the documented IllegalArgumentException. SignerInfo is decoded lazily by CMSSignedData.getSignerInfos() / SignerInformation.getCounterSignatures() outside the ClassCastException guard in getSignedData(), so a malformed-but-parseable CMS signed message escaped the throws-CMSException contract of CMSSignedData.verifySignatures(), which enumerates the signers before verifying. SignerInfo now decodes both elements via ASN1Integer.getInstance / ASN1TaggedObject.getInstance, matching org.bouncycastle.asn1.pkcs.SignerInfo, and rejects a bad element with IllegalArgumentException. Well-formed SignerInfos are unaffected.</li>
<li>The CMS attribute decoder org.bouncycastle.asn1.cms.Attribute cast the first two elements of its SEQUENCE directly to ASN1ObjectIdentifier / ASN1Set, so an attribute whose type element is not an OBJECT IDENTIFIER (or whose value is not a SET) - for example a tagged object - leaked an unchecked ClassCastException instead of the documented IllegalArgumentException. This escaped the throws-CMSException contract of SignerInformation.verify() / getSignedAttributes(), which build an AttributeTable over the parsed signed-attribute SET, when verifying a malformed-but-parseable CMS signed message. Attribute now decodes both elements via ASN1ObjectIdentifier.getInstance / ASN1Set.getInstance, rejecting a bad element with IllegalArgumentException. Well-formed attributes are unaffected.</li>
<li>Materializing a definite-length ASN.1 object through DefiniteLengthInputStream.toByteArray() allocated the full declared length up front (new byte[length]) before reading any content. When ASN1InputStream wraps a raw InputStream (a socket, a decompressed payload, etc.) the per-object length limit falls back to Runtime.maxMemory() in StreamUtil.findLimit(), so a short crafted header - for example a 6-byte OCTET STRING declaring a near-heap length with no body - could drive an OutOfMemoryError before a single content byte was read (CWE-789, memory allocation with excessive size value; reported by Micha&#322; Majchrowicz and Marcin Wyczechowski of AFINE). The buffer is now grown incrementally as bytes actually arrive (capped initial allocation, doubling towards the declared length), so the allocation a short input can force is bounded: a truncated stream fails with the same "DEF length ... object truncated by ..." EOFException as before, and a well-formed object of any size still materializes correctly. Callers wrapping a raw stream should still set an explicit limit via the ASN1InputStream(InputStream, int) constructor or the org.bouncycastle.asn1.max_limit property; this change removes the small-input-to-large-allocation amplification but the declared length remains bounded only by that limit.</li>
<li>The SP 800-208 XMSS / XMSS^MT parameter sets (SHA-256/192, SHAKE256/256, SHAKE256/192) could not be carried through a PKCS#8 PrivateKeyInfo: PrivateKeyInfoFactory always encoded XMSS / XMSS^MT private keys as the legacy PQCObjectIdentifiers.xmss / xmss_mt form with an XMSSKeyParams / XMSSMTKeyParams carrying only the tree height and tree-digest OID, which cannot express these sets. Encoding a SHAKE256/256 or SHAKE256/192 private key threw IllegalArgumentException ("unknown tree digest: SHAKE256-LEN"), and a SHA-256/192 private key (which shares id-sha256 with the RFC 8391 SHA-256/256 set, differing only in the security parameter n=24) encoded losslessly to the wrong OID and round-tripped through PrivateKeyFactory to a different, n=32 parameter set. Separately, the matching public key already encoded in the RFC 9802 id-alg-xmss-hashsig / id-alg-xmssmt-hashsig form for every standard set, so a keypair's private and public halves carried different algorithm OIDs. XMSS / XMSS^MT private keys for all standard (RFC 8391 and SP 800-208) parameter sets are now encoded in the same RFC 9802 form as the public key (id-alg-xmss-hashsig / id-alg-xmssmt-hashsig, with the 4-octet parameter-set identifier carried ahead of the raw key), so the private and public halves share one OID and a private key round-trips losslessly to its exact parameter set. Keys generated for non-standard tree heights (which have no RFC 8391 parameter-set identifier) continue to use the legacy PQCObjectIdentifiers.xmss / xmss_mt form, and that legacy form is still read on decode (issue #2176).</li>
Expand Down
4 changes: 2 additions & 2 deletions util/src/main/java/org/bouncycastle/asn1/cms/SignerInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ private SignerInfo(
{
Enumeration e = seq.getObjects();

version = (ASN1Integer)e.nextElement();
version = ASN1Integer.getInstance(e.nextElement());
sid = SignerIdentifier.getInstance(e.nextElement());
digAlgorithm = AlgorithmIdentifier.getInstance(e.nextElement());

Expand All @@ -207,7 +207,7 @@ private SignerInfo(

if (e.hasMoreElements())
{
unauthenticatedAttributes = ASN1Set.getInstance((ASN1TaggedObject)e.nextElement(), false);
unauthenticatedAttributes = ASN1Set.getInstance(ASN1TaggedObject.getInstance(e.nextElement()), false);
}
else
{
Expand Down
103 changes: 103 additions & 0 deletions util/src/test/java/org/bouncycastle/asn1/cms/test/SignerInfoTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.bouncycastle.asn1.cms.test;

import java.math.BigInteger;

import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.Attributes;
import org.bouncycastle.asn1.cms.CMSAttributes;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.cms.SignerIdentifier;
import org.bouncycastle.asn1.cms.SignerInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.util.test.SimpleTest;

public class SignerInfoTest
extends SimpleTest
{
private static final AlgorithmIdentifier digAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("2.16.840.1.101.3.4.2.1"));
private static final AlgorithmIdentifier sigAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.1.1.1"));

public String getName()
{
return "SignerInfo";
}

private static Attributes attrs(ASN1ObjectIdentifier type, ASN1ObjectIdentifier value)
{
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(new Attribute(type, new DERSet(value)));
return new Attributes(v);
}

public void performTest()
throws Exception
{
SignerIdentifier sid = new SignerIdentifier(
new IssuerAndSerialNumber(new X500Name("CN=Test"), new BigInteger("42")));

SignerInfo si = new SignerInfo(sid, digAlg,
attrs(CMSAttributes.contentType, new ASN1ObjectIdentifier("1.2.3")),
sigAlg, new DEROctetString(new byte[]{ 9, 9, 9 }),
attrs(CMSAttributes.counterSignature, new ASN1ObjectIdentifier("1.2.4")));

byte[] enc = si.getEncoded(ASN1Encoding.DER);
SignerInfo back = SignerInfo.getInstance(ASN1Primitive.fromByteArray(enc));
if (!areEqual(enc, back.getEncoded(ASN1Encoding.DER)))
{
fail("well-formed SignerInfo did not round-trip");
}

// SignerInfo.getInstance must reject a structurally-valid SEQUENCE whose version element is
// not an INTEGER (here an OBJECT IDENTIFIER) with IllegalArgumentException, rather than leak
// a ClassCastException from a (ASN1Integer) cast out of the getInstance contract.
ASN1EncodableVector badVersion = new ASN1EncodableVector();
badVersion.add(new ASN1ObjectIdentifier("1.2.3.4"));
badVersion.add(new DEROctetString(new byte[]{ 1 }));
badVersion.add(digAlg);
badVersion.add(sigAlg);
badVersion.add(new DEROctetString(new byte[]{ 2 }));
try
{
SignerInfo.getInstance(new DERSequence(badVersion));
fail("SignerInfo.getInstance accepted a non-INTEGER version element");
}
catch (IllegalArgumentException e)
{
// expected - documented malformed reject
}

// The unsignedAttrs slot must likewise reject a non-tagged trailing element with
// IllegalArgumentException rather than a leaked (ASN1TaggedObject) ClassCastException.
ASN1EncodableVector badUnsigned = new ASN1EncodableVector();
badUnsigned.add(new ASN1Integer(1));
badUnsigned.add(new DEROctetString(new byte[]{ 1 }));
badUnsigned.add(digAlg);
badUnsigned.add(sigAlg);
badUnsigned.add(new DEROctetString(new byte[]{ 2 }));
badUnsigned.add(new ASN1Integer(99));
try
{
SignerInfo.getInstance(new DERSequence(badUnsigned));
fail("SignerInfo.getInstance accepted a non-tagged unsignedAttrs element");
}
catch (IllegalArgumentException e)
{
// expected - documented malformed reject
}
}

public static void main(
String[] args)
{
runTest(new SignerInfoTest());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.bouncycastle.asn1.cmp.test.PollReqContentTest;
import org.bouncycastle.asn1.cms.test.AttributeTableUnitTest;
import org.bouncycastle.asn1.cms.test.CMSTest;
import org.bouncycastle.asn1.cms.test.SignerInfoTest;
import org.bouncycastle.asn1.crmf.test.DhSigStaticTest;
import org.bouncycastle.asn1.crmf.test.PKIPublicationInfoTest;
import org.bouncycastle.asn1.esf.test.CommitmentTypeIndicationUnitTest;
Expand Down Expand Up @@ -108,6 +109,7 @@ public class RegressionTest
new PollReqContentTest(),
new AttributeTableUnitTest(),
new CMSTest(),
new SignerInfoTest(),
new DhSigStaticTest(),
new PKIPublicationInfoTest(),
new CommitmentTypeIndicationUnitTest(),
Expand Down