Skip to content

Commit cd23bd0

Browse files
committed
test the returned type in oer getInstance identity guards
1 parent 22db5b9 commit cd23bd0

7 files changed

Lines changed: 89 additions & 5 deletions

File tree

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ <h2>2.0 Release History</h2>
2323
Date:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2026, TBD
2424
<h3>2.1.2 Defects Fixed</h3>
2525
<ul>
26+
<li>Four type-coercion helpers in the OER / IEEE 1609.2 (ITS) decoder tested the wrong type in the identity fast path that lets a getInstance() factory return an argument that is already of the target type. org.bouncycastle.oer.its.ieee1609dot2.basetypes.UINT32.getInstance and org.bouncycastle.oer.its.etsi102941.basetypes.Version.getInstance guarded on UINT8 - a sibling of UINT32 under UintBase, and unrelated to Version - so passing a UINT8 threw ClassCastException, while passing an actual UINT32 or Version missed the fast path and fell through to ASN1Integer.getInstance, which rejects them: neither factory accepted its own type. org.bouncycastle.oer.its.etsi103097.EtsiTs103097DataEncryptedUnicast.getInstance guarded on its sibling EtsiTs103097DataEncrypted and then cast to the unicast type, so an EtsiTs103097DataEncrypted threw ClassCastException. org.bouncycastle.oer.OEROptional.getObject(Class) called value.getClass().isInstance(type) with the arguments transposed, which is always false because the argument is a java.lang.Class, so the cast path was dead and every optional field was resolved reflectively, failing with IllegalStateException for a target type with no static getInstance. Each guard now names the type it returns, matching the sibling UINT8 / UINT16 / UINT64 and EtsiTs103097DataEncrypted factories.</li>
2627
<li>The opt-in key-size validation on CMS key-transport recipients (org.bouncycastle.cms.jcajce.JceKeyTransRecipient.setKeySizeValidation(true)) never ran for a message using RFC 9709 CEK derivation (id-alg-cek-hkdf-sha256): the branch that should have selected the actual content-encryption algorithm carried in the KDF AlgorithmIdentifier's parameters compared the encrypted-key byte array against the id-alg-cek-hkdf-sha256 object identifier - a comparison that is always false - so the check fell through to a key-size lookup on the outer KDF OID, which has no registered key size, and silently checked nothing. A key-transport EnvelopedData/AuthEnvelopedData whose transported (and HKDF-derived) content-encryption key did not match the key size of the advertised content-encryption algorithm was therefore accepted even with validation enabled. The recipient now dispatches on the content-encryption AlgorithmIdentifier's algorithm OID, so key-size validation of RFC 9709 messages checks the recovered key against the inner content-encryption algorithm. Messages with a matching key size, non-HKDF messages, and recipients that do not enable validation are unaffected.</li>
2728
<li>org.bouncycastle.util.BigIntegers.intValueExact (and the byte/short/long variants) delegated to BigInteger.intValueExact from 1.85, a Java 8 method Android only provides from API level 33, so on earlier Android versions any code path using them - most visibly loading a PKCS12 keystore, whose iteration-count validation calls intValueExact - crashed with NoSuchMethodError. The range checks are open-coded again, as they were in 1.84 (github #2369).</li>
2829
<li>The OpenPGP v6 SEIPD (Symmetrically Encrypted Integrity Protected Data, version 2 / RFC 9580 sec. 5.13.2) packet parser read the AEAD chunk-size octet without bounding it. The chunk length is 2^(chunkSize + 6) bytes and the AEAD decryptor allocates a buffer of that size up front, so a crafted v6 encrypted message (reachable with only the recipient's public key) declaring chunkSize 24 forced a 1 GiB allocation on decrypt, and chunkSize 25 (where the int cast of the chunk length wraps negative) threw a NegativeArraySizeException -- a pre-authentication resource-exhaustion denial of service. This is the version 6 sibling of the version 5 AEADEncDataPacket issue fixed under CVE-2026-3505, which bounded that packet's chunk size at 16 but left the v6 SymmetricEncIntegrityPacket unbounded. SymmetricEncIntegrityPacket now rejects a chunk-size octet outside 0..16 (a 4 MiB chunk, matching the v5 ceiling) with a MalformedPacketException at parse, before any allocation.</li>

util/src/main/java/org/bouncycastle/oer/OEROptional.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public <T> T getObject(final Class<T> type)
6262

6363
if (defined)
6464
{
65-
if (value.getClass().isInstance(type))
65+
if (type.isInstance(value))
6666
{
6767
return type.cast(value);
6868
}

util/src/main/java/org/bouncycastle/oer/its/etsi102941/basetypes/Version.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import org.bouncycastle.asn1.ASN1Integer;
66
import org.bouncycastle.asn1.ASN1Object;
77
import org.bouncycastle.asn1.ASN1Primitive;
8-
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.UINT8;
98

109
public class Version
1110
extends ASN1Object
@@ -39,7 +38,7 @@ public BigInteger getVersion()
3938

4039
public static Version getInstance(Object o)
4140
{
42-
if (o instanceof UINT8)
41+
if (o instanceof Version)
4342
{
4443
return (Version)o;
4544
}

util/src/main/java/org/bouncycastle/oer/its/etsi103097/EtsiTs103097DataEncryptedUnicast.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ protected EtsiTs103097DataEncryptedUnicast(ASN1Sequence src)
1818

1919
public static EtsiTs103097DataEncryptedUnicast getInstance(Object o)
2020
{
21-
if (o instanceof EtsiTs103097DataEncrypted)
21+
if (o instanceof EtsiTs103097DataEncryptedUnicast)
2222
{
2323
return (EtsiTs103097DataEncryptedUnicast)o;
2424
}

util/src/main/java/org/bouncycastle/oer/its/ieee1609dot2/basetypes/UINT32.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ protected UINT32(ASN1Integer integer)
3232

3333
public static UINT32 getInstance(Object o)
3434
{
35-
if (o instanceof UINT8)
35+
if (o instanceof UINT32)
3636
{
3737
return (UINT32)o;
3838
}

util/src/test/java/org/bouncycastle/oer/test/AllTests.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public static Test suite()
4141
suite.addTestSuite(TestBuilders.class);
4242
suite.addTestSuite(OERExtensionTest.class);
4343
suite.addTestSuite(OERInputStreamLimitTest.class);
44+
suite.addTestSuite(OERTypeGuardTest.class);
4445

4546
return new BCTestSetup(suite);
4647
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.bouncycastle.oer.test;
2+
3+
import java.math.BigInteger;
4+
5+
import junit.framework.TestCase;
6+
import org.bouncycastle.asn1.ASN1Encodable;
7+
import org.bouncycastle.asn1.ASN1Integer;
8+
import org.bouncycastle.oer.OEROptional;
9+
import org.bouncycastle.oer.its.etsi102941.basetypes.Version;
10+
import org.bouncycastle.oer.its.etsi103097.EtsiTs103097DataEncrypted;
11+
import org.bouncycastle.oer.its.etsi103097.EtsiTs103097DataEncryptedUnicast;
12+
import org.bouncycastle.oer.its.ieee1609dot2.Ieee1609Dot2Content;
13+
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.UINT32;
14+
import org.bouncycastle.oer.its.ieee1609dot2.basetypes.UINT8;
15+
16+
/**
17+
* The identity fast path in the OER getInstance factories has to test the type it returns.
18+
*/
19+
public class OERTypeGuardTest
20+
extends TestCase
21+
{
22+
public void testUINT32GetInstance()
23+
{
24+
UINT32 uint32 = new UINT32(7);
25+
26+
assertSame(uint32, UINT32.getInstance(uint32));
27+
assertEquals(BigInteger.valueOf(7), UINT32.getInstance(new ASN1Integer(7)).getValue());
28+
29+
// a UINT8 is a sibling of UINT32 under UintBase, not a UINT32
30+
try
31+
{
32+
UINT32.getInstance(new UINT8(7));
33+
fail("UINT8 accepted as UINT32");
34+
}
35+
catch (IllegalArgumentException e)
36+
{
37+
// expected
38+
}
39+
}
40+
41+
public void testVersionGetInstance()
42+
{
43+
Version version = new Version(1);
44+
45+
assertSame(version, Version.getInstance(version));
46+
assertEquals(BigInteger.ONE, Version.getInstance(new ASN1Integer(1)).getVersion());
47+
48+
try
49+
{
50+
Version.getInstance(new UINT8(1));
51+
fail("UINT8 accepted as Version");
52+
}
53+
catch (IllegalArgumentException e)
54+
{
55+
// expected
56+
}
57+
}
58+
59+
public void testEtsiTs103097DataEncryptedUnicastGetInstance()
60+
{
61+
Ieee1609Dot2Content content = Ieee1609Dot2Content.unsecuredData(new byte[]{1, 2, 3});
62+
63+
EtsiTs103097DataEncryptedUnicast unicast = new EtsiTs103097DataEncryptedUnicast(content);
64+
65+
assertSame(unicast, EtsiTs103097DataEncryptedUnicast.getInstance(unicast));
66+
67+
// EtsiTs103097DataEncrypted is a sibling under EtsiTs103097Data, so it has to be decoded
68+
// rather than cast
69+
EtsiTs103097DataEncrypted encrypted = new EtsiTs103097DataEncrypted(content);
70+
71+
assertEquals(encrypted, EtsiTs103097DataEncryptedUnicast.getInstance(encrypted));
72+
}
73+
74+
public void testOEROptionalGetObject()
75+
{
76+
ASN1Integer value = new ASN1Integer(3);
77+
78+
assertSame(value, OEROptional.getInstance(value).getObject(ASN1Integer.class));
79+
80+
// ASN1Encodable has no getInstance, so only the cast path can satisfy this
81+
assertSame(value, OEROptional.getValue(ASN1Encodable.class, value));
82+
}
83+
}

0 commit comments

Comments
 (0)