Skip to content

Commit 513f10a

Browse files
committed
Add Merkle tree proof generation to org.bouncycastle.cert.plants.MerkleTreePrimitives alongside the existing verifiers: computeMerkleTreeHash() for the RFC 9162 Merkle Tree Hash over a range of entries, generateSubtreeInclusionProof() for the RFC 9162 PATH inclusion proof of an entry within a subtree and generateSubtreeConsistencyProof() implementing SUBTREE_PROOF from draft-ietf-plants-merkle-tree-certs section 4.4.1, each over either an in-memory list of entry hashes or the new MerkleTreeNodeSource interface through which a storage-backed log supplies the full-subtree node hashes it already keeps, with ListMerkleTreeNodeSource as the in-memory implementation, MerkleTreeCertificatesTest rewritten onto the public API with generate-then-verify round trips and a misc example of the log-side flow.
1 parent a7267aa commit 513f10a

6 files changed

Lines changed: 938 additions & 35 deletions

File tree

docs/releasenotes.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ <h3>2.1.3 Additional Features and Functionality</h3>
201201
<li>Classic McEliece can now be used as a recipient KEM in CMS EnvelopedData / AuthEnvelopedData via the RFC 9629 KEMRecipientInfo structure. DefaultKemEncapsulationLengthProvider now knows the encapsulation (ciphertext) lengths for all sixteen parameter sets standardised under the ISO/IEC 18033-2 arc (mceliece460896 156, mceliece6688128 208, mceliece6960119 194, mceliece8192128 208, each of the plaintext-confirmation "pc" sets adding a 32 byte confirmation hash, and the semi-systematic key generation "f" variants leaving the ciphertext unchanged), which JceKEMRecipientInfoGenerator previously lacked - a Classic McEliece recipient failed the wrap. DefaultAlgorithmNameFinder and DefaultKemAlgorithmIdentifierFinder already carried the parameter set names. Unlike ML-KEM and FrodoKEM there is no CMS profile for Classic McEliece, so the key-wrap algorithm is the caller's choice; the round-trip tests pair mceliece460896 (level 3) with AES-Wrap-192 and the level 5 sets with AES-Wrap-256, keying the wrap with HKDF-SHA256.</li>
202202
<li>BCJSSE: the provider now offers server-side OCSP stapling, completing for the JSSE layer what the previous entry describes for the low-level TLS API. With the boolean system property jdk.tls.server.enableStatusRequestExtension set to true - default false, as in SunJSSE, since enabling it has the server make outbound OCSP requests on behalf of whoever connects to it - a BCJSSE server answers a client's "status_request" (RFC 6066 sec. 8) and "status_request_v2" (RFC 6961 sec. 2.2) extensions. For TLS 1.2 it sends a certificate_status message, preferring an ocsp_multi list covering the end-entity and every intermediate that has an issuer in the chain, and falling back to a single ocsp response for the end-entity; per RFC 6961 sec. 2.2 that list may be shorter than the certificate list and an element may be zero-length where no response is held. For TLS 1.3 there is no such message and the response instead rides in a "status_request" extension of the CertificateEntry it belongs to, holding an RFC 6066 CertificateStatus, per RFC 8446 sec. 4.4.2.1; status_request_v2 is deprecated there and is not honoured. A status request naming particular responders in its responder_id_list is left unanswered rather than answered with what some other responder said. Responses are fetched over HTTP per RFC 6960 App. A.1 from the responder named by each certificate's Authority Information Access extension - or from jdk.tls.stapling.responderURI when jdk.tls.stapling.responderOverride is set - read subject to the existing org.bouncycastle.ocsp.max_response_size ceiling, and discarded unless they carry a response bound to the certificate asked about. They are held in a per-SSLContext cache: a response is reused only while within both its own nextUpdate and jdk.tls.stapling.cacheLifetime, one stating no nextUpdate is stapled but never cached (RFC 6960 sec. 4.2.2.1 has an absent nextUpdate mean newer information is available all the time), and one dated more than the fifteen minute clock-skew allowance ahead of local time is treated as unreliable and not stapled at all. At most one request is in flight per certificate however many handshakes are waiting on it, a failed lookup suppresses further attempts for a short interval, and a handshake's wait is bounded by jdk.tls.stapling.responseTimeout - exactly, for one waiting on another's fetch; to within a read already in flight, for the one performing it, since a read timeout bounds each read rather than the exchange - a missing staple being an optimisation forgone, never a handshake failure. The server does not verify what it relays; verifying the response is the receiving client's part. On the client side a BCJSSE client now also reads TLS 1.3 staples, which BCExtendedSSLSession.getStatusResponses() returns positionally against the certificate chain - one element per certificate, zero-length where an entry carried no response - so that ProvX509TrustManager pairs each response with the certificate it answers for when handing them to the CertPath validator. Note this makes a client stricter about what a server sends it: RFC 8446 sec. 4.4.2.1 admits only a CertificateStatus of type ocsp in a CertificateEntry's "status_request" extension, so an extension carrying anything else - trailing bytes, the ocsp_multi form of the deprecated status_request_v2, an unparseable body - now fails the handshake with a decode_error alert, where previously these extension bodies were not read at all and a server that mis-stapled went unnoticed. Clients that do not want to request staples at all can clear jdk.tls.client.enableStatusRequestExtension. The TLS 1.2 client path is unchanged. See github issue #1157, and #1485 for the client-side TLS 1.3 read.</li>
203203
<li>TlsPeer.getHandshakeTimeoutMillis() is now also honoured by the transport-agnostic blocking TlsClientProtocol / TlsServerProtocol stream API, completing what 1.85 provided only for the (BC)JSSE blocking-socket path (see above). A peer returning a non-zero value has the handshake abandoned with a TlsTimeoutException once the deadline has passed since the handshake began, checked at each record boundary; because a caller-supplied InputStream has no timed-read primitive, a peer that stalls part way through a record still blocks in the read itself, so bounding that still requires the transport's own read timeout - which the (BC)JSSE provider already supplies for blocking sockets. There the caller's SO_TIMEOUT is restored once the handshake concludes, and a handshake interrupted part way (the blocking sockets always start a resumable handshake, so a read cut short by the caller's own tighter SO_TIMEOUT leaves the connection intact to retry) keeps the deadline armed across the interruption, so retrying cannot extend the total budget. The default behaviour is unchanged (AbstractTlsPeer returns zero, meaning no timeout), and the non-blocking API is left to the caller's own I/O loop (issue #1666).</li>
204+
<li>org.bouncycastle.cert.plants.MerkleTreePrimitives now generates Merkle Tree Certificate proofs as well as verifying them: computeMerkleTreeHash() computes the RFC 9162 Merkle Tree Hash over a range of entry hashes, generateSubtreeInclusionProof() computes the RFC 9162 PATH inclusion proof of an entry within a subtree, and generateSubtreeConsistencyProof() implements the SUBTREE_PROOF algorithm of draft-ietf-plants-merkle-tree-certs Section 4.4.1, the one Section 4 algorithm the class did not previously cover. The proofs they produce are accepted by the existing evaluateSubtreeInclusionProof(), verifySubtreeInclusionProof() and verifySubtreeConsistencyProof() methods, so an issuer or log built on MTCContentSigner or LandmarkCertificateManager no longer has to bring its own tree code to supply the proofs those APIs take as input. Each generator comes in two forms: one over an in-memory List of entry hashes, for tests, prototypes and the small standalone-certificate subtrees, and one over the new MerkleTreeNodeSource interface, through which a production log whose tree lives in storage supplies the full-subtree node hashes it already keeps - the only hashes the generators ever request, at most one per tree level per proof element - so proofs can be generated over a tree of any size without holding it in memory; ListMerkleTreeNodeSource is the in-memory implementation the List forms use.</li>
204205
</ul>
205206

206207
<h3>2.1.4 Additional Notes</h3>
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package org.bouncycastle.cert.plants.examples;
2+
3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import org.bouncycastle.cert.plants.MerkleTreeHash;
9+
import org.bouncycastle.cert.plants.MerkleTreeNodeSource;
10+
import org.bouncycastle.cert.plants.MerkleTreePrimitives;
11+
import org.bouncycastle.cert.plants.bc.BcSha256MerkleTreeHash;
12+
import org.bouncycastle.util.Arrays;
13+
import org.bouncycastle.util.Strings;
14+
import org.bouncycastle.util.encoders.Hex;
15+
16+
/**
17+
* Log-side walkthrough of the Merkle tree proof generators in
18+
* {@link MerkleTreePrimitives} (Section 4 of draft-ietf-plants-merkle-tree-certs).
19+
*
20+
* <p>The other examples in this package build their proofs by hand from a
21+
* handful of {@link MerkleTreeHash#hashNode} calls, which is fine for a
22+
* two-entry tree but not for anything a real log or CA would keep. This
23+
* example runs the same steps a log takes for a tree of arbitrary size:</p>
24+
* <ol>
25+
* <li><b>Entry hashes</b> — each log entry is hashed once with
26+
* {@link MerkleTreeHash#hashLeaf} (for certificates this is
27+
* {@code MerkleTreeCertificateValidator.computeEntryHash}); the
28+
* generators only ever see these hashes, never the entries.</li>
29+
* <li><b>Subtree hashes</b> — {@link MerkleTreePrimitives#computeMerkleTreeHash}
30+
* gives {@code MTH(D[start:end])} for the tree root, a landmark subtree, or
31+
* any other range (RFC 9162 Section 2.1.1).</li>
32+
* <li><b>Inclusion proof</b> — {@link MerkleTreePrimitives#generateSubtreeInclusionProof}
33+
* produces the sibling hashes an {@code MTCProof} carries for one entry
34+
* within a subtree (Section 4.3; RFC 9162 {@code PATH}). This is the
35+
* list {@code LandmarkCertificateManager.buildLandmarkCertificate} and
36+
* {@code MTCContentSigner} take as input.</li>
37+
* <li><b>Consistency proof</b> — {@link MerkleTreePrimitives#generateSubtreeConsistencyProof}
38+
* implements {@code SUBTREE_PROOF} (Section 4.4.1), relating a landmark
39+
* subtree to a later, larger checkpoint so a relying party can accept
40+
* the subtree per Section 7.4 ({@code TrustedSubtreeManager}).</li>
41+
* <li><b>Storage-backed trees</b> — the same generators over a
42+
* {@link MerkleTreeNodeSource}, the interface a production log implements
43+
* so proofs can be generated from stored node hashes without holding the
44+
* entries in memory.</li>
45+
* </ol>
46+
* <p>Each proof is then checked with the verifier the relying party would
47+
* run, so the example doubles as a demonstration that the two directions
48+
* agree.</p>
49+
*/
50+
public class MerkleTreeProofGenerationExample
51+
{
52+
public static void main(String[] args)
53+
{
54+
MerkleTreeHash hashFunc = new BcSha256MerkleTreeHash();
55+
56+
// 1. A log with 13 entries. In an MTC log each entry would be a
57+
// MerkleTreeCertEntry; here a short string stands in for it, and
58+
// the entry hash MTH({entry}) is all the tree code ever needs.
59+
int treeSize = 13;
60+
List<byte[]> entryHashes = new ArrayList<byte[]>();
61+
for (int i = 0; i < treeSize; i++)
62+
{
63+
entryHashes.add(hashFunc.hashLeaf(Strings.toByteArray("log entry " + i)));
64+
}
65+
System.out.println("Tree size: " + treeSize);
66+
67+
// 2. Subtree hashes. The root hash is what the log's checkpoint
68+
// commits to; [8, 12) is a subtree in the Section 4.1 sense (its
69+
// start is a multiple of its size), so it could be published as a
70+
// landmark subtree.
71+
byte[] rootHash = MerkleTreePrimitives.computeMerkleTreeHash(entryHashes, 0, treeSize, hashFunc);
72+
long start = 8, end = 12;
73+
byte[] subtreeHash = MerkleTreePrimitives.computeMerkleTreeHash(entryHashes, start, end, hashFunc);
74+
System.out.println("MTH(D[0:" + treeSize + "]): " + Hex.toHexString(rootHash));
75+
System.out.println("MTH(D[" + start + ":" + end + "]): " + Hex.toHexString(subtreeHash));
76+
System.out.println("[" + start + ", " + end + ") is a valid subtree: "
77+
+ MerkleTreePrimitives.isValidSubtree(start, end));
78+
79+
// 3. Inclusion proof for entry 10 within the subtree [8, 12): the
80+
// sibling hashes from the entry up to the subtree root. For a
81+
// four-entry subtree that is two hashes.
82+
long index = 10;
83+
List<byte[]> inclusionProof = MerkleTreePrimitives.generateSubtreeInclusionProof(
84+
index, start, end, entryHashes, hashFunc);
85+
System.out.println();
86+
System.out.println("Inclusion proof for entry " + index + " in [" + start + ", " + end + "):");
87+
print(inclusionProof);
88+
89+
// The relying party recomputes the subtree hash from the entry hash
90+
// and the proof and compares it with the subtree it trusts.
91+
boolean inclusionOk = MerkleTreePrimitives.verifySubtreeInclusionProof(
92+
index, start, end, entryHashes.get((int)index), subtreeHash, inclusionProof, hashFunc);
93+
System.out.println("Inclusion proof verifies: " + inclusionOk);
94+
95+
// 4. Consistency proof showing that the subtree [8, 12) is contained
96+
// in the tree of size 13 - what a relying party needs, alongside a
97+
// cosigned checkpoint for MTH(D[0:13]), before it will trust the
98+
// subtree for landmark-relative certificates.
99+
List<byte[]> consistencyProof = MerkleTreePrimitives.generateSubtreeConsistencyProof(
100+
start, end, treeSize, entryHashes, hashFunc);
101+
System.out.println();
102+
System.out.println("Consistency proof for [" + start + ", " + end + ") in tree of size " + treeSize + ":");
103+
print(consistencyProof);
104+
105+
boolean consistencyOk = MerkleTreePrimitives.verifySubtreeConsistencyProof(
106+
start, end, treeSize, subtreeHash, rootHash, consistencyProof, hashFunc);
107+
System.out.println("Consistency proof verifies: " + consistencyOk);
108+
109+
// 5. The log grows. A proof generated against the old size does not
110+
// verify against the new root, so the log regenerates it - the
111+
// subtree hash itself is unchanged, only the path to the root is.
112+
entryHashes.add(hashFunc.hashLeaf(Strings.toByteArray("log entry " + treeSize)));
113+
entryHashes.add(hashFunc.hashLeaf(Strings.toByteArray("log entry " + (treeSize + 1))));
114+
int newSize = entryHashes.size();
115+
byte[] newRoot = MerkleTreePrimitives.computeMerkleTreeHash(entryHashes, 0, newSize, hashFunc);
116+
List<byte[]> newProof = MerkleTreePrimitives.generateSubtreeConsistencyProof(
117+
start, end, newSize, entryHashes, hashFunc);
118+
System.out.println();
119+
System.out.println("Tree grown to size " + newSize + "; old proof verifies against new root: "
120+
+ MerkleTreePrimitives.verifySubtreeConsistencyProof(
121+
start, end, newSize, subtreeHash, newRoot, consistencyProof, hashFunc));
122+
System.out.println("Regenerated proof verifies: "
123+
+ MerkleTreePrimitives.verifySubtreeConsistencyProof(
124+
start, end, newSize, subtreeHash, newRoot, newProof, hashFunc));
125+
System.out.println("Subtree hash unchanged: "
126+
+ Arrays.areEqual(subtreeHash,
127+
MerkleTreePrimitives.computeMerkleTreeHash(entryHashes, start, end, hashFunc)));
128+
129+
// 6. Two identities from Section 4.4.1: with start == 0 the subtree
130+
// consistency proof is the RFC 9162 consistency proof PROOF(end, D_n),
131+
// and with end == start + 1 it is the inclusion proof PATH(start, D_n).
132+
List<byte[]> asPath = MerkleTreePrimitives.generateSubtreeConsistencyProof(
133+
index, index + 1, newSize, entryHashes, hashFunc);
134+
List<byte[]> path = MerkleTreePrimitives.generateSubtreeInclusionProof(
135+
index, 0, newSize, entryHashes, hashFunc);
136+
System.out.println("SUBTREE_PROOF(" + index + ", " + (index + 1) + ", D_n) == PATH(" + index + ", D_n): "
137+
+ sameHashes(asPath, path));
138+
139+
// 7. A production log does not hold its entries in a List. It stores
140+
// (or caches) the hashes of the tree's full subtrees - ranges whose
141+
// size is a power of two and whose start is aligned to it - and
142+
// implements MerkleTreeNodeSource over that storage; the generators
143+
// request only such nodes, one per tree level per proof element.
144+
// Here a HashMap stands in for the storage.
145+
final Map<String, byte[]> storage = new HashMap<String, byte[]>();
146+
for (long size = 1; size <= newSize; size <<= 1)
147+
{
148+
for (long from = 0; from + size <= newSize; from += size)
149+
{
150+
storage.put(from + ":" + (from + size),
151+
MerkleTreePrimitives.computeMerkleTreeHash(entryHashes, from, from + size, hashFunc));
152+
}
153+
}
154+
MerkleTreeNodeSource storedTree = new MerkleTreeNodeSource()
155+
{
156+
public byte[] getFullSubtreeHash(long from, long to)
157+
{
158+
byte[] node = storage.get(from + ":" + to);
159+
if (node == null)
160+
{
161+
throw new IllegalArgumentException("node [" + from + ", " + to + ") not in storage");
162+
}
163+
return node;
164+
}
165+
};
166+
List<byte[]> storedProof = MerkleTreePrimitives.generateSubtreeConsistencyProof(
167+
start, end, newSize, storedTree, hashFunc);
168+
System.out.println();
169+
System.out.println("Stored full subtrees: " + storage.size());
170+
System.out.println("Consistency proof from storage equals in-memory proof: "
171+
+ sameHashes(storedProof, newProof));
172+
System.out.println("Inclusion proof from storage equals in-memory proof: "
173+
+ sameHashes(
174+
MerkleTreePrimitives.generateSubtreeInclusionProof(index, start, end, storedTree, hashFunc),
175+
MerkleTreePrimitives.generateSubtreeInclusionProof(index, start, end, entryHashes, hashFunc)));
176+
}
177+
178+
private static void print(List<byte[]> proof)
179+
{
180+
for (int i = 0; i < proof.size(); i++)
181+
{
182+
System.out.println(" [" + i + "] " + Hex.toHexString(proof.get(i)));
183+
}
184+
}
185+
186+
private static boolean sameHashes(List<byte[]> a, List<byte[]> b)
187+
{
188+
if (a.size() != b.size())
189+
{
190+
return false;
191+
}
192+
for (int i = 0; i < a.size(); i++)
193+
{
194+
if (!Arrays.areEqual(a.get(i), b.get(i)))
195+
{
196+
return false;
197+
}
198+
}
199+
return true;
200+
}
201+
}

0 commit comments

Comments
 (0)