Skip to content

Commit bd3d286

Browse files
committed
The three argument doFinal on TupleHash and ParallelHash now binds the requested output length as the L parameter of NIST SP 800-185 sec. 5.3 and sec. 6.3, as KMAC already did, rather than encoding the object's configured output size.
1 parent d8dca60 commit bd3d286

5 files changed

Lines changed: 121 additions & 3 deletions

File tree

‎core/src/main/java/org/bouncycastle/crypto/digests/ParallelHash.java‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private void wrapUp(int outputSize)
175175
compress();
176176
}
177177
byte[] nOut = XofUtils.rightEncode(nCount);
178-
byte[] encOut = XofUtils.rightEncode(outputSize * 8);
178+
byte[] encOut = XofUtils.rightEncode(outputSize * 8L);
179179

180180
cshake.update(nOut, 0, nOut.length);
181181
cshake.update(encOut, 0, encOut.length);
@@ -198,11 +198,26 @@ public int doFinal(byte[] out, int outOff)
198198
return rv;
199199
}
200200

201+
/**
202+
* Output the results of the final calculation for this digest to outLen number of bytes.
203+
* <p>
204+
* The requested output length is the L parameter bound into the calculation, so the result is
205+
* ParallelHash(X, B, outLen * 8, S) as defined in NIST SP 800-185 section 6.3, rather than the
206+
* first outLen bytes of the digest produced at this object's configured output size. Section
207+
* 6.1 requires this: "Changing any input parameter to ParallelHash, even the requested output
208+
* length, will result in unrelated output."
209+
* </p>
210+
*
211+
* @param out output array to write the output bytes to.
212+
* @param outOff offset to start writing the bytes at.
213+
* @param outLen the number of output bytes requested.
214+
* @return the number of bytes written
215+
*/
201216
public int doFinal(byte[] out, int outOff, int outLen)
202217
{
203218
if (firstOutput)
204219
{
205-
wrapUp(outputLength);
220+
wrapUp(outLen);
206221
}
207222

208223
int rv = cshake.doFinal(out, outOff, outLen);

‎core/src/main/java/org/bouncycastle/crypto/digests/TupleHash.java‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,26 @@ public int doFinal(byte[] out, int outOff)
123123
return rv;
124124
}
125125

126+
/**
127+
* Output the results of the final calculation for this digest to outLen number of bytes.
128+
* <p>
129+
* The requested output length is the L parameter bound into the calculation, so the result is
130+
* TupleHash(X, outLen * 8, S) as defined in NIST SP 800-185 section 5.3, rather than the first
131+
* outLen bytes of the digest produced at this object's configured output size. Section 5.1
132+
* requires this: "Changing any input to the function, including the requested output length,
133+
* will almost certainly change the final output."
134+
* </p>
135+
*
136+
* @param out output array to write the output bytes to.
137+
* @param outOff offset to start writing the bytes at.
138+
* @param outLen the number of output bytes requested.
139+
* @return the number of bytes written
140+
*/
126141
public int doFinal(byte[] out, int outOff, int outLen)
127142
{
128143
if (firstOutput)
129144
{
130-
wrapUp(getDigestSize());
145+
wrapUp(outLen);
131146
}
132147

133148
int rv = cshake.doFinal(out, outOff, outLen);

‎core/src/test/java/org/bouncycastle/crypto/test/ParallelHashTest.java‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,48 @@ public void performTest()
120120

121121
testEmpty();
122122
testClone();
123+
testVariableOutputLength();
124+
}
125+
126+
private void testVariableOutputLength()
127+
{
128+
byte[] data = Hex.decode("00 01 02 03 04 05 06 07 08 09 0A 0B 10 11 12 13 14 15 16 17 18 19 1A 1B 20 21 22 23 24 25 26 27 28 29 2A 2B 30 31 32 33 34 35 36 37 38 39 3A 3B 40 41 42 43 44 45 46 47 48 49 4A 4B 50 51 52 53 54 55 56 57 58 59 5A 5B");
129+
130+
// the requested length is the L parameter, so asking a 512 bit ParallelHash for 32 bytes
131+
// is ParallelHash128(X, 12, 256, S), the sample value above, not a truncation of the 64 byte one.
132+
ParallelHash pHash = new ParallelHash(128, Strings.toByteArray("Parallel Data"), 12, 512);
133+
134+
pHash.update(data, 0, data.length);
135+
136+
byte[] res = new byte[32];
137+
138+
pHash.doFinal(res, 0, res.length);
139+
140+
isTrue("oops!", Arrays.areEqual(Hex.decode("F7 FD 53 12 89 6C 66 85 C8 28 AF 7E 2A DB 97 E3 93 E7 F8 D5 4E 3C 2E A4 B9 5E 5A CA 37 96 E8 FC"), res));
141+
142+
pHash = new ParallelHash(256, Strings.toByteArray("Parallel Data"), 12, 256);
143+
144+
pHash.update(data, 0, data.length);
145+
146+
res = new byte[64];
147+
148+
pHash.doFinal(res, 0, res.length);
149+
150+
isTrue("oops!", Arrays.areEqual(Hex.decode("69 D0 FC B7 64 EA 05 5D D0 93 34 BC 60 21 CB 7E 4B 61 34 8D FF 37 5D A2 62 67 1C DE C3 EF FA 8D 1B 45 68 A6 CC E1 6B 1C AD 94 6D DD E2 7F 6C E2 B8 DE E4 CD 1B 24 85 1E BF 00 EB 90 D4 38 13 E9"), res));
151+
152+
// SP 800-185 sec. 6.1: changing the requested output length gives unrelated output, so the shorter one is not a prefix of the longer.
153+
byte[] shortRes = new byte[32];
154+
byte[] longRes = new byte[64];
155+
156+
pHash = new ParallelHash(128, Strings.toByteArray("Parallel Data"), 12);
157+
pHash.update(data, 0, data.length);
158+
pHash.doFinal(shortRes, 0, shortRes.length);
159+
160+
pHash = new ParallelHash(128, Strings.toByteArray("Parallel Data"), 12);
161+
pHash.update(data, 0, data.length);
162+
pHash.doFinal(longRes, 0, longRes.length);
163+
164+
isTrue("oops!", !Arrays.areEqual(shortRes, Arrays.copyOfRange(longRes, 0, 32)));
123165
}
124166

