Skip to main content

ctclient_async/internal/
consistency.rs

1use std::convert::TryInto;
2
3use crate::Error;
4use crate::internal::get_json;
5use crate::jsons;
6use crate::utils::{combine_tree_hash, largest_power_of_2_smaller_than, u8_to_hex};
7use log::trace;
8
9/// Function used by
10/// [`verify_consistency_proof`] to construct a consistency proof client side
11/// (which is used to check against the server proof)
12///
13/// Returns an array of (u64, u64)s. Each (x: u64, y: u64) denotes that this part
14/// of the proof should be the hash of the subtree formed by leafs with number [x, y).
15///
16/// This function is only useful to those who want to do some custom proof
17/// handling. You should probably use
18/// [`verify_consistency_proof`] instead.
19///
20/// Will not omit the first component even if it's the same as `prev_tree_hash`
21/// (the server will). This means that the subtree represented by ret\[0] will always be
22/// contained within (0, from_size) (i.e. already known).
23///
24/// # Example
25///
26/// ```
27/// # use ctclient_async::internal::consistency_proof_parts;
28/// // Examples from RFC 6962 2.1.3 (https://tools.ietf.org/html/rfc6962#section-2.1.3)
29/// assert_eq!(consistency_proof_parts(3, 7), vec![(2, 3), (3, 4), (0, 2), (4, 7)]);
30/// assert_eq!(consistency_proof_parts(4, 7), vec![(0, 4), (4, 7)]);
31/// assert_eq!(consistency_proof_parts(6, 7), vec![(4, 6), (6, 7), (0, 4)]);
32/// ```
33pub fn consistency_proof_parts(from_size: u64, to_size: u64) -> Vec<(u64, u64)> {
34    // The function 'verify_consistency_proof' contains detailed comments about the nature of consistency proofs.
35
36    fn inner(result_store: &mut Vec<(u64, u64)>, subtree: (u64, u64), from_size: u64) {
37        assert!(subtree.0 < subtree.1);
38        assert!(from_size <= subtree.1);
39        if from_size == subtree.1 {
40            result_store.push(subtree);
41            return;
42        }
43        let subtree_size = subtree.1 - subtree.0;
44        let start_of_right_branch = largest_power_of_2_smaller_than(subtree_size);
45        if from_size - subtree.0 <= start_of_right_branch {
46            // go left
47            result_store.push((subtree.0 + start_of_right_branch, subtree.1));
48            inner(
49                result_store,
50                (subtree.0, subtree.0 + start_of_right_branch),
51                from_size,
52            );
53        } else {
54            // go right
55            result_store.push((subtree.0, subtree.0 + start_of_right_branch));
56            inner(
57                result_store,
58                (subtree.0 + start_of_right_branch, subtree.1),
59                from_size,
60            );
61        }
62    }
63    let mut result_store = Vec::new();
64    inner(&mut result_store, (0, to_size), from_size);
65    result_store.reverse();
66    result_store
67}
68
69#[test]
70fn consistency_proof_partial_test() {
71    assert_eq!(consistency_proof_parts(753913835, 753913848).len(), 25);
72    assert_eq!(consistency_proof_parts(6, 6), vec![(0, 6)]);
73    assert_eq!(consistency_proof_parts(7, 7), vec![(0, 7)]);
74
75    assert_eq!(consistency_proof_parts(4, 7), vec![(0, 4), (4, 7)]);
76}
77
78/// A subtree hash provided by the server in a consistency proof.
79pub struct ConsistencyProofPart {
80    /// (start, non-inclusive end)
81    pub subtree: (u64, u64),
82
83    /// The hash of this subtree as from the proof
84    pub server_hash: [u8; 32],
85}
86
87/// Verify that the consistency proof given by `server_provided_proof` gets us
88/// from `perv_root` to `next_root`, returning an `Ok(Vec<ConsistencyProofPart>)`
89/// if the proof checks, otherwise a `Err(String)` describing why the proof is
90/// invalid.
91///
92/// This function is only useful to those who want to do some custom API calling.
93/// If you're using a [`CTClient`](crate::CTClient), it will handle proof
94/// checking for you.
95///
96/// To fetch the consistency proof from the server and verifies it, call
97/// [`check_consistency_proof`].
98///
99/// # `Ok(Vec<ConsistencyProofPart>)`
100///
101/// The `Ok` result of this function contains all components of the proof which
102/// describes a new tree (that's not in the previous tree). This can be useful if
103/// you want to then get all the new certificates and verify that those forms the
104/// new tree.
105///
106/// To do this, calculate the leaf hash of all the new certificates, and call
107/// [`ConsistencyProofPart::verify`] with the array of leaf hashes. See its
108/// documentation for more info.
109///
110/// # Panic
111///
112/// `verify_consistency_proof` panics if `perv_size` > `next_size`.
113///
114pub fn verify_consistency_proof(
115    perv_size: u64,
116    next_size: u64,
117    server_provided_proof: &[[u8; 32]],
118    perv_root: &[u8; 32],
119    next_root: &[u8; 32],
120) -> Result<Vec<ConsistencyProofPart>, String> {
121    // todo: add test for this.
122
123    if perv_size > next_size {
124        panic!("perv_size must be <= next_size");
125    }
126    if perv_size == next_size {
127        return Ok(Vec::new());
128    }
129    if perv_size == 0 {
130        // An empty tree is a subtree of every tree. No need to prove.
131        return Ok(vec![ConsistencyProofPart {
132            subtree: (0, next_size),
133            server_hash: *next_root,
134        }]);
135    }
136
137    // A consistency proof is an array of hashes of some subtrees of the current
138    // tree. These subtrees will entirely cover the previous tree, and will also
139    // include some new parts which is only in the current tree. To validate the
140    // proof, we attempt to derive the new root hash based on these provided
141    // hashes. If we got the same hash as the server signed tree hash, we know that
142    // the previous tree is entirely contained in the new tree. In addition, we
143    // also need to check that the hashes which corresponds to subtrees that
144    // contains previous nodes are genuine. We do this by attempting to construct
145    // the previous root hash based on these hashes, and see if we came up with a
146    // hash that is the same as the `perv_root` provided by the caller.
147
148    // A subtree is represented with (u64, u64), where the first number is the
149    // starting index, and the second number is the non-inclusive ending index. For
150    // example, (2, 4) denote the 2-level subtree made by the nodes with index 2
151    // and 3, which looks like this:
152    //
153    //      23
154    //     /  \
155    //    2    3
156
157    // Calculate the proof ourselves first so that we know how to use the server
158    // provided proof.
159    let calculated_proof = consistency_proof_parts(perv_size, next_size);
160
161    // The server will omit the first hash if it will otherwise simply be the
162    // previous root hash. This happens when previous tree is a complete balanced
163    // tree, sitting in the bottom-left corner of the current tree. Since these
164    // trees always start at 0, we only need to check if the size is a power of 2
165    // (hence a balanced tree)
166    let omit_first = u64::is_power_of_two(perv_size);
167
168    let mut expected_proof_len = calculated_proof.len();
169    if omit_first {
170        expected_proof_len -= 1;
171    }
172    if server_provided_proof.len() != expected_proof_len {
173        return Err(format!(
174            "wrong proof length: expected {}, got {}",
175            expected_proof_len,
176            server_provided_proof.len()
177        ));
178    }
179
180    let mut hashes = Vec::new();
181    hashes.reserve(calculated_proof.len());
182    if omit_first {
183        hashes.push(*perv_root);
184    }
185    hashes.extend_from_slice(server_provided_proof);
186    assert_eq!(hashes.len(), calculated_proof.len());
187
188    // Now each element of `hashes` and `calculated_proof` match up
189    // (hash[i] is the hash of the subtree calculated_proof[i]), we could start to
190    // do our hashing, and try to derive the new root hash.
191
192    let mut derived_new_hash = hashes[0];
193    let mut derived_new_hash_subtree = calculated_proof[0];
194    for (subtree, hash) in (1..hashes.len()).map(|i| (calculated_proof[i], &hashes[i])) {
195        // Proof can't have overlapping subtrees
196        assert_ne!(derived_new_hash_subtree.0, subtree.0);
197        // Two possibilities: either the current proof part represent a subtree which
198        // exists in the previous tree, or it represents an entirely new subtree. Proof entries
199        // can't represent overlapping trees/trees that cover both old and new nodes (otherwise there is
200        // no way to derive the hash of the old tree because the hashes to some part of the old tree is "mixed" together
201        // with some part of the new tree).
202        //
203        // In the first case, it will always be the "left" branch, and in the second case, "right" branch.
204        //
205        // We need to combine the hashes in the right order:
206        //  Left branch (previous branch) first, then right branch (new branch).
207        if subtree.0 > derived_new_hash_subtree.0 {
208            // Right branch
209            assert_eq!(subtree.0, derived_new_hash_subtree.1);
210            derived_new_hash = combine_tree_hash(&derived_new_hash, hash);
211            derived_new_hash_subtree = (derived_new_hash_subtree.0, subtree.1);
212        } else {
213            // Left branch
214            assert_eq!(subtree.1, derived_new_hash_subtree.0);
215            derived_new_hash = combine_tree_hash(hash, &derived_new_hash);
216            derived_new_hash_subtree = (subtree.0, derived_new_hash_subtree.1);
217        }
218    }
219    if derived_new_hash != *next_root {
220        return Err(format!(
221            "calculated tree root {} does not match given tree root {}",
222            u8_to_hex(&derived_new_hash),
223            u8_to_hex(next_root)
224        ));
225    }
226
227    // Now make sure we can derive the hash of the previous tree from this proof.
228    if omit_first {
229        // we are sure that last tree is included in the new tree, because we used last tree's hash to calculate the new hash.
230        trace!(
231            "consistency checked from {} to {}; previous tree is complete.",
232            &u8_to_hex(perv_root),
233            &u8_to_hex(next_root)
234        );
235        Ok(hashes
236            .iter()
237            .zip(calculated_proof.iter())
238            .skip(1)
239            .map(|(hash, subtree)| ConsistencyProofPart {
240                subtree: *subtree,
241                server_hash: *hash,
242            })
243            .collect())
244    } else {
245        // First component of proof is always a part of the previous tree.
246        assert!(calculated_proof[0].1 <= perv_size);
247        let mut derived_old_hash: [u8; 32] = hashes[0];
248        let mut derived_old_hash_subtree: (u64, u64) = calculated_proof[0];
249        let mut new_parts = Vec::new();
250        for (subtree, hash) in (1..hashes.len()).map(|i| (calculated_proof[i], &hashes[i])) {
251            if subtree.1 <= perv_size {
252                // if next_subtree is part of the previous tree...
253                if subtree.0 > derived_old_hash_subtree.0 {
254                    assert_eq!(subtree.0, derived_old_hash_subtree.1);
255                    derived_old_hash = combine_tree_hash(&derived_old_hash, hash);
256                    derived_old_hash_subtree = (derived_old_hash_subtree.0, subtree.1);
257                } else {
258                    assert_eq!(subtree.1, derived_old_hash_subtree.0);
259                    derived_old_hash = combine_tree_hash(hash, &derived_old_hash);
260                    derived_old_hash_subtree = (subtree.0, derived_old_hash_subtree.1);
261                }
262            } else {
263                // Proof entries is either entirely new tree or entirely old tree.
264                assert!(subtree.0 >= perv_size);
265                new_parts.push(ConsistencyProofPart {
266                    subtree,
267                    server_hash: *hash,
268                });
269            }
270        }
271        if derived_old_hash != *perv_root {
272            return Err(format!(
273                "calculated perv_root {} does not match given perv_root {}",
274                u8_to_hex(&derived_old_hash),
275                u8_to_hex(perv_root)
276            ));
277        }
278
279        trace!(
280            "consistency checked from {} to {}",
281            &u8_to_hex(perv_root),
282            &u8_to_hex(next_root)
283        );
284        Ok(new_parts)
285    }
286}
287
288impl ConsistencyProofPart {
289    /// Verify that an array of leaf_hashes could reconstruct this subtree's
290    /// `server_hash`, returning `Ok(())` when success and `Err(String)` when
291    /// failure, with a string describing the reason of failure.
292    ///
293    /// This function is only useful to those who want to do some custom API calling.
294    /// If you're using a [`CTClient`](crate::CTClient), it will handle proof
295    /// checking for you.
296    ///
297    /// # Panic
298    ///
299    /// `verify` panics when `leaf_hashes` does not have the right length, which
300    /// should be `subtree.1 - subtree.0`.
301    pub fn verify(&self, leaf_hashes: &[[u8; 32]]) -> Result<(), String> {
302        let subtree_size = self.subtree.1 - self.subtree.0;
303        if leaf_hashes.len() as u64 != subtree_size {
304            panic!(
305                "expected leaf_hashes to have length {}, got {}",
306                subtree_size,
307                leaf_hashes.len()
308            );
309        }
310        if subtree_size == 1 {
311            return if leaf_hashes[0] != self.server_hash {
312                Err(format!(
313                    "expected leaf_hashes to be [{}], got [{}]",
314                    u8_to_hex(&self.server_hash),
315                    u8_to_hex(&leaf_hashes[0])
316                ))
317            } else {
318                Ok(())
319            };
320        }
321        let mut round_hashes = Vec::from(leaf_hashes);
322        loop {
323            let mut new_round_hashes = Vec::new();
324            new_round_hashes.reserve(round_hashes.len() / 2);
325            for i in 0..(round_hashes.len() / 2) {
326                let hash_left = round_hashes[2 * i];
327                let hash_right = round_hashes[2 * i + 1];
328                new_round_hashes.push(combine_tree_hash(&hash_left, &hash_right));
329            }
330            if round_hashes.len() % 2 != 0 {
331                new_round_hashes.push(*round_hashes.last().unwrap());
332            }
333            round_hashes = new_round_hashes;
334            if round_hashes.len() == 1 {
335                break;
336            }
337        }
338        assert_eq!(round_hashes.len(), 1);
339        let calculated_hash = round_hashes[0];
340        if self.server_hash == calculated_hash {
341            Ok(())
342        } else {
343            Err(format!(
344                "Subtree {:?}: calculated that tree hash should be {}, but got {} from consistency check.",
345                &self.subtree,
346                u8_to_hex(&calculated_hash),
347                u8_to_hex(&self.server_hash)
348            ))
349        }
350    }
351}
352
353#[test]
354fn verify_consistency_proof_new_tree_leaf_hashes_test() {
355    use crate::utils::sha256;
356    fn h(s: &str) -> [u8; 32] {
357        sha256(s.as_bytes())
358    }
359    fn c(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
360        combine_tree_hash(a, b)
361    }
362
363    (ConsistencyProofPart {
364        subtree: (0, 1),
365        server_hash: h("a"),
366    })
367    .verify(&[h("a")])
368    .unwrap();
369
370    (ConsistencyProofPart {
371        subtree: (0, 1),
372        server_hash: h("a"),
373    })
374    .verify(&[h("b")])
375    .expect_err("!");
376
377    (ConsistencyProofPart {
378        subtree: (2, 4),
379        server_hash: c(&h("c"), &h("d")),
380    })
381    .verify(&[h("c"), h("d")])
382    .unwrap();
383
384    (ConsistencyProofPart {
385        subtree: (0, 6),
386        server_hash: c(
387            &c(&c(&h("a"), &h("b")), &c(&h("c"), &h("d"))),
388            &c(&h("e"), &h("f")),
389        ),
390    })
391    .verify(&[h("a"), h("b"), h("c"), h("d"), h("e"), h("f")])
392    .unwrap();
393
394    (ConsistencyProofPart {
395        subtree: (0, 6),
396        server_hash: c(
397            &c(&c(&h("a"), &h("b")), &c(&h("c"), &h("d"))),
398            &c(&h("e"), &h("f")),
399        ),
400    })
401    .verify(&[h("a"), h("b"), h("c"), h("g"), h("e"), h("f")])
402    .expect_err("!");
403
404    (ConsistencyProofPart {
405        subtree: (0, 6),
406        server_hash: c(
407            &c(&c(&h("a"), &h("b")), &c(&h("c"), &h("d"))),
408            &c(&h("e"), &h("f")),
409        ),
410    })
411    .verify(&[h("a"), h("b"), h("c"), h("d"), h("e"), h("g")])
412    .expect_err("!");
413
414    (ConsistencyProofPart {
415        subtree: (0, 4),
416        server_hash: c(&c(&h("a"), &h("b")), &c(&h("c"), &h("d"))),
417    })
418    .verify(&[h("a"), h("b"), h("c"), h("d")])
419    .unwrap();
420
421    (ConsistencyProofPart {
422        subtree: (0, 4),
423        server_hash: c(&c(&h("a"), &h("b")), &c(&h("c"), &h("d"))),
424    })
425    .verify(&[h("c"), h("d"), h("a"), h("b")])
426    .expect_err("!");
427}
428
429/// Fetch the consistency proof from prev_size to next_size from the server and
430/// verifies it, returning a `Vec<ConsistencyProofPart>` if successful, which can later be
431/// used to verify the integrity of certificates downloaded from the server
432/// later. An `Err(...)` is returned if the proof is invalid, or some network
433/// error happened during the request.
434///
435/// # `Ok(Vec<ConsistencyProofPart>)`
436///
437/// The `Ok` result of this function contains all components of the proof which
438/// describes a new tree (that's not in the previous tree). This can be useful if
439/// you want to then get all the new certificates and verify that those forms the
440/// new tree.
441///
442/// To do this, calculate the leaf hash of all the new certificates, and call
443/// [`ConsistencyProofPart::verify`] with the array of leaf hashes. See its
444/// documentation for more info.
445///
446/// # Panics
447///
448/// ...if prev_size >= next_size
449pub async fn check_consistency_proof(
450    client: &reqwest::Client,
451    base_url: &reqwest::Url,
452    prev_size: u64,
453    next_size: u64,
454    perv_root: &[u8; 32],
455    next_root: &[u8; 32],
456) -> Result<Vec<ConsistencyProofPart>, Error> {
457    assert!(prev_size < next_size);
458    let server_consistency_proof: jsons::ConsistencyProof = get_json(
459        client,
460        base_url,
461        &format!(
462            "ct/v1/get-sth-consistency?first={}&second={}",
463            prev_size, next_size
464        ),
465    )
466    .await?;
467    let server_consistency_proof = server_consistency_proof.consistency;
468    let mut parsed_server_proof: Vec<[u8; 32]> = Vec::new();
469    parsed_server_proof.reserve(server_consistency_proof.len());
470    let mut n = 0;
471    for i in server_consistency_proof.into_iter() {
472        n += 1;
473        let decoded = base64::decode(&i).map_err(|e| {
474            Error::MalformedResponseBody(format!(
475                "Can not base64 decode consistency proof element: {}",
476                &e
477            ))
478        })?;
479        if decoded.len() != 32 {
480            return Err(Error::MalformedResponseBody(
481                "Consistency proof element has length other than 32.".to_owned(),
482            ));
483        }
484        parsed_server_proof.push(decoded[..].try_into().unwrap());
485    }
486    assert_eq!(parsed_server_proof.len(), n);
487    verify_consistency_proof(
488        prev_size,
489        next_size,
490        &parsed_server_proof,
491        perv_root,
492        next_root,
493    )
494    .map_err(|e| Error::InvalidConsistencyProof {
495        prev_size,
496        new_size: next_size,
497        desc: e,
498    })
499}