OXYGENHUB
# 51d4862a16d8 oxygen / src / cli / tlog.rs
5.9 KiB · Rust · last changed oxy witness serve: a transparency-log witness anyone can run 18 days ago source blame history raw
e59512f2 Andy McCall ↶ 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;
51d4862a Andy McCall ↶ 9
use std::path::{Path, PathBuf};
e59512f2 Andy McCall ↶ 10
11
pub fn verify(hub: Option<&str>, key: Option<&str>, all: bool, as_json: bool) -> Result<i32> {
12
    let repo = open()?;
13
    let remote = repo.default_remote();
14
    let url = match hub {
15
        Some(h) if h.contains("://") => h.to_string(),
16
        Some(h) => format!("https://{h}"),
17
        None => repo.config.remotes.get(&remote).cloned().ok_or_else(|| {
18
            anyhow!("name the hub to check (this checkout has no remote {remote})")
19
        })?,
20
    };
21
    let ca = repo
22
        .config
23
        .remote_cas
24
        .get(&remote)
25
        .map(Path::new)
26
        .filter(|_| hub.is_none());
27
    let audit = repo.audit_log(&url, ca).map_err(|e| anyhow!("{e}"))?;
28
    let key = match key {
29
        Some(k) => Some(
30
            KeyId::from_hex(k)
31
                .ok_or_else(|| anyhow!("--key takes a full KeyId (64 hex digits)"))?,
32
        ),
33
        None => None,
34
    };
35
    let rid = repo.id().ok();
36
    let listed: Vec<(usize, &oxy_core::tlog::Entry)> = audit
37
        .entries
38
        .iter()
39
        .enumerate()
40
        .filter(|(_, e)| match (&key, all) {
41
            (_, true) => true,
42
            (Some(k), _) => e.signers.contains(k),
43
            (None, _) => Some(e.repo) == rid,
44
        })
45
        .collect();
46
    let what = match (&key, all) {
47
        (_, true) => "every entry".to_string(),
48
        (Some(k), _) => format!("entries signed by {}", k.short(12)),
49
        (None, _) => "this repository's entries".to_string(),
50
    };
51
    if as_json {
52
        println!(
53
            "{}",
54
            json::obj([
55
                ("hub", json::s(&audit.hub)),
56
                ("origin", json::s(&audit.checkpoint.origin)),
57
                ("size", json::n(audit.checkpoint.size)),
58
                (
59
                    "root",
60
                    json::s(&oxy_core::hash::ObjectRef(audit.checkpoint.root).to_hex())
61
                ),
62
                (
63
                    "entries",
64
                    json::arr(listed.iter().map(|(i, e)| {
65
                        json::obj([
66
                            ("index", json::n(*i)),
67
                            ("kind", json::s(&e.kind)),
68
                            (
69
                                "repo",
70
                                json::s(
71
                                    &e.repo
72
                                        .iter()
73
                                        .map(|b| format!("{b:02x}"))
74
                                        .collect::<String>(),
75
                                ),
76
                            ),
77
                            ("object", json::s(&e.object.to_hex())),
78
                            (
79
                                "signers",
80
                                json::arr(e.signers.iter().map(|k| json::s(&k.to_hex()))),
81
                            ),
82
                            ("received_ms", json::n(e.received)),
83
                        ])
84
                    })),
85
                ),
86
            ])
87
        );
88
        return Ok(0);
89
    }
90
    println!(
91
        "{} {}: {} entries make the root its log key signed, and extend what you saw before",
92
        ui::done("Verified"),
93
        ui::path(&audit.checkpoint.origin),
94
        audit.checkpoint.size
95
    );
96
    if listed.is_empty() {
97
        println!("  {} {}", ui::note(), ui::dim(format!("no {what} in it")));
98
        return Ok(0);
99
    }
100
    println!(
101
        "  {} {}",
102
        ui::note(),
103
        ui::dim(format!("{} of {what}:", listed.len()))
104
    );
105
    for (i, e) in listed {
106
        let signers: Vec<String> = e.signers.iter().map(|k| k.short(12)).collect();
107
        println!(
108
            "  #{i:<6} {:<11} {} {} {}",
109
            e.kind,
110
            ui::hash(e.object.short(12)),
111
            ui::dim(if signers.is_empty() {
112
                "unsigned".to_string()
113
            } else {
114
                format!("by {}", signers.join(", "))
115
            }),
116
            ui::dim(ui::ago(e.received))
117
        );
118
    }
119
    Ok(0)
120
}
51d4862a Andy McCall ↶ 121
122
/// `oxy witness serve`: cosign the named logs' checkpoints.
123
pub fn witness_serve(
124
    name: &str,
125
    logs: &[String],
126
    state: PathBuf,
127
    bind: &str,
128
    tls: Option<(PathBuf, PathBuf)>,
129
) -> Result<i32> {
130
    let home = oxy_core::repo::default_home();
131
    let (identity, sk) = oxy_core::repo::home_key(&home)
132
        .map_err(|e| anyhow!("{e}"))?
133
        .ok_or_else(|| anyhow!("{} names no key (author.key) with its secret: a witness signs with it; oxy identity create", home.display()))?;
134
    let mut parsed = Vec::new();
135
    for l in logs {
136
        let (origin, file) = l
137
            .split_once('=')
138
            .ok_or_else(|| anyhow!("--log takes ORIGIN=FILE, as oxygenhub.io/log=hub.identity"))?;
139
        let bytes = std::fs::read(file).map_err(|e| anyhow!("{file}: {e}"))?;
140
        let key =
141
            oxy_core::identity::Identity::decode(&bytes).map_err(|e| anyhow!("{file}: {e}"))?;
142
        parsed.push((origin.to_string(), key));
143
    }
144
    let listener = std::net::TcpListener::bind(bind).map_err(|e| anyhow!("binding {bind}: {e}"))?;
145
    println!(
146
        "{} {} logs as {} with key {} on {}://{bind}/add-checkpoint",
147
        ui::done("Witnessing"),
148
        parsed.len(),
149
        ui::path(name),
150
        identity.hash().short(12),
151
        if tls.is_some() { "https" } else { "http" }
152
    );
153
    for (o, k) in &parsed {
154
        println!(
155
            "  {} {} {}",
156
            ui::note(),
157
            o,
158
            ui::dim(format!("signed by {}", k.hash().short(12)))
159
        );
160
    }
161
    oxy_server::witness::run_blocking(
162
        listener,
163
        oxy_server::witness::WitnessConfig {
164
            name: name.to_string(),
165
            identity,
166
            sk,
167
            logs: parsed,
168
            state,
169
            tls: tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key }),
170
        },
171
    )?;
172
    Ok(0)
173
}