From 8523e9c67e375747147a0b05ec191ad81ce2e318 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:21 +0100 Subject: signatures: changes signed by their node Every node signs the changes it commits with its node key, an Ed25519 signature over the change's hash, kept beside the document. One signature on an actor's latest change vouches for its earlier ones. Protocol data and paths listed in _continuum/access/signed are signed, and writes with ?signed=true. Auth plan M5. --- src/state/host.rs | 2 ++ src/state/mod.rs | 30 +++++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) (limited to 'src/state') diff --git a/src/state/host.rs b/src/state/host.rs index b4a785b..47fd946 100644 --- a/src/state/host.rs +++ b/src/state/host.rs @@ -24,6 +24,8 @@ impl AppState { /// document. (Lock order doc -> peer, as in sync generation.) fn swap_document(&self, replica: &mut Replica, epoch: Epoch, doc: Document) -> Document { let old = std::mem::replace(&mut replica.doc, doc); + // The old document's signatures are of its changes. + replica.signatures = Default::default(); replica.epoch = epoch; replica.epoch_seq += 1; // Publishes the epoch with the heads (`Replica::stamps`), under the same lock, diff --git a/src/state/mod.rs b/src/state/mod.rs index 671aff7..ef1559d 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -38,6 +38,7 @@ use tracing::{debug, error, warn}; use crate::document::lock::DocumentLock; use crate::document::replica::{Epoch, Stamp}; +use crate::document::signatures::Sign; use crate::document::store::{ChangeSet, StoreError}; use crate::document::{Document, Transaction}; use crate::locks::{MutexExt, RwLockExt}; @@ -143,7 +144,8 @@ pub(crate) struct AppState { pub(crate) protocol_writers: Arc>, /// The swarm's first users, recorded by a genesis node (`NodeConfig::first_users`). pub(crate) first_users: Arc, - /// Signs the tokens this node issues (`NodeConfig::node_key`). + /// Signs the tokens this node issues and the changes it commits + /// (`NodeConfig::node_key`). pub(crate) node_key: Option>, pub(crate) token_lifetime: Duration, pub(crate) session_lifetime: Duration, @@ -452,7 +454,7 @@ impl AppState { /// cancelled request can't leave a committed write unannounced. While a rejoin runs, /// the write goes into the document before it (`StaleView`), replayed when the /// rejoin ends; on a node that keeps none, it is refused (`StoreError::Rejoining`, - /// `f` not called). + /// `f` not called). Protocol data: the change is signed (`document::signatures`). pub(crate) async fn mutate_doc( &self, f: impl FnOnce(&mut Transaction<'_>) -> Result + Send + 'static, @@ -461,15 +463,19 @@ impl AppState { T: Send + 'static, E: From + Send + 'static, { - self.mutate_doc_for(None, f).await.map(|(out, _)| out) + self.mutate_doc_for(None, Sign::Protocol, f) + .await + .map(|(out, _)| out) } /// [`mutate_doc`](Self::mutate_doc) for `user`, who wrote through the API: the - /// change names them (`replica::Writer`). With the heads of the document written to - /// after the write (sorted): what a read must have to include it. + /// change names them (`replica::Writer`), and is signed as `sign` says, with this + /// node's key. With the heads of the document written to after the + /// write (sorted): what a read must have to include it. pub(crate) async fn mutate_doc_for( &self, user: Option, + sign: Sign, f: impl FnOnce(&mut Transaction<'_>) -> Result + Send + 'static, ) -> Result<(T, Vec), E> where @@ -511,7 +517,21 @@ impl AppState { node: state.local_id(), user: user.as_deref(), }; + let before = replica.doc.get_heads(); let result = replica.transact(state.clock.now_millis(), &writer, f); + if result.is_ok() + && let Some(key) = &state.node_key + { + // The change made, if any: the one head after it, as it builds on + // every head before. + let after = replica.doc.get_heads(); + if after != before + && let [hash] = after[..] + && sign.wanted(&replica.doc) + { + replica.signatures.sign(hash, key); + } + } replica.refresh_heads(); result.map(|out| (out, replica.heads().to_vec())) }; // lock released before notifying -- cgit v1.3.1