| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 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 |
|
| 22 |
pub struct WitnessConfig { |
| 23 |
|
| 24 |
pub name: String, |
| 25 |
pub identity: Identity, |
| 26 |
pub sk: SecretKey, |
| 27 |
|
| 28 |
pub logs: Vec<(String, Identity)>, |
| 29 |
|
| 30 |
pub state: PathBuf, |
| 31 |
pub tls: Option<crate::TlsFiles>, |
| 32 |
} |
| 33 |
|
| 34 |
struct Witness { |
| 35 |
cfg: WitnessConfig, |
| 36 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
} |