125167
private void testEmpty()

‎core/src/test/java/org/bouncycastle/crypto/test/TupleHashTest.java‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,50 @@ public void performTest()
124124
isTrue("oops!", Arrays.areEqual(Hex.decode("0c59b11464f2336c34663ed51b2b950bec743610856f36c28d1d088d8a2446284dd09830a6a178dc752376199fae935d86cfdee5913d4922dfd369b66a53c897"), res));
125125

126126
testClone();
127+
testVariableOutputLength();
128+
}
129+
130+
private void testVariableOutputLength()
131+
{
132+
// the requested length is the L parameter, so asking a 512 bit TupleHash for 32 bytes
133+
// is TupleHash128(X, 256, S), the sample value above, not a truncation of the 64 byte one.
134+
TupleHash tHash = new TupleHash(128, Strings.toByteArray("My Tuple App"), 512);
135+
136+
tHash.update(Hex.decode("000102"), 0, 3);
137+
tHash.update(Hex.decode("101112131415"), 0, 6);
138+
tHash.update(Hex.decode("202122232425262728"), 0, 9);
139+
140+
byte[] res = new byte[32];
141+
142+
tHash.doFinal(res, 0, res.length);
143+
144+
isTrue("oops!", Arrays.areEqual(Hex.decode("E6 0F 20 2C 89 A2 63 1E DA 8D 4C 58 8C A5 FD 07 F3 9E 51 51 99 8D EC CF 97 3A DB 38 04 BB 6E 84"), res));
145+
146+
tHash = new TupleHash(256, Strings.toByteArray("My Tuple App"), 256);
147+
148+
tHash.update(Hex.decode("000102"), 0, 3);
149+
tHash.update(Hex.decode("101112131415"), 0, 6);
150+
tHash.update(Hex.decode("202122232425262728"), 0, 9);
151+
152+
res = new byte[64];
153+
154+
tHash.doFinal(res, 0, res.length);
155+
156+
isTrue("oops!", Arrays.areEqual(Hex.decode("45 00 0B E6 3F 9B 6B FD 89 F5 47 17 67 0F 69 A9 BC 76 35 91 A4 F0 5C 50 D6 88 91 A7 44 BC C6 E7 D6 D5 B5 E8 2C 01 8D A9 99 ED 35 B0 BB 49 C9 67 8E 52 6A BD 8E 85 C1 3E D2 54 02 1D B9 E7 90 CE"), res));
157+
158+
// SP 800-185 sec. 5.1: changing the requested output length changes the output, so the shorter one is not a prefix of the longer.
159+
byte[] shortRes = new byte[32];
160+
byte[] longRes = new byte[64];
161+
162+
tHash = new TupleHash(128, Strings.toByteArray("My Tuple App"));
163+
tHash.update(Hex.decode("000102"), 0, 3);
164+
tHash.doFinal(shortRes, 0, shortRes.length);
165+
166+
tHash = new TupleHash(128, Strings.toByteArray("My Tuple App"));
167+
tHash.update(Hex.decode("000102"), 0, 3);
168+
tHash.doFinal(longRes, 0, longRes.length);
169+
170+
isTrue("oops!", !Arrays.areEqual(shortRes, Arrays.copyOfRange(longRes, 0, 32)));
127171
}
128172

