OXYGENHUB

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

1138 of 1231 lines run, 172 of 273 functions · run #427 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)`.
5870268×
pub fn leaf_hash(entry: &[u8]) -> Hash {
5970268×
    let mut h = blake3::Hasher::new();
6070268×
    h.update(b"oxygen-tlog-leaf\0");
6170268×
    h.update(entry);
6270268×
    *h.finalize().as_bytes()
6370268×
}
64
65
/// An interior node's hash: `BLAKE3("oxygen-tlog-node\0" || left || right)`.
66564519×
pub fn node_hash(left: &Hash, right: &Hash) -> Hash {
67564519×
    let mut h = blake3::Hasher::new();
68564519×
    h.update(b"oxygen-tlog-node\0");
69564519×
    h.update(left);
70564519×
    h.update(right);
71564519×
    *h.finalize().as_bytes()
72564519×
}
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 {
10060×
    pub fn to_value(&self) -> Value {
10160×
        Value::map([
10260×
            (0, Value::Uint(1)),
10360×
            (1, Value::Text(self.kind.clone())),
10460×
            (2, Value::Bytes(self.repo.to_vec())),
10560×
            (3, Value::Bytes(self.object.0.to_vec())),
106
            (
107
                4,
108
                Value::Array(
10960×
                    self.signers
11060×
                        .iter()
11160×
                        .map(|k| Value::Bytes(k.0.to_vec()))
11260×
                        .collect(),
113
                ),
114
            ),
11560×
            (5, Value::Uint(self.received)),
116
        ])
11760×
    }
118
11960×
    pub fn encode(&self) -> Vec<u8> {
12060×
        cbor::encode(&self.to_value())
12160×
    }
122
1235×
    pub fn decode(b: &[u8]) -> Result<Entry> {
1245×
        let v = cbor::decode(b).map_err(ObjectError::from)?;
1255×
        let m = map(&v, "log entry")?;
1265×
        check_version(m)?;
1275×
        let repo = bytes(field(m, 2, "repo")?, "repo")?;
1285×
        let repo: [u8; 16] = repo
1295×
            .try_into()
1305×
            .map_err(|_| TlogError::Schema("log entry: repo is not 16 bytes".into()))?;
1315×
        let mut signers = Vec::new();
1325×
        for s in array(field(m, 4, "signers")?, "signers")? {
1335×
            signers.push(oref(s, "signer")?);
134
        }
135
        Ok(Entry {
1365×
            kind: text(field(m, 1, "kind")?, "kind")?.to_owned(),
1375×
            repo,
1385×
            object: oref(field(m, 3, "object")?, "object")?,
1395×
            signers,
1405×
            received: uint(field(m, 5, "received")?, "received")?,
141
        })
1425×
    }
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] {
190100173×
    fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> {
191100173×
        indexes
192100173×
            .iter()
193331646×
            .map(|&i| {
194331646×
                self.get(i as usize)
195331646×
                    .copied()
196331646×
                    .ok_or_else(|| TlogError::Read(format!("no stored hash {i}")))
197331646×
            })
198100173×
            .collect()
199100173×
    }
200
}
201
202
impl HashReader for Vec<Hash> {
203100173×
    fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> {
204100173×
        self.as_slice().read_hashes(indexes)
205100173×
    }
206
}
207
208
/// The largest power of two below `n` (n ≥ 2), and its log.
209948330×
fn maxpow2(n: u64) -> (u64, u32) {
210948330×
    let mut l = 0;
2113629898×
    while (1u64 << (l + 1)) < n {
2122681568×
        l += 1;
2132681568×
    }
214948330×
    (1 << l, l)
215948330×
}
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 + ….
220515626×
pub fn stored_hash_index(level: u32, mut n: u64) -> u64 {
221549189×
    for _ in 0..level {
222549189×
        n = 2 * n + 1;
223549189×
    }
224515626×
    let mut i = 0;
225515626×
    let mut m = n;
2266528807×
    while m > 0 {
2276013181×
        i += m;
2286013181×
        m >>= 1;
2296013181×
    }
230515626×
    i + level as u64
231515626×
}
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.
26770247×
pub fn stored_hashes_for_leaf(
26870247×
    n: u64,
26970247×
    leaf: Hash,
27070247×
    r: &(impl HashReader + ?Sized),
27170247×
) -> Result<Vec<Hash>> {
27270247×
    let m = (n + 1).trailing_zeros();
27370247×
    let indexes: Vec<u64> = (0..m)
27470247×
        .rev()
27570247×
        .map(|i| stored_hash_index(i, (n >> i) - 1))
27670247×
        .collect();
27770247×
    let old = r.read_hashes(&indexes)?;
27870247×
    let mut h = leaf;
27970247×
    let mut out = vec![h];
28070247×
    for i in 0..m as usize {
28170229×
        h = node_hash(&old[m as usize - 1 - i], &h);
28270229×
        out.push(h);
28370229×
    }
28470247×
    Ok(out)
28570247×
}
286
287141470×
fn sub_tree_index(mut lo: u64, hi: u64, need: &mut Vec<u64>) {
288332613×
    while lo < hi {
289191143×
        let (k, level) = maxpow2(hi - lo + 1);
290191143×
        debug_assert_eq!(lo & (k - 1), 0);
291191143×
        need.push(stored_hash_index(level, lo >> level));
292191143×
        lo += k;
293
    }
294141470×
}
295
296141470×
fn sub_tree_hash(mut lo: u64, hi: u64, hashes: &[Hash]) -> (Hash, &[Hash]) {
297141470×
    let mut count = 0;
298332613×
    while lo < hi {
299191143×
        let (k, _) = maxpow2(hi - lo + 1);
300191143×
        count += 1;
301191143×
        lo += k;
302191143×
    }
303141470×
    let mut h = hashes[count - 1];
304141470×
    for i in (0..count - 1).rev() {
30549673×
        h = node_hash(&hashes[i], &h);
30649673×
    }
307141470×
    (h, &hashes[count..])
308141470×
}
309
310
/// The root of the first `n` leaves.
31110030×
pub fn tree_hash(n: u64, r: &(impl HashReader + ?Sized)) -> Result<Hash> {
31210030×
    if n == 0 {
3131×
        return Ok(empty_root());
31410029×
    }
31510029×
    let mut need = Vec::new();
31610029×
    sub_tree_index(0, n, &mut need);
31710029×
    let hashes = r.read_hashes(&need)?;
31810029×
    Ok(sub_tree_hash(0, n, &hashes).0)
31910030×
}
320
32176061×
fn leaf_proof_index(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) {
32276061×
    if lo + 1 == hi {
3239887×
        return;
32466174×
    }
32566174×
    let (k, _) = maxpow2(hi - lo);
32666174×
    if n < lo + k {
32735738×
        leaf_proof_index(lo, lo + k, n, need);
32835738×
        sub_tree_index(lo + k, hi, need);
32935738×
    } else {
33030436×
        sub_tree_index(lo, lo + k, need);
33130436×
        leaf_proof_index(lo + k, hi, n, need);
33230436×
    }
33376061×
}
334
33576060×
fn leaf_proof(lo: u64, hi: u64, n: u64, hashes: &[Hash]) -> (Vec<Hash>, &[Hash]) {
33676060×
    if lo + 1 == hi {
337
        // The verifier has the leaf's hash.
3389886×
        return (Vec::new(), hashes);
33966174×
    }
34066174×
    let (k, _) = maxpow2(hi - lo);
341
    let (mut p, th, rest);
34266174×
    if n < lo + k {
34335738×
        let (pp, h1) = leaf_proof(lo, lo + k, n, hashes);
34435738×
        let (t, h2) = sub_tree_hash(lo + k, hi, h1);
34535738×
        (p, th, rest) = (pp, t, h2);
34635738×
    } else {
34730436×
        let (t, h1) = sub_tree_hash(lo, lo + k, hashes);
34830436×
        let (pp, h2) = leaf_proof(lo + k, hi, n, h1);
34930436×
        (p, th, rest) = (pp, t, h2);
35030436×
    }
35166174×
    p.push(th);
35266174×
    (p, rest)
35376060×
}
354
355
/// The inclusion proof of leaf `n` in the tree of the first `t` leaves.
3569887×
pub fn prove_inclusion(t: u64, n: u64, r: &(impl HashReader + ?Sized)) -> Result<Vec<Hash>> {
3579887×
    if n >= t {
3580×
        return Err(TlogError::BadIndex(format!(
3590×
            "leaf {n} is not in a tree of {t}"
3600×
        )));
3619887×
    }
3629887×
    let mut need = Vec::new();
3639887×
    leaf_proof_index(0, t, n, &mut need);
3649887×
    if need.is_empty() {
3651×
        return Ok(Vec::new());
3669886×
    }
3679886×
    let hashes = r.read_hashes(&need)?;
3689886×
    Ok(leaf_proof(0, t, n, &hashes).0)
3699887×
}
370
371228020×
fn run_inclusion(p: &[Hash], lo: u64, hi: u64, n: u64, leaf: Hash) -> Result<Hash> {
372228020×
    if lo + 1 == hi {
37329626×
        return if p.is_empty() {
37429625×
            Ok(leaf)
375
        } else {
3761×
            Err(TlogError::ProofFailed)
377
        };
378198394×
    }
379198394×
    let Some((last, rest)) = p.split_last() else {
3800×
        return Err(TlogError::ProofFailed);
381
    };
382198394×
    let (k, _) = maxpow2(hi - lo);
383198394×
    if n < lo + k {
384107175×
        Ok(node_hash(&run_inclusion(rest, lo, lo + k, n, leaf)?, last))
385
    } else {
38691219×
        Ok(node_hash(last, &run_inclusion(rest, lo + k, hi, n, leaf)?))
387
    }
388228020×
}
389
390
/// Check that leaf `n`, with hash `leaf`, is in the tree of `t` leaves
391
/// whose root is `root`.
39229627×
pub fn check_inclusion(proof: &[Hash], t: u64, root: &Hash, n: u64, leaf: &Hash) -> Result<()> {
39329627×
    if n >= t {
3941×
        return Err(TlogError::ProofFailed);
39529626×
    }
39629626×
    if run_inclusion(proof, 0, t, n, *leaf)? == *root {
3979886×
        Ok(())
398
    } else {
39919739×
        Err(TlogError::ProofFailed)
400
    }
40129627×
}
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
723162×
fn key_hash(name: &str, sig_type: u8, key: &[u8]) -> [u8; 4] {
724162×
    let mut h = Sha256::new();
725162×
    h.update(name.as_bytes());
726162×
    h.update(b"\n");
727162×
    h.update([sig_type]);
728162×
    h.update(key);
729162×
    let d = h.finalize();
730162×
    [d[0], d[1], d[2], d[3]]
731162×
}
732
733
/// What a composite signature of a note signs (domain `checkpoint`).
73446×
fn note_target(text: &str) -> ObjectRef {
73546×
    let mut h = blake3::Hasher::new();
73646×
    h.update(b"oxygen-tlog-note\0");
73746×
    h.update(text.as_bytes());
73846×
    ObjectRef(*h.finalize().as_bytes())
73946×
}
740
741180×
fn valid_name(name: &str) -> bool {
7422265×
    !name.is_empty() && !name.contains(|c: char| c.is_whitespace() || c == '+')
743180×
}
744
745
impl Note {
746
    /// The note as served: the text, a blank line, the signature lines.
74728×
    pub fn encode(&self) -> String {
74828×
        let mut out = self.text.clone();
74928×
        out.push('\n');
75080×
        for s in &self.sigs {
75180×
            let mut b = s.key_hash.to_vec();
75280×
            b.extend_from_slice(&s.sig);
75380×
            out.push_str(&format!(
75480×
                "\u{2014} {} {}\n",
75580×
                s.name,
75680×
                crate::dsse::base64(&b)
75780×
            ));
75880×
        }
75928×
        out
76028×
    }
761
76243×
    pub fn parse(s: &str) -> Result<Note> {
76343×
        let bad = |w: &str| TlogError::BadNote(w.to_string());
76443×
        let split = s
76543×
            .rfind("\n\n")
76643×
            .ok_or_else(|| bad("no blank line before the signatures"))?;
76743×
        let text = &s[..split + 1];
7682731×
        if text.chars().any(|c| c.is_control() && c != '\n') {
7690×
            return Err(bad("control characters in the text"));
77043×
        }
77143×
        let mut sigs = Vec::new();
772148×
        for line in s[split + 2..].lines() {
773148×
            let rest = line
774148×
                .strip_prefix("\u{2014} ")
775148×
                .ok_or_else(|| bad("a signature line without its dash"))?;
776148×
            let (name, b64) = rest
777148×
                .split_once(' ')
778148×
                .ok_or_else(|| bad("a signature line without a key"))?;
779148×
            let b =
780148×
                crate::dsse::base64_decode(b64).ok_or_else(|| bad("a signature is not base64"))?;
781148×
            if b.len() < 5 || !valid_name(name) {
7820×
                return Err(bad("a malformed signature line"));
783148×
            }
784148×
            sigs.push(NoteSignature {
785148×
                name: name.to_string(),
786148×
                key_hash: [b[0], b[1], b[2], b[3]],
787148×
                sig: b[4..].to_vec(),
788148×
            });
789
        }
79043×
        if sigs.is_empty() {
7910×
            return Err(bad("no signatures"));
79243×
        }
79343×
        Ok(Note {
79443×
            text: text.to_string(),
79543×
            sigs,
79643×
        })
79743×
    }
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.
80119×
    pub fn sign(text: &str, name: &str, identity: &Identity, sk: &SecretKey) -> Result<Note> {
80219×
        if !valid_name(name) {
8030×
            return Err(TlogError::BadNote(format!(
8040×
                "{name:?} cannot name a note's key"
8050×
            )));
80619×
        }
80719×
        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×
            ));
