siglog 0.1.0

A minimal Tessera-compatible transparency log server
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
//! Consistency proof verification for Merkle trees.
//!
//! Implements RFC 9162 §2.1.4.2 consistency proof verification.

use sigstore_merkle::hash_children;
use sigstore_types::Sha256Hash;
use thiserror::Error;

/// A consistency proof between two tree sizes.
#[derive(Debug, Clone, Default)]
pub struct ConsistencyProof {
    /// The proof hashes.
    hashes: Vec<Sha256Hash>,
}

impl ConsistencyProof {
    /// Create a new consistency proof from hashes.
    pub fn new(hashes: Vec<Sha256Hash>) -> Self {
        Self { hashes }
    }

    /// Get the proof hashes.
    pub fn hashes(&self) -> &[Sha256Hash] {
        &self.hashes
    }

    /// Check if the proof is empty.
    pub fn is_empty(&self) -> bool {
        self.hashes.is_empty()
    }

    /// Get the number of hashes in the proof.
    pub fn len(&self) -> usize {
        self.hashes.len()
    }
}

/// Errors from consistency proof verification.
#[derive(Debug, Error)]
pub enum ProofError {
    #[error("old size ({old}) > new size ({new})")]
    InvalidSizes { old: u64, new: u64 },

    #[error("proof length mismatch: expected {expected}, got {actual}")]
    WrongLength { expected: usize, actual: usize },

    #[error("old root mismatch")]
    OldRootMismatch,

    #[error("new root mismatch")]
    NewRootMismatch,

    #[error("non-empty proof for trees of equal size")]
    NonEmptyProofForEqualSize,

    #[error("non-empty proof for empty old tree")]
    NonEmptyProofForEmptyOld,

    #[error("invalid root for empty tree")]
    InvalidEmptyRoot,
}

/// Verify a consistency proof between two tree sizes.
///
/// This implements RFC 9162 §2.1.4.2 (which is the same as RFC 6962 §2.1.4).
///
/// The proof demonstrates that the tree at `old_size` is a prefix of the tree
/// at `new_size` - i.e., the first `old_size` leaves are identical.
pub fn verify_consistency(
    old_size: u64,
    new_size: u64,
    old_root: &Sha256Hash,
    new_root: &Sha256Hash,
    proof: &ConsistencyProof,
) -> Result<(), ProofError> {
    // Handle edge cases
    if old_size > new_size {
        return Err(ProofError::InvalidSizes {
            old: old_size,
            new: new_size,
        });
    }

    if old_size == new_size {
        if !proof.is_empty() {
            return Err(ProofError::NonEmptyProofForEqualSize);
        }
        if old_root != new_root {
            return Err(ProofError::OldRootMismatch);
        }
        return Ok(());
    }

    if old_size == 0 {
        if !proof.is_empty() {
            return Err(ProofError::NonEmptyProofForEmptyOld);
        }
        // Verify old_root is the empty tree hash
        if old_root != &empty_root_hash() {
            return Err(ProofError::InvalidEmptyRoot);
        }
        return Ok(());
    }

    // Calculate expected proof length
    let expected_len = proof_length(old_size, new_size);
    if proof.len() != expected_len {
        return Err(ProofError::WrongLength {
            expected: expected_len,
            actual: proof.len(),
        });
    }

    // Run the verification algorithm
    verify_consistency_inner(old_size, new_size, old_root, new_root, proof.hashes())
}

/// Inner verification logic implementing RFC 9162 algorithm.
///
/// This uses a recursive approach that mirrors SUBPROOF generation.
fn verify_consistency_inner(
    old_size: u64,
    new_size: u64,
    old_root: &Sha256Hash,
    new_root: &Sha256Hash,
    proof: &[Sha256Hash],
) -> Result<(), ProofError> {
    if old_size == new_size {
        if proof.is_empty() && old_root == new_root {
            return Ok(());
        }
        return Err(ProofError::OldRootMismatch);
    }

    // For power-of-2 old_size, use simpler path-based verification
    if old_size.is_power_of_two() {
        return verify_power_of_2(old_size, new_size, old_root, new_root, proof);
    }

    // For non-power-of-2 old_size, use recursive verification matching SUBPROOF
    let mut idx = 0;
    let (computed_old, computed_new) = verify_subproof(old_size, new_size, proof, &mut idx, true)?;

    if idx != proof.len() {
        return Err(ProofError::WrongLength {
            expected: idx,
            actual: proof.len(),
        });
    }

    if &computed_old != old_root {
        return Err(ProofError::OldRootMismatch);
    }
    if &computed_new != new_root {
        return Err(ProofError::NewRootMismatch);
    }

    Ok(())
}