129173
protected Digest cloneDigest(Digest digest)

‎docs/releasenotes.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ Date: 2026, TBD
208208

209209
- The PKCS12-PBMAC1 keystore asked PBKDF2 for a 256-octet MAC key against an HMAC-SHA-512 message-authentication scheme, four times the size of the key that scheme uses. RFC 9579 sec. 5 says the derived key "SHOULD be the same size as the HMAC function output size", giving the worked example of a 32-octet key for HMAC-SHA-256, so the keystore now asks for 64 - and so does the default on PKCS12StoreParameter.pbmac1WithPBKDF2Builder(), the public builder that configures the same field. Nothing was weaker for the extra length; the shorter derivation is the conformant one and is marginally cheaper. Reading is unaffected either way, since the length is taken from the file: a PFX written by an earlier release, asking for 256, still verifies, and that is why the keyLength upper bound is well above the HMAC output size rather than at it. The builder's setKeySize() parameter was named keySizeinBits but has always been used as octets, being passed straight into PBKDF2Params, whose keyLength RFC 8018 app. A.2 defines as "the length in octets of the derived key"; it is renamed keySizeInOctets, which changes no behaviour and no signature - only what the parameter is called. PBMAC1WithPBKDF2Builder had no javadoc at all, so the units were undiscoverable from the API docs; it and its six methods are now documented, including the RFC 9579 sec. 5 rule that the key length match the output size of the message-authentication scheme.
210210

211+
- The three argument doFinal(byte[], int, int) on org.bouncycastle.crypto.digests.TupleHash and ParallelHash ignored the requested output length when binding the L parameter, encoding right_encode of the object's configured output size instead. NIST SP 800-185 sec. 5.3 step 4 and sec. 6.3 step 4 make L both the value encoded into the message and the number of bits squeezed, so asking a TupleHash128 for 64 bytes produced neither TupleHash128(X, 512, S) nor any other function the specification names. Worse, because a 32 byte and a 64 byte request encoded the same L and then read different amounts of one output stream, the shorter result was a prefix of the longer one - contradicting the property the specification states outright for both functions, in sec. 5.1 ("Changing any input to the function, including the requested output length, will almost certainly change the final output") and sec. 6.1 ("Changing any input parameter to ParallelHash, even the requested output length, will result in unrelated output"). The requested length is now the L parameter, as it already was in KMAC, whose doFinal(out, outOff, outLen) is KMAC(K, X, outLen * 8, S). This changes the output of both classes for any caller passing an outLen other than the configured digest size; the two argument doFinal, which uses the configured size, and doOutput, which encodes right_encode(0) for the XOF variants of sec. 5.3.1 and sec. 6.3.1, are unaffected, as are the TupleHash / ParallelHash MessageDigest services of the BC provider, which only use the two argument form.
212+
211213
### 2.1.3 Additional Features and Functionality
212214

213215
- BIP340Signer can now be initialised from an AsymmetricCipherKeyPair, through the new init(boolean, AsymmetricCipherKeyPair) and init(boolean, AsymmetricCipherKeyPair, SecureRandom) methods. BIP-340 signing needs the public point twice, for the parity of d and for bytes(P) in the nonce and challenge hashes (sec. 3.3 steps 3-4), and derived it with a d'*G multiplication every time; a caller that already holds the public key can now supply it and skip that. Since the two fixed-base multiplications - that one and R = k'*G - are very nearly the whole cost of a signature, producing one measures about twice as fast this way (github #2420). The public key is not checked against the private key, because checking it means performing the multiplication being avoided; a mismatched pair simply produces a signature that does not verify, and the javadoc says so. Independently, the public point is now derived once per init() rather than on every generateSignature() call, so a signer initialised once and used for several signatures gets most of the same saving with no change on the caller's part. Signatures are unchanged in every case, and the existing CipherParameters init is untouched.

0 commit comments

Comments
 (0)