OXYGENHUB

Coverage / crates/oxy-core/src/tlog.rs 90.8%

982 of 1081 lines run, 153 of 246 functions · run #325 on main · run missed

1
//! The transparency log (spec §14.8, oxygenhub#238, oxygenhub
2
//! docs/transparency.md): an append-only Merkle tree of what a hub
3
//! accepts, served as C2SP tlog-tiles with C2SP signed-note checkpoints,
4
//! so a hub cannot show one history to some and another to others.
5
//!
6
//! The tree is RFC 6962's, with BLAKE3 for its hashes, domain-separated
7
//! for leaves and nodes. Hashes are kept in *stored order*: every hash of
8
//! the tree, each written as soon as it is known, so the store is
9
//! append-only (the arithmetic is that of Go's `sumdb/tlog`). Tiles are
10
//! views of it: level L tile N holds the hashes of tree level 8·L for
11
//! leaves N·256 onwards.
12
//!
13
//! A checkpoint is a signed note: the origin, the tree size and the root,
14
//! signed with the hub's Ed25519 key (what note tools read) and with its
15
//! composite key in an extra line, so the log stays post-quantum.
16
17
use crate::cbor::{self, Value};
18
use crate::hash::{KeyId, ObjectRef};
19
use crate::identity::{verify, Domain, Identity, SecretKey, Signature};
20
use crate::objects::{array, bytes, check_version, field, map, oref, text, uint, ObjectError};
21
use sha2::{Digest, Sha256};
22
23
/// A hash in the tree.
24
pub type Hash = [u8; 32];
25
26
/// Tile height: a tile holds up to 2⁸ hashes (C2SP tlog-tiles).
27
pub const TILE_HEIGHT: u32 = 8;
28
/// Hashes in a full tile.
29
pub const TILE_WIDTH: u64 = 1 << TILE_HEIGHT;
30
31
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
32
pub enum TlogError {
33
    #[error("the proof does not verify")]
34
    ProofFailed,
35
    #[error("{0}")]
36
    BadIndex(String),
37
    #[error("reading the log's hashes: {0}")]
38
    Read(String),
39
    #[error("not a valid note: {0}")]
40
    BadNote(String),
41
    #[error("{0}")]
42
    BadSignature(String),
43
    #[error("{0}")]
44
    Schema(String),
45
}
46
47
impl From<ObjectError> for TlogError {
480×
    fn from(e: ObjectError) -> Self {
490×
        TlogError::Schema(e.to_string())
500×
    }
51
}
52
53
type Result<T> = std::result::Result<T, TlogError>;
54
55
// ------------------------------------------------------------- hashing
56
57
/// A leaf's hash: `BLAKE3("oxygen-tlog-leaf\0" || entry bytes)`.
5870201×
pub fn leaf_hash(entry: &[u8]) -> Hash {
5970201×
    let mut h = blake3::Hasher::new();
6070201×
    h.update(b"oxygen-tlog-leaf\0");
6170201×
    h.update(entry);
6270201×
    *h.finalize().as_bytes()
6370201×
}
64
65
/// An interior node's hash: `BLAKE3("oxygen-tlog-node\0" || left || right)`.
66564412×
pub fn node_hash(left: &Hash, right: &Hash) -> Hash {
67564412×
    let mut h = blake3::Hasher::new();
68564412×
    h.update(b"oxygen-tlog-node\0");
69564412×
    h.update(left);
70564412×
    h.update(right);
71564412×
    *h.finalize().as_bytes()
72564412×
}
73
74
/// The root of the empty tree: the hash of nothing, as RFC 6962 has it.
759×
pub fn empty_root() -> Hash {
769×
    *blake3::hash(b"").as_bytes()
779×
}
78
79
// --------------------------------------------------------------- entries
80
81
/// One accepted object (spec §14.8): small and fixed-shape, never the
82
/// object itself, so a private repository's entries reveal only ids and
83
/// hashes.
84
#[derive(Clone, Debug, PartialEq, Eq)]
85
pub struct Entry {
86
    /// `ref`, `seal`, `tag`, `rotate`, `revoke`, `receipt`, `approval`,
87
    /// `policy`, `identity`, `certificate`, `accepted`.
88
    pub kind: String,
89
    /// The repository's id; zero for identities.
90
    pub repo: [u8; 16],
91
    /// The attestation, policy, identity or certificate.
92
    pub object: ObjectRef,
93
    /// The keys whose signatures were checked.
94
    pub signers: Vec<KeyId>,
95
    /// The hub's clock when it accepted it, in milliseconds (its claim).
96
    pub received: u64,
97
}
98
99
impl Entry {
1005×
    pub fn to_value(&self) -> Value {
1015×
        Value::map([
1025×
            (0, Value::Uint(1)),
1035×
            (1, Value::Text(self.kind.clone())),
1045×
            (2, Value::Bytes(self.repo.to_vec())),
1055×
            (3, Value::Bytes(self.object.0.to_vec())),
106
            (
107
                4,
108
                Value::Array(
1095×
                    self.signers
1105×
                        .iter()
1115×
                        .map(|k| Value::Bytes(k.0.to_vec()))
1125×
                        .collect(),
113
                ),
114
            ),
1155×
            (5, Value::Uint(self.received)),
116
        ])
1175×
    }
118
1195×
    pub fn encode(&self) -> Vec<u8> {
1205×
        cbor::encode(&self.to_value())
1215×
    }
122
1231×
    pub fn decode(b: &[u8]) -> Result<Entry> {
1241×
        let v = cbor::decode(b).map_err(ObjectError::from)?;
1251×
        let m = map(&v, "log entry")?;
1261×
        check_version(m)?;
1271×
        let repo = bytes(field(m, 2, "repo")?, "repo")?;
1281×
        let repo: [u8; 16] = repo
1291×
            .try_into()
1301×
            .map_err(|_| TlogError::Schema("log entry: repo is not 16 bytes".into()))?;
1311×
        let mut signers = Vec::new();
1321×
        for s in array(field(m, 4, "signers")?, "signers")? {
1331×
            signers.push(oref(s, "signer")?);
134
        }
135
        Ok(Entry {
1361×
            kind: text(field(m, 1, "kind")?, "kind")?.to_owned(),
1371×
            repo,
1381×
            object: oref(field(m, 3, "object")?, "object")?,
1391×
            signers,
1401×
            received: uint(field(m, 5, "received")?, "received")?,
141
        })
1421×
    }
143
144
    /// Its leaf hash in the tree.
1451×
    pub fn leaf_hash(&self) -> Hash {
1461×
        leaf_hash(&self.encode())
1471×
    }
148
}
149
150
/// An entry bundle (C2SP tlog-tiles): each entry as a big-endian u16
151
/// length and its bytes.
1521×
pub fn encode_bundle(entries: &[Vec<u8>]) -> Result<Vec<u8>> {
1531×
    let mut out = Vec::new();
1542×
    for e in entries {
1552×
        let n = u16::try_from(e.len())
1562×
            .map_err(|_| TlogError::Schema("a log entry over 65535 bytes".into()))?;
1572×
        out.extend_from_slice(&n.to_be_bytes());
1582×
        out.extend_from_slice(e);
159
    }
1601×
    Ok(out)
1611×
}
162
163
/// The entries of a bundle.
1642×
pub fn decode_bundle(mut b: &[u8]) -> Result<Vec<Vec<u8>>> {
1652×
    let mut out = Vec::new();
1665×
    while !b.is_empty() {
1674×
        if b.len() < 2 {
1680×
            return Err(TlogError::Schema("entry bundle: a truncated length".into()));
1694×
        }
1704×
        let n = u16::from_be_bytes([b[0], b[1]]) as usize;
1714×
        if b.len() < 2 + n {
1721×
            return Err(TlogError::Schema("entry bundle: a truncated entry".into()));
1733×
        }
1743×
        out.push(b[2..2 + n].to_vec());
1753×
        b = &b[2 + n..];
176
    }
1771×
    Ok(out)
1782×
}
179
180
// -------------------------------------------------- the tree's arithmetic
181
182
/// Reads stored hashes by their stored index.
183
pub trait HashReader {
184
    /// The hashes at `indexes`, in that order.
185
    fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>>;
186
}
187
188
/// Every stored hash in order, as a log in memory holds them.
189
impl HashReader for [Hash] {
190100100×
    fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> {
191100100×
        indexes
192100100×
            .iter()
193331525×
            .map(|&i| {
194331525×
                self.get(i as usize)
195331525×
                    .copied()
196331525×
                    .ok_or_else(|| TlogError::Read(format!("no stored hash {i}")))
197331525×
            })
198100100×
            .collect()
199100100×
    }
200
}
201
202
impl HashReader for Vec<Hash> {
203100100×
    fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> {
204100100×
        self.as_slice().read_hashes(indexes)
205100100×
    }
206
}
207
208
/// The largest power of two below `n` (n ≥ 2), and its log.
209947991×
fn maxpow2(n: u64) -> (u64, u32) {
210947991×
    let mut l = 0;
2113629109×
    while (1u64 << (l + 1)) < n {
2122681118×
        l += 1;
2132681118×
    }
214947991×
    (1 << l, l)
215947991×
}
216
217
/// Where the `n`th hash of tree level `level` is stored: level L's nth
218
/// hash is written right after level L+1's (2n+1)th, and level 0's nth
219
/// at n + n/2 + n/4 + ….
220515505×
pub fn stored_hash_index(level: u32, mut n: u64) -> u64 {
221549060×
    for _ in 0..level {
222549060×
        n = 2 * n + 1;
223549060×
    }
224515505×
    let mut i = 0;
225515505×
    let mut m = n;
2266528289×
    while m > 0 {
2276012784×
        i += m;
2286012784×
        m >>= 1;
2296012784×
    }
230515505×
    i + level as u64
231515505×
}
232
233
/// The level and position of the hash at a stored index.
23441800×
pub fn split_stored_hash_index(index: u64) -> (u32, u64) {
23541800×
    let mut n = index / 2;
23641800×
    let mut index_n = stored_hash_index(0, n);
23741800×
    debug_assert!(index_n <= index);
238
    loop {
239
        // Each new leaf n adds 1 + trailing_zeros(n+1) hashes.
240203734×
        let x = index_n + 1 + (n + 1).trailing_zeros() as u64;
241203734×
        if x > index {
24241800×
            break;
243161934×
        }
244161934×
        n += 1;
245161934×
        index_n = x;
246
    }
24741800×
    let level = (index - index_n) as u32;
24841800×
    (level, n >> level)
24941800×
}
250
251
/// How many hashes a tree of `n` leaves stores.
25270190×
pub fn stored_hash_count(n: u64) -> u64 {
25370190×
    if n == 0 {
2540×
        return 0;
25570190×
    }
25670190×
    let mut count = stored_hash_index(0, n - 1) + 1;
25770190×
    let mut i = n - 1;
258140367×
    while i & 1 != 0 {
25970177×
        count += 1;
26070177×
        i >>= 1;
26170177×
    }
26270190×
    count
26370190×
}
264
265
/// The hashes to append when leaf `n` (with hash `leaf`) joins a tree of
266
/// `n` leaves: the leaf's, then those of the subtrees it completes.
26770199×
pub fn stored_hashes_for_leaf(
26870199×
    n: u64,
26970199×
    leaf: Hash,
27070199×
    r: &(impl HashReader + ?Sized),
27170199×
) -> Result<Vec<Hash>> {
27270199×
    let m = (n + 1).trailing_zeros();
27370199×
    let indexes: Vec<u64> = (0..m)
27470199×
        .rev()
27570199×
        .map(|i| stored_hash_index(i, (n >> i) - 1))
27670199×
        .collect();
27770199×
    let old = r.read_hashes(&indexes)?;
27870199×
    let mut h = leaf;
27970199×
    let mut out = vec![h];
28070199×
    for i in 0..m as usize {
28170184×
        h = node_hash(&old[m as usize - 1 - i], &h);
28270184×
        out.push(h);
28370184×
    }
28470199×
    Ok(out)
28570199×
}
286
287141399×
fn sub_tree_index(mut lo: u64, hi: u64, need: &mut Vec<u64>) {
288332466×
    while lo < hi {
289191067×
        let (k, level) = maxpow2(hi - lo + 1);
290191067×
        debug_assert_eq!(lo & (k - 1), 0);
291191067×
        need.push(stored_hash_index(level, lo >> level));
292191067×
        lo += k;
293
    }
294141399×
}
295
296141399×
fn sub_tree_hash(mut lo: u64, hi: u64, hashes: &[Hash]) -> (Hash, &[Hash]) {
297141399×
    let mut count = 0;
298332466×
    while lo < hi {
299191067×
        let (k, _) = maxpow2(hi - lo + 1);
300191067×
        count += 1;
301191067×
        lo += k;
302191067×
    }
303141399×
    let mut h = hashes[count - 1];
304141399×
    for i in (0..count - 1).rev() {
30549668×
        h = node_hash(&hashes[i], &h);
30649668×
    }
307141399×
    (h, &hashes[count..])
308141399×
}
309
310
/// The root of the first `n` leaves.
31110022×
pub fn tree_hash(n: u64, r: &(impl HashReader + ?Sized)) -> Result<Hash> {
31210022×
    if n == 0 {
3131×
        return Ok(empty_root());
31410021×
    }
31510021×
    let mut need = Vec::new();
31610021×
    sub_tree_index(0, n, &mut need);
31710021×
    let hashes = r.read_hashes(&need)?;
31810021×
    Ok(sub_tree_hash(0, n, &hashes).0)
31910022×
}
320
32175981×
fn leaf_proof_index(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) {
32275981×
    if lo + 1 == hi {
3239870×
        return;
32466111×
    }
32566111×
    let (k, _) = maxpow2(hi - lo);
32666111×
    if n < lo + k {
32735720×
        leaf_proof_index(lo, lo + k, n, need);
32835720×
        sub_tree_index(lo + k, hi, need);
32935720×
    } else {
33030391×
        sub_tree_index(lo, lo + k, need);
33130391×
        leaf_proof_index(lo + k, hi, n, need);
33230391×
    }
33375981×
}
334
33575980×
fn leaf_proof(lo: u64, hi: u64, n: u64, hashes: &[Hash]) -> (Vec<Hash>, &[Hash]) {
33675980×
    if lo + 1 == hi {
337
        // The verifier has the leaf's hash.
3389869×
        return (Vec::new(), hashes);
33966111×
    }
34066111×
    let (k, _) = maxpow2(hi - lo);
341
    let (mut p, th, rest);
34266111×
    if n < lo + k {
34335720×
        let (pp, h1) = leaf_proof(lo, lo + k, n, hashes);
34435720×
        let (t, h2) = sub_tree_hash(lo + k, hi, h1);
34535720×
        (p, th, rest) = (pp, t, h2);
34635720×
    } else {
34730391×
        let (t, h1) = sub_tree_hash(lo, lo + k, hashes);
34830391×
        let (pp, h2) = leaf_proof(lo + k, hi, n, h1);
34930391×
        (p, th, rest) = (pp, t, h2);
35030391×
    }
35166111×
    p.push(th);
35266111×
    (p, rest)
35375980×
}
354
355
/// The inclusion proof of leaf `n` in the tree of the first `t` leaves.
3569870×
pub fn prove_inclusion(t: u64, n: u64, r: &(impl HashReader + ?Sized)) -> Result<Vec<Hash>> {
3579870×
    if n >= t {
3580×
        return Err(TlogError::BadIndex(format!(
3590×
            "leaf {n} is not in a tree of {t}"
3600×
        )));
3619870×
    }
3629870×
    let mut need = Vec::new();
3639870×
    leaf_proof_index(0, t, n, &mut need);
3649870×
    if need.is_empty() {
3651×
        return Ok(Vec::new());
3669869×
    }
3679869×
    let hashes = r.read_hashes(&need)?;
3689869×
    Ok(leaf_proof(0, t, n, &hashes).0)
3699870×
}
370
371227941×
fn run_inclusion(p: &[Hash], lo: u64, hi: u64, n: u64, leaf: Hash) -> Result<Hash> {
372227941×
    if lo + 1 == hi {
37329608×
        return if p.is_empty() {
37429608×
            Ok(leaf)
375
        } else {
3760×
            Err(TlogError::ProofFailed)
377
        };
378198333×
    }
379198333×
    let Some((last, rest)) = p.split_last() else {
3800×
        return Err(TlogError::ProofFailed);
381
    };
382198333×
    let (k, _) = maxpow2(hi - lo);
383198333×
    if n < lo + k {
384107160×
        Ok(node_hash(&run_inclusion(rest, lo, lo + k, n, leaf)?, last))
385
    } else {
38691173×
        Ok(node_hash(last, &run_inclusion(rest, lo + k, hi, n, leaf)?))
387
    }
388227941×
}
389
390
/// Check that leaf `n`, with hash `leaf`, is in the tree of `t` leaves
391
/// whose root is `root`.
39229608×
pub fn check_inclusion(proof: &[Hash], t: u64, root: &Hash, n: u64, leaf: &Hash) -> Result<()> {
39329608×
    if n >= t {
3940×
        return Err(TlogError::ProofFailed);
39529608×
    }
39629608×
    if run_inclusion(proof, 0, t, n, *leaf)? == *root {
3979870×
        Ok(())
398
    } else {
39919738×
        Err(TlogError::ProofFailed)
400
    }
40129608×
}
402
40366273×
fn tree_proof_index(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) {
40466273×
    if n == hi {
4059874×
        if lo != 0 {
4068868×
            sub_tree_index(lo, hi, need);
4078868×
        }
4089874×
        return;
40956399×
    }
41056399×
    let (k, _) = maxpow2(hi - lo);
41156399×
    if n <= lo + k {
41235735×
        tree_proof_index(lo, lo + k, n, need);
41335735×
        sub_tree_index(lo + k, hi, need);
41435735×
    } else {
41520664×
        sub_tree_index(lo, lo + k, need);
41620664×
        tree_proof_index(lo + k, hi, n, need);
41720664×
    }
41866273×
}
419
42066133×
fn tree_proof(lo: u64, hi: u64, n: u64, hashes: &[Hash]) -> (Vec<Hash>, &[Hash]) {
42166133×
    if n == hi {
4229734×
        if lo == 0 {
423
            // Exactly the old tree, whose root the verifier has.
424866×
            return (Vec::new(), hashes);
4258868×
        }
4268868×
        let (th, rest) = sub_tree_hash(lo, hi, hashes);
4278868×
        return (vec![th], rest);
42856399×
    }
42956399×
    let (k, _) = maxpow2(hi - lo);
430
    let (mut p, th, rest);
43156399×
    if n <= lo + k {
43235735×
        let (pp, h1) = tree_proof(lo, lo + k, n, hashes);
43335735×
        let (t, h2) = sub_tree_hash(lo + k, hi, h1);
43435735×
        (p, th, rest) = (pp, t, h2);
43535735×
    } else {
43620664×
        let (t, h1) = sub_tree_hash(lo, lo + k, hashes);
43720664×
        let (pp, h2) = tree_proof(lo + k, hi, n, h1);
43820664×
        (p, th, rest) = (pp, t, h2);
43920664×
    }
44056399×
    p.push(th);
44156399×
    (p, rest)
44266133×
}
443
444
/// The consistency proof that the tree of `n` leaves is a prefix of the
445
/// tree of `t`.
4469874×
pub fn prove_consistency(t: u64, n: u64, r: &(impl HashReader + ?Sized)) -> Result<Vec<Hash>> {
4479874×
    if n < 1 || n > t {
4480×
        return Err(TlogError::BadIndex(format!(
4490×
            "no consistency proof from {n} to {t}"
4500×
        )));
4519874×
    }
4529874×
    let mut need = Vec::new();
4539874×
    tree_proof_index(0, t, n, &mut need);
4549874×
    if need.is_empty() {
455140×
        return Ok(Vec::new());
4569734×
    }
4579734×
    let hashes = r.read_hashes(&need)?;
4589734×
    Ok(tree_proof(0, t, n, &hashes).0)
4599874×
}
460
461132377×
fn run_consistency(p: &[Hash], lo: u64, hi: u64, n: u64, old: Hash) -> Result<(Hash, Hash)> {
462132377×
    if n == hi {
46319603×
        if lo == 0 {
4641871×
            return if p.is_empty() {
4651871×
                Ok((old, old))
466
            } else {
4670×
                Err(TlogError::ProofFailed)
468
            };
46917732×
        }
47017732×
        return match p {
47117732×
            [h] => Ok((*h, *h)),
4720×
            _ => Err(TlogError::ProofFailed),
473
        };
474112774×
    }
475112774×
    let Some((last, rest)) = p.split_last() else {
4760×
        return Err(TlogError::ProofFailed);
477
    };
478112774×
    let (k, _) = maxpow2(hi - lo);
479112774×
    if n <= lo + k {
48071450×
        let (oh, th) = run_consistency(rest, lo, lo + k, n, old)?;
48171450×
        Ok((oh, node_hash(&th, last)))
482
    } else {
48341324×
        let (oh, th) = run_consistency(rest, lo + k, hi, n, old)?;
48441324×
        Ok((node_hash(last, &oh), node_hash(last, &th)))
485
    }
486132377×
}
487
488
/// Check that the tree of `n` leaves with root `old_root` is a prefix of
489
/// the tree of `t` leaves with root `root`.
49019603×
pub fn check_consistency(
49119603×
    proof: &[Hash],
49219603×
    t: u64,
49319603×
    root: &Hash,
49419603×
    n: u64,
49519603×
    old_root: &Hash,
49619603×
) -> Result<()> {
49719603×
    if n < 1 || n > t {
4980×
        return Err(TlogError::ProofFailed);
49919603×
    }
50019603×
    let (oh, th) = run_consistency(proof, 0, t, n, *old_root)?;
50119603×
    if oh == *old_root && th == *root {
5029872×
        Ok(())
503
    } else {
5049731×
        Err(TlogError::ProofFailed)
505
    }
50619603×
}
507
508
// ----------------------------------------------------------------- tiles
509
510
/// A tile (C2SP tlog-tiles, height 8): level `level`'s hashes for leaves
511
/// from `n`·256, `width` of them (256 when full).
512
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
513
pub struct Tile {
514
    pub level: u32,
515
    pub n: u64,
516
    pub width: u32,
517
}
518
519
/// A tile index as a path: three-digit groups, all but the last with an
520
/// `x`, as `x001/x234/067` for 1234067.
5215×
pub fn index_path(n: u64) -> String {
5225×
    let mut groups = Vec::new();
5235×
    let mut m = n;
524
    loop {
5259×
        groups.push(m % 1000);
5269×
        m /= 1000;
5279×
        if m == 0 {
5285×
            break;
5294×
        }
530
    }
5315×
    groups.reverse();
5325×
    let last = groups.len() - 1;
5335×
    groups
5345×
        .iter()
5355×
        .enumerate()
5369×
        .map(|(i, g)| {
5379×
            if i < last {
5384×
                format!("x{g:03}")
539
            } else {
5405×
                format!("{g:03}")
541
            }
5429×
        })
5435×
        .collect::<Vec<_>>()
5445×
        .join("/")
5455×
}
546
547
/// The index a path names (the inverse of [`index_path`]).
5483×
pub fn parse_index_path(path: &str) -> Option<u64> {
5493×
    let parts: Vec<&str> = path.split('/').collect();
5503×
    let mut n: u64 = 0;
5517×
    for (i, p) in parts.iter().enumerate() {
5527×
        let digits = if i + 1 < parts.len() {
5535×
            p.strip_prefix('x')?
554
        } else {
5552×
            p
556
        };
55718×
        if digits.len() != 3 || !digits.bytes().all(|c| c.is_ascii_digit()) {
5580×
            return None;
5596×
        }
5606×
        n = n.checked_mul(1000)?.checked_add(digits.parse().ok()?)?;
561
    }
5622×
    Some(n)
5633×
}
564
565
impl Tile {
566
    /// `tile/<L>/<N>[.p/<W>]`.
5671×
    pub fn path(&self) -> String {
5681×
        format!(
569
            "tile/{}/{}{}",
570
            self.level,
5711×
            index_path(self.n),
5721×
            self.partial()
573
        )
5741×
    }
575
576
    /// The entry bundle beside a level-0 tile: `tile/entries/<N>[.p/<W>]`.
5771×
    pub fn entries_path(&self) -> String {
5781×
        format!("tile/entries/{}{}", index_path(self.n), self.partial())
5791×
    }
580
5812×
    fn partial(&self) -> String {
5822×
        if (self.width as u64) < TILE_WIDTH {
5831×
            format!(".p/{}", self.width)
584
        } else {
5851×
            String::new()
586
        }
5872×
    }
588
589
    /// The tile a path names (`<L>/<N>[.p/<W>]` after `tile/`).
5902×
    pub fn parse(path: &str) -> Option<Tile> {
5912×
        let (level, rest) = path.split_once('/')?;
5922×
        let level: u32 = level.parse().ok()?;
5932×
        let (index, width) = match rest.split_once(".p/") {
5942×
            Some((i, w)) => {
5952×
                let w: u32 = w.parse().ok()?;
5962×
                if w == 0 || w as u64 >= TILE_WIDTH {
5971×
                    return None;
5981×
                }
5991×
                (i, w)
600
            }
6010×
            None => (rest, TILE_WIDTH as u32),
602
        };
603
        Some(Tile {
6041×
            level,
6051×
            n: parse_index_path(index)?,
6061×
            width,
607
        })
6082×
    }
609
610
    /// The tile holding the stored hash at `index`, and the byte range its
611
    /// hash comes from in the tile's data.
61240000×
    pub fn for_index(index: u64) -> (Tile, usize, usize) {
61340000×
        let (level, mut n) = split_stored_hash_index(index);
61440000×
        let tl = level / TILE_HEIGHT;
61540000×
        let within = level - tl * TILE_HEIGHT;
61640000×
        let tn = (n << within) >> TILE_HEIGHT;
61740000×
        n -= (tn << TILE_HEIGHT) >> within;
61840000×
        let width = ((n + 1) << within) as u32;
61940000×
        let tile = Tile {
62040000×
            level: tl,
62140000×
            n: tn,
62240000×
            width,
62340000×
        };
62440000×
        (
62540000×
            tile,
62640000×
            ((n << within) * 32) as usize,
62740000×
            (((n + 1) << within) * 32) as usize,
62840000×
        )
62940000×
    }
630
631
    /// The tile's data: its hashes, read from stored order.
632277×
    pub fn read_data(&self, r: &(impl HashReader + ?Sized)) -> Result<Vec<u8>> {
633277×
        let start = self.n << TILE_HEIGHT;
634277×
        let indexes: Vec<u64> = (0..self.width as u64)
63570274×
            .map(|i| stored_hash_index(TILE_HEIGHT * self.level, start + i))
636277×
            .collect();
637277×
        Ok(r.read_hashes(&indexes)?.concat())
638277×
    }
639
640
    /// The stored hash at `index` from this tile's data.
64120000×
    pub fn hash_at(&self, data: &[u8], index: u64) -> Result<Hash> {
64220000×
        if self.width == 0
64320000×
            || self.width as u64 > TILE_WIDTH
64420000×
            || data.len() < self.width as usize * 32
645
        {
6460×
            return Err(TlogError::Read(format!("tile {} is short", self.path())));
64720000×
        }
64820000×
        let (t, start, end) = Tile::for_index(index);
64920000×
        if t.level != self.level || t.n != self.n || t.width > self.width {
6500×
            return Err(TlogError::Read(format!(
6510×
                "stored hash {index} is not in tile {}",
6520×
                self.path()
6530×
            )));
65420000×
        }
65520000×
        Ok(tile_hash(&data[start..end]))
65620000×
    }
657
658
    /// The tiles a tree that grows from `old` to `new` leaves writes, full
659
    /// ones and the new partial ones.
6602×
    pub fn new_tiles(old: u64, new: u64) -> Vec<Tile> {
6612×
        let mut tiles = Vec::new();
6622×
        let mut level = 0u32;
6638×
        while level < 8 && (new >> (TILE_HEIGHT * level)) > 0 {
6646×
            let old_n = old >> (TILE_HEIGHT * level);
6656×
            let new_n = new >> (TILE_HEIGHT * level);
6666×
            if old_n != new_n {
667275×
                for n in (old_n >> TILE_HEIGHT)..(new_n >> TILE_HEIGHT) {
668275×
                    tiles.push(Tile {
669275×
                        level,
670275×
                        n,
671275×
                        width: TILE_WIDTH as u32,
672275×
                    });
673275×
                }
6745×
                let n = new_n >> TILE_HEIGHT;
6755×
                let w = new_n - (n << TILE_HEIGHT);
6765×
                if w > 0 {
6775×
                    tiles.push(Tile {
6785×
                        level,
6795×
                        n,
6805×
                        width: w as u32,
6815×
                    });
6825×
                }
6831×
            }
6846×
            level += 1;
685
        }
6862×
        tiles
6872×
    }
688
}
689
690145600×
fn tile_hash(data: &[u8]) -> Hash {
691145600×
    if data.len() == 32 {
69282800×
        let mut h = [0u8; 32];
69382800×
        h.copy_from_slice(data);
69482800×
        return h;
69562800×
    }
69662800×
    let half = data.len() / 2;
69762800×
    node_hash(&tile_hash(&data[..half]), &tile_hash(&data[half..]))
698145600×
}
699
700
// ----------------------------------------------------- notes, checkpoints
701
702
/// One signature line of a note: `— <name> <base64(key hash || sig)>`.
703
#[derive(Clone, Debug, PartialEq, Eq)]
704
pub struct NoteSignature {
705
    pub name: String,
706
    pub key_hash: [u8; 4],
707
    pub sig: Vec<u8>,
708
}
709
710
/// A C2SP signed note: its text and its signature lines.
711
#[derive(Clone, Debug, PartialEq, Eq)]
712
pub struct Note {
713
    /// The text, ending with a newline.
714
    pub text: String,
715
    pub sigs: Vec<NoteSignature>,
716
}
717
718
/// Signature types in a note's key hash (C2SP signed-note).
719
const SIG_ED25519: u8 = 0x01;
720
/// Oxygen's composite Ed25519 + ML-DSA-65 signature (spec §14.8).
721
const SIG_COMPOSITE: u8 = 0x4f;
722
72358×
fn key_hash(name: &str, sig_type: u8, key: &[u8]) -> [u8; 4] {
72458×
    let mut h = Sha256::new();
72558×
    h.update(name.as_bytes());
72658×
    h.update(b"\n");
72758×
    h.update([sig_type]);
72858×
    h.update(key);
72958×
    let d = h.finalize();
73058×
    [d[0], d[1], d[2], d[3]]
73158×
}
732
733
/// What a composite signature of a note signs (domain `checkpoint`).
73419×
fn note_target(text: &str) -> ObjectRef {
73519×
    let mut h = blake3::Hasher::new();
73619×
    h.update(b"oxygen-tlog-note\0");
73719×
    h.update(text.as_bytes());
73819×
    ObjectRef(*h.finalize().as_bytes())
73919×
}
740
74149×
fn valid_name(name: &str) -> bool {
742714×
    !name.is_empty() && !name.contains(|c: char| c.is_whitespace() || c == '+')
74349×
}
744
745
impl Note {
746
    /// The note as served: the text, a blank line, the signature lines.
74714×
    pub fn encode(&self) -> String {
74814×
        let mut out = self.text.clone();
74914×
        out.push('\n');
75030×
        for s in &self.sigs {
75130×
            let mut b = s.key_hash.to_vec();
75230×
            b.extend_from_slice(&s.sig);
75330×
            out.push_str(&format!(
75430×
                "\u{2014} {} {}\n",
75530×
                s.name,
75630×
                crate::dsse::base64(&b)
75730×
            ));
75830×
        }
75914×
        out
76014×
    }
761
76216×
    pub fn parse(s: &str) -> Result<Note> {
76316×
        let bad = |w: &str| TlogError::BadNote(w.to_string());
76416×
        let split = s
76516×
            .rfind("\n\n")
76616×
            .ok_or_else(|| bad("no blank line before the signatures"))?;
76716×
        let text = &s[..split + 1];
7681017×
        if text.chars().any(|c| c.is_control() && c != '\n') {
7690×
            return Err(bad("control characters in the text"));
77016×
        }
77116×
        let mut sigs = Vec::new();
77234×
        for line in s[split + 2..].lines() {
77334×
            let rest = line
77434×
                .strip_prefix("\u{2014} ")
77534×
                .ok_or_else(|| bad("a signature line without its dash"))?;
77634×
            let (name, b64) = rest
77734×
                .split_once(' ')
77834×
                .ok_or_else(|| bad("a signature line without a key"))?;
77934×
            let b =
78034×
                crate::dsse::base64_decode(b64).ok_or_else(|| bad("a signature is not base64"))?;
78134×
            if b.len() < 5 || !valid_name(name) {
7820×
                return Err(bad("a malformed signature line"));
78334×
            }
78434×
            sigs.push(NoteSignature {
78534×
                name: name.to_string(),
78634×
                key_hash: [b[0], b[1], b[2], b[3]],
78734×
                sig: b[4..].to_vec(),
78834×
            });
789
        }
79016×
        if sigs.is_empty() {
7910×
            return Err(bad("no signatures"));
79216×
        }
79316×
        Ok(Note {
79416×
            text: text.to_string(),
79516×
            sigs,
79616×
        })
79716×
    }
798
799
    /// Sign as `name` with `identity`'s key: the Ed25519 line note tools
800
    /// read, and the composite line that keeps it post-quantum.
80111×
    pub fn sign(text: &str, name: &str, identity: &Identity, sk: &SecretKey) -> Result<Note> {
80211×
        if !valid_name(name) {
8030×
            return Err(TlogError::BadNote(format!(
8040×
                "{name:?} cannot name a note's key"
8050×
            )));
80611×
        }
80711×
        if !text.ends_with('\n') || text.contains("\n\n") {
8080×
            return Err(TlogError::BadNote(
8090×
                "the text must end with one newline and hold no blank line".into(),
8100×
            ));
81111×
        }
81211×
        let ed = NoteSignature {
81311×
            name: name.to_string(),
81411×
            key_hash: key_hash(name, SIG_ED25519, &identity.ed25519_pk),
81511×
            sig: sk.sign_ed25519(text.as_bytes()).to_vec(),
81611×
        };
81711×
        let composite = NoteSignature {
81811×
            name: name.to_string(),
81911×
            key_hash: key_hash(name, SIG_COMPOSITE, &identity.encode()),
82011×
            sig: cbor::encode(
82111×
                &sk.sign(Domain::Checkpoint, &identity.hash(), &note_target(text))
82211×
                    .to_value(),
82311×
            ),
82411×
        };
82511×
        Ok(Note {
82611×
            text: text.to_string(),
82711×
            sigs: vec![ed, composite],
82811×
        })
82911×
    }
830
831
    /// Both of `name`'s signatures by `identity` verify (a note signed
832
    /// with Ed25519 only is not enough).
83313×
    pub fn verify(&self, name: &str, identity: &Identity) -> Result<()> {
83422×
        let find = |t: u8, key: &[u8]| {
83522×
            let h = key_hash(name, t, key);
83633×
            self.sigs.iter().find(|s| s.name == name && s.key_hash == h)
83722×
        };
83813×
        let ed = find(SIG_ED25519, &identity.ed25519_pk)
83913×
            .ok_or_else(|| TlogError::BadSignature(format!("no Ed25519 signature by {name}")))?;
84010×
        identity
84110×
            .verify_ed25519(self.text.as_bytes(), &ed.sig)
84210×
            .map_err(|_| {
8431×
                TlogError::BadSignature(format!("{name}'s Ed25519 signature does not verify"))
8441×
            })?;
8459×
        let c = find(SIG_COMPOSITE, &identity.encode())
8469×
            .ok_or_else(|| TlogError::BadSignature(format!("no composite signature by {name}")))?;
8478×
        let sig = cbor::decode(&c.sig)
8488×
            .map_err(|e| e.to_string())
8498×
            .and_then(|v| Signature::from_value(&v).map_err(|e| e.to_string()))
8508×
            .map_err(|e| TlogError::BadSignature(format!("{name}'s composite signature: {e}")))?;
8518×
        verify(identity, &sig, Domain::Checkpoint, &note_target(&self.text)).map_err(|e| {
8520×
            TlogError::BadSignature(format!("{name}'s composite signature does not verify: {e}"))
8530×
        })
85413×
    }
855
}
856
857
/// A checkpoint (C2SP tlog-checkpoint): the log's origin, size and root.
858
#[derive(Clone, Debug, PartialEq, Eq)]
859
pub struct Checkpoint {
860
    /// The log's name: the hub's host and `/log`.
861
    pub origin: String,
862
    pub size: u64,
863
    pub root: Hash,
864
    /// Further lines, each without its newline.
865
    pub extensions: Vec<String>,
866
}
867
868
impl Checkpoint {
86911×
    pub fn text(&self) -> String {
87011×
        let mut t = format!(
871
            "{}\n{}\n{}\n",
872
            self.origin,
873
            self.size,
87411×
            crate::dsse::base64(&self.root)
875
        );
87611×
        for e in &self.extensions {
8770×
            t.push_str(e);
8780×
            t.push('\n');
8790×
        }
88011×
        t
88111×
    }
882
88311×
    pub fn parse(text: &str) -> Result<Checkpoint> {
88411×
        let bad = |w: &str| TlogError::BadNote(format!("checkpoint: {w}"));
88511×
        if !text.ends_with('\n') {
8860×
            return Err(bad("no final newline"));
88711×
        }
88811×
        let mut lines = text[..text.len() - 1].split('\n');
88911×
        let origin = lines
89011×
            .next()
89111×
            .filter(|o| !o.is_empty())
89211×
            .ok_or_else(|| bad("no origin"))?;
89311×
        let size = lines
89411×
            .next()
89511×
            .and_then(|s| {
89611×
                (s == "0" || !s.starts_with('0'))
89711×
                    .then(|| s.parse().ok())
89811×
                    .flatten()
89911×
            })
90011×
            .ok_or_else(|| bad("no tree size"))?;
90111×
        let root = lines
90211×
            .next()
90311×
            .and_then(crate::dsse::base64_decode)
90411×
            .and_then(|b| <Hash>::try_from(b).ok())
90511×
            .ok_or_else(|| bad("no root hash"))?;
90611×
        Ok(Checkpoint {
90711×
            origin: origin.to_string(),
90811×
            size,
90911×
            root,
91011×
            extensions: lines.map(String::from).collect(),
91111×
        })
91211×
    }
913
914
    /// Signed by the log's key, named by its origin.
91511×
    pub fn sign(&self, identity: &Identity, sk: &SecretKey) -> Result<Note> {
91611×
        Note::sign(&self.text(), &self.origin, identity, sk)
91711×
    }
918
919
    /// The checkpoint a note carries, once the log's key (`identity`)
920
    /// signed it for `origin`.
92113×
    pub fn verify(note: &Note, origin: &str, identity: &Identity) -> Result<Checkpoint> {
92213×
        note.verify(origin, identity)?;
9238×
        let c = Checkpoint::parse(&note.text)?;
9248×
        if c.origin != origin {
9250×
            return Err(TlogError::BadNote(format!(
9260×
                "checkpoint of {}, not of {origin}",
9270×
                c.origin
9280×
            )));
9298×
        }
9308×
        Ok(c)
93113×
    }
932
}
933
934
// ------------------------------------------------------------- witnesses
935
936
/// C2SP tlog-cosignature: a witness's Ed25519 cosignature.
937
const SIG_COSIGNATURE: u8 = 0x04;
938
/// Oxygen's composite cosignature beside it (spec §14.8).
939
const SIG_COSIG_COMPOSITE: u8 = 0x4e;
940
941
/// What a cosignature signs: `cosignature/v1`, the witness's time in
942
/// seconds, then the checkpoint's text.
9436×
fn cosigned_message(text: &str, time: u64) -> String {
9446×
    format!("cosignature/v1\ntime {time}\n{text}")
9456×
}
946
9476×
fn cosig_target(message: &str) -> ObjectRef {
9486×
    let mut h = blake3::Hasher::new();
9496×
    h.update(b"oxygen-tlog-cosig\0");
9506×
    h.update(message.as_bytes());
9516×
    ObjectRef(*h.finalize().as_bytes())
9526×
}
953
954
/// A witness's cosignature of a checkpoint's text at `time` (seconds):
955
/// the C2SP cosignature/v1 line, and a composite line that keeps it
956
/// post-quantum. Each carries the time before the signature.
9574×
pub fn cosign(
9584×
    text: &str,
9594×
    name: &str,
9604×
    witness: &Identity,
9614×
    sk: &SecretKey,
9624×
    time: u64,
9634×
) -> Result<Vec<NoteSignature>> {
9644×
    if !valid_name(name) {
9650×
        return Err(TlogError::BadNote(format!(
9660×
            "{name:?} cannot name a witness"
9670×
        )));
9684×
    }
9694×
    let message = cosigned_message(text, time);
9704×
    let mut ed = time.to_be_bytes().to_vec();
9714×
    ed.extend_from_slice(&sk.sign_ed25519(message.as_bytes()));
9724×
    let mut composite = time.to_be_bytes().to_vec();
9734×
    composite.extend_from_slice(&cbor::encode(
9744×
        &sk.sign(Domain::Checkpoint, &witness.hash(), &cosig_target(&message))
9754×
            .to_value(),
9764×
    ));
9774×
    Ok(vec![
9784×
        NoteSignature {
9794×
            name: name.to_string(),
9804×
            key_hash: key_hash(name, SIG_COSIGNATURE, &witness.ed25519_pk),
9814×
            sig: ed,
9824×
        },
9834×
        NoteSignature {
9844×
            name: name.to_string(),
9854×
            key_hash: key_hash(name, SIG_COSIG_COMPOSITE, &witness.encode()),
9864×
            sig: composite,
9874×
        },
9884×
    ])
9894×
}
990
991
impl Note {
992
    /// The time `witness` (named `name`) cosigned this note, once both its
993
    /// lines verify and agree on the time.
9943×
    pub fn cosigned_by(&self, name: &str, witness: &Identity) -> Result<u64> {
9955×
        let find = |t: u8, key: &[u8]| {
9965×
            let h = key_hash(name, t, key);
99714×
            self.sigs.iter().find(|s| s.name == name && s.key_hash == h)
9985×
        };
9994×
        let time_of = |sig: &[u8]| -> Result<(u64, Vec<u8>)> {
10004×
            if sig.len() < 9 {
10010×
                return Err(TlogError::BadSignature(format!(
10020×
                    "{name}'s cosignature is short"
10030×
                )));
10044×
            }
10054×
            let mut t = [0u8; 8];
10064×
            t.copy_from_slice(&sig[..8]);
10074×
            Ok((u64::from_be_bytes(t), sig[8..].to_vec()))
10084×
        };
10093×
        let ed = find(SIG_COSIGNATURE, &witness.ed25519_pk)
10103×
            .ok_or_else(|| TlogError::BadSignature(format!("no cosignature by {name}")))?;
10112×
        let (time, sig) = time_of(&ed.sig)?;
10122×
        let message = cosigned_message(&self.text, time);
10132×
        witness
10142×
            .verify_ed25519(message.as_bytes(), &sig)
10152×
            .map_err(|_| {
10160×
                TlogError::BadSignature(format!("{name}'s cosignature does not verify"))
10170×
            })?;
10182×
        let c = find(SIG_COSIG_COMPOSITE, &witness.encode()).ok_or_else(|| {
10190×
            TlogError::BadSignature(format!("no composite cosignature by {name}"))
10200×
        })?;
10212×
        let (ctime, csig) = time_of(&c.sig)?;
10222×
        if ctime != time {
10230×
            return Err(TlogError::BadSignature(format!(
10240×
                "{name}'s two cosignatures disagree on the time"
10250×
            )));
10262×
        }
10272×
        let sig = cbor::decode(&csig)
10282×
            .map_err(|e| e.to_string())
10292×
            .and_then(|v| Signature::from_value(&v).map_err(|e| e.to_string()))
10302×
            .map_err(|e| TlogError::BadSignature(format!("{name}'s composite cosignature: {e}")))?;
10312×
        verify(witness, &sig, Domain::Checkpoint, &cosig_target(&message)).map_err(|e| {
10320×
            TlogError::BadSignature(format!(
10330×
                "{name}'s composite cosignature does not verify: {e}"
10340×
            ))
10350×
        })?;
10362×
        Ok(time)
10373×
    }
1038
}
1039
1040
/// A C2SP tlog-witness `add-checkpoint` request: the size of the last
1041
/// checkpoint the witness cosigned for this log, the consistency proof
1042
/// from it, and the new checkpoint note.
1043
#[derive(Clone, Debug, PartialEq, Eq)]
1044
pub struct AddCheckpoint {
1045
    pub old_size: u64,
1046
    pub proof: Vec<Hash>,
1047
    pub note: String,
1048
}
1049
1050
impl AddCheckpoint {
10517×
    pub fn encode(&self) -> String {
10527×
        let mut out = format!("old {}\n", self.old_size);
105321×
        for h in &self.proof {
105421×
            out.push_str(&crate::dsse::base64(h));
105521×
            out.push('\n');
105621×
        }
10577×
        out.push('\n');
10587×
        out.push_str(&self.note);
10597×
        out
10607×
    }
1061
10627×
    pub fn parse(body: &str) -> Result<AddCheckpoint> {
10637×
        let bad = |w: &str| TlogError::BadNote(format!("add-checkpoint: {w}"));
10647×
        let (head, note) = body
10657×
            .split_once("\n\n")
10667×
            .ok_or_else(|| bad("no blank line before the checkpoint"))?;
10677×
        let mut lines = head.split('\n');
10687×
        let old_size = lines
10697×
            .next()
10707×
            .and_then(|l| l.strip_prefix("old "))
10717×
            .and_then(|n| n.parse().ok())
10727×
            .ok_or_else(|| bad("no old size"))?;
10737×
        let mut proof = Vec::new();
107421×
        for l in lines {
107521×
            let h = crate::dsse::base64_decode(l)
107621×
                .and_then(|b| <Hash>::try_from(b).ok())
107721×
                .ok_or_else(|| bad("a proof line is not a hash"))?;
107821×
            proof.push(h);
1079
        }
10807×
        Ok(AddCheckpoint {
10817×
            old_size,
10827×
            proof,
10837×
            note: note.to_string(),
10847×
        })
10857×
    }
1086
}
1087
1088
/// What a witness answers.
1089
#[derive(Clone, Debug, PartialEq, Eq)]
1090
pub enum Witnessed {
1091
    /// Cosigned: the checkpoint, and the cosignature lines to return.
1092
    Cosigned {
1093
        checkpoint: Checkpoint,
1094
        lines: Vec<NoteSignature>,
1095
    },
1096
    /// The request's old size is not the witness's: it answers with its own
1097
    /// (409, so the log retries with a proof from there).
1098
    Conflict(u64),
1099
}
1100
1101
/// Why a witness refused.
1102
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
1103
pub enum WitnessError {
1104
    /// Not signed by the log's key (403).
1105
    #[error("{0}")]
1106
    Unsigned(String),
1107
    /// The checkpoint does not extend what the witness cosigned (422).
1108
    #[error("{0}")]
1109
    Inconsistent(String),
1110
    /// Malformed (400).
1111
    #[error("{0}")]
1112
    Bad(String),
1113
}
1114
1115
/// A witness: its name in cosignatures, and its key.
1116
pub struct WitnessKey<'a> {
1117
    pub name: &'a str,