81119×
        }
81219×
        let ed = NoteSignature {
81319×
            name: name.to_string(),
81419×
            key_hash: key_hash(name, SIG_ED25519, &identity.ed25519_pk),
81519×
            sig: sk.sign_ed25519(text.as_bytes()).to_vec(),
81619×
        };
81719×
        let composite = NoteSignature {
81819×
            name: name.to_string(),
81919×
            key_hash: key_hash(name, SIG_COMPOSITE, &identity.encode()),
82019×
            sig: cbor::encode(
82119×
                &sk.sign(Domain::Checkpoint, &identity.hash(), &note_target(text))
82219×
                    .to_value(),
82319×
            ),
82419×
        };
82519×
        Ok(Note {
82619×
            text: text.to_string(),
82719×
            sigs: vec![ed, composite],
82819×
        })
82919×
    }
830
831
    /// Both of `name`'s signatures by `identity` verify (a note signed
832
    /// with Ed25519 only is not enough).
83334×
    pub fn verify(&self, name: &str, identity: &Identity) -> Result<()> {
83462×
        let find = |t: u8, key: &[u8]| {
83562×
            let h = key_hash(name, t, key);
836102×
            self.sigs.iter().find(|s| s.name == name && s.key_hash == h)
83762×
        };
83834×
        let ed = find(SIG_ED25519, &identity.ed25519_pk)
83934×
            .ok_or_else(|| TlogError::BadSignature(format!("no Ed25519 signature by {name}")))?;
84029×
        identity
84129×
            .verify_ed25519(self.text.as_bytes(), &ed.sig)
84229×
            .map_err(|_| {
8431×
                TlogError::BadSignature(format!("{name}'s Ed25519 signature does not verify"))
8441×
            })?;
84528×
        let c = find(SIG_COMPOSITE, &identity.encode())
84628×
            .ok_or_else(|| TlogError::BadSignature(format!("no composite signature by {name}")))?;
84727×
        let sig = cbor::decode(&c.sig)
84827×
            .map_err(|e| e.to_string())
84927×
            .and_then(|v| Signature::from_value(&v).map_err(|e| e.to_string()))
85027×
            .map_err(|e| TlogError::BadSignature(format!("{name}'s composite signature: {e}")))?;
85127×
        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×
        })
