//! `oxy tlog verify` (spec §14.8, oxygenhub#238): audit a hub's whole //! transparency log and list the entries that concern you. use super::json; use super::open; use super::ui; use anyhow::{anyhow, Result}; use oxy_core::hash::KeyId; use std::path::{Path, PathBuf}; pub fn verify(hub: Option<&str>, key: Option<&str>, all: bool, as_json: bool) -> Result { let repo = open()?; let remote = repo.default_remote(); let url = match hub { Some(h) if h.contains("://") => h.to_string(), Some(h) => format!("https://{h}"), None => repo.config.remotes.get(&remote).cloned().ok_or_else(|| { anyhow!("name the hub to check (this checkout has no remote {remote})") })?, }; let ca = repo .config .remote_cas .get(&remote) .map(Path::new) .filter(|_| hub.is_none()); let audit = repo.audit_log(&url, ca).map_err(|e| anyhow!("{e}"))?; let key = match key { Some(k) => Some( KeyId::from_hex(k) .ok_or_else(|| anyhow!("--key takes a full KeyId (64 hex digits)"))?, ), None => None, }; let rid = repo.id().ok(); let listed: Vec<(usize, &oxy_core::tlog::Entry)> = audit .entries .iter() .enumerate() .filter(|(_, e)| match (&key, all) { (_, true) => true, (Some(k), _) => e.signers.contains(k), (None, _) => Some(e.repo) == rid, }) .collect(); let what = match (&key, all) { (_, true) => "every entry".to_string(), (Some(k), _) => format!("entries signed by {}", k.short(12)), (None, _) => "this repository's entries".to_string(), }; if as_json { println!( "{}", json::obj([ ("hub", json::s(&audit.hub)), ("origin", json::s(&audit.checkpoint.origin)), ("size", json::n(audit.checkpoint.size)), ( "root", json::s(&oxy_core::hash::ObjectRef(audit.checkpoint.root).to_hex()) ), ( "witnesses", json::arr(audit.witnessed.iter().map(|(name, t)| { json::obj([ ("name", json::s(name)), ("time_ms", json::n(t.saturating_mul(1000))), ]) })), ), ( "entries", json::arr(listed.iter().map(|(i, e)| { json::obj([ ("index", json::n(*i)), ("kind", json::s(&e.kind)), ( "repo", json::s( &e.repo .iter() .map(|b| format!("{b:02x}")) .collect::(), ), ), ("object", json::s(&e.object.to_hex())), ( "signers", json::arr(e.signers.iter().map(|k| json::s(&k.to_hex()))), ), ("received_ms", json::n(e.received)), ]) })), ), ]) ); return Ok(0); } for n in &audit.notes { eprintln!("{} {n}", ui::caution()); } println!( "{} {}: {} entries make the root its log key signed, and extend what you saw before", ui::done("Verified"), ui::path(&audit.checkpoint.origin), audit.checkpoint.size ); // Its cosignatures: an upper bound on when every entry in it was made // (spec §14.8, *Integrated time*). if audit.witnessed.is_empty() { println!( " {} {}", ui::note(), ui::dim("cosigned by no witness you trust") ); } else { let by: Vec = audit .witnessed .iter() .map(|(name, t)| format!("{name} {}", ui::ago(t.saturating_mul(1000)))) .collect(); println!( " {} {}", ui::note(), ui::dim(format!("cosigned by {}", by.join(", "))) ); } if listed.is_empty() { println!(" {} {}", ui::note(), ui::dim(format!("no {what} in it"))); return Ok(0); } println!( " {} {}", ui::note(), ui::dim(format!("{} of {what}:", listed.len())) ); for (i, e) in listed { let signers: Vec = e.signers.iter().map(|k| k.short(12)).collect(); println!( " #{i:<6} {:<11} {} {} {}", e.kind, ui::hash(e.object.short(12)), ui::dim(if signers.is_empty() { "unsigned".to_string() } else { format!("by {}", signers.join(", ")) }), ui::dim(ui::ago(e.received)) ); } Ok(0) } /// `oxy witness serve`: cosign the named logs' checkpoints. pub fn witness_serve( name: &str, logs: &[String], state: PathBuf, bind: &str, tls: Option<(PathBuf, PathBuf)>, ) -> Result { let home = oxy_core::repo::default_home(); let (identity, sk) = oxy_core::repo::home_key(&home) .map_err(|e| anyhow!("{e}"))? .ok_or_else(|| anyhow!("{} names no key (author.key) with its secret: a witness signs with it; oxy identity create", home.display()))?; let mut parsed = Vec::new(); for l in logs { let (origin, file) = l .split_once('=') .ok_or_else(|| anyhow!("--log takes ORIGIN=FILE, as oxygenhub.io/log=hub.identity"))?; let bytes = std::fs::read(file).map_err(|e| anyhow!("{file}: {e}"))?; let key = oxy_core::identity::Identity::decode(&bytes).map_err(|e| anyhow!("{file}: {e}"))?; parsed.push((origin.to_string(), key)); } let listener = std::net::TcpListener::bind(bind).map_err(|e| anyhow!("binding {bind}: {e}"))?; println!( "{} {} logs as {} with key {} on {}://{bind}/add-checkpoint", ui::done("Witnessing"), parsed.len(), ui::path(name), identity.hash().short(12), if tls.is_some() { "https" } else { "http" } ); for (o, k) in &parsed { println!( " {} {} {}", ui::note(), o, ui::dim(format!("signed by {}", k.hash().short(12))) ); } oxy_server::witness::run_blocking( listener, oxy_server::witness::WitnessConfig { name: name.to_string(), identity, sk, logs: parsed, state, tls: tls.map(|(cert, key)| oxy_server::TlsFiles { cert, key }), }, )?; Ok(0) }