OXYGENHUB

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

770 of 831 lines run, 82 of 186 functions · run #322 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)`.
5870152×
pub fn leaf_hash(entry: &[u8]) -> Hash {
5970152×
    let mut h = blake3::Hasher::new();
6070152×
    h.update(b"oxygen-tlog-leaf\0");
6170152×
    h.update(entry);
6270152×
    *h.finalize().as_bytes()
6370152×
}
64
65
/// An interior node's hash: `BLAKE3("oxygen-tlog-node\0" || left || right)`.
66564345×
pub fn node_hash(left: &Hash, right: &Hash) -> Hash {
67564345×
    let mut h = blake3::Hasher::new();
68564345×
    h.update(b"oxygen-tlog-node\0");
69564345×
    h.update(left);
70564345×
    h.update(right);
71564345×
    *h.finalize().as_bytes()
72564345×
}
73
74
/// The root of the empty tree: the hash of nothing, as RFC 6962 has it.
752×
pub fn empty_root() -> Hash {
762×
    *blake3::hash(b"").as_bytes()
772×
}
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] {
190100037×
    fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> {
191100037×
        indexes
192100037×
            .iter()
193331441×
            .map(|&i| {
194331441×
                self.get(i as usize)
195331441×
                    .copied()
196331441×
                    .ok_or_else(|| TlogError::Read(format!("no stored hash {i}")))
197331441×
            })
198100037×
            .collect()
199100037×
    }
200
}
201
202
impl HashReader for Vec<Hash> {
203100037×
    fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>> {
204100037×
        self.as_slice().read_hashes(indexes)
205100037×
    }
206
}
207
208
/// The largest power of two below `n` (n ≥ 2), and its log.
209947865×
fn maxpow2(n: u64) -> (u64, u32) {
210947865×
    let mut l = 0;
2113628670×
    while (1u64 << (l + 1)) < n {
2122680805×
        l += 1;
2132680805×
    }
214947865×
    (1 << l, l)
215947865×
}
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 + ….
220515341×
pub fn stored_hash_index(level: u32, mut n: u64) -> u64 {
221548937×
    for _ in 0..level {
222548937×
        n = 2 * n + 1;
223548937×
    }
224515341×
    let mut i = 0;
225515341×
    let mut m = n;
2266527434×
    while m > 0 {
2276012093×
        i += m;
2286012093×
        m >>= 1;
2296012093×
    }
230515341×
    i + level as u64
231515341×
}
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.
25270150×
pub fn stored_hash_count(n: u64) -> u64 {
25370150×
    if n == 0 {
2540×
        return 0;
25570150×
    }
25670150×
    let mut count = stored_hash_index(0, n - 1) + 1;
25770150×
    let mut i = n - 1;
258140289×
    while i & 1 != 0 {
25970139×
        count += 1;
26070139×
        i >>= 1;
26170139×
    }
26270150×
    count
26370150×
}
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.
26770150×
pub fn stored_hashes_for_leaf(
26870150×
    n: u64,
26970150×
    leaf: Hash,
27070150×
    r: &(impl HashReader + ?Sized),
27170150×
) -> Result<Vec<Hash>> {
27270150×
    let m = (n + 1).trailing_zeros();
27370150×
    let indexes: Vec<u64> = (0..m)
27470150×
        .rev()
27570150×
        .map(|i| stored_hash_index(i, (n >> i) - 1))
27670150×
        .collect();
27770150×
    let old = r.read_hashes(&indexes)?;
27870150×
    let mut h = leaf;
27970150×
    let mut out = vec![h];
28070150×
    for i in 0..m as usize {
28170139×
        h = node_hash(&old[m as usize - 1 - i], &h);
28270139×
        out.push(h);
28370139×
    }
28470150×
    Ok(out)
28570150×
}
286
287141368×
fn sub_tree_index(mut lo: u64, hi: u64, need: &mut Vec<u64>) {
288332396×
    while lo < hi {
289191028×
        let (k, level) = maxpow2(hi - lo + 1);
290191028×
        debug_assert_eq!(lo & (k - 1), 0);
291191028×
        need.push(stored_hash_index(level, lo >> level));
292191028×
        lo += k;
293
    }
294141368×
}
295
296141368×
fn sub_tree_hash(mut lo: u64, hi: u64, hashes: &[Hash]) -> (Hash, &[Hash]) {
297141368×
    let mut count = 0;
298332396×
    while lo < hi {
299191028×
        let (k, _) = maxpow2(hi - lo + 1);
300191028×
        count += 1;
301191028×
        lo += k;
302191028×
    }
303141368×
    let mut h = hashes[count - 1];
304141368×
    for i in (0..count - 1).rev() {
30549660×
        h = node_hash(&hashes[i], &h);
30649660×
    }
307141368×
    (h, &hashes[count..])
308141368×
}
309
310
/// The root of the first `n` leaves.
31110012×
pub fn tree_hash(n: u64, r: &(impl HashReader + ?Sized)) -> Result<Hash> {
31210012×
    if n == 0 {
3131×
        return Ok(empty_root());
31410011×
    }
31510011×
    let mut need = Vec::new();
31610011×
    sub_tree_index(0, n, &mut need);
31710011×
    let hashes = r.read_hashes(&need)?;
31810011×
    Ok(sub_tree_hash(0, n, &hashes).0)
31910012×
}
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
40366251×
fn tree_proof_index(lo: u64, hi: u64, n: u64, need: &mut Vec<u64>) {
40466251×
    if n == hi {
4059870×
        if lo != 0 {
4068865×
            sub_tree_index(lo, hi, need);
4078865×
        }
4089870×
        return;
40956381×
    }
41056381×
    let (k, _) = maxpow2(hi - lo);
41156381×
    if n <= lo + k {
41235720×
        tree_proof_index(lo, lo + k, n, need);
41335720×
        sub_tree_index(lo + k, hi, need);
41435720×
    } else {
41520661×
        sub_tree_index(lo, lo + k, need);
41620661×
        tree_proof_index(lo + k, hi, n, need);
41720661×
    }
41866251×
}
419
42066111×
fn tree_proof(lo: u64, hi: u64, n: u64, hashes: &[Hash]) -> (Vec<Hash>, &[Hash]) {
42166111×
    if n == hi {
4229730×
        if lo == 0 {
423
            // Exactly the old tree, whose root the verifier has.
424865×
            return (Vec::new(), hashes);
4258865×
        }
4268865×
        let (th, rest) = sub_tree_hash(lo, hi, hashes);
4278865×
        return (vec![th], rest);
42856381×
    }
42956381×
    let (k, _) = maxpow2(hi - lo);
430
    let (mut p, th, rest);
43156381×
    if n <= lo + k {
43235720×
        let (pp, h1) = tree_proof(lo, lo + k, n, hashes);
43335720×
        let (t, h2) = sub_tree_hash(lo + k, hi, h1);
43435720×
        (p, th, rest) = (pp, t, h2);
43535720×
    } else {
43620661×
        let (t, h1) = sub_tree_hash(lo, lo + k, hashes);
43720661×
        let (pp, h2) = tree_proof(lo + k, hi, n, h1);
43820661×
        (p, th, rest) = (pp, t, h2);
43920661×
    }
44056381×
    p.push(th);
44156381×
    (p, rest)
44266111×
}
443
444
/// The consistency proof that the tree of `n` leaves is a prefix of the
445
/// tree of `t`.
4469870×
pub fn prove_consistency(t: u64, n: u64, r: &(impl HashReader + ?Sized)) -> Result<Vec<Hash>> {
4479870×
    if n < 1 || n > t {
4480×
        return Err(TlogError::BadIndex(format!(
4490×
            "no consistency proof from {n} to {t}"
4500×
        )));
4519870×
    }
4529870×
    let mut need = Vec::new();
4539870×
    tree_proof_index(0, t, n, &mut need);
4549870×
    if need.is_empty() {
455140×
        return Ok(Vec::new());
4569730×
    }
4579730×
    let hashes = r.read_hashes(&need)?;
4589730×
    Ok(tree_proof(0, t, n, &hashes).0)
4599870×
}
460
461132362×
fn run_consistency(p: &[Hash], lo: u64, hi: u64, n: u64, old: Hash) -> Result<(Hash, Hash)> {
462132362×
    if n == hi {
46319600×
        if lo == 0 {
4641870×
            return if p.is_empty() {
4651870×
                Ok((old, old))
466
            } else {
4670×
                Err(TlogError::ProofFailed)
468
            };
46917730×
        }
47017730×
        return match p {
47117730×
            [h] => Ok((*h, *h)),
4720×
            _ => Err(TlogError::ProofFailed),
473
        };
474112762×
    }
475112762×
    let Some((last, rest)) = p.split_last() else {
4760×
        return Err(TlogError::ProofFailed);
477
    };
478112762×
    let (k, _) = maxpow2(hi - lo);
479112762×
    if n <= lo + k {
48071440×
        let (oh, th) = run_consistency(rest, lo, lo + k, n, old)?;
48171440×
        Ok((oh, node_hash(&th, last)))
482
    } else {
48341322×
        let (oh, th) = run_consistency(rest, lo + k, hi, n, old)?;
48441322×
        Ok((node_hash(last, &oh), node_hash(last, &th)))
485
    }
486132362×
}
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`.
49019600×
pub fn check_consistency(
49119600×
    proof: &[Hash],
49219600×
    t: u64,
49319600×
    root: &Hash,
49419600×
    n: u64,
49519600×
    old_root: &Hash,
49619600×
) -> Result<()> {
49719600×
    if n < 1 || n > t {
4980×
        return Err(TlogError::ProofFailed);
49919600×
    }
50019600×
    let (oh, th) = run_consistency(proof, 0, t, n, *old_root)?;
50119600×
    if oh == *old_root && th == *root {
5029870×
        Ok(())
503
    } else {
5049730×
        Err(TlogError::ProofFailed)
505
    }
50619600×
}
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
72310×
fn key_hash(name: &str, sig_type: u8, key: &[u8]) -> [u8; 4] {
72410×
    let mut h = Sha256::new();
72510×
    h.update(name.as_bytes());
72610×
    h.update(b"\n");
72710×
    h.update([sig_type]);
72810×
    h.update(key);
72910×
    let d = h.finalize();
73010×
    [d[0], d[1], d[2], d[3]]
73110×
}
732
733
/// What a composite signature of a note signs (domain `checkpoint`).
7342×
fn note_target(text: &str) -> ObjectRef {
7352×
    let mut h = blake3::Hasher::new();
7362×
    h.update(b"oxygen-tlog-note\0");
7372×
    h.update(text.as_bytes());
7382×
    ObjectRef(*h.finalize().as_bytes())
7392×
}
740
7413×
fn valid_name(name: &str) -> bool {
74248×
    !name.is_empty() && !name.contains(|c: char| c.is_whitespace() || c == '+')
7433×
}
744
745
impl Note {
746
    /// The note as served: the text, a blank line, the signature lines.
7471×
    pub fn encode(&self) -> String {
7481×
        let mut out = self.text.clone();
7491×
        out.push('\n');
7502×
        for s in &self.sigs {
7512×
            let mut b = s.key_hash.to_vec();
7522×
            b.extend_from_slice(&s.sig);
7532×
            out.push_str(&format!(
7542×
                "\u{2014} {} {}\n",
7552×
                s.name,
7562×
                crate::dsse::base64(&b)
7572×
            ));
7582×
        }
7591×
        out
7601×
    }
761
7621×
    pub fn parse(s: &str) -> Result<Note> {
7631×
        let bad = |w: &str| TlogError::BadNote(w.to_string());
7641×
        let split = s
7651×
            .rfind("\n\n")
7661×
            .ok_or_else(|| bad("no blank line before the signatures"))?;
7671×
        let text = &s[..split + 1];
76865×
        if text.chars().any(|c| c.is_control() && c != '\n') {
7690×
            return Err(bad("control characters in the text"));
7701×
        }
7711×
        let mut sigs = Vec::new();
7722×
        for line in s[split + 2..].lines() {
7732×
            let rest = line
7742×
                .strip_prefix("\u{2014} ")
7752×
                .ok_or_else(|| bad("a signature line without its dash"))?;
7762×
            let (name, b64) = rest
7772×
                .split_once(' ')
7782×
                .ok_or_else(|| bad("a signature line without a key"))?;
7792×
            let b =
7802×
                crate::dsse::base64_decode(b64).ok_or_else(|| bad("a signature is not base64"))?;
7812×
            if b.len() < 5 || !valid_name(name) {
7820×
                return Err(bad("a malformed signature line"));
7832×
            }
7842×
            sigs.push(NoteSignature {
7852×
                name: name.to_string(),
7862×
                key_hash: [b[0], b[1], b[2], b[3]],
7872×
                sig: b[4..].to_vec(),
7882×
            });
789
        }
7901×
        if sigs.is_empty() {
7910×
            return Err(bad("no signatures"));
7921×
        }
7931×
        Ok(Note {
7941×
            text: text.to_string(),
7951×
            sigs,
7961×
        })
7971×
    }
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.
8011×
    pub fn sign(text: &str, name: &str, identity: &Identity, sk: &SecretKey) -> Result<Note> {
8021×
        if !valid_name(name) {
8030×
            return Err(TlogError::BadNote(format!(
8040×
                "{name:?} cannot name a note's key"
8050×
            )));
8061×
        }
8071×
        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×
            ));
8111×
        }
8121×
        let ed = NoteSignature {
8131×
            name: name.to_string(),
8141×
            key_hash: key_hash(name, SIG_ED25519, &identity.ed25519_pk),
8151×
            sig: sk.sign_ed25519(text.as_bytes()).to_vec(),
8161×
        };
8171×
        let composite = NoteSignature {
8181×
            name: name.to_string(),
8191×
            key_hash: key_hash(name, SIG_COMPOSITE, &identity.encode()),
8201×
            sig: cbor::encode(
8211×
                &sk.sign(Domain::Checkpoint, &identity.hash(), &note_target(text))
8221×
                    .to_value(),
8231×
            ),
8241×
        };
8251×
        Ok(Note {
8261×
            text: text.to_string(),
8271×
            sigs: vec![ed, composite],
8281×
        })
8291×
    }
830
831
    /// Both of `name`'s signatures by `identity` verify (a note signed
832
    /// with Ed25519 only is not enough).
8335×
    pub fn verify(&self, name: &str, identity: &Identity) -> Result<()> {
8347×
        let find = |t: u8, key: &[u8]| {
8357×
            let h = key_hash(name, t, key);
83610×
            self.sigs.iter().find(|s| s.name == name && s.key_hash == h)
8377×
        };
8385×
        let ed = find(SIG_ED25519, &identity.ed25519_pk)
8395×
            .ok_or_else(|| TlogError::BadSignature(format!("no Ed25519 signature by {name}")))?;
8403×
        identity
8413×
            .verify_ed25519(self.text.as_bytes(), &ed.sig)
8423×
            .map_err(|_| {
8431×
                TlogError::BadSignature(format!("{name}'s Ed25519 signature does not verify"))
8441×
            })?;
8452×
        let c = find(SIG_COMPOSITE, &identity.encode())
8462×
            .ok_or_else(|| TlogError::BadSignature(format!("no composite signature by {name}")))?;
8471×
        let sig = cbor::decode(&c.sig)
8481×
            .map_err(|e| e.to_string())
8491×
            .and_then(|v| Signature::from_value(&v).map_err(|e| e.to_string()))
8501×
            .map_err(|e| TlogError::BadSignature(format!("{name}'s composite signature: {e}")))?;
8511×
        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×
        })
8545×
    }
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 {
8691×
    pub fn text(&self) -> String {
8701×
        let mut t = format!(
871
            "{}\n{}\n{}\n",
872
            self.origin,
873
            self.size,
8741×
            crate::dsse::base64(&self.root)
875
        );
8761×
        for e in &self.extensions {
8770×
            t.push_str(e);
8780×
            t.push('\n');
8790×
        }
8801×
        t
8811×
    }
882
8831×
    pub fn parse(text: &str) -> Result<Checkpoint> {
8841×
        let bad = |w: &str| TlogError::BadNote(format!("checkpoint: {w}"));
8851×
        if !text.ends_with('\n') {
8860×
            return Err(bad("no final newline"));
8871×
        }
8881×
        let mut lines = text[..text.len() - 1].split('\n');
8891×
        let origin = lines
8901×
            .next()
8911×
            .filter(|o| !o.is_empty())
8921×
            .ok_or_else(|| bad("no origin"))?;
8931×
        let size = lines
8941×
            .next()
8951×
            .and_then(|s| {
8961×
                (s == "0" || !s.starts_with('0'))
8971×
                    .then(|| s.parse().ok())
8981×
                    .flatten()
8991×
            })
9001×
            .ok_or_else(|| bad("no tree size"))?;
9011×
        let root = lines
9021×
            .next()
9031×
            .and_then(crate::dsse::base64_decode)
9041×
            .and_then(|b| <Hash>::try_from(b).ok())
9051×
            .ok_or_else(|| bad("no root hash"))?;
9061×
        Ok(Checkpoint {
9071×
            origin: origin.to_string(),
9081×
            size,
9091×
            root,
9101×
            extensions: lines.map(String::from).collect(),
9111×
        })
9121×
    }
913
914
    /// Signed by the log's key, named by its origin.
9151×
    pub fn sign(&self, identity: &Identity, sk: &SecretKey) -> Result<Note> {
9161×
        Note::sign(&self.text(), &self.origin, identity, sk)
9171×
    }
918
919
    /// The checkpoint a note carries, once the log's key (`identity`)
920
    /// signed it for `origin`.
9215×
    pub fn verify(note: &Note, origin: &str, identity: &Identity) -> Result<Checkpoint> {
9225×
        note.verify(origin, identity)?;
9231×
        let c = Checkpoint::parse(&note.text)?;
9241×
        if c.origin != origin {
9250×
            return Err(TlogError::BadNote(format!(
9260×
                "checkpoint of {}, not of {origin}",
9270×
                c.origin
9280×
            )));
9291×
        }
9301×
        Ok(c)
9315×
    }
932
}
933
934
#[cfg(test)]
935
mod tests {
936
    use super::*;
937
938
    /// A log in memory: its stored hashes, grown leaf by leaf.
9393×
    fn grow(leaves: u64) -> (Vec<Hash>, Vec<Hash>) {
9403×
        let mut stored: Vec<Hash> = Vec::new();
9413×
        let mut leaf_hashes = Vec::new();
94270150×
        for n in 0..leaves {
94370150×
            let h = leaf_hash(format!("entry {n}").as_bytes());
94470150×
            leaf_hashes.push(h);
94570150×
            let add = stored_hashes_for_leaf(n, h, &stored).unwrap();
94670150×
            assert_eq!(stored.len() as u64, stored_hash_index(0, n));
94770150×
            stored.extend(add);
94870150×
            assert_eq!(stored.len() as u64, stored_hash_count(n + 1));
949
        }
9503×
        (stored, leaf_hashes)
9513×
    }
952
953
    /// RFC 6962's MTH, straight from its definition.
95419600×
    fn mth(leaves: &[Hash]) -> Hash {
95519600×
        match leaves.len() {
9560×
            0 => empty_root(),
9579870×
            1 => leaves[0],
9589730×
            n => {
9599730×
                let (k, _) = maxpow2(n as u64);
9609730×
                node_hash(&mth(&leaves[..k as usize]), &mth(&leaves[k as usize..]))
961
            }
962
        }
96319600×
    }
964
965
    #[test]
9661×
    fn stored_indexes_round_trip() {
9676×
        for level in 0..6 {
9681800×
            for n in 0..300 {
9691800×
                let i = stored_hash_index(level, n);
9701800×
                assert_eq!(split_stored_hash_index(i), (level, n));
971
            }
972
        }
9731×
    }
974
975
    #[test]
9761×
    fn roots_match_rfc_6962_and_every_proof_verifies() {
9771×
        let (stored, leaves) = grow(140);
978140×
        for t in 1..=140u64 {
979140×
            let root = tree_hash(t, &stored).unwrap();
980140×
            assert_eq!(root, mth(&leaves[..t as usize]), "root of {t}");
9819870×
            for n in 0..t {
9829870×
                let p = prove_inclusion(t, n, &stored).unwrap();
9839870×
                check_inclusion(&p, t, &root, n, &leaves[n as usize]).unwrap();
984
                // Another leaf, another tree, a tampered path: refused.
9859870×
                let other = (n + 1) % t;
9869870×
                if other != n {
9879869×
                    assert!(check_inclusion(&p, t, &root, n, &leaves[other as usize]).is_err());
9881×
                }
9899870×
                if let Some(first) = p.first() {
9909869×
                    let mut bad = p.clone();
9919869×
                    bad[0] = node_hash(first, first);
9929869×
                    assert!(check_inclusion(&bad, t, &root, n, &leaves[n as usize]).is_err());
9931×
                }
9949870×
                let old = n + 1;
9959870×
                let old_root = tree_hash(old, &stored).unwrap();
9969870×
                let c = prove_consistency(t, old, &stored).unwrap();
9979870×
                check_consistency(&c, t, &root, old, &old_root).unwrap();
9989870×
                if old < t {
9999730×
                    let fork = node_hash(&old_root, &old_root);
10009730×
                    assert!(
10019730×
                        check_consistency(&c, t, &root, old, &fork).is_err(),
1002
                        "a forked history"
1003
                    );
1004140×
                }
1005
            }
1006
        }
10071×
        assert_eq!(tree_hash(0, &stored).unwrap(), empty_root());
10081×
    }
1009
1010
    #[test]
10111×
    fn tiles_hold_the_stored_hashes() {
10121×
        let n = 70_000u64;
10131×
        let (stored, _) = grow(n);
10141×
        let tiles = Tile::new_tiles(0, n);
1015
        // 273 full tiles of level 0 and a partial one, one full and one
1016
        // partial at level 1, a partial at level 2.
1017277×
        assert_eq!(tiles.iter().filter(|t| t.level == 0).count(), 274);
1018277×
        assert_eq!(tiles.iter().filter(|t| t.level == 2).count(), 1);
10191×
        let datas: Vec<(Tile, Vec<u8>)> = tiles
10201×
            .iter()
1021277×
            .map(|t| (*t, t.read_data(&stored).unwrap()))
10221×
            .collect();
102320000×
        for (i, want) in stored.iter().enumerate().step_by(7) {
102420000×
            let (t, ..) = Tile::for_index(i as u64);
102520000×
            let (tile, data) = datas
102620000×
                .iter()
10272756800×
                .find(|(d, _)| d.level == t.level && d.n == t.n && d.width >= t.width)
102820000×
                .expect("the tile holding it was written");
102920000×
            assert_eq!(
103020000×
                tile.hash_at(data, i as u64).unwrap(),
1031
                *want,
1032
                "stored hash {i}"
1033
            );
1034
        }
1035
        // Growing writes only what changed.
10361×
        let grown = Tile::new_tiles(n, n + 300);
10373×
        assert!(grown.iter().all(|t| t.level > 0 || t.n >= n / 256));
10381×
    }
1039
1040
    #[test]
10411×
    fn tile_paths() {
10421×
        assert_eq!(index_path(1_234_067), "x001/x234/067");
10431×
        assert_eq!(index_path(5), "005");
10441×
        assert_eq!(index_path(1000), "x001/000");
10451×
        assert_eq!(parse_index_path("x001/x234/067"), Some(1_234_067));
10461×
        assert_eq!(parse_index_path("x001/234/067"), None);
10471×
        let t = Tile {
10481×
            level: 1,
10491×
            n: 1234,
10501×
            width: 17,
10511×
        };
10521×
        assert_eq!(t.path(), "tile/1/x001/234.p/17");
10531×
        assert_eq!(Tile::parse("1/x001/234.p/17"), Some(t));
10541×
        assert_eq!(
10551×
            Tile {
10561×
                level: 0,
10571×
                n: 3,
10581×
                width: 256
10591×
            }
10601×
            .entries_path(),
1061
            "tile/entries/003"
1062
        );
10631×
        assert_eq!(Tile::parse("0/003.p/256"), None, "a full tile has no .p");
10641×
    }
1065
1066
    #[test]
10671×
    fn entries_and_bundles_round_trip() {
10681×
        let e = Entry {
10691×
            kind: "ref".into(),
10701×
            repo: [7; 16],
10711×
            object: ObjectRef([1; 32]),
10721×
            signers: vec![ObjectRef([2; 32])],
10731×
            received: 1_792_000_000_000,
10741×
        };
10751×
        assert_eq!(Entry::decode(&e.encode()).unwrap(), e);
10761×
        let b = encode_bundle(&[e.encode(), b"x".to_vec()]).unwrap();
10771×
        assert_eq!(decode_bundle(&b).unwrap(), vec![e.encode(), b"x".to_vec()]);
10781×
        assert!(decode_bundle(&b[..b.len() - 1]).is_err());
10791×
        assert_eq!(e.leaf_hash(), leaf_hash(&e.encode()));
10801×
    }
1081
1082
    #[test]
10831×
    fn checkpoints_are_signed_twice_and_checked() {
10841×
        let sk = SecretKey::generate();
10851×
        let hub = sk.identity("oxygenhub.io", "[email protected]", 1);
10861×
        let (stored, _) = grow(10);
10871×
        let c = Checkpoint {
10881×
            origin: "oxygenhub.io/log".into(),
10891×
            size: 10,
10901×
            root: tree_hash(10, &stored).unwrap(),
10911×
            extensions: vec![],
10921×
        };
10931×
        let note = c.sign(&hub, &sk).unwrap();
10941×
        let served = note.encode();
10951×
        assert!(served.starts_with("oxygenhub.io/log\n10\n"));
10961×
        assert_eq!(served.matches("\n\u{2014} oxygenhub.io/log ").count(), 2);
10971×
        let back = Note::parse(&served).unwrap();
10981×
        assert_eq!(
10991×
            Checkpoint::verify(&back, "oxygenhub.io/log", &hub).unwrap(),
1100
            c
1101
        );
1102
        // Another key, another origin, a changed size, a lost line: refused.
11031×
        let other_sk = SecretKey::generate();
11041×
        let other = other_sk.identity("x", "x@x", 1);
11051×
        assert!(Checkpoint::verify(&back, "oxygenhub.io/log", &other).is_err());
11061×
        assert!(Checkpoint::verify(&back, "elsewhere/log", &hub).is_err());
11071×
        let forged = Note {
11081×
            text: back.text.replace("\n10\n", "\n11\n"),
11091×
            ..back.clone()
11101×
        };
11111×
        assert!(Checkpoint::verify(&forged, "oxygenhub.io/log", &hub).is_err());
11121×
        let ed_only = Note {
11131×
            sigs: vec![back.sigs[0].clone()],
11141×
            ..back.clone()
11151×
        };
11161×
        assert!(
11171×
            matches!(Checkpoint::verify(&ed_only, "oxygenhub.io/log", &hub), Err(TlogError::BadSignature(m)) if m.contains("composite")),
1118
            "the post-quantum line is required"
1119
        );
1120
        // The Ed25519 line is a standard note signature over the text.
11211×
        let h = key_hash("oxygenhub.io/log", SIG_ED25519, &hub.ed25519_pk);
11221×
        assert_eq!(back.sigs[0].key_hash, h);
11231×
        assert!(hub
11241×
            .verify_ed25519(back.text.as_bytes(), &back.sigs[0].sig)
11251×
            .is_ok());
11261×
    }
1127
}