diff options
| author | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 22:35:21 +0100 |
| commit | f2db469dd9380414ba11c573695f305c9b8c0b34 (patch) | |
| tree | be532d28c371cf0a7c7efd10771f5b5689adba37 | |
| parent | 5471e5285e94ce7576e6c86f260fea15ed9503d1 (diff) | |
simtest: signed changes
Every change vouched for on every node within a second; a forged
signature vouches for nothing.
Auth plan M5.
| -rw-r--r-- | src/simtest/mod.rs | 3 | ||||
| -rw-r--r-- | src/simtest/signatures.rs | 113 |
2 files changed, 115 insertions, 1 deletions
diff --git a/src/simtest/mod.rs b/src/simtest/mod.rs index 5acc886..cd0c2c7 100644 --- a/src/simtest/mod.rs +++ b/src/simtest/mod.rs @@ -16,6 +16,7 @@ mod identity; mod reach; mod relay; mod remakes; +mod signatures; mod soak; mod timing; @@ -403,7 +404,7 @@ pub(crate) fn sim_key(id: &str) -> NodeKey { NodeKey::from_seed(sha2::Sha256::digest(id.as_bytes()).into()) } -fn path(path: &str) -> Vec<String> { +pub(crate) fn path(path: &str) -> Vec<String> { path.split('/').map(str::to_owned).collect() } diff --git a/src/simtest/signatures.rs b/src/simtest/signatures.rs new file mode 100644 index 0000000..2425a16 --- /dev/null +++ b/src/simtest/signatures.rs @@ -0,0 +1,113 @@ +//! Signed changes end to end (`document::signatures`, `swarm::signatures`): nodes under +//! the identity layer, whose node keys sign, write through the API as a user would, +//! and every node comes to see the writes verified, by signatures that came with sync +//! messages or on their own. + +use std::time::Duration; + +use serde_json::json; + +use super::{Sim, path}; +use crate::document::ActorId; +use crate::document::signatures::{Sign, SignEvery}; +use crate::document::store; +use crate::state::AppState; +use crate::swarm::bootstrap::Start; + +const SOON: Duration = Duration::from_secs(20); + +/// Writes `value` at `at` on `node` as a write through the API does, signed at once if +/// `signed` (`?signed=true`); the change's actor and seq. +async fn write_as_user( + node: &AppState, + at: &str, + value: serde_json::Value, + signed: bool, +) -> (ActorId, u64) { + let at = path(at); + let sign = Sign::Write { + path: at.clone(), + asked: signed, + }; + node.mutate_doc_for(Some("alice".to_owned()), sign, move |doc| { + store::write_path(doc, &at, &value) + }) + .await + .expect("write"); + let replica = node.doc.read().await; + let change = replica.doc.get_last_local_change().expect("the write"); + (change.actor_id().clone(), change.seq()) +} + +/// Whether a signature vouches for `actor`'s change `seq` on `node`. +async fn verified(node: &AppState, actor: &ActorId, seq: u64) -> bool { + node.doc.read().await.signatures.verified(actor, seq) +} + +/// A write no rule signs is signed a while after it (`--sign-every`, 1 s by default), +/// and the signature reaches every node on its own (SIGNATURES): the change went before +/// it was signed. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn unsigned_writes_are_verified_within_the_interval() { + let sim = Sim::with_identity(61); + let a = sim.start("a", &[], Start::Genesis); + let b = sim.start("b", &["a"], Start::Join); + let c = sim.start("c", &["a"], Start::Join); + sim.wait_ready(&[&a, &b, &c], SOON).await; + + let (actor, seq) = write_as_user(&b, "metrics/cpu", json!(42), false).await; + sim.wait_converged(&[&a, &b, &c], SOON).await; + for node in [&a, &b, &c] { + let actor = actor.clone(); + sim.wait_until("the write is verified", SOON, move || { + let actor = actor.clone(); + async move { verified(node, &actor, seq).await } + }) + .await; + } +} + +/// With `--sign-every none`, a write no rule signs stays unverified; the node's next +/// signed write vouches for it too, wherever its signature goes. +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn a_signed_write_vouches_for_the_writes_before() { + let sim = Sim::with_identity(62); + let a = sim.start("a", &[], Start::Genesis); + let b = sim.start_with("b", &["a"], Start::Join, |config| { + config.sign_every = SignEvery::Never; + }); + let c = sim.start("c", &["a"], Start::Join); + sim.wait_ready(&[&a, &b, &c], SOON).await; + + let (actor, first) = write_as_user(&b, "metrics/cpu", json!(42), false).await; + sim.wait_converged(&[&a, &b, &c], SOON).await; + // Signatures go within a second: none came for it. Unless b wrote protocol data + // since (always signed), whose signature vouches for it too. + tokio::time::sleep(Duration::from_secs(3)).await; + let latest = b + .doc + .read() + .await + .doc + .get_last_local_change() + .map(|change| change.seq()); + if latest == Some(first) { + for node in [&a, &b, &c] { + assert!(!verified(node, &actor, first).await); + } + } + + let (same, second) = write_as_user(&b, "config/mode", json!("fast"), true).await; + assert_eq!(same, actor); + sim.wait_converged(&[&a, &b, &c], SOON).await; + for node in [&a, &b, &c] { + let actor = actor.clone(); + sim.wait_until("both writes are verified", SOON, move || { + let actor = actor.clone(); + async move { + verified(node, &actor, first).await && verified(node, &actor, second).await + } + }) + .await; + } +} |
