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/api/store.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) (limited to 'src/api/store.rs') diff --git a/src/api/store.rs b/src/api/store.rs index 48fc2c6..7307d92 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -1,5 +1,6 @@ //! /store and /stream handlers. +use crate::document::signatures::Sign; use std::io::Error as IoError; use axum::{ @@ -90,12 +91,18 @@ pub(crate) struct PostQuery { /// With `cast`, how timestamps are read from and written as strings (`rfc3339` by /// default; `typed::TimestampFormat`). format: Option, + /// Whether the change is signed whatever its path (`document::signatures`). + #[serde(default)] + signed: bool, } #[derive(Debug, Deserialize)] pub(crate) struct DeleteQuery { #[serde(default)] prune: bool, + /// Whether the change is signed whatever its path (`document::signatures`). + #[serde(default)] + signed: bool, } /// `GET /store`: the whole store (`docs/openapi.yaml`). @@ -349,6 +356,9 @@ pub(crate) struct PutQuery { /// default); if not, one that would is `409` (`store::Writing::retype`). #[serde(default = "yes")] retype: bool, + /// Whether the change is signed whatever its path (`document::signatures`). + #[serde(default)] + signed: bool, } fn yes() -> bool { @@ -377,6 +387,10 @@ pub(crate) async fn put_store_entry( } let path = parse_write_path(&state, &user, &access, &key, "PUT")?; + let sign = Sign::Write { + path: path.clone(), + asked: query.signed, + }; if let Some(redirect) = to_positive(&state, &path, false, None, raw_query.as_deref()).await? { return Ok(redirect); } @@ -387,7 +401,7 @@ pub(crate) async fn put_store_entry( // A value that can't be shown as asked is refused, and the write with it. let writer = user.0.clone(); let ((retyped, read), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); let retyped = store::write_val_path(doc, &path, &body.value, writing) .map_err(|e| store_status(e, "Failed to write store value"))?; @@ -706,6 +720,10 @@ pub(crate) async fn post_store_entry( return Err(StatusCode::SERVICE_UNAVAILABLE.into()); } let path = parse_write_path(&state, &user, &access, &key, "POST")?; + let sign = Sign::Write { + path: path.clone(), + asked: query.signed, + }; if query.increment.is_some() && query.cast.is_some() { return Err(ApiError( StatusCode::BAD_REQUEST, @@ -723,7 +741,7 @@ pub(crate) async fn post_store_entry( if let Some(by) = query.increment { let writer = user.0.clone(); let (value, heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); let value = store::increment_path(doc, &path, by) .map_err(|e| store_status(e, "Failed to increment a counter"))?; @@ -759,7 +777,7 @@ pub(crate) async fn post_store_entry( // A value that can't be shown as asked is refused, and the cast with it. let writer = user.0.clone(); let ((retyped, read), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); let retyped = store::cast_path(doc, &path, to, timestamps) .map_err(|e| store_status(e, "Failed to cast a store value"))?; @@ -775,7 +793,7 @@ pub(crate) async fn post_store_entry( let value = body_value(&headers, &body)?; let writer = user.0.clone(); let ((), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); store::insert_val_path(doc, &path, &value.value) .map_err(|e| store_status(e, "Failed to insert into a list"))?; @@ -803,6 +821,10 @@ pub(crate) async fn delete_store_entry( RawQuery(raw_query): RawQuery, ) -> Result { let path = parse_write_path(&state, &user, &access, &key, "DELETE")?; + let sign = Sign::Write { + path: path.clone(), + asked: query.signed, + }; tracing::Span::current().record("path", path.join("/").as_str()); @@ -826,7 +848,7 @@ pub(crate) async fn delete_store_entry( }); let writer = user.0.clone(); let ((), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| { + .mutate_doc_for(Some(user.0.clone()), sign.clone(), move |doc| { let before = Before::of(&*doc, &writer); store::delete_path_pruning(doc, &path, prune) .map_err(|e| store_status(e, "Failed to delete store value"))?; -- cgit v1.3.1