OXYGENHUB

Coverage / crates/oxy-core/src/repo/transparency.rs 57.9%

246 of 425 lines run, 29 of 74 functions · run #411 on main · run missed

1
//! Checking a host's transparency log on fetch (spec §14.8, oxygenhub#238).
2
//!
3
//! A host that keeps a log sends its latest checkpoint with the refs, and
4
//! for each bookmark's attestation its entry and an inclusion proof. This
5
//! user remembers, per hub, the log's key and the last checkpoint seen
6
//! (`<oxy home>/tlog/<hub>`): the key on first sight, then never another.
7
//! Each new checkpoint must extend the last one, which oxy proves from the
8
//! hub's public tiles (C2SP tlog-tiles); one that does not means the hub
9
//! showed two histories, and the fetch fails. An attestation that is not
10
//! in the log yet, or whose proof does not verify, is only reported while
11
//! the log is new (oxygenhub docs/transparency.md, decision 3).
12
//!
13
//! The witnesses that cosign a hub's checkpoints are pinned the same way
14
//! (`<oxy home>/tlog/<hub>.witnesses`), and give each bookmark's
15
//! attestation an *integrated time* (oxygenhub#240): the earliest time a
16
//! trusted witness cosigned a checkpoint holding it. A fetch checks
17
//! delegations and certificates at that time when it has one.
18
19
use super::*;
20
use crate::credentials::hub_of;
21
use crate::protocol::http_get;
22
use crate::tlog::{self, Checkpoint, Entry, Hash, HashReader, Integrated, Note, Tile, TlogError};
23
use std::cell::RefCell;
24
25
/// Tiles fetched, by level and number, with the width fetched.
26
type TileCache = RefCell<HashMap<(u32, u64), (Tile, Vec<u8>)>>;
27
28
/// A hub's log as its tiles serve it, for a tree of `size` entries.
29
struct Tiles<'a> {
30
    hub: &'a str,
31
    size: u64,
32
    options: &'a RemoteOptions,
33
    cache: TileCache,
34
}
35
36
impl HashReader for Tiles<'_> {
370×
    fn read_hashes(&self, indexes: &[u64]) -> std::result::Result<Vec<Hash>, TlogError> {
380×
        let mut out = Vec::with_capacity(indexes.len());
390×
        for &i in indexes {
400×
            let (t, ..) = Tile::for_index(i);
410×
            let key = (t.level, t.n);
420×
            if !self.cache.borrow().contains_key(&key) {
430×
                let held =
440×
                    (self.size >> (tlog::TILE_HEIGHT * t.level)) - (t.n << tlog::TILE_HEIGHT);
450×
                let tile = Tile {
460×
                    width: held.min(tlog::TILE_WIDTH) as u32,
470×
                    ..t
480×
                };
490×
                let data = http_get(&format!("{}/log/{}", self.hub, tile.path()), self.options)
500×
                    .map_err(|e| TlogError::Read(e.to_string()))?;
510×
                self.cache.borrow_mut().insert(key, (tile, data));
520×
            }
530×
            let cache = self.cache.borrow();
540×
            let (tile, data) = &cache[&key];
550×
            out.push(tile.hash_at(data, i)?);
56
        }
570×
        Ok(out)
580×
    }
