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