Skip to content

Commit 9d36de8

Browse files
committed
check signed-message length in AIMerSigner.verifySignature
1 parent 2feaf10 commit 9d36de8

3 files changed

Lines changed: 50 additions & 0 deletions

File tree

‎core/src/main/java/org/bouncycastle/pqc/crypto/aimer/AIMerSigner.java‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ public byte[] generateSignature(byte[] message)
7777
@Override
7878
public boolean verifySignature(byte[] message, byte[] signature)
7979
{
80+
// generateSignature returns the message followed by the signature (the
81+
// signed-message envelope), so the signature proper starts at
82+
// message.length. Reject anything but exactly that envelope before
83+
// slicing it: a shorter buffer would throw
84+
// ArrayIndexOutOfBoundsException, and a longer one would have its
85+
// trailing bytes ignored, so a valid signature with data appended
86+
// would still verify.
87+
if (signature.length != message.length + params.getSignatureBytes())
88+
{
89+
return false;
90+
}
8091
byte[] sig = new byte[params.getSignatureBytes()];
8192
AIMerEngine engine = new AIMerEngine(params);
8293
System.arraycopy(signature, message.length, sig, 0, params.getSignatureBytes());

‎core/src/test/java/org/bouncycastle/pqc/crypto/test/AIMerTest.java‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.security.SecureRandom;
44

55
import junit.framework.TestCase;
6+
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
67
import org.bouncycastle.crypto.AsymmetricCipherKeyPairGenerator;
78
import org.bouncycastle.crypto.CipherParameters;
89
import org.bouncycastle.crypto.Signer;
@@ -14,6 +15,8 @@
1415
import org.bouncycastle.pqc.crypto.aimer.AIMerPrivateKeyParameters;
1516
import org.bouncycastle.pqc.crypto.aimer.AIMerPublicKeyParameters;
1617
import org.bouncycastle.pqc.crypto.aimer.AIMerSigner;
18+
import org.bouncycastle.util.Arrays;
19+
import org.bouncycastle.util.Strings;
1720

1821
public class AIMerTest
1922
extends TestCase
@@ -23,6 +26,7 @@ public static void main(String[] args)
2326
{
2427
AIMerTest test = new AIMerTest();
2528
test.testTestVectors();
29+
test.testWrongLengthSignatureRejected();
2630
}
2731

2832
private static final AIMerParameters[] PARAMETER_SETS = new AIMerParameters[]
@@ -94,4 +98,38 @@ public MessageSigner getMessageSigner()
9498
long end = System.currentTimeMillis();
9599
System.out.println("time cost: " + (end - start) + "\n");
96100
}
101+
102+
public void testWrongLengthSignatureRejected()
103+
{
104+
SecureRandom random = new SecureRandom();
105+
byte[] message = Strings.toByteArray("AIMer wrong length signature");
106+
107+
for (int i = 0; i != PARAMETER_SETS.length; i++)
108+
{
109+
AIMerParameters parameters = PARAMETER_SETS[i];
110+
111+
AIMerKeyPairGenerator kpGen = new AIMerKeyPairGenerator();
112+
kpGen.init(new AIMerKeyGenerationParameters(random, parameters));
113+
AsymmetricCipherKeyPair kp = kpGen.generateKeyPair();
114+
115+
AIMerSigner signer = new AIMerSigner();
116+
signer.init(true, kp.getPrivate());
117+
byte[] signature = signer.generateSignature(message);
118+
119+
AIMerSigner verifier = new AIMerSigner();
120+
verifier.init(false, kp.getPublic());
121+
122+
assertEquals(parameters.getName(), message.length + parameters.getSignatureBytes(), signature.length);
123+
assertTrue(parameters.getName(), verifier.verifySignature(message, signature));
124+
125+
// a short buffer must be rejected rather than indexed past its end
126+
assertFalse(parameters.getName(), verifier.verifySignature(message, new byte[0]));
127+
assertFalse(parameters.getName(), verifier.verifySignature(message, Arrays.copyOf(signature, signature.length - 1)));
128+
assertFalse(parameters.getName(), verifier.verifySignature(message, Arrays.copyOf(signature, message.length)));
129+
130+
// trailing data must not be silently ignored
131+
assertFalse(parameters.getName(), verifier.verifySignature(message, Arrays.append(signature, (byte)0)));
132+
assertFalse(parameters.getName(), verifier.verifySignature(message, Arrays.concatenate(signature, new byte[16])));
133+
}
134+
}
97135
}

‎docs/releasenotes.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ <h3>2.1.2 Defects Fixed</h3>
135135
<li>The BCJSSE provider now only computes active early key share groups for clients offering TLS 1.3+. Previously it was computed for all connections, which was mostly harmless, but could generate misleading log messages (at WARNING level) for servers or pre-TLS1.3 clients where the logged condition was irrelevant (github #2392).</li>
136136
<li>Initialising a BC signature, cipher or key-agreement service with a private key from another provider whose key material is not accessible - a hardware-backed key whose getModulus() / getX() / getS() / getParams() throws, as an IBM CCA RSAPrivateHWKey does with "Hardware error, function getModulus has no meaning in hardware" - let that provider-specific unchecked UnsupportedOperationException escape a method declared to throw only InvalidKeyException, so an mTLS handshake failed with a raw hardware error rather than the documented type. The key-parameter helpers that examine a key through a java.security.interfaces or javax.crypto.interfaces type a foreign provider may implement - RSAUtil, DSAUtil, ECUtil (its java.security.interfaces.ECPrivateKey branch), both DHUtil copies and ElGamalUtil - now catch a failure to read the key's parameters and raise InvalidKeyException with the original exception chained as its cause. A caller, or a JSSE layer that keys its provider fallback on InvalidKeyException, sees the declared type and a clear message. This does not let BC sign or decrypt with a non-exportable hardware key, which is not possible; it makes the refusal in contract. BC's own keys and any exportable key are unaffected (github #1440).</li>
137137
<li>org.bouncycastle.operator.DefaultKemEncapsulationLengthProvider.getEncapsulationLength() looked its argument up in a table of the KEMs whose encapsulation lengths are registered - ML-KEM, NTRU, HQC, FrodoKEM and composite ML-KEM - and dereferenced the result without checking it. A CMS RFC 9629 KEMRecipientInfo recipient using any other KEM which does register a key wrapping cipher, BIKE, NTRU+ or SMAUG-T, got as far as the wrap and then failed with a NullPointerException carrying no indication of which algorithm was at fault. The lookup now throws IllegalArgumentException naming the KEM's OID, as the sibling DefaultKemAlgorithmIdentifierFinder does, and the contract is documented on the KemEncapsulationLengthProvider interface (github #2398).</li>
138+
<li>AIMerSigner.verifySignature (org.bouncycastle.pqc.crypto.aimer) sliced the signature out of the signed-message envelope generateSignature produces - the message followed by the signature - at offset message.length without first checking the envelope was that long, so a truncated or otherwise short signature threw ArrayIndexOutOfBoundsException out of verify rather than returning false, reaching the caller unchecked through Signature.verify() on the BCPQC "AIMer" services; and because only the bytes at that offset were read, a valid signature with data appended still verified, so the accepted encoding was not unique. The envelope is now required to be exactly message.length + the parameter set's signature size before it is read, and anything else is reported as a failed verification, matching the guard the Falcon, Faest, Mayo, Snova and QRUOV signers apply. A correctly formed signature is unaffected.</li>
138139
</ul>
139140

140141
<h3>2.1.3 Additional Features and Functionality</h3>

0 commit comments

Comments
 (0)