/// Verify consistency proof for power-of-2 old_size.
fn verify_power_of_2(
    old_size: u64,
    new_size: u64,
    old_root: &Sha256Hash,
    new_root: &Sha256Hash,
    proof: &[Sha256Hash],
) -> Result<(), ProofError> {
    // For power-of-2 old_size, walk from old_root to new_root via siblings
    let mut sr = *old_root;
    let _old_level = old_size.trailing_zeros();

    // Walk up the tree, combining with siblings
    let mut size = old_size;
    let mut proof_idx = 0;

    while size < new_size {
        if proof_idx >= proof.len() {
            return Err(ProofError::WrongLength {
                expected: proof_idx + 1,
                actual: proof.len(),
            });
        }

        let sibling = &proof[proof_idx];
        proof_idx += 1;

        // Sibling is always on the right (we start from leftmost subtree)
        sr = hash_children(&sr, sibling);
        size *= 2;

        // If new_size < size, we've overshot - need to adjust
        // This happens for non-power-of-2 new_size
        if size > new_size {
            break;
        }
    }

    // Handle any remaining proof elements for non-power-of-2 new_size
    // The tree structure for non-power-of-2 needs special handling
    while proof_idx < proof.len() {
        let sibling = &proof[proof_idx];
        proof_idx += 1;
        sr = hash_children(&sr, sibling);
    }

    if &sr != new_root {
        return Err(ProofError::NewRootMismatch);
    }

    Ok(())
}

/// Recursive verification matching SUBPROOF generation.
/// Returns (old_tree_hash, new_tree_hash).
///
/// The proof elements are consumed in the same order as generated:
/// - For m <= k: recurse first (inner elements), then right hash
/// - For m > k: recurse first (inner elements), then left hash
fn verify_subproof(
    m: u64,
    n: u64,
    proof: &[Sha256Hash],
    idx: &mut usize,
    complete: bool,
) -> Result<(Sha256Hash, Sha256Hash), ProofError> {
    if m == n {
        if complete {
            // complete=true means we don't have proof element for this
            // We can't compute the hash without more context
            // This case shouldn't happen in practice for verification
            return Err(ProofError::WrongLength {
                expected: *idx + 1,
                actual: proof.len(),
            });
        }
        // Get the subtree hash from proof
        if *idx >= proof.len() {
            return Err(ProofError::WrongLength {
                expected: *idx + 1,
                actual: proof.len(),
            });
        }
        let hash = proof[*idx];
        *idx += 1;
        return Ok((hash, hash));
    }

    let k = largest_power_of_2_less_than(n);

    if m <= k {
        // Recurse into left subtree FIRST (matching generation order)
        let (left_old, left_new) = verify_subproof(m, k, proof, idx, complete)?;

        // THEN get right subtree hash from proof
        if *idx >= proof.len() {
            return Err(ProofError::WrongLength {
                expected: *idx + 1,
                actual: proof.len(),
            });
        }
        let right = proof[*idx];
        *idx += 1;

        let new_hash = hash_children(&left_new, &right);
        Ok((left_old, new_hash))
    } else {
        // Recurse into right subtree FIRST (matching generation order)
        let (right_old, right_new) = verify_subproof(m - k, n - k, proof, idx, false)?;

        // THEN get left subtree hash from proof
        if *idx >= proof.len() {
            return Err(ProofError::WrongLength {
                expected: *idx + 1,
                actual: proof.len(),
            });
        }
        let left = proof[*idx];
        *idx += 1;

        let old_hash = hash_children(&left, &right_old);
        let new_hash = hash_children(&left, &right_new);
        Ok((old_hash, new_hash))
    }
}

/// Calculate the expected proof length for a consistency proof.
///
/// For power-of-2 old_size: proof_length = tree_height(new_size) - log2(old_size)
/// For non-power-of-2: we use the RFC SUBPROOF recursion pattern
fn proof_length(old_size: u64, new_size: u64) -> usize {
    if old_size == 0 || old_size == new_size {
        return 0;
    }

    if old_size.is_power_of_two() {
        // For power-of-2 old_size, count siblings from old_size level to new root
        let old_level = old_size.trailing_zeros() as usize;
        let tree_height = if new_size.is_power_of_two() {
            new_size.trailing_zeros() as usize
        } else {
            (64 - (new_size - 1).leading_zeros()) as usize
        };
        return tree_height - old_level;
    }

    // For non-power-of-2 old_size, use RFC SUBPROOF counting
    subproof_length(old_size, new_size, true)
}

