oxy witness serve: a transparency-log witness anyone can run 51d4862a16d8
The third kind of witness in oxygenhub docs/transparency.md, beside hubs witnessing each other. `oxy witness serve --name HOST --log ORIGIN=FILE ...` serves C2SP tlog-witness POST /add-checkpoint (oxy-server's witness module). It cosigns, with this oxy home's key and its own clock, each checkpoint that verifies with the log's key and extends the last one it cosigned. The same rule as the hub's witness: 409 with its size, 422 to a fork, 403 to another key, 404 to an unknown log. It keeps what it cosigned per log in --state, replaced atomically. Test: cosigns, keeps its state across a restart, tells a wrong size, cosigns the next with its proof. Guide: Transparency logs. Refs oxygenhub#238
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 758a6d7c52c9
- When
- · 2026-10-18 00:46 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
51d4862a16d8de259898ae8d07a48ad1887fc810ee30de0e46b1cddaa8f6e95e- Depends on
- 4dee277c327a 61e3bce7ea5b 750251b21a96 e59512f2f075 ee06a6c557b7 fce44ae9ad29 ff0af1ee299d
- Depended on by
- nothing yet
- In bookmarks
- carried-not-removed doctor-trust identical-sides keyless-tags main merge-remote-bookmark paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state
- In tags
- v0.10.0 v0.11.0 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 11 atoms, 1 tree operation, 7 files
Browse the tree at this change
added crates/oxy-server/src/witness.rs +272 −0
| 1 | + | //! `oxy witness serve` (Oxygen spec §14.8, oxygenhub docs/transparency.md): |
|
| 2 | + | //! a small transparency-log witness anyone can run. It cosigns the |
|
| 3 | + | //! checkpoints of the logs it is told about (C2SP tlog-witness), each only |
|
| 4 | + | //! when it extends the last one this witness cosigned, with its own key |
|
| 5 | + | //! and clock, so a log cannot show two histories without the witness |
|
| 6 | + | //! seeing it. |
|
| 7 | + | //! |
|
| 8 | + | //! State is one file per log in the state directory: the size and root of |
|
| 9 | + | //! the last checkpoint cosigned, replaced atomically. |
|
| 10 | + | ||
| 11 | + | use axum::extract::State; |
|
| 12 | + | use axum::http::{header, StatusCode}; |
|
| 13 | + | use axum::response::{IntoResponse, Response}; |
|
| 14 | + | use axum::routing::post; |
|
| 15 | + | use axum::Router; |
|
| 16 | + | use oxy_core::identity::{Identity, SecretKey}; |
|
| 17 | + | use oxy_core::tlog::{self, AddCheckpoint, Checkpoint, Note, WitnessError, WitnessKey, Witnessed}; |
|
| 18 | + | use std::path::PathBuf; |
|
| 19 | + | use std::sync::{Arc, Mutex}; |
|
| 20 | + | ||
| 21 | + | /// What a witness serves. |
|
| 22 | + | pub struct WitnessConfig { |
|
| 23 | + | /// Its name in cosignatures (a host name, no spaces). |
|
| 24 | + | pub name: String, |
|
| 25 | + | pub identity: Identity, |
|
| 26 | + | pub sk: SecretKey, |
|
| 27 | + | /// The logs it witnesses: their origins and keys. |
|
| 28 | + | pub logs: Vec<(String, Identity)>, |
|
| 29 | + | /// Where it keeps what it cosigned. |
|
| 30 | + | pub state: PathBuf, |
|
| 31 | + | pub tls: Option<crate::TlsFiles>, |
|
| 32 | + | } |
|
| 33 | + | ||
| 34 | + | struct Witness { |
|
| 35 | + | cfg: WitnessConfig, |
|
| 36 | + | /// One add-checkpoint at a time: each reads and moves its log's state. |
|
| 37 | + | lock: Mutex<()>, |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | fn state_file(w: &Witness, origin: &str) -> PathBuf { |
|
| 41 | + | let safe: String = origin |
|
| 42 | + | .chars() |
|
| 43 | + | .map(|c| { |
|
| 44 | + | if c.is_ascii_alphanumeric() || c == '.' || c == '-' { |
|
| 45 | + | c |
|
| 46 | + | } else { |
|
| 47 | + | '_' |
|
| 48 | + | } |
|
| 49 | + | }) |
|
| 50 | + | .collect(); |
|
| 51 | + | w.cfg.state.join(safe) |
|
| 52 | + | } |
|
| 53 | + | ||
| 54 | + | fn read_state(w: &Witness, origin: &str) -> Option<(u64, tlog::Hash)> { |
|
| 55 | + | let text = std::fs::read_to_string(state_file(w, origin)).ok()?; |
|
| 56 | + | let (size, root) = text.trim().split_once(' ')?; |
|
| 57 | + | let root = oxy_core::hash::ObjectRef::from_hex(root)?; |
|
| 58 | + | Some((size.parse().ok()?, root.0)) |
|
| 59 | + | } |
|
| 60 | + | ||
| 61 | + | fn plain(status: StatusCode, why: &str) -> Response { |
|
| 62 | + | ( |
|
| 63 | + | status, |
|
| 64 | + | [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], |
|
| 65 | + | format!("{why}\n"), |
|
| 66 | + | ) |
|
| 67 | + | .into_response() |
|
| 68 | + | } |
|
| 69 | + | ||
| 70 | + | async fn add_checkpoint(State(w): State<Arc<Witness>>, body: String) -> Response { |
|
| 71 | + | let Ok(req) = AddCheckpoint::parse(&body) else { |
|
| 72 | + | return plain(StatusCode::BAD_REQUEST, "not an add-checkpoint request"); |
|
| 73 | + | }; |
|
| 74 | + | let Some(origin) = Note::parse(&req.note) |
|
| 75 | + | .ok() |
|
| 76 | + | .and_then(|n| Checkpoint::parse(&n.text).ok()) |
|
| 77 | + | .map(|c| c.origin) |
|
| 78 | + | else { |
|
| 79 | + | return plain(StatusCode::BAD_REQUEST, "the checkpoint does not parse"); |
|
| 80 | + | }; |
|
| 81 | + | let Some(key) = w |
|
| 82 | + | .cfg |
|
| 83 | + | .logs |
|
| 84 | + | .iter() |
|
| 85 | + | .find(|(o, _)| *o == origin) |
|
| 86 | + | .map(|(_, k)| k.clone()) |
|
| 87 | + | else { |
|
| 88 | + | return plain( |
|
| 89 | + | StatusCode::NOT_FOUND, |
|
| 90 | + | &format!("this witness does not witness {origin}"), |
|
| 91 | + | ); |
|
| 92 | + | }; |
|
| 93 | + | let _held = w.lock.lock().unwrap_or_else(|p| p.into_inner()); |
|
| 94 | + | let latest = read_state(&w, &origin); |
|
| 95 | + | let me = WitnessKey { |
|
| 96 | + | name: &w.cfg.name, |
|
| 97 | + | identity: &w.cfg.identity, |
|
| 98 | + | sk: &w.cfg.sk, |
|
| 99 | + | }; |
|
| 100 | + | let now = std::time::SystemTime::now() |
|
| 101 | + | .duration_since(std::time::UNIX_EPOCH) |
|
| 102 | + | .map(|d| d.as_secs()) |
|
| 103 | + | .unwrap_or(0); |
|
| 104 | + | let conflict = |n: u64| { |
|
| 105 | + | ( |
|
| 106 | + | StatusCode::CONFLICT, |
|
| 107 | + | [(header::CONTENT_TYPE, "text/x.tlog.size")], |
|
| 108 | + | format!("{n}\n"), |
|
| 109 | + | ) |
|
| 110 | + | .into_response() |
|
| 111 | + | }; |
|
| 112 | + | match tlog::witness(&req, &origin, &key, latest, &me, now) { |
|
| 113 | + | Ok(Witnessed::Cosigned { checkpoint, lines }) => { |
|
| 114 | + | let text = format!( |
|
| 115 | + | "{} {}\n", |
|
| 116 | + | checkpoint.size, |
|
| 117 | + | oxy_core::hash::ObjectRef(checkpoint.root).to_hex() |
|
| 118 | + | ); |
|
| 119 | + | if let Err(e) = std::fs::create_dir_all(&w.cfg.state).and_then(|_| { |
|
| 120 | + | oxy_core::durable::write_atomic(&state_file(&w, &origin), text.as_bytes()) |
|
| 121 | + | }) { |
|
| 122 | + | return plain( |
|
| 123 | + | StatusCode::INTERNAL_SERVER_ERROR, |
|
| 124 | + | &format!("keeping the witness's state: {e}"), |
|
| 125 | + | ); |
|
| 126 | + | } |
|
| 127 | + | let body = Note { |
|
| 128 | + | text: String::new(), |
|
| 129 | + | sigs: lines, |
|
| 130 | + | } |
|
| 131 | + | .encode(); |
|
| 132 | + | ( |
|
| 133 | + | [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], |
|
| 134 | + | body.trim_start_matches('\n').to_string(), |
|
| 135 | + | ) |
|
| 136 | + | .into_response() |
|
| 137 | + | } |
|
| 138 | + | Ok(Witnessed::Conflict(n)) => conflict(n), |
|
| 139 | + | Err(WitnessError::Unsigned(e)) => plain(StatusCode::FORBIDDEN, &e), |
|
| 140 | + | Err(WitnessError::Inconsistent(e)) => plain(StatusCode::UNPROCESSABLE_ENTITY, &e), |
|
| 141 | + | Err(WitnessError::Bad(e)) => plain(StatusCode::BAD_REQUEST, &e), |
|
| 142 | + | } |
|
| 143 | + | } |
|
| 144 | + | ||
| 145 | + | /// The witness's routes: `POST /add-checkpoint`. |
|
| 146 | + | pub fn router(cfg: WitnessConfig) -> Router { |
|
| 147 | + | Router::new() |
|
| 148 | + | .route("/add-checkpoint", post(add_checkpoint)) |
|
| 149 | + | .with_state(Arc::new(Witness { |
|
| 150 | + | cfg, |
|
| 151 | + | lock: Mutex::new(()), |
|
| 152 | + | })) |
|
| 153 | + | } |
|
| 154 | + | ||
| 155 | + | /// Serve a witness on `listener` until stopped. |
|
| 156 | + | pub fn run_blocking( |
|
| 157 | + | listener: std::net::TcpListener, |
|
| 158 | + | mut cfg: WitnessConfig, |
|
| 159 | + | ) -> std::io::Result<()> { |
|
| 160 | + | let rt = tokio::runtime::Builder::new_multi_thread() |
|
| 161 | + | .enable_all() |
|
| 162 | + | .build()?; |
|
| 163 | + | let tls = cfg.tls.take(); |
|
| 164 | + | rt.block_on(async move { |
|
| 165 | + | listener.set_nonblocking(true)?; |
|
| 166 | + | match tls { |
|
| 167 | + | Some(tls) => { |
|
| 168 | + | let _ = rustls::crypto::ring::default_provider().install_default(); |
|
| 169 | + | let rustls_cfg = |
|
| 170 | + | axum_server::tls_rustls::RustlsConfig::from_pem_file(&tls.cert, &tls.key) |
|
| 171 | + | .await |
|
| 172 | + | .map_err(|e| std::io::Error::other(format!("loading TLS files: {e}")))?; |
|
| 173 | + | axum_server::from_tcp_rustls(listener, rustls_cfg)? |
|
| 174 | + | .serve(router(cfg).into_make_service()) |
|
| 175 | + | .await |
|
| 176 | + | } |
|
| 177 | + | None => axum::serve(tokio::net::TcpListener::from_std(listener)?, router(cfg)).await, |
|
| 178 | + | } |
|
| 179 | + | }) |
|
| 180 | + | } |
|
| 181 | + | ||
| 182 | + | #[cfg(test)] |
|
| 183 | + | mod tests { |
|
| 184 | + | use super::*; |
|
| 185 | + | use axum::body::Body; |
|
| 186 | + | use axum::http::Request; |
|
| 187 | + | use tower::ServiceExt; |
|
| 188 | + | ||
| 189 | + | #[tokio::test] |
|
| 190 | + | async fn the_witness_cosigns_and_keeps_what_it_cosigned() { |
|
| 191 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 192 | + | let log_sk = SecretKey::generate(); |
|
| 193 | + | let log = log_sk.identity("log", "[email protected]", 1); |
|
| 194 | + | let w_sk = SecretKey::generate(); |
|
| 195 | + | let w = w_sk.identity("witness", "[email protected]", 1); |
|
| 196 | + | let cfg = || WitnessConfig { |
|
| 197 | + | name: "witness.example".into(), |
|
| 198 | + | identity: w.clone(), |
|
| 199 | + | sk: w_sk.clone(), |
|
| 200 | + | logs: vec![("hub.example/log".into(), log.clone())], |
|
| 201 | + | state: dir.path().join("state"), |
|
| 202 | + | tls: None, |
|
| 203 | + | }; |
|
| 204 | + | let mut stored: Vec<tlog::Hash> = Vec::new(); |
|
| 205 | + | for n in 0..9u64 { |
|
| 206 | + | stored.extend( |
|
| 207 | + | tlog::stored_hashes_for_leaf(n, tlog::leaf_hash(&[n as u8]), &stored).unwrap(), |
|
| 208 | + | ); |
|
| 209 | + | } |
|
| 210 | + | let note = |n: u64| { |
|
| 211 | + | Checkpoint { |
|
| 212 | + | origin: "hub.example/log".into(), |
|
| 213 | + | size: n, |
|
| 214 | + | root: tlog::tree_hash(n, &stored).unwrap(), |
|
| 215 | + | extensions: vec![], |
|
| 216 | + | } |
|
| 217 | + | .sign(&log, &log_sk) |
|
| 218 | + | .unwrap() |
|
| 219 | + | .encode() |
|
| 220 | + | }; |
|
| 221 | + | let post = |body: String| async { |
|
| 222 | + | let res = router(cfg()) |
|
| 223 | + | .oneshot( |
|
| 224 | + | Request::post("/add-checkpoint") |
|
| 225 | + | .body(Body::from(body)) |
|
| 226 | + | .unwrap(), |
|
| 227 | + | ) |
|
| 228 | + | .await |
|
| 229 | + | .unwrap(); |
|
| 230 | + | let st = res.status(); |
|
| 231 | + | let b = axum::body::to_bytes(res.into_body(), 1 << 20) |
|
| 232 | + | .await |
|
| 233 | + | .unwrap(); |
|
| 234 | + | (st, String::from_utf8(b.to_vec()).unwrap()) |
|
| 235 | + | }; |
|
| 236 | + | let (st, lines) = post( |
|
| 237 | + | AddCheckpoint { |
|
| 238 | + | old_size: 0, |
|
| 239 | + | proof: vec![], |
|
| 240 | + | note: note(4), |
|
| 241 | + | } |
|
| 242 | + | .encode(), |
|
| 243 | + | ) |
|
| 244 | + | .await; |
|
| 245 | + | assert_eq!(st, StatusCode::OK, "{lines}"); |
|
| 246 | + | let cosigned = |
|
| 247 | + | Note::parse(&format!("{}\n{lines}", Note::parse(¬e(4)).unwrap().text)).unwrap(); |
|
| 248 | + | assert!(cosigned.cosigned_by("witness.example", &w).is_ok()); |
|
| 249 | + | // Kept across a restart (a new router): a wrong old size is told. |
|
| 250 | + | let (st, size) = post( |
|
| 251 | + | AddCheckpoint { |
|
| 252 | + | old_size: 0, |
|
| 253 | + | proof: vec![], |
|
| 254 | + | note: note(9), |
|
| 255 | + | } |
|
| 256 | + | .encode(), |
|
| 257 | + | ) |
|
| 258 | + | .await; |
|
| 259 | + | assert_eq!((st, size.trim()), (StatusCode::CONFLICT, "4")); |
|
| 260 | + | let proof = tlog::prove_consistency(9, 4, &stored).unwrap(); |
|
| 261 | + | let (st, _) = post( |
|
| 262 | + | AddCheckpoint { |
|
| 263 | + | old_size: 4, |
|
| 264 | + | proof, |
|
| 265 | + | note: note(9), |
|
| 266 | + | } |
|
| 267 | + | .encode(), |
|
| 268 | + | ) |
|
| 269 | + | .await; |
|
| 270 | + | assert_eq!(st, StatusCode::OK); |
|
| 271 | + | } |
|
| 272 | + | } |
modified Cargo.lock +1 −0
| … | 1524 unchanged lines |
||
| 1525 | 1525 | "rustls", |
|
| 1526 | 1526 | "tempfile", |
|
| 1527 | 1527 | "tokio", |
|
| 1528 | + | "tower", |
|
| 1528 | 1529 | ] |
|
| 1529 | 1530 | ||
| 1530 | 1531 | [[package]] |
|
| … | 1424 unchanged lines |
modified crates/oxy-server/Cargo.toml +1 −0
| … | 14 unchanged lines |
||
| 15 | 15 | [dev-dependencies] |
|
| 16 | 16 | rcgen = "0.14" |
|
| 17 | 17 | tempfile = "3" |
|
| 18 | + | tower = { version = "0.5", features = ["util"] } |
modified crates/oxy-server/src/lib.rs +2 −0
| … | 10 unchanged lines |
||
| 11 | 11 | //! and authorization) and routing to [`Server::handle`]; `router` is the |
|
| 12 | 12 | //! standalone server built on [`DirBackend`]. |
|
| 13 | 13 | ||
| 14 | + | pub mod witness; |
|
| 15 | + | ||
| 14 | 16 | use axum::body::Bytes; |
|
| 15 | 17 | use axum::extract::{Path, State}; |
|
| 16 | 18 | use axum::http::{header, HeaderMap, Method, StatusCode}; |
|
| … | 438 unchanged lines |
modified docs/guide.md +10 −0
| … | 705 unchanged lines |
||
| 706 | 706 | oxy tlog verify https://oxygenhub.io --all |
|
| 707 | 707 | oxy tlog verify --key <KeyId> # what a key signed |
|
| 708 | 708 | ``` |
|
| 709 | + | ||
| 710 | + | Anyone can **witness** a hub's log: a witness cosigns each checkpoint that extends the last one it cosigned, with its own key and clock, so a hub cannot show two histories without the witness noticing. The hub lists its witnesses in `hub.toml` (`[tlog] witnesses`), and its served checkpoint carries their cosignatures. |
|
| 711 | + | ||
| 712 | + | ``` |
|
| 713 | + | curl -fsSL https://oxygenhub.io/.well-known/oxyhub-key -o oxygenhub.identity |
|
| 714 | + | oxy witness serve --name witness.example.org --log oxygenhub.io/log=oxygenhub.identity \ |
|
| 715 | + | --bind 0.0.0.0:8891 --tls-cert cert.pem --tls-key key.pem |
|
| 716 | + | ``` |
|
| 717 | + | ||
| 718 | + | It signs with your oxy home's key and keeps what it cosigned in `--state` (default `witness-state`). |
modified src/cli/mod.rs +44 −0
| … | 372 unchanged lines |
||
| 373 | 373 | #[command(subcommand)] |
|
| 374 | 374 | cmd: SubmoduleCmd, |
|
| 375 | 375 | }, |
|
| 376 | + | /// Witness hubs' transparency logs: cosign each checkpoint that extends |
|
| 377 | + | /// the last one cosigned (spec §14.8) |
|
| 378 | + | Witness { |
|
| 379 | + | #[command(subcommand)] |
|
| 380 | + | cmd: WitnessCmd, |
|
| 381 | + | }, |
|
| 376 | 382 | /// Hubs' transparency logs: check that a hub shows everyone the same |
|
| 377 | 383 | /// history (spec §14.8) |
|
| 378 | 384 | Tlog { |
|
| … | 287 unchanged lines |
||
| 666 | 672 | /// versions of the files they touch |
|
| 667 | 673 | #[arg(long, conflicts_with = "discard")] |
|
| 668 | 674 | carry: bool, |
|
| 675 | + | }, |
|
| 676 | + | } |
|
| 677 | + | ||
| 678 | + | #[derive(Subcommand)] |
|
| 679 | + | enum WitnessCmd { |
|
| 680 | + | /// Serve a witness (C2SP tlog-witness: POST /add-checkpoint), signing |
|
| 681 | + | /// with this oxy home's key (author.key) |
|
| 682 | + | Serve { |
|
| 683 | + | /// The witness's name in cosignatures: a host name |
|
| 684 | + | #[arg(long)] |
|
| 685 | + | name: String, |
|
| 686 | + | /// A log to witness, as ORIGIN=FILE: its origin (HOST/log) and its |
|
| 687 | + | /// key's identity file (a hub's /.well-known/oxyhub-key); repeatable |
|
| 688 | + | #[arg(long = "log", required = true)] |
|
| 689 | + | logs: Vec<String>, |
|
| 690 | + | /// Where to keep what it cosigned |
|
| 691 | + | #[arg(long, default_value = "witness-state")] |
|
| 692 | + | state: PathBuf, |
|
| 693 | + | /// Address and port to listen on |
|
| 694 | + | #[arg(long, default_value = "127.0.0.1:8891")] |
|
| 695 | + | bind: String, |
|
| 696 | + | /// PEM certificate chain; enables HTTPS together with --tls-key |
|
| 697 | + | #[arg(long, requires = "tls_key")] |
|
| 698 | + | tls_cert: Option<PathBuf>, |
|
| 699 | + | /// PEM private key for --tls-cert |
|
| 700 | + | #[arg(long, requires = "tls_cert")] |
|
| 701 | + | tls_key: Option<PathBuf>, |
|
| 669 | 702 | }, |
|
| 670 | 703 | } |
|
| 671 | 704 | ||
| … | 655 unchanged lines |
||
| 1327 | 1360 | SubmoduleCmd::Status { json } => sync::submodule_status(json), |
|
| 1328 | 1361 | SubmoduleCmd::Remove { path } => sync::submodule_remove(&path), |
|
| 1329 | 1362 | }, |
|
| 1363 | + | Cmd::Witness { |
|
| 1364 | + | cmd: |
|
| 1365 | + | WitnessCmd::Serve { |
|
| 1366 | + | name, |
|
| 1367 | + | logs, |
|
| 1368 | + | state, |
|
| 1369 | + | bind, |
|
| 1370 | + | tls_cert, |
|
| 1371 | + | tls_key, |
|
| 1372 | + | }, |
|
| 1373 | + | } => tlog::witness_serve(&name, &logs, state, &bind, tls_cert.zip(tls_key)), |
|
| 1330 | 1374 | Cmd::Tlog { cmd } => match cmd { |
|
| 1331 | 1375 | TlogCmd::Verify { |
|
| 1332 | 1376 | hub, |
|
| … | 213 unchanged lines |
modified src/cli/tlog.rs +54 −1
| … | 5 unchanged lines |
||
| 6 | 6 | use super::ui; |
|
| 7 | 7 | use anyhow::{anyhow, Result}; |
|
| 8 | 8 | use oxy_core::hash::KeyId; |
|
| 9 | - | use std::path::Path; |
|
| 9 | + | use std::path::{Path, PathBuf}; |
|
| 10 | 10 | ||
| 11 | 11 | pub fn verify(hub: Option<&str>, key: Option<&str>, all: bool, as_json: bool) -> Result<i32> { |
|
| 12 | 12 | let repo = open()?; |
|
| … | 105 unchanged lines |
||
| 118 | 118 | } |
|
| 119 | 119 | Ok(0) |
|
| 120 | 120 | } |
|
| 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 | + | } |