1118
    pub identity: &'a Identity,
1119
    pub sk: &'a SecretKey,
1120
}
1121
1122
/// A witness's decision (C2SP tlog-witness) on `req` for the log named
1123
/// `origin` with key `log_key`, whose last cosigned checkpoint was
1124
/// `latest` (size and root): cosign when the new checkpoint verifies and
1125
/// extends it, as `me` at `now` (seconds).
11268×
pub fn witness(
11278×
    req: &AddCheckpoint,
11288×
    origin: &str,
11298×
    log_key: &Identity,
11308×
    latest: Option<(u64, Hash)>,
11318×
    me: &WitnessKey,
11328×
    now: u64,
11338×
) -> std::result::Result<Witnessed, WitnessError> {
11348×
    let note = Note::parse(&req.note).map_err(|e| WitnessError::Bad(e.to_string()))?;
11358×
    let cp = Checkpoint::verify(&note, origin, log_key)
11368×
        .map_err(|e| WitnessError::Unsigned(e.to_string()))?;
11377×
    let (old, old_root) = latest.unwrap_or((0, empty_root()));
11387×
    if req.old_size != old {
11392×
        return Ok(Witnessed::Conflict(old));
11405×
    }
11415×
    if cp.size < old {
11420×
        return Err(WitnessError::Bad(format!(
11430×
            "a checkpoint of {} entries is older than the {old} cosigned",
11440×
            cp.size
11450×
        )));
11465×
    }
11475×
    let consistent = match (old, cp.size == old) {
11482×
        (0, _) => req.proof.is_empty(),
11490×
        (_, true) => req.proof.is_empty() && cp.root == old_root,
11503×
        _ => check_consistency(&req.proof, cp.size, &cp.root, old, &old_root).is_ok(),
1151
    };
11525×
    if !consistent {
11531×
        return Err(WitnessError::Inconsistent(format!(
11541×
            "the checkpoint of {} entries does not extend the one of {old} cosigned",
11551×
            cp.size
11561×
        )));
11574×
    }
11584×
    let lines = cosign(&note.text, me.name, me.identity, me.sk, now)
11594×
        .map_err(|e| WitnessError::Bad(e.to_string()))?;
11604×
    Ok(Witnessed::Cosigned {
11614×
        checkpoint: cp,
11624×
        lines,
11634×
    })
11648×
}
1165
1166
#[cfg(test)]
1167
mod tests {
1168
    use super::*;
1169
1170
    /// A log in memory: its stored hashes, grown leaf by leaf.
11714×
    fn grow(leaves: u64) -> (Vec<Hash>, Vec<Hash>) {
11724×
        let mut stored: Vec<Hash> = Vec::new();
11734×
        let mut leaf_hashes = Vec::new();
117470190×
        for n in 0..leaves {
117570190×
            let h = leaf_hash(format!("entry {n}").as_bytes());
117670190×
            leaf_hashes.push(h);
117770190×
            let add = stored_hashes_for_leaf(n, h, &stored).unwrap();
117870190×
            assert_eq!(stored.len() as u64, stored_hash_index(0, n));
117970190×
            stored.extend(add);
118070190×
            assert_eq!(stored.len() as u64, stored_hash_count(n + 1));
1181
        }
11824×
        (stored, leaf_hashes)
11834×
    }
1184
1185
    /// RFC 6962's MTH, straight from its definition.
118619600×
    fn mth(leaves: &[Hash]) -> Hash {
118719600×
        match leaves.len() {
11880×
            0 => empty_root(),
11899870×
            1 => leaves[0],
11909730×
            n => {
11919730×
                let (k, _) = maxpow2(n as u64);
11929730×
                node_hash(&mth(&leaves[..k as usize]), &mth(&leaves[k as usize..]))
1193
            }
1194
        }
119519600×
    }
1196
1197
    #[test]
11981×
    fn stored_indexes_round_trip() {
11996×
        for level in 0..6 {
12001800×
            for n in 0..300 {
12011800×
                let i = stored_hash_index(level, n);
12021800×
                assert_eq!(split_stored_hash_index(i), (level, n));
1203
            }
1204
        }
12051×
    }
1206
1207
    #[test]
12081×
    fn roots_match_rfc_6962_and_every_proof_verifies() {
12091×
        let (stored, leaves) = grow(140);
1210140×
        for t in 1..=140u64 {
1211140×
            let root = tree_hash(t, &stored).unwrap();
1212140×
            assert_eq!(root, mth(&leaves[..t as usize]), "root of {t}");
12139870×
            for n in 0..t {
12149870×
                let p = prove_inclusion(t, n, &stored).unwrap();
12159870×
                check_inclusion(&p, t, &root, n, &leaves[n as usize]).unwrap();
1216
                // Another leaf, another tree, a tampered path: refused.
12179870×
                let other = (n + 1) % t;
12189870×
                if other != n {
12199869×
                    assert!(check_inclusion(&p, t, &root, n, &leaves[other as usize]).is_err());
12201×
                }
12219870×
                if let Some(first) = p.first() {
12229869×
                    let mut bad = p.clone();
12239869×
                    bad[0] = node_hash(first, first);
12249869×
                    assert!(check_inclusion(&bad, t, &root, n, &leaves[n as usize]).is_err());
12251×
                }
12269870×
                let old = n + 1;
12279870×
                let old_root = tree_hash(old, &stored).unwrap();
12289870×
                let c = prove_consistency(t, old, &stored).unwrap();
12299870×
                check_consistency(&c, t, &root, old, &old_root).unwrap();
12309870×
                if old < t {
12319730×
                    let fork = node_hash(&old_root, &old_root);
12329730×
                    assert!(
12339730×
                        check_consistency(&c, t, &root, old, &fork).is_err(),
1234
                        "a forked history"
1235
                    );
1236140×
                }
1237
            }
1238
        }
12391×
        assert_eq!(tree_hash(0, &stored).unwrap(), empty_root());
12401×
    }
1241
1242
    #[test]
12431×
    fn tiles_hold_the_stored_hashes() {
12441×
        let n = 70_000u64;
12451×
        let (stored, _) = grow(n);
12461×
        let tiles = Tile::new_tiles(0, n);
1247
        // 273 full tiles of level 0 and a partial one, one full and one
1248
        // partial at level 1, a partial at level 2.
1249277×
        assert_eq!(tiles.iter().filter(|t| t.level == 0).count(), 274);
1250277×
        assert_eq!(tiles.iter().filter(|t| t.level == 2).count(), 1);
12511×
        let datas: Vec<(Tile, Vec<u8>)> = tiles
12521×
            .iter()
1253277×
            .map(|t| (*t, t.read_data(&stored).unwrap()))
12541×
            .collect();
125520000×
        for (i, want) in stored.iter().enumerate().step_by(7) {
125620000×
            let (t, ..) = Tile::for_index(i as u64);
125720000×
            let (tile, data) = datas
125820000×
                .iter()
12592756800×
                .find(|(d, _)| d.level == t.level && d.n == t.n && d.width >= t.width)
126020000×
                .expect("the tile holding it was written");
126120000×
            assert_eq!(
126220000×
                tile.hash_at(data, i as u64).unwrap(),
1263
                *want,
1264
                "stored hash {i}"
1265
            );
1266
        }
1267
        // Growing writes only what changed.
12681×
        let grown = Tile::new_tiles(n, n + 300);
12693×
        assert!(grown.iter().all(|t| t.level > 0 || t.n >= n / 256));
12701×
    }
1271
1272
    #[test]
12731×
    fn tile_paths() {
12741×
        assert_eq!(index_path(1_234_067), "x001/x234/067");
12751×
        assert_eq!(index_path(5), "005");
12761×
        assert_eq!(index_path(1000), "x001/000");
12771×
        assert_eq!(parse_index_path("x001/x234/067"), Some(1_234_067));
12781×
        assert_eq!(parse_index_path("x001/234/067"), None);
12791×
        let t = Tile {
12801×
            level: 1,
12811×
            n: 1234,
12821×
            width: 17,
12831×
        };
12841×
        assert_eq!(t.path(), "tile/1/x001/234.p/17");
12851×
        assert_eq!(Tile::parse("1/x001/234.p/17"), Some(t));
12861×
        assert_eq!(
12871×
            Tile {
12881×
                level: 0,
12891×
                n: 3,
12901×
                width: 256
12911×
            }
12921×
            .entries_path(),
1293
            "tile/entries/003"
1294
        );
12951×
        assert_eq!(Tile::parse("0/003.p/256"), None, "a full tile has no .p");
12961×
    }
1297
1298
    #[test]
12991×
    fn entries_and_bundles_round_trip() {
13001×
        let e = Entry {
13011×
            kind: "ref".into(),
13021×
            repo: [7; 16],
13031×
            object: ObjectRef([1; 32]),
13041×
            signers: vec![ObjectRef([2; 32])],
13051×
            received: 1_792_000_000_000,
13061×
        };
13071×
        assert_eq!(Entry::decode(&e.encode()).unwrap(), e);
13081×
        let b = encode_bundle(&[e.encode(), b"x".to_vec()]).unwrap();
13091×
        assert_eq!(decode_bundle(&b).unwrap(), vec![e.encode(), b"x".to_vec()]);
13101×
        assert!(decode_bundle(&b[..b.len() - 1]).is_err());
13111×
        assert_eq!(e.leaf_hash(), leaf_hash(&e.encode()));
13121×
    }
1313
1314
    #[test]
13151×
    fn a_witness_cosigns_what_extends_what_it_saw_and_nothing_else() {
13161×
        let log_sk = SecretKey::generate();
13171×
        let log = log_sk.identity("log", "[email protected]", 1);
13181×
        let w_sk = SecretKey::generate();
13191×
        let w = w_sk.identity("witness", "[email protected]", 1);
13201×
        let origin = "hub.example/log";
13211×
        let (stored, _) = grow(40);
13226×
        let note_of = |n: u64| {
13236×
            Checkpoint {
13246×
                origin: origin.into(),
13256×
                size: n,
13266×
                root: tree_hash(n, &stored).unwrap(),
13276×
                extensions: vec![],
13286×
            }
13296×
            .sign(&log, &log_sk)
13306×
            .unwrap()
13316×
            .encode()
13326×
        };
13334×
        let ask = |old: u64, n: u64, latest: Option<(u64, Hash)>| {
13344×
            let proof = if old == 0 || old == n {
13351×
                vec![]
1336
            } else {
13373×
                prove_consistency(n, old, &stored).unwrap()
1338
            };
13394×
            let req = AddCheckpoint {
13404×
                old_size: old,
13414×
                proof,
13424×
                note: note_of(n),
13434×
            };
13444×
            let req = AddCheckpoint::parse(&req.encode()).unwrap();
13454×
            let me = WitnessKey {
13464×
                name: "w.example",
13474×
                identity: &w,
13484×
                sk: &w_sk,
13494×
            };
13504×
            witness(&req, origin, &log, latest, &me, 1_792_300_000)
13514×
        };
1352
        // First sight, then a later checkpoint with its proof.
13531×
        let Ok(Witnessed::Cosigned { checkpoint, lines }) = ask(0, 10, None) else {
13540×
            panic!()
1355
        };
13561×
        let mut note = Note::parse(&note_of(10)).unwrap();
13571×
        note.sigs.extend(lines);
13581×
        let back = Note::parse(&note.encode()).unwrap();
13591×
        assert_eq!(back.cosigned_by("w.example", &w).unwrap(), 1_792_300_000);
13601×
        assert!(
13611×
            back.cosigned_by("w.example", &log).is_err(),
1362
            "another witness's key"
1363
        );
13641×
        let latest = Some((10, checkpoint.root));
13651×
        assert!(matches!(
13661×
            ask(10, 40, latest),
1367
            Ok(Witnessed::Cosigned { .. })
1368
        ));
1369
        // The log's idea of what the witness saw is wrong: told the size.
13701×
        assert_eq!(ask(5, 40, latest), Ok(Witnessed::Conflict(10)));
1371
        // A fork: what the witness saw is not a prefix.
13721×
        let forked = Some((10, [9; 32]));
13731×
        assert!(matches!(
13741×
            ask(10, 40, forked),
1375
            Err(WitnessError::Inconsistent(_))
1376
        ));
1377
        // Signed by someone else.
13781×
        let req = AddCheckpoint {
13791×
            old_size: 0,
13801×
            proof: vec![],
13811×
            note: note_of(10),
13821×
        };
13831×
        assert!(matches!(
13841×
            witness(
13851×
                &req,
13861×
                origin,
13871×
                &w,
13881×
                None,
13891×
                &WitnessKey {
13901×
                    name: "w.example",
13911×
                    identity: &w,
13921×
                    sk: &w_sk
13931×
                },
1394
                1
1395
            ),
1396
            Err(WitnessError::Unsigned(_))
1397
        ));
13981×
    }
1399
1400
    #[test]
14011×
    fn checkpoints_are_signed_twice_and_checked() {
14021×
        let sk = SecretKey::generate();
14031×
        let hub = sk.identity("oxygenhub.io", "[email protected]", 1);
14041×
        let (stored, _) = grow(10);
14051×
        let c = Checkpoint {
14061×
            origin: "oxygenhub.io/log".into(),
14071×
            size: 10,
14081×
            root: tree_hash(10, &stored).unwrap(),
14091×
            extensions: vec![],
14101×
        };
14111×
        let note = c.sign(&hub, &sk).unwrap();
14121×
        let served = note.encode();
14131×
        assert!(served.starts_with("oxygenhub.io/log\n10\n"));
14141×
        assert_eq!(served.matches("\n\u{2014} oxygenhub.io/log ").count(), 2);
14151×
        let back = Note::parse(&served).unwrap();
14161×
        assert_eq!(
14171×
            Checkpoint::verify(&back, "oxygenhub.io/log", &hub).unwrap(),
1418
            c
1419
        );
1420
        // Another key, another origin, a changed size, a lost line: refused.
14211×
        let other_sk = SecretKey::generate();
14221×
        let other = other_sk.identity("x", "x@x", 1);
14231×
        assert!(Checkpoint::verify(&back, "oxygenhub.io/log", &other).is_err());
14241×
        assert!(Checkpoint::verify(&back, "elsewhere/log", &hub).is_err());
14251×
        let forged = Note {
14261×
            text: back.text.replace("\n10\n", "\n11\n"),
14271×
            ..back.clone()
14281×
        };
14291×
        assert!(Checkpoint::verify(&forged, "oxygenhub.io/log", &hub).is_err());
14301×
        let ed_only = Note {
14311×
            sigs: vec![back.sigs[0].clone()],
14321×
            ..back.clone()
14331×
        };
14341×
        assert!(
14351×
            matches!(Checkpoint::verify(&ed_only, "oxygenhub.io/log", &hub), Err(TlogError::BadSignature(m)) if m.contains("composite")),
1436
            "the post-quantum line is required"
1437
        );
1438
        // The Ed25519 line is a standard note signature over the text.
14391×
        let h = key_hash("oxygenhub.io/log", SIG_ED25519, &hub.ed25519_pk);
14401×
        assert_eq!(back.sigs[0].key_hash, h);
14411×
        assert!(hub
14421×
            .verify_ed25519(back.text.as_bytes(), &back.sigs[0].sig)
14431×
            .is_ok());
14441×
    }
1445
}