From f2db469dd9380414ba11c573695f305c9b8c0b34 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:21 +0100 Subject: simtest: signed changes Every change vouched for on every node within a second; a forged signature vouches for nothing. Auth plan M5. --- src/simtest/mod.rs | 3 +- src/simtest/signatures.rs | 113 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/simtest/signatures.rs (limited to 'src/simtest') 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 { +pub(crate) fn path(path: &str) -> Vec { 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; + } +} -- cgit v1.3.1