Skip to content

Commit d207ecc

Browse files
committed
ERS: a data group's hash is computed in calculateHash() again, so it comes from the ERSCachingData cache like every other data object's, and the copy loop that would have digested nothing is gone.
1 parent 009b7de commit d207ecc

3 files changed

Lines changed: 107 additions & 27 deletions

File tree

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Date: 2026, TBD
2525
- SSLContext.createSSLEngine() from the BCJSSE provider in the 1.86 bctls jar failed with NoSuchMethodError on every JDK from 9 up, leaving engine-based users of the provider (Netty, Vert.x and the like) unable to open a connection. The jdk1.5 and jdk1.9 copies of the package-private SSLEngineUtil had declared create(ContextData) with different return types since 2019 - SSLEngine and ProvSSLEngine - and the root ProvSSLContextSpi, compiled against the first, is paired at runtime with the versions/9 copy on any modern JDK. Until 1.86 the java9 compile had hidden this by implicitly recompiling the whole base tree into META-INF/versions/9 (447 classes, ProvSSLContextSpi among them); the -implicit:none added in 1.86 to stop that duplication exposed the mismatch. The jdk1.9 copy now declares the same return type as the root one, a JDK 25 test creates engines against the built jar, and a new multiReleaseCheck Gradle task on every distributed jar reads the constant pool of each class in the jar and in the sibling BC jars it depends on and fails the build when a member reference does not resolve against the copy of its target that a JDK would pair it with, so the class of defect cannot ship again; the same check runs on arbitrary jars, a published release included, as multiReleaseCheckJar (github #2448).
2626
- The BCFKS key store derived its scrypt keys with the block size r in place of the parallelization parameter p, while writing the p the caller configured out to the store: BcFKSKeyStoreSpi passed getBlockSize() to SCrypt.generate for both arguments and never read the encoded parallelization parameter at all, so every store whose ScryptConfig gave a p other than its r encoded parameters that do not derive its own keys. The store was self-consistent - BC read back what BC wrote - but a conformant RFC 7914 reader computed a different key and so failed the integrity check and the store decryption, and BC could not open such a store written by anyone else. Derivation now follows RFC 7914. A store written by 1.86 or earlier is still read: the integrity check is retried under the old convention, and where a signature check leaves no MAC to settle it the store decryption is retried instead, in both cases reporting the failure of the encoded parameters rather than of the fallback. The write side is governed by org.bouncycastle.bcfks.scrypt_p_eq_r, default true, which writes p equal to r whatever the ScryptConfig asked for: the two conventions then agree, so a store written here is both RFC 7914 correct and readable by 1.86 and earlier. Clearing the property honours the configured p, which those releases cannot read unless p already equals r; the default is intended to become false in a later release, once enough of the installed base is writing parameters that describe themselves. Loading with a BCFKSLoadStoreParameter carrying a ScryptConfig accepts an encoded p equal to either the configured p or the block size, so a store round trips under the configuration that wrote it whichever way the property was set; every other parameter is compared as before. The parallelization parameter is now bounded alongside the cost parameter before the derivation, as the PKCS#8 and PKCS#12 scrypt paths already bound it.
2727
- Building an evidence record was cubic in the number of data objects: SortedHashList and SortedIndexedHashList held their hashes in a LinkedList and found each insertion point by walking it with get(index), so a single add() was quadratic in the position it inserted at and building a list of n hashes cubic, and both lists sit on the generation path - the reduced hash tree of ERSArchiveTimeStampGenerator, the Merkle tree of BinaryTreeRootCalculator.computeRootHash(), and the hash list of every ERSDataGroup. Each now collects its hashes and sorts them once, in toList(); the sort is stable and the old insertion placed a hash after the last one comparing equal to it, which is where a stable sort puts it, so the order of the leaves and every root hash are unchanged. getFirst() answers with a scan rather than a sort and toList() sorts a copy, so neither accessor disturbs what has been added. Generating a time-stamp request over 8,000 data objects goes from about 210 seconds to under a tenth of a second, and 100,000 objects, which the old code could not reach in any practical time, takes about 0.2 seconds (github #2456).
28+
- An ERSDataGroup recomputed its hash on every request rather than taking it from the cache ERSCachingData exists to provide: the group overrode getHash(), which left the calculateHash() the cache calls unreachable - and wrong, as it copied the member hashes with a loop bounded by the size of the empty list it was copying into, so it would have digested nothing had anything reached it. The computation is back in calculateHash() and the override is gone, so a group's hash is computed once per digest algorithm and previous-chain hash, as every other ERSData's is. The value itself is unchanged.
2829

2930
### 2.1.3 Additional Features and Functionality
3031

‎pkix/src/main/java/org/bouncycastle/tsp/ers/ERSDataGroup.java‎

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -66,29 +66,11 @@ public List<byte[]> getHashes(
6666
}
6767

6868
/**
69-
* Return the calculated hash for the Data
70-
*
71-
* @param digestCalculator digest calculator to use.
72-
* @param previousChainHash hash from an earlier chain if it needs to be included.
73-
* @return calculated hash.
74-
*/
75-
public byte[] getHash(DigestCalculator digestCalculator, byte[] previousChainHash)
76-
{
77-
List<byte[]> hashes = getHashes(digestCalculator, previousChainHash);
78-
if (hashes.size() > 1)
79-
{
80-
return ERSUtil.calculateDigest(digestCalculator, hashes.iterator());
81-
}
82-
else
83-
{
84-
return (byte[])hashes.get(0);
85-
}
86-
}
87-
88-
/**
89-
* Generates a hash for the whole DataGroup.
69+
* Generates a hash for the whole DataGroup. Called through
70+
* {@link ERSCachingData#getHash(DigestCalculator, byte[])}, which caches the result.
9071
*
9172
* @param digestCalculator the {@link DigestCalculator} to use for computing the hash
73+
* @param previousChainHash hash from an earlier chain if it needs to be included.
9274
* @return a hash that is representative of the whole DataGroup
9375
*/
9476
protected byte[] calculateHash(DigestCalculator digestCalculator, byte[] previousChainHash)
@@ -97,12 +79,7 @@ protected byte[] calculateHash(DigestCalculator digestCalculator, byte[] previou
9779

9880
if (hashes.size() > 1)
9981
{
100-
List<byte[]> dHashes = new ArrayList<byte[]>(hashes.size());
101-
for (int i = 0; i != dHashes.size(); i++)
102-
{
103-
dHashes.add(hashes.get(i));
104-
}
105-
return ERSUtil.calculateDigest(digestCalculator, dHashes.iterator());
82+
return ERSUtil.calculateDigest(digestCalculator, hashes.iterator());
10683
}
10784
else
10885
{

‎pkix/src/test/java/org/bouncycastle/tsp/test/ERSTest.java‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1562,6 +1562,108 @@ private byte[] rootOf(List<ERSData> dataObjects, DigestCalculatorProvider digest
15621562
return ersGen.generateTimeStampRequest(tspReqGen).getMessageImprintDigest();
15631563
}
15641564

1565+
/**
1566+
* The leaves are built once and kept, so adding data after a request has been generated has
1567+
* to discard them - the generator is otherwise still describing the data it was asked about
1568+
* the first time.
1569+
*/
1570+
public void testDataAddedAfterRequest()
1571+
throws Exception
1572+
{
1573+
DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build();
1574+
1575+
ERSData doc1 = new ERSByteData(Strings.toByteArray("document 1"));
1576+
ERSData doc2 = new ERSByteData(Strings.toByteArray("document 2"));
1577+
ERSData doc3 = new ERSByteData(Strings.toByteArray("document 3"));
1578+
1579+
List<ERSData> two = new ArrayList<ERSData>();
1580+
two.add(doc1);
1581+
two.add(doc2);
1582+
1583+
List<ERSData> three = new ArrayList<ERSData>(two);
1584+
three.add(doc3);
1585+
1586+
ERSArchiveTimeStampGenerator ersGen = new ERSArchiveTimeStampGenerator(
1587+
digestCalculatorProvider.get(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256)));
1588+
1589+
ersGen.addData(doc1);
1590+
ersGen.addData(doc2);
1591+
1592+
TimeStampRequestGenerator tspReqGen = new TimeStampRequestGenerator();
1593+
1594+
tspReqGen.setCertReq(true);
1595+
1596+
byte[] twoRoot = ersGen.generateTimeStampRequest(tspReqGen).getMessageImprintDigest();
1597+
1598+
assertTrue(Arrays.areEqual(rootOf(two, digestCalculatorProvider), twoRoot));
1599+
1600+
// a repeat request over unchanged data gives the same root
1601+
assertTrue(Arrays.areEqual(twoRoot, ersGen.generateTimeStampRequest(tspReqGen).getMessageImprintDigest()));
1602+
1603+
ersGen.addData(doc3);
1604+
1605+
byte[] threeRoot = ersGen.generateTimeStampRequest(tspReqGen).getMessageImprintDigest();
1606+
1607+
assertFalse(Arrays.areEqual(twoRoot, threeRoot));
1608+
assertTrue(Arrays.areEqual(rootOf(three, digestCalculatorProvider), threeRoot));
1609+
1610+
// and the same by way of addAllData()
1611+
ERSArchiveTimeStampGenerator allGen = new ERSArchiveTimeStampGenerator(
1612+
digestCalculatorProvider.get(new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256)));
1613+
1614+
allGen.addData(doc1);
1615+
1616+
assertTrue(Arrays.areEqual(rootOf(Collections.singletonList(doc1), digestCalculatorProvider),
1617+
allGen.generateTimeStampRequest(tspReqGen).getMessageImprintDigest()));
1618+
1619+
List<ERSData> rest = new ArrayList<ERSData>();
1620+
rest.add(doc2);
1621+
rest.add(doc3);
1622+
1623+
allGen.addAllData(rest);
1624+
1625+
assertTrue(Arrays.areEqual(threeRoot, allGen.generateTimeStampRequest(tspReqGen).getMessageImprintDigest()));
1626+
}
1627+
1628+
/**
1629+
* A data group's hash is the digest of its members' hashes in ascending order, and it comes
1630+
* from the cache ERSCachingData provides, as every other ERSData's does.
1631+
*/
1632+
public void testDataGroupHash()
1633+
throws Exception
1634+
{
1635+
DigestCalculatorProvider digestCalculatorProvider = new JcaDigestCalculatorProviderBuilder().build();
1636+
DigestCalculator digestCalculator = digestCalculatorProvider.get(
1637+
new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256));
1638+
1639+
ERSData doc1 = new ERSByteData(Strings.toByteArray("document 1"));
1640+
ERSData doc2 = new ERSByteData(Strings.toByteArray("document 2"));
1641+
ERSData doc3 = new ERSByteData(Strings.toByteArray("document 3"));
1642+
1643+
ERSDataGroup group = new ERSDataGroup(new ERSData[]{doc1, doc2, doc3});
1644+
1645+
List<byte[]> hashes = group.getHashes(digestCalculator, null);
1646+
1647+
assertEquals(3, hashes.size());
1648+
1649+
MessageDigest digest = MessageDigest.getInstance("SHA-256", "BC");
1650+
1651+
for (int i = 0; i != hashes.size(); i++)
1652+
{
1653+
digest.update((byte[])hashes.get(i));
1654+
}
1655+
1656+
assertTrue(Arrays.areEqual(digest.digest(), group.getHash(digestCalculator, null)));
1657+
1658+
// the group hash is cached, so the same value comes back rather than being recomputed
1659+
assertTrue(group.getHash(digestCalculator, null) == group.getHash(digestCalculator, null));
1660+
1661+
// a group of one is the hash of its member
1662+
ERSDataGroup single = new ERSDataGroup(doc1);
1663+
1664+
assertTrue(Arrays.areEqual(doc1.getHash(digestCalculator, null), single.getHash(digestCalculator, null)));
1665+
}
1666+
15651667
public void testReducedHashTrees()
15661668
throws Exception
15671669
{

0 commit comments

Comments
 (0)