Skip to main content

ctclient_async/internal/
inclusion.rs

1use crate::Error;
2use crate::internal::get_json;
3use crate::jsons::AuditProof;
4use crate::utils::{combine_tree_hash, u8_to_hex};
5use std::convert::TryInto;
6use std::ops::Range;
7
8/// Returns an array of `Range<u64>`s. Each x..y denotes that this part
9/// of the proof should be the hash of the subtree formed by leafs with number [x, y).
10///
11/// # Panics
12///
13/// If `index` >= `tree_size`.
14///
15/// # Examples
16///
17/// ```
18/// # use ctclient_async::internal::inclusion_proof_parts;
19/// // Examples from https://tools.ietf.org/html/rfc6962#section-2.1.3
20/// assert_eq!(inclusion_proof_parts(7, 0), vec![1..2, 2..4, 4..7]);
21/// assert_eq!(inclusion_proof_parts(7, 3), vec![2..3, 0..2, 4..7]);
22/// assert_eq!(inclusion_proof_parts(7, 4), vec![5..6, 6..7, 0..4]);
23/// assert_eq!(inclusion_proof_parts(7, 6), vec![4..6, 0..4]);
24/// ```
25pub fn inclusion_proof_parts(tree_size: u64, index: u64) -> Vec<Range<u64>> {
26    assert!(index < tree_size);
27    let mut current_subtree = index..(index + 1);
28    let mut result = Vec::new();
29    while current_subtree.end - current_subtree.start < tree_size {
30        let next_subtree_len = (current_subtree.end - current_subtree.start) * 2;
31        let next_subtree_start = current_subtree.start / next_subtree_len * next_subtree_len;
32        let next_subtree = next_subtree_start..(next_subtree_start + next_subtree_len);
33        let mid = next_subtree_start + next_subtree_len / 2;
34        if index < mid {
35            // hash right
36            if mid < tree_size {
37                result.push(mid..u64::min(next_subtree.end, tree_size));
38            } else {
39                // Happens if the last part of the tree is incomplete.
40                // Do nothing.
41            }
42        } else {
43            // hash left
44            result.push(next_subtree_start..mid);
45        }
46        current_subtree = next_subtree;
47    }
48    result
49}
50
51#[test]
52fn test_inclusion_proof_parts() {
53    assert_eq!(inclusion_proof_parts(1, 0), vec![]);
54    assert_eq!(inclusion_proof_parts(2, 0), vec![1..2]);
55    assert_eq!(inclusion_proof_parts(2, 1), vec![0..1]);
56    assert_eq!(inclusion_proof_parts(3, 1), vec![0..1, 2..3]);
57    assert_eq!(inclusion_proof_parts(5, 0), vec![1..2, 2..4, 4..5]);
58    assert_eq!(inclusion_proof_parts(5, 4), vec![0..4]);
59}
60
61/// Fetch the required inclusion proof from the server and see if it convinces us that `leaf_hash` is
62/// in the tree with hash `tree_hash` and size `tree_size`. On success, return the index number of the
63/// leaf corresponding with the hash.
64pub async fn check_inclusion_proof(
65    client: &reqwest::Client,
66    base_url: &reqwest::Url,
67    tree_size: u64,
68    tree_hash: &[u8; 32],
69    leaf_hash: &[u8; 32],
70) -> Result<u64, Error> {
71    let res = fetch_inclusion_proof(client, base_url, tree_size, leaf_hash).await?;
72    if &res.calculated_tree_hash != tree_hash {
73        return Err(Error::InvalidInclusionProof {
74            tree_size,
75            leaf_index: res.leaf_index,
76            desc: format!(
77                "Expected the proof to yield a tree hash of {}, but instead got {}.",
78                u8_to_hex(tree_hash),
79                u8_to_hex(&res.calculated_tree_hash)
80            ),
81        });
82    }
83    Ok(res.leaf_index)
84}
85
86pub struct FetchInclusionProofResult {
87    pub calculated_tree_hash: [u8; 32],
88    pub leaf_index: u64,
89}
90
91pub async fn fetch_inclusion_proof(
92    client: &reqwest::Client,
93    base_url: &reqwest::Url,
94    tree_size: u64,
95    leaf_hash: &[u8; 32],
96) -> Result<FetchInclusionProofResult, Error> {
97    let json: AuditProof = get_json(
98        client,
99        base_url,
100        &format!(
101            "ct/v1/get-proof-by-hash?{}",
102            serde_urlencoded::to_string(&[
103                ("hash", base64::encode(leaf_hash)),
104                ("tree_size", tree_size.to_string())
105            ])
106            .map_err(|e| Error::Unknown(format!("{}", e)))?
107        ),
108    )
109    .await?;
110    let leaf_index = json.leaf_index;
111    if json.leaf_index >= tree_size {
112        return Err(Error::InvalidInclusionProof {
113            tree_size,
114            leaf_index,
115            desc: "returned leaf_index >= tree_size.".to_owned(),
116        });
117    }
118    let proof_parts = inclusion_proof_parts(tree_size, leaf_index);
119    if proof_parts.len() != json.audit_path.len() {
120        return Err(Error::InvalidInclusionProof {
121            tree_size,
122            leaf_index,
123            desc: format!(
124                "Expected proof with {} parts, got {}.",
125                proof_parts.len(),
126                json.audit_path.len()
127            ),
128        });
129    }
130    let mut provided_proof: Vec<[u8; 32]> = Vec::with_capacity(proof_parts.len());
131    for i in 0..proof_parts.len() {
132        let hash = base64::decode(&json.audit_path[i]).map_err(|e| {
133            Error::MalformedResponseBody(format!("Unable to decode base64 in proof: {}", e))
134        })?;
135        if hash.len() != 32 {
136            return Err(Error::MalformedResponseBody(
137                "One or more component in the proof does not has length 32.".to_owned(),
138            ));
139        }
140        provided_proof.push(hash[..].try_into().unwrap());
141    }
142    let got_hash = hash_inclusion_proof(&proof_parts, &provided_proof, leaf_hash, leaf_index);
143    Ok(FetchInclusionProofResult {
144        calculated_tree_hash: got_hash,
145        leaf_index,
146    })
147}
148
149/// Attempt to derive the root hash from the server provided inclusion proof and our calculated proof_parts.
150///
151/// Used by [`check_inclusion_proof`].
152pub fn hash_inclusion_proof(
153    proof_parts: &[Range<u64>],
154    provided_proof: &[[u8; 32]],
155    leaf_hash: &[u8; 32],
156    leaf_index: u64,
157) -> [u8; 32] {
158    let mut current_hash = *leaf_hash;
159    let mut current_subtree = leaf_index..leaf_index + 1;
160    assert_eq!(proof_parts.len(), provided_proof.len());
161    for (proof_part, proof_hash) in proof_parts.iter().zip(provided_proof.iter()) {
162        if proof_part.start == current_subtree.end {
163            //          .
164            //       /     \
165            // [current] [proof]
166            current_hash = combine_tree_hash(&current_hash, proof_hash);
167            current_subtree = current_subtree.start..proof_part.end;
168        } else if proof_part.end == current_subtree.start {
169            // [proof]   [current]
170            current_hash = combine_tree_hash(proof_hash, &current_hash);
171            current_subtree = proof_part.start..current_subtree.end;
172        } else {
173            unreachable!()
174        }
175    }
176    current_hash
177}
178
179#[test]
180fn test() {}