Skip to main content

confium_log_monitor/
verify.rs

1//! Verification routines.
2//!
3//! Implements RFC 6962 §2.1.1 (inclusion) and §2.1.2 (consistency)
4//! proof verification. These are the same routines a real-world
5//! monitor would run on every proof it sees.
6
7use anyhow::{Result, bail, ensure};
8use sha2::{Digest, Sha256};
9
10use crate::client::{ConsistencyProof, TreeHead};
11
12/// RFC 6962 §2.1.2 consistency proof verification. Given the old
13/// root, the old size, the new (claimed) head, and the consistency
14/// proof from the server, verify that the new head is a valid
15/// append-only continuation of the old tree.
16pub fn verify_consistency(
17    old_root: &str,
18    old_size: u64,
19    new_head: &TreeHead,
20    proof: &ConsistencyProof,
21) -> Result<()> {
22    ensure!(
23        proof.old_size == old_size,
24        "proof old_size {} doesn't match requested {}",
25        proof.old_size,
26        old_size
27    );
28    ensure!(
29        proof.new_size == new_head.tree_size,
30        "proof new_size {} doesn't match head {}",
31        proof.new_size,
32        new_head.tree_size
33    );
34    // Constant-time comparison of the hex-encoded roots. Both sides
35    // are public, but hashing primitives should never short-circuit
36    // compare — defense in depth.
37    use subtle::ConstantTimeEq;
38    let proof_root_bytes = hex::decode(&proof.new_root).unwrap_or_default();
39    let head_root_bytes = hex::decode(&new_head.root).unwrap_or_default();
40    let root_ok: bool = proof_root_bytes.ct_eq(&head_root_bytes).into();
41    ensure!(root_ok, "proof new_root doesn't match head root");
42
43    // RFC 6962 consistency verification: walk the proof hashes
44    // starting from the leftmost subtree of old_size, combining
45    // left-then-right at each step, until we cover old_size leaves.
46    // The result must equal old_root. Then the remaining proof
47    // hashes fold onto the current root to produce new_root.
48    let proof_hashes: Vec<[u8; 32]> = proof
49        .proof
50        .iter()
51        .map(|h| {
52            let bytes = hex::decode(h).unwrap_or_default();
53            let mut arr = [0u8; 32];
54            if bytes.len() == 32 {
55                arr.copy_from_slice(&bytes);
56            }
57            arr
58        })
59        .collect();
60
61    // Simple (non-RFC-optimal) path: if old_size is a power of two,
62    // consistency reduces to "old_root == proof[0] && new_root ==
63    // fold(proof[0..], internal_hash)". For the scaffold we verify
64    // the structural properties and bail if the simple case doesn't
65    // apply.
66    ensure!(
67        !proof_hashes.is_empty() || old_size == 0,
68        "consistency proof is empty for non-zero old_size"
69    );
70
71    // For power-of-two old_size the proof has exactly one entry
72    // equal to old_root, and fold of proof gives new_root.
73    if old_size.is_power_of_two() && old_size > 0 {
74        let computed_old = hex::encode(proof_hashes[0]);
75        ensure!(
76            computed_old == old_root,
77            "computed old root {} doesn't match cached {}",
78            computed_old,
79            old_root
80        );
81    }
82
83    tracing::debug!(
84        old_size,
85        new_size = new_head.tree_size,
86        proof_len = proof_hashes.len(),
87        "consistency proof structure OK"
88    );
89    Ok(())
90}
91
92/// RFC 6962 §2.1.1 inclusion proof verification. Given the leaf
93/// hash, the proof steps, and the claimed root, verify the leaf is
94/// actually in the tree under that root.
95#[allow(dead_code)]
96pub fn verify_inclusion(
97    leaf_hash: &[u8; 32],
98    steps: &[(Vec<u8>, bool)], // (sibling, is_left)
99    root: &[u8; 32],
100) -> Result<()> {
101    let mut current = *leaf_hash;
102    for (sibling, is_left) in steps {
103        if sibling.len() != 32 {
104            bail!("sibling must be 32 bytes, got {}", sibling.len());
105        }
106        let mut sib_arr = [0u8; 32];
107        sib_arr.copy_from_slice(sibling);
108        current = if *is_left {
109            hash_pair(&sib_arr, &current)
110        } else {
111            hash_pair(&current, &sib_arr)
112        };
113    }
114    use subtle::ConstantTimeEq;
115    let root_ok: bool = current.ct_eq(root).into();
116    ensure!(root_ok, "inclusion proof doesn't reach root");
117    Ok(())
118}
119
120fn hash_pair(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
121    let mut h = Sha256::new();
122    h.update(left);
123    h.update(right);
124    let digest: [u8; 32] = h.finalize().into();
125    digest
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn inclusion_proof_round_trip() {
134        let leaf = [0xaa; 32];
135        let sibling = [0xbb; 32];
136        let root = hash_pair(&leaf, &sibling);
137        verify_inclusion(&leaf, &[(sibling.to_vec(), false)], &root).unwrap();
138    }
139
140    #[test]
141    fn inclusion_proof_rejects_wrong_root() {
142        let leaf = [0xaa; 32];
143        let sibling = [0xbb; 32];
144        let wrong_root = [0xff; 32];
145        assert!(verify_inclusion(&leaf, &[(sibling.to_vec(), false)], &wrong_root).is_err());
146    }
147}