diff --git a/docs/releasenotes.html b/docs/releasenotes.html
index 7e53639b5c..fccfc1397b 100644
--- a/docs/releasenotes.html
+++ b/docs/releasenotes.html
@@ -23,6 +23,7 @@
2.0 Release History
Date: 2026, TBD
2.1.2 Defects Fixed
+- 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.
- 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.
- 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ł 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.
- 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).
diff --git a/util/src/main/java/org/bouncycastle/asn1/cms/SignerInfo.java b/util/src/main/java/org/bouncycastle/asn1/cms/SignerInfo.java
index f88ff37aec..dd066ae94e 100644
--- a/util/src/main/java/org/bouncycastle/asn1/cms/SignerInfo.java
+++ b/util/src/main/java/org/bouncycastle/asn1/cms/SignerInfo.java
@@ -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());
@@ -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
{
diff --git a/util/src/test/java/org/bouncycastle/asn1/cms/test/SignerInfoTest.java b/util/src/test/java/org/bouncycastle/asn1/cms/test/SignerInfoTest.java
new file mode 100644
index 0000000000..57a90272bc
--- /dev/null
+++ b/util/src/test/java/org/bouncycastle/asn1/cms/test/SignerInfoTest.java
@@ -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());
+ }
+}
diff --git a/util/src/test/java/org/bouncycastle/asn1/util/test/RegressionTest.java b/util/src/test/java/org/bouncycastle/asn1/util/test/RegressionTest.java
index ce424174cc..b7fa7778ad 100644
--- a/util/src/test/java/org/bouncycastle/asn1/util/test/RegressionTest.java
+++ b/util/src/test/java/org/bouncycastle/asn1/util/test/RegressionTest.java
@@ -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;
@@ -108,6 +109,7 @@ public class RegressionTest
new PollReqContentTest(),
new AttributeTableUnitTest(),
new CMSTest(),
+ new SignerInfoTest(),
new DhSigStaticTest(),
new PKIPublicationInfoTest(),
new CommitmentTypeIndicationUnitTest(),