/// Count proof elements for RFC SUBPROOF algorithm.
fn subproof_length(m: u64, n: u64, complete: bool) -> usize {
    if m == n {
        return if complete { 0 } else { 1 };
    }

    let k = largest_power_of_2_less_than(n);

    if m <= k {
        // SUBPROOF(m, D[0:k], complete) + 1 for right hash
        subproof_length(m, k, complete) + 1
    } else {
        // 1 for left hash + SUBPROOF(m-k, D[k:n], false)
        1 + subproof_length(m - k, n - k, false)
    }
}

/// Largest power of 2 less than n.
fn largest_power_of_2_less_than(n: u64) -> u64 {
    if n <= 1 {
        return 0;
    }
    1 << (63 - (n - 1).leading_zeros())
}

/// RFC 6962 empty tree root hash.
fn empty_root_hash() -> Sha256Hash {
    Sha256Hash::from_bytes([
        0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9,
        0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52,
        0xb8, 0x55,
    ])
}

#[cfg(test)]
mod tests {
    use super::*;
    use sigstore_merkle::hash_leaf;

    /// Build a complete tree and return the root hash.
    #[allow(dead_code)]
    fn build_tree(leaves: &[Sha256Hash]) -> Sha256Hash {
        if leaves.is_empty() {
            return empty_root_hash();
        }
        if leaves.len() == 1 {
            return leaves[0];
        }

        // Build compact range
        let mut range: Vec<Option<Sha256Hash>> = Vec::new();

        for (i, &leaf) in leaves.iter().enumerate() {
            let mut hash = leaf;
            let mut idx = i as u64;
            let mut level = 0usize;

            while idx & 1 == 1 {
                if let Some(Some(left)) = range.get(level) {
                    hash = hash_children(left, &hash);
                    range[level] = None;
                    level += 1;
                    idx >>= 1;
                } else {
                    break;
                }
            }

            while range.len() <= level {
                range.push(None);
            }
            range[level] = Some(hash);
        }

        // Compute root from range
        let mut root: Option<Sha256Hash> = None;
        for hash in range.iter().flatten() {
            root = Some(match root {
                None => *hash,
                Some(r) => hash_children(hash, &r),
            });
        }
        root.unwrap_or_else(empty_root_hash)
    }

    #[test]
    fn test_empty_to_empty() {
        let empty = empty_root_hash();
        let proof = ConsistencyProof::new(vec![]);
        assert!(verify_consistency(0, 0, &empty, &empty, &proof).is_ok());
    }

    #[test]
    fn test_same_size() {
        let leaf = hash_leaf(&[1u8]);
        let proof = ConsistencyProof::new(vec![]);
        assert!(verify_consistency(1, 1, &leaf, &leaf, &proof).is_ok());
    }

    #[test]
    fn test_same_size_different_roots() {
        let leaf1 = hash_leaf(&[1u8]);
        let leaf2 = hash_leaf(&[2u8]);
        let proof = ConsistencyProof::new(vec![]);
        assert!(matches!(
            verify_consistency(1, 1, &leaf1, &leaf2, &proof),
            Err(ProofError::OldRootMismatch)
        ));
    }

    #[test]
    fn test_old_greater_than_new() {
        let leaf = hash_leaf(&[1u8]);
        let proof = ConsistencyProof::new(vec![]);
        assert!(matches!(
            verify_consistency(2, 1, &leaf, &leaf, &proof),
            Err(ProofError::InvalidSizes { .. })
        ));
    }

    #[test]
    fn test_proof_length() {
        // Basic edge cases
        assert_eq!(proof_length(0, 0), 0);
        assert_eq!(proof_length(0, 1), 0);
        assert_eq!(proof_length(1, 1), 0);
        assert_eq!(proof_length(2, 2), 0);

        // Power-of-2 old sizes need fewer proof elements
        assert_eq!(proof_length(1, 2), 1);
        assert_eq!(proof_length(2, 4), 1);
        assert_eq!(proof_length(4, 8), 1);

        // Non-power-of-2 old sizes need the extra element for the subtree
        // The actual length depends on the bit structure
        assert!(proof_length(3, 4) > 0);
        assert!(proof_length(1, 4) > 0);
    }
}