OXYGENHUB

Coverage / src/cli/tlog.rs 0.0%

0 of 174 lines run, 0 of 40 functions · run #427 on main · run missed

1
//! `oxy tlog verify` (spec §14.8, oxygenhub#238): audit a hub's whole
2
//! transparency log and list the entries that concern you.
3
4
use super::json;
5
use super::open;
6
use super::ui;
7
use anyhow::{anyhow, Result};
8
use oxy_core::hash::KeyId;
9
use std::path::{Path, PathBuf};
10
110×
pub fn verify(hub: Option<&str>, key: Option<&str>, all: bool, as_json: bool) -> Result<i32> {
120×
    let repo = open()?;
130×
    let remote = repo.default_remote();
140×
    let url = match hub {
150×
        Some(h) if h.contains("://") => h.to_string(),
160×
        Some(h) => format!("https://{h}"),
170×
        None => repo.config.remotes.get(&remote).cloned().ok_or_else(|| {
180×
            anyhow!("name the hub to check (this checkout has no remote {remote})")
190×
        })?,
20
    };
210×
    let ca = repo
220×
        .config
230×
        .remote_cas
240×
        .get(&remote)
250×
        .map(Path::new)
260×
        .filter(|_| hub.is_none());
270×
    let audit = repo.audit_log(&url, ca).map_err(|e| anyhow!("{e}"))?;
280×
    let key = match key {
290×
        Some(k) => Some(
300×
            KeyId::from_hex(k)
310×
                .ok_or_else(|| anyhow!("--key takes a full KeyId (64 hex digits)"))?,
32
        ),
330×
        None => None,
34
    };
350×
    let rid = repo.id().ok();
360×
    let listed: Vec<(usize, &oxy_core::tlog::Entry)> = audit
370×
        .entries
380×
        .iter()
390×
        .enumerate()
400×
        .filter(|(_, e)| match (&key, all) {
410×
            (_, true) => true,
420×
            (Some(k), _) => e.signers.contains(k),
430×
            (None, _) => Some(e.repo) == rid,
440×
        })
450×
        .collect();
460×
    let what = match (&key, all) {
470×
        (_, true) => "every entry".to_string(),
480×
        (Some(k), _) => format!("entries signed by {}", k.short(12)),
490×
        (None, _) => "this repository's entries".to_string(),
50
    };
510×
    if as_json {
520×
        println!(
53
            "{}",
540×
            json::obj([
550×
                ("hub", json::s(&audit.hub)),
560×
                ("origin", json::s(&audit.checkpoint.origin)),
570×
                ("size", json::n(audit.checkpoint.size)),
580×
                (
590×
                    "root",
600×
                    json::s(&oxy_core::hash::ObjectRef(audit.checkpoint.root).to_hex())
610×
                ),
62
                (
630×
                    "witnesses",
640×
                    json::arr(audit.witnessed.iter().map(|(name, t)| {
650×
                        json::obj([
660×
                            ("name", json::s(name)),
670×
                            ("time_ms", json::n(t.saturating_mul(1000))),
680×
                        ])
690×
                    })),
70
                ),
71
                (
720×
                    "entries",
730×
                    json::arr(listed.iter().map(|(i, e)| {
740×
                        json::obj([
750×
                            ("index", json::n(*i)),
760×
                            ("kind", json::s(&e.kind)),
77
                            (
780×
                                "repo",
790×
                                json::s(
800×
                                    &e.repo
810×
                                        .iter()
820×
                                        .map(|b| format!("{b:02x}"))
830×
                                        .collect::<String>(),
84
                                ),
85
                            ),
860×
                            ("object", json::s(&e.object.to_hex())),
87
                            (
880×
                                "signers",
890×
                                json::arr(e.signers.iter().map(|k| json::s(&k.to_hex()))),
90
                            ),
910×
                            ("received_ms", json::n(e.received)),
92
                        ])
930×
                    })),
94
                ),
95
            ])
96
        );
970×
        return Ok(0);
980×
    }
990×
    for n in &audit.notes {
1000×
        eprintln!("{} {n}", ui::caution());
1010×
    }
1020×
    println!(
103
        "{} {}: {} entries make the root its log key signed, and extend what you saw before",
1040×
        ui::done("Verified"),
1050×
        ui::path(&audit.checkpoint.origin),
106
        audit.checkpoint.size
107
    );
108
    // Its cosignatures: an upper bound on when every entry in it was made
109
    // (spec §14.8, *Integrated time*).
1100×
    if audit.witnessed.is_empty() {
1110×
        println!(
1120×
            "  {} {}",
1130×
            ui::note(),
1140×
            ui::dim("cosigned by no witness you trust")
1150×
        );
1160×
    } else {
1170×
        let by: Vec<String> = audit
1180×
            .witnessed
1190×
            .iter()
1200×
            .map(|(name, t)| format!("{name} {}", ui::ago(t.saturating_mul(1000))))
1210×
            .collect();
1220×
        println!(
123
            "  {} {}",
1240×
            ui::note(),
1250×
            ui::dim(format!("cosigned by {}", by.join(", ")))
126
        );
127
    }
1280×
    if listed.is_empty() {
1290×
        println!("  {} {}", ui::note(), ui::dim(format!("no {what} in it")));
1300×
        return Ok(0);
1310×
    }
1320×
    println!(
133
        "  {} {}",
1340×
        ui::note(),
1350×
        ui::dim(format!("{} of {what}:", listed.len()))
136
    );
1370×
    for (i, e) in listed {
1380×
        let signers: Vec<String> = e.signers.iter().map(|k| k.short(12)).collect();
1390×
        println!(
140
            "  #{i:<6} {:<11} {} {} {}",
141
            e.kind,
1420×
            ui::hash(e.object.short(12)),
1430×
            ui::dim(if signers.is_empty() {
1440×
                "unsigned".to_string()
145
            } else {
1460×
                format!("by {}", signers.join(", "))
147
            }),
1480×
            ui::dim(ui::ago(e.received))
149
        );
150
    }
1510×
    Ok(0)
1520×
}
153
154
/// `oxy witness serve`: cosign the named logs' checkpoints.
1550×
pub fn witness_serve(
1560×
    name: &str,
1570×
    logs: &[String],
1580×
    state: PathBuf,
1590×
    bind: &str,
1600×
    tls: Option<(PathBuf, PathBuf)>,
1610×
) -> Result<i32> {
1620×
    let home = oxy_core::repo::default_home();
1630×
    let (identity, sk) = oxy_core::repo::home_key(&home)
1640×
        .map_err(|e| anyhow!("{e}"))?
1650×
        .ok_or_else(|| anyhow!("{} names no key (author.key) with its secret: a witness signs with it; oxy identity create", home.display()))?;
1660×
    let mut parsed = Vec::new();
1670×
    for l in logs {
1680×
        let (origin, file) = l
1690×
            .split_once('=')
1700×
            .ok_or_else(|| anyhow!("--log takes ORIGIN=FILE, as oxygenhub.io/log=hub.identity"))?;
1710×
        let bytes = std::fs::read(file).map_err(|e| anyhow!("{file}: {e}"))?;
1720×
        let key =
1730×
            oxy_core::identity::Identity::decode(&bytes).map_err(|e| anyhow!("{file}: {e}"))?;
1740×
        parsed.push((origin.to_string(), key));
175
    }
1760×
    let listener = std::net::TcpListener::bind(bind).map_err(|e| anyhow!("binding {bind}: {e}"))?;
1770×
    println!(
178
        "{} {} logs as {} with key {} on {}://{bind}/add-checkpoint",
1790×
        ui::done("Witnessing"),
1800×
        parsed.len(),
1810×
        ui::path(name),
1820×
        identity.hash().short(12),
1830×
        if tls.is_some() { "https" } else { "http" }
184
    );
1850×
    for (o, k) in &parsed {
1860×
        println!(
1870×
            "  {} {} {}",
1880×
            ui::note(),
1890×
            o,
1900×
            ui::dim(format!("signed by {}", k.hash().short(12)))
1910×
        );
1920×
    }
1930×
    oxy_server::witness::run_blocking(
1940×
        listener,
195
        oxy_server::witness::WitnessConfig {
1960×
            name: name.to_string(),
1970×
            identity,
1980×
            sk,
1990×
            logs: parsed,
2000×
            state,
2010×
            tls: tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key }),
202
        },
2030×
    )?;
2040×
    Ok(0)
2050×
}