59
}
60
61
/// What this user last saw of a hub's log.
62
struct Seen {
63
    identity: Identity,
64
    checkpoint: Checkpoint,
65
}
66
6725×
fn file_name(hub: &str) -> String {
6825×
    hub.chars()
69475×
        .map(|c| {
70475×
            if c.is_ascii_alphanumeric() || c == '.' || c == '-' {
71400×
                c
72
            } else {
7375×
                '_'
74
            }
75475×
        })
7625×
        .collect()
7725×
}
78
79
impl Repo {
8017×
    fn tlog_file(&self, hub: &str) -> PathBuf {
8117×
        self.user_config
8217×
            .parent()
8317×
            .map(Path::to_path_buf)
8417×
            .unwrap_or_else(default_home)
8517×
            .join("tlog")
8617×
            .join(file_name(hub))
8717×
    }
88
894×
    fn seen_log(&self, hub: &str) -> Option<Seen> {
904×
        let text = fs::read_to_string(self.tlog_file(hub)).ok()?;
914×
        let (key, note) = text.split_once('\n')?;
924×
        let identity = Identity::decode(&crate::dsse::base64_decode(key)?).ok()?;
934×
        let note = Note::parse(note).ok()?;
94
        Some(Seen {
954×
            checkpoint: Checkpoint::parse(&note.text).ok()?,
964×
            identity,
97
        })
984×
    }
99
1005×
    pub(crate) fn remember_log(&self, hub: &str, identity: &Identity, note: &Note) -> Result<()> {
1015×
        let path = self.tlog_file(hub);
1025×
        if let Some(dir) = path.parent() {
1035×
            fs::create_dir_all(dir)?;
1040×
        }
1055×
        let text = format!(
106
            "{}\n{}",
1075×
            crate::dsse::base64(&identity.encode()),
1085×
            note.encode()
109
        );
1105×
        crate::durable::write_atomic(&path, text.as_bytes())?;
1115×
        Ok(())
1125×
    }
113
114
    /// Take a checkpoint note `text` from `hub` as the newest seen, once it
115
    /// verifies with the hub's pinned log key and extends the last one
116
    /// seen. `Err` when the hub showed two histories; `Ok(Err(why))` when
117
    /// it could not be checked (the last one seen is kept).
1184×
    pub(crate) fn accept_checkpoint(
1194×
        &self,
1204×
        hub: &str,
1214×
        text: &str,
1224×
        options: &RemoteOptions,
1234×
    ) -> Result<std::result::Result<Accepted, String>> {
1244×
        let unchecked = |why: String| Ok(Err(why));
1254×
        let note = match Note::parse(text) {
1264×
            Ok(n) => n,
1270×
            Err(e) => return unchecked(e.to_string()),
128
        };
1294×
        let cp = match Checkpoint::parse(&note.text) {
1304×
            Ok(c) => c,
1310×
            Err(e) => return unchecked(e.to_string()),
132
        };
1334×
        let fetch_key = || {
1340×
            http_get(&format!("{hub}/.well-known/oxyhub-key"), options)
1350×
                .and_then(|b| Ok(Identity::decode(&b)?))
1360×
        };
1374×
        let mut notes = Vec::new();
1384×
        let mut seen = self.seen_log(hub);
1394×
        let mut identity = match &seen {
1404×
            Some(s) => s.identity.clone(),
1410×
            None => match fetch_key() {
1420×
                Ok(i) => i,
1430×
                Err(e) => return unchecked(format!("its key could not be fetched ({e})")),
144
            },
145
        };
1464×
        if let Err(e) = note.verify(&cp.origin, &identity) {
147
            // Signed by another key than the one pinned: a hub given a new
148
            // key, or one starting another log. Not proof of a fork, so a
149
            // loud note while logs are new (decision 3), and the new key is
150
            // pinned; a fork under one key is what fails.
1510×
            let Some(old) = &seen else {
1520×
                return unchecked(e.to_string());
153
            };
1540×
            match fetch_key() {
1550×
                Ok(new) if note.verify(&cp.origin, &new).is_ok() => {
1560×
                    notes.push(format!(
1570×
                        "{hub}'s log is signed by another key than before ({} now, {} then): if its operator did not announce a new key, it may be showing others a different log",
1580×
                        new.hash().short(12),
1590×
                        old.identity.hash().short(12)
1600×
                    ));
1610×
                    identity = new;
1620×
                    seen = None;
1630×
                }
1640×
                _ => return unchecked(format!("its checkpoint does not verify ({e})")),
165
            }
1664×
        }
1674×
        let two = |why: String| {
1680×
            fail(format!(
169
                "{hub} showed two histories: {why}. Do not trust what it serves; tell its operator. \
170
                 (What this user saw of its log is in {}.)",
1710×
                self.tlog_file(hub).display()
172
            ))
1730×
        };
1744×
        if let Some(s) = &seen {
1754×
            let old = &s.checkpoint;
1764×
            if old.origin != cp.origin {
1770×
                // A hub moved to another name keeps its key: a new log as
1780×
                // far as this user can tell, said so.
1790×
                notes.push(format!(
1800×
                    "{hub}'s log is now {}, not {}: checking it afresh",
1810×
                    cp.origin, old.origin
1820×
                ));
1834×
            } else if cp.size < old.size {
1840×
                return two(format!(
1850×
                    "its log has {} entries, fewer than the {} it showed before",
1860×
                    cp.size, old.size
1870×
                ));
1884×
            } else if cp.size == old.size && cp.root != old.root {
1890×
                return two(format!("two different logs of {} entries", cp.size));
1904×
            } else if cp.size > old.size && old.size > 0 {
1910×
                let tiles = Tiles {
1920×
                    hub,
1930×
                    size: cp.size,
1940×
                    options,
1950×
                    cache: RefCell::new(HashMap::new()),
1960×
                };
1970×
                match tlog::prove_consistency(cp.size, old.size, &tiles).and_then(|p| {
1980×
                    tlog::check_consistency(&p, cp.size, &cp.root, old.size, &old.root)
1990×
                }) {
2000×
                    Ok(()) => {}
201
                    Err(TlogError::ProofFailed) | Err(TlogError::BadIndex(_)) => {
2020×
                        return two(format!(
2030×
                            "its log of {} entries does not extend the one of {} it showed before",
2040×
                            cp.size, old.size
2050×
                        ))
206
                    }
207
                    // Keep the older checkpoint: the next fetch tries again.
2080×
                    Err(e) => return unchecked(format!("its tiles could not be read ({e})")),
209
                }
2104×
            }
2110×
        }
2124×
        self.remember_log(hub, &identity, &note)?;
2134×
        Ok(Ok(Accepted {
2144×
            checkpoint: cp,
2154×
            identity,
2164×
            note,
2174×
            notes,
2184×
        }))
2194×
    }
220
2218×
    fn witnesses_file(&self, hub: &str) -> PathBuf {
2228×
        let mut p = self.tlog_file(hub);
2238×
        p.set_file_name(format!("{}.witnesses", file_name(hub)));
2248×
        p
2258×
    }
226
227
    /// The witnesses this user pinned for `hub`: each one's name in
228
    /// cosignatures and its identity.
2294×
    pub(crate) fn pinned_witnesses(&self, hub: &str) -> Vec<(String, Identity)> {
2304×
        let Ok(text) = fs::read_to_string(self.witnesses_file(hub)) else {
2311×
            return vec![];
232
        };
2333×
        text.lines()
2343×
            .filter_map(|l| {
2353×
                let (name, key) = l.split_once(' ')?;
2363×
                let id = Identity::decode(&crate::dsse::base64_decode(key)?).ok()?;
2373×
                Some((name.to_string(), id))
2383×
            })
2393×
            .collect()
2404×
    }
241
242
    /// Pin the witnesses `hub` lists (name and identity bytes), as its log
243
    /// key is pinned: on first sight silently, later with a note for a
244
    /// witness that is new or has another key than before. A witness the
245
    /// hub no longer lists stays pinned. Returns every pinned witness.
2464×
    pub(crate) fn pin_witnesses(
2474×
        &self,
2484×
        hub: &str,
2494×
        listed: &[(String, Vec<u8>)],
2504×
        notes: &mut Vec<String>,
2514×
    ) -> Result<Vec<(String, Identity)>> {
2524×
        let path = self.witnesses_file(hub);
2534×
        let first = !path.exists();
2544×
        let mut pinned = self.pinned_witnesses(hub);
2554×
        let mut changed = false;
2564×
        for (name, bytes) in listed {
2573×
            if name.is_empty() || name.contains(char::is_whitespace) {
2580×
                notes.push(format!(
259
                    "{hub} lists a witness named {name:?}, which cannot name one"
260
                ));
2610×
                continue;
2623×
            }
2633×
            let Ok(id) = Identity::decode(bytes) else {
2640×
                notes.push(format!(
265
                    "{hub} lists witness {name} with a key that does not decode"
266
                ));
2670×
                continue;
268
            };
2693×
            match pinned.iter_mut().find(|(n, _)| n == name) {
2702×
                Some((_, old)) if old.hash() == id.hash() => {}
2712×
                Some((_, old)) => {
2722×
                    notes.push(format!(
2732×
                        "{hub}'s witness {name} has another key than before ({} now, {} then): if its operator did not announce one, {hub} may be naming a witness of its own",
2742×
                        id.hash().short(12),
2752×
                        old.hash().short(12)
2762×
                    ));
2772×
                    *old = id;
2782×
                    changed = true;
2792×
                }
280
                None => {
2811×
                    if !first {
2820×
                        notes.push(format!(
2830×
                            "{hub} is now cosigned by witness {name} (key {}), trusted from now on",
2840×
                            id.hash().short(12)
2850×
                        ));
2861×
                    }
2871×
                    pinned.push((name.clone(), id));
2881×
                    changed = true;
289
                }
290
            }
291
        }
2924×
        if changed {
2933×
            if let Some(dir) = path.parent() {
2943×
                fs::create_dir_all(dir)?;
2950×
            }
2963×
            let text: String = pinned
2973×
                .iter()
2983×
                .map(|(n, id)| format!("{n} {}\n", crate::dsse::base64(&id.encode())))
2993×
                .collect();
3003×
            crate::durable::write_atomic(&path, text.as_bytes())?;
3011×
        }
3024×
        Ok(pinned)
3034×
    }
304
305
    /// Check what a refs answer from `url` says about its host's log, and
306
    /// read each bookmark attestation's integrated time. Fails only when
307
    /// the host showed two histories under one key; everything else is a
308
    /// note.
309419×
    pub(crate) fn check_log(
310419×
        &self,
311419×
        url: &str,
312419×
        ca: Option<&Path>,
313419×
        resp: &RefsResponse,
314419×
    ) -> Result<LogCheck> {
315419×
        let mut out = LogCheck::default();
316419×
        let (Some(text), Some(hub)) = (&resp.log, hub_of(url)) else {
317415×
            return Ok(out);
318
        };
3194×
        out.kept = true;
3204×
        let options = RemoteOptions {
3214×
            ca: ca.map(Path::to_path_buf),
3224×
            ..RemoteOptions::default()
3234×
        };
3244×
        let accepted = match self.accept_checkpoint(&hub, text, &options)? {
3254×
            Ok(c) => c,
3260×
            Err(why) => {
3270×
                out.notes
3280×
                    .push(format!("{hub}'s transparency log was not checked: {why}"));
3290×
                out.unchecked = Some(why);
3300×
                return Ok(out);
331
            }
332
        };
3334×
        let cp = &accepted.checkpoint;
3344×
        out.notes.extend(accepted.notes.iter().cloned());
3354×
        let witnesses = self.pin_witnesses(&hub, &resp.witnesses, &mut out.notes)?;
3364×
        let mut missing = 0;
337
        // Each attestation's place in the log, and its integrated time.
3384×
        let mut place = |name: &str,
339
                         kind: &str,
340
                         a: &SignedAttest,
341
                         logged: Option<&crate::protocol::Logged>,
342
                         integration: &[crate::protocol::Integration]|
3434×
         -> std::result::Result<Integrated, String> {
3444×
            let Some(l) = logged else {
3450×
                missing += 1;
3460×
                return Err("it is not in the log yet".into());
347
            };
3484×
            let fits = Entry::decode(&l.entry).is_ok_and(|e| {
3494×
                e.kind == kind && e.object == a.attest.hash() && e.repo == a.attest.repo
3504×
            });
3514×
            let included = fits
3524×
                && tlog::check_inclusion(
3534×
                    &l.proof,
3544×
                    cp.size,
3554×
                    &cp.root,
3564×
                    l.index,
3574×
                    &tlog::leaf_hash(&l.entry),
3584×
                )
3594×
                .is_ok();
3604×
            if !included {
3610×
                out.notes.push(format!(
3620×
                    "{hub}'s log entry {} for {name} does not prove its attestation is in the log",
3630×
                    l.index
3640×
                ));
3654×
            }
3664×
            if !fits {
3670×
                Err("its log entry is not its attestation's".to_string())
3684×
            } else if witnesses.is_empty() {
3690×
                Err(format!("{hub} names no witness"))
370
            } else {
3714×
                let cosigned: Vec<(&str, &[Hash])> = integration
3724×
                    .iter()
3734×
                    .map(|i| (i.note.as_str(), i.proof.as_slice()))
3744×
                    .collect();
3754×
                tlog::integrated_time(
3764×
                    &l.entry,
3774×
                    l.index,
3784×
                    &cosigned,
3794×
                    &cp.origin,
3804×
                    &accepted.identity,
3814×
                    &witnesses,
382
                )
383
            }
3844×
        };
3854×
        let mut times = HashMap::new();
3864×
        for r in &resp.refs {
3874×
            let Some(a) = &r.attestation else { continue };
3884×
            let time = place(&r.name, "ref", a, r.logged.as_ref(), &r.integration);
3894×
            times.insert(r.name.clone(), time);
390
        }
391
        // Tags too (oxygen#129): a short-lived key's tag is checked at its
392
        // integrated time on every later fetch.
3934×
        let mut tag_times = HashMap::new();
3944×
        for t in &resp.tags {
3950×
            let Some(a) = &t.attestation else { continue };
3960×
            let name = format!("tags/{}", t.name);
3970×
            let time = place(&name, "tag", a, t.logged.as_ref(), &t.integration);
3980×
            tag_times.insert(t.name.clone(), time);
399
        }
4004×
        out.times = times;
4014×
        out.tag_times = tag_times;
4024×
        if missing > 0 {
4030×
            out.notes.push(format!(
404
                "{} of {hub}'s attestations {} not in its transparency log yet (logged within seconds of a push; checked on a later fetch)",
405
                missing,
4060×
                if missing == 1 { "is" } else { "are" }
407
            ));
4084×
        }
4094×
        Ok(out)
410419×
    }
411
}
412
413
/// A checkpoint `accept_checkpoint` took: the checkpoint, the log key it
414
/// verified with, its note as served, and what accepting it noted.
415
pub(crate) struct Accepted {
416
    pub checkpoint: Checkpoint,
417
    pub identity: Identity,
418
    pub note: Note,
419
    pub notes: Vec<String>,
420
}
421
422
/// What a refs answer said about its host's log.
423
#[derive(Debug, Default)]
424
pub(crate) struct LogCheck {
425
    /// Whether the host keeps a log (it sent a checkpoint).
426
    pub kept: bool,
427
    /// Why the log could not be checked, when it could not.
428
    pub unchecked: Option<String>,
429
    pub notes: Vec<String>,
430
    /// Each attested bookmark's integrated time, or why it has none.
431
    pub times: HashMap<String, std::result::Result<Integrated, String>>,
432
    /// Each attested tag's, by tag name.
433
    pub tag_times: HashMap<String, std::result::Result<Integrated, String>>,
434
}
435
436
impl LogCheck {
437
    /// Bookmark `name`'s integrated time, or why there is none.
43830×
    pub fn time_of(&self, name: &str) -> std::result::Result<&Integrated, String> {
43930×
        self.lookup(&self.times, name)
44030×
    }
441
442
    /// Tag `name`'s integrated time, or why there is none.
4438×
    pub fn tag_time_of(&self, name: &str) -> std::result::Result<&Integrated, String> {
4448×
        self.lookup(&self.tag_times, name)
4458×
    }
446
44738×
    fn lookup<'a>(
44838×
        &self,
44938×
        times: &'a HashMap<String, std::result::Result<Integrated, String>>,
45038×
        name: &str,
45138×
    ) -> std::result::Result<&'a Integrated, String> {
