//! Changes signed by the node that committed them: an Ed25519 signature by its node //! key over the change's hash (`message`), kept beside the document, not in it (a //! change's hash covers its message, so it can't carry its own signature). //! //! One signature vouches for more than its change: a change's hash covers its //! dependencies, and a node's change builds on its previous one, so a signed change of //! an actor vouches for all that actor's changes up to it (`check`: the actor and seq //! it verifies up to). So not every change needs one: a change is signed when it //! touches protocol data (`_continuum`) or a path the swarm lists as signed //! (`_continuum/access/signed`, `listed`), or when its write asks for it (`Sign`); a //! change left unsigned is vouched for by the node's next signed one, which, as //! `--sign-every` says, is its latest change, signed a while after it if nothing signed //! it before (`SignEvery`), or every change at once. use std::collections::HashMap; use super::{ChangeHash, Document}; /// Where the swarm lists the paths whose writes are signed: a list of paths, each a /// list of segments, as capabilities name them (`access.md`). pub(crate) const SIGNED_PATHS: [&str; 3] = ["_continuum", "access", "signed"]; /// What a signature is made over: a domain, then the change's hash. pub(crate) fn message(hash: &ChangeHash) -> Vec { let mut message = b"continuum/change/1\0".to_vec(); message.extend_from_slice(&hash.0); message } /// Whether a write is signed (`wanted`). #[derive(Debug, Clone)] pub(crate) enum Sign { /// The node's own work: protocol data, always signed. Protocol, /// A write through the API at `path`, signed if `asked` (`?signed=true`) or if the /// path is protocol data or listed as signed. Write { path: Vec, asked: bool }, } impl Sign { /// Whether a change made so, in `doc`, is to be signed. pub(crate) fn wanted(&self, doc: &Document) -> bool { match self { Sign::Protocol => true, Sign::Write { path, asked } => { *asked || path.first().map(String::as_str) == Some(SIGNED_PATHS[0]) || listed(doc).iter().any(|signed| related(signed, path)) } } } } /// Whether a write at `path` touches what is at `signed`: within it, or above it /// (replacing or deleting what holds it). fn related(signed: &[String], path: &[String]) -> bool { path.starts_with(signed) || signed.starts_with(path) } /// The paths `doc` lists as signed (`SIGNED_PATHS`); an entry that isn't a list of /// strings is left out. pub(crate) fn listed(doc: &Document) -> Vec> { let at: Vec = SIGNED_PATHS.iter().map(|s| (*s).to_owned()).collect(); let Ok(Some(serde_json::Value::Array(entries))) = super::store::read_path(doc, &at) else { return Vec::new(); }; entries .iter() .filter_map(|entry| { entry .as_array()? .iter() .map(|segment| segment.as_str().map(str::to_owned)) .collect() }) .collect() } /// Which of this node's changes it signs besides those `Sign` says (`--sign-every`). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SignEvery { /// Every change, as it is made (`all`, `true`). Change, /// No others (`none`, `false`): one left unsigned waits for the node's next signed /// change. Never, /// Its latest change, if unsigned, this often: every change is vouched for within it. Interval(std::time::Duration), } impl Default for SignEvery { fn default() -> Self { SignEvery::Interval(std::time::Duration::from_secs(1)) } } impl std::fmt::Display for SignEvery { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SignEvery::Change => f.write_str("all"), SignEvery::Never => f.write_str("none"), SignEvery::Interval(every) => f.write_str(&crate::units::format_duration(*every)), } } } impl std::str::FromStr for SignEvery { type Err = String; /// `all` or `true`, `none` or `false`, or a duration (`units::parse_duration`; 0 is /// `none`). fn from_str(text: &str) -> Result { match text.trim() { "all" | "true" => Ok(SignEvery::Change), "none" | "false" => Ok(SignEvery::Never), other => match crate::units::parse_duration(other) { Ok(every) if every.is_zero() => Ok(SignEvery::Never), Ok(every) => Ok(SignEvery::Interval(every)), Err(error) => Err(format!( "{other:?} is neither all, none nor a duration ({error})" )), }, } } } impl serde::Serialize for SignEvery { fn serialize(&self, s: S) -> Result { s.serialize_str(&self.to_string()) } } impl<'de> serde::Deserialize<'de> for SignEvery { fn deserialize>(d: D) -> Result { use serde::de::Error as _; match serde_json::Value::deserialize(d)? { serde_json::Value::Bool(all) => Ok(if all { SignEvery::Change } else { SignEvery::Never }), serde_json::Value::String(text) => text.parse().map_err(D::Error::custom), serde_json::Value::Number(number) => { number.to_string().parse().map_err(D::Error::custom) } other => Err(D::Error::custom(format!( "{other} is neither all, none nor a duration" ))), } } } /// A document's signatures, by change. #[derive(Debug, Default)] pub(crate) struct Signatures { by_change: HashMap, /// This node's latest change, if no signature vouches for it yet (`SignEvery`). unsigned: Option, } impl Signatures { pub(crate) fn insert(&mut self, hash: ChangeHash, signature: [u8; 64]) { self.by_change.insert(hash, signature); } /// Signs `hash`, this node's latest change, with `key` and keeps the signature: it /// vouches for every change of the node's before it too. pub(crate) fn sign(&mut self, hash: ChangeHash, key: &crate::identity::NodeKey) { self.insert(hash, key.sign(&message(&hash))); self.unsigned = None; } /// Whether this node's latest change waits for a signature (`sign_latest`). pub(crate) fn pending(&self) -> bool { self.unsigned.is_some() } /// `hash`, this node's latest change, is left unsigned for now. pub(crate) fn left_unsigned(&mut self, hash: ChangeHash) { self.unsigned = Some(hash); } /// Signs this node's latest change if no signature vouches for it yet; whether it /// did. pub(crate) fn sign_latest(&mut self, key: &crate::identity::NodeKey) -> bool { match self.unsigned { Some(hash) => { self.sign(hash, key); true } None => false, } } } /// What a valid signature of a change in `doc` verifies: its actor's changes up to its /// seq, committed by the node its author names (its node ID, whose key it is). None if /// `doc` doesn't hold the change, its author isn't a node ID, or the signature isn't /// that node's over it. #[cfg_attr(not(test), allow(dead_code))] pub(crate) fn check( doc: &Document, hash: &ChangeHash, signature: &[u8; 64], ) -> Option<(super::ActorId, u64)> { let meta = doc.get_change_meta_by_hash(hash)?; let author = std::str::from_utf8(meta.author.as_ref()?.as_bytes()).ok()?; let key = crate::identity::public_of(author)?; crate::identity::verify(&key, &message(hash), signature) .then(|| (meta.actor.into_owned(), meta.seq)) } #[cfg(test)] mod tests { use super::*; use crate::clock::UnixMillis; use crate::document::replica::{Writer, transact}; use crate::document::store; use crate::identity::NodeKey; use serde_json::json; fn write(doc: &mut Document, node: &str, path: &[&str], value: serde_json::Value) { let path: Vec = path.iter().map(|s| (*s).to_owned()).collect(); let writer = Writer { node, user: None }; transact(doc, UnixMillis(1), &writer, |doc| { store::write_path(doc, &path, &value) }) .unwrap(); } /// A signature verifies its actor's changes up to it, by the node its author names; /// another node's key, or another change, doesn't. #[test] fn a_signature_vouches_for_its_actors_changes_up_to_it() { let key = NodeKey::from_seed([1; 32]); let other = NodeKey::from_seed([2; 32]); let node = key.id(); let mut doc = Document::new(); write(&mut doc, &node, &["a"], json!(1)); write(&mut doc, &node, &["b"], json!(2)); let last = doc.get_heads()[0]; let (actor, seq) = check(&doc, &last, &key.sign(&message(&last))).unwrap(); assert_eq!(&actor, doc.get_actor()); assert_eq!(seq, 2); assert!(check(&doc, &last, &other.sign(&message(&last))).is_none()); let first = doc.get_changes(&[])[0].hash(); assert!(check(&doc, &last, &key.sign(&message(&first))).is_none()); // A change whose author isn't a node ID: nobody's to check. let mut named = Document::new(); write(&mut named, "node", &["a"], json!(1)); let hash = named.get_heads()[0]; assert!(check(&named, &hash, &key.sign(&message(&hash))).is_none()); } /// `--sign-every`: `all`, `none`, a duration; 0 is `none`. #[test] fn sign_every_reads() { use std::time::Duration; let read = |text: &str| text.parse::(); assert_eq!(read("all"), Ok(SignEvery::Change)); assert_eq!(read("true"), Ok(SignEvery::Change)); assert_eq!(read("none"), Ok(SignEvery::Never)); assert_eq!(read("0"), Ok(SignEvery::Never)); assert_eq!( read("500ms"), Ok(SignEvery::Interval(Duration::from_millis(500))) ); assert!(read("sometimes").is_err()); let toml: SignEvery = serde_json::from_value(json!(false)).unwrap(); assert_eq!(toml, SignEvery::Never); assert_eq!(SignEvery::default().to_string(), "1s"); } /// The latest change left unsigned is signed later, once; a signature since covers it. #[test] fn the_latest_change_is_signed_later() { let key = NodeKey::from_seed([1; 32]); let mut signatures = Signatures::default(); assert!(!signatures.sign_latest(&key)); let a = ChangeHash([1; 32]); let b = ChangeHash([2; 32]); signatures.left_unsigned(a); assert!(signatures.sign_latest(&key)); assert!(signatures.by_change.contains_key(&a)); assert!(!signatures.sign_latest(&key)); signatures.left_unsigned(a); signatures.sign(b, &key); assert!(!signatures.sign_latest(&key)); } /// Protocol data, listed paths (within or above them) and asked-for writes are /// signed; others aren't. #[test] fn which_writes_are_signed() { let mut doc = Document::new(); let write_at = |path: &[&str], asked| Sign::Write { path: path.iter().map(|s| (*s).to_owned()).collect(), asked, }; assert!(Sign::Protocol.wanted(&doc)); assert!(write_at(&["_continuum", "users", "a"], false).wanted(&doc)); assert!(!write_at(&["metrics", "cpu"], false).wanted(&doc)); assert!(write_at(&["metrics", "cpu"], true).wanted(&doc)); write( &mut doc, "node", &SIGNED_PATHS, json!([["config"], ["servers", "eu"], "junk", [1]]), ); assert_eq!( listed(&doc), vec![ vec!["config".to_owned()], vec!["servers".to_owned(), "eu".to_owned()] ] ); assert!(write_at(&["config", "x"], false).wanted(&doc)); assert!(write_at(&["servers"], false).wanted(&doc)); assert!(!write_at(&["servers", "us"], false).wanted(&doc)); assert!(!write_at(&["configs"], false).wanted(&doc)); } }