85434×
    }
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 {
86919×
    pub fn text(&self) -> String {
87019×
        let mut t = format!(
871
            "{}\n{}\n{}\n",
872
            self.origin,
873
            self.size,
87419×
            crate::dsse::base64(&self.root)
875
        );
87619×
        for e in &self.extensions {
8770×
            t.push_str(e);
8780×
            t.push('\n');
8790×
        }
88019×
        t
88119×
    }
882
88334×
    pub fn parse(text: &str) -> Result<Checkpoint> {
88434×
        let bad = |w: &str| TlogError::BadNote(format!("checkpoint: {w}"));
88534×
        if !text.ends_with('\n') {
8860×
            return Err(bad("no final newline"));
88734×
        }
88834×
        let mut lines = text[..text.len() - 1].split('\n');
88934×
        let origin = lines
89034×
            .next()
89134×
            .filter(|o| !o.is_empty())
89234×
            .ok_or_else(|| bad("no origin"))?;
89334×
        let size = lines
89434×
            .next()
89534×
            .and_then(|s| {
89634×
                (s == "0" || !s.starts_with('0'))
89734×
                    .then(|| s.parse().ok())
89834×
                    .flatten()
89934×
            })
90034×
            .ok_or_else(|| bad("no tree size"))?;
90134×
        let root = lines
90234×
            .next()
90334×
            .and_then(crate::dsse::base64_decode)
90434×
            .and_then(|b| <Hash>::try_from(b).ok())
90534×
            .ok_or_else(|| bad("no root hash"))?;
90634×
        Ok(Checkpoint {
90734×
            origin: origin.to_string(),
90834×
            size,
90934×
            root,
91034×
            extensions: lines.map(String::from).collect(),
91134×
        })
91234×
    }
913
914
    /// Signed by the log's key, named by its origin.
91519×
    pub fn sign(&self, identity: &Identity, sk: &SecretKey) -> Result<Note> {
91619×
        Note::sign(&self.text(), &self.origin, identity, sk)
91719×
    }
918
919
    /// The checkpoint a note carries, once the log's key (`identity`)
920
    /// signed it for `origin`.
92130×
    pub fn verify(note: &Note, origin: &str, identity: &Identity) -> Result<Checkpoint> {
92230×
        note.verify(origin, identity)?;
92323×
        let c = Checkpoint::parse(&note.text)?;
92423×
        if c.origin != origin {
9250×
            return Err(TlogError::BadNote(format!(
9260×
                "checkpoint of {}, not of {origin}",
9270×
                c.origin
9280×
            )));
92923×
        }
93023×
        Ok(c)
93130×
    }
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.
94326×
fn cosigned_message(text: &str, time: u64) -> String {
94426×
    format!("cosignature/v1\ntime {time}\n{text}")
94526×
}
946
94725×
fn cosig_target(message: &str) -> ObjectRef {
94825×
    let mut h = blake3::Hasher::new();
94925×
    h.update(b"oxygen-tlog-cosig\0");
95025×
    h.update(message.as_bytes());
95125×
    ObjectRef(*h.finalize().as_bytes())
95225×
}
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.
95713×
pub fn cosign(
95813×
    text: &str,
95913×
    name: &str,
96013×
    witness: &Identity,
96113×
    sk: &SecretKey,
96213×
    time: u64,
96313×
) -> Result<Vec<NoteSignature>> {
96413×
    if !valid_name(name) {
9650×
        return Err(TlogError::BadNote(format!(
9660×
            "{name:?} cannot name a witness"
9670×
        )));
96813×
    }
96913×
    let message = cosigned_message(text, time);
97013×
    let mut ed = time.to_be_bytes().to_vec();
97113×
    ed.extend_from_slice(&sk.sign_ed25519(message.as_bytes()));
97213×
    let mut composite = time.to_be_bytes().to_vec();
97313×
    composite.extend_from_slice(&cbor::encode(
97413×
        &sk.sign(Domain::Checkpoint, &witness.hash(), &cosig_target(&message))
97513×
            .to_value(),
97613×
    ));
97713×
    Ok(vec![
97813×
        NoteSignature {
97913×
            name: name.to_string(),
98013×
            key_hash: key_hash(name, SIG_COSIGNATURE, &witness.ed25519_pk),
98113×
            sig: ed,
98213×
        },
98313×
        NoteSignature {
98413×
            name: name.to_string(),
98513×
            key_hash: key_hash(name, SIG_COSIG_COMPOSITE, &witness.encode()),
98613×
            sig: composite,
98713×
        },
98813×
    ])
98913×
}
990
991
impl Note {
992
    /// The time `witness` (named `name`) cosigned this note, once both its
993
    /// lines verify and agree on the time.
99423×
    pub fn cosigned_by(&self, name: &str, witness: &Identity) -> Result<u64> {
99535×
        let find = |t: u8, key: &[u8]| {
99635×
            let h = key_hash(name, t, key);
997139×
            self.sigs.iter().find(|s| s.name == name && s.key_hash == h)
99835×
        };
99925×
        let time_of = |sig: &[u8]| -> Result<(u64, Vec<u8>)> {
100025×
            if sig.len() < 9 {
10010×
                return Err(TlogError::BadSignature(format!(
10020×
                    "{name}'s cosignature is short"
10030×
                )));
100425×
            }
100525×
            let mut t = [0u8; 8];
100625×
            t.copy_from_slice(&sig[..8]);
100725×
            Ok((u64::from_be_bytes(t), sig[8..].to_vec()))
100825×
        };
100923×
        let ed = find(SIG_COSIGNATURE, &witness.ed25519_pk)
101023×
            .ok_or_else(|| TlogError::BadSignature(format!("no cosignature by {name}")))?;
101113×
        let (time, sig) = time_of(&ed.sig)?;
101213×
        let message = cosigned_message(&self.text, time);
101313×
        witness
101413×
            .verify_ed25519(message.as_bytes(), &sig)
101513×
            .map_err(|_| {
10161×
                TlogError::BadSignature(format!("{name}'s cosignature does not verify"))
10171×
            })?;
101812×
        let c = find(SIG_COSIG_COMPOSITE, &witness.encode()).ok_or_else(|| {
10190×
            TlogError::BadSignature(format!("no composite cosignature by {name}"))
10200×
        })?;
102112×
        let (ctime, csig) = time_of(&c.sig)?;
102212×
        if ctime != time {
10230×
            return Err(TlogError::BadSignature(format!(
10240×
                "{name}'s two cosignatures disagree on the time"
10250×
            )));
102612×
        }
102712×
        let sig = cbor::decode(&csig)
102812×
            .map_err(|e| e.to_string())
102912×
            .and_then(|v| Signature::from_value(&v).map_err(|e| e.to_string()))
103012×
            .map_err(|e| TlogError::BadSignature(format!("{name}'s composite cosignature: {e}")))?;
103112×
        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×
        })?;
103612×
        Ok(time)
103723×
    }
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
// -------------------------------------------------------- integrated time
1167
1168
/// How far a claimed time may stray from an integrated time before it is
1169
/// noted (spec §14.8, *Integrated time*): ten minutes, in milliseconds.
1170
pub const TIME_TOLERANCE_MS: u64 = 10 * 60 * 1000;
1171
1172
impl Note {
1173
    /// The witnesses among `trusted` (name and identity) whose
1174
    /// cosignatures of this note verify, with their times in seconds.
1175
    /// Lines by anyone else are ignored.
117612×
    pub fn cosignatures(&self, trusted: &[(String, Identity)]) -> Vec<(String, u64)> {
117712×
        trusted
117812×
            .iter()
117920×
            .filter_map(|(n, id)| self.cosigned_by(n, id).ok().map(|t| (n.clone(), t)))
118012×
            .collect()
118112×
    }
1182
}
1183
1184
/// When witnesses first saw a log entry (spec §14.8): an upper bound on
1185
/// when what it logs was signed, by a clock neither its signer nor the
1186
/// log chose.
1187
#[derive(Clone, Debug, PartialEq, Eq)]
1188
pub struct Integrated {
1189
    /// The earliest trusted cosignature's time, in seconds.
1190
    pub time: u64,
1191
    /// The witness that made it.
1192
    pub witness: String,
1193
    /// The size of the checkpoint it cosigned.
1194
    pub size: u64,
1195
}
1196
1197
impl Integrated {
1198
    /// The time in milliseconds, as Oxygen's other times are.
119910×
    pub fn ms(&self) -> u64 {
120010×
        self.time.saturating_mul(1000)
120110×
    }
1202
}
1203
1204
/// Entry `index` (`entry`, its bytes)'s integrated time: the earliest time
1205
/// a witness in `trusted` cosigned one of `cosigned` (checkpoint notes,
1206
/// each with the entry's inclusion proof against it) that `log_key`
1207
/// signed for `origin` and that includes the entry. A note that does not
1208
/// verify, or does not include the entry, counts for nothing; `Err` says
1209
/// why no time could be read.
121016×
pub fn integrated_time(
121116×
    entry: &[u8],
121216×
    index: u64,
121316×
    cosigned: &[(&str, &[Hash])],
121416×
    origin: &str,
121516×
    log_key: &Identity,
121616×
    trusted: &[(String, Identity)],
121716×
) -> std::result::Result<Integrated, String> {
121816×
    if cosigned.is_empty() {
12191×
        return Err("no witness has cosigned a checkpoint holding it yet".into());
122015×
    }
122115×
    let leaf = leaf_hash(entry);
122215×
    let mut best: Option<Integrated> = None;
122315×
    let mut why = String::new();
122417×
    for (text, proof) in cosigned {
122517×
        let checked = Note::parse(text)
122617×
            .and_then(|n| Checkpoint::verify(&n, origin, log_key).map(|c| (n, c)))
122717×
            .and_then(|(n, c)| {
122815×
                check_inclusion(proof, c.size, &c.root, index, &leaf)?;
122912×
                Ok((n, c))
123015×
            });
123117×
        let (note, cp) = match checked {
123212×
            Ok(x) => x,
12335×
            Err(e) => {
12345×
                why = format!("a cosigned checkpoint served for it does not hold it ({e})");
12355×
                continue;
1236
            }
1237
        };
123812×
        let times = note.cosignatures(trusted);
123912×
        if times.is_empty() {
12405×
            why = format!(
1241
                "the checkpoint of {} entries holding it is cosigned by no witness this user trusts",
1242
                cp.size
1243
            );
12445×
            continue;
12457×
        }
124610×
        for (witness, time) in times {
124710×
            if best.as_ref().is_none_or(|b| time < b.time) {
12488×
                best = Some(Integrated {
12498×
                    time,
12508×
                    witness,
12518×
                    size: cp.size,
12528×
                });
12538×
            }
1254
        }
1255
    }
125615×
    best.ok_or(why)
125716×
}
1258
1259
#[cfg(test)]
1260
mod tests {
1261
    use super::*;
1262
1263
    /// A log in memory: its stored hashes, grown leaf by leaf.
12644×
    fn grow(leaves: u64) -> (Vec<Hash>, Vec<Hash>) {
12654×
        let mut stored: Vec<Hash> = Vec::new();
12664×
        let mut leaf_hashes = Vec::new();
126770190×
        for n in 0..leaves {
126870190×
            let h = leaf_hash(format!("entry {n}").as_bytes());
126970190×
            leaf_hashes.push(h);
127070190×
            let add = stored_hashes_for_leaf(n, h, &stored).unwrap();
127170190×
            assert_eq!(stored.len() as u64, stored_hash_index(0, n));
127270190×
            stored.extend(add);
127370190×
            assert_eq!(stored.len() as u64, stored_hash_count(n + 1));
1274
        }
12754×
        (stored, leaf_hashes)
12764×
    }
1277
1278
    /// RFC 6962's MTH, straight from its definition.
127919600×
    fn mth(leaves: &[Hash]) -> Hash {
128019600×
        match leaves.len() {
12810×
            0 => empty_root(),
12829870×
            1 => leaves[0],
12839730×
            n => {
12849730×
                let (k, _) = maxpow2(n as u64);
12859730×
                node_hash(&mth(&leaves[..k as usize]), &mth(&leaves[k as usize..]))
1286
            }
1287
        }
128819600×
    }
1289
1290
    #[test]
12911×
    fn stored_indexes_round_trip() {
12926×
        for level in 0..6 {
12931800×
            for n in 0..300 {
12941800×
                let i = stored_hash_index(level, n);
12951800×
                assert_eq!(split_stored_hash_index(i), (level, n));
1296
            }
1297
        }
12981×
    }
1299
1300
    #[test]
13011×
    fn roots_match_rfc_6962_and_every_proof_verifies() {
13021×
        let (stored, leaves) = grow(140);
1303140×
        for t in 1..=140u64 {
1304140×
            let root = tree_hash(t, &stored).unwrap();
1305140×
            assert_eq!(root, mth(&leaves[..t as usize]), "root of {t}");
13069870×
            for n in 0..t {
13079870×
                let p = prove_inclusion(t, n, &stored).unwrap();
13089870×
                check_inclusion(&p, t, &root, n, &leaves[n as usize]).unwrap();
1309
                // Another leaf, another tree, a tampered path: refused.
13109870×
                let other = (n + 1) % t;
13119870×
                if other != n {
13129869×
                    assert!(check_inclusion(&p, t, &root, n, &leaves[other as usize]).is_err());
13131×
                }
13149870×
                if let Some(first) = p.first() {
13159869×
                    let mut bad = p.clone();
13169869×
                    bad[0] = node_hash(first, first);
13179869×
                    assert!(check_inclusion(&bad, t, &root, n, &leaves[n as usize]).is_err());
13181×
                }
13199870×
                let old = n + 1;
13209870×
                let old_root = tree_hash(old, &stored).unwrap();
13219870×
                let c = prove_consistency(t, old, &stored).unwrap();
13229870×
                check_consistency(&c, t, &root, old, &old_root).unwrap();
13239870×
                if old < t {
13249730×
                    let fork = node_hash(&old_root, &old_root);
13259730×
                    assert!(
13269730×
                        check_consistency(&c, t, &root, old, &fork).is_err(),
1327
                        "a forked history"
1328
                    );
1329140×
                }
1330
            }
1331
        }
13321×
        assert_eq!(tree_hash(0, &stored).unwrap(), empty_root());
13331×
    }
1334
1335
    #[test]
13361×
    fn tiles_hold_the_stored_hashes() {
13371×
        let n = 70_000u64;
13381×
        let (stored, _) = grow(n);
13391×
        let tiles = Tile::new_tiles(0, n);
1340
        // 273 full tiles of level 0 and a partial one, one full and one
1341
        // partial at level 1, a partial at level 2.
1342277×
        assert_eq!(tiles.iter().filter(|t| t.level == 0).count(), 274);
1343277×
        assert_eq!(tiles.iter().filter(|t| t.level == 2).count(), 1);
13441×
        let datas: Vec<(Tile, Vec<u8>)> = tiles
13451×
            .iter()
1346277×
            .map(|t| (*t, t.read_data(&stored).unwrap()))
13471×
            .collect();
134820000×
        for (i, want) in stored.iter().enumerate().step_by(7) {
134920000×
            let (t, ..) = Tile::for_index(i as u64);
135020000×
            let (tile, data) = datas
135120000×
                .iter()
13522756800×
                .find(|(d, _)| d.level == t.level && d.n == t.n && d.width >= t.width)
135320000×
                .expect("the tile holding it was written");
135420000×
            assert_eq!(
135520000×
                tile.hash_at(data, i as u64).unwrap(),
1356
                *want,
1357
                "stored hash {i}"
1358
            );
1359
        }
1360
        // Growing writes only what changed.
13611×
        let grown = Tile::new_tiles(n, n + 300);
13623×
        assert!(grown.iter().all(|t| t.level > 0 || t.n >= n / 256));
13631×
    }
1364
1365
    #[test]
13661×
    fn tile_paths() {
13671×
        assert_eq!(index_path(1_234_067), "x001/x234/067");
13681×
        assert_eq!(index_path(5), "005");
13691×
        assert_eq!(index_path(1000), "x001/000");
13701×
        assert_eq!(parse_index_path("x001/x234/067"), Some(1_234_067));
13711×
        assert_eq!(parse_index_path("x001/234/067"), None);
13721×
        let t = Tile {
13731×
            level: 1,
13741×
            n: 1234,
13751×
            width: 17,
13761×
        };
13771×
        assert_eq!(t.path(), "tile/1/x001/234.p/17");
13781×
        assert_eq!(Tile::parse("1/x001/234.p/17"), Some(t));
13791×
        assert_eq!(
13801×
            Tile {
13811×
                level: 0,
13821×
                n: 3,
13831×
                width: 256
13841×
            }
13851×
            .entries_path(),
1386
            "tile/entries/003"
1387
        );
13881×
        assert_eq!(Tile::parse("0/003.p/256"), None, "a full tile has no .p");
13891×
    }
1390
1391
    #[test]
13921×
    fn entries_and_bundles_round_trip() {
13931×
        let e = Entry {
13941×
            kind: "ref".into(),
13951×
            repo: [7; 16],
13961×
            object: ObjectRef([1; 32]),
13971×
            signers: vec![ObjectRef([2; 32])],
13981×
            received: 1_792_000_000_000,
13991×
        };
14001×
        assert_eq!(Entry::decode(&e.encode()).unwrap(), e);
14011×
        let b = encode_bundle(&[e.encode(), b"x".to_vec()]).unwrap();
14021×
        assert_eq!(decode_bundle(&b).unwrap(), vec![e.encode(), b"x".to_vec()]);
14031×
        assert!(decode_bundle(&b[..b.len() - 1]).is_err());
14041×
        assert_eq!(e.leaf_hash(), leaf_hash(&e.encode()));
14051×
    }
1406
1407
    #[test]
14081×
    fn a_witness_cosigns_what_extends_what_it_saw_and_nothing_else() {
14091×
        let log_sk = SecretKey::generate();
14101×
        let log = log_sk.identity("log", "[email protected]", 1);
14111×
        let w_sk = SecretKey::generate();
14121×
        let w = w_sk.identity("witness", "[email protected]", 1);
14131×
        let origin = "hub.example/log";
14141×
        let (stored, _) = grow(40);
14156×
        let note_of = |n: u64| {
14166×
            Checkpoint {
14176×
                origin: origin.into(),
14186×
                size: n,
14196×
                root: tree_hash(n, &stored).unwrap(),
14206×
                extensions: vec![],
14216×
            }
14226×
            .sign(&log, &log_sk)
14236×
            .unwrap()
14246×
            .encode()
14256×
        };
14264×
        let ask = |old: u64, n: u64, latest: Option<(u64, Hash)>| {
14274×
            let proof = if old == 0 || old == n {
14281×
                vec![]
1429
            } else {
14303×
                prove_consistency(n, old, &stored).unwrap()
1431
            };
14324×
            let req = AddCheckpoint {
14334×
                old_size: old,
14344×
                proof,
14354×
                note: note_of(n),
14364×
            };
14374×
            let req = AddCheckpoint::parse(&req.encode()).unwrap();
14384×
            let me = WitnessKey {
14394×
                name: "w.example",
14404×
                identity: &w,
14414×
                sk: &w_sk,
14424×
            };
14434×
            witness(&req, origin, &log, latest, &me, 1_792_300_000)
14444×
        };
1445
        // First sight, then a later checkpoint with its proof.
14461×
        let Ok(Witnessed::Cosigned { checkpoint, lines }) = ask(0, 10, None) else {
14470×
            panic!()
1448
        };
14491×
        let mut note = Note::parse(&note_of(10)).unwrap();
14501×
        note.sigs.extend(lines);
14511×
        let back = Note::parse(&note.encode()).unwrap();
14521×
        assert_eq!(back.cosigned_by("w.example", &w).unwrap(), 1_792_300_000);
14531×
        assert!(
14541×
            back.cosigned_by("w.example", &log).is_err(),
1455
            "another witness's key"
1456
        );
14571×
        let latest = Some((10, checkpoint.root));
14581×
        assert!(matches!(
14591×
            ask(10, 40, latest),
1460
            Ok(Witnessed::Cosigned { .. })
1461
        ));
1462
        // The log's idea of what the witness saw is wrong: told the size.
14631×
        assert_eq!(ask(5, 40, latest), Ok(Witnessed::Conflict(10)));
1464
        // A fork: what the witness saw is not a prefix.
14651×
        let forked = Some((10, [9; 32]));
14661×
        assert!(matches!(
14671×
            ask(10, 40, forked),
1468
            Err(WitnessError::Inconsistent(_))
1469
        ));
1470
        // Signed by someone else.
14711×
        let req = AddCheckpoint {
14721×
            old_size: 0,
14731×
            proof: vec![],
14741×
            note: note_of(10),
14751×
        };
14761×
        assert!(matches!(
14771×
            witness(
14781×
                &req,
14791×
                origin,
14801×
                &w,
14811×
                None,
14821×
                &WitnessKey {
14831×
                    name: "w.example",
14841×
                    identity: &w,
14851×
                    sk: &w_sk
14861×
                },
1487
                1
1488
            ),
1489
            Err(WitnessError::Unsigned(_))
1490
        ));
14911×
    }
1492
1493
    #[test]
14941×
    fn checkpoints_are_signed_twice_and_checked() {
14951×
        let sk = SecretKey::generate();
14961×
        let hub = sk.identity("oxygenhub.io", "[email protected]", 1);
14971×
        let (stored, _) = grow(10);
14981×
        let c = Checkpoint {
14991×
            origin: "oxygenhub.io/log".into(),
15001×
            size: 10,
15011×
            root: tree_hash(10, &stored).unwrap(),
15021×
            extensions: vec![],
15031×
        };
15041×
        let note = c.sign(&hub, &sk).unwrap();
15051×
        let served = note.encode();
15061×
        assert!(served.starts_with("oxygenhub.io/log\n10\n"));
15071×
        assert_eq!(served.matches("\n\u{2014} oxygenhub.io/log ").count(), 2);
15081×
        let back = Note::parse(&served).unwrap();
15091×
        assert_eq!(
15101×
            Checkpoint::verify(&back, "oxygenhub.io/log", &hub).unwrap(),
1511
            c
1512
        );
1513
        // Another key, another origin, a changed size, a lost line: refused.
15141×
        let other_sk = SecretKey::generate();
15151×
        let other = other_sk.identity("x", "x@x", 1);
15161×
        assert!(Checkpoint::verify(&back, "oxygenhub.io/log", &other).is_err());
15171×
        assert!(Checkpoint::verify(&back, "elsewhere/log", &hub).is_err());
15181×
        let forged = Note {
15191×
            text: back.text.replace("\n10\n", "\n11\n"),
15201×
            ..back.clone()
15211×
        };
15221×
        assert!(Checkpoint::verify(&forged, "oxygenhub.io/log", &hub).is_err());
15231×
        let ed_only = Note {
15241×
            sigs: vec![back.sigs[0].clone()],
15251×
            ..back.clone()
15261×
        };
15271×
        assert!(
15281×
            matches!(Checkpoint::verify(&ed_only, "oxygenhub.io/log", &hub), Err(TlogError::BadSignature(m)) if m.contains("composite")),
1529
            "the post-quantum line is required"
1530
        );
1531
        // The Ed25519 line is a standard note signature over the text.
15321×
        let h = key_hash("oxygenhub.io/log", SIG_ED25519, &hub.ed25519_pk);
15331×
        assert_eq!(back.sigs[0].key_hash, h);
15341×
        assert!(hub
15351×
            .verify_ed25519(back.text.as_bytes(), &back.sigs[0].sig)
15361×
            .is_ok());
15371×
    }
1538
1539
    #[test]
15401×
    fn an_entry_is_integrated_when_a_trusted_witness_first_cosigned_it() {
15411×
        let log_sk = SecretKey::generate();
15421×
        let log = log_sk.identity("log", "[email protected]", 1);
15431×
        let origin = "hub.example/log";
15443×
        let witness = |name: &str| {
15453×
            let sk = SecretKey::generate();
15463×
            (name.to_string(), sk.identity(name, "[email protected]", 1), sk)
15473×
        };
15481×
        let (a, b, c) = (
15491×
            witness("a.example"),
15501×
            witness("b.example"),
15511×
            witness("c.example"),
15521×
        );
15531×
        let trusted = vec![(a.0.clone(), a.1.clone()), (b.0.clone(), b.1.clone())];
1554
        // A log of 40 real entries, so `integrated_time` hashes the leaf.
155542×
        let leaf_bytes = |n: u64| {
155642×
            Entry {
155742×
                kind: "ref".into(),
155842×
                repo: [n as u8; 16],
155942×
                object: ObjectRef([n as u8; 32]),
156042×
                signers: vec![],
156142×
                received: n,
156242×
            }
156342×
            .encode()
156442×
        };
15651×
        let mut stored: Vec<Hash> = Vec::new();
156640×
        for n in 0..40 {
156740×
            let add = stored_hashes_for_leaf(n, leaf_hash(&leaf_bytes(n)), &stored).unwrap();
156840×
            stored.extend(add);
156940×
        }
1570
        // Checkpoint `n`, cosigned by each of `by` at its time.
1571
        type W = (String, Identity, SecretKey);
15725×
        let cosigned = |n: u64, by: &[(&W, u64)]| {
15735×
            let cp = Checkpoint {
15745×
                origin: origin.into(),
15755×
                size: n,
15765×
                root: tree_hash(n, &stored).unwrap(),
15775×
                extensions: vec![],
15785×
            };
15795×
            let mut note = cp.sign(&log, &log_sk).unwrap();
15806×
            for ((name, id, sk), t) in by {
15816×
                note.sigs
15826×
                    .extend(cosign(&note.text, name, id, sk, *t).unwrap());
15836×
            }
15845×
            note.encode()
15855×
        };
158611×
        let proof = |n: u64| prove_inclusion(n, 7, &stored).unwrap();
15871×
        let entry = leaf_bytes(7);
15889×
        let time = |served: &[(String, Vec<Hash>)]| {
15899×
            let s: Vec<(&str, &[Hash])> = served
15909×
                .iter()
159110×
                .map(|(n, p)| (n.as_str(), p.as_slice()))
15929×
                .collect();
15939×
            integrated_time(&entry, 7, &s, origin, &log, &trusted)
15949×
        };
1595
1596
        // One checkpoint, two witnesses: the earlier cosignature.
15971×
        let one = cosigned(10, &[(&a, 1_792_300_050), (&b, 1_792_300_020)]);
15981×
        let got = time(&[(one.clone(), proof(10))]).unwrap();
15991×
        assert_eq!(
16001×
            (got.time, got.witness.as_str(), got.size),
1601
            (1_792_300_020, "b.example", 10)
1602
        );
16031×
        assert_eq!(got.ms(), 1_792_300_020_000);
1604
        // Witnesses that first cosigned different checkpoints: the
1605
        // earliest of all.
16061×
        let later = cosigned(20, &[(&a, 1_792_300_010)]);
16071×
        let got = time(&[(later.clone(), proof(20)), (one.clone(), proof(10))]).unwrap();
16081×
        assert_eq!(
16091×
            (got.time, got.witness.as_str()),
1610
            (1_792_300_010, "a.example")
1611
        );
1612
1613
        // A witness this user does not trust is no witness, however early.
16141×
        let stranger = cosigned(10, &[(&c, 1)]);
16151×
        let err = time(&[(stranger, proof(10))]).unwrap_err();
16161×
        assert!(err.contains("no witness this user trusts"), "{err}");
1617
        // A forged cosignature under a trusted name: c's key signed it.
16181×
        let forged = cosigned(10, &[(&(a.0.clone(), c.1.clone(), c.2.clone()), 1)]);
16191×
        assert!(time(&[(forged.clone(), proof(10))]).is_err());
1620
        // Beside a real one, it changes nothing.
16211×
        let got = time(&[(forged, proof(10)), (one.clone(), proof(10))]).unwrap();
16221×
        assert_eq!(got.time, 1_792_300_020);
1623
        // A time moved after cosigning no longer verifies.
16241×
        let mut moved = Note::parse(&one).unwrap();
16256×
        for s in moved.sigs.iter_mut().filter(|s| s.name == "b.example") {
16262×
            s.sig[..8].copy_from_slice(&1u64.to_be_bytes());
16272×
        }
16281×
        let got = time(&[(moved.encode(), proof(10))]).unwrap();
16291×
        assert_eq!(got.witness, "a.example", "b's lines no longer verify");
1630
1631
        // A checkpoint that does not hold the entry, a proof for another
1632
        // size, another log's key: no time.
16331×
        let early = cosigned(5, &[(&a, 1)]);
16341×
        assert!(time(&[(early, proof(10))]).is_err());
16351×
        assert!(time(&[(one.clone(), proof(20))]).is_err());
16361×
        let other_sk = SecretKey::generate();
16371×
        let other = other_sk.identity("x", "x@x", 1);
16381×
        let s = [(one.as_str(), proof(10))];
16391×
        let s: Vec<(&str, &[Hash])> = s.iter().map(|(n, p)| (*n, p.as_slice())).collect();
16401×
        assert!(integrated_time(&entry, 7, &s, origin, &other, &trusted).is_err());
16411×
        assert!(integrated_time(&entry, 7, &s, "elsewhere/log", &log, &trusted).is_err());
16421×
        assert!(integrated_time(&leaf_bytes(8), 7, &s, origin, &log, &trusted).is_err());
1643
        // Nothing served.
16441×
        assert!(time(&[]).unwrap_err().contains("no witness has cosigned"));
16451×
    }
1646
}