45238×
        if let Some(why) = &self.unchecked {
4530×
            return Err(format!("its host's log was not checked ({why})"));
45438×
        }
45538×
        if !self.kept {
45629×
            return Err("its host keeps no transparency log".into());
4579×
        }
4589×
        match times.get(name) {
4596×
            Some(Ok(i)) => Ok(i),
4603×
            Some(Err(why)) => Err(why.clone()),
4610×
            None => Err("it has no log entry".into()),
462
        }
46338×
    }
464
}
465
466
/// How a check that depends on when an attestation was signed came out
467
/// (spec §14.8, *Integrated time*).
468
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
469
pub(crate) enum Timed {
470
    /// Passed at the integrated time.
471
    Integrated,
472
    /// No integrated time: passed by the earlier rule (the issuer's
473
    /// acceptance, else the attestation's own time).
474
    Fallback,
475
    /// Failed at the integrated time but passed by the earlier rule:
476
    /// accepted with a note while logs are new.
477
    Disagrees,
478
    /// Failed.
479
    Refused,
480
}
481
482
/// Judge a check (`at`, made at a time in milliseconds) at the integrated
483
/// time when there is one, else by the earlier rule (`before`).
48433×
pub(crate) fn judge(
48533×
    integrated: Option<u64>,
48633×
    at: impl Fn(u64) -> bool,
48733×
    before: impl FnOnce() -> bool,
48833×
) -> Timed {
4895×
    match integrated {
4905×
        Some(t) if at(t) => Timed::Integrated,
491
        Some(_) => {
4922×
            if before() {
4931×
                Timed::Disagrees
494
            } else {
4951×
                Timed::Refused
496
            }
497
        }
498
        None => {
49928×
            if before() {
50028×
                Timed::Fallback
501
            } else {
5020×
                Timed::Refused
503
            }
504
        }
505
    }
50633×
}
507
508
/// A note when an attestation's claimed time (`claimed`, its own clock)
509
/// is more than `tlog::TIME_TOLERANCE_MS` from its integrated time.
5104×
pub(crate) fn gap_note(claimed: u64, integrated: &Integrated) -> Option<String> {
5114×
    let t = integrated.ms();
5124×
    let minutes = |d: u64| d.div_ceil(60_000);
5134×
    if claimed.saturating_add(tlog::TIME_TOLERANCE_MS) < t {
5141×
        Some(format!(
5151×
            "claims to be from {} minutes before witness {} first saw it: backdated, held back before it was logged, or signed on a slow clock",
5161×
            minutes(t - claimed),
5171×
            integrated.witness
5181×
        ))
5193×
    } else if claimed > t.saturating_add(tlog::TIME_TOLERANCE_MS) {
5201×
        Some(format!(
5211×
            "claims to be from {} minutes after witness {} saw it: signed on a fast clock",
5221×
            minutes(claimed - t),
5231×
            integrated.witness
5241×
        ))
525
    } else {
5262×
        None
527
    }
5284×
}
529
530
/// A hub's whole log, checked (`oxy tlog verify`).
531
#[derive(Clone, Debug)]
532
pub struct LogAudit {
533
    pub hub: String,
534
    pub checkpoint: Checkpoint,
535
    /// Every entry, numbered from 0.
536
    pub entries: Vec<Entry>,
537
    /// What accepting the checkpoint noted (a new log key, a new name).
538
    pub notes: Vec<String>,
539
    /// The trusted witnesses that cosigned the checkpoint, with their
540
    /// times in seconds, earliest first.
541
    pub witnessed: Vec<(String, u64)>,
542
}
543
544
impl Repo {
545
    /// Fetch the log of `url`'s hub whole: its checkpoint, accepted as a
546
    /// fetch accepts one, and every entry bundle, whose entries must make
547
    /// the root the checkpoint signs.
5480×
    pub fn audit_log(&self, url: &str, ca: Option<&Path>) -> Result<LogAudit> {
5490×
        let hub =
5500×
            hub_of(url).ok_or_else(|| RepoError::Msg(format!("{url} is not a hub's address")))?;
5510×
        let options = RemoteOptions {
5520×
            ca: ca.map(Path::to_path_buf),
5530×
            ..RemoteOptions::default()
5540×
        };
5550×
        let tl = |e: TlogError| RepoError::Msg(e.to_string());
5560×
        let text = String::from_utf8(http_get(&format!("{hub}/log/checkpoint"), &options)?)
5570×
            .map_err(|_| RepoError::Msg(format!("{hub}'s checkpoint is not text")))?;
5580×
        let accepted = match self.accept_checkpoint(&hub, &text, &options)? {
5590×
            Ok(c) => c,
5600×
            Err(why) => return fail(format!("{hub}'s log could not be checked: {why}")),
561
        };
5620×
        let mut notes = accepted.notes;
563
        // The witnesses it names (one `<name> <base64 identity>` line
564
        // each), pinned as a fetch pins them; a hub that does not list
565
        // them leaves those pinned before.
5660×
        let listed: Vec<(String, Vec<u8>)> = http_get(&format!("{hub}/log/witnesses"), &options)
5670×
            .ok()
5680×
            .and_then(|b| String::from_utf8(b).ok())
5690×
            .map(|t| {
5700×
                t.lines()
5710×
                    .filter_map(|l| {
5720×
                        let (n, k) = l.split_once(' ')?;
5730×
                        Some((n.to_string(), crate::dsse::base64_decode(k.trim())?))
5740×
                    })
5750×
                    .collect()
5760×
            })
5770×
            .unwrap_or_default();
5780×
        let witnesses = self.pin_witnesses(&hub, &listed, &mut notes)?;
5790×
        let mut witnessed = accepted.note.cosignatures(&witnesses);
5800×
        witnessed.sort_by_key(|(_, t)| *t);
5810×
        let cp = accepted.checkpoint;
5820×
        let mut entries = Vec::with_capacity(cp.size as usize);
5830×
        let mut stored: Vec<Hash> = Vec::new();
5840×
        let mut n = 0u64;
5850×
        while n < cp.size {
5860×
            let t = Tile {
5870×
                level: 0,
5880×
                n: n >> tlog::TILE_HEIGHT,
5890×
                width: (cp.size - n).min(tlog::TILE_WIDTH) as u32,
5900×
            };
5910×
            let bytes = http_get(&format!("{hub}/log/{}", t.entries_path()), &options)?;
5920×
            let bundle = tlog::decode_bundle(&bytes).map_err(tl)?;
5930×
            if bundle.len() != t.width as usize {
5940×
                return fail(format!(
595
                    "{hub}'s entry bundle {} holds {} entries, not {}",
5960×
                    t.entries_path(),
5970×
                    bundle.len(),
598
                    t.width
599
                ));
6000×
            }
6010×
            for b in bundle {
6020×
                let add =
6030×
                    tlog::stored_hashes_for_leaf(n, tlog::leaf_hash(&b), &stored).map_err(tl)?;
6040×
                stored.extend(add);
6050×
                entries.push(Entry::decode(&b).map_err(tl)?);
6060×
                n += 1;
607
            }
608
        }
6090×
        if tlog::tree_hash(cp.size, &stored).map_err(tl)? != cp.root {
6100×
            return fail(format!(
611
                "{hub} serves entries that do not make the root it signed for {} entries: its log is not the one it attests",
612
                cp.size
613
            ));
6140×
        }
6150×
        Ok(LogAudit {
6160×
            hub,
6170×
            checkpoint: cp,
6180×
            entries,
6190×
            notes,
6200×
            witnessed,
6210×
        })
6220×